@tiangong-ai/cli 0.0.59 → 0.0.61

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 (104) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +287 -41
  3. package/dist/research/orchestration.js +209 -62
  4. package/dist/research/orchestration.js.map +1 -1
  5. package/dist/research/workspace/acquisition-forecast.d.ts +3 -0
  6. package/dist/research/workspace/acquisition-forecast.js +28 -3
  7. package/dist/research/workspace/acquisition-forecast.js.map +1 -1
  8. package/dist/research/workspace/acquisition-revision.d.ts +13 -0
  9. package/dist/research/workspace/acquisition-revision.js +142 -0
  10. package/dist/research/workspace/acquisition-revision.js.map +1 -0
  11. package/dist/research/workspace/acquisition-routes.d.ts +1 -1
  12. package/dist/research/workspace/acquisition-routes.js +2 -11
  13. package/dist/research/workspace/acquisition-routes.js.map +1 -1
  14. package/dist/research/workspace/acquisition.d.ts +3 -1
  15. package/dist/research/workspace/acquisition.js +67 -35
  16. package/dist/research/workspace/acquisition.js.map +1 -1
  17. package/dist/research/workspace/analysis-run.d.ts +2 -0
  18. package/dist/research/workspace/analysis-run.js +28 -0
  19. package/dist/research/workspace/analysis-run.js.map +1 -0
  20. package/dist/research/workspace/artifact-read-audit.d.ts +16 -0
  21. package/dist/research/workspace/artifact-read-audit.js +176 -0
  22. package/dist/research/workspace/artifact-read-audit.js.map +1 -0
  23. package/dist/research/workspace/artifact-trace.d.ts +12 -0
  24. package/dist/research/workspace/artifact-trace.js +109 -0
  25. package/dist/research/workspace/artifact-trace.js.map +1 -0
  26. package/dist/research/workspace/artifact-view-mcp.d.ts +75 -0
  27. package/dist/research/workspace/artifact-view-mcp.js +214 -0
  28. package/dist/research/workspace/artifact-view-mcp.js.map +1 -0
  29. package/dist/research/workspace/artifact-views.d.ts +100 -0
  30. package/dist/research/workspace/artifact-views.js +395 -0
  31. package/dist/research/workspace/artifact-views.js.map +1 -0
  32. package/dist/research/workspace/artifacts.d.ts +3 -0
  33. package/dist/research/workspace/artifacts.js +13 -0
  34. package/dist/research/workspace/artifacts.js.map +1 -1
  35. package/dist/research/workspace/audit-bundle.d.ts +9 -0
  36. package/dist/research/workspace/audit-bundle.js +124 -8
  37. package/dist/research/workspace/audit-bundle.js.map +1 -1
  38. package/dist/research/workspace/content-evidence.d.ts +3 -0
  39. package/dist/research/workspace/content-evidence.js +149 -22
  40. package/dist/research/workspace/content-evidence.js.map +1 -1
  41. package/dist/research/workspace/executor.d.ts +5 -1
  42. package/dist/research/workspace/executor.js +106 -22
  43. package/dist/research/workspace/executor.js.map +1 -1
  44. package/dist/research/workspace/native-run.d.ts +77 -0
  45. package/dist/research/workspace/native-run.js +637 -0
  46. package/dist/research/workspace/native-run.js.map +1 -0
  47. package/dist/research/workspace/preflight.d.ts +8 -2
  48. package/dist/research/workspace/preflight.js +17 -14
  49. package/dist/research/workspace/preflight.js.map +1 -1
  50. package/dist/research/workspace/project-authority.d.ts +21 -0
  51. package/dist/research/workspace/project-authority.js +186 -0
  52. package/dist/research/workspace/project-authority.js.map +1 -0
  53. package/dist/research/workspace/project-mutations.d.ts +26 -0
  54. package/dist/research/workspace/project-mutations.js +332 -0
  55. package/dist/research/workspace/project-mutations.js.map +1 -0
  56. package/dist/research/workspace/projects.d.ts +2 -1
  57. package/dist/research/workspace/projects.js +133 -91
  58. package/dist/research/workspace/projects.js.map +1 -1
  59. package/dist/research/workspace/publication-workflow.d.ts +3 -0
  60. package/dist/research/workspace/publication-workflow.js +15 -3
  61. package/dist/research/workspace/publication-workflow.js.map +1 -1
  62. package/dist/research/workspace/request-provenance.d.ts +67 -0
  63. package/dist/research/workspace/request-provenance.js +115 -0
  64. package/dist/research/workspace/request-provenance.js.map +1 -0
  65. package/dist/research/workspace/review-executor.js +22 -11
  66. package/dist/research/workspace/review-executor.js.map +1 -1
  67. package/dist/research/workspace/runtime.d.ts +57 -0
  68. package/dist/research/workspace/runtime.js +292 -140
  69. package/dist/research/workspace/runtime.js.map +1 -1
  70. package/dist/research/workspace/sanitization.js +1 -1
  71. package/dist/research/workspace/sanitization.js.map +1 -1
  72. package/dist/research/workspace/schema-compatibility.d.ts +5 -0
  73. package/dist/research/workspace/schema-compatibility.js +55 -0
  74. package/dist/research/workspace/schema-compatibility.js.map +1 -0
  75. package/dist/research/workspace/schemas.d.ts +6 -0
  76. package/dist/research/workspace/schemas.js +13 -2
  77. package/dist/research/workspace/schemas.js.map +1 -1
  78. package/dist/research/workspace/scientific-design.js +8 -17
  79. package/dist/research/workspace/scientific-design.js.map +1 -1
  80. package/dist/research/workspace/scientific-fulfillment-audit.d.ts +3 -0
  81. package/dist/research/workspace/scientific-fulfillment-audit.js +137 -0
  82. package/dist/research/workspace/scientific-fulfillment-audit.js.map +1 -0
  83. package/dist/research/workspace/scientific-fulfillment.d.ts +121 -0
  84. package/dist/research/workspace/scientific-fulfillment.js +539 -0
  85. package/dist/research/workspace/scientific-fulfillment.js.map +1 -0
  86. package/dist/research/workspace/scientific-review-execution.js +84 -61
  87. package/dist/research/workspace/scientific-review-execution.js.map +1 -1
  88. package/dist/research/workspace/scientific-review.d.ts +8 -2
  89. package/dist/research/workspace/scientific-review.js +205 -53
  90. package/dist/research/workspace/scientific-review.js.map +1 -1
  91. package/dist/research/workspace/setup-catalog.js +2 -2
  92. package/dist/research/workspace/task-acceptance.d.ts +134 -0
  93. package/dist/research/workspace/task-acceptance.js +544 -0
  94. package/dist/research/workspace/task-acceptance.js.map +1 -0
  95. package/dist/research/workspace/task-audit.d.ts +12 -0
  96. package/dist/research/workspace/task-audit.js +231 -0
  97. package/dist/research/workspace/task-audit.js.map +1 -0
  98. package/dist/research/workspace/task-contract.d.ts +142 -0
  99. package/dist/research/workspace/task-contract.js +506 -0
  100. package/dist/research/workspace/task-contract.js.map +1 -0
  101. package/dist/research/workspace/types.d.ts +6 -3
  102. package/dist/research/workspace/workspace.js +11 -8
  103. package/dist/research/workspace/workspace.js.map +1 -1
  104. package/package.json +1 -1
