The executor SCOPE BOUNDARY convention (agents/gsd-executor.md) logs out-of-scope discoveries to a phase directory's deferred-items.md, but no reader ever consumed it — forensic_audit, cmdAuditUat, and capture --list all skipped it — so deferred items were permanently invisible. cmdAuditUat (src/uat.cts) now scans each phase dir's deferred-items.md via a new parseDeferredItems (reusing the collectSection/splitGapsEntries/ extractGapEntryFields seams) and surfaces entries whose status != resolved (fail-safe: a missing/garbled status surfaces rather than hides, matching the false-negative-averse posture of #2286). forensic_audit (gsd-core/workflows/progress.md) gains Check 7 that globs .planning/phases/*/deferred-items.md and reports unresolved entries. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
5
.changeset/clever-voles-swim.md
Normal file
5
.changeset/clever-voles-swim.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2318
|
||||
---
|
||||
**Deferred out-of-scope findings logged to `deferred-items.md` are now surfaced** — the executor's SCOPE BOUNDARY convention writes discoveries to a phase directory's `deferred-items.md`, but nothing read it back, so those items were permanently invisible. `/gsd-progress`'s forensic audit and `audit-uat` now glob `.planning/phases/*/deferred-items.md` and surface unresolved entries. (#2287)
|
||||
@@ -264,6 +264,7 @@ Track: `outstanding_debt` — `summary.total_items` from the audit.
|
||||
|-------|------|-------|
|
||||
| {phase} | {filename} | {pending_count} pending, {skipped_count} skipped, {blocked_count} blocked |
|
||||
| {phase} | {filename} | human_needed — {count} items |
|
||||
| {phase} | {filename} | {unresolved_count} deferred items |
|
||||
|
||||
Review: `/gsd:audit-uat ${GSD_WS}` — full cross-phase audit
|
||||
Resume testing: `/gsd:verify-work {phase} ${GSD_WS}` — retest specific phase
|
||||
@@ -675,7 +676,7 @@ If `--forensic` IS present: after the standard report and routing suggestion hav
|
||||
|
||||
## Forensic Integrity Audit
|
||||
|
||||
Running 6 deep checks against project state...
|
||||
Running 7 deep checks against project state...
|
||||
|
||||
Run each check in order. For each check, emit ✓ (pass) or ⚠ (warning) with concrete evidence when a problem is found.
|
||||
|
||||
@@ -748,11 +749,24 @@ Emit:
|
||||
- ✓ `Working tree clean` — if no modified files outside `.planning/`
|
||||
- ⚠ `Uncommitted changes in source files` — list up to 10 file paths
|
||||
|
||||
**Check 7 — Unresolved deferred items**
|
||||
|
||||
Glob every phase directory's SCOPE BOUNDARY log (executor writes out-of-scope discoveries here per `agents/gsd-executor.md`):
|
||||
```bash
|
||||
ls .planning/phases/*/deferred-items.md 2>/dev/null || true
|
||||
```
|
||||
|
||||
For each `deferred-items.md` found, read its entries (bullet list, one entry per top-level `- ` line, continuation lines indented beneath it). An entry is RESOLVED only if it carries an explicit `status: resolved` field (case-insensitive) on one of its lines; every other entry — including one with no `status:` field at all — is UNRESOLVED and must be surfaced (fail-safe: never silently drop a possibly-open item).
|
||||
|
||||
Emit:
|
||||
- ✓ `No unresolved deferred items` — if no `deferred-items.md` files exist, or every entry in every file is `status: resolved`
|
||||
- ⚠ `Unresolved deferred items found` — list each file's phase directory and its unresolved entry text (max 5 per file, truncated at 80 chars)
|
||||
|
||||
---
|
||||
|
||||
After all 6 checks, display the verdict:
|
||||
After all 7 checks, display the verdict:
|
||||
|
||||
**If all 6 checks passed:**
|
||||
**If all 7 checks passed:**
|
||||
```
|
||||
### Verdict: CLEAN
|
||||
|
||||
@@ -773,6 +787,7 @@ Then for each failed check, add a concrete next action:
|
||||
- Check 4 (memory pending): `Review the flagged memory entries and resolve or clear them`
|
||||
- Check 5 (blocking todos): `Complete the operational steps in .planning/todos/pending/ before continuing`
|
||||
- Check 6 (uncommitted code): `Commit or stash the uncommitted changes before advancing`
|
||||
- Check 7 (unresolved deferred items): `Address each deferred item and mark it status: resolved in its deferred-items.md, or fold it into the roadmap`
|
||||
- Check 1 (STATE inconsistency): `Run /gsd:verify-work ${PHASE} ${GSD_WS} to reconcile state`
|
||||
</step>
|
||||
|
||||
|
||||
85
src/uat.cts
85
src/uat.cts
@@ -40,7 +40,7 @@ import { requireSafePath, sanitizeForDisplay } from './security.cjs';
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
type UatResult = string;
|
||||
type UatCategory = 'server_blocked' | 'device_needed' | 'build_needed' | 'third_party' | 'blocked' | 'skipped_unresolved' | 'pending' | 'human_uat' | 'unknown';
|
||||
type UatCategory = 'server_blocked' | 'device_needed' | 'build_needed' | 'third_party' | 'blocked' | 'skipped_unresolved' | 'pending' | 'human_uat' | 'unknown' | 'deferred';
|
||||
|
||||
interface UatItem {
|
||||
test?: number;
|
||||
@@ -57,7 +57,7 @@ interface UatFileResult {
|
||||
phase_dir: string;
|
||||
file: string;
|
||||
file_path: string;
|
||||
type: 'uat' | 'verification';
|
||||
type: 'uat' | 'verification' | 'deferred';
|
||||
status: string;
|
||||
items: UatItem[];
|
||||
}
|
||||
@@ -129,6 +129,30 @@ function cmdAuditUat(cwd: string, raw: boolean): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process deferred-items.md (#2287) — the SCOPE BOUNDARY convention
|
||||
// (agents/gsd-executor.md) has the executor log out-of-scope discoveries
|
||||
// to this file; nothing previously read it back. Surface every
|
||||
// UNRESOLVED entry (see parseDeferredItems for the resolved/unresolved
|
||||
// parsing rule) as a 'deferred'-typed result, keeping deferred-items.md
|
||||
// itself the single source of truth — no duplicate pending-todo entry
|
||||
// required.
|
||||
const deferredFile = 'deferred-items.md';
|
||||
if (files.includes(deferredFile)) {
|
||||
const content = fs.readFileSync(path.join(phaseDir, deferredFile), 'utf-8');
|
||||
const items = parseDeferredItems(content);
|
||||
if (items.length > 0) {
|
||||
results.push({
|
||||
phase: phaseNum,
|
||||
phase_dir: dir,
|
||||
file: deferredFile,
|
||||
file_path: toPosixPath(path.relative(cwd, path.join(phaseDir, deferredFile))),
|
||||
type: 'deferred',
|
||||
status: 'unresolved',
|
||||
items,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute summary
|
||||
@@ -445,6 +469,62 @@ function parseGapsItems(content: string): UatItem[] {
|
||||
return items;
|
||||
}
|
||||
|
||||
// ─── parseDeferredItems ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Extract unresolved entries from a phase directory's `deferred-items.md`
|
||||
* (#2287) — the SCOPE BOUNDARY convention `agents/gsd-executor.md` instructs
|
||||
* the executor to follow: "Log out-of-scope discoveries to `deferred-items.md`
|
||||
* in the phase directory". Nothing previously read this file back, so a
|
||||
* deferred entry was permanently invisible outside the phase directory.
|
||||
*
|
||||
* The writer convention (unchanged by this fix, per the issue's stated
|
||||
* out-of-scope) emits a plain bullet list, typically under a `## Deferred
|
||||
* Items` heading (see the issue's own reproduction fixture), one entry per
|
||||
* top-level `- ` line with optional indented continuation lines. There is no
|
||||
* mandated heading text, so if no `## Deferred Items`-shaped level-2 heading
|
||||
* is found, the WHOLE file is scanned as the entry list — fail-safe, so an
|
||||
* agent writing a differently-headed (or headless) deferred-items.md still
|
||||
* has its entries surfaced rather than silently skipped.
|
||||
*
|
||||
* Reuses the same per-line field/entry-splitting seams as `parseGapsItems`
|
||||
* (`splitGapsEntries`, `extractGapEntryFields`, `rawGapEntryText`) — an entry
|
||||
* is RESOLVED only when it carries an explicit `status: resolved` field
|
||||
* (case-insensitive), mirroring the established Gaps convention so a human or
|
||||
* follow-up agent can mark a deferred item done in place, keeping
|
||||
* `deferred-items.md` the single source of truth (no duplicate
|
||||
* `.planning/todos/pending/*.md` entry required). Every other entry —
|
||||
* including one with no `status:` field at all — is UNRESOLVED and is
|
||||
* surfaced.
|
||||
*/
|
||||
function parseDeferredItems(content: string): UatItem[] {
|
||||
const deferredSection = collectSection(
|
||||
content,
|
||||
(h) => /^deferred\s+items$/i.test(h.text) && h.level === 2,
|
||||
{ levelBounded: true },
|
||||
);
|
||||
const sectionBody = deferredSection ? deferredSection.body : content;
|
||||
|
||||
const items: UatItem[] = [];
|
||||
|
||||
for (const entryLines of splitGapsEntries(sectionBody)) {
|
||||
const fields = extractGapEntryFields(entryLines);
|
||||
const rawStatus = fields.status;
|
||||
if (rawStatus && rawStatus.toLowerCase() === 'resolved') continue;
|
||||
|
||||
const text = rawGapEntryText(entryLines);
|
||||
if (!text) continue;
|
||||
|
||||
items.push({
|
||||
name: text,
|
||||
result: 'unresolved',
|
||||
category: 'deferred',
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a `## Gaps` section body into per-entry line groups on TOP-LEVEL
|
||||
* `- ` bullet openers.
|
||||
@@ -747,4 +827,5 @@ export = {
|
||||
cmdRenderCheckpoint,
|
||||
parseCurrentTest,
|
||||
buildCheckpoint,
|
||||
parseDeferredItems,
|
||||
};
|
||||
|
||||
341
tests/fix-2287-deferred-items-reader.test.cjs
Normal file
341
tests/fix-2287-deferred-items-reader.test.cjs
Normal file
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* #2287 — deferred-items.md has no reader anywhere in gsd-core.
|
||||
*
|
||||
* The SCOPE BOUNDARY convention (`agents/gsd-executor.md`) instructs the
|
||||
* executor to log out-of-scope discoveries to `deferred-items.md` inside the
|
||||
* phase directory. Nothing read that file back: `cmdAuditUat` (src/uat.cts)
|
||||
* filtered phase-directory files down to `*-UAT.md` / `*-VERIFICATION.md`
|
||||
* only, and the `forensic_audit` workflow step (gsd-core/workflows/
|
||||
* progress.md) ran 6 checks, none of which globbed the phase-directory
|
||||
* `deferred-items.md` path. An entry written there was permanently invisible.
|
||||
*
|
||||
* This fix:
|
||||
* - `cmdAuditUat` gains a `deferred-items.md` scan per phase directory,
|
||||
* surfacing every UNRESOLVED entry as a `type: 'deferred'` result. An
|
||||
* entry is resolved only when it carries an explicit `status: resolved`
|
||||
* field (mirroring the established `## Gaps` convention from #2286) — a
|
||||
* missing/garbled status fails safe and is surfaced.
|
||||
* - `forensic_audit` gains a 7th check that globs the same path and reports
|
||||
* unresolved entries with the same ✓/⚠ semantics as the other 6 checks.
|
||||
*
|
||||
* `deferred-items.md` remains the single source of truth — no duplicate
|
||||
* `.planning/todos/pending/*.md` entry is required.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { test, describe, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const fc = require('./helpers/fast-check-setup.cjs');
|
||||
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
||||
const { parseDeferredItems } = require('../gsd-core/bin/lib/uat.cjs');
|
||||
|
||||
// ─── cmdAuditUat behavioral coverage ───────────────────────────────────────
|
||||
|
||||
describe('#2287 cmdAuditUat: deferred-items.md awareness', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('no deferred-items.md present (0 entries) → no results, no false positive', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(phaseDir, '.gitkeep'), '');
|
||||
|
||||
const result = runGsdTools('audit-uat --raw', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.deepStrictEqual(output.results, []);
|
||||
assert.strictEqual(output.summary.total_items, 0);
|
||||
assert.strictEqual(output.summary.total_files, 0);
|
||||
});
|
||||
|
||||
test('deferred-items.md with only a resolved entry (0 unresolved) → no result surfaced', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(phaseDir, 'deferred-items.md'), [
|
||||
'## Deferred Items',
|
||||
'',
|
||||
'- Already handled unrelated lint warning.',
|
||||
' status: resolved',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools('audit-uat --raw', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.deepStrictEqual(output.results, [],
|
||||
'a fully-resolved deferred-items.md must not surface any result');
|
||||
assert.strictEqual(output.summary.total_items, 0);
|
||||
});
|
||||
|
||||
test('deferred-items.md with 1 unresolved entry → surfaced in structured JSON output', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(phaseDir, 'deferred-items.md'), [
|
||||
'## Deferred Items',
|
||||
'',
|
||||
'- Found an unrelated pre-existing test failure in `some-other-module` while working on',
|
||||
' this phase\'s task. Out of scope for this task — logged here per SCOPE BOUNDARY.',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools('audit-uat --raw', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.summary.total_items, 1);
|
||||
assert.strictEqual(output.summary.total_files, 1);
|
||||
assert.strictEqual(output.summary.by_category.deferred, 1);
|
||||
assert.strictEqual(output.summary.by_phase['01'], 1);
|
||||
|
||||
const deferredResult = output.results.find(r => r.type === 'deferred');
|
||||
assert.ok(deferredResult, 'a deferred-typed result must be present');
|
||||
assert.strictEqual(deferredResult.phase, '01');
|
||||
assert.strictEqual(deferredResult.file, 'deferred-items.md');
|
||||
assert.strictEqual(
|
||||
deferredResult.file_path,
|
||||
'.planning/phases/01-foundation/deferred-items.md',
|
||||
);
|
||||
assert.strictEqual(deferredResult.items.length, 1);
|
||||
assert.match(deferredResult.items[0].name, /unrelated pre-existing test failure/);
|
||||
assert.strictEqual(deferredResult.items[0].result, 'unresolved');
|
||||
assert.strictEqual(deferredResult.items[0].category, 'deferred');
|
||||
});
|
||||
|
||||
test('deferred-items.md with 2+ entries (mixed resolved/unresolved) → only unresolved surfaced', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(phaseDir, 'deferred-items.md'), [
|
||||
'## Deferred Items',
|
||||
'',
|
||||
'- First unrelated finding, still open.',
|
||||
'- Second unrelated finding, also still open.',
|
||||
'- Third finding, already fixed separately.',
|
||||
' status: resolved',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools('audit-uat --raw', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
const deferredResult = output.results.find(r => r.type === 'deferred');
|
||||
assert.ok(deferredResult);
|
||||
assert.strictEqual(deferredResult.items.length, 2,
|
||||
'exactly the 2 unresolved entries must surface; the resolved 3rd must not');
|
||||
const names = deferredResult.items.map(i => i.name);
|
||||
assert.ok(names.some(n => n.includes('First unrelated finding')));
|
||||
assert.ok(names.some(n => n.includes('Second unrelated finding')));
|
||||
assert.ok(!names.some(n => n.includes('Third finding')));
|
||||
});
|
||||
|
||||
test('deferred entries surface across multiple phase directories', () => {
|
||||
const phase1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
||||
const phase2 = path.join(tmpDir, '.planning', 'phases', '02-auth');
|
||||
fs.mkdirSync(phase1, { recursive: true });
|
||||
fs.mkdirSync(phase2, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(phase1, 'deferred-items.md'), [
|
||||
'## Deferred Items',
|
||||
'',
|
||||
'- Phase 1 unrelated finding.',
|
||||
].join('\n'));
|
||||
fs.writeFileSync(path.join(phase2, 'deferred-items.md'), [
|
||||
'## Deferred Items',
|
||||
'',
|
||||
'- Phase 2 unrelated finding.',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools('audit-uat --raw', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
const deferredResults = output.results.filter(r => r.type === 'deferred');
|
||||
assert.strictEqual(deferredResults.length, 2);
|
||||
assert.strictEqual(output.summary.total_items, 2);
|
||||
assert.strictEqual(output.summary.by_phase['01'], 1);
|
||||
assert.strictEqual(output.summary.by_phase['02'], 1);
|
||||
});
|
||||
|
||||
test('an entry with a garbled/missing status fails safe and is surfaced (not silently dropped)', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(phaseDir, 'deferred-items.md'), [
|
||||
'## Deferred Items',
|
||||
'',
|
||||
'- An entry with no status field at all.',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools('audit-uat --raw', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.summary.total_items, 1,
|
||||
'missing status must SURFACE the entry, not silently drop it');
|
||||
});
|
||||
|
||||
test('existing UAT/VERIFICATION scanning is unchanged when a deferred-items.md is also present', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(phaseDir, '01-UAT.md'), [
|
||||
'---',
|
||||
'status: testing',
|
||||
'phase: 01-foundation',
|
||||
'started: 2025-01-01T00:00:00Z',
|
||||
'updated: 2025-01-01T00:00:00Z',
|
||||
'---',
|
||||
'',
|
||||
'## Tests',
|
||||
'',
|
||||
'### 1. Login Form',
|
||||
'expected: Form displays with email and password fields',
|
||||
'result: pending',
|
||||
].join('\n'));
|
||||
|
||||
fs.writeFileSync(path.join(phaseDir, 'deferred-items.md'), [
|
||||
'## Deferred Items',
|
||||
'',
|
||||
'- An unrelated out-of-scope finding.',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools('audit-uat --raw', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.results.length, 2, 'both the UAT file and deferred-items.md must surface as separate results');
|
||||
const uatResult = output.results.find(r => r.type === 'uat');
|
||||
const deferredResult = output.results.find(r => r.type === 'deferred');
|
||||
assert.ok(uatResult, 'existing uat-type result must still be present');
|
||||
assert.strictEqual(uatResult.items.length, 1);
|
||||
assert.strictEqual(uatResult.items[0].result, 'pending');
|
||||
assert.ok(deferredResult, 'new deferred-type result must be present');
|
||||
assert.strictEqual(deferredResult.items.length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── forensic_audit workflow-prose source-contract guard ──────────────────
|
||||
|
||||
const PROGRESS_MD = path.join(__dirname, '..', 'gsd-core', 'workflows', 'progress.md');
|
||||
|
||||
describe('#2287 progress.md forensic_audit: deferred-items.md contract', () => {
|
||||
const content = fs.readFileSync(PROGRESS_MD, 'utf-8');
|
||||
const stepStart = content.indexOf('<step name="forensic_audit">');
|
||||
const stepEnd = content.indexOf('</step>', stepStart);
|
||||
const section = stepStart !== -1 && stepEnd !== -1 ? content.slice(stepStart, stepEnd) : '';
|
||||
|
||||
test('forensic_audit step exists', () => {
|
||||
assert.notEqual(stepStart, -1, 'progress.md must contain the forensic_audit step');
|
||||
});
|
||||
|
||||
test('forensic_audit now runs 7 checks (was 6) and globs deferred-items.md', () => {
|
||||
assert.ok(/running 7 deep checks/i.test(section),
|
||||
'forensic_audit must advertise 7 deep checks (was 6) now that deferred-items.md is read');
|
||||
assert.ok(/\.planning\/phases\/\*\/deferred-items\.md/.test(section),
|
||||
'forensic_audit must glob .planning/phases/*/deferred-items.md');
|
||||
});
|
||||
|
||||
test('the new check reports unresolved deferred items with the same ✓/⚠ semantics as the other checks', () => {
|
||||
assert.ok(/check\s*7/i.test(section),
|
||||
'a 7th check must be present');
|
||||
assert.ok(/unresolved deferred items/i.test(section),
|
||||
'the check must be framed around unresolved deferred items');
|
||||
assert.ok(/✓[^\n]*no unresolved deferred items/i.test(section),
|
||||
'the check must emit a ✓ pass line when no unresolved deferred items exist');
|
||||
assert.ok(/⚠[^\n]*unresolved deferred items found/i.test(section),
|
||||
'the check must emit a ⚠ warning line when unresolved deferred items exist');
|
||||
});
|
||||
|
||||
test('an entry is resolved only via an explicit status: resolved field (fail-safe otherwise)', () => {
|
||||
assert.ok(/status:\s*resolved/i.test(section),
|
||||
'the resolved/unresolved parsing rule must be documented in the step prose');
|
||||
});
|
||||
|
||||
test('the verdict summary now gates on 7 checks (was 6)', () => {
|
||||
assert.ok(/after all 7 checks/i.test(section),
|
||||
'the verdict section must say "after all 7 checks"');
|
||||
assert.ok(/if all 7 checks passed/i.test(section),
|
||||
'the verdict section must say "if all 7 checks passed"');
|
||||
assert.ok(!/after all 6 checks/i.test(section) && !/if all 6 checks passed/i.test(section),
|
||||
'stale "6 checks" phrasing must not remain in the step');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── parseDeferredItems property test ──────────────────────────────────────
|
||||
|
||||
describe('#2287 parseDeferredItems: property (status: resolved fail-safe)', () => {
|
||||
// Single-line entry text: no newlines (would break bullet-entry splitting),
|
||||
// non-empty after trim, and never itself SHAPED like a `status:` field line
|
||||
// (that would be indistinguishable from a real field regardless of intent).
|
||||
const plainText = fc.string({ minLength: 1, maxLength: 40 })
|
||||
.map((s) => s.replace(/[\r\n]/g, ' ').trim())
|
||||
.filter((s) => s.length > 0 && !/^status:/i.test(s));
|
||||
|
||||
// Decoy: entry text that CONTAINS a `status: resolved`-shaped substring
|
||||
// mid-line (not at line start) — must never be misread as a resolved
|
||||
// marker, since extractGapEntryFields only recognises a field anchored to
|
||||
// the START of its own trimmed line (see parseDeferredItems' doc comment).
|
||||
const decoyText = plainText.map((s) => `${s} status: resolved trailing note`);
|
||||
|
||||
const textArb = fc.oneof(plainText, decoyText);
|
||||
const entryArb = fc.record({ text: textArb, resolved: fc.boolean() });
|
||||
|
||||
test('property: an entry is surfaced iff it is NOT marked status: resolved; surfaced count == non-resolved count', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(entryArb, { maxLength: 20 }),
|
||||
(rawEntries) => {
|
||||
// Index-prefix for uniqueness so surfaced items can be mapped back
|
||||
// to their source entry unambiguously even with colliding random text.
|
||||
const entries = rawEntries.map((e, i) => ({ text: `E${i}_${e.text}`, resolved: e.resolved }));
|
||||
|
||||
const lines = ['## Deferred Items', ''];
|
||||
for (const e of entries) {
|
||||
lines.push(`- ${e.text}`);
|
||||
if (e.resolved) lines.push(' status: resolved');
|
||||
}
|
||||
const content = lines.join('\n');
|
||||
|
||||
const items = parseDeferredItems(content);
|
||||
const surfacedNames = new Set(items.map((it) => it.name));
|
||||
|
||||
const expectedUnresolved = entries.filter((e) => !e.resolved);
|
||||
const expectedResolved = entries.filter((e) => e.resolved);
|
||||
|
||||
// Total surfaced count equals the count of non-resolved entries.
|
||||
assert.strictEqual(items.length, expectedUnresolved.length);
|
||||
|
||||
// Every non-resolved entry IS surfaced (including status:-shaped
|
||||
// decoy substrings embedded mid-line — those must not flip the
|
||||
// outcome).
|
||||
for (const e of expectedUnresolved) {
|
||||
assert.ok(surfacedNames.has(e.text), `expected unresolved entry to surface: ${e.text}`);
|
||||
}
|
||||
|
||||
// No status:-resolved entry is EVER surfaced.
|
||||
for (const e of expectedResolved) {
|
||||
assert.ok(!surfacedNames.has(e.text), `status: resolved entry must never surface: ${e.text}`);
|
||||
}
|
||||
|
||||
// Every returned item carries the fixed deferred category/result shape.
|
||||
for (const item of items) {
|
||||
assert.strictEqual(item.result, 'unresolved');
|
||||
assert.strictEqual(item.category, 'deferred');
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -279,7 +279,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "6cf726be6427a35a",
|
||||
"gsd-core/workflows/pr-branch.md": "dc5598ae8accdecd",
|
||||
"gsd-core/workflows/profile-user.md": "355af92ac285567f",
|
||||
"gsd-core/workflows/progress.md": "4f0c285ce35553e3",
|
||||
"gsd-core/workflows/progress.md": "0e41db02a270a2b2",
|
||||
"gsd-core/workflows/quick.md": "424c0eb61769e747",
|
||||
"gsd-core/workflows/reapply-patches.md": "4dcd6117d0a507ca",
|
||||
"gsd-core/workflows/remove-phase.md": "23b9eb0858a2535e",
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
|
||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||
"gsd-core/workflows/profile-user.md": "14263db831230142",
|
||||
"gsd-core/workflows/progress.md": "281a77e800d11614",
|
||||
"gsd-core/workflows/progress.md": "9457fb10437c9402",
|
||||
"gsd-core/workflows/quick.md": "25f0cc40bc22ed3e",
|
||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
|
||||
@@ -349,7 +349,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "fbe964fcdb244802",
|
||||
"gsd-core/workflows/pr-branch.md": "513f6cff722eff2d",
|
||||
"gsd-core/workflows/profile-user.md": "3b34dcb337d50f4b",
|
||||
"gsd-core/workflows/progress.md": "eb0885959b4ac66e",
|
||||
"gsd-core/workflows/progress.md": "f6739fc007de6f66",
|
||||
"gsd-core/workflows/quick.md": "f1b474b46327034f",
|
||||
"gsd-core/workflows/reapply-patches.md": "44a96b52b975e9bb",
|
||||
"gsd-core/workflows/remove-phase.md": "8effc8742d58a11a",
|
||||
|
||||
@@ -278,7 +278,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "af50e9f10d3cc6e1",
|
||||
"gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd",
|
||||
"gsd-core/workflows/profile-user.md": "ff3820a27731ceb8",
|
||||
"gsd-core/workflows/progress.md": "5ce5a11b468bd419",
|
||||
"gsd-core/workflows/progress.md": "8aa2fcebb8b4876e",
|
||||
"gsd-core/workflows/quick.md": "b4237633eaf5a246",
|
||||
"gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041",
|
||||
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "a2cdd513663226f1",
|
||||
"gsd-core/workflows/pr-branch.md": "9923878a4f6a2d91",
|
||||
"gsd-core/workflows/profile-user.md": "26f74db0a7fcd268",
|
||||
"gsd-core/workflows/progress.md": "c5ce768042ca9b8b",
|
||||
"gsd-core/workflows/progress.md": "a2d948b084a48500",
|
||||
"gsd-core/workflows/quick.md": "797b1c8d62c3d33d",
|
||||
"gsd-core/workflows/reapply-patches.md": "eb4272145a117904",
|
||||
"gsd-core/workflows/remove-phase.md": "e336350f8113a328",
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
|
||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||
"gsd-core/workflows/profile-user.md": "4fa910d15dea5695",
|
||||
"gsd-core/workflows/progress.md": "281a77e800d11614",
|
||||
"gsd-core/workflows/progress.md": "9457fb10437c9402",
|
||||
"gsd-core/workflows/quick.md": "0bd83bd88c5e63d1",
|
||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
|
||||
@@ -385,7 +385,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "5b07de07e4593281",
|
||||
"gsd-core/workflows/pr-branch.md": "d13e1cc81de40896",
|
||||
"gsd-core/workflows/profile-user.md": "05828c8cc61ef384",
|
||||
"gsd-core/workflows/progress.md": "7ec6495e273904cf",
|
||||
"gsd-core/workflows/progress.md": "03f12b223eb3a96e",
|
||||
"gsd-core/workflows/quick.md": "bf70b7beb314aa50",
|
||||
"gsd-core/workflows/reapply-patches.md": "26297b84736e66a4",
|
||||
"gsd-core/workflows/remove-phase.md": "9ee0fddd11a0d9d4",
|
||||
|
||||
@@ -280,7 +280,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "21e461cd39e5181b",
|
||||
"gsd-core/workflows/pr-branch.md": "2833905f119b5722",
|
||||
"gsd-core/workflows/profile-user.md": "5cc032206c99ef71",
|
||||
"gsd-core/workflows/progress.md": "9bd7472d2efe14dd",
|
||||
"gsd-core/workflows/progress.md": "449b7f4de1bef7ae",
|
||||
"gsd-core/workflows/quick.md": "4a53899b69b8c801",
|
||||
"gsd-core/workflows/reapply-patches.md": "8fd59e24b486f180",
|
||||
"gsd-core/workflows/remove-phase.md": "e262654e319d1bc4",
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "7bccd151ce7b69f2",
|
||||
"gsd-core/workflows/pr-branch.md": "c67d90c65da47168",
|
||||
"gsd-core/workflows/profile-user.md": "8c943983241260b5",
|
||||
"gsd-core/workflows/progress.md": "a980339715a431f0",
|
||||
"gsd-core/workflows/progress.md": "0f03aaf96f5abf7e",
|
||||
"gsd-core/workflows/quick.md": "3b37350964cfe391",
|
||||
"gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041",
|
||||
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
||||
|
||||
@@ -279,7 +279,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "f862f77fca983749",
|
||||
"gsd-core/workflows/pr-branch.md": "ecabd55e4eabf229",
|
||||
"gsd-core/workflows/profile-user.md": "de5030437226cf2c",
|
||||
"gsd-core/workflows/progress.md": "2641aa5457ab784f",
|
||||
"gsd-core/workflows/progress.md": "debe980226d9260d",
|
||||
"gsd-core/workflows/quick.md": "01560a250a6040ea",
|
||||
"gsd-core/workflows/reapply-patches.md": "158083a310859594",
|
||||
"gsd-core/workflows/remove-phase.md": "fce799aae3ab2715",
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "ffa5774304243649",
|
||||
"gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd",
|
||||
"gsd-core/workflows/profile-user.md": "203ebe3f8f3876a8",
|
||||
"gsd-core/workflows/progress.md": "fb1ea7476a839fd8",
|
||||
"gsd-core/workflows/progress.md": "c2a8a79af6603e70",
|
||||
"gsd-core/workflows/quick.md": "4ddd6d3214f57f57",
|
||||
"gsd-core/workflows/reapply-patches.md": "becf9728cdb124c4",
|
||||
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
||||
|
||||
@@ -343,7 +343,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
|
||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||
"gsd-core/workflows/profile-user.md": "5abfae83739fa978",
|
||||
"gsd-core/workflows/progress.md": "281a77e800d11614",
|
||||
"gsd-core/workflows/progress.md": "9457fb10437c9402",
|
||||
"gsd-core/workflows/quick.md": "8dcda37c46954ebe",
|
||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "b8dad652e31c2318",
|
||||
"gsd-core/workflows/pr-branch.md": "929b7cb0c99c7b9e",
|
||||
"gsd-core/workflows/profile-user.md": "248d59a31948e0ed",
|
||||
"gsd-core/workflows/progress.md": "8e01238a8ac6b73d",
|
||||
"gsd-core/workflows/progress.md": "352920a61ed4f4db",
|
||||
"gsd-core/workflows/quick.md": "774d646560a74c93",
|
||||
"gsd-core/workflows/reapply-patches.md": "a0e9b53f90abceb2",
|
||||
"gsd-core/workflows/remove-phase.md": "dea4661e8f89596f",
|
||||
|
||||
2
tests/fixtures/golden-install-parity/pi.json
vendored
2
tests/fixtures/golden-install-parity/pi.json
vendored
@@ -246,7 +246,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
|
||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||
"gsd-core/workflows/profile-user.md": "1bac7f69142801ef",
|
||||
"gsd-core/workflows/progress.md": "281a77e800d11614",
|
||||
"gsd-core/workflows/progress.md": "9457fb10437c9402",
|
||||
"gsd-core/workflows/quick.md": "31787c1e8a867e3f",
|
||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
|
||||
@@ -279,7 +279,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "0a92ba12993ac261",
|
||||
"gsd-core/workflows/pr-branch.md": "cef0f65b16d500b4",
|
||||
"gsd-core/workflows/profile-user.md": "263c0693563d98da",
|
||||
"gsd-core/workflows/progress.md": "b1b3f88614d6b9cc",
|
||||
"gsd-core/workflows/progress.md": "4ceb0f6f5e34c4a4",
|
||||
"gsd-core/workflows/quick.md": "d0fffe516abaf47a",
|
||||
"gsd-core/workflows/reapply-patches.md": "de0ee8acfe7245b2",
|
||||
"gsd-core/workflows/remove-phase.md": "e8ae4fbbfac700f0",
|
||||
|
||||
@@ -279,7 +279,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "856ad565b2eb0c47",
|
||||
"gsd-core/workflows/pr-branch.md": "79fd55b88ea2db9c",
|
||||
"gsd-core/workflows/profile-user.md": "672821e6b1266645",
|
||||
"gsd-core/workflows/progress.md": "e7d05715aff75881",
|
||||
"gsd-core/workflows/progress.md": "00de1afbe907efa5",
|
||||
"gsd-core/workflows/quick.md": "0529abcf9913ec04",
|
||||
"gsd-core/workflows/reapply-patches.md": "21b38c374f19fd78",
|
||||
"gsd-core/workflows/remove-phase.md": "a46c2fe853bf4e86",
|
||||
|
||||
@@ -279,7 +279,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "6cf61f540cdbd8b9",
|
||||
"gsd-core/workflows/pr-branch.md": "acd59f915d018ad4",
|
||||
"gsd-core/workflows/profile-user.md": "c4313672b81b5bcd",
|
||||
"gsd-core/workflows/progress.md": "d4a4f96975419d09",
|
||||
"gsd-core/workflows/progress.md": "7b6b9564b34fcabb",
|
||||
"gsd-core/workflows/quick.md": "71fbcf38e2305de0",
|
||||
"gsd-core/workflows/reapply-patches.md": "d449a23d3acf6379",
|
||||
"gsd-core/workflows/remove-phase.md": "e7a6af429b36e77b",
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
|
||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||
"gsd-core/workflows/profile-user.md": "e23bea0a69c0bb4b",
|
||||
"gsd-core/workflows/progress.md": "281a77e800d11614",
|
||||
"gsd-core/workflows/progress.md": "9457fb10437c9402",
|
||||
"gsd-core/workflows/quick.md": "7ad8d250c536c4dd",
|
||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"plant-seed.md": 11785,
|
||||
"pr-branch.md": 15963,
|
||||
"profile-user.md": 21246,
|
||||
"progress.md": 30628,
|
||||
"progress.md": 31789,
|
||||
"quick.md": 50699,
|
||||
"reapply-patches.md": 20312,
|
||||
"remove-phase.md": 8513,
|
||||
|
||||
Reference in New Issue
Block a user