Skip to content

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.

LocationRole today
plugin/skills/*/invariants.yamlThe 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.tsThe bespoke matcher that compiles and runs the sidecars; normalizes whitespace for required phrases, matches forbidden phrases raw
plugin/lib/model/entities/_contracts.tsWhere the entity-corpus markdown-contract contracts live — the pattern the skill-prose contract follows

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.ts holds today). Export a slug → constraints registry plus a builder that compiles each skill’s constraints into a markdown-contract contract, and a runner the op calls per SKILL.md. The op selects a skill’s contract by its directory slug.
  • Map each invariants.yaml construct 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 → document textRule({ requires: [{ pattern, normalize: true, note }] }).
    • section-scoped required_phrasessection("<exact heading>", { rules: [requires([{ pattern, note }])] }).
    • required_h2_sectionssection("<heading>") presence in the body.
    • forbidden_phrasesforbids([{ pattern, normalize: false, note }]) (raw bytes). The upstream fixture tests/fixtures/validation/23-text-forbids-body-root.ts already demonstrates the exact }scripts/ + normalize: false case.
    • required_tool_refs → document textRule({ requires: [{ pattern: tool, normalize: false }] }) (raw substring, matching the old text.includes).
  • Normalize asymmetry preserved: requires → normalize: true (matches the old phraseInProse), 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. Use section([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 the skill-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.
LocationKindChange
plugin/lib/services/gate/ops/_skill_prose_contract.tsnewThe 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.tsnewUnit test: registry covers all 14 skills; a removed required phrase and a present forbidden phrase each fire (coverage-preservation)
  • AC-1: The contract covers the constraints of all 14 skills that ship an invariants.yaml today (glob plugin/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 set normalize: false so the brace-anchored }scripts/ byte match still fires; required phrases keep normalize: 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.
  • Rewiring the gate op to run the contract — that is the next task; this task only authors it.
  • Deleting _skill_prose_core.ts or the invariants.yaml files — later phase-C tasks.
  • T-3TAJ-bump-markdown-contract-text-constraints-pin — the contract uses the requires / forbids vocabulary 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.

T-3TAJ-bump-markdown-contract-text-constraints-pin


← Back to Tasks