@tea-agent/loop-agent 0.40.0-next.2 → 0.40.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +8 -2
  3. package/README.md +2 -2
  4. package/dist/application/task-lifecycle/advance.js +12 -0
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/update/runtime-activity.js +1 -29
  7. package/dist/commands/dag-rerun.js +1 -1
  8. package/dist/shared/dag-prompt-override.js +4 -5
  9. package/dist/shared/update/npm-client.js +33 -11
  10. package/dist/task/contract/apply.js +11 -1
  11. package/dist/task/contract/constants.js +2 -0
  12. package/dist/task/contract/hash.js +3 -0
  13. package/dist/task/contract/observe.js +25 -4
  14. package/dist/task/contract/paths.js +2 -1
  15. package/dist/task/contract/project.js +15 -1
  16. package/dist/task/contract/schema.js +2 -0
  17. package/dist/task/contract/transaction.js +12 -0
  18. package/dist/task/source-prepare/completeness.js +188 -0
  19. package/dist/task/source-prepare/fragment-inventory.js +474 -0
  20. package/dist/task/source-prepare/index.js +3 -0
  21. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  22. package/dist/task/source-prepare/ledger-review.js +214 -0
  23. package/dist/task/source-prepare/ledger.js +545 -0
  24. package/dist/task/source-prepare/prepare.js +262 -1
  25. package/dist/task/source-prepare/semantic-intake.js +19 -3
  26. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  27. package/dist/task/source-prepare/types.js +22 -0
  28. package/dist/worker/cli.js +12 -26
  29. package/dist/worker/console/console-handoff.js +82 -0
  30. package/dist/worker/console/console-update-and-init.js +115 -0
  31. package/dist/worker/console/console-update-runtime.js +84 -0
  32. package/dist/worker/console/draft-store.js +49 -0
  33. package/dist/worker/console/operation-store.js +1 -1
  34. package/dist/worker/console/operator-actions.js +129 -4
  35. package/dist/worker/console/operator-surface-health.js +1 -0
  36. package/dist/worker/console/prd-intake-bridge.js +13 -0
  37. package/dist/worker/console/routes.js +3 -0
  38. package/dist/worker/console/server.js +73 -5
  39. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DLMldIE8.js → abnfDiagram-N423BO3Z-CTx1TrNa.js} +1 -1
  40. package/dist/worker/console/static/assets/{arc-DVOg1dSW.js → arc-B4i9cVxX.js} +1 -1
  41. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-715v7joo.js → architectureDiagram-T3A2C74G-BAXE8_Rl.js} +1 -1
  42. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BtMEGtAQ.js → blockDiagram-VBNYF7ZC-C5Xh59at.js} +1 -1
  43. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-qUxXsdxu.js → c4Diagram-5PPSVZJV-B_asOzAu.js} +1 -1
  44. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +1 -0
  45. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BYMag72f.js → chunk-2GRJ4B5K-wRO_iRNc.js} +1 -1
  46. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-C5jt5rtx.js → chunk-2Q5K7J3B-ByOaSh_G.js} +1 -1
  47. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DvZghfS2.js → chunk-5RXB4S5H-DjLzudan.js} +1 -1
  48. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CXutxAmY.js → chunk-5VM5RSS4-JCNGkXYT.js} +1 -1
  49. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BkANIDWk.js → chunk-6Q2QTUOP-BhrPXXMZ.js} +1 -1
  50. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bwy1wwlr.js → chunk-GF5L2VYU-TJ5JuEBT.js} +1 -1
  51. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3pWYVCG.js → chunk-JWPE2WC7-VVthn_mD.js} +1 -1
  52. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DRzjzmFD.js → chunk-KBJHAD2P-D60-Mny4.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C4xXDxXL.js → chunk-RYQCIY6F-zXv7aVye.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BngIZ90b.js → chunk-XXDRQBXY-By9VQoi1.js} +1 -1
  55. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +1 -0
  56. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +1 -0
  57. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-B40ACoFn.js → cose-bilkent-JH36ORCC-C4zQLe-M.js} +1 -1
  58. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BE-1ZRAI.js → cynefin-VYW2F7L2-wh5N2ReN.js} +1 -1
  59. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BoYbwVTh.js → cynefinDiagram-MW4NZA55-Dlu5wIqs.js} +1 -1
  60. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-B5-KA0fK.js → dagre-VZM6K2ZE-CrDgthc6.js} +1 -1
  61. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DJpCToT5.js → diagram-7IWD3JNH-zN1clg27.js} +1 -1
  62. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-De8R04Ng.js → diagram-B4RE2ZJO-BN-o7J7-.js} +1 -1
  63. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BU1N8DiA.js → diagram-LBJQPF4R-C8SuPt0d.js} +1 -1
  64. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BUUUzSYI.js → diagram-Q27KOJAE-BoizuHbZ.js} +1 -1
  65. package/dist/worker/console/static/assets/{diagram-UB23O5K3-zLoT1RC4.js → diagram-UB23O5K3-BzUzXixG.js} +1 -1
  66. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-_q4-4N8c.js → ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js} +1 -1
  67. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CTqzoNSL.js → erDiagram-JOGREHBK-GALUzRbs.js} +1 -1
  68. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMKSYKJM.js → flowDiagram-UKHOOZJN-CZJN65Sb.js} +1 -1
  69. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Dj6U8Sb7.js → ganttDiagram-PKOTCBZU-JLB9OESl.js} +1 -1
  70. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BfoxVYky.js → gitGraphDiagram-DS77QQ5N-CcvCr4GD.js} +1 -1
  71. package/dist/worker/console/static/assets/{index-CA0nQEbb.js → index-CW88Fjbw.js} +81 -81
  72. package/dist/worker/console/static/assets/index-DP0ZJl39.css +1 -0
  73. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-T5NfvHGV.js → infoDiagram-6WML65LV-HG5z7aVg.js} +1 -1
  74. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BEhUMkut.js → ishikawaDiagram-WSZJBQD7-BHEu9chR.js} +1 -1
  75. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-B2Ld6nNt.js → journeyDiagram-NVQOT4AX-Cp_Up8-a.js} +1 -1
  76. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-nV9T85yS.js → kanban-definition-27J2QSJJ-DvVeY-em.js} +1 -1
  77. package/dist/worker/console/static/assets/{linear-CIO1zDYh.js → linear-aGmcjgTn.js} +1 -1
  78. package/dist/worker/console/static/assets/{mermaid.core-B_ucAqLq.js → mermaid.core-Biyo-tqA.js} +5 -5
  79. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BIA3VNcZ.js → mindmap-definition-FAOFIHXS-DyxY2pIb.js} +1 -1
  80. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CNzLjNV3.js → pegDiagram-VL7TDLO6-CkdGuc50.js} +1 -1
  81. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-PWytzfc_.js → pieDiagram-7S7Q4E2Y-NNQI9xVR.js} +1 -1
  82. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CS0wGoIq.js → quadrantDiagram-CIZ2JOQS-PAJkuz_G.js} +1 -1
  83. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-C3Wsqph8.js → railroadDiagram-AXF67PYL-ZwrQod0O.js} +1 -1
  84. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DF6FDTYg.js → requirementDiagram-LRYGKXZP-D3ZfrzM2.js} +1 -1
  85. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BdQUJm6y.js → sankeyDiagram-W5VNT64P-DDGFmWk5.js} +1 -1
  86. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-Bv9rppGg.js → sequenceDiagram-SI44F4Z6-DO6mmXTA.js} +1 -1
  87. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-C3Hpvb_B.js → sizeCapture-X5ZJPWSS-DMbCQiJt.js} +1 -1
  88. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CZOWLpno.js → stateDiagram-OKZ733FA-Di_YUEfC.js} +1 -1
  89. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +1 -0
  90. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CPQRIZx5.js → swimlanes-SLNWSIFB-Bn2PphNJ.js} +2 -2
  91. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +8 -0
  92. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CkwM9BgA.js → timeline-definition-Z64GVDOM-DPh8pRsp.js} +1 -1
  93. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-wGSoDzYc.js → vennDiagram-T6HMQDX7-GMfl26pW.js} +1 -1
  94. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-GNVas2gx.js → wardleyDiagram-T6FBY63Y-DTlmVrmF.js} +1 -1
  95. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-C5USnHw7.js → xychartDiagram-ELKLHX3M-YGBjKAS5.js} +1 -1
  96. package/dist/worker/console/static/index.html +2 -2
  97. package/dist/worker/console/static-src/app/console-types.js +40 -1
  98. package/dist/worker/console/static-src/app/useOperatorActions.js +5 -0
  99. package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
  100. package/dist/worker/observability/init-runtime-activity.js +26 -0
  101. package/dist/worker/observe/node-input.js +88 -2
  102. package/dist/worker/observe/routes.js +188 -0
  103. package/dist/worker/observe/static/api.js +3 -3
  104. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  105. package/dist/worker/observe/static/dag-node-purpose.js +203 -0
  106. package/dist/worker/observe/static/format.js +37 -0
  107. package/dist/worker/observe/static/index.html +5 -0
  108. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  109. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  110. package/dist/worker/observe/static/state.js +2 -2
  111. package/dist/worker/observe/static/styles.css +338 -35
  112. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  113. package/dist/worker/observe/static/views/dag-inspector.js +531 -78
  114. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  115. package/dist/worker/observe/static/views/dag.js +4 -3
  116. package/dist/worker/observe/static/views/task.js +55 -0
  117. package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
  118. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  119. package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
  120. package/dist/workflows/dag/frontend-review-context.js +92 -10
  121. package/dist/workflows/dag/init-hybrid.js +125 -4
  122. package/dist/workflows/dag/node-execution.js +45 -0
  123. package/dist/workflows/dag/types.js +42 -11
  124. package/docs/README.md +1 -0
  125. package/docs/architecture/evolution.md +45 -0
  126. package/docs/init-surface.manifest.json +1 -0
  127. package/docs/templates/README.md +1 -1
  128. package/docs/templates/frontend-implementation-contract.schema.json +681 -24
  129. package/package.json +1 -1
  130. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  131. package/dist/worker/console/static/assets/channel-BG6TLen_.js +0 -1
  132. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-GuyUqs0h.js +0 -1
  133. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-GuyUqs0h.js +0 -1
  134. package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
  135. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dgsvn63q.js +0 -1
  136. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DaNfG1BJ.js +0 -8
