@tiangong-ai/cli 0.0.60 → 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 (73) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +167 -26
  3. package/dist/research/orchestration.js +108 -18
  4. package/dist/research/orchestration.js.map +1 -1
  5. package/dist/research/workspace/acquisition-routes.d.ts +1 -1
  6. package/dist/research/workspace/acquisition-routes.js +2 -11
  7. package/dist/research/workspace/acquisition-routes.js.map +1 -1
  8. package/dist/research/workspace/artifact-read-audit.d.ts +16 -0
  9. package/dist/research/workspace/artifact-read-audit.js +176 -0
  10. package/dist/research/workspace/artifact-read-audit.js.map +1 -0
  11. package/dist/research/workspace/artifact-trace.d.ts +12 -0
  12. package/dist/research/workspace/artifact-trace.js +109 -0
  13. package/dist/research/workspace/artifact-trace.js.map +1 -0
  14. package/dist/research/workspace/artifact-view-mcp.d.ts +75 -0
  15. package/dist/research/workspace/artifact-view-mcp.js +214 -0
  16. package/dist/research/workspace/artifact-view-mcp.js.map +1 -0
  17. package/dist/research/workspace/artifact-views.d.ts +100 -0
  18. package/dist/research/workspace/artifact-views.js +395 -0
  19. package/dist/research/workspace/artifact-views.js.map +1 -0
  20. package/dist/research/workspace/audit-bundle.d.ts +4 -0
  21. package/dist/research/workspace/audit-bundle.js +107 -8
  22. package/dist/research/workspace/audit-bundle.js.map +1 -1
  23. package/dist/research/workspace/executor.d.ts +5 -1
  24. package/dist/research/workspace/executor.js +106 -22
  25. package/dist/research/workspace/executor.js.map +1 -1
  26. package/dist/research/workspace/native-run.d.ts +77 -0
  27. package/dist/research/workspace/native-run.js +637 -0
  28. package/dist/research/workspace/native-run.js.map +1 -0
  29. package/dist/research/workspace/preflight.d.ts +8 -2
  30. package/dist/research/workspace/preflight.js +17 -14
  31. package/dist/research/workspace/preflight.js.map +1 -1
  32. package/dist/research/workspace/project-mutations.d.ts +1 -1
  33. package/dist/research/workspace/project-mutations.js +5 -3
  34. package/dist/research/workspace/project-mutations.js.map +1 -1
  35. package/dist/research/workspace/projects.js +1 -0
  36. package/dist/research/workspace/projects.js.map +1 -1
  37. package/dist/research/workspace/request-provenance.d.ts +67 -0
  38. package/dist/research/workspace/request-provenance.js +115 -0
  39. package/dist/research/workspace/request-provenance.js.map +1 -0
  40. package/dist/research/workspace/review-executor.js +22 -11
  41. package/dist/research/workspace/review-executor.js.map +1 -1
  42. package/dist/research/workspace/runtime.d.ts +52 -0
  43. package/dist/research/workspace/runtime.js +221 -98
  44. package/dist/research/workspace/runtime.js.map +1 -1
  45. package/dist/research/workspace/sanitization.js +1 -1
  46. package/dist/research/workspace/sanitization.js.map +1 -1
  47. package/dist/research/workspace/schema-compatibility.d.ts +5 -0
  48. package/dist/research/workspace/schema-compatibility.js +55 -0
  49. package/dist/research/workspace/schema-compatibility.js.map +1 -0
  50. package/dist/research/workspace/scientific-design.js +8 -17
  51. package/dist/research/workspace/scientific-design.js.map +1 -1
  52. package/dist/research/workspace/scientific-fulfillment-audit.d.ts +3 -0
  53. package/dist/research/workspace/scientific-fulfillment-audit.js +137 -0
  54. package/dist/research/workspace/scientific-fulfillment-audit.js.map +1 -0
  55. package/dist/research/workspace/scientific-fulfillment.d.ts +121 -0
  56. package/dist/research/workspace/scientific-fulfillment.js +539 -0
  57. package/dist/research/workspace/scientific-fulfillment.js.map +1 -0
  58. package/dist/research/workspace/scientific-review-execution.js +82 -61
  59. package/dist/research/workspace/scientific-review-execution.js.map +1 -1
  60. package/dist/research/workspace/scientific-review.d.ts +5 -1
  61. package/dist/research/workspace/scientific-review.js +197 -52
  62. package/dist/research/workspace/scientific-review.js.map +1 -1
  63. package/dist/research/workspace/setup-catalog.js +2 -2
  64. package/dist/research/workspace/task-acceptance.d.ts +10 -2
  65. package/dist/research/workspace/task-acceptance.js +72 -8
  66. package/dist/research/workspace/task-acceptance.js.map +1 -1
  67. package/dist/research/workspace/task-audit.js +52 -2
  68. package/dist/research/workspace/task-audit.js.map +1 -1
  69. package/dist/research/workspace/task-contract.d.ts +4 -1
  70. package/dist/research/workspace/task-contract.js +26 -5
  71. package/dist/research/workspace/task-contract.js.map +1 -1
  72. package/dist/research/workspace/types.d.ts +6 -3
  73. package/package.json +1 -1
