diff --git a/conductor/contracts/substack-cli-maturity.contract.json b/conductor/contracts/substack-cli-maturity.contract.json index d9e13e21..c4c77be6 100644 --- a/conductor/contracts/substack-cli-maturity.contract.json +++ b/conductor/contracts/substack-cli-maturity.contract.json @@ -2600,6 +2600,26 @@ "url": "https://github.com/edithatogo/substack-cli-ts/issues/537", "title": "Prevent title and subtitle duplication in published post bodies" } + }, + { + "id": "T18-02", + "phaseId": "P18", + "title": "Metadata duplication transport regression matrix", + "status": "planned", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "requiredTaskCount": 3, + "existingIssues": [ + 544 + ], + "issue": { + "number": 544, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/544", + "title": "T18-02 Metadata duplication transport regression matrix" + } } ], "tasks": [ @@ -7193,6 +7213,57 @@ "UX-CLI-CONTRACT-005" ], "release": "next patch" + }, + { + "id": "T18-02-TASK-01", + "title": "Add table-driven preparation regression matrix", + "trackId": "T18-02", + "phaseId": "P18", + "status": "planned", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "issue": { + "number": 545, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/545", + "title": "T18-02 Add table-driven preparation regression matrix" + } + }, + { + "id": "T18-02-TASK-02", + "title": "Assert transport payload preserves metadata/body separation", + "trackId": "T18-02", + "phaseId": "P18", + "status": "planned", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "issue": { + "number": 546, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/546", + "title": "T18-02 Assert transport payload preserves metadata/body separation" + } + }, + { + "id": "T18-02-TASK-03", + "title": "Run and record deterministic regression verification", + "trackId": "T18-02", + "phaseId": "P18", + "status": "planned", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "issue": { + "number": 547, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/547", + "title": "T18-02 Run and record deterministic regression verification" + } } ], "issueMap": { diff --git a/conductor/programme-state.json b/conductor/programme-state.json index 99114be9..da961292 100644 --- a/conductor/programme-state.json +++ b/conductor/programme-state.json @@ -88,8 +88,8 @@ { "id": "P18", "issue": 538, - "state": "CLOSED", - "status": "Done" + "state": "OPEN", + "status": "In Progress" } ], "tracks": [ @@ -468,6 +468,13 @@ "state": "CLOSED", "status": "Done", "evidence": "conductor/archive/T18-01/verification.md" + }, + { + "id": "T18-02", + "phase": "P18", + "issue": 544, + "state": "OPEN", + "status": "Todo" } ], "tasks": [ @@ -2022,6 +2029,27 @@ "state": "CLOSED", "status": "Done", "evidence": "conductor/archive/T18-01/verification.md" + }, + { + "id": "T18-02-TASK-01", + "track": "T18-02", + "issue": 545, + "state": "OPEN", + "status": "Todo" + }, + { + "id": "T18-02-TASK-02", + "track": "T18-02", + "issue": 546, + "state": "OPEN", + "status": "Todo" + }, + { + "id": "T18-02-TASK-03", + "track": "T18-02", + "issue": 547, + "state": "OPEN", + "status": "Todo" } ], "supplemental": [ diff --git a/conductor/traceability.json b/conductor/traceability.json index 69591b71..d400fcce 100644 --- a/conductor/traceability.json +++ b/conductor/traceability.json @@ -157,9 +157,10 @@ "title": "Metadata/body de-duplication correctness", "releaseTarget": "next patch", "tracks": [ - "T18-01" + "T18-01", + "T18-02" ], - "status": "contract_verified" + "status": "in_progress" } }, "tracks": { @@ -862,6 +863,26 @@ 537 ], "issue": null + }, + "T18-02": { + "id": "T18-02", + "phaseId": "P18", + "title": "Metadata duplication transport regression matrix", + "status": "planned", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "requiredTaskCount": 3, + "existingIssues": [ + 544 + ], + "issue": { + "number": 544, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/544", + "title": "T18-02 Metadata duplication transport regression matrix" + } } }, "tasks": { @@ -4679,6 +4700,57 @@ "UX-CLI-CONTRACT-005" ], "release": "next patch" + }, + "T18-02-TASK-01": { + "id": "T18-02-TASK-01", + "title": "Add table-driven preparation regression matrix", + "trackId": "T18-02", + "phaseId": "P18", + "status": "planned", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "issue": { + "number": 545, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/545", + "title": "T18-02 Add table-driven preparation regression matrix" + } + }, + "T18-02-TASK-02": { + "id": "T18-02-TASK-02", + "title": "Assert transport payload preserves metadata/body separation", + "trackId": "T18-02", + "phaseId": "P18", + "status": "planned", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "issue": { + "number": 546, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/546", + "title": "T18-02 Assert transport payload preserves metadata/body separation" + } + }, + "T18-02-TASK-03": { + "id": "T18-02-TASK-03", + "title": "Run and record deterministic regression verification", + "trackId": "T18-02", + "phaseId": "P18", + "status": "planned", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "issue": { + "number": 547, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/547", + "title": "T18-02 Run and record deterministic regression verification" + } } }, "contractMap": {}, diff --git a/conductor/tracks.md b/conductor/tracks.md index 58c788eb..64941b55 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -90,3 +90,4 @@ Canonical status is evidence-gated by [programme-state.json](./programme-state.j ## P18 Metadata/body de-duplication correctness - [x] **T18-01 Metadata/body de-duplication correctness** ([#537](https://github.com/edithatogo/substack-cli-ts/issues/537)) ([evidence](./archive/T18-01/index.md)) +- [ ] **T18-02 Metadata duplication transport regression matrix** ([#544](https://github.com/edithatogo/substack-cli-ts/issues/544)) diff --git a/conductor/tracks/T18-02/contracts.json b/conductor/tracks/T18-02/contracts.json new file mode 100644 index 00000000..e175c0c9 --- /dev/null +++ b/conductor/tracks/T18-02/contracts.json @@ -0,0 +1,8 @@ +{ + "schemaVersion": 1, + "trackId": "T18-02", + "contractIds": ["PARSER-AST-006", "FEATURE-FIDELITY-008", "UX-CLI-CONTRACT-005"], + "programmeIssue": 184, + "project": 38 +} + diff --git a/conductor/tracks/T18-02/design.md b/conductor/tracks/T18-02/design.md new file mode 100644 index 00000000..84521838 --- /dev/null +++ b/conductor/tracks/T18-02/design.md @@ -0,0 +1,15 @@ +# Design T18-02 + +```mermaid +flowchart LR + A["Markdown case matrix"] --> B["Shared preparePost boundary"] + B --> C["Markdown / HTML / ProseMirror assertions"] + C --> D["buildSubstackDraftPayload"] + D --> E["Serialized draft-write body assertion"] + E --> F{"Metadata absent from body?"} + F -->|Yes| G["Regression passes"] + F -->|No| H["Regression fails"] +``` + +The tests intentionally inspect each representation rather than only checking a final rendered string. This prevents a later transport adapter from reintroducing a defect already fixed at preparation time. + diff --git a/conductor/tracks/T18-02/index.md b/conductor/tracks/T18-02/index.md new file mode 100644 index 00000000..d4b82590 --- /dev/null +++ b/conductor/tracks/T18-02/index.md @@ -0,0 +1,10 @@ +# T18-02 Metadata duplication transport regression matrix + +- [Specification](./spec.md) +- [Requirements](./requirements.md) +- [Design](./design.md) +- [Plan](./plan.md) +- [Verification](./verification.md) +- [Metadata](./metadata.json) +- [GitHub issue #544](https://github.com/edithatogo/substack-cli-ts/issues/544) + diff --git a/conductor/tracks/T18-02/metadata.json b/conductor/tracks/T18-02/metadata.json new file mode 100644 index 00000000..f52fd811 --- /dev/null +++ b/conductor/tracks/T18-02/metadata.json @@ -0,0 +1,12 @@ +{ + "trackId": "T18-02", + "title": "Metadata duplication transport regression matrix", + "type": "quality", + "status": "in_progress", + "parentIssue": 538, + "issue": 544, + "taskIssues": [545, 546, 547], + "project": 38, + "createdAt": "2026-08-11T00:10:00+10:00" +} + diff --git a/conductor/tracks/T18-02/plan.md b/conductor/tracks/T18-02/plan.md new file mode 100644 index 00000000..4c3d4e3f --- /dev/null +++ b/conductor/tracks/T18-02/plan.md @@ -0,0 +1,8 @@ +# Plan T18-02 + +1. Add the preparation matrix for metadata/body representations and preservation cases. +2. Add a payload serialization regression proving transport does not reintroduce duplication. +3. Run focused, type, full, contract, governance, formatting, and diff checks. +4. Open a focused pull request, monitor checks and comments, and merge only after required checks pass. +5. Record evidence, close the track, and remove its disposable worktree and branch. + diff --git a/conductor/tracks/T18-02/requirements.md b/conductor/tracks/T18-02/requirements.md new file mode 100644 index 00000000..f16ccf3b --- /dev/null +++ b/conductor/tracks/T18-02/requirements.md @@ -0,0 +1,24 @@ +# Requirements T18-02 + +## Must + +- Add table-driven regression cases for plain, formatted, and heading metadata representations. +- Assert synchronized Markdown, HTML, and ProseMirror output after preparation. +- Assert the serialized publish payload does not reintroduce removed metadata. +- Preserve mismatch and later-body-match behavior. +- Keep the tests deterministic and independent of live credentials. + +## Should + +- Exercise title fallback when front matter does not provide a title. +- Keep the matrix easy to extend for future parser representations. + +## Could + +- Add fixture-backed cases for additional Markdown constructs. + +## Won't + +- Add fuzzy or semantic deletion of body content. +- Add live Substack calls to the regression suite. + diff --git a/conductor/tracks/T18-02/risks.md b/conductor/tracks/T18-02/risks.md new file mode 100644 index 00000000..9eb67b1d --- /dev/null +++ b/conductor/tracks/T18-02/risks.md @@ -0,0 +1,6 @@ +# Risks T18-02 + +- Parser output changes could make a representation-specific case fail; the matrix should expose that drift before publication. +- A future transport adapter could serialize a different body shape; the payload assertion covers the current draft-write boundary. +- Live Substack behavior remains outside deterministic CI and requires the existing canary controls. + diff --git a/conductor/tracks/T18-02/spec.md b/conductor/tracks/T18-02/spec.md new file mode 100644 index 00000000..8ec9a712 --- /dev/null +++ b/conductor/tracks/T18-02/spec.md @@ -0,0 +1,6 @@ +# Specification T18-02 + +The publishing pipeline must preserve a single canonical copy of title and subtitle metadata. Regression coverage must prove that normalization is retained through preparation, ProseMirror payload construction, and serialized draft-write request construction. + +The matrix covers plain, formatted, and heading representations; title fallback; mismatches; and later body matches. Exact leading metadata is removable, while non-leading or non-matching content remains publishable body content. + diff --git a/conductor/tracks/T18-02/verification.md b/conductor/tracks/T18-02/verification.md new file mode 100644 index 00000000..ff80e8f7 --- /dev/null +++ b/conductor/tracks/T18-02/verification.md @@ -0,0 +1,4 @@ +# Verification T18-02 + +Pending implementation and hosted CI evidence. + diff --git a/src/publish/prepare.test.ts b/src/publish/prepare.test.ts index 78c413fe..d01a6aad 100644 --- a/src/publish/prepare.test.ts +++ b/src/publish/prepare.test.ts @@ -6,6 +6,116 @@ import { describe, it } from "vitest"; import { preparePost } from "./prepare.js"; describe("preparePost", () => { + const metadataCases = [ + { + name: "plain title and subtitle paragraphs", + source: `--- +title: Post Title +subtitle: Post subtitle +--- +Post Title + +Post subtitle + +Body. +`, + expectedBody: "Body.", + }, + { + name: "formatted title and subtitle paragraphs", + source: `--- +title: Formatted title +subtitle: Formatted subtitle +--- +**Formatted title** + +*Formatted subtitle* + +Body. +`, + expectedBody: "Body.", + }, + { + name: "heading title and subtitle", + source: `--- +title: Heading title +subtitle: Heading subtitle +--- +# Heading title + +## Heading subtitle + +Body. +`, + expectedBody: "Body.", + }, + { + name: "mismatched metadata remains in the body", + source: `--- +title: Header title +subtitle: Header subtitle +--- +# Different title + +Different subtitle + +Body. +`, + expectedBody: "# Different title\n\nDifferent subtitle\n\nBody.", + }, + { + name: "matching text later in the body remains", + source: `--- +title: Header title +subtitle: Header subtitle +--- +Introductory text + +Header title + +Header subtitle +`, + expectedBody: "Introductory text\n\nHeader title\n\nHeader subtitle", + }, + { + name: "heading fallback becomes metadata without duplication", + source: `# Heading fallback + +Body. +`, + expectedBody: "Body.", + }, + ] as const; + + it.each(metadataCases)( + "keeps metadata and body representations consistent: $name", + async (testCase) => { + const file = await writeTempMarkdown(testCase.source); + + try { + const prepared = await preparePost(file); + const bodyText = prepared.post.document.content + ?.flatMap((node) => node.content ?? []) + .map((node) => node.text ?? "") + .filter(Boolean) + .join("\n"); + + assert.equal(prepared.post.markdown.trim(), testCase.expectedBody); + assert.doesNotMatch( + prepared.post.html, + /