﻿<feed xmlns="http://www.w3.org/2005/Atom">
  <title type="text" xml:lang="en">Project Layout</title>
  <link type="application/atom+xml" href="https://d.moonfire.us/tags/project-layout/atom.xml" rel="self" />
  <link type="text/html" href="https://d.moonfire.us/tags/project-layout/" rel="alternate" />
  <updated>2026-03-16T17:43:08Z</updated>
  <id>https://d.moonfire.us/tags/project-layout/</id>
  <author>
    <name>D. Moonfire</name>
  </author>
  <rights>Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</rights>
  <entry>
    <title>A Week of Dependencies plus Nor Curse Be Found 13</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/01/29/a-week-of-dependicies/" />
    <updated>2023-01-29T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/01/29/a-week-of-dependicies/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="writing" scheme="https://d.moonfire.us/categories/" label="Writing" />
    <category term="markdowny" scheme="https://d.moonfire.us/tags/" label="Markdowny" />
    <category term="nor-curse-be-found" scheme="https://d.moonfire.us/tags/" label="Nor Curse Be Found" />
    <category term="entanglement-2021" scheme="https://d.moonfire.us/tags/" label="Entanglement 2021" />
    <category term="fedran" scheme="https://d.moonfire.us/tags/" label="Fedran" />
    <category term="project-layout" scheme="https://d.moonfire.us/tags/" label="Project Layout" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="woodpecker-ci" scheme="https://d.moonfire.us/tags/" label="Woodpecker CI" />
    <summary type="html">To write a chapter in *Nor Curse Be Found*, I ended up spending three days working on projects and tasks that needed to be done to provide the tools I use while writing.
</summary>
    <content type="html">&lt;p&gt;I'm trying to get get back to the one-week iterations/sprints for my personal projects. As one of my goals for the year, I want to write at least one short story or a chapter a month. &amp;ldquo;At least&amp;rdquo; being the goal since I want to write more, but writing &lt;a href="https://fedran.com/"&gt;Fedran&lt;/a&gt; is still a struggle because of burnout, &lt;a href="/tags/entanglement-2021/"&gt;entanglement&lt;/a&gt;, and just life.&lt;/p&gt;