@@ -1,15 +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";
5
6
  import { taskContext } from "./task-contract.js";
7
+ import { nativeRunArtifactRecords } from "./native-run.js";
8
+ import { loadEvidenceArtifactRecords } from "./artifacts.js";
6
9
  import { sourceTypeRequirementGaps } from "./evidence-role-coverage.js";
7
10
  import { appendJournalEvent, verifyJournal } from "./journal.js";
8
11
  import { loadProject, nextScientificGate, saveProject } from "./projects.js";
9
12
  import { projectWithEffectiveAuthority, readProjectAuthorityIndex } from "./project-authority.js";
10
13
  import { assertResearchPolicyBinding } from "./research-policy.js";
11
- import { evaluateScientificDesign, parseScientificDesign, scientificDesignPolicyGaps, } from "./scientific-design.js";
14
+ import { evaluateScientificDesign, scientificDesignPolicyGaps, } from "./scientific-design.js";
12
15
  import { resolveScientificObjectBinding } from "./scientific-objects.js";
16
+ import { loadScientificFulfillmentView, scientificFulfillmentLocator, } from "./scientific-fulfillment.js";
13
17
  import { sanitizeResearchValue } from "./sanitization.js";
14
18
  import { canonicalJson, fileRecord, isObject, pathExists, resolveContained, sha256File, sha256Text, workspacePaths, writeBytesAtomic, writeJsonAtomic, writeTextAtomic, } from "./storage.js";
15
19
  import { loadWorkspaceConfig, verifyDoctorAttestation, withWorkspaceLock } from "./workspace.js";
@@ -265,7 +269,8 @@ export async function prepareScientificReview(input) {
265
269
  exitCode: 3,
266
270
  });
267
271
  }
268
- const design = await loadBoundScientificDesign(input.root, project);
272
+ const fulfillment = await loadScientificFulfillmentView(input.root, project, input.role);
273
+ const design = fulfillment.contract;
269
274
  const assessment = await readAssessment(input.assessmentPath, input.role);
270
275
  if (assessment.designSha256 !== project.scientificDesign.designSha256) {
271
276
  throw new CliError("Scientific assessment does not match the frozen design.", {
@@ -274,6 +279,68 @@ export async function prepareScientificReview(input) {
274
279
  });
275
280
  }
276
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
+ }
277
344
  const assessmentSha256 = exactJsonSha256(assessment);
278
345
  const assessmentLocator = `projects/${project.id}/scientific/assessments/${input.role}/${assessmentSha256}.json`;
279
346
  await writeImmutableJson(join(paths.control, assessmentLocator), assessment);
@@ -289,7 +356,7 @@ export async function prepareScientificReview(input) {
289
356
  };
290
357
  await assertBoundPolicyObject(input.root, project, packetPolicy);
291
358
  const designEvaluation = evaluateScientificDesign(design);
292
- 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);
293
360
  const futureGateObligations = scientificFutureGateObligations(design, input.role);
