Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/agent/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -317,7 +319,7 @@ A stacklet renders as "<id> Stacklet" and a bot as "<name> 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`
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/framework/test_commit_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) == []
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 9 additions & 3 deletions tools/commit-lint
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 "
Expand Down
Loading