T-FQFJ-author-skill-prose-contract
Status: closed/done · Impact: high · Complexity: large
Express every constraint the 14 invariants.yaml sidecars assert today
as a single markdown-contract contract, so skill-prose runs on the
same engine that already validates the entity corpus. The contract is a
base SKILL.md shape (required H2 sections as section() presence) plus
per-skill requires / forbids overlays. This is the effort
concentration of M-0008: the ~109 fuzzy section hints across the 14
sidecars must each resolve to a real heading (markdown-contract
matches headings exactly, not the old "5." -> ## 5. … fuzzy form),
and the few skills that scoped phrases to H3 subheadings need their
subsection trees modeled.
| Location | Role today |
|---|---|
plugin/skills/*/invariants.yaml | The 14 sidecars — per-skill required phrases (section-scoped and document-wide), forbidden phrases, required H2 sections, required tool refs; section hints matched fuzzily |
plugin/lib/services/gate/ops/_skill_prose_core.ts | The bespoke matcher that compiles and runs the sidecars; normalizes whitespace for required phrases, matches forbidden phrases raw |
plugin/lib/model/entities/_contracts.ts | Where the entity-corpus markdown-contract contracts live — the pattern the skill-prose contract follows |
Approach
Section titled “Approach”Mirror the sibling skill_md_harness.ts (build a contract() + inject rules,
run validate, filter findings by id), but skill-prose constraints are
per-skill, not universal — so the central design choice is keying.
- Module + shape. Add
plugin/lib/services/gate/ops/_skill_prose_contract.ts(co-located with the op, the role_skill_prose_core.tsholds today). Export a slug → constraints registry plus a builder that compiles each skill’s constraints into amarkdown-contractcontract, and a runner the op calls per SKILL.md. The op selects a skill’s contract by its directory slug. - Map each
invariants.yamlconstruct to a builder (audit: 14 skills · 122 required phrases — 81 section-scoped, 41 global · 22 required H2 · ~60 forbidden · 21 tool refs):- global
required_phrases→ documenttextRule({ requires: [{ pattern, normalize: true, note }] }). - section-scoped
required_phrases→section("<exact heading>", { rules: [requires([{ pattern, note }])] }). required_h2_sections→section("<heading>")presence in the body.forbidden_phrases→forbids([{ pattern, normalize: false, note }])(raw bytes). The upstream fixturetests/fixtures/validation/23-text-forbids-body-root.tsalready demonstrates the exact}scripts/+normalize: falsecase.required_tool_refs→ documenttextRule({ requires: [{ pattern: tool, normalize: false }] })(raw substring, matching the oldtext.includes).
- global
- Normalize asymmetry preserved: requires →
normalize: true(matches the oldphraseInProse), forbids + tool-refs →normalize: false. - Effort concentration — exact-heading resolution. The old matcher matched
hints fuzzily (
"Output contract"→## Output contract — deterministic markers;"5."→## 5. …;"5a."→ an H3). markdown-contract matches headings exactly, so resolve each of the ~81 hints to its real heading and model the H2 → H3 subsection trees the numbered-step hints need. Usesection([alias, …])where a heading has variant spellings. - Stable finding ids. Give each constraint an explicit
id(e.g.skill-prose/<slug>/<n>) so identities survive edits and the op can filter, mirroring theskill-md/*ids in the harness. - Verify standalone. Add a colocated unit test that loads the registry, asserts all 14 skills are covered, and asserts a known removed phrase and a present forbidden phrase each fire — the coverage-preservation home the old fixtures used to be.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/lib/services/gate/ops/_skill_prose_contract.ts | new | The skill-prose contract: a slug → constraints registry compiled into per-skill markdown-contract contracts via contract() / section() / requires / forbids / textRule; a builder + runner mirroring skill_md_harness.ts |
plugin/lib/services/gate/ops/_skill_prose_contract.test.ts | new | Unit test: registry covers all 14 skills; a removed required phrase and a present forbidden phrase each fire (coverage-preservation) |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: The contract covers the constraints of all 14 skills that ship an
invariants.yamltoday (globplugin/skills/*/invariants.yaml). - AC-2: Section-scoped phrases anchor to real section / subsection nodes via exact heading match; the H3 subsection trees the scoped phrases need are modeled, so the contract validates a skill’s section structure as a side effect, not just phrase presence.
- AC-3: All four constraint kinds the 14 sidecars assert — required phrases, forbidden phrases, required H2 sections, required tool refs — are preserved.
- AC-4: Forbidden phrases and tool-refs keep raw (non-normalized) matching —
forbids/ tool-ref entries setnormalize: falseso the brace-anchored}scripts/byte match still fires; required phrases keepnormalize: true. - AC-5: Per-skill constraints are keyed by skill slug, so a SKILL.md is validated against its own overlay (not a single universal contract).
- AC-6: A colocated unit test asserts the 14 skills (glob
plugin/skills/*/invariants.yaml) are covered and that a known regression — a removed required phrase and a present forbidden phrase — fails on the contract path.
Out of scope
Section titled “Out of scope”- Rewiring the gate op to run the contract — that is the next task; this task only authors it.
- Deleting
_skill_prose_core.tsor theinvariants.yamlfiles — later phase-C tasks.
Dependencies
Section titled “Dependencies”- T-3TAJ-bump-markdown-contract-text-constraints-pin — the contract uses the
requires/forbidsvocabulary that the dep bump pins. - The D-0011 surface (node attributes vs. content leaf vs. general rule) must be resolved upstream at PR review before authoring begins; it sets the contract-authoring syntax.
Depends on
Section titled “Depends on”T-3TAJ-bump-markdown-contract-text-constraints-pin