&lt;h2&gt;Markdowny&lt;/h2&gt;
&lt;p&gt;So I dedicated this week to writing chapter thirteen of &lt;a href="/tags/nor-curse-be-found/"&gt;Nor Curse Be Found&lt;/a&gt;, but in the process of setting it up, I finally hit the point where bugs in &lt;a href="/tags/markdown/"&gt;Markdowny&lt;/a&gt; were too much for me. The main one is that I used the wrong shebang in the scripts but didn't realize it until I switched to &lt;a href="/tags/nixos/"&gt;NixOs&lt;/a&gt;. The key part is that I need to use &lt;code&gt;/usr/bin/env bash&lt;/code&gt; for all my shebangs instead of assuming the path to &lt;code&gt;bash&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;# !/usr/bin/env bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So that meant I needed to go into that project to fix it (and add a few features I've been wanting). But that was so old that I spent a few days bringing it up to fix my new &lt;a href="/tags/project-layout/"&gt;project layout&lt;/a&gt; so I had a consistent environment. While doing that, I ended up on yet another tangent to create a script that could create my script files. (Not sure where to store that, to be honest.)&lt;/p&gt;
&lt;p&gt;But then I finished the script to get the layout working to get Markdown working. After another day or so to coerce &lt;code&gt;markdowny&lt;/code&gt; into &lt;a href="/tags/woodpecker-ci/"&gt;Woodpecker CI&lt;/a&gt;, I was finally ready to actual write a feature I've been needing for a while.&lt;/p&gt;
&lt;h2&gt;New Feature&lt;/h2&gt;
&lt;p&gt;To submit entries to my writing group, I need to post a &amp;ldquo;events so far&amp;rdquo; document because it could be weeks or months between submissions. I use &lt;code&gt;markdowny&lt;/code&gt; to do that with the &lt;code&gt;list&lt;/code&gt; command:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ markdowny list chapters/*.md | head -n 5
1. Always Moving: While Linsan waits for her mother to come home, she bounces around on the furniture and talks to her father. She announces that she has named a violin her father is making Palisis and learns that the violin is for her father's first wife who got married to her mother's best friend.

2. Early Lessons: Years later, Linsan is learning how to play the violin from her father. The lesson is interrupted when Dukan, her father's best friend and manager for the business, visits in a panic to tell him that the family's workshop in the valley is on fire.

3. Home Early: Unable to visit the burnt remains of the family's workshop, Linsan comes home to find her father work in depression. He had given up working on instruments and switched to writing articles about music. She goes into the attic to put some books away and finds Palisis in a corner, returned after Marin's death. She plays it, but then finds out that no one had ever played it before.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I needed a bit more details, so I decided to expand it to use Handlebars to give me a template for the chapters and to access all of the components inside the YAML header.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ markdowny list ~/src/fedran/sources/allegro/chapters/*.md --template='- Chapter {{_number}} - {{{title}}} =&amp;gt; {{{summary}}}' --trim-whitespace | head -n 5
- Chapter 1 - Always Moving =&amp;gt; While Linsan waits for her mother to come home, she bounces around on the furniture and talks to her father. She announces that she has named a violin her father is making Palisis and learns that the violin is for her father's first wife who got married to her mother's best friend.
- Chapter 2 - Early Lessons =&amp;gt; Years later, Linsan is learning how to play the violin from her father. The lesson is interrupted when Dukan, her father's best friend and manager for the business, visits in a panic to tell him that the family's workshop in the valley is on fire.
- Chapter 3 - Home Early =&amp;gt; Unable to visit the burnt remains of the family's workshop, Linsan comes home to find her father work in depression. He had given up working on instruments and switched to writing articles about music. She goes into the attic to put some books away and finds Palisis in a corner, returned after Marin's death. She plays it, but then finds out that no one had ever played it before.
- Chapter 4 - Solace in Memories =&amp;gt; As Linsan frequently did, she visited the family's ruins after school. The spot gave her peace despite everything they had lost. However, a bully from school, Dukan's daughter Brook, follows after her and they fight. During the brawl, they both manifest their powers: Linsan with music and Brook with concussion powers.
- Chapter 5 - Bitter Partings =&amp;gt; Linsan comes limping home after her fight with Brook. Her parents are surprised she is there, but then Dukan and Brook show up. Dukan has his daughter apologizes and then offers to send money to the Sterlig's. Linsan's father tries to refuse it, but Dukan phrases it as helping Linsan and they accept.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It might not be much, but it was something I've been looking for. It also means that I can include things like POV, when it happened, or other details in the list to help work timelines, locations, and the like. Or use it as a simple index page generator for HTML and Gemini pages, if someone found a use for that.&lt;/p&gt;
&lt;h3&gt;Tables&lt;/h3&gt;
&lt;p&gt;Of course, there is also a table approach to the same thing:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;markdowny table chapters/*.md --fields _basename title when.start locations.primary | head -n 5
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left;"&gt;_basename&lt;/th&gt;
&lt;th style="text-align: left;"&gt;title&lt;/th&gt;
&lt;th style="text-align: left;"&gt;when.start&lt;/th&gt;
&lt;th style="text-align: left;"&gt;locations.primary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;chapter-01.md&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Rutejìmo&lt;/td&gt;
&lt;td style="text-align: left;"&gt;1471/3/28 MTR 4::22&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Shimusogo Valley&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;chapter-02.md&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Confession&lt;/td&gt;
&lt;td style="text-align: left;"&gt;1471/3/28 MTR 4::75&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Shimusogo Valley&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;chapter-03.md&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Morning&lt;/td&gt;
&lt;td style="text-align: left;"&gt;1471/3/28 MTR 11::71&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Shimusogo Valley&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Nor Curse Be Found&lt;/h2&gt;
&lt;p&gt;Despite all that, I got another chapter of Nor Curse Be Found written this afternoon. It isn't polished as I hope, I'm still struggling with the &amp;ldquo;feel&amp;rdquo; for the chapter and how the prince responds to things, but that is also something I have to finish to really understand and go back to edit.&lt;/p&gt;
&lt;p&gt;Overall, it was a good day (it took me three days to write the chapter).&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Semantic Release and Woodpecker CI</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2022/08/07/semantic-release-and-woodpecker-ci/" />
    <updated>2022-08-07T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2022/08/07/semantic-release-and-woodpecker-ci/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="semantic-release" scheme="https://d.moonfire.us/tags/" label="Semantic Release" />
    <category term="woodpecker-ci" scheme="https://d.moonfire.us/tags/" label="Woodpecker CI" />
    <category term="gitlab" scheme="https://d.moonfire.us/tags/" label="Gitlab" />
    <category term="gitea" scheme="https://d.moonfire.us/tags/" label="Gitea" />
    <category term="conventional-commits" scheme="https://d.moonfire.us/tags/" label="Conventional Commits" />
    <category term="gitversion" scheme="https://d.moonfire.us/tags/" label="GitVersion" />
    <category term="lefthook" scheme="https://d.moonfire.us/tags/" label="Lefthook" />
    <category term="sourcehut" scheme="https://d.moonfire.us/tags/" label="Sourcehut" />
    <category term="fedran" scheme="https://d.moonfire.us/tags/" label="Fedran" />
    <category term="project-layout" scheme="https://d.moonfire.us/tags/" label="Project Layout" />
    <summary type="html">In my migration from GitLab to Gitea, I've started moving my CI/CD server over to Woodpecker. Here is some of the struggles I've done through in the process of getting it to work.
</summary>
    <content type="html">&lt;p&gt;With the recent drama of &lt;a href="/tags/gitlab/"&gt;GitLab&lt;/a&gt;, both with the CI/CD changes and then more recent possible threat of deleting old repositories, I continue my migration to a local &lt;a href="/tags/gitea/"&gt;Gitea&lt;/a&gt; instance, &lt;a href="https://src.mfgames.com/"&gt;https://src.mfgames.com/&lt;/a&gt; for the bulk of my code and writing.&lt;/p&gt;
&lt;p&gt;For the most part, migrating is just a matter of shuffling data. I have a &lt;em&gt;lot&lt;/em&gt; of repositories, both active and inactive, and it will take me months to move them over. Plus I haven't decided if I'm going to purge them from my GitLab account so there is a single source of truth or just mirror back to them.&lt;/p&gt;
&lt;p&gt;Currently, the most difficult task was figuring out how to handle the build processing. I've mentioned previously that I use &lt;a href="/tags/conventional-commits/"&gt;Conventional Commits&lt;/a&gt; and &lt;a href="/tags/semantic-release/"&gt;Semantic Release&lt;/a&gt; fairly heavily. I've branched out a little from there using &lt;a href="/tags/lefthook/"&gt;Lefthook&lt;/a&gt; and my &lt;a href="/garden/project-layout/"&gt;project layout&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Currently, the CI does the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build the project&lt;/li&gt;
&lt;li&gt;Test various conditions including valid commit messages&lt;/li&gt;
&lt;li&gt;If the commits indicate a new build:
&lt;ol&gt;
&lt;li&gt;Tag it&lt;/li&gt;
&lt;li&gt;Build the release version&lt;/li&gt;
&lt;li&gt;Create a release on Gitea&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This changes over time, but it is the basic pattern.&lt;/p&gt;
&lt;h1&gt;Tags and Git Depth&lt;/h1&gt;
&lt;p&gt;Woodpecker does not automatically download the needed tags for &lt;code&gt;semantic-release&lt;/code&gt; (and &lt;a href="/tags/gitversion/"&gt;GitVersion&lt;/a&gt;). This means that the &lt;code&gt;.woodpecker.yml&lt;/code&gt; file needs to include tags.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;clone:
    git:
        image: woodpeckerci/plugin-git
        settings:
            tags: true
pipeline:
    # The pipeline elements
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unike GitLab, which only limits to the last ten commits, it appears that Woodpecker &lt;a href="https://woodpecker-ci.org/plugins/plugin-git"&gt;downloads the full repository&lt;/a&gt; by default which is also needed by GitVersion because it calculates every version. Not entirely sure about &lt;code&gt;semantic-release&lt;/code&gt; logging indicates it doesn't need the full repository, just enough back to find a version.&lt;/p&gt;
&lt;h1&gt;Building and Testing&lt;/h1&gt;
&lt;p&gt;To support task branches, I have a basic build and test code that runs on pushes and pull requests. This lets me identify bugs earlier and catch typos with my commits.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;build:
    image: registry.gitlab.com/dmoonfire/nix-flake-docker:latest
    commands:
        - nix develop --command scripts/build.sh
    when:
        # We need both &amp;quot;tag&amp;quot; for the next section.
        event: [push, pull_request, tag]
        tag: v*

test:
    image: registry.gitlab.com/dmoonfire/nix-flake-docker:latest
    commands:
        - nix develop --command scripts/test.sh
    when:
        event: [push, pull_request]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From the build tasks, you can see that I'm using my current project layout which uses scripts in the &lt;code&gt;scripts/&lt;/code&gt; folder instead of &lt;code&gt;npm run&lt;/code&gt; or &lt;code&gt;dotnet run&lt;/code&gt;. This is to make it easier to work with polyglot plus works around the issue that I need to use &lt;code&gt;nix develop&lt;/code&gt; to get into my reproducible environment since the Docker image doesn't automatically do that. This is because both Gitlab and Woodpecker use the image which bypasses initialization files and I couldn't have it run &lt;code&gt;direnv allow&lt;/code&gt; automatically to set up environment variables.&lt;/p&gt;
&lt;p&gt;One thing that is missing is that Woodpecker doesn't have a clean mechanism for temporary build artifacts. I can't upload the build files and then download them so I can see the final results. Instead, I have to script it out or use a S3 plugin.&lt;/p&gt;
&lt;h1&gt;Building on Versions&lt;/h1&gt;
&lt;p&gt;With most cases, I build the release version of the project when the conventional commits indicate that there is a new version (&lt;code&gt;feat&lt;/code&gt; and &lt;code&gt;fix&lt;/code&gt;). This is an additional pipeline that comes after the &lt;code&gt;test:&lt;/code&gt; line.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;release-main:
    image: registry.gitlab.com/dmoonfire/nix-flake-docker:latest
    commands:
        - export DRONE=&amp;quot;true&amp;quot; # Required to convince `env-ci`
        # semantic-release needs this locally
        - git branch $DRONE_BRANCH origin/$DRONE_BRANCH
        - nix develop --command scripts/release.sh
    secrets:
        - gitea_token
        - git_credentials
    when:
        event: push
        branch: main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are a number of things in this block that took me a while. The first is the &lt;code&gt;event&lt;/code&gt; and &lt;code&gt;branch&lt;/code&gt;. We only do releases on the &lt;code&gt;main&lt;/code&gt; (I'm still moving away from &lt;code&gt;master&lt;/code&gt; as racist language).&lt;/p&gt;
&lt;p&gt;The second is the &lt;code&gt;export DRONE&lt;/code&gt; line. At the time I set this up, &lt;a href="https://www.npmjs.com/package/env-ci"&gt;env-ci&lt;/a&gt; wasn't aware of Woodpecker, but it was &lt;a href="https://github.com/woodpecker-ci/woodpecker/pull/1035"&gt;recently added&lt;/a&gt; thanks to 6543 on the Woodpecker Matrix channel, &lt;code&gt;#woodpecker-ci:matrix.org&lt;/code&gt;. I don't know when the latest &lt;code&gt;semantic-release&lt;/code&gt; will have it, but it shouldn't be needed soon, if not already.&lt;/p&gt;
&lt;p&gt;The third is the &lt;code&gt;git branch&lt;/code&gt; line in the above script. Woodpecker creates a detached head, as does Gitlab. But when it doesn't do is also create a local branch for the one being created. This causes a problem because the release process appears to &amp;ldquo;jump&amp;rdquo; to the branch to figure out the changes between the detached head (the commit being built) and the actual branch.&lt;/p&gt;
&lt;p&gt;Finally, we have the secrets. &lt;code&gt;semantic-release&lt;/code&gt; automatically picks up $GITEA_TOKEN for the release process but also needs $GIT_CREDENTIALS to verify Git access.&lt;/p&gt;
&lt;p&gt;The token is easy, that is what given by Gitea for the user.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/semantic-release/semantic-release/blob/master/docs/usage/ci-configuration.md"&gt;GIT_CREDENTIALS&lt;/a&gt; is slightly harder, it is a colon-separate tuple of the user name and the Gitea access token. From observations, the Gitea is basically just a bunch of URL-safe characters, so the URL-escaping isn't needed in my case (you need to URL-escape the left and right of the colon but not the colon itself).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;$ export GITEA_TOKEN=9fc6d72c72e4b149f07491a0b2d3ec9215d57caf
$ export GIT_CREDENTIALS=&amp;quot;dmoonfire:$GITEA_TOKEN&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These need to be set on a per-project basis since Woodpecker, unlike GitLab and &lt;a href="/tags/sourcehut/"&gt;sourcehut&lt;/a&gt;, there doesn't appear to be a good way of having a shared set of secrets for projects (GitLab has organization/group level secrets, sourcehut has the secret storage). This means I have to set the same GITEA_TOKEN and GIT_CREDENTIALS for all 80+ of my &lt;a href="/tags/fedran/"&gt;Fedran&lt;/a&gt; repositories†.&lt;/p&gt;
&lt;p&gt;† Woodpecker has a CLI, &lt;code&gt;woodpecker-cli&lt;/code&gt; which will let me automate that. I will use that.&lt;/p&gt;
&lt;h1&gt;Create Release on Tag&lt;/h1&gt;
&lt;p&gt;One thing I'm moving toward is creating a release entry on the forge. Since this happens after the build process and Woodpecker uses a different Docker image, I need it to be a post-release event so I hang it off the tagging process instead of the push to &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;6543 came to my rescue again with this one, so that is why there are those two &amp;ldquo;tag&amp;rdquo; elements in the script above. I also have a new stanza for the release process:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;release-gitea:
    image: plugins/gitea-release
    settings:
        base_url: https://src.mfgames.com
        files:
            - &amp;quot;*.pdf&amp;quot;
            - &amp;quot;*.epub&amp;quot;
        api_key:
            from_secret: gitea_token
    when:
        event: tag
        tag: v*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If I didn't have the &lt;code&gt;event:&lt;/code&gt; and &lt;code&gt;tag:&lt;/code&gt; in the &lt;code&gt;build:&lt;/code&gt; stanza, it wasn't working for the tags. This caused me some difficulties because I usually treat hashes as being unordered, but Woodpecker uses file order for processing pipelines. So, I needed to have the &lt;code&gt;build:&lt;/code&gt; target build the file (with the correct version because it was tagged) and then &lt;code&gt;release-gitea:&lt;/code&gt; to use that output for the release process. The &lt;code&gt;test:&lt;/code&gt; and &lt;code&gt;release-main:&lt;/code&gt; are skipped because they don't have those events listed.&lt;/p&gt;
&lt;p&gt;In addition, secrets are handled differently when done as a parameter for a plugin. That is why I have the &lt;code&gt;from_secret:&lt;/code&gt; element in the above script. This inconsistency threw me for a few days.&lt;/p&gt;
&lt;h1&gt;Putting it Together&lt;/h1&gt;
&lt;p&gt;If you want to see the final version, check out &lt;a href="https://src.mfgames.com/dmoonfire-garden/project-layout/src/branch/main/.woodpecker.yml"&gt;this example&lt;/a&gt; which has my current version as a single file.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;I'm happy to move over to Woodpecker (you know, except for the cost of hosting) both because of the control and the challenge. I also don't have a need for speed, so if it takes a while to get through the queue, I'm okay.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Author Intrusion v0.9.0</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2018/06/26/author-intrusion-0.9.0/" />
    <updated>2018-06-26T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2018/06/26/author-intrusion-0.9.0/</id>
    <category term="programming" scheme="https://d.moonfire.us/categories/" label="Programming" />
    <category term="author-intrusion" scheme="https://d.moonfire.us/tags/" label="Author Intrusion" />
    <category term="fast-trip" scheme="https://d.moonfire.us/tags/" label="Fast Trip" />
    <category term="project-layout" scheme="https://d.moonfire.us/tags/" label="Project Layout" />
    <summary type="html">After a few weeks of work, the current rewrite of Author Intrusion got to a stopping point. This has the minimum functionality to detect echo works but it has a long way to go.
</summary>
    <content type="html">&lt;p&gt;After a few weeks of work, the current rewrite of Author Intrusion got to a stopping point. This has the &lt;strong&gt;barest&lt;/strong&gt; minimum functionality to detect echo words but it has a &lt;strong&gt;long&lt;/strong&gt; way to go. Depressingly long way, but I need to let it settle a little before I jump back into it.&lt;/p&gt;
&lt;p&gt;I also want a little encouragement so I'm going to toot my own horn and show some progress.&lt;/p&gt;
&lt;h1&gt;Starting Over Again&lt;/h1&gt;
&lt;p&gt;I think this is the ninth attempt I've had to write &lt;span class="missing-link" data-path="/tags/author-intrusion"&gt;Author Intrusion&lt;/span&gt;. Each time, I've encountered various walls where my ideas didn't have the performance or couldn't conceptually move beyond the proof of concept. Over the last eight years, I've learned a lot about writing this and each time I hope &amp;ldquo;this is it&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The current goal is to write a command-line interface (CLI) inspired by compilers (like GCC and TypeScript &lt;code&gt;tsc&lt;/code&gt;) and Git. Basically, have AI have a core set of function but don't worry about re-implementing a text editor (which was, in all honesty, one of my more common mistakes for previous versions).&lt;/p&gt;
&lt;p&gt;I spent a &lt;em&gt;lot&lt;/em&gt; of time trying to get a solid, cross-platform GUI for this. That includes various Gtk# and Electron implementations before I decided to switch to CLIs.&lt;/p&gt;
&lt;p&gt;For reference, this is some of my efforts five years ago:&lt;/p&gt;
&lt;img class="img-responsive post-img-link" src="/assets/2011/03/editor-screenshot.png" alt="Author Intrusion v0.1.0" /&gt;
&lt;img class="img-responsive post-img-link" src="/assets/2013/10/author-intrusion-0.4.0.png" alt="Author Intrusion v0.4.0" /&gt;
&lt;img class="img-responsive post-img-link" src="/assets/2013/11/author-intrusion-0.5.0-3.png" alt="Author Intrusion v0.5.0" /&gt;
&lt;p&gt;The &amp;ldquo;mmm&amp;rdquo; was a search and replace on one of my larger commissions to test performance. In this case, it was a 100k commissioned novel.&lt;/p&gt;
&lt;p&gt;There were a lot of attempts in there and I spent a lot of time trying to create an editor. While I was &amp;ldquo;fairly&amp;rdquo; successful, I think it made the project too big for one person to do. Each major iteration, I've been removing features trying to get to a core functionality while still honoring my core goals of helping me write.&lt;/p&gt;
&lt;h1&gt;Current Implementation&lt;/h1&gt;
&lt;p&gt;After the attempt to write it in Typescript, I decided this version is going to use .NET Core. The folks at Microsoft have done an amazing job of writing something that is fast and capable while running on both Windows and Linux (one of my requirements). It also is my core language, so I'm not struggling with learning and the tools like I was with Typescript. My primary environment is Visual Studio 2017 with &lt;a href="http://www.jetbrains.com/resharper/"&gt;ReSharper&lt;/a&gt;, &lt;a href="http://www.jetbrains.com/rider/"&gt;Rider&lt;/a&gt;, or &lt;a href="https://code.visualstudio.com/"&gt;Visual Studio Code&lt;/a&gt;. While I use &lt;a href="https://atom.io/"&gt;Atom&lt;/a&gt;, global configuration options pretty much means my Atom setup is specifically for writing, not coding.&lt;/p&gt;
&lt;h1&gt;NuGet&lt;/h1&gt;
&lt;p&gt;However, after the Typescript implementation and later code, I realize that the approach &lt;code&gt;npm&lt;/code&gt; uses to manage packages is perfect for what I'm looking for. My struggles with various incarnations of &lt;a href="https://gitlab.com/mfgames-writing"&gt;MfGames Writing&lt;/a&gt; showed me that bitrot is a major problem while writing over years. The earlier incarnations of the build framework would evolve to handle new novels but then it would break older generation in the process. With &lt;code&gt;npm&lt;/code&gt;, I can have a specific version in one project, then the library could continue to evolve while still providing the ability to stay at an older version for those older works.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://nuget.org/"&gt;NuGet&lt;/a&gt; has a number of C# libraries for writing a client that would give me the same thing. Various utilities, analyzers, and libraries can be packaged up as NuGet packages and then installed. If they evolve, the older version can remain behind and still work.&lt;/p&gt;
&lt;p&gt;This version has the basics of this in, I can install packages and have various functionality available for processing.&lt;/p&gt;
&lt;img class="img-responsive post-img-link" src="ai-plugins.gif" alt="4 screenshots of adding plugins to author intrusion" /&gt;
&lt;p&gt;This part is actually one of the neatest parts, I think. I'm using &lt;a href="https://autofac.org/"&gt;Autofac&lt;/a&gt;, a bit of reflection, and the NuGet libraries to install packages and then load assemblies from those packages without needing to pull them into a central location.&lt;/p&gt;
&lt;p&gt;As soon as the plugins load, the system rebuilds the plugins and injects functions. This can be various plugins, new XSLT functions, or anything else.&lt;/p&gt;
&lt;h1&gt;Layout Plugins&lt;/h1&gt;
&lt;p&gt;The above screenshot is an example of the layout plugin. It tags files in the project as to their purpose. I made a mistake earlier on this when I started doing grammar checking on notes. In this case, a plugin can indicate something as &amp;ldquo;content&amp;rdquo; (e.g., the novel or story) or &amp;ldquo;lookup&amp;rdquo; (notes) or something else.&lt;/p&gt;
&lt;p&gt;Right now, I'm only implementing my &amp;ldquo;standard&amp;rdquo; project layout that I've used for the last few projects. Eventually I'll write more but I'm trying to get end-to-end before fleshing out ideas as needed.&lt;/p&gt;
&lt;p&gt;A layout plugin is responsible for gathering metadata about a file so decisions can be made. Eventually this will go into the YAML header for the file. This means I'll be able to identify files that have a specific point of view with something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: Chapter 1
pov: Dylan
swain: scene
---

It was a bright and depressingly sunny day....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The idea for this is to be able to list all chapters of a given POV and then arrange them chronologically while listing the location. Or tag a file as being a &lt;a href="https://en.wikipedia.org/wiki/Scene_and_sequel"&gt;scene or sequel&lt;/a&gt; so they are interspersed correctly.&lt;/p&gt;
&lt;h1&gt;Structure Plugins&lt;/h1&gt;
&lt;p&gt;A structure plugin basically figures out the structure of a file, such as figuring out if it is broken into paragraphs, sentences, and words. I didn't want to hard-code this because sentence splitting is hard and expensive plus my &lt;a href="https://fedran.com/sand-and-blood/chapter-01/"&gt;fantasy novels&lt;/a&gt; all have epigraphs. Those who like Scrivner may want to arrange it into scenes.&lt;/p&gt;
&lt;p&gt;Structure plugins are boring but critical.&lt;/p&gt;
&lt;h1&gt;Xpath&lt;/h1&gt;
&lt;p&gt;An important part of the structure is &lt;em&gt;not&lt;/em&gt; applying a structure to certain files. Lookup files don't need to know the individual words or paragraphs. To limit it, I'm using a &lt;code&gt;scope&lt;/code&gt; variable which is an Xpath into the project.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;project&amp;gt;
  &amp;lt;file path=&amp;quot;/chapters/chapter-01.md&amp;quot; class=&amp;quot;content&amp;quot; /&amp;gt;
  &amp;lt;file path=&amp;quot;/characters/dylan.md&amp;quot; class=&amp;quot;lookup&amp;quot; /&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means, using a path of &lt;code&gt;/content[is-content()]&lt;/code&gt; will select only the content files but not the lookup. Originally, I implemented this as a CSS-like library which I eventually realized I was going down a rabbit hole (I still broke apart the library for later if I need it).&lt;/p&gt;
&lt;p&gt;Again, C# has the ability to have defined XSLT functions so I wrote &lt;code&gt;is-content()&lt;/code&gt; (injected via Autofac) that does custom logic.&lt;/p&gt;
&lt;p&gt;As the various structure plugins operate (defined by the &lt;code&gt;author-intrusion.aipy&lt;/code&gt; file), it will extend the XML structure used for selections.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;project&amp;gt;
  &amp;lt;file path=&amp;quot;/chapters/chapter-01.md&amp;quot; class=&amp;quot;content&amp;quot;&amp;gt;
    &amp;lt;para start=&amp;quot;0&amp;quot; length=&amp;quot;10&amp;quot; /&amp;gt;
    &amp;lt;para start=&amp;quot;11&amp;quot; length=&amp;quot;21&amp;quot; /&amp;gt;
  &amp;lt;/file&amp;gt;
  &amp;lt;file path=&amp;quot;/characters/dylan.md&amp;quot; class=&amp;quot;lookup&amp;quot; /&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;project&amp;gt;
  &amp;lt;file path=&amp;quot;/chapters/chapter-01.md&amp;quot; class=&amp;quot;content&amp;quot;&amp;gt;
    &amp;lt;para start=&amp;quot;0&amp;quot; length=&amp;quot;10&amp;quot;&amp;gt;
      &amp;lt;token start=&amp;quot;0&amp;quot; length=&amp;quot;3&amp;quot; /&amp;gt;
      &amp;lt;token start=&amp;quot;5&amp;quot; length=&amp;quot;4&amp;quot; /&amp;gt;
      &amp;lt;token start=&amp;quot;9&amp;quot; length=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/para&amp;gt;
    &amp;lt;para start=&amp;quot;11&amp;quot; length=&amp;quot;21&amp;quot;&amp;gt;
      &amp;lt;token start=&amp;quot;11&amp;quot; length=&amp;quot;3&amp;quot; /&amp;gt;
      &amp;lt;token start=&amp;quot;16&amp;quot; length=&amp;quot;4&amp;quot; /&amp;gt;
      &amp;lt;token start=&amp;quot;20&amp;quot; length=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;/para&amp;gt;
  &amp;lt;/file&amp;gt;
  &amp;lt;file path=&amp;quot;/characters/dylan.md&amp;quot; class=&amp;quot;lookup&amp;quot; /&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is because of another previous mistake (yeah, I made a lot). Various implementations tried to normalize contents while writing. This meant it would correct double spaces after periods or adjust the text.&lt;/p&gt;
&lt;p&gt;That didn't work.&lt;/p&gt;
&lt;p&gt;I also couldn't break it down into a simple tree structure because English didn't fit well. So, this XML just goes into the original file to get the text.&lt;/p&gt;
&lt;h1&gt;Analysis Plugins&lt;/h1&gt;
&lt;p&gt;The entire reason to break apart a document into a structure is for the analysis. An analysis plugin, such as echo detection, uses the XML structure to gather information.&lt;/p&gt;
&lt;p&gt;All plugins are configured in the &lt;code&gt;author-intrusion.aipy&lt;/code&gt; (project file in YAML format).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plugins:
  analysis:
  - plugin: EchoDetection
    key: echoes

    scope: content
    select: //token[length() &amp;gt; 3]

    compare: text()

    within: 20
    warning: 2
    error: 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;plugin&lt;/code&gt; attribute is the class to use the plugin. The key is just a label for Atom's linter in case someone uses multiple echo detections in a file.&lt;/p&gt;
&lt;p&gt;The second section is to figure out what is being detected. The &lt;code&gt;scope&lt;/code&gt; uses &lt;code&gt;content&lt;/code&gt; which is a shorthand for &lt;code&gt;/file[is-content()]&lt;/code&gt; or &lt;code&gt;/file[has-class(&amp;quot;content&amp;quot;)]&lt;/code&gt;. This breaks apart the search process. Using &lt;code&gt;/&lt;/code&gt; for the path would do echo detection across chapters (and require more memory and would be slower) while the file-level ones makes it more efficient by only comparing a single file against itself.&lt;/p&gt;
&lt;p&gt;For every scope, the &lt;code&gt;select&lt;/code&gt; figures out what is going to be compared. In this example, for every file, we select every word over three characters long (&lt;code&gt;//token[length() &amp;gt; 3]&lt;/code&gt;). If we had three chapters of a thousand words each, this is a different of a single list of three thousand items (&lt;code&gt;scope: /&lt;/code&gt;) verses three lists of a thousand each (&lt;code&gt;scope: //file[is-content()]&lt;/code&gt;), or three hundred paragraphs of ten words each (&lt;code&gt;scope: //file[is-content()]/para&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The third section is the &lt;code&gt;compare&lt;/code&gt;. This basically figures out what to compare. The &lt;code&gt;text()&lt;/code&gt; means the raw text of the file. However, functions to handle case-insensitivity, stemming (base words so &lt;code&gt;I jumped over the jumper&lt;/code&gt; would have two echoes), Soundex (to find similar-sounding words near each other), or whatever else I need.&lt;/p&gt;
&lt;p&gt;Finally, the echo detection has the rules for what is a detection. It basically counts how many identically entries (as determined by &lt;code&gt;compare&lt;/code&gt;) in each &lt;code&gt;select&lt;/code&gt;ed tag inside the &lt;code&gt;scope&lt;/code&gt; within &lt;code&gt;within&lt;/code&gt; entries. If that number is equal to or greater than &lt;code&gt;error&lt;/code&gt;, then it marks that &lt;code&gt;select&lt;/code&gt;ed element as an error.&lt;/p&gt;
&lt;p&gt;In the above example, the echo detection says &amp;ldquo;for every word in a file, look at the twenty surrounding words. If there are five or more, it's an error, otherwise if there is two more then it's a warning&amp;rdquo;.&lt;/p&gt;
&lt;img class="img-responsive post-img-link" src="ai-analyze.png" alt="Author Intrusion v0.9.0 analyze example" /&gt;
&lt;h1&gt;Functionality&lt;/h1&gt;
&lt;p&gt;This isn't even remotely polished at this point. The project is self contained in that building it will generate the correct data, it just isn't&amp;hellip; pretty.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet run -v:q --no-build --no-restore --project src/AuthorIntrusion.Cli -- file-list -p &amp;quot;Examples/Sand and Blood&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Eventually, it should be something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aicli file lists
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is also a lot of missing functionality, using it probably requires me to understand, though I'd like to think it is pretty simple. Then again, I wrote it, of course it's simple.&lt;/p&gt;
&lt;h1&gt;Complexity&lt;/h1&gt;
&lt;p&gt;You may have noticed that the &lt;code&gt;author-intrusion.aipy&lt;/code&gt; file is somewhat complicated. This is actually intentional. There are a lot of tools for writers. I'm looking for something very specifically to help with flaws I'm aware of in my fiction, not a generic &amp;ldquo;one size fits all&amp;rdquo; application. Because of that, I need it to work for me instead of trying to inflict&lt;/p&gt;
&lt;p&gt;This desire to customize to the author is purely inspired by James White's &lt;a href="http://www.sectorgeneral.com/shortstories/fasttrip.html"&gt;Fast Trip&lt;/a&gt;. If you have a chance, consider reading it. It talks about modifying the environment the way you need to work, not the other way around.&lt;/p&gt;
&lt;p&gt;However, flexibility comes at a price: simplicity. I considered trying to make it easy, but I'm looking for something that looks for overuse of adverbs (technically correct) or gerunds. I want to be able to make sure a character only speaks in past tense or doesn't use a pronoun to identify themselves. One of the earlier ones I'm going to get done is looking for present tense outside of a quote. These are pie in the sky items, but I think possible.&lt;/p&gt;
&lt;p&gt;Later, if this gains traction (e.g., users besides me), someone may come up with a fancy GUI or configuration wizard to add common settings.&lt;/p&gt;
&lt;h1&gt;Development&lt;/h1&gt;
&lt;p&gt;Author Intrusion is currently being managed via its &lt;a href="https://gitlab.com/author-intrusion/author-intrusion-cil"&gt;Gitlab project&lt;/a&gt;. I'm not sure if it would be worthwhile for anyone to consider joining, but if you want to watch it, this would be the place.&lt;/p&gt;
&lt;p&gt;If you have questions, please don't hesitate to poke me on any &lt;a href="/contact/"&gt;social network I'm on&lt;/a&gt;. I always love to bounce ideas or talk about future place. The more I do, the more I can make it useful for everyone, not just myself.&lt;/p&gt;
</content>
  </entry>
</feed>
