fix(#3805): audit-uat honours the audit_acknowledged marker via the shared predicate (#4025)

* test(#3805): audit-uat must honour the audit_acknowledged marker

* fix(#3805): route audit-uat's UAT and VERIFICATION scans through the shared acknowledged predicate

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

* chore(#3805): backfill changeset PR number (4025)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-29 02:51:54 -04:00
committed by GitHub
parent f4fefb0bef
commit 3a4c3cb83e
5 changed files with 182 additions and 4 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 4025
---
**Acknowledged moot items stay closed in `audit-uat`** — the `audit_acknowledged` frontmatter marker (the documented, self-invalidating "this item is moot" seam) now suppresses items in `query audit-uat` exactly as it already does in `audit-open`, with the same snapshot keys and a visible `acknowledged_files` count — no more choosing between lying (`status: passed`), inventing tokens, or deleting the planning record. (#3805)

View File

@@ -256,9 +256,10 @@
"audit-command-cutover.test.cjs",
"audit-fix-command.test.cjs",
"audit-milestone-filename-guard.test.cjs",
"audit-uat-acknowledged.test.cjs",
"audit-workstream-layouts.test.cjs"
],
"issue": "#3804 \u2014 the layout tests drive the real audit-uat CLI against on-disk workstream fixtures; the existing audit suites cover the CLI cutover and the filename contract, and this covers phase-directory enumeration"
"issue": "#3804/#3805 \u2014 audit-layout and acknowledged-marker suites drive the real audit-uat CLI against on-disk fixtures"
}
}
}

View File

@@ -1789,6 +1789,13 @@ export = {
formatAuditReport,
listAuditPhaseTargets,
cmdAuditAcknowledge,
// #3805: exported so uat.cts's cmdAuditUat routes the SAME artifacts'
// suppression through the ONE predicate instead of hand-rolling a tenth
// copy outside this file's visibility (the exact defect class the
// predicate's own header warns about). The snapshot derivations ride
// along so the snapshotKeys cannot drift between the two consumers.
isAuditItemAcknowledged,
deriveUatGapSnapshotValue,
// #2142: exported so src/milestone.cts's archiveQuickTaskDirectories README
// index generator shares this ONE discovery rule rather than re-deriving it.
resolveQuickTaskSummaryFile,

View File

@@ -35,6 +35,9 @@ const { PHASE_NUMBER_TOKEN_SOURCE, scopeToPhase } = phaseIdMod;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import phaseLocator = require('./phase-locator.cjs');
const { listMilestonePhaseDirs, getAllArchivedPhaseDirs } = phaseLocator;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import auditMod = require('./audit.cjs');
const { isAuditItemAcknowledged, deriveUatGapSnapshotValue } = auditMod;
import { requireSafePath, sanitizeForDisplay } from './security.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
import configLoader = require('./config-loader.cjs');
@@ -158,6 +161,7 @@ function cmdAuditUat(cwd: string, raw: boolean): void {
}
const results: UatFileResult[] = [];
let acknowledgedFiles = 0;
// Active dirs are milestone-filtered; archived dirs deliberately are NOT.
// listMilestonePhaseDirs derives the CURRENT milestone's phase directories
@@ -198,7 +202,16 @@ function cmdAuditUat(cwd: string, raw: boolean): void {
const uatFilePath = path.join(phaseDir, file);
const content = readNormalizedDocument(uatFilePath);
const { items, headingsSeen } = parseUatItemsWithStats(content);
const status = (extractFrontmatter(content, uatFilePath).status as string || 'unknown');
const uatFm = extractFrontmatter(content, uatFilePath) as Record<string, unknown>;
const status = ((uatFm.status as string) || 'unknown').toLowerCase();
// #3805: honour the audit_acknowledged marker with the SAME snapshot
// key audit.cts's scanUatGaps uses ('gap_snapshot', derived value
// composed by the shared derivation) — one acknowledgement means the
// same thing to both commands.
if (isAuditItemAcknowledged(uatFm, { snapshotKey: 'gap_snapshot', currentValue: deriveUatGapSnapshotValue(status, content) })) {
acknowledgedFiles++;
continue;
}
// `parse_gap` means the file contained `### N.` test blocks that
// yielded no items — NOT merely "zero items and not complete" (#3707
// MAJOR: that broader signal false-positived on an all-pass file and on
@@ -258,8 +271,17 @@ function cmdAuditUat(cwd: string, raw: boolean): void {
for (const file of scopeToPhase(files.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md')), dir)) {
const verificationFilePath = path.join(phaseDir, file);
const content = readNormalizedDocument(verificationFilePath);
const status = extractFrontmatter(content, verificationFilePath).status as string || 'unknown';
const verFm = extractFrontmatter(content, verificationFilePath) as Record<string, unknown>;
const status = ((verFm.status as string) || 'unknown').toLowerCase();
// #3805: same marker, same 'status' snapshot key as scanVerificationGaps,
// and the same ORDERING — the open-status gate runs FIRST (a marker on
// a file that would never surface is not a suppressed item), then the
// acknowledgement suppresses what the gate surfaced.
if (status === 'human_needed' || status === 'gaps_found') {
if (isAuditItemAcknowledged(verFm, { snapshotKey: 'status', currentValue: status })) {
acknowledgedFiles++;
continue;
}
const items = parseVerificationItems(content, status, verificationFilePath);
if (items.length > 0) {
results.push({
@@ -349,7 +371,9 @@ function cmdAuditUat(cwd: string, raw: boolean): void {
}
}
output({ results, summary }, raw, undefined);
// #3805: acknowledged files surface as a COUNT (audit-open's honesty
// model: the marker fired, the items are suppressed, both facts visible).
output({ results, summary, acknowledged_files: acknowledgedFiles }, raw, undefined);
}
// ─── cmdRenderCheckpoint ──────────────────────────────────────────────────────

View File

@@ -0,0 +1,141 @@
'use strict';
// ─────────────────────────────────────────────────────────────────────────────
// #3805 — audit-uat must honour the audit_acknowledged marker that
// audit-open honours.
//
// isAuditItemAcknowledged is the ONE shared suppression predicate, but its
// "every scanner below" scope ended at audit.cts's file boundary:
// cmdAuditUat (src/uat.cts) hand-rolled discovery over the SAME UAT and
// VERIFICATION artifacts and never read the marker — so the documented,
// self-invalidating "this item is moot" seam worked for audit-open and was
// ignored by audit-uat, leaving no honest way to close a moot item.
// ─────────────────────────────────────────────────────────────────────────────
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createTempProject, cleanup, runGsdTools } = require('./helpers.cjs');
const UAT_BODY = [
'## Tests',
'',
'### 1. Deleted Feature Probe',
'expected: The deleted admin route no longer exists',
'result: pending',
'',
].join('\n');
function seedUatPhase(tmpDir, phaseDir, frontmatterExtra) {
const dir = path.join(tmpDir, '.planning', 'phases', phaseDir);
fs.mkdirSync(dir, { recursive: true });
const fm = [
'---',
'status: human_needed',
...(frontmatterExtra || []),
'---',
'',
].join('\n');
fs.writeFileSync(path.join(dir, `${phaseDir}-UAT.md`), fm + UAT_BODY);
return dir;
}
function seedVerificationPhase(tmpDir, phaseDir, frontmatterExtra) {
const dir = path.join(tmpDir, '.planning', 'phases', phaseDir);
fs.mkdirSync(dir, { recursive: true });
const fm = [
'---',
'status: human_needed',
...(frontmatterExtra || []),
'---',
'',
].join('\n');
fs.writeFileSync(path.join(dir, `${phaseDir}-VERIFICATION.md`), fm + [
'# Verification',
'',
'## Human Verification',
'',
'1. **Test:** the deleted route stays deleted',
' - **Result:** pending',
'',
].join('\n'));
return dir;
}
function roadmapWith(phases) {
const lines = ['# Roadmap', ''];
for (const p of phases) lines.push(`### Phase ${p}: P${p}`, '- [ ] w', '');
return lines.join('\n');
}
function runAudit(cwd) {
const r = runGsdTools(['query', 'audit-uat'], cwd);
assert.ok(r.success, r.error);
return JSON.parse(r.output);
}
describe('#3805: audit-uat honours audit_acknowledged', () => {
test('#3805: an acknowledged UAT file is suppressed and counted', (t) => {
const tmpDir = createTempProject('gsd-3805-uat-');
t.after(() => cleanup(tmpDir));
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), roadmapWith(['02']));
seedUatPhase(tmpDir, '02-probe', [
'audit_acknowledged:',
' milestone: v1.0',
' at: 2026-08-24',
' gap_snapshot: human_needed::scenarios=1',
]);
// Note: the gap_snapshot must match the CURRENT derived value for the
// marker to suppress (self-invalidation on edit).
const out = runAudit(tmpDir);
assert.equal(out.summary.total_items, 0,
`#3805: the acknowledged UAT item must be suppressed; got ${JSON.stringify(out.results)}`);
assert.equal(out.acknowledged_files, 1,
'#3805: the suppressed file must be visible in acknowledged_files (audit-open honesty model)');
});
test('#3805: a STALE gap_snapshot (content changed since ack) still surfaces', (t) => {
const tmpDir = createTempProject('gsd-3805-uatstale-');
t.after(() => cleanup(tmpDir));
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), roadmapWith(['03']));
// Marker snapshots scenarios=2 but the file now has 1 — self-invalidation.
seedUatPhase(tmpDir, '03-probe', [
'audit_acknowledged:',
' milestone: v1.0',
' at: 2026-08-24',
' gap_snapshot: human_needed::scenarios=2',
]);
const out = runAudit(tmpDir);
assert.ok(out.summary.total_items > 0,
'a marker whose snapshot no longer matches must NOT suppress (self-invalidation)');
assert.equal(out.acknowledged_files, 0, 'a stale marker does not count as acknowledged');
});
test('#3805: an acknowledged VERIFICATION file is suppressed (status snapshot)', (t) => {
const tmpDir = createTempProject('gsd-3805-ver-');
t.after(() => cleanup(tmpDir));
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), roadmapWith(['04']));
seedVerificationPhase(tmpDir, '04-probe', [
'audit_acknowledged:',
' milestone: v1.0',
' at: 2026-08-24',
' status: human_needed',
]);
const out = runAudit(tmpDir);
assert.equal(out.summary.total_items, 0,
`#3805: the acknowledged VERIFICATION item must be suppressed; got ${JSON.stringify(out.results)}`);
assert.equal(out.acknowledged_files, 1,
'#3805: the suppressed VERIFICATION file must be visible in acknowledged_files');
});
test('#3805 control: an unacknowledged open UAT file still surfaces', (t) => {
const tmpDir = createTempProject('gsd-3805-ctl-');
t.after(() => cleanup(tmpDir));
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), roadmapWith(['05']));
seedUatPhase(tmpDir, '05-probe');
const out = runAudit(tmpDir);
assert.ok(out.summary.total_items > 0, 'no marker → item surfaces (pre-existing behavior)');
});
});