@tiangong-ai/cli 0.0.42 → 0.0.44

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 (57) hide show
  1. package/AGENTS.md +3 -2
  2. package/README.md +87 -24
  3. package/dist/research/orchestration.js +318 -14
  4. package/dist/research/orchestration.js.map +1 -1
  5. package/dist/research/setup-command.js +13 -1
  6. package/dist/research/setup-command.js.map +1 -1
  7. package/dist/research/workspace/acquisition.d.ts +7 -0
  8. package/dist/research/workspace/acquisition.js +37 -10
  9. package/dist/research/workspace/acquisition.js.map +1 -1
  10. package/dist/research/workspace/audit-bundle.d.ts +18 -0
  11. package/dist/research/workspace/audit-bundle.js +100 -3
  12. package/dist/research/workspace/audit-bundle.js.map +1 -1
  13. package/dist/research/workspace/content-evidence.d.ts +98 -0
  14. package/dist/research/workspace/content-evidence.js +715 -0
  15. package/dist/research/workspace/content-evidence.js.map +1 -0
  16. package/dist/research/workspace/evidence-ledger.d.ts +1 -1
  17. package/dist/research/workspace/evidence-ledger.js +2 -0
  18. package/dist/research/workspace/evidence-ledger.js.map +1 -1
  19. package/dist/research/workspace/executor.d.ts +9 -0
  20. package/dist/research/workspace/executor.js +161 -4
  21. package/dist/research/workspace/executor.js.map +1 -1
  22. package/dist/research/workspace/inference.d.ts +69 -0
  23. package/dist/research/workspace/inference.js +360 -0
  24. package/dist/research/workspace/inference.js.map +1 -0
  25. package/dist/research/workspace/native-activity.js +14 -5
  26. package/dist/research/workspace/native-activity.js.map +1 -1
  27. package/dist/research/workspace/preflight.d.ts +2 -0
  28. package/dist/research/workspace/preflight.js +6 -2
  29. package/dist/research/workspace/preflight.js.map +1 -1
  30. package/dist/research/workspace/projects.js +17 -1
  31. package/dist/research/workspace/projects.js.map +1 -1
  32. package/dist/research/workspace/publication-workflow.d.ts +26 -0
  33. package/dist/research/workspace/publication-workflow.js +314 -3
  34. package/dist/research/workspace/publication-workflow.js.map +1 -1
  35. package/dist/research/workspace/review-executor.d.ts +50 -0
  36. package/dist/research/workspace/review-executor.js +854 -0
  37. package/dist/research/workspace/review-executor.js.map +1 -0
  38. package/dist/research/workspace/runtime.js +194 -45
  39. package/dist/research/workspace/runtime.js.map +1 -1
  40. package/dist/research/workspace/schemas.js +83 -4
  41. package/dist/research/workspace/schemas.js.map +1 -1
  42. package/dist/research/workspace/scientific-review.js +57 -0
  43. package/dist/research/workspace/scientific-review.js.map +1 -1
  44. package/dist/research/workspace/setup-catalog.js +22 -2
  45. package/dist/research/workspace/setup-catalog.js.map +1 -1
  46. package/dist/research/workspace/setup-declarative.d.ts +4 -0
  47. package/dist/research/workspace/setup-declarative.js +16 -1
  48. package/dist/research/workspace/setup-declarative.js.map +1 -1
  49. package/dist/research/workspace/setup-wizard.js +46 -3
  50. package/dist/research/workspace/setup-wizard.js.map +1 -1
  51. package/dist/research/workspace/setup.d.ts +3 -1
  52. package/dist/research/workspace/setup.js +77 -19
  53. package/dist/research/workspace/setup.js.map +1 -1
  54. package/dist/research/workspace/types.d.ts +43 -1
  55. package/dist/research/workspace/workspace.js +62 -5
  56. package/dist/research/workspace/workspace.js.map +1 -1
  57. package/package.json +1 -1
@@ -9,7 +9,7 @@ import { loadProject } from "./projects.js";
9
9
  import { assertResearchPolicyBinding } from "./research-policy.js";
10
10
  import { assertScientificGateForStage } from "./scientific-review.js";
