@tea-agent/loop-agent 0.41.0-next.3 → 0.41.1-beta.0
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 +264 -32
- package/dist/application/dag/generate-task-dag.js +75 -19
- package/dist/application/task-lifecycle/advance.js +24 -5
- package/dist/application/task-lifecycle/observe.js +171 -17
- package/dist/application/task-lifecycle/plan-transitions.js +42 -7
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +5 -0
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/init.js +27 -1
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +3784 -231
- package/dist/executors/pi-executor.js +15 -4
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/shell-executor.js +945 -217
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-failure-category.js +12 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +7 -0
- package/dist/task/config-types.js +34 -7
- package/dist/task/contract/adopt.js +4 -0
- package/dist/task/contract/import-revision.js +4 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +203 -20
- package/dist/task/runtime.js +5 -2
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +15 -9
- package/dist/task/source-prepare/prepare.js +89 -1
- package/dist/task/source-prepare/semantic-intake.js +6 -2
- package/dist/task/source-references.js +22 -1
- package/dist/worker/console/chat/pi-runtime.js +2 -3
- package/dist/worker/console/chat/routes.js +3 -12
- package/dist/worker/console/chat/semantic-activity.js +11 -12
- package/dist/worker/console/chat/session-stats.js +0 -96
- package/dist/worker/console/chat/turn-process.js +4 -28
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operation-store.js +226 -6
- package/dist/worker/console/operator-actions.js +169 -7
- package/dist/worker/console/pi-plugins.js +1 -3
- package/dist/worker/console/server.js +3 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DwVJxUxz.js → abnfDiagram-N423BO3Z-Bho4we4c.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DqfCcXdu.js → arc-TBDkTeK0.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-qMVtggRv.js → architectureDiagram-T3A2C74G-BF_fZrn2.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-ECrV38dh.js → blockDiagram-VBNYF7ZC-CCnPgUe2.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DBaaLqQv.js → c4Diagram-5PPSVZJV-BCYZJLAT.js} +1 -1
- package/dist/worker/console/static/assets/channel-bnXW0U3C.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BvfNHkAz.js → chunk-2GRJ4B5K-D8K6adhU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-C5StiIbc.js → chunk-2Q5K7J3B-DkT1Q78b.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-HabGiJkS.js → chunk-5RXB4S5H-DuhvpIr7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BiTg127i.js → chunk-5VM5RSS4-BnK6dC49.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CZsXBPuR.js → chunk-6Q2QTUOP-DPz__z7r.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D-r1MxLK.js → chunk-GF5L2VYU-0kkppM2j.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DQxb9plr.js → chunk-JWPE2WC7-BB41iK5t.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DMndGvCT.js → chunk-KBJHAD2P-CiVrxKHU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ByL4BgOJ.js → chunk-RYQCIY6F-C2h9Xje1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BVe1b1gL.js → chunk-XXDRQBXY-3iOSHdbM.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BAyGzN8j.js → cose-bilkent-JH36ORCC-BN3A7tkk.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C5yVYU7E.js → cynefin-VYW2F7L2-CTuBy0Or.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-x-MwsEm9.js → cynefinDiagram-MW4NZA55-wb6tUlMN.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CT5Tle5F.js → dagre-VZM6K2ZE-GP1Ivx8B.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BARERhlS.js → diagram-7IWD3JNH-ejrcK6L0.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BwfUz0VM.js → diagram-B4RE2ZJO-YFkCCvUU.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Mk4r2WvJ.js → diagram-LBJQPF4R-B4rrvIaL.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-KUrxuBs0.js → diagram-Q27KOJAE-C-YIZcUf.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CjZOceZn.js → diagram-UB23O5K3-BPF5wcL1.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-rnwUCiEI.js → ebnfDiagram-BXEA7PRR-CU5SzXjO.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DM4j23FM.js → erDiagram-JOGREHBK-CpL414DE.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DD5HdDEs.js → flowDiagram-UKHOOZJN-MrhLzNau.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CIAGSF47.js → ganttDiagram-PKOTCBZU-Bg4qFuUo.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DbwFbVEL.js → gitGraphDiagram-DS77QQ5N-ColxSXLR.js} +1 -1
- package/dist/worker/console/static/assets/index-D3CC4eUz.css +1 -0
- package/dist/worker/console/static/assets/index-DLt4ZFvD.js +437 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-B-Pb0T66.js → infoDiagram-6WML65LV-Czvtm6gc.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B9Unk-YX.js → ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C2F3JkPt.js → journeyDiagram-NVQOT4AX-DWYZcvph.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Bl8-Xcoz.js → kanban-definition-27J2QSJJ-KTLZRUjx.js} +1 -1
- package/dist/worker/console/static/assets/{linear-yK4uZmC5.js → linear-_Exn7bfl.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Bpa6PmuE.js → mermaid.core-Cxg8kX8V.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BfGRoCSe.js → mindmap-definition-FAOFIHXS-oKrlo2Bt.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Bz6_HtST.js → pegDiagram-VL7TDLO6-XAeteiNd.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-i0Umbsby.js → pieDiagram-7S7Q4E2Y-BTf1K-lp.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DaVQBobe.js → quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DMTGnSq7.js → railroadDiagram-AXF67PYL-BVbKtZhW.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-2ao_DaSq.js → requirementDiagram-LRYGKXZP-C5e4JFcr.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-2SG1lTB9.js → sankeyDiagram-W5VNT64P-Cz30hc29.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BQYOLybR.js → sequenceDiagram-SI44F4Z6-DS33n6d0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-C8g3yjuI.js → sizeCapture-X5ZJPWSS-46w7v-9T.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-q_F8nAQG.js → stateDiagram-OKZ733FA-pbcD5SsZ.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-8LrWpH2s.js → swimlanes-SLNWSIFB-DuivYBSj.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BQoY3qzl.js → timeline-definition-Z64GVDOM-Cb9VZVIl.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BDu1UjSp.js → vennDiagram-T6HMQDX7-D2-mfOgR.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C48M7Y0a.js → wardleyDiagram-T6FBY63Y-DeS2aC76.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YU6FTl_F.js → xychartDiagram-ELKLHX3M-WGsrFjxH.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -9
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +0 -6
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +3 -28
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +0 -106
- package/dist/worker/console/workspace-context.js +111 -1
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observability/read-model.js +0 -4
- package/dist/worker/observe/static/dag-node-purpose.js +5 -0
- package/dist/worker/observe/static/views/dag-inspector.js +7 -2
- package/dist/worker/observe/static/views/session-timeline.js +15 -46
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +932 -162
- package/dist/workflows/dag/frontend-plan-render.js +2 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +255 -349
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-read-budgets.js +12 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +1 -432
- package/dist/workflows/dag/frontend-review-context.js +261 -15
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +914 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +427 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +249 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +285 -0
- package/dist/workflows/dag/frontend-writer-status.js +256 -0
- package/dist/workflows/dag/init-hybrid.js +685 -494
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +732 -4
- package/dist/workflows/dag/prompt.js +34 -1
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +256 -1
- package/dist/workflows/dag/rerun-plan.js +10 -2
- package/dist/workflows/dag/rerun-run.js +23 -0
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +214 -104
- package/dist/workflows/dag/runner.js +429 -122
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/types.js +239 -12
- package/dist/workflows/dag/validate.js +28 -12
- package/docs/init-surface.manifest.json +30 -12
- package/docs/skills/vetted-skill-registry.md +4 -2
- package/docs/templates/README.md +2 -0
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/frontend-implementation-contract.schema.json +4 -1
- package/docs/templates/frontend-implementation-dag.json +89 -0
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +3 -3
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +15 -14
- package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
- package/skills/frontend-contract/SKILL.md +23 -0
- package/skills/frontend-contract/references/contract-protocol.md +34 -0
- package/skills/frontend-design-review/SKILL.md +22 -41
- package/skills/frontend-plan/SKILL.md +26 -0
- package/skills/frontend-plan/references/decision-contract.md +37 -0
- package/skills/frontend-plan/references/design-decisions.md +17 -0
- package/skills/frontend-review/SKILL.md +20 -15
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/frontend-scout/SKILL.md +25 -0
- package/skills/frontend-scout/references/design-evidence.md +16 -0
- package/skills/frontend-scout/references/scout-evidence.md +23 -0
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/worker/console/static/assets/channel-CL1NRAmS.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-PBGzzjZA.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-PBGzzjZA.js +0 -1
- package/dist/worker/console/static/assets/index-BfleX-qj.css +0 -1
- package/dist/worker/console/static/assets/index-Bih9w9h9.js +0 -451
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrxGBDJu.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BhpDxzAf.js +0 -8
- package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +0 -22
- package/skills/frontend-implementation/SKILL.md +0 -52
- package/skills/frontend-implementation/references/code-standards.md +0 -33
- package/skills/frontend-implementation/references/design-spec.md +0 -56
- package/skills/frontend-implementation/references/node-contracts.md +0 -31
|
@@ -2,21 +2,17 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
6
|
-
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
|
+
import { OPENSPEC_SPEC_EXT_RE, isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
7
6
|
import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
|
|
8
7
|
import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
8
|
+
import { checkFrontendSourceFidelityLedger, } from "./frontend-source-fidelity-ledger.js";
|
|
9
|
+
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
12
10
|
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
13
11
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
14
12
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
15
13
|
import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
|
|
16
14
|
export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
|
|
17
15
|
export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
|
|
18
|
-
/** Canonical contract serialization must stay at or below this byte size. */
|
|
19
|
-
export const FRONTEND_CONTRACT_MAX_BYTES = 65536;
|
|
20
16
|
/**
|
|
21
17
|
* Machine-readable failure classification for blocked/retryable-invalid
|
|
22
18
|
* prewrite outcomes. Kept alongside the human-readable failureReason so
|
|
@@ -37,13 +33,12 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
|
|
|
37
33
|
"verification-target-outside-write-set",
|
|
38
34
|
"openspec-not-read",
|
|
39
35
|
"candidate-missing-drift",
|
|
40
|
-
"
|
|
41
|
-
"openspec-
|
|
42
|
-
"openspec-citation-not-read",
|
|
36
|
+
"candidate-content-drift",
|
|
37
|
+
"openspec-selection-invalid",
|
|
43
38
|
"component-spec-reference-invalid",
|
|
44
39
|
"component-spec-not-cited",
|
|
40
|
+
"component-spec-choice-unbound",
|
|
45
41
|
"component-choices-missing",
|
|
46
|
-
"contract-too-large",
|
|
47
42
|
"revision-patch-unmergeable",
|
|
48
43
|
// Source fidelity ledger 类目(SOURCE_FIDELITY_*/source-* 稳定错误码)。
|
|
49
44
|
"source-fidelity-ledger-missing",
|
|
@@ -140,7 +135,7 @@ export const frontendPrewriteResultV1Schema = z
|
|
|
140
135
|
export const frontendWriterAdmissionSchema = z
|
|
141
136
|
.object({
|
|
142
137
|
schemaVersion: z.literal(1),
|
|
143
|
-
writerNodeId: z.enum(["frontend-implement-pi"
|
|
138
|
+
writerNodeId: z.enum(["frontend-implement-pi"]),
|
|
144
139
|
decision: z.enum(["authorized", "denied"]),
|
|
145
140
|
sourceArtifact: z.string().min(1),
|
|
146
141
|
artifactHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
@@ -274,6 +269,7 @@ function isFailedToolResult(event) {
|
|
|
274
269
|
}
|
|
275
270
|
async function collectOpenspecReadPaths(input) {
|
|
276
271
|
const { runDir, planNodeId, reviewNodeId, repoRoot } = input;
|
|
272
|
+
const candidateSet = input.candidatePaths ? new Set(input.candidatePaths) : undefined;
|
|
277
273
|
const matched = new Set();
|
|
278
274
|
for (const nodeId of [planNodeId, reviewNodeId]) {
|
|
279
275
|
const eventsPath = path.join(runDir, nodeId, "session-events.jsonl");
|
|
@@ -308,7 +304,7 @@ async function collectOpenspecReadPaths(input) {
|
|
|
308
304
|
: null;
|
|
309
305
|
if (relativePath &&
|
|
310
306
|
!isFailedToolResult(event) &&
|
|
311
|
-
|
|
307
|
+
(candidateSet ? candidateSet.has(relativePath) : OPENSPEC_SPEC_EXT_RE.test(relativePath))) {
|
|
312
308
|
matched.add(relativePath);
|
|
313
309
|
}
|
|
314
310
|
startedReads.delete(event.toolCallId);
|
|
@@ -323,21 +319,6 @@ async function collectOpenspecReadPaths(input) {
|
|
|
323
319
|
}
|
|
324
320
|
return [...matched];
|
|
325
321
|
}
|
|
326
|
-
async function checkOpenspecReadEvidence(input) {
|
|
327
|
-
const { runDir, candidatePaths, planNodeId, reviewNodeId, repoRoot } = input;
|
|
328
|
-
if (candidatePaths.length === 0)
|
|
329
|
-
return [];
|
|
330
|
-
const allReadPaths = await collectOpenspecReadPaths({
|
|
331
|
-
runDir,
|
|
332
|
-
planNodeId,
|
|
333
|
-
reviewNodeId,
|
|
334
|
-
repoRoot,
|
|
335
|
-
});
|
|
336
|
-
const normalizedCandidates = new Set(candidatePaths
|
|
337
|
-
.map((candidate) => toRepoRelativePath(candidate, repoRoot))
|
|
338
|
-
.filter((candidate) => Boolean(candidate && isOpenspecSpecFilePath(candidate))));
|
|
339
|
-
return allReadPaths.filter((readPath) => normalizedCandidates.has(readPath));
|
|
340
|
-
}
|
|
341
322
|
function normalizeCitationPath(rawPath, repoRoot) {
|
|
342
323
|
let candidate = rawPath.trim();
|
|
343
324
|
const hashIndex = candidate.indexOf("#");
|
|
@@ -352,57 +333,132 @@ function normalizeCitationPath(rawPath, repoRoot) {
|
|
|
352
333
|
}
|
|
353
334
|
return candidate;
|
|
354
335
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
336
|
+
async function resolveRequiredOpenspecPaths(input) {
|
|
337
|
+
if (input.policy === "scan-strict")
|
|
338
|
+
return { ok: true, paths: input.candidatePaths };
|
|
339
|
+
// Legacy cited DAGs had no selector: preserve their full candidate must-read
|
|
340
|
+
// semantics instead of silently weakening an existing run.
|
|
341
|
+
if (!input.selectionNodeId)
|
|
342
|
+
return { ok: true, paths: input.candidatePaths };
|
|
343
|
+
// OpenSpec classifications arrive as individual typed `openspec-selection`
|
|
344
|
+
// facts (one path per record_openspec_selection call) so a large candidate
|
|
345
|
+
// set never exceeds a single model output budget. Only positive
|
|
346
|
+
// classifications (required | relevant) are legal facts; candidates that
|
|
347
|
+
// were never mentioned default to irrelevant. Explicit task declarations /
|
|
348
|
+
// source citations (`mandatoryPaths`) are never downgraded: they are
|
|
349
|
+
// always required and always must-read, whether or not the model declared
|
|
350
|
+
// them. Fall back to the legacy single fenced JSON for older runs (which
|
|
351
|
+
// may still carry explicit irrelevant rows).
|
|
352
|
+
let selections;
|
|
353
|
+
let allowIrrelevant = false;
|
|
354
|
+
const selectionNodeId = input.selectionNodeId; // guarded above: no selector -> full candidate semantics
|
|
355
|
+
try {
|
|
356
|
+
const facts = await readCommittedContractFacts(input.runDir, selectionNodeId);
|
|
357
|
+
selections = facts
|
|
358
|
+
.filter((fact) => fact.kind === "openspec-selection")
|
|
359
|
+
.map((fact) => ({
|
|
360
|
+
path: String(fact.path ?? ""),
|
|
361
|
+
disposition: String(fact.disposition ?? ""),
|
|
362
|
+
rationale: String(fact.rationale ?? ""),
|
|
363
|
+
}));
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
selections = [];
|
|
367
|
+
}
|
|
368
|
+
if (selections.length === 0) {
|
|
369
|
+
const legacy = await readLegacyOpenspecSelection({
|
|
370
|
+
runDir: input.runDir,
|
|
371
|
+
selectionNodeId,
|
|
372
|
+
candidatePaths: input.candidatePaths,
|
|
373
|
+
mandatoryPaths: input.mandatoryPaths,
|
|
374
|
+
});
|
|
375
|
+
if (!legacy.ok)
|
|
376
|
+
return legacy;
|
|
377
|
+
selections = legacy.selections;
|
|
378
|
+
allowIrrelevant = true;
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
const byPath = new Map();
|
|
382
|
+
for (const row of selections) {
|
|
383
|
+
const legalDispositions = allowIrrelevant
|
|
384
|
+
? ["required", "relevant", "irrelevant"]
|
|
385
|
+
: ["required", "relevant"];
|
|
386
|
+
if (typeof row.path !== "string" ||
|
|
387
|
+
!legalDispositions.includes(row.disposition) ||
|
|
388
|
+
typeof row.rationale !== "string" ||
|
|
389
|
+
!row.rationale.trim()) {
|
|
390
|
+
throw new Error(`invalid selection row (path+${legalDispositions.join("|")}+non-empty rationale expected): ${JSON.stringify(row)}`);
|
|
386
391
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
unparseable = true;
|
|
390
|
-
continue;
|
|
392
|
+
if (!input.candidatePaths.includes(row.path)) {
|
|
393
|
+
throw new Error(`invalid selection ${row.path}: not a candidate path`);
|
|
391
394
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const section = typeof parsed.section === "string"
|
|
396
|
-
? parsed.section
|
|
397
|
-
: "";
|
|
398
|
-
const lineValue = parsed.line;
|
|
399
|
-
const citationLine = typeof lineValue === "number" && Number.isInteger(lineValue)
|
|
400
|
-
? lineValue
|
|
401
|
-
: null;
|
|
402
|
-
citations.push({ path: normalized, section, line: citationLine });
|
|
395
|
+
if (byPath.has(row.path))
|
|
396
|
+
throw new Error(`duplicate selection ${row.path}`);
|
|
397
|
+
byPath.set(row.path, row.disposition);
|
|
403
398
|
}
|
|
399
|
+
// Only explicitly required/relevant candidates must be read; everything
|
|
400
|
+
// unmentioned defaults to irrelevant. mandatoryPaths are intrinsic:
|
|
401
|
+
// they must be read even if the model omitted them, and they can never
|
|
402
|
+
// be downgraded by a declaration.
|
|
403
|
+
const mustRead = [
|
|
404
|
+
...input.mandatoryPaths,
|
|
405
|
+
...input.candidatePaths.filter((candidate) => byPath.get(candidate) === "required"),
|
|
406
|
+
];
|
|
407
|
+
return { ok: true, paths: [...new Set(mustRead)].sort() };
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
return {
|
|
411
|
+
ok: false,
|
|
412
|
+
reason: `openspec selector invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
async function readCommittedContractFacts(runDir, nodeId) {
|
|
417
|
+
const { readTypedEventStoreFromJsonl } = await import("./frontend-typed-event-store.js");
|
|
418
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, nodeId, "contract-typed-facts.jsonl"));
|
|
419
|
+
return records
|
|
420
|
+
.filter((record) => record.phase === "committed")
|
|
421
|
+
.map((record) => record.fact)
|
|
422
|
+
.filter((fact) => Boolean(fact));
|
|
423
|
+
}
|
|
424
|
+
async function readLegacyOpenspecSelection(input) {
|
|
425
|
+
let text;
|
|
426
|
+
try {
|
|
427
|
+
text = await readNodeText(input.runDir, input.selectionNodeId);
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
return {
|
|
431
|
+
ok: false,
|
|
432
|
+
reason: `openspec selector output unreadable: ${error instanceof Error ? error.message : String(error)}`,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const fenced = /```json\s*\n([\s\S]*?)\n```/.exec(text);
|
|
436
|
+
if (!fenced) {
|
|
437
|
+
return {
|
|
438
|
+
ok: false,
|
|
439
|
+
reason: "openspec selector emitted neither typed openspec-selection facts nor a legacy fenced JSON selection object",
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
const parsed = JSON.parse(fenced[1] ?? "");
|
|
444
|
+
if (parsed.schemaVersion !== 1 ||
|
|
445
|
+
parsed.schemaId !== "frontend-openspec-selection-v1" ||
|
|
446
|
+
!Array.isArray(parsed.selections)) {
|
|
447
|
+
throw new Error("schemaVersion/schemaId/selections invalid");
|
|
448
|
+
}
|
|
449
|
+
const rows = parsed.selections.map((row) => ({
|
|
450
|
+
path: String(row.path ?? ""),
|
|
451
|
+
disposition: String(row.disposition ?? ""),
|
|
452
|
+
rationale: String(row.rationale ?? ""),
|
|
453
|
+
}));
|
|
454
|
+
return { ok: true, selections: rows };
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
return {
|
|
458
|
+
ok: false,
|
|
459
|
+
reason: `openspec selector invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
460
|
+
};
|
|
404
461
|
}
|
|
405
|
-
return { blockPresent, unparseable, citations };
|
|
406
462
|
}
|
|
407
463
|
async function finalizePrewrite(input, pending) {
|
|
408
464
|
const sourceBindingSha256 = input.sourceBinding
|
|
@@ -453,162 +509,7 @@ async function finalizePrewrite(input, pending) {
|
|
|
453
509
|
openspecPolicy: pending.openspecPolicy,
|
|
454
510
|
};
|
|
455
511
|
}
|
|
456
|
-
|
|
457
|
-
* Source fidelity ledger 门禁检查(AC-FIDELITY-002/003/006):绑定携带 ledger 指针时,
|
|
458
|
-
* 依次核验 ledger 存在、绑定 hash 一致、inputDigest 新鲜、无 unresolved/conflict/
|
|
459
|
-
* 高风险未覆盖、canonical requirement 全覆盖、来源引用有效且非伪造。任一失败 fail closed。
|
|
460
|
-
*/
|
|
461
|
-
export async function checkFrontendSourceFidelityLedger(input) {
|
|
462
|
-
const { binding } = input;
|
|
463
|
-
// v1 绑定不携带 ledger 指针:非 frontend task / 无可解析引用 → 无 v2 校验路径。
|
|
464
|
-
if (!isDagSourceBindingV2(binding)) {
|
|
465
|
-
return { ok: true, failureCode: null, failureReason: null, ledger: null };
|
|
466
|
-
}
|
|
467
|
-
const absolute = resolveDagTaskSourcePath({
|
|
468
|
-
workspaceRoot: input.workspaceRoot,
|
|
469
|
-
taskId: binding.taskId,
|
|
470
|
-
sourcePath: binding.ledgerPath,
|
|
471
|
-
});
|
|
472
|
-
let raw;
|
|
473
|
-
try {
|
|
474
|
-
raw = await readFile(absolute, "utf8");
|
|
475
|
-
}
|
|
476
|
-
catch (error) {
|
|
477
|
-
return {
|
|
478
|
-
ok: false,
|
|
479
|
-
failureCode: "source-fidelity-ledger-missing",
|
|
480
|
-
failureReason: `frontend source fidelity ledger missing: ${binding.ledgerPath} (${error instanceof Error ? error.message : String(error)})`,
|
|
481
|
-
ledger: null,
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
const actualSha = createHash("sha256").update(raw).digest("hex");
|
|
485
|
-
if (actualSha !== binding.ledgerSha256) {
|
|
486
|
-
return {
|
|
487
|
-
ok: false,
|
|
488
|
-
failureCode: "source-fidelity-ledger-stale",
|
|
489
|
-
failureReason: `frontend source fidelity ledger stale or tampered: ${binding.ledgerPath} (bound ${binding.ledgerSha256}, observed ${actualSha})`,
|
|
490
|
-
ledger: null,
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
let ledger;
|
|
494
|
-
try {
|
|
495
|
-
ledger = parseLedgerJson(raw);
|
|
496
|
-
}
|
|
497
|
-
catch (error) {
|
|
498
|
-
return {
|
|
499
|
-
ok: false,
|
|
500
|
-
failureCode: "source-fidelity-ledger-stale",
|
|
501
|
-
failureReason: `frontend source fidelity ledger is not a valid v1 ledger: ${error instanceof Error ? error.message : String(error)}`,
|
|
502
|
-
ledger: null,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
if (ledger.inputDigest !== binding.inputDigest) {
|
|
506
|
-
return {
|
|
507
|
-
ok: false,
|
|
508
|
-
failureCode: "source-fidelity-ledger-stale",
|
|
509
|
-
failureReason: `frontend source fidelity ledger inputDigest mismatch: bound ${binding.inputDigest}, ledger ${ledger.inputDigest}`,
|
|
510
|
-
ledger,
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
const validation = validateRequirementLedger(ledger);
|
|
514
|
-
const firstError = (code) => validation.find((error) => error.code === code)?.message ?? null;
|
|
515
|
-
if (ledger.stats.unresolvedCount > 0 || validation.some((e) => e.code === "SOURCE_FIDELITY_UNRESOLVED")) {
|
|
516
|
-
return {
|
|
517
|
-
ok: false,
|
|
518
|
-
failureCode: "source-fidelity-unresolved",
|
|
519
|
-
failureReason: firstError("SOURCE_FIDELITY_UNRESOLVED") ??
|
|
520
|
-
`frontend source fidelity ledger has ${ledger.stats.unresolvedCount} unresolved fragment(s)`,
|
|
521
|
-
ledger,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
if (ledger.stats.conflictCount > 0 || validation.some((e) => e.code === "SOURCE_FIDELITY_CONFLICT")) {
|
|
525
|
-
return {
|
|
526
|
-
ok: false,
|
|
527
|
-
failureCode: "source-fidelity-conflict",
|
|
528
|
-
failureReason: firstError("SOURCE_FIDELITY_CONFLICT") ??
|
|
529
|
-
`frontend source fidelity ledger has ${ledger.stats.conflictCount} conflict(s)`,
|
|
530
|
-
ledger,
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
if (ledger.stats.highRiskUncoveredCount > 0 ||
|
|
534
|
-
validation.some((e) => e.code === "SOURCE_FIDELITY_HIGH_RISK_UNCOVERED")) {
|
|
535
|
-
return {
|
|
536
|
-
ok: false,
|
|
537
|
-
failureCode: "source-fidelity-high-risk-uncovered",
|
|
538
|
-
failureReason: firstError("SOURCE_FIDELITY_HIGH_RISK_UNCOVERED") ??
|
|
539
|
-
`frontend source fidelity ledger has ${ledger.stats.highRiskUncoveredCount} high-risk uncovered fragment(s)`,
|
|
540
|
-
ledger,
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
// AC-BLOCK-001:canonical requirement 缺少 fragment/sourceRef 证据(ledger 侧
|
|
544
|
-
// REQUIREMENT_NOT_COVERED)与来源引用/片段无效(SOURCE_REF_INVALID /
|
|
545
|
-
// FRAGMENT_INVALID)均为阻断性失败,结构化失败结果不得产生可继续执行的成功。
|
|
546
|
-
const notCoveredError = validation.find((e) => e.code === "SOURCE_FIDELITY_REQUIREMENT_NOT_COVERED");
|
|
547
|
-
if (notCoveredError) {
|
|
548
|
-
return {
|
|
549
|
-
ok: false,
|
|
550
|
-
failureCode: "source-fidelity-requirement-not-covered",
|
|
551
|
-
failureReason: notCoveredError.message,
|
|
552
|
-
ledger,
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
const refInvalidError = validation.find((e) => e.code === "SOURCE_FIDELITY_SOURCE_REF_INVALID" ||
|
|
556
|
-
e.code === "SOURCE_FIDELITY_FRAGMENT_INVALID");
|
|
557
|
-
if (refInvalidError) {
|
|
558
|
-
return {
|
|
559
|
-
ok: false,
|
|
560
|
-
failureCode: "source-fidelity-source-ref-invalid",
|
|
561
|
-
failureReason: refInvalidError.message,
|
|
562
|
-
ledger,
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
const ledgerIds = new Set(ledger.canonicalRequirements.map((requirement) => requirement.id));
|
|
566
|
-
const contractIds = new Set(input.contractRequirements.map((r) => r.id));
|
|
567
|
-
const uncoveredRequirements = [...ledgerIds].filter((id) => !contractIds.has(id));
|
|
568
|
-
if (uncoveredRequirements.length > 0) {
|
|
569
|
-
return {
|
|
570
|
-
ok: false,
|
|
571
|
-
failureCode: "source-fidelity-requirement-not-covered",
|
|
572
|
-
failureReason: `frontend contract requirements do not cover ledger canonical requirement ids: ${uncoveredRequirements.join(", ")}`,
|
|
573
|
-
ledger,
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
const fragmentById = new Map(ledger.fragments.map((fragment) => [fragment.id, fragment]));
|
|
577
|
-
for (const requirement of input.contractRequirements) {
|
|
578
|
-
if (!ledgerIds.has(requirement.id))
|
|
579
|
-
continue;
|
|
580
|
-
const declared = requirement.sourceFragmentIds ?? [];
|
|
581
|
-
if (declared.length === 0) {
|
|
582
|
-
return {
|
|
583
|
-
ok: false,
|
|
584
|
-
failureCode: "source-fidelity-source-ref-invalid",
|
|
585
|
-
failureReason: `frontend contract requirement ${requirement.id} lacks sourceFragmentIds for the bound ledger`,
|
|
586
|
-
ledger,
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
const boundFragments = new Set(ledger.canonicalRequirements.find((r) => r.id === requirement.id)
|
|
590
|
-
?.sourceFragmentIds ?? []);
|
|
591
|
-
for (const fragmentId of declared) {
|
|
592
|
-
if (!fragmentById.has(fragmentId)) {
|
|
593
|
-
return {
|
|
594
|
-
ok: false,
|
|
595
|
-
failureCode: "source-fidelity-source-ref-invalid",
|
|
596
|
-
failureReason: `frontend contract requirement ${requirement.id} references unknown fragment ${fragmentId}`,
|
|
597
|
-
ledger,
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
if (!boundFragments.has(fragmentId)) {
|
|
601
|
-
return {
|
|
602
|
-
ok: false,
|
|
603
|
-
failureCode: "source-fidelity-source-ref-invalid",
|
|
604
|
-
failureReason: `frontend contract requirement ${requirement.id} declares fabricated fragment reference ${fragmentId} (not bound in ledger)`,
|
|
605
|
-
ledger,
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
return { ok: true, failureCode: null, failureReason: null, ledger };
|
|
611
|
-
}
|
|
512
|
+
export { checkFrontendSourceFidelityLedger } from "./frontend-source-fidelity-ledger.js";
|
|
612
513
|
export async function runFrontendPrewriteGate(input) {
|
|
613
514
|
const workspaceRoot = input.workspaceRoot ?? input.repoRoot;
|
|
614
515
|
// selectNode throws for a scheduling defect (no FINISHED plan/review). That
|
|
@@ -657,9 +558,6 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
657
558
|
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
658
559
|
openspecPolicy,
|
|
659
560
|
};
|
|
660
|
-
const planText = [planArtifact.contractText, planArtifact.citationsText]
|
|
661
|
-
.filter(Boolean)
|
|
662
|
-
.join("\n");
|
|
663
561
|
let reviewText;
|
|
664
562
|
try {
|
|
665
563
|
reviewText = await readNodeText(input.runDir, reviewNodeId);
|
|
@@ -783,21 +681,6 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
783
681
|
});
|
|
784
682
|
}
|
|
785
683
|
}
|
|
786
|
-
// Size guard: a canonical contract above 64KB is a retryable-invalid defect
|
|
787
|
-
// (compress the contract) rather than a writer authorization failure.
|
|
788
|
-
const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8");
|
|
789
|
-
if (contractSizeBytes > FRONTEND_CONTRACT_MAX_BYTES) {
|
|
790
|
-
return finalizePrewrite(input, {
|
|
791
|
-
...basePending,
|
|
792
|
-
verdict,
|
|
793
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
794
|
-
normalizationActions: [],
|
|
795
|
-
mockStrategy: analysis.canonical.mockApi.strategy,
|
|
796
|
-
classification: "retryable-invalid",
|
|
797
|
-
failureReason: `frontend prewrite gate: canonical contract serialization is ${contractSizeBytes} bytes, exceeding the ${FRONTEND_CONTRACT_MAX_BYTES}-byte limit. 压缩契约(移除冗余 prose、重复目标或过长描述)后重试。`,
|
|
798
|
-
failureCode: "contract-too-large",
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
684
|
const mockStrategy = analysis.canonical.mockApi.strategy;
|
|
802
685
|
if (!input.config.allowedMockStrategies.includes(mockStrategy)) {
|
|
803
686
|
const quotedAllowed = input.config.allowedMockStrategies
|
|
@@ -869,14 +752,31 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
869
752
|
// Effective plan/review must actually read the candidate openspec paths when
|
|
870
753
|
// any exist. This fail-closes BEFORE the canonical contract is materialized
|
|
871
754
|
// so a writer is never authorized on top of unread normative evidence.
|
|
872
|
-
const
|
|
755
|
+
const frozenCandidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
756
|
+
const selection = await resolveRequiredOpenspecPaths({
|
|
757
|
+
runDir: input.runDir,
|
|
758
|
+
selectionNodeId: input.config.openspecSelectionNodeId,
|
|
759
|
+
candidatePaths: frozenCandidatePaths,
|
|
760
|
+
mandatoryPaths: input.config.openspecMandatoryPaths ?? [],
|
|
761
|
+
policy: openspecPolicy,
|
|
762
|
+
});
|
|
763
|
+
if (!selection.ok) {
|
|
764
|
+
return finalizePrewrite(input, {
|
|
765
|
+
...basePending, verdict, candidateJsonSha256: analysis.candidateJsonSha256,
|
|
766
|
+
normalizationActions: [], mockStrategy, classification: "retryable-invalid",
|
|
767
|
+
failureReason: `frontend prewrite gate: ${selection.reason}`,
|
|
768
|
+
failureCode: "openspec-selection-invalid",
|
|
769
|
+
openspecCandidatePaths: frozenCandidatePaths,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
const candidatePaths = selection.paths;
|
|
873
773
|
const candidateRoot = workspaceRoot ?? process.cwd();
|
|
874
774
|
// Drift precheck: candidates are frozen at generation time. A candidate that
|
|
875
775
|
// no longer exists on disk was deleted/renamed after generation and is a
|
|
876
776
|
// generation-contract defect, not an "unread" failure.
|
|
877
777
|
const missingCandidates = [];
|
|
878
|
-
if (
|
|
879
|
-
for (const candidate of
|
|
778
|
+
if (frozenCandidatePaths.length > 0) {
|
|
779
|
+
for (const candidate of frozenCandidatePaths) {
|
|
880
780
|
try {
|
|
881
781
|
await stat(path.join(candidateRoot, candidate));
|
|
882
782
|
}
|
|
@@ -899,99 +799,78 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
899
799
|
openspecCandidatePaths: candidatePaths,
|
|
900
800
|
});
|
|
901
801
|
}
|
|
902
|
-
|
|
903
|
-
if (
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
const
|
|
912
|
-
if (!
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
...new Set(parsed.citations
|
|
947
|
-
.filter((citation) => !readSet.has(citation.path))
|
|
948
|
-
.map((citation) => citation.path)),
|
|
949
|
-
];
|
|
950
|
-
if (uncorroborated.length > 0) {
|
|
951
|
-
return finalizePrewrite(input, {
|
|
952
|
-
...basePending,
|
|
953
|
-
verdict,
|
|
954
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
955
|
-
normalizationActions: [],
|
|
956
|
-
mockStrategy,
|
|
957
|
-
classification: "retryable-invalid",
|
|
958
|
-
failureReason: `frontend prewrite gate: these openspec paths were cited in the plan but have no successful read event in the effective plan/review: ${uncorroborated.join(", ")}`,
|
|
959
|
-
failureCode: "openspec-citation-not-read",
|
|
960
|
-
openspecReadPaths,
|
|
961
|
-
openspecCandidatePaths: candidatePaths,
|
|
962
|
-
});
|
|
963
|
-
}
|
|
802
|
+
const frozenSnapshots = input.config.openspecCandidateSnapshots;
|
|
803
|
+
if (frozenSnapshots) {
|
|
804
|
+
const snapshotByPath = new Map(frozenSnapshots.map((snapshot) => [snapshot.path, snapshot.sha256]));
|
|
805
|
+
const incompleteSnapshots = frozenCandidatePaths.filter((candidate) => !snapshotByPath.has(candidate));
|
|
806
|
+
const unexpectedSnapshots = frozenSnapshots
|
|
807
|
+
.map((snapshot) => snapshot.path)
|
|
808
|
+
.filter((candidate) => !frozenCandidatePaths.includes(candidate));
|
|
809
|
+
const changedCandidates = [];
|
|
810
|
+
for (const candidate of frozenCandidatePaths) {
|
|
811
|
+
const expected = snapshotByPath.get(candidate);
|
|
812
|
+
if (!expected)
|
|
813
|
+
continue;
|
|
814
|
+
const actual = createHash("sha256")
|
|
815
|
+
.update(await readFile(path.join(candidateRoot, candidate)))
|
|
816
|
+
.digest("hex");
|
|
817
|
+
if (actual !== expected)
|
|
818
|
+
changedCandidates.push(candidate);
|
|
819
|
+
}
|
|
820
|
+
if (incompleteSnapshots.length > 0 ||
|
|
821
|
+
unexpectedSnapshots.length > 0 ||
|
|
822
|
+
changedCandidates.length > 0) {
|
|
823
|
+
const details = [
|
|
824
|
+
...(incompleteSnapshots.length > 0
|
|
825
|
+
? [`missing snapshots: ${incompleteSnapshots.join(", ")}`]
|
|
826
|
+
: []),
|
|
827
|
+
...(unexpectedSnapshots.length > 0
|
|
828
|
+
? [`unexpected snapshots: ${unexpectedSnapshots.join(", ")}`]
|
|
829
|
+
: []),
|
|
830
|
+
...(changedCandidates.length > 0
|
|
831
|
+
? [`content changed: ${changedCandidates.join(", ")}`]
|
|
832
|
+
: []),
|
|
833
|
+
].join("; ");
|
|
834
|
+
return finalizePrewrite(input, {
|
|
835
|
+
...basePending,
|
|
836
|
+
verdict,
|
|
837
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
838
|
+
normalizationActions: [],
|
|
839
|
+
mockStrategy,
|
|
840
|
+
classification: "blocked",
|
|
841
|
+
failureReason: `frontend prewrite gate blocked: the generation-frozen OpenSpec content snapshot no longer matches the workspace (${details}). Re-read the changed specification and regenerate the DAG.`,
|
|
842
|
+
failureCode: "candidate-content-drift",
|
|
843
|
+
openspecReadPaths: [],
|
|
844
|
+
openspecCandidatePaths: candidatePaths,
|
|
845
|
+
});
|
|
964
846
|
}
|
|
965
|
-
// cited with empty candidates: no read enforcement. The generation step
|
|
966
|
-
// emits an advisory instead of failing the gate.
|
|
967
847
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
}
|
|
848
|
+
let openspecReadPaths;
|
|
849
|
+
openspecReadPaths = await collectOpenspecReadPaths({
|
|
850
|
+
runDir: input.runDir,
|
|
851
|
+
planNodeId,
|
|
852
|
+
reviewNodeId,
|
|
853
|
+
repoRoot: candidateRoot,
|
|
854
|
+
...(candidatePaths.length > 0 ? { candidatePaths } : {}),
|
|
855
|
+
});
|
|
856
|
+
if (candidatePaths.length > 0) {
|
|
857
|
+
const readSet = new Set(openspecReadPaths);
|
|
858
|
+
const unread = candidatePaths.filter((candidate) => !readSet.has(candidate));
|
|
859
|
+
if (unread.length > 0) {
|
|
860
|
+
const governanceRoot = await resolveOpenspecGovernanceRoot(candidateRoot);
|
|
861
|
+
const excludedGovernanceCount = await countExcludedGovernanceSpecFiles(candidateRoot, governanceRoot);
|
|
862
|
+
return finalizePrewrite(input, {
|
|
863
|
+
...basePending,
|
|
864
|
+
verdict,
|
|
865
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
866
|
+
normalizationActions: [],
|
|
867
|
+
mockStrategy,
|
|
868
|
+
classification: "retryable-invalid",
|
|
869
|
+
failureReason: `frontend prewrite gate: effective plan/review did not read these openspec candidate paths: ${unread.join(", ")}. Read each candidate and report applicable rules plus hit path/section/line number, conflicts, and missing specifications, then rerun. 实际成功读取: ${openspecReadPaths.length > 0 ? openspecReadPaths.join(", ") : "(none)"}; 候选总数: ${candidatePaths.length}; 排除治理目录 ${governanceRoot} 下 ${excludedGovernanceCount} 个文件(运行期诊断口径); 候选集在生成期冻结,若候选与本任务无关属于生成期契约配置问题,应修正后重新生成 DAG。`,
|
|
870
|
+
failureCode: "openspec-not-read",
|
|
871
|
+
openspecReadPaths,
|
|
872
|
+
openspecCandidatePaths: candidatePaths,
|
|
873
|
+
});
|
|
995
874
|
}
|
|
996
875
|
}
|
|
997
876
|
// Component selection spec conformance (ADR 0016). Deterministic cross-check
|
|
@@ -1013,13 +892,17 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
1013
892
|
normalizationActions: [],
|
|
1014
893
|
mockStrategy,
|
|
1015
894
|
classification: "retryable-invalid",
|
|
1016
|
-
failureReason: "frontend prewrite gate: the canonical contract has UI-visible work but uiComponentChoices is missing or empty while the generation-frozen component/theme specification bucket is non-empty. 为每个 UI 用途声明组件选型(decision: specified | reuse-existing | new
|
|
895
|
+
failureReason: "frontend prewrite gate: the canonical contract has UI-visible work but uiComponentChoices is missing or empty while the generation-frozen component/theme specification bucket is non-empty. 为每个 UI 用途声明组件选型(decision: specified | reuse-existing | new);specified 必须给 OpenSpec specReference.path/section/line,new 必须给任务源 PRD specReference.path/section/line。",
|
|
1017
896
|
failureCode: "component-choices-missing",
|
|
1018
897
|
openspecReadPaths,
|
|
1019
898
|
openspecCandidatePaths: candidatePaths,
|
|
1020
899
|
});
|
|
1021
900
|
}
|
|
901
|
+
// Only `specified` choices cite the frozen OpenSpec candidate bucket. A
|
|
902
|
+
// `new` choice instead cites its task-source/PRD declaration and must not be
|
|
903
|
+
// rejected merely because that source is outside the OpenSpec roots.
|
|
1022
904
|
const componentSpecReferences = componentChoices
|
|
905
|
+
.filter((choice) => choice.decision === "specified")
|
|
1023
906
|
.map((choice) => ({
|
|
1024
907
|
raw: choice.specReference?.path ?? "",
|
|
1025
908
|
normalized: choice.specReference?.path
|
|
@@ -1027,11 +910,37 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
1027
910
|
: null,
|
|
1028
911
|
}))
|
|
1029
912
|
.filter((ref) => ref.raw.length > 0);
|
|
913
|
+
if (componentSpecCandidates.length > 0 && hasUiVisibleWork) {
|
|
914
|
+
const componentCandidateSet = new Set(componentSpecCandidates);
|
|
915
|
+
const hasBoundSpecifiedChoice = componentChoices.some((choice) => {
|
|
916
|
+
if (choice.decision !== "specified" || !choice.specReference?.path) {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
const normalized = normalizeCitationPath(choice.specReference.path, candidateRoot);
|
|
920
|
+
return Boolean(normalized && componentCandidateSet.has(normalized));
|
|
921
|
+
});
|
|
922
|
+
if (!hasBoundSpecifiedChoice) {
|
|
923
|
+
return finalizePrewrite(input, {
|
|
924
|
+
...basePending,
|
|
925
|
+
verdict,
|
|
926
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
927
|
+
normalizationActions: [],
|
|
928
|
+
mockStrategy,
|
|
929
|
+
classification: "retryable-invalid",
|
|
930
|
+
failureReason: "frontend prewrite gate: UI-visible work has a generation-frozen component/theme specification bucket, but no uiComponentChoices decision=specified is bound to a path in that bucket. At least one declared UI purpose must use and cite the applicable component/theme specification; reuse-existing/new alone cannot bypass the bucket.",
|
|
931
|
+
failureCode: "component-spec-choice-unbound",
|
|
932
|
+
openspecReadPaths,
|
|
933
|
+
openspecCandidatePaths: candidatePaths,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
1030
937
|
if (componentSpecReferences.length > 0) {
|
|
1031
938
|
const invalidRefs = [
|
|
1032
939
|
...new Set(componentSpecReferences
|
|
1033
940
|
.filter((ref) => !ref.normalized ||
|
|
1034
|
-
!
|
|
941
|
+
!(candidatePaths.length > 0
|
|
942
|
+
? candidatePaths.includes(ref.normalized)
|
|
943
|
+
: isOpenspecSpecFilePath(ref.normalized)))
|
|
1035
944
|
.map((ref) => ref.raw)),
|
|
1036
945
|
];
|
|
1037
946
|
if (invalidRefs.length > 0) {
|
|
@@ -1049,13 +958,10 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
1049
958
|
});
|
|
1050
959
|
}
|
|
1051
960
|
if (openspecPolicy === "cited") {
|
|
1052
|
-
const parsedCitations = parseOpenspecCitations(planText, candidateRoot);
|
|
1053
|
-
const citedPaths = new Set(parsedCitations.citations.map((citation) => citation.path));
|
|
1054
961
|
const readSet = new Set(openspecReadPaths);
|
|
1055
962
|
const uncorroborated = [
|
|
1056
963
|
...new Set(componentSpecReferences
|
|
1057
964
|
.filter((ref) => !ref.normalized ||
|
|
1058
|
-
!citedPaths.has(ref.normalized) ||
|
|
1059
965
|
!readSet.has(ref.normalized))
|
|
1060
966
|
.map((ref) => ref.raw)),
|
|
1061
967
|
];
|
|
@@ -1067,7 +973,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
1067
973
|
normalizationActions: [],
|
|
1068
974
|
mockStrategy,
|
|
1069
975
|
classification: "retryable-invalid",
|
|
1070
|
-
failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths
|
|
976
|
+
failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths are missing a ledger specReference or lack a successful read event: ${uncorroborated.join(", ")}`,
|
|
1071
977
|
failureCode: "component-spec-not-cited",
|
|
1072
978
|
openspecReadPaths,
|
|
1073
979
|
openspecCandidatePaths: candidatePaths,
|