@@ -0,0 +1,13 @@
1
+ export interface DagNodeRemark {
2
+ failed: boolean;
3
+ text: string;
4
+ isErrorSummary: boolean;
5
+ }
6
+
7
+ export function resolveNodeRemark(node: {
8
+ status?: string | null;
9
+ errorPreview?: string | null;
10
+ label?: string | null;
11
+ nodeId: string;
12
+ rank?: string | number | null;
13
+ } | null | undefined): DagNodeRemark;
@@ -70,6 +70,7 @@ import {
70
70
  dashboardVisibleFeatures,
71
71
  findDagForRun,
72
72
  } from "../dag-model.js";
73
+ import { describeDagNodePurpose } from "../dag-node-purpose.js";
73
74
  import { layoutDag } from "../dag-layout.js";
74
75
  import {
75
76
  showView,
@@ -136,7 +137,7 @@ export function extractFirstErrorSummary(errorPreview) {
136
137
  return effective ?? "";
137
138
  }
138
139
 
139
- /** Resolve 备注 cell content for a DAG node (failed → summary; else label). */
140
+ /** Resolve 备注 cell content for a DAG node (failed → summary; else purpose). */
140
141
  export function resolveNodeRemark(node) {
141
142
  const failed = isNodeFailed(node?.status);
142
143
  if (failed) {
@@ -147,10 +148,10 @@ export function resolveNodeRemark(node) {
147
148
  isErrorSummary: Boolean(summary),
148
149
  };
149
150
  }
150
- const label = typeof node?.label === "string" ? node.label.trim() : "";
151
+ const purpose = describeDagNodePurpose(node?.nodeId, node);
151
152
  return {
152
153
  failed: false,
153
- text: label || "—",
154
+ text: purpose || "—",
154
155
  isErrorSummary: false,
155
156
  };
156
157
  }
@@ -34,6 +34,7 @@ import {
34
34
  repoBaseName,
35
35
  parseMarkdownBlocks,
36
36
  markdownLinkHref,
37
+ formatLedgerSummary,
37
38
  } from "../format.js";
38
39
  import { parseRoute, navigate, buildHashPath } from "../router.js";
39
40
  import {
@@ -94,6 +95,7 @@ export async function renderTaskDetail(featureId, taskId) {
94
95
  const linksEl = document.getElementById("task-links");
95
96
  const evidenceEl = document.getElementById("task-evidence");
96
97
  const runsEl = document.getElementById("task-runs");
98
+ const ledgerEl = document.getElementById("task-ledger");
97
99
  if (!metaEl || !linksEl || !evidenceEl || !runsEl) return false;
98
100
  if (titleEl) titleEl.textContent = displayLabel;
99
101
  mountViewState(metaEl, "loading", "加载 Task…");
@@ -252,9 +254,62 @@ export async function renderTaskDetail(featureId, taskId) {
252
254
  const active = ["running", "pending", "blocked", "stale"].includes(
253
255
  (task.status ?? "").toLowerCase(),
254
256
  );
257
+ if (ledgerEl) {
258
+ void renderTaskLedger(ledgerEl, taskId);
259
+ }
255
260
  return active;
256
261
  }
257
262
 
263
+ /**
264
+ * Task 详情页 ledger 安全摘要(AC-009):从只读 GET /api/tasks/:id/ledger 拉取,
265
+ * 计数/状态/failure code + 有界排除理由;无 ledger 显示空态,失败安全降级。
266
+ */
267
+ async function renderTaskLedger(ledgerEl, taskId) {
268
+ clearNode(ledgerEl);
269
+ ledgerEl.appendChild(el("h3", "section-heading", "Source fidelity ledger"));
270
+ mountViewState(ledgerEl, "loading", "加载 ledger…");
271
+ let body = null;
272
+ try {
273
+ body = await fetchJson(`/api/tasks/${encodeURIComponent(taskId)}/ledger`);
274
+ } catch {
275
+ body = null;
276
+ }
277
+ if (!body) {
278
+ mountViewState(ledgerEl, "error", "ledger 只读接口失败(安全降级)。");
279
+ return;
280
+ }
281
+ if (body.present !== true) {
282
+ mountViewState(
283
+ ledgerEl,
284
+ body.error ? "error" : "empty",
285
+ body.error
286
+ ? `ledger 不可用(${body.error})。`
287
+ : "无 source fidelity ledger(任务无引用或 ledger 缺失,不伪造计数)。",
288
+ );
289
+ return;
290
+ }
291
+ const pre = el(
292
+ "pre",
293
+ "cmd",
294
+ formatLedgerSummary({
295
+ present: true,
296
+ fragmentCount: body.fragmentCount,
297
+ canonicalRequirementCount: body.canonicalRequirementCount,
298
+ contextOnlyCount: body.contextOnlyCount,
299
+ duplicateCount: body.duplicateCount,
300
+ unresolvedCount: body.unresolvedCount,
301
+ conflictCount: body.conflictCount,
302
+ gateStatus: body.gateStatus,
303
+ failureCode: body.failureCode,
304
+ exclusions: body.exclusions ?? [],
305
+ }),
306
+ );
307
+ pre.dataset.testid = "task-ledger-summary";
308
+ clearNode(ledgerEl);
309
+ ledgerEl.appendChild(el("h3", "section-heading", "Source fidelity ledger"));
310
+ ledgerEl.appendChild(pre);
311
+ }
312
+
258
313
  export async function loadOlderTaskRuns(featureId, taskId, before, runsEl) {
259
314
  if (runsEl === undefined) {
260
315
  runsEl = before;
@@ -8,6 +8,7 @@ import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-sto
8
8
  import { findPackageRoot } from "../../shared/package-metadata.js";
9
9
  import { pathMatchesPattern } from "../../shared/git-progress.js";
10
10
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
11
+ import { isDagSourceBindingV2 } from "./types.js";
11
12
  import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, openspecCitationsRel, parseOpenspecCitationsBlock, persistRuntimeSkeleton, } from "./structured-output-repair.js";
12
13
  export const frontendNormalizationActionSchema = z.enum([
13
14
  "remove-trailing-commas",
@@ -279,8 +280,33 @@ export const frontendImplementationContractSchema = z
279
280
  requirementSha256: z.string().regex(/^[a-f0-9]{64}$/),
280
281
  referencePaths: z.array(safePath),
281
282
  requirementIds: z.array(id),
283
+ // Source binding v2 (frontend): bound ledger pointer + fragment map.
284
+ ledgerPath: safePath.optional(),
285
+ ledgerSha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
286
+ inputDigest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
287
+ requirementToFragments: z
288
+ .record(z.string(), z.array(z.string()))
289
+ .optional(),
282
290
  })
283
- .strict(),
291
+ .strict()
292
+ .superRefine((value, ctx) => {
293
+ // v2 all-or-none:任一 ledger 字段出现则四项必须齐全,防止 v1+optional 冒充 v2。
294
+ const anyLedgerField = value.ledgerPath !== undefined ||
295
+ value.ledgerSha256 !== undefined ||
296
+ value.inputDigest !== undefined ||
297
+ value.requirementToFragments !== undefined;
298
+ const allLedgerFields = value.ledgerPath !== undefined &&
299
+ value.ledgerSha256 !== undefined &&
300
+ value.inputDigest !== undefined &&
301
+ value.requirementToFragments !== undefined;
302
+ if (anyLedgerField && !allLedgerFields) {
303
+ ctx.addIssue({
304
+ code: z.ZodIssueCode.custom,
305
+ path: ["sourceBinding"],
306
+ message: "source binding v2 ledger fields (ledgerPath/ledgerSha256/inputDigest/requirementToFragments) must be provided together; partial or optional v1+ledger bindings are rejected",
307
+ });
308
+ }
309
+ }),
284
310
  riskLevel: z.enum(["small", "standard", "high-risk"]),
285
311
  targets: z
286
312
  .object({
@@ -296,6 +322,10 @@ export const frontendImplementationContractSchema = z
296
322
  implementationTargets: z.array(safePath),
297
323
  verificationTargetIds: z.array(z.string().min(1)),
298
324
  evidenceGap: gap.optional(),
325
+ // Source fidelity ledger binding (AC-005/AC-006): 每个 requirement
326
+ // 携带来源 fragment 证据,prewrite 门禁校验其真实性与覆盖。
327
+ sourceFragmentIds: z.array(z.string().min(1)).optional(),
328
+ sourceRefs: z.array(safePath).optional(),
299
329
  })
300
330
  .strict()).min(1),
301
331
  uiStates: z.array(z
@@ -432,6 +462,30 @@ export const frontendImplementationContractSchema = z
432
462
  message: `unknown verification target ${targetId}`,
433
463
  path: ["requirements"],
434
464
  });
465
+ // Source fidelity ledger binding (AC-005/AC-006): 绑定携带
466
+ // requirementToFragments(ledger v2)时,每个 requirement 必须携带
467
+ // 有效 sourceFragmentIds/sourceRefs,否则 fail closed(防引用伪造/缺失)。
468
+ const boundFragments = value.sourceBinding.requirementToFragments?.[requirement.id];
469
+ if (value.sourceBinding.ledgerPath &&
470
+ (!requirement.sourceFragmentIds ||
471
+ requirement.sourceFragmentIds.length === 0 ||
472
+ !requirement.sourceRefs ||
473
+ requirement.sourceRefs.length === 0)) {
474
+ ctx.addIssue({
475
+ code: "custom",
476
+ message: `requirement ${requirement.id} must carry sourceFragmentIds/sourceRefs when a source fidelity ledger is bound`,
477
+ path: ["requirements", requirement.id, "sourceFragmentIds"],
478
+ });
479
+ }
480
+ else if (boundFragments !== undefined &&
481
+ (!requirement.sourceFragmentIds ||
482
+ requirement.sourceFragmentIds.length === 0)) {
483
+ ctx.addIssue({
484
+ code: "custom",
485
+ message: `requirement ${requirement.id} is bound to ledger fragments ${boundFragments.join(", ")} but declares none`,
486
+ path: ["requirements", requirement.id, "sourceFragmentIds"],
487
+ });
488
+ }
435
489
  }
436
490
  for (const state of value.uiStates) {
437
491
  if (state.applicable &&
@@ -568,6 +622,29 @@ export async function assertFrontendSourceBindingFresh(input) {
568
622
  throw new Error(`frontend source binding is stale: ${source.path}`);
569
623
  }
570
624
  }
625
+ // Source binding v2 ledger 新鲜度(AC-FIDELITY-REPAIR-001/002):v2 绑定携带的
626
+ // ledger 文件必须存在且 sha256 与绑定一致,否则 fail closed;v1 绑定无此路径。
627
+ if (isDagSourceBindingV2(input.binding)) {
628
+ const absolute = resolveDagTaskSourcePath({
629
+ workspaceRoot: input.workspaceRoot,
630
+ taskId: input.binding.taskId,
631
+ sourcePath: input.binding.ledgerPath,
632
+ });
633
+ let content;
634
+ try {
635
+ content = await readFile(absolute);
636
+ const info = await stat(absolute);
637
+ if (!info.isFile())
638
+ throw new Error("not a regular file");
639
+ }
640
+ catch (error) {
641
+ throw new Error(`frontend source fidelity ledger unavailable: ${input.binding.ledgerPath}: ${error instanceof Error ? error.message : String(error)}`);
642
+ }
643
+ const actual = createHash("sha256").update(content).digest("hex");
644
+ if (actual !== input.binding.ledgerSha256) {
645
+ throw new Error(`frontend source fidelity ledger is stale or tampered: ${input.binding.ledgerPath} (bound ${input.binding.ledgerSha256}, observed ${actual})`);
646
+ }
647
+ }
571
648
  }
572
649
  const SECRET_KEY = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credential|authorization)/i;