@@ -1,13 +1,19 @@
1
1
  import { Ajv2020 } from "ajv/dist/2020.js";
2
- import { lstat, readFile } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { chmod, copyFile, lstat, mkdir, readFile } from "node:fs/promises";
3
4
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
5
  import { CliError } from "../../errors.js";
6
+ import { taskContext } from "./task-contract.js";
7
+ import { nativeRunArtifactRecords } from "./native-run.js";
8
+ import { loadEvidenceArtifactRecords } from "./artifacts.js";
5
9
  import { sourceTypeRequirementGaps } from "./evidence-role-coverage.js";
6
10
  import { appendJournalEvent, verifyJournal } from "./journal.js";
7
11
  import { loadProject, nextScientificGate, saveProject } from "./projects.js";
12
+ import { projectWithEffectiveAuthority, readProjectAuthorityIndex } from "./project-authority.js";
8
13
  import { assertResearchPolicyBinding } from "./research-policy.js";
9
- import { evaluateScientificDesign, parseScientificDesign, scientificDesignPolicyGaps, } from "./scientific-design.js";
14
+ import { evaluateScientificDesign, scientificDesignPolicyGaps, } from "./scientific-design.js";
10
15
  import { resolveScientificObjectBinding } from "./scientific-objects.js";
16
+ import { loadScientificFulfillmentView, scientificFulfillmentLocator, } from "./scientific-fulfillment.js";
11
17
  import { sanitizeResearchValue } from "./sanitization.js";
