@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.
Files changed (164) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +23 -5
  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/command-definitions.js +7 -0
  7. package/dist/cli/program.js +5 -0
  8. package/dist/cli/update/runtime-activity.js +1 -29
  9. package/dist/commands/dag-follow-up.js +138 -0
  10. package/dist/commands/dag-rerun.js +1 -1
  11. package/dist/commands/init.js +26 -0
  12. package/dist/executors/dag-pi-executor.js +326 -8
  13. package/dist/executors/pi-read-budget-policy.js +166 -0
  14. package/dist/executors/shell-executor.js +26 -30
  15. package/dist/shared/dag-prompt-override.js +4 -5
  16. package/dist/shared/update/npm-client.js +33 -11
  17. package/dist/task/config-types.js +15 -0
  18. package/dist/task/contract/apply.js +11 -1
  19. package/dist/task/contract/constants.js +2 -0
  20. package/dist/task/contract/hash.js +3 -0
  21. package/dist/task/contract/observe.js +25 -4
  22. package/dist/task/contract/paths.js +2 -1
  23. package/dist/task/contract/project.js +15 -1
  24. package/dist/task/contract/schema.js +2 -0
  25. package/dist/task/contract/transaction.js +12 -0
  26. package/dist/task/frontend-project-capability.js +123 -4
  27. package/dist/task/source-prepare/completeness.js +188 -0
  28. package/dist/task/source-prepare/fragment-inventory.js +477 -0
  29. package/dist/task/source-prepare/index.js +3 -0
  30. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  31. package/dist/task/source-prepare/ledger-review.js +214 -0
  32. package/dist/task/source-prepare/ledger.js +545 -0
  33. package/dist/task/source-prepare/prepare.js +262 -1
  34. package/dist/task/source-prepare/semantic-intake.js +25 -5
  35. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  36. package/dist/task/source-prepare/types.js +22 -0
  37. package/dist/worker/cli.js +12 -26
  38. package/dist/worker/console/console-handoff.js +82 -0
  39. package/dist/worker/console/console-update-and-init.js +115 -0
  40. package/dist/worker/console/console-update-runtime.js +84 -0
  41. package/dist/worker/console/draft-store.js +49 -0
  42. package/dist/worker/console/operation-store.js +1 -1
  43. package/dist/worker/console/operator-actions.js +129 -4
  44. package/dist/worker/console/operator-surface-health.js +1 -0
  45. package/dist/worker/console/prd-intake-bridge.js +13 -0
  46. package/dist/worker/console/routes.js +3 -0
  47. package/dist/worker/console/server.js +73 -5
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-bPer1waP.js → abnfDiagram-N423BO3Z-D1-Z-BBa.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-BLdkwucW.js → arc-q8qAYm5m.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Bw4wo4I1.js → architectureDiagram-T3A2C74G-CAu4_lve.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DUUkc0gr.js → blockDiagram-VBNYF7ZC-D5GhQvHm.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BC3O0nob.js → c4Diagram-5PPSVZJV-ClQjeLkO.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-ClAuhSUL.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Dd4g2nti.js → chunk-2GRJ4B5K-DLqWlFbo.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-s2EDm7-n.js → chunk-2Q5K7J3B-DkRT_usL.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-GqtHM2yQ.js → chunk-5RXB4S5H-Cqp7Chi3.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BzRFrN8j.js → chunk-5VM5RSS4-CedEdFB9.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bw9nFjCJ.js → chunk-6Q2QTUOP-BHM8vP5x.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B4n-03Qm.js → chunk-GF5L2VYU-D7EFr377.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-Dt75ZdBD.js → chunk-JWPE2WC7-DR38mTWV.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-B_afMhHB.js → chunk-KBJHAD2P-BFv2LsR3.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Bd8Rqqg9.js → chunk-RYQCIY6F-BAvpJS5z.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-9boOMji4.js → chunk-XXDRQBXY-eKI9TO72.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-cBgeCpVU.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-cBgeCpVU.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BtCycfCc.js → cose-bilkent-JH36ORCC-BMJXpgo4.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-eTOts9L1.js → cynefin-VYW2F7L2-CKJ2ofZx.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cibd2mjg.js → cynefinDiagram-MW4NZA55-CsKnsazm.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CDjDZtDV.js → dagre-VZM6K2ZE-DVQ46vIw.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Btexn-JJ.js → diagram-7IWD3JNH-D_RuonYS.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cyl8sdpM.js → diagram-B4RE2ZJO-D2Mt0nMu.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Ev-83E1t.js → diagram-LBJQPF4R-DjBDuye6.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-41LqE0Mc.js → diagram-Q27KOJAE-8VZkqpE_.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BC39T5R3.js → diagram-UB23O5K3-D0izYxia.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DzcNRuri.js → ebnfDiagram-BXEA7PRR-CncG2I95.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-B5Frvvnz.js → erDiagram-JOGREHBK-DiBUUXR8.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BapxROIM.js → flowDiagram-UKHOOZJN-Dtc0fArb.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cd4d4wQ0.js → ganttDiagram-PKOTCBZU-BPNFP5OZ.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CrL6buFW.js → gitGraphDiagram-DS77QQ5N-CsgpxEEd.js} +1 -1
  80. package/dist/worker/console/static/assets/{index-DBh_x_Ek.js → index-Bai8Z25q.js} +81 -81
  81. package/dist/worker/console/static/assets/index-CH0Tr5hY.css +1 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BrSiNxyd.js → infoDiagram-6WML65LV-kHGxkJUS.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-gvlvY-n9.js → ishikawaDiagram-WSZJBQD7-CbLe9wrr.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-YaPzRQ.js → journeyDiagram-NVQOT4AX-BOkwxAcP.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BsmDv9E_.js → kanban-definition-27J2QSJJ-BHYQ2rDu.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-UWaoDN3h.js → linear-CHT4ujob.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-BlvpGVG8.js → mermaid.core-yL8sfqnY.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CUCIP5CP.js → mindmap-definition-FAOFIHXS-DZx1coV-.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CB1aQUAK.js → pegDiagram-VL7TDLO6-DStWRCI8.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BwehHxhf.js → pieDiagram-7S7Q4E2Y-DSKZkwzK.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CFz30APB.js → quadrantDiagram-CIZ2JOQS-CmoEGOuW.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwNmw1i6.js → railroadDiagram-AXF67PYL-BLqzhKyq.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-jg-tPXlI.js → requirementDiagram-LRYGKXZP-gbHeUabM.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BlyReDKl.js → sankeyDiagram-W5VNT64P-CaKkBCKO.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DBUzW1f3.js → sequenceDiagram-SI44F4Z6-PUbi3Upu.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BYcI2RAr.js → sizeCapture-X5ZJPWSS-dm7S9Qfe.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-D7AiyJdo.js → stateDiagram-OKZ733FA-C7WSzv4p.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-sQDv5AP3.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BHGt7Kkv.js → swimlanes-SLNWSIFB-CBKCiQwd.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BXwsmcPs.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-OPjooWcX.js → timeline-definition-Z64GVDOM-CvnUlylk.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-ggsWDvrO.js → vennDiagram-T6HMQDX7-xM0StY1O.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DjNe-BjZ.js → wardleyDiagram-T6FBY63Y-CWtJMagy.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Byp63Hei.js → xychartDiagram-ELKLHX3M-BO_-MeK8.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static-src/app/console-types.js +40 -1
  107. package/dist/worker/console/static-src/app/useOperatorActions.js +5 -0
  108. package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
  109. package/dist/worker/observability/init-runtime-activity.js +26 -0
  110. package/dist/worker/observe/node-input.js +88 -2
  111. package/dist/worker/observe/routes.js +188 -0
  112. package/dist/worker/observe/static/api.js +3 -3
  113. package/dist/worker/observe/static/format.js +37 -0
  114. package/dist/worker/observe/static/index.html +5 -0
  115. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  116. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  117. package/dist/worker/observe/static/state.js +2 -2
  118. package/dist/worker/observe/static/styles.css +331 -35
  119. package/dist/worker/observe/static/views/dag-inspector.js +509 -78
  120. package/dist/worker/observe/static/views/task.js +55 -0
  121. package/dist/workflows/dag/frontend-design-policy.js +19 -9
  122. package/dist/workflows/dag/frontend-implementation-contract.js +117 -50
  123. package/dist/workflows/dag/frontend-plan-render.js +27 -0
  124. package/dist/workflows/dag/frontend-prewrite-gate.js +45 -1
  125. package/dist/workflows/dag/frontend-recovery-controller.js +1 -0
  126. package/dist/workflows/dag/frontend-review-context.js +102 -15
  127. package/dist/workflows/dag/frontend-review-findings.js +42 -10
  128. package/dist/workflows/dag/frontend-shadow-dual-write.js +36 -2
  129. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  130. package/dist/workflows/dag/frontend-worktree-diff.js +154 -0
  131. package/dist/workflows/dag/frontend-writer-admission.js +12 -0
  132. package/dist/workflows/dag/init-hybrid.js +297 -65
  133. package/dist/workflows/dag/interrupt-request.js +7 -0
  134. package/dist/workflows/dag/node-execution.js +111 -1
  135. package/dist/workflows/dag/prompt.js +34 -1
  136. package/dist/workflows/dag/recovery-lease.js +80 -0
  137. package/dist/workflows/dag/rerun-feedback.js +31 -8
  138. package/dist/workflows/dag/rerun-run.js +23 -0
  139. package/dist/workflows/dag/rerun-task.js +29 -0
  140. package/dist/workflows/dag/retry-policy.js +34 -9
  141. package/dist/workflows/dag/runner.js +83 -16
  142. package/dist/workflows/dag/types.js +67 -11
  143. package/docs/README.md +1 -0
  144. package/docs/architecture/evolution.md +45 -0
  145. package/docs/init-surface.manifest.json +4 -0
  146. package/docs/templates/README.md +2 -1
  147. package/docs/templates/frontend-implementation-contract.schema.json +684 -24
  148. package/docs/templates/spec-registry.schema.json +45 -0
  149. package/harness.json +3 -3
  150. package/package.json +1 -1
  151. package/skills/frontend-bounded-implement/SKILL.md +8 -14
  152. package/skills/frontend-design-review/SKILL.md +20 -41
  153. package/skills/frontend-implementation/SKILL.md +2 -3
  154. package/skills/frontend-implementation/references/design-spec.md +4 -3
  155. package/skills/frontend-implementation/references/node-contracts.md +4 -3
  156. package/skills/frontend-review/SKILL.md +22 -14
  157. package/skills/frontend-review/references/review-findings.md +6 -7
  158. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  159. package/dist/worker/console/static/assets/channel-AbFtVnFm.js +0 -1
  160. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-n1yf2Jcv.js +0 -1
  161. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-n1yf2Jcv.js +0 -1
  162. package/dist/worker/console/static/assets/index-DwOGauxU.css +0 -1
  163. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BBSH5c8m.js +0 -1
  164. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D2F7-U4H.js +0 -8
