@tiangong-ai/cli 0.0.41 → 0.0.43
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.
- package/AGENTS.md +2 -2
- package/README.md +46 -19
- package/dist/research/orchestration.js +198 -7
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/workspace/acquisition.d.ts +7 -0
- package/dist/research/workspace/acquisition.js +37 -10
- package/dist/research/workspace/acquisition.js.map +1 -1
- package/dist/research/workspace/audit-bundle.d.ts +18 -0
- package/dist/research/workspace/audit-bundle.js +99 -3
- package/dist/research/workspace/audit-bundle.js.map +1 -1
- package/dist/research/workspace/content-evidence.d.ts +98 -0
- package/dist/research/workspace/content-evidence.js +715 -0
- package/dist/research/workspace/content-evidence.js.map +1 -0
- package/dist/research/workspace/evidence-ledger.d.ts +1 -1
- package/dist/research/workspace/evidence-ledger.js +2 -0
- package/dist/research/workspace/evidence-ledger.js.map +1 -1
- package/dist/research/workspace/inference.d.ts +69 -0
- package/dist/research/workspace/inference.js +360 -0
- package/dist/research/workspace/inference.js.map +1 -0
- package/dist/research/workspace/native-activity.js +14 -5
- package/dist/research/workspace/native-activity.js.map +1 -1
- package/dist/research/workspace/projects.js +16 -0
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/publication-workflow.d.ts +26 -0
- package/dist/research/workspace/publication-workflow.js +313 -2
- package/dist/research/workspace/publication-workflow.js.map +1 -1
- package/dist/research/workspace/runtime.js +131 -30
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/schemas.js +83 -4
- package/dist/research/workspace/schemas.js.map +1 -1
- package/dist/research/workspace/scientific-review.js +57 -0
- package/dist/research/workspace/scientific-review.js.map +1 -1
- package/dist/research/workspace/setup-catalog.js +3 -3
- package/dist/research/workspace/setup.js +1 -1
- package/dist/research/workspace/setup.js.map +1 -1
- 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,
|
|
@@ -633,6 +925,20 @@ async function loadCurrentGeneration(root, projectId) {
|
|
|
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,
|
|
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"
|