From 451c5dff1cc7ce99f52e871d5bd156f96914530d Mon Sep 17 00:00:00 2001 From: jaylfc Date: Wed, 12 Aug 2026 07:43:09 +0000 Subject: [PATCH 1/3] Expand doc-gate: modification triggers, broad changelog, agent-manual coverage, trailer logging - routes, installers, app-catalog, auth_middleware now trigger on plain modifications (on_modify = true) - user-visible-changelog broadened to cover all changes under tinyagentos/ and desktop/src/ except test-only - agent-manual rule now covers agent_scope_requests_store.py for agent identity/scope surface - Docs-Reviewed trailer override is logged in CI with commit hash, author, and reason - added tests for modification-triggers-gate, changelog-required, test-only-exempt, trailer-logged - updated CONTRIBUTING.md and CHANGELOG.md --- CHANGELOG.md | 21 ++++++ CONTRIBUTING.md | 48 ++++++++++--- docs/doc-gate.toml | 34 +++++++--- scripts/check_doc_gate.py | 40 ++++++++++- tests/test_doc_gate.py | 138 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 262 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fbfdd830..a7160f1f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,27 @@ Versions follow semver beta: `1.0.0-beta.N`, bumped on each dev->master promotio ## [Unreleased] +### Added + +- Doc-gate now triggers on plain modifications (not only add/delete) for + behaviour-bearing trees: routes, installers, app-catalog, and auth_middleware. + A modified route file now requires `docs/agent-coordination.md` to be touched + in the same PR. + +- A new CHANGELOG rule covers every code change under `tinyagentos/` or + `desktop/src/` that is not test-only: such changes require a `CHANGELOG.md` + edit or a new `changelog.d/` fragment. + +- Agent-facing coverage is broadened: changes to the agent identity and scope + surface (`agent_scope_requests_store.py`, `agent_auth_requests.py`, and + related token-auth files) now require `docs/agent-manual/` to be touched. + +### Changed + +- The Docs-Reviewed trailer override is now logged in CI output: when a commit + carries the trailer, the gate prints the commit hash, author, and trailer + text. The escape hatch still works exactly as before. + ## [1.0.0-beta.48] - 2026-08-11 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c994ef046..890b98846 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -253,24 +253,56 @@ Applied to your own changes: ## Documentation gate -A gate blocks PRs that add or remove certain feature code without a matching doc update. It only fires on structural changes (a file added or deleted), never on a plain edit, and only for a small set of conservative rules configured in `docs/doc-gate.toml`: +A gate blocks PRs that change feature code without a matching doc update, unless +a commit in the PR carries a non-empty `Docs-Reviewed: ` trailer. The gate +is configured in `docs/doc-gate.toml`; rules are data, no code changes needed to +add new ones. + +### What triggers the gate + +The gate fires on **additions, deletions, and modifications** (plain edits count) +for the following trees: | Change | Requires editing one of | |--------|--------------------------| -| A desktop app under `desktop/src/apps/` is added or removed | `README.md` | -| A route module under `tinyagentos/routes/` is added or removed | `docs/agent-coordination.md` | -| An installer under `tinyagentos/installers/` or `scripts/install*` is added or removed | `README.md` | -| A manifest under `app-catalog/` is added or removed | `README.md` | +| A route module under `tinyagentos/routes/` is added, removed, or modified | `docs/agent-coordination.md` | +| An installer under `tinyagentos/installers/` or `scripts/install*` is added, removed, or modified | `README.md` | +| A manifest under `app-catalog/` is added, removed, or modified | `README.md` | +| `tinyagentos/auth_middleware.py` is added, removed, or modified | `docs/agent-coordination.md` | + +In addition, **any code change under `tinyagentos/` or `desktop/src/`** (except +test-only changes) requires a changelog entry: either a line in `CHANGELOG.md` +or a new file under `changelog.d/` (preferred, avoids merge conflicts on the +shared `[Unreleased]` anchor). + +Agent-facing changes (agent registry, token auth, scope requests, agent routes, +MCP surface) also require touching the agent manual under `docs/agent-manual/`. + +### Test-file exemption + +Test files are never structural changes and do not trip the gate. This covers +Python `test_*.py` modules, co-located `__tests__/` directories, and frontend +`*.test.*` / `*.spec.*` files. + +### Docs-Reviewed trailer -If your PR trips a rule and there is genuinely nothing to document (or you already covered it elsewhere), add a trailer line to a commit message instead of editing a doc: +If your PR trips a rule and there is genuinely nothing to document (or you +already covered it elsewhere), add a trailer line to a commit message instead of +editing a doc: ``` Docs-Reviewed: no user-facing change, internal refactor only ``` -The trailer must have non-empty text after the colon; a bare `Docs-Reviewed:` does not count. +The trailer must have non-empty text after the colon; a bare `Docs-Reviewed:` +does not count. When CI detects the trailer it logs which commit used it and who +authored it, then passes all rules for that PR. -Run `scripts/install-git-hooks.sh` once to enable local hooks (`.githooks/pre-commit` and `.githooks/commit-msg`) so the gate runs before you push instead of after you open the PR. Local hooks are a convenience only: `.github/workflows/doc-gate.yml` is the authoritative check and runs on every PR regardless of local setup or `--no-verify`. +Run `scripts/install-git-hooks.sh` once to enable local hooks (`.githooks/pre-commit` +and `.githooks/commit-msg`) so the gate runs before you push instead of after you +open the PR. Local hooks are a convenience only: `.github/workflows/doc-gate.yml` +is the authoritative check and runs on every PR regardless of local setup or +`--no-verify`. To add a new rule, edit `docs/doc-gate.toml` -- rules are data, no code changes needed. diff --git a/docs/doc-gate.toml b/docs/doc-gate.toml index a37324d52..1f2befdbb 100644 --- a/docs/doc-gate.toml +++ b/docs/doc-gate.toml @@ -3,11 +3,17 @@ # Read by scripts/check_doc_gate.py. Rules are data: add more [[rules]] # entries here to cover new feature areas without touching the script. # -# Precision over recall: `when_changed` only triggers a rule on a structural -# change (a file added or deleted, or a plain modification when the rule sets -# `on_modify = true`), and every glob below is scoped to the specific tree it -# names. This is deliberate -- a noisy gate gets disabled, so false positives -# are worse than an occasional missed doc update. +# Two trigger modes: +# when_changed -- fires on structural add/delete by default; set +# `on_modify = true` to also count plain modifications. +# require_doc -- any changed file matching these globs satisfies the rule. +# +# Test paths (test_*.py, *.test.*, *.spec.*, __tests__/) are never structural +# and are always excluded from triggering. +# +# The Docs-Reviewed: trailer is an explicit escape hatch: when present in a +# commit message, the gate prints which commit used it and who authored it, +# then passes all rules for that PR. [gate] trailer = "Docs-Reviewed:" @@ -41,27 +47,31 @@ hint = "a desktop app was added or removed" [[rules]] name = "routes" +on_modify = true when_changed = ["tinyagentos/routes/*.py"] require_doc = ["docs/agent-coordination.md"] -hint = "an API route module was added or removed" +hint = "an API route module was added, removed, or modified" [[rules]] name = "installers" +on_modify = true when_changed = ["tinyagentos/installers/*", "scripts/install*"] require_doc = ["README.md"] -hint = "an installer was added or removed" +hint = "an installer was added, removed, or modified" [[rules]] # The repo's catalog tree is app-catalog/, not catalog/ (there is no # catalog/ directory in this repo). Scoped to the real path so the rule can # actually fire instead of being permanently dead config. name = "catalog" +on_modify = true when_changed = ["app-catalog/**"] require_doc = ["README.md"] -hint = "a catalog manifest was added or removed" +hint = "a catalog manifest was added, removed, or modified" [[rules]] name = "agent-api" +on_modify = true when_changed = ["tinyagentos/auth_middleware.py"] require_doc = ["docs/agent-coordination.md"] hint = "the agent-token route allowlist changed; update the agent-facing API surface (or add a Docs-Reviewed trailer explaining why not)" @@ -81,10 +91,14 @@ hint = "the agent-token route allowlist changed; update the agent-facing API sur # PR that merely touched the README satisfy the rule. Verified by probe: with the # doc inside the directory the gate went green on a change carrying no changelog # at all. +# +# Broadened to cover every code change under tinyagentos/ or desktop/src/ that +# is not test-only: any such change requires a CHANGELOG.md edit or a new +# changelog.d/ fragment in the same PR. [[rules]] name = "user-visible-changelog" on_modify = true -when_changed = ["tinyagentos/routes/*.py", "desktop/src/apps/*/**", "desktop/src/App.tsx", "desktop/src/components/**", "desktop/src/stores/**", "tinyagentos/installers/*"] +when_changed = ["tinyagentos/**", "desktop/src/**"] require_doc = ["CHANGELOG.md", "changelog.d/*.md"] hint = "user-visible behaviour changed; add a changelog.d/-.md fragment (preferred) or a CHANGELOG.md line (or add a Docs-Reviewed trailer explaining why not)" @@ -92,7 +106,7 @@ hint = "user-visible behaviour changed; add a changelog.d/-.md fragmen [[rules]] name = "agent-manual" on_modify = true -when_changed = ["tinyagentos/agent_registry*.py", "tinyagentos/agent_token_auth.py", "tinyagentos/routes/agent_*.py", "tinyagentos/mcp/**"] +when_changed = ["tinyagentos/agent_registry*.py", "tinyagentos/agent_token_auth.py", "tinyagentos/agent_scope_requests_store.py", "tinyagentos/routes/agent_*.py", "tinyagentos/mcp/**"] require_doc = ["docs/agent-manual/*.md", "docs/agent-coordination.md"] hint = "agent-facing behaviour changed; review the agent manual or coordination doc (or add a Docs-Reviewed trailer explaining why not)" diff --git a/scripts/check_doc_gate.py b/scripts/check_doc_gate.py index 8666a61c3..f224384f5 100644 --- a/scripts/check_doc_gate.py +++ b/scripts/check_doc_gate.py @@ -315,6 +315,42 @@ def _git_commit_messages(base_ref: str) -> list[str]: return [m for m in out.split("\x00") if m.strip()] +def _git_commits_with_messages(base_ref: str) -> list[tuple[str, str, str]]: + """Return (hash, author_name, message_body) for each commit in the range.""" + out = _run_git(["log", f"{base_ref}..HEAD", "--format=%H%x1f%an%x1f%B%x1f"]) + commits: list[tuple[str, str, str]] = [] + for block in out.split("\x1f"): + block = block.strip() + if not block: + continue + parts = block.split("\x1f", 1) + if len(parts) < 2: + continue + commit_hash = parts[0] + rest = parts[1] + newline_idx = rest.find("\n") + if newline_idx >= 0: + author = rest[:newline_idx] + body = rest[newline_idx + 1:] + else: + author = rest + body = "" + commits.append((commit_hash, author, body)) + return commits + + +def _log_trailer_usage(commits: list[tuple[str, str, str]], trailer: str) -> None: + """Print a log line for each commit that carries a non-empty trailer.""" + for commit_hash, author, message in commits: + for line in message.splitlines(): + stripped = line.strip() + if stripped.startswith(trailer) and stripped[len(trailer):].strip(): + short_hash = commit_hash[:8] + why = stripped[len(trailer):].strip() + print(f"doc-gate: trailer override used in {short_hash} by {author}: {why}") + break + + def get_trailer(config: dict) -> str: """Single source of truth for the commit-message trailer prefix, shared by the diff-gate check and the hooks (via the print-trailer command).""" @@ -365,7 +401,9 @@ def main(argv: list[str] | None = None) -> int: commit_messages: list[str] = [] else: changed = _git_changed_base(args.base) - commit_messages = _git_commit_messages(args.base) + commits_meta = _git_commits_with_messages(args.base) + commit_messages = [msg for _hash, _author, msg in commits_meta] + _log_trailer_usage(commits_meta, get_trailer(config)) failures = evaluate_rules(changed, commit_messages, config) return _report(failures) diff --git a/tests/test_doc_gate.py b/tests/test_doc_gate.py index 2e7a10ad3..3cfe3051b 100644 --- a/tests/test_doc_gate.py +++ b/tests/test_doc_gate.py @@ -421,3 +421,141 @@ def test_exit_codes_are_mutually_distinguishable(self): assert dg.EXIT_CONFIG_ERROR != 1 assert dg.EXIT_CONFIG_ERROR != 2 + +ROUTES_MODIFY_CONFIG = { + "gate": {"trailer": "Docs-Reviewed:"}, + "rules": [ + { + "name": "routes", + "on_modify": True, + "when_changed": ["tinyagentos/routes/*.py"], + "require_doc": ["docs/agent-coordination.md"], + "hint": "an API route module was added, removed, or modified", + }, + ], +} + + +class TestModificationTriggersGate: + def test_modification_to_route_fails_without_doc(self): + """A plain modification to a route with no doc edit FAILS the gate.""" + changed = [("M", "tinyagentos/routes/agents.py")] + failures = dg.evaluate_rules(changed, [], ROUTES_MODIFY_CONFIG) + assert len(failures) == 1 + assert failures[0].startswith("routes -- ") + + def test_modification_to_route_passes_with_doc_edit(self): + changed = [ + ("M", "tinyagentos/routes/agents.py"), + ("M", "docs/agent-coordination.md"), + ] + assert dg.evaluate_rules(changed, [], ROUTES_MODIFY_CONFIG) == [] + + def test_modification_to_route_passes_with_trailer(self): + changed = [("M", "tinyagentos/routes/agents.py")] + messages = ["feat: modify agents route\n\nDocs-Reviewed: internal refactor\n"] + assert dg.evaluate_rules(changed, messages, ROUTES_MODIFY_CONFIG) == [] + + def test_modified_test_file_does_not_trigger(self): + """Modifying a test file is never structural, even with on_modify.""" + changed = [("M", "tests/test_agents.py")] + assert dg.evaluate_rules(changed, [], ROUTES_MODIFY_CONFIG) == [] + + +BROAD_CHANGELOG_CONFIG = { + "gate": {"trailer": "Docs-Reviewed:"}, + "rules": [ + { + "name": "user-visible-changelog", + "on_modify": True, + "when_changed": ["tinyagentos/**", "desktop/src/**"], + "require_doc": ["CHANGELOG.md", "changelog.d/*.md"], + "hint": "user-visible behaviour changed", + }, + ], +} + + +class TestBroadChangelogRequired: + def test_tinyagentos_modification_requires_changelog(self): + changed = [("M", "tinyagentos/app.py")] + failures = dg.evaluate_rules(changed, [], BROAD_CHANGELOG_CONFIG) + assert len(failures) == 1 + assert failures[0].startswith("user-visible-changelog -- ") + + def test_desktop_src_modification_requires_changelog(self): + changed = [("M", "desktop/src/components/Foo.tsx")] + failures = dg.evaluate_rules(changed, [], BROAD_CHANGELOG_CONFIG) + assert len(failures) == 1 + assert failures[0].startswith("user-visible-changelog -- ") + + def test_changelog_fragment_satisfies_broad_rule(self): + changed = [ + ("M", "tinyagentos/app.py"), + ("A", "changelog.d/1234-fix.md"), + ] + assert dg.evaluate_rules(changed, [], BROAD_CHANGELOG_CONFIG) == [] + + def test_changelog_md_edit_satisfies_broad_rule(self): + changed = [ + ("M", "tinyagentos/app.py"), + ("M", "CHANGELOG.md"), + ] + assert dg.evaluate_rules(changed, [], BROAD_CHANGELOG_CONFIG) == [] + + def test_trailer_satisfies_broad_rule(self): + changed = [("M", "tinyagentos/app.py")] + messages = ["feat: update app\n\nDocs-Reviewed: no user-facing change\n"] + assert dg.evaluate_rules(changed, messages, BROAD_CHANGELOG_CONFIG) == [] + + def test_test_file_under_tinyagentos_exempt(self): + """A test file under tinyagentos/ is not a structural change.""" + changed = [("M", "tests/test_agents.py")] + assert dg.evaluate_rules(changed, [], BROAD_CHANGELOG_CONFIG) == [] + + def test_doc_file_under_tinyagentos_still_triggers(self): + """A non-test doc file under tinyagentos/ should still require changelog.""" + changed = [("M", "tinyagentos/README.md")] + failures = dg.evaluate_rules(changed, [], BROAD_CHANGELOG_CONFIG) + assert len(failures) == 1 + + +class TestTrailerLogged: + def test_trailer_usage_is_logged(self, capsys): + commits = [ + ("abc1234567890", "John Doe", "fix: something\n\nDocs-Reviewed: internal refactor\n"), + ] + dg._log_trailer_usage(commits, "Docs-Reviewed:") + captured = capsys.readouterr() + assert "trailer override" in captured.out + assert "John Doe" in captured.out + assert "abc12345" in captured.out + + def test_no_trailer_no_log(self, capsys): + commits = [ + ("abc1234567890", "John Doe", "fix: something\n"), + ] + dg._log_trailer_usage(commits, "Docs-Reviewed:") + captured = capsys.readouterr() + assert captured.out == "" + + def test_empty_trailer_text_not_logged(self, capsys): + commits = [ + ("abc1234567890", "John Doe", "fix: something\n\nDocs-Reviewed:\n"), + ] + dg._log_trailer_usage(commits, "Docs-Reviewed:") + captured = capsys.readouterr() + assert captured.out == "" + + def test_multiple_trailer_commits_all_logged(self, capsys): + commits = [ + ("aaa1111111111", "Alice", "feat: add feature\n\nDocs-Reviewed: new feature\n"), + ("bbb2222222222", "Bob", "fix: bug\n\nDocs-Reviewed: bug fix\n"), + ] + dg._log_trailer_usage(commits, "Docs-Reviewed:") + captured = capsys.readouterr() + assert "Alice" in captured.out + assert "Bob" in captured.out + assert "aaa11111" in captured.out + assert "bbb22222" in captured.out + From dc7016238dd0fc94ba1c85d619b0ba6301ad6abe Mon Sep 17 00:00:00 2001 From: jaylfc Date: Wed, 12 Aug 2026 17:51:19 +0000 Subject: [PATCH 2/3] fix(doc-gate): make the trailer audit actually parse git output, document the expanded rules Lead completion of the queued work on this branch. _git_commits_with_messages used the same separator for fields and records, so splitting the log output produced a flat list and every block failed the len(parts) < 2 guard: _log_trailer_usage never received a commit and no override was ever logged. The four existing tests passed because they hand-build the tuples and never exercise the producer. Records are now terminated with %x1e and fields separated with %x1f. Verified against real history, not only a fixture: over a range containing genuine Docs-Reviewed commits the gate now prints doc-gate: trailer override used in 58d035bb by jaylfc: release version bump... Also updates the contributor skill's doc-gate table, which was stale before this PR and wrong after it: every rule, its trigger mode, and the test-path exemption are now listed, plus what the trailer override costs you. --- .../skills/taos-development-skill/SKILL.md | 36 +++++++++++++----- scripts/check_doc_gate.py | 27 ++++++------- tests/test_doc_gate.py | 38 +++++++++++++++++++ 3 files changed, 75 insertions(+), 26 deletions(-) diff --git a/.claude/skills/taos-development-skill/SKILL.md b/.claude/skills/taos-development-skill/SKILL.md index cba0f7238..9ac405b28 100644 --- a/.claude/skills/taos-development-skill/SKILL.md +++ b/.claude/skills/taos-development-skill/SKILL.md @@ -428,16 +428,28 @@ normalized form (`1.0.0bN`). Only pyproject vs uv.lock is test-gated ## Documentation gate -A gate blocks PRs that add or remove certain feature code without a matching doc update -(configured in `docs/doc-gate.toml`): - -| Change | Requires editing | -|--------|-----------------| -| Desktop app under `desktop/src/apps/` added/removed | `README.md` | -| Route module under `tinyagentos/routes/` added/removed | `docs/agent-coordination.md` | -| Installer under `tinyagentos/installers/` or `scripts/install*` added/removed | `README.md` | -| Manifest under `app-catalog/` added/removed | `README.md` | -| `tinyagentos/auth_middleware.py` (agent-token route allowlist) changed | `docs/agent-coordination.md` | +A gate blocks PRs that change certain feature code without a matching doc update +(configured in `docs/doc-gate.toml`). Rules marked **any change** also fire on a plain +modification; the rest fire only when a matching file is added or deleted. Test files +(`test_*.py`, `*.test.*`, `*.spec.*`, `__tests__/`) never trigger any rule. + +| Change | Fires on | Requires editing | +|--------|----------|-----------------| +| Desktop app under `desktop/src/apps/` | add/remove | `README.md` | +| Route module under `tinyagentos/routes/` | any change | `docs/agent-coordination.md` | +| Installer under `tinyagentos/installers/` or `scripts/install*` | any change | `README.md` | +| Manifest under `app-catalog/` | any change | `README.md` | +| `tinyagentos/auth_middleware.py` (agent-token route allowlist) | any change | `docs/agent-coordination.md` | +| Anything under `tinyagentos/` or `desktop/src/` | any change | `CHANGELOG.md` or a `changelog.d/*.md` fragment | +| Agent registry, token auth, scope-requests store, `routes/agent_*.py`, `tinyagentos/mcp/` | any change | `docs/agent-manual/*.md` or `docs/agent-coordination.md` | +| `.github/workflows/*.yml`, `pyproject.toml`, `CONTRIBUTING.md` | any change | this skill or `docs/*.md` | +| `routes/desktop.py`, `routes/desktop_control.py`, `routes/taos_agent.py` | any change | `.claude/skills/taos-agent/*.md` or `docs/agent-manual/*.md` | +| `update_runner.py`, `auto_update.py`, `restart_orchestrator.py`, `scripts/collate_changelog.py` | any change | `docs/RELEASING.md`, a runbook, or another `docs/*.md` | +| `tinyagentos/worker/` | add/remove | `tinyagentos/worker/README.md` | + +The changelog rule is the one that catches most PRs: any non-test change under +`tinyagentos/` or `desktop/src/` needs a `changelog.d/-.md` fragment (preferred +over editing `CHANGELOG.md` directly, which conflicts between PRs). `.github/workflows/doc-gate.yml` is authoritative (a local `--no-verify` does not bypass it) and also runs `scripts/check_schema_migrations.py` (the SCHEMA-before-migrations guard, see Pitfalls). @@ -446,6 +458,10 @@ If your PR trips a rule and there is genuinely nothing to document, add a traile ``` Docs-Reviewed: no user-facing change, internal refactor only ``` +The trailer passes **every** rule for that PR, so it is an escape hatch, not a shortcut: +the gate prints `doc-gate: trailer override used in by : ` in its CI +log for each commit that carries one, and that line is reviewable. A reviewer may ask for +a real doc instead. Run `scripts/install-git-hooks.sh` to enable local hooks (`.githooks/pre-commit` and `.githooks/commit-msg`) so the gate runs before you push. diff --git a/scripts/check_doc_gate.py b/scripts/check_doc_gate.py index f224384f5..b5010952e 100644 --- a/scripts/check_doc_gate.py +++ b/scripts/check_doc_gate.py @@ -317,25 +317,20 @@ def _git_commit_messages(base_ref: str) -> list[str]: def _git_commits_with_messages(base_ref: str) -> list[tuple[str, str, str]]: """Return (hash, author_name, message_body) for each commit in the range.""" - out = _run_git(["log", f"{base_ref}..HEAD", "--format=%H%x1f%an%x1f%B%x1f"]) + # %x1e terminates each commit record and %x1f separates the three fields + # inside it. A record terminator distinct from the field separator is what + # makes this parseable: with one separator for both, the flat split cannot + # tell a new commit's hash from the previous commit's body. + out = _run_git(["log", f"{base_ref}..HEAD", "--format=%H%x1f%an%x1f%B%x1e"]) commits: list[tuple[str, str, str]] = [] - for block in out.split("\x1f"): - block = block.strip() - if not block: + for record in out.split("\x1e"): + if not record.strip(): continue - parts = block.split("\x1f", 1) - if len(parts) < 2: + fields = record.lstrip("\n").split("\x1f") + if len(fields) < 3: continue - commit_hash = parts[0] - rest = parts[1] - newline_idx = rest.find("\n") - if newline_idx >= 0: - author = rest[:newline_idx] - body = rest[newline_idx + 1:] - else: - author = rest - body = "" - commits.append((commit_hash, author, body)) + commit_hash, author, body = fields[0], fields[1], fields[2] + commits.append((commit_hash.strip(), author, body)) return commits diff --git a/tests/test_doc_gate.py b/tests/test_doc_gate.py index 3cfe3051b..bfb53ecf8 100644 --- a/tests/test_doc_gate.py +++ b/tests/test_doc_gate.py @@ -559,3 +559,41 @@ def test_multiple_trailer_commits_all_logged(self, capsys): assert "aaa11111" in captured.out assert "bbb22222" in captured.out + + +class TestCommitsWithMessagesParsing: + """The producer half of the trailer audit. + + The tests above hand-build the tuples, so they pass whether or not + anything can actually produce them. These drive the parser with the exact + bytes `git log --format=%H%x1f%an%x1f%B%x1f` emits. + """ + + LOG_FORMAT_OUTPUT = ( + "abc1234567890\x1fJohn Doe\x1ffix: something\n\nDocs-Reviewed: internal refactor\n\x1e" + "\ndef4567890123\x1fJane Roe\x1ffeat: another thing\n\x1e" + ) + + def _parse(self, monkeypatch, out): + monkeypatch.setattr(dg, "_run_git", lambda args: out) + return dg._git_commits_with_messages("origin/dev") + + def test_parses_one_record_per_commit(self, monkeypatch): + commits = self._parse(monkeypatch, self.LOG_FORMAT_OUTPUT) + assert len(commits) == 2 + assert [c[0] for c in commits] == ["abc1234567890", "def4567890123"] + assert [c[1] for c in commits] == ["John Doe", "Jane Roe"] + assert "Docs-Reviewed: internal refactor" in commits[0][2] + assert "Docs-Reviewed" not in commits[1][2] + + def test_parsed_commits_reach_the_log(self, monkeypatch, capsys): + """End to end over the seam: real log bytes must produce a log line.""" + commits = self._parse(monkeypatch, self.LOG_FORMAT_OUTPUT) + dg._log_trailer_usage(commits, "Docs-Reviewed:") + captured = capsys.readouterr() + assert "trailer override" in captured.out + assert "John Doe" in captured.out + assert "Jane Roe" not in captured.out + + def test_empty_range_is_empty(self, monkeypatch): + assert self._parse(monkeypatch, "") == [] From 45928ead917a3805324a55181a6a66bc9b7fbf06 Mon Sep 17 00:00:00 2001 From: jaylfc Date: Wed, 12 Aug 2026 18:04:39 +0000 Subject: [PATCH 3/3] fix(doc-gate): make the test-exemption tests actually test the exemption Both exemption cases used tests/test_agents.py, which matches neither rule's when_changed glob, so they passed whether or not _is_test_path existed. They now use paths inside the globs (tinyagentos/routes/test_agents.py, and both a Python and a co-located frontend test for the broad rule). Proven sensitive: neutering _is_test_path turns both red, which it could not do before. Also corrects two descriptions that did not match the config: the broad rule covers every NON-test path under those trees, not only code (the suite pins tinyagentos/README.md as a trigger), and the agent-manual rule is satisfied by docs/agent-coordination.md as well as an agent-manual page. Fenced example carries a language identifier (MD040). --- CONTRIBUTING.md | 13 ++++++++----- docs/doc-gate.toml | 7 ++++--- tests/test_doc_gate.py | 19 ++++++++++++++----- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 890b98846..248c88868 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -270,13 +270,16 @@ for the following trees: | A manifest under `app-catalog/` is added, removed, or modified | `README.md` | | `tinyagentos/auth_middleware.py` is added, removed, or modified | `docs/agent-coordination.md` | -In addition, **any code change under `tinyagentos/` or `desktop/src/`** (except -test-only changes) requires a changelog entry: either a line in `CHANGELOG.md` +In addition, **any non-test change under `tinyagentos/` or `desktop/src/`** +(docs and assets under those trees included, not only code) requires a +changelog entry: either a line in `CHANGELOG.md` or a new file under `changelog.d/` (preferred, avoids merge conflicts on the shared `[Unreleased]` anchor). -Agent-facing changes (agent registry, token auth, scope requests, agent routes, -MCP surface) also require touching the agent manual under `docs/agent-manual/`. +Agent-facing changes (agent registry, token auth, the scope-requests store, +`tinyagentos/routes/agent_*.py`, and the MCP surface) also require a doc: +either a page under `docs/agent-manual/` or `docs/agent-coordination.md` +satisfies the rule. ### Test-file exemption @@ -290,7 +293,7 @@ If your PR trips a rule and there is genuinely nothing to document (or you already covered it elsewhere), add a trailer line to a commit message instead of editing a doc: -``` +```text Docs-Reviewed: no user-facing change, internal refactor only ``` diff --git a/docs/doc-gate.toml b/docs/doc-gate.toml index 1f2befdbb..960c56f3f 100644 --- a/docs/doc-gate.toml +++ b/docs/doc-gate.toml @@ -92,9 +92,10 @@ hint = "the agent-token route allowlist changed; update the agent-facing API sur # doc inside the directory the gate went green on a change carrying no changelog # at all. # -# Broadened to cover every code change under tinyagentos/ or desktop/src/ that -# is not test-only: any such change requires a CHANGELOG.md edit or a new -# changelog.d/ fragment in the same PR. +# Broadened to cover every NON-TEST change under tinyagentos/ or desktop/src/, +# not only code: a doc or asset under those trees triggers it too (the test +# suite pins tinyagentos/README.md as a trigger). Any such change requires a +# CHANGELOG.md edit or a new changelog.d/ fragment in the same PR. [[rules]] name = "user-visible-changelog" on_modify = true diff --git a/tests/test_doc_gate.py b/tests/test_doc_gate.py index bfb53ecf8..d3a613c33 100644 --- a/tests/test_doc_gate.py +++ b/tests/test_doc_gate.py @@ -457,8 +457,12 @@ def test_modification_to_route_passes_with_trailer(self): assert dg.evaluate_rules(changed, messages, ROUTES_MODIFY_CONFIG) == [] def test_modified_test_file_does_not_trigger(self): - """Modifying a test file is never structural, even with on_modify.""" - changed = [("M", "tests/test_agents.py")] + """Modifying a test file is never structural, even with on_modify. + + The path must MATCH the rule's glob, otherwise the test passes for the + wrong reason -- it would pass with the exemption deleted. + """ + changed = [("M", "tinyagentos/routes/test_agents.py")] assert dg.evaluate_rules(changed, [], ROUTES_MODIFY_CONFIG) == [] @@ -509,9 +513,14 @@ def test_trailer_satisfies_broad_rule(self): assert dg.evaluate_rules(changed, messages, BROAD_CHANGELOG_CONFIG) == [] def test_test_file_under_tinyagentos_exempt(self): - """A test file under tinyagentos/ is not a structural change.""" - changed = [("M", "tests/test_agents.py")] - assert dg.evaluate_rules(changed, [], BROAD_CHANGELOG_CONFIG) == [] + """A test file under tinyagentos/ is not a structural change. + + `tests/` is outside the rule's globs, so it cannot prove the exemption; + these paths are inside them. + """ + for path in ("tinyagentos/test_helpers.py", "desktop/src/apps/Foo/Foo.test.tsx"): + changed = [("M", path)] + assert dg.evaluate_rules(changed, [], BROAD_CHANGELOG_CONFIG) == [], path def test_doc_file_under_tinyagentos_still_triggers(self): """A non-test doc file under tinyagentos/ should still require changelog."""