12
18
  import { canonicalJson, fileRecord, isObject, pathExists, resolveContained, sha256File, sha256Text, workspacePaths, writeBytesAtomic, writeJsonAtomic, writeTextAtomic, } from "./storage.js";
13
19
  import { loadWorkspaceConfig, verifyDoctorAttestation, withWorkspaceLock } from "./workspace.js";
@@ -263,7 +269,8 @@ export async function prepareScientificReview(input) {
263
269
  exitCode: 3,
264
270
  });
265
271
  }
266
- const design = await loadBoundScientificDesign(input.root, project);
272
+ const fulfillment = await loadScientificFulfillmentView(input.root, project, input.role);
273
+ const design = fulfillment.contract;
267
274
  const assessment = await readAssessment(input.assessmentPath, input.role);
268
275
  if (assessment.designSha256 !== project.scientificDesign.designSha256) {
269
276
  throw new CliError("Scientific assessment does not match the frozen design.", {
@@ -272,6 +279,68 @@ export async function prepareScientificReview(input) {
272
279
  });
273
280
  }
274
281
  const stageInputs = await stageInputRecords(input.root, project, input.role, design, assessment, input.canaryArtifactPaths ?? []);
282
+ const currentTask = await taskContext(input.root, project.id);
283
+ if (currentTask) {
284
+ for (const record of await nativeRunArtifactRecords(input.root, project.id)) {
285
+ const locator = `projects/${project.id}/${record.path}`;
286
+ stageInputs.push({
287
+ ...record,
288
+ path: locator,
289
+ sourceLocator: locator,
290
+ purpose: "native-calculation",
291
+ ownerId: project.id,
292
+ hashBasis: "raw-file-bytes",
293
+ mediaType: record.path.endsWith(".json")
294
+ ? "application/json"
295
+ : "application/octet-stream",
296
+ objectKind: null,
297
+ registrationRecordSha256: null,
298
+ });
299
+ }
300
+ }
301
+ const requestSource = currentTask?.requestProvenance.source;
302
+ if (requestSource) {
303
+ const locator = `projects/${project.id}/task/request-sources/${requestSource.objectSha256}.json`;
304
+ stageInputs.push({
305
+ ...(await fileRecord(resolveContained(paths.control, locator), locator)),
306
+ purpose: "original-request-source",
307
+ ownerId: project.id,
308
+ sourceLocator: locator,
309
+ hashBasis: "raw-file-bytes",
310
+ mediaType: "application/json",
311
+ objectKind: null,
312
+ registrationRecordSha256: null,
313
+ });
314
+ }
315
+ if (input.role !== "research-design")
316
+ stageInputs.push(...(await modelObjectInputs(input.root, project.id, design)));
317
+ if (fulfillment.records.length) {
318
+ for (const record of fulfillment.records) {
319
+ const locator = scientificFulfillmentLocator(project.id, record.recordSha256);
320
+ stageInputs.push({
321
+ ...(await fileRecord(resolveContained(paths.control, locator), locator)),
322
+ purpose: "scientific-fulfillment",
323
+ ownerId: project.id,
324
+ sourceLocator: locator,
325
+ hashBasis: "raw-file-bytes",
326
+ mediaType: "application/json",
327
+ objectKind: null,
328
+ registrationRecordSha256: null,
329
+ });
330
+ }
331
+ const locator = `projects/${project.id}/scientific/design/views/${fulfillment.effectiveSha256}.json`;
332
+ await writeImmutableJson(resolveContained(paths.control, locator), design);
333
+ stageInputs.push({
334
+ ...(await fileRecord(resolveContained(paths.control, locator), locator)),
335
+ purpose: "effective-scientific-design",
336
+ ownerId: project.id,
337
+ sourceLocator: locator,
338
+ hashBasis: "raw-file-bytes",
339
+ mediaType: "application/json",
340
+ objectKind: null,
341
+ registrationRecordSha256: null,
342
+ });
343
+ }
275
344
  const assessmentSha256 = exactJsonSha256(assessment);
276
345
  const assessmentLocator = `projects/${project.id}/scientific/assessments/${input.role}/${assessmentSha256}.json`;
277
346
  await writeImmutableJson(join(paths.control, assessmentLocator), assessment);
@@ -287,7 +356,7 @@ export async function prepareScientificReview(input) {
287
356
  };
288
357
  await assertBoundPolicyObject(input.root, project, packetPolicy);
289
358
  const designEvaluation = evaluateScientificDesign(design);
290
- const issues = evaluateAssessment(input.role, assessment, design, project.evidenceRequirements, project.publicationPolicy, await assessmentEvidenceContext(input.root, project, input.role, stageInputs));
359
+ const issues = evaluateAssessment(input.role, assessment, design, project.evidenceRequirements, project.publicationPolicy, await assessmentEvidenceContext(input.root, project, input.role, stageInputs), fulfillment.deferredObjectRuleIds);
291
360
  const futureGateObligations = scientificFutureGateObligations(design, input.role);
292
361
  const packetCore = {
293
362
  schemaVersion: 1,
@@ -297,11 +366,16 @@ export async function prepareScientificReview(input) {
297
366
  design: {
298
367
  sha256: project.scientificDesign.designSha256,
299
368
  objectLocator: project.scientificDesign.objectLocator,
369
+ fulfillment: {
370
+ headSha256: fulfillment.headSha256,
371
+ effectiveSha256: fulfillment.effectiveSha256,
372
+ },
300
373
  },
301
374
  policy: packetPolicy,
302
375
  reviewer: { agent: input.reviewerAgent, sessionSha256: reviewerSessionSha256 },
303
376
  preparedAt: new Date().toISOString(),
304
377
  stageInputs,
378
+ taskContract: currentTask,
305
379
  assessment: { sha256: assessmentSha256, objectLocator: assessmentLocator },
306
380
  mechanicalAssessment: {
307
381
  canPass: issues.length === 0 && assessment.recommendation === "pass",
@@ -328,7 +402,10 @@ export async function prepareScientificReview(input) {
328
402
  newGenerationOnMaterialChange: true,
329
403
  revisionReserveIncluded: true,
330
404
  },
331
- instructions: reviewInstructions(input.role),
405
+ instructions: [
406
+ ...reviewInstructions(input.role),
407
+ "Scientific fulfillment files supply only previously declared pending object slots. The base design and policy dispositions remain immutable. Discharging an object-filing blocker does not establish calibration, justification or scientific correctness: independently assess the actual code, locks and source-bound values under every original policy rule.",
408
+ ],
332
409
  };
333
410
  const packetSha256 = sha256Text(canonicalJson(packetCore));
334
411
  const packet = { ...packetCore, packetSha256 };
@@ -500,8 +577,9 @@ export { readReview as readScientificReviewOutput };
500
577
  export async function assertScientificGateForStage(root, project, stage) {
501
578
  if (!project.scientificDesign)
502
579
  return;
503
- const design = await loadBoundScientificDesign(root, project);
504
580
  for (const role of requiredGateRoles(stage)) {
581
+ const fulfillment = await loadScientificFulfillmentView(root, project, role);
582
+ const design = fulfillment.contract;
505
583
  const gate = project.scientificDesign.gates[role];
506
584
  if (gate.status !== "passed" || !gate.packetSha256 || !gate.reviewSha256) {
507
585
  throw new CliError(`Scientific ${role} review must pass before ${stage}.`, {
@@ -513,7 +591,7 @@ export async function assertScientificGateForStage(root, project, stage) {
513
591
  const packet = await loadBoundPacket(root, project, role, gate.packetSha256);
514
592
  const review = await loadBoundReview(root, project, role, gate.reviewSha256);
515
593
  const assessment = await loadBoundAssessment(root, project, role, packet);
516
- const currentIssues = evaluateAssessment(role, assessment, design, project.evidenceRequirements, project.publicationPolicy, await assessmentEvidenceContext(root, project, role, packet.stageInputs)).map((issue) => issue.code);
594
+ const currentIssues = evaluateAssessment(role, assessment, design, project.evidenceRequirements, project.publicationPolicy, await assessmentEvidenceContext(root, project, role, packet.stageInputs), fulfillment.deferredObjectRuleIds).map((issue) => issue.code);
517
595
  if (packet.assessment.sha256 !== gate.assessmentSha256 ||
518
596
  packet.reviewer.sessionSha256 !== gate.reviewerSessionSha256 ||
519
597
  review.packetSha256 !== packet.packetSha256 ||
@@ -526,8 +604,9 @@ export async function assertScientificGateForStage(root, project, stage) {
526
604
  }
527
605
  }
528
606
  }
529
- export async function inspectScientificReviewStatus(root, projectId) {
530
- const project = await loadProject(root, projectId);
607
+ export async function inspectScientificReviewStatus(root, projectId, knownProject) {
608
+ const project = knownProject ??
609
+ projectWithEffectiveAuthority(await loadProject(root, projectId), await readProjectAuthorityIndex(root));
531
610
  if (!project.scientificDesign) {
532
611
  return { projectId, reviewState: "not-required", nextGate: null, gates: null };
533
612
  }
@@ -546,7 +625,7 @@ export async function inspectScientificReviewStatus(root, projectId) {
546
625
  gates: structuredClone(project.scientificDesign.gates),
547
626
  };
548
627
  }
549
- function evaluateAssessment(role, assessment, design, requirements, policy, evidenceContext = null) {
628
+ function evaluateAssessment(role, assessment, design, requirements, policy, evidenceContext = null, deferredObjectRuleIds = []) {
550
629
  const issues = new Map();
551
630
  const add = (code, message, objectIds = []) => {
552
631
  if (!issues.has(code))
@@ -562,25 +641,26 @@ function evaluateAssessment(role, assessment, design, requirements, policy, evid
562
641
  add(`POLICY_${gap.replaceAll(/[^A-Za-z0-9]+/gu, "_").toUpperCase()}`, "The frozen scientific design no longer matches its approved Research Policy disposition contract.", [gap]);
563
642
  }
564
643
  const duePolicyRules = design.policyRuleDispositions.filter((disposition) => disposition.status === "planned" &&
644
+ !deferredObjectRuleIds.includes(disposition.ruleId) &&
565
645
  disposition.dueGate !== "publication-freeze" &&
566
646
  gateRank[disposition.dueGate] <= gateRank[role]);
567
647
  if (duePolicyRules.length) {
568
- add("POLICY_RULE_DUE_UNRESOLVED", "A planned Research Policy rule reached its declared early-review gate without a new design generation that records how it was satisfied.", duePolicyRules.map((disposition) => disposition.ruleId));
648
+ add("POLICY_RULE_DUE_UNRESOLVED", "A planned Research Policy rule reached its early-review gate without its declared object fulfillment or reviewed design resolution.", duePolicyRules.map((disposition) => disposition.ruleId));
569
649
  }
570
650
  const dueUncertaintyFreezes = design.uncertaintyParameters.filter((parameter) => parameter.stateValueStatus === "pending-source-acquisition" &&
571
651
  gateRank[parameter.freezeBeforeGate] <= gateRank[role]);
572
652
  if (dueUncertaintyFreezes.length) {
573
- add("UNCERTAINTY_STATE_VALUES_NOT_FROZEN", "Source-derived uncertainty values reached their declared freeze gate without a new authoritative design generation containing exact states and source bindings.", dueUncertaintyFreezes.map((parameter) => parameter.id));
653
+ add("UNCERTAINTY_STATE_VALUES_NOT_FROZEN", "Source-derived uncertainty values reached their freeze gate without exact states and admitted source bindings.", dueUncertaintyFreezes.map((parameter) => parameter.id));
574
654
  }
575
655
  const dueModelImplementations = design.identity.modelStructures.filter((model) => model.implementationStatus === "pending-source-acquisition" &&
576
656
  gateRank[model.implementationFreezeBeforeGate] <= gateRank[role]);
577
657
  if (dueModelImplementations.length) {
578
- add("MODEL_IMPLEMENTATION_NOT_FROZEN", "A model implementation reached its declared freeze gate without a new authoritative design generation binding executable model bytes.", dueModelImplementations.map((model) => model.id));
658
+ add("MODEL_IMPLEMENTATION_NOT_FROZEN", "A model implementation reached its freeze gate without binding the declared executable bytes.", dueModelImplementations.map((model) => model.id));
579
659
  }
580
660
  const dueModelEnvironmentLocks = design.identity.modelStructures.filter((model) => model.environmentLockStatus === "pending-runtime-lock" &&
581
661
  gateRank[model.environmentLockFreezeBeforeGate] <= gateRank[role]);
582
662
  if (dueModelEnvironmentLocks.length) {
583
- add("MODEL_ENVIRONMENT_LOCK_NOT_FROZEN", "A model environment reached its declared freeze gate without a new authoritative design generation binding an exact runtime and dependency lock.", dueModelEnvironmentLocks.map((model) => model.id));
663
+ add("MODEL_ENVIRONMENT_LOCK_NOT_FROZEN", "A model environment reached its freeze gate without an exact runtime and dependency lock.", dueModelEnvironmentLocks.map((model) => model.id));
584
664
  }
585
665
  if (role === "research-design") {
586
666
  const value = assessment;
@@ -844,30 +924,7 @@ async function stageInputRecords(root, project, role, design, assessment, canary
844
924
  registrationRecordSha256: null,
845
925
  });
846
926
  };
847
- for (const model of design.identity.modelStructures) {
848
- if (model.implementationStatus === "executable-frozen") {
849
- records.push(await promoteRegisteredModelObject({
850
- root,
851
- projectId: project.id,
852
- objectKind: "model-implementation",
853
- sourceLocator: model.implementationArtifactLocator,
854
- sha256: model.implementationArtifactSha256,
855
- purpose: "model-implementation",
856
- ownerId: model.id,
857
- }));
858
- }
859
- if (model.environmentLockStatus === "exact-frozen") {
860
- records.push(await promoteRegisteredModelObject({
861
- root,
862
- projectId: project.id,
863
- objectKind: "environment-lock",
864
- sourceLocator: model.environmentLockLocator,
865
- sha256: model.environmentLockSha256,
866
- purpose: "model-environment-lock",
867
- ownerId: model.id,
868
- }));
869
- }
870
- }
927
+ records.push(...(await modelObjectInputs(root, project.id, design)));
871
928
  for (const gap of design.knownGaps) {
872
929
  for (const artifact of gap.sourceArtifacts) {
873
930
  await promoteLegacyJson({
@@ -891,8 +948,7 @@ async function stageInputRecords(root, project, role, design, assessment, canary
891
948
  });
892
949
  }
893
950
  const relativeOutputs = ["outputs/acquisition.json", "outputs/evidence-snapshot.json"];
894
- if (role === "evidence-construct" &&
895
- (await pathExists(join(projectRoot, "outputs", "content-snapshot.json")))) {
951
+ if (await pathExists(join(projectRoot, "outputs", "content-snapshot.json"))) {
896
952
  relativeOutputs.push("outputs/content-snapshot.json");
897
953
  }
898
954
  const stageOutputs = await Promise.all(relativeOutputs.map(async (relativePath) => {
@@ -908,6 +964,62 @@ async function stageInputRecords(root, project, role, design, assessment, canary
908
964
  registrationRecordSha256: null,
909
965
  };
910
966
  }));
967
+ const snapshot = await readExactJson(join(projectRoot, "outputs/evidence-snapshot.json"), "Scientific evidence snapshot", "RESEARCH_SCIENTIFIC_GATE_INVALID");
968
+ if (!isObject(snapshot) || !Array.isArray(snapshot.artifacts))
969
+ throw scientificGateError("Scientific evidence snapshot has no exact artifact set.", role);
970
+ if (snapshot.artifacts.length) {
971
+ const registered = new Map((await loadEvidenceArtifactRecords(root, project.id)).map((artifact) => [
972
+ artifact.artifactId,
973
+ artifact,
974
+ ]));
975
+ for (const selected of snapshot.artifacts) {
976
+ const artifact = isObject(selected) && typeof selected.artifactId === "string"
977
+ ? registered.get(selected.artifactId)
978
+ : undefined;
979
+ if (!artifact ||
980
+ !isObject(selected) ||
981
+ artifact.sha256 !== selected.sha256 ||
982
+ artifact.bytes !== selected.bytes ||
983
+ artifact.locator !== selected.locator ||
984
+ artifact.mediaType !== selected.mediaType)
985
+ throw scientificGateError("Scientific review source is not the exact frozen registered artifact.", role);
986
+ let directory = projectRoot;
987
+ for (const part of ["scientific", "evidence-objects", artifact.sha256]) {
988
+ directory = join(directory, part);
989
+ await mkdir(directory, { mode: 0o700 }).catch((error) => {
990
+ if (error.code !== "EEXIST")
991
+ throw error;
992
+ });
993
+ const info = await lstat(directory);
994
+ if (!info.isDirectory() || info.isSymbolicLink())
995
+ throw scientificGateError("Scientific evidence directories cannot redirect through links.", role);
996
+ }
997
+ const path = `projects/${project.id}/scientific/evidence-objects/${artifact.sha256}/blob`;
998
+ const target = resolveContained(paths.control, path);
999
+ if (!(await pathExists(target))) {
1000
+ await copyFile(resolveContained(paths.control, artifact.locator), target, constants.COPYFILE_EXCL | constants.COPYFILE_FICLONE);
1001
+ await chmod(target, 0o444);
1002
+ }
1003
+ const info = await lstat(target);
1004
+ if (!info.isFile() ||
1005
+ info.isSymbolicLink() ||
1006
+ info.size !== artifact.bytes ||
1007
+ (await sha256File(target)) !== artifact.sha256)
1008
+ throw scientificGateError("Scientific review source bytes changed while staging.", role);
1009
+ stageOutputs.push({
1010
+ path,
1011
+ sha256: artifact.sha256,
1012
+ bytes: artifact.bytes,
1013
+ purpose: "acquired-evidence",
1014
+ ownerId: artifact.artifactId,
1015
+ sourceLocator: artifact.locator,
1016
+ hashBasis: "raw-file-bytes",
1017
+ mediaType: artifact.mediaType,
1018
+ objectKind: null,
1019
+ registrationRecordSha256: null,
1020
+ });
1021
+ }
1022
+ }
911
1023
  if (role !== "evidence-construct") {
912
1024
  if (canaryArtifactPaths.length)
913
1025
  throw canaryArtifactError("Only evidence-construct accepts canary artifacts.");
@@ -981,6 +1093,32 @@ async function stageInputRecords(root, project, role, design, assessment, canary
981
1093
  }
982
1094
  return [...stageOutputs, ...canaryRecords];
983
1095
  }
1096
+ async function modelObjectInputs(root, projectId, design) {
1097
+ const records = [];
1098
+ for (const model of design.identity.modelStructures) {
1099
+ if (model.implementationStatus === "executable-frozen")
1100
+ records.push(await promoteRegisteredModelObject({
1101
+ root,
1102
+ projectId,
1103
+ objectKind: "model-implementation",
1104
+ sourceLocator: model.implementationArtifactLocator,
1105
+ sha256: model.implementationArtifactSha256,
1106
+ purpose: "model-implementation",
1107
+ ownerId: model.id,
1108
+ }));
1109
+ if (model.environmentLockStatus === "exact-frozen")
1110
+ records.push(await promoteRegisteredModelObject({
1111
+ root,
1112
+ projectId,
1113
+ objectKind: "environment-lock",
1114
+ sourceLocator: model.environmentLockLocator,
1115
+ sha256: model.environmentLockSha256,
1116
+ purpose: "model-environment-lock",
1117
+ ownerId: model.id,
1118
+ }));
1119
+ }
1120
+ return records;
1121
+ }
984
1122
  async function promoteRegisteredModelObject(input) {
985
1123
  const resolvedObject = await resolveScientificObjectBinding({
986
1124
  root: input.root,
@@ -1116,19 +1254,7 @@ async function assessmentEvidenceContext(root, project, role, stageInputs) {
1116
1254
  };
1117
1255
  }
1118
1256
  async function loadBoundScientificDesign(root, project) {
1119
- const binding = project.scientificDesign;
1120
- if (!binding)
1121
- throw scientificGateError("Project has no scientific design binding.");
1122
- const path = join(workspacePaths(root).control, binding.objectLocator);
1123
- const value = await readExactJson(path, "Scientific design", "RESEARCH_SCIENTIFIC_GATE_INVALID");
1124
- const design = parseScientificDesign(value);
1125
- const normalized = normalizedJson(design);
1126
- if (design.projectId !== project.id ||
1127
- sha256Text(normalized) !== binding.designSha256 ||
1128
- (await readFile(path, "utf8")) !== normalized) {
1129
- throw scientificGateError("Frozen scientific design failed its exact binding.");
1130
- }
1131
- return design;
1257
+ return (await loadScientificFulfillmentView(root, project)).contract;
1132
1258
  }
1133
1259
  async function loadBoundPacket(root, project, role, packetSha256) {
1134
1260
  const path = join(workspacePaths(root).projects, project.id, "scientific", "review-packets", role, `${packetSha256}.json`);
@@ -1143,6 +1269,20 @@ async function loadBoundPacket(root, project, role, packetSha256) {
1143
1269
  throw scientificGateError("Scientific review packet failed its immutable hash binding.", role);
1144
1270
  }
1145
1271
  await assertBoundPolicyObject(root, project, value.policy);
1272
+ const fulfillment = await loadScientificFulfillmentView(root, project, role);
1273
+ if (value.design.fulfillment) {
1274
+ if (value.design.fulfillment.effectiveSha256 !== fulfillment.effectiveSha256 ||
1275
+ (value.design.fulfillment.headSha256 !== null &&
1276
+ !fulfillment.records.some((record) => record.recordSha256 === value.design.fulfillment.headSha256))) {
1277
+ throw scientificGateError("Scientific review has a stale or uncommitted fulfillment view.", role);
1278
+ }
1279
+ }
1280
+ else if (canonicalJson(fulfillment.contract) !== canonicalJson(fulfillment.base)) {
1281
+ throw scientificGateError("Scientific review does not bind the fulfilled scientific objects.", role);
1282
+ }
1283
+ if (canonicalJson(value.taskContract ?? null) !== canonicalJson(await taskContext(root, project.id))) {
1284
+ throw scientificGateError("Scientific review no longer binds the current authorized task scope.", role);
1285
+ }
1146
1286
  await assertBoundStageInputs(root, value.stageInputs, role);
1147
1287
  return value;
1148
1288
  }
@@ -1305,6 +1445,13 @@ function isScientificReviewPacket(value, projectId, role, finalPublicationReview
1305
1445
  isObject(value.design) &&
1306
1446
  typeof value.design.sha256 === "string" &&
1307
1447
  typeof value.design.objectLocator === "string" &&
1448
+ (value.design.fulfillment === undefined ||
1449
+ (isObject(value.design.fulfillment) &&
1450
+ (value.design.fulfillment.headSha256 === null ||
1451
+ (typeof value.design.fulfillment.headSha256 === "string" &&
1452
+ new RegExp(SHA256_PATTERN).test(value.design.fulfillment.headSha256))) &&
1453
+ typeof value.design.fulfillment.effectiveSha256 === "string" &&
1454
+ new RegExp(SHA256_PATTERN).test(value.design.fulfillment.effectiveSha256))) &&
1308
1455
  isObject(value.policy) &&
1309
1456
  typeof value.policy.resolvedPolicySha256 === "string" &&
1310
1457
  typeof value.policy.approvalSha256 === "string" &&
@@ -1330,6 +1477,11 @@ function isScientificReviewPacket(value, projectId, role, finalPublicationReview
1330
1477
  "inherited-gap",
1331
1478
  "model-implementation",
1332
1479
  "model-environment-lock",
1480
+ "scientific-fulfillment",
1481
+ "effective-scientific-design",
1482
+ "original-request-source",
1483
+ "native-calculation",
1484
+ "acquired-evidence",
1333
1485
  "stage-output",
1334
1486
  ].includes(String(record.purpose)) &&
1335
1487
  typeof record.ownerId === "string" &&