fix(#3781): span-carrying heading walk unblocks acknowledge on heading-shaped deferred items (#3998)

* test(#3781): heading-shaped deferred entries must be acknowledgeable

* fix(#3781): span-carrying heading walk unblocks acknowledge on heading-shaped deferred items

* test(#3781): table fixture counts the row; BLOCKER 1 updated to the supported contract

* chore(#3781): changeset fragment (pr number backfilled after PR creation)

* chore(#3781): backfill changeset PR number (3998)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-28 10:21:57 -04:00
committed by GitHub
parent b351c83e03
commit 2012e8cc7f
5 changed files with 537 additions and 65 deletions

View File

@@ -2077,17 +2077,39 @@ describe('bug #950: quick-task SUMMARY must carry status: complete', () => {
assert.match(result.error, /no deferred item matched/i);
});
test('BLOCKER 1: heading-delimited (#3457) deferred-items shape is refused as unsupported_heading_shape, not guessed at', () => {
test('BLOCKER 1 (updated for #3781): heading-delimited (#3457) entries ack via the CLI; only table-embedded spans still refuse', () => {
const phaseDir = planningPath('phases', '01-alpha');
fs.mkdirSync(phaseDir, { recursive: true });
const filePath = path.join(phaseDir, 'deferred-items.md');
const before = ['## Deferred Items', '', '### Something out of scope', '', 'Some detail line.', ''].join('\n');
fs.writeFileSync(filePath, before);
const result = ack(tmpDir, ['--category', 'deferred_items', '--phase', '01', '--file', 'deferred-items.md', '--text', 'Something out of scope', '--milestone', 'v1.0']);
assert.equal(result.success, false, 'heading-delimited shape must be refused');
assert.match(result.error, /heading-delimited/i);
assert.equal(fs.readFileSync(filePath, 'utf-8'), before, 'file must be byte-identical — nothing written on refusal');
// #3781: the heading shape is now SUPPORTED — a bullet-bearing leaf
// acks through the CLI writer.
const ackable = ['## Deferred Items', '', '### Something out of scope', '', '- a real bullet', ''].join('\n');
fs.writeFileSync(filePath, ackable);
// --text must be the reader-derived identity: rawGapEntryText joins the
// heading text, the blank line, and the bullet with single spaces —
// the blank line contributes an empty segment, hence the double space.
const okResult = ack(tmpDir, ['--category', 'deferred_items', '--phase', '01', '--file', 'deferred-items.md', '--text', 'Something out of scope - a real bullet', '--milestone', 'v1.0']);
assert.equal(okResult.success, true, `heading-shaped entries must be acknowledgeable; stderr: ${okResult.error}`);
assert.match(fs.readFileSync(filePath, 'utf-8'), /status: acknowledged/);
// A table row embedded in the entry's span is still refused — a
// non-contiguous span cannot anchor a safe write.
const tabled = ['## Deferred Items', '', '### Tabled finding', '', '- a bullet', '| x | y |', ''].join('\n');
fs.writeFileSync(filePath, tabled);
const refuse = ack(tmpDir, ['--category', 'deferred_items', '--phase', '01', '--file', 'deferred-items.md', '--text', 'Tabled finding - a bullet', '--milestone', 'v1.0']);
assert.equal(refuse.success, false, 'table-embedded spans must still refuse');
assert.match(refuse.error, /GFM table row/i);
assert.equal(fs.readFileSync(filePath, 'utf-8'), tabled, 'file must be byte-identical — nothing written on refusal');
// Prose-only headings were never items (reader contract) — not_found,
// never a write.
const proseOnly = ['## Deferred Items', '', '### Something out of scope', '', 'Some detail line.', ''].join('\n');
fs.writeFileSync(filePath, proseOnly);
const nf = ack(tmpDir, ['--category', 'deferred_items', '--phase', '01', '--file', 'deferred-items.md', '--text', 'Something out of scope', '--milestone', 'v1.0']);
assert.equal(nf.success, false, 'prose-only headings contribute no entry');
assert.match(nf.error, /no deferred item matched/i);
assert.equal(fs.readFileSync(filePath, 'utf-8'), proseOnly, 'file must be byte-identical');
});
// ── F1 (#3458 follow-up review, HIGH): the writer must splice by the