@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.
- package/AGENTS.md +2 -2
- package/README.md +167 -26
- package/dist/research/orchestration.js +108 -18
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/workspace/acquisition-routes.d.ts +1 -1
- package/dist/research/workspace/acquisition-routes.js +2 -11
- package/dist/research/workspace/acquisition-routes.js.map +1 -1
- package/dist/research/workspace/artifact-read-audit.d.ts +16 -0
- package/dist/research/workspace/artifact-read-audit.js +176 -0
- package/dist/research/workspace/artifact-read-audit.js.map +1 -0
- package/dist/research/workspace/artifact-trace.d.ts +12 -0
- package/dist/research/workspace/artifact-trace.js +109 -0
- package/dist/research/workspace/artifact-trace.js.map +1 -0
- package/dist/research/workspace/artifact-view-mcp.d.ts +75 -0
- package/dist/research/workspace/artifact-view-mcp.js +214 -0
- package/dist/research/workspace/artifact-view-mcp.js.map +1 -0
- package/dist/research/workspace/artifact-views.d.ts +100 -0
- package/dist/research/workspace/artifact-views.js +395 -0
- package/dist/research/workspace/artifact-views.js.map +1 -0
- package/dist/research/workspace/audit-bundle.d.ts +4 -0
- package/dist/research/workspace/audit-bundle.js +107 -8
- package/dist/research/workspace/audit-bundle.js.map +1 -1
- package/dist/research/workspace/executor.d.ts +5 -1
- package/dist/research/workspace/executor.js +106 -22
- package/dist/research/workspace/executor.js.map +1 -1
- package/dist/research/workspace/native-run.d.ts +77 -0
- package/dist/research/workspace/native-run.js +637 -0
- package/dist/research/workspace/native-run.js.map +1 -0
- package/dist/research/workspace/preflight.d.ts +8 -2
- package/dist/research/workspace/preflight.js +17 -14
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/project-mutations.d.ts +1 -1
- package/dist/research/workspace/project-mutations.js +5 -3
- package/dist/research/workspace/project-mutations.js.map +1 -1
- package/dist/research/workspace/projects.js +1 -0
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/request-provenance.d.ts +67 -0
- package/dist/research/workspace/request-provenance.js +115 -0
- package/dist/research/workspace/request-provenance.js.map +1 -0
- package/dist/research/workspace/review-executor.js +22 -11
- package/dist/research/workspace/review-executor.js.map +1 -1
- package/dist/research/workspace/runtime.d.ts +52 -0
- package/dist/research/workspace/runtime.js +221 -98
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/sanitization.js +1 -1
- package/dist/research/workspace/sanitization.js.map +1 -1
- package/dist/research/workspace/schema-compatibility.d.ts +5 -0
- package/dist/research/workspace/schema-compatibility.js +55 -0
- package/dist/research/workspace/schema-compatibility.js.map +1 -0
- package/dist/research/workspace/scientific-design.js +8 -17
- package/dist/research/workspace/scientific-design.js.map +1 -1
- package/dist/research/workspace/scientific-fulfillment-audit.d.ts +3 -0
- package/dist/research/workspace/scientific-fulfillment-audit.js +137 -0
- package/dist/research/workspace/scientific-fulfillment-audit.js.map +1 -0
- package/dist/research/workspace/scientific-fulfillment.d.ts +121 -0
- package/dist/research/workspace/scientific-fulfillment.js +539 -0
- package/dist/research/workspace/scientific-fulfillment.js.map +1 -0
- package/dist/research/workspace/scientific-review-execution.js +82 -61
- package/dist/research/workspace/scientific-review-execution.js.map +1 -1
- package/dist/research/workspace/scientific-review.d.ts +5 -1
- package/dist/research/workspace/scientific-review.js +197 -52
- package/dist/research/workspace/scientific-review.js.map +1 -1
- package/dist/research/workspace/setup-catalog.js +2 -2
- package/dist/research/workspace/task-acceptance.d.ts +10 -2
- package/dist/research/workspace/task-acceptance.js +72 -8
- package/dist/research/workspace/task-acceptance.js.map +1 -1
- package/dist/research/workspace/task-audit.js +52 -2
- package/dist/research/workspace/task-audit.js.map +1 -1
- package/dist/research/workspace/task-contract.d.ts +4 -1
- package/dist/research/workspace/task-contract.js +26 -5
- package/dist/research/workspace/task-contract.js.map +1 -1
- package/dist/research/workspace/types.d.ts +6 -3
- package/package.json +1 -1
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2
|
-
import {
|
|
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,
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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" &&
|