@@ -55,6 +55,25 @@ export function observationBlocksPrepare(state) {
55
55
  }
56
56
  return null;
57
57
  }
58
+ /**
59
+ * 精确 identity 绑定的人类 resolution 判定(AC-001 / AC-005):decidedBy==='human'、
60
+ * decision trim 后非空、sourceRef 与冲突 id 精确匹配三者同时成立才算有效;model/
61
+ * system 伪造判定、stale sourceRef(不匹配当前冲突 identity)或空/空白 decision 一律
62
+ * 不解锁。resolution 信息取自 draft 层(不重排流水线,listPrepareGaps 仍先于 ledger
63
+ * 构建执行)。
64
+ */
65
+ export function hasValidHumanConflictResolution(draft, conflictId) {
66
+ for (const resolution of draft.sourceResolutions ?? []) {
67
+ if (resolution.sourceRef !== conflictId)
68
+ continue;
69
+ if (resolution.decidedBy !== "human")
70
+ continue;
71
+ if (!resolution.decision?.trim())
72
+ continue;
73
+ return true;
74
+ }
75
+ return false;
76
+ }
58
77
  export function listPrepareGaps(input) {
59
78
  const gaps = [];
60
79
  const risks = [];
@@ -108,6 +127,12 @@ export function listPrepareGaps(input) {
108
127
  }
109
128
  if (input.acceptanceConflicts && input.acceptanceConflicts.length > 0) {
110
129
  for (const conflict of input.acceptanceConflicts) {
130
+ // AC-001:有效 human resolution(human + 非空 decision + sourceRef 与冲突 id
131
+ // 精确匹配)解除该 id 的 legacy blocking gap;无 resolution 或 resolution 不
132
+ // 满足三条件(AC-005:伪造判定 / stale sourceRef / 空 decision)仍发射阻断。
133
+ if (hasValidHumanConflictResolution(input.draft, conflict.id)) {
134
+ continue;
135
+ }
111
136
  gaps.push({
112
137
  code: "ACCEPTANCE_ID_CONFLICT",
113
138
  level: "blocking",
@@ -246,3 +271,166 @@ export function hasBlockingGaps(gaps) {
246
271
  export function hasBlockingRisks(risks) {
247
272
  return risks.some((risk) => risk.level === "blocking");
248
273
  }
274
+ /**
275
+ * Source fidelity ledger 分级 fail-closed 阻断(AC-003 / AC-FIDELITY-003):
276
+ * 高风险未映射/排除、unresolved、冲突返回稳定错误并阻断;低风险 context-only 仅当
277
+ * 理由完整且独立 reviewer pass 后继续;duplicate 保留全部来源(warning 可见)。
278
+ * 仅 taskKind 为 frontend-implementation 时全量阻断,其他 taskKind 保持 warning。
279
+ */
280
+ export function appendLedgerGaps(input) {
281
+ const { gaps, risks } = input;
282
+ if (!input.ledgerSummary?.present)
283
+ return;
284
+ const blocking = input.taskKind === "frontend-implementation";
285
+ const level = blocking ? "blocking" : "warning";
286
+ const summary = input.ledgerSummary;
287
+ if (summary.unresolvedCount > 0) {
288
+ const gap = {
289
+ code: "SOURCE_FIDELITY_UNRESOLVED",
290
+ level,
291
+ message: `ledger 有 ${summary.unresolvedCount} 个 unresolved fragment(需人工 sourceResolutions 确认后才可继续)`,
292
+ details: {
293
+ fragmentIds: summary.exclusions
294
+ .filter((e) => /^REQ-SRC-/.test(e.fragmentId) || e.fragmentId.startsWith("AC-"))
295
+ .slice(0, 10)
296
+ .map((e) => e.fragmentId),
297
+ },
298
+ selfHealing: blocking
299
+ ? {
300
+ action: "reimport-prd",
301
+ steps: [
302
+ "检查原始 PRD 中无法映射到 canonical AC 的可执行片段(unresolved)",
303
+ "补齐/修正 PRD 使每个可执行片段都能绑定 AC,或由人工在 sourceResolutions 中确认排除理由",
304
+ "重新 importPrd(同 taskId)后 task advance 重建 ledger",
305
+ ],
306
+ }
307
+ : undefined,
308
+ };
309
+ gaps.push(gap);
310
+ risks.push({
311
+ code: "SOURCE_FIDELITY_UNRESOLVED",
312
+ level: blocking ? "blocking" : "elevated",
313
+ message: gap.message,
314
+ });
315
+ }
316
+ if (summary.conflictCount > 0) {
317
+ const gap = {
318
+ code: "SOURCE_FIDELITY_CONFLICT",
319
+ level,
320
+ message: `ledger 检测到 ${summary.conflictCount} 个 AC 文本冲突(同一 ID 不同文本)`,
321
+ selfHealing: blocking
322
+ ? {
323
+ action: "reimport-prd",
324
+ steps: [
325
+ "统一同一 AC ID 的文本后重新 importPrd(同 taskId)",
326
+ "或由人工在 sourceResolutions 中声明冲突取舍(conflict → decision)",
327
+ ],
328
+ }
329
+ : undefined,
330
+ };
331
+ gaps.push(gap);
332
+ risks.push({
333
+ code: "SOURCE_FIDELITY_CONFLICT",
334
+ level: blocking ? "blocking" : "elevated",
335
+ message: gap.message,
336
+ });
337
+ }
338
+ if (summary.failureCode === "SOURCE_FIDELITY_HIGH_RISK_UNCOVERED") {
339
+ const gap = {
340
+ code: "SOURCE_FIDELITY_HIGH_RISK_UNCOVERED",
341
+ level,
342
+ message: "ledger 存在高风险 acceptance 片段未映射到 canonical requirement",
343
+ selfHealing: blocking
344
+ ? {
345
+ action: "reimport-prd",
346
+ steps: [
347
+ "为每个未覆盖的高风险片段补齐 canonical AC(或人工 sourceResolutions 确认)",
348
+ "重新 importPrd(同 taskId)后 task advance 重建 ledger",
349
+ ],
350
+ }
351
+ : undefined,
352
+ };
353
+ gaps.push(gap);
354
+ risks.push({
355
+ code: "SOURCE_FIDELITY_HIGH_RISK_UNCOVERED",
356
+ level: blocking ? "blocking" : "elevated",
357
+ message: gap.message,
358
+ });
359
+ }
360
+ for (const error of input.ledgerErrors ?? []) {
361
+ if (error.code === "SOURCE_FIDELITY_LEDGER_MISSING") {
362
+ const gap = {
363
+ code: "SOURCE_FIDELITY_LEDGER_MISSING",
364
+ level,
365
+ message: error.message,
366
+ };
367
+ gaps.push(gap);
368
+ }
369
+ // AC-BLOCK-001:canonical requirement 缺少 fragment/sourceRef 证据、来源引用
370
+ // 无效、fragment 无效均为 blocking gap(frontend task),保证 prepare/apply
371
+ // 以稳定码 fail closed(appendLedgerGaps 是 prepare 补洞的唯一入口)。
372
+ if (error.code === "SOURCE_FIDELITY_REQUIREMENT_NOT_COVERED" ||
373
+ error.code === "SOURCE_FIDELITY_SOURCE_REF_INVALID" ||
374
+ error.code === "SOURCE_FIDELITY_FRAGMENT_INVALID") {
375
+ const gap = {
376
+ code: error.code,
377
+ level,
378
+ message: error.message,
379
+ selfHealing: blocking
380
+ ? {
381
+ action: "reimport-prd",
382
+ steps: [
383
+ "检查 PRD 中每个 canonical requirement 是否都有可审计的 fragment/sourceRef 证据",
384
+ "补齐缺失的验收标准映射或修正引用后重新 importPrd(同 taskId)",
385
+ "重新 importPrd(同 taskId)后 task advance 重建 ledger",
386
+ ],
387
+ }
388
+ : undefined,
389
+ };
390
+ gaps.push(gap);
391
+ risks.push({
392
+ code: error.code,
393
+ level: blocking ? "blocking" : "elevated",
394
+ message: error.message,
395
+ });
396
+ }
397
+ if (error.code === "SOURCE_FIDELITY_CONTEXT_ONLY_UNREVIEWED" &&
398
+ blocking) {
399
+ // 低风险 context-only 仅独立 reviewer pass 放行;未通过则 frontend 任务阻断。
400
+ const gap = {
401
+ code: "SOURCE_FIDELITY_CONTEXT_ONLY_UNREVIEWED",
402
+ level: "blocking",
403
+ message: error.message,
404
+ selfHealing: {
405
+ action: "reimport-prd",
406
+ steps: [
407
+ "检查 context-only 片段是否缺少完整排除理由,或由人工在 sourceResolutions 中确认",
408
+ "重新 importPrd(同 taskId)后 task advance 重建 ledger 并复核 reviewer 判定",
409
+ ],
410
+ },
411
+ };
412
+ gaps.push(gap);
413
+ }
414
+ }
415
+ // 低风险 context-only:记录并提示排除理由/独立 reviewer 要求(不阻断普通任务)。
416
+ if (summary.contextOnlyCount > 0) {
417
+ gaps.push({
418
+ code: "SOURCE_FIDELITY_CONTEXT_ONLY_UNREVIEWED",
419
+ level: "warning",
420
+ message: `ledger 有 ${summary.contextOnlyCount} 个 context-only fragment(低风险排除;frontend 任务需完整理由且独立 reviewer pass)`,
421
+ details: {
422
+ fragmentIds: summary.exclusions
423
+ .filter((e) => e.fragmentId.startsWith("REQ-SRC-"))
424
+ .slice(0, 10)
425
+ .map((e) => e.fragmentId),
426
+ },
427
+ });
428
+ }
429
+ if (summary.duplicateCount > 0) {
430
+ gaps.push({
431
+ code: "SOURCE_FIDELITY_DUPLICATE_PRESERVED",
432
+ level: "warning",
433
+ message: `ledger 保留 ${summary.duplicateCount} 个 duplicate fragment(duplicate 保留全部来源)`,
434
+ });
435
+ }
436
+ }
@@ -0,0 +1,477 @@
1
+ import { createHash } from "node:crypto";
2
+ // ---------------------------------------------------------------------------
3
+ // Deterministic fragment inventory (AC-001 / AC-FIDELITY-001)
4
+ // 对 .md/.markdown/.txt 按标题、段落、列表项、checklist、表格行确定性切分。
5
+ // 稳定 fragment ID:显式 AC ID 原样保留(含 `#### AC-*` 标题式);无 ID 片段生成
6
+ // 跨文档稳定 REQ-SRC-*(基于 path+行号+文本的确定性哈希,与文档枚举顺序无关,
7
+ // 消除 per-document counter reset 碰撞)。不按文档前 24K 字符裁剪——尾部片段仍
8
+ // 进入 inventory(覆盖统计证明)。
9
+ // ---------------------------------------------------------------------------
10
+ export const FRAGMENT_ID_PREFIX = "REQ-SRC";
11
+ /** 生成 fragment ID 的合法形态(防伪造:拒绝自由格式 ID)。 */
12
+ export const FRAGMENT_ID_PATTERN = /^(?:AC(?:-[A-Z0-9]+)+|REQ-SRC-[A-Z0-9-]+)$/;
13
+ const ACCEPTANCE_HEADING_RE = /(?:^|\s)(?:验收标准|acceptance criteria|acceptance|完成标准|完成定义|验收条件|验收准则|dod|definition of done|验收)(?:\s|$)/iu;
14
+ const CONSTRAINT_HEADING_RE = /(?:^|\s)(?:约束|constraints|invariants|不变式)(?:\s|$)/iu;
15
+ const NON_GOAL_HEADING_RE = /(?:^|\s)(?:非目标|out of scope|non-goals|non goals)(?:\s|$)/iu;
16
+ const OBJECTIVE_HEADING_RE = /(?:^|\s)(?:目标|objective|问题与目标|业务目标|需求概述)(?:\s|$)/iu;
17
+ /** 可执行片段启发式(AC 风格/祈使语气):命中则不可按 context-only 降级。 */
18
+ const EXECUTABLE_HINT_RE = /(?:必须|应当|应支持|需要|需|不得|should|must|shall|will|require|support|implement|provide|ensure|verify|当.*?时|when.*?then)/iu;
19
+ /** 显式 AC 式 ID 前缀:AC-001 / AC-FE-001 / AC-FE-INS-001 等(允许 bullet 标记后空格
20
+ * 与 checklist 勾选框)。收紧解析(AC-001 / AC-HARD-001):AC 后必须跟分隔符
21
+ * (-/_)+ 大写字母/数字 id 形态(含可选 FE/BE/UI/API 作用域前缀与 V4 `#### AC-*`
22
+ * 标题归属);`## Acceptance Criteria` / `## Acceptance` / Ac 开头普通单词永不产生
23
+ * 伪造 id(ACCEPTANCE 类 id 全量消失)。捕获组 1 = AC id,捕获组 2 = 余下文本。 */
24
+ const EXPLICIT_AC_ID_RE = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(AC(?:[-_](?:FE|BE|UI|API))?[-_][A-Z0-9]+(?:[-_][A-Z0-9]+)*)\s*[::]?\s*(.*)$/iu;
25
+ /** 显式 AC 标题式:`#### AC-FE-001 Save nickname` / `### AC-001:正文`。
26
+ * AC 后必须跟分隔符(-/_)+ id 形态;普通单词(Acceptance Criteria / Acceptance)
27
+ * 永不匹配,按 ACCEPTANCE_HEADING_RE 章节分类生成稳定 REQ-SRC-* 片段。
28
+ * 捕获组 1 = AC id,捕获组 2 = 标题余下文本。 */
29
+ const EXPLICIT_AC_HEADING_RE = /^\s*(AC(?:[-_](?:FE|BE|UI|API))?[-_][A-Z0-9]+(?:[-_][A-Z0-9]+)*)\s*[::]?\s*(.*)$/iu;
30
+ /** Markdown 表格数据行(非分隔行)。 */
31
+ const TABLE_DATA_ROW_RE = /^\s*\|.*\|\s*$/;
32
+ const TABLE_SEPARATOR_ROW_RE = /^\s*\|[\s:|-]+\|\s*$/;
33
+ /** 表格表头行:数据行且下一行是分隔行(`| 列 | 列 |` + `| --- | --- |`)。 */
34
+ function isTableHeaderRow(line, nextLine) {
35
+ return isTableDataRow(line) && TABLE_SEPARATOR_ROW_RE.test(nextLine.trim());
36
+ }
37
+ /** 表格行首格显式 AC ID:`| AC-001 | 订单列表加载完成 |` → id=AC-001, rest=订单列表加载完成。 */
38
+ function matchTableRowAcId(line) {
39
+ const trimmed = line.trim().replace(/^\|/, "");
40
+ const cells = trimmed.split("|");
41
+ const first = cells[0]?.trim() ?? "";
42
+ const idMatch = EXPLICIT_AC_HEADING_RE.exec(first);
43
+ if (!idMatch)
44
+ return null;
45
+ const id = (idMatch[1] ?? "").trim().toUpperCase().replace(/_/g, "-");
46
+ if (!id)
47
+ return null;
48
+ const rest = cells
49
+ .slice(1)
50
+ .join("|")
51
+ .replace(/\|\s*$/, "")
52
+ .trim();
53
+ return { id, rest: rest || id };
54
+ }
55
+ export function sha256Text(text) {
56
+ return createHash("sha256").update(text, "utf8").digest("hex");
57
+ }
58
+ export function normalizeFragmentText(text) {
59
+ return text
60
+ .replace(/\r\n/g, "\n")
61
+ .split("\n")
62
+ .map((line) => line.trim())
63
+ .filter(Boolean)
64
+ .join(" ")
65
+ .replace(/\s+/g, " ")
66
+ .trim();
67
+ }
68
+ export function matchExplicitAcId(line) {
69
+ const match = EXPLICIT_AC_ID_RE.exec(line.trim());
70
+ if (!match)
71
+ return null;
72
+ const rawId = match[1] ?? "";
73
+ const id = rawId.trim().toUpperCase().replace(/_/g, "-");
74
+ if (!id)
75
+ return null;
76
+ return { id, rest: (match[2] ?? "").trim() };
77
+ }
78
+ /** V4 显式 AC 标题式匹配:`#### AC-FE-001 Save nickname` → id=AC-FE-001, rest=Save nickname。 */
79
+ export function matchExplicitAcHeading(headingText) {
80
+ const match = EXPLICIT_AC_HEADING_RE.exec(headingText.trim());
81
+ if (!match)
82
+ return null;
83
+ const rawId = match[1] ?? "";
84
+ const id = rawId.trim().toUpperCase().replace(/_/g, "-");
85
+ if (!id)
86
+ return null;
87
+ return { id, rest: (match[2] ?? "").trim() };
88
+ }
89
+ function isAtxHeading(line) {
90
+ const match = /^(#{1,6})\s+(.+?)\s*$/u.exec(line);
91
+ if (!match)
92
+ return null;
93
+ return { level: match[1].length, text: match[2].trim() };
94
+ }
95
+ function isListItem(line) {
96
+ return /^\s*(?:[-*+]|\d+[.)])\s+(?:\[(?: |x|X)\]\s+)?/.test(line);
97
+ }
98
+ function isTableDataRow(line) {
99
+ const trimmed = line.trim();
100
+ return TABLE_DATA_ROW_RE.test(trimmed) && !TABLE_SEPARATOR_ROW_RE.test(trimmed);
101
+ }
102
+ function classifySectionHeading(headingPath) {
103
+ const normalized = headingPath.toLowerCase();
104
+ if (ACCEPTANCE_HEADING_RE.test(normalized))
105
+ return "acceptance";
106
+ if (CONSTRAINT_HEADING_RE.test(normalized))
107
+ return "constraint";
108
+ if (NON_GOAL_HEADING_RE.test(normalized))
109
+ return "non-goal";
110
+ if (OBJECTIVE_HEADING_RE.test(normalized))
111
+ return "objective";
112
+ return "other";
113
+ }
114
+ /**
115
+ * 确定性生成 fragment ID:REQ-SRC-<sha256(path+行号+文本) 前 12 位大写>。
116
+ * 与文档枚举顺序无关(跨文档不碰撞、重排不换 ID),相同输入恒等复现。
117
+ */
118
+ export function generatedFragmentId(pathValue, lineRange, text) {
119
+ const digest = sha256Text(`${pathValue}\u0000${lineRange.start}\u0000${lineRange.end}\u0000${text}`);
120
+ return `${FRAGMENT_ID_PREFIX}-${digest.slice(0, 12).toUpperCase()}`;
121
+ }
122
+ /**
123
+ * 将单个文档切分为标题段落(section),再按列表项/段落/表格行切分为原始片段。
124
+ * 完全确定性:相同输入总是产出相同片段序列。generated ID 使用跨文档稳定哈希,
125
+ * 不依赖文档内自增 counter(消除多文档 counter reset 碰撞)。
126
+ */
127
+ export function splitDocumentIntoFragments(doc) {
128
+ const normalized = doc.content.replace(/\r\n/g, "\n");
129
+ const lines = normalized.split("\n");
130
+ const sections = [];
131
+ const headingStack = [];
132
+ let current = null;
133
+ const flushSection = (endLine) => {
134
+ if (!current)
135
+ return;
136
+ current.endLine = Math.max(current.startLine, endLine);
137
+ current.lines = lines.slice(current.startLine - 1, Math.min(current.endLine, lines.length));
138
+ sections.push(current);
139
+ current = null;
140
+ };
141
+ for (let index = 0; index < lines.length; index += 1) {
142
+ const line = lines[index] ?? "";
143
+ const heading = isAtxHeading(line);
144
+ if (heading) {
145
+ flushSection(index - 1);
146
+ while (headingStack.length > 0 &&
147
+ headingStack[headingStack.length - 1].level >= heading.level) {
148
+ headingStack.pop();
149
+ }
150
+ headingStack.push({ level: heading.level, text: heading.text });
151
+ current = {
152
+ headingPath: headingStack.map((h) => h.text).join(" > "),
153
+ level: heading.level,
154
+ startLine: index + 1,
155
+ endLine: index + 1,
156
+ lines: [],
157
+ };
158
+ continue;
159
+ }
160
+ if (!current) {
161
+ current = {
162
+ headingPath: "",
163
+ level: 0,
164
+ startLine: index + 1,
165
+ endLine: index + 1,
166
+ lines: [],
167
+ };
168
+ }
169
+ }
170
+ flushSection(lines.length);
171
+ const rawFragments = [];
172
+ for (const section of sections) {
173
+ // V4 显式 AC 标题(`#### AC-*`)本身即验收段:标题行的 ID 原样保留。
174
+ const lastHeadingSegment = section.headingPath.split(" > ").pop() ?? section.headingPath;
175
+ const explicitSectionAc = matchExplicitAcHeading(lastHeadingSegment);
176
+ const sectionKind = classifySectionHeading(section.headingPath);
177
+ const effectiveKind = explicitSectionAc ? "acceptance" : sectionKind;
178
+ const bodyStart = section.headingPath
179
+ ? section.startLine + (explicitSectionAc ? 0 : 1)
180
+ : section.startLine;
181
+ const bodyEnd = section.endLine;
182
+ const bodyLines = lines.slice(Math.max(0, bodyStart - 1), Math.min(bodyEnd, lines.length));
183
+ // 当前验收章节内最近的显式 AC ID(V4 标题或 AC 条目),正文片段继承它。
184
+ let pendingAcId = null;
185
+ const emit = (fragment) => {
186
+ const explicit = fragment.explicitId ?? null;
187
+ let id = explicit;
188
+ if (!id) {
189
+ id = generatedFragmentId(doc.path, fragment.lineRange, fragment.text);
190
+ }
191
+ rawFragments.push({
192
+ id,
193
+ path: doc.path,
194
+ headingPath: fragment.headingPath,
195
+ lineRange: fragment.lineRange,
196
+ text: fragment.text,
197
+ ...(explicit
198
+ ? {}
199
+ : fragment.inheritAcId && pendingAcId
200
+ ? { explicitAcId: pendingAcId }
201
+ : {}),
202
+ });
203
+ };
204
+ if (explicitSectionAc) {
205
+ // 标题式 AC 段:标题行本身是一个 acceptance 片段(ID 原样保留)。
206
+ pendingAcId = explicitSectionAc.id;
207
+ emit({
208
+ headingPath: section.headingPath,
209
+ lineRange: { start: section.startLine, end: section.startLine },
210
+ text: explicitSectionAc.rest || explicitSectionAc.id,
211
+ explicitId: explicitSectionAc.id,
212
+ });
213
+ }
214
+ if (effectiveKind === "acceptance") {
215
+ // acceptance 章节:每个列表项/checklist/AC 标题行/表格数据行 = 一个片段。
216
+ let paragraphStart = null;
217
+ const flushParagraph = (endLine) => {
218
+ if (paragraphStart === null)
219
+ return;
220
+ const text = lines
221
+ .slice(paragraphStart - 1, Math.min(endLine, lines.length))
222
+ .map((line) => line.trim())
223
+ .filter(Boolean)
224
+ .join(" ")
225
+ .trim();
226
+ if (text) {
227
+ emit({
228
+ headingPath: section.headingPath,
229
+ lineRange: { start: paragraphStart, end: Math.max(paragraphStart, endLine) },
230
+ text,
231
+ inheritAcId: true,
232
+ });
233
+ }
234
+ paragraphStart = null;
235
+ };
236
+ for (let index = bodyStart; index <= bodyEnd; index += 1) {
237
+ const line = lines[index - 1] ?? "";
238
+ const trimmed = line.trim();
239
+ if (!trimmed) {
240
+ flushParagraph(index - 1);
241
+ continue;
242
+ }
243
+ if (TABLE_SEPARATOR_ROW_RE.test(trimmed)) {
244
+ // 表格分隔行不进入 inventory。
245
+ flushParagraph(index - 1);
246
+ continue;
247
+ }
248
+ const heading = isAtxHeading(trimmed);
249
+ if (heading) {
250
+ flushParagraph(index - 1);
251
+ const explicit = matchExplicitAcHeading(heading.text);
252
+ if (explicit) {
253
+ pendingAcId = explicit.id;
254
+ emit({
255
+ headingPath: section.headingPath,
256
+ lineRange: { start: index, end: index },
257
+ text: explicit.rest || explicit.id,
258
+ explicitId: explicit.id,
259
+ });
260
+ }
261
+ else {
262
+ pendingAcId = null;
263
+ }
264
+ continue;
265
+ }
266
+ if (isTableDataRow(trimmed)) {
267
+ const nextLine = lines[index] ?? "";
268
+ if (isTableHeaderRow(trimmed, nextLine)) {
269
+ // 表头行(后随分隔行)不进入 inventory。
270
+ flushParagraph(index - 1);
271
+ continue;
272
+ }
273
+ flushParagraph(index - 1);
274
+ const rowExplicit = matchTableRowAcId(trimmed);
275
+ emit({
276
+ headingPath: section.headingPath,
277
+ lineRange: { start: index, end: index },
278
+ text: rowExplicit ? rowExplicit.rest : trimmed,
279
+ explicitId: rowExplicit?.id,
280
+ ...(rowExplicit ? {} : { inheritAcId: true }),
281
+ });
282
+ continue;
283
+ }
284
+ if (isListItem(trimmed)) {
285
+ flushParagraph(index - 1);
286
+ const explicit = matchExplicitAcId(trimmed);
287
+ // 列表项自包含:显式 AC ID 只属于该项,不改变 heading 作用域
288
+ // (pendingAcId 仅由 V4 `#### AC-*` 标题设置)。
289
+ const text = explicit
290
+ ? explicit.rest
291
+ : trimmed.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "");
292
+ if (text.trim()) {
293
+ emit({
294
+ headingPath: section.headingPath,
295
+ lineRange: { start: index, end: index },
296
+ text: text.trim(),
297
+ explicitId: explicit?.id,
298
+ ...(explicit ? {} : { inheritAcId: true }),
299
+ });
300
+ }
301
+ continue;
302
+ }
303
+ if (paragraphStart === null)
304
+ paragraphStart = index;
305
+ }
306
+ flushParagraph(bodyEnd);
307
+ continue;
308
+ }
309
+ // 非 acceptance 章节:连续非空行 = 一个段落片段(空行分隔);表格数据行独立成片。
310
+ let paragraphStart = null;
311
+ const flushParagraph = (endLine) => {
312
+ if (paragraphStart === null)
313
+ return;
314
+ const text = lines
315
+ .slice(paragraphStart - 1, Math.min(endLine, lines.length))
316
+ .map((line) => line.trim())
317
+ .filter(Boolean)
318
+ .join(" ")
319
+ .trim();
320
+ if (text) {
321
+ emit({
322
+ headingPath: section.headingPath,
323
+ lineRange: { start: paragraphStart, end: Math.max(paragraphStart, endLine) },
324
+ text,
325
+ });
326
+ }
327
+ paragraphStart = null;
328
+ };
329
+ for (let index = bodyStart; index <= bodyEnd; index += 1) {
330
+ const line = lines[index - 1] ?? "";
331
+ const trimmed = line.trim();
332
+ if (!trimmed) {
333
+ flushParagraph(index - 1);
334
+ continue;
335
+ }
336
+ if (isAtxHeading(trimmed)) {
337
+ flushParagraph(index - 1);
338
+ continue;
339
+ }
340
+ if (TABLE_SEPARATOR_ROW_RE.test(trimmed)) {
341
+ flushParagraph(index - 1);
342
+ continue;
343
+ }
344
+ if (isTableDataRow(trimmed)) {
345
+ const nextLine = lines[index] ?? "";
346
+ if (isTableHeaderRow(trimmed, nextLine)) {
347
+ flushParagraph(index - 1);
348
+ continue;
349
+ }
350
+ flushParagraph(index - 1);
351
+ emit({
352
+ headingPath: section.headingPath,
353
+ lineRange: { start: index, end: index },
354
+ text: trimmed,
355
+ });
356
+ continue;
357
+ }
358
+ if (paragraphStart === null)
359
+ paragraphStart = index;
360
+ }
361
+ flushParagraph(bodyEnd);
362
+ }
363
+ return rawFragments;
364
+ }
365
+ /**
366
+ * 对多个原始文档确定性切分并做 disposition 分类(AC-001)。
367
+ * 每个非空有效片段携带稳定 fragment ID、原始路径、标题路径、行号、原文哈希、
368
+ * 分级风险与且仅一个 disposition。duplicate 保留全部来源。文档先按 path 排序,
369
+ * 使 duplicate 判定与文档枚举顺序无关(同一输入恒等复现)。
370
+ */
371
+ export function buildFragmentInventory(input) {
372
+ const sortedDocuments = [...input.documents].sort((a, b) => a.path.localeCompare(b.path));
373
+ const fragments = [];
374
+ for (const doc of sortedDocuments) {
375
+ fragments.push(...splitDocumentIntoFragments(doc));
376
+ }
377
+ const canonicalById = new Map(input.canonicalRequirements.map((req) => [req.id, req.text]));
378
+ const canonicalTextSet = new Set(input.canonicalRequirements.map((req) => normalizeFragmentText(req.text)));
379
+ const seenText = new Map();
380
+ const records = fragments.map((fragment) => {
381
+ const text = normalizeFragmentText(fragment.text);
382
+ const sectionKind = classifySectionHeading(fragment.headingPath);
383
+ // 切分器已把显式 AC ID 放入 fragment.id 并从文本剥离前缀;此处从 id 恢复。
384
+ const explicitIdFromId = /^AC(?:-[A-Z0-9]+)+$/iu.test(fragment.id)
385
+ ? fragment.id
386
+ : null;
387
+ const explicit = explicitIdFromId
388
+ ? { id: explicitIdFromId, rest: fragment.text }
389
+ : matchExplicitAcId(fragment.text);
390
+ // V4 标题/条目下正文继承的显式 AC ID(不生成取代它的 REQ-SRC-*)。
391
+ const inherited = !explicit && fragment.explicitAcId
392
+ ? { id: fragment.explicitAcId, rest: fragment.text }
393
+ : null;
394
+ const effectiveExplicit = explicit ?? inherited;
395
+ const mappedIds = [];
396
+ if (effectiveExplicit && canonicalById.has(effectiveExplicit.id)) {
397
+ mappedIds.push(effectiveExplicit.id);
398
+ }
399
+ else if (effectiveExplicit && !canonicalById.has(effectiveExplicit.id)) {
400
+ // 显式 AC ID 但不在 canonical 集合:按文本匹配兜底,仍不中则 unresolved。
401
+ if (canonicalTextSet.has(text)) {
402
+ for (const req of input.canonicalRequirements) {
403
+ if (normalizeFragmentText(req.text) === text) {
404
+ mappedIds.push(req.id);
405
+ }
406
+ }
407
+ }
408
+ }
409
+ else if (canonicalTextSet.has(text)) {
410
+ for (const req of input.canonicalRequirements) {
411
+ if (normalizeFragmentText(req.text) === text) {
412
+ mappedIds.push(req.id);
413
+ }
414
+ }
415
+ }
416
+ let disposition;
417
+ let reason;
418
+ if (effectiveExplicit && canonicalById.has(effectiveExplicit.id) && mappedIds.length === 0) {
419
+ disposition = "acceptance";
420
+ reason = `explicit id ${effectiveExplicit.id} declared in source but canonical requirement has no fragment text match`;
421
+ }
422
+ else if (sectionKind === "acceptance" || effectiveExplicit) {
423
+ disposition = mappedIds.length > 0 ? "acceptance" : "unresolved";
424
+ if (disposition === "unresolved") {
425
+ reason =
426
+ effectiveExplicit && !canonicalById.has(effectiveExplicit.id)
427
+ ? `explicit AC id ${effectiveExplicit.id} not present in canonical requirements`
428
+ : "acceptance fragment without a canonical requirement mapping";
429
+ }
430
+ }
431
+ else if (sectionKind === "objective") {
432
+ disposition = "requirement";
433
+ }
434
+ else if (sectionKind === "constraint") {
435
+ disposition = "constraint";
436
+ }
437
+ else if (sectionKind === "non-goal") {
438
+ disposition = "non-goal";
439
+ }
440
+ else {
441
+ // Non-goal/constraint/acceptance/objective sections are classified
442
+ // above. An "other" section (背景 / 概述 / 说明 …) is contextual
443
+ // description, not an executable requirement: even when it mentions
444
+ // implementation verbs ("需要", "渲染", "组件"), it must not be
445
+ // promoted to a high-risk unresolved executable fragment — that
446
+ // blocks PRDs whose 背景 prose happens to use such words (merge
447
+ // regression: "需要一个小组件" was flagged executable via 需要).
448
+ // Explicit AC ids are already handled by the branches above, so
449
+ // this branch is always contextual.
450
+ disposition = "context-only";
451
+ reason = "context-only fragment; excluded from canonical requirement coverage";
452
+ }
453
+ const priorId = seenText.get(text);
454
+ if (priorId) {
455
+ disposition = "duplicate";
456
+ reason = `duplicate of fragment ${priorId} (duplicate 保留全部来源)`;
457
+ }
458
+ else if (!priorId && text) {
459
+ seenText.set(text, fragment.id);
460
+ }
461
+ const risk = disposition === "context-only" || disposition === "duplicate"
462
+ ? "low"
463
+ : "high";
464
+ return {
465
+ id: fragment.id,
466
+ path: fragment.path,
467
+ headingPath: fragment.headingPath,
468
+ lineRange: fragment.lineRange,
469
+ sha256: sha256Text(fragment.text),
470
+ risk,
471
+ disposition,
472
+ ...(reason ? { reason } : {}),
473
+ canonicalRequirementIds: mappedIds,
474
+ };
475
+ });
476
+ return { fragments, records, documents: sortedDocuments };
477
+ }