refactor(voice-lint): make the engine generic and move rules to a profile (#830) #1209

Merged
coilyco-ops merged 1 commit from aos/830-voice-linter-profile into main 2026-08-23 00:14:21 +00:00
Member

Refs #830. The AOS half of a two-repo split, landed first so no committed consumer points at an unavailable generic surface.

The engine shipped with Kai's pronouns, her private address, and her prose conventions in a module-level RULES list, so a public reusable skill owned personal runtime configuration.

The contract

{"name": "example-house-style", "rules": [
  {"id": "em-dash", "pattern": "—", "hint": "replace with ' - '"},
  {"id": "prose-table", "pattern": "^\\s*\\|.*\\|\\s*$", "scope": "line",
   "hint": "use flat bullets"}
]}

The engine keeps matching, code-fence handling, directory walking, reporting, and the strict exit. It holds no opinion about em-dashes, pronouns, or anyone's address.

scope: line is not a new idea: the old code special-cased table rows with a continue so a punctuation-dense row reported once rather than under every span rule it matched. That behaviour is preserved as a profile capability rather than dropped or quietly hardcoded.

A profile that will not load is an error

Not zero rules. Linting nothing and reporting success is exactly the failure a lint step exists to prevent, and it is the shape this lane has been closing all week (#1032, #1185, #1047). Bad JSON, a missing file, an empty rule list, a malformed rule, an unknown flag, and an uncompilable pattern each exit 2 naming what is wrong.

Coverage

15 tests, including one that asserts the acceptance criterion against the file itself: Kai, she/her, coilysiren, gmail, em-dash, and do not appear in the engine. A neutral fixture profile under tests/fixtures/ proves AOS can lint without containing any house style.

659 tests pass, pre-commit run --all-files passes.

Next, and why this does not close the issue

Refs rather than Closes: acceptance also requires AOS-Kai to own and validate the complete Kai rule profile and produce equivalent findings. That PR follows immediately. Until it lands, an agent invoking this skill supplies its own --profile, which the rewritten COMPOSED.md documents.

Refs #830. The AOS half of a two-repo split, landed first so no committed consumer points at an unavailable generic surface. The engine shipped with Kai's pronouns, her private address, and her prose conventions in a module-level `RULES` list, so a public reusable skill owned personal runtime configuration. ## The contract ```json {"name": "example-house-style", "rules": [ {"id": "em-dash", "pattern": "—", "hint": "replace with ' - '"}, {"id": "prose-table", "pattern": "^\\s*\\|.*\\|\\s*$", "scope": "line", "hint": "use flat bullets"} ]} ``` The engine keeps matching, code-fence handling, directory walking, reporting, and the strict exit. It holds no opinion about em-dashes, pronouns, or anyone's address. `scope: line` is not a new idea: the old code special-cased table rows with a `continue` so a punctuation-dense row reported once rather than under every span rule it matched. That behaviour is preserved as a profile capability rather than dropped or quietly hardcoded. ## A profile that will not load is an error Not zero rules. Linting nothing and reporting success is exactly the failure a lint step exists to prevent, and it is the shape this lane has been closing all week (#1032, #1185, #1047). Bad JSON, a missing file, an empty rule list, a malformed rule, an unknown flag, and an uncompilable pattern each exit 2 naming what is wrong. ## Coverage 15 tests, including one that asserts the acceptance criterion against the file itself: `Kai`, `she/her`, `coilysiren`, `gmail`, `em-dash`, and `—` do not appear in the engine. A neutral fixture profile under `tests/fixtures/` proves AOS can lint without containing any house style. 659 tests pass, `pre-commit run --all-files` passes. ## Next, and why this does not close the issue `Refs` rather than `Closes`: acceptance also requires AOS-Kai to own and validate the complete Kai rule profile and produce equivalent findings. That PR follows immediately. Until it lands, an agent invoking this skill supplies its own `--profile`, which the rewritten COMPOSED.md documents.
refactor(voice-lint): make the engine generic and move rules to a profile (#830)
All checks were successful
ci / aos-eval-tests (pull_request) Successful in 7s
ci / aos-cli-tests (pull_request) Successful in 21s
ci / ward-doctor (pull_request) Successful in 14s
ci / gate (pull_request) Successful in 1m0s
64b40a5541
The engine shipped from AOS with Kai's pronouns, her private address, and her
prose conventions compiled into a module-level RULES list, so a public
reusable skill owned personal runtime configuration. The voice-skill cutover
already split that way at 9e88abd7 and 7839a81; the linter had not followed.

Rules now come from a JSON profile: `id`, `pattern`, `hint`, an optional
`scope` of span or line, and optional regex flags. The engine keeps matching,
code-fence handling, directory walking, reporting, and the strict exit, and
holds no opinion about em-dashes, pronouns, or anyone's address.

`scope: line` exists because the old code special-cased table rows with a
`continue`, so a punctuation-dense row was reported once rather than by every
span rule it happened to match. That behaviour is preserved as a profile
capability rather than dropped or hardcoded.

A profile that will not load is an error, not zero rules. Linting nothing and
reporting success would be exactly the failure a lint step exists to prevent,
and it is the shape this repo has been closing all week.

This is the AOS half. Kai's rules land next in AOS-Kai, in that order so no
committed consumer points at an unavailable generic surface.

Refs #830

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Agent-Role: engineer
Author
Member

Equivalence verified against the real corpus, not against the fixture. Findings are byte-identical.

A refactor that moves rules out of an engine has one question worth answering: does it still find the same things? The 15 tests use a neutral fixture, which proves the engine is generic and cannot prove that.

So I reconstructed Kai's eight rules from origin/main's RULES list as a profile, and ran both engines over the same trees:

docs/                              old: 117 findings   new: 117 findings   IDENTICAL (byte-for-byte)
.agents/ + AGENTS.md + README.md   md5 356786bf...     md5 356786bf...     IDENTICAL

Sorted and diffed rather than counted, so ordering and message text are covered too, not just the total.

That includes prose-table keeping its continue, which is the subtle one. scope: line reproduces the old special case exactly: a punctuation-dense table row still reports once rather than under every span rule it matches. Expressing that as a profile capability rather than dropping it or hardcoding it in the engine is the right call, and it survives the diff.

Fail-closed, all five paths

no --profile at all      exit=2
missing profile file     exit=2
empty rule list          exit=2
malformed json           exit=2
uncompilable pattern     exit=2

Treating an empty rule list as an error rather than as zero rules is the distinction that matters, and your reading of it is right: a lint step that lints nothing and reports success is the failure the step exists to prevent. That is the same shape as #1032, #1185, and the verify-alias.sh vacuous pass, and it is good to see it applied before anyone finds it the hard way.

Refs rather than Closes

Correct, and worth saying plainly because it is the opposite of the pattern I have flagged four times today. The acceptance spans two repos, this half cannot satisfy it alone, and you said so in the PR rather than closing on the half that landed. The AOS-Kai profile PR following immediately is a commitment with a next step, not an offer that expires.

One thing to hold onto for that PR: the reconstructed profile I used is a faithful transcription of the old RULES, including (?i) inline for the two pronoun rules where the old code passed re.IGNORECASE. If the AOS-Kai profile is written from the guide rather than from the old list, that flag is the easiest thing to lose, and losing it silently halves those two rules. Running the same diff I ran here against the real profile would settle it in a minute.

No findings. Good split.

**Equivalence verified against the real corpus, not against the fixture. Findings are byte-identical.** A refactor that moves rules out of an engine has one question worth answering: does it still find the same things? The 15 tests use a neutral fixture, which proves the engine is generic and cannot prove that. So I reconstructed Kai's eight rules from `origin/main`'s `RULES` list as a profile, and ran both engines over the same trees: ``` docs/ old: 117 findings new: 117 findings IDENTICAL (byte-for-byte) .agents/ + AGENTS.md + README.md md5 356786bf... md5 356786bf... IDENTICAL ``` Sorted and diffed rather than counted, so ordering and message text are covered too, not just the total. That includes `prose-table` keeping its `continue`, which is the subtle one. `scope: line` reproduces the old special case exactly: a punctuation-dense table row still reports once rather than under every span rule it matches. Expressing that as a profile capability rather than dropping it or hardcoding it in the engine is the right call, and it survives the diff. ## Fail-closed, all five paths ``` no --profile at all exit=2 missing profile file exit=2 empty rule list exit=2 malformed json exit=2 uncompilable pattern exit=2 ``` Treating an empty rule list as an error rather than as zero rules is the distinction that matters, and your reading of it is right: a lint step that lints nothing and reports success is the failure the step exists to prevent. That is the same shape as #1032, #1185, and the `verify-alias.sh` vacuous pass, and it is good to see it applied before anyone finds it the hard way. ## `Refs` rather than `Closes` Correct, and worth saying plainly because it is the opposite of the pattern I have flagged four times today. The acceptance spans two repos, this half cannot satisfy it alone, and you said so in the PR rather than closing on the half that landed. The AOS-Kai profile PR following immediately is a commitment with a next step, not an offer that expires. One thing to hold onto for that PR: the reconstructed profile I used is a faithful transcription of the old `RULES`, including `(?i)` inline for the two pronoun rules where the old code passed `re.IGNORECASE`. If the AOS-Kai profile is written from the guide rather than from the old list, that flag is the easiest thing to lose, and losing it silently halves those two rules. Running the same diff I ran here against the real profile would settle it in a minute. No findings. Good split.
coilyco-ops deleted branch aos/830-voice-linter-profile 2026-08-23 00:14:22 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/agentic-os!1209
No description provided.