@tea-agent/loop-agent 0.26.1 → 0.26.3
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/CHANGELOG.md +47 -0
- package/dist/application/dag/generate-task-dag.js +33 -0
- package/dist/commands/task-source-prepare.js +6 -0
- package/dist/executors/dag-pi-executor.js +156 -9
- package/dist/executors/shell-executor.js +111 -0
- package/dist/executors/shell-presets.js +12 -4
- package/dist/executors/shell-write-guard.js +145 -12
- package/dist/task/config-types.js +6 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/project.js +8 -0
- package/dist/task/contract/schema.js +1 -0
- package/dist/task/frontend-preflight.js +131 -0
- package/dist/task/runtime.js +2 -4
- package/dist/task/source-prepare/build-draft.js +9 -0
- package/dist/task/source-prepare/completeness.js +1 -1
- package/dist/worker/observability/read-model.js +134 -0
- package/dist/worker/observe/static/state.js +61 -0
- package/dist/worker/observe/static/styles.css +8 -0
- package/dist/worker/observe/static/views/dag-graph.js +107 -31
- package/dist/worker/observe/static/views/dag-inspector.js +374 -157
- package/dist/worker/observe/static/views/dag.js +4 -11
- package/dist/workflows/dag/backend-test-pytest-collection.js +277 -0
- package/dist/workflows/dag/convergence/controller.js +110 -21
- package/dist/workflows/dag/frontend-implementation-contract.js +218 -17
- package/dist/workflows/dag/frontend-repair.js +29 -29
- package/dist/workflows/dag/frontend-review-context.js +7 -1
- package/dist/workflows/dag/frontend-verification-trace.js +26 -5
- package/dist/workflows/dag/frontend-worktree-diff.js +14 -3
- package/dist/workflows/dag/governance-profile.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +115 -39
- package/dist/workflows/dag/output-protocol.js +180 -7
- package/dist/workflows/dag/runner.js +141 -52
- package/dist/workflows/dag/types.js +5 -1
- package/dist/workflows/dag/validate.js +3 -2
- package/docs/templates/backend-test-dag.json +100 -8
- package/package.json +1 -1
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
|
@@ -10,7 +10,7 @@ import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
|
10
10
|
import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
|
|
11
11
|
import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
|
|
12
12
|
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
|
|
13
|
-
import { REVIEW_VERDICT_OUTPUT_PROTOCOL } from "./output-protocol.js";
|
|
13
|
+
import { REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL, REVIEW_VERDICT_OUTPUT_PROTOCOL, } from "./output-protocol.js";
|
|
14
14
|
import { resolveAdapter } from "../../adapters/index.js";
|
|
15
15
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
16
16
|
import { buildAuthoritySurfaceAuditNode, buildAuthoritySurfaceGateNode, resolveAuthoritySurfaceAudit, } from "./authority-surface.js";
|
|
@@ -890,7 +890,9 @@ function chooseFrontendVerifyCommands(input) {
|
|
|
890
890
|
if (input.parsedCommands.length > 0) {
|
|
891
891
|
return { commands: input.parsedCommands, commandSource: "inline" };
|
|
892
892
|
}
|
|
893
|
-
if (input.
|
|
893
|
+
if (input.allowAdapter !== false &&
|
|
894
|
+
input.adapterCommands &&
|
|
895
|
+
input.adapterCommands.length > 0) {
|
|
894
896
|
return { commands: input.adapterCommands, commandSource: "adapter" };
|
|
895
897
|
}
|
|
896
898
|
return { commandSource: "inline" };
|
|
@@ -1252,7 +1254,19 @@ export function extractTaskScopedRequirementIds(requirementMarkdown, ...fallback
|
|
|
1252
1254
|
if (fromSection.length > 0)
|
|
1253
1255
|
return fromSection;
|
|
1254
1256
|
}
|
|
1255
|
-
|
|
1257
|
+
const explicit = extractExplicitRequirementIds(requirementMarkdown, ...fallbackMarkdown);
|
|
1258
|
+
if (explicit.length > 0)
|
|
1259
|
+
return explicit;
|
|
1260
|
+
// Requirements frequently use a numbered acceptance list instead of
|
|
1261
|
+
// writing AC-* identifiers. Give that list a deterministic canonical
|
|
1262
|
+
// namespace so model-generated AC-1 references bind to the same source
|
|
1263
|
+
// facts instead of failing as unknown requirements at the prewrite gate.
|
|
1264
|
+
const acceptanceSection = requirementMarkdown.match(/(?:^|\n)##\s*(?:验收标准|Acceptance Criteria)\s*\n([\s\S]*?)(?=\n##\s+|\n#\s+|$)/i)?.[1] ?? "";
|
|
1265
|
+
const numbered = [...acceptanceSection.matchAll(/(?:^|\n)\s*(\d+)[.、)]\s+/g)]
|
|
1266
|
+
.map((match) => Number(match[1]))
|
|
1267
|
+
.filter((value, index, values) => Number.isFinite(value) && values.indexOf(value) === index)
|
|
1268
|
+
.sort((a, b) => a - b);
|
|
1269
|
+
return numbered.map((value) => `AC-${value}`);
|
|
1256
1270
|
}
|
|
1257
1271
|
function buildDagSourceBinding(sources) {
|
|
1258
1272
|
const sourceEntries = [
|
|
@@ -2146,7 +2160,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2146
2160
|
"Frontend planning must consume the read-only Mock assessment strategy produced after scouting; MOCK_STRATEGY: blocked must not pass the deterministic Mock contract gate.",
|
|
2147
2161
|
"Mock implementations must preserve the real request path as the default, require explicit test/dev activation, and never rely on commenting out the real request.",
|
|
2148
2162
|
"Mock-backed behavior evidence proves only the documented frontend contract, never real API integration.",
|
|
2149
|
-
"frontend-implementation DAGs must complete deterministic static verification
|
|
2163
|
+
"frontend-implementation DAGs must complete deterministic static verification before final review. Behavior verification is also required when the task declares a behavior entrypoint or the implementation contract contains a non-static verification target; static-only contracts must map every target to the declared static entrypoint.",
|
|
2150
2164
|
"frontend review must block closeout unless review verdict is exactly VERDICT: pass.",
|
|
2151
2165
|
`Frontend risk classification: ${frontendRisk.selectedRisk} — ${frontendRisk.reason}`,
|
|
2152
2166
|
frontendRisk.forceFullGates
|
|
@@ -2171,6 +2185,10 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2171
2185
|
...explicitFrontendVerifyCommands.behaviorCommands,
|
|
2172
2186
|
].map(verifyCommandKey));
|
|
2173
2187
|
const adapterVerifyCommands = (sources.verifyCommands?.final ?? []).filter((command) => !explicitCommandKeys.has(verifyCommandKey(command)));
|
|
2188
|
+
const hasDeclaredFrontendVerification = explicitFrontendVerifyCommands.staticCommands.length > 0 ||
|
|
2189
|
+
explicitFrontendVerifyCommands.behaviorCommands.length > 0 ||
|
|
2190
|
+
parsedFrontendVerifyCommands.staticCommands.length > 0 ||
|
|
2191
|
+
parsedFrontendVerifyCommands.behaviorCommands.length > 0;
|
|
2174
2192
|
const staticVerifyCommands = chooseFrontendVerifyCommands({
|
|
2175
2193
|
explicitCommands: explicitFrontendVerifyCommands.staticCommands,
|
|
2176
2194
|
parsedCommands: parsedFrontendVerifyCommands.staticCommands,
|
|
@@ -2185,6 +2203,9 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2185
2203
|
explicitCommands: explicitFrontendVerifyCommands.behaviorCommands,
|
|
2186
2204
|
parsedCommands: parsedFrontendVerifyCommands.behaviorCommands,
|
|
2187
2205
|
adapterCommands: adapterVerifyCommands,
|
|
2206
|
+
// A declared task verifier owns this task's verification boundary. A
|
|
2207
|
+
// static-only task must not inherit unrelated root-level test commands.
|
|
2208
|
+
allowAdapter: !hasDeclaredFrontendVerification,
|
|
2188
2209
|
});
|
|
2189
2210
|
const staticShellCommands = buildVerifyShellCommands({
|
|
2190
2211
|
repoRoot: sources.repoRoot,
|
|
@@ -2199,8 +2220,13 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2199
2220
|
const behaviorShellCommands = buildVerifyShellCommands({
|
|
2200
2221
|
repoRoot: sources.repoRoot,
|
|
2201
2222
|
commands: behaviorVerifyCommands.commands,
|
|
2202
|
-
fallbackCommands:
|
|
2223
|
+
fallbackCommands: hasDeclaredFrontendVerification
|
|
2224
|
+
? []
|
|
2225
|
+
: behaviorFallbackCommands,
|
|
2203
2226
|
});
|
|
2227
|
+
const effectiveBehaviorFallbackCommands = hasDeclaredFrontendVerification
|
|
2228
|
+
? []
|
|
2229
|
+
: behaviorFallbackCommands;
|
|
2204
2230
|
const staticVerifyEvidence = buildVerifyEvidence({
|
|
2205
2231
|
phase: "intermediate",
|
|
2206
2232
|
quota: strategy.intermediateQuota ?? "full",
|
|
@@ -2226,7 +2252,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2226
2252
|
quota: "full",
|
|
2227
2253
|
commandSource: behaviorVerifyCommands.commandSource,
|
|
2228
2254
|
commands: behaviorVerifyCommands.commands,
|
|
2229
|
-
fallbackCommands:
|
|
2255
|
+
fallbackCommands: effectiveBehaviorFallbackCommands,
|
|
2230
2256
|
commandTexts: behaviorShellCommands,
|
|
2231
2257
|
finalFullRequired: true,
|
|
2232
2258
|
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
@@ -2287,10 +2313,11 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2287
2313
|
allowedPaths: readOnlyPaths,
|
|
2288
2314
|
forbiddenPaths,
|
|
2289
2315
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2290
|
-
outputContract: "Markdown scout report covering frontend stack, routes, components, styling system, existing design conventions, state/data flow, test entry points, reuse opportunities, and risks. No file writes.",
|
|
2316
|
+
outputContract: "Markdown scout report with a required TARGET_SURFACE section covering frontend stack, routes, components, styling system, existing design conventions, state/data flow, test entry points, reuse opportunities, and risks. No file writes.",
|
|
2291
2317
|
subtask_prompt: [
|
|
2292
2318
|
"Inspect frontend code, routing, components, styles, package scripts, and tests.",
|
|
2293
2319
|
"Return code and design observations, existing reuse opportunities, and verification entry points.",
|
|
2320
|
+
"Begin with a TARGET_SURFACE section containing exactly these labels: entrypoint, routeOrMount, implementationPaths, testPaths, dataSource, allowedPathConflicts. Use repository-relative POSIX paths. implementationPaths and testPaths must name the existing files/directories that actually own the requested behavior; allowedPathConflicts must list every discovered path not covered by task allowedPaths, or [] when none exists.",
|
|
2294
2321
|
"Derive all file paths from this target workspace. Do not assume the project uses src/, test/, React, or the loop-agent repository layout.",
|
|
2295
2322
|
"Read-only: do not modify repository files.",
|
|
2296
2323
|
sourceContext,
|
|
@@ -2314,6 +2341,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2314
2341
|
"Select the Mock / API strategy inside the plan and structured contract. Carry endpoint/fixture mapping, explicit activation, production-default-off rule, verification commands, and Real Integration Gap into both outputs.",
|
|
2315
2342
|
"Include ordered steps, target files, UI state handling, styling/component strategy, interaction notes, Mock/API strategy, dependency policy, deterministic verification entrypoints, and residual risks. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
|
|
2316
2343
|
"Every target file and verification target must be selected from the current target workspace and task scope. Do not reuse paths or symbols from examples, prior tasks, or loop-agent itself; if the project uses app/, packages/, spec/, __tests__, or another layout, preserve that layout.",
|
|
2344
|
+
"Consume the Scout TARGET_SURFACE evidence before selecting files. Preserve the discovered existing entrypoint and data source. If implementationPaths or testPaths are outside task allowedPaths, record a blocking scope conflict; do not substitute a new page or silently broaden the writeSet.",
|
|
2317
2345
|
"End with exactly one fenced json object conforming to frontend-implementation-contract-v1 so small topology can materialize the contract without plan-revision.",
|
|
2318
2346
|
"Each requirement must state its user-observable or logic-observable expectedOutcome. Each interaction must state its trigger and expectedBehavior. IDs plus file paths are not sufficient behavior semantics.",
|
|
2319
2347
|
requirementCoverageInstruction,
|
|
@@ -2338,7 +2366,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2338
2366
|
subtask_prompt: [
|
|
2339
2367
|
"Audit the frontend plan before implementation.",
|
|
2340
2368
|
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
2341
|
-
"Request revision when the Mock strategy is MOCK_STRATEGY: blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for
|
|
2369
|
+
"Request revision when the Mock strategy is MOCK_STRATEGY: blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for a declared behavior target or selected Mock strategy. Mock strategies require Mock-backed evidence. A static-only contract is allowed only when every verification target is static and maps to a declared static entrypoint. not-needed otherwise requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case the plan must preserve the real request path and record the Real Integration Gap.",
|
|
2342
2370
|
"Also request revision for missing applicable UI states, unsupported dependency additions, design-system drift without reason, weak interaction coverage, broad scope, inline fake data, schema drift, or missing deterministic verification commands.",
|
|
2343
2371
|
"Read-only: do not modify repository files.",
|
|
2344
2372
|
fixedVerificationContext,
|
|
@@ -2641,11 +2669,14 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2641
2669
|
allowedPaths: readOnlyPaths,
|
|
2642
2670
|
forbiddenPaths,
|
|
2643
2671
|
skills: FRONTEND_REVIEW_SKILLS,
|
|
2644
|
-
outputContract:
|
|
2672
|
+
outputContract: 'Structured JSON review verdict only: {"schemaVersion":1,"verdict":"pass|request-revision","findings":[...],"verificationAssessment":"...","uxAssessment":"...","residualRisks":[...]}. No file writes.',
|
|
2673
|
+
outputProtocol: REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL,
|
|
2645
2674
|
subtask_prompt: [
|
|
2646
2675
|
"Review the frontend implementation and verification evidence.",
|
|
2647
|
-
"
|
|
2648
|
-
"
|
|
2676
|
+
"Return exactly one final JSON object in this response. Do not repeat it, do not emit a second revision, do not wrap it in Markdown, and do not include prose outside the JSON.",
|
|
2677
|
+
'Required fields: schemaVersion: 1; verdict: "pass" or "request-revision"; findings: array of objects with severity ("Critical" | "Important" | "Minor" | "Info"), optional file, optional positive integer line, issue, and optional requiredChange.',
|
|
2678
|
+
'verdict "request-revision" requires at least one finding. verdict "pass" is invalid if any finding severity is Critical or Important.',
|
|
2679
|
+
'Any Critical or Important finding must force verdict "request-revision".',
|
|
2649
2680
|
"Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds the validated implementation contract, frontend lint assessment when lint is configured, effective initial-or-post-repair verification trace, repair assessment, and the run-owned actual diff (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
|
|
2650
2681
|
"Treat lint status exactly as passed | baseline-debt | failed | unavailable. baseline-debt may continue only with intact evidence and zero diagnostics on writer-changed files; report the tolerated debt count and never rewrite it as lint passed. Typecheck, build, and test still require successful final exits.",
|
|
2651
2682
|
"Flag .skip/.only, deleted or weakened tests, unauthorized config changes, Mock-only evidence claimed as real integration, and Browser/visual claims (always not-run in this workflow).",
|
|
@@ -2665,15 +2696,16 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2665
2696
|
writePolicy: "read-only",
|
|
2666
2697
|
allowedPaths: readOnlyPaths,
|
|
2667
2698
|
forbiddenPaths,
|
|
2668
|
-
outputContract:
|
|
2669
|
-
subtask_prompt:
|
|
2699
|
+
outputContract: 'Deterministic frontend review verdict gate: exit 0 only when frontend-review-pi emits JSON verdict "pass".',
|
|
2700
|
+
subtask_prompt: 'Deterministic gate: block downstream closeout unless frontend-review-pi emitted JSON verdict "pass".',
|
|
2670
2701
|
shell: {
|
|
2671
2702
|
commands: [],
|
|
2672
2703
|
verdictGate: {
|
|
2673
2704
|
fromNodeId: "frontend-review-pi",
|
|
2674
|
-
accept: ["
|
|
2705
|
+
accept: ["pass"],
|
|
2706
|
+
routingAccept: ["request-revision"],
|
|
2675
2707
|
label: "frontend review",
|
|
2676
|
-
|
|
2708
|
+
source: "json-review-verdict",
|
|
2677
2709
|
},
|
|
2678
2710
|
cwd: ".",
|
|
2679
2711
|
timeoutMs: 60000,
|
|
@@ -3577,13 +3609,50 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3577
3609
|
"Do not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON. For best-effort cleanup, catch only the narrow transport exception actually raised by the selected HTTP client (for example `requests.RequestException` or `urllib.error.URLError`); never use bare `except`, `Exception`, or `BaseException` with `pass`.",
|
|
3578
3610
|
].join("\n\n"),
|
|
3579
3611
|
};
|
|
3580
|
-
const
|
|
3612
|
+
const collectionAssess = shellNode("assess-backend-pytest-collection-shell", [generatePytest.id], "markdown-collection-assess", "Run pytest collection only over final Markdown-mapped scripts before any business test body execution. Materialize hash-bound PASS/REPAIRABLE/BLOCKED facts. Only generated testcase-local syntax/import inconsistencies are repairable; dependency, plugin, production-module, environment, safety and unknown failures remain blocked.", "Run-owned reports/backend-test-pytest-collection-initial.md and contracts/backend-test-pytest-collection-initial.json with bounded diagnostics, asset hashes, collected item IDs and deterministic repair eligibility.", [], 120000);
|
|
3613
|
+
const repairPytest = {
|
|
3614
|
+
id: "repair-backend-pytest-collection-pi",
|
|
3615
|
+
depends_on: [collectionAssess.id],
|
|
3616
|
+
runIf: "$.nodes['assess-backend-pytest-collection-shell'].json.repairEligible == true",
|
|
3617
|
+
role: "implementer",
|
|
3618
|
+
executor: "pi",
|
|
3619
|
+
toolProfile: "write",
|
|
3620
|
+
complexity: "HIGH",
|
|
3621
|
+
writePolicy: "exclusive",
|
|
3622
|
+
writeSet: [
|
|
3623
|
+
"testcase/**/test_*.py",
|
|
3624
|
+
"testcase/**/helpers/**",
|
|
3625
|
+
"testcase/**/factories/**",
|
|
3626
|
+
],
|
|
3627
|
+
allowedPaths: Array.from(new Set([...ro, "testcase/**"])),
|
|
3628
|
+
forbiddenPaths: Array.from(new Set([
|
|
3629
|
+
...forbidden,
|
|
3630
|
+
"testcase/md/**",
|
|
3631
|
+
"conftest.py",
|
|
3632
|
+
"pytest.ini",
|
|
3633
|
+
"pyproject.toml",
|
|
3634
|
+
"setup.cfg",
|
|
3635
|
+
])),
|
|
3636
|
+
writerOutcomePolicy: { type: "implementation-outcome-v1" },
|
|
3637
|
+
outputContract: "First non-empty line is IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked, followed by a concise repair summary. Modify only generated pytest scripts/helpers/factories and preserve every Markdown Case, Test Point, primary symbol and assertion meaning.",
|
|
3638
|
+
subtask_prompt: [
|
|
3639
|
+
"Repair the generated backend pytest asset as one bounded program using the direct upstream collection assessment. This is the only repair attempt and happens before any business test body execution.",
|
|
3640
|
+
"Fix only collection-proven generated testcase-local syntax, module path, missing symbol, circular import, fixture-name, decorator or parameterization inconsistencies. Inspect all affected importers and providers so the repair is cross-file consistent.",
|
|
3641
|
+
"Preserve final testcase/md/** semantics, every Case ID, Rule/Test Point binding, primary symbol, parameter ID, expected status/body/schema assertion, HTTP logging, redaction and truncation behavior.",
|
|
3642
|
+
"Do not read task source/** or reinterpret requirements. Do not modify Markdown, conftest, pytest config, production code or dependencies.",
|
|
3643
|
+
"Do not add skip/skipif/xfail, remove tests, reduce collected items, loosen assertions, swallow exceptions, use try/except ImportError fallback, mutate sys.path/PYTHONPATH, or replace the real API with mocks.",
|
|
3644
|
+
"Do not execute pytest; the deterministic effective collection gate owns the final collection attempt.",
|
|
3645
|
+
].join("\n\n"),
|
|
3646
|
+
};
|
|
3647
|
+
const collectionEffective = shellNode("effective-backend-pytest-collection-gate-shell", [collectionAssess.id, repairPytest.id], "markdown-collection-effective", "If initial collection passed, verify unchanged asset hashes and reuse it without another collection. If the single repair ran, collect the final mapped scripts once and fail closed unless it passes. BLOCKED initial facts, repair failure, final collection failure or hash drift must prevent business pytest execution.", "Run-owned reports/backend-test-pytest-collection-effective.md and contracts/backend-test-pytest-collection-effective.json proving the exact final assets are collectable; initial PASS is reused, repair path records attempt=1.", [], 120000);
|
|
3648
|
+
collectionEffective.dependsPolicy = "all-or-condition-skip";
|
|
3649
|
+
const traceability = shellNode("backend-test-traceability-gate-shell", [collectionEffective.id], "markdown-traceability", "Deterministically scan only Markdown-mapped pytest scripts after the effective hash-bound collection gate. Keep the existing traceability/logging checks and produce a bidirectional Markdown module/Case/Test Point ↔ pytest file/primary symbol correspondence analysis. Map variant Test Points from stable parameter IDs, assertion Test Points from the primary symbol docstring, and cross-cutting Test Points from the primary symbol evidence binding. Report 1:1, 1:0, 1:N, 0:1, script/primary-symbol mismatch, missing Case ID, missing variant parameter IDs, missing assertion/cross-cutting bindings, duplicate modes and extra bindings. Human and machine evidence must come from the same facts. Findings are advisory and never block pytest.", "Run-owned reports/backend-test-traceability.md, reports/backend-test-markdown-pytest-correspondence.md and contracts/backend-test-markdown-pytest-correspondence-facts.json with PASS/FAIL/UNAVAILABLE correspondence facts bound after effective collection.");
|
|
3581
3650
|
const manifest = shellNode("backend-test-case-manifest-shell", [traceability.id], "markdown-manifest", "Materialize the canonical Backend Test Case Manifest only from contracts/backend-test-case-coverage-facts.json and contracts/backend-test-markdown-pytest-correspondence-facts.json. Validate schema, task binding, input hashes and freshness; never re-read source semantics, re-analyze Coverage Matrix, rescan pytest symbols or recompute a second set of metrics. Missing/stale/conflicting facts produce partial/unavailable diagnostics rather than fabricated zeros.", "Run-owned contracts/backend-test-case-manifest.json with materializationStatus, sourceFactsIssues, validated coverageScope, coverageSummary, ruleCoverageSummary and correspondenceSummary; this is the single machine input for L-5 and closeout.");
|
|
3582
3651
|
const pytestCommand = [
|
|
3583
3652
|
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
3584
3653
|
'echo "pytest targets are resolved at runtime from final Markdown 自动化映射"',
|
|
3585
3654
|
].join("; ");
|
|
3586
|
-
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [manifest.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Prefer the deterministic module one-to-one path when a mapped script is missing but the module stem file exists. Generate a native pytest-html self-contained report, then render the primary self-contained Chinese HTML report from the same pytest-html plus final Markdown case metadata without rerun. Keep 测试结论 and quality status; make node 4 Markdown validation + case coverage and node
|
|
3655
|
+
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [manifest.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Prefer the deterministic module one-to-one path when a mapped script is missing but the module stem file exists. Generate a native pytest-html self-contained report, then render the primary self-contained Chinese HTML report from the same pytest-html plus final Markdown case metadata without rerun. Keep 测试结论 and quality status; make node 4 Markdown validation + case coverage and node 9 traceability + Markdown-to-pytest correspondence expandable to their full escaped details; show each failure overview item with its original pytest message plus deterministic evidence-based reason analysis; list failure/error case cards before the remaining cases while preserving stable order. Each polished per-case result card includes concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.", "One scoped pytest execution over Markdown-mapped scripts producing a valid pytest-html report with per-case captured output, self-contained reports/backend-test.html, reports/backend-test.md, reports/backend-test-facts.md, a deterministic self-contained reports/backend-test-l5-dashboard.html (machine-computed L-5 metrics, no JSON), and an optional contracts/code-coverage-v1.json when jacocoCoverage is configured (JaCoCo TCP dump → jacoco.xml → parsed; failure-safe); exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
|
|
3587
3656
|
if (execute.shell) {
|
|
3588
3657
|
execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
|
|
3589
3658
|
}
|
|
@@ -3607,10 +3676,10 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3607
3676
|
? "Final Markdown report and L-5 conclusion under docs/test-reports/**; the deterministic L-5 dashboard at reports/backend-test-l5-dashboard.html is the authoritative visualization and must be linked, not re-rendered; no JSON."
|
|
3608
3677
|
: "Final Markdown report and L-5 conclusion in assistant output; the deterministic L-5 dashboard at reports/backend-test-l5-dashboard.html is the authoritative visualization and must be linked; no JSON or writes.",
|
|
3609
3678
|
subtask_prompt: [
|
|
3610
|
-
"Generate the final Markdown report only from authoritative run-owned artifacts. Read node 1 reports/backend-test-environment.md; node 4 backend-md-case-validation.md and backend-test-case-coverage-analysis.md;
|
|
3679
|
+
"Generate the final Markdown report only from authoritative run-owned artifacts. Read node 1 reports/backend-test-environment.md; node 4 backend-md-case-validation.md and backend-test-case-coverage-analysis.md; nodes 6/8 backend-test-pytest-collection initial/effective reports and facts; node 9 backend-test-traceability.md and backend-test-markdown-pytest-correspondence.md; node 10 contracts/backend-test-case-manifest.json; and node 11 backend-test-result.json, backend-test-facts.md, pytest-html/HTML and L-5 dashboard. Do not use node 2/3/5 assistant prose as facts. Do not emit JSON.",
|
|
3611
3680
|
"Use this exact human-facing section order: 测试结论 → 执行概览 → 质量校验 → 失败分析 → 风险与建议 → 证据与 L-5. Put the decision and key numbers first, use compact tables/bullets, and keep headings concise. Do not paste entire upstream reports, duplicate per-case tables already present in facts, or repeat the same evidence in multiple sections; link to paths/hashes and quote only the findings needed for the conclusion.",
|
|
3612
|
-
"The L-5 metrics and visualization are produced deterministically by node
|
|
3613
|
-
"Always state the exact Coverage Scope classification, policy, affected operations, regression floor, completeness claim, PASS/FAIL/UNAVAILABLE status and findings from node 4 case validation + coverage, plus node
|
|
3681
|
+
"The L-5 metrics and visualization are produced deterministically by node 11 at reports/backend-test-l5-dashboard.html. Link to that dashboard as the authoritative L-5 view. Pytest execution facts come from node 11; coverage/correspondence numbers and materializationStatus come from node 10; collection authorization comes from nodes 6/8; detailed coverage findings come from node 4; detailed mapping findings come from node 9. Never recompute these values. If machine manifest and human reports disagree, report evidence inconsistency rather than silently choosing.",
|
|
3682
|
+
"Always state the exact Coverage Scope classification, policy, affected operations, regression floor, completeness claim, PASS/FAIL/UNAVAILABLE status and findings from node 4 case validation + coverage, plus node 9 traceability + correspondence. Affected-scope or affected-operations-full coverage must never be described as whole-API completeness unless every operation is explicitly listed. Their FAIL status does not block pytest, but it must remain visible and must never be rewritten as PASS.",
|
|
3614
3683
|
"Include environment, case quality/review, automation mapping, exact pytest facts, failure classification/analysis, risks, regression recommendations, evidence paths/hashes, coverage availability, and L-5 READY/NOT READY. Distinguish Markdown Case count, primary pytest symbol count, collected pytest item count, variant/assertion/cross-cutting Test Point counts and execution amplification; never describe pytest item count as the number of business scenarios.",
|
|
3615
3684
|
"Never override Shell/pytest-html facts. L-5 requires pass=100%, AC=100%, automation>=90%, line>=80%, branch>=70%, skipped=0 and no blocking Critical risk.",
|
|
3616
3685
|
canWriteReport
|
|
@@ -3628,11 +3697,11 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3628
3697
|
globalConstraints: [
|
|
3629
3698
|
...taskConfig.hardConstraints,
|
|
3630
3699
|
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
3631
|
-
"backend-test-dag uses exactly
|
|
3700
|
+
"backend-test-dag uses exactly 12 real top-level tasks. Pytest collection runs once on the green path and at most twice only when one bounded pre-execution repair is eligible; business pytest test bodies execute exactly once over safe scripts explicitly mapped by final Markdown cases.",
|
|
3632
3701
|
"Model nodes produce Markdown and pytest assets, never backend-test business JSON envelopes.",
|
|
3633
|
-
"Environment, advisory Markdown validation/coverage, advisory traceability/correspondence, canonical manifest, pytest-html, HTML and execution facts are deterministic evidence.
|
|
3702
|
+
"Environment, advisory Markdown validation/coverage, collection initial/effective facts, advisory traceability/correspondence, canonical manifest, pytest-html, HTML and execution facts are deterministic evidence. Node 4 quality findings stay advisory; nodes 6/8 form the fail-closed collection authorization; node 9 traceability findings stay advisory; node 10 partial/unavailable manifest does not block node 11 when effective collection remains fresh.",
|
|
3634
3703
|
"Only Markdown case generation/review may read source facts; pytest generation must not read source/**.",
|
|
3635
|
-
"Functional case IDs use canonical BE-<MODULE>-<NNN> with exactly three digits and no alphabetic suffix. Every Test Point has exactly one variant/assertion/cross-cutting binding; only variant bindings create pytest parameter items. Production code/config, skip/xfail, repair and rerun are forbidden.",
|
|
3704
|
+
"Functional case IDs use canonical BE-<MODULE>-<NNN> with exactly three digits and no alphabetic suffix. Every Test Point has exactly one variant/assertion/cross-cutting binding; only variant bindings create pytest parameter items. Production code/config, skip/xfail, execution-result repair and business pytest rerun are forbidden; the only repair is one pre-execution collection-proven generated-test asset repair.",
|
|
3636
3705
|
],
|
|
3637
3706
|
defaults: {
|
|
3638
3707
|
...BACKEND_TEST_DEFAULTS,
|
|
@@ -3646,6 +3715,9 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3646
3715
|
reviewCases,
|
|
3647
3716
|
validateCases,
|
|
3648
3717
|
generatePytest,
|
|
3718
|
+
collectionAssess,
|
|
3719
|
+
repairPytest,
|
|
3720
|
+
collectionEffective,
|
|
3649
3721
|
traceability,
|
|
3650
3722
|
manifest,
|
|
3651
3723
|
execute,
|
|
@@ -5286,11 +5358,13 @@ function buildReviewNode(sources) {
|
|
|
5286
5358
|
writePolicy: "read-only",
|
|
5287
5359
|
allowedPaths: commonReadOnlyPaths(sources),
|
|
5288
5360
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
5289
|
-
outputContract:
|
|
5290
|
-
outputProtocol:
|
|
5361
|
+
outputContract: 'Structured JSON review verdict only: {"schemaVersion":1,"verdict":"pass|request-revision","findings":[...]}; Critical/Important findings force request-revision. No file writes.',
|
|
5362
|
+
outputProtocol: REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL,
|
|
5291
5363
|
subtask_prompt: [
|
|
5292
5364
|
"Review upstream implementation and verification evidence.",
|
|
5293
|
-
"
|
|
5365
|
+
"Return exactly one final JSON object in this response. Do not repeat it, do not emit a second revision, do not wrap it in Markdown, and do not include prose outside the JSON.",
|
|
5366
|
+
'Required fields: schemaVersion: 1; verdict: "pass" or "request-revision"; findings: array of objects with severity ("Critical" | "Important" | "Minor" | "Info"), optional file, optional positive integer line, issue, and optional requiredChange.',
|
|
5367
|
+
'verdict "request-revision" requires at least one finding. verdict "pass" is invalid if any finding severity is Critical or Important.',
|
|
5294
5368
|
"List Critical/Important findings when present; any Critical/Important finding must force request-revision. Read-only: do not modify files.",
|
|
5295
5369
|
[
|
|
5296
5370
|
"Three-way source fidelity check (required):",
|
|
@@ -5319,15 +5393,16 @@ function buildReviewVerdictRecoveryNode(sources) {
|
|
|
5319
5393
|
writePolicy: "read-only",
|
|
5320
5394
|
allowedPaths: commonReadOnlyPaths(sources),
|
|
5321
5395
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
5322
|
-
outputContract:
|
|
5323
|
-
outputProtocol:
|
|
5396
|
+
outputContract: 'Structured JSON review verdict only, preserving the original review conclusion/findings without substantive changes. No file writes.',
|
|
5397
|
+
outputProtocol: REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL,
|
|
5324
5398
|
subtask_prompt: [
|
|
5325
5399
|
"Normalize the output format of review-pi; this is the single read-only format-recovery attempt for the review verdict protocol.",
|
|
5326
|
-
"
|
|
5327
|
-
"
|
|
5328
|
-
"If
|
|
5329
|
-
|
|
5330
|
-
|
|
5400
|
+
"Return only one JSON object. Do not wrap it in Markdown and do not include prose outside the JSON.",
|
|
5401
|
+
'Required fields: schemaVersion: 1; verdict: "pass" or "request-revision"; findings: array of objects with severity ("Critical" | "Important" | "Minor" | "Info"), optional file, optional positive integer line, issue, and optional requiredChange.',
|
|
5402
|
+
"If review-pi already contains valid JSON, preserve its verdict exactly and keep the original findings.",
|
|
5403
|
+
'If it omitted or malformed JSON but states an unambiguous request-revision conclusion, emit verdict "request-revision" and preserve the findings.',
|
|
5404
|
+
'Do not invent verdict "pass" from natural-language phrases such as 通过, PASS, or general approval prose.',
|
|
5405
|
+
'If the upstream conclusion is ambiguous or cannot be preserved safely, emit verdict "request-revision" and report the format ambiguity as a finding.',
|
|
5331
5406
|
"Do not re-review code, expand task allowedPaths, or edit files.",
|
|
5332
5407
|
buildSourceContextBlock(sources),
|
|
5333
5408
|
].join("\n\n"),
|
|
@@ -5343,15 +5418,16 @@ function buildReviewGateNode(sources) {
|
|
|
5343
5418
|
writePolicy: "read-only",
|
|
5344
5419
|
allowedPaths: commonReadOnlyPaths(sources),
|
|
5345
5420
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
5346
|
-
outputContract:
|
|
5347
|
-
subtask_prompt:
|
|
5421
|
+
outputContract: 'Deterministic review verdict gate: exit 0 only when review-verdict-recovery-pi emits JSON verdict "pass".',
|
|
5422
|
+
subtask_prompt: 'Deterministic gate: block downstream closeout unless review-verdict-recovery-pi emitted JSON verdict "pass".',
|
|
5348
5423
|
shell: {
|
|
5349
5424
|
commands: [],
|
|
5350
5425
|
verdictGate: {
|
|
5351
5426
|
fromNodeId: "review-verdict-recovery-pi",
|
|
5352
|
-
accept: ["
|
|
5427
|
+
accept: ["pass"],
|
|
5428
|
+
routingAccept: ["request-revision"],
|
|
5353
5429
|
label: "review",
|
|
5354
|
-
|
|
5430
|
+
source: "json-review-verdict",
|
|
5355
5431
|
},
|
|
5356
5432
|
cwd: ".",
|
|
5357
5433
|
timeoutMs: 60000,
|
|
@@ -5920,8 +5996,8 @@ function buildSupervisedHybridDag(standard, sources) {
|
|
|
5920
5996
|
buildReviewGateNode(sources),
|
|
5921
5997
|
buildDecisionNode(sources),
|
|
5922
5998
|
cloneTask(closeout, {
|
|
5923
|
-
depends_on: ["decision-pi"],
|
|
5924
|
-
failureAwareDependsOn: ["decision-pi"],
|
|
5999
|
+
depends_on: ["decision-pi", "review-gate-shell"],
|
|
6000
|
+
failureAwareDependsOn: ["decision-pi", "review-gate-shell"],
|
|
5925
6001
|
}),
|
|
5926
6002
|
],
|
|
5927
6003
|
};
|
|
@@ -3,22 +3,54 @@ import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Machine-readable output protocol for safe read-only Pi nodes.
|
|
5
5
|
*
|
|
6
|
-
* Phase 1 only supports first-line-enum (e.g. VERDICT lines). Structured JSON
|
|
7
|
-
* continues to use existing structured-required / deterministic gates.
|
|
8
6
|
*/
|
|
9
|
-
|
|
7
|
+
const firstLineEnumOutputProtocolSchema = z
|
|
10
8
|
.object({
|
|
11
9
|
type: z.literal("first-line-enum"),
|
|
12
10
|
validLines: z.array(z.string().min(1)).min(1),
|
|
13
11
|
retryOnInvalid: z.boolean().default(true),
|
|
14
12
|
})
|
|
15
13
|
.strict();
|
|
14
|
+
const jsonReviewVerdictOutputProtocolSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
type: z.literal("json-review-verdict"),
|
|
17
|
+
retryOnInvalid: z.boolean().default(true),
|
|
18
|
+
})
|
|
19
|
+
.strict();
|
|
20
|
+
export const dagOutputProtocolSchema = z.discriminatedUnion("type", [
|
|
21
|
+
firstLineEnumOutputProtocolSchema,
|
|
22
|
+
jsonReviewVerdictOutputProtocolSchema,
|
|
23
|
+
]);
|
|
16
24
|
/** Reviewer VERDICT protocol used by reviewed/supervised DAGs. */
|
|
17
25
|
export const REVIEW_VERDICT_OUTPUT_PROTOCOL = {
|
|
18
26
|
type: "first-line-enum",
|
|
19
27
|
validLines: ["VERDICT: pass", "VERDICT: request-revision"],
|
|
20
28
|
retryOnInvalid: true,
|
|
21
29
|
};
|
|
30
|
+
/** Structured reviewer verdict protocol used when deterministic gates parse JSON. */
|
|
31
|
+
export const REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL = {
|
|
32
|
+
type: "json-review-verdict",
|
|
33
|
+
retryOnInvalid: true,
|
|
34
|
+
};
|
|
35
|
+
const reviewFindingSchema = z
|
|
36
|
+
.object({
|
|
37
|
+
severity: z.enum(["Critical", "Important", "Minor", "Info"]),
|
|
38
|
+
file: z.string().min(1).optional(),
|
|
39
|
+
line: z.number().int().positive().optional(),
|
|
40
|
+
issue: z.string().min(1),
|
|
41
|
+
requiredChange: z.string().min(1).optional(),
|
|
42
|
+
})
|
|
43
|
+
.strict();
|
|
44
|
+
const reviewJsonVerdictSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
schemaVersion: z.literal(1),
|
|
47
|
+
verdict: z.enum(["pass", "request-revision"]),
|
|
48
|
+
findings: z.array(reviewFindingSchema),
|
|
49
|
+
verificationAssessment: z.string().min(1).optional(),
|
|
50
|
+
uxAssessment: z.string().min(1).optional(),
|
|
51
|
+
residualRisks: z.array(z.string().min(1)).optional(),
|
|
52
|
+
})
|
|
53
|
+
.strict();
|
|
22
54
|
/**
|
|
23
55
|
* Extract the first non-empty line from assistant/stdout text.
|
|
24
56
|
*/
|
|
@@ -30,12 +62,136 @@ export function firstNonEmptyLine(text) {
|
|
|
30
62
|
}
|
|
31
63
|
return undefined;
|
|
32
64
|
}
|
|
65
|
+
function extractSingleJsonObjectText(text) {
|
|
66
|
+
const trimmed = String(text).trim();
|
|
67
|
+
if (!trimmed)
|
|
68
|
+
return { ok: false, reason: "missing JSON output" };
|
|
69
|
+
const extractBalancedObject = (source) => {
|
|
70
|
+
for (let start = 0; start < source.length; start += 1) {
|
|
71
|
+
if (source[start] !== "{")
|
|
72
|
+
continue;
|
|
73
|
+
let depth = 0;
|
|
74
|
+
let inString = false;
|
|
75
|
+
let escaped = false;
|
|
76
|
+
for (let index = start; index < source.length; index += 1) {
|
|
77
|
+
const char = source[index];
|
|
78
|
+
if (inString) {
|
|
79
|
+
if (escaped)
|
|
80
|
+
escaped = false;
|
|
81
|
+
else if (char === "\\")
|
|
82
|
+
escaped = true;
|
|
83
|
+
else if (char === '"')
|
|
84
|
+
inString = false;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (char === '"')
|
|
88
|
+
inString = true;
|
|
89
|
+
else if (char === "{")
|
|
90
|
+
depth += 1;
|
|
91
|
+
else if (char === "}" && --depth === 0) {
|
|
92
|
+
const candidate = source.slice(start, index + 1);
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(candidate);
|
|
95
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
96
|
+
return candidate;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Continue scanning for the next complete object.
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
};
|
|
107
|
+
const direct = extractBalancedObject(trimmed);
|
|
108
|
+
if (direct)
|
|
109
|
+
return { ok: true, jsonText: direct };
|
|
110
|
+
const fencedMatches = Array.from(trimmed.matchAll(/```(?:json)?\s*([\s\S]*?)\s*```/gi));
|
|
111
|
+
if (fencedMatches.length === 1) {
|
|
112
|
+
const jsonText = fencedMatches[0][1].trim();
|
|
113
|
+
const fencedObject = extractBalancedObject(jsonText);
|
|
114
|
+
if (fencedObject)
|
|
115
|
+
return { ok: true, jsonText: fencedObject };
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
reason: "single fenced block is not a JSON object",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (fencedMatches.length > 1) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
reason: "multiple fenced JSON candidates found; expected exactly one JSON object",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
reason: "output is not a single JSON object; expected only JSON with no Markdown or prose",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function validateJsonReviewVerdict(text) {
|
|
133
|
+
const extracted = extractSingleJsonObjectText(text);
|
|
134
|
+
if (!extracted.ok) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
failureCategory: "protocol-invalid",
|
|
138
|
+
reason: extracted.reason,
|
|
139
|
+
firstNonEmptyLine: firstNonEmptyLine(text),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
let parsed;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(extracted.jsonText);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
failureCategory: "protocol-invalid",
|
|
150
|
+
reason: `invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
151
|
+
firstNonEmptyLine: firstNonEmptyLine(text),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const checked = reviewJsonVerdictSchema.safeParse(parsed);
|
|
155
|
+
if (!checked.success) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
failureCategory: "protocol-invalid",
|
|
159
|
+
reason: `JSON review verdict schema violation: ${checked.error.issues
|
|
160
|
+
.map((issue) => `${issue.path.join(".") || "<root>"} ${issue.message}`)
|
|
161
|
+
.join("; ")}`,
|
|
162
|
+
firstNonEmptyLine: firstNonEmptyLine(text),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const blockingFindings = checked.data.findings.filter((finding) => finding.severity === "Critical" || finding.severity === "Important");
|
|
166
|
+
if (checked.data.verdict === "pass" && blockingFindings.length > 0) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
failureCategory: "protocol-invalid",
|
|
170
|
+
reason: "JSON review verdict cannot be pass when Critical or Important findings are present",
|
|
171
|
+
firstNonEmptyLine: firstNonEmptyLine(text),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (checked.data.verdict === "request-revision" &&
|
|
175
|
+
checked.data.findings.length === 0) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
failureCategory: "protocol-invalid",
|
|
179
|
+
reason: "JSON review verdict request-revision requires at least one finding",
|
|
180
|
+
firstNonEmptyLine: firstNonEmptyLine(text),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return { ok: true, verdict: checked.data.verdict };
|
|
184
|
+
}
|
|
33
185
|
/**
|
|
34
186
|
* Validate node output against an explicit outputProtocol.
|
|
35
187
|
* Pure function — does not mutate run facts.
|
|
36
188
|
*/
|
|
37
189
|
export function validateOutputProtocol(protocol, text) {
|
|
38
|
-
if (protocol.type
|
|
190
|
+
if (protocol.type === "json-review-verdict") {
|
|
191
|
+
return validateJsonReviewVerdict(text);
|
|
192
|
+
}
|
|
193
|
+
const validLines = "validLines" in protocol ? protocol.validLines : undefined;
|
|
194
|
+
if (!validLines) {
|
|
39
195
|
return {
|
|
40
196
|
ok: false,
|
|
41
197
|
failureCategory: "protocol-invalid",
|
|
@@ -64,6 +220,17 @@ export function validateOutputProtocol(protocol, text) {
|
|
|
64
220
|
* Correction instruction appended on protocol-invalid retry attempts.
|
|
65
221
|
*/
|
|
66
222
|
export function buildProtocolRetryInstruction(protocol, reason) {
|
|
223
|
+
if (protocol.type === "json-review-verdict") {
|
|
224
|
+
return [
|
|
225
|
+
"<retry_instruction>",
|
|
226
|
+
"Previous attempt violated the structured review output protocol:",
|
|
227
|
+
reason,
|
|
228
|
+
"Return ONLY a JSON object, with no Markdown fence and no surrounding prose.",
|
|
229
|
+
'Required schema: {"schemaVersion":1,"verdict":"pass|request-revision","findings":[{"severity":"Critical|Important|Minor|Info","file":"optional path","line":1,"issue":"required","requiredChange":"optional"}],"verificationAssessment":"optional","uxAssessment":"optional","residualRisks":["optional"]}.',
|
|
230
|
+
'Rules: verdict "request-revision" requires at least one finding; verdict "pass" is invalid if any finding severity is Critical or Important.',
|
|
231
|
+
"</retry_instruction>",
|
|
232
|
+
].join("\n");
|
|
233
|
+
}
|
|
67
234
|
const expected = protocol.validLines
|
|
68
235
|
.map((line) => JSON.stringify(line))
|
|
69
236
|
.join(" or ");
|
|
@@ -102,9 +269,15 @@ export function buildProtocolRetryInstruction(protocol, reason) {
|
|
|
102
269
|
* output, and structured protocols are never loosely tolerated.
|
|
103
270
|
*/
|
|
104
271
|
export function normalizeReviewVerdictAfterRetries(protocol, text) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
272
|
+
if (protocol.type !== "first-line-enum") {
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
reason: "deterministic verdict normalization only applies to the canonical review verdict protocol",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const validLines = protocol.validLines;
|
|
279
|
+
const isReviewVerdictProtocol = Array.from(validLines).every((line, index) => line === REVIEW_VERDICT_OUTPUT_PROTOCOL.validLines[index]) &&
|
|
280
|
+
validLines.length ===
|
|
108
281
|
REVIEW_VERDICT_OUTPUT_PROTOCOL.validLines.length;
|
|
109
282
|
if (!isReviewVerdictProtocol) {
|
|
110
283
|
return {
|