T-PTDG-retire-pyyaml-emitter
Status: closed/done · Impact: medium · Complexity: small
lib/util/yaml.ts carried a ~550-line port of PyYAML’s emitter
(analyze_scalar, implicit resolvers, column-tracking scalar writers), kept
byte-identical to the retired Python implementation’s yaml.safe_dump
output. The Python substrate is fully deleted, so the byte-parity contract
has no counterparty — the port is pure legacy. Replace it with the npm
yaml package’s serializer under one house options object, and re-state the
emitter’s invariants as SEMANTIC (round-trip equality + idempotence), not
byte parity with a dead codebase.
| Location | Role today |
|---|---|
apps/sdlc/lib/util/yaml.ts | The PyYAML emitter port: PlainScalar, implicit-resolver regexes, Emitter scalar writers, pyStrip — behind dumpFrontmatter. |
apps/sdlc/lib/util/yaml.test.ts | Pins PyYAML represent_none parity output. |
apps/sdlc/lib/model/entity.ts | Scaffold assembly via dumpFrontmatter (new files). |
apps/sdlc/lib/model/authoring.ts | authorEntity emission via dumpFrontmatter (new files). |
apps/sdlc/lib/model/ops/migrate.ts | Whole-block re-emission during migrations. |
apps/sdlc/lib/util/diagnostics.ts | Prose says the PyYAML emitter “stays” — stale. |
apps/sdlc/skills/task-ensure-ready/ensure_ready_mutate.ts | Header still claims PyYAML-compatible emission; the code already moved to span-preserving edits. |
apps/sdlc/skills/entities-migrate/tests/entities_migrate.test.ts | Eval pins a PyYAML-quoted timestamp. |
Proposed
Section titled “Proposed”lib/util/yaml.tsbecomes the thin canonical seam:parse/loadre-exports,dumpFrontmatteroveryaml’sstringifywith one exported house options object (sortMapEntries: false,lineWidth: 0,singleQuote: true,indentSeq: false— chosen to match the corpus style), coercion (coerceYamlValue/coerceFrontmatter) retained so raw numbers land as the strings the schemas declare.PlainScalar/plainand the emitter machinery are deleted.- Invariants become semantic:
parse(dump(x))deep-equalsx; dumping is idempotent. Visible style deltas (measured over the 795-file corpus): date/timestamp-like strings emit bare (they were quoted only so PyYAML would not retype them — no YAML-1.1 reader remains), multiline strings emit as block scalars, quote style falls to theyamlpackage’s rules. Only whole-block writers restyle existing files (scaffold/authoring touch new files only; migrate runs are already restyle events; all in-place field writers are span-preserving via@sksizer/yaml-splice), so there is no corpus churn outside deliberate rewrites. - Stale PyYAML prose corrected (
diagnostics.ts,ensure_ready_mutate.ts); remaining mentions are historical narration only.
Approach
Section titled “Approach”- Rewrite
lib/util/yaml.ts(delete the emitter port; keep the parse chokepoint,YamlValue, coercion; add the house options export). - Replace
yaml.test.tsparity pins with house-style pins + semantic round-trip/idempotence cases over representative frontmatter shapes. - Update the one golden that pinned PyYAML quoting
(
entities_migrate.test.tsbare-timestamp expectation) and the stale prose indiagnostics.ts/ensure_ready_mutate.ts. - Full suite + typecheck.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
apps/sdlc/lib/util/yaml.ts | modify | Emitter port deleted; thin stringify seam + house options. |
apps/sdlc/lib/util/yaml.test.ts | modify | Parity pins → house-style + semantic invariants. |
apps/sdlc/lib/util/diagnostics.ts | modify | Stale “emitter stays” prose corrected. |
apps/sdlc/skills/task-ensure-ready/ensure_ready_mutate.ts | modify | Stale PyYAML-emission header corrected. |
apps/sdlc/skills/entities-migrate/tests/entities_migrate.test.ts | modify | Quoted-timestamp golden → bare. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
lib/util/yaml.tscontains no PyYAML emulation (no implicit resolver regexes, no scalar writers, noPlainScalar);dumpFrontmatteris astringifycall under the exported house options. - AC-2: yaml tests assert semantic round-trip + idempotence and the house
style (bare dates, indentless sequences,
'5'coercion, block scalars); no test in the repo requires byte parity with PyYAML. - AC-3: full
bun testforapps/sdlcpasses andtsc --noEmitis clean; the only golden change is the migrate eval’s bare timestamp. - AC-4: remaining “PyYAML” mentions in
apps/sdlcsource are historical narration only (grep-verifiable — no live requirement).
Out of scope
Section titled “Out of scope”- Corpus-wide restyle commit: not needed — in-place writers are span-preserving, so existing files restyle only under deliberate whole-block rewrites (migrate).
@sksizer/yaml-spliceand the span-preserving field editors — already the write path; untouched.- Historical references to PyYAML in docs/planning entities (records, not requirements).
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”- Operator directive (2026-07-31): “ditch any legacy requirements based on
pyyaml, as we are no longer using it at all.” Surfaced while discussing why
start_task.tsstayed out of T-LWEH’s scope (its then byte-faithful PyYAML re-emission); the write paths had since moved to span-preserving edits, leaving the emitter port with no remaining justification.