294
361
  const packetCore = {
295
362
  schemaVersion: 1,
@@ -299,12 +366,16 @@ export async function prepareScientificReview(input) {
299
366
  design: {
300
367
  sha256: project.scientificDesign.designSha256,
301
368
  objectLocator: project.scientificDesign.objectLocator,
369
+ fulfillment: {
370
+ headSha256: fulfillment.headSha256,
371
+ effectiveSha256: fulfillment.effectiveSha256,
372
+ },
302
373
  },
303
374
  policy: packetPolicy,
304
375
  reviewer: { agent: input.reviewerAgent, sessionSha256: reviewerSessionSha256 },
305
376
  preparedAt: new Date().toISOString(),
306
377
  stageInputs,
307
- taskContract: await taskContext(input.root, project.id),
378
+ taskContract: currentTask,
308
379
  assessment: { sha256: assessmentSha256, objectLocator: assessmentLocator },
309
380
  mechanicalAssessment: {
310
381
  canPass: issues.length === 0 && assessment.recommendation === "pass",
@@ -331,7 +402,10 @@ export async function prepareScientificReview(input) {
331
402
  newGenerationOnMaterialChange: true,
332
403
  revisionReserveIncluded: true,
333
404
  },
334
- 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
+ ],
335
409
  };
336
410
  const packetSha256 = sha256Text(canonicalJson(packetCore));
337
411
  const packet = { ...packetCore, packetSha256 };
@@ -503,8 +577,9 @@ export { readReview as readScientificReviewOutput };
503
577
  export async function assertScientificGateForStage(root, project, stage) {
504
578
  if (!project.scientificDesign)
505
579
  return;
506
- const design = await loadBoundScientificDesign(root, project);
507
580
  for (const role of requiredGateRoles(stage)) {
581
+ const fulfillment = await loadScientificFulfillmentView(root, project, role);
582
+ const design = fulfillment.contract;
508
583
  const gate = project.scientificDesign.gates[role];
509
584
  if (gate.status !== "passed" || !gate.packetSha256 || !gate.reviewSha256) {
510
585
  throw new CliError(`Scientific ${role} review must pass before ${stage}.`, {
@@ -516,7 +591,7 @@ export async function assertScientificGateForStage(root, project, stage) {
516
591
  const packet = await loadBoundPacket(root, project, role, gate.packetSha256);
517
592
  const review = await loadBoundReview(root, project, role, gate.reviewSha256);
518
593
  const assessment = await loadBoundAssessment(root, project, role, packet);
519
- 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);
520
595
  if (packet.assessment.sha256 !== gate.assessmentSha256 ||
521
596
  packet.reviewer.sessionSha256 !== gate.reviewerSessionSha256 ||
522
597
  review.packetSha256 !== packet.packetSha256 ||
@@ -550,7 +625,7 @@ export async function inspectScientificReviewStatus(root, projectId, knownProjec
550
625
  gates: structuredClone(project.scientificDesign.gates),
551
626
  };
552
627
  }
553
- function evaluateAssessment(role, assessment, design, requirements, policy, evidenceContext = null) {
628
+ function evaluateAssessment(role, assessment, design, requirements, policy, evidenceContext = null, deferredObjectRuleIds = []) {
554
629
  const issues = new Map();
555
630
  const add = (code, message, objectIds = []) => {
556
631
  if (!issues.has(code))
@@ -566,25 +641,26 @@ function evaluateAssessment(role, assessment, design, requirements, policy, evid
566
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]);
567
642
  }
568
643
  const duePolicyRules = design.policyRuleDispositions.filter((disposition) => disposition.status === "planned" &&
644
+ !deferredObjectRuleIds.includes(disposition.ruleId) &&
569
645
  disposition.dueGate !== "publication-freeze" &&
570
646
  gateRank[disposition.dueGate] <= gateRank[role]);
571
647
  if (duePolicyRules.length) {
572
- 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));
573
649
  }
574
650
  const dueUncertaintyFreezes = design.uncertaintyParameters.filter((parameter) => parameter.stateValueStatus === "pending-source-acquisition" &&
575
651
  gateRank[parameter.freezeBeforeGate] <= gateRank[role]);
576
652
  if (dueUncertaintyFreezes.length) {
577
- 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));
578
654
  }
579
655
  const dueModelImplementations = design.identity.modelStructures.filter((model) => model.implementationStatus === "pending-source-acquisition" &&
580
656
  gateRank[model.implementationFreezeBeforeGate] <= gateRank[role]);
581
657
  if (dueModelImplementations.length) {
582
- 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));
583
659
  }
584
660
  const dueModelEnvironmentLocks = design.identity.modelStructures.filter((model) => model.environmentLockStatus === "pending-runtime-lock" &&
585
661
  gateRank[model.environmentLockFreezeBeforeGate] <= gateRank[role]);
586
662
  if (dueModelEnvironmentLocks.length) {
587
- 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));
588
664
  }
589
665
  if (role === "research-design") {
590
666
  const value = assessment;
@@ -848,30 +924,7 @@ async function stageInputRecords(root, project, role, design, assessment, canary
848
924
  registrationRecordSha256: null,
849
925
  });
850
926
  };
