T-LWEH-entity-title-accessor
Status: closed/done · Impact: medium · Complexity: small
Three call sites hand-roll H1 extraction from entity bodies with regex/line
scans, none fence-aware (a # heading inside a code fence matches), while the
read layer’s projection already computes the fence-aware H1
(tree.root.name) and discards it. Surface the title on the read wrapper,
give bulk scans a cheap fence-aware helper, and converge the read-only H1
sites. This is the honest residue of the “typed body plane” wave-2: a
per-file assessment (2026-07-19) found every other body-parsing site LEAVE —
their hand-rolled parsing exists for fidelity the typed plane cannot serve.
| Location | Role today |
|---|---|
apps/sdlc/lib/model/read.ts | readEntity runs contract.validate and returns the wrapper; the projection’s tree.root.name (fence-aware H1) is computed and thrown away. |
apps/sdlc/lib/services/lease/ops/task/transition.ts | extractTaskTitle: splitFrontmatter + /^# (.+)$/m first match — not fence-aware; feeds the handoff title. |
apps/sdlc/lib/services/project/cleanup/index.ts | loadTasks: per-file line scan for # -prefixed line — not fence-aware; feeds TaskRecord.headline. |
apps/sdlc/skills/task-work/start_task.ts | extractHeadline: third copy of the regex — deliberately NOT converged (welded to the byte-faithful start-mutation flow; see Out of scope). |
Proposed
Section titled “Proposed”EntityReadResult(both arms) carriestitle: string | null— the fence-aware first H1 off the projection’stree.root.name,nullwhen the document has none.read.tsexportsextractTitle(text): string | null— a cheap fence-aware first-H1 scan (tracks ``` fences, skips frontmatter) for BULK contexts where the full-contract wrapper is disproportionate, mirroring thereadRawFrontmattersplit.transition.ts#extractTaskTitleandcleanup#loadTasksconverge (read-only call sites; the fence-awareness change is a bug fix, not a behavior regression).lib/model/README.mddocuments the title accessor next to the wrapper / bulk-primitive split.
Approach
Section titled “Approach”- Add
titletoReadResultBaseinlib/model/read.ts(populated fromresult.tree.root.name, empty →null). - Add
extractTitle(text)— strip frontmatter viasplitFrontmatter, walk lines tracking fence state, return the first#heading’s text trimmed, elsenull. Unit-test agreement with the projection on valid docs and on the fenced-# xcase the old regexes get wrong. - Converge
transition.ts#extractTaskTitleandcleanup#loadTasksontoextractTitle. Export fromlib/model/index.ts; note in README. - Tests in
lib/model/tests/read.test.ts:titleon both wrapper arms;extractTitlefence case, no-H1 case, frontmatter-only case.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
apps/sdlc/lib/model/read.ts | modify | title on the wrapper; extractTitle bulk helper. |
apps/sdlc/lib/model/index.ts | modify | Export extractTitle. |
apps/sdlc/lib/services/lease/ops/task/transition.ts | modify | extractTaskTitle → extractTitle. |
apps/sdlc/lib/services/project/cleanup/index.ts | modify | loadTasks H1 scan → extractTitle. |
apps/sdlc/lib/model/README.md | modify | Document the title accessor. |
apps/sdlc/lib/model/tests/read.test.ts | modify | Title + extractTitle coverage. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
readTaskon a valid task yieldstitleequal to the H1 on both arms; a document whose only# headingis inside a code fence yieldstitle: nullfrom both the wrapper andextractTitle(regression test — the old regexes matched it). - AC-2:
transition.tsandcleanup/index.tscontain no local H1 regex/line scan (grep-verifiable); handoff title andTaskRecord.headlinebehavior on real task files is unchanged. - AC-3:
bun testpasses forapps/sdlc;tsc --noEmitclean.
Out of scope
Section titled “Out of scope”parse-touchpoints/resolve-touchpoints/scan-placeholders/gap-report/scan_validate_ac/start_task.ts— per the 2026-07-19 body-plane assessment: their hand parsing preserves fidelity the typed plane discards (verbatim backticked cells, literal cell counts, the table/prose/bulleted-legacy/missing classification, raw line offsets and inline-code masking, PyYAML byte-faithful re-emission).start_task’s third H1 copy stays with its welded flow.- Exposing the full
DocTreeon the wrapper — deferred until a consumer needs it (gap-report needs the READY contract’s tree, not the birth-floor one the wrapper runs). - Dashboard typed reads (captured on the backlog separately).
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”- Wave-2 follow-up to
T-N9PM(PRs #928/#930). Scoped down from “converge body-plane consumers” after a per-file assessment returned LEAVE/PARTIAL on every candidate; the title accessor is the surviving clean win, and the fence-awareness fix is the latent-bug payoff.