diff --git a/docs/agent/dev.md b/docs/agent/dev.md index 8517b257..3343ff31 100644 --- a/docs/agent/dev.md +++ b/docs/agent/dev.md @@ -306,7 +306,9 @@ The scope is what a reader recognises, never a file or a module: | `mail` | Mail Bot | | `scribe` | Scribe Bot | | `stacker` | Stacker Bot | -| `curator`, `wiki` | Memory Stacklet | +| `curator`, `wiki`, `diary` | Memory Stacklet | +| `capture` | Archivist Bot | +| `voice` | AI Stacklet | | `stack`, `cli`, `doctor`, `update`, `install` | stack CLI | | `web` | web fetch | | *(none)* | General | @@ -317,7 +319,7 @@ A stacklet renders as " Stacklet" and a bot as " Bot": one release line saying "Archivist Bot" and the next saying "Docs Stacklet" tells you which one changed without opening anything. -Anything that is neither, a component like the wiki or the curator, renders +Anything that is neither, a component like the wiki, the curator or capture, renders as the stacklet that owns it. `docs` is both a type and a scope and they mean different things. Type `docs` diff --git a/tests/unit/framework/test_commit_lint.py b/tests/unit/framework/test_commit_lint.py index adaef6b4..39b73923 100644 --- a/tests/unit/framework/test_commit_lint.py +++ b/tests/unit/framework/test_commit_lint.py @@ -41,6 +41,9 @@ class TestSubjectsThatPass: "feat(docs)!: move Paperless to 3.0.4", "chore(release): v0.3.0-beta.4", "fix(ai): install the local AI engine on current Homebrew (#72)", + "fix(diary): front-page intro addresses the family directly", + "feat(capture): file a link card when a page cannot be read (#96)", + "feat(voice): treat a voice message like any other message (#85)", ]) def test_real_subjects_from_the_history(self, subject): assert commit_lint.check(subject) == [] @@ -81,6 +84,13 @@ def test_too_long_to_read_in_a_list(self): subject = "fix(core): " + "x" * 80 assert any("limit is 72" in p for p in commit_lint.check(subject)) + def test_the_pr_number_github_appends_is_not_counted(self): + """CI checks the PR title before a squash merge appends " (#106)", + so counting the suffix failed subjects that passed CI.""" + subject = "test(archivist): pin the note in the demo-rig todo capture scenario" + assert len(subject) <= commit_lint.SUBJECT_LIMIT + assert commit_lint.check(f"{subject} (#106)") == [] + def test_written_as_a_title(self): problems = commit_lint.check("feat(photos): Stop the upload stalling") assert any("capital" in p for p in problems) diff --git a/tools/commit-lint b/tools/commit-lint index 9440c758..88cb4709 100755 --- a/tools/commit-lint +++ b/tools/commit-lint @@ -64,9 +64,12 @@ SCOPES = { "mail": "Mail Bot", "scribe": "Scribe Bot", "stacker": "Stacker Bot", - # Components that are neither: the stacklet that owns them. + # Components that are neither: the stacklet or bot that owns them. "curator": "Memory Stacklet", "wiki": "Memory Stacklet", + "diary": "Memory Stacklet", + "capture": "Archivist Bot", + "voice": "AI Stacklet", # The framework itself. "stack": "stack CLI", "cli": "stack CLI", @@ -126,9 +129,12 @@ def check(header: str) -> list[str]: f"'{scope}' is not a scope. Use one of: {', '.join(sorted(SCOPES))}" " (or no scope at all)") - if len(header) > SUBJECT_LIMIT: + # A squash merge appends " (#106)" after CI has checked the title, so + # the number GitHub adds is not the author's to budget for. + length = len(header) - (len(f" (#{match['pr']})") if match["pr"] else 0) + if length > SUBJECT_LIMIT: problems.append( - f"{len(header)} characters, limit is {SUBJECT_LIMIT}") + f"{length} characters, limit is {SUBJECT_LIMIT}") if subject[:1].isupper(): problems.append("starts with a capital; the subject is a sentence "