@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,26 @@
1
+ /** Explicit re-execution is distinct from a failed historical Execution. */
2
+ export function continuationRepairTargets(continuation) {
3
+ const requested = Array.isArray(continuation?.requestedTargets) ? continuation.requestedTargets : [];
4
+ const candidates = requested.length ? requested : continuation?.failedTargets ?? [];
5
+ return candidates.filter(validTarget);
6
+ }
7
+
8
+ export function selectContinuationTargets(targets, continuation) {
9
+ const requested = Array.isArray(continuation?.requestedTargets) ? continuation.requestedTargets : [];
10
+ if (!requested.length) return targets;
11
+ const keys = new Set(requested.map(targetKey));
12
+ const selected = targets.filter((target) => keys.has(targetKey(target)));
13
+ if (selected.length !== keys.size) throw new Error('Requested continuation targets are missing from the immutable Research Plan');
14
+ return selected;
15
+ }
16
+
17
+ export function priorCompletedContinuationTargets(continuation) {
18
+ const requested = new Set((continuation?.requestedTargets ?? []).map(targetKey));
19
+ return (continuation?.completedTargets ?? []).filter((target) => !requested.has(targetKey(target)));
20
+ }
21
+
22
+ function validTarget(value) {
23
+ return value && typeof value.claimId === 'string' && value.claimId.trim()
24
+ && typeof value.experimentId === 'string' && value.experimentId.trim();
25
+ }
26
+ function targetKey(value) { return `${value?.claimId}\u0000${value?.experimentId}`; }
@@ -0,0 +1,43 @@
1
+ import { sha256Value } from '../util.mjs';
2
+
3
+ export function executionGraphContext(research, experiment) {
4
+ if (!research.researchObjects?.length && !experiment.steps?.length) return null;
5
+ const definitions = new Map((research.experiments ?? []).flatMap(item => item.steps ?? []).map(step => [step.id, step]));
6
+ const included = new Set(), steps = [];
7
+ const includeStep = step => {
8
+ if (!step || included.has(step.id)) return;
9
+ included.add(step.id);
10
+ for (const id of step.dependsOn ?? []) includeStep(definitions.get(id));
11
+ steps.push(structuredClone(step));
12
+ };
13
+ for (const step of experiment.steps ?? []) includeStep(step);
14
+ const ids = new Set([...(experiment.inputIds ?? []), ...steps.flatMap(step => [...(step.inputIds ?? []), ...(step.outputIds ?? [])])]);
15
+ const objects = new Map((research.researchObjects ?? []).map(object => [object.id, object]));
16
+ const include = id => {
17
+ for (const parent of objects.get(id)?.derivedFromIds ?? []) if (!ids.has(parent)) { ids.add(parent); include(parent); }
18
+ };
19
+ for (const id of [...ids]) include(id);
20
+ return { objects: [...ids].map(id => structuredClone(objects.get(id))), steps, inputIds: [...(experiment.inputIds ?? [])] };
21
+ }
22
+
23
+ export function mergeExecutionGraphContexts(contracts) {
24
+ const contexts = contracts.map(contract => contract.researchGraph).filter(Boolean);
25
+ if (!contexts.length) return null;
26
+ const objects = new Map(), steps = new Map(), inputs = new Set();
27
+ for (const context of contexts) {
28
+ for (const object of context.objects) {
29
+ if (objects.has(object.id) && sha256Value(objects.get(object.id)) !== sha256Value(object)) throw Error(`Conflicting research object ${object.id}`);
30
+ objects.set(object.id, object);
31
+ }
32
+ for (const step of context.steps) {
33
+ const { claimIds, ...definition } = step;
34
+ if (steps.has(step.id)) {
35
+ const { claimIds: priorClaims, ...prior } = steps.get(step.id);
36
+ if (sha256Value(prior) !== sha256Value(definition)) throw Error(`Conflicting scientific step ${step.id}`);
37
+ steps.set(step.id, { ...definition, claimIds: [...new Set([...(priorClaims ?? []), ...(claimIds ?? [])])] });
38
+ } else steps.set(step.id, structuredClone(step));
39
+ }
40
+ for (const id of context.inputIds) inputs.add(id);
41
+ }
42
+ return { objects: [...objects.values()], steps: [...steps.values()], inputIds: [...inputs] };
43
+ }
@@ -0,0 +1,15 @@
1
+ /** Editorial planning metadata, never admission, budget or scientific evidence. */
2
+ export function normalizeExperimentImportance(value, experimentIds) {
3
+ const seen = new Set();
4
+ return (Array.isArray(value) ? value : []).flatMap((item) => {
5
+ if (!item || !experimentIds.has(item.experimentId) || seen.has(item.experimentId)
6
+ || ![1, 2, 3].includes(item.stars) || typeof item.rationale !== "string"
7
+ || !item.rationale.trim()) return [];
8
+ seen.add(item.experimentId);
9
+ return [{ experimentId: item.experimentId, stars: item.stars,
10
+ rationale: item.rationale.trim(),
11
+ ...(typeof item.headline === "string" && item.headline.trim()
12
+ ? { headline: item.headline.trim().slice(0, 180) } : {}),
13
+ }];
14
+ });
15
+ }
@@ -0,0 +1,104 @@
1
+ import { mergeResearchObjects } from './research-objects.mjs';
2
+ import path from "node:path";
3
+ import { CiteArkError, readJson, writeJson, sha256Value } from "../util.mjs";
4
+ import { mergeAgentUsage } from "../telemetry/usage.mjs";
5
+ import { normalizeResearchStructure } from "./structure.mjs";
6
+ import { revisedInventoryClaims } from "./inventory-revisions.mjs";
7
+
8
+ export function mergeInventoryPreparation(inventory, draft) {
9
+ const source = normalizeResearchStructure(inventory);
10
+ if (source.compilationStage !== "inventory") throw new CiteArkError("Reproduction preparation requires a source inventory");
11
+ if (draft.reading !== undefined || draft.researchRelations !== undefined || draft.claims !== undefined || draft.hypotheses !== undefined || draft.work !== undefined || draft.sources !== undefined)
12
+ throw new CiteArkError("Reproduction preparation must return experiments and dispositions only; source content is host-owned");
13
+ if (!Array.isArray(draft.dispositions) || !Array.isArray(draft.experiments)) throw new CiteArkError("Preparation requires experiments and dispositions arrays");
14
+ const claims = normalizeResearchStructure({ ...source,
15
+ claims: revisedInventoryClaims(source, draft.inventoryRevisions).map((claim) => ({ ...claim, reproduction: { status: "deferred", reason: "Provisional source interpretation awaiting preparation." } })),
16
+ }).claims;
17
+ const ids = new Set(claims.map((claim) => claim.id));
18
+ const decisions = new Map();
19
+ for (const item of draft.dispositions) {
20
+ if (!ids.has(item?.claimId) || decisions.has(item.claimId) || !item.reproduction || typeof item.reproduction !== "object")
21
+ throw new CiteArkError("Invalid or duplicate inventory disposition");
22
+ decisions.set(item.claimId, item.reproduction);
23
+ }
24
+ for (const id of ids) if (!decisions.has(id)) decisions.set(id, {
25
+ status: "deferred", reason: "Initial preparation has not resolved this question; retain it for subsequent investigation and coverage assessment.",
26
+ });
27
+ const result = { ...structuredClone(source),
28
+ claims: claims.map((claim) => ({ ...claim, reproduction: structuredClone(decisions.get(claim.id)) })),
29
+ experiments: structuredClone(draft.experiments),
30
+ ...((source.researchObjects !== undefined || draft.researchObjects !== undefined)
31
+ ? { researchObjects: mergeResearchObjects(source.researchObjects, draft.researchObjects) } : {}),
32
+ ...(draft.experimentSelection && typeof draft.experimentSelection === "object"
33
+ ? { experimentSelection: structuredClone(draft.experimentSelection) }
34
+ : {}),
35
+ sourceInventory: { digest: `sha256:${sha256Value(source)}`, research: structuredClone(source),
36
+ ...(draft.inventoryRevisions?.length ? { revisions: structuredClone(draft.inventoryRevisions) } : {}) },
37
+ };
38
+ delete result.compilationStage;
39
+ delete result.coverage;
40
+ delete result.lineage;
41
+ delete result.provenance.compilerSourceReview;
42
+ return result;
43
+ }
44
+
45
+ /** Prepare using the original fixed sources and the caller's existing transport.
46
+ * Merely writes a task. The caller owns model execution, budget and checkpoints.
47
+ */
48
+ export async function writeReproductionPreparationTask({ compilation, sourceTaskPath, outputPath, computeContext }) {
49
+ const inventory = normalizeResearchStructure(compilation.research);
50
+ if (inventory.compilationStage !== "inventory") throw new CiteArkError("Expected source inventory");
51
+ const original = await readJson(sourceTaskPath);
52
+ const runtimePaper = compilation.bundle.runtimeTask.paper;
53
+ if (!runtimePaper.path?.startsWith("/job/input/")) throw new CiteArkError("Preparation requires the fixed local paper snapshot");
54
+ const paperPath = path.join(compilation.bundle.directories.input, runtimePaper.path.slice("/job/input/".length));
55
+ const inventoryPath = path.join(path.dirname(outputPath), "source-inventory.json");
56
+ await writeJson(inventoryPath, inventory);
57
+ const repository = compilation.bundle.repositoryIdentity?.available
58
+ ? { path: compilation.bundle.directories.repositorySnapshot } : undefined;
59
+ const paper = { ...runtimePaper, path: paperPath,
60
+ ...(runtimePaper.reading?.status === "ready" ? { preparedReading: { ...runtimePaper.reading,
61
+ directory: path.join(compilation.bundle.directories.input, "paper-reading") } } : {}),
62
+ };
63
+ delete paper.url;
64
+ delete paper.markdownPath;
65
+ delete paper.sourceMapPath;
66
+ const task = { ...original, name: `${original.name}-reproduction-preparation`, compilationStage: "execution_plan",
67
+ paper, ...(computeContext ? { computeContext } : {}), ...(repository ? { repository } : {}), sourceInventory: { path: inventoryPath, digest: `sha256:${sha256Value(inventory)}` } };
68
+ await writeJson(outputPath, task);
69
+ return outputPath;
70
+ }
71
+
72
+ export async function prepareInventoryForExecution({ compilation, sourceTaskPath, outputDirectory, execute, computeContext, progress = () => {} }) {
73
+ if (compilation.research?.compilationStage !== "inventory") return compilation;
74
+ progress("论文研究清单已完成;复现准备开始读取清单与固定来源,选择验证路线和资源。");
75
+ const taskPath = await writeReproductionPreparationTask({ compilation, sourceTaskPath,
76
+ outputPath: path.join(outputDirectory, "prepare-reproduction-task.json"), computeContext });
77
+ const preparation = await execute({ taskPath, runId: `${compilation.bundle.runId}-prepare` });
78
+ return { ...preparation,
79
+ inventoryCompilation: { runId: compilation.bundle.runId, outputPath: path.join(compilation.bundle.directories.output, "research.normalized.json"),
80
+ startedAt: compilation.bundle.state.startedAt, finishedAt: compilation.bundle.state.finishedAt, usage: compilation.bundle.state.usage },
81
+ totalUsage: mergeAgentUsage(compilation.bundle.state.usage, preparation.bundle.state.usage),
82
+ };
83
+ }
84
+
85
+ /** Standalone inventories can enter the pipeline with their original sources. */
86
+ export async function writePreparationTaskFromInventory({ inventoryPath, paperPath, repositoryPath, outputPath, computeContext }) {
87
+ const inventory = normalizeResearchStructure(await readJson(inventoryPath));
88
+ if (inventory.compilationStage !== "inventory") throw new CiteArkError("Expected a research inventory");
89
+ const paperUri = inventory.sources.find((source) => source.kind === "paper")?.uri;
90
+ const paper = paperPath ? { path: path.resolve(paperPath) }
91
+ : /^https?:\/\//.test(paperUri ?? "") ? { url: paperUri } : null;
92
+ if (!paper) throw new CiteArkError("研究清单需要原论文文件;请为复现准备提供 --paper");
93
+ const identity = inventory.provenance.sourceIdentity?.repository;
94
+ const repository = identity?.available ? repositoryPath ? { path: path.resolve(repositoryPath) }
95
+ : /^https?:\/\//.test(identity.url ?? "") ? { url: identity.url, commit: identity.commit } : null : undefined;
96
+ if (identity?.available && !repository) throw new CiteArkError("研究清单绑定本地仓库快照;请提供相同内容的 --repository");
97
+ const task = { schemaVersion: "0.1", name: `${inventory.work.id}-reproduction-preparation`, compilationStage: "execution_plan",
98
+ reproductionScope: inventory.reproductionScope, paper, ...(repository ? { repository } : {}),
99
+ ...(computeContext ? { computeContext } : {}),
100
+ licensePolicy: { executable: inventory.license.executionAllowed, paperLicense: inventory.license.paper, codeLicense: inventory.license.code },
101
+ sourceInventory: { path: path.resolve(inventoryPath), digest: `sha256:${sha256Value(inventory)}` } };
102
+ await writeJson(outputPath, task);
103
+ return outputPath;
104
+ }
@@ -0,0 +1,32 @@
1
+ import { CiteArkError } from "../util.mjs";
2
+
3
+ export const sourceClaim = ({ reproduction, versionId, ...claim }) => claim;
4
+
5
+ /** The fixed paper is the source; a model's reading of it is revisable.
6
+ * Retain the initial extraction and all identities so corrections remain visible.
7
+ */
8
+ export function revisedInventoryClaims(inventory, revisions = []) {
9
+ if (!Array.isArray(revisions)) throw new CiteArkError("inventoryRevisions must be an array");
10
+ const claims = inventory.claims.map((claim) => structuredClone(sourceClaim(claim)));
11
+ const seen = new Set();
12
+ for (const revision of revisions) {
13
+ const { claimId, claim, reason, sources } = revision ?? {};
14
+ if (!claimId || claim?.id !== claimId || seen.has(claimId)
15
+ || typeof reason !== "string" || !reason.trim()
16
+ || !Array.isArray(sources) || !sources.length
17
+ || sources.some((source) => !inventory.sources.some((item) => item.id === source?.sourceId)
18
+ || typeof source.locator !== "string" || !source.locator.trim())) {
19
+ throw new CiteArkError("Inventory revision requires a unique claim ID, corrected claim, reason and fixed-source locators");
20
+ }
21
+ seen.add(claimId);
22
+ const index = claims.findIndex((item) => item.id === claimId);
23
+ if (index >= 0) {
24
+ const ids = new Set((claim.reportedMeasurements ?? []).map((item) => item.id));
25
+ if ((claims[index].reportedMeasurements ?? []).some((item) => !ids.has(item.id))) {
26
+ throw new CiteArkError("Inventory correction must retain existing measurement IDs and disclose mistaken interpretations");
27
+ }
28
+ claims[index] = structuredClone(sourceClaim(claim));
29
+ } else claims.push(structuredClone(sourceClaim(claim)));
30
+ }
31
+ return claims;
32
+ }
@@ -0,0 +1,73 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { access, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { researchSignal } from '../workspace/control.mjs';
7
+
8
+ export const MINERU_RUNTIME_DIRECTORY = fileURLToPath(new URL('../../runtime/mineru/', import.meta.url));
9
+ export const mineruHome = () => path.resolve(process.env.CITEARK_MINERU_HOME ?? path.join(os.homedir(), '.citeark', 'mineru-4.0.10'));
10
+ export const mineruPython = () => process.env.CITEARK_MINERU_PYTHON ?? path.join(mineruHome(), 'venv', process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python');
11
+
12
+ async function run(command, args, { env = process.env, timeoutMs = 3_600_000, signal } = {}) {
13
+ signal?.throwIfAborted();
14
+ return await new Promise((resolve, reject) => {
15
+ const child = spawn(command, args, { env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: process.platform !== 'win32' });
16
+ // Keep progress visible without allowing large buffered native-model logs.
17
+ child.stderr.on('data', chunk => process.stderr.write(chunk));
18
+ child.stdout.on('data', chunk => process.stderr.write(chunk));
19
+ let timedOut = false;
20
+ const stop = () => {
21
+ if (process.platform === 'win32') child.kill('SIGKILL');
22
+ else { try { process.kill(-child.pid, 'SIGKILL'); } catch { /* Process already exited. */ } }
23
+ };
24
+ const timer = setTimeout(() => { timedOut = true; stop(); }, timeoutMs);
25
+ signal?.addEventListener('abort', stop, { once: true });
26
+ const cleanup = () => {
27
+ clearTimeout(timer);
28
+ signal?.removeEventListener('abort', stop);
29
+ };
30
+ child.once('error', error => { cleanup(); reject(error); });
31
+ child.once('close', code => {
32
+ cleanup();
33
+ if (signal?.aborted) { reject(signal.reason); return; }
34
+ code === 0 ? resolve() : reject(new Error(timedOut ? 'MinerU conversion timed out' : 'Local MinerU conversion failed'));
35
+ });
36
+ });
37
+ }
38
+
39
+ export async function setupMineru({ python = process.platform === 'win32' ? 'python' : 'python3.12', source = 'huggingface', runImpl = run } = {}) {
40
+ if (!['huggingface', 'modelscope'].includes(source)) throw Error('Model source must be huggingface or modelscope');
41
+ const home = mineruHome();
42
+ await mkdir(home, { recursive: true });
43
+ const config = path.join(home, 'citeark-config.json');
44
+ await writeFile(config, '{}');
45
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('MINERU_') && key !== 'HF_HUB_OFFLINE'));
46
+ Object.assign(env, { MINERU_HOME: home, MINERU_CONFIG: config, HF_HUB_DISABLE_XET: '1', DO_NOT_TRACK: '1',
47
+ HF_HOME: path.join(home, 'hf-cache'), HF_HUB_CACHE: path.join(home, 'hf-cache', 'hub') });
48
+ if (!process.env.CITEARK_MINERU_PYTHON) await runImpl(python, ['-m', 'venv', path.join(home, 'venv')], { env });
49
+ const executable = mineruPython();
50
+ await runImpl(executable, ['-m', 'pip', 'install', '-r', path.join(MINERU_RUNTIME_DIRECTORY, 'requirements.txt')], { env });
51
+ const kit = path.join(path.dirname(executable), process.platform === 'win32' ? 'mineru-kit.exe' : 'mineru-kit');
52
+ const backends = ['--tier', 'standard', '--small-backend', 'onnx', '--vlm-engine', 'llama-cpp'];
53
+ await runImpl(kit, ['models', 'download', ...backends, '--source', source], { env });
54
+ await runImpl(kit, ['models', 'verify', ...backends], { env });
55
+ return { status: 'ready', home, python: executable, mineruVersion: '4.0.10', gpu: 0 };
56
+ }
57
+
58
+ export async function convertPaperWithMinerU({ bytes, sourceDigest, onProgress = async () => {}, runImpl = run, signal = researchSignal() }) {
59
+ signal?.throwIfAborted();
60
+ const python = mineruPython();
61
+ try { await access(python); } catch { throw Error('Local MinerU is not installed; run citeark paper setup --python python3.12'); }
62
+ const directory = await mkdtemp(path.join(os.tmpdir(), 'citeark-mineru-'));
63
+ try {
64
+ const input = path.join(directory, 'paper.pdf'), output = path.join(directory, 'paper.zip');
65
+ await writeFile(input, bytes);
66
+ await onProgress({ stage: 'local_cpu_parse', providerState: 'running' });
67
+ await runImpl(python, [path.join(MINERU_RUNTIME_DIRECTORY, 'parse.py'), '--input', input, '--output', output,
68
+ '--source-digest', sourceDigest, '--threads', process.env.CITEARK_MINERU_THREADS ?? '4'], { signal });
69
+ signal?.throwIfAborted();
70
+ await onProgress({ providerState: 'done' });
71
+ return { archive: await readFile(output), batchId: 'local-cpu' };
72
+ } finally { await rm(directory, { recursive: true, force: true }); }
73
+ }
@@ -0,0 +1,19 @@
1
+ import path from 'node:path';
2
+ import { setupMineru } from './mineru-local.mjs';
3
+ import { preparePaperMarkdown } from './paper-markdown.mjs';
4
+
5
+ export async function paperCommand([command, ...rest], parseOptions) {
6
+ if (command === 'setup') {
7
+ const options = parseOptions(rest, { '--python': { type: 'string' }, '--source': { type: 'string', default: 'huggingface' } });
8
+ console.log(JSON.stringify(await setupMineru({ python: options['--python'], source: options['--source'] }), null, 2));
9
+ return;
10
+ }
11
+ if (command === 'parse') {
12
+ const options = parseOptions(rest, { '--input': { type: 'string', required: true }, '--output': { type: 'string', required: true }, '--cache': { type: 'string' } });
13
+ const outputDirectory = path.resolve(options['--output']);
14
+ console.log(JSON.stringify(await preparePaperMarkdown({ paperPath: path.resolve(options['--input']), outputDirectory,
15
+ cacheDirectory: path.resolve(options['--cache'] ?? path.join(path.dirname(outputDirectory), '.paper-markdown-cache')) }), null, 2));
16
+ return;
17
+ }
18
+ throw Error('Usage: citeark paper setup [--python python3.12] [--source huggingface|modelscope] | citeark paper parse --input paper.pdf --output ./paper-reading');
19
+ }
@@ -0,0 +1,180 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { appendFile, cp, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { convertPaperWithMinerU } from "./mineru-local.mjs";
7
+ export { convertPaperWithMinerU } from "./mineru-local.mjs";
8
+ import { mappedMineru4Markdown } from "./paper-source-map.mjs";
9
+ import { researchSignal } from "../workspace/control.mjs";
10
+
11
+ const exec = promisify(execFile);
12
+ const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
13
+ export const PAPER_MARKDOWN_CONFIG = Object.freeze({ version: 3, provider: "mineru-local", mineruVersion: "4.0.10", llamaVersion: "0.1.2", docvortexVersion: "0.5.8", onnxVersion: "1.30.0", tier: "standard", smallBackend: "onnx", vlmEngine: "llama-cpp", device: "cpu", ocrMode: "auto", imageAnalysis: true });
14
+ const safePath = (name) => typeof name === "string" && name.length > 0 && !name.startsWith("/") && !name.includes("\\") && !name.split("/").some((part) => !part || part === "." || part === "..");
15
+
16
+ const networkCodes = new Set(["ECONNRESET", "ECONNREFUSED", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_SOCKET", "ABORT_ERR"]);
17
+ const controlledFailureCodes = new Map([
18
+ ["Paper conversion response exceeds size limit", "response_too_large"],
19
+ ["Local MinerU conversion failed", "mineru_local_process_failed"],
20
+ ["Local MinerU is not installed; run citeark paper setup --python python3.12", "mineru_runtime_missing"],
21
+ ["MinerU conversion failed", "mineru_conversion_failed"],
22
+ ["MinerU conversion timed out", "mineru_conversion_timed_out"],
23
+ ["Invalid MinerU archive paths", "mineru_archive_paths_invalid"],
24
+ ["MinerU archive must contain exactly one markdown.md", "mineru_markdown_missing"],
25
+ ["MinerU reading assets exceed size limit", "mineru_reading_assets_too_large"],
26
+ ["MinerU archive must contain one content list for page mapping", "mineru_content_list_missing"],
27
+ ["Mapped paper Markdown exceeds reading limit", "mineru_mapped_markdown_too_large"],
28
+ ["MinerU Markdown is empty", "mineru_markdown_empty"],
29
+ ["MinerU Markdown references an unavailable image", "mineru_image_reference_missing"],
30
+ ]);
31
+
32
+ function safeFailure(error, stage) {
33
+ const code = error?.cause?.code ?? error?.code;
34
+ const message = error instanceof Error ? error.message : "";
35
+ const failureCode = controlledFailureCodes.get(message)
36
+ ?? (error instanceof SyntaxError
37
+ ? (stage === "unpack" ? "mineru_archive_json_invalid" : "mineru_result_json_invalid")
38
+ : undefined)
39
+ ?? (networkCodes.has(code) ? "mineru_network_error" : undefined)
40
+ ?? (stage === "unpack" ? "mineru_archive_unpack_failed" : "paper_markdown_preparation_failed");
41
+ const timeout = error?.name === "TimeoutError"
42
+ || error?.cause?.name === "TimeoutError"
43
+ || code === "ABORT_ERR";
44
+ return {
45
+ failureCode,
46
+ ...(networkCodes.has(code) ? { networkCode: code } : {}),
47
+ ...(timeout ? { timeout: true } : {}),
48
+ };
49
+ }
50
+
51
+ function logSafeFailure(failure) {
52
+ console.error(JSON.stringify({
53
+ severity: "ERROR",
54
+ event: "paper_markdown.failed",
55
+ provider: "mineru",
56
+ ...failure,
57
+ }));
58
+ }
59
+
60
+ /** Extract just reading assets. Original ZIP, including raw JSON, stays backend-only. */
61
+ export async function unpackPaperMarkdown(archivePath, outputDirectory) {
62
+ const { stdout } = await exec("unzip", ["-Z1", archivePath], { maxBuffer: 2_000_000 });
63
+ const members = stdout.trim().split(/\r?\n/).filter((name) => !name.endsWith("/"));
64
+ if (members.length > 20_000 || members.some((name) => !safePath(name)) || new Set(members).size !== members.length) throw new Error("Invalid MinerU archive paths");
65
+ const markdowns = members.filter((name) => path.posix.basename(name) === "markdown.md");
66
+ if (markdowns.length !== 1) throw new Error("MinerU archive must contain exactly one markdown.md");
67
+ const root = path.posix.dirname(markdowns[0]);
68
+ const prefix = root === "." ? "" : `${root}/`;
69
+ const selected = members.filter((name) => name === markdowns[0] || (name.startsWith(`${prefix}images/`) && /\.(png|jpe?g|webp)$/i.test(name)));
70
+ let total = 0;
71
+ const files = [];
72
+ for (const member of selected) {
73
+ // -p writes bytes only, so even ZIP symlinks cannot create filesystem links.
74
+ const { stdout: bytes } = await exec("unzip", ["-p", archivePath, member], { encoding: "buffer", maxBuffer: member === markdowns[0] ? 2_000_000 : 20_000_000 });
75
+ total += bytes.length;
76
+ if (total > 160_000_000) throw new Error("MinerU reading assets exceed size limit");
77
+ const relative = member === markdowns[0] ? "paper.md" : member.slice(prefix.length);
78
+ await mkdir(path.dirname(path.join(outputDirectory, relative)), { recursive: true });
79
+ await writeFile(path.join(outputDirectory, relative), bytes);
80
+ files.push({ path: relative, digest: digest(bytes), size: bytes.length });
81
+ }
82
+ const contentLists = members.filter((name) => name.startsWith(prefix) && name === `${prefix}structured_content.json`);
83
+ if (contentLists.length !== 1) throw new Error("MinerU archive must contain one content list for page mapping");
84
+ const { stdout: rawBlocks } = await exec("unzip", ["-p", archivePath, contentLists[0]], { maxBuffer: 20_000_000 });
85
+ const mapped = mappedMineru4Markdown(JSON.parse(rawBlocks));
86
+ if (Buffer.byteLength(mapped.markdown) > 2_000_000) throw new Error("Mapped paper Markdown exceeds reading limit");
87
+ for (const [relative, content] of [["paper.md", mapped.markdown], ["source-map.json", JSON.stringify(mapped.sourceMap)]]) {
88
+ await writeFile(path.join(outputDirectory, relative), content);
89
+ const oldIndex = files.findIndex((file) => file.path === relative);
90
+ if (oldIndex >= 0) files.splice(oldIndex, 1);
91
+ files.push({ path: relative, digest: digest(content), size: Buffer.byteLength(content) });
92
+ }
93
+ const markdown = mapped.markdown;
94
+ if (!markdown.trim()) throw new Error("MinerU Markdown is empty");
95
+ const imagePaths = new Set(files.map((file) => file.path));
96
+ for (const match of markdown.matchAll(/!\[[^\]]*\]\(<?([^\s)>]+)>?(?:\s+[^)]*)?\)|<img\b[^>]*\bsrc=["']([^"']+)["']/g)) {
97
+ const reference = (match[1] ?? match[2]).replace(/^\.\//, "");
98
+ if (!safePath(reference) || !imagePaths.has(reference)) throw new Error("MinerU Markdown references an unavailable image");
99
+ }
100
+ return files;
101
+ }
102
+
103
+ async function readCache(directory, sourceDigest, configDigest) {
104
+ try {
105
+ const manifest = JSON.parse(await readFile(path.join(directory, "manifest.json"), "utf8"));
106
+ if (manifest.sourceDigest !== sourceDigest || manifest.configDigest !== configDigest || !manifest.files?.some((file) => file.path === "paper.md")) return null;
107
+ for (const file of manifest.files) {
108
+ if (!safePath(file.path) || (!["paper.md", "source-map.json"].includes(file.path) && !file.path.startsWith("images/"))) return null;
109
+ const bytes = await readFile(path.join(directory, "reading", file.path));
110
+ if (bytes.length !== file.size || digest(bytes) !== file.digest) return null;
111
+ }
112
+ return manifest;
113
+ } catch { return null; }
114
+ }
115
+
116
+ export async function preparePaperMarkdown({ paperPath, outputDirectory, cacheDirectory, convert = convertPaperWithMinerU, unpack = unpackPaperMarkdown, logFailure = logSafeFailure, signal = researchSignal() }) {
117
+ signal?.throwIfAborted();
118
+ const bytes = await readFile(paperPath);
119
+ if (bytes.subarray(0, 5).toString() !== "%PDF-") return { status: "not_pdf" };
120
+ const sourceDigest = digest(bytes);
121
+ const configDigest = digest(JSON.stringify(PAPER_MARKDOWN_CONFIG));
122
+ const cache = path.join(cacheDirectory, `${sourceDigest}-${configDigest}`);
123
+ let manifest = await readCache(cache, sourceDigest, configDigest);
124
+ const cacheHit = Boolean(manifest);
125
+ if (!manifest) {
126
+ if (bytes.length > 80_000_000) throw new Error("Paper exceeds conversion input size limit");
127
+ await mkdir(cacheDirectory, { recursive: true });
128
+ // Remove an invalid entry before conversion so it cannot block atomic publication.
129
+ await rm(cache, { recursive: true, force: true });
130
+ const attempt = `${cache}.attempt-${randomUUID()}`;
131
+ await mkdir(path.join(attempt, "reading"), { recursive: true });
132
+ let diagnostic = { stage: "convert" };
133
+ const onProgress = async (event) => {
134
+ if (event.stage) {
135
+ const { batchId } = diagnostic;
136
+ diagnostic = { ...(batchId ? { batchId } : {}) };
137
+ }
138
+ diagnostic = { ...diagnostic, ...event, recordedAt: new Date().toISOString() };
139
+ await appendFile(path.join(attempt, "progress.jsonl"), JSON.stringify(diagnostic) + "\n");
140
+ };
141
+ try {
142
+ const converted = await convert({ bytes, sourceDigest, onProgress, signal });
143
+ signal?.throwIfAborted();
144
+ await onProgress({ stage: "archive_write", batchId: converted.batchId });
145
+ await writeFile(path.join(attempt, "source.zip"), converted.archive);
146
+ await onProgress({ stage: "unpack" });
147
+ const files = await unpack(path.join(attempt, "source.zip"), path.join(attempt, "reading"));
148
+ manifest = { sourceDigest, configDigest, config: PAPER_MARKDOWN_CONFIG, batchId: converted.batchId, files, archiveDigest: digest(converted.archive) };
149
+ await onProgress({ stage: "cache_publish" });
150
+ await writeFile(path.join(attempt, "manifest.json"), JSON.stringify(manifest, null, 2));
151
+ try { await rename(attempt, cache); }
152
+ catch (error) {
153
+ // A concurrent conversion may have completed first. Never overwrite it.
154
+ const winner = await readCache(cache, sourceDigest, configDigest);
155
+ if (!winner) throw error;
156
+ manifest = winner;
157
+ }
158
+ } catch (error) {
159
+ if (signal?.aborted) throw signal.reason;
160
+ // Provider response bodies and signed URLs must not appear in persistent diagnostics.
161
+ const failure = {
162
+ status: "failed",
163
+ sourceDigest,
164
+ configDigest,
165
+ ...diagnostic,
166
+ ...safeFailure(error, diagnostic.stage),
167
+ };
168
+ await writeFile(path.join(attempt, "failure.json"), JSON.stringify(failure));
169
+ try { await logFailure(failure); } catch { /* Logging must not mask the conversion failure. */ }
170
+ throw new Error(error.message?.startsWith("Local MinerU is not installed;") ? error.message : "Paper Markdown preparation failed; conversion attempt retained in backend cache");
171
+ }
172
+ }
173
+ await mkdir(outputDirectory, { recursive: true });
174
+ for (const file of manifest.files) {
175
+ const destination = path.join(outputDirectory, file.path);
176
+ await mkdir(path.dirname(destination), { recursive: true });
177
+ await cp(path.join(cache, "reading", file.path), destination);
178
+ }
179
+ return { status: "ready", sourceDigest, configDigest, cacheHit, files: manifest.files };
180
+ }
@@ -0,0 +1,69 @@
1
+ /** The v4 public format has normalized boxes and nested captions/footnotes. */
2
+ export function mappedMineru4Markdown(document) {
3
+ if (!Array.isArray(document?.pages) || !document.pages.length || document.is_full_document !== true)
4
+ throw Error('Missing complete MinerU document pages');
5
+ const entries = [];
6
+ let markdown = '# Paper reading copy\n\nRead Markdown first. Markers name physical PDF pages starting at 1. For uncertain text, equations, captions or values, inspect that page in the original PDF; a caption may continue on an adjacent page.\n\n';
7
+ const location = (bbox) => Array.isArray(bbox) && bbox.length === 4
8
+ && bbox.every(value => Number.isFinite(value) && value >= 0 && value <= 1)
9
+ && bbox[0] <= bbox[2] && bbox[1] <= bbox[3];
10
+ function append(block, page, parentId) {
11
+ if (!location(block.bbox) || typeof block.type !== 'string' || typeof block.content !== 'string')
12
+ throw Error('MinerU block lacks valid content or page location');
13
+ const id = `p${page}-b${entries.length + 1}`;
14
+ const parts = [];
15
+ if (block.image_source) parts.push(`![](${block.image_source})`);
16
+ if (block.content) {
17
+ const content = block.type === 'equation' ? `$$\n${block.content}\n$$`
18
+ : ['doc_title', 'paragraph_title'].includes(block.type)
19
+ ? `${'#'.repeat(Math.min(6, Math.max(1, Number(block.level) || 1)))} ${block.content}` : block.content;
20
+ parts.push(content);
21
+ }
22
+ const startLine = markdown.split('\n').length;
23
+ markdown += `<!-- PDF page ${page} | block ${id} -->\n${parts.join('\n\n') || '[No text extracted; inspect original PDF page.]'}\n\n`;
24
+ entries.push({ id, page, bbox: block.bbox, type: block.type, ...(parentId ? { parentId } : {}), startLine, endLine: markdown.split('\n').length - 2 });
25
+ for (const [field, type] of [['captions', 'caption'], ['footnotes', 'footnote']]) {
26
+ if (block[field] !== undefined && !Array.isArray(block[field])) throw Error('Invalid MinerU nested content');
27
+ for (const child of block[field] ?? []) append({ ...child, type }, page, id);
28
+ }
29
+ }
30
+ for (const [index, page] of document.pages.entries()) {
31
+ if (page.page_idx !== index || !Array.isArray(page.blocks)) throw Error('MinerU pages are missing or unordered');
32
+ if (!page.blocks.length) markdown += `<!-- PDF page ${index + 1} | no extracted blocks -->\n\n`;
33
+ for (const block of page.blocks) append(block, index + 1);
34
+ }
35
+ return { markdown, sourceMap: { version: 2, pageNumbering: 'physical_pdf_one_based',
36
+ bboxConvention: 'normalized_0_1', pageCount: document.pages.length, blocks: entries } };
37
+ }
38
+
39
+ /** Legacy serializer retained for previously saved archive inspection. */
40
+ export function mappedPaperMarkdown(blocks) {
41
+ if (!Array.isArray(blocks) || !blocks.length) throw new Error("Missing MinerU content blocks");
42
+ const entries = [];
43
+ let markdown = "# Paper reading copy\n\nRead Markdown first. Markers name physical PDF pages starting at 1, not printed page labels. If unclear, open that page in the original PDF. Boxes are navigation hints; a merged paragraph or caption may continue onto an adjacent page.\n\n";
44
+ for (const [index, block] of blocks.entries()) {
45
+ if (!Number.isInteger(block.page_idx) || block.page_idx < 0 || !Array.isArray(block.bbox) || block.bbox.length !== 4 || !block.bbox.every(Number.isFinite)) throw new Error("MinerU block lacks a valid page location");
46
+ const page = block.page_idx + 1;
47
+ const id = `p${page}-b${index + 1}`;
48
+ const parts = [];
49
+ if (typeof block.text === "string") {
50
+ const heading = block.type === "text" && Number.isInteger(block.text_level) ? `${"#".repeat(Math.min(6, Math.max(1, block.text_level)))} ` : "";
51
+ parts.push(heading + block.text);
52
+ } else if (block.type === "list" && Array.isArray(block.list_items) && block.list_items.every((item) => typeof item === "string")) {
53
+ parts.push(...block.list_items);
54
+ } else if (["table", "image", "chart", "code"].includes(block.type)) {
55
+ if (block.img_path) parts.push(`![](${block.img_path})`);
56
+ for (const suffix of ["body", "caption", "footnote"]) {
57
+ const value = block[`${block.type}_${suffix}`];
58
+ if (typeof value === "string") parts.push(value);
59
+ else if (Array.isArray(value) && value.every((item) => typeof item === "string")) parts.push(...value);
60
+ }
61
+ if (typeof block.content === "string" && block.content) parts.push(block.content);
62
+ } else throw new Error(`Unsupported MinerU block type: ${block.type}`);
63
+ const content = parts.filter(Boolean).join("\n\n") || "[No text extracted for this block; consult the marked PDF page if needed.]";
64
+ const startLine = markdown.split("\n").length;
65
+ markdown += `<!-- PDF page ${page} | block ${id} -->\n${content}\n\n`;
66
+ entries.push({ id, page, bbox: block.bbox, type: block.type, startLine, endLine: markdown.split("\n").length - 2 });
67
+ }
68
+ return { markdown, sourceMap: { version: 1, pageNumbering: "physical_pdf_one_based", bboxConvention: "mineru_content_list_0_1000", blocks: entries } };
69
+ }