851
- for (const model of design.identity.modelStructures) {
852
- if (model.implementationStatus === "executable-frozen") {
853
- records.push(await promoteRegisteredModelObject({
854
- root,
855
- projectId: project.id,
856
- objectKind: "model-implementation",
857
- sourceLocator: model.implementationArtifactLocator,
858
- sha256: model.implementationArtifactSha256,
859
- purpose: "model-implementation",
860
- ownerId: model.id,
861
- }));
862
- }
863
- if (model.environmentLockStatus === "exact-frozen") {
864
- records.push(await promoteRegisteredModelObject({
865
- root,
866
- projectId: project.id,
867
- objectKind: "environment-lock",
868
- sourceLocator: model.environmentLockLocator,
869
- sha256: model.environmentLockSha256,
870
- purpose: "model-environment-lock",
871
- ownerId: model.id,
872
- }));
873
- }
874
- }
927
+ records.push(...(await modelObjectInputs(root, project.id, design)));
875
928
  for (const gap of design.knownGaps) {
876
929
  for (const artifact of gap.sourceArtifacts) {
877
930
  await promoteLegacyJson({
@@ -895,8 +948,7 @@ async function stageInputRecords(root, project, role, design, assessment, canary
895
948
  });
896
949
  }
897
950
  const relativeOutputs = ["outputs/acquisition.json", "outputs/evidence-snapshot.json"];
898
- if (role === "evidence-construct" &&
899
- (await pathExists(join(projectRoot, "outputs", "content-snapshot.json")))) {
951
+ if (await pathExists(join(projectRoot, "outputs", "content-snapshot.json"))) {
900
952
  relativeOutputs.push("outputs/content-snapshot.json");
901
953
  }
902
954
  const stageOutputs = await Promise.all(relativeOutputs.map(async (relativePath) => {
@@ -912,6 +964,62 @@ async function stageInputRecords(root, project, role, design, assessment, canary
912
964
  registrationRecordSha256: null,
913
965
  };
914
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
+ }
915
1023
  if (role !== "evidence-construct") {
916
1024
  if (canaryArtifactPaths.length)
917
1025
  throw canaryArtifactError("Only evidence-construct accepts canary artifacts.");
@@ -985,6 +1093,32 @@ async function stageInputRecords(root, project, role, design, assessment, canary
985
1093
  }
986
1094
  return [...stageOutputs, ...canaryRecords];
987
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
+ }
988
1122
  async function promoteRegisteredModelObject(input) {
989
1123
  const resolvedObject = await resolveScientificObjectBinding({
990
1124
  root: input.root,
@@ -1120,19 +1254,7 @@ async function assessmentEvidenceContext(root, project, role, stageInputs) {
1120
1254
  };
1121
1255
  }
1122
1256
  async function loadBoundScientificDesign(root, project) {
1123
- const binding = project.scientificDesign;
1124
- if (!binding)
1125
- throw scientificGateError("Project has no scientific design binding.");
1126
- const path = join(workspacePaths(root).control, binding.objectLocator);
1127
- const value = await readExactJson(path, "Scientific design", "RESEARCH_SCIENTIFIC_GATE_INVALID");
1128
- const design = parseScientificDesign(value);
1129
- const normalized = normalizedJson(design);
1130
- if (design.projectId !== project.id ||
1131
- sha256Text(normalized) !== binding.designSha256 ||
1132
- (await readFile(path, "utf8")) !== normalized) {
1133
- throw scientificGateError("Frozen scientific design failed its exact binding.");
1134
- }
1135
- return design;
1257
+ return (await loadScientificFulfillmentView(root, project)).contract;
1136
1258
  }
1137
1259
  async function loadBoundPacket(root, project, role, packetSha256) {
1138
1260
  const path = join(workspacePaths(root).projects, project.id, "scientific", "review-packets", role, `${packetSha256}.json`);
@@ -1147,6 +1269,17 @@ async function loadBoundPacket(root, project, role, packetSha256) {
1147
1269
  throw scientificGateError("Scientific review packet failed its immutable hash binding.", role);
1148
1270
  }
1149
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
+ }
1150
1283
  if (canonicalJson(value.taskContract ?? null) !== canonicalJson(await taskContext(root, project.id))) {
1151
1284
  throw scientificGateError("Scientific review no longer binds the current authorized task scope.", role);
1152
1285
  }
@@ -1312,6 +1445,13 @@ function isScientificReviewPacket(value, projectId, role, finalPublicationReview
1312
1445
  isObject(value.design) &&
1313
1446
  typeof value.design.sha256 === "string" &&
1314
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))) &&
1315
1455
  isObject(value.policy) &&
1316
1456
  typeof value.policy.resolvedPolicySha256 === "string" &&
1317
1457
  typeof value.policy.approvalSha256 === "string" &&
@@ -1337,6 +1477,11 @@ function isScientificReviewPacket(value, projectId, role, finalPublicationReview
1337
1477
  "inherited-gap",
1338
1478
  "model-implementation",
1339
1479
  "model-environment-lock",
1480
+ "scientific-fulfillment",
1481
+ "effective-scientific-design",
1482
+ "original-request-source",
1483
+ "native-calculation",
1484
+ "acquired-evidence",
1340
1485
  "stage-output",
1341
1486
  ].includes(String(record.purpose)) &&
1342
1487
  typeof record.ownerId === "string" &&