@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,44 @@
1
+ // Default is local input preparation only. --apply sends fixed sources and
2
+ // candidates to the configured OpenRouter model using the supplied budget.
3
+ import { readFile, mkdir } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { prepareSourceReviewInput, reviewCompilerSources } from '../src/research/source-review.mjs';
6
+ import { writeJson } from '../src/util.mjs';
7
+ const args = process.argv.slice(2);
8
+ const value = (flag) => { const i = args.indexOf(flag); return i < 0 ? null : args[i + 1]; };
9
+ const candidates = args.flatMap((arg, i) => arg === '--candidate' ? [args[i + 1]] : []);
10
+ const paperPath = value('--paper');
11
+ const repositoryRoot = value('--repository');
12
+ const output = value('--output');
13
+ if (!paperPath || !repositoryRoot || !output || !candidates.length) throw new Error('Required: --paper FILE --repository SNAPSHOT --output DIR --candidate FILE (repeatable). Default: no model calls.');
14
+ await mkdir(output, { recursive: true });
15
+ const sources = await prepareSourceReviewInput({ paperPath, repositoryRoot, directory: path.join(output, 'source-review') });
16
+ const manifest = { ...sources.identity, pageCount: sources.pages.length, initialImages: 0,
17
+ repositoryFiles: sources.repositoryIndex, initialRepositoryContentBytes: 0,
18
+ orientationBytes: Buffer.byteLength(JSON.stringify(sources.orientation)),
19
+ sourceIndexBytes: Buffer.byteLength(JSON.stringify({pages:sources.pageIndex,repository:sources.repositoryIndex})),
20
+ candidateFiles: candidates.map((filename) => path.resolve(filename)), applied: args.includes('--apply') };
21
+ await writeJson(path.join(output, 'input-manifest.json'), manifest);
22
+ if (!args.includes('--apply')) { console.log(JSON.stringify({ status: 'prepared_only', pageCount: manifest.pageCount, files: manifest.repositoryFiles.length, orientationBytes: manifest.orientationBytes, sourceIndexBytes: manifest.sourceIndexBytes })); }
23
+ else {
24
+ if (!value('--runtime-json') || !value('--budget-json')) throw new Error('--apply requires --runtime-json and --budget-json; no unmetered fallback');
25
+ const runtimeAgent = JSON.parse(await readFile(value('--runtime-json'), 'utf8'));
26
+ runtimeAgent.paperBudgetEnforced = true;
27
+ const paperBudget = JSON.parse(await readFile(value('--budget-json'), 'utf8'));
28
+ if (!paperBudget.url || !paperBudget.token || !paperBudget.workerId) throw new Error('Budget descriptor must include url, token and workerId');
29
+ const bundle = { directories: { execution: output, repositorySnapshot: repositoryRoot }, runtimeAgent, paperBudget,
30
+ sourceReviewProviderSecret: value('--provider-secret'), runtimeTask: { paper: { digest: `sha256:${sources.identity.paperDigest}` }, reproductionScope: value('--scope') ?? 'low' } };
31
+ const results = [];
32
+ for (const filename of candidates) {
33
+ const candidate = JSON.parse(await readFile(filename, 'utf8'));
34
+ if (candidate.provenance) delete candidate.provenance.compilerSourceReview;
35
+ try { const review = await reviewCompilerSources({ bundle, candidate, paperPath, prepare: async () => sources }); results.push({ candidate: path.resolve(filename), accepted: true, reportDigest: review.reportDigest }); }
36
+ catch (error) {
37
+ results.push({ candidate: path.resolve(filename), accepted: false, code: error.failureCode, message: error.message });
38
+ if (error.failureCode !== 'compiler.source_review_rejected') break;
39
+ }
40
+ await writeJson(path.join(output, 'results.json'), results);
41
+ }
42
+ await writeJson(path.join(output, 'results.json'), results);
43
+ console.log(JSON.stringify(results));
44
+ }
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+ set -uo pipefail
3
+
4
+ deterministic_script="$1"
5
+ shift
6
+ set +e
7
+ python3 "$deterministic_script"
8
+ status=$?
9
+ set -e
10
+ if [ "$status" -eq 0 ]; then
11
+ exit 0
12
+ fi
13
+ if [ "$status" -ne 86 ]; then
14
+ exit "$status"
15
+ fi
16
+ printf '%s\n' '{"type":"citeark.asset_preparation_agent_fallback","reason":"deterministic-fast-path-declined"}'
17
+ exec "$@"
@@ -0,0 +1,98 @@
1
+ // The staged helper imports the same plan validator as the coordinator.
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { createWriteStream } from "node:fs";
4
+ import { appendFile, mkdir, open, readFile, writeFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import { finished } from "node:stream/promises";
8
+ import { resolveResearchPlan } from "../src/reproduction/plan.mjs";
9
+ import { inspectResearchPlanEvidence } from "../src/reproduction/evidence-feedback.mjs";
10
+
11
+ const [planPath = "/job/output/research-plan.json", ...args] = process.argv.slice(2);
12
+ const scientificStepId = args[0] === '--step' ? args[1] : null;
13
+ const command = args[0] === '--step' ? args.slice(2) : args;
14
+ const contractPath = path.resolve(path.dirname(planPath), "../input/task.json");
15
+ const contract = JSON.parse(await readFile(contractPath, "utf8"));
16
+ let resolved;
17
+ try {
18
+ if (args[0] === '--step' && (!scientificStepId || !contract.researchGraph?.steps?.some(step => step.id === scientificStepId))) {
19
+ throw new Error(`Unknown scientific step: ${String(scientificStepId)}`);
20
+ }
21
+ resolved = await resolveResearchPlan(contract, path.dirname(planPath));
22
+ if (!resolved.executionPlan || path.basename(planPath) !== "research-plan.json") {
23
+ throw new Error("Expected research-plan.json for a workspacePlan-enabled task");
24
+ }
25
+ } catch (error) {
26
+ console.error(`${error.message}\nCorrect the plan and run this helper again. Reuse existing raw evidence; plan validation does not require repeating the experiment.`);
27
+ process.exit(1);
28
+ }
29
+ const plan = resolved.executionPlan.plan;
30
+ const canonical = (value) => Array.isArray(value) ? value.map(canonical)
31
+ : value && typeof value === "object"
32
+ ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])])) : value;
33
+ const bytes = JSON.stringify(canonical(plan));
34
+ const digest = createHash("sha256").update(bytes).digest("hex");
35
+ const directory = path.join(path.dirname(planPath), "plan-history");
36
+ await mkdir(directory, { recursive: true });
37
+ let newRevision = true;
38
+ try { await writeFile(path.join(directory, `${digest}.json`), bytes, { flag: "wx" }); }
39
+ catch (error) { if (error.code !== "EEXIST") throw error; newRevision = false; }
40
+ // The runner captures this statement and its exact plan bytes outside the
41
+ // workspace. Snapshot files alone are Agent declarations, not execution proof.
42
+ // Always recapture complete final bytes. Repeated commands with an unchanged
43
+ // plan can refer to the revision, avoiding the same large snapshot on every probe.
44
+ console.log(JSON.stringify({ event: "research_plan_execution", planDigest: `sha256:${digest}`,
45
+ ...(newRevision || !command.length ? { plan } : {}), command,
46
+ ...(!command.length && contract.workspacePlan?.version === 2
47
+ ? { evidenceFeedback: await inspectResearchPlanEvidence(resolved, path.dirname(planPath)) } : {}),
48
+ }));
49
+ if (command.length) {
50
+ const startedAt = new Date().toISOString();
51
+ const capture = contract.workspacePlan?.version === 2;
52
+ const executionId = randomUUID();
53
+ const stdoutPath = `experiment-logs/${executionId}.stdout.log`;
54
+ const stderrPath = `experiment-logs/${executionId}.stderr.log`;
55
+ const record = { planDigest: `sha256:${digest}`, command, startedAt,
56
+ ...(scientificStepId ? { scientificStepId } : {}),
57
+ ...(capture ? { executionId, stdoutPath, stderrPath } : {}) };
58
+ const recordPath = path.join(path.dirname(planPath), "experiment-records.jsonl");
59
+ const recordEvent = async (event) => {
60
+ if (contract.workspacePlan?.version !== 2) return;
61
+ const line = JSON.stringify({ ...record, ...event });
62
+ await appendFile(recordPath, `${line}\n`);
63
+ console.log(line);
64
+ };
65
+ const logs = [];
66
+ if (capture) {
67
+ await mkdir(path.join(path.dirname(planPath), "experiment-logs"), { recursive: true });
68
+ for (const filename of [stdoutPath, stderrPath]) {
69
+ const target = path.join(path.dirname(planPath), filename);
70
+ // Open before starting the command: a logging failure must not silently
71
+ // discard a scientific run. Each invocation gets fresh files.
72
+ const handle = await open(target, "wx");
73
+ const stream = createWriteStream(target, { fd: handle.fd, autoClose: false });
74
+ logs.push({ handle, stream });
75
+ }
76
+ }
77
+ await recordEvent({ event: "experiment_started" });
78
+ const child = spawn(command[0], command.slice(1), { stdio: capture ? ["inherit", "pipe", "pipe"] : "inherit" });
79
+ const written = logs.map(({ stream }) => finished(stream));
80
+ if (capture) {
81
+ child.stdout.pipe(logs[0].stream);
82
+ child.stderr.pipe(logs[1].stream);
83
+ child.stdout.pipe(process.stdout, { end: false });
84
+ child.stderr.pipe(process.stderr, { end: false });
85
+ }
86
+ let spawnError = null;
87
+ child.on("error", (error) => { spawnError = error.message; console.error(error.message); });
88
+ const [[code, signal]] = await Promise.all([
89
+ new Promise((resolve) => child.once("close", (...args) => resolve(args))),
90
+ ...written,
91
+ ]);
92
+ for (const { handle } of logs) { await handle.sync(); await handle.close(); }
93
+ process.exitCode = spawnError ? 1 : code ?? (signal ? 1 : 0);
94
+ await recordEvent({ event: "experiment_finished", finishedAt: new Date().toISOString(),
95
+ exitCode: process.exitCode, signal, ...(spawnError ? { error: spawnError } : {}),
96
+ ...(capture ? { evidenceFeedback: await inspectResearchPlanEvidence(resolved, path.dirname(planPath)) } : {}),
97
+ });
98
+ }
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministically validate and finalize an asset-preparation manifest."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import fnmatch
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ import subprocess
12
+ import sys
13
+
14
+
15
+ JOB_ROOT = Path(os.environ.get("CITEARK_JOB_ROOT", "/job")).resolve()
16
+
17
+
18
+ def canonical(value: object) -> bytes:
19
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode()
20
+
21
+
22
+ def digest_bytes(value: bytes) -> str:
23
+ return "sha256:" + hashlib.sha256(value).hexdigest()
24
+
25
+
26
+ def digest_file(target: Path) -> str:
27
+ digest = hashlib.sha256()
28
+ with target.open("rb") as handle:
29
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
30
+ digest.update(chunk)
31
+ return "sha256:" + digest.hexdigest()
32
+
33
+
34
+ def inside_job(value: str, *, allow_final_symlink: bool = False) -> Path:
35
+ target = Path(value)
36
+ if target.is_absolute() and target.parts[:2] == ("/", "job") and JOB_ROOT != Path("/job"):
37
+ target = JOB_ROOT.joinpath(*target.parts[2:])
38
+ elif not target.is_absolute():
39
+ target = JOB_ROOT / target
40
+ # Task-local virtual environments intentionally use a python symlink into
41
+ # the immutable base image. Permit that final executable symlink only for
42
+ # the explicit runtime probe; all evidence and asset paths still resolve
43
+ # their complete chain before the containment check.
44
+ resolved = Path(os.path.abspath(target)) if allow_final_symlink else target.resolve()
45
+ if resolved != JOB_ROOT and JOB_ROOT not in resolved.parents:
46
+ raise ValueError(f"path escapes /job: {value}")
47
+ return resolved
48
+
49
+
50
+ def relative_output(value: str) -> Path:
51
+ if not value or value.startswith("/") or ".." in Path(value).parts:
52
+ raise ValueError(f"unsafe evidence path: {value}")
53
+ return inside_job(f"output/{value}")
54
+
55
+
56
+ def inventory_asset(asset: dict, record: dict) -> None:
57
+ root = inside_job(asset["localPath"])
58
+ # localPath is a host-owned binding. A model may restate it incorrectly in
59
+ # the draft manifest, but it is never allowed to redirect prepared bytes
60
+ # away from the immutable plan. Canonicalize, then inspect the planned path.
61
+ record["localPath"] = asset["localPath"]
62
+ if record.get("status") not in {"prepared", "stream_ready"}:
63
+ return
64
+ if not root.is_dir():
65
+ raise ValueError(f"prepared asset directory missing: {root}")
66
+ files = []
67
+ for target in sorted(root.rglob("*")):
68
+ if target.is_symlink():
69
+ raise ValueError(f"asset symlink is not allowed: {target}")
70
+ if not target.is_file():
71
+ continue
72
+ relative = target.relative_to(root).as_posix()
73
+ files.append({
74
+ "path": relative,
75
+ "bytes": target.stat().st_size,
76
+ "digest": digest_file(target),
77
+ })
78
+ if not files:
79
+ raise ValueError(f"prepared asset has no files: {asset['id']}")
80
+ total = sum(item["bytes"] for item in files)
81
+ if total > asset["maximumExpandedBytes"]:
82
+ raise ValueError(f"asset exceeds approved disk budget: {asset['id']}")
83
+ paths = [item["path"] for item in files]
84
+ requested = asset.get("requiredPaths", [])
85
+ record["selectionObservation"] = {
86
+ "requested": requested,
87
+ "matched": [
88
+ pattern for pattern in requested
89
+ if any(fnmatch.fnmatchcase(candidate, pattern) for candidate in paths)
90
+ ],
91
+ "unmatched": [
92
+ pattern for pattern in requested
93
+ if not any(fnmatch.fnmatchcase(candidate, pattern) for candidate in paths)
94
+ ],
95
+ }
96
+ for pattern in asset.get("excludedPaths", []):
97
+ if any(fnmatch.fnmatchcase(candidate, pattern) for candidate in paths):
98
+ raise ValueError(f"asset contains excluded path {pattern}: {asset['id']}")
99
+ record["files"] = files
100
+ record["totalBytes"] = total
101
+
102
+
103
+ def validate_evidence(check: dict, record: dict, *, require_passed: bool = True) -> None:
104
+ if require_passed and record.get("status") in {"verified", "passed"}:
105
+ record["status"] = "passed"
106
+ if require_passed and record.get("status") != "passed":
107
+ raise ValueError(f"real preflight did not pass: {check['id']}")
108
+ commands = record.get("commands")
109
+ if not isinstance(commands, list) or not any(isinstance(item, str) and item.strip() for item in commands):
110
+ raise ValueError(f"preflight command missing: {check['id']}")
111
+ descriptors = record.get("evidence")
112
+ if not isinstance(descriptors, list) or not descriptors:
113
+ raise ValueError(f"preflight evidence missing: {check['id']}")
114
+ normalized_descriptors = []
115
+ for descriptor in descriptors:
116
+ if isinstance(descriptor, str):
117
+ descriptor = {"path": descriptor}
118
+ if not isinstance(descriptor, dict):
119
+ raise ValueError(f"preflight evidence descriptor invalid: {check['id']}")
120
+ target = relative_output(descriptor.get("path", ""))
121
+ if not target.is_file():
122
+ raise ValueError(f"preflight evidence file missing: {target}")
123
+ descriptor["bytes"] = target.stat().st_size
124
+ descriptor["digest"] = digest_file(target)
125
+ normalized_descriptors.append(descriptor)
126
+ record["evidence"] = normalized_descriptors
127
+
128
+
129
+ def validate_asset_identity(asset: dict, record: dict) -> None:
130
+ if record.get("identity") != asset.get("identity"):
131
+ raise ValueError(f"asset identity mismatch: {asset['id']}")
132
+ if asset.get("revision") and record.get("revision") != asset.get("revision"):
133
+ raise ValueError(f"asset revision mismatch: {asset['id']}")
134
+ allowed_sources = {
135
+ item.get("url") for item in asset.get("sourceLocators", []) if isinstance(item, dict)
136
+ }
137
+ if allowed_sources and record.get("sourceLocator") not in allowed_sources:
138
+ raise ValueError(f"asset source is outside the approved plan: {asset['id']}")
139
+ verification = record.get("verification")
140
+ if not isinstance(verification, dict):
141
+ raise ValueError(f"asset identity verification missing: {asset['id']}")
142
+ validate_evidence({"id": f"asset:{asset['id']}"}, verification)
143
+
144
+
145
+ def validate_acquisition(asset: dict, record: dict) -> int:
146
+ acquisition = record.get("acquisition")
147
+ if not isinstance(acquisition, dict):
148
+ raise ValueError(f"asset transfer audit missing: {asset['id']}")
149
+ download_bytes = acquisition.get("downloadBytes")
150
+ if not isinstance(download_bytes, int) or isinstance(download_bytes, bool) or download_bytes < 0:
151
+ raise ValueError(f"asset download bytes invalid: {asset['id']}")
152
+ if download_bytes > asset["maximumDownloadBytes"]:
153
+ raise ValueError(f"asset exceeds approved download budget: {asset['id']}")
154
+ locators = asset.get("sourceLocators", [])
155
+ selected = next(
156
+ (item for item in locators if item.get("url") == record.get("sourceLocator")),
157
+ None,
158
+ )
159
+ if selected:
160
+ expected_digest = selected.get("expectedDigest")
161
+ if expected_digest and acquisition.get("sourceDigest") != expected_digest:
162
+ raise ValueError(f"asset source digest mismatch: {asset['id']}")
163
+ if selected.get("fallbackRequiresDigestMatch") and not expected_digest:
164
+ raise ValueError(f"unverified mirror is not an acceptable fallback: {asset['id']}")
165
+ earlier = [
166
+ item for item in locators
167
+ if item.get("rank", 999999) < selected.get("rank", 999999)
168
+ ]
169
+ attempts = {
170
+ item.get("url"): item
171
+ for item in acquisition.get("attemptedSources", [])
172
+ if isinstance(item, dict)
173
+ }
174
+ for candidate in earlier:
175
+ if attempts.get(candidate.get("url"), {}).get("status") != "failed":
176
+ raise ValueError(
177
+ f"higher-priority source failure was not recorded: {asset['id']}"
178
+ )
179
+ validate_evidence(
180
+ {"id": f"acquisition:{asset['id']}"},
181
+ acquisition,
182
+ require_passed=False,
183
+ )
184
+ return download_bytes
185
+
186
+
187
+ def runtime_environment() -> dict:
188
+ python = inside_job("runtime-venv/bin/python", allow_final_symlink=True)
189
+ if not python.is_file():
190
+ raise ValueError("task-local /job/runtime-venv Python is missing")
191
+ version = subprocess.run(
192
+ [str(python), "--version"], check=True, capture_output=True, text=True
193
+ ).stdout.strip()
194
+ if not version:
195
+ version = subprocess.run(
196
+ [str(python), "--version"], check=True, capture_output=True, text=True
197
+ ).stderr.strip()
198
+ packages = subprocess.run(
199
+ [str(python), "-m", "pip", "freeze", "--all"],
200
+ check=True,
201
+ capture_output=True,
202
+ text=True,
203
+ ).stdout
204
+ normalized = "\n".join(sorted(line.strip() for line in packages.splitlines() if line.strip())) + "\n"
205
+ target = inside_job("output/logs/asset-preparation/package-inventory.txt")
206
+ target.parent.mkdir(parents=True, exist_ok=True)
207
+ target.write_text(normalized, encoding="utf-8")
208
+ inventory_digest = digest_bytes(normalized.encode())
209
+ return {
210
+ "path": "/job/runtime-venv",
211
+ "pythonVersion": version,
212
+ "lockPath": "logs/asset-preparation/package-inventory.txt",
213
+ "lockDigest": inventory_digest,
214
+ "packageInventoryDigest": inventory_digest,
215
+ }
216
+
217
+
218
+ def main() -> int:
219
+ if len(sys.argv) != 3:
220
+ raise ValueError("usage: validate-asset-preparation.py PLAN MANIFEST")
221
+ plan = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
222
+ manifest_path = Path(sys.argv[2])
223
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
224
+ if manifest.get("schemaVersion") != "0.1" or manifest.get("kind") != "citeark.asset-preparation-manifest":
225
+ raise ValueError("invalid manifest envelope")
226
+ if manifest.get("planDigest") != plan.get("planDigest"):
227
+ raise ValueError("manifest plan digest mismatch")
228
+ records = {item.get("assetId"): item for item in manifest.get("assets", []) if isinstance(item, dict)}
229
+ if len(records) != len(manifest.get("assets", [])):
230
+ raise ValueError("asset manifest contains duplicate or invalid records")
231
+ planned_asset_ids = {asset["id"] for asset in plan.get("assets", [])}
232
+ if set(records) != planned_asset_ids:
233
+ raise ValueError("asset manifest records do not exactly match the plan")
234
+ total_downloaded = 0
235
+ for asset in plan.get("assets", []):
236
+ record = records.get(asset["id"])
237
+ if record is None:
238
+ raise ValueError(f"asset record missing: {asset['id']}")
239
+ action = asset["decision"]["action"]
240
+ accepted = {
241
+ "metadata": {"metadata_verified", "prepared"},
242
+ "stream": {"stream_ready", "prepared"},
243
+ "blocked": {"blocked"},
244
+ "inspect": {"prepared", "stream_ready"} if asset.get("supportsStreaming") else {"prepared"},
245
+ "prepare": {"prepared"},
246
+ }[action]
247
+ if record.get("status") not in accepted:
248
+ raise ValueError(f"asset status mismatch: {asset['id']}")
249
+ validate_asset_identity(asset, record)
250
+ if record.get("status") in {"prepared", "stream_ready"}:
251
+ total_downloaded += validate_acquisition(asset, record)
252
+ inventory_asset(asset, record)
253
+ if total_downloaded > plan["limits"]["totalDownloadBytes"]:
254
+ raise ValueError("prepared assets exceed the total download budget")
255
+ total_prepared = sum(
256
+ record.get("totalBytes", 0)
257
+ for record in records.values()
258
+ if isinstance(record.get("totalBytes", 0), int)
259
+ )
260
+ if total_prepared > plan["limits"]["totalExpandedBytes"]:
261
+ raise ValueError("prepared assets exceed the total disk budget")
262
+ disk = os.statvfs(JOB_ROOT)
263
+ free_bytes = disk.f_bavail * disk.f_frsize
264
+ if free_bytes < plan["limits"]["minimumFreeBytesAfterPreparation"]:
265
+ raise ValueError("free disk space after preparation is below the approved reserve")
266
+ checks = {item.get("checkId"): item for item in manifest.get("checks", []) if isinstance(item, dict)}
267
+ if len(checks) != len(manifest.get("checks", [])):
268
+ raise ValueError("asset manifest contains duplicate or invalid checks")
269
+ if set(checks) != {check["id"] for check in plan.get("checks", [])}:
270
+ raise ValueError("asset manifest checks do not exactly match the plan")
271
+ for check in plan.get("checks", []):
272
+ record = checks.get(check["id"])
273
+ if record is None or record.get("kind") != check["kind"]:
274
+ raise ValueError(f"preflight record missing: {check['id']}")
275
+ validate_evidence(check, record)
276
+ manifest["runtimeEnvironment"] = runtime_environment()
277
+ manifest["validatedAt"] = __import__("datetime").datetime.now(__import__("datetime").timezone.utc).isoformat().replace("+00:00", "Z")
278
+ manifest.pop("manifestDigest", None)
279
+ manifest["manifestDigest"] = digest_bytes(canonical(manifest))
280
+ manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
281
+ print(json.dumps({"status": "passed", "manifestDigest": manifest["manifestDigest"]}))
282
+ return 0
283
+
284
+
285
+ if __name__ == "__main__":
286
+ try:
287
+ raise SystemExit(main())
288
+ except Exception as error:
289
+ print(f"asset manifest validation failed: {error}", file=sys.stderr)
290
+ raise SystemExit(2)
@@ -0,0 +1,57 @@
1
+ // No model service or paper execution: exercise Docker mounts, Python isolation
2
+ // and the authenticated host relay with a local HTTP fixture.
3
+ import assert from 'node:assert/strict';
4
+ import http from 'node:http';
5
+ import { mkdtemp, mkdir, readFile, rm } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import path from 'node:path';
8
+ import { startProviderSession } from '../src/provider/relay.mjs';
9
+ import { buildDockerRun } from '../src/sandbox/docker.mjs';
10
+ import { runProcess } from '../src/process.mjs';
11
+ import { HANDOFF_SCHEMA, stageExecutionHandoff } from '../src/deployment/handoff.mjs';
12
+
13
+ const root=await mkdtemp(path.join(tmpdir(),'CiteArk 本地 runtime '));
14
+ const requests=[];
15
+ const api=http.createServer((request,response)=>{
16
+ requests.push({url:request.url,authorization:request.headers.authorization});
17
+ request.resume();
18
+ response.writeHead(200,{'content-type':'application/json'});
19
+ response.end(JSON.stringify({choices:[{message:{role:'assistant',content:'local-fixture-ok'}}]}));
20
+ });
21
+ let relay;
22
+ try {
23
+ await new Promise(resolve=>api.listen(0,'127.0.0.1',resolve));
24
+ const runtimeAgent={runtime:'opencode',model:'fixture',api:{protocol:'openai-chat-completions',
25
+ baseUrl:`http://127.0.0.1:${api.address().port}`,apiKey:'local-fixture-credential',credentialSource:'test'}};
26
+ relay=await startProviderSession(runtimeAgent);
27
+ const directories=Object.fromEntries(['input','workspace','output','runtimeHome','execution'].map(k=>[k,path.join(root,k)]));
28
+ await Promise.all(Object.values(directories).map(p=>mkdir(p,{recursive:true})));
29
+ await mkdir(path.join(directories.workspace,'repository'));
30
+ const bundle={runId:`local-probe-${Date.now()}`,directories,runtimeAgent,
31
+ executionEnvironment:{image:'citeark-agent/runtime:cpu',cpus:1,memoryGb:2,shmGb:1,gpu:'none'}};
32
+ await stageExecutionHandoff(bundle,{schema:HANDOFF_SCHEMA,instructions:'Local runtime fixture only.',files:[]});
33
+ const python=`import json,os,pathlib,urllib.request,torch
34
+ cfg=json.loads(os.environ['OPENCODE_CONFIG_CONTENT'])
35
+ url=cfg['provider']['citeark']['options']['baseURL']+'/chat/completions'
36
+ req=urllib.request.Request(url,data=json.dumps({'model':'fixture','messages':[{'role':'user','content':'fixture'}]}).encode(),headers={'Content-Type':'application/json','Authorization':'Bearer '+os.environ['CITEARK_API_KEY']})
37
+ with urllib.request.urlopen(req,timeout=30) as r: answer=json.load(r)
38
+ assert answer['choices'][0]['message']['content']=='local-fixture-ok'
39
+ assert (torch.ones((8,8)) @ torch.ones((8,8))).sum().item()==512
40
+ pathlib.Path('/job/workspace/repository/源文件.txt').write_text('workspace-ok')
41
+ pathlib.Path('/job/assets/input.txt').write_text('external-input-fixture')
42
+ pathlib.Path('/job/output/probe.json').write_text(json.dumps({'relay':'ok','python':os.environ['VIRTUAL_ENV'],'torch':torch.__version__}))`;
43
+ const invocation=buildDockerRun({bundle,runtimeCommand:'python',runtimeArgs:['-c',python],attempt:1,providerSession:relay});
44
+ const result=await runProcess(invocation.command,invocation.args,{timeoutMs:120000});
45
+ assert.equal(result.code,0);
46
+ const output=JSON.parse(await readFile(path.join(directories.output,'probe.json'),'utf8'));
47
+ assert.equal(output.python,'/job/runtime-venv');
48
+ assert.equal(await readFile(path.join(directories.workspace,'repository','源文件.txt'),'utf8'),'workspace-ok');
49
+ assert.equal(requests.length,1);
50
+ assert.equal(requests[0].authorization,'Bearer local-fixture-credential');
51
+ console.log(JSON.stringify({status:'passed',mounts:'unicode-and-space-paths',relay:'container-to-host',...output}));
52
+ } finally {
53
+ await relay?.close();
54
+ api.closeAllConnections();
55
+ await new Promise(resolve=>api.close(resolve));
56
+ await rm(root,{recursive:true,force:true});
57
+ }
@@ -0,0 +1,57 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFile } from 'node:child_process';
3
+ import { access, mkdtemp, readFile, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { promisify } from 'node:util';
8
+
9
+ const run = promisify(execFile);
10
+ const root = fileURLToPath(new URL('..', import.meta.url));
11
+ const manifest = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
12
+ const temporary = await mkdtemp(path.join(tmpdir(), 'citeark-npm-'));
13
+ if (!process.env.npm_execpath) throw Error('请运行 npm run test:package。');
14
+ const npm = (args, options = {}) => run(process.execPath, [process.env.npm_execpath, ...args], { cwd: root, ...options });
15
+ try {
16
+ const { stdout } = await npm(['pack', '--json', '--ignore-scripts', '--pack-destination', temporary]);
17
+ const [packed] = JSON.parse(stdout);
18
+ const files = new Set(packed.files.map(file => file.path));
19
+ for (const required of ['src/cli.mjs', 'src/public/host.mjs', 'src/public/cap.mjs',
20
+ 'dist/arkgraph/viewer.js', 'dist/arkgraph/viewer.css', 'dist/arkgraph/viewer.en.js', 'dist/arkgraph/viewer.en.css', 'dist/arkgraph/index.html', 'dist/arkgraph/boot.js', 'scripts/preview-arkgraph.mjs',
21
+ 'docker/claude-code/Dockerfile', 'runtime/requirements-baseline.txt', 'runtime/install-local-cpu-runtime.sh',
22
+ 'scripts/run-research-plan.mjs', 'examples/toy-evaluation/paper.md', 'protocol/CAP.md',
23
+ 'runtime/mineru/parse.py', 'runtime/mineru/requirements.txt', 'src/research/paper-command.mjs']) {
24
+ assert.ok(files.has(required), `Missing runtime input: ${required}`);
25
+ }
26
+ for (const file of files) {
27
+ if (file.startsWith('dist/arkgraph/')) continue;
28
+ assert.doesNotMatch(file, /(^|\/)(?:\.env(?:\.|$)|\.npmrc$|\.git\/|node_modules\/|tests\/|dist\/)/);
29
+ }
30
+ const prefix = path.join(temporary, 'install');
31
+ await npm(['install', '--global', '--prefix', prefix, '--no-audit', '--no-fund', path.join(temporary, packed.filename)]);
32
+ const windows = process.platform === 'win32';
33
+ const bin = name => path.join(prefix, windows ? `${name}.cmd` : `bin/${name}`);
34
+ const cli = (name, args) => run(windows ? `"${bin(name)}"` : bin(name), args, { cwd: temporary, shell: windows });
35
+ for (const name of ['citeark', 'citeark-agent']) {
36
+ assert.equal((await cli(name, ['--version'])).stdout.trim(), manifest.version);
37
+ }
38
+ assert.match((await cli('citeark', ['--help'])).stdout, /citeark start/);
39
+ assert.match((await cli('citeark', ['--help'])).stdout, /citeark paper setup/);
40
+ // No terminal means help, not a blocking prompt or an accidental research run.
41
+ assert.match((await cli('citeark', [])).stdout, /CiteArk Agent/);
42
+ await cli('citeark', ['init', '--dir', 'example']);
43
+ await access(path.join(temporary, 'example', 'repository', 'evaluate.py'));
44
+ await cli('citeark', ['start', '--paper', 'example/paper.md', '--dry-run', '--work-dir', 'research']);
45
+ const session = JSON.parse(await readFile(path.join(temporary, 'research', 'workspace.json'), 'utf8'));
46
+ assert.equal(session.input.platformRepositoryId, null);
47
+ // Resolve the exported APIs from a consumer next to the installed node_modules.
48
+ const consumerRoot = windows ? prefix : path.join(prefix, 'lib');
49
+ await run(process.execPath, ['--input-type=module', '-e',
50
+ `import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url';
51
+ const require = createRequire(${JSON.stringify(path.join(consumerRoot, 'consumer.cjs'))});
52
+ await import(pathToFileURL(require.resolve('@citeark/agent/host')));
53
+ await import(pathToFileURL(require.resolve('@citeark/agent/cap')));`], { cwd: temporary });
54
+ console.log(`${manifest.name}@${manifest.version}: global commands, help, example, independent dry-run and public APIs passed (${packed.entryCount} package files).`);
55
+ } finally {
56
+ await rm(temporary, { recursive: true, force: true });
57
+ }