@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,137 @@
1
+ import { appendFile, rm, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { format } from "node:util";
4
+ import { main } from "../cli.mjs";
5
+ import { hasSessionEvents, sessionEvent } from "../terminal/events.mjs";
6
+ import { withResearchControl, cancellationError } from "./control.mjs";
7
+ import { jobJson, saveJobJson } from "./jobs.mjs";
8
+
9
+ const root = process.argv[2];
10
+ const request = await jobJson(root, "request.json");
11
+ const abort = new AbortController();
12
+ let state = {
13
+ pid: process.pid,
14
+ status: "running",
15
+ startedAt: new Date().toISOString(),
16
+ };
17
+ let action = "run";
18
+ let saving = Promise.resolve();
19
+ const save = (patch) =>
20
+ (saving = saving.then(async () => {
21
+ state = { ...state, ...patch, updatedAt: new Date().toISOString() };
22
+ await saveJobJson(root, "state.json", state);
23
+ }));
24
+ await save({});
25
+ const cancel = () => abort.abort(cancellationError());
26
+ process.on("SIGTERM", cancel);
27
+ process.on("SIGINT", cancel);
28
+ const originalLog = console.log;
29
+ console.log = (...args) =>
30
+ hasSessionEvents()
31
+ ? sessionEvent({ kind: "status", text: format(...args) })
32
+ : originalLog(...args);
33
+ let polling = false;
34
+ const poll = async () => {
35
+ if (polling) return;
36
+ polling = true;
37
+ try {
38
+ action = (await jobJson(root, "control.json", { action: "run" })).action;
39
+ if (action === "cancel" && !abort.signal.aborted) {
40
+ await save({ status: "cancelling" });
41
+ cancel();
42
+ }
43
+ } finally {
44
+ polling = false;
45
+ }
46
+ };
47
+ const timer = setInterval(
48
+ () => poll().catch((error) => abort.abort(error)),
49
+ 1000,
50
+ );
51
+ try {
52
+ const result = await withResearchControl(
53
+ {
54
+ signal: abort.signal,
55
+ cleanup: async (record) => {
56
+ await save({
57
+ resourceCleanup: [...(state.resourceCleanup ?? []), record],
58
+ });
59
+ sessionEvent({
60
+ kind: record.status === "failed" ? "error" : "status",
61
+ text: `${record.provider} ${record.resource}: cleanup ${record.status}${record.error ? ` — ${record.error}` : ""}`,
62
+ });
63
+ },
64
+ instructions: async () => {
65
+ let lines;
66
+ try {
67
+ lines = await readFile(
68
+ path.join(root, ".citeark-job", "notes.jsonl"),
69
+ "utf8",
70
+ );
71
+ } catch (error) {
72
+ if (error.code === "ENOENT") return "";
73
+ throw error;
74
+ }
75
+ const notes = lines.split("\n").filter(Boolean).map(JSON.parse);
76
+ sessionEvent({
77
+ kind: "status",
78
+ text: `Including ${notes.length} follow-up instruction(s) in the next Agent invocation.`,
79
+ });
80
+ return notes.map((note) => `[${note.at}] ${note.text}`).join("\n");
81
+ },
82
+ boundary: async (stage) => {
83
+ await poll();
84
+ abort.signal.throwIfAborted();
85
+ while (action === "pause") {
86
+ if (state.status !== "paused") {
87
+ await save({ status: "paused", stage });
88
+ sessionEvent({
89
+ kind: "status",
90
+ text: "Research paused. Use session resume to continue.",
91
+ });
92
+ }
93
+ await new Promise((resolve) => setTimeout(resolve, 500));
94
+ await poll();
95
+ abort.signal.throwIfAborted();
96
+ }
97
+ await save({ status: "running", stage });
98
+ },
99
+ },
100
+ () => main(["start", ...request.argv]),
101
+ );
102
+ clearInterval(timer);
103
+ abort.signal.throwIfAborted();
104
+ await save({
105
+ status:
106
+ result?.phase === "completed"
107
+ ? "completed"
108
+ : result?.phase === "awaiting_selection"
109
+ ? "awaiting_selection"
110
+ : "prepared",
111
+ finishedAt: new Date().toISOString(),
112
+ });
113
+ } catch (error) {
114
+ clearInterval(timer);
115
+ await save({
116
+ status: abort.signal.aborted ? "cancelled" : "failed",
117
+ error: error.message,
118
+ finishedAt: new Date().toISOString(),
119
+ });
120
+ await appendFile(
121
+ path.join(root, "session.events.jsonl"),
122
+ JSON.stringify({
123
+ at: new Date().toISOString(),
124
+ kind: "error",
125
+ text: error.message,
126
+ }) + "\n",
127
+ );
128
+ process.exitCode = abort.signal.aborted ? 130 : 1;
129
+ } finally {
130
+ await rm(path.join(root, ".citeark-job", "starting"), {
131
+ recursive: true,
132
+ force: true,
133
+ });
134
+ clearInterval(timer);
135
+ process.removeListener("SIGTERM", cancel);
136
+ process.removeListener("SIGINT", cancel);
137
+ }
@@ -0,0 +1,10 @@
1
+ // Fixed design units. The collision solver reserves the entire motion envelope.
2
+ export const motionEnvelope=n=>n.main?{x:2.25,y:1.65}:{x:3.15,y:2.45};
3
+ export const layoutPosition=n=>({x:n.restX??n.x,y:n.restY??n.y});
4
+ export function ambientOffset(n,seconds,group){
5
+ const phase=group*.64,ramp=1-(1-Math.min(1,seconds/2.4))**3;
6
+ return {
7
+ x:((n.main?1.8:2.7)*Math.sin(seconds*.36+phase)+.45*Math.sin(seconds*.67+n.phase))*ramp,
8
+ y:((n.main?1.3:2.1)*Math.sin(seconds*.29+phase+.9)+.35*Math.sin(seconds*.53+n.phase))*ramp
9
+ };
10
+ }
@@ -0,0 +1,153 @@
1
+ import sourceTypes from './locales/source-types.json' with {type:'json'};
2
+ import {useExperimentSelection,ExperimentToolbar,ExperimentDock,createExperimentPanel} from './experiment-ui.jsx';
3
+ import {createOutline} from './outline.jsx';
4
+ import {contentGroups,createReadingColors} from './reading-presentation.mjs';
5
+ import React, {useState,useEffect,useMemo,useRef,useDeferredValue} from 'react';
6
+
7
+ import {createRecordDetails} from './record-details.jsx';
8
+ import {createPortal} from 'react-dom';
9
+ import {createOpenGraph} from './open-graph.jsx';
10
+ import {MD,typeColors,coreLabels,coreHints,coreTypeOf,colorOf,shapeOf,alpha,semanticLabelOf,descriptionOf,classificationOf,shortTypeLabel} from './research-types.mjs';
11
+ import './style.css';
12
+ import './steering-style.css';
13
+ import './material-style.css';
14
+ export function createArkGraphView(D,{sourceUrl=D.sourceDocument??null}={}){
15
+ const byKey=new Map(D.nodes.map(n=>[n.key,n])),byLocal=new Map(D.nodes.map(n=>[n.local,n]));
16
+ const roleLabel={'claim':'主张','hypothesis':'假说','observation':'观测','procedure':'实验计划','procedure-step':'计划步骤','model':'模型','checkpoint':'检查点','dataset':'数据集','sample':'样本','method':'方法','material':'材料','code':'代码','agent':'参与者','source-work':'论文来源','assessment':'阅读分析'};
17
+ const typeLabel=semanticLabelOf;
18
+ const categoryOf=coreTypeOf;
19
+ const categoryLabels=coreLabels;
20
+ const categoryHints=coreHints;
21
+ const allCategories=true;
22
+ const typeHint=descriptionOf;
23
+ const contextRoles=n=>classificationOf(n).contextualRoles;
24
+ const isResult=n=>coreTypeOf(n)==='result';
25
+ const isMethodIllustration=n=>!!n&&coreTypeOf(n)==='method';
26
+ const predicateLabel={requires:'需要',used:'使用',derivedFrom:'派生自',assertedIn:'发表于',associatedWith:'参与',follows:'遵循',basedOn:'依据',describes:'描述',assesses:'评估',expects:'预期产出',partOf:'属于',revises:'修订',supersedes:'替代',about:'涉及',evidenceFor:'证据关联',plannedFor:'拟检验',needs:'需要',hasStep:'包含步骤',uses:'使用',wasDerivedFrom:'派生自',attributedTo:'归属于',wasGeneratedBy:'生成自',wasAssociatedWith:'参与',generated:'生成',usesMethod:'使用方法',explains:'解释假说',evaluates:'阅读核对'};
27
+ const incident=new Map(D.nodes.map(n=>[n.key,[]]));
28
+ D.edges.forEach(e=>{incident.get(e.from)?.push(e);if(e.to!==e.from)incident.get(e.to)?.push(e)});
29
+ const recordSourcePage=n=>{if(n.sourcePage)return n.sourcePage;const m=n.source?.match(/\b(?:pp?\.|pages?\s+)\s*(\d+)|物理页\s*(\d+)|第\s*(\d+)\s*页/i);const page=Number(m?.slice(1).find(Boolean));return Number.isSafeInteger(page)&&page>0?page:null};
30
+ function sourceLink(n){const page=recordSourcePage(n);return sourceUrl?sourceUrl+(page?'#page='+page:''):undefined}
31
+ const pretty=x=>JSON.stringify(x,null,2);
32
+ const recordLabel=n=>n?n.label:'未解析的外部引用';
33
+ const displayValue=m=>String(m.printed??m.value)+(m.printed?'':m.unit==='%'?'%':m.unit?' '+m.unit:'');
34
+ const evidenceCount=k=>new Set((incident.get(k)||[]).map(e=>e.from===k?e.to:e.from).filter(k=>{const n=byKey.get(k);return (isResult(n)||isMethodIllustration(n))&&!n.prospective})).size;
35
+ const metricNames={};
36
+
37
+ let completeData=D;
38
+ const searchText=new WeakMap();
39
+ const loadCompleteData=()=>Promise.resolve(D);
40
+ function useCompleteData(enabled=true){
41
+ const [value,setValue]=useState(completeData),[error,setError]=useState(null);
42
+ useEffect(()=>{let active=true;if(enabled&&!value)loadCompleteData().then(d=>{if(active)setValue(d)}).catch(e=>{if(active)setError(e.message)});return()=>{active=false}},[enabled,value]);
43
+ return {value,error,retry:()=>{setError(null);loadCompleteData().then(setValue).catch(e=>setError(e.message))}};
44
+ }
45
+ function Disclosure({title,children,defaultOpen=false}){
46
+ const [open,setOpen]=useState(defaultOpen);
47
+ return <details open={open} onToggle={e=>setOpen(e.currentTarget.open)}><summary>{title}</summary>{open&&children()}</details>;
48
+ }
49
+ function RawRecord({record,edge=false}){
50
+ const {value,error,retry}=useCompleteData();
51
+ if(error)return <p role="alert">{error} <button className="text-button" onClick={retry}>重新加载</button></p>;
52
+ if(!value)return <p className="section-note" role="status">正在加载完整记录…</p>;
53
+ const original=(edge?value.edges:value.nodes).find(o=>edge?o.id===record.id:o.key===record.key);
54
+ return <pre>{pretty(original.raw)}</pre>;
55
+ }
56
+ function cachedSearch(o){if(!searchText.has(o))searchText.set(o,JSON.stringify(o).toLowerCase());return searchText.get(o)}
57
+
58
+ function Measurements({items}) {
59
+ const [all,setAll]=useState(false);if(!items.length)return null;
60
+ return <section><h3>报告数值 <small>{items.length} 项</small></h3><div className="table-scroll"><table><thead><tr><th>条件</th><th>指标</th><th>数值</th></tr></thead><tbody>{(all?items:items.slice(0,8)).map((m,i)=><tr key={m.id||i}><td>{Object.entries(m.dimensions||{}).map(([k,v])=><div key={k}>{k+':'+(typeof v==='object'?JSON.stringify(v):String(v))}</div>)}</td><td>{metricNames[m.metric]||m.metric}</td><td><b>{displayValue(m)}</b></td></tr>)}</tbody></table></div>{items.length>8&&<button className="text-button" onClick={()=>setAll(!all)}>{all?'收起数值':'查看全部 '+items.length+' 项数值'}</button>}<Disclosure title="数值的完整条件与来源">{()=> <pre>{pretty(items)}</pre>}</Disclosure></section>
61
+ }
62
+ function RecordMeta({n}){return <div className="detail-eyebrow" style={{'--record-color':colorOf(n)}}>{coreLabels[coreTypeOf(n)]} · {typeLabel(n)} · {n.prospective?'研究计划 · 尚未执行':n.role==='assessment'?'研究评估':n.role==='claim'?'来源理解':n.basis==='declared'?'原文报告':n.basis==='observed'?'运行观测':'来源资料'}</div>;}
63
+ const canPreview=n=>!!n.image||(n.category==='result'&&!!n.sourcePage&&!!sourceUrl&&(/\.pdf(?:[?#]|$)|arxiv\.org\/pdf\//i.test(sourceUrl)));
64
+ const RecordDetails=createRecordDetails({canPreview,incident,byKey,isResult,isMethodIllustration,recordLabel,typeLabel,sourceLink,FigureCard,Measurements,Disclosure,predicateLabel});
65
+ // The reading projection is separate from the immutable CAP graph.
66
+ const shortLabels=D.presentation.shortLabels;
67
+ const spine=D.presentation.spine;
68
+ const allPrimary=D.presentation.primary.map(key=>byKey.get(key));
69
+ const themeFor=new Map(D.nodes.map(n=>[n.key,n.stage]));
70
+ const stageOf=n=>n.stage;
71
+ const groups=D.presentation.groups;
72
+ const readingColorOf=createReadingColors(spine);
73
+ const ReadingGraph=createOpenGraph({D,byKey,spine,allPrimary,groups,shortLabels,recordLabel,predicateLabel,readingColorOf});
74
+ function FigureCard({n}){
75
+ const [large,setLarge]=useState(false);
76
+ useEffect(()=>{const close=e=>{if(e.key==='Escape'&&large){e.preventDefault();e.stopImmediatePropagation();setLarge(false)}};window.addEventListener('keydown',close,true);return()=>window.removeEventListener('keydown',close,true)},[large]);
77
+ return <><figure><button className="figure-button" onClick={()=>setLarge(true)} aria-label={'放大'+(n.imageCaption||n.label)}>{n.image?<img src={n.image} alt={n.imageCaption||n.label}/>:<iframe className="source-page" src={sourceLink(n)} title={n.label} loading="lazy"/>}</button><figcaption>{n.imageCaption||n.source}</figcaption></figure>{large&&createPortal(<div className="lightbox" role="dialog" aria-label="原文图表" onClick={()=>setLarge(false)}><button className="lightbox-close" aria-label="关闭放大图表" onClick={()=>setLarge(false)}><Icon name="close" size={22}/></button><figure onClick={e=>e.stopPropagation()}>{n.image?<img src={n.image} alt={n.imageCaption||n.label}/>:<iframe className="source-page" src={sourceLink(n)} title={n.label} loading="lazy"/>}<figcaption>{n.imageCaption||n.label} <a href={sourceLink(n)} target="_blank" rel="noopener">查看原论文 ↗</a></figcaption></figure></div>,document.body)}</>;
78
+ }
79
+ function DetailPanel({selection,onClose,openRecord,openEdge,showEvidence,expanded,onExpand,navigation}){
80
+ if(!selection)return null;
81
+ const n=selection.kind==='record'?byKey.get(selection.id):null,e=selection.kind==='edge'?D.edges.find(x=>x.id===selection.id):null;
82
+ return <aside className="inspector"><div className="drawer-top">{n?<RecordMeta n={n}/>:<span>整理关系</span>}<div className="drawer-actions">{navigation}<button className="detail-resize" aria-label={expanded?"缩小详情,显示图谱":"展开详情"} aria-expanded={expanded} onClick={onExpand}><Icon name={expanded?"minimize":"maximize"} size={17}/></button><button aria-label="关闭详情" onClick={onClose}><Icon name="close" size={18}/></button></div></div><div className="drawer-body" key={selection.id}>
83
+ {n&&<RecordDetails n={n} openRecord={openRecord} openEdge={openEdge} showEvidence={showEvidence}/>}
84
+ {e&&<><div className="detail-eyebrow">整理关系</div><h2>{predicateLabel[e.predicate]||e.predicate}</h2><button className="detail-row" onClick={()=>openRecord(e.from)}><span><small>起点</small><b>{recordLabel(byKey.get(e.from))}</b></span></button><div className="arrow-between">↓ {e.predicate}</div><button className="detail-row" onClick={()=>openRecord(e.to)}><span><small>终点</small><b>{recordLabel(byKey.get(e.to))}</b></span></button>{e.predicate==='evidenceFor'&&<p className="section-note">证据关联来自原文阅读,不表示已完成独立验证。</p>}<p className="original">{e.qualification}</p><p className="section-note">{e.source}</p><Disclosure title="完整整理关系">{()=> <RawRecord record={e} edge/>}</Disclosure></>}
85
+ </div></aside>;
86
+ }
87
+ const legend=contentGroups.map(group=>({...group,count:D.nodes.filter(n=>group.types.includes(coreTypeOf(n))).length})).filter(group=>group.count>0);
88
+ function Icon({name,size=16}){
89
+ const shapes={
90
+ outline:<><path d="M8 5h13M8 12h13M8 19h13"/><circle cx="3" cy="5" r=".6"/><circle cx="3" cy="12" r=".6"/><circle cx="3" cy="19" r=".6"/></>,
91
+ comment:<><path d="M5 4h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H9l-5 3v-3H3V6a2 2 0 0 1 2-2Z"/><path d="M8 8h9M8 12h6"/></>,
92
+ chart:<><path d="M4 3v17h17M8 16v-5M13 16V6M18 16V9"/></>,
93
+ motion:<><path d="M3 8c4-7 8 7 12 0M5 16c4-7 8 7 12 0"/><circle cx="20" cy="6" r="1"/></>,
94
+ fit:<><path d="M9 4H4v5m11-5h5v5M4 15v5h5m11-5v5h-5"/><circle cx="12" cy="12" r="2"/></>,
95
+ maximize:<><path d="M8 3H3v5m13-5h5v5M3 16v5h5m13-5v5h-5"/></>,
96
+ minimize:<><path d="M3 8h5V3m8 0v5h5M8 21v-5H3m13 5v-5h5"/></>,
97
+ brand:<><path d="M5 18 12 5l7 13M5 18h14M8 12h8"/><circle cx="12" cy="5" r="2"/><circle cx="5" cy="18" r="2"/><circle cx="19" cy="18" r="2"/></>,
98
+ route:<><circle cx="5" cy="18" r="2"/><circle cx="19" cy="6" r="2"/><path d="M7 18h8a3 3 0 0 0 0-6H9a3 3 0 0 1 0-6h8"/></>,
99
+ settings:<><path d="M4 7h16M4 17h16"/><circle cx="9" cy="7" r="3"/><circle cx="15" cy="17" r="3"/></>,
100
+ search:<><circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 4 4"/></>,
101
+ reset:<><path d="M5 8a8 8 0 1 1-1 8M5 3v5h5"/></>,
102
+ rotate:<><path d="M20 8a8 8 0 0 0-14-3L3 8m0-5v5h5M4 16a8 8 0 0 0 14 3l3-3m0 5v-5h-5"/></>,
103
+ label:<><path d="M4 5h16M12 5v15M8 20h8"/></>,
104
+ document:<><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9l-6-6Zm0 0v6h6M8 13h8m-8 4h5"/></>,
105
+ external:<><path d="M14 4h6v6m0-6L10 14M11 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5"/></>,
106
+ 'chevron-up':<path d="m6 15 6-6 6 6"/>, 'chevron-down':<path d="m6 9 6 6 6-6"/>,
107
+ 'arrow-left':<path d="M20 12H4m6-6-6 6 6 6"/>, 'arrow-right':<path d="M4 12h16m-6-6 6 6-6 6"/>,
108
+ close:<path d="m6 6 12 12M6 18 18 6"/>
109
+ };
110
+ return <svg className="ui-icon" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.65" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">{shapes[name]}</svg>;
111
+ }
112
+
113
+ function TypeMark({type}){
114
+ const outlined=['hypothesis','plan','result'].includes(type),dashed=['hypothesis','plan'].includes(type),square=['method','plan','activity','result'].includes(type);
115
+ return <svg className="type-mark" width="12" height="12" viewBox="0 0 16 16" aria-hidden="true" style={{color:typeColors[type]}} fill={outlined?'none':'currentColor'} stroke={outlined?'currentColor':'none'} strokeWidth="1.5" strokeDasharray={dashed?'2 2':undefined}>{square?<rect x="2" y="2" width="12" height="12" rx={type==='result'?1:3}/>:type==='assessment'?<path d="M8 1 15 8 8 15 1 8Z"/>:<circle cx="8" cy="8" r="6"/>}{type==='result'&&<path d="M5 6h6M5 10h6"/>}</svg>;
116
+ }
117
+ const Catalog=createOutline({D,byKey,spine,groups,recordLabel,typeLabel,coreLabels,coreHints,coreTypeOf,colorOf:readingColorOf,Icon,TypeMark,useCompleteData,cachedSearch,predicateLabel});
118
+ function MainlineNavigation({idx,openRecord}){return <>{idx>=0?<nav className="spine-nav" aria-label="主线导航"><button aria-label="上一个主线节点" disabled={idx===0} onClick={()=>openRecord(spine[idx-1])}><Icon name="arrow-left" size={18}/></button><span>{idx+1} / {spine.length}</span><button aria-label="下一个主线节点" disabled={idx===spine.length-1} onClick={()=>openRecord(spine[idx+1])}><Icon name="arrow-right" size={18}/></button></nav>:null}</>;}
119
+ const ExperimentPanel=createExperimentPanel({D,byKey,Icon,recordLabel});
120
+ function App({selectionControl}={}){
121
+ const experiments=useExperimentSelection(D,selectionControl);
122
+ const [experimentReturn,setExperimentReturn]=useState(null);
123
+ const [selection,setSelection]=useState(null),[catalog,setCatalog]=useState(false),[labels,setLabels]=useState(true),[hidden,setHidden]=useState(new Set()),[extras,setExtras]=useState([]),[resetSignal,setResetSignal]=useState(0),[fitSignal,setFitSignal]=useState(0),[detailExpanded,setDetailExpanded]=useState(false),[ambient,setAmbient]=useState(true),[folded,setFolded]=useState(true),[visibleCount,setVisibleCount]=useState(allPrimary.length);
124
+ const openExperiment=id=>{setExperimentReturn(null);setCatalog(false);setSelection({kind:id?'experiment':'experiment-scope',id:id||'experiment-scope'});};
125
+ const openRecord=(k,returnTo=null)=>{setExperimentReturn(returnTo);if(!k){setSelection(null);return}if(!byKey.has(k))return;setCatalog(false);setSelection({kind:'record',id:k});setFolded(true);};
126
+ const openEdge=id=>{setCatalog(false);setSelection({kind:'edge',id})};
127
+ const showEvidence=k=>{const ks=[...new Set((incident.get(k)||[]).map(e=>e.from===k?e.to:e.from))].filter(key=>{const n=byKey.get(key);return (isResult(n)||isMethodIllustration(n))&&!n.prospective});const xs=[...new Set([k,...ks])].filter(key=>!allPrimary.some(n=>n.key===key));xs.owner=k;setExtras(xs);setSelection({kind:'record',id:k});};
128
+ const reset=()=>{setSelection(null);setCatalog(false);setExtras([]);setHidden(new Set());setResetSignal(x=>x+1)};
129
+ useEffect(()=>{const close=e=>{if(e.key==='Escape'){setCatalog(false);setSelection(null)}};window.addEventListener('keydown',close);return()=>window.removeEventListener('keydown',close)},[]);
130
+
131
+ useEffect(()=>setDetailExpanded(false),[selection?.id]);
132
+ const isExperiment=selection?.kind==='experiment'||selection?.kind==='experiment-scope';
133
+ const idx=spine.indexOf(selection?.id);
134
+ const graphCount=visibleCount;
135
+
136
+ return <main className={'reference-app '+(selection||catalog?'panel-open ':'')+(selection?'detail-open ':'')+(detailExpanded?'detail-expanded':'')}>
137
+ <header className="app-header"><span className="brand"><span className="brand-mark"><Icon name="brand" size={23}/></span>ArkGraph</span><span className="header-divider"/><span className="header-context">论文阅读地图</span>{!selection&&!catalog&&<button className="header-start" onClick={()=>openRecord(spine[0])}><Icon name="route"/>从主线开始</button>}{sourceUrl&&<a className="original-link" href={sourceUrl} target="_blank" rel="noopener"><Icon name="document"/>论文原文<Icon name="external" size={13}/></a>}</header>
138
+ <section className={'paper-card '+(folded?'folded':'')}><div className="paper-summary"><div className="paper-card-title"><div><div className="paper-eyebrow">研究概览</div><h1>{D.titleZh}</h1></div><button aria-label={folded?'展开论文说明':'收起论文说明'} onClick={()=>setFolded(!folded)}><Icon name={folded?'chevron-down':'chevron-up'} size={15}/></button></div>{folded&&<p className="paper-mini-meta">图中 {graphCount} 个对象 · 全文 {D.nodes.length} 个</p>}{!folded&&<><p>{D.question}</p><div className="paper-card-source"><span><b>{D.nodes.filter(n=>n.role==='claim').length}</b> 项主张 <i/> <b>{D.nodes.filter(n=>n.role==='hypothesis').length}</b> 个假说</span><span className="source-status"><i/>{D.provenance===sourceTypes.provenance?'Agent 生成 · 来源声明':D.provenance}</span></div></>}</div>
139
+ <nav className="toolbar" aria-label="视图控制">
140
+ <button className="icon-button" aria-label="查看全图" onClick={()=>{setSelection(null);setCatalog(false);setFitSignal(x=>x+1)}} title="查看全图"><Icon name="fit"/></button>
141
+ <ExperimentToolbar state={experiments} onEnter={()=>{setSelection(null);setCatalog(false);experiments.setSelecting(v=>!v)}}/><span className="toolbar-divider"/><button className="catalog-button" aria-pressed={catalog} onClick={()=>{setSelection(null);setCatalog(!catalog)}}><Icon name="outline"/>大纲</button>
142
+ </nav></section>
143
+ <section className="scene"><div className="scene-main">
144
+ <ReadingGraph onVisibleCount={setVisibleCount} experiments={experiments.model.experiments} experimentSelecting={experiments.selecting} experimentLocked={experiments.locked} experimentSelectable={experiments.selectable} experimentSelected={experiments.selected} experimentNodes={experiments.scope.nodeIds} onExperiment={id=>{if(experiments.selecting)experiments.toggle(id);else openExperiment(id)}} onInspectExperiment={openExperiment} selection={selection} labels={labels} folded={folded} hidden={hidden} extras={extras} onPick={openRecord} onPickEdge={openEdge} resetSignal={resetSignal} fitSignal={fitSignal} ambient={ambient} detailExpanded={detailExpanded} motionBlocked={catalog}/>
145
+ <ExperimentDock state={experiments} onReview={()=>openExperiment(null)}/><div className="legend" aria-label="内容分类"><span className="legend-title">内容</span>{legend.map(group=><button key={group.id} title={group.types.map(type=>coreLabels[type]).join('、')+" · 全文 "+group.count+" 项 · 点击弱化或恢复"} aria-label={group.label} aria-pressed={!group.types.every(type=>hidden.has(type))} onClick={()=>setHidden(old=>{const next=new Set(old),restore=group.types.every(type=>old.has(type));for(const type of group.types){if(restore)next.delete(type);else next.add(type)}return next})}><span className="legend-group-mark" style={{color:group.color}}>{group.id==='context'?<Icon name="search" size={13}/>:group.id==='argument'?<Icon name="comment" size={13}/>:group.id==='process'?<Icon name="route" size={13}/>:group.id==='evidence'?<Icon name="chart" size={13}/>:<Icon name="document" size={13}/>}</span>{group.label}</button>)}</div><div className="projection-note">{extras.length>0&&<button onClick={()=>setExtras([])}>回到研究概览</button>}</div>
146
+ </div>{isExperiment?<ExperimentPanel state={experiments} selection={selection} onClose={()=>setSelection(null)} onOpenRecord={k=>openRecord(k,selection)} onOpenExperiment={openExperiment} expanded={detailExpanded} onExpand={()=>setDetailExpanded(v=>!v)}/>:<DetailPanel selection={selection} onClose={()=>setSelection(null)} navigation={<>{experimentReturn&&<button aria-label="返回实验详情" onClick={()=>setSelection(experimentReturn)}>←</button>}<MainlineNavigation idx={idx} openRecord={openRecord}/></>} expanded={detailExpanded} onExpand={()=>setDetailExpanded(v=>!v)} openRecord={openRecord} openEdge={openEdge} showEvidence={showEvidence}/>} {catalog&&<Catalog onClose={()=>setCatalog(false)} openRecord={openRecord} openEdge={openEdge}/>}</section>
147
+ </main>;
148
+ }
149
+ return App;
150
+ }
151
+
152
+ import './experiment-style.css';
153
+ import './record-detail.css';
@@ -0,0 +1,6 @@
1
+ (() => {
2
+ const english=!new URL(location.href).searchParams.get('locale')?.startsWith('zh');
3
+ document.documentElement.lang=english?'en':'zh-CN';
4
+ const style=document.createElement('link');style.rel='stylesheet';style.href=english?'viewer.en.css':'viewer.css';document.head.append(style);
5
+ const script=document.createElement('script');script.src=english?'viewer.en.js':'viewer.js';document.body.append(script);
6
+ })();
@@ -0,0 +1,20 @@
1
+ // One cancellable transition owns scale and center together. No library tween queue.
2
+ export function createCameraMotion({graph,element,reduced,onActivity}){
3
+ let frame=0,target=null;
4
+ const cancel=()=>{cancelAnimationFrame(frame);frame=0;target=null;element.dataset.cameraMotion='idle';};
5
+ const move=(next,ms)=>{
6
+ if(target&&['x','y','k'].every(key=>Math.abs(target[key]-next[key])<1e-5))return;
7
+ const start={...graph.centerAt(),k:graph.zoom()};
8
+ if(!frame&&['x','y','k'].every(key=>Math.abs(start[key]-next[key])<1e-6))return;
9
+ cancel();target=next;const time=reduced()?0:ms,began=performance.now();
10
+ element.dataset.cameraStarts=String(Number(element.dataset.cameraStarts||0)+1);
11
+ const tick=now=>{
12
+ const t=time?Math.min(1,(now-began)/time):1,e=1-(1-t)**3;
13
+ onActivity();graph.zoom(Math.exp(Math.log(start.k)+(Math.log(next.k)-Math.log(start.k))*e));graph.centerAt(start.x+(next.x-start.x)*e,start.y+(next.y-start.y)*e);
14
+ element.dataset.cameraMotion=t<1?'moving':'idle';element.dataset.cameraProgress=t.toFixed(3);
15
+ if(t<1)frame=requestAnimationFrame(tick);else{frame=0;target=null;}
16
+ };
17
+ tick(began);
18
+ };
19
+ return {move,cancel,get active(){return !!frame;},dispose:cancel};
20
+ }
@@ -0,0 +1,39 @@
1
+ // A view-local expansion of existing records; source identities and relations stay intact.
2
+ export function createContextIndex(D,primary,spine){
3
+ const base=new Set(primary),hidden=new Set(D.nodes.filter(n=>!base.has(n.key)&&n.role!=='source-work'&&n.category!=='agent'&&n.role!=='procedure'&&n.role!=='procedure-step').map(n=>n.key));
4
+ const groups=D.presentation?.contextGroups;
5
+ const children=new Map([...new Set([...primary,...(groups??[]).map(g=>g.parent)])].map(id=>[id,[]])),owners=new Map();
6
+ const add=(owner,id)=>{if(!hidden.has(id)||!children.has(owner))return;if(!children.get(owner).includes(id))children.get(owner).push(id);if(!owners.has(id))owners.set(id,[]);if(!owners.get(id).includes(owner))owners.get(id).push(owner);};
7
+ // Reading ownership controls disclosure. Dense scientific adjacency stays in details.
8
+ if(groups)for(const group of groups)for(const id of group.children)add(group.parent,id);
9
+ else for(const owner of primary)for(const edge of D.edges){const id=edge.from===owner?edge.to:edge.to===owner?edge.from:null;add(owner,id);}
10
+ return {children,owners,base,spine};
11
+ }
12
+ export function contextRecords(index,{hoverOwner=null,selected=null,previousOwner=null}={}){
13
+ const selectedOwner=index.children.has(selected)?selected:index.owners.get(selected)?.includes(previousOwner)?previousOwner:index.owners.get(selected)?.[0];
14
+ const active=[...new Set([selectedOwner,hoverOwner].filter(Boolean))],records=new Map();
15
+ for(const owner of active)for(const id of index.children.get(owner)||[])if(!records.has(id))records.set(id,{id,owner});
16
+ if(selected&&!index.base.has(selected)&&!records.has(selected))records.set(selected,{id:selected,owner:selectedOwner||index.spine[0]});
17
+ return [...records.values()];
18
+ }
19
+ const intersects=(a,b,pad=7)=>a.x<b.x+b.w+pad&&a.x+a.w+pad>b.x&&a.y<b.y+b.h+pad&&a.y+a.h+pad>b.y;
20
+ export function placeContext(items,obstacles){
21
+ const occupied=obstacles.map(b=>({...b})),result=new Map();
22
+ for(const item of items){let choice=null,score=Infinity;
23
+ for(let ring=0;ring<18;ring++){
24
+ const radius=70+ring*22;
25
+ for(let j=0;j<24;j++){
26
+ const angle=-Math.PI/2+j*Math.PI/12,x=item.owner.x+Math.cos(angle)*radius,y=item.owner.y+Math.sin(angle)*radius;
27
+ const side=Math.cos(angle)<-.15?'left':'right',dx=side==='left'?-item.w-12:12,dy=-item.h/2;
28
+ const node={x:x-7,y:y-7,w:14,h:14},label={x:x+dx,y:y+dy,w:item.w,h:item.h};
29
+ if(occupied.some(o=>intersects(node,o)||intersects(label,o)))continue;
30
+ const cost=radius+Math.abs(Math.cos(angle))*12;
31
+ if(cost<score){choice={x,y,dx,dy,side,node,label};score=cost;}
32
+ }
33
+ if(choice)break;
34
+ }
35
+ if(!choice)throw Error('Could not place context record '+item.id);
36
+ result.set(item.id,choice);occupied.push(choice.node,choice.label);
37
+ }
38
+ return result;
39
+ }
@@ -0,0 +1,3 @@
1
+ .inspector,.catalog{width:365px;flex:none;border-left:1px solid #e1e7ee;background:#fff;display:flex;flex-direction:column;min-height:0}.drawer-top{padding:13px 21px;border-bottom:1px solid #edf0f4;display:flex;justify-content:space-between;align-items:center;color:#98a4b3;font-size:10px;letter-spacing:.8px;flex:none}.drawer-top button{font-size:23px;font-weight:300;color:#9eaabc;line-height:20px;padding:0 3px}.drawer-body{padding:23px 23px 35px;overflow:auto;min-height:0}.detail-eyebrow{font-size:10px;color:#8295ad;letter-spacing:.7px;margin-bottom:12px}h2{font-size:20px;line-height:1.6;font-weight:550;margin:0 0 13px;color:#35495f}.lead{font-size:12px;color:#758397;line-height:1.95;margin:12px 0 18px}.source-chip{display:block;padding:8px 10px;background:#f5f7fa;border-radius:6px;color:#7a90a9;font-size:10px;line-height:1.6}.primary,.secondary{border-radius:6px;padding:9px 13px;font-size:11px;line-height:1.5}.primary{border:1px solid #ccd8e6;background:#edf3fa;color:#527397;width:100%;margin-top:6px}.secondary{border:1px solid #dfe5ed;color:#6f859f;background:#fafcfe;margin-top:12px;width:100%}section h3{font-size:11px;font-weight:550;color:#536881;display:flex;justify-content:space-between;align-items:center;margin:24px 0 11px}h3 small{font-weight:400;color:#a2aebc;font-size:10px}.detail-row{width:100%;display:flex;justify-content:space-between;align-items:center;gap:12px;text-align:left;border-bottom:1px solid #edf1f5;padding:12px 2px;font-size:11px;color:#657b94;line-height:1.6}.detail-row:hover{background:#f8fafc}.detail-row span:first-child{min-width:0}.detail-row b{display:block;font-weight:500;color:#4e647d}.detail-row small{display:block;font-size:9px;color:#a1afbf;line-height:1.7;margin:3px 0}.section-note{font-size:10px;line-height:1.9;color:#a0aaba;margin:10px 0}.number-story{padding:15px 16px;margin:20px 0;border:1px solid #e5ebf3;border-radius:9px;background:#f8fafc}.number-story>span{font-size:10px;color:#91a0b2}.number-story>div{display:flex;align-items:center;gap:12px;margin:11px 0;color:#486685}.number-story b{font-size:22px;font-weight:500;font-variant-numeric:tabular-nums}.number-story i{font-size:11px;color:#a2b1c1;font-style:normal}.number-story small{font-size:9px;color:#9caabd}.table-scroll{overflow:auto;margin:10px 0}table{width:100%;border-collapse:collapse;font-size:10px;color:#6e839c}th{text-align:left;color:#a0adbd;font-size:9px;font-weight:400;padding:8px 6px;border-bottom:1px solid #dfe7f0}td{padding:9px 6px;border-bottom:1px solid #edf1f6;vertical-align:top;line-height:1.6}td b{white-space:nowrap;color:#5a7493;font-variant-numeric:tabular-nums}td:first-child{max-width:120px;overflow-wrap:anywhere}.text-button{font-size:10px;color:#6483a5;padding:8px 0}details{border-bottom:1px solid #edf1f5;padding:12px 0;margin-top:9px;font-size:10px;color:#7f94ad}summary{cursor:pointer;color:#8596ac;font-size:10px;line-height:1.7}pre{white-space:pre-wrap;overflow-wrap:anywhere;background:#f7f9fc;color:#6d829b;padding:12px;border-radius:6px;font:9px/1.75 ui-monospace,SFMono-Regular,monospace;max-height:500px;overflow:auto}.original{font-size:10px;line-height:1.9;color:#8797ab}.identity{display:block;overflow-wrap:anywhere;font-size:9px;color:#a2afbf;margin:10px 0}.arrow-between{text-align:center;padding:14px;color:#9cabbf;font-size:10px}figure{margin:20px 0}figure img{width:100%;display:block;border:1px solid #eef1f6;border-radius:4px}figcaption{font-size:9px;color:#a1aebe;line-height:1.6;margin-top:8px}.catalog-tools{padding:17px 20px;display:flex;flex-direction:column;gap:12px;border-bottom:1px solid #e8edf3}.catalog-tools input,.catalog-tools select{width:100%;font-size:10px;padding:10px;border:1px solid #dfe6ef;border-radius:6px;color:#7a8ea8;background:#fff}.catalog-tools .segmented button{flex:1}.catalog-tools>small{font-size:9px;color:#a0adbd}.catalog-list{overflow:auto;flex:1;min-height:0;padding:0 20px}.pagination{padding:12px 20px;display:flex;gap:12px;border-top:1px solid #edf1f5;justify-content:space-between;font-size:10px;color:#8da0b8}.archive{display:flex;gap:20px;padding:12px 20px;border-top:1px solid #edf1f5;font-size:9px;color:#a1afbf}
2
+
3
+ .source-page{display:block;width:100%;height:380px;border:0;background:#FFFFFF}.lightbox .source-page{width:80vw;height:78dvh}
@@ -0,0 +1,28 @@
1
+ import React,{useState,useEffect} from 'react';
2
+ import {createRoot} from 'react-dom/client';
3
+ import {createArkGraphView} from './app.jsx';
4
+ const root=createRoot(document.getElementById('root'));
5
+ const embedded=window.parent!==window;
6
+ const channel='citeark-arkgraph/1';
7
+ function Hosted({data,options}){
8
+ const [control,setControl]=useState(options.selectionControl);
9
+ const [App]=useState(()=>createArkGraphView(data,{sourceUrl:options.sourceUrl}));
10
+ useEffect(()=>{const update=e=>{
11
+ if(e.source===parent&&e.data?.channel===channel&&e.data.type==='selection')setControl(e.data.control);
12
+ };window.addEventListener('message',update);return()=>window.removeEventListener('message',update)},[]);
13
+ const selectionControl=control?{...control,onToggle:id=>parent.postMessage({channel,type:'toggle',digest:data.digest,id},'*'),onClear:()=>parent.postMessage({channel,type:'clear',digest:data.digest},'*')}:undefined;
14
+ return <App selectionControl={selectionControl}/>;
15
+ }
16
+ if(embedded){
17
+ let initialized=false;
18
+ window.addEventListener('message',e=>{
19
+ if(e.source!==parent||e.data?.channel!==channel||e.data.type!=='init'||initialized)return;
20
+ const {data,options={}}=e.data;
21
+ if(data?.schemaVersion!=='arkgraph-presentation/1'||!data.digest||!Array.isArray(data.nodes))return;
22
+ initialized=true;root.render(<Hosted data={data} options={options}/>);
23
+ });
24
+ parent.postMessage({channel,type:'ready'},'*');
25
+ }else{
26
+ const element=document.getElementById('graphData');
27
+ if(element){const data=JSON.parse(element.textContent),App=createArkGraphView(data);root.render(<App/>);}
28
+ }
@@ -0,0 +1,17 @@
1
+ export function curvePoint([a,c,b],t){const s=1-t;return{x:s*s*a.x+2*s*t*c.x+t*t*b.x,y:s*s*a.y+2*s*t*c.y+t*t*b.y};}
2
+ // Route interaction affordances around existing labels; never feed them back into node or label layout.
3
+ export function experimentCurve(a,b,rectangles,index,unit,labelSize={w:80,h:80}){
4
+ const dx=b.x-a.x,dy=b.y-a.y,length=Math.hypot(dx,dy)||1,nx=-dy/length,ny=dx/length;
5
+ let best=null,score=Infinity;
6
+ for(const amount of [35,-35,65,-65,100,-100,145,-145,200,-200,260,-260,340,-340,440,-440,560,-560]){
7
+ for(const along of [0,-70,70,-150,150,-240,240,-340,340]){
8
+ const bend=amount*unit,shift=along*unit,c={x:(a.x+b.x)/2+nx*bend+dx/length*shift,y:(a.y+b.y)/2+ny*bend+dy/length*shift},p=[a,c,b],m=curvePoint(p,.5);
9
+ const hit=(q,pad)=>rectangles.some(r=>q.x>=r.x-pad&&q.x<=r.x+r.w+pad&&q.y>=r.y-pad&&q.y<=r.y+r.h+pad);
10
+ let cost=Math.abs(amount)*.08+Math.abs(along)*.025+(Math.sign(amount)===(index%2?-1:1)?0:2);
11
+ for(let t=.12;t<.9;t+=.08)if(hit(curvePoint(p,t),2*unit))cost+=12;
12
+ if(rectangles.some(r=>m.x-labelSize.w*unit/2<r.x+r.w&&m.x+labelSize.w*unit/2>r.x&&m.y-labelSize.h*unit/2<r.y+r.h&&m.y+labelSize.h*unit/2>r.y))cost+=1000000;
13
+ if(cost<score){score=cost;best=p;}
14
+ }
15
+ }
16
+ return best;
17
+ }
@@ -0,0 +1,15 @@
1
+ export function createExperimentSelection(D){return {D,experiments:D.experiments??[]};}
2
+ export function selectedExperimentScope(model,ids){
3
+ const byId=new Map(model.experiments.map(e=>[e.id,e])),nodes=new Map(model.D.nodes.map(n=>[n.key,n]));
4
+ const chosen=new Set(),visiting=new Set();
5
+ const visit=id=>{if(chosen.has(id))return;if(visiting.has(id))throw Error('实验依赖存在循环');const e=byId.get(id);if(!e)throw Error('实验依赖不可用');visiting.add(id);for(const dependency of e.requiredProcedureKeys)visit(dependency);visiting.delete(id);chosen.add(id);};
6
+ ids.forEach(visit);const experiments=[...chosen].map(id=>byId.get(id));
7
+ const stepIds=[...new Set(experiments.flatMap(e=>e.stepKeys))];
8
+ const operations=stepIds.map(id=>({id,title:nodes.get(id)?.label??id,methodRef:id,experimentIds:experiments.filter(e=>e.stepKeys.includes(id)).map(e=>e.id)}));
9
+ const resources=[...new Set(experiments.flatMap(e=>e.inputKeys))].filter(id=>!byId.has(id));
10
+ return {experiments,operations,resources,nodeIds:[...new Set(experiments.flatMap(e=>e.contextNodeIds))],addedIds:[...chosen].filter(id=>!ids.includes(id)),gaps:experiments.flatMap(e=>e.unresolvedInputs.map(r=>`缺少输入:${r.ref}`))};
11
+ }
12
+ export function experimentDraft(model,ids){
13
+ const scope=selectedExperimentScope(model,ids);
14
+ return {artifactDigest:model.D.digest,procedures:scope.experiments.map(e=>({ref:e.procedure.ref,digest:e.procedure.digest}))};
15
+ }
@@ -0,0 +1,26 @@
1
+ /* Add selection affordances to the existing ArkGraph shell; keep its layout and typography. */
2
+ .reference-app .experiment-toggle{font-size:12px;gap:6px;border:0;background:transparent;color:#616161;padding:7px 10px;display:flex;align-items:center;white-space:nowrap;border-radius:8px}.reference-app .experiment-toggle.active{color:#1976D2;background:#E3F2FD}.experiment-toggle span{font-size:10px;border-radius:8px;background:#E3F2FD;color:#1976D2;padding:1px 5px}.experiment-overlay{position:absolute;inset:0;pointer-events:none;overflow:hidden;z-index:3}.experiment-edge-group{position:absolute;left:0;top:0;display:flex;align-items:center;gap:7px;padding:5px 7px 5px 9px;border-radius:9px;background:#FAFAFA;box-shadow:0 2px 12px #00000008;pointer-events:auto;transition:background .15s,box-shadow .15s}.experiment-edge-group[data-hovered=true]{background:#E3F2FD;box-shadow:0 3px 14px #1976D212}.experiment-edge-label{border:0;background:none;padding:0;display:flex;flex-direction:column;align-items:flex-start;gap:3px;white-space:nowrap;cursor:pointer;text-align:left}.experiment-edge-label>span{font:500 10.5px/1.35 -apple-system,"PingFang SC",sans-serif;color:#1976D2}.experiment-edge-label>small{font:400 9px/1.3 -apple-system,"PingFang SC",sans-serif;color:#757575}.experiment-edge-pick{flex:none;width:22px;height:22px;display:grid;place-items:center;padding:0;border:0;border-radius:50%;background:#E3F2FD;color:#1976D2;cursor:pointer;font:500 15px/1 -apple-system,sans-serif}.experiment-edge-pick[aria-pressed=true]{background:#1E88E5;color:#FFFFFF}.experiment-edge-label:focus-visible,.experiment-edge-pick:focus-visible{outline:2px solid #1976D2;outline-offset:3px}.experiment-edge-pick span{pointer-events:none}.experiment-dock{position:absolute;bottom:82px;left:50%;transform:translateX(-50%);display:flex;align-items:center;justify-content:space-between;gap:20px;padding:15px 18px;width:min(470px,calc(100% - 40px));border-radius:14px;background:#FFFFFF;box-shadow:0 6px 26px #0000000D;z-index:7}.experiment-dock-copy{display:flex;flex-direction:column;gap:5px}.experiment-dock-copy b{font-size:12px;font-weight:550;color:#424242}.experiment-dock-copy small{font-size:10px;color:#757575}.experiment-dock>div:last-child{display:flex;align-items:center;gap:12px;white-space:nowrap}.experiment-clear{border:0;background:transparent;padding:4px;color:#757575;font-size:11px}.experiment-review{background:#1976D2;color:#FFFFFF;border:0;padding:10px 12px;border-radius:8px;font-size:11px;cursor:pointer}.experiment-review:disabled{opacity:.35;cursor:default}.experiment-reminder{position:absolute;right:24px;bottom:82px;border:0;background:#E3F2FD;color:#1976D2;border-radius:9px;padding:10px 14px;font-size:12px;z-index:7}.reference-app.detail-open .experiment-dock{left:22px;right:420px;width:auto;transform:none;gap:12px}.reference-app.detail-open .experiment-reminder{display:none}.exp-source{width:100%;display:flex;align-items:center;justify-content:space-between;gap:12px;text-align:left;border:0;background:#FAFAFA;border-radius:9px;padding:11px 12px;margin:7px 0;color:#424242;cursor:pointer}.exp-source>span:first-child{display:flex;flex-direction:column;gap:4px}.exp-source b{font-size:12px;font-weight:500;line-height:1.6}.exp-source small{font-size:10px;color:#757575;line-height:1.5}.exp-source>span:last-child{color:#9E9E9E}.exp-source:hover{background:#F5F5F5}.exp-source.selected{background:#E3F2FD}.exp-relation{margin:22px 0 16px}.exp-relation-arrow{text-align:center;font-size:10px;color:#757575;padding:5px}.exp-primary{width:100%;border:0;border-radius:9px;background:#1976D2;color:#FFFFFF;padding:12px;font-size:12px;cursor:pointer;margin:10px 0}.exp-primary.chosen{background:#E3F2FD;color:#1976D2}.exp-primary:disabled{opacity:.4;cursor:default}.exp-step-group h4{font-size:11px;color:#757575;margin:15px 0 9px;font-weight:500}.exp-steps{padding:0 0 0 8px;list-style:none;margin:0}.exp-steps li{position:relative;border-left:1px solid #E0E0E0;padding-left:20px}.exp-steps li:last-child{border-color:transparent}.exp-steps li:before{content:'';position:absolute;left:-4px;top:16px;width:7px;height:7px;border-radius:50%;background:#BDBDBD}.exp-steps li:last-child:before{background:#1976D2}.exp-steps button{width:100%;display:flex;justify-content:space-between;gap:10px;border:0;background:none;padding:11px 0;text-align:left;cursor:pointer}.exp-steps b{font-size:12px;color:#616161;font-weight:500}.exp-steps small{font-size:10px;color:#757575}.exp-decisions{list-style:none;padding:0}.exp-decisions li{font-size:12px;color:#616161;line-height:1.8;padding-left:13px;position:relative;margin:8px 0}.exp-decisions li:before{content:'·';position:absolute;left:0;color:#1976D2}.exp-note{font-size:11px;line-height:1.8;color:#757575}.exp-estimates{display:flex;gap:28px;padding:15px 0}.exp-estimates span{display:flex;flex-direction:column;gap:5px;color:#757575;font-size:10px}.exp-estimates b{font-size:12px;color:#616161;font-weight:500}.exp-status{color:#00897B;font-size:11px;line-height:1.7}.exp-raw{font-size:11px;margin:14px 0;color:#757575}.exp-raw pre{max-height:260px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;background:#FAFAFA;padding:12px;font-size:10px}.experiment-inspector .drawer-body section{margin:24px 0}.experiment-inspector .drawer-body h3{font-size:12px}.experiment-inspector .drawer-body h3 small{color:#9E9E9E;font-size:10px}.experiment-inspector .drawer-top>span{font-size:11px;color:#757575}.experiment-inspector .drawer-body h2{font-size:22px}.experiment-inspector .drawer-body .lead{font-size:13px;line-height:1.8}
3
+ @media(max-width:780px){.reference-app .experiment-toggle{padding:7px 8px;font-size:11px}.reference-app .toolbar{max-width:calc(100% - 32px)}.experiment-dock{bottom:80px;padding:13px;width:calc(100% - 32px);gap:8px}.experiment-dock-copy b{font-size:11px}.experiment-dock-copy small{font-size:9px}.experiment-dock>div:last-child{gap:8px}.reference-app.detail-open .experiment-dock{display:none}.experiment-reminder{bottom:80px;right:16px}.reference-app .paper-card h1{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}}
4
+
5
+ .exp-comparison{margin:16px 0 7px;padding:13px 15px;background:#FAFAFA;border-radius:10px}.exp-cohort{display:block;color:#757575;font-size:10px;text-align:center}.exp-compare-models{display:flex;align-items:center;justify-content:space-around;padding:15px 0}.exp-compare-models>span{display:flex;flex-direction:column;gap:5px;color:#757575;font-size:10px;text-align:center}.exp-compare-models b{font-size:14px;color:#424242;font-weight:550}.exp-compare-models .exp-compare-arrow{color:#9E9E9E;font-size:17px}.exp-compare-metric{display:flex;justify-content:center;gap:10px;font-size:10px;color:#757575;padding-top:8px;border-top:1px solid #EEEEEE}.exp-compare-metric b{font-weight:500;color:#1976D2}.exp-evidence{margin:13px 0;font-size:11px;color:#757575}.exp-evidence summary{cursor:pointer}.exp-evidence .exp-relation{margin-top:12px}.experiment-inspector .drawer-body h2{font-size:21px;line-height:1.5}
6
+ @media(prefers-reduced-motion:reduce){.experiment-edge-group{transition:none}}
7
+
8
+ /* One continuous plus-to-check gesture, including interrupted and reverse toggles. */
9
+ .selection-mark{display:block;flex:none;width:16px;height:16px;overflow:visible;pointer-events:none;fill:none;stroke:currentColor;stroke-width:2.1;stroke-linecap:round;stroke-linejoin:round}
10
+ .selection-mark path{transition:d 460ms cubic-bezier(.22,1.28,.36,1)}
11
+ .selection-mark-short{d:path("M5 12 L19 12")}
12
+ .selection-mark-long{d:path("M12 19 L12 5");transition-delay:20ms}
13
+ .selection-mark[data-selected=true] .selection-mark-short{d:path("M6 12 L10 16")}
14
+ .selection-mark[data-selected=true] .selection-mark-long{d:path("M10 16 L18 8")}
15
+ .experiment-edge-pick{transition:background-color 340ms cubic-bezier(.22,.61,.36,1),color 340ms cubic-bezier(.22,.61,.36,1),transform 420ms cubic-bezier(.22,1.28,.36,1);-webkit-tap-highlight-color:transparent}
16
+ .experiment-edge-pick[aria-pressed=true]{transform:scale(1.035)}
17
+ @media(hover:hover){.experiment-edge-pick:hover{transform:scale(1.1)}}
18
+ .experiment-edge-pick:active{transform:scale(.9);transition-duration:90ms}
19
+ .exp-selection-button{display:flex;align-items:center;justify-content:center;gap:7px;transition:background-color 340ms cubic-bezier(.22,.61,.36,1),color 340ms cubic-bezier(.22,.61,.36,1)}
20
+ .exp-selection-button .selection-mark{width:17px;height:17px}
21
+ @media(prefers-reduced-motion:reduce){.selection-mark path,.experiment-edge-pick,.exp-selection-button{transition:none}.experiment-edge-pick:active,.experiment-edge-pick:hover,.experiment-edge-pick[aria-pressed=true]{transform:none}}
22
+
23
+ /* Readable route labels share the graph coordinate scale; full questions open in details. */
24
+ .experiment-edge-label{white-space:normal;max-width:190px}.experiment-edge-label>span{font-size:12px;line-height:1.5}
25
+
26
+ .experiment-edge-pick:disabled{opacity:.4;cursor:default}.exp-selection-button:disabled{opacity:.5;cursor:default}
@@ -0,0 +1,32 @@
1
+ import React,{useState,useMemo} from 'react';
2
+ import {SelectionMark} from './selection-mark.jsx';
3
+ import {createExperimentSelection,selectedExperimentScope,experimentDraft} from './experiment-selection.mjs';
4
+ export function useExperimentSelection(D,control){
5
+ const model=useMemo(()=>createExperimentSelection(D),[D]),key='arkgraph-procedures:'+D.digest;
6
+ const [selecting,setSelecting]=useState(false),[requested,setRequested]=useState(()=>{if(control)return [];try{const draft=JSON.parse(localStorage.getItem(key));return draft?.artifactDigest===D.digest?model.experiments.filter(e=>draft.procedures.some(p=>p.ref===e.procedure.ref&&p.digest===e.procedure.digest)).map(e=>e.id):[];}catch{return[];}}),[saved,setSaved]=useState(false),[error,setError]=useState('');
7
+ const scope=useMemo(()=>selectedExperimentScope(model,control?control.selected:requested),[model,requested,control]),selected=scope.experiments.map(e=>e.id);
8
+ const toggle=id=>{if(control){if(!control.locked&&control.selectable.includes(id))control.onToggle(id);return;}setSaved(false);setRequested(old=>selected.includes(id)?old.filter(root=>!selectedExperimentScope(model,[root]).experiments.some(e=>e.id===id)):[...old,id]);};
9
+ const clear=()=>{if(control){if(!control.locked)control.onClear();return;}setRequested([]);setSaved(false);try{localStorage.removeItem(key);}catch{}};
10
+ const draft=()=>experimentDraft(model,requested);
11
+ const save=()=>{try{localStorage.setItem(key,JSON.stringify(draft()));setSaved(true);setError('');}catch{setError('此浏览器无法保存草案。');}};
12
+ return {controlled:!!control,locked:!!control?.locked,selectable:control?.selectable,model,selecting,setSelecting,selected,toggle,clear,scope,save,saved,error,draft};
13
+ }
14
+ export function ExperimentToolbar({state,onEnter}){return state.model.experiments.length>0&&<button className={'experiment-toggle '+(state.selecting?'active':'')} aria-pressed={state.selecting} onClick={onEnter}>{state.selecting?'完成选择':'选择实验'}{state.selected.length>0&&<span>{state.selected.length}</span>}</button>}
15
+ export function ExperimentDock({state,onReview}){
16
+ if(!state.selecting)return state.selected.length?<button className="experiment-reminder" onClick={onReview}>{state.selected.length} 项实验已选 <span>→</span></button>:null;
17
+ return <div className="experiment-dock"><div className="experiment-dock-copy" aria-live="polite"><b>{state.selected.length?`已选 ${state.selected.length} 项实验`:'先看问题,再选择实验'}</b><small>{state.selected.length?`包含 ${state.scope.operations.length} 个必要步骤`:'点问题看比较方式 · + 加入范围'}</small></div><div>{state.selected.length>0&&<button className="experiment-clear" onClick={state.clear}>清空</button>}<button className="experiment-review" disabled={!state.selected.length} onClick={onReview}>查看范围 →</button></div></div>;
18
+ }
19
+ export function createExperimentPanel({byKey,Icon,recordLabel}){
20
+ return function ExperimentPanel({state,selection,onClose,onOpenRecord,onOpenExperiment,expanded,onExpand}){
21
+ const current=state.model.experiments.find(e=>e.id===selection.id),review=selection.kind==='experiment-scope',local=current?selectedExperimentScope(state.model,[current.id]):state.scope;
22
+ const sources=ids=>ids.map(id=>{const node=byKey.get(id);return node&&<button className="exp-source" key={id} onClick={()=>onOpenRecord(id)}><span><b>{recordLabel(node)}</b><small>{node.source}</small></span><span>↗</span></button>});
23
+ return <aside className="inspector experiment-inspector"><div className="drawer-top"><span>{review?'实验范围':'候选实验'}</span><div className="drawer-actions"><button className="detail-resize" aria-label={expanded?'缩小详情,显示图谱':'展开详情'} onClick={onExpand}><Icon name={expanded?'minimize':'maximize'} size={17}/></button><button aria-label="关闭详情" onClick={onClose}><Icon name="close" size={18}/></button></div></div><div className="drawer-body" key={selection.id}>
24
+ {review?<><h2>本次选择的实验</h2><p className="lead">{local.experiments.length} 项实验,共用 {local.operations.length} 个科研步骤。</p>{local.experiments.map(e=><button key={e.id} className="exp-source selected" onClick={()=>onOpenExperiment(e.id)}><b>{e.question}</b><span>↗</span></button>)}</>:<><h2>{current.question}</h2><p className="lead">{current.summary}</p>{current.criterion&&<section><h3>观察与判断</h3><p>{current.criterion}</p></section>}<button disabled={state.locked||(state.selectable&&!state.selectable.includes(current.id))} className={'exp-primary exp-selection-button '+(state.selected.includes(current.id)?'chosen':'')} aria-pressed={state.selected.includes(current.id)} onClick={()=>{state.setSelecting(true);state.toggle(current.id)}}><SelectionMark selected={state.selected.includes(current.id)}/><span>{state.selected.includes(current.id)?'已选择 · 移出范围':'选择这项实验'}</span></button><details className="exp-evidence"><summary>原文依据与目标</summary>{sources([...new Set([current.source,...current.targetKeys])])}</details></>}
25
+ {!review&&!!current.limitations?.length&&<section><h3>待核对条件</h3>{current.limitations.map((text,i)=><p key={i}>{text}</p>)}</section>}
26
+ {!!local.operations.length&&<section><h3>包含的必要步骤</h3><ol className="exp-steps">{local.operations.map(o=><li key={o.id}><button onClick={()=>onOpenRecord(o.methodRef)}><b>{o.title}</b>{o.experimentIds.length>1&&<small>{o.experimentIds.length} 项实验共用</small>}</button></li>)}</ol></section>}
27
+ {!!local.resources.length&&<section><h3>实验材料 <small>{local.resources.length}</small></h3>{sources(local.resources)}</section>}
28
+ {!!local.gaps.length&&<section><h3>待明确的输入</h3>{local.gaps.map(g=><p key={g}>{g}</p>)}</section>}
29
+ {review&&!state.controlled&&<><p className="exp-note">保存选择后,正式执行前仍需确认资源与费用。</p><button className="exp-primary" disabled={!state.selected.length} onClick={state.save}>{state.saved?'已保存到此浏览器':'保存实验范围'}</button>{state.error&&<p role="alert">{state.error}</p>}</>}
30
+ </div></aside>;
31
+ };
32
+ }
@@ -0,0 +1 @@
1
+ <!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ArkGraph</title></head><body><div id="root"></div><script src="boot.js"></script></body></html>