@tiangong-ai/cli 0.0.34 → 0.0.36

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 (49) hide show
  1. package/AGENTS.md +18 -6
  2. package/README.md +147 -2
  3. package/dist/research/orchestration.js +651 -24
  4. package/dist/research/orchestration.js.map +1 -1
  5. package/dist/research/workspace/audit-bundle.d.ts +44 -0
  6. package/dist/research/workspace/audit-bundle.js +357 -0
  7. package/dist/research/workspace/audit-bundle.js.map +1 -0
  8. package/dist/research/workspace/input-plan.js +27 -1
  9. package/dist/research/workspace/input-plan.js.map +1 -1
  10. package/dist/research/workspace/preflight.d.ts +34 -2
  11. package/dist/research/workspace/preflight.js +145 -1
  12. package/dist/research/workspace/preflight.js.map +1 -1
  13. package/dist/research/workspace/projects.d.ts +32 -5
  14. package/dist/research/workspace/projects.js +263 -7
  15. package/dist/research/workspace/projects.js.map +1 -1
  16. package/dist/research/workspace/publication-workflow.d.ts +146 -0
  17. package/dist/research/workspace/publication-workflow.js +994 -0
  18. package/dist/research/workspace/publication-workflow.js.map +1 -0
  19. package/dist/research/workspace/publication.d.ts +110 -0
  20. package/dist/research/workspace/publication.js +246 -0
  21. package/dist/research/workspace/publication.js.map +1 -0
  22. package/dist/research/workspace/research-policy-wizard.d.ts +40 -0
  23. package/dist/research/workspace/research-policy-wizard.js +167 -0
  24. package/dist/research/workspace/research-policy-wizard.js.map +1 -0
  25. package/dist/research/workspace/research-policy.d.ts +70 -0
  26. package/dist/research/workspace/research-policy.js +886 -0
  27. package/dist/research/workspace/research-policy.js.map +1 -0
  28. package/dist/research/workspace/runtime.d.ts +17 -0
  29. package/dist/research/workspace/runtime.js +84 -2
  30. package/dist/research/workspace/runtime.js.map +1 -1
  31. package/dist/research/workspace/sanitization.js +9 -3
  32. package/dist/research/workspace/sanitization.js.map +1 -1
  33. package/dist/research/workspace/scientific-design.d.ts +336 -0
  34. package/dist/research/workspace/scientific-design.js +1845 -0
  35. package/dist/research/workspace/scientific-design.js.map +1 -0
  36. package/dist/research/workspace/scientific-review.d.ts +101 -0
  37. package/dist/research/workspace/scientific-review.js +1167 -0
  38. package/dist/research/workspace/scientific-review.js.map +1 -0
  39. package/dist/research/workspace/setup-catalog.js +2 -2
  40. package/dist/research/workspace/setup-wizard.d.ts +18 -1
  41. package/dist/research/workspace/setup-wizard.js +1 -1
  42. package/dist/research/workspace/setup-wizard.js.map +1 -1
  43. package/dist/research/workspace/setup.d.ts +1 -0
  44. package/dist/research/workspace/setup.js +64 -0
  45. package/dist/research/workspace/setup.js.map +1 -1
  46. package/dist/research/workspace/types.d.ts +58 -0
  47. package/dist/research/workspace/workspace.js +25 -7
  48. package/dist/research/workspace/workspace.js.map +1 -1
  49. package/package.json +4 -2
