@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
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { buildFragmentInventory, FRAGMENT_ID_PATTERN, FRAGMENT_ID_PREFIX, normalizeFragmentText, sha256Text, } from "./fragment-inventory.js";
|
|
3
|
+
import { mergeInventoryItems, reconcileLedgerFragments, } from "./ledger-reconciliation.js";
|
|
4
|
+
import { LEDGER_SCHEMA_RECONCILER_VERSION, LEDGER_SCHEMA_VERSION, } from "./types.js";
|
|
5
|
+
function canonicalizeJsonValue(value) {
|
|
6
|
+
if (value === null)
|
|
7
|
+
return "null";
|
|
8
|
+
if (typeof value === "boolean")
|
|
9
|
+
return value ? "true" : "false";
|
|
10
|
+
if (typeof value === "number")
|
|
11
|
+
return String(value);
|
|
12
|
+
if (typeof value === "string")
|
|
13
|
+
return JSON.stringify(value);
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return `[${value.map(canonicalizeJsonValue).join(",")}]`;
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === "object") {
|
|
18
|
+
const record = value;
|
|
19
|
+
return `{${Object.keys(record)
|
|
20
|
+
.sort()
|
|
21
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalizeJsonValue(record[key])}`)
|
|
22
|
+
.join(",")}}`;
|
|
23
|
+
}
|
|
24
|
+
return "undefined";
|
|
25
|
+
}
|
|
26
|
+
export function sha256OfCanonicalJson(value) {
|
|
27
|
+
return createHash("sha256").update(canonicalizeJsonValue(value)).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 输入 digest:规范化源文档(path+content)+ canonical requirements +
|
|
31
|
+
* schema/reconciler version 的确定性摘要;派生导航视图 需求.md 投影不参与键计算。
|
|
32
|
+
* 账本复用键:相同 digest → 不重建(复用);不同 → 立即失效重建。
|
|
33
|
+
*/
|
|
34
|
+
export function computeLedgerInputDigest(input) {
|
|
35
|
+
return sha256OfCanonicalJson({
|
|
36
|
+
schemaVersion: input.schemaVersion ?? LEDGER_SCHEMA_VERSION,
|
|
37
|
+
reconcilerVersion: input.reconcilerVersion ?? LEDGER_SCHEMA_RECONCILER_VERSION,
|
|
38
|
+
sources: input.sourceDocuments
|
|
39
|
+
.map((doc) => ({ path: doc.path, content: doc.content.replace(/\r\n/g, "\n") }))
|
|
40
|
+
.sort((a, b) => a.path.localeCompare(b.path)),
|
|
41
|
+
canonicalRequirements: input.canonicalRequirements
|
|
42
|
+
.map((req) => ({ id: req.id, text: req.text }))
|
|
43
|
+
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function isLedgerFresh(ledger, expectedInputDigest) {
|
|
47
|
+
return ledger.inputDigest === expectedInputDigest;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 从持久化 ledger 恢复 build 侧的权威输入合同(AC-DIGEST-001/002/003):init-hybrid
|
|
51
|
+
* freshness 计算与 ledger build 使用完全相同的输入。canonical 侧按 build 顺序原样
|
|
52
|
+
* 保留输入 canonical requirements(id/text 逐字节),过滤物化 REQ-SRC-*(AC-HARD-002
|
|
53
|
+
* "物化不入键"不变量)即精确恢复 digest 输入集;source 侧原样返回权威 sourcePaths
|
|
54
|
+
* (repo-root relative POSIX,build 时 sourceDocuments 的路径集合)。
|
|
55
|
+
*/
|
|
56
|
+
export function recoverLedgerInputContract(ledger) {
|
|
57
|
+
return {
|
|
58
|
+
sourcePaths: [...ledger.sourcePaths],
|
|
59
|
+
canonicalRequirements: ledger.canonicalRequirements
|
|
60
|
+
.filter((requirement) => !requirement.id.startsWith(`${FRAGMENT_ID_PREFIX}-`))
|
|
61
|
+
.map((requirement) => ({
|
|
62
|
+
id: requirement.id,
|
|
63
|
+
text: requirement.text,
|
|
64
|
+
})),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 从派生 需求.md 中确定性移除 `## Source ledger` 区块,得到不含该区块的规范化
|
|
69
|
+
* 投影(供派生视图 requirementSha256 审计计算使用;不再参与 inputDigest)。
|
|
70
|
+
* 投影器写入的区块结构固定(标题 + 引用块 + 列表),故剥离是确定性的。
|
|
71
|
+
*/
|
|
72
|
+
export function stripLedgerSection(markdown) {
|
|
73
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
74
|
+
const sectionStart = lines.findIndex((line) => /^##\s+Source ledger\s*$/u.test(line.trim()));
|
|
75
|
+
if (sectionStart < 0)
|
|
76
|
+
return markdown.replace(/\r\n/g, "\n");
|
|
77
|
+
let sectionEnd = lines.length;
|
|
78
|
+
for (let index = sectionStart + 1; index < lines.length; index += 1) {
|
|
79
|
+
if (/^##\s+/u.test(lines[index] ?? "")) {
|
|
80
|
+
sectionEnd = index;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const kept = lines.slice(0, sectionStart);
|
|
85
|
+
// 移除区块前的一个空行分隔,再规范化尾部。
|
|
86
|
+
while (kept.length > 0 && kept[kept.length - 1]?.trim() === "") {
|
|
87
|
+
kept.pop();
|
|
88
|
+
}
|
|
89
|
+
return kept.join("\n") + "\n";
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 与当前账本 conflict/fragment identity 匹配、human-confirmed 且带明确 decision
|
|
93
|
+
* 的 resolution 才有效(AC-008 / AC-FIDELITY-REPAIR-008)。stale(sourceRef 不匹配
|
|
94
|
+
* 任何当前冲突/片段)、伪造(decidedBy ≠ human)或缺少 decision 的 resolution 不生效。
|
|
95
|
+
*/
|
|
96
|
+
export function effectiveSourceResolutions(ledger) {
|
|
97
|
+
const conflictIds = new Set(ledger.conflicts.map((entry) => entry.id));
|
|
98
|
+
const fragmentIds = new Set(ledger.fragments.map((record) => record.id));
|
|
99
|
+
const effective = new Set();
|
|
100
|
+
for (const resolution of ledger.sourceResolutions ?? []) {
|
|
101
|
+
if (resolution.decidedBy !== "human")
|
|
102
|
+
continue;
|
|
103
|
+
if (!resolution.decision?.trim())
|
|
104
|
+
continue;
|
|
105
|
+
if (conflictIds.has(resolution.sourceRef) ||
|
|
106
|
+
fragmentIds.has(resolution.sourceRef)) {
|
|
107
|
+
effective.add(resolution.sourceRef);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return effective;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 显式 AC 冲突(AC-007 / AC-HARD-001):同 ID 不同文本的每个冲突文本保留独立
|
|
114
|
+
* fragment identity(path/lineRange/sha256),绝不互相覆盖;同一规范化文本的多个
|
|
115
|
+
* 片段保留首个 identity(去重),texts 与 fragments 顺序一致、一一对应。
|
|
116
|
+
*/
|
|
117
|
+
function computeExplicitAcConflicts(input) {
|
|
118
|
+
const identityById = new Map();
|
|
119
|
+
for (const record of input.records) {
|
|
120
|
+
const explicit = /^AC(?:-[A-Z0-9]+)+$/iu.test(record.id)
|
|
121
|
+
? record.id
|
|
122
|
+
: null;
|
|
123
|
+
if (!explicit)
|
|
124
|
+
continue;
|
|
125
|
+
const rawFragment = input.fragments.find((fragment) => fragment.id === record.id &&
|
|
126
|
+
fragment.path === record.path &&
|
|
127
|
+
fragment.lineRange.start === record.lineRange.start &&
|
|
128
|
+
fragment.lineRange.end === record.lineRange.end);
|
|
129
|
+
const rawText = rawFragment?.text ?? record.id;
|
|
130
|
+
const text = normalizeFragmentText(rawText);
|
|
131
|
+
const identities = identityById.get(explicit) ?? new Map();
|
|
132
|
+
if (!identities.has(text)) {
|
|
133
|
+
identities.set(text, {
|
|
134
|
+
path: record.path,
|
|
135
|
+
lineRange: record.lineRange,
|
|
136
|
+
sha256: record.sha256,
|
|
137
|
+
text: rawText,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
identityById.set(explicit, identities);
|
|
141
|
+
}
|
|
142
|
+
return [...identityById.entries()]
|
|
143
|
+
.map(([id, identities]) => ({
|
|
144
|
+
id,
|
|
145
|
+
texts: [...identities.keys()],
|
|
146
|
+
fragments: [...identities.values()],
|
|
147
|
+
}))
|
|
148
|
+
.filter((entry) => entry.texts.length > 1);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 高维规范性 disposition(AC-002 / AC-HARD-002):requirement/acceptance/
|
|
152
|
+
* constraint/non-goal/unresolved 可执行内容均为高风险,必须被表示/映射;
|
|
153
|
+
* context-only / duplicate 为排除类 disposition,不走高风险未覆盖判定。
|
|
154
|
+
*/
|
|
155
|
+
export const HIGH_RISK_FRAGMENT_DISPOSITIONS = new Set([
|
|
156
|
+
"requirement",
|
|
157
|
+
"acceptance",
|
|
158
|
+
"constraint",
|
|
159
|
+
"non-goal",
|
|
160
|
+
"unresolved",
|
|
161
|
+
]);
|
|
162
|
+
/**
|
|
163
|
+
* 物化稳定 REQ-SRC-* canonical requirement id(AC-002 / AC-HARD-002):无既有
|
|
164
|
+
* canonical 匹配的高风险规范性片段由既有 generatedFragmentId 确定性哈希物化;
|
|
165
|
+
* 显式 AC id(如 AC-999)不在 canonical 集合时派生独立 REQ-SRC-* id。纯哈希派生、
|
|
166
|
+
* 不依赖遍历顺序,同一输入跨 run 恒等复现。
|
|
167
|
+
*/
|
|
168
|
+
export function materializedCanonicalRequirementId(record) {
|
|
169
|
+
if (/^REQ-SRC-/.test(record.id))
|
|
170
|
+
return record.id;
|
|
171
|
+
const digest = sha256Text(`materialized-canonical\u0000${record.id}\u0000${record.text}`);
|
|
172
|
+
return `${FRAGMENT_ID_PREFIX}-${digest.slice(0, 12).toUpperCase()}`;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 构建完整 requirement ledger:fragment inventory + disposition + 覆盖映射 + 统计。
|
|
176
|
+
* 完全确定性:相同输入产出 byte 一致结果(builtAt 由 inputDigest 确定性派生,
|
|
177
|
+
* 保证 DAG 生成期与 apply 期哈希一致)。reconcile executor 提供时仅叙述性/歧义
|
|
178
|
+
* 片段经顺序 reconciliation,失败即 fail closed。
|
|
179
|
+
*/
|
|
180
|
+
export async function buildRequirementLedger(input) {
|
|
181
|
+
const inventory = buildFragmentInventory({
|
|
182
|
+
documents: input.sourceDocuments,
|
|
183
|
+
canonicalRequirements: input.canonicalRequirements,
|
|
184
|
+
});
|
|
185
|
+
let records = inventory.records;
|
|
186
|
+
if (input.reconcile?.executor) {
|
|
187
|
+
const items = mergeInventoryItems({
|
|
188
|
+
records: inventory.records,
|
|
189
|
+
fragments: inventory.fragments,
|
|
190
|
+
});
|
|
191
|
+
const reconciled = await reconcileLedgerFragments({
|
|
192
|
+
fragments: items,
|
|
193
|
+
canonicalRequirements: input.canonicalRequirements,
|
|
194
|
+
executor: input.reconcile.executor,
|
|
195
|
+
...(input.reconcile.budgetChars !== undefined
|
|
196
|
+
? { budgetChars: input.reconcile.budgetChars }
|
|
197
|
+
: {}),
|
|
198
|
+
});
|
|
199
|
+
const shaById = new Map(inventory.records.map((record) => [record.id, record.sha256]));
|
|
200
|
+
records = reconciled.fragments.map((item) => ({
|
|
201
|
+
id: item.id,
|
|
202
|
+
path: item.path,
|
|
203
|
+
headingPath: item.headingPath,
|
|
204
|
+
lineRange: item.lineRange,
|
|
205
|
+
sha256: shaById.get(item.id) ?? sha256Text(item.text),
|
|
206
|
+
risk: item.risk,
|
|
207
|
+
disposition: item.disposition,
|
|
208
|
+
...(item.reason ? { reason: item.reason } : {}),
|
|
209
|
+
canonicalRequirementIds: item.canonicalRequirementIds,
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
const allResolutions = input.sourceResolutions ?? [];
|
|
213
|
+
// duplicate 保留全部来源:duplicate 片段仍计入 fragments 与 sourcePaths。
|
|
214
|
+
const duplicates = records.filter((record) => record.disposition === "duplicate");
|
|
215
|
+
const rawConflicts = computeExplicitAcConflicts({
|
|
216
|
+
records,
|
|
217
|
+
fragments: inventory.fragments,
|
|
218
|
+
});
|
|
219
|
+
const conflictIds = new Set(rawConflicts.map((entry) => entry.id));
|
|
220
|
+
const fragmentIds = new Set(records.map((record) => record.id));
|
|
221
|
+
// 有效 resolution:human-confirmed + 明确 decision + 匹配当前冲突/片段 identity。
|
|
222
|
+
const effectiveResolutions = allResolutions.filter((resolution) => resolution.decidedBy === "human" &&
|
|
223
|
+
resolution.decision?.trim().length > 0 &&
|
|
224
|
+
(conflictIds.has(resolution.sourceRef) ||
|
|
225
|
+
fragmentIds.has(resolution.sourceRef)));
|
|
226
|
+
const resolvedSourceRefs = new Set(effectiveResolutions.map((resolution) => resolution.sourceRef));
|
|
227
|
+
// 物化(AC-002 / AC-HARD-002):无既有 canonical 匹配的高风险规范性片段
|
|
228
|
+
// (requirement/acceptance/constraint/non-goal/unresolved 可执行内容)确定性
|
|
229
|
+
// 物化为稳定 REQ-SRC-* canonical requirement,绝不保留空 canonicalRequirementIds。
|
|
230
|
+
// 物化结果只进入 ledger 输出(ledger.canonicalRequirements / fragmentsByRequirement),
|
|
231
|
+
// 不改 computeLedgerInputDigest 输入集合(避免 SOURCE_FIDELITY_LEDGER_STALE)。
|
|
232
|
+
const fragmentTextById = new Map(inventory.fragments.map((fragment) => [fragment.id, fragment.text]));
|
|
233
|
+
const materialized = [];
|
|
234
|
+
const materializedIds = new Set();
|
|
235
|
+
const materializedRecords = records.map((record) => {
|
|
236
|
+
if (HIGH_RISK_FRAGMENT_DISPOSITIONS.has(record.disposition) &&
|
|
237
|
+
!resolvedSourceRefs.has(record.id) &&
|
|
238
|
+
record.canonicalRequirementIds.length === 0) {
|
|
239
|
+
const rawText = fragmentTextById.get(record.id) ?? record.id;
|
|
240
|
+
const canonicalId = materializedCanonicalRequirementId({
|
|
241
|
+
id: record.id,
|
|
242
|
+
text: rawText,
|
|
243
|
+
});
|
|
244
|
+
if (!materializedIds.has(canonicalId)) {
|
|
245
|
+
materializedIds.add(canonicalId);
|
|
246
|
+
materialized.push({ id: canonicalId, text: rawText });
|
|
247
|
+
}
|
|
248
|
+
return { ...record, canonicalRequirementIds: [canonicalId] };
|
|
249
|
+
}
|
|
250
|
+
return record;
|
|
251
|
+
});
|
|
252
|
+
const unresolved = materializedRecords.filter((record) => record.disposition === "unresolved" &&
|
|
253
|
+
!resolvedSourceRefs.has(record.id));
|
|
254
|
+
// 高风险未覆盖:全部高维 disposition(AC-002),空映射保持阻断。
|
|
255
|
+
const highRiskUncovered = materializedRecords.filter((record) => {
|
|
256
|
+
if (resolvedSourceRefs.has(record.id))
|
|
257
|
+
return false;
|
|
258
|
+
return (HIGH_RISK_FRAGMENT_DISPOSITIONS.has(record.disposition) &&
|
|
259
|
+
record.canonicalRequirementIds.length === 0);
|
|
260
|
+
});
|
|
261
|
+
const conflicts = rawConflicts.filter((entry) => !resolvedSourceRefs.has(entry.id));
|
|
262
|
+
// canonical → fragments 映射(含物化 REQ-SRC-* canonical;任何已映射片段都
|
|
263
|
+
// 贡献来源证据,不再仅限 acceptance disposition)。
|
|
264
|
+
const fragmentsByRequirement = new Map();
|
|
265
|
+
for (const record of materializedRecords) {
|
|
266
|
+
for (const reqId of record.canonicalRequirementIds) {
|
|
267
|
+
const ids = fragmentsByRequirement.get(reqId) ?? [];
|
|
268
|
+
if (!ids.includes(record.id))
|
|
269
|
+
ids.push(record.id);
|
|
270
|
+
fragmentsByRequirement.set(reqId, ids);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const sourceRefsFor = (fragmentIdsForRequirement) => [
|
|
274
|
+
...new Set(fragmentIdsForRequirement
|
|
275
|
+
.map((fragmentId) => materializedRecords.find((r) => r.id === fragmentId)
|
|
276
|
+
?.path ?? "")
|
|
277
|
+
.filter((path) => path.length > 0)),
|
|
278
|
+
];
|
|
279
|
+
const canonicalRequirements = [
|
|
280
|
+
...input.canonicalRequirements.map((req) => ({
|
|
281
|
+
id: req.id,
|
|
282
|
+
text: req.text,
|
|
283
|
+
sourceFragmentIds: fragmentsByRequirement.get(req.id) ?? [],
|
|
284
|
+
sourceRefs: sourceRefsFor(fragmentsByRequirement.get(req.id) ?? []),
|
|
285
|
+
})),
|
|
286
|
+
// 物化 canonical requirement(确定性插入顺序 = 片段枚举顺序)。
|
|
287
|
+
...materialized.map((req) => ({
|
|
288
|
+
id: req.id,
|
|
289
|
+
text: req.text,
|
|
290
|
+
sourceFragmentIds: fragmentsByRequirement.get(req.id) ?? [],
|
|
291
|
+
sourceRefs: sourceRefsFor(fragmentsByRequirement.get(req.id) ?? []),
|
|
292
|
+
})),
|
|
293
|
+
];
|
|
294
|
+
const stats = {
|
|
295
|
+
fragmentCount: records.length,
|
|
296
|
+
canonicalRequirementCount: canonicalRequirements.length,
|
|
297
|
+
acceptanceCount: records.filter((record) => record.disposition === "acceptance").length,
|
|
298
|
+
requirementCount: records.filter((record) => record.disposition === "requirement").length,
|
|
299
|
+
contextOnlyCount: records.filter((record) => record.disposition === "context-only").length,
|
|
300
|
+
duplicateCount: duplicates.length,
|
|
301
|
+
unresolvedCount: unresolved.length,
|
|
302
|
+
conflictCount: conflicts.length,
|
|
303
|
+
highRiskUncoveredCount: highRiskUncovered.length,
|
|
304
|
+
};
|
|
305
|
+
const inputDigest = computeLedgerInputDigest({
|
|
306
|
+
sourceDocuments: input.sourceDocuments,
|
|
307
|
+
canonicalRequirements: input.canonicalRequirements,
|
|
308
|
+
});
|
|
309
|
+
// builtAt 必须是确定性的:若未显式给定,由 inputDigest 派生固定时间戳,
|
|
310
|
+
// 使同一输入在任意时间构建的账本字节一致(binding ledgerSha256 可比对)。
|
|
311
|
+
const builtAt = input.builtAt ??
|
|
312
|
+
new Date(1_600_000_000_000 + (parseInt(inputDigest.slice(0, 8), 16) % 300_000_000_000))
|
|
313
|
+
.toISOString();
|
|
314
|
+
return {
|
|
315
|
+
schemaVersion: LEDGER_SCHEMA_VERSION,
|
|
316
|
+
taskId: input.taskId,
|
|
317
|
+
inputDigest,
|
|
318
|
+
requirementSha256: input.requirementSha256,
|
|
319
|
+
sourcePaths: input.sourceDocuments
|
|
320
|
+
.map((doc) => doc.path)
|
|
321
|
+
.sort((a, b) => a.localeCompare(b)),
|
|
322
|
+
fragments: materializedRecords,
|
|
323
|
+
canonicalRequirements,
|
|
324
|
+
stats,
|
|
325
|
+
unresolved: unresolved.map((record) => ({
|
|
326
|
+
fragmentId: record.id,
|
|
327
|
+
reason: record.reason ?? "unresolved fragment",
|
|
328
|
+
})),
|
|
329
|
+
conflicts,
|
|
330
|
+
sourceResolutions: allResolutions,
|
|
331
|
+
schemaReconcilerVersion: LEDGER_SCHEMA_RECONCILER_VERSION,
|
|
332
|
+
builtAt,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/** 验证账本完整性:无效路径/行号/hash/伪造 fragment ID、缺失 canonical 覆盖、
|
|
336
|
+
* unresolved/conflict(有效 human resolution 覆盖的项不阻断)。 */
|
|
337
|
+
export function validateRequirementLedger(ledger, options) {
|
|
338
|
+
const errors = [];
|
|
339
|
+
const allowedUnresolved = options?.allowedUnresolved ?? false;
|
|
340
|
+
const allowedConflicts = options?.allowedConflicts ?? false;
|
|
341
|
+
if (ledger.schemaVersion !== LEDGER_SCHEMA_VERSION) {
|
|
342
|
+
errors.push({
|
|
343
|
+
code: "SOURCE_FIDELITY_RECONCILIATION_FAILED",
|
|
344
|
+
message: `ledger schemaVersion ${ledger.schemaVersion} is not supported`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (ledger.schemaReconcilerVersion !== LEDGER_SCHEMA_RECONCILER_VERSION) {
|
|
348
|
+
errors.push({
|
|
349
|
+
code: "SOURCE_FIDELITY_RECONCILIATION_FAILED",
|
|
350
|
+
message: `ledger reconciler version ${ledger.schemaReconcilerVersion} does not match ${LEDGER_SCHEMA_RECONCILER_VERSION}`,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
for (const record of ledger.fragments) {
|
|
354
|
+
if (!record.id ||
|
|
355
|
+
!record.path ||
|
|
356
|
+
!Number.isInteger(record.lineRange.start) ||
|
|
357
|
+
!Number.isInteger(record.lineRange.end) ||
|
|
358
|
+
record.lineRange.start < 1 ||
|
|
359
|
+
record.lineRange.end < record.lineRange.start) {
|
|
360
|
+
errors.push({
|
|
361
|
+
code: "SOURCE_FIDELITY_FRAGMENT_INVALID",
|
|
362
|
+
message: `fragment ${record.id} has invalid path/lineRange`,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (!FRAGMENT_ID_PATTERN.test(record.id)) {
|
|
366
|
+
errors.push({
|
|
367
|
+
code: "SOURCE_FIDELITY_FRAGMENT_INVALID",
|
|
368
|
+
message: `fragment ${record.id} has a forged/unsupported fragment id (expected AC-* or REQ-SRC-*)`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (!/^[a-f0-9]{64}$/.test(record.sha256)) {
|
|
372
|
+
errors.push({
|
|
373
|
+
code: "SOURCE_FIDELITY_FRAGMENT_INVALID",
|
|
374
|
+
message: `fragment ${record.id} has invalid sha256`,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if ((record.disposition === "context-only" ||
|
|
378
|
+
record.disposition === "duplicate" ||
|
|
379
|
+
record.disposition === "unresolved") &&
|
|
380
|
+
!record.reason) {
|
|
381
|
+
errors.push({
|
|
382
|
+
code: "SOURCE_FIDELITY_CONTEXT_ONLY_UNREVIEWED",
|
|
383
|
+
message: `fragment ${record.id} (${record.disposition}) requires a complete exclusion reason`,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const canonicalIds = new Set(ledger.canonicalRequirements.map((req) => req.id));
|
|
388
|
+
// 高风险未覆盖(AC-002 / AC-HARD-002):requirement/acceptance/constraint/
|
|
389
|
+
// non-goal/unresolved 高维 disposition 空映射即阻断;引用未知 canonical 同样阻断。
|
|
390
|
+
for (const record of ledger.fragments) {
|
|
391
|
+
if (!HIGH_RISK_FRAGMENT_DISPOSITIONS.has(record.disposition))
|
|
392
|
+
continue;
|
|
393
|
+
if (record.canonicalRequirementIds.length === 0) {
|
|
394
|
+
errors.push({
|
|
395
|
+
code: "SOURCE_FIDELITY_HIGH_RISK_UNCOVERED",
|
|
396
|
+
message: `high-risk ${record.disposition} fragment ${record.id} is not mapped to any canonical requirement`,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
for (const reqId of record.canonicalRequirementIds) {
|
|
400
|
+
if (!canonicalIds.has(reqId)) {
|
|
401
|
+
errors.push({
|
|
402
|
+
code: "SOURCE_FIDELITY_REQUIREMENT_NOT_COVERED",
|
|
403
|
+
message: `fragment ${record.id} references unknown canonical requirement ${reqId}`,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const req of ledger.canonicalRequirements) {
|
|
409
|
+
if (req.sourceFragmentIds.length === 0) {
|
|
410
|
+
errors.push({
|
|
411
|
+
code: "SOURCE_FIDELITY_REQUIREMENT_NOT_COVERED",
|
|
412
|
+
message: `canonical requirement ${req.id} has no source fragment evidence`,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
for (const fragmentId of req.sourceFragmentIds) {
|
|
416
|
+
const record = ledger.fragments.find((f) => f.id === fragmentId);
|
|
417
|
+
if (!record) {
|
|
418
|
+
errors.push({
|
|
419
|
+
code: "SOURCE_FIDELITY_SOURCE_REF_INVALID",
|
|
420
|
+
message: `requirement ${req.id} references missing fragment ${fragmentId}`,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// 有效 human resolution 覆盖的 unresolved/conflict 不阻断(audit 保留在
|
|
426
|
+
// ledger.sourceResolutions;stale/伪造/未确认 resolution 不生效)。
|
|
427
|
+
const effective = effectiveSourceResolutions(ledger);
|
|
428
|
+
const blockingUnresolved = ledger.unresolved.filter((entry) => !effective.has(entry.fragmentId));
|
|
429
|
+
if (!allowedUnresolved && blockingUnresolved.length > 0) {
|
|
430
|
+
errors.push({
|
|
431
|
+
code: "SOURCE_FIDELITY_UNRESOLVED",
|
|
432
|
+
message: `ledger has ${blockingUnresolved.length} unresolved fragment(s): ${blockingUnresolved
|
|
433
|
+
.map((entry) => entry.fragmentId)
|
|
434
|
+
.join(", ")}`,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
const blockingConflicts = ledger.conflicts.filter((entry) => !effective.has(entry.id));
|
|
438
|
+
if (!allowedConflicts && blockingConflicts.length > 0) {
|
|
439
|
+
errors.push({
|
|
440
|
+
code: "SOURCE_FIDELITY_CONFLICT",
|
|
441
|
+
message: `ledger has ${blockingConflicts.length} conflict(s): ${blockingConflicts
|
|
442
|
+
.map((entry) => entry.id)
|
|
443
|
+
.join(", ")}`,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return errors;
|
|
447
|
+
}
|
|
448
|
+
/** 分级 gate:稳定错误码 + 阻断信息(AC-FIDELITY-003)。 */
|
|
449
|
+
export function classifyLedgerGate(ledger, options) {
|
|
450
|
+
if (!isLedgerFresh(ledger, options?.expectedInputDigest ?? ledger.inputDigest)) {
|
|
451
|
+
return {
|
|
452
|
+
ok: false,
|
|
453
|
+
code: "SOURCE_FIDELITY_LEDGER_STALE",
|
|
454
|
+
message: "ledger inputDigest does not match current source input; rebuild required",
|
|
455
|
+
ledger,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const errors = validateRequirementLedger(ledger);
|
|
459
|
+
if (errors.length > 0) {
|
|
460
|
+
// 分级 fail-closed:按优先级表顺序取首个命中错误(AC-BLOCK-001:
|
|
461
|
+
// REQUIREMENT_NOT_COVERED 纳入优先级表;与 UNRESOLVED/CONFLICT 并存时仍由
|
|
462
|
+
// 表序决定 primary code,而不是 errors 数组顺序)。
|
|
463
|
+
const priority = [
|
|
464
|
+
"SOURCE_FIDELITY_UNRESOLVED",
|
|
465
|
+
"SOURCE_FIDELITY_CONFLICT",
|
|
466
|
+
"SOURCE_FIDELITY_HIGH_RISK_UNCOVERED",
|
|
467
|
+
"SOURCE_FIDELITY_REQUIREMENT_NOT_COVERED",
|
|
468
|
+
"SOURCE_FIDELITY_RECONCILIATION_FAILED",
|
|
469
|
+
];
|
|
470
|
+
const first = priority
|
|
471
|
+
.map((code) => errors.find((error) => error.code === code))
|
|
472
|
+
.find((error) => error !== undefined) ?? errors[0];
|
|
473
|
+
return {
|
|
474
|
+
ok: false,
|
|
475
|
+
code: first.code,
|
|
476
|
+
message: errors.map((error) => error.message).join("; "),
|
|
477
|
+
ledger,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
return { ok: true, ledger };
|
|
481
|
+
}
|
|
482
|
+
/** 安全摘要(Console/Inspect 展示,不泄漏敏感原文)。 */
|
|
483
|
+
export function summarizeLedger(ledger, options) {
|
|
484
|
+
const exclusions = ledger.fragments
|
|
485
|
+
.filter((record) => record.disposition === "context-only" ||
|
|
486
|
+
record.disposition === "duplicate" ||
|
|
487
|
+
record.disposition === "unresolved")
|
|
488
|
+
.map((record) => ({
|
|
489
|
+
fragmentId: record.id,
|
|
490
|
+
reason: truncateReason(record.reason ?? record.disposition),
|
|
491
|
+
}));
|
|
492
|
+
return {
|
|
493
|
+
present: true,
|
|
494
|
+
fragmentCount: ledger.stats.fragmentCount,
|
|
495
|
+
canonicalRequirementCount: ledger.stats.canonicalRequirementCount,
|
|
496
|
+
contextOnlyCount: ledger.stats.contextOnlyCount,
|
|
497
|
+
duplicateCount: ledger.stats.duplicateCount,
|
|
498
|
+
unresolvedCount: ledger.stats.unresolvedCount,
|
|
499
|
+
conflictCount: ledger.stats.conflictCount,
|
|
500
|
+
gateStatus: options?.gateStatus ?? "unknown",
|
|
501
|
+
failureCode: options?.failureCode ?? null,
|
|
502
|
+
exclusions,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
export function truncateReason(reason, max = 120) {
|
|
506
|
+
return reason.length > max ? `${reason.slice(0, max)}…` : reason;
|
|
507
|
+
}
|
|
508
|
+
export function ledgerStatsLine(ledger) {
|
|
509
|
+
const s = ledger.stats;
|
|
510
|
+
return [
|
|
511
|
+
`fragments=${s.fragmentCount}`,
|
|
512
|
+
`canonical=${s.canonicalRequirementCount}`,
|
|
513
|
+
`contextOnly=${s.contextOnlyCount}`,
|
|
514
|
+
`duplicate=${s.duplicateCount}`,
|
|
515
|
+
`unresolved=${s.unresolvedCount}`,
|
|
516
|
+
`conflict=${s.conflictCount}`,
|
|
517
|
+
`highRiskUncovered=${s.highRiskUncoveredCount}`,
|
|
518
|
+
].join(" ");
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* 损坏/不支持 ledger 的结构化错误(AC-009 / AC-HARD-003):稳定 SOURCE_FIDELITY_*
|
|
522
|
+
* 错误码 + 原因 message 的 envelope,替代裸 Error。routes 的 handleTaskLedger 解析
|
|
523
|
+
* 失败分支与 prewrite/init-hybrid 消费同一稳定码,杜绝字符串匹配裸 Error 文本。
|
|
524
|
+
*/
|
|
525
|
+
export class LedgerParseError extends Error {
|
|
526
|
+
code = "SOURCE_FIDELITY_LEDGER_STALE";
|
|
527
|
+
constructor(message, options) {
|
|
528
|
+
super(message, options);
|
|
529
|
+
this.name = "LedgerParseError";
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
export function parseLedgerJson(raw) {
|
|
533
|
+
let parsed;
|
|
534
|
+
try {
|
|
535
|
+
parsed = JSON.parse(raw);
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
throw new LedgerParseError("SOURCE_FIDELITY_LEDGER_STALE: requirement-ledger.json is not valid JSON");
|
|
539
|
+
}
|
|
540
|
+
const record = parsed;
|
|
541
|
+
if (record?.schemaVersion !== LEDGER_SCHEMA_VERSION) {
|
|
542
|
+
throw new LedgerParseError(`SOURCE_FIDELITY_LEDGER_STALE: unsupported requirement-ledger schemaVersion ${String(record?.schemaVersion)}`);
|
|
543
|
+
}
|
|
544
|
+
return parsed;
|
|
545
|
+
}
|