@tea-agent/loop-agent 0.39.0-beta.7 → 0.39.0-beta.9
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/AGENTS.md +1 -1
- package/CHANGELOG.md +23 -5
- package/README.md +2 -2
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +5 -0
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/dag-rerun.js +1 -1
- package/dist/commands/init.js +26 -0
- package/dist/executors/dag-pi-executor.js +326 -8
- package/dist/executors/pi-read-budget-policy.js +166 -0
- package/dist/executors/shell-executor.js +26 -30
- package/dist/shared/dag-prompt-override.js +4 -5
- package/dist/shared/update/npm-client.js +33 -11
- package/dist/task/config-types.js +15 -0
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +2 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +15 -1
- package/dist/task/contract/schema.js +2 -0
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/frontend-project-capability.js +123 -4
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +477 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/prepare.js +262 -1
- package/dist/task/source-prepare/semantic-intake.js +25 -5
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/worker/cli.js +12 -26
- package/dist/worker/console/console-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/operation-store.js +1 -1
- package/dist/worker/console/operator-actions.js +129 -4
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +3 -0
- package/dist/worker/console/server.js +73 -5
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-bPer1waP.js → abnfDiagram-N423BO3Z-D1-Z-BBa.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BLdkwucW.js → arc-q8qAYm5m.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Bw4wo4I1.js → architectureDiagram-T3A2C74G-CAu4_lve.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DUUkc0gr.js → blockDiagram-VBNYF7ZC-D5GhQvHm.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BC3O0nob.js → c4Diagram-5PPSVZJV-ClQjeLkO.js} +1 -1
- package/dist/worker/console/static/assets/channel-ClAuhSUL.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Dd4g2nti.js → chunk-2GRJ4B5K-DLqWlFbo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-s2EDm7-n.js → chunk-2Q5K7J3B-DkRT_usL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-GqtHM2yQ.js → chunk-5RXB4S5H-Cqp7Chi3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BzRFrN8j.js → chunk-5VM5RSS4-CedEdFB9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bw9nFjCJ.js → chunk-6Q2QTUOP-BHM8vP5x.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B4n-03Qm.js → chunk-GF5L2VYU-D7EFr377.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-Dt75ZdBD.js → chunk-JWPE2WC7-DR38mTWV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-B_afMhHB.js → chunk-KBJHAD2P-BFv2LsR3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Bd8Rqqg9.js → chunk-RYQCIY6F-BAvpJS5z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-9boOMji4.js → chunk-XXDRQBXY-eKI9TO72.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-cBgeCpVU.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-cBgeCpVU.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BtCycfCc.js → cose-bilkent-JH36ORCC-BMJXpgo4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-eTOts9L1.js → cynefin-VYW2F7L2-CKJ2ofZx.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cibd2mjg.js → cynefinDiagram-MW4NZA55-CsKnsazm.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CDjDZtDV.js → dagre-VZM6K2ZE-DVQ46vIw.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Btexn-JJ.js → diagram-7IWD3JNH-D_RuonYS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cyl8sdpM.js → diagram-B4RE2ZJO-D2Mt0nMu.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Ev-83E1t.js → diagram-LBJQPF4R-DjBDuye6.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-41LqE0Mc.js → diagram-Q27KOJAE-8VZkqpE_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BC39T5R3.js → diagram-UB23O5K3-D0izYxia.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DzcNRuri.js → ebnfDiagram-BXEA7PRR-CncG2I95.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-B5Frvvnz.js → erDiagram-JOGREHBK-DiBUUXR8.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BapxROIM.js → flowDiagram-UKHOOZJN-Dtc0fArb.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cd4d4wQ0.js → ganttDiagram-PKOTCBZU-BPNFP5OZ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CrL6buFW.js → gitGraphDiagram-DS77QQ5N-CsgpxEEd.js} +1 -1
- package/dist/worker/console/static/assets/{index-DBh_x_Ek.js → index-Bai8Z25q.js} +81 -81
- package/dist/worker/console/static/assets/index-CH0Tr5hY.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BrSiNxyd.js → infoDiagram-6WML65LV-kHGxkJUS.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-gvlvY-n9.js → ishikawaDiagram-WSZJBQD7-CbLe9wrr.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-YaPzRQ.js → journeyDiagram-NVQOT4AX-BOkwxAcP.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BsmDv9E_.js → kanban-definition-27J2QSJJ-BHYQ2rDu.js} +1 -1
- package/dist/worker/console/static/assets/{linear-UWaoDN3h.js → linear-CHT4ujob.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BlvpGVG8.js → mermaid.core-yL8sfqnY.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CUCIP5CP.js → mindmap-definition-FAOFIHXS-DZx1coV-.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CB1aQUAK.js → pegDiagram-VL7TDLO6-DStWRCI8.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BwehHxhf.js → pieDiagram-7S7Q4E2Y-DSKZkwzK.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CFz30APB.js → quadrantDiagram-CIZ2JOQS-CmoEGOuW.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwNmw1i6.js → railroadDiagram-AXF67PYL-BLqzhKyq.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-jg-tPXlI.js → requirementDiagram-LRYGKXZP-gbHeUabM.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BlyReDKl.js → sankeyDiagram-W5VNT64P-CaKkBCKO.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DBUzW1f3.js → sequenceDiagram-SI44F4Z6-PUbi3Upu.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BYcI2RAr.js → sizeCapture-X5ZJPWSS-dm7S9Qfe.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-D7AiyJdo.js → stateDiagram-OKZ733FA-C7WSzv4p.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-sQDv5AP3.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BHGt7Kkv.js → swimlanes-SLNWSIFB-CBKCiQwd.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BXwsmcPs.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-OPjooWcX.js → timeline-definition-Z64GVDOM-CvnUlylk.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-ggsWDvrO.js → vennDiagram-T6HMQDX7-xM0StY1O.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DjNe-BjZ.js → wardleyDiagram-T6FBY63Y-CWtJMagy.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Byp63Hei.js → xychartDiagram-ELKLHX3M-BO_-MeK8.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +40 -1
- package/dist/worker/console/static-src/app/useOperatorActions.js +5 -0
- package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observe/node-input.js +88 -2
- package/dist/worker/observe/routes.js +188 -0
- package/dist/worker/observe/static/api.js +3 -3
- package/dist/worker/observe/static/format.js +37 -0
- package/dist/worker/observe/static/index.html +5 -0
- package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- package/dist/worker/observe/static/state.js +2 -2
- package/dist/worker/observe/static/styles.css +331 -35
- package/dist/worker/observe/static/views/dag-inspector.js +509 -78
- package/dist/worker/observe/static/views/task.js +55 -0
- package/dist/workflows/dag/frontend-design-policy.js +19 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +117 -50
- package/dist/workflows/dag/frontend-plan-render.js +27 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +45 -1
- package/dist/workflows/dag/frontend-recovery-controller.js +1 -0
- package/dist/workflows/dag/frontend-review-context.js +102 -15
- package/dist/workflows/dag/frontend-review-findings.js +42 -10
- package/dist/workflows/dag/frontend-shadow-dual-write.js +36 -2
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-worktree-diff.js +154 -0
- package/dist/workflows/dag/frontend-writer-admission.js +12 -0
- package/dist/workflows/dag/init-hybrid.js +297 -65
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +111 -1
- package/dist/workflows/dag/prompt.js +34 -1
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/rerun-feedback.js +31 -8
- 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 +34 -9
- package/dist/workflows/dag/runner.js +83 -16
- package/dist/workflows/dag/types.js +67 -11
- package/docs/README.md +1 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +4 -0
- package/docs/templates/README.md +2 -1
- package/docs/templates/frontend-implementation-contract.schema.json +684 -24
- 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 +8 -14
- package/skills/frontend-design-review/SKILL.md +20 -41
- package/skills/frontend-implementation/SKILL.md +2 -3
- package/skills/frontend-implementation/references/design-spec.md +4 -3
- package/skills/frontend-implementation/references/node-contracts.md +4 -3
- package/skills/frontend-review/SKILL.md +22 -14
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/loop-agent/references/hybrid-dag.md +7 -0
- package/dist/worker/console/static/assets/channel-AbFtVnFm.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-n1yf2Jcv.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-n1yf2Jcv.js +0 -1
- package/dist/worker/console/static/assets/index-DwOGauxU.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BBSH5c8m.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D2F7-U4H.js +0 -8
|
@@ -5,6 +5,8 @@ import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-s
|
|
|
5
5
|
import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "./frontend-worktree-diff.js";
|
|
6
6
|
import { frontendLintAssessmentArtifactSchema } from "./frontend-lint-baseline.js";
|
|
7
7
|
import { deterministicSha256, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
|
|
8
|
+
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
9
|
+
import { ledgerStatsLine, parseLedgerJson } from "../../task/source-prepare/ledger.js";
|
|
8
10
|
export const FRONTEND_REVIEW_CONTEXT_SCHEMA_ID = "frontend-review-context-v1";
|
|
9
11
|
/**
|
|
10
12
|
* Deterministically assemble the cumulative diff from a task baseline and the
|
|
@@ -20,7 +22,8 @@ export function assembleCumulativeDiffContext(input) {
|
|
|
20
22
|
...currentByPath.keys(),
|
|
21
23
|
]);
|
|
22
24
|
const sortedPaths = [...allPaths].sort();
|
|
23
|
-
const perPath = sortedPaths
|
|
25
|
+
const perPath = sortedPaths
|
|
26
|
+
.map((p) => {
|
|
24
27
|
const baselineSha256 = baselineByPath.get(p) ?? null;
|
|
25
28
|
const hasCurrent = currentByPath.has(p);
|
|
26
29
|
const currentSha256 = hasCurrent
|
|
@@ -34,10 +37,17 @@ export function assembleCumulativeDiffContext(input) {
|
|
|
34
37
|
status = hasCurrent ? "deleted" : "unchanged";
|
|
35
38
|
}
|
|
36
39
|
else {
|
|
37
|
-
status =
|
|
40
|
+
status =
|
|
41
|
+
currentSha256 === baselineSha256 ? "unchanged" : "modified";
|
|
38
42
|
}
|
|
39
43
|
return { path: p, status, baselineSha256, currentSha256 };
|
|
40
|
-
})
|
|
44
|
+
})
|
|
45
|
+
// Cumulative diff carries only actual changes. The worktree baseline is
|
|
46
|
+
// the full repository inventory (git ls-files -co), so an unfiltered
|
|
47
|
+
// perPath enumerates thousands of unchanged hashes (e.g. 3000+ entries
|
|
48
|
+
// ≈ 700KB), ballooning the review context and blowing the review node
|
|
49
|
+
// read budget on a single read. Unchanged entries add no diff evidence.
|
|
50
|
+
.filter((entry) => entry.status !== "unchanged");
|
|
41
51
|
const baselineDigest = deterministicSha256([...input.baselineEntries].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
42
52
|
const cumulativeDigest = deterministicSha256(perPath);
|
|
43
53
|
return {
|
|
@@ -45,7 +55,7 @@ export function assembleCumulativeDiffContext(input) {
|
|
|
45
55
|
baselineDigest,
|
|
46
56
|
perPath,
|
|
47
57
|
cumulativeDigest,
|
|
48
|
-
empty: perPath.
|
|
58
|
+
empty: perPath.length === 0,
|
|
49
59
|
};
|
|
50
60
|
}
|
|
51
61
|
async function sha256File(filePath) {
|
|
@@ -154,12 +164,31 @@ export function compareTypedReviewToLegacyJsonVerdict(input) {
|
|
|
154
164
|
: `typed review verdict ${typedVerdict} maps to ${expected} but legacy JSON verdict is ${legacyVerdict}`,
|
|
155
165
|
};
|
|
156
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* review-context 稳定失败枚举(AC-011 / AC-HARD-005):v2 drift / manifest 失败返回
|
|
169
|
+
* 结构化、可消费的 failure code(稳定枚举 + reason 的 envelope),既有调用方无需字符串
|
|
170
|
+
* 匹配裸 Error 文本。message 保持人类可读(shell stderr 兼容),code 供机械化路由。
|
|
171
|
+
*/
|
|
172
|
+
export const FRONTEND_REVIEW_CONTEXT_FAILURE_CODES = [
|
|
173
|
+
"review-context-invalid-contract",
|
|
174
|
+
"review-context-verification-unbound",
|
|
175
|
+
"review-context-ledger-drift",
|
|
176
|
+
"review-context-manifest-missing",
|
|
177
|
+
];
|
|
178
|
+
export class FrontendReviewContextFailure extends Error {
|
|
179
|
+
code;
|
|
180
|
+
constructor(code, message, options) {
|
|
181
|
+
super(message, options);
|
|
182
|
+
this.name = "FrontendReviewContextFailure";
|
|
183
|
+
this.code = code;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
157
186
|
async function readRequiredJson(runDir, relativePath) {
|
|
158
187
|
try {
|
|
159
188
|
return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
|
|
160
189
|
}
|
|
161
|
-
catch {
|
|
162
|
-
throw new
|
|
190
|
+
catch (error) {
|
|
191
|
+
throw new FrontendReviewContextFailure("review-context-manifest-missing", `frontend review context missing or invalid ${relativePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
163
192
|
}
|
|
164
193
|
}
|
|
165
194
|
async function readOptionalLintAssessment(runDir) {
|
|
@@ -211,8 +240,9 @@ async function readOptionalVerifyFailure(runDir) {
|
|
|
211
240
|
}
|
|
212
241
|
function assertReviewEvidence(input) {
|
|
213
242
|
const contract = frontendImplementationContractSchema.safeParse(input.contract);
|
|
214
|
-
if (!contract.success)
|
|
215
|
-
throw new
|
|
243
|
+
if (!contract.success) {
|
|
244
|
+
throw new FrontendReviewContextFailure("review-context-invalid-contract", "frontend review context invalid implementation contract");
|
|
245
|
+
}
|
|
216
246
|
const trace = input.verificationTrace;
|
|
217
247
|
const expectedTargets = new Map(contract.data.verificationTargets.map((target) => [target.id, target]));
|
|
218
248
|
const actualTargets = Array.isArray(trace?.targets) ? trace.targets : [];
|
|
@@ -248,17 +278,15 @@ function assertReviewEvidence(input) {
|
|
|
248
278
|
actualIds.size !== expectedTargets.size ||
|
|
249
279
|
!mockEvidenceBound ||
|
|
250
280
|
trace.mockStrategy !== contract.data.mockApi.strategy) {
|
|
251
|
-
throw new
|
|
281
|
+
throw new FrontendReviewContextFailure("review-context-verification-unbound", "frontend review context verification trace is not bound to a passing contract");
|
|
252
282
|
}
|
|
253
|
-
// M8: no repair-assessment hard requirement. When the artifact exists it must
|
|
254
|
-
// be schema-valid; when absent the verify failure capsule (if any) is
|
|
255
|
-
// already surfaced through the payload as verifyFailure.
|
|
256
283
|
}
|
|
257
284
|
export async function runFrontendReviewContextGate(input) {
|
|
258
285
|
const contract = await readRequiredJson(input.runDir, "contracts/frontend-implementation-contract.json");
|
|
259
286
|
const parsedContract = frontendImplementationContractSchema.safeParse(contract);
|
|
260
|
-
if (!parsedContract.success)
|
|
261
|
-
throw new
|
|
287
|
+
if (!parsedContract.success) {
|
|
288
|
+
throw new FrontendReviewContextFailure("review-context-invalid-contract", "frontend review context invalid implementation contract");
|
|
289
|
+
}
|
|
262
290
|
const diff = await runFrontendWorktreeDiffGate({
|
|
263
291
|
...input,
|
|
264
292
|
authorizedChangedPaths: parsedContract.data.targets.files,
|
|
@@ -267,12 +295,15 @@ export async function runFrontendReviewContextGate(input) {
|
|
|
267
295
|
const repairAssessment = await readOptionalRepairAssessment(input.runDir);
|
|
268
296
|
const verifyFailure = await readOptionalVerifyFailure(input.runDir);
|
|
269
297
|
const lintAssessment = await readOptionalLintAssessment(input.runDir);
|
|
270
|
-
assertReviewEvidence({ contract, verificationTrace });
|
|
271
298
|
const cumulativeDiff = await captureCumulativeDiffContext({
|
|
272
299
|
runDir: input.runDir,
|
|
273
300
|
workspaceRoot: input.workspaceRoot,
|
|
274
301
|
diff,
|
|
275
302
|
});
|
|
303
|
+
assertReviewEvidence({ contract, verificationTrace, repairAssessment });
|
|
304
|
+
// Source fidelity ledger 核对(AC-008 / AC-FIDELITY-008):ledger 缺失/漂移即阻断;
|
|
305
|
+
// 同时把核对清单注入 review-context 供最终 reviewer 机械化核对。
|
|
306
|
+
const sourceFidelity = await buildReviewSourceFidelityChecklist(input, parsedContract.data);
|
|
276
307
|
const payload = {
|
|
277
308
|
schemaVersion: 1,
|
|
278
309
|
schemaId: FRONTEND_REVIEW_CONTEXT_SCHEMA_ID,
|
|
@@ -281,10 +312,13 @@ export async function runFrontendReviewContextGate(input) {
|
|
|
281
312
|
...(repairAssessment ? { repairAssessment } : {}),
|
|
282
313
|
...(verifyFailure ? { verifyFailure } : {}),
|
|
283
314
|
...(lintAssessment ? { lintAssessment } : {}),
|
|
315
|
+
sourceFidelity,
|
|
284
316
|
diff: {
|
|
285
317
|
schemaId: diff.schemaId,
|
|
286
318
|
patchPath: diff.patchPath,
|
|
287
319
|
patchSha256: diff.patchSha256,
|
|
320
|
+
reviewSummaryPath: diff.reviewSummaryPath,
|
|
321
|
+
reviewIndexPath: diff.reviewIndexPath,
|
|
288
322
|
changedFiles: diff.changedFiles,
|
|
289
323
|
untrackedFiles: diff.untrackedFiles,
|
|
290
324
|
empty: diff.empty,
|
|
@@ -294,6 +328,59 @@ export async function runFrontendReviewContextGate(input) {
|
|
|
294
328
|
const artifactPath = await writeDagRunJsonArtifact(input.runDir, "contracts/frontend-review-context.json", payload);
|
|
295
329
|
return { ok: true, artifactPath, diff };
|
|
296
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* 最终 review 核对清单(机械化):ledger 缺失/漂移、canonical requirement 漏项、
|
|
333
|
+
* diff 越界、验证证据缺失均阻断 pass。清单以有界安全摘要注入提示词,不泄漏原文。
|
|
334
|
+
*/
|
|
335
|
+
async function buildReviewSourceFidelityChecklist(input, contract) {
|
|
336
|
+
const binding = contract.sourceBinding;
|
|
337
|
+
const checklist = [];
|
|
338
|
+
// 契约级 sourceBinding 的 v2 语义:四个 ledger 字段必须齐全(缺失即 v1 遗留绑定)。
|
|
339
|
+
const boundV2 = Boolean(binding.ledgerPath) &&
|
|
340
|
+
Boolean(binding.ledgerSha256) &&
|
|
341
|
+
Boolean(binding.inputDigest) &&
|
|
342
|
+
binding.requirementToFragments !== undefined;
|
|
343
|
+
if (!boundV2) {
|
|
344
|
+
return {
|
|
345
|
+
bound: false,
|
|
346
|
+
ledger: null,
|
|
347
|
+
checklist: [
|
|
348
|
+
"source fidelity ledger: not bound (v1 binding); 按现有语义评审",
|
|
349
|
+
"canonical contract 与实际 diff 逐项核对,验证证据齐全后 verdict 可为 pass",
|
|
350
|
+
],
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const absolute = resolveDagTaskSourcePath({
|
|
354
|
+
workspaceRoot: input.workspaceRoot,
|
|
355
|
+
taskId: contract.sourceBinding.taskId,
|
|
356
|
+
sourcePath: binding.ledgerPath,
|
|
357
|
+
});
|
|
358
|
+
let ledger = null;
|
|
359
|
+
try {
|
|
360
|
+
const raw = await readFile(absolute, "utf8");
|
|
361
|
+
const actualSha = createHash("sha256").update(raw).digest("hex");
|
|
362
|
+
if (actualSha !== binding.ledgerSha256) {
|
|
363
|
+
throw new Error(`ledger sha256 mismatch (bound ${binding.ledgerSha256}, observed ${actualSha})`);
|
|
364
|
+
}
|
|
365
|
+
ledger = parseLedgerJson(raw);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
// AC-011 / AC-HARD-005:v2 drift 返回结构化 failure code(稳定枚举 + reason),
|
|
369
|
+
// 不再裸 throw new Error;message 保持人类可读供 shell stderr。
|
|
370
|
+
throw new FrontendReviewContextFailure("review-context-ledger-drift", `frontend review context source fidelity ledger unavailable or drifted: ${error instanceof Error ? error.message : String(error)}`);
|
|
371
|
+
}
|
|
372
|
+
checklist.push("ledger: 存在且绑定 sha256 一致(无缺失/漂移)", `ledger 覆盖统计: ${ledgerStatsLine(ledger)}`, "canonical contract requirements 必须覆盖 ledger 全部 canonical requirement IDs", "实际 diff 必须限制在契约 targets 内(越界即阻断 pass)", "验证证据必须绑定契约 verificationTargets(缺失即阻断 pass)", "仅满足派生摘要而遗漏 ledger requirement 时 verdict 不得为 pass");
|
|
373
|
+
return {
|
|
374
|
+
bound: true,
|
|
375
|
+
ledger: {
|
|
376
|
+
path: binding.ledgerPath,
|
|
377
|
+
sha256: binding.ledgerSha256,
|
|
378
|
+
inputDigest: binding.inputDigest,
|
|
379
|
+
stats: ledgerStatsLine(ledger),
|
|
380
|
+
},
|
|
381
|
+
checklist,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
297
384
|
export function formatFrontendReviewContextStdout(result) {
|
|
298
385
|
return [
|
|
299
386
|
"Frontend review context: pass",
|
|
@@ -10,10 +10,17 @@ import { readTypedEventStoreFromJsonl, } from "./frontend-typed-event-store.js";
|
|
|
10
10
|
* rendered into a downstream-task PRD draft that a later DAG round can consume
|
|
11
11
|
* instead of repeating the previous flow. Rendering is deterministic (pure);
|
|
12
12
|
* only the JSONL read and PRD write are IO.
|
|
13
|
+
*
|
|
14
|
+
* The design-review analog (`request_design_changes`) reuses the same PRD
|
|
15
|
+
* rendering and typed-fact channel: a design rejection is a definite finding
|
|
16
|
+
* (not a protocol/environment error), so it auto-re-enters the next plan round
|
|
17
|
+
* with the committed design findings instead of terminating the run.
|
|
13
18
|
*/
|
|
14
19
|
export const FRONTEND_REVIEW_FINDINGS_PRD_REL = "contracts/frontend-review-findings-prd.md";
|
|
15
20
|
/** Review node id whose committed typed facts drive the findings PRD. */
|
|
16
21
|
export const FRONTEND_REVIEW_NODE_ID = "frontend-review-pi";
|
|
22
|
+
/** Design review node id whose committed typed facts drive the findings PRD. */
|
|
23
|
+
export const FRONTEND_DESIGN_REVIEW_NODE_ID = "frontend-design-review-pi";
|
|
17
24
|
/** Deterministic issueCategory → failure owner mapping (A+B AC-009 §4.10). */
|
|
18
25
|
export function classifyReviewIssueCategoryFailureOwner(issueCategory) {
|
|
19
26
|
switch (issueCategory) {
|
|
@@ -122,10 +129,34 @@ export function deriveFrontendReviewFixScope(fact) {
|
|
|
122
129
|
* approve_review, missing store, or an unreadable store).
|
|
123
130
|
*/
|
|
124
131
|
export async function readCommittedReviewRequestFact(input) {
|
|
132
|
+
const { runDir: reviewRunDir } = input;
|
|
125
133
|
const reviewNodeId = input.reviewNodeId ?? FRONTEND_REVIEW_NODE_ID;
|
|
134
|
+
return readCommittedRequestFactByKind({
|
|
135
|
+
runDir: reviewRunDir,
|
|
136
|
+
nodeId: reviewNodeId,
|
|
137
|
+
factsFile: "review-typed-facts.jsonl",
|
|
138
|
+
kind: "request_review_changes",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Read the committed `request_design_changes` fact from the design review
|
|
143
|
+
* node's typed event store. Returns undefined when no committed request fact
|
|
144
|
+
* exists (e.g. approve_design, missing store, or an unreadable store).
|
|
145
|
+
*/
|
|
146
|
+
export async function readCommittedDesignRequestFact(input) {
|
|
147
|
+
const { runDir: designRunDir } = input;
|
|
148
|
+
const designNodeId = input.designNodeId ?? FRONTEND_DESIGN_REVIEW_NODE_ID;
|
|
149
|
+
return readCommittedRequestFactByKind({
|
|
150
|
+
runDir: designRunDir,
|
|
151
|
+
nodeId: designNodeId,
|
|
152
|
+
factsFile: "design-typed-facts.jsonl",
|
|
153
|
+
kind: "request_design_changes",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async function readCommittedRequestFactByKind(input) {
|
|
126
157
|
let records;
|
|
127
158
|
try {
|
|
128
|
-
records = await readTypedEventStoreFromJsonl(path.join(input.runDir,
|
|
159
|
+
records = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.nodeId, input.factsFile));
|
|
129
160
|
}
|
|
130
161
|
catch {
|
|
131
162
|
return undefined;
|
|
@@ -133,9 +164,7 @@ export async function readCommittedReviewRequestFact(input) {
|
|
|
133
164
|
const committed = records.filter((record) => record.phase === "committed");
|
|
134
165
|
if (committed.length === 0)
|
|
135
166
|
return undefined;
|
|
136
|
-
|
|
137
|
-
// request_review_changes carries a "not passed" verdict.
|
|
138
|
-
const request = committed.find((record) => record.fact.kind === "request_review_changes");
|
|
167
|
+
const request = committed.find((record) => record.fact.kind === input.kind);
|
|
139
168
|
if (!request)
|
|
140
169
|
return undefined;
|
|
141
170
|
const fact = request.fact;
|
|
@@ -166,25 +195,28 @@ function findingLines(findings) {
|
|
|
166
195
|
}
|
|
167
196
|
/**
|
|
168
197
|
* Deterministically render a downstream-task PRD draft from a committed
|
|
169
|
-
* `request_review_changes` fact. Same fact always
|
|
170
|
-
* output; the parent run id and review node id are the
|
|
198
|
+
* `request_review_changes` / `request_design_changes` fact. Same fact always
|
|
199
|
+
* renders byte-identical output; the parent run id and review node id are the
|
|
200
|
+
* only context.
|
|
171
201
|
*/
|
|
172
202
|
export function renderFrontendReviewFindingsPrd(input) {
|
|
173
203
|
const { parentRunId, fact } = input;
|
|
174
|
-
const
|
|
204
|
+
const isDesign = fact.kind === "request_design_changes";
|
|
205
|
+
const reviewNodeId = input.reviewNodeId ??
|
|
206
|
+
(isDesign ? FRONTEND_DESIGN_REVIEW_NODE_ID : FRONTEND_REVIEW_NODE_ID);
|
|
175
207
|
const restartPhase = classifyReviewIssueCategoryFailureOwner(fact.issueCategory);
|
|
176
208
|
const scope = deriveFrontendReviewFixScope(fact);
|
|
177
209
|
const targetSummary = scope.targetFiles.length > 0
|
|
178
210
|
? scope.targetFiles.join(", ")
|
|
179
211
|
: fact.issueCategory;
|
|
180
212
|
return [
|
|
181
|
-
`# 修复 review 发现(${scope.shape}):${targetSummary}`,
|
|
213
|
+
`# 修复 ${isDesign ? "设计" : "review"} 发现(${scope.shape}):${targetSummary}`,
|
|
182
214
|
"",
|
|
183
215
|
"## 背景",
|
|
184
216
|
"",
|
|
185
217
|
`- parentRunId: \`${parentRunId}\``,
|
|
186
218
|
`- reviewNodeId: \`${reviewNodeId}\``,
|
|
187
|
-
`- typedVerdict:
|
|
219
|
+
`- typedVerdict: \`${fact.kind}\``,
|
|
188
220
|
`- issueCategory: \`${fact.issueCategory}\``,
|
|
189
221
|
`- restartPhase/failureOwner: \`${restartPhase}\``,
|
|
190
222
|
"",
|
|
@@ -216,7 +248,7 @@ export function renderFrontendReviewFindingsPrd(input) {
|
|
|
216
248
|
"",
|
|
217
249
|
`- 按 issueCategory 映射的重启阶段(${restartPhase})定点修复,不复用上一轮未通过的实现。`,
|
|
218
250
|
"- 逐条关闭上面「问题清单」中的 findings,并保留上一轮已通过的验证目标继续有效。",
|
|
219
|
-
|
|
251
|
+
`- 完成修复后重新走 frontend DAG 的 ${isDesign ? "plan → design" : "implement → verify → review"} 链,直到${isDesign ? " design-review 提交 approve_design" : " review 提交 approve_review"}。`,
|
|
220
252
|
"",
|
|
221
253
|
].join("\n");
|
|
222
254
|
}
|
|
@@ -270,7 +270,7 @@ export function restorePlanPatchFromCommittedFacts(records) {
|
|
|
270
270
|
* writeSet-external compile authority (`frontend-implementation-contract.ts`)
|
|
271
271
|
* can still restore a plain `patch` object.
|
|
272
272
|
*/
|
|
273
|
-
export function assemblePlanPatchFromCommittedFacts(records) {
|
|
273
|
+
export function assemblePlanPatchFromCommittedFacts(records, contractRequirements) {
|
|
274
274
|
const facts = records
|
|
275
275
|
.filter((record) => record.phase === "committed")
|
|
276
276
|
.map((record) => record.fact)
|
|
@@ -344,7 +344,41 @@ export function assemblePlanPatchFromCommittedFacts(records) {
|
|
|
344
344
|
return { ...entry, expectedOutcome: `implied by contract: ${id}` };
|
|
345
345
|
}
|
|
346
346
|
return entry;
|
|
347
|
-
})
|
|
347
|
+
})
|
|
348
|
+
// Source fidelity ledger (AC-005/AC-006): the contract node declared the
|
|
349
|
+
// authoritative sourceFragmentIds/sourceRefs for each requirement from
|
|
350
|
+
// the ledger's requirement→fragment mapping. The plan inherits them by
|
|
351
|
+
// requirement id so the compiled canonical contract carries the same
|
|
352
|
+
// provenance evidence without the plan re-deriving (or fabricating) it.
|
|
353
|
+
.map((entry) => {
|
|
354
|
+
if (!contractRequirements)
|
|
355
|
+
return entry;
|
|
356
|
+
const id = asString(entry.id);
|
|
357
|
+
if (!id)
|
|
358
|
+
return entry;
|
|
359
|
+
const inherited = contractRequirements.get(id);
|
|
360
|
+
if (!inherited)
|
|
361
|
+
return entry;
|
|
362
|
+
const merged = { ...entry };
|
|
363
|
+
// Inherit the contract-declared provenance only when the plan did
|
|
364
|
+
// not already declare it (model override wins; missing inherits).
|
|
365
|
+
if (!Array.isArray(entry.sourceFragmentIds)) {
|
|
366
|
+
if (inherited.sourceFragmentIds) {
|
|
367
|
+
merged.sourceFragmentIds = inherited.sourceFragmentIds;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (!Array.isArray(entry.sourceRefs)) {
|
|
371
|
+
if (inherited.sourceRefs) {
|
|
372
|
+
merged.sourceRefs = inherited.sourceRefs;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return merged;
|
|
376
|
+
})
|
|
377
|
+
// A requirement id is a canonical identity; drop duplicates so the
|
|
378
|
+
// compiled patch never carries the same requirement twice (the
|
|
379
|
+
// record_plan_requirement tool rejects re-records, this dedupes any
|
|
380
|
+
// legacy/edge duplicates at the compile authority).
|
|
381
|
+
.filter((entry, index, all) => all.findIndex((other) => asString(other.id) === asString(entry.id)) === index);
|
|
348
382
|
if (requirements.length > 0)
|
|
349
383
|
patch.requirements = requirements;
|
|
350
384
|
const verificationTargets = facts
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
4
|
+
import { parseLedgerJson, validateRequirementLedger, } from "../../task/source-prepare/ledger.js";
|
|
5
|
+
import { isDagSourceBindingV2 } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Validate the semantic source-fidelity ledger binding for a canonical frontend
|
|
8
|
+
* contract. This is deliberately shared: the compatibility prewrite gate may
|
|
9
|
+
* report a structured prewrite failure, while the generated production DAG
|
|
10
|
+
* must deny writer admission on exactly the same evidence.
|
|
11
|
+
*/
|
|
12
|
+
export async function checkFrontendSourceFidelityLedger(input) {
|
|
13
|
+
const { binding } = input;
|
|
14
|
+
if (!isDagSourceBindingV2(binding)) {
|
|
15
|
+
return { ok: true, failureCode: null, failureReason: null, ledger: null };
|
|
16
|
+
}
|
|
17
|
+
const absolute = resolveDagTaskSourcePath({
|
|
18
|
+
workspaceRoot: input.workspaceRoot,
|
|
19
|
+
taskId: binding.taskId,
|
|
20
|
+
sourcePath: binding.ledgerPath,
|
|
21
|
+
});
|
|
22
|
+
let raw;
|
|
23
|
+
try {
|
|
24
|
+
raw = await readFile(absolute, "utf8");
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
failureCode: "source-fidelity-ledger-missing",
|
|
30
|
+
failureReason: `frontend source fidelity ledger missing: ${binding.ledgerPath} (${error instanceof Error ? error.message : String(error)})`,
|
|
31
|
+
ledger: null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const actualSha = createHash("sha256").update(raw).digest("hex");
|
|
35
|
+
if (actualSha !== binding.ledgerSha256) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
failureCode: "source-fidelity-ledger-stale",
|
|
39
|
+
failureReason: `frontend source fidelity ledger stale or tampered: ${binding.ledgerPath} (bound ${binding.ledgerSha256}, observed ${actualSha})`,
|
|
40
|
+
ledger: null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
let ledger;
|
|
44
|
+
try {
|
|
45
|
+
ledger = parseLedgerJson(raw);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
failureCode: "source-fidelity-ledger-stale",
|
|
51
|
+
failureReason: `frontend source fidelity ledger is not a valid v1 ledger: ${error instanceof Error ? error.message : String(error)}`,
|
|
52
|
+
ledger: null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (ledger.inputDigest !== binding.inputDigest) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
failureCode: "source-fidelity-ledger-stale",
|
|
59
|
+
failureReason: `frontend source fidelity ledger inputDigest mismatch: bound ${binding.inputDigest}, ledger ${ledger.inputDigest}`,
|
|
60
|
+
ledger,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const validation = validateRequirementLedger(ledger);
|
|
64
|
+
const firstError = (code) => validation.find((error) => error.code === code)?.message ?? null;
|
|
65
|
+
const invalid = (failureCode, failureReason) => ({ ok: false, failureCode, failureReason, ledger });
|
|
66
|
+
if (ledger.stats.unresolvedCount > 0 || validation.some((e) => e.code === "SOURCE_FIDELITY_UNRESOLVED")) {
|
|
67
|
+
return invalid("source-fidelity-unresolved", firstError("SOURCE_FIDELITY_UNRESOLVED") ?? `frontend source fidelity ledger has ${ledger.stats.unresolvedCount} unresolved fragment(s)`);
|
|
68
|
+
}
|
|
69
|
+
if (ledger.stats.conflictCount > 0 || validation.some((e) => e.code === "SOURCE_FIDELITY_CONFLICT")) {
|
|
70
|
+
return invalid("source-fidelity-conflict", firstError("SOURCE_FIDELITY_CONFLICT") ?? `frontend source fidelity ledger has ${ledger.stats.conflictCount} conflict(s)`);
|
|
71
|
+
}
|
|
72
|
+
if (ledger.stats.highRiskUncoveredCount > 0 || validation.some((e) => e.code === "SOURCE_FIDELITY_HIGH_RISK_UNCOVERED")) {
|
|
73
|
+
return invalid("source-fidelity-high-risk-uncovered", firstError("SOURCE_FIDELITY_HIGH_RISK_UNCOVERED") ?? `frontend source fidelity ledger has ${ledger.stats.highRiskUncoveredCount} high-risk uncovered fragment(s)`);
|
|
74
|
+
}
|
|
75
|
+
const notCoveredError = validation.find((e) => e.code === "SOURCE_FIDELITY_REQUIREMENT_NOT_COVERED");
|
|
76
|
+
if (notCoveredError) {
|
|
77
|
+
return invalid("source-fidelity-requirement-not-covered", notCoveredError.message);
|
|
78
|
+
}
|
|
79
|
+
const refInvalidError = validation.find((e) => e.code === "SOURCE_FIDELITY_SOURCE_REF_INVALID" || e.code === "SOURCE_FIDELITY_FRAGMENT_INVALID");
|
|
80
|
+
if (refInvalidError) {
|
|
81
|
+
return invalid("source-fidelity-source-ref-invalid", refInvalidError.message);
|
|
82
|
+
}
|
|
83
|
+
const ledgerIds = new Set(ledger.canonicalRequirements.map((requirement) => requirement.id));
|
|
84
|
+
const contractIds = new Set(input.contractRequirements.map((r) => r.id));
|
|
85
|
+
const uncoveredRequirements = [...ledgerIds].filter((id) => !contractIds.has(id));
|
|
86
|
+
if (uncoveredRequirements.length > 0) {
|
|
87
|
+
return invalid("source-fidelity-requirement-not-covered", `frontend contract requirements do not cover ledger canonical requirement ids: ${uncoveredRequirements.join(", ")}`);
|
|
88
|
+
}
|
|
89
|
+
const fragmentById = new Map(ledger.fragments.map((fragment) => [fragment.id, fragment]));
|
|
90
|
+
for (const requirement of input.contractRequirements) {
|
|
91
|
+
if (!ledgerIds.has(requirement.id))
|
|
92
|
+
continue;
|
|
93
|
+
const declared = requirement.sourceFragmentIds ?? [];
|
|
94
|
+
if (declared.length === 0) {
|
|
95
|
+
return invalid("source-fidelity-source-ref-invalid", `frontend contract requirement ${requirement.id} lacks sourceFragmentIds for the bound ledger`);
|
|
96
|
+
}
|
|
97
|
+
const boundFragments = new Set(ledger.canonicalRequirements.find((r) => r.id === requirement.id)?.sourceFragmentIds ?? []);
|
|
98
|
+
for (const fragmentId of declared) {
|
|
99
|
+
if (!fragmentById.has(fragmentId)) {
|
|
100
|
+
return invalid("source-fidelity-source-ref-invalid", `frontend contract requirement ${requirement.id} references unknown fragment ${fragmentId}`);
|
|
101
|
+
}
|
|
102
|
+
if (!boundFragments.has(fragmentId)) {
|
|
103
|
+
return invalid("source-fidelity-source-ref-invalid", `frontend contract requirement ${requirement.id} declares fabricated fragment reference ${fragmentId} (not bound in ledger)`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { ok: true, failureCode: null, failureReason: null, ledger };
|
|
108
|
+
}
|
|
@@ -5,6 +5,10 @@ import { runResilientGitCommand } from "../../shared/resilient-git.js";
|
|
|
5
5
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
6
6
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
7
7
|
export const FRONTEND_WORKTREE_DIFF_SCHEMA_ID = "frontend-worktree-diff-v1";
|
|
8
|
+
export const FRONTEND_REVIEW_DIFF_INDEX_SCHEMA_ID = "frontend-review-diff-index-v1";
|
|
9
|
+
const MAX_REVIEW_DIFF_SUMMARY_ENTRIES = 48;
|
|
10
|
+
/** Maximum diff payload per review artifact; metadata adds a small fixed header. */
|
|
11
|
+
const MAX_REVIEW_DIFF_FRAGMENT_BYTES = 20 * 1024;
|
|
8
12
|
export const FRONTEND_WORKTREE_BASELINE_SCHEMA_ID = "frontend-worktree-baseline-v1";
|
|
9
13
|
async function runGit(cwd, args) {
|
|
10
14
|
const result = await runResilientGitCommand({ cwd, args, readOnly: true });
|
|
@@ -56,6 +60,98 @@ function splitLines(text) {
|
|
|
56
60
|
.map((line) => line.trim())
|
|
57
61
|
.filter(Boolean);
|
|
58
62
|
}
|
|
63
|
+
function buildReviewDiffSummary(input) {
|
|
64
|
+
const files = [...input.hunks.reduce((grouped, hunk) => {
|
|
65
|
+
const existing = grouped.get(hunk.path);
|
|
66
|
+
if (existing) {
|
|
67
|
+
existing.bytes += hunk.bytes;
|
|
68
|
+
existing.lastPatchPath = hunk.patchPath;
|
|
69
|
+
return grouped;
|
|
70
|
+
}
|
|
71
|
+
grouped.set(hunk.path, {
|
|
72
|
+
path: hunk.path,
|
|
73
|
+
bytes: hunk.bytes,
|
|
74
|
+
parts: hunk.parts,
|
|
75
|
+
firstPatchPath: hunk.patchPath,
|
|
76
|
+
lastPatchPath: hunk.patchPath,
|
|
77
|
+
});
|
|
78
|
+
return grouped;
|
|
79
|
+
}, new Map()).values()];
|
|
80
|
+
const listedFiles = files.slice(0, MAX_REVIEW_DIFF_SUMMARY_ENTRIES);
|
|
81
|
+
const remainingFiles = files.length - listedFiles.length;
|
|
82
|
+
const listedUntracked = input.untrackedFiles.slice(0, MAX_REVIEW_DIFF_SUMMARY_ENTRIES);
|
|
83
|
+
const remainingUntracked = input.untrackedFiles.length - listedUntracked.length;
|
|
84
|
+
return [
|
|
85
|
+
"# Frontend review diff summary",
|
|
86
|
+
"",
|
|
87
|
+
"Read this bounded summary first. Do not read artifacts/diff_patch.patch in full; it is retained only as the canonical audit artifact.",
|
|
88
|
+
"For a semantic finding, read only the matching per-file diff fragment below and then the current source file when necessary. Read fragments in part order when a file has more than one.",
|
|
89
|
+
"",
|
|
90
|
+
`Canonical patch SHA-256: ${input.patchSha256}`,
|
|
91
|
+
`Canonical patch bytes: ${input.patchBytes}`,
|
|
92
|
+
`Tracked changed files: ${files.length}`,
|
|
93
|
+
`Review diff fragments: ${input.hunks.length}`,
|
|
94
|
+
`Untracked files (inventory only): ${input.untrackedFiles.length}`,
|
|
95
|
+
"",
|
|
96
|
+
"## Tracked file diff fragment references",
|
|
97
|
+
...(listedFiles.length > 0
|
|
98
|
+
? listedFiles.map((file) => file.parts === 1
|
|
99
|
+
? `- ${file.path} — ${file.bytes} bytes — read ${file.firstPatchPath} only if this file needs review`
|
|
100
|
+
: `- ${file.path} — ${file.bytes} bytes across ${file.parts} ordered fragments — read ${file.firstPatchPath} through ${file.lastPatchPath} only if this file needs review`)
|
|
101
|
+
: ["- (none)"]),
|
|
102
|
+
...(remainingFiles > 0
|
|
103
|
+
? [`- … ${remainingFiles} additional tracked files are recorded in contracts/frontend-review-diff-index.json; do not bulk-read them.`]
|
|
104
|
+
: []),
|
|
105
|
+
"",
|
|
106
|
+
"## Untracked inventory",
|
|
107
|
+
...(listedUntracked.length > 0
|
|
108
|
+
? listedUntracked.map((file) => `- ${file}`)
|
|
109
|
+
: ["- (none)"]),
|
|
110
|
+
...(remainingUntracked > 0
|
|
111
|
+
? [`- … ${remainingUntracked} additional untracked files are recorded in contracts/frontend-review-diff-index.json.`]
|
|
112
|
+
: []),
|
|
113
|
+
"",
|
|
114
|
+
].join("\n");
|
|
115
|
+
}
|
|
116
|
+
function splitReviewDiffFragments(text) {
|
|
117
|
+
const fragments = [];
|
|
118
|
+
let current = "";
|
|
119
|
+
for (const line of text.match(/[^\n]*\n|[^\n]+/g) ?? []) {
|
|
120
|
+
const lineBytes = Buffer.byteLength(line);
|
|
121
|
+
if (current.length > 0 &&
|
|
122
|
+
Buffer.byteLength(current) + lineBytes >
|
|
123
|
+
MAX_REVIEW_DIFF_FRAGMENT_BYTES) {
|
|
124
|
+
fragments.push(current);
|
|
125
|
+
current = "";
|
|
126
|
+
}
|
|
127
|
+
if (lineBytes <= MAX_REVIEW_DIFF_FRAGMENT_BYTES) {
|
|
128
|
+
current += line;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Generated/minified files can produce a single diff line larger than the
|
|
132
|
+
// review budget. Split by Unicode code point so no fragment exceeds the
|
|
133
|
+
// byte limit while preserving the original patch bytes when concatenated.
|
|
134
|
+
let oversizedLineFragment = "";
|
|
135
|
+
let oversizedLineFragmentBytes = 0;
|
|
136
|
+
for (const codePoint of line) {
|
|
137
|
+
const codePointBytes = Buffer.byteLength(codePoint);
|
|
138
|
+
if (oversizedLineFragmentBytes > 0 &&
|
|
139
|
+
oversizedLineFragmentBytes + codePointBytes >
|
|
140
|
+
MAX_REVIEW_DIFF_FRAGMENT_BYTES) {
|
|
141
|
+
fragments.push(oversizedLineFragment);
|
|
142
|
+
oversizedLineFragment = "";
|
|
143
|
+
oversizedLineFragmentBytes = 0;
|
|
144
|
+
}
|
|
145
|
+
oversizedLineFragment += codePoint;
|
|
146
|
+
oversizedLineFragmentBytes += codePointBytes;
|
|
147
|
+
}
|
|
148
|
+
if (oversizedLineFragment)
|
|
149
|
+
fragments.push(oversizedLineFragment);
|
|
150
|
+
}
|
|
151
|
+
if (current.length > 0)
|
|
152
|
+
fragments.push(current);
|
|
153
|
+
return fragments.length > 0 ? fragments : [""];
|
|
154
|
+
}
|
|
59
155
|
async function sha256File(filePath) {
|
|
60
156
|
try {
|
|
61
157
|
const bytes = await readFile(filePath);
|
|
@@ -206,11 +302,65 @@ export async function runFrontendWorktreeDiffGate(input) {
|
|
|
206
302
|
const absolutePatch = path.join(input.runDir, ...relativePatch.split("/"));
|
|
207
303
|
await mkdir(path.dirname(absolutePatch), { recursive: true });
|
|
208
304
|
await writeFile(absolutePatch, patchText, "utf8");
|
|
305
|
+
// Keep the complete patch as immutable audit evidence, but create a bounded
|
|
306
|
+
// reviewer entrypoint plus one file-scoped artifact per tracked change. This
|
|
307
|
+
// prevents final review from loading a multi-thousand-line patch into a
|
|
308
|
+
// single Pi session merely to inspect one or two relevant files.
|
|
309
|
+
const reviewHunks = [];
|
|
310
|
+
const reviewHunksDir = path.join(input.runDir, "artifacts", "diff-hunks");
|
|
311
|
+
await mkdir(reviewHunksDir, { recursive: true });
|
|
312
|
+
for (const [index, file] of changedFiles.entries()) {
|
|
313
|
+
const filePatch = await run(root, ["diff", "--binary", "HEAD", "--", file]);
|
|
314
|
+
if (filePatch.code !== 0) {
|
|
315
|
+
throw new Error(`frontend worktree diff hunk failed for ${file}: ${filePatch.stderr.trim() || filePatch.stdout.trim()}`);
|
|
316
|
+
}
|
|
317
|
+
const text = filePatch.stdout.endsWith("\n") || filePatch.stdout.length === 0
|
|
318
|
+
? filePatch.stdout
|
|
319
|
+
: `${filePatch.stdout}\n`;
|
|
320
|
+
const fragments = splitReviewDiffFragments(text);
|
|
321
|
+
for (const [partIndex, fragment] of fragments.entries()) {
|
|
322
|
+
const part = partIndex + 1;
|
|
323
|
+
const patchPath = `artifacts/diff-hunks/${String(index + 1).padStart(3, "0")}-${String(part).padStart(3, "0")}.patch`;
|
|
324
|
+
const artifact = [
|
|
325
|
+
"# Frontend review diff fragment",
|
|
326
|
+
`# path: ${file}`,
|
|
327
|
+
`# part: ${part}/${fragments.length}`,
|
|
328
|
+
"",
|
|
329
|
+
fragment,
|
|
330
|
+
].join("\n");
|
|
331
|
+
await writeFile(path.join(input.runDir, ...patchPath.split("/")), artifact, "utf8");
|
|
332
|
+
reviewHunks.push({
|
|
333
|
+
path: file,
|
|
334
|
+
patchPath,
|
|
335
|
+
sha256: createHash("sha256").update(artifact).digest("hex"),
|
|
336
|
+
bytes: Buffer.byteLength(artifact),
|
|
337
|
+
part,
|
|
338
|
+
parts: fragments.length,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const reviewIndexPath = "contracts/frontend-review-diff-index.json";
|
|
343
|
+
await writeDagRunJsonArtifact(input.runDir, reviewIndexPath, {
|
|
344
|
+
schemaVersion: 1,
|
|
345
|
+
schemaId: FRONTEND_REVIEW_DIFF_INDEX_SCHEMA_ID,
|
|
346
|
+
patchSha256,
|
|
347
|
+
hunks: reviewHunks,
|
|
348
|
+
untrackedFiles,
|
|
349
|
+
});
|
|
350
|
+
const reviewSummaryPath = "artifacts/diff_summary.md";
|
|
351
|
+
await writeFile(path.join(input.runDir, ...reviewSummaryPath.split("/")), buildReviewDiffSummary({
|
|
352
|
+
patchSha256,
|
|
353
|
+
patchBytes: Buffer.byteLength(patchText),
|
|
354
|
+
hunks: reviewHunks,
|
|
355
|
+
untrackedFiles,
|
|
356
|
+
}), "utf8");
|
|
209
357
|
const payload = {
|
|
210
358
|
schemaVersion: 1,
|
|
211
359
|
schemaId: FRONTEND_WORKTREE_DIFF_SCHEMA_ID,
|
|
212
360
|
patchPath: relativePatch,
|
|
213
361
|
patchSha256,
|
|
362
|
+
reviewSummaryPath,
|
|
363
|
+
reviewIndexPath,
|
|
214
364
|
changedFiles,
|
|
215
365
|
untrackedFiles,
|
|
216
366
|
empty: changedFiles.length === 0 && untrackedFiles.length === 0,
|
|
@@ -223,6 +373,8 @@ export async function runFrontendWorktreeDiffGate(input) {
|
|
|
223
373
|
patchPath: relativePatch,
|
|
224
374
|
patchAbsolutePath: absolutePatch,
|
|
225
375
|
patchSha256,
|
|
376
|
+
reviewSummaryPath,
|
|
377
|
+
reviewIndexPath,
|
|
226
378
|
changedFiles,
|
|
227
379
|
untrackedFiles,
|
|
228
380
|
empty: payload.empty,
|
|
@@ -235,6 +387,8 @@ export function formatFrontendWorktreeDiffStdout(result) {
|
|
|
235
387
|
`schemaId=${result.schemaId}`,
|
|
236
388
|
`patchPath=${result.patchPath}`,
|
|
237
389
|
`patchSha256=${result.patchSha256}`,
|
|
390
|
+
`reviewSummaryPath=${result.reviewSummaryPath}`,
|
|
391
|
+
`reviewIndexPath=${result.reviewIndexPath}`,
|
|
238
392
|
`changedFiles=${result.changedFiles.length}`,
|
|
239
393
|
`untrackedFiles=${result.untrackedFiles.length}`,
|
|
240
394
|
`empty=${result.empty ? "true" : "false"}`,
|