@tiangong-ai/cli 0.0.40 → 0.0.42
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 +11 -6
- package/dist/research/orchestration.js +28 -12
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/workspace/projects.js +1 -1
- package/dist/research/workspace/runtime.d.ts +1 -0
- package/dist/research/workspace/runtime.js +26 -15
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/scientific-review.d.ts +2 -1
- package/dist/research/workspace/scientific-review.js +229 -24
- package/dist/research/workspace/scientific-review.js.map +1 -1
- package/dist/research/workspace/setup-catalog.js +3 -3
- package/package.json +1 -1
|
@@ -15,7 +15,7 @@ interface ScientificReviewStageInput {
|
|
|
15
15
|
path: string;
|
|
16
16
|
sha256: string;
|
|
17
17
|
bytes: number;
|
|
18
|
-
purpose: "inherited-gap" | "model-implementation" | "model-environment-lock" | "stage-output";
|
|
18
|
+
purpose: "construct-canary" | "inherited-gap" | "model-implementation" | "model-environment-lock" | "stage-output";
|
|
19
19
|
ownerId: string;
|
|
20
20
|
sourceLocator: string;
|
|
21
21
|
hashBasis: "raw-file-bytes";
|
|
@@ -85,6 +85,7 @@ export declare function prepareScientificReview(input: {
|
|
|
85
85
|
assessmentPath: string;
|
|
86
86
|
reviewerAgent: AgentKind;
|
|
87
87
|
reviewerSessionId: string;
|
|
88
|
+
canaryArtifactPaths?: string[];
|
|
88
89
|
}): Promise<ScientificReviewPacket>;
|
|
89
90
|
export declare function submitScientificReview(input: {
|
|
90
91
|
root: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2
2
|
import { lstat, readFile } from "node:fs/promises";
|
|
3
|
-
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { CliError } from "../../errors.js";
|
|
5
5
|
import { appendJournalEvent, verifyJournal } from "./journal.js";
|
|
6
6
|
import { loadProject, nextScientificGate, saveProject } from "./projects.js";
|
|
@@ -268,7 +268,7 @@ export async function prepareScientificReview(input) {
|
|
|
268
268
|
exitCode: 3,
|
|
269
269
|
});
|
|
270
270
|
}
|
|
271
|
-
const stageInputs = await stageInputRecords(input.root, project, input.role, design);
|
|
271
|
+
const stageInputs = await stageInputRecords(input.root, project, input.role, design, assessment, input.canaryArtifactPaths ?? []);
|
|
272
272
|
const assessmentSha256 = exactJsonSha256(assessment);
|
|
273
273
|
const assessmentLocator = `projects/${project.id}/scientific/assessments/${input.role}/${assessmentSha256}.json`;
|
|
274
274
|
await writeImmutableJson(join(paths.control, assessmentLocator), assessment);
|
|
@@ -284,7 +284,7 @@ export async function prepareScientificReview(input) {
|
|
|
284
284
|
};
|
|
285
285
|
await assertBoundPolicyObject(input.root, project, packetPolicy);
|
|
286
286
|
const designEvaluation = evaluateScientificDesign(design);
|
|
287
|
-
const issues = evaluateAssessment(input.role, assessment, design, project.evidenceRequirements, project.publicationPolicy);
|
|
287
|
+
const issues = evaluateAssessment(input.role, assessment, design, project.evidenceRequirements, project.publicationPolicy, await assessmentEvidenceContext(input.root, project, input.role, stageInputs));
|
|
288
288
|
const futureGateObligations = scientificFutureGateObligations(design, input.role);
|
|
289
289
|
const packetCore = {
|
|
290
290
|
schemaVersion: 1,
|
|
@@ -440,7 +440,7 @@ export async function assertScientificGateForStage(root, project, stage) {
|
|
|
440
440
|
const packet = await loadBoundPacket(root, project, role, gate.packetSha256);
|
|
441
441
|
const review = await loadBoundReview(root, project, role, gate.reviewSha256);
|
|
442
442
|
const assessment = await loadBoundAssessment(root, project, role, packet);
|
|
443
|
-
const currentIssues = evaluateAssessment(role, assessment, design, project.evidenceRequirements, project.publicationPolicy).map((issue) => issue.code);
|
|
443
|
+
const currentIssues = evaluateAssessment(role, assessment, design, project.evidenceRequirements, project.publicationPolicy, await assessmentEvidenceContext(root, project, role, packet.stageInputs)).map((issue) => issue.code);
|
|
444
444
|
if (packet.assessment.sha256 !== gate.assessmentSha256 ||
|
|
445
445
|
packet.reviewer.sessionSha256 !== gate.reviewerSessionSha256 ||
|
|
446
446
|
review.packetSha256 !== packet.packetSha256 ||
|
|
@@ -473,7 +473,7 @@ export async function inspectScientificReviewStatus(root, projectId) {
|
|
|
473
473
|
gates: structuredClone(project.scientificDesign.gates),
|
|
474
474
|
};
|
|
475
475
|
}
|
|
476
|
-
function evaluateAssessment(role, assessment, design, requirements, policy) {
|
|
476
|
+
function evaluateAssessment(role, assessment, design, requirements, policy, evidenceContext = null) {
|
|
477
477
|
const issues = new Map();
|
|
478
478
|
const add = (code, message, objectIds = []) => {
|
|
479
479
|
if (!issues.has(code))
|
|
@@ -529,38 +529,102 @@ function evaluateAssessment(role, assessment, design, requirements, policy) {
|
|
|
529
529
|
canary.artifactSha256s.length < 1) {
|
|
530
530
|
add("CANARY_NOT_REAL", "Construct canary must use real records without inspecting outcomes.");
|
|
531
531
|
}
|
|
532
|
+
const declaredCanarySha256s = new Set(canary.artifactSha256s);
|
|
533
|
+
if (!evidenceContext ||
|
|
534
|
+
declaredCanarySha256s.size !== canary.artifactSha256s.length ||
|
|
535
|
+
declaredCanarySha256s.size !== evidenceContext.canaryArtifactSha256s.size ||
|
|
536
|
+
[...declaredCanarySha256s].some((sha256) => !evidenceContext.canaryArtifactSha256s.has(sha256))) {
|
|
537
|
+
add("CANARY_ARTIFACT_UNBOUND", "Every construct-canary digest must bind one exact promoted artifact and no undeclared artifact is permitted.");
|
|
538
|
+
}
|
|
532
539
|
const centralEdges = design.edges
|
|
533
540
|
.filter((edge) => edge.role === "central")
|
|
534
541
|
.map((edge) => edge.id);
|
|
542
|
+
const knownEdgeIds = new Set(design.edges.map((edge) => edge.id));
|
|
543
|
+
const unknownCanaryEdgeIds = [...canary.constructedEdgeIds, ...canary.failedEdgeIds].filter((edgeId) => !knownEdgeIds.has(edgeId));
|
|
544
|
+
if (unknownCanaryEdgeIds.length) {
|
|
545
|
+
add("CANARY_EDGE_ID_UNKNOWN", "Construct-canary edge IDs must exist in the frozen scientific design.", [...new Set(unknownCanaryEdgeIds)]);
|
|
546
|
+
}
|
|
535
547
|
const unconstructed = centralEdges.filter((edgeId) => !canary.constructedEdgeIds.includes(edgeId) || canary.failedEdgeIds.includes(edgeId));
|
|
536
548
|
if (unconstructed.length) {
|
|
537
549
|
add("CENTRAL_EDGE_UNCONSTRUCTED", "Every central claim edge must survive a real-record construct canary.", unconstructed);
|
|
538
550
|
}
|
|
551
|
+
const expectedRoleIds = new Set(design.evidenceRoles.filter((item) => item.required).map((item) => item.id));
|
|
552
|
+
const providedRoleIds = value.evidenceRoleCoverage.map((coverage) => coverage.roleId);
|
|
553
|
+
if (new Set(providedRoleIds).size !== providedRoleIds.length ||
|
|
554
|
+
providedRoleIds.some((roleId) => !expectedRoleIds.has(roleId))) {
|
|
555
|
+
add("EVIDENCE_ROLE_COVERAGE_INVALID", "Evidence-role coverage must contain only distinct required roles from the frozen design.");
|
|
556
|
+
}
|
|
557
|
+
const allIndependentSourceIds = new Set();
|
|
558
|
+
const allFullTextSourceIds = new Set();
|
|
559
|
+
const allDatedSourceIds = new Set();
|
|
539
560
|
for (const required of design.evidenceRoles.filter((item) => item.required)) {
|
|
540
561
|
const coverage = value.evidenceRoleCoverage.find((item) => item.roleId === required.id);
|
|
562
|
+
const independentIds = new Set(coverage?.independentSourceIds ?? []);
|
|
563
|
+
const fullTextIds = new Set(coverage?.fullTextSourceIds ?? []);
|
|
564
|
+
const datedIds = new Set(coverage?.datedSourceIds ?? []);
|
|
565
|
+
const peerReviewedIds = new Set(coverage?.peerReviewedSourceIds ?? []);
|
|
566
|
+
const referencedIds = new Set([
|
|
567
|
+
...independentIds,
|
|
568
|
+
...fullTextIds,
|
|
569
|
+
...datedIds,
|
|
570
|
+
...peerReviewedIds,
|
|
571
|
+
]);
|
|
572
|
+
const unknownIds = [...referencedIds].filter((sourceId) => !evidenceContext?.sources.has(sourceId));
|
|
573
|
+
if (unknownIds.length) {
|
|
574
|
+
add("EVIDENCE_SOURCE_ID_UNKNOWN", "Evidence-role coverage may reference only source IDs from the frozen post-acquisition snapshot.", unknownIds);
|
|
575
|
+
}
|
|
576
|
+
const knownIndependent = [...independentIds].filter((sourceId) => evidenceContext?.sources.has(sourceId));
|
|
577
|
+
const validFullText = [...fullTextIds].filter((sourceId) => evidenceContext?.sources.get(sourceId)?.fullTextAvailable === true);
|
|
578
|
+
const validDated = [...datedIds].filter((sourceId) => Boolean(evidenceContext?.sources.get(sourceId)?.publicationDate));
|
|
579
|
+
const validPeerReviewed = [...peerReviewedIds].filter((sourceId) => {
|
|
580
|
+
const source = evidenceContext?.sources.get(sourceId);
|
|
581
|
+
return source?.fullTextAvailable === true;
|
|
582
|
+
});
|
|
583
|
+
for (const sourceId of knownIndependent)
|
|
584
|
+
allIndependentSourceIds.add(sourceId);
|
|
585
|
+
for (const sourceId of validFullText)
|
|
586
|
+
allFullTextSourceIds.add(sourceId);
|
|
587
|
+
for (const sourceId of validDated)
|
|
588
|
+
allDatedSourceIds.add(sourceId);
|
|
589
|
+
if ([...fullTextIds].some((sourceId) => !validFullText.includes(sourceId))) {
|
|
590
|
+
add("EVIDENCE_SOURCE_FULLTEXT_INVALID", "A claimed full-text source is not producer-readable in the frozen evidence snapshot.", [required.id]);
|
|
591
|
+
}
|
|
592
|
+
if ([...datedIds].some((sourceId) => !validDated.includes(sourceId))) {
|
|
593
|
+
add("EVIDENCE_SOURCE_DATE_INVALID", "A claimed dated source has no publication date in the frozen evidence snapshot.", [required.id]);
|
|
594
|
+
}
|
|
595
|
+
if ([...peerReviewedIds].some((sourceId) => !validPeerReviewed.includes(sourceId))) {
|
|
596
|
+
add("EVIDENCE_SOURCE_PEER_REVIEW_INVALID", "A claimed peer-reviewed source must be full text in the frozen evidence snapshot; the independent reviewer verifies its publication status.", [required.id]);
|
|
597
|
+
}
|
|
541
598
|
if (!coverage ||
|
|
542
|
-
new Set(
|
|
543
|
-
new Set(
|
|
599
|
+
new Set(validFullText).size < required.minimumFullText ||
|
|
600
|
+
new Set(knownIndependent).size < required.minimumIndependentSources) {
|
|
544
601
|
add("EVIDENCE_ROLE_FULLTEXT_INSUFFICIENT", "A required evidence role lacks its frozen full-text or independence minimum.", [required.id]);
|
|
545
602
|
}
|
|
546
|
-
if (!coverage || new Set(
|
|
603
|
+
if (!coverage || new Set(validDated).size < required.minimumDatedSources) {
|
|
547
604
|
add("EVIDENCE_ROLE_DATED_INSUFFICIENT", "A required evidence role lacks its frozen dated-source minimum.", [required.id]);
|
|
548
605
|
}
|
|
549
606
|
if (required.peerReviewedRequired &&
|
|
550
|
-
(!coverage || new Set(
|
|
607
|
+
(!coverage || new Set(validPeerReviewed).size < required.minimumFullText)) {
|
|
551
608
|
add("EVIDENCE_ROLE_PEER_REVIEWED_INSUFFICIENT", "A peer-reviewed evidence role lacks its required frozen peer-reviewed full texts.", [required.id]);
|
|
552
609
|
}
|
|
553
|
-
const dimensions = new Set(
|
|
610
|
+
const dimensions = new Set(knownIndependent.flatMap((sourceId) => evidenceContext?.sources.get(sourceId)?.coverageDimensions ?? []));
|
|
611
|
+
if (coverage?.dimensionIds.some((dimension) => !dimensions.has(dimension))) {
|
|
612
|
+
add("EVIDENCE_ROLE_DIMENSION_CLAIM_INVALID", "Declared role dimensions must be supported by the role's frozen snapshot sources.", [required.id]);
|
|
613
|
+
}
|
|
554
614
|
if (required.coverageDimensionIds.some((dimension) => !dimensions.has(dimension))) {
|
|
555
615
|
add("EVIDENCE_ROLE_DIMENSION_UNCOVERED", "A required evidence role did not demonstrate every declared research dimension.", [required.id]);
|
|
556
616
|
}
|
|
557
|
-
const sourceTypes = new Set(
|
|
617
|
+
const sourceTypes = new Set(knownIndependent.flatMap((sourceId) => {
|
|
618
|
+
const sourceType = evidenceContext?.sources.get(sourceId)?.sourceType;
|
|
619
|
+
return sourceType ? [sourceType] : [];
|
|
620
|
+
}));
|
|
621
|
+
if (coverage?.sourceTypes.some((sourceType) => !sourceTypes.has(sourceType))) {
|
|
622
|
+
add("EVIDENCE_ROLE_SOURCE_TYPE_CLAIM_INVALID", "Declared role source types must be present among the role's frozen snapshot sources.", [required.id]);
|
|
623
|
+
}
|
|
558
624
|
if (required.sourceTypeRequirements.some((sourceType) => !sourceTypes.has(sourceType))) {
|
|
559
625
|
add("EVIDENCE_ROLE_SOURCE_TYPE_UNCOVERED", "A required evidence role did not demonstrate every declared source type.", [required.id]);
|
|
560
626
|
}
|
|
561
627
|
if (coverage) {
|
|
562
|
-
const independentIds = new Set(coverage.independentSourceIds);
|
|
563
|
-
const fullTextIds = new Set(coverage.fullTextSourceIds);
|
|
564
628
|
if (coverage.fullTextSourceIds.some((sourceId) => !independentIds.has(sourceId)) ||
|
|
565
629
|
coverage.datedSourceIds.some((sourceId) => !independentIds.has(sourceId)) ||
|
|
566
630
|
coverage.peerReviewedSourceIds.some((sourceId) => !independentIds.has(sourceId) || !fullTextIds.has(sourceId))) {
|
|
@@ -568,9 +632,6 @@ function evaluateAssessment(role, assessment, design, requirements, policy) {
|
|
|
568
632
|
}
|
|
569
633
|
}
|
|
570
634
|
}
|
|
571
|
-
const allIndependentSourceIds = new Set(value.evidenceRoleCoverage.flatMap((coverage) => coverage.independentSourceIds));
|
|
572
|
-
const allFullTextSourceIds = new Set(value.evidenceRoleCoverage.flatMap((coverage) => coverage.fullTextSourceIds));
|
|
573
|
-
const allDatedSourceIds = new Set(value.evidenceRoleCoverage.flatMap((coverage) => coverage.datedSourceIds));
|
|
574
635
|
if (allIndependentSourceIds.size < requirements.minSources) {
|
|
575
636
|
add("EVIDENCE_UNIQUE_SOURCE_COVERAGE_INSUFFICIENT", "Unique evidence sources across roles do not meet the project minimum.");
|
|
576
637
|
}
|
|
@@ -581,7 +642,7 @@ function evaluateAssessment(role, assessment, design, requirements, policy) {
|
|
|
581
642
|
add("EVIDENCE_UNIQUE_DATED_COVERAGE_INSUFFICIENT", "Unique dated sources across roles do not meet the project minimum.");
|
|
582
643
|
}
|
|
583
644
|
if (!value.closestWorkDispositionComplete) {
|
|
584
|
-
add("CLOSEST_WORK_DISPOSITION_INCOMPLETE", "Closest prior work must be obtained, compared, and dispositioned before
|
|
645
|
+
add("CLOSEST_WORK_DISPOSITION_INCOMPLETE", "Closest prior work must be obtained, compared, and dispositioned before analysis.");
|
|
585
646
|
}
|
|
586
647
|
if (!value.centralEvidenceFitsContext) {
|
|
587
648
|
add("CENTRAL_CONTEXT_OVERFLOW", "Central evidence does not fit the planned context and extraction route.");
|
|
@@ -651,7 +712,7 @@ function evaluateAssessment(role, assessment, design, requirements, policy) {
|
|
|
651
712
|
}
|
|
652
713
|
return [...issues.values()];
|
|
653
714
|
}
|
|
654
|
-
async function stageInputRecords(root, project, role, design) {
|
|
715
|
+
async function stageInputRecords(root, project, role, design, assessment, canaryArtifactPaths) {
|
|
655
716
|
const paths = workspacePaths(root);
|
|
656
717
|
const projectRoot = join(paths.projects, project.id);
|
|
657
718
|
if (role === "research-design") {
|
|
@@ -728,7 +789,7 @@ async function stageInputRecords(root, project, role, design) {
|
|
|
728
789
|
}
|
|
729
790
|
return records;
|
|
730
791
|
}
|
|
731
|
-
const requiredPackage =
|
|
792
|
+
const requiredPackage = "acquire";
|
|
732
793
|
const packageState = project.packages.find((item) => item.id === requiredPackage);
|
|
733
794
|
if (packageState?.status !== "complete") {
|
|
734
795
|
throw new CliError(`Scientific ${role} review requires completed ${requiredPackage}.`, {
|
|
@@ -737,10 +798,8 @@ async function stageInputRecords(root, project, role, design) {
|
|
|
737
798
|
details: { role, requiredPackage, status: packageState?.status ?? null },
|
|
738
799
|
});
|
|
739
800
|
}
|
|
740
|
-
const relativeOutputs =
|
|
741
|
-
|
|
742
|
-
: ["outputs/acquisition.json", "outputs/evidence-snapshot.json"];
|
|
743
|
-
return Promise.all(relativeOutputs.map(async (relativePath) => {
|
|
801
|
+
const relativeOutputs = ["outputs/acquisition.json", "outputs/evidence-snapshot.json"];
|
|
802
|
+
const stageOutputs = await Promise.all(relativeOutputs.map(async (relativePath) => {
|
|
744
803
|
const sourceLocator = `projects/${project.id}/${relativePath}`;
|
|
745
804
|
return {
|
|
746
805
|
...(await fileRecord(join(projectRoot, relativePath), sourceLocator)),
|
|
@@ -750,6 +809,130 @@ async function stageInputRecords(root, project, role, design) {
|
|
|
750
809
|
hashBasis: "raw-file-bytes",
|
|
751
810
|
};
|
|
752
811
|
}));
|
|
812
|
+
if (role !== "evidence-construct") {
|
|
813
|
+
if (canaryArtifactPaths.length)
|
|
814
|
+
throw canaryArtifactError("Only evidence-construct accepts canary artifacts.");
|
|
815
|
+
return stageOutputs;
|
|
816
|
+
}
|
|
817
|
+
const expectedSha256s = new Set(assessment.constructCanary.artifactSha256s);
|
|
818
|
+
const seenSha256s = new Set();
|
|
819
|
+
const canaryRecords = [];
|
|
820
|
+
const controlRoot = resolve(paths.control);
|
|
821
|
+
for (const selected of canaryArtifactPaths) {
|
|
822
|
+
if (!isAbsolute(selected) || resolve(selected) !== selected) {
|
|
823
|
+
throw canaryArtifactError("Canary artifact paths must be absolute and canonical.");
|
|
824
|
+
}
|
|
825
|
+
const sourcePath = resolve(selected);
|
|
826
|
+
const relation = relative(controlRoot, sourcePath);
|
|
827
|
+
if (sourcePath === controlRoot || (!relation.startsWith(`..${sep}`) && relation !== "..")) {
|
|
828
|
+
throw canaryArtifactError("Canary artifacts must originate outside .tiangong-research.");
|
|
829
|
+
}
|
|
830
|
+
const info = await lstat(sourcePath).catch(() => undefined);
|
|
831
|
+
if (!info?.isFile() ||
|
|
832
|
+
info.isSymbolicLink() ||
|
|
833
|
+
info.size < 2 ||
|
|
834
|
+
info.size > MAX_SCIENTIFIC_REVIEW_BYTES) {
|
|
835
|
+
throw canaryArtifactError("Canary artifacts must be bounded regular non-symlink files.");
|
|
836
|
+
}
|
|
837
|
+
const sourceBytes = await readFile(sourcePath, "utf8");
|
|
838
|
+
let value;
|
|
839
|
+
try {
|
|
840
|
+
value = JSON.parse(sourceBytes);
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
throw canaryArtifactError("Canary artifacts must contain reviewable JSON.");
|
|
844
|
+
}
|
|
845
|
+
if (containsSensitiveCanaryField(value)) {
|
|
846
|
+
throw canaryArtifactError("Canary artifacts cannot contain credential-like fields.");
|
|
847
|
+
}
|
|
848
|
+
const sha256 = await sha256File(sourcePath);
|
|
849
|
+
if (!expectedSha256s.has(sha256)) {
|
|
850
|
+
throw canaryArtifactError("A supplied canary artifact is not declared by the assessment.");
|
|
851
|
+
}
|
|
852
|
+
if (seenSha256s.has(sha256)) {
|
|
853
|
+
throw canaryArtifactError("Canary artifacts must have distinct content hashes.");
|
|
854
|
+
}
|
|
855
|
+
seenSha256s.add(sha256);
|
|
856
|
+
const locator = `projects/${project.id}/scientific/canary-artifacts/evidence-construct/${sha256}.json`;
|
|
857
|
+
const promoted = resolveContained(paths.control, locator);
|
|
858
|
+
if (await pathExists(promoted)) {
|
|
859
|
+
const promotedInfo = await lstat(promoted).catch(() => undefined);
|
|
860
|
+
if (!promotedInfo?.isFile() ||
|
|
861
|
+
promotedInfo.isSymbolicLink() ||
|
|
862
|
+
promotedInfo.size !== info.size ||
|
|
863
|
+
(await sha256File(promoted)) !== sha256) {
|
|
864
|
+
throw scientificGateError("A promoted construct-canary artifact failed its immutable binding.", role);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
await writeTextAtomic(promoted, sourceBytes);
|
|
869
|
+
}
|
|
870
|
+
canaryRecords.push({
|
|
871
|
+
path: locator,
|
|
872
|
+
sha256,
|
|
873
|
+
bytes: info.size,
|
|
874
|
+
purpose: "construct-canary",
|
|
875
|
+
ownerId: "evidence-construct",
|
|
876
|
+
sourceLocator: `native-canary:${sha256}`,
|
|
877
|
+
hashBasis: "raw-file-bytes",
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
return [...stageOutputs, ...canaryRecords];
|
|
881
|
+
}
|
|
882
|
+
async function assessmentEvidenceContext(root, project, role, stageInputs) {
|
|
883
|
+
if (role !== "evidence-construct")
|
|
884
|
+
return null;
|
|
885
|
+
const snapshotRecord = stageInputs.find((record) => record.purpose === "stage-output" &&
|
|
886
|
+
record.sourceLocator === `projects/${project.id}/outputs/evidence-snapshot.json`);
|
|
887
|
+
if (!snapshotRecord) {
|
|
888
|
+
throw scientificGateError("Evidence-construct review requires the frozen post-acquisition evidence snapshot.", role);
|
|
889
|
+
}
|
|
890
|
+
const snapshot = await readExactJson(resolveContained(workspacePaths(root).control, snapshotRecord.path), "Evidence snapshot", "RESEARCH_SCIENTIFIC_GATE_INVALID");
|
|
891
|
+
if (!isObject(snapshot) ||
|
|
892
|
+
snapshot.schemaVersion !== 1 ||
|
|
893
|
+
snapshot.kind !== "tiangong-evidence-snapshot" ||
|
|
894
|
+
snapshot.projectId !== project.id ||
|
|
895
|
+
!Array.isArray(snapshot.sources)) {
|
|
896
|
+
throw scientificGateError("Evidence snapshot is malformed for scientific review.", role);
|
|
897
|
+
}
|
|
898
|
+
const snapshotSha256 = typeof snapshot.snapshotSha256 === "string" ? snapshot.snapshotSha256 : "";
|
|
899
|
+
const snapshotId = typeof snapshot.snapshotId === "string" ? snapshot.snapshotId : "";
|
|
900
|
+
const { snapshotSha256: _recordedSnapshotSha256, ...snapshotCore } = snapshot;
|
|
901
|
+
const immutableSnapshot = resolveContained(workspacePaths(root).projects, `${project.id}/evidence/snapshots/${snapshotSha256}.json`);
|
|
902
|
+
const snapshotBytes = normalizedJson(snapshot);
|
|
903
|
+
if (!new RegExp(SHA256_PATTERN).test(snapshotSha256) ||
|
|
904
|
+
!snapshotId ||
|
|
905
|
+
sha256Text(canonicalJson(snapshotCore)) !== snapshotSha256 ||
|
|
906
|
+
project.evidenceState.currentSnapshotId !== snapshotId ||
|
|
907
|
+
project.evidenceState.currentSnapshotSha256 !== snapshotSha256 ||
|
|
908
|
+
!(await pathExists(immutableSnapshot)) ||
|
|
909
|
+
(await readFile(immutableSnapshot, "utf8")) !== snapshotBytes) {
|
|
910
|
+
throw scientificGateError("Evidence-construct review requires the current immutable evidence snapshot binding.", role);
|
|
911
|
+
}
|
|
912
|
+
const sources = new Map();
|
|
913
|
+
for (const item of snapshot.sources) {
|
|
914
|
+
if (!isObject(item) || typeof item.id !== "string" || !item.id) {
|
|
915
|
+
throw scientificGateError("Evidence snapshot contains a malformed source.", role);
|
|
916
|
+
}
|
|
917
|
+
if (sources.has(item.id)) {
|
|
918
|
+
throw scientificGateError("Evidence snapshot contains duplicate source IDs.", role);
|
|
919
|
+
}
|
|
920
|
+
sources.set(item.id, {
|
|
921
|
+
id: item.id,
|
|
922
|
+
sourceType: typeof item.sourceType === "string" ? item.sourceType : null,
|
|
923
|
+
publicationDate: typeof item.publicationDate === "string" ? item.publicationDate : null,
|
|
924
|
+
fullTextAvailable: item.fullTextAvailable === true,
|
|
925
|
+
coverageDimensions: Array.isArray(item.coverageDimensions)
|
|
926
|
+
? item.coverageDimensions.filter((value) => typeof value === "string")
|
|
927
|
+
: [],
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
return {
|
|
931
|
+
sources,
|
|
932
|
+
canaryArtifactSha256s: new Set(stageInputs
|
|
933
|
+
.filter((record) => record.purpose === "construct-canary")
|
|
934
|
+
.map((record) => record.sha256)),
|
|
935
|
+
};
|
|
753
936
|
}
|
|
754
937
|
async function loadBoundScientificDesign(root, project) {
|
|
755
938
|
const binding = project.scientificDesign;
|
|
@@ -962,6 +1145,7 @@ function isScientificReviewPacket(value, projectId, role, finalPublicationReview
|
|
|
962
1145
|
Number.isSafeInteger(record.bytes) &&
|
|
963
1146
|
record.bytes >= 0 &&
|
|
964
1147
|
[
|
|
1148
|
+
"construct-canary",
|
|
965
1149
|
"inherited-gap",
|
|
966
1150
|
"model-implementation",
|
|
967
1151
|
"model-environment-lock",
|
|
@@ -1027,9 +1211,30 @@ function requiredGateRoles(stage) {
|
|
|
1027
1211
|
if (stage === "discover")
|
|
1028
1212
|
return ordered.slice(0, 1);
|
|
1029
1213
|
if (stage === "acquire")
|
|
1030
|
-
return ordered.slice(0,
|
|
1214
|
+
return ordered.slice(0, 1);
|
|
1031
1215
|
return ordered;
|
|
1032
1216
|
}
|
|
1217
|
+
function containsSensitiveCanaryField(value) {
|
|
1218
|
+
if (Array.isArray(value))
|
|
1219
|
+
return value.some(containsSensitiveCanaryField);
|
|
1220
|
+
if (typeof value === "string") {
|
|
1221
|
+
return (/\b(?:authorization|cookie|set-cookie)\b\s*[:=]/iu.test(value) ||
|
|
1222
|
+
/\bbearer\s+[A-Za-z0-9._~+/=-]+/iu.test(value) ||
|
|
1223
|
+
/[?&](?:access[_-]?token|api[_-]?key|apikey|auth|authorization|cookie|credential|key|password|secret|session(?:[_-]?id)?|sig|signature|token)=/iu.test(value));
|
|
1224
|
+
}
|
|
1225
|
+
if (!isObject(value))
|
|
1226
|
+
return false;
|
|
1227
|
+
return Object.entries(value).some(([key, item]) => (/^(?:access[_-]?token|api[_-]?key|apikey|auth|authorization|cookie|credential|key|password|secret|session(?:[_-]?id)?|sig|signature|token)$/iu.test(key) &&
|
|
1228
|
+
item !== null &&
|
|
1229
|
+
item !== "") ||
|
|
1230
|
+
containsSensitiveCanaryField(item));
|
|
1231
|
+
}
|
|
1232
|
+
function canaryArtifactError(message) {
|
|
1233
|
+
return new CliError(message, {
|
|
1234
|
+
code: "RESEARCH_SCIENTIFIC_CANARY_ARTIFACT_INVALID",
|
|
1235
|
+
exitCode: 3,
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1033
1238
|
function reviewInstructions(role) {
|
|
1034
1239
|
return [
|
|
1035
1240
|
`Review only the exact immutable ${role} packet and its referenced objects.`,
|