11
11
  import { canonicalJson, ensureDirectory, isObject, pathExists, readJsonFile, sha256File, sha256Text, workspacePaths, writeJsonAtomic, } from "./storage.js";
12
- import { withWorkspaceLock } from "./workspace.js";
12
+ import { loadWorkspaceConfig, withWorkspaceLock } from "./workspace.js";
13
13
  const REQUIRED_REVIEW_ROLES = [
14
14
  "evidence",
15
15
  "methods-reproducibility",
@@ -24,6 +24,20 @@ const EDITOR_DECISIONS = new Set([
24
24
  "reject-and-redesign",
25
25
  "desk-reject",
26
26
  ]);
27
+ const REQUIRED_SUBMISSION_ROLES = [
28
+ "cover-letter",
29
+ "title-page",
30
+ "reporting-checklist",
31
+ "data-availability",
32
+ "code-availability",
33
+ "source-data",
34
+ ];
35
+ const SUBMISSION_ROLES = new Set([
36
+ ...REQUIRED_SUBMISSION_ROLES,
37
+ "figure-table-index",
38
+ "extended-data",
39
+ "supplementary-methods",
40
+ ]);
27
41
  export function publicationAssessmentSchema() {
28
42
  const stringArraySchema = { type: "array", items: { type: "string", minLength: 1 } };
29
43
  return {
@@ -220,6 +234,13 @@ export function publicationReviewSchema(role) {
220
234
  export async function freezePublicationManuscript(input) {
221
235
  return withWorkspaceLock(input.root, "research.publication.freeze", async () => {
222
236
  const project = await requireClosedTopJournalProject(input.root, input.projectId);
237
+ const config = await loadWorkspaceConfig(input.root);
238
+ if (config.producer.agent !== input.producerAgent) {
239
+ throw publicationError("RESEARCH_PUBLICATION_PRODUCER_MISMATCH", "Publication generation must be frozen by the configured native producer agent family.", 3, {
240
+ configuredProducer: config.producer.agent,
241
+ requestedProducer: input.producerAgent,
242
+ });
243
+ }
223
244
  await assertScientificGateForStage(input.root, project, "close");
224
245
  const producerSessionId = requireSessionId(input.producerSessionId, "producer");
225
246
  const producerSessionSha256 = sha256Text(producerSessionId);
@@ -227,24 +248,75 @@ export async function freezePublicationManuscript(input) {
227
248
  throw publicationError("RESEARCH_PUBLICATION_REVIEW_NOT_INDEPENDENT", "A native producer session must not reuse any prior independent reviewer session.");
228
249
  }
229
250
  const assessmentValue = parsePublicationAssessment(JSON.parse(await readRegularTextFile(input.assessmentPath, "publication assessment")));
251
+ await validateSubmissionManuscript(input.manuscriptPath);
230
252
  const projectRoot = projectDirectory(input.root, project.id);
231
253
  const outputRoot = join(projectRoot, "outputs");
232
254
  const snapshotValue = await readJsonFile(join(outputRoot, "evidence-snapshot.json"), "Frozen evidence snapshot");
233
255
  const snapshotSha256 = verifiedSnapshotSha256(project, snapshotValue);
234
256
  const closureValue = await readJsonFile(join(outputRoot, "closure.json"), "Base research closure");
235
257
  assertBaseClosure(project, closureValue, snapshotSha256);
258
+ const submissionBindings = await validateSubmissionBindings(outputRoot, project.id, String(snapshotValue.snapshotId), snapshotSha256, project.publicationPolicy.resolvedPolicySha256);
236
259
  const manuscript = await storePublicationObject(input.root, project.id, input.manuscriptPath, "manuscript");
237
260
  const assessment = await storePublicationObject(input.root, project.id, input.assessmentPath, "publication-assessment");
238
261
  const supplements = [];
239
262
  for (const [index, path] of [...new Set(input.supplementPaths)].entries()) {
240
263
  supplements.push(await storePublicationObject(input.root, project.id, path, `supplement-${index + 1}`));
241
264
  }
265
+ const declaredSubmissionFiles = input.submissionFiles ?? [];
266
+ const declaredRoles = declaredSubmissionFiles.map((file) => file.role);
267
+ const missingSubmissionRoles = REQUIRED_SUBMISSION_ROLES.filter((role) => !declaredRoles.includes(role));
268
+ if (missingSubmissionRoles.length ||
269
+ new Set(declaredRoles).size !== declaredRoles.length ||
270
+ new Set(declaredSubmissionFiles.map((file) => resolve(file.path))).size !==
271
+ declaredSubmissionFiles.length ||
272
+ declaredSubmissionFiles.some((file) => !SUBMISSION_ROLES.has(file.role))) {
273
+ throw publicationError("RESEARCH_PUBLICATION_SUBMISSION_PACKAGE_INVALID", "Submission package roles must be explicit, unique, complete, and bound to distinct files.", 3, { missingSubmissionRoles });
274
+ }
275
+ const submissionFiles = [];
276
+ for (const file of declaredSubmissionFiles.sort((left, right) => left.role.localeCompare(right.role))) {
277
+ submissionFiles.push({
278
+ role: file.role,
279
+ ...(await storePublicationObject(input.root, project.id, file.path, `submission-${file.role}`)),
280
+ });
281
+ }
242
282
  const evidenceSnapshot = await storePublicationObject(input.root, project.id, join(outputRoot, "evidence-snapshot.json"), "evidence-snapshot", true);
243
283
  const baseResearch = {
244
284
  closure: await storePublicationObject(input.root, project.id, join(outputRoot, "closure.json"), "base-closure", true),
245
285
  analysis: await storePublicationObject(input.root, project.id, join(outputRoot, "analysis.json"), "analysis", true),
246
286
  report: await storePublicationObject(input.root, project.id, join(outputRoot, "report.md"), "research-report", true),
247
287
  };
288
+ const contentSnapshot = await storePublicationObject(input.root, project.id, join(outputRoot, "content-snapshot.json"), "content-snapshot", true);
289
+ const inferenceSnapshot = await storePublicationObject(input.root, project.id, join(outputRoot, "inference-snapshot.json"), "inference-snapshot", true);
290
+ const claimEvidenceGraph = await storePublicationObject(input.root, project.id, join(outputRoot, "claim-evidence-graph.json"), "claim-evidence-graph", true);
291
+ const analysisValue = submissionBindings.analysis;
292
+ const reproducibilityPath = join(outputRoot, "submission-reproducibility.json");
293
+ await writeJsonAtomic(reproducibilityPath, {
294
+ schemaVersion: 1,
295
+ kind: "tiangong-submission-reproducibility",
296
+ projectId: project.id,
297
+ analysisRun: isObject(analysisValue.analysisRun) ? analysisValue.analysisRun : null,
298
+ bindings: {
299
+ evidenceSnapshotSha256: evidenceSnapshot.sha256,
300
+ contentSnapshotSha256: contentSnapshot.sha256,
301
+ inferenceSnapshotSha256: inferenceSnapshot.sha256,
302
+ claimEvidenceGraphSha256: claimEvidenceGraph.sha256,
303
+ analysisSha256: baseResearch.analysis.sha256,
304
+ },
305
+ });
306
+ const reproducibilityManifest = await storePublicationObject(input.root, project.id, reproducibilityPath, "reproducibility-manifest", true);
307
+ const submissionPackageCore = {
308
+ schemaVersion: 1,
309
+ requiredRoles: REQUIRED_SUBMISSION_ROLES,
310
+ files: submissionFiles,
311
+ contentSnapshot,
312
+ inferenceSnapshot,
313
+ claimEvidenceGraph,
314
+ reproducibilityManifest,
315
+ };
316
+ const submissionPackage = {
317
+ ...submissionPackageCore,
318
+ packageSha256: sha256Text(canonicalJson(submissionPackageCore)),
319
+ };
248
320
  const assessmentResult = evaluateTopJournalAssessment({
249
321
  policy: project.publicationPolicy,
250
322
  evidenceSnapshot: snapshotValue,
@@ -268,6 +340,7 @@ export async function freezePublicationManuscript(input) {
268
340
  manuscript,
269
341
  assessment,
270
342
  supplements,
343
+ submissionPackage,
271
344
  assessmentResult,
272
345
  requiredReviewRoles: requiredReviewRoles(project.publicationPolicy),
273
346
  };
@@ -289,6 +362,7 @@ export async function freezePublicationManuscript(input) {
289
362
  manuscriptSha256: manuscript.sha256,
290
363
  assessmentSha256: assessment.sha256,
291
364
  evidenceSnapshotSha256: snapshotSha256,
365
+ submissionPackageSha256: submissionPackage.packageSha256,
292
366
  policySha256: project.publicationPolicy.resolvedPolicySha256,
293
367
  producerAgent: input.producerAgent,
294
368
  mechanicalIssueCodes: assessmentResult.issueCodes,
@@ -300,6 +374,16 @@ export async function preparePublicationReview(input) {
300
374
  return withWorkspaceLock(input.root, "research.publication.review.prepare", async () => {
301
375
  const project = await requireClosedTopJournalProject(input.root, input.projectId);
302
376
  const generation = await loadCurrentGeneration(input.root, project.id);
377
+ const config = await loadWorkspaceConfig(input.root);
378
+ if (input.reviewerAgent === generation.producer.agent) {
379
+ throw publicationError("RESEARCH_PUBLICATION_REVIEW_NOT_INDEPENDENT", "Publication review must use a different agent family from the native producer.");
380
+ }
381
+ if (config.reviewer.agent !== input.reviewerAgent) {
382
+ throw publicationError("RESEARCH_PUBLICATION_REVIEWER_MISMATCH", "Publication review must use the configured independent reviewer route.", 3, {
383
+ configuredReviewer: config.reviewer.agent,
384
+ requestedReviewer: input.reviewerAgent,
385
+ });
386
+ }
303
387
  const sessionId = requireSessionId(input.reviewerSessionId, "reviewer");
304
388
  if (!generation.requiredReviewRoles.includes(input.role)) {
305
389
  throw publicationError("RESEARCH_PUBLICATION_REVIEW_ROLE_INVALID", `The ${input.role} review is not declared by the approved policy.`, 2);
@@ -342,6 +426,7 @@ export async function preparePublicationReview(input) {
342
426
  manuscript: generation.manuscript,
343
427
  assessment: generation.assessment,
344
428
  supplements: generation.supplements,
429
+ submissionPackage: generation.submissionPackage,
345
430
  mechanicalAssessment: generation.assessmentResult,
346
431
  instructions: reviewInstructions(input.role),
347
432
  };
@@ -407,6 +492,11 @@ export async function inspectPublicationStatus(root, projectId) {
407
492
  projectId,
408
493
  generationSha256: null,
409
494
  manuscriptSha256: null,
495
+ submissionPackageSha256: null,
496
+ submissionRoles: [],
497
+ contentSnapshotSha256: null,
498
+ inferenceSnapshotSha256: null,
499
+ claimEvidenceGraphSha256: null,
410
500
  generationStatus: "waiting-for-base-research",
411
501
  reviewState: "not-started",
412
502
  requiredReviewRoles: requiredReviewRoles(project.publicationPolicy),
@@ -425,6 +515,11 @@ export async function inspectPublicationStatus(root, projectId) {
425
515
  projectId,
426
516
  generationSha256: null,
427
517
  manuscriptSha256: null,
518
+ submissionPackageSha256: null,
519
+ submissionRoles: [],
520
+ contentSnapshotSha256: null,
521
+ inferenceSnapshotSha256: null,
522
+ claimEvidenceGraphSha256: null,
428
523
  generationStatus: "not-started",
429
524
  reviewState: "not-started",
430
525
  requiredReviewRoles: REQUIRED_REVIEW_ROLES,
@@ -456,6 +551,11 @@ export async function inspectPublicationStatus(root, projectId) {
456
551
  projectId,
457
552
  generationSha256: generation.generationSha256,
458
553
  manuscriptSha256: generation.manuscript.sha256,
554
+ submissionPackageSha256: generation.submissionPackage.packageSha256,
555
+ submissionRoles: generation.submissionPackage.files.map((file) => file.role),
556
+ contentSnapshotSha256: generation.submissionPackage.contentSnapshot.sha256,
557
+ inferenceSnapshotSha256: generation.submissionPackage.inferenceSnapshot.sha256,
558
+ claimEvidenceGraphSha256: generation.submissionPackage.claimEvidenceGraph.sha256,
459
559
  generationStatus: "manuscript-frozen",
460
560
  reviewState,
461
561
  requiredReviewRoles: generation.requiredReviewRoles,
@@ -495,6 +595,7 @@ export async function closePublication(root, projectId) {
495
595
  manuscript: generation.manuscript,
496
596
  assessment: generation.assessment,
497
597
  supplements: generation.supplements,
598
+ submissionPackage: generation.submissionPackage,
498
599
  reviews: reviews.map((entry) => ({
499
600
  role: entry.role,
500
601
  packetSha256: entry.packet.packetSha256,
@@ -566,6 +667,197 @@ function assertBaseClosure(project, closure, snapshotSha256) {
566
667
  throw publicationError("RESEARCH_PUBLICATION_BINDING_INVALID", "The base research closure does not bind the current evidence snapshot and approved policy.");
567
668
  }
568
669
  }
670
+ async function validateSubmissionManuscript(path) {
671
+ const canonical = requireAbsolutePath(path, "manuscript");
672
+ if (![".md", ".txt"].includes(extname(canonical).toLowerCase())) {
673
+ throw publicationError("RESEARCH_PUBLICATION_MANUSCRIPT_INCOMPLETE", "The submission manuscript source must be Markdown or plain text for deterministic section validation.", 3);
674
+ }
675
+ const content = await readRegularTextFile(canonical, "submission manuscript");
676
+ const headings = [...content.matchAll(/^#{1,6}\s+(.+?)\s*$/gmu)].map((match) => (match[1] ?? "")
677
+ .trim()
678
+ .toLowerCase()
679
+ .replaceAll(/[^a-z0-9]+/gu, " ")
680
+ .trim());
681
+ const required = [
682
+ { id: "abstract", aliases: ["abstract"] },
683
+ { id: "introduction", aliases: ["introduction"] },
684
+ { id: "methods", aliases: ["methods", "materials and methods"] },
685
+ { id: "results", aliases: ["results"] },
686
+ { id: "discussion", aliases: ["discussion"] },
687
+ { id: "data-availability", aliases: ["data availability", "data availability statement"] },
688
+ { id: "code-availability", aliases: ["code availability", "code availability statement"] },
689
+ { id: "references", aliases: ["references", "bibliography"] },
690
+ ];
691
+ const missingSections = required
692
+ .filter((section) => !section.aliases.some((alias) => headings.includes(alias)))
693
+ .map((section) => section.id);
694
+ if (missingSections.length) {
695
+ throw publicationError("RESEARCH_PUBLICATION_MANUSCRIPT_INCOMPLETE", "The manuscript is missing required submission sections.", 3, { missingSections });
696
+ }
697
+ }
698
+ async function validateSubmissionBindings(outputRoot, projectId, evidenceSnapshotId, evidenceSnapshotSha256, policySha256) {
699
+ const [content, inference, analysis, graph] = await Promise.all([
700
+ readJsonFile(join(outputRoot, "content-snapshot.json"), "Frozen content snapshot"),
701
+ readJsonFile(join(outputRoot, "inference-snapshot.json"), "Frozen inference snapshot"),
702
+ readJsonFile(join(outputRoot, "analysis.json"), "Frozen analysis"),
703
+ readJsonFile(join(outputRoot, "claim-evidence-graph.json"), "Frozen Claim-Evidence Graph"),
704
+ ]);
705
+ const contentGate = isObject(content.gate) ? content.gate : {};
706
+ const inferenceGate = isObject(inference.gate) ? inference.gate : {};
707
+ const acquisition = isObject(inference.acquisitionSnapshot) ? inference.acquisitionSnapshot : {};
708
+ const contentBinding = isObject(inference.contentSnapshot) ? inference.contentSnapshot : {};
709
+ const graphNodes = Array.isArray(graph.nodes) ? graph.nodes.filter(isObject) : [];
710
+ const graphEdges = Array.isArray(graph.edges) ? graph.edges.filter(isObject) : [];
711
+ const contentAtoms = Array.isArray(content.atoms) ? content.atoms.filter(isObject) : [];
712
+ const inferenceAtoms = Array.isArray(inference.atoms) ? inference.atoms.filter(isObject) : [];
713
+ const inferenceSources = Array.isArray(inference.sources)
714
+ ? inference.sources.filter(isObject)
715
+ : [];
716
+ const inferenceClaims = Array.isArray(inference.claims) ? inference.claims.filter(isObject) : [];
717
+ const findings = Array.isArray(analysis.findings) ? analysis.findings.filter(isObject) : [];
718
+ const analysisRun = isObject(analysis.analysisRun) ? analysis.analysisRun : {};
719
+ const nodeById = new Map(graphNodes.flatMap((node) => typeof node.id === "string"
720
+ ? [[node.id, node]]
721
+ : []));
722
+ const edgeBindings = new Set(graphEdges.flatMap((edge) => typeof edge.type === "string" && typeof edge.from === "string" && typeof edge.to === "string"
723
+ ? [`${edge.type}:${edge.from}:${edge.to}`]
724
+ : []));
725
+ if (!hashBoundSnapshot(content, "snapshotSha256") ||
726
+ !hashBoundSnapshot(inference, "snapshotSha256") ||
727
+ !hashBoundSnapshot(graph, "graphSha256") ||
728
+ content.kind !== "tiangong-evidence-content-snapshot" ||
729
+ content.projectId !== projectId ||
730
+ content.acquisitionSnapshotId !== evidenceSnapshotId ||
731
+ content.acquisitionSnapshotSha256 !== evidenceSnapshotSha256 ||
732
+ contentGate.decision !== "pass" ||
733
+ inference.kind !== "tiangong-inference-snapshot" ||
734
+ inference.projectId !== projectId ||
735
+ inferenceGate.decision !== "pass" ||
736
+ acquisition.snapshotId !== evidenceSnapshotId ||
737
+ acquisition.snapshotSha256 !== evidenceSnapshotSha256 ||
738
+ contentBinding.snapshotId !== content.snapshotId ||
739
+ contentBinding.snapshotSha256 !== content.snapshotSha256 ||
740
+ canonicalJson(inferenceAtoms) !== canonicalJson(contentAtoms) ||
741
+ inference.policySha256 !== policySha256 ||
742
+ analysis.schemaVersion !== 2 ||
743
+ analysis.inferenceSnapshotSha256 !== inference.snapshotSha256 ||
744
+ analysisRun.status !== "reproduced" ||
745
+ typeof analysisRun.id !== "string" ||
746
+ findings.length === 0 ||
747
+ graph.kind !== "tiangong-claim-evidence-graph" ||
748
+ graph.projectId !== projectId ||
749
+ graph.inferenceSnapshotSha256 !== inference.snapshotSha256 ||
750
+ graph.analysisSha256 !== (await sha256File(join(outputRoot, "analysis.json"))) ||
751
+ graph.analysisRunId !== analysisRun.id ||
752
+ nodeById.size !== graphNodes.length ||
753
+ new Set(graphEdges.flatMap((edge) => (typeof edge.id === "string" ? [edge.id] : []))).size !==
754
+ graphEdges.length ||
755
+ graphEdges.some((edge) => typeof edge.from !== "string" ||
756
+ typeof edge.to !== "string" ||
757
+ !nodeById.has(edge.from) ||
758
+ !nodeById.has(edge.to)) ||
759
+ !validClaimEvidenceTopology({
760
+ findings,
761
+ analysisRunId: analysisRun.id,
762
+ atoms: inferenceAtoms,
763
+ sources: inferenceSources,
764
+ claims: inferenceClaims,
765
+ nodeById,
766
+ edgeBindings,
767
+ })) {
768
+ throw publicationError("RESEARCH_PUBLICATION_SUBMISSION_BINDING_INVALID", "The submission package does not bind passing content/inference gates, a reproduced analysis, and a complete Claim–Evidence Graph.", 3);
769
+ }
770
+ return { analysis };
771
+ }
772
+ function hashBoundSnapshot(value, hashKey) {
773
+ const recorded = value[hashKey];
774
+ if (typeof recorded !== "string" || !/^[a-f0-9]{64}$/u.test(recorded))
775
+ return false;
776
+ const core = { ...value };
777
+ delete core[hashKey];
778
+ return sha256Text(canonicalJson(core)) === recorded;
779
+ }
780
+ function validClaimEvidenceTopology(input) {
781
+ const atomById = objectMap(input.atoms, "atomId");
782
+ const sourceById = objectMap(input.sources, "id");
783
+ const claimById = objectMap(input.claims, "id");
784
+ const runNodeId = `analysis-run:${input.analysisRunId}`;
785
+ const runNode = input.nodeById.get(runNodeId);
786
+ if (!runNode || runNode.type !== "analysis-run" || runNode.label !== input.analysisRunId) {
787
+ return false;
788
+ }
789
+ for (const finding of input.findings) {
790
+ if (typeof finding.id !== "string" ||
791
+ typeof finding.statement !== "string" ||
792
+ !nonEmptyStringArray(finding.evidence) ||
793
+ !nonEmptyStringArray(finding.evidenceAtomIds) ||
794
+ !nonEmptyStringArray(finding.claimIds)) {
795
+ return false;
796
+ }
797
+ const findingNodeId = `finding:${finding.id}`;
798
+ const findingNode = input.nodeById.get(findingNodeId);
799
+ if (!findingNode ||
800
+ findingNode.type !== "finding" ||
801
+ findingNode.label !== finding.statement ||
802
+ findingNode.sha256 !== sha256Text(canonicalJson(finding)) ||
803
+ !input.edgeBindings.has(`finding-produced-by-analysis-run:${findingNodeId}:${runNodeId}`)) {
804
+ return false;
805
+ }
806
+ const atomSourceIds = new Set();
807
+ for (const atomId of finding.evidenceAtomIds) {
808
+ const atom = atomById.get(atomId);
809
+ if (!atom ||
810
+ typeof atom.sourceId !== "string" ||
811
+ typeof atom.statement !== "string" ||
812
+ typeof atom.atomSha256 !== "string") {
813
+ return false;
814
+ }
815
+ const source = sourceById.get(atom.sourceId);
816
+ const atomNodeId = `atom:${atomId}`;
817
+ const sourceNodeId = `source:${atom.sourceId}`;
818
+ const atomNode = input.nodeById.get(atomNodeId);
819
+ const sourceNode = input.nodeById.get(sourceNodeId);
820
+ if (!source ||
821
+ !atomNode ||
822
+ atomNode.type !== "atom" ||
823
+ atomNode.label !== atom.statement ||
824
+ atomNode.sha256 !== atom.atomSha256 ||
825
+ !sourceNode ||
826
+ sourceNode.type !== "source" ||
827
+ sourceNode.label !== atom.sourceId ||
828
+ sourceNode.sha256 !== sha256Text(canonicalJson(source)) ||
829
+ !input.edgeBindings.has(`finding-supported-by-atom:${findingNodeId}:${atomNodeId}`) ||
830
+ !input.edgeBindings.has(`atom-derived-from-source:${atomNodeId}:${sourceNodeId}`)) {
831
+ return false;
832
+ }
833
+ atomSourceIds.add(atom.sourceId);
834
+ }
835
+ if (finding.evidence.some((sourceId) => !atomSourceIds.has(sourceId)))
836
+ return false;
837
+ for (const claimId of finding.claimIds) {
838
+ const claim = claimById.get(claimId);
839
+ const claimNodeId = `design-claim:${claimId}`;
840
+ const claimNode = input.nodeById.get(claimNodeId);
841
+ if (!claim ||
842
+ !claimNode ||
843
+ claimNode.type !== "design-claim" ||
844
+ claimNode.label !== (typeof claim.statement === "string" ? claim.statement : claimId) ||
845
+ claimNode.sha256 !== sha256Text(canonicalJson(claim)) ||
846
+ !input.edgeBindings.has(`finding-addresses-design-claim:${findingNodeId}:${claimNodeId}`)) {
847
+ return false;
848
+ }
849
+ }
850
+ }
851
+ return true;
852
+ }
853
+ function objectMap(values, idKey) {
854
+ return new Map(values.flatMap((value) => typeof value[idKey] === "string"
855
+ ? [[value[idKey], value]]
856
+ : []));
857
+ }
858
+ function nonEmptyStringArray(value) {
859
+ return (Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string"));
860
+ }
569
861
  function policySummary(policy) {
570
862
  return {
571
863
  projectId: policy.projectId,
@@ -626,13 +918,27 @@ async function loadCurrentGeneration(root, projectId) {
626
918
  if (generation.kind !== "tiangong-publication-generation" ||
627
919
  generation.projectId !== projectId ||
628
920
  !isObject(generation.producer) ||
629
- !["codex", "claude"].includes(String(generation.producer.agent)) ||
921
+ !["codex", "claude", "workbuddy", "codebuddy"].includes(String(generation.producer.agent)) ||
630
922
  typeof generation.producer.sessionSha256 !== "string" ||
631
923
  !/^[a-f0-9]{64}$/.test(generation.producer.sessionSha256) ||
632
924
  generationSha256 !== pointer.generationSha256 ||
633
925
  sha256Text(canonicalJson(withoutHash)) !== generationSha256) {
634
926
  throw publicationError("RESEARCH_PUBLICATION_STATE_INVALID", "The publication generation failed its content hash binding.");
635
927
  }
928
+ if (!isObject(generation.submissionPackage) ||
929
+ generation.submissionPackage.schemaVersion !== 1 ||
930
+ !Array.isArray(generation.submissionPackage.requiredRoles) ||
931
+ !Array.isArray(generation.submissionPackage.files) ||
932
+ generation.submissionPackage.requiredRoles.some((role) => !REQUIRED_SUBMISSION_ROLES.includes(role)) ||
933
+ REQUIRED_SUBMISSION_ROLES.some((role) => !generation.submissionPackage.requiredRoles.includes(role)) ||
934
+ generation.submissionPackage.files.some((file) => !isObject(file) || !SUBMISSION_ROLES.has(file.role)) ||
935
+ typeof generation.submissionPackage.packageSha256 !== "string") {
936
+ throw publicationError("RESEARCH_PUBLICATION_STATE_INVALID", "The publication submission package is invalid.");
937
+ }
938
+ const { packageSha256, ...submissionPackageCore } = generation.submissionPackage;
939
+ if (sha256Text(canonicalJson(submissionPackageCore)) !== packageSha256) {
940
+ throw publicationError("RESEARCH_PUBLICATION_STATE_INVALID", "The publication submission package hash binding is invalid.");
941
+ }
636
942
  await verifyFrozenFiles(root, projectId, [
637
943
  generation.manuscript,
638
944
  generation.assessment,
@@ -641,6 +947,11 @@ async function loadCurrentGeneration(root, projectId) {
641
947
  generation.baseResearch.analysis,
642
948
  generation.baseResearch.report,
643
949
  ...generation.supplements,
950
+ ...generation.submissionPackage.files,
951
+ generation.submissionPackage.contentSnapshot,
952
+ generation.submissionPackage.inferenceSnapshot,
953
+ generation.submissionPackage.claimEvidenceGraph,
954
+ generation.submissionPackage.reproducibilityManifest,
644
955
  ]);
645
956
  return generation;
646
957
  }
@@ -890,7 +1201,7 @@ function boundedStatement(verdict) {
890
1201
  }
891
1202
  function reviewInstructions(role) {
892
1203
  return [
893
- "Review only the exact content-addressed manuscript, supplements, evidence snapshot, base research outputs, and policy in this packet.",
1204
+ "Review only the exact content-addressed manuscript, role-complete submission package, evidence and inference snapshots, Claim–Evidence Graph, reproducibility manifest, base research outputs, and policy in this packet.",
894
1205
  "Use a fresh independent reviewer session; do not inherit producer reasoning or an earlier manuscript review.",
895
1206
  "Do not upgrade the mechanical assessment or policy verdict ceiling.",
896
1207
  role === "journal-editor"