feat(#247): runtime-neutral phase uat-passed predicate from HUMAN-UAT results (#1063)

* feat(#247): runtime-neutral phase uat-passed predicate from HUMAN-UAT results

Wire the already-reserved `phase.uat-passed` alias (subcommand `uat-passed`,
mutation:false) into the phase command router with a new markdown-aware
predicate that evaluates HUMAN-UAT results and reports pass only when every
required check passes. Post-SDK-retirement (ADR-0174/#174) successor to the
SDK-framed #70, with no SDK-specific API surface.

New pure module src/uat-predicate.cts:
- stripFalsePositiveContexts: frontmatter -> HTML-comment -> CommonMark-style
  fenced-block state machine (tracks delimiter char+length) -> blockquote,
  each a small composable step, so a `result: passed` inside frontmatter, a
  fenced/~~~ block (incl. ~~~ nested in a ``` fence), a comment, or a
  blockquote is never counted.
- parseUatResultItems: heading-block parser, column-0-anchored same-line
  result; a heading with no result -> `missing` (fail-closed).
- analyzeMarkdown: unterminated fence/comment detection (malformed -> blocker).
- evaluateUatPassed: allowlist pass/verification semantics; passed = no
  blockers && >=1 check && all passing; no_uat_artifacts discriminator (no
  vacuous pass); optional requireVerification policy hook.

Thin cmdPhaseUatPassed handler in phase.cts; router closure rejects unknown
flags via makeInvalidArgs. Hardened across two Codex adversarial passes
(vacuous pass, dropped failing tests, permissive verification status,
nested-fence escape, cross-line result value, masked unterminated comment) —
all fixed fail-closed. New unit + CLI-integration suites incl. a fast-check
property test; docs, CONTEXT glossary, inventory, and changeset updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#247): backfill changeset PR number (#1063)

* fix(#247): indexOf paired-scan for unterminated-comment detection

CodeQL js/incomplete-multi-character-sanitization (high) flagged the
`raw.replace(/<!--[\s\S]*?-->/g,'')`-then-`.includes('<!--')` detection in
analyzeMarkdown as incomplete sanitization (a single regex pass can leave a
residual `<!--`). Replace it with a paired left-to-right indexOf scan that
contains no `.replace()` of the comment token — CodeQL-clean and strictly
more correct (a closed earlier comment can never mask a later unterminated
one). Behaviour unchanged; 98 predicate tests + scoped docker run green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-11 16:37:17 -04:00
committed by GitHub
parent 8813ee5f95
commit 9223f2f4c8
14 changed files with 2081 additions and 1 deletions

View File

@@ -29,6 +29,9 @@ import stateMod = require('./state.cjs');
import { platformWriteSync, platformReadSync, platformEnsureDir } from './shell-command-projection.cjs';
import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs';
import { deriveProgressFromRoadmap, clampPercent } from './phase-lifecycle.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports -- uat-predicate.cjs is an export= CommonJS module
import uatPredicate = require('./uat-predicate.cjs');
const { evaluateUatPassed } = uatPredicate;
const {
escapeRegex,
@@ -1712,6 +1715,28 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
output(result, raw);
}
function cmdPhaseUatPassed(
cwd: string,
phaseNum: string | undefined,
raw: boolean,
opts: { policy?: { requireVerification?: boolean } } = {},
): void {
if (!phaseNum) {
error('phase number required for phase uat-passed');
}
const phaseInfoRaw = findPhaseInternal(cwd, phaseNum!);
if (!phaseInfoRaw) {
error(`Phase ${phaseNum} not found`);
}
const phaseInfo = phaseInfoRaw as unknown as Record<string, unknown>;
const phaseFullDir = path.join(cwd, phaseInfo['directory'] as string);
const report = evaluateUatPassed(phaseFullDir, { policy: opts.policy });
output({ phase: phaseNum, ...report }, raw);
}
export = {
cmdPhasesList,
cmdPhaseNextDecimal,
@@ -1723,5 +1748,6 @@ export = {
cmdPhaseInsert,
cmdPhaseRemove,
cmdPhaseComplete,
cmdPhaseUatPassed,
computeDependencyLevels,
};