vibe-coding-master 0.7.30 → 0.7.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/backend/services/auto-memory-service.js +88 -8
- package/dist/backend/services/gate-review-service.js +25 -7
- package/dist/backend/services/harness-feedback-service.js +41 -3
- package/dist/backend/services/memory-proposal-validation.js +76 -31
- package/dist/backend/services/memory-review-validation.js +207 -15
- package/dist/backend/templates/handoff.js +21 -1
- package/dist/backend/templates/harness/gate-review.js +19 -0
- package/dist/backend/templates/harness/harness-engineer-agent.js +6 -3
- package/dist/backend/templates/harness/project-manager-agent.js +21 -13
- package/dist/backend/templates/harness/tester-agent.js +35 -4
- package/dist/shared/types/gate-review.js +4 -0
- package/dist/shared/validation/artifact-check.js +55 -1
- package/dist/shared/validation/artifact-contract.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -503,11 +503,14 @@ shared or current-role target and its supporting evidence. Adds and updates
|
|
|
503
503
|
also state why the memory is necessary, the impact of omitting it, and whether
|
|
504
504
|
the knowledge belongs in memory or a durable document. Harness Engineer first
|
|
505
505
|
reviews every existing memory entry, recording its retention reason, removal
|
|
506
|
-
impact, and durable-document disposition, then evaluates
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
and
|
|
510
|
-
|
|
506
|
+
impact, and durable-document disposition, then evaluates every proposal item
|
|
507
|
+
independently. Each Add or Update decision records why the memory is necessary,
|
|
508
|
+
what happens if it is absent, whether it belongs in memory or a durable
|
|
509
|
+
document, and the exact final memory content when retained. VCM validates that
|
|
510
|
+
every candidate has one matching decision and that the reviewed memory files
|
|
511
|
+
implement those decisions before applying them. Harness Engineer review and
|
|
512
|
+
retrospective work remain tool role activity and do not participate in Round
|
|
513
|
+
completion.
|
|
511
514
|
|
|
512
515
|
When Auto Memory is disabled, Review Task Harness does not collect proposals or
|
|
513
516
|
ask Harness Engineer to update memory. When enabled, both automatic and manual
|
|
@@ -6,8 +6,8 @@ import { VcmError } from "../errors.js";
|
|
|
6
6
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
7
7
|
import { readVcmMemoryBlock, replaceVcmMemoryBlock } from "../templates/harness/memory-block.js";
|
|
8
8
|
import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
|
|
9
|
-
import { validateMemoryProposal } from "./memory-proposal-validation.js";
|
|
10
|
-
import {
|
|
9
|
+
import { parseMemoryProposal, validateMemoryProposal } from "./memory-proposal-validation.js";
|
|
10
|
+
import { parseMemoryReviewReport, validateMemoryReviewOutput } from "./memory-review-validation.js";
|
|
11
11
|
const MEMORY_FILE_DEFINITIONS = [
|
|
12
12
|
{ path: "CLAUDE.md", title: "Shared Memory" },
|
|
13
13
|
{ path: ".claude/agents/project-manager.md", title: "Project Manager Memory", role: "project-manager" },
|
|
@@ -251,6 +251,7 @@ export function createAutoMemoryService(deps) {
|
|
|
251
251
|
statusCode: 409
|
|
252
252
|
});
|
|
253
253
|
}
|
|
254
|
+
const proposalCandidates = await readReviewCandidates(taskRepoRoot, state);
|
|
254
255
|
const timestamp = now();
|
|
255
256
|
state.reviewPromptDispatchedAt = timestamp;
|
|
256
257
|
state.retrospectiveReportPath = retrospectiveReportPath;
|
|
@@ -263,7 +264,7 @@ export function createAutoMemoryService(deps) {
|
|
|
263
264
|
roleDraftsPath: path.join(runRoot, "drafts"),
|
|
264
265
|
currentMemoryPath: path.join(runRoot, "before"),
|
|
265
266
|
reviewedMemoryPath: path.join(runRoot, "after"),
|
|
266
|
-
|
|
267
|
+
proposalCandidates,
|
|
267
268
|
...(planningCandidatePath
|
|
268
269
|
? { planningCandidatePath: resolveRepoPath(taskRepoRoot, planningCandidatePath) }
|
|
269
270
|
: {})
|
|
@@ -360,11 +361,24 @@ export function createAutoMemoryService(deps) {
|
|
|
360
361
|
await failReview(input.taskRepoRoot, state, "Task Harness Retrospective did not write the required retrospective report.");
|
|
361
362
|
return true;
|
|
362
363
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
await
|
|
364
|
+
try {
|
|
365
|
+
const report = await deps.fs.readText(state.retrospectiveReportPath);
|
|
366
|
+
const currentMemorySnapshot = await readRunMemorySet(input.taskRepoRoot, state.runId, "before");
|
|
367
|
+
const reviewedMemorySnapshot = await readRunMemorySet(input.taskRepoRoot, state.runId, "after");
|
|
368
|
+
const candidates = await readReviewCandidates(input.taskRepoRoot, state);
|
|
369
|
+
const reportResult = parseMemoryReviewReport(report, candidates, hasSubstantiveMemory(currentMemorySnapshot));
|
|
370
|
+
if (reportResult.error) {
|
|
371
|
+
await failReview(input.taskRepoRoot, state, `Task Harness Retrospective memory review report ${reportResult.error}.`);
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
const outputError = await validateReviewedMemoryOutput(input.taskRepoRoot, currentMemorySnapshot, reviewedMemorySnapshot, reportResult.decisions ?? []);
|
|
375
|
+
if (outputError) {
|
|
376
|
+
await failReview(input.taskRepoRoot, state, `Task Harness Retrospective reviewed memory ${outputError}.`);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
await failReview(input.taskRepoRoot, state, `Task Harness Retrospective memory review validation failed: ${errorMessage(error)}`);
|
|
368
382
|
return true;
|
|
369
383
|
}
|
|
370
384
|
await applyReviewedMemory(input.taskRepoRoot, state);
|
|
@@ -525,6 +539,41 @@ export function createAutoMemoryService(deps) {
|
|
|
525
539
|
? relativePath
|
|
526
540
|
: undefined;
|
|
527
541
|
}
|
|
542
|
+
async function readReviewCandidates(taskRepoRoot, state) {
|
|
543
|
+
const candidates = [];
|
|
544
|
+
for (const draft of state.drafts) {
|
|
545
|
+
const draftPath = resolveRepoPath(taskRepoRoot, draft.path);
|
|
546
|
+
const parsed = parseMemoryProposal(await deps.fs.readText(draftPath));
|
|
547
|
+
if (!parsed.proposal) {
|
|
548
|
+
throw new Error(`${draft.role} memory draft ${parsed.error ?? "could not be parsed"}`);
|
|
549
|
+
}
|
|
550
|
+
candidates.push(...toReviewCandidates(draft.role, draft.role, parsed.proposal.items));
|
|
551
|
+
}
|
|
552
|
+
const planningCandidatePath = await findPlanningCandidateSnapshot(taskRepoRoot, state.runId);
|
|
553
|
+
if (planningCandidatePath) {
|
|
554
|
+
const parsed = parseMemoryProposal(await deps.fs.readText(resolveRepoPath(taskRepoRoot, planningCandidatePath)));
|
|
555
|
+
if (!parsed.proposal) {
|
|
556
|
+
throw new Error(`Architect planning-session memory candidate ${parsed.error ?? "could not be parsed"}`);
|
|
557
|
+
}
|
|
558
|
+
candidates.push(...toReviewCandidates("architect-planning", "architect", parsed.proposal.items));
|
|
559
|
+
}
|
|
560
|
+
return candidates;
|
|
561
|
+
}
|
|
562
|
+
async function validateReviewedMemoryOutput(taskRepoRoot, before, after, decisions) {
|
|
563
|
+
return validateMemoryReviewOutput({
|
|
564
|
+
before: memorySetByTarget(before),
|
|
565
|
+
after: memorySetByTarget(after),
|
|
566
|
+
decisions,
|
|
567
|
+
durableDocExists: async (durableDocPath) => {
|
|
568
|
+
try {
|
|
569
|
+
return await deps.fs.pathExists(resolveRepoPath(taskRepoRoot, durableDocPath));
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
}
|
|
528
577
|
async function applyReviewedMemory(taskRepoRoot, state) {
|
|
529
578
|
try {
|
|
530
579
|
const before = await readRunMemorySet(taskRepoRoot, state.runId, "before");
|
|
@@ -756,6 +805,37 @@ export async function assertMemoryBlocksInstalled(fs, taskRepoRoot) {
|
|
|
756
805
|
function currentDraft(state) {
|
|
757
806
|
return state.drafts.find((draft) => draft.status !== "completed");
|
|
758
807
|
}
|
|
808
|
+
function toReviewCandidates(source, currentRole, items) {
|
|
809
|
+
return items.map((item) => ({
|
|
810
|
+
id: `${source}:${item.operation}:${item.ordinal}`,
|
|
811
|
+
source,
|
|
812
|
+
operation: item.operation,
|
|
813
|
+
target: item.target === "shared" ? "shared" : currentRole,
|
|
814
|
+
...(item.content ? { content: item.content } : {}),
|
|
815
|
+
...(item.existing ? { existing: item.existing } : {})
|
|
816
|
+
}));
|
|
817
|
+
}
|
|
818
|
+
function memoryPathForTarget(target) {
|
|
819
|
+
if (target === "shared") {
|
|
820
|
+
return "CLAUDE.md";
|
|
821
|
+
}
|
|
822
|
+
const definition = MEMORY_FILE_DEFINITIONS.find((candidate) => "role" in candidate && candidate.role === target);
|
|
823
|
+
if (!definition) {
|
|
824
|
+
throw new Error(`Missing memory file definition for review target: ${target}`);
|
|
825
|
+
}
|
|
826
|
+
return definition.path;
|
|
827
|
+
}
|
|
828
|
+
function memorySetByTarget(memory) {
|
|
829
|
+
return {
|
|
830
|
+
shared: memory["CLAUDE.md"],
|
|
831
|
+
"project-manager": memory[memoryPathForTarget("project-manager")],
|
|
832
|
+
architect: memory[memoryPathForTarget("architect")],
|
|
833
|
+
coder: memory[memoryPathForTarget("coder")],
|
|
834
|
+
tester: memory[memoryPathForTarget("tester")],
|
|
835
|
+
reviewer: memory[memoryPathForTarget("reviewer")],
|
|
836
|
+
"harness-engineer": memory[memoryPathForTarget("harness-engineer")]
|
|
837
|
+
};
|
|
838
|
+
}
|
|
759
839
|
function toActiveReview(state) {
|
|
760
840
|
return {
|
|
761
841
|
runId: state.runId,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { CODE_DIFF_SOURCES, GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
|
|
3
|
+
import { CODE_DIFF_FINDING_SCOPES, CODE_DIFF_SOURCES, GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
|
|
4
4
|
import { checkMarkdownArtifact } from "../../shared/validation/artifact-check.js";
|
|
5
5
|
import { VcmError } from "../errors.js";
|
|
6
6
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
@@ -38,6 +38,7 @@ const VALIDATION_ANALYSIS_FIELDS = [
|
|
|
38
38
|
"Boundary And Failure Coverage",
|
|
39
39
|
"Public Contract Coverage",
|
|
40
40
|
"Test Integrity",
|
|
41
|
+
"Test Infrastructure",
|
|
41
42
|
"Skips And Gaps",
|
|
42
43
|
"User Approval And Gap Disposition",
|
|
43
44
|
"Validation Readiness"
|
|
@@ -91,6 +92,7 @@ const CORE_INPUT_ARTIFACTS = {
|
|
|
91
92
|
"validation-adequacy": ".ai/vcm/handoffs/test-report.md"
|
|
92
93
|
};
|
|
93
94
|
const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low"]);
|
|
95
|
+
const VALID_CODE_DIFF_FINDING_SCOPES = new Set(CODE_DIFF_FINDING_SCOPES);
|
|
94
96
|
export function createGateReviewService(deps) {
|
|
95
97
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
96
98
|
const reportPollIntervalMs = deps.reportPollIntervalMs ?? DEFAULT_REPORT_POLL_INTERVAL_MS;
|
|
@@ -1067,11 +1069,18 @@ async function readValidationReportError(fs, taskRepoRoot) {
|
|
|
1067
1069
|
const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
|
|
1068
1070
|
const content = await fs.pathExists(absolutePath) ? await fs.readText(absolutePath) : null;
|
|
1069
1071
|
const check = checkMarkdownArtifact("test-report", relativePath, content);
|
|
1070
|
-
if (check.status
|
|
1071
|
-
return
|
|
1072
|
+
if (check.status !== "ok") {
|
|
1073
|
+
return `${relativePath} is incomplete and cannot start validation-adequacy review. `
|
|
1074
|
+
+ formatValidationArtifactFailure(check, content);
|
|
1075
|
+
}
|
|
1076
|
+
const infrastructureStatus = matchField(extractMarkdownSection(content ?? "", "Test Infrastructure") ?? "", "Status");
|
|
1077
|
+
if (infrastructureStatus === "repair-required") {
|
|
1078
|
+
return `${relativePath} cannot start validation-adequacy review while Test Infrastructure Status is repair-required. Route Tester repair first.`;
|
|
1079
|
+
}
|
|
1080
|
+
if (infrastructureStatus === "production-change-required") {
|
|
1081
|
+
return `${relativePath} cannot start validation-adequacy review while Test Infrastructure Status is production-change-required. Route the active flow's implementation-failure branch first.`;
|
|
1072
1082
|
}
|
|
1073
|
-
return
|
|
1074
|
-
+ formatValidationArtifactFailure(check, content);
|
|
1083
|
+
return undefined;
|
|
1075
1084
|
}
|
|
1076
1085
|
async function readCodeDiffPrerequisiteError(deps, context, index) {
|
|
1077
1086
|
const reportError = await readValidationReportError(deps.fs, context.taskRepoRoot);
|
|
@@ -1367,11 +1376,13 @@ function validateRequestChangeFindings(findings) {
|
|
|
1367
1376
|
}
|
|
1368
1377
|
}
|
|
1369
1378
|
function validateCodeDiffFindings(findings) {
|
|
1370
|
-
const incomplete = findings.find((finding) => !finding.file?.trim()
|
|
1379
|
+
const incomplete = findings.find((finding) => (!finding.file?.trim()
|
|
1380
|
+
|| !finding.location?.trim()
|
|
1381
|
+
|| !finding.scope));
|
|
1371
1382
|
if (incomplete) {
|
|
1372
1383
|
throw new VcmError({
|
|
1373
1384
|
code: "GATE_REVIEW_CODE_DIFF_FINDING_LOCATION_MISSING",
|
|
1374
|
-
message: `Code-diff finding ${incomplete.title} must contain File
|
|
1385
|
+
message: `Code-diff finding ${incomplete.title} must contain File, Line Or Symbol, and Finding Scope.`,
|
|
1375
1386
|
statusCode: 500
|
|
1376
1387
|
});
|
|
1377
1388
|
}
|
|
@@ -1465,6 +1476,7 @@ function extractFindings(content) {
|
|
|
1465
1476
|
file: matchField(block, "file"),
|
|
1466
1477
|
line: parsePositiveInteger(matchField(block, "line")),
|
|
1467
1478
|
location: matchField(block, "line or symbol"),
|
|
1479
|
+
scope: normalizeCodeDiffFindingScope(matchField(block, "finding scope")),
|
|
1468
1480
|
evidence: matchField(block, "evidence") ?? "",
|
|
1469
1481
|
expected: matchField(block, "expected") ?? "",
|
|
1470
1482
|
gap: matchField(block, "gap") ?? "",
|
|
@@ -1527,6 +1539,12 @@ function normalizeSeverity(value) {
|
|
|
1527
1539
|
? normalized
|
|
1528
1540
|
: undefined;
|
|
1529
1541
|
}
|
|
1542
|
+
function normalizeCodeDiffFindingScope(value) {
|
|
1543
|
+
const normalized = typeof value === "string" ? value.toLowerCase() : "";
|
|
1544
|
+
return VALID_CODE_DIFF_FINDING_SCOPES.has(normalized)
|
|
1545
|
+
? normalized
|
|
1546
|
+
: undefined;
|
|
1547
|
+
}
|
|
1530
1548
|
function normalizeCallbackStatus(value) {
|
|
1531
1549
|
return value === "not_sent" || value === "sent" || value === "skipped" || value === "failed"
|
|
1532
1550
|
? value
|
|
@@ -260,9 +260,13 @@ export function createHarnessFeedbackService(deps) {
|
|
|
260
260
|
"Before evaluating proposals, review every substantive entry in every current memory snapshot against current code, documentation, and final task evidence.",
|
|
261
261
|
"For each existing entry, decide retain, update, remove, or move-to-durable-doc. Record the decision reason, the impact of removing it, and whether memory or a durable document is the correct source.",
|
|
262
262
|
"Complete this full existing-memory review even when every proposal says no-change.",
|
|
263
|
-
"Then evaluate every proposal
|
|
263
|
+
"Then evaluate every proposal item independently. Do not accept or reject a whole role draft as one decision.",
|
|
264
|
+
"For every Add or Update candidate, independently explain why the memory is necessary, what fails if it is absent, and why memory or a durable document is the correct destination.",
|
|
265
|
+
"Do not copy the proposer rationale as the review. Verify it against current code, durable documentation, and final task evidence.",
|
|
264
266
|
"Keep only verified, durable, reusable project knowledge. Merge duplicates and keep role-specific knowledge in the matching role file.",
|
|
265
267
|
"Do not record task narrative, temporary state, unverified conclusions, or Harness rules in memory.",
|
|
268
|
+
"Final content must be one exact line written to the selected reviewed-memory file. Use none when the decision does not keep memory.",
|
|
269
|
+
"For keep-in-memory use Durable doc disposition: memory. For keep-memory-reference use memory-reference. For move-to-durable-doc use durable-doc.",
|
|
266
270
|
"Existing-memory Target must be shared or the exact role name. Decision must be retain, update, remove, or move-to-durable-doc.",
|
|
267
271
|
"Durable doc disposition must be memory, durable-doc, or memory-reference. Use Durable doc path: none with memory and an actual path with the other dispositions.",
|
|
268
272
|
"Do not keep full content in memory when durable-doc is correct. Use memory-reference only when an ongoing role needs the document pointer.",
|
|
@@ -272,8 +276,10 @@ export function createHarnessFeedbackService(deps) {
|
|
|
272
276
|
"## Memory Review",
|
|
273
277
|
"Existing memory reviewed: complete",
|
|
274
278
|
"",
|
|
275
|
-
"### Proposal
|
|
276
|
-
...memoryReview.
|
|
279
|
+
"### Proposal Decisions",
|
|
280
|
+
...(memoryReview.proposalCandidates.length > 0
|
|
281
|
+
? memoryReview.proposalCandidates.flatMap(renderMemoryProposalDecisionTemplate)
|
|
282
|
+
: ["none"]),
|
|
277
283
|
"",
|
|
278
284
|
"### Existing Memory Decisions",
|
|
279
285
|
"#### Item 1",
|
|
@@ -299,6 +305,38 @@ export function createHarnessFeedbackService(deps) {
|
|
|
299
305
|
"End your turn after writing the result."
|
|
300
306
|
].join("\n");
|
|
301
307
|
}
|
|
308
|
+
function renderMemoryProposalDecisionTemplate(candidate) {
|
|
309
|
+
if (candidate.operation === "remove") {
|
|
310
|
+
return [
|
|
311
|
+
`#### Candidate ${candidate.id}`,
|
|
312
|
+
`Source: ${candidate.source}`,
|
|
313
|
+
"Operation: remove",
|
|
314
|
+
`Target: ${candidate.target}`,
|
|
315
|
+
`Existing: ${candidate.existing}`,
|
|
316
|
+
"Decision: remove|retain",
|
|
317
|
+
"Reason: <why the proposed removal should be applied or rejected>",
|
|
318
|
+
"Evidence checked: <current code, durable documentation, or final task evidence>",
|
|
319
|
+
""
|
|
320
|
+
];
|
|
321
|
+
}
|
|
322
|
+
return [
|
|
323
|
+
`#### Candidate ${candidate.id}`,
|
|
324
|
+
`Source: ${candidate.source}`,
|
|
325
|
+
`Operation: ${candidate.operation}`,
|
|
326
|
+
`Target: ${candidate.target}`,
|
|
327
|
+
`Candidate: ${candidate.content}`,
|
|
328
|
+
"Decision: keep-in-memory|keep-memory-reference|move-to-durable-doc|reject",
|
|
329
|
+
"Final target: shared|project-manager|architect|coder|tester|reviewer|harness-engineer|none",
|
|
330
|
+
"Why memory is necessary: <independent reason, or why it is not necessary>",
|
|
331
|
+
"Impact if absent: <specific impact, or why no durable impact exists>",
|
|
332
|
+
"Durable doc disposition: memory|durable-doc|memory-reference",
|
|
333
|
+
"Durable doc analysis: <why this destination is correct>",
|
|
334
|
+
"Durable doc path: <none or a project-relative durable doc path>",
|
|
335
|
+
"Evidence checked: <current code, durable documentation, or final task evidence>",
|
|
336
|
+
"Final content: <exact one-line reviewed-memory content or none>",
|
|
337
|
+
""
|
|
338
|
+
];
|
|
339
|
+
}
|
|
302
340
|
function buildPendingFeedbackPrompt(repoRoot, feedbackPath) {
|
|
303
341
|
return [
|
|
304
342
|
"[VCM Harness Feedback]",
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
const OPERATIONS = ["Add", "Update", "Remove"];
|
|
2
2
|
export function validateMemoryProposal(content) {
|
|
3
|
+
return parseMemoryProposal(content).error;
|
|
4
|
+
}
|
|
5
|
+
export function parseMemoryProposal(content) {
|
|
3
6
|
if (!/^# Memory Proposal\s*$/m.test(content)) {
|
|
4
|
-
return "is missing the # Memory Proposal heading";
|
|
7
|
+
return { error: "is missing the # Memory Proposal heading" };
|
|
5
8
|
}
|
|
6
9
|
const decisionMatches = [...content.matchAll(/^Decision:[ \t]*(update|no-change)[ \t]*$/gm)];
|
|
7
10
|
if (decisionMatches.length !== 1) {
|
|
8
|
-
return "must contain exactly one Decision: update or Decision: no-change field";
|
|
11
|
+
return { error: "must contain exactly one Decision: update or Decision: no-change field" };
|
|
9
12
|
}
|
|
10
13
|
const levelTwoHeadings = [...content.matchAll(/^## ([^\r\n]+?)[ \t]*$/gm)];
|
|
11
14
|
const sections = [...content.matchAll(/^## (Add|Update|Remove)[ \t]*$/gm)];
|
|
12
15
|
if (levelTwoHeadings.length !== OPERATIONS.length
|
|
13
16
|
|| sections.length !== OPERATIONS.length
|
|
14
17
|
|| sections.some((section, index) => section[1] !== OPERATIONS[index])) {
|
|
15
|
-
return "must contain only the Add, Update, and Remove sections, exactly once and in that order";
|
|
18
|
+
return { error: "must contain only the Add, Update, and Remove sections, exactly once and in that order" };
|
|
16
19
|
}
|
|
17
|
-
|
|
20
|
+
const items = [];
|
|
18
21
|
for (let index = 0; index < sections.length; index += 1) {
|
|
19
22
|
const section = sections[index];
|
|
20
23
|
const operation = section[1];
|
|
@@ -22,30 +25,31 @@ export function validateMemoryProposal(content) {
|
|
|
22
25
|
const bodyEnd = index + 1 < sections.length
|
|
23
26
|
? sections[index + 1].index ?? content.length
|
|
24
27
|
: content.length;
|
|
25
|
-
const result =
|
|
26
|
-
if (
|
|
27
|
-
return result;
|
|
28
|
+
const result = parseOperationBody(operation, content.slice(bodyStart, bodyEnd));
|
|
29
|
+
if (result.error) {
|
|
30
|
+
return { error: result.error };
|
|
28
31
|
}
|
|
29
|
-
|
|
32
|
+
items.push(...result.items);
|
|
30
33
|
}
|
|
31
34
|
const decision = decisionMatches[0][1];
|
|
32
|
-
if (decision === "no-change" &&
|
|
33
|
-
return "uses Decision: no-change but contains a memory item";
|
|
35
|
+
if (decision === "no-change" && items.length !== 0) {
|
|
36
|
+
return { error: "uses Decision: no-change but contains a memory item" };
|
|
34
37
|
}
|
|
35
|
-
if (decision === "update" &&
|
|
36
|
-
return "uses Decision: update without a structured memory item";
|
|
38
|
+
if (decision === "update" && items.length === 0) {
|
|
39
|
+
return { error: "uses Decision: update without a structured memory item" };
|
|
37
40
|
}
|
|
38
|
-
return
|
|
41
|
+
return { proposal: { decision, items } };
|
|
39
42
|
}
|
|
40
|
-
function
|
|
43
|
+
function parseOperationBody(section, rawBody) {
|
|
41
44
|
const body = rawBody.trim();
|
|
42
45
|
if (body === "none") {
|
|
43
|
-
return
|
|
46
|
+
return { items: [] };
|
|
44
47
|
}
|
|
45
48
|
const itemHeadings = [...body.matchAll(/^### Item \d+\s*$/gm)];
|
|
46
49
|
if (itemHeadings.length === 0 || body.slice(0, itemHeadings[0].index).trim()) {
|
|
47
|
-
return `${
|
|
50
|
+
return { items: [], error: `${section} must contain none or one or more ### Item N blocks` };
|
|
48
51
|
}
|
|
52
|
+
const items = [];
|
|
49
53
|
for (let index = 0; index < itemHeadings.length; index += 1) {
|
|
50
54
|
const heading = itemHeadings[index];
|
|
51
55
|
const itemStart = (heading.index ?? 0) + heading[0].length;
|
|
@@ -53,28 +57,69 @@ function validateOperationBody(operation, rawBody) {
|
|
|
53
57
|
? itemHeadings[index + 1].index ?? body.length
|
|
54
58
|
: body.length;
|
|
55
59
|
const item = body.slice(itemStart, itemEnd).trim();
|
|
56
|
-
const expectedPattern =
|
|
60
|
+
const expectedPattern = section === "Add"
|
|
57
61
|
? /^Target:[ \t]*(shared|current-role)[ \t]*\nContent:[ \t]*(\S.*)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nImpact if absent:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/
|
|
58
|
-
:
|
|
62
|
+
: section === "Update"
|
|
59
63
|
? /^Target:[ \t]*(shared|current-role)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nContent:[ \t]*(\S.*)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nImpact if absent:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/
|
|
60
64
|
: /^Target:[ \t]*(shared|current-role)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/;
|
|
61
|
-
const
|
|
62
|
-
if (!
|
|
63
|
-
const fields =
|
|
65
|
+
const match = expectedPattern.exec(item);
|
|
66
|
+
if (!match) {
|
|
67
|
+
const fields = section === "Add"
|
|
64
68
|
? "Target, Content, Reason, Impact if absent, Durable doc disposition, Durable doc path, and Evidence"
|
|
65
|
-
:
|
|
69
|
+
: section === "Update"
|
|
66
70
|
? "Target, Existing, Content, Reason, Impact if absent, Durable doc disposition, Durable doc path, and Evidence"
|
|
67
71
|
: "Target, Existing, and Evidence";
|
|
68
|
-
return
|
|
72
|
+
return {
|
|
73
|
+
items: [],
|
|
74
|
+
error: `${section} ${heading[0].trim()} must contain one-line ${fields} fields in that order`
|
|
75
|
+
};
|
|
69
76
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
const parsed = parseProposalItem(section, index + 1, match);
|
|
78
|
+
if (parsed.durableDocDisposition
|
|
79
|
+
&& ((parsed.durableDocDisposition === "memory" && parsed.durableDocPath !== "none")
|
|
80
|
+
|| (parsed.durableDocDisposition !== "memory" && parsed.durableDocPath === "none"))) {
|
|
81
|
+
return {
|
|
82
|
+
items: [],
|
|
83
|
+
error: `${section} ${heading[0].trim()} must use Durable doc path: none only with Durable doc disposition: memory`
|
|
84
|
+
};
|
|
77
85
|
}
|
|
86
|
+
items.push(parsed);
|
|
87
|
+
}
|
|
88
|
+
return { items };
|
|
89
|
+
}
|
|
90
|
+
function parseProposalItem(section, ordinal, match) {
|
|
91
|
+
if (section === "Add") {
|
|
92
|
+
return {
|
|
93
|
+
operation: "add",
|
|
94
|
+
ordinal,
|
|
95
|
+
target: match[1],
|
|
96
|
+
content: match[2],
|
|
97
|
+
reason: match[3],
|
|
98
|
+
impactIfAbsent: match[4],
|
|
99
|
+
durableDocDisposition: match[5],
|
|
100
|
+
durableDocPath: match[6],
|
|
101
|
+
evidence: match[7]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (section === "Update") {
|
|
105
|
+
return {
|
|
106
|
+
operation: "update",
|
|
107
|
+
ordinal,
|
|
108
|
+
target: match[1],
|
|
109
|
+
existing: match[2],
|
|
110
|
+
content: match[3],
|
|
111
|
+
reason: match[4],
|
|
112
|
+
impactIfAbsent: match[5],
|
|
113
|
+
durableDocDisposition: match[6],
|
|
114
|
+
durableDocPath: match[7],
|
|
115
|
+
evidence: match[8]
|
|
116
|
+
};
|
|
78
117
|
}
|
|
79
|
-
return
|
|
118
|
+
return {
|
|
119
|
+
operation: "remove",
|
|
120
|
+
ordinal,
|
|
121
|
+
target: match[1],
|
|
122
|
+
existing: match[2],
|
|
123
|
+
evidence: match[3]
|
|
124
|
+
};
|
|
80
125
|
}
|
|
@@ -1,47 +1,222 @@
|
|
|
1
|
-
export function validateMemoryReviewReport(content,
|
|
1
|
+
export function validateMemoryReviewReport(content, candidates, hasExistingMemory = false) {
|
|
2
|
+
return parseMemoryReviewReport(content, candidates, hasExistingMemory).error;
|
|
3
|
+
}
|
|
4
|
+
export function parseMemoryReviewReport(content, candidates, hasExistingMemory = false) {
|
|
2
5
|
const memoryReview = /^## Memory Review\s*$/m.exec(content);
|
|
3
6
|
if (!memoryReview || memoryReview.index === undefined) {
|
|
4
|
-
return "is missing the ## Memory Review section";
|
|
7
|
+
return { error: "is missing the ## Memory Review section" };
|
|
5
8
|
}
|
|
6
9
|
const sectionStart = memoryReview.index + memoryReview[0].length;
|
|
7
10
|
const nextSection = /^## (?!#)/m.exec(content.slice(sectionStart));
|
|
8
11
|
const section = content.slice(sectionStart, nextSection?.index === undefined ? content.length : sectionStart + nextSection.index);
|
|
9
12
|
if (!/^Existing memory reviewed:[ \t]*complete[ \t]*$/m.test(section)) {
|
|
10
|
-
return "must declare Existing memory reviewed: complete";
|
|
13
|
+
return { error: "must declare Existing memory reviewed: complete" };
|
|
11
14
|
}
|
|
12
15
|
if (!/^Reviewed memory set:[ \t]*complete[ \t]*$/m.test(section)) {
|
|
13
|
-
return "must declare Reviewed memory set: complete";
|
|
16
|
+
return { error: "must declare Reviewed memory set: complete" };
|
|
14
17
|
}
|
|
15
|
-
const dispositions = extractReportSubsection(section, "Proposal
|
|
18
|
+
const dispositions = extractReportSubsection(section, "Proposal Decisions", "Existing Memory Decisions");
|
|
16
19
|
if (dispositions === undefined) {
|
|
17
|
-
return "is missing the Proposal
|
|
20
|
+
return { error: "is missing the Proposal Decisions subsection" };
|
|
18
21
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return `must record exactly one accepted, rejected, or no-change disposition for ${role}`;
|
|
23
|
-
}
|
|
22
|
+
const decisionsResult = parseProposalDecisions(dispositions, candidates);
|
|
23
|
+
if (decisionsResult.error) {
|
|
24
|
+
return decisionsResult;
|
|
24
25
|
}
|
|
25
26
|
const existingDecisions = extractReportSubsection(section, "Existing Memory Decisions", "Existing Memory Changes");
|
|
26
27
|
if (existingDecisions === undefined) {
|
|
27
|
-
return "is missing the Existing Memory Decisions subsection";
|
|
28
|
+
return { error: "is missing the Existing Memory Decisions subsection" };
|
|
28
29
|
}
|
|
29
30
|
const existingDecisionError = validateExistingMemoryDecisions(existingDecisions, hasExistingMemory);
|
|
30
31
|
if (existingDecisionError) {
|
|
31
|
-
return existingDecisionError;
|
|
32
|
+
return { error: existingDecisionError };
|
|
32
33
|
}
|
|
33
34
|
const existingChanges = extractReportSubsection(section, "Existing Memory Changes");
|
|
34
35
|
if (existingChanges === undefined) {
|
|
35
|
-
return "is missing the Existing Memory Changes subsection";
|
|
36
|
+
return { error: "is missing the Existing Memory Changes subsection" };
|
|
36
37
|
}
|
|
37
38
|
for (const field of ["retained", "updated", "removed"]) {
|
|
38
39
|
const matches = existingChanges.match(new RegExp(`^- ${field}:[ \\t]*\\S.*$`, "gm"));
|
|
39
40
|
if (matches?.length !== 1) {
|
|
40
|
-
return `must record exactly one non-empty ${field} summary
|
|
41
|
+
return { error: `must record exactly one non-empty ${field} summary` };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { decisions: decisionsResult.decisions };
|
|
45
|
+
}
|
|
46
|
+
export async function validateMemoryReviewOutput(input) {
|
|
47
|
+
const retainedProposalContent = new Set(input.decisions
|
|
48
|
+
.filter((decision) => (decision.decision === "keep-in-memory"
|
|
49
|
+
|| decision.decision === "keep-memory-reference"))
|
|
50
|
+
.map((decision) => decision.finalContent)
|
|
51
|
+
.filter((content) => Boolean(content && content !== "none")));
|
|
52
|
+
for (const decision of input.decisions) {
|
|
53
|
+
const originalBefore = memoryLines(input.before[decision.target]);
|
|
54
|
+
const originalAfter = memoryLines(input.after[decision.target]);
|
|
55
|
+
if (decision.operation === "remove") {
|
|
56
|
+
if (decision.decision === "remove" && originalAfter.has(decision.existing ?? "")) {
|
|
57
|
+
return `still contains ${decision.candidateId}, which the report decided to remove`;
|
|
58
|
+
}
|
|
59
|
+
if (decision.decision === "retain" && !originalAfter.has(decision.existing ?? "")) {
|
|
60
|
+
return `does not retain ${decision.candidateId} as required by the report`;
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const keepsMemory = decision.decision === "keep-in-memory"
|
|
65
|
+
|| decision.decision === "keep-memory-reference";
|
|
66
|
+
if (keepsMemory) {
|
|
67
|
+
const finalTarget = decision.finalTarget;
|
|
68
|
+
if (!memoryLines(input.after[finalTarget]).has(decision.finalContent ?? "")) {
|
|
69
|
+
return `does not contain the exact Final content for ${decision.candidateId} in ${finalTarget}`;
|
|
70
|
+
}
|
|
71
|
+
if (decision.operation === "update"
|
|
72
|
+
&& decision.existing
|
|
73
|
+
&& decision.existing !== decision.finalContent
|
|
74
|
+
&& originalAfter.has(decision.existing)) {
|
|
75
|
+
return `still contains the superseded Existing value for ${decision.candidateId}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (decision.candidate
|
|
79
|
+
&& !memorySetContains(input.before, decision.candidate)
|
|
80
|
+
&& memorySetContains(input.after, decision.candidate)
|
|
81
|
+
&& !retainedProposalContent.has(decision.candidate)) {
|
|
82
|
+
return `contains the rejected or durable-doc-only candidate ${decision.candidateId}`;
|
|
83
|
+
}
|
|
84
|
+
if (decision.durableDocDisposition === "memory-reference"
|
|
85
|
+
&& decision.durableDocPath
|
|
86
|
+
&& !(await input.durableDocExists(decision.durableDocPath))) {
|
|
87
|
+
return `references a missing durable document for ${decision.candidateId}: ${decision.durableDocPath}`;
|
|
41
88
|
}
|
|
42
89
|
}
|
|
43
90
|
return undefined;
|
|
44
91
|
}
|
|
92
|
+
function parseProposalDecisions(content, candidates) {
|
|
93
|
+
const body = content.trim();
|
|
94
|
+
if (candidates.length === 0) {
|
|
95
|
+
return body === "none"
|
|
96
|
+
? { decisions: [] }
|
|
97
|
+
: { error: "Proposal Decisions must be none when no memory candidate exists" };
|
|
98
|
+
}
|
|
99
|
+
if (body === "none") {
|
|
100
|
+
return { error: "Proposal Decisions cannot be none while memory candidates exist" };
|
|
101
|
+
}
|
|
102
|
+
const headings = [...body.matchAll(/^#### Candidate ([A-Za-z0-9._:-]+)[ \t]*$/gm)];
|
|
103
|
+
if (headings.length === 0 || body.slice(0, headings[0].index).trim()) {
|
|
104
|
+
return { error: "Proposal Decisions must contain one #### Candidate <id> block per memory candidate" };
|
|
105
|
+
}
|
|
106
|
+
const expected = new Map(candidates.map((candidate) => [candidate.id, candidate]));
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
const decisions = [];
|
|
109
|
+
for (let index = 0; index < headings.length; index += 1) {
|
|
110
|
+
const heading = headings[index];
|
|
111
|
+
const candidateId = heading[1];
|
|
112
|
+
if (seen.has(candidateId)) {
|
|
113
|
+
return { error: `contains duplicate proposal decision for ${candidateId}` };
|
|
114
|
+
}
|
|
115
|
+
const candidate = expected.get(candidateId);
|
|
116
|
+
if (!candidate) {
|
|
117
|
+
return { error: `contains an unexpected proposal decision for ${candidateId}` };
|
|
118
|
+
}
|
|
119
|
+
seen.add(candidateId);
|
|
120
|
+
const itemStart = (heading.index ?? 0) + heading[0].length;
|
|
121
|
+
const itemEnd = index + 1 < headings.length
|
|
122
|
+
? headings[index + 1].index ?? body.length
|
|
123
|
+
: body.length;
|
|
124
|
+
const result = parseProposalDecision(candidate, body.slice(itemStart, itemEnd).trim());
|
|
125
|
+
if (result.error) {
|
|
126
|
+
return { error: result.error };
|
|
127
|
+
}
|
|
128
|
+
decisions.push(result.decision);
|
|
129
|
+
}
|
|
130
|
+
const missing = candidates.find((candidate) => !seen.has(candidate.id));
|
|
131
|
+
if (missing) {
|
|
132
|
+
return { error: `is missing the proposal decision for ${missing.id}` };
|
|
133
|
+
}
|
|
134
|
+
return { decisions };
|
|
135
|
+
}
|
|
136
|
+
function parseProposalDecision(candidate, item) {
|
|
137
|
+
if (candidate.operation === "remove") {
|
|
138
|
+
const match = /^Source:[ \t]*(\S.*)[ \t]*\nOperation:[ \t]*remove[ \t]*\nTarget:[ \t]*(shared|project-manager|architect|coder|tester|reviewer|harness-engineer)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nDecision:[ \t]*(remove|retain)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nEvidence checked:[ \t]*(\S.*)[ \t]*$/.exec(item);
|
|
139
|
+
if (!match) {
|
|
140
|
+
return {
|
|
141
|
+
error: `Proposal Decision ${candidate.id} must contain one-line Source, Operation, Target, Existing, Decision, Reason, and Evidence checked fields in that order`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (match[1] !== candidate.source
|
|
145
|
+
|| match[2] !== candidate.target
|
|
146
|
+
|| match[3] !== candidate.existing) {
|
|
147
|
+
return { error: `Proposal Decision ${candidate.id} does not match its source proposal` };
|
|
148
|
+
}
|
|
149
|
+
if (isUnresolvedReviewText(match[5]) || isUnresolvedReviewText(match[6])) {
|
|
150
|
+
return { error: `Proposal Decision ${candidate.id} must replace every review placeholder with verified reasoning and evidence` };
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
decision: {
|
|
154
|
+
candidateId: candidate.id,
|
|
155
|
+
source: match[1],
|
|
156
|
+
operation: "remove",
|
|
157
|
+
target: match[2],
|
|
158
|
+
existing: match[3],
|
|
159
|
+
decision: match[4],
|
|
160
|
+
reason: match[5],
|
|
161
|
+
evidenceChecked: match[6]
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const match = /^Source:[ \t]*(\S.*)[ \t]*\nOperation:[ \t]*(add|update)[ \t]*\nTarget:[ \t]*(shared|project-manager|architect|coder|tester|reviewer|harness-engineer)[ \t]*\nCandidate:[ \t]*(\S.*)[ \t]*\nDecision:[ \t]*(keep-in-memory|keep-memory-reference|move-to-durable-doc|reject)[ \t]*\nFinal target:[ \t]*(shared|project-manager|architect|coder|tester|reviewer|harness-engineer|none)[ \t]*\nWhy memory is necessary:[ \t]*(\S.*)[ \t]*\nImpact if absent:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc analysis:[ \t]*(\S.*)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence checked:[ \t]*(\S.*)[ \t]*\nFinal content:[ \t]*(\S.*)[ \t]*$/.exec(item);
|
|
166
|
+
if (!match) {
|
|
167
|
+
return {
|
|
168
|
+
error: `Proposal Decision ${candidate.id} must contain one-line Source, Operation, Target, Candidate, Decision, Final target, Why memory is necessary, Impact if absent, Durable doc disposition, Durable doc analysis, Durable doc path, Evidence checked, and Final content fields in that order`
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (match[1] !== candidate.source
|
|
172
|
+
|| match[2] !== candidate.operation
|
|
173
|
+
|| match[3] !== candidate.target
|
|
174
|
+
|| match[4] !== candidate.content) {
|
|
175
|
+
return { error: `Proposal Decision ${candidate.id} does not match its source proposal` };
|
|
176
|
+
}
|
|
177
|
+
if ([match[7], match[8], match[10], match[12]].some(isUnresolvedReviewText)) {
|
|
178
|
+
return { error: `Proposal Decision ${candidate.id} must replace every review placeholder with independent analysis and verified evidence` };
|
|
179
|
+
}
|
|
180
|
+
const decision = match[5];
|
|
181
|
+
const finalTarget = match[6];
|
|
182
|
+
const durableDocDisposition = match[9];
|
|
183
|
+
const durableDocPath = match[11];
|
|
184
|
+
const finalContent = match[13];
|
|
185
|
+
const keepsMemory = decision === "keep-in-memory" || decision === "keep-memory-reference";
|
|
186
|
+
if (keepsMemory && (finalTarget === "none" || finalContent === "none")) {
|
|
187
|
+
return { error: `Proposal Decision ${candidate.id} must provide Final target and Final content when keeping memory` };
|
|
188
|
+
}
|
|
189
|
+
if (!keepsMemory && (finalTarget !== "none" || finalContent !== "none")) {
|
|
190
|
+
return { error: `Proposal Decision ${candidate.id} must use Final target: none and Final content: none when not keeping memory` };
|
|
191
|
+
}
|
|
192
|
+
if ((decision === "keep-in-memory" && durableDocDisposition !== "memory")
|
|
193
|
+
|| (decision === "keep-memory-reference" && durableDocDisposition !== "memory-reference")
|
|
194
|
+
|| (decision === "move-to-durable-doc" && durableDocDisposition !== "durable-doc")) {
|
|
195
|
+
return { error: `Proposal Decision ${candidate.id} has inconsistent Decision and Durable doc disposition values` };
|
|
196
|
+
}
|
|
197
|
+
if ((durableDocDisposition === "memory" && durableDocPath !== "none")
|
|
198
|
+
|| (durableDocDisposition !== "memory" && durableDocPath === "none")) {
|
|
199
|
+
return { error: `Proposal Decision ${candidate.id} must use Durable doc path: none only with Durable doc disposition: memory` };
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
decision: {
|
|
203
|
+
candidateId: candidate.id,
|
|
204
|
+
source: match[1],
|
|
205
|
+
operation: match[2],
|
|
206
|
+
target: match[3],
|
|
207
|
+
candidate: match[4],
|
|
208
|
+
decision,
|
|
209
|
+
finalTarget,
|
|
210
|
+
whyMemoryIsNecessary: match[7],
|
|
211
|
+
impactIfAbsent: match[8],
|
|
212
|
+
durableDocDisposition,
|
|
213
|
+
durableDocAnalysis: match[10],
|
|
214
|
+
durableDocPath,
|
|
215
|
+
evidenceChecked: match[12],
|
|
216
|
+
finalContent
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
45
220
|
function validateExistingMemoryDecisions(content, hasExistingMemory) {
|
|
46
221
|
const body = content.trim();
|
|
47
222
|
if (body === "none") {
|
|
@@ -92,3 +267,20 @@ function extractReportSubsection(content, heading, nextHeading) {
|
|
|
92
267
|
function escapeRegExp(value) {
|
|
93
268
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
94
269
|
}
|
|
270
|
+
function memorySetContains(memory, value) {
|
|
271
|
+
return Object.values(memory).some((content) => memoryLines(content).has(value));
|
|
272
|
+
}
|
|
273
|
+
function memoryLines(content) {
|
|
274
|
+
return new Set(content
|
|
275
|
+
.split(/\r?\n/)
|
|
276
|
+
.map((line) => line.trim())
|
|
277
|
+
.filter(Boolean));
|
|
278
|
+
}
|
|
279
|
+
function isUnresolvedReviewText(value) {
|
|
280
|
+
const normalized = value.trim().toLowerCase();
|
|
281
|
+
return (/[<>]/.test(value)
|
|
282
|
+
|| normalized === "none"
|
|
283
|
+
|| normalized === "tbd"
|
|
284
|
+
|| normalized === "unknown"
|
|
285
|
+
|| normalized === "n/a");
|
|
286
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_PLAN_RESULTS, DOCS_SYNC_DECISIONS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, STRICT_NONE_VALUE, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
|
|
1
|
+
import { ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_PLAN_RESULTS, DOCS_SYNC_DECISIONS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
|
|
2
2
|
export function renderArchitectureBriefTemplate(taskSlug) {
|
|
3
3
|
return `# Architecture Brief: ${taskSlug}
|
|
4
4
|
|
|
@@ -201,6 +201,26 @@ TBD
|
|
|
201
201
|
|
|
202
202
|
TBD
|
|
203
203
|
|
|
204
|
+
## Test Infrastructure
|
|
205
|
+
|
|
206
|
+
Status: ${renderArtifactOptions(TEST_INFRASTRUCTURE_STATUSES)}
|
|
207
|
+
|
|
208
|
+
### Affected Files
|
|
209
|
+
|
|
210
|
+
${STRICT_NONE_VALUE}
|
|
211
|
+
|
|
212
|
+
### Boundary Evidence
|
|
213
|
+
|
|
214
|
+
${STRICT_NONE_VALUE}
|
|
215
|
+
|
|
216
|
+
### Defect-Class Sweep
|
|
217
|
+
|
|
218
|
+
${STRICT_NONE_VALUE}
|
|
219
|
+
|
|
220
|
+
### Repair Commit
|
|
221
|
+
|
|
222
|
+
${STRICT_NONE_VALUE}
|
|
223
|
+
|
|
204
224
|
## Failed Expectations
|
|
205
225
|
|
|
206
226
|
${STRICT_NONE_VALUE}
|
|
@@ -185,6 +185,14 @@ when they are relevant to the changed behavior. Check that tests were not
|
|
|
185
185
|
weakened, over-mocked, tied only to fixture values or implementation details,
|
|
186
186
|
or made green by bypassing the real behavior path.
|
|
187
187
|
|
|
188
|
+
Inspect the \`Test Infrastructure\` section of \`test-report.md\`. A report with
|
|
189
|
+
\`repair-required\` or \`production-change-required\` is not gate-ready. When
|
|
190
|
+
status is \`repaired\`, verify the affected files remain Tester-owned, the
|
|
191
|
+
boundary evidence excludes production or shared changes, the defect-class sweep
|
|
192
|
+
covers the affected test-infrastructure family, the repair commit exists in the
|
|
193
|
+
current range, and required clean-state validation was rerun. Request changes
|
|
194
|
+
for missing, contradictory, incomplete, weakened, or unverified repair evidence.
|
|
195
|
+
|
|
188
196
|
Do not approve only because \`Test Result: pass\` or all recorded commands are
|
|
189
197
|
green. Request changes when the report is incomplete or inconsistent with the
|
|
190
198
|
actual tests, validation level does not match risk, an important behavior has
|
|
@@ -239,6 +247,13 @@ scaffold, and coder completion evidence. Verify that the complete planned
|
|
|
239
247
|
behavior is implemented without changing architect-owned boundaries or
|
|
240
248
|
contracts.
|
|
241
249
|
|
|
250
|
+
When the range contains Tester-authored changes recorded in \`test-report.md\`,
|
|
251
|
+
review those tests, fixtures, test-only helpers, and \`docs/TESTING.md\` against
|
|
252
|
+
the Tester role, the repair or coverage evidence, and
|
|
253
|
+
\`docs/CODING_STANDARDS.md\`. Do not reject a valid Tester-owned change merely
|
|
254
|
+
because it is not a Coder scaffold item. Verify that Tester changes remain
|
|
255
|
+
test-only, preserve real behavior paths, and are committed and validated.
|
|
256
|
+
|
|
242
257
|
For \`architect-debug\`, compare the commits with the current Architect route
|
|
243
258
|
command and \`.ai/vcm/handoffs/architect-debug.md\`. Verify that the confirmed
|
|
244
259
|
root cause is supported by the code, the implementation fixes that cause rather
|
|
@@ -319,6 +334,7 @@ Use this findings structure:
|
|
|
319
334
|
- Boundary And Failure Coverage:
|
|
320
335
|
- Public Contract Coverage:
|
|
321
336
|
- Test Integrity:
|
|
337
|
+
- Test Infrastructure:
|
|
322
338
|
- Skips And Gaps:
|
|
323
339
|
- User Approval And Gap Disposition:
|
|
324
340
|
- Validation Readiness:
|
|
@@ -344,6 +360,8 @@ Use this findings structure:
|
|
|
344
360
|
<!-- File and Line Or Symbol are required for code-diff findings. -->
|
|
345
361
|
- File:
|
|
346
362
|
- Line Or Symbol:
|
|
363
|
+
<!-- Finding Scope is required for code-diff findings. Use test-only only when correction needs no production, runtime, public-contract, dependency, generated-context, architecture, or shared-production change. -->
|
|
364
|
+
- Finding Scope: test-only|implementation
|
|
347
365
|
- Evidence:
|
|
348
366
|
- Expected:
|
|
349
367
|
- Gap:
|
|
@@ -382,6 +400,7 @@ If there are no findings, write:
|
|
|
382
400
|
- Boundary And Failure Coverage:
|
|
383
401
|
- Public Contract Coverage:
|
|
384
402
|
- Test Integrity:
|
|
403
|
+
- Test Infrastructure:
|
|
385
404
|
- Skips And Gaps:
|
|
386
405
|
- User Approval And Gap Disposition:
|
|
387
406
|
- Validation Readiness:
|
|
@@ -77,14 +77,17 @@ You are not part of the task workflow round state.
|
|
|
77
77
|
even when every proposal says \`no-change\`.
|
|
78
78
|
- After reviewing existing memory, verify every role proposal against task
|
|
79
79
|
evidence, including any Architect planning-session candidate assigned by VCM.
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
Review every proposal item separately; never accept or reject an entire role
|
|
81
|
+
draft as one decision. For every Add or Update candidate, independently state
|
|
82
|
+
why the memory is necessary, what fails if it is absent, and why memory or a
|
|
83
|
+
durable document is the correct destination. Do not copy the proposer rationale
|
|
84
|
+
as the review. Treat every candidate as a proposal rather than authority, merge
|
|
82
85
|
duplicates, remove stale entries, and keep role-specific knowledge in the
|
|
83
86
|
matching role memory output.
|
|
84
87
|
- Do not keep the full content in memory when a durable document is the correct
|
|
85
88
|
source. Use a short memory reference only when the role needs that document
|
|
86
89
|
pointer across tasks.
|
|
87
|
-
- Record every proposal
|
|
90
|
+
- Record every proposal decision and the retained, updated, and removed
|
|
88
91
|
existing-memory decisions and summary in the exact Memory Review report block
|
|
89
92
|
assigned by VCM.
|
|
90
93
|
- Do not record task narrative, temporary state, unverified conclusions, or
|
|
@@ -93,9 +93,11 @@ PM may leave this path only through the allowed branches below.
|
|
|
93
93
|
- **Coder Continuation:** If Coder returns \`Decision: incomplete\`, lacks the required completion artifact, or has not completed implementation and L0/L1 validation, route Coder again — this is the only route for an in-progress sweep. Problems recorded inside an incomplete report are sweep state, not routable failures; PM routes problems onward only from a post-sweep \`failed\` report carrying the consolidated per-item disposition.
|
|
94
94
|
- **Coder Failure Debug:** If Coder returns \`Decision: failed\` with compile, typecheck, or L0/L1 failure evidence after implementation, suspend the main flow and enter Architect Debug Branch.
|
|
95
95
|
- **Tester Continuation:** If Tester returns \`Test Result: incomplete\`, route Tester again to continue the recorded remaining validation. Do not enter Debug, Diagnosis, or validation-adequacy Gate Review.
|
|
96
|
-
- **Tester
|
|
97
|
-
- **
|
|
98
|
-
- **
|
|
96
|
+
- **Tester Test-Infrastructure Repair:** If Tester returns \`Test Result: fail\` with \`Test Infrastructure Status: repair-required\`, route Tester to repair and commit the confined test-infrastructure defect, rerun required validation, and replace \`test-report.md\`. Do not enter Architect Debug while this Tester-owned repair remains available.
|
|
97
|
+
- **Tester Failure:** If Tester returns \`Test Result: fail\` with \`Test Infrastructure Status: none\`, \`repaired\`, or \`production-change-required\`, suspend the main flow and enter Architect Debug Branch.
|
|
98
|
+
- **Validation Revision:** If the validation-adequacy Gate returns \`request_changes\`, route the complete report to Tester. Tester must correct the tests or evidence, rerun required validation, commit tracked Tester-owned changes, and replace \`test-report.md\` before PM reruns the Gate. If corrected validation returns \`fail\`, apply Tester Test-Infrastructure Repair or Tester Failure from that result.
|
|
99
|
+
- **Tester Code-Diff Correction:** If every code-diff finding has \`Finding Scope: test-only\`, route the complete report to Tester. After correction, repeat Tester validation, validation-adequacy Gate, and \`code-diff --source coder\`.
|
|
100
|
+
- **Code-Diff Correction:** If any code-diff finding has \`Finding Scope: implementation\`, suspend the main flow and enter Architect Debug Branch with the complete Gate report.
|
|
99
101
|
- **Docs Sync Correction:** \`Decision: synced\` or \`unchanged\` continues to Final Acceptance. \`Decision: blocked\` remains at docs sync unless the report identifies an allowed Debug, Diagnosis, or user-decision branch.
|
|
100
102
|
- **Final Acceptance Follow-Up:** Route \`needs-coder-follow-up\` to Coder, \`needs-architect-follow-up\` to Architect, \`needs-docs-sync\` to Architect docs sync, and \`blocked-by-user-decision\` to the user. After follow-up work, resume from the earliest affected Code-Change Flow step and repeat every downstream Gate.
|
|
101
103
|
- **User Decision:** Pause only when the flow requires user intent, external authorization, or an exact user-approved exception. Resume from the suspended step after the user's decision is recorded.
|
|
@@ -116,7 +118,7 @@ The flow completes only when Final Acceptance returns:
|
|
|
116
118
|
- Route user-originated or flow-required architecture, scope, contract, dependency, public surface, durable docs, and implementation-plan questions to Architect.
|
|
117
119
|
- Do not treat Coder architecture doubts, design concerns, scaffold objections, or validation predictions as architecture questions.
|
|
118
120
|
- Route validation strategy, test coverage, test-report, and validation adequacy questions to Tester.
|
|
119
|
-
- Route bugs, build/runtime errors, and
|
|
121
|
+
- Route bugs, build/runtime errors, and production-implementation validation failures from a code-delivery flow to Architect Debug Mode according to the active flow. Route a confined \`repair-required\` test-infrastructure failure back to Tester. Do not route a Validation-Only Flow \`Test Result: fail\` to Debug unless the accepted outcome requires implementation repair.
|
|
120
122
|
- Ask the user only when user intent, priority, approval, external authorization, secrets, real cost, production permission, sensitive data access, or durable-doc conflict requires user decision.
|
|
121
123
|
- Non-PM role results, blockers, findings, and requests must come back to PM. PM decides the next route.
|
|
122
124
|
- Only PM decides the next VCM route, gate, pause, retry, final acceptance, or PR-Preparation Flow step. Non-PM role messages are evidence and status only; any requested next action from a non-PM role is advisory and must be reclassified by PM against the active flow, required artifacts, gate state, and PM routing rules.
|
|
@@ -134,6 +136,7 @@ Every branch must end in exactly one of these outcomes:
|
|
|
134
136
|
|
|
135
137
|
- return to the recorded main-flow resume point
|
|
136
138
|
- repeat the current responsible role
|
|
139
|
+
- route to Tester for an explicitly allowed test-only repair or correction
|
|
137
140
|
- route to Architect Debug Mode
|
|
138
141
|
- route to Architecture Diagnosis Mode
|
|
139
142
|
- pause for user decision
|
|
@@ -152,9 +155,11 @@ The shared path is:
|
|
|
152
155
|
|
|
153
156
|
- **Normal Plan Required:** If Architect returns \`normal architecture plan required\`, enter Code-Change Flow at Architect planning. When Debug is a branch of Code-Change Flow, resume that parent flow at Architect planning.
|
|
154
157
|
- **Tester Continuation:** If Tester returns \`Test Result: incomplete\`, route Tester again to continue the recorded remaining validation.
|
|
155
|
-
- **
|
|
156
|
-
- **
|
|
157
|
-
- **
|
|
158
|
+
- **Tester Test-Infrastructure Repair:** If Tester returns \`Test Result: fail\` with \`Test Infrastructure Status: repair-required\`, route Tester to repair and commit the confined test-infrastructure defect, rerun required validation, and replace \`test-report.md\`.
|
|
159
|
+
- **Architecture Diagnosis:** If Tester returns \`Test Result: fail\` with \`Test Infrastructure Status: none\`, \`repaired\`, or \`production-change-required\`, enter Architecture Diagnosis Branch.
|
|
160
|
+
- **Validation Revision:** If the validation-adequacy Gate returns \`request_changes\`, route the complete report to Tester. After Tester corrects tests or evidence, reruns validation, commits tracked Tester-owned changes, and replaces \`test-report.md\`, rerun the Gate.
|
|
161
|
+
- **Tester Code-Diff Correction:** If every code-diff finding has \`Finding Scope: test-only\`, route the complete report to Tester. After correction, repeat Tester validation, validation-adequacy Gate, and \`code-diff --source architect-debug\`.
|
|
162
|
+
- **Code-Diff Revision:** If any code-diff finding has \`Finding Scope: implementation\`, route the complete report to Architect Debug Mode. After correction, repeat Tester validation, validation-adequacy Gate, and \`code-diff --source architect-debug\`.
|
|
158
163
|
|
|
159
164
|
#### Successful Exit
|
|
160
165
|
|
|
@@ -183,9 +188,11 @@ Architecture Diagnosis Mode must run before another Debug Mode fix or Coder disp
|
|
|
183
188
|
#### Allowed Branches
|
|
184
189
|
|
|
185
190
|
- **Tester Continuation:** If Tester returns \`Test Result: incomplete\`, route Tester again to continue the recorded remaining validation.
|
|
186
|
-
- **Tester
|
|
187
|
-
- **
|
|
188
|
-
- **
|
|
191
|
+
- **Tester Test-Infrastructure Repair:** If Tester returns \`Test Result: fail\` with \`Test Infrastructure Status: repair-required\`, route Tester to repair and commit the confined test-infrastructure defect, rerun required validation, and replace \`test-report.md\`.
|
|
192
|
+
- **Tester Failure:** If Tester returns \`Test Result: fail\` with \`Test Infrastructure Status: none\`, \`repaired\`, or \`production-change-required\`, pause and report to the user. If required validation remains unavailable, ask whether the user explicitly approves retaining that exact Coverage Gap.
|
|
193
|
+
- **Validation Revision:** If the validation-adequacy Gate returns \`request_changes\`, route the complete report to Tester. After Tester corrects tests or evidence, reruns validation, commits tracked Tester-owned changes, and replaces \`test-report.md\`, rerun the Gate.
|
|
194
|
+
- **Tester Code-Diff Correction:** If every code-diff finding has \`Finding Scope: test-only\`, route the complete report to Tester. After correction, repeat Tester validation, validation-adequacy Gate, and \`code-diff --source architect-diagnosis\`.
|
|
195
|
+
- **Code-Diff Revision:** If any code-diff finding has \`Finding Scope: implementation\`, route the complete report to Architecture Diagnosis Mode. After correction, repeat Tester validation, validation-adequacy Gate, and \`code-diff --source architect-diagnosis\`.
|
|
189
196
|
|
|
190
197
|
#### Successful Exit
|
|
191
198
|
|
|
@@ -258,7 +265,8 @@ PM may leave this path only through the allowed branches below.
|
|
|
258
265
|
#### Allowed Branches
|
|
259
266
|
|
|
260
267
|
- **Tester Continuation:** If Tester returns \`Test Result: incomplete\`, route Tester again to continue the recorded remaining validation.
|
|
261
|
-
- **
|
|
268
|
+
- **Tester Test-Infrastructure Repair:** If Tester returns \`Test Result: fail\` with \`Test Infrastructure Status: repair-required\`, route Tester to repair and commit the confined defect, rerun required validation, and replace \`test-report.md\`.
|
|
269
|
+
- **Validation Revision:** If the validation-adequacy Gate returns \`request_changes\`, route the complete report to Tester and rerun the Gate after Tester corrects tests or evidence, reruns validation, commits tracked Tester-owned changes, and replaces \`test-report.md\`.
|
|
262
270
|
- **Code Change Required:** If the accepted outcome requires production-code, runtime-behavior, public-contract, dependency, or system-architecture changes, enter Code-Change Flow at Architect planning.
|
|
263
271
|
- **User Decision:** If validation requires missing user intent, credentials, environment access, sensitive data, real cost, or external authorization, pause and ask the user.
|
|
264
272
|
|
|
@@ -335,11 +343,11 @@ PM may lightly rewrite the user's words to:
|
|
|
335
343
|
|
|
336
344
|
- Gate Review requests are mandatory and unconditional. At every trigger point, use the \`vcm-gate-review\` skill to run \`.ai/tools/request-gate-review\` with the matching gate and code source arguments without first judging whether Gate Review is enabled. The tool (via VCM) is the single source of truth for enable state; never skip the run because you assume Gate Review is off or because the worktree has no gate-review index yet.
|
|
337
345
|
- The tool's first output line decides the next step: \`disabled\`, \`not_required\`, or \`already_approved\` continue the normal VCM flow; \`started\` or \`running\` stop the turn and wait for the VCM callback; \`failed_to_start\` is a hard stop — report it to the user and do not silently proceed past the gate.
|
|
338
|
-
- Trigger points (run each unconditionally): after the architecture brief is confirmed and Architect completes planning, before coder dispatch run \`architecture-plan\`; after Tester returns a terminal \`Test Result: pass|fail\` that the active flow permits to reach the gate, run \`validation-adequacy\`; after that validation-adequacy Gate completes successfully, run \`code-diff --source coder\` for Coder implementation, \`code-diff --source architect-debug\` for an Architect Debug fix, or \`code-diff --source architect-diagnosis\` for an Architecture Diagnosis fix. Validation-Only Flow stops after validation-adequacy and does not run code-diff. Never run either post-implementation Gate for \`Test Result: incomplete\`.
|
|
346
|
+
- Trigger points (run each unconditionally): after the architecture brief is confirmed and Architect completes planning, before coder dispatch run \`architecture-plan\`; after Tester returns a terminal \`Test Result: pass|fail\` that the active flow permits to reach the gate, run \`validation-adequacy\`; after that validation-adequacy Gate completes successfully, run \`code-diff --source coder\` for Coder implementation, \`code-diff --source architect-debug\` for an Architect Debug fix, or \`code-diff --source architect-diagnosis\` for an Architecture Diagnosis fix. A test report with \`Test Infrastructure Status: repair-required\` or \`production-change-required\` does not reach a Gate; route the matching allowed branch first. Validation-Only Flow stops after validation-adequacy and does not run code-diff. Never run either post-implementation Gate for \`Test Result: incomplete\`.
|
|
339
347
|
- PM does not inspect commits or decide whether code changes exist. At a \`code-diff\` trigger point, run the tool; the tool decides \`disabled\`, \`not_required\`, \`already_approved\`, or starts review.
|
|
340
348
|
- Do not run \`code-diff\` before Tester completes, while validation-adequacy is unresolved, or for incomplete, unresolved failed, planning-only, Docs-Only Flow, Validation-Only Flow, PR-Preparation Flow, or Communication-Only Flow. A terminal \`fail\` with the exact required user-approved testing gap may proceed only through the recorded validation-adequacy disposition.
|
|
341
349
|
- Gate Review trigger points apply only when the active delivery flow reaches that milestone. Do not run Gate Review for Communication-Only Flow.
|
|
342
|
-
- On a callback, accept only \`approve\` or \`request_changes\`. Apply \`request_changes\` through the allowed branch defined by the active flow;
|
|
350
|
+
- On a callback, accept only \`approve\` or \`request_changes\`. Apply \`request_changes\` through the allowed branch defined by the active flow; for code-diff, use Tester Code-Diff Correction only when every finding is \`test-only\`, otherwise use the flow's Architect correction branch.
|
|
343
351
|
- Do not ask Reviewer to choose owners, fixes, Replan, or user-intervention needs.
|
|
344
352
|
- Record gate decision, report path, and any skip or override reason.
|
|
345
353
|
|
|
@@ -9,7 +9,7 @@ ${renderRoleMemoryRules("tester")}
|
|
|
9
9
|
|
|
10
10
|
- Own independent validation, tester-owned test design, test implementation, test adequacy, \`docs/TESTING.md\`, and final validation confidence.
|
|
11
11
|
- Read production code only to understand public behavior, test seams, fixtures, and coverage gaps.
|
|
12
|
-
- Do not edit production code
|
|
12
|
+
- Do not edit production code or decide architecture. Diagnose and repair only PM-routed defects confined to Tester-owned tests, fixtures, test-only helpers, and \`docs/TESTING.md\`; otherwise report validation evidence without proposing a fix.
|
|
13
13
|
|
|
14
14
|
### Inputs
|
|
15
15
|
|
|
@@ -40,7 +40,7 @@ ${renderRoleMemoryRules("tester")}
|
|
|
40
40
|
- If project-manager asks for clarification, clarify only the validation evidence, expected behavior, affected path, or coverage gap.
|
|
41
41
|
- If validation fails or expected behavior is unclear, report the evidence to project-manager; architect owns diagnosis, and project-manager decides the next route.
|
|
42
42
|
- After Architect Debug or Architecture Diagnosis changes, rerun the required validation independently. Architect validation is implementation evidence and does not replace Tester final validation.
|
|
43
|
-
- Add or modify tests, test fixtures, or test-only helpers needed for validation
|
|
43
|
+
- Add or modify tests, test fixtures, or test-only helpers needed for correct, reliable validation and approved behavior coverage.
|
|
44
44
|
- Tester changes to tests, fixtures, and test-only helpers must follow \`docs/CODING_STANDARDS.md\` and prove the approved behavior contract.
|
|
45
45
|
- Do not edit production code, public contracts, runtime wiring, generated context, or shared production helpers while adding validation coverage.
|
|
46
46
|
- Do not weaken assertions, reshape fixtures to match the current implementation, bypass real behavior paths, skip tests, or add test-only shortcuts.
|
|
@@ -57,6 +57,16 @@ ${renderRoleMemoryRules("tester")}
|
|
|
57
57
|
- A required check that fails, is skipped, or cannot be completed by Tester continuation is a blocking validation issue and requires \`Test Result: fail\`.
|
|
58
58
|
- Update \`docs/TESTING.md\` when validation strategy, commands, level mapping, integration/E2E case definitions, selection rules, final-validation cleanup, test gaps, or test expectations change.
|
|
59
59
|
|
|
60
|
+
### Test-Infrastructure Repair
|
|
61
|
+
|
|
62
|
+
- Use this repair path only when project-manager routes a reported test-infrastructure defect back to Tester.
|
|
63
|
+
- The repair must remain confined to tests, fixtures, test-only helpers, or \`docs/TESTING.md\`. It must not change production code, runtime behavior, public contracts, dependencies, generated context, system architecture, or shared production helpers.
|
|
64
|
+
- Confirm the defect mechanism from current files and reproducible evidence. In the affected test-infrastructure family, inspect every occurrence of the same mechanism and repair every confirmed instance.
|
|
65
|
+
- Do not replace a repair with a workaround that bypasses the defective path, weakens assertions, skips validation, or hides the failure.
|
|
66
|
+
- After repair, perform the required clean-state validation again and replace \`test-report.md\` with current results.
|
|
67
|
+
- If the repair requires any prohibited production or shared scope, do not make that change. Record \`Test Infrastructure Status: production-change-required\` and the concrete boundary evidence for project-manager.
|
|
68
|
+
- A current validation path with an unresolved test-infrastructure defect cannot return \`Test Result: pass\`.
|
|
69
|
+
|
|
60
70
|
### Mandatory L3 End-To-End Coverage
|
|
61
71
|
|
|
62
72
|
L3 validates a complete externally observable flow from a project-defined
|
|
@@ -126,7 +136,27 @@ Coverage Gap.
|
|
|
126
136
|
|
|
127
137
|
### Outputs
|
|
128
138
|
|
|
129
|
-
- Write \`.ai/vcm/handoffs/test-report.md\` with \`Test Result: pass|fail|incomplete\`, evidence reviewed, tests added or updated, coverage mapping, validation progress, commands run or checked, validation results, failed expectations, reproduction steps, skipped checks with reasons, coverage gaps, blocking validation issues, and user approval evidence.
|
|
139
|
+
- Write \`.ai/vcm/handoffs/test-report.md\` with \`Test Result: pass|fail|incomplete\`, evidence reviewed, tests added or updated, coverage mapping, validation progress, commands run or checked, validation results, test-infrastructure status and evidence, failed expectations, reproduction steps, skipped checks with reasons, coverage gaps, blocking validation issues, and user approval evidence.
|
|
140
|
+
- \`test-report.md\` must include this test-infrastructure section:
|
|
141
|
+
|
|
142
|
+
\`\`\`md
|
|
143
|
+
## Test Infrastructure
|
|
144
|
+
|
|
145
|
+
Status: none|repair-required|repaired|production-change-required
|
|
146
|
+
|
|
147
|
+
### Affected Files
|
|
148
|
+
|
|
149
|
+
### Boundary Evidence
|
|
150
|
+
|
|
151
|
+
### Defect-Class Sweep
|
|
152
|
+
|
|
153
|
+
### Repair Commit
|
|
154
|
+
\`\`\`
|
|
155
|
+
|
|
156
|
+
- Use \`none\` when no test-infrastructure defect was found. Every subsection must then be exactly \`None.\`.
|
|
157
|
+
- Use \`repair-required\` only for a confirmed defect confined to Tester-owned scope that project-manager must route back to Tester. Record affected files, boundary evidence, and the completed defect-class sweep; set Repair Commit to exactly \`None.\` and return \`Test Result: fail\`.
|
|
158
|
+
- Use \`repaired\` after the PM-routed repair is committed and required validation is rerun. Record affected files, boundary evidence, defect-class sweep, and the repair commit.
|
|
159
|
+
- Use \`production-change-required\` when repair requires production or shared scope. Record affected files, boundary evidence, and the completed defect-class sweep; set Repair Commit to exactly \`None.\` and return \`Test Result: fail\`.
|
|
130
160
|
- \`test-report.md\` must include this L3 section:
|
|
131
161
|
|
|
132
162
|
\`\`\`md
|
|
@@ -148,13 +178,14 @@ L3 Required: yes|no
|
|
|
148
178
|
|
|
149
179
|
- When \`L3 Required: yes\`, include at least one complete flow-to-case mapping. \`Action\` must be \`run-existing\`, \`updated\`, or \`added\`.
|
|
150
180
|
- When \`L3 Required: no\`, use \`Not-Required Evidence\` to prove every condition in the L3 not-required rule.
|
|
151
|
-
- In
|
|
181
|
+
- In every flow, if tests, fixtures, test-only helpers, or \`docs/TESTING.md\` changed, commit those changes before reporting a terminal result and record the changed files and commit in \`test-report.md\`. If no tracked files changed, record that no commit was required.
|
|
152
182
|
- \`test-report.md\` is the current validation evidence, not a log; when rewriting it, carry forward still-unresolved findings or explicitly mark them resolved instead of dropping them.
|
|
153
183
|
- In \`Coverage Mapping\`, map each accepted changed behavior or relevant risk to its validation level, actual test file and case or external evidence, exercised entry path and key assertions, result, and any remaining gap.
|
|
154
184
|
- In \`Validation Progress\`, record \`Completed Validation\` and \`Remaining Validation\`. A final \`pass\` report must set remaining validation to \`None\`.
|
|
155
185
|
- Use \`pass\` only when required validation completed and no blocking test failure, missing required coverage, unacceptable test weakness, or unresolved validation risk remains.
|
|
156
186
|
- Use \`fail\` only when tests fail, coverage is insufficient and Tester continuation cannot resolve it, required validation is blocked from completion, test quality is unacceptable, or validation risk needs project-manager routing.
|
|
157
187
|
- Use \`incomplete\` only when required validation remains, no blocking issue has been found, and another Tester turn can continue the recorded remaining work.
|
|
188
|
+
- \`Test Infrastructure Status: repair-required\` or \`production-change-required\` requires \`Test Result: fail\`; neither status may appear in a \`pass\` or \`incomplete\` report.
|
|
158
189
|
- When \`Test Result: pass\`, the entire body of \`Remaining Validation\`, \`Failed Expectations\`, \`Coverage Gaps\`, \`Blocking Validation Issues\`, and \`User Approval Evidence\` must be exactly \`None.\` with no additional text.
|
|
159
190
|
- When \`Test Result: incomplete\`, \`Completed Validation\` and \`Remaining Validation\` must both contain concrete progress, while the entire body of \`Failed Expectations\`, \`Coverage Gaps\`, \`Blocking Validation Issues\`, and \`User Approval Evidence\` must be exactly \`None.\` with no additional text.
|
|
160
191
|
- When \`Test Result: fail\`, \`Blocking Validation Issues\` must list concrete blocking evidence.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_PLAN_RESULTS, DOCS_SYNC_DECISIONS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, STRICT_NONE_VALUE, TEST_RESULTS } from "./artifact-contract.js";
|
|
1
|
+
import { ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_PLAN_RESULTS, DOCS_SYNC_DECISIONS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS } from "./artifact-contract.js";
|
|
2
2
|
const REQUIRED_HEADINGS = {
|
|
3
3
|
"architecture-brief": [
|
|
4
4
|
"Accepted Outcome",
|
|
@@ -50,6 +50,11 @@ const REQUIRED_HEADINGS = {
|
|
|
50
50
|
"Not-Required Evidence",
|
|
51
51
|
"Commands Run Or Checked",
|
|
52
52
|
"Validation Results",
|
|
53
|
+
"Test Infrastructure",
|
|
54
|
+
"Affected Files",
|
|
55
|
+
"Boundary Evidence",
|
|
56
|
+
"Defect-Class Sweep",
|
|
57
|
+
"Repair Commit",
|
|
53
58
|
"Failed Expectations",
|
|
54
59
|
"Reproduction Steps",
|
|
55
60
|
"Skipped Checks With Reasons",
|
|
@@ -152,6 +157,55 @@ function validateArtifactFields(kind, content) {
|
|
|
152
157
|
const invalidFields = isAllowedValue(result, TEST_RESULTS)
|
|
153
158
|
? []
|
|
154
159
|
: [renderExactFieldError("Test Result", TEST_RESULTS, result)];
|
|
160
|
+
const infrastructureStatus = readInlineField(readArtifactSectionContent(content, "Test Infrastructure") ?? "", "Status");
|
|
161
|
+
if (!isAllowedValue(infrastructureStatus, TEST_INFRASTRUCTURE_STATUSES)) {
|
|
162
|
+
invalidFields.push(renderExactFieldError("Test Infrastructure Status", TEST_INFRASTRUCTURE_STATUSES, infrastructureStatus));
|
|
163
|
+
}
|
|
164
|
+
const infrastructureFiles = readArtifactSectionContent(content, "Affected Files");
|
|
165
|
+
const infrastructureBoundary = readArtifactSectionContent(content, "Boundary Evidence");
|
|
166
|
+
const infrastructureSweep = readArtifactSectionContent(content, "Defect-Class Sweep");
|
|
167
|
+
const infrastructureCommit = readArtifactSectionContent(content, "Repair Commit");
|
|
168
|
+
if (infrastructureStatus === "none") {
|
|
169
|
+
for (const [heading, value] of [
|
|
170
|
+
["Affected Files", infrastructureFiles],
|
|
171
|
+
["Boundary Evidence", infrastructureBoundary],
|
|
172
|
+
["Defect-Class Sweep", infrastructureSweep],
|
|
173
|
+
["Repair Commit", infrastructureCommit]
|
|
174
|
+
]) {
|
|
175
|
+
if (!isExactNone(value)) {
|
|
176
|
+
invalidFields.push(renderExactSectionError(heading, STRICT_NONE_VALUE, value, "when Test Infrastructure Status is none"));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (infrastructureStatus === "repair-required" || infrastructureStatus === "production-change-required") {
|
|
181
|
+
for (const [heading, value] of [
|
|
182
|
+
["Affected Files", infrastructureFiles],
|
|
183
|
+
["Boundary Evidence", infrastructureBoundary],
|
|
184
|
+
["Defect-Class Sweep", infrastructureSweep]
|
|
185
|
+
]) {
|
|
186
|
+
if (!hasSubstantiveSectionValue(value)) {
|
|
187
|
+
invalidFields.push(`${heading} is required when Test Infrastructure Status is ${infrastructureStatus}.`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!isExactNone(infrastructureCommit)) {
|
|
191
|
+
invalidFields.push(renderExactSectionError("Repair Commit", STRICT_NONE_VALUE, infrastructureCommit, `when Test Infrastructure Status is ${infrastructureStatus}`));
|
|
192
|
+
}
|
|
193
|
+
if (result !== "fail") {
|
|
194
|
+
invalidFields.push(`Test Result must be fail when Test Infrastructure Status is ${infrastructureStatus}.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (infrastructureStatus === "repaired") {
|
|
198
|
+
for (const [heading, value] of [
|
|
199
|
+
["Affected Files", infrastructureFiles],
|
|
200
|
+
["Boundary Evidence", infrastructureBoundary],
|
|
201
|
+
["Defect-Class Sweep", infrastructureSweep],
|
|
202
|
+
["Repair Commit", infrastructureCommit]
|
|
203
|
+
]) {
|
|
204
|
+
if (!hasSubstantiveSectionValue(value)) {
|
|
205
|
+
invalidFields.push(`${heading} is required when Test Infrastructure Status is repaired.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
155
209
|
const l3Required = readInlineField(content, "L3 Required");
|
|
156
210
|
if (!isAllowedValue(l3Required, L3_REQUIRED_VALUES)) {
|
|
157
211
|
invalidFields.push(renderExactFieldError("L3 Required", L3_REQUIRED_VALUES, l3Required));
|
|
@@ -6,6 +6,12 @@ export const ARCHITECTURE_PLAN_RESULTS = [
|
|
|
6
6
|
"user clarification required"
|
|
7
7
|
];
|
|
8
8
|
export const TEST_RESULTS = ["pass", "fail", "incomplete"];
|
|
9
|
+
export const TEST_INFRASTRUCTURE_STATUSES = [
|
|
10
|
+
"none",
|
|
11
|
+
"repair-required",
|
|
12
|
+
"repaired",
|
|
13
|
+
"production-change-required"
|
|
14
|
+
];
|
|
9
15
|
export const L3_REQUIRED_VALUES = ["yes", "no"];
|
|
10
16
|
export const L3_ACTIONS = ["run-existing", "updated", "added"];
|
|
11
17
|
export const DOCS_SYNC_DECISIONS = ["synced", "unchanged", "blocked"];
|