@citeark/agent 0.3.20

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 (347) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +128 -0
  3. package/data/dataset-source-registry.v1.json +300 -0
  4. package/dist/arkgraph/boot.js +6 -0
  5. package/dist/arkgraph/index.html +1 -0
  6. package/dist/arkgraph/viewer.css +1 -0
  7. package/dist/arkgraph/viewer.en.css +1 -0
  8. package/dist/arkgraph/viewer.en.js +49 -0
  9. package/dist/arkgraph/viewer.en.js.LEGAL.txt +56 -0
  10. package/dist/arkgraph/viewer.js +49 -0
  11. package/dist/arkgraph/viewer.js.LEGAL.txt +56 -0
  12. package/docker/claude-code/Dockerfile +97 -0
  13. package/docker/claude-code/codex-pro-relay.mjs +466 -0
  14. package/docker/claude-code/runtime-contract-check.mjs +79 -0
  15. package/docs/arkgraph-reading.md +79 -0
  16. package/docs/configuration.md +100 -0
  17. package/docs/integration.md +92 -0
  18. package/docs/maturity-plan.md +27 -0
  19. package/docs/npm-release.md +44 -0
  20. package/docs/paper-reading.md +40 -0
  21. package/docs/research-plan-granularity.md +27 -0
  22. package/docs/terminal.md +49 -0
  23. package/examples/toy-evaluation/compile-task.json +27 -0
  24. package/examples/toy-evaluation/paper.md +5 -0
  25. package/examples/toy-evaluation/repository/README.md +9 -0
  26. package/examples/toy-evaluation/repository/checkpoint.json +4 -0
  27. package/examples/toy-evaluation/repository/evaluate.py +17 -0
  28. package/examples/toy-evaluation/task.json +81 -0
  29. package/package.json +59 -0
  30. package/prompts/compile-research.md +58 -0
  31. package/prompts/execute-contract.md +72 -0
  32. package/prompts/execute-workspace-simple.md +51 -0
  33. package/prompts/execute-workspace.md +34 -0
  34. package/prompts/prepare-reproduction.md +82 -0
  35. package/prompts/repair-research.md +45 -0
  36. package/protocol/CAP.md +129 -0
  37. package/protocol/LICENSE +12 -0
  38. package/protocol/MAPPINGS.md +72 -0
  39. package/protocol/README.md +38 -0
  40. package/protocol/conformance-v2.0-alpha.1.json +36 -0
  41. package/protocol/examples/arkgraph/checkpoint-evaluation.json +309 -0
  42. package/protocol/examples/arkgraph/fixtures.mjs +49 -0
  43. package/protocol/examples/arkgraph/paper-free.json +291 -0
  44. package/protocol/examples/arkgraph/partial-failure.json +344 -0
  45. package/protocol/examples/arkgraph/training-evaluation.json +443 -0
  46. package/protocol/profiles/agent-trace.md +16 -0
  47. package/protocol/profiles/computational-run.md +16 -0
  48. package/protocol/profiles/core.md +15 -0
  49. package/protocol/profiles/public-bundle.md +18 -0
  50. package/protocol/profiles/reproduction.md +29 -0
  51. package/protocol/profiles/research-compilation.md +44 -0
  52. package/protocol/profiles/research-plan.md +39 -0
  53. package/protocol/profiles/restricted-evidence.md +15 -0
  54. package/runtime/bootstrap-autodl-runtime.sh +314 -0
  55. package/runtime/create-runtime-venv.sh +41 -0
  56. package/runtime/install-local-cpu-runtime.sh +23 -0
  57. package/runtime/install-scientific-runtime.sh +153 -0
  58. package/runtime/mineru/parse.py +62 -0
  59. package/runtime/mineru/requirements.txt +4 -0
  60. package/runtime/requirements-baseline.txt +38 -0
  61. package/schemas/cap/v2/activity.schema.json +47 -0
  62. package/schemas/cap/v2/agent.schema.json +32 -0
  63. package/schemas/cap/v2/assertion.schema.json +110 -0
  64. package/schemas/cap/v2/descriptor.schema.json +243 -0
  65. package/schemas/cap/v2/entity.schema.json +64 -0
  66. package/schemas/cap/v2/manifest.schema.json +67 -0
  67. package/schemas/cap/v2/relation.schema.json +82 -0
  68. package/schemas/compute-catalog.schema.json +63 -0
  69. package/schemas/compute-decision.schema.json +27 -0
  70. package/schemas/execution-contract.schema.json +1024 -0
  71. package/schemas/research-card.schema.json +30 -0
  72. package/schemas/research-inventory-draft.schema.json +366 -0
  73. package/schemas/research.schema.json +1044 -0
  74. package/schemas/result.schema.json +173 -0
  75. package/schemas/verification-policy.schema.json +47 -0
  76. package/schemas/verified-conclusion.schema.json +58 -0
  77. package/schemas/workspace-summary.schema.json +24 -0
  78. package/scripts/build-arkgraph-view.mjs +12 -0
  79. package/scripts/check-execution-feasibility.mjs +24 -0
  80. package/scripts/check-syntax.mjs +15 -0
  81. package/scripts/deterministic-asset-preparation.py +438 -0
  82. package/scripts/package-cap.mjs +23 -0
  83. package/scripts/package-local-agent.mjs +23 -0
  84. package/scripts/preview-arkgraph.mjs +25 -0
  85. package/scripts/replay-research-compiler-candidate.mjs +134 -0
  86. package/scripts/review-compiler-sources.mjs +44 -0
  87. package/scripts/run-asset-preparation.sh +17 -0
  88. package/scripts/run-research-plan.mjs +98 -0
  89. package/scripts/validate-asset-preparation.py +290 -0
  90. package/scripts/verify-local-runtime.mjs +57 -0
  91. package/scripts/verify-npm-package.mjs +57 -0
  92. package/src/adapters/paper2agent.mjs +107 -0
  93. package/src/assets/cache.mjs +159 -0
  94. package/src/assets/compute.mjs +98 -0
  95. package/src/assets/executor.mjs +145 -0
  96. package/src/assets/lifecycle.mjs +213 -0
  97. package/src/assets/manifest.mjs +242 -0
  98. package/src/assets/opportunistic-preparation.mjs +81 -0
  99. package/src/assets/plan.mjs +411 -0
  100. package/src/assets/prompts.mjs +29 -0
  101. package/src/assets/public-asset-probe.mjs +525 -0
  102. package/src/assets/qualification.mjs +119 -0
  103. package/src/assets/readiness.mjs +130 -0
  104. package/src/assets/reproduction-admission.mjs +355 -0
  105. package/src/assets/requirements.mjs +152 -0
  106. package/src/assets/source-grounding.mjs +341 -0
  107. package/src/assets/source-policy.mjs +118 -0
  108. package/src/autodl/client.mjs +260 -0
  109. package/src/autodl/ssh.mjs +380 -0
  110. package/src/autodl/tools.mjs +129 -0
  111. package/src/cap/redaction.mjs +38 -0
  112. package/src/cap/v2/archive.mjs +152 -0
  113. package/src/cap/v2/attestation.mjs +204 -0
  114. package/src/cap/v2/canonical-json.mjs +114 -0
  115. package/src/cap/v2/compilation-artifact.mjs +240 -0
  116. package/src/cap/v2/core.mjs +282 -0
  117. package/src/cap/v2/measurement-assessment-records.mjs +23 -0
  118. package/src/cap/v2/pipeline-artifact.mjs +922 -0
  119. package/src/cap/v2/read.mjs +41 -0
  120. package/src/cap/v2/reassessment-artifact.mjs +383 -0
  121. package/src/cap/v2/research-artifact.mjs +231 -0
  122. package/src/cap/v2/research-map-records.mjs +46 -0
  123. package/src/cap/v2/research-object-records.mjs +163 -0
  124. package/src/cap/v2/research-records.mjs +187 -0
  125. package/src/cap/v2/verify.mjs +642 -0
  126. package/src/cli.mjs +1146 -0
  127. package/src/compute/autodl-pro-compiler.mjs +347 -0
  128. package/src/compute/autodl-pro-executor.mjs +459 -0
  129. package/src/compute/autodl-pro-job.mjs +843 -0
  130. package/src/compute/autodl-pro-network.mjs +295 -0
  131. package/src/compute/autodl-pro-remote.mjs +810 -0
  132. package/src/compute/autodl-pro-staging.mjs +117 -0
  133. package/src/compute/campaign.mjs +110 -0
  134. package/src/compute/catalog.mjs +123 -0
  135. package/src/compute/checkpoint-protocol.mjs +154 -0
  136. package/src/compute/codex-account-lock.mjs +111 -0
  137. package/src/compute/codex-account-session.mjs +107 -0
  138. package/src/compute/compiler-profile.mjs +38 -0
  139. package/src/compute/compiler-router.mjs +23 -0
  140. package/src/compute/coordinator-recovery.mjs +210 -0
  141. package/src/compute/executor-router.mjs +29 -0
  142. package/src/compute/gcp-batch-compiler.mjs +685 -0
  143. package/src/compute/gcp-batch-executor.mjs +1215 -0
  144. package/src/compute/gcp-batch-failure.mjs +92 -0
  145. package/src/compute/gcp-batch-job.mjs +527 -0
  146. package/src/compute/gcp-batch-lifecycle.mjs +81 -0
  147. package/src/compute/gcp-checkpoint-worker.mjs +1633 -0
  148. package/src/compute/local-codex-compiler.mjs +52 -0
  149. package/src/compute/measurement-hardware.mjs +128 -0
  150. package/src/compute/remote-attempt.mjs +226 -0
  151. package/src/compute/requirements.mjs +124 -0
  152. package/src/compute/research-phases.mjs +48 -0
  153. package/src/compute/scheduler.mjs +452 -0
  154. package/src/compute/shared-workloads.mjs +26 -0
  155. package/src/compute/stage-archive.mjs +79 -0
  156. package/src/contracts/campaign-contract.mjs +52 -0
  157. package/src/contracts/execution-contract.mjs +819 -0
  158. package/src/contracts/execution-mode.mjs +19 -0
  159. package/src/contracts/execution-timeouts.mjs +45 -0
  160. package/src/contracts/execution-workload.mjs +68 -0
  161. package/src/contracts/preflight-schema.mjs +25 -0
  162. package/src/contracts/public-contract.mjs +63 -0
  163. package/src/contracts/subject-tags.mjs +31 -0
  164. package/src/dashboard/data.mjs +898 -0
  165. package/src/dashboard/server.mjs +79 -0
  166. package/src/dashboard/static/dashboard.css +366 -0
  167. package/src/dashboard/static/dashboard.js +560 -0
  168. package/src/dashboard/static/index.html +85 -0
  169. package/src/deployment/community-policy.mjs +9 -0
  170. package/src/deployment/environment.mjs +112 -0
  171. package/src/deployment/guided.mjs +98 -0
  172. package/src/deployment/handoff.mjs +102 -0
  173. package/src/deployment/local-contract.mjs +31 -0
  174. package/src/deployment/local.mjs +100 -0
  175. package/src/deployment/prepare.mjs +46 -0
  176. package/src/deployment/recipe.mjs +108 -0
  177. package/src/deployment/supplement.mjs +51 -0
  178. package/src/deployment/terminal.mjs +43 -0
  179. package/src/diagnosis/renderer.mjs +75 -0
  180. package/src/diagnosis/target-failure.mjs +46 -0
  181. package/src/evidence/parser-registry.mjs +54 -0
  182. package/src/evidence/parsers/fasttext-classification.mjs +82 -0
  183. package/src/evidence/parsers/json-scalar.mjs +96 -0
  184. package/src/evidence/parsers/simcse-senteval.mjs +104 -0
  185. package/src/evidence/parsers/starspace-classification.mjs +78 -0
  186. package/src/evidence/registry.mjs +147 -0
  187. package/src/execution/runner-audit.mjs +473 -0
  188. package/src/gcp/auth.mjs +106 -0
  189. package/src/gcp/batch-client.mjs +120 -0
  190. package/src/gcp/resource-discovery.mjs +177 -0
  191. package/src/gcp/rest.mjs +82 -0
  192. package/src/gcp/secret-manager.mjs +34 -0
  193. package/src/gcp/signed-url.mjs +133 -0
  194. package/src/gcp/storage.mjs +220 -0
  195. package/src/graph/command.mjs +41 -0
  196. package/src/graph/execution.mjs +97 -0
  197. package/src/graph/model.mjs +37 -0
  198. package/src/graph/presentation.mjs +110 -0
  199. package/src/graph/query.mjs +159 -0
  200. package/src/graph/research-relations.mjs +69 -0
  201. package/src/graph/source-page.mjs +12 -0
  202. package/src/graph/source-preview.mjs +34 -0
  203. package/src/graph/validate.mjs +76 -0
  204. package/src/job.mjs +496 -0
  205. package/src/network/autodl-routing-proxy.mjs +462 -0
  206. package/src/network/egress-proxy.mjs +158 -0
  207. package/src/observability/event-contract.mjs +230 -0
  208. package/src/observability/pipeline-monitor.mjs +166 -0
  209. package/src/pipeline/orchestrator.mjs +1281 -0
  210. package/src/pipeline/recovery-error.mjs +11 -0
  211. package/src/pipeline/replay.mjs +304 -0
  212. package/src/pipeline/shared-execution.mjs +115 -0
  213. package/src/pipeline/stage-checkpoint.mjs +86 -0
  214. package/src/pipeline/stage-recovery.mjs +101 -0
  215. package/src/pipeline/targets.mjs +110 -0
  216. package/src/process.mjs +143 -0
  217. package/src/protocol.mjs +312 -0
  218. package/src/provider/codex-account.mjs +44 -0
  219. package/src/provider/codex-completion.mjs +49 -0
  220. package/src/provider/completion.mjs +292 -0
  221. package/src/provider/model-client.mjs +44 -0
  222. package/src/provider/model-route.mjs +29 -0
  223. package/src/provider/openrouter-readiness.mjs +189 -0
  224. package/src/provider/reader-bridge.mjs +35 -0
  225. package/src/provider/relay.mjs +263 -0
  226. package/src/provider/runtime-auth.mjs +40 -0
  227. package/src/public/cap.d.mts +90 -0
  228. package/src/public/cap.mjs +12 -0
  229. package/src/public/contracts.d.mts +2 -0
  230. package/src/public/host.mjs +171 -0
  231. package/src/public/operations.d.mts +11 -0
  232. package/src/public/presentation.d.mts +4 -0
  233. package/src/records/views.mjs +26 -0
  234. package/src/remote/command.mjs +178 -0
  235. package/src/remote/ssh.mjs +59 -0
  236. package/src/repository-origin.mjs +81 -0
  237. package/src/reproduction/evidence-feedback.mjs +96 -0
  238. package/src/reproduction/incomplete-initialization.mjs +25 -0
  239. package/src/reproduction/lifecycle.mjs +253 -0
  240. package/src/reproduction/plan.mjs +132 -0
  241. package/src/reproduction/prompts.mjs +70 -0
  242. package/src/reproduction/runner.mjs +188 -0
  243. package/src/reproduction/summary.mjs +130 -0
  244. package/src/reproduction/workspace-mode.mjs +7 -0
  245. package/src/research/automatic-admission.mjs +156 -0
  246. package/src/research/compiler-coverage.mjs +85 -0
  247. package/src/research/compiler-failure.mjs +24 -0
  248. package/src/research/compiler-normalization-guards.mjs +112 -0
  249. package/src/research/compiler-repair.mjs +3 -0
  250. package/src/research/compiler.mjs +853 -0
  251. package/src/research/continuation-selection.mjs +26 -0
  252. package/src/research/execution-graph-context.mjs +43 -0
  253. package/src/research/experiment-importance.mjs +15 -0
  254. package/src/research/inventory-handoff.mjs +104 -0
  255. package/src/research/inventory-revisions.mjs +32 -0
  256. package/src/research/mineru-local.mjs +73 -0
  257. package/src/research/paper-command.mjs +19 -0
  258. package/src/research/paper-markdown.mjs +180 -0
  259. package/src/research/paper-source-map.mjs +69 -0
  260. package/src/research/planning-policy.mjs +88 -0
  261. package/src/research/reference-materials.mjs +11 -0
  262. package/src/research/reproduction-scope.mjs +30 -0
  263. package/src/research/research-map.mjs +94 -0
  264. package/src/research/research-objects.mjs +88 -0
  265. package/src/research/source-discovery.mjs +646 -0
  266. package/src/research/source-observations.mjs +75 -0
  267. package/src/research/source-review-cli-mcp.mjs +26 -0
  268. package/src/research/source-review-input.mjs +209 -0
  269. package/src/research/source-review-local-codex.mjs +36 -0
  270. package/src/research/source-review-model.mjs +70 -0
  271. package/src/research/source-review.mjs +173 -0
  272. package/src/research/structure.mjs +3163 -0
  273. package/src/research-card/renderer.mjs +277 -0
  274. package/src/research-card/verified-conclusion.mjs +143 -0
  275. package/src/results/output-registry.mjs +183 -0
  276. package/src/runtime/claude-code.mjs +52 -0
  277. package/src/runtime/codex-capacity-retry.mjs +87 -0
  278. package/src/runtime/codex.mjs +64 -0
  279. package/src/runtime/config.mjs +157 -0
  280. package/src/runtime/final-output.mjs +40 -0
  281. package/src/runtime/index.mjs +21 -0
  282. package/src/runtime/local-codex.mjs +74 -0
  283. package/src/runtime/opencode.mjs +95 -0
  284. package/src/runtime/prompt.mjs +13 -0
  285. package/src/sandbox/docker.mjs +363 -0
  286. package/src/settings/command.mjs +297 -0
  287. package/src/settings/store.mjs +119 -0
  288. package/src/telemetry/pricing.mjs +68 -0
  289. package/src/telemetry/usage.mjs +265 -0
  290. package/src/terminal/events.mjs +97 -0
  291. package/src/terminal/input.mjs +40 -0
  292. package/src/terminal/plain.mjs +40 -0
  293. package/src/terminal/remote-stream.mjs +22 -0
  294. package/src/terminal/screen.mjs +214 -0
  295. package/src/terminal/transcript.mjs +69 -0
  296. package/src/util.mjs +107 -0
  297. package/src/verification/ai-assessor.mjs +534 -0
  298. package/src/verification/claim-evaluator.mjs +242 -0
  299. package/src/verification/evidence-context.mjs +165 -0
  300. package/src/verification/evidence-reader.mjs +95 -0
  301. package/src/verification/integrity.mjs +570 -0
  302. package/src/verification/tolerance.mjs +32 -0
  303. package/src/workloads/cpu-research-preparation.mjs +56 -0
  304. package/src/workloads/definition.mjs +74 -0
  305. package/src/workloads/phase-aware-reproduction.mjs +46 -0
  306. package/src/workloads/reproduction.mjs +85 -0
  307. package/src/workspace/command.mjs +242 -0
  308. package/src/workspace/control.mjs +49 -0
  309. package/src/workspace/entry.mjs +28 -0
  310. package/src/workspace/input.mjs +93 -0
  311. package/src/workspace/interactive.mjs +94 -0
  312. package/src/workspace/jobs.mjs +418 -0
  313. package/src/workspace/session.mjs +97 -0
  314. package/src/workspace/worker.mjs +137 -0
  315. package/ui/arkgraph/ambient-motion.mjs +10 -0
  316. package/ui/arkgraph/app.jsx +153 -0
  317. package/ui/arkgraph/boot.js +6 -0
  318. package/ui/arkgraph/camera-motion.mjs +20 -0
  319. package/ui/arkgraph/context-reveal.mjs +39 -0
  320. package/ui/arkgraph/details.css +3 -0
  321. package/ui/arkgraph/entry.jsx +28 -0
  322. package/ui/arkgraph/experiment-curves.mjs +17 -0
  323. package/ui/arkgraph/experiment-selection.mjs +15 -0
  324. package/ui/arkgraph/experiment-style.css +26 -0
  325. package/ui/arkgraph/experiment-ui.jsx +32 -0
  326. package/ui/arkgraph/frame.html +1 -0
  327. package/ui/arkgraph/graph-gestures.mjs +62 -0
  328. package/ui/arkgraph/label-layout.mjs +57 -0
  329. package/ui/arkgraph/locales/en.json +229 -0
  330. package/ui/arkgraph/locales/source-types.json +15 -0
  331. package/ui/arkgraph/localization-build.mjs +27 -0
  332. package/ui/arkgraph/material-build.mjs +23 -0
  333. package/ui/arkgraph/material-colors.mjs +39 -0
  334. package/ui/arkgraph/material-style.css +15 -0
  335. package/ui/arkgraph/open-graph.jsx +326 -0
  336. package/ui/arkgraph/outline.jsx +49 -0
  337. package/ui/arkgraph/package-lock.json +888 -0
  338. package/ui/arkgraph/package.json +17 -0
  339. package/ui/arkgraph/reading-layout.mjs +130 -0
  340. package/ui/arkgraph/reading-presentation.mjs +73 -0
  341. package/ui/arkgraph/record-detail.css +51 -0
  342. package/ui/arkgraph/record-details.jsx +29 -0
  343. package/ui/arkgraph/research-types.mjs +31 -0
  344. package/ui/arkgraph/selection-mark.jsx +6 -0
  345. package/ui/arkgraph/soft-spine.mjs +26 -0
  346. package/ui/arkgraph/steering-style.css +187 -0
  347. package/ui/arkgraph/style.css +272 -0