@@ -0,0 +1,994 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmod, lstat, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
4
+ import { CliError } from "../../errors.js";
5
+ import { RESEARCH_CONTROL_DIRECTORY } from "./constants.js";
6
+ import { appendJournalEvent, readJournal, verifyJournal } from "./journal.js";
7
+ import { evaluateTopJournalAssessment, } from "./publication.js";
8
+ import { loadProject } from "./projects.js";
9
+ import { assertResearchPolicyBinding } from "./research-policy.js";
10
+ import { assertScientificGateForStage } from "./scientific-review.js";
11
+ import { canonicalJson, ensureDirectory, isObject, pathExists, readJsonFile, sha256File, sha256Text, workspacePaths, writeJsonAtomic, } from "./storage.js";
12
+ import { withWorkspaceLock } from "./workspace.js";
13
+ const REQUIRED_REVIEW_ROLES = [
14
+ "evidence",
15
+ "methods-reproducibility",
16
+ "domain-novelty",
17
+ "journal-editor",
18
+ ];
19
+ const SPECIALIST_DECISIONS = new Set(["pass", "revise", "reject"]);
20
+ const EDITOR_DECISIONS = new Set([
21
+ "submission-ready",
22
+ "minor-revision",
23
+ "major-revision",
24
+ "reject-and-redesign",
25
+ "desk-reject",
26
+ ]);
27
+ export function publicationAssessmentSchema() {
28
+ const stringArraySchema = { type: "array", items: { type: "string", minLength: 1 } };
29
+ return {
30
+ $schema: "https://json-schema.org/draft/2020-12/schema",
31
+ title: "Tiangong top-journal publication assessment",
32
+ type: "object",
33
+ additionalProperties: false,
34
+ required: [
35
+ "schemaVersion",
36
+ "title",
37
+ "claims",
38
+ "outcomes",
39
+ "titleOutcomeIds",
40
+ "results",
41
+ "sourceClassifications",
42
+ "recallAudit",
43
+ ],
44
+ properties: {
45
+ schemaVersion: { const: 1 },
46
+ title: { type: "string", minLength: 8 },
47
+ claims: {
48
+ type: "array",
49
+ items: {
50
+ type: "object",
51
+ additionalProperties: false,
52
+ required: ["id", "role", "statement", "evidenceSourceIds", "dimensionIds", "resultIds"],
53
+ properties: {
54
+ id: { type: "string", minLength: 1 },
55
+ role: { enum: ["central", "supporting", "contextual", "future-research"] },
56
+ statement: { type: "string", minLength: 1 },
57
+ evidenceSourceIds: stringArraySchema,
58
+ dimensionIds: stringArraySchema,
59
+ resultIds: stringArraySchema,
60
+ },
61
+ },
62
+ },
63
+ outcomes: {
64
+ type: "array",
65
+ items: {
66
+ type: "object",
67
+ additionalProperties: false,
68
+ required: ["id", "role", "label", "supportStatus", "claimIds", "resultIds"],
69
+ properties: {
70
+ id: { type: "string", minLength: 1 },
71
+ role: { enum: ["central", "supporting", "contextual"] },
72
+ label: { type: "string", minLength: 1 },
73
+ supportStatus: {
74
+ enum: [
75
+ "unobserved",
76
+ "future-work",
77
+ "conceptual-proposition",
78
+ "calibrated-model",
79
+ "causal-estimate",
80
+ "field-observation",
81
+ "validated-forecast",
82
+ "systematic-synthesis",
83
+ ],
84
+ },
85
+ claimIds: stringArraySchema,
86
+ resultIds: stringArraySchema,
87
+ },
88
+ },
89
+ },
90
+ titleOutcomeIds: stringArraySchema,
91
+ results: {
92
+ type: "array",
93
+ items: {
94
+ type: "object",
95
+ additionalProperties: false,
96
+ required: [
97
+ "id",
98
+ "role",
99
+ "resultClass",
100
+ "statement",
101
+ "evidenceSourceIds",
102
+ "independentlyReproduced",
103
+ ],
104
+ properties: {
105
+ id: { type: "string", minLength: 1 },
106
+ role: { enum: ["central", "supporting", "contextual"] },
107
+ resultClass: {
108
+ enum: [
109
+ "definition",
110
+ "accounting-identity",
111
+ "illustrative-sensitivity",
112
+ "calibrated-model",
113
+ "causal-estimate",
114
+ "field-observation",
115
+ "validated-forecast",
116
+ "systematic-synthesis",
117
+ "conceptual-proposition",
118
+ ],
119
+ },
120
+ statement: { type: "string", minLength: 1 },
121
+ evidenceSourceIds: stringArraySchema,
122
+ independentlyReproduced: { type: "boolean" },
123
+ },
124
+ },
125
+ },
126
+ sourceClassifications: {
127
+ type: "array",
128
+ items: {
129
+ type: "object",
130
+ additionalProperties: false,
131
+ required: ["sourceId", "relationship", "evidenceKind"],
132
+ properties: {
133
+ sourceId: { type: "string", minLength: 1 },
134
+ relationship: { enum: ["direct", "adjacent", "contextual"] },
135
+ evidenceKind: {
136
+ enum: [
137
+ "peer-reviewed-empirical",
138
+ "peer-reviewed-model",
139
+ "peer-reviewed-review",
140
+ "official-data",
141
+ "administrative-record",
142
+ "patent",
143
+ "news",
144
+ "owner-provided-input",
145
+ "internal-model",
146
+ "other",
147
+ ],
148
+ },
149
+ },
150
+ },
151
+ },
152
+ recallAudit: {
153
+ type: "object",
154
+ additionalProperties: false,
155
+ required: [
156
+ "status",
157
+ "candidateDispositionComplete",
158
+ "databaseCoverageComplete",
159
+ "backwardCitationChasing",
160
+ "forwardCitationChasing",
161
+ "adversarialSearch",
162
+ "closestPriorWorkCompared",
163
+ "missingCoreWorkIds",
164
+ ],
165
+ properties: {
166
+ status: { enum: ["pass", "fail", "incomplete"] },
167
+ candidateDispositionComplete: { type: "boolean" },
168
+ databaseCoverageComplete: { type: "boolean" },
169
+ backwardCitationChasing: { type: "boolean" },
170
+ forwardCitationChasing: { type: "boolean" },
171
+ adversarialSearch: { type: "boolean" },
172
+ closestPriorWorkCompared: { type: "boolean" },
173
+ missingCoreWorkIds: stringArraySchema,
174
+ },
175
+ },
176
+ },
177
+ };
178
+ }
179
+ export function publicationReviewSchema(role) {
180
+ return {
181
+ $schema: "https://json-schema.org/draft/2020-12/schema",
182
+ title: `Tiangong ${role} publication review`,
183
+ type: "object",
184
+ additionalProperties: false,
185
+ required: [
186
+ "schemaVersion",
187
+ "role",
188
+ "packetSha256",
189
+ "reviewerSessionSha256",
190
+ "decision",
191
+ "findings",
192
+ "boundedRecommendation",
193
+ ],
194
+ properties: {
195
+ schemaVersion: { const: 1 },
196
+ role: { const: role },
197
+ packetSha256: { type: "string", pattern: "^[a-f0-9]{64}$" },
198
+ reviewerSessionSha256: { type: "string", pattern: "^[a-f0-9]{64}$" },
199
+ decision: {
200
+ enum: role === "journal-editor" ? [...EDITOR_DECISIONS] : [...SPECIALIST_DECISIONS],
201
+ },
202
+ findings: {
203
+ type: "array",
204
+ items: {
205
+ type: "object",
206
+ additionalProperties: false,
207
+ required: ["code", "severity", "message", "evidenceIds"],
208
+ properties: {
209
+ code: { type: "string", pattern: "^[A-Z][A-Z0-9_]{2,63}$" },
210
+ severity: { enum: ["blocking", "major", "minor"] },
211
+ message: { type: "string", minLength: 1 },
212
+ evidenceIds: { type: "array", items: { type: "string", minLength: 1 } },
213
+ },
214
+ },
215
+ },
216
+ boundedRecommendation: { type: "string", minLength: 8, maxLength: 4_000 },
217
+ },
218
+ };
219
+ }
220
+ export async function freezePublicationManuscript(input) {
221
+ return withWorkspaceLock(input.root, "research.publication.freeze", async () => {
222
+ const project = await requireClosedTopJournalProject(input.root, input.projectId);
223
+ await assertScientificGateForStage(input.root, project, "close");
224
+ const producerSessionId = requireSessionId(input.producerSessionId, "producer");
225
+ const producerSessionSha256 = sha256Text(producerSessionId);
226
+ if ((await usedReviewerSessionHashes(input.root, project.id)).has(producerSessionSha256)) {
227
+ throw publicationError("RESEARCH_PUBLICATION_REVIEW_NOT_INDEPENDENT", "A native producer session must not reuse any prior independent reviewer session.");
228
+ }
229
+ const assessmentValue = parsePublicationAssessment(JSON.parse(await readRegularTextFile(input.assessmentPath, "publication assessment")));
230
+ const projectRoot = projectDirectory(input.root, project.id);
231
+ const outputRoot = join(projectRoot, "outputs");
232
+ const snapshotValue = await readJsonFile(join(outputRoot, "evidence-snapshot.json"), "Frozen evidence snapshot");
233
+ const snapshotSha256 = verifiedSnapshotSha256(project, snapshotValue);
234
+ const closureValue = await readJsonFile(join(outputRoot, "closure.json"), "Base research closure");
235
+ assertBaseClosure(project, closureValue, snapshotSha256);
236
+ const manuscript = await storePublicationObject(input.root, project.id, input.manuscriptPath, "manuscript");
237
+ const assessment = await storePublicationObject(input.root, project.id, input.assessmentPath, "publication-assessment");
238
+ const supplements = [];
239
+ for (const [index, path] of [...new Set(input.supplementPaths)].entries()) {
240
+ supplements.push(await storePublicationObject(input.root, project.id, path, `supplement-${index + 1}`));
241
+ }
242
+ const evidenceSnapshot = await storePublicationObject(input.root, project.id, join(outputRoot, "evidence-snapshot.json"), "evidence-snapshot", true);
243
+ const baseResearch = {
244
+ closure: await storePublicationObject(input.root, project.id, join(outputRoot, "closure.json"), "base-closure", true),
245
+ analysis: await storePublicationObject(input.root, project.id, join(outputRoot, "analysis.json"), "analysis", true),
246
+ report: await storePublicationObject(input.root, project.id, join(outputRoot, "report.md"), "research-report", true),
247
+ };
248
+ const assessmentResult = evaluateTopJournalAssessment({
249
+ policy: project.publicationPolicy,
250
+ evidenceSnapshot: snapshotValue,
251
+ inputs: project.inputs,
252
+ assessment: assessmentValue,
253
+ });
254
+ const frozenAt = new Date().toISOString();
255
+ const generationCore = {
256
+ schemaVersion: 1,
257
+ kind: "tiangong-publication-generation",
258
+ projectId: project.id,
259
+ frozenAt,
260
+ producer: { agent: input.producerAgent, sessionSha256: producerSessionSha256 },
261
+ policy: policySummary(project.publicationPolicy),
262
+ evidenceSnapshot: {
263
+ id: String(snapshotValue.snapshotId),
264
+ sha256: snapshotSha256,
265
+ object: evidenceSnapshot,
266
+ },
267
+ baseResearch,
268
+ manuscript,
269
+ assessment,
270
+ supplements,
271
+ assessmentResult,
272
+ requiredReviewRoles: requiredReviewRoles(project.publicationPolicy),
273
+ };
274
+ const generationSha256 = sha256Text(canonicalJson(generationCore));
275
+ const generation = { ...generationCore, generationSha256 };
276
+ const manifestLocator = generationManifestLocator(generationSha256);
277
+ await writeImmutableJson(join(projectRoot, manifestLocator), generation, generationSha256, "publication generation");
278
+ const pointer = {
279
+ schemaVersion: 1,
280
+ projectId: project.id,
281
+ generationSha256,
282
+ manifestLocator,
283
+ updatedAt: frozenAt,
284
+ };
285
+ await writeJsonAtomic(publicationCurrentPath(input.root, project.id), pointer);
286
+ await appendJournalEvent(workspacePaths(input.root).journal, "publication.manuscript.frozen", project.id, {
287
+ projectId: project.id,
288
+ generationSha256,
289
+ manuscriptSha256: manuscript.sha256,
290
+ assessmentSha256: assessment.sha256,
291
+ evidenceSnapshotSha256: snapshotSha256,
292
+ policySha256: project.publicationPolicy.resolvedPolicySha256,
293
+ producerAgent: input.producerAgent,
294
+ mechanicalIssueCodes: assessmentResult.issueCodes,
295
+ });
296
+ return { ...generation, status: "manuscript-frozen" };
297
+ });
298
+ }
299
+ export async function preparePublicationReview(input) {
300
+ return withWorkspaceLock(input.root, "research.publication.review.prepare", async () => {
301
+ const project = await requireClosedTopJournalProject(input.root, input.projectId);
302
+ const generation = await loadCurrentGeneration(input.root, project.id);
303
+ const sessionId = requireSessionId(input.reviewerSessionId, "reviewer");
304
+ if (!generation.requiredReviewRoles.includes(input.role)) {
305
+ throw publicationError("RESEARCH_PUBLICATION_REVIEW_ROLE_INVALID", `The ${input.role} review is not declared by the approved policy.`, 2);
306
+ }
307
+ const sessionSha256 = sha256Text(sessionId);
308
+ if (sessionSha256 === generation.producer.sessionSha256) {
309
+ throw publicationError("RESEARCH_PUBLICATION_REVIEW_NOT_INDEPENDENT", "A reviewer session must differ from the native producer session.");
310
+ }
311
+ const registry = await loadReviewerRegistry(input.root, project.id);
312
+ const usedSessions = await usedReviewerSessionHashes(input.root, project.id);
313
+ if (usedSessions.has(sessionSha256) ||
314
+ registry.sessions.some((entry) => entry.sessionSha256 === sessionSha256)) {
315
+ throw publicationError("RESEARCH_PUBLICATION_REVIEW_NOT_INDEPENDENT", "Each required review must use a fresh independent reviewer session.");
316
+ }
317
+ const packetPath = reviewPacketPath(input.root, project.id, generation.generationSha256, input.role);
318
+ if (await pathExists(packetPath)) {
319
+ throw publicationError("RESEARCH_PUBLICATION_REVIEW_ALREADY_PREPARED", `The ${input.role} review packet is already prepared for this frozen generation.`);
320
+ }
321
+ const preparedAt = new Date().toISOString();
322
+ const packetCore = {
323
+ schemaVersion: 1,
324
+ kind: "tiangong-publication-review-packet",
325
+ projectId: project.id,
326
+ generationSha256: generation.generationSha256,
327
+ role: input.role,
328
+ reviewer: { agent: input.reviewerAgent, sessionSha256 },
329
+ preparedAt,
330
+ policy: {
331
+ ...generation.policy,
332
+ documents: project.publicationPolicy.documents,
333
+ resolvedRules: project.publicationPolicy.resolvedRules,
334
+ resolvedConstraints: project.publicationPolicy.resolvedConstraints,
335
+ },
336
+ evidenceSnapshot: {
337
+ id: generation.evidenceSnapshot.id,
338
+ sha256: generation.evidenceSnapshot.sha256,
339
+ objectLocator: generation.evidenceSnapshot.object.objectLocator,
340
+ },
341
+ baseResearch: generation.baseResearch,
342
+ manuscript: generation.manuscript,
343
+ assessment: generation.assessment,
344
+ supplements: generation.supplements,
345
+ mechanicalAssessment: generation.assessmentResult,
346
+ instructions: reviewInstructions(input.role),
347
+ };
348
+ const packet = {
349
+ ...packetCore,
350
+ packetSha256: sha256Text(canonicalJson(packetCore)),
351
+ };
352
+ await writeImmutableJson(packetPath, packet, packet.packetSha256, "publication review packet");
353
+ registry.sessions.push({
354
+ sessionSha256,
355
+ projectId: project.id,
356
+ generationSha256: generation.generationSha256,
357
+ role: input.role,
358
+ agent: input.reviewerAgent,
359
+ registeredAt: preparedAt,
360
+ });
361
+ registry.sessions.sort((left, right) => left.sessionSha256.localeCompare(right.sessionSha256));
362
+ await writeJsonAtomic(reviewerRegistryPath(input.root, project.id), registry);
363
+ await appendJournalEvent(workspacePaths(input.root).journal, "publication.review.prepared", project.id, {
364
+ projectId: project.id,
365
+ generationSha256: generation.generationSha256,
366
+ role: input.role,
367
+ reviewerAgent: input.reviewerAgent,
368
+ reviewerSessionSha256: sessionSha256,
369
+ packetSha256: packet.packetSha256,
370
+ });
371
+ return packet;
372
+ });
373
+ }
374
+ export async function submitPublicationReview(input) {
375
+ return withWorkspaceLock(input.root, "research.publication.review.submit", async () => {
376
+ await requireClosedTopJournalProject(input.root, input.projectId);
377
+ const generation = await loadCurrentGeneration(input.root, input.projectId);
378
+ const packet = await loadReviewPacket(input.root, input.projectId, generation, input.role);
379
+ const review = parsePublicationReview(JSON.parse(await readRegularTextFile(input.reviewPath, "publication review")), input.role);
380
+ if (review.packetSha256 !== packet.packetSha256 ||
381
+ review.reviewerSessionSha256 !== packet.reviewer.sessionSha256) {
382
+ throw publicationError("RESEARCH_PUBLICATION_REVIEW_BINDING_INVALID", "The submitted review does not bind the prepared packet and reviewer session.");
383
+ }
384
+ const path = submittedReviewPath(input.root, input.projectId, generation.generationSha256, input.role);
385
+ if (await pathExists(path)) {
386
+ throw publicationError("RESEARCH_PUBLICATION_REVIEW_ALREADY_SUBMITTED", `The ${input.role} review is already submitted for this frozen generation.`);
387
+ }
388
+ const reviewSha256 = sha256Text(canonicalJson(review));
389
+ await writeImmutableJson(path, review, reviewSha256, "publication review");
390
+ await appendJournalEvent(workspacePaths(input.root).journal, "publication.review.submitted", input.projectId, {
391
+ projectId: input.projectId,
392
+ generationSha256: generation.generationSha256,
393
+ role: input.role,
394
+ packetSha256: packet.packetSha256,
395
+ reviewSha256,
396
+ decision: review.decision,
397
+ });
398
+ return { role: input.role, reviewSha256, decision: review.decision };
399
+ });
400
+ }
401
+ export async function inspectPublicationStatus(root, projectId) {
402
+ const project = await requireTopJournalProject(root, projectId);
403
+ if (project.status !== "complete" ||
404
+ project.packages.some((item) => item.status !== "complete")) {
405
+ return {
406
+ schemaVersion: 1,
407
+ projectId,
408
+ generationSha256: null,
409
+ manuscriptSha256: null,
410
+ generationStatus: "waiting-for-base-research",
411
+ reviewState: "not-started",
412
+ requiredReviewRoles: requiredReviewRoles(project.publicationPolicy),
413
+ completedReviewRoles: [],
414
+ missingReviewRoles: requiredReviewRoles(project.publicationPolicy),
415
+ mechanicalIssues: [],
416
+ pivotOptions: [],
417
+ readinessVerdict: "independent-review-incomplete",
418
+ boundedStatement: "The final manuscript cannot be frozen until base research closes mechanically.",
419
+ closureSha256: null,
420
+ };
421
+ }
422
+ if (!(await pathExists(publicationCurrentPath(root, projectId)))) {
423
+ return {
424
+ schemaVersion: 1,
425
+ projectId,
426
+ generationSha256: null,
427
+ manuscriptSha256: null,
428
+ generationStatus: "not-started",
429
+ reviewState: "not-started",
430
+ requiredReviewRoles: REQUIRED_REVIEW_ROLES,
431
+ completedReviewRoles: [],
432
+ missingReviewRoles: REQUIRED_REVIEW_ROLES,
433
+ mechanicalIssues: [],
434
+ pivotOptions: [],
435
+ readinessVerdict: "independent-review-incomplete",
436
+ boundedStatement: "No final manuscript has been frozen for independent review.",
437
+ closureSha256: null,
438
+ };
439
+ }
440
+ const generation = await loadCurrentGeneration(root, projectId);
441
+ const reviews = await loadSubmittedReviews(root, projectId, generation);
442
+ const completedReviewRoles = reviews.map((entry) => entry.role);
443
+ const missingReviewRoles = generation.requiredReviewRoles.filter((role) => !completedReviewRoles.includes(role));
444
+ const reviewState = !completedReviewRoles.length
445
+ ? "not-started"
446
+ : missingReviewRoles.length
447
+ ? "partial"
448
+ : "complete";
449
+ const readinessVerdict = computeReadinessVerdict(generation, reviews, missingReviewRoles);
450
+ const closurePath = publicationClosurePath(root, projectId, generation.generationSha256);
451
+ const closureSha256 = (await pathExists(closurePath))
452
+ ? (await loadPublicationClosure(closurePath, generation.generationSha256)).closureSha256
453
+ : null;
454
+ return {
455
+ schemaVersion: 1,
456
+ projectId,
457
+ generationSha256: generation.generationSha256,
458
+ manuscriptSha256: generation.manuscript.sha256,
459
+ generationStatus: "manuscript-frozen",
460
+ reviewState,
461
+ requiredReviewRoles: generation.requiredReviewRoles,
462
+ completedReviewRoles,
463
+ missingReviewRoles,
464
+ mechanicalIssues: generation.assessmentResult.issueCodes,
465
+ pivotOptions: generation.assessmentResult.pivotOptions,
466
+ readinessVerdict,
467
+ boundedStatement: boundedStatement(readinessVerdict),
468
+ closureSha256,
469
+ };
470
+ }
471
+ export async function closePublication(root, projectId) {
472
+ return withWorkspaceLock(root, "research.publication.close", async () => {
473
+ await requireClosedTopJournalProject(root, projectId);
474
+ const generation = await loadCurrentGeneration(root, projectId);
475
+ const reviews = await loadSubmittedReviews(root, projectId, generation);
476
+ const missing = generation.requiredReviewRoles.filter((role) => !reviews.some((entry) => entry.role === role));
477
+ if (missing.length) {
478
+ throw publicationError("RESEARCH_PUBLICATION_REVIEW_INCOMPLETE", "Publication closure requires every policy-mandated independent review.", 3, { missingReviewRoles: missing });
479
+ }
480
+ const existingPath = publicationClosurePath(root, projectId, generation.generationSha256);
481
+ if (await pathExists(existingPath)) {
482
+ return loadPublicationClosure(existingPath, generation.generationSha256);
483
+ }
484
+ const readinessVerdict = computeReadinessVerdict(generation, reviews, []);
485
+ const closedAt = new Date().toISOString();
486
+ const closureCore = {
487
+ schemaVersion: 1,
488
+ kind: "tiangong-publication-closure",
489
+ projectId,
490
+ generationSha256: generation.generationSha256,
491
+ closedAt,
492
+ policy: generation.policy,
493
+ evidenceSnapshot: generation.evidenceSnapshot,
494
+ baseResearch: generation.baseResearch,
495
+ manuscript: generation.manuscript,
496
+ assessment: generation.assessment,
497
+ supplements: generation.supplements,
498
+ reviews: reviews.map((entry) => ({
499
+ role: entry.role,
500
+ packetSha256: entry.packet.packetSha256,
501
+ reviewSha256: entry.reviewSha256,
502
+ reviewerSessionSha256: entry.review.reviewerSessionSha256,
503
+ decision: entry.review.decision,
504
+ })),
505
+ mechanicalIssues: generation.assessmentResult.issueCodes,
506
+ pivotOptions: generation.assessmentResult.pivotOptions,
507
+ readinessVerdict,
508
+ boundedStatement: boundedStatement(readinessVerdict),
509
+ };
510
+ const closure = {
511
+ ...closureCore,
512
+ closureSha256: sha256Text(canonicalJson(closureCore)),
513
+ };
514
+ await writeImmutableJson(existingPath, closure, closure.closureSha256, "publication closure");
515
+ await appendJournalEvent(workspacePaths(root).journal, "publication.closed", projectId, {
516
+ projectId,
517
+ generationSha256: generation.generationSha256,
518
+ closureSha256: closure.closureSha256,
519
+ readinessVerdict,
520
+ policySha256: generation.policy.resolvedPolicySha256,
521
+ evidenceSnapshotSha256: generation.evidenceSnapshot.sha256,
522
+ manuscriptSha256: generation.manuscript.sha256,
523
+ });
524
+ return closure;
525
+ });
526
+ }
527
+ async function requireClosedTopJournalProject(root, projectId) {
528
+ const project = await requireTopJournalProject(root, projectId);
529
+ if (project.status !== "complete" ||
530
+ project.packages.some((item) => item.status !== "complete")) {
531
+ throw publicationError("RESEARCH_PUBLICATION_BASE_RESEARCH_INCOMPLETE", "Freeze the final manuscript only after the evidence-report research project is mechanically closed.", 3);
532
+ }
533
+ return project;
534
+ }
535
+ async function requireTopJournalProject(root, projectId) {
536
+ const project = await loadProject(root, projectId);
537
+ if (!project.publicationPolicy) {
538
+ throw publicationError("RESEARCH_PUBLICATION_POLICY_REQUIRED", "The publication workflow requires an approved top-journal policy binding.", 3);
539
+ }
540
+ await assertResearchPolicyBinding(root, project.publicationPolicy);
541
+ return project;
542
+ }
543
+ function verifiedSnapshotSha256(project, snapshot) {
544
+ const recorded = snapshot.snapshotSha256;
545
+ if (typeof recorded !== "string" || !/^[a-f0-9]{64}$/.test(recorded)) {
546
+ throw publicationError("RESEARCH_PUBLICATION_BINDING_INVALID", "The evidence snapshot hash is invalid.");
547
+ }
548
+ const { snapshotSha256: _ignored, ...withoutHash } = snapshot;
549
+ if (sha256Text(canonicalJson(withoutHash)) !== recorded ||
550
+ snapshot.snapshotId !== project.evidenceState.currentSnapshotId ||
551
+ recorded !== project.evidenceState.currentSnapshotSha256 ||
552
+ snapshot.snapshotId !== project.evidenceState.closureSnapshotId) {
553
+ throw publicationError("RESEARCH_PUBLICATION_BINDING_INVALID", "The final manuscript must bind the current mechanically closed evidence snapshot.");
554
+ }
555
+ return recorded;
556
+ }
557
+ function assertBaseClosure(project, closure, snapshotSha256) {
558
+ const evidenceSnapshot = isObject(closure.evidenceSnapshot) ? closure.evidenceSnapshot : {};
559
+ const policy = isObject(closure.publicationPolicy) ? closure.publicationPolicy : {};
560
+ if (closure.projectId !== project.id ||
561
+ closure.status !== "complete" ||
562
+ evidenceSnapshot.snapshotSha256 !== snapshotSha256 ||
563
+ evidenceSnapshot.snapshotId !== project.evidenceState.closureSnapshotId ||
564
+ policy.resolvedPolicySha256 !== project.publicationPolicy.resolvedPolicySha256 ||
565
+ policy.approvalSha256 !== project.publicationPolicy.approvalSha256) {
566
+ throw publicationError("RESEARCH_PUBLICATION_BINDING_INVALID", "The base research closure does not bind the current evidence snapshot and approved policy.");
567
+ }
568
+ }
569
+ function policySummary(policy) {
570
+ return {
571
+ projectId: policy.projectId,
572
+ resolvedPolicySha256: policy.resolvedPolicySha256,
573
+ approvalSha256: policy.approvalSha256,
574
+ verdictCeiling: policy.verdictCeiling,
575
+ targetJournal: policy.targetJournal,
576
+ };
577
+ }
578
+ function requiredReviewRoles(policy) {
579
+ const declared = policy.requiredReviewers.filter(isPublicationReviewRole);
580
+ return [...new Set([...REQUIRED_REVIEW_ROLES, ...declared])].sort();
581
+ }
582
+ function isPublicationReviewRole(value) {
583
+ return REQUIRED_REVIEW_ROLES.includes(value);
584
+ }
585
+ async function storePublicationObject(root, projectId, sourcePath, logicalName, allowControlPath = false) {
586
+ const canonical = requireAbsolutePath(sourcePath, logicalName);
587
+ if (!allowControlPath && canonical.split(sep).includes(RESEARCH_CONTROL_DIRECTORY)) {
588
+ throw publicationError("RESEARCH_PUBLICATION_FILE_INVALID", "Publication source files cannot be read from a research control directory.", 2);
589
+ }
590
+ const info = await lstat(canonical).catch(() => undefined);
591
+ if (!info?.isFile() || info.isSymbolicLink()) {
592
+ throw publicationError("RESEARCH_PUBLICATION_FILE_INVALID", `The ${logicalName} must be a regular non-symlink file.`, 2);
593
+ }
594
+ const sha256 = await sha256File(canonical);
595
+ const extension = safeExtension(extname(basename(canonical)));
596
+ const objectLocator = `publication/objects/${sha256}/content${extension}`;
597
+ const destination = join(projectDirectory(root, projectId), objectLocator);
598
+ if (await pathExists(destination)) {
599
+ if ((await sha256File(destination)) !== sha256) {
600
+ throw publicationError("RESEARCH_PUBLICATION_OBJECT_INVALID", "A content-addressed publication object failed hash verification.");
601
+ }
602
+ }
603
+ else {
604
+ await ensureDirectory(dirname(destination));
605
+ const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;
606
+ await writeFile(temporary, await readFile(canonical), { mode: 0o600 });
607
+ if ((await sha256File(temporary)) !== sha256) {
608
+ throw publicationError("RESEARCH_PUBLICATION_OBJECT_INVALID", "A publication object changed while it was being frozen.");
609
+ }
610
+ await rename(temporary, destination);
611
+ await chmod(destination, 0o444);
612
+ }
613
+ return { logicalName, sha256, bytes: info.size, objectLocator };
614
+ }
615
+ async function loadCurrentGeneration(root, projectId) {
616
+ const pointer = await readJsonFile(publicationCurrentPath(root, projectId), "Current publication generation");
617
+ if (pointer.schemaVersion !== 1 ||
618
+ pointer.projectId !== projectId ||
619
+ !/^[a-f0-9]{64}$/.test(pointer.generationSha256) ||
620
+ pointer.manifestLocator !== generationManifestLocator(pointer.generationSha256)) {
621
+ throw publicationError("RESEARCH_PUBLICATION_STATE_INVALID", "The publication pointer is invalid.");
622
+ }
623
+ const manifestPath = join(projectDirectory(root, projectId), pointer.manifestLocator);
624
+ const generation = await readJsonFile(manifestPath, "Publication generation");
625
+ const { generationSha256, ...withoutHash } = generation;
626
+ if (generation.kind !== "tiangong-publication-generation" ||
627
+ generation.projectId !== projectId ||
628
+ !isObject(generation.producer) ||
629
+ !["codex", "claude"].includes(String(generation.producer.agent)) ||
630
+ typeof generation.producer.sessionSha256 !== "string" ||
631
+ !/^[a-f0-9]{64}$/.test(generation.producer.sessionSha256) ||
632
+ generationSha256 !== pointer.generationSha256 ||
633
+ sha256Text(canonicalJson(withoutHash)) !== generationSha256) {
634
+ throw publicationError("RESEARCH_PUBLICATION_STATE_INVALID", "The publication generation failed its content hash binding.");
635
+ }
636
+ await verifyFrozenFiles(root, projectId, [
637
+ generation.manuscript,
638
+ generation.assessment,
639
+ generation.evidenceSnapshot.object,
640
+ generation.baseResearch.closure,
641
+ generation.baseResearch.analysis,
642
+ generation.baseResearch.report,
643
+ ...generation.supplements,
644
+ ]);
645
+ return generation;
646
+ }
647
+ async function verifyFrozenFiles(root, projectId, files) {
648
+ for (const file of files) {
649
+ const path = join(projectDirectory(root, projectId), file.objectLocator);
650
+ const info = await lstat(path).catch(() => undefined);
651
+ if (!info?.isFile() ||
652
+ info.isSymbolicLink() ||
653
+ info.size !== file.bytes ||
654
+ (await sha256File(path)) !== file.sha256) {
655
+ throw publicationError("RESEARCH_PUBLICATION_OBJECT_INVALID", "A frozen publication object is missing or failed hash verification.");
656
+ }
657
+ }
658
+ }
659
+ async function loadReviewerRegistry(root, projectId) {
660
+ const path = reviewerRegistryPath(root, projectId);
661
+ if (!(await pathExists(path)))
662
+ return { schemaVersion: 1, sessions: [] };
663
+ const value = await readJsonFile(path, "Publication reviewer registry");
664
+ if (value.schemaVersion !== 1 ||
665
+ !Array.isArray(value.sessions) ||
666
+ value.sessions.some((entry) => typeof entry.sessionSha256 !== "string" ||
667
+ !/^[a-f0-9]{64}$/.test(entry.sessionSha256) ||
668
+ entry.projectId !== projectId ||
669
+ typeof entry.generationSha256 !== "string" ||
670
+ !/^[a-f0-9]{64}$/.test(entry.generationSha256) ||
671
+ !isPublicationReviewRole(entry.role) ||
672
+ !["codex", "claude"].includes(entry.agent) ||
673
+ typeof entry.registeredAt !== "string")) {
674
+ throw publicationError("RESEARCH_PUBLICATION_STATE_INVALID", "The publication reviewer registry is invalid.");
675
+ }
676
+ return value;
677
+ }
678
+ async function usedReviewerSessionHashes(root, projectId) {
679
+ const journalPath = workspacePaths(root).journal;
680
+ await verifyJournal(journalPath);
681
+ const hashes = new Set();
682
+ for (const event of await readJournal(journalPath)) {
683
+ if (event.type !== "publication.review.prepared" ||
684
+ event.scope !== projectId ||
685
+ event.payload.projectId !== projectId) {
686
+ continue;
687
+ }
688
+ const value = event.payload.reviewerSessionSha256;
689
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) {
690
+ throw publicationError("RESEARCH_PUBLICATION_STATE_INVALID", "A publication review journal event is missing its session hash binding.");
691
+ }
692
+ hashes.add(value);
693
+ }
694
+ return hashes;
695
+ }
696
+ async function loadReviewPacket(root, projectId, generation, role) {
697
+ const packet = await readJsonFile(reviewPacketPath(root, projectId, generation.generationSha256, role), `Publication ${role} review packet`);
698
+ const { packetSha256, ...withoutHash } = packet;
699
+ if (packet.kind !== "tiangong-publication-review-packet" ||
700
+ packet.projectId !== projectId ||
701
+ packet.generationSha256 !== generation.generationSha256 ||
702
+ packet.role !== role ||
703
+ !isObject(packet.reviewer) ||
704
+ !["codex", "claude"].includes(String(packet.reviewer.agent)) ||
705
+ typeof packet.reviewer.sessionSha256 !== "string" ||
706
+ !/^[a-f0-9]{64}$/.test(packet.reviewer.sessionSha256) ||
707
+ sha256Text(canonicalJson(withoutHash)) !== packetSha256) {
708
+ throw publicationError("RESEARCH_PUBLICATION_REVIEW_BINDING_INVALID", "The publication review packet failed its content hash binding.");
709
+ }
710
+ return packet;
711
+ }
712
+ async function loadSubmittedReviews(root, projectId, generation) {
713
+ const reviews = [];
714
+ for (const role of generation.requiredReviewRoles) {
715
+ const path = submittedReviewPath(root, projectId, generation.generationSha256, role);
716
+ if (!(await pathExists(path)))
717
+ continue;
718
+ const packet = await loadReviewPacket(root, projectId, generation, role);
719
+ const raw = await readJsonFile(path, `Publication ${role} review`);
720
+ const review = parsePublicationReview(raw, role);
721
+ if (review.packetSha256 !== packet.packetSha256 ||
722
+ review.reviewerSessionSha256 !== packet.reviewer.sessionSha256) {
723
+ throw publicationError("RESEARCH_PUBLICATION_REVIEW_BINDING_INVALID", "A submitted publication review failed its packet binding.");
724
+ }
725
+ reviews.push({ role, review, reviewSha256: sha256Text(canonicalJson(review)), packet });
726
+ }
727
+ return reviews;
728
+ }
729
+ function parsePublicationReview(value, expectedRole) {
730
+ if (!isObject(value))
731
+ throw malformedReview();
732
+ const decisionSet = expectedRole === "journal-editor" ? EDITOR_DECISIONS : SPECIALIST_DECISIONS;
733
+ if (value.schemaVersion !== 1 ||
734
+ value.role !== expectedRole ||
735
+ typeof value.packetSha256 !== "string" ||
736
+ !/^[a-f0-9]{64}$/.test(value.packetSha256) ||
737
+ typeof value.reviewerSessionSha256 !== "string" ||
738
+ !/^[a-f0-9]{64}$/.test(value.reviewerSessionSha256) ||
739
+ !decisionSet.has(String(value.decision)) ||
740
+ !Array.isArray(value.findings) ||
741
+ value.findings.some((finding) => !isReviewFinding(finding)) ||
742
+ typeof value.boundedRecommendation !== "string" ||
743
+ value.boundedRecommendation.trim().length < 8 ||
744
+ value.boundedRecommendation.length > 4_000) {
745
+ throw malformedReview();
746
+ }
747
+ return value;
748
+ }
749
+ function isReviewFinding(value) {
750
+ return (isObject(value) &&
751
+ typeof value.code === "string" &&
752
+ /^[A-Z][A-Z0-9_]{2,63}$/.test(value.code) &&
753
+ ["blocking", "major", "minor"].includes(String(value.severity)) &&
754
+ typeof value.message === "string" &&
755
+ Array.isArray(value.evidenceIds) &&
756
+ value.evidenceIds.every((id) => typeof id === "string"));
757
+ }
758
+ function malformedReview() {
759
+ return publicationError("RESEARCH_PUBLICATION_REVIEW_INVALID", "The publication review does not match the role-specific structured schema.", 2);
760
+ }
761
+ function parsePublicationAssessment(value) {
762
+ if (!isObject(value))
763
+ throw malformedAssessment();
764
+ if (value.schemaVersion !== 1 ||
765
+ typeof value.title !== "string" ||
766
+ value.title.trim().length < 8 ||
767
+ !Array.isArray(value.claims) ||
768
+ value.claims.some((claim) => !isAssessmentClaim(claim)) ||
769
+ !Array.isArray(value.outcomes) ||
770
+ value.outcomes.some((outcome) => !isAssessmentOutcome(outcome)) ||
771
+ !Array.isArray(value.titleOutcomeIds) ||
772
+ value.titleOutcomeIds.some((id) => typeof id !== "string") ||
773
+ !Array.isArray(value.results) ||
774
+ value.results.some((result) => !isAssessmentResult(result)) ||
775
+ !Array.isArray(value.sourceClassifications) ||
776
+ value.sourceClassifications.some((item) => !isSourceClassification(item)) ||
777
+ !isRecallAudit(value.recallAudit)) {
778
+ throw malformedAssessment();
779
+ }
780
+ return value;
781
+ }
782
+ function isAssessmentClaim(value) {
783
+ return (isObject(value) &&
784
+ nonEmptyString(value.id) &&
785
+ ["central", "supporting", "contextual", "future-research"].includes(String(value.role)) &&
786
+ nonEmptyString(value.statement) &&
787
+ stringArray(value.evidenceSourceIds) &&
788
+ stringArray(value.dimensionIds) &&
789
+ stringArray(value.resultIds));
790
+ }
791
+ function isAssessmentOutcome(value) {
792
+ return (isObject(value) &&
793
+ nonEmptyString(value.id) &&
794
+ ["central", "supporting", "contextual"].includes(String(value.role)) &&
795
+ nonEmptyString(value.label) &&
796
+ [
797
+ "unobserved",
798
+ "future-work",
799
+ "conceptual-proposition",
800
+ "calibrated-model",
801
+ "causal-estimate",
802
+ "field-observation",
803
+ "validated-forecast",
804
+ "systematic-synthesis",
805
+ ].includes(String(value.supportStatus)) &&
806
+ stringArray(value.claimIds) &&
807
+ stringArray(value.resultIds));
808
+ }
809
+ function isAssessmentResult(value) {
810
+ return (isObject(value) &&
811
+ nonEmptyString(value.id) &&
812
+ ["central", "supporting", "contextual"].includes(String(value.role)) &&
813
+ [
814
+ "definition",
815
+ "accounting-identity",
816
+ "illustrative-sensitivity",
817
+ "calibrated-model",
818
+ "causal-estimate",
819
+ "field-observation",
820
+ "validated-forecast",
821
+ "systematic-synthesis",
822
+ "conceptual-proposition",
823
+ ].includes(String(value.resultClass)) &&
824
+ nonEmptyString(value.statement) &&
825
+ stringArray(value.evidenceSourceIds) &&
826
+ typeof value.independentlyReproduced === "boolean");
827
+ }
828
+ function isSourceClassification(value) {
829
+ return (isObject(value) &&
830
+ nonEmptyString(value.sourceId) &&
831
+ ["direct", "adjacent", "contextual"].includes(String(value.relationship)) &&
832
+ [
833
+ "peer-reviewed-empirical",
834
+ "peer-reviewed-model",
835
+ "peer-reviewed-review",
836
+ "official-data",
837
+ "administrative-record",
838
+ "patent",
839
+ "news",
840
+ "owner-provided-input",
841
+ "internal-model",
842
+ "other",
843
+ ].includes(String(value.evidenceKind)));
844
+ }
845
+ function isRecallAudit(value) {
846
+ return (isObject(value) &&
847
+ ["pass", "fail", "incomplete"].includes(String(value.status)) &&
848
+ typeof value.candidateDispositionComplete === "boolean" &&
849
+ typeof value.databaseCoverageComplete === "boolean" &&
850
+ typeof value.backwardCitationChasing === "boolean" &&
851
+ typeof value.forwardCitationChasing === "boolean" &&
852
+ typeof value.adversarialSearch === "boolean" &&
853
+ typeof value.closestPriorWorkCompared === "boolean" &&
854
+ stringArray(value.missingCoreWorkIds));
855
+ }
856
+ function malformedAssessment() {
857
+ return publicationError("RESEARCH_PUBLICATION_ASSESSMENT_INVALID", "The publication assessment does not match the authoritative structured schema.", 2);
858
+ }
859
+ function computeReadinessVerdict(generation, reviews, missing) {
860
+ if (missing.length)
861
+ return "independent-review-incomplete";
862
+ const specialistPass = reviews
863
+ .filter((entry) => entry.role !== "journal-editor")
864
+ .every((entry) => entry.review.decision === "pass");
865
+ const editorReady = reviews.some((entry) => entry.role === "journal-editor" && entry.review.decision === "submission-ready");
866
+ if (!specialistPass || !editorReady || generation.assessmentResult.issueCodes.length > 0) {
867
+ return "revision-required";
868
+ }
869
+ if (!generation.assessmentResult.canClaimSubmissionReady) {
870
+ return generation.policy.verdictCeiling === "top-journal-class-ready"
871
+ ? "top-journal-class-ready"
872
+ : "top-journal-candidate";
873
+ }
874
+ return "target-journal-submission-ready";
875
+ }
876
+ function boundedStatement(verdict) {
877
+ if (verdict === "target-journal-submission-ready") {
878
+ return "The exact frozen manuscript passed all required independent reviews and is mechanically bounded as target-journal submission-ready; acceptance is not guaranteed.";
879
+ }
880
+ if (verdict === "top-journal-class-ready") {
881
+ return "The exact frozen manuscript is bounded as top-journal-class-ready, not target-journal submission-ready.";
882
+ }
883
+ if (verdict === "top-journal-candidate") {
884
+ return "The exact frozen manuscript remains a top-journal candidate, not submission-ready.";
885
+ }
886
+ if (verdict === "revision-required") {
887
+ return "The exact frozen manuscript is not submission-ready; revision or a policy-declared research pivot is required.";
888
+ }
889
+ return "The exact frozen manuscript is not submission-ready because required independent reviews are incomplete.";
890
+ }
891
+ function reviewInstructions(role) {
892
+ return [
893
+ "Review only the exact content-addressed manuscript, supplements, evidence snapshot, base research outputs, and policy in this packet.",
894
+ "Use a fresh independent reviewer session; do not inherit producer reasoning or an earlier manuscript review.",
895
+ "Do not upgrade the mechanical assessment or policy verdict ceiling.",
896
+ role === "journal-editor"
897
+ ? "Act as a skeptical target-journal editor and return one allowed editorial decision."
898
+ : `Apply the ${role} rubric and return pass, revise, or reject with structured findings.`,
899
+ ];
900
+ }
901
+ async function writeImmutableJson(path, value, expectedSha256, label) {
902
+ if (await pathExists(path)) {
903
+ const existing = JSON.parse(await readFile(path, "utf8"));
904
+ const actual = sha256Text(canonicalJson(withoutBindingHash(existing)));
905
+ if (actual !== expectedSha256) {
906
+ throw publicationError("RESEARCH_PUBLICATION_OBJECT_INVALID", `The immutable ${label} already exists with different content.`);
907
+ }
908
+ return;
909
+ }
910
+ await writeJsonAtomic(path, value, 0o444);
911
+ }
912
+ function withoutBindingHash(value) {
913
+ if (typeof value.generationSha256 === "string") {
914
+ const { generationSha256: _ignored, ...rest } = value;
915
+ return rest;
916
+ }
917
+ if (typeof value.packetSha256 === "string") {
918
+ const { packetSha256: _ignored, ...rest } = value;
919
+ return rest;
920
+ }
921
+ if (typeof value.closureSha256 === "string") {
922
+ const { closureSha256: _ignored, ...rest } = value;
923
+ return rest;
924
+ }
925
+ return value;
926
+ }
927
+ async function loadPublicationClosure(path, generationSha256) {
928
+ const closure = await readJsonFile(path, "Publication closure");
929
+ const { closureSha256, ...withoutHash } = closure;
930
+ if (closure.kind !== "tiangong-publication-closure" ||
931
+ closure.generationSha256 !== generationSha256 ||
932
+ sha256Text(canonicalJson(withoutHash)) !== closureSha256) {
933
+ throw publicationError("RESEARCH_PUBLICATION_STATE_INVALID", "The publication closure failed its content hash binding.");
934
+ }
935
+ return closure;
936
+ }
937
+ async function readRegularTextFile(path, label) {
938
+ const canonical = requireAbsolutePath(path, label);
939
+ const info = await lstat(canonical).catch(() => undefined);
940
+ if (!info?.isFile() || info.isSymbolicLink()) {
941
+ throw publicationError("RESEARCH_PUBLICATION_FILE_INVALID", `The ${label} must be a regular non-symlink file.`, 2);
942
+ }
943
+ if (info.size > 16 * 1024 * 1024) {
944
+ throw publicationError("RESEARCH_PUBLICATION_FILE_INVALID", `The ${label} exceeds the 16 MiB structured-input limit.`, 2);
945
+ }
946
+ return readFile(canonical, "utf8");
947
+ }
948
+ function requireAbsolutePath(path, label) {
949
+ if (!isAbsolute(path) || resolve(path) !== path) {
950
+ throw publicationError("RESEARCH_PUBLICATION_FILE_INVALID", `The ${label} path must be absolute and canonical.`, 2);
951
+ }
952
+ return path;
953
+ }
954
+ function requireSessionId(value, label) {
955
+ const normalized = value.trim();
956
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/.test(normalized)) {
957
+ throw publicationError("RESEARCH_PUBLICATION_SESSION_INVALID", `The ${label} session ID must contain 8-128 safe opaque characters.`, 2);
958
+ }
959
+ return normalized;
960
+ }
961
+ function safeExtension(value) {
962
+ return /^\.[A-Za-z0-9]{1,10}$/.test(value) ? value.toLowerCase() : ".bin";
963
+ }
964
+ function nonEmptyString(value) {
965
+ return typeof value === "string" && value.trim().length > 0;
966
+ }
967
+ function stringArray(value) {
968
+ return Array.isArray(value) && value.every((item) => nonEmptyString(item));
969
+ }
970
+ function projectDirectory(root, projectId) {
971
+ return join(workspacePaths(root).projects, projectId);
972
+ }
973
+ function generationManifestLocator(generationSha256) {
974
+ return `publication/generations/${generationSha256}/manifest.json`;
975
+ }
976
+ function publicationCurrentPath(root, projectId) {
977
+ return join(projectDirectory(root, projectId), "publication", "current.json");
978
+ }
979
+ function reviewerRegistryPath(root, projectId) {
980
+ return join(projectDirectory(root, projectId), "publication", "reviewer-sessions.json");
981
+ }
982
+ function reviewPacketPath(root, projectId, generationSha256, role) {
983
+ return join(projectDirectory(root, projectId), "publication", "generations", generationSha256, "review-packets", `${role}.json`);
984
+ }
985
+ function submittedReviewPath(root, projectId, generationSha256, role) {
986
+ return join(projectDirectory(root, projectId), "publication", "generations", generationSha256, "reviews", `${role}.json`);
987
+ }
988
+ function publicationClosurePath(root, projectId, generationSha256) {
989
+ return join(projectDirectory(root, projectId), "publication", "generations", generationSha256, "closure.json");
990
+ }
991
+ function publicationError(code, message, exitCode = 3, details) {
992
+ return new CliError(message, { code, exitCode, details });
993
+ }
994
+ //# sourceMappingURL=publication-workflow.js.map