@tiangong-ai/cli 0.0.35 → 0.0.37
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 +16 -6
- package/README.md +128 -10
- package/dist/research/orchestration.js +334 -29
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/workspace/acquisition-routes.d.ts +14 -0
- package/dist/research/workspace/acquisition-routes.js +45 -0
- package/dist/research/workspace/acquisition-routes.js.map +1 -0
- package/dist/research/workspace/audit-bundle.d.ts +44 -0
- package/dist/research/workspace/audit-bundle.js +357 -0
- package/dist/research/workspace/audit-bundle.js.map +1 -0
- package/dist/research/workspace/broker.js +54 -10
- package/dist/research/workspace/broker.js.map +1 -1
- package/dist/research/workspace/downloads.d.ts +6 -0
- package/dist/research/workspace/downloads.js +32 -8
- package/dist/research/workspace/downloads.js.map +1 -1
- package/dist/research/workspace/evidence-exhaustion.d.ts +45 -0
- package/dist/research/workspace/evidence-exhaustion.js +365 -0
- package/dist/research/workspace/evidence-exhaustion.js.map +1 -0
- package/dist/research/workspace/native-activity.d.ts +6 -0
- package/dist/research/workspace/native-activity.js +24 -7
- package/dist/research/workspace/native-activity.js.map +1 -1
- package/dist/research/workspace/preflight.d.ts +34 -2
- package/dist/research/workspace/preflight.js +161 -1
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/projects.d.ts +28 -4
- package/dist/research/workspace/projects.js +301 -12
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/publication-workflow.js +2 -0
- package/dist/research/workspace/publication-workflow.js.map +1 -1
- package/dist/research/workspace/runtime.d.ts +151 -3
- package/dist/research/workspace/runtime.js +180 -6
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/sanitization.js +9 -3
- package/dist/research/workspace/sanitization.js.map +1 -1
- package/dist/research/workspace/scientific-design.d.ts +359 -0
- package/dist/research/workspace/scientific-design.js +2021 -0
- package/dist/research/workspace/scientific-design.js.map +1 -0
- package/dist/research/workspace/scientific-review.d.ts +101 -0
- package/dist/research/workspace/scientific-review.js +1167 -0
- package/dist/research/workspace/scientific-review.js.map +1 -0
- package/dist/research/workspace/setup-catalog.js +2 -2
- package/dist/research/workspace/setup.js +12 -3
- package/dist/research/workspace/setup.js.map +1 -1
- package/dist/research/workspace/types.d.ts +54 -0
- package/dist/research/workspace/workspace.js +25 -7
- package/dist/research/workspace/workspace.js.map +1 -1
- package/package.json +2 -1
|
@@ -10,10 +10,11 @@ import { cloneProjectArtifactRecords } from "./artifacts.js";
|
|
|
10
10
|
import { freezeEvidenceSnapshot, loadCurrentEvidenceSnapshot, loadImmutableEvidenceSnapshotChain, } from "./acquisition.js";
|
|
11
11
|
import { projectInputsFromPlan, reverifyProjectInputPlan } from "./input-plan.js";
|
|
12
12
|
import { evaluateProjectPreflight } from "./preflight.js";
|
|
13
|
-
import {
|
|
13
|
+
import { evaluateScientificDesign, scientificDesignPolicyGaps, } from "./scientific-design.js";
|
|
14
|
+
import { ensureDirectory, fileRecord, fileSize, isObject, readJsonFile, sha256File, sha256Text, workspacePaths, writeJsonAtomic, } from "./storage.js";
|
|
14
15
|
import { loadWorkspaceConfig, withWorkspaceLock } from "./workspace.js";
|
|
15
16
|
const PROJECT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{2,63}$/;
|
|
16
|
-
export async function initializeProject(root, projectId, question, evidenceRequirements, budgetConfirmed = false, inputPlan, publicationPolicy) {
|
|
17
|
+
export async function initializeProject(root, projectId, question, evidenceRequirements, budgetConfirmed = false, inputPlan, publicationPolicy, scientificDesignInput) {
|
|
17
18
|
validateProjectId(projectId);
|
|
18
19
|
const normalizedQuestion = question.trim();
|
|
19
20
|
if (normalizedQuestion.length < 8 || normalizedQuestion.length > 4000) {
|
|
@@ -53,9 +54,27 @@ export async function initializeProject(root, projectId, question, evidenceRequi
|
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
const requirements = normalizeEvidenceRequirements(evidenceRequirements ?? defaultEvidenceRequirements(config));
|
|
57
|
+
if (publicationPolicy && !scientificDesignInput) {
|
|
58
|
+
throw new CliError("Top-journal research requires an explicit scientific design contract.", {
|
|
59
|
+
code: "RESEARCH_SCIENTIFIC_DESIGN_REQUIRED",
|
|
60
|
+
exitCode: 2,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (!publicationPolicy && scientificDesignInput) {
|
|
64
|
+
throw new CliError("A scientific design contract requires an approved top-journal policy.", {
|
|
65
|
+
code: "RESEARCH_SCIENTIFIC_DESIGN_POLICY_REQUIRED",
|
|
66
|
+
exitCode: 2,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const scientificDesign = scientificDesignInput
|
|
70
|
+
? prepareScientificDesignBinding(projectId, publicationPolicy, scientificDesignInput)
|
|
71
|
+
: null;
|
|
56
72
|
const admittedInputPlan = inputPlan ? await reverifyProjectInputPlan(inputPlan) : undefined;
|
|
57
73
|
if (config.mode === "production-research") {
|
|
58
|
-
const preflight = await evaluateProjectPreflight(root, normalizedQuestion, requirements, admittedInputPlan ?? null
|
|
74
|
+
const preflight = await evaluateProjectPreflight(root, normalizedQuestion, requirements, admittedInputPlan ?? null, {
|
|
75
|
+
publicationPolicy: publicationPolicy ?? null,
|
|
76
|
+
scientificDesign: scientificDesignInput?.design ?? null,
|
|
77
|
+
});
|
|
59
78
|
if (!preflight.readyToInitialize) {
|
|
60
79
|
throw new CliError("Production project initialization was blocked by preflight.", {
|
|
61
80
|
code: "RESEARCH_PREFLIGHT_BLOCKED",
|
|
@@ -76,6 +95,7 @@ export async function initializeProject(root, projectId, question, evidenceRequi
|
|
|
76
95
|
inputs: admittedInputPlan ? projectInputsFromPlan(admittedInputPlan, now) : [],
|
|
77
96
|
evidenceRequirements: requirements,
|
|
78
97
|
publicationPolicy: publicationPolicy ?? null,
|
|
98
|
+
scientificDesign,
|
|
79
99
|
packages: defaultWorkPackages(config),
|
|
80
100
|
usage: {
|
|
81
101
|
tokens: 0,
|
|
@@ -94,6 +114,9 @@ export async function initializeProject(root, projectId, question, evidenceRequi
|
|
|
94
114
|
ensureDirectory(join(projectRoot, "outputs")),
|
|
95
115
|
ensureDirectory(join(projectRoot, "runs")),
|
|
96
116
|
]);
|
|
117
|
+
if (scientificDesign && scientificDesignInput) {
|
|
118
|
+
await writeJsonAtomic(join(paths.control, scientificDesign.objectLocator), scientificDesignInput.design.contract);
|
|
119
|
+
}
|
|
97
120
|
await writeJsonAtomic(projectPath, project);
|
|
98
121
|
await registerProjectInputCandidates(root, projectId, project.inputs);
|
|
99
122
|
await appendJournalEvent(paths.journal, "project.initialized", projectId, {
|
|
@@ -101,6 +124,8 @@ export async function initializeProject(root, projectId, question, evidenceRequi
|
|
|
101
124
|
questionSha256: await hashQuestion(normalizedQuestion),
|
|
102
125
|
inputPlanSha256: admittedInputPlan?.sha256 ?? null,
|
|
103
126
|
publicationPolicySha256: publicationPolicy?.resolvedPolicySha256 ?? null,
|
|
127
|
+
scientificDesignSha256: scientificDesign?.designSha256 ?? null,
|
|
128
|
+
scientificDesignProducerSessionSha256: scientificDesign?.producer.sessionSha256 ?? null,
|
|
104
129
|
inputs: project.inputs.map((input) => ({
|
|
105
130
|
id: input.id,
|
|
106
131
|
role: input.role,
|
|
@@ -252,7 +277,7 @@ export async function retryProjectPackage(root, projectId, packageId) {
|
|
|
252
277
|
return project;
|
|
253
278
|
});
|
|
254
279
|
}
|
|
255
|
-
export async function forkProject(root, sourceProjectId, targetProjectId, resumeThrough) {
|
|
280
|
+
export async function forkProject(root, sourceProjectId, targetProjectId, resumeThrough, scientificReapproval) {
|
|
256
281
|
validateProjectId(sourceProjectId);
|
|
257
282
|
validateProjectId(targetProjectId);
|
|
258
283
|
if (sourceProjectId === targetProjectId) {
|
|
@@ -263,6 +288,16 @@ export async function forkProject(root, sourceProjectId, targetProjectId, resume
|
|
|
263
288
|
}
|
|
264
289
|
return withWorkspaceLock(root, "project.fork", async () => {
|
|
265
290
|
const source = await loadProject(root, sourceProjectId);
|
|
291
|
+
const sourceRequiresScientificReapproval = Boolean(source.publicationPolicy || source.scientificDesign);
|
|
292
|
+
if (sourceRequiresScientificReapproval && !scientificReapproval) {
|
|
293
|
+
throw new CliError("A top-journal recovery fork requires a newly approved project-specific policy and scientific design.", { code: "RESEARCH_SCIENTIFIC_DESIGN_REAPPROVAL_REQUIRED", exitCode: 3 });
|
|
294
|
+
}
|
|
295
|
+
if (!sourceRequiresScientificReapproval && scientificReapproval) {
|
|
296
|
+
throw new CliError("Scientific reapproval is valid only for a top-journal source project.", {
|
|
297
|
+
code: "RESEARCH_SCIENTIFIC_DESIGN_REAPPROVAL_INVALID",
|
|
298
|
+
exitCode: 2,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
266
301
|
if (source.lineage.supersededBy) {
|
|
267
302
|
throw new CliError(`Project ${sourceProjectId} is historical; fork the authoritative project ${source.lineage.supersededBy}.`, { code: "RESEARCH_PROJECT_NOT_AUTHORITATIVE", exitCode: 3 });
|
|
268
303
|
}
|
|
@@ -283,6 +318,29 @@ export async function forkProject(root, sourceProjectId, targetProjectId, resume
|
|
|
283
318
|
});
|
|
284
319
|
}
|
|
285
320
|
const config = await loadWorkspaceConfig(root);
|
|
321
|
+
const targetPolicy = scientificReapproval?.publicationPolicy ?? null;
|
|
322
|
+
if (targetPolicy && targetPolicy.projectId !== targetProjectId) {
|
|
323
|
+
throw new CliError("Recovery policy must be approved for the target project generation.", {
|
|
324
|
+
code: "RESEARCH_SCIENTIFIC_DESIGN_REAPPROVAL_INVALID",
|
|
325
|
+
exitCode: 2,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const targetScientificDesign = scientificReapproval
|
|
329
|
+
? prepareScientificDesignBinding(targetProjectId, scientificReapproval.publicationPolicy, scientificReapproval.scientificDesign)
|
|
330
|
+
: null;
|
|
331
|
+
if (config.mode === "production-research") {
|
|
332
|
+
const preflight = await evaluateProjectPreflight(root, source.question, source.evidenceRequirements, null, {
|
|
333
|
+
publicationPolicy: targetPolicy,
|
|
334
|
+
scientificDesign: scientificReapproval?.scientificDesign.design ?? null,
|
|
335
|
+
});
|
|
336
|
+
if (!preflight.readyToInitialize) {
|
|
337
|
+
throw new CliError("Recovery generation was blocked by production preflight.", {
|
|
338
|
+
code: "RESEARCH_PREFLIGHT_BLOCKED",
|
|
339
|
+
exitCode: 3,
|
|
340
|
+
details: { gaps: preflight.gaps, preflightSha256: preflight.preflightSha256 },
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
286
344
|
const packages = defaultWorkPackages(config);
|
|
287
345
|
const inheritedStages = resumeThrough
|
|
288
346
|
? ["discover", "acquire", "analyze", "synthesize"].slice(0, ["discover", "acquire", "analyze", "synthesize"].indexOf(resumeThrough) + 1)
|
|
@@ -320,9 +378,8 @@ export async function forkProject(root, sourceProjectId, targetProjectId, resume
|
|
|
320
378
|
requiredCompanionIds: [...(source.evidenceRequirements.requiredCompanionIds ?? [])],
|
|
321
379
|
requiredDiscoveryScopes: [...(source.evidenceRequirements.requiredDiscoveryScopes ?? [])],
|
|
322
380
|
},
|
|
323
|
-
publicationPolicy:
|
|
324
|
-
|
|
325
|
-
: null,
|
|
381
|
+
publicationPolicy: targetPolicy,
|
|
382
|
+
scientificDesign: targetScientificDesign,
|
|
326
383
|
packages,
|
|
327
384
|
usage: {
|
|
328
385
|
tokens: 0,
|
|
@@ -345,6 +402,9 @@ export async function forkProject(root, sourceProjectId, targetProjectId, resume
|
|
|
345
402
|
ensureDirectory(join(targetRoot, "outputs")),
|
|
346
403
|
ensureDirectory(join(targetRoot, "runs")),
|
|
347
404
|
]);
|
|
405
|
+
if (targetScientificDesign && scientificReapproval) {
|
|
406
|
+
await writeJsonAtomic(join(workspacePaths(root).control, targetScientificDesign.objectLocator), scientificReapproval.scientificDesign.design.contract);
|
|
407
|
+
}
|
|
348
408
|
const inheritedOutputs = [];
|
|
349
409
|
const stageOutput = {
|
|
350
410
|
discover: "outputs/evidence.json",
|
|
@@ -391,6 +451,9 @@ export async function forkProject(root, sourceProjectId, targetProjectId, resume
|
|
|
391
451
|
inheritedOutputs,
|
|
392
452
|
inheritedUsage: false,
|
|
393
453
|
sourceSuperseded: true,
|
|
454
|
+
publicationPolicySha256: targetPolicy?.resolvedPolicySha256 ?? null,
|
|
455
|
+
scientificDesignSha256: targetScientificDesign?.designSha256 ?? null,
|
|
456
|
+
scientificDesignProducerSessionSha256: targetScientificDesign?.producer.sessionSha256 ?? null,
|
|
394
457
|
});
|
|
395
458
|
return project;
|
|
396
459
|
});
|
|
@@ -437,7 +500,7 @@ export async function setProjectDisposition(root, projectId, disposition, reason
|
|
|
437
500
|
return project;
|
|
438
501
|
});
|
|
439
502
|
}
|
|
440
|
-
export async function createProjectAddendum(root, sourceProjectId, targetProjectId) {
|
|
503
|
+
export async function createProjectAddendum(root, sourceProjectId, targetProjectId, scientificReapproval) {
|
|
441
504
|
validateProjectId(sourceProjectId);
|
|
442
505
|
validateProjectId(targetProjectId);
|
|
443
506
|
if (sourceProjectId === targetProjectId) {
|
|
@@ -448,6 +511,16 @@ export async function createProjectAddendum(root, sourceProjectId, targetProject
|
|
|
448
511
|
}
|
|
449
512
|
return withWorkspaceLock(root, "project.addendum", async () => {
|
|
450
513
|
const source = refreshProject(await loadProject(root, sourceProjectId));
|
|
514
|
+
const sourceRequiresScientificReapproval = Boolean(source.publicationPolicy || source.scientificDesign);
|
|
515
|
+
if (sourceRequiresScientificReapproval && !scientificReapproval) {
|
|
516
|
+
throw new CliError("A top-journal addendum requires a newly approved project-specific policy and scientific design.", { code: "RESEARCH_SCIENTIFIC_DESIGN_REAPPROVAL_REQUIRED", exitCode: 3 });
|
|
517
|
+
}
|
|
518
|
+
if (!sourceRequiresScientificReapproval && scientificReapproval) {
|
|
519
|
+
throw new CliError("Scientific reapproval is valid only for a top-journal source project.", {
|
|
520
|
+
code: "RESEARCH_SCIENTIFIC_DESIGN_REAPPROVAL_INVALID",
|
|
521
|
+
exitCode: 2,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
451
524
|
if (source.status !== "complete" || source.packages.at(-1)?.stage !== "close") {
|
|
452
525
|
throw new CliError("An addendum requires a mechanically closed source project.", {
|
|
453
526
|
code: "RESEARCH_PROJECT_ADDENDUM_INVALID",
|
|
@@ -484,6 +557,29 @@ export async function createProjectAddendum(root, sourceProjectId, targetProject
|
|
|
484
557
|
});
|
|
485
558
|
}
|
|
486
559
|
const config = await loadWorkspaceConfig(root);
|
|
560
|
+
const targetPolicy = scientificReapproval?.publicationPolicy ?? null;
|
|
561
|
+
if (targetPolicy && targetPolicy.projectId !== targetProjectId) {
|
|
562
|
+
throw new CliError("Addendum policy must be approved for the target project generation.", {
|
|
563
|
+
code: "RESEARCH_SCIENTIFIC_DESIGN_REAPPROVAL_INVALID",
|
|
564
|
+
exitCode: 2,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
const targetScientificDesign = scientificReapproval
|
|
568
|
+
? prepareScientificDesignBinding(targetProjectId, scientificReapproval.publicationPolicy, scientificReapproval.scientificDesign)
|
|
569
|
+
: null;
|
|
570
|
+
if (config.mode === "production-research") {
|
|
571
|
+
const preflight = await evaluateProjectPreflight(root, source.question, source.evidenceRequirements, null, {
|
|
572
|
+
publicationPolicy: targetPolicy,
|
|
573
|
+
scientificDesign: scientificReapproval?.scientificDesign.design ?? null,
|
|
574
|
+
});
|
|
575
|
+
if (!preflight.readyToInitialize) {
|
|
576
|
+
throw new CliError("Addendum generation was blocked by production preflight.", {
|
|
577
|
+
code: "RESEARCH_PREFLIGHT_BLOCKED",
|
|
578
|
+
exitCode: 3,
|
|
579
|
+
details: { gaps: preflight.gaps, preflightSha256: preflight.preflightSha256 },
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
487
583
|
const now = new Date().toISOString();
|
|
488
584
|
const target = {
|
|
489
585
|
schemaVersion: 1,
|
|
@@ -502,9 +598,8 @@ export async function createProjectAddendum(root, sourceProjectId, targetProject
|
|
|
502
598
|
requiredCompanionIds: [...(source.evidenceRequirements.requiredCompanionIds ?? [])],
|
|
503
599
|
requiredDiscoveryScopes: [...(source.evidenceRequirements.requiredDiscoveryScopes ?? [])],
|
|
504
600
|
},
|
|
505
|
-
publicationPolicy:
|
|
506
|
-
|
|
507
|
-
: null,
|
|
601
|
+
publicationPolicy: targetPolicy,
|
|
602
|
+
scientificDesign: targetScientificDesign,
|
|
508
603
|
packages: defaultWorkPackages(config),
|
|
509
604
|
usage: {
|
|
510
605
|
tokens: 0,
|
|
@@ -531,6 +626,9 @@ export async function createProjectAddendum(root, sourceProjectId, targetProject
|
|
|
531
626
|
ensureDirectory(join(targetRoot, "runs")),
|
|
532
627
|
ensureDirectory(join(targetRoot, "evidence", "snapshots")),
|
|
533
628
|
]);
|
|
629
|
+
if (targetScientificDesign && scientificReapproval) {
|
|
630
|
+
await writeJsonAtomic(join(workspacePaths(root).control, targetScientificDesign.objectLocator), scientificReapproval.scientificDesign.design.contract);
|
|
631
|
+
}
|
|
534
632
|
const inheritedOutputs = [];
|
|
535
633
|
for (const logicalPath of ["outputs/evidence.json", "outputs/acquisition.json"]) {
|
|
536
634
|
const sourcePath = join(workspacePaths(root).projects, sourceProjectId, logicalPath);
|
|
@@ -576,6 +674,9 @@ export async function createProjectAddendum(root, sourceProjectId, targetProject
|
|
|
576
674
|
baseSnapshotSha256: snapshot.snapshotSha256,
|
|
577
675
|
inheritedOutputs,
|
|
578
676
|
originalClosurePreserved: true,
|
|
677
|
+
publicationPolicySha256: targetPolicy?.resolvedPolicySha256 ?? null,
|
|
678
|
+
scientificDesignSha256: targetScientificDesign?.designSha256 ?? null,
|
|
679
|
+
scientificDesignProducerSessionSha256: targetScientificDesign?.producer.sessionSha256 ?? null,
|
|
579
680
|
});
|
|
580
681
|
return target;
|
|
581
682
|
});
|
|
@@ -617,7 +718,32 @@ export function nextReadyPackage(project) {
|
|
|
617
718
|
refreshProject(project);
|
|
618
719
|
if (project.handoff.state !== "agent-actionable")
|
|
619
720
|
return undefined;
|
|
620
|
-
|
|
721
|
+
const candidate = project.packages.find((workPackage) => workPackage.status === "ready");
|
|
722
|
+
if (!candidate)
|
|
723
|
+
return undefined;
|
|
724
|
+
const gate = nextScientificGate(project);
|
|
725
|
+
if (gate && gate.status !== "passed") {
|
|
726
|
+
const packageOrder = ["discover", "acquire", "analyze", "synthesize", "review", "close"];
|
|
727
|
+
if (packageOrder.indexOf(candidate.id) >= packageOrder.indexOf(gate.blocksPackage)) {
|
|
728
|
+
return undefined;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return candidate;
|
|
732
|
+
}
|
|
733
|
+
export function nextScientificGate(project) {
|
|
734
|
+
if (!project.scientificDesign)
|
|
735
|
+
return null;
|
|
736
|
+
const ordered = [
|
|
737
|
+
{ role: "research-design", blocksPackage: "discover" },
|
|
738
|
+
{ role: "evidence-construct", blocksPackage: "acquire" },
|
|
739
|
+
{ role: "pilot-methods", blocksPackage: "analyze" },
|
|
740
|
+
];
|
|
741
|
+
for (const item of ordered) {
|
|
742
|
+
const status = project.scientificDesign.gates[item.role].status;
|
|
743
|
+
if (status !== "passed")
|
|
744
|
+
return { ...item, status };
|
|
745
|
+
}
|
|
746
|
+
return null;
|
|
621
747
|
}
|
|
622
748
|
export function packageById(project, packageId) {
|
|
623
749
|
const workPackage = project.packages.find((candidate) => candidate.id === packageId);
|
|
@@ -677,6 +803,8 @@ function validateProjectShape(project, expectedId) {
|
|
|
677
803
|
(project.budgetConfirmedAt !== null && typeof project.budgetConfirmedAt !== "string") ||
|
|
678
804
|
!Array.isArray(project.inputs) ||
|
|
679
805
|
!isEvidenceRequirements(project.evidenceRequirements) ||
|
|
806
|
+
!isScientificDesignBinding(project.scientificDesign, expectedId) ||
|
|
807
|
+
(Boolean(project.publicationPolicy) && project.scientificDesign === null) ||
|
|
680
808
|
!Array.isArray(project.packages) ||
|
|
681
809
|
!project.usage ||
|
|
682
810
|
typeof project.usage.tokens !== "number" ||
|
|
@@ -726,15 +854,127 @@ function initialEvidenceState() {
|
|
|
726
854
|
function initialHandoffState() {
|
|
727
855
|
return {
|
|
728
856
|
state: "agent-actionable",
|
|
857
|
+
kind: null,
|
|
729
858
|
reasonCode: null,
|
|
730
859
|
summary: null,
|
|
731
860
|
requestedActions: [],
|
|
732
861
|
evidenceGaps: [],
|
|
862
|
+
exhaustion: null,
|
|
863
|
+
accessRequests: [],
|
|
733
864
|
requestedAt: null,
|
|
734
865
|
resolvedAt: null,
|
|
735
866
|
resolutionNote: null,
|
|
736
867
|
};
|
|
737
868
|
}
|
|
869
|
+
function prepareScientificDesignBinding(projectId, policy, input) {
|
|
870
|
+
const contract = input.design.contract;
|
|
871
|
+
const normalized = `${JSON.stringify(contract, null, 2)}\n`;
|
|
872
|
+
if (contract.projectId !== projectId ||
|
|
873
|
+
input.design.sha256 !== sha256Text(normalized) ||
|
|
874
|
+
input.design.bytes !== Buffer.byteLength(normalized, "utf8")) {
|
|
875
|
+
throw new CliError("Scientific design does not match its verified project and hash binding.", {
|
|
876
|
+
code: "RESEARCH_SCIENTIFIC_DESIGN_PROJECT_MISMATCH",
|
|
877
|
+
exitCode: 2,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
if (!input.producerSessionId.trim()) {
|
|
881
|
+
throw new CliError("Scientific design requires an opaque native producer session identifier.", {
|
|
882
|
+
code: "RESEARCH_SCIENTIFIC_DESIGN_PRODUCER_INVALID",
|
|
883
|
+
exitCode: 2,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
if (policy.targetJournal &&
|
|
887
|
+
policy.targetJournal.trim().toLocaleLowerCase("en-US") !==
|
|
888
|
+
contract.identity.targetJournals.primary.trim().toLocaleLowerCase("en-US")) {
|
|
889
|
+
throw new CliError("Scientific design primary journal does not match Research Policy.", {
|
|
890
|
+
code: "RESEARCH_SCIENTIFIC_DESIGN_POLICY_MISMATCH",
|
|
891
|
+
exitCode: 2,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
const expectedJournalApprovalStatus = policy.targetJournal ? "policy-approved" : "candidate-only";
|
|
895
|
+
if (contract.identity.targetJournals.approvalStatus !== expectedJournalApprovalStatus) {
|
|
896
|
+
throw new CliError(policy.targetJournal
|
|
897
|
+
? "An exact-journal Research Policy requires a policy-approved scientific design target."
|
|
898
|
+
: "A generic Research Policy may list exact journals only as candidates.", {
|
|
899
|
+
code: "RESEARCH_SCIENTIFIC_DESIGN_POLICY_MISMATCH",
|
|
900
|
+
exitCode: 2,
|
|
901
|
+
details: {
|
|
902
|
+
expectedApprovalStatus: expectedJournalApprovalStatus,
|
|
903
|
+
actualApprovalStatus: contract.identity.targetJournals.approvalStatus,
|
|
904
|
+
},
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
const evaluation = evaluateScientificDesign(contract);
|
|
908
|
+
if (!evaluation.readyForDesignReview) {
|
|
909
|
+
throw new CliError("Scientific design has blocking mechanical issues.", {
|
|
910
|
+
code: "RESEARCH_SCIENTIFIC_DESIGN_BLOCKED",
|
|
911
|
+
exitCode: 3,
|
|
912
|
+
details: { issueCodes: evaluation.issueCodes },
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
const policyGaps = scientificDesignPolicyGaps(contract, policy);
|
|
916
|
+
if (policyGaps.length) {
|
|
917
|
+
throw new CliError("Scientific design does not discharge the approved Research Policy.", {
|
|
918
|
+
code: "RESEARCH_SCIENTIFIC_DESIGN_POLICY_MISMATCH",
|
|
919
|
+
exitCode: 3,
|
|
920
|
+
details: { gaps: policyGaps },
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
const pendingGate = () => ({
|
|
924
|
+
status: "pending",
|
|
925
|
+
packetSha256: null,
|
|
926
|
+
assessmentSha256: null,
|
|
927
|
+
reviewSha256: null,
|
|
928
|
+
reviewerSessionSha256: null,
|
|
929
|
+
});
|
|
930
|
+
return {
|
|
931
|
+
schemaVersion: 1,
|
|
932
|
+
designSha256: input.design.sha256,
|
|
933
|
+
objectLocator: `projects/${projectId}/scientific/design/objects/${input.design.sha256}.json`,
|
|
934
|
+
centralStudyKind: contract.identity.centralStudyKind,
|
|
935
|
+
producer: {
|
|
936
|
+
agent: input.producerAgent,
|
|
937
|
+
sessionSha256: sha256Text(input.producerSessionId),
|
|
938
|
+
},
|
|
939
|
+
mechanicalIssueCodes: evaluation.issueCodes,
|
|
940
|
+
gates: {
|
|
941
|
+
"research-design": pendingGate(),
|
|
942
|
+
"evidence-construct": pendingGate(),
|
|
943
|
+
"pilot-methods": pendingGate(),
|
|
944
|
+
},
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
function isScientificDesignBinding(value, projectId) {
|
|
948
|
+
if (value === null)
|
|
949
|
+
return true;
|
|
950
|
+
if (!isObject(value) || value.schemaVersion !== 1)
|
|
951
|
+
return false;
|
|
952
|
+
if (typeof value.designSha256 !== "string" ||
|
|
953
|
+
!/^[a-f0-9]{64}$/.test(value.designSha256) ||
|
|
954
|
+
value.objectLocator !==
|
|
955
|
+
`projects/${projectId}/scientific/design/objects/${value.designSha256}.json` ||
|
|
956
|
+
typeof value.centralStudyKind !== "string" ||
|
|
957
|
+
!isObject(value.producer) ||
|
|
958
|
+
!["codex", "claude"].includes(String(value.producer.agent)) ||
|
|
959
|
+
typeof value.producer.sessionSha256 !== "string" ||
|
|
960
|
+
!/^[a-f0-9]{64}$/.test(value.producer.sessionSha256) ||
|
|
961
|
+
!Array.isArray(value.mechanicalIssueCodes) ||
|
|
962
|
+
value.mechanicalIssueCodes.some((item) => typeof item !== "string") ||
|
|
963
|
+
!isObject(value.gates)) {
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
const roles = ["research-design", "evidence-construct", "pilot-methods"];
|
|
967
|
+
const gates = value.gates;
|
|
968
|
+
return roles.every((role) => isScientificGateBinding(gates[role]));
|
|
969
|
+
}
|
|
970
|
+
function isScientificGateBinding(value) {
|
|
971
|
+
return (isObject(value) &&
|
|
972
|
+
["pending", "prepared", "passed", "revision-required", "stopped"].includes(String(value.status)) &&
|
|
973
|
+
nullableSha256(value.packetSha256) &&
|
|
974
|
+
nullableSha256(value.assessmentSha256) &&
|
|
975
|
+
nullableSha256(value.reviewSha256) &&
|
|
976
|
+
nullableSha256(value.reviewerSessionSha256));
|
|
977
|
+
}
|
|
738
978
|
function isProjectLineage(value) {
|
|
739
979
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
740
980
|
return false;
|
|
@@ -759,16 +999,65 @@ function isProjectHandoff(value) {
|
|
|
759
999
|
if (!isObject(value))
|
|
760
1000
|
return false;
|
|
761
1001
|
return (["agent-actionable", "user-action-required", "external-response-required"].includes(String(value.state)) &&
|
|
1002
|
+
(value.kind === null ||
|
|
1003
|
+
["interactive-challenge", "external-wait", "evidence-exhausted"].includes(String(value.kind))) &&
|
|
762
1004
|
nullableString(value.reasonCode) &&
|
|
763
1005
|
nullableString(value.summary) &&
|
|
764
1006
|
Array.isArray(value.requestedActions) &&
|
|
765
1007
|
value.requestedActions.every((item) => typeof item === "string") &&
|
|
766
1008
|
Array.isArray(value.evidenceGaps) &&
|
|
767
1009
|
value.evidenceGaps.every((item) => typeof item === "string") &&
|
|
1010
|
+
isEvidenceExhaustion(value.exhaustion) &&
|
|
1011
|
+
Array.isArray(value.accessRequests) &&
|
|
1012
|
+
value.accessRequests.every(isAccessRequest) &&
|
|
768
1013
|
nullableString(value.requestedAt) &&
|
|
769
1014
|
nullableString(value.resolvedAt) &&
|
|
770
1015
|
nullableString(value.resolutionNote));
|
|
771
1016
|
}
|
|
1017
|
+
function isEvidenceExhaustion(value) {
|
|
1018
|
+
if (value === null)
|
|
1019
|
+
return true;
|
|
1020
|
+
if (!isObject(value))
|
|
1021
|
+
return false;
|
|
1022
|
+
return (Array.isArray(value.missingEvidenceRoleIds) &&
|
|
1023
|
+
value.missingEvidenceRoleIds.every(isIdentifier) &&
|
|
1024
|
+
Array.isArray(value.routeAttempts) &&
|
|
1025
|
+
value.routeAttempts.every((attempt) => isObject(attempt) &&
|
|
1026
|
+
isIdentifier(attempt.routeId) &&
|
|
1027
|
+
Array.isArray(attempt.terminalEventHashes) &&
|
|
1028
|
+
attempt.terminalEventHashes.every((hash) => typeof hash === "string" && /^[a-f0-9]{64}$/.test(hash)) &&
|
|
1029
|
+
["completed-insufficient", "access-blocked", "deterministic-unavailable"].includes(String(attempt.outcome))) &&
|
|
1030
|
+
Array.isArray(value.remainingRouteIds) &&
|
|
1031
|
+
value.remainingRouteIds.every(isIdentifier));
|
|
1032
|
+
}
|
|
1033
|
+
function isAccessRequest(value) {
|
|
1034
|
+
if (!isObject(value))
|
|
1035
|
+
return false;
|
|
1036
|
+
return (isIdentifier(value.id) &&
|
|
1037
|
+
isIdentifier(value.routeId) &&
|
|
1038
|
+
[
|
|
1039
|
+
"database-subscription",
|
|
1040
|
+
"article-purchase",
|
|
1041
|
+
"institutional-access",
|
|
1042
|
+
"licensed-dataset",
|
|
1043
|
+
"owner-provided-material",
|
|
1044
|
+
"external-data-request",
|
|
1045
|
+
"field-data-collection",
|
|
1046
|
+
].includes(String(value.resourceType)) &&
|
|
1047
|
+
typeof value.resourceName === "string" &&
|
|
1048
|
+
nullableString(value.officialLocator) &&
|
|
1049
|
+
Array.isArray(value.evidenceRoleIds) &&
|
|
1050
|
+
value.evidenceRoleIds.every(isIdentifier) &&
|
|
1051
|
+
typeof value.rationale === "string" &&
|
|
1052
|
+
Array.isArray(value.alternativesTriedRouteIds) &&
|
|
1053
|
+
value.alternativesTriedRouteIds.every(isIdentifier) &&
|
|
1054
|
+
typeof value.requestedAction === "string" &&
|
|
1055
|
+
typeof value.resumeCriteria === "string" &&
|
|
1056
|
+
["unknown", "provider-quote-required"].includes(String(value.costStatus)));
|
|
1057
|
+
}
|
|
1058
|
+
function isIdentifier(value) {
|
|
1059
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value);
|
|
1060
|
+
}
|
|
772
1061
|
function nullableString(value) {
|
|
773
1062
|
return value === null || typeof value === "string";
|
|
774
1063
|
}
|