Skip to content

T-JQSF-doc-heading-contiguity-check

State: open/ready · Impact: medium · Complexity: small

AUTO-DEFINED: this spec was best-effort machine-authored by /sdlc:task-auto-define on 2026-09-26 because the task is autonomy: autonomous/pr. Review the Goal, Approach, Today, Areas, and Acceptance-criteria carefully before trusting it.

A gap on this repo, surfaced by the post-mortem of T-IEAL-config-path-globs-survive-moves in git@github.com:sksizer/dev.git, needs closing:

During T-IEAL, a plain string insertion of a new ### 1h. heading into .claude/skills/project-check/SKILL.md landed between section 1g’s closing paragraph and 1g’s own trailing command-seam bullet, orphaning that bullet under the new 1h heading instead of leaving it under 1g. A plain string insertion after a section’s last paragraph does not account for trailing content that is not a paragraph (e.g. a bullet list). Add a lint (a candidate for .claude/skills/project-check/check_*.ts, matching the sibling check_config_globs.ts this same task added) that flags a bare bullet list immediately following a ### heading with no leading prose, or more generally asserts a document’s headings and their body content stay contiguous, so this class of misplacement is caught before review rather than by manual inspection.

.claude/skills/project-check/ holds eleven deterministic gate scripts (check_entities.ts, check_pipe_tail.ts, check_skill_docs.ts, check_entity_bases.ts, check_identifier_shape.ts, check_op_modules.ts, check_no_legacy_script_paths.ts, check_util_redeclarations.ts, check_apps_imports.ts, check_skill_prose.ts, check_config_globs.ts), each cross-referenced in SKILL.md’s “What this skill checks today” catalogue and wired as its own project-check-* pre-commit command in lefthook.yml. check_config_globs.ts landed via T-IEAL-config-path-globs-survive-moves (#2358) as ### 1h. — see Approach step 5 for what that means for this task’s own slot. Three of them (check_pipe_tail.ts, check_no_legacy_script_paths.ts, check_skill_docs.ts) share one markdown-contract harness at .claude/skills/project-check/skill_md_harness.ts — a single fence-aware SKILL.md contract with pluggable docRules (pipeTailRule, legacyScriptPathRule) — rather than each hand-rolling its own scan.

check_skill_prose.ts is the closest existing sibling in spirit: it already runs a markdown-contract-based structural lint (gate skill-prose, delegating to solutions/ontological/lib/services/gate/ops/_skill_prose_contract.ts) over skill notes. It doesn’t already cover this case, though — its overlay checks required_h2_sections presence and pinned phrases per skill (opt-in, via SKILL_PROSE_REGISTRY), not whether a heading’s body stays contiguous with that heading, and it only walks the distributed solutions/ontological/plugin/plugins/sdlc/skills/ corpus, not the project-local .claude/skills/*/SKILL.md corpus where the T-IEAL incident happened.

SKILL.md’s own catalogue is itself the numbered/lettered H3 sequence this task is about: ### 0., ### 1., ### 1a. … ### 1g. under “What this skill checks today”, mirrored by ### Step 0 … ### Step 1g. under “Steps”. That is exactly the shape a plain-string heading insertion can corrupt — as it did during T-IEAL-config-path-globs-survive-moves’s implementation, where a ### 1h. insertion landed between 1g’s closing paragraph and 1g’s own trailing bullet, silently reassigning that bullet to the new section (see Goal). No existing check inspects whether a heading’s own body stays contiguous with that heading; this class of edit mistake is currently only caught by manual review.

Add .claude/skills/project-check/check_heading_contiguity.ts, a new sibling gate, that flags a ### (H3) heading whose body opens with a bare list item and no leading prose — the structural signature of a trailing bullet left behind by a heading inserted just above it.

A bare “H3 immediately followed by a list” is not always a mistake (a heading can legitimately exist just to introduce a list — confirmed by scanning the current skill-note corpus: docs-sweep.md’s ### Never touch and milestones-from-file.md’s ### Frontmatter fields (passed to \sdlc milestone create`) both open directly with a bullet and are fine). To keep the gate low-noise, only a heading that carries the project's numbered/lettered catalogue shape (### 1a. …, ### Step 1a — …, and siblings) — the exact shape a mechanical sequence-heading insertion produces — fails the gate; any other bare-bullet-after-H3 hit is reported as a non-failing informational note, mirroring check_entities.ts`‘s existing warning-vs-failure split for Operations-table drift.

M-8DQW-one-contract-layer (open/planned) is converging the repo’s several markdown-structure dialects into one markdown-contract layer, and lists mdschema’s “heading-integrity” family (no_skip_levels / unique / max_depth, per D-7VMX-markdown-ecosystem-strategy) as a vocabulary gap to file upstream rather than patch locally. That family checks heading level skips and uniqueness, not whether a heading’s body stays contiguous with the heading above it — a different specific defect. Going local now rather than waiting on or feeding that convergence is deliberate: this gate is a small, narrow docRule on the existing skill_md_harness.ts (following pipeTailRule / legacyScriptPathRule’s own precedent), scoped to the skill-note corpus where the incident actually happened, not a new mdschema rule shipped through the one-contract-layer effort. If M-8DQW’s convergence later grows a content-contiguity primitive, this check is a candidate to fold into it then.

  1. Extend .claude/skills/project-check/skill_md_harness.ts with a new docRule, headingContiguityRule, and finding id ID_HEADING_CONTIGUITY = 'skill-md/heading-contiguity', alongside the existing pipeTailRule / legacyScriptPathRule. For each ### heading in the doc (fence-aware — reuse the same markdown-contract primitives scan-placeholders.ts already imports, sectionSpans / codeBlockLines, so a ### inside a code fence is never treated as a heading), find the first non-blank line of its body before the next heading of any level. If that line is a bullet (- / *) or numbered-list item, record a finding at that line citing the heading text.
  2. Classify each finding: if the heading’s own text matches the numbered or lettered catalogue shape (a leading N[a-z]?. token, or a Step N[a-z]? — token, matching the shapes already in SKILL.md), mark it disqualifier-class — the rendered message names the heading and the line, and says the bullet may be orphaned from the section above it. Otherwise mark it informational — printed but never a failure, matching check_entities.ts’s Operations-table-drift warning precedent.
  3. Add the thin renderer .claude/skills/project-check/check_heading_contiguity.ts (#!/usr/bin/env bun), matching the house shape of check_pipe_tail.ts / check_no_legacy_script_paths.ts: module docstring, parseArgs, main(argv) returning an exit code, if (import.meta.main) process.exit(main(process.argv.slice(2))). It walks BOTH skill-note corpora — the project-local .claude/skills/*/SKILL.md (where the T-IEAL incident happened) and the distributed solutions/ontological/plugin/plugins/sdlc/skills/*/*.md (the existing sibling checks’ default corpus) — and prints per-file OK / FAIL lines plus a stderr summary. Exit codes: 0 no disqualifying findings (informational notes may still print), 1 at least one disqualifying finding, 2 script failure (no skill files found under either corpus).
  4. Add .claude/skills/project-check/tests/check_heading_contiguity.test.ts, mirroring .claude/skills/project-check/tests/check_util_redeclarations.test.ts’s fixture pattern: (a) a clean fixture tree → exit 0; (b) a regression fixture reproducing the T-IEAL 1g/1h incident shape (a numbered heading directly followed by a bullet that reads as a continuation of the section above) → exit 1, citing the right file:line; (c) fixtures for the two known non-numbered headings already in the corpus (docs-sweep.md’s ### Never touch, milestones-from-file.md’s ### Frontmatter fields ...) → exit 0, each surfacing only as an informational note.
  5. Wire the check into .claude/skills/project-check/SKILL.md: a ### 1i. entry under “What this skill checks today” and a matching ### Step 1i entry under “Steps”, following the 1g / Step 1g pair’s shape. 1h is landed — T-IEAL-config-path-globs-survive-moves’s check_config_globs.ts merged via #2358 — so 1i is the next live slot, not a provisional guess: as of this task’s own revision, nothing else on origin/main claims it. T-YYZU-moon-yml-schema-depth-check (open/ready) still says ### 1h. in its own body, but that slot is now taken; its own next rebase needs to move to 1j, not 1h — that’s T-YYZU’s problem to fix on its own file, not evidence this task’s 1i is contested.
  6. Add a project-check-heading-contiguity pre-commit command to lefthook.yml, alongside the other project-check-* commands, running bun run .claude/skills/project-check/check_heading_contiguity.ts.
  7. Verify: run the new script against the current tree (expect exit 0, with informational notes on the two known non-numbered headings), run the new test suite, and confirm the lefthook wiring fires on a throwaway commit.
AreaNote
.claude/skills/project-check/skill_md_harness.tsNew shared docRule (headingContiguityRule) + finding id skill-md/heading-contiguity, alongside pipeTailRule / legacyScriptPathRule
.claude/skills/project-check/check_heading_contiguity.tsNew thin renderer: walks both skill-note corpora, prints OK/FAIL, exits 0/1/2
.claude/skills/project-check/tests/check_heading_contiguity.test.tsFixture suite: clean tree, T-IEAL-shape regression fixture, two known-false-positive fixtures
.claude/skills/project-check/SKILL.mdNew ### 1i. catalogue entry + ### Step 1i runner entry
lefthook.ymlNew project-check-heading-contiguity pre-commit command
  • AC-1: bun run .claude/skills/project-check/check_heading_contiguity.ts exits 0 against the current working tree.
  • AC-2: Against a fixture reproducing the T-IEAL 1g/1h incident shape (a numbered/lettered ### heading whose first body line is a bare list item with no leading prose, immediately preceded by another section’s closing paragraph), the script exits 1 and its output names the offending heading and cites <file>:<line>.
  • AC-3: Against the two known non-numbered headings already in the corpus (solutions/ontological/plugin/plugins/sdlc/skills/docs-sweep/docs-sweep.md’s ### Never touch and solutions/ontological/plugin/plugins/sdlc/skills/milestones-from-file/milestones-from-file.md’s ### Frontmatter fields ...), the script still exits 0 — both surface only as informational notes, not failures.
  • AC-4: bun test .claude/skills/project-check/tests/check_heading_contiguity.test.ts passes.
  • AC-5: .claude/skills/project-check/SKILL.md contains both a ### 1i. heading and a ### Step 1i heading naming check_heading_contiguity.ts.
  • AC-6: lefthook.yml contains a project-check-heading-contiguity key under pre-commit.commands running bun run .claude/skills/project-check/check_heading_contiguity.ts.
  • AC-7: The script’s per-file output names .claude/skills/project-check/SKILL.md and at least one file under solutions/ontological/plugin/plugins/sdlc/skills/ (e.g. solutions/ontological/plugin/plugins/sdlc/skills/task-work/task-work.md), demonstrating both scanned corpora ran.
  • Detecting the same class of mistake outside the skill-note corpus (e.g. docs/planning/tasks/*.md, general prose docs). Those corpora use H2 sections that legitimately open with a bare bullet (## Out of scope / ## Dependencies followed by - none), so a blanket H2-or-higher rule there would false-fail constantly; revisit with its own opt-in mechanism if a similar incident recurs elsewhere.
  • Auto-repairing an orphaned bullet once flagged. This task adds detection only; moving the bullet back under its correct section stays a manual fix.
  • Any heading level other than ### (H3). The incident and the existing numbered-catalogue convention are H3-specific.
  • none

Spawned by /sdlc:spawn-task-pr on 2026-09-27 UTC from T-IEAL-config-path-globs-survive-moves in git@github.com:sksizer/dev.git.


← Back to Tasks