573
650
  const SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b)/;
@@ -742,7 +819,7 @@ export function canonicalFrontendContractSourceBinding(binding) {
742
819
  const requirement = binding.sources.find((source) => source.kind === "requirement");
743
820
  if (!requirement)
744
821
  throw new Error("frontend implementation contract gate requires a requirement source in DAG sourceBinding");
745
- return {
822
+ const sourceBinding = {
746
823
  taskId: binding.taskId,
747
824
  requirementPath: requirement.path,
748
825
  requirementSha256: requirement.sha256,
@@ -752,6 +829,15 @@ export function canonicalFrontendContractSourceBinding(binding) {
752
829
  .sort(),
753
830
  requirementIds: binding.requirementIds.map(canonicalizeRequirementId),
754
831
  };
832
+ // Source binding v2(schemaVersion=2 强制字段):透传 ledger 指针与
833
+ // requirement→fragment 映射。v1 绑定不得以 optional 字段冒充 v2。
834
+ if (isDagSourceBindingV2(binding)) {
835
+ sourceBinding.ledgerPath = binding.ledgerPath;
836
+ sourceBinding.ledgerSha256 = binding.ledgerSha256;
837
+ sourceBinding.inputDigest = binding.inputDigest;
838
+ sourceBinding.requirementToFragments = binding.requirementToFragments;
839
+ }
840
+ return sourceBinding;
755
841
  }
756
842
  function asRecord(value) {
757
843
  return value && typeof value === "object" && !Array.isArray(value)
@@ -47,6 +47,27 @@ function componentSelection(contract) {
47
47
  })
48
48
  .join("\n");
49
49
  }
50
+ /**
51
+ * 有界 source fidelity ledger 指针(AC-007 / AC-FIDELITY-007):长原文不重新
52
+ * 全量内联到节点提示词;只注入紧凑 canonical contract 与 bound ledger 指针
53
+ * (路径/哈希/inputDigest),ledger coverage 证明长尾未丢失。
54
+ */
55
+ function sourceFidelityLedger(contract) {
56
+ const binding = contract.sourceBinding;
57
+ if (!binding.ledgerPath)
58
+ return "_(not bound — v1 source binding)_";
59
+ const lines = [
60
+ `- ledger: ${binding.ledgerPath}`,
61
+ `- ledgerSha256: ${binding.ledgerSha256 ?? "(missing)"}`,
62
+ `- inputDigest: ${binding.inputDigest ?? "(missing)"}`,
63
+ "- 语义权威:原始 requirement/acceptance 文档与 requirement-ledger.json;本计划只消费派生摘要与 bound ledger 指针,长原文不内联。",
64
+ ];
65
+ const boundCount = Object.keys(binding.requirementToFragments ?? {}).length;
66
+ if (boundCount > 0) {
67
+ lines.push(`- requirement→fragment bindings: ${boundCount}`);
68
+ }
69
+ return lines.join("\n");
70
+ }
50
71
  export function renderFrontendPlanMarkdown(contract) {
51
72
  return [
52
73
  "# Frontend Implementation Plan",
@@ -54,6 +75,9 @@ export function renderFrontendPlanMarkdown(contract) {
54
75
  "## Requirement Coverage",
55
76
  requirementsCoverage(contract),
56
77
  "",
78
+ "## Source Fidelity Ledger",
79
+ sourceFidelityLedger(contract),
80
+ "",
57
81
  "## Implementation Steps",
58
82
  bulletList(contract.implementationSteps) || "_(not specified)_",
59
83
  "",
@@ -2,9 +2,12 @@ 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 { isDagSourceBindingV2 } from "./types.js";
5
6
  import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
6
7
  import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
7
8
  import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
9
+ import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
10
+ import { parseLedgerJson, validateRequirementLedger, } from "../../task/source-prepare/ledger.js";
8
11
  import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
9
12
  import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
10
13
  import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
@@ -42,6 +45,14 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
42
45
  "component-choices-missing",
43
46
  "contract-too-large",
44
47
  "revision-patch-unmergeable",
48
+ // Source fidelity ledger 类目(SOURCE_FIDELITY_*/source-* 稳定错误码)。
49
+ "source-fidelity-ledger-missing",
50
+ "source-fidelity-ledger-stale",
51
+ "source-fidelity-unresolved",
52
+ "source-fidelity-conflict",
53
+ "source-fidelity-high-risk-uncovered",
54
+ "source-fidelity-requirement-not-covered",
55
+ "source-fidelity-source-ref-invalid",
45
56
  ]);
46
57
  export const frontendPrewriteResultV1Schema = z
47
58
  .object({
@@ -442,6 +453,162 @@ async function finalizePrewrite(input, pending) {
442
453
  openspecPolicy: pending.openspecPolicy,
443
454
  };
444
455
  }
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
+ }
445
612
  export async function runFrontendPrewriteGate(input) {
446
613
  const workspaceRoot = input.workspaceRoot ?? input.repoRoot;
447
614
  // selectNode throws for a scheduling defect (no FINISHED plan/review). That
@@ -589,6 +756,33 @@ export async function runFrontendPrewriteGate(input) {
589
756
  }
590
757
  throw error;
591
758
  }
759
+ // Source fidelity ledger gate (20260824-frontend-source-fidelity-ledger-v1):
760
+ // 在 verdict/missing-requirement-ids/source-binding-not-fresh 之后插入 ledger
761
+ // 检查。缺失即 fail closed 且 writer 不获授权;所有检查通过才继续物化契约。
762
+ if (input.sourceBinding) {
763
+ const ledgerCheck = await checkFrontendSourceFidelityLedger({
764
+ workspaceRoot: workspaceRoot ?? "",
765
+ binding: input.sourceBinding,
766
+ contractRequirements: analysis.canonical.requirements.map((requirement) => ({
767
+ id: requirement.id,
768
+ sourceFragmentIds: requirement.sourceFragmentIds,
769
+ sourceRefs: requirement.sourceRefs,
770
+ })),
771
+ });
772
+ if (!ledgerCheck.ok && ledgerCheck.failureCode) {
773
+ return finalizePrewrite(input, {
774
+ ...basePending,
775
+ verdict,
776
+ candidateJsonSha256: analysis.candidateJsonSha256,
777
+ normalizationActions: [],
778
+ mockStrategy: analysis.canonical.mockApi.strategy,
779
+ classification: "blocked",
780
+ failureReason: ledgerCheck.failureReason ??
781
+ "frontend source fidelity ledger gate failed",
782
+ failureCode: ledgerCheck.failureCode,
783
+ });
784
+ }
785
+ }
592
786
  // Size guard: a canonical contract above 64KB is a retryable-invalid defect
593
787
  // (compress the contract) rather than a writer authorization failure.
594
788
  const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8");
@@ -947,6 +1141,9 @@ export function formatFrontendPrewriteGateStdout(result) {
947
1141
  }
948
1142
  lines.push(`Prewrite result: ${result.prewriteResult.path}`);
949
1143
  lines.push(`Openspec policy: ${result.openspecPolicy}`);
1144
+ if (result.failureCode?.startsWith("source-fidelity-")) {
1145
+ lines.push(`Source fidelity ledger: blocked (${result.failureCode})`);
1146
+ }
950
1147
  if (result.failureReason)
951
1148
  lines.push(`Failure reason: ${result.failureReason}`);
952
1149
  if (result.openspecReadPaths.length > 0) {