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.
Proposed
Section titled “Proposed”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.
Approach
Section titled “Approach”- Extend
.claude/skills/project-check/skill_md_harness.tswith a newdocRule,headingContiguityRule, and finding idID_HEADING_CONTIGUITY = 'skill-md/heading-contiguity', alongside the existingpipeTailRule/legacyScriptPathRule. For each###heading in the doc (fence-aware — reuse the samemarkdown-contractprimitivesscan-placeholders.tsalready 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. - Classify each finding: if the heading’s own text matches the numbered or
lettered catalogue shape (a leading
N[a-z]?.token, or aStep N[a-z]? —token, matching the shapes already inSKILL.md), mark itdisqualifier-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, matchingcheck_entities.ts’s Operations-table-drift warning precedent. - Add the thin renderer
.claude/skills/project-check/check_heading_contiguity.ts(#!/usr/bin/env bun), matching the house shape ofcheck_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 distributedsolutions/ontological/plugin/plugins/sdlc/skills/*/*.md(the existing sibling checks’ default corpus) — and prints per-fileOK/FAILlines plus a stderr summary. Exit codes:0no disqualifying findings (informational notes may still print),1at least one disqualifying finding,2script failure (no skill files found under either corpus). - 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 rightfile: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. - Wire the check into
.claude/skills/project-check/SKILL.md: a### 1i.entry under “What this skill checks today” and a matching### Step 1ientry under “Steps”, following the1g/Step 1gpair’s shape.1his landed — T-IEAL-config-path-globs-survive-moves’scheck_config_globs.tsmerged via #2358 — so1iis the next live slot, not a provisional guess: as of this task’s own revision, nothing else onorigin/mainclaims 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 to1j, not1h— that’s T-YYZU’s problem to fix on its own file, not evidence this task’s1iis contested. - Add a
project-check-heading-contiguitypre-commit command tolefthook.yml, alongside the otherproject-check-*commands, runningbun run .claude/skills/project-check/check_heading_contiguity.ts. - 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.
| Area | Note |
|---|---|
.claude/skills/project-check/skill_md_harness.ts | New shared docRule (headingContiguityRule) + finding id skill-md/heading-contiguity, alongside pipeTailRule / legacyScriptPathRule |
.claude/skills/project-check/check_heading_contiguity.ts | New thin renderer: walks both skill-note corpora, prints OK/FAIL, exits 0/1/2 |
.claude/skills/project-check/tests/check_heading_contiguity.test.ts | Fixture suite: clean tree, T-IEAL-shape regression fixture, two known-false-positive fixtures |
.claude/skills/project-check/SKILL.md | New ### 1i. catalogue entry + ### Step 1i runner entry |
lefthook.yml | New project-check-heading-contiguity pre-commit command |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
bun run .claude/skills/project-check/check_heading_contiguity.tsexits0against 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 exits1and 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 touchandsolutions/ontological/plugin/plugins/sdlc/skills/milestones-from-file/milestones-from-file.md’s### Frontmatter fields ...), the script still exits0— both surface only as informational notes, not failures. - AC-4:
bun test .claude/skills/project-check/tests/check_heading_contiguity.test.tspasses. - AC-5:
.claude/skills/project-check/SKILL.mdcontains both a### 1i.heading and a### Step 1iheading namingcheck_heading_contiguity.ts. - AC-6:
lefthook.ymlcontains aproject-check-heading-contiguitykey underpre-commit.commandsrunningbun run .claude/skills/project-check/check_heading_contiguity.ts. - AC-7: The script’s per-file output names
.claude/skills/project-check/SKILL.mdand at least one file undersolutions/ontological/plugin/plugins/sdlc/skills/(e.g.solutions/ontological/plugin/plugins/sdlc/skills/task-work/task-work.md), demonstrating both scanned corpora ran.
Out of scope
Section titled “Out of scope”- 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/## Dependenciesfollowed 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.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”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.