@@ -0,0 +1,159 @@
1
+ import { CAP_RECORD_TYPE, referenceKey, primitiveOf } from './model.mjs';
2
+
3
+ export const ARKGRAPH_QUERY_VERSION = 'arkgraph/1';
4
+ const routePredicates = new Set(['requires', 'partOf', 'used', 'generated', 'follows', 'derivedFrom', 'plannedFor', 'evidenceFor', 'assesses', 'basedOn']);
5
+
6
+ /** Callers MUST supply only artifacts readable by the current principal.
7
+ * A content digest is identity, never an authorization credential.
8
+ */
9
+ export function createArkGraph(artifacts) {
10
+ const records = new Map();
11
+ const artifactIds = new Set(artifacts.map(item => item.artifactDigest));
12
+ for (const artifact of artifacts) {
13
+ const descriptors = new Map(artifact.manifest.records.map(d => [d.id, d]));
14
+ for (const record of artifact.graphRecords) {
15
+ const descriptor = descriptors.get(record.id);
16
+ if (!descriptor) throw Error(`Missing authenticated descriptor for ${record.id}`);
17
+ const key = referenceKey({ ref: record.id, digest: descriptor.digest });
18
+ const existing = records.get(key);
19
+ if (existing) { if (!existing.artifacts.includes(artifact.artifactDigest)) existing.artifacts.push(artifact.artifactDigest); continue; }
20
+ records.set(key, { key, ref: record.id, digest: descriptor.digest, recordType: record.type,
21
+ role: record.role, record, artifacts: [artifact.artifactDigest] });
22
+ }
23
+ }
24
+ const resolve = reference => {
25
+ const candidate = records.get(referenceKey(reference));
26
+ if (!candidate || (reference.artifactDigest && (!artifactIds.has(reference.artifactDigest) || !candidate.artifacts.includes(reference.artifactDigest)))) return 'unavailable';
27
+ return candidate.recordType === reference.recordType ? 'resolved' : 'type-mismatch';
28
+ };
29
+ const nodes = [], relations = [];
30
+ for (const record of records.values()) {
31
+ record.artifacts.sort();
32
+ if (record.recordType === CAP_RECORD_TYPE.relation) {
33
+ relations.push({ ...record, predicate: record.record.predicate,
34
+ subject: record.record.subject, object: record.record.object,
35
+ resolution: { subject: resolve(record.record.subject), object: resolve(record.record.object) } });
36
+ } else nodes.push(record);
37
+ }
38
+ const byKey = (a, b) => a.key.localeCompare(b.key, 'en');
39
+ return { queryVersion: ARKGRAPH_QUERY_VERSION, artifacts: [...artifactIds].sort(),
40
+ nodes: nodes.sort(byKey), relations: relations.sort(byKey),
41
+ attestations: artifacts.map(a => ({ artifactDigest: a.artifactDigest, attestations: a.attestations ?? [] })) };
42
+ }
43
+
44
+ export function queryArkGraph(graph, { roots = [], predicates, depth = 4, limit = 200, cursor } = {}) {
45
+ if (!Number.isInteger(depth) || depth < 0 || depth > 12) throw Error('depth must be between 0 and 12');
46
+ if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw Error('limit must be between 1 and 500');
47
+ const offset = cursor == null ? 0 : Number(cursor);
48
+ if (!Number.isSafeInteger(offset) || offset < 0 || String(offset) !== String(cursor ?? 0)) throw Error('invalid graph cursor');
49
+ const validRoots = roots.filter(root => getArkGraphRecord(graph, root));
50
+ const rootKeys = new Set(validRoots.map(referenceKey));
51
+ const edges = graph.relations.filter(edge => !predicates || predicates.includes(edge.predicate));
52
+ const reachable = new Set(rootKeys);
53
+ let frontier = new Set(rootKeys);
54
+ for (let i = 0; i < depth && frontier.size; i++) {
55
+ const next = new Set();
56
+ for (const edge of edges) {
57
+ const subject = referenceKey(edge.subject), object = referenceKey(edge.object);
58
+ if (edge.resolution.subject !== 'resolved' || edge.resolution.object !== 'resolved') continue;
59
+ if (frontier.has(subject) && !reachable.has(object)) next.add(object);
60
+ if (frontier.has(object) && !reachable.has(subject)) next.add(subject);
61
+ }
62
+ for (const key of next) reachable.add(key);
63
+ frontier = next;
64
+ }
65
+ const selected = graph.nodes.filter(node => !roots.length || reachable.has(node.key));
66
+ const nodes = selected.slice(offset, offset + limit);
67
+ const visible = new Set(nodes.map(node => node.key));
68
+ const relations = edges.filter(edge => visible.has(referenceKey(edge.subject)) || visible.has(referenceKey(edge.object)))
69
+ .map(edge => ({ ...edge, selection: { subject: visible.has(referenceKey(edge.subject)), object: visible.has(referenceKey(edge.object)) } }));
70
+ const boundary = rootKeys.size && edges.some(edge => reachable.has(referenceKey(edge.subject)) !== reachable.has(referenceKey(edge.object)));
71
+ return { queryVersion: graph.queryVersion, artifacts: graph.artifacts, nodes, relations,
72
+ scope: { roots, predicates: predicates ?? null, depth },
73
+ missingRoots: roots.filter(root => !getArkGraphRecord(graph, root)),
74
+ truncated: Boolean(graph.truncated || boundary || offset > 0 || offset + limit < selected.length),
75
+ nextCursor: offset + limit < selected.length ? String(offset + limit) : null,
76
+ totalSelected: selected.length, attestations: graph.attestations };
77
+ }
78
+
79
+ export function getArkGraphRecord(graph, reference) {
80
+ const key = referenceKey(reference);
81
+ const item = [...graph.nodes, ...graph.relations].find(record => record.key === key);
82
+ if (!item || (reference.recordType && item.recordType !== reference.recordType) || (reference.artifactDigest && !item.artifacts.includes(reference.artifactDigest))) return null;
83
+ return { ...item, relations: graph.relations.filter(edge => edge.key === key || referenceKey(edge.subject) === key || referenceKey(edge.object) === key) };
84
+ }
85
+
86
+ export function traceArkGraphProvenance(graph, reference, options = {}) {
87
+ const record = getArkGraphRecord(graph, reference);
88
+ if (!record) return null;
89
+ const roots = record.recordType === CAP_RECORD_TYPE.relation
90
+ ? [record.record.subject, record.record.object, record.record.attributedTo, ...(record.record.sources ?? [])]
91
+ : [reference];
92
+ return { record, ...queryArkGraph(graph, { ...options, roots,
93
+ predicates: ['derivedFrom', 'assertedIn', 'attributedTo', 'generated', 'used', 'basedOn', 'revises', 'supersedes'] }) };
94
+ }
95
+
96
+ export function deriveArkRoute(graph, { target, activities = [], artifactDigests, limit = 300, depth = 8 } = {}) {
97
+ if (artifactDigests?.length) {
98
+ const selected = new Set(artifactDigests);
99
+ const nodes = graph.nodes.filter(node => node.artifacts.some(id => selected.has(id)));
100
+ const visible = new Map(nodes.map(node => [node.key, node]));
101
+ const relations = graph.relations.filter(edge => edge.artifacts.some(id => selected.has(id))).map(edge => ({ ...edge,
102
+ resolution: Object.fromEntries(['subject', 'object'].map(field => {
103
+ const ref = edge[field], node = visible.get(referenceKey(ref));
104
+ return [field, node && (!ref.artifactDigest || selected.has(ref.artifactDigest) && node.artifacts.includes(ref.artifactDigest)) ? node.recordType === ref.recordType ? 'resolved' : 'type-mismatch' : 'unavailable'];
105
+ })),
106
+ }));
107
+ graph = { ...graph, nodes, relations, artifacts: graph.artifacts.filter(id => selected.has(id)),
108
+ attestations: graph.attestations.filter(item => selected.has(item.artifactDigest)) };
109
+ }
110
+ if (!target?.ref || !target.digest) throw Error('route requires an exact target reference');
111
+ const targetNode = getArkGraphRecord(graph, target);
112
+ if (!targetNode) return null;
113
+ const selectedActivities = new Set(activities.map(referenceKey));
114
+ const selectedGraph = selectedActivities.size ? { ...graph, relations: graph.relations.filter(edge => {
115
+ for (const endpoint of [edge.subject, edge.object]) {
116
+ if (endpoint.recordType === CAP_RECORD_TYPE.activity && !selectedActivities.has(referenceKey(endpoint))) return false;
117
+ }
118
+ return true;
119
+ }) } : graph;
120
+ const route = queryArkGraph(selectedGraph, { roots: [target], predicates: [...routePredicates], depth, limit });
121
+ const chosen = new Set(route.nodes.map(node => node.key));
122
+ // Include immediate provenance, but do not let a shared author or paper merge all routes.
123
+ const origins = selectedGraph.relations.filter(edge => !routePredicates.has(edge.predicate) && chosen.has(referenceKey(edge.subject)));
124
+ const originKeys = new Set(origins.map(edge => referenceKey(edge.object)));
125
+ const allNodes = [...route.nodes, ...graph.nodes.filter(node => originKeys.has(node.key) && !chosen.has(node.key))];
126
+ const nodes = allNodes.slice(0, limit);
127
+ const relations = [...route.relations, ...origins];
128
+ const unresolved = relations.flatMap(edge => ['subject', 'object'].filter(field => edge.resolution[field] !== 'resolved').map(field => ({ relation: edge.key, reference: edge[field], reason: 'dependency-unavailable-in-authorized-scope' })));
129
+ const assessments = nodes.filter(node => node.role === 'assessment').map(node => ({ reference: { ref: node.ref, digest: node.digest }, conclusion: node.record.conclusion, scope: node.record.scope, limitations: node.record.limitations, performedBy: node.record.performedBy }));
130
+ return { ...route, truncated: route.truncated || allNodes.length > limit, nodes, relations, target, activities,
131
+ groups: { reference: nodes.filter(n => n.record.basis === 'declared' || n.role === 'source-work').map(n => n.key),
132
+ planned: nodes.filter(n => n.role === 'procedure').map(n => n.key),
133
+ actual: nodes.filter(n => primitiveOf(n.record) === 'activity').map(n => n.key) },
134
+ assessments, unknowns: unresolved,
135
+ independence: { status: 'unknown', reason: 'Distinct record or artifact digests do not establish independent execution.' } };
136
+ }
137
+
138
+ /** Compare exact identities against one explicit target set; never align by metric label or text. */
139
+ export function compareArkRoutes(left, right, { targets = [] } = {}) {
140
+ if (!left || !right) throw Error('both routes must be available');
141
+ const leftKeys = new Set(left.nodes.map(n => n.key)), rightKeys = new Set(right.nodes.map(n => n.key));
142
+ const versions = new Map();
143
+ for (const node of [...left.nodes, ...right.nodes]) {
144
+ const ds = versions.get(node.ref) ?? new Set(); ds.add(node.digest); versions.set(node.ref, ds);
145
+ }
146
+ const coverage = route => targets.map(target => {
147
+ const targetKey = referenceKey(target);
148
+ const assessmentKeys = new Set(route.relations.filter(edge => edge.predicate === 'assesses' && referenceKey(edge.object) === targetKey && edge.resolution.object === 'resolved').map(edge => referenceKey(edge.subject)));
149
+ const assessments = route.nodes.filter(n => n.role === 'assessment' && assessmentKeys.has(n.key)).map(n => ({ ref: n.ref, digest: n.digest, conclusion: n.record.conclusion, limitations: n.record.limitations }));
150
+ return { target, status: assessments.length ? 'assessed' : route.truncated || route.unknowns.length ? 'unknown' : 'unassessed', assessments };
151
+ });
152
+ return { queryVersion: ARKGRAPH_QUERY_VERSION,
153
+ inputs: { left: { target: left.target, artifacts: left.artifacts, activities: left.activities }, right: { target: right.target, artifacts: right.artifacts, activities: right.activities }, targets },
154
+ shared: [...leftKeys].filter(k => rightKeys.has(k)), onlyLeft: [...leftKeys].filter(k => !rightKeys.has(k)), onlyRight: [...rightKeys].filter(k => !leftKeys.has(k)),
155
+ changedVersions: [...versions].filter(([, digests]) => digests.size > 1).map(([ref, digests]) => ({ ref, digests: [...digests].sort() })),
156
+ coverage: { left: coverage(left), right: coverage(right) },
157
+ incomplete: left.truncated || right.truncated || left.unknowns.length > 0 || right.unknowns.length > 0,
158
+ independence: { status: 'unknown', reason: 'Shared inputs and different digests alone do not establish independence.' } };
159
+ }
@@ -0,0 +1,69 @@
1
+ import { CAP_RECORD_TYPE } from './model.mjs';
2
+ import { canonicalJsonBytes, sha256Bytes } from '../cap/v2/canonical-json.mjs';
3
+
4
+ /** Project explicitly recorded scientific bindings into independently addressable edges.
5
+ * This never infers a missing scientific step from a command or final measurement.
6
+ */
7
+ export function researchRelations(records, creator) {
8
+ const edges = [];
9
+ const add = (owner, predicate, subject, object, pointer, basis = 'declared', qualifiers = {}) => {
10
+ if (!subject?.ref || !object?.ref) return;
11
+ const identity = { owner: owner.id, predicate, subject: subject.ref, object: object.ref, pointer };
12
+ edges.push({
13
+ $schema: 'https://citeark.com/schemas/cap/v2/relation.schema.json',
14
+ id: `urn:citeark:relation:${sha256Bytes(canonicalJsonBytes(identity)).slice(7)}`,
15
+ type: CAP_RECORD_TYPE.relation, role: 'provenance', predicate, subject, object,
16
+ basis, attributedTo: { ref: creator },
17
+ sources: [{ ref: owner.id }], qualifiers: { field: pointer, ...qualifiers },
18
+ });
19
+ };
20
+ for (const record of records) {
21
+ const self = { ref: record.id };
22
+ if (record.type === CAP_RECORD_TYPE.relation) continue;
23
+ if (record.source?.work) add(record, 'assertedIn', self, record.source.work, '/source', record.origin?.basis ?? 'declared', { selector: record.source.selector ?? null });
24
+ if (record.origin?.actor) add(record, 'attributedTo', self, record.origin.actor, '/origin', record.origin.basis ?? 'declared');
25
+ // Array bindings are separate source occurrences, even when endpoints match.
26
+ // Their exact pointer preserves distinct selectors, roles and declarations.
27
+ for (const [index, context] of (record.sourceContext ?? []).entries()) add(record, 'describes', context.source, self, `/sourceContext/${index}`, record.basis ?? 'declared', { selector: context.selector ?? null });
28
+ const objectsField = record.objects != null ? 'objects' : 'contextObjects';
29
+ for (const [index, object] of (record[objectsField] ?? []).entries()) add(record, 'about', self, object, `/${objectsField}/${index}`, record.contextBasis ?? record.origin?.basis ?? record.basis ?? 'declared');
30
+ if (record.type === CAP_RECORD_TYPE.entity && ['procedure', 'procedure-step'].includes(record.role)) {
31
+ for (const [index, input] of (record.inputs ?? []).entries()) if (input.record) add(record, 'requires', self, input.record, `/inputs/${index}`, input.basis ?? 'declared', { role: input.role, required: input.required !== false });
32
+ for (const [index, output] of (record.expectedOutputs ?? []).entries()) add(record, 'expects', self, output, `/expectedOutputs/${index}`, 'inferred', { prospective: true });
33
+ for (const [index, step] of (record.steps ?? []).entries()) add(record, 'hasStep', self, step, `/steps/${index}`, 'inferred');
34
+ for (const [index, step] of (record.dependencies ?? []).entries()) add(record, 'requires', self, step, `/dependencies/${index}`, 'inferred', { role: 'procedure-dependency' });
35
+ }
36
+ for (const [index, target] of (record.tests ?? []).entries()) add(record, 'plannedFor', self, target, `/tests/${index}`);
37
+ if (record.type === CAP_RECORD_TYPE.activity) {
38
+ if (record.experiment) add(record, 'follows', self, record.experiment, '/experiment');
39
+ for (const [index, item] of (record.actors ?? []).entries()) add(record, 'associatedWith', self, item.actor, `/actors/${index}`, 'declared', { role: item.role });
40
+ for (const [index, item] of (record.inputs ?? []).entries()) if (item.record) add(record, 'used', self, item.record, `/inputs/${index}`, item.basis ?? 'declared', { role: item.role });
41
+ for (const [index, item] of (record.outputs ?? []).entries()) if (item.record) add(record, 'generated', self, item.record, `/outputs/${index}`, 'declared', { role: item.role });
42
+ }
43
+ if (record.generatedBy) add(record, 'generated', record.generatedBy, self, '/generatedBy', record.basis ?? 'declared');
44
+ for (const [index, source] of (record.derivedFrom ?? []).entries()) add(record, 'derivedFrom', self, source, `/derivedFrom/${index}`, record.basis ?? 'declared');
45
+ for (const [index, evidence] of (record.reportedEvidence ?? []).entries()) add(record, 'evidenceFor', evidence, self, `/reportedEvidence/${index}`, 'declared');
46
+ if (record.role === 'assessment' || record.role === 'measurement-assessment') {
47
+ add(record, 'assesses', self, record.subject ?? record.claim, '/subject', 'inferred');
48
+ for (const [index, evidence] of (record.evidence ?? []).entries()) add(record, 'basedOn', self, evidence, `/evidence/${index}`, 'inferred');
49
+ for (const [index, component] of (record.componentAssessments ?? []).entries()) add(record, 'basedOn', self, component, `/componentAssessments/${index}`, 'inferred');
50
+ if (record.execution) add(record, 'basedOn', self, record.execution, '/execution', 'inferred');
51
+ if (record.performedBy) add(record, 'attributedTo', self, record.performedBy, '/performedBy', 'declared');
52
+ }
53
+ }
54
+ return edges;
55
+ }
56
+
57
+ /** Resolve only local bindings. Cross-artifact references stay explicit and unresolved. */
58
+ export function bindRelation(record, descriptors) {
59
+ const bind = reference => {
60
+ if (!reference?.ref || reference.artifactDigest) return reference;
61
+ const target = descriptors.get(reference.ref);
62
+ if (!target) return reference; // The verifier reports the missing dependency.
63
+ if (reference.digest && reference.digest !== target.digest) return { ...reference, recordType: reference.recordType ?? target.recordType };
64
+ if (reference.recordType && reference.recordType !== target.recordType) return { ...reference, digest: reference.digest ?? target.digest };
65
+ return { ...reference, digest: target.digest, recordType: target.recordType };
66
+ };
67
+ return { ...record, subject: bind(record.subject), object: bind(record.object),
68
+ attributedTo: bind(record.attributedTo), sources: (record.sources ?? []).map(bind) };
69
+ }
@@ -0,0 +1,12 @@
1
+ export function sourcePage(locators){
2
+ for(const locator of locators??[]){
3
+ if(Number.isSafeInteger(locator.page)&&locator.page>0)return locator.page;
4
+ const text=String(locator.locator??'');
5
+ const match=text.match(/\b(?:p\.|page\s+)\s*(\d+)\b|物理页\s*(\d+)|第\s*(\d+)\s*页/i);
6
+ if(match&&!/^\s*(?:[-–—~]|至)\s*\d/.test(text.slice(match.index+match[0].length))){
7
+ const page=Number(match.slice(1).find(Boolean));
8
+ if(Number.isSafeInteger(page)&&page>0)return page;
9
+ }
10
+ }
11
+ return null;
12
+ }
@@ -0,0 +1,34 @@
1
+ import {readFile,mkdir,copyFile} from 'node:fs/promises';
2
+ import {execFile} from 'node:child_process';
3
+ import {promisify} from 'node:util';
4
+ import path from 'node:path';
5
+ import {sha256Bytes} from '../cap/v2/canonical-json.mjs';
6
+ const exec=promisify(execFile);
7
+
8
+ import {sourcePage} from './source-page.mjs';
9
+ export {sourcePage} from './source-page.mjs';
10
+ /** Render a source page, never infer a figure crop or invent data from plot pixels.
11
+ * The optional PDF must match the signed source identity before it is displayed.
12
+ */
13
+ export async function attachLocalSourcePreviews(view,pdfPath,directory){
14
+ const bytes=await readFile(pdfPath),digest=sha256Bytes(bytes);
15
+ const works=view.nodes.filter(n=>n.role==='source-work');
16
+ const signedDigests=works.flatMap(n=>[...(n.raw.sources??[]).filter(s=>s.kind==='paper').map(s=>s.digest),n.raw.citeark?.provenance?.sourceIdentity?.paperDigest]);
17
+ if(!signedDigests.includes(digest))throw Error('Preview PDF does not match a signed source digest');
18
+ await copyFile(pdfPath,path.join(directory,'source-paper.pdf'));
19
+ view.sourceDocument='source-paper.pdf';
20
+ const {stdout}=await exec('pdfinfo',[pdfPath],{maxBuffer:100000});
21
+ const pages=Number(stdout.match(/^Pages:\s*(\d+)/m)?.[1]);
22
+ if(!pages)throw Error('Cannot determine source PDF page count');
23
+ const rendered=new Set();await mkdir(path.join(directory,'source-pages'),{recursive:true});
24
+ for(const node of view.nodes){
25
+ const page=sourcePage(node.sourceLocators);if(!page||page>pages)continue;node.sourcePage=page;
26
+ if(node.category!=='result')continue;
27
+ if(!rendered.has(page)){
28
+ await exec('pdftoppm',['-f',String(page),'-l',String(page),'-singlefile','-scale-to','1600','-png',pdfPath,path.join(directory,'source-pages',String(page))],{maxBuffer:100000});
29
+ rendered.add(page);
30
+ }
31
+ node.image=`source-pages/${page}.png`;node.imageCaption=`${node.label} · 原文第 ${page} 页`;
32
+ }
33
+ return {sourceDigest:digest,pages:[...rendered]};
34
+ }
@@ -0,0 +1,76 @@
1
+ import { CAP_RECORD_TYPE, CAP_ROLE_TYPE, CAP_PREDICATES, CAP_BASES, CAP_ACTIVITY_STATUSES, primitiveOf } from './model.mjs';
2
+ const digestPattern = /^sha256:[0-9a-f]{64}$/;
3
+ const conclusions = new Set(['supports', 'challenges', 'contradicts', 'inconclusive']);
4
+
5
+ export function validateGraphRecords(bodies, descriptors, issues) {
6
+ for (const body of bodies.values()) {
7
+ const fail = message => issues.push(`Record ${body.id}: ${message}`);
8
+ const primitive = primitiveOf(body);
9
+ if (!primitive) { fail('unsupported scientific primitive'); continue; }
10
+ if (typeof body.id !== 'string' || !/^(urn:|https?:\/\/)/.test(body.id)) fail('id must have a producer namespace (URN or URL)');
11
+ if (typeof body.role !== 'string' || !body.role) fail('role is required');
12
+ if (descriptors.get(body.id)?.role !== body.role) fail('role does not match descriptor');
13
+ if (CAP_ROLE_TYPE[body.role] && CAP_ROLE_TYPE[body.role] !== primitive) fail('role is incompatible with primitive');
14
+ if (primitive === 'agent' && (typeof body.name !== 'string' || !body.name)) fail('agent requires name');
15
+ if (primitive === 'activity') {
16
+ if (!CAP_ACTIVITY_STATUSES.has(body.status)) fail('activity status must describe actual execution');
17
+ if (body.reproduced != null || body.verificationStatus != null) fail('activity must not contain a scientific reproduction verdict');
18
+ for (const key of ['startedAt', 'endedAt']) if (body[key] != null && !Number.isFinite(Date.parse(body[key]))) fail(`invalid ${key}`);
19
+ if (body.startedAt && body.endedAt && Date.parse(body.startedAt) > Date.parse(body.endedAt)) fail('activity ends before it starts');
20
+ }
21
+ if (primitive === 'assertion') {
22
+ if (['assessment', 'measurement-assessment'].includes(body.role)) {
23
+ if (!(body.subject ?? body.claim)?.ref) fail('assessment requires a subject');
24
+ if (!conclusions.has(body.conclusion)) fail('assessment conclusion is invalid');
25
+ if (!Array.isArray(body.evidence)) fail('assessment requires evidence array (empty is allowed)');
26
+ if (!body.method || typeof body.method !== 'object') fail('assessment requires method');
27
+ if (!body.scope || typeof body.scope !== 'object') fail('assessment requires scope');
28
+ if (!Array.isArray(body.limitations)) fail('assessment requires limitations');
29
+ if (!body.performedBy?.ref) fail('assessment requires performedBy');
30
+ } else if (!body.statement?.value) fail('assertion requires statement.value');
31
+ }
32
+ if (body.measurement) {
33
+ if (primitive !== 'entity') fail('measurement belongs to an Entity');
34
+ const m = body.measurement;
35
+ if (typeof m.metric !== 'string' || !m.metric) fail('measurement requires metric');
36
+ if (typeof m.value?.decimal !== 'string' || !/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/.test(m.value.decimal)) fail('measurement requires an exact decimal string');
37
+ if (typeof m.unit !== 'string') fail('measurement requires unit (empty means dimensionless)');
38
+ if (!CAP_BASES.has(body.basis)) fail('measurement requires explicit basis');
39
+ }
40
+ if (primitive !== 'relation') continue;
41
+ if (!CAP_BASES.has(body.basis)) fail('relation requires explicit basis');
42
+ const signature = CAP_PREDICATES[body.predicate];
43
+ if (!signature) { fail('unsupported relation predicate'); continue; }
44
+ for (const [index, field] of ['subject', 'object'].entries()) {
45
+ const endpoint = body[field];
46
+ validateExactReference(endpoint, descriptors, fail, field);
47
+ const actual = endpoint?.artifactDigest ? endpoint.recordType : descriptors.get(endpoint?.ref)?.recordType;
48
+ if (signature[index] && actual !== CAP_RECORD_TYPE[signature[index]]) fail(`${field} has wrong primitive for ${body.predicate}`);
49
+ if (actual === CAP_RECORD_TYPE.relation) fail('relation endpoints must be scientific objects');
50
+ }
51
+ if (['expects', 'hasStep'].includes(body.predicate)) {
52
+ const subject = bodies.get(body.subject?.ref), object = bodies.get(body.object?.ref);
53
+ if (subject && !['procedure', 'procedure-step'].includes(subject.role)) fail(`${body.predicate} subject must be a prospective procedure`);
54
+ if (body.predicate === 'expects' && object && object.prospective !== true) fail('expects must reference a prospective specification, never an actual generated object');
55
+ if (body.predicate === 'hasStep' && object && object.role !== 'procedure-step') fail('hasStep must reference a scientific procedure step');
56
+ }
57
+ if (['revises', 'supersedes'].includes(body.predicate) && body.subject?.recordType !== body.object?.recordType) fail('revision endpoints must have the same primitive');
58
+ validateExactReference(body.attributedTo, descriptors, fail, 'attributedTo');
59
+ const authorType = body.attributedTo?.artifactDigest ? body.attributedTo.recordType : descriptors.get(body.attributedTo?.ref)?.recordType;
60
+ if (authorType !== CAP_RECORD_TYPE.agent) fail('relation attributedTo must reference an Agent');
61
+ if (!Array.isArray(body.sources)) fail('relation requires provenance sources array');
62
+ for (const source of body.sources ?? []) validateExactReference(source, descriptors, fail, 'source');
63
+ }
64
+ }
65
+
66
+ function validateExactReference(reference, descriptors, fail, label) {
67
+ if (!reference?.ref || !digestPattern.test(reference?.digest) || !Object.values(CAP_RECORD_TYPE).includes(reference?.recordType)) {
68
+ fail(`${label} requires exact ref, digest and recordType`); return;
69
+ }
70
+ if (reference.artifactDigest) {
71
+ if (!digestPattern.test(reference.artifactDigest)) fail(`${label} has invalid artifactDigest`);
72
+ return; // Dependency authenticity is unknown until separately resolved.
73
+ }
74
+ const local = descriptors.get(reference.ref);
75
+ if (!local || local.digest !== reference.digest || local.recordType !== reference.recordType) fail(`${label} does not bind a local record version`);
76
+ }