@tiangong-ai/cli 0.0.61 → 0.0.63
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 +17 -6
- package/CONTRIBUTING.md +5 -5
- package/README.md +417 -36
- package/dist/cli.js +2 -2
- package/dist/data/builtins.js +33 -5
- package/dist/data/builtins.js.map +1 -1
- package/dist/data/catalog.js +46 -0
- package/dist/data/catalog.js.map +1 -1
- package/dist/data/commands.js +15 -1
- package/dist/data/commands.js.map +1 -1
- package/dist/data/connectors/airnow-hourly-observations.js +60 -10
- package/dist/data/connectors/airnow-hourly-observations.js.map +1 -1
- package/dist/data/connectors/epa-eis-records.js +16 -2
- package/dist/data/connectors/epa-eis-records.js.map +1 -1
- package/dist/data/connectors/gdelt-doc-search.js +54 -18
- package/dist/data/connectors/gdelt-doc-search.js.map +1 -1
- package/dist/data/connectors/gdelt-file-feeds.js +20 -11
- package/dist/data/connectors/gdelt-file-feeds.js.map +1 -1
- package/dist/data/connectors/gdelt-web-ngrams.d.ts +2 -0
- package/dist/data/connectors/gdelt-web-ngrams.js +350 -0
- package/dist/data/connectors/gdelt-web-ngrams.js.map +1 -0
- package/dist/data/connectors/gdelt-web-ngrams.schemas.d.ts +188 -0
- package/dist/data/connectors/gdelt-web-ngrams.schemas.js +138 -0
- package/dist/data/connectors/gdelt-web-ngrams.schemas.js.map +1 -0
- package/dist/data/connectors/nasa-firms-fire.js +4 -0
- package/dist/data/connectors/nasa-firms-fire.js.map +1 -1
- package/dist/data/connectors/open-meteo-air-quality.js +27 -22
- package/dist/data/connectors/open-meteo-air-quality.js.map +1 -1
- package/dist/data/connectors/open-meteo-air-quality.schemas.d.ts +4 -1
- package/dist/data/connectors/open-meteo-air-quality.schemas.js +21 -1
- package/dist/data/connectors/open-meteo-air-quality.schemas.js.map +1 -1
- package/dist/data/connectors/open-meteo-flood.js +28 -23
- package/dist/data/connectors/open-meteo-flood.js.map +1 -1
- package/dist/data/connectors/open-meteo-flood.schemas.d.ts +4 -1
- package/dist/data/connectors/open-meteo-flood.schemas.js +21 -1
- package/dist/data/connectors/open-meteo-flood.schemas.js.map +1 -1
- package/dist/data/connectors/open-meteo-historical-weather.js +26 -21
- package/dist/data/connectors/open-meteo-historical-weather.js.map +1 -1
- package/dist/data/connectors/open-meteo-historical-weather.schemas.d.ts +4 -1
- package/dist/data/connectors/open-meteo-historical-weather.schemas.js +21 -1
- package/dist/data/connectors/open-meteo-historical-weather.schemas.js.map +1 -1
- package/dist/data/connectors/openaq-air-quality.js +50 -12
- package/dist/data/connectors/openaq-air-quality.js.map +1 -1
- package/dist/data/connectors/openaq-air-quality.schemas.d.ts +0 -4
- package/dist/data/connectors/openaq-air-quality.schemas.js +2 -2
- package/dist/data/connectors/openaq-air-quality.schemas.js.map +1 -1
- package/dist/data/connectors/youtube-public-content.js +76 -6
- package/dist/data/connectors/youtube-public-content.js.map +1 -1
- package/dist/data/connectors/youtube-public-content.schemas.d.ts +63 -3
- package/dist/data/connectors/youtube-public-content.schemas.js +56 -2
- package/dist/data/connectors/youtube-public-content.schemas.js.map +1 -1
- package/dist/data/contracts.d.ts +22 -1
- package/dist/data/runtime/bounded-http.d.ts +2 -0
- package/dist/data/runtime/bounded-http.js +221 -59
- package/dist/data/runtime/bounded-http.js.map +1 -1
- package/dist/data/runtime/execute.d.ts +2 -0
- package/dist/data/runtime/execute.js +82 -60
- package/dist/data/runtime/execute.js.map +1 -1
- package/dist/data/runtime/http-timing.d.ts +15 -0
- package/dist/data/runtime/http-timing.js +59 -0
- package/dist/data/runtime/http-timing.js.map +1 -0
- package/dist/data/runtime/http-transport.d.ts +1 -0
- package/dist/data/runtime/http-transport.js +27 -0
- package/dist/data/runtime/http-transport.js.map +1 -0
- package/dist/data/schemas/catalog.v1.json +46 -0
- package/dist/data/schemas/discovery.v1.json +12 -0
- package/dist/data/schemas/error.v1.json +1 -0
- package/dist/data/schemas/manifest.v1.json +19 -1
- package/dist/research/orchestration.js +233 -10
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/setup-command.js +37 -6
- package/dist/research/setup-command.js.map +1 -1
- package/dist/research/workspace/audit-bundle.js +28 -8
- package/dist/research/workspace/audit-bundle.js.map +1 -1
- package/dist/research/workspace/broker.js +25 -4
- package/dist/research/workspace/broker.js.map +1 -1
- package/dist/research/workspace/constants.d.ts +1 -0
- package/dist/research/workspace/constants.js +1 -0
- package/dist/research/workspace/constants.js.map +1 -1
- package/dist/research/workspace/content-evidence.d.ts +1 -0
- package/dist/research/workspace/content-evidence.js +1 -1
- package/dist/research/workspace/content-evidence.js.map +1 -1
- package/dist/research/workspace/credentials.js +2 -1
- package/dist/research/workspace/credentials.js.map +1 -1
- package/dist/research/workspace/data-evidence-adapter.d.ts +71 -0
- package/dist/research/workspace/data-evidence-adapter.js +269 -59
- package/dist/research/workspace/data-evidence-adapter.js.map +1 -1
- package/dist/research/workspace/data-evidence-view.d.ts +58 -0
- package/dist/research/workspace/data-evidence-view.js +411 -0
- package/dist/research/workspace/data-evidence-view.js.map +1 -0
- package/dist/research/workspace/evidence-ledger.js +32 -29
- package/dist/research/workspace/evidence-ledger.js.map +1 -1
- package/dist/research/workspace/evidence.d.ts +28 -0
- package/dist/research/workspace/evidence.js +48 -0
- package/dist/research/workspace/evidence.js.map +1 -1
- package/dist/research/workspace/executor.d.ts +27 -1
- package/dist/research/workspace/executor.js +180 -20
- package/dist/research/workspace/executor.js.map +1 -1
- package/dist/research/workspace/external-skills.d.ts +1 -0
- package/dist/research/workspace/external-skills.js +4 -2
- package/dist/research/workspace/external-skills.js.map +1 -1
- package/dist/research/workspace/investigation-attempt.d.ts +159 -0
- package/dist/research/workspace/investigation-attempt.js +619 -0
- package/dist/research/workspace/investigation-attempt.js.map +1 -0
- package/dist/research/workspace/investigation-audit.d.ts +20 -0
- package/dist/research/workspace/investigation-audit.js +306 -0
- package/dist/research/workspace/investigation-audit.js.map +1 -0
- package/dist/research/workspace/investigation-candidate.d.ts +84 -0
- package/dist/research/workspace/investigation-candidate.js +156 -0
- package/dist/research/workspace/investigation-candidate.js.map +1 -0
- package/dist/research/workspace/investigation-certification.d.ts +39 -0
- package/dist/research/workspace/investigation-certification.js +152 -0
- package/dist/research/workspace/investigation-certification.js.map +1 -0
- package/dist/research/workspace/investigation-close.d.ts +47 -0
- package/dist/research/workspace/investigation-close.js +153 -0
- package/dist/research/workspace/investigation-close.js.map +1 -0
- package/dist/research/workspace/investigation-lineage.d.ts +8 -0
- package/dist/research/workspace/investigation-lineage.js +10 -0
- package/dist/research/workspace/investigation-lineage.js.map +1 -0
- package/dist/research/workspace/investigation-observer.d.ts +22 -0
- package/dist/research/workspace/investigation-observer.js +122 -0
- package/dist/research/workspace/investigation-observer.js.map +1 -0
- package/dist/research/workspace/investigation-promotion.d.ts +129 -0
- package/dist/research/workspace/investigation-promotion.js +316 -0
- package/dist/research/workspace/investigation-promotion.js.map +1 -0
- package/dist/research/workspace/investigation-requirements.d.ts +5 -0
- package/dist/research/workspace/investigation-requirements.js +32 -0
- package/dist/research/workspace/investigation-requirements.js.map +1 -0
- package/dist/research/workspace/investigation-resources.d.ts +12 -0
- package/dist/research/workspace/investigation-resources.js +78 -0
- package/dist/research/workspace/investigation-resources.js.map +1 -0
- package/dist/research/workspace/investigation-source-export.d.ts +6 -0
- package/dist/research/workspace/investigation-source-export.js +68 -0
- package/dist/research/workspace/investigation-source-export.js.map +1 -0
- package/dist/research/workspace/investigation-store.d.ts +9 -0
- package/dist/research/workspace/investigation-store.js +55 -0
- package/dist/research/workspace/investigation-store.js.map +1 -0
- package/dist/research/workspace/investigation.d.ts +424 -0
- package/dist/research/workspace/investigation.js +716 -0
- package/dist/research/workspace/investigation.js.map +1 -0
- package/dist/research/workspace/native-process-supervisor.d.ts +1 -0
- package/dist/research/workspace/native-process-supervisor.js +97 -0
- package/dist/research/workspace/native-process-supervisor.js.map +1 -0
- package/dist/research/workspace/native-run.d.ts +79 -6
- package/dist/research/workspace/native-run.js +293 -51
- package/dist/research/workspace/native-run.js.map +1 -1
- package/dist/research/workspace/preflight.d.ts +3 -0
- package/dist/research/workspace/preflight.js +11 -6
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/project-authority.js +2 -0
- package/dist/research/workspace/project-authority.js.map +1 -1
- package/dist/research/workspace/project-budget.d.ts +41 -0
- package/dist/research/workspace/project-budget.js +265 -0
- package/dist/research/workspace/project-budget.js.map +1 -0
- package/dist/research/workspace/project-mutations.d.ts +1 -1
- package/dist/research/workspace/project-mutations.js +27 -3
- package/dist/research/workspace/project-mutations.js.map +1 -1
- package/dist/research/workspace/projects.d.ts +66 -0
- package/dist/research/workspace/projects.js +181 -3
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/provider-budget.d.ts +3 -0
- package/dist/research/workspace/provider-budget.js +45 -0
- package/dist/research/workspace/provider-budget.js.map +1 -0
- package/dist/research/workspace/provider-state.d.ts +1 -0
- package/dist/research/workspace/provider-state.js +20 -0
- package/dist/research/workspace/provider-state.js.map +1 -0
- package/dist/research/workspace/publication-lineage.d.ts +34 -0
- package/dist/research/workspace/publication-lineage.js +165 -0
- package/dist/research/workspace/publication-lineage.js.map +1 -0
- package/dist/research/workspace/publication-workflow.d.ts +24 -0
- package/dist/research/workspace/publication-workflow.js +118 -14
- package/dist/research/workspace/publication-workflow.js.map +1 -1
- package/dist/research/workspace/review-executor.d.ts +6 -1
- package/dist/research/workspace/review-executor.js +17 -3
- package/dist/research/workspace/review-executor.js.map +1 -1
- package/dist/research/workspace/runtime.d.ts +12 -1
- package/dist/research/workspace/runtime.js +143 -19
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/scientific-amendment.d.ts +103 -0
- package/dist/research/workspace/scientific-amendment.js +537 -0
- package/dist/research/workspace/scientific-amendment.js.map +1 -0
- package/dist/research/workspace/scientific-fulfillment-audit.d.ts +20 -2
- package/dist/research/workspace/scientific-fulfillment-audit.js +87 -7
- package/dist/research/workspace/scientific-fulfillment-audit.js.map +1 -1
- package/dist/research/workspace/scientific-fulfillment.d.ts +10 -1
- package/dist/research/workspace/scientific-fulfillment.js +20 -6
- package/dist/research/workspace/scientific-fulfillment.js.map +1 -1
- package/dist/research/workspace/scientific-review-execution.js +162 -22
- package/dist/research/workspace/scientific-review-execution.js.map +1 -1
- package/dist/research/workspace/scientific-review.d.ts +2 -1
- package/dist/research/workspace/scientific-review.js +33 -2
- package/dist/research/workspace/scientific-review.js.map +1 -1
- package/dist/research/workspace/setup-catalog.js +13 -13
- package/dist/research/workspace/setup-catalog.js.map +1 -1
- package/dist/research/workspace/setup-release.d.ts +22 -0
- package/dist/research/workspace/setup-release.js +426 -0
- package/dist/research/workspace/setup-release.js.map +1 -0
- package/dist/research/workspace/setup-runtime-integrity.d.ts +2 -0
- package/dist/research/workspace/setup-runtime-integrity.js +31 -0
- package/dist/research/workspace/setup-runtime-integrity.js.map +1 -0
- package/dist/research/workspace/setup-upgrade.d.ts +17 -0
- package/dist/research/workspace/setup-upgrade.js +784 -0
- package/dist/research/workspace/setup-upgrade.js.map +1 -0
- package/dist/research/workspace/setup.d.ts +31 -39
- package/dist/research/workspace/setup.js +228 -19
- package/dist/research/workspace/setup.js.map +1 -1
- package/dist/research/workspace/task-acceptance.d.ts +8 -1
- package/dist/research/workspace/task-acceptance.js +37 -5
- package/dist/research/workspace/task-acceptance.js.map +1 -1
- package/dist/research/workspace/task-audit.d.ts +5 -1
- package/dist/research/workspace/task-audit.js +33 -18
- package/dist/research/workspace/task-audit.js.map +1 -1
- package/dist/research/workspace/types.d.ts +44 -0
- package/dist/research/workspace/workspace.js +34 -4
- package/dist/research/workspace/workspace.js.map +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { type ScientificDesignContract } from "./scientific-design.js";
|
|
2
|
+
import { type ScientificFulfillmentRecord } from "./scientific-fulfillment.js";
|
|
3
|
+
import type { JournalEvent, ProjectState } from "./types.js";
|
|
4
|
+
type Rule = ScientificDesignContract["policyRuleDispositions"][number];
|
|
5
|
+
export interface ScientificAmendmentPlan {
|
|
6
|
+
schemaVersion: 1;
|
|
7
|
+
kind: "tiangong-scientific-amendment-plan";
|
|
8
|
+
projectId: string;
|
|
9
|
+
parentDesignSha256: string;
|
|
10
|
+
parentFulfillmentSha256: string | null;
|
|
11
|
+
parentAmendmentSha256: string | null;
|
|
12
|
+
parentEffectiveDesignSha256: string;
|
|
13
|
+
parentProjectSha256: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
changes: Array<{
|
|
16
|
+
ruleId: string;
|
|
17
|
+
before: Rule;
|
|
18
|
+
after: Rule;
|
|
19
|
+
}>;
|
|
20
|
+
proposedEffectiveDesignSha256: string;
|
|
21
|
+
invalidatedScientificRoles: string[];
|
|
22
|
+
affectedTaskRequirementIds: string[];
|
|
23
|
+
preservedAcquisitionSnapshotSha256: string | null;
|
|
24
|
+
planSha256: string;
|
|
25
|
+
nextAction: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ScientificAmendmentRecord {
|
|
28
|
+
schemaVersion: 1;
|
|
29
|
+
kind: "tiangong-scientific-amendment";
|
|
30
|
+
projectId: string;
|
|
31
|
+
plan: ScientificAmendmentPlan;
|
|
32
|
+
amendmentAuthorization: {
|
|
33
|
+
kind: "operator-confirmation";
|
|
34
|
+
planSha256: string;
|
|
35
|
+
sourceSha256: string;
|
|
36
|
+
sourceLocator: string;
|
|
37
|
+
sourceBytes: number;
|
|
38
|
+
};
|
|
39
|
+
design: {
|
|
40
|
+
sha256: string;
|
|
41
|
+
objectLocator: string;
|
|
42
|
+
};
|
|
43
|
+
recordSha256: string;
|
|
44
|
+
}
|
|
45
|
+
export interface ScientificAmendmentImpact {
|
|
46
|
+
claims: Map<string, {
|
|
47
|
+
sha256: string;
|
|
48
|
+
ordinal: number;
|
|
49
|
+
}>;
|
|
50
|
+
coverage: Map<string, {
|
|
51
|
+
sha256: string;
|
|
52
|
+
ordinal: number;
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
/** An operation-local dependency index over the already verified design history. */
|
|
56
|
+
export declare function scientificAmendmentImpact(records: ScientificAmendmentRecord[], design: ScientificDesignContract): ScientificAmendmentImpact;
|
|
57
|
+
export declare function requirementAmendmentBinding(requirement: {
|
|
58
|
+
designClaimIds: string[];
|
|
59
|
+
coverageDimensionIds: string[];
|
|
60
|
+
}, impact?: ScientificAmendmentImpact): string | null;
|
|
61
|
+
export declare function loadScientificAmendmentImpact(root: string, project: ProjectState, events: JournalEvent[]): Promise<ScientificAmendmentImpact | undefined>;
|
|
62
|
+
export declare function scientificAmendmentSchema(): Record<string, unknown>;
|
|
63
|
+
/** Derive a reviewable proposal without changing frozen bytes or authority. */
|
|
64
|
+
export declare function planScientificAmendment(root: string, projectId: string, value: unknown): Promise<ScientificAmendmentPlan>;
|
|
65
|
+
export declare function scientificAmendmentLocator(projectId: string, hash: string): string;
|
|
66
|
+
export declare function validateScientificAmendmentRecord(value: unknown, projectId: string, hash: string): ScientificAmendmentRecord;
|
|
67
|
+
/** Shared by the live loader and portable verifier; rule patches commute with slot fulfillment. */
|
|
68
|
+
export declare function projectScientificAmendments(base: ScientificDesignContract, baseSha256: string, fulfillments: ScientificFulfillmentRecord[], records: ScientificAmendmentRecord[]): ScientificDesignContract;
|
|
69
|
+
export declare function loadScientificAmendments(root: string, project: ProjectState, events: JournalEvent[]): Promise<ScientificAmendmentRecord[]>;
|
|
70
|
+
export declare function applyScientificAmendment(root: string, projectId: string, value: unknown, confirmation: string | undefined, sourcePath: string | undefined): Promise<ScientificAmendmentRecord>;
|
|
71
|
+
export declare function inspectScientificAmendment(root: string, projectId: string): Promise<{
|
|
72
|
+
projectId: string;
|
|
73
|
+
amendmentSha256: string | null;
|
|
74
|
+
originalDesignSha256: string | undefined;
|
|
75
|
+
effectiveDesignSha256: string;
|
|
76
|
+
plannedRules: {
|
|
77
|
+
ruleId: string;
|
|
78
|
+
dueGate: "evidence-construct" | "pilot-methods" | "publication-freeze" | "research-design";
|
|
79
|
+
rationale: string;
|
|
80
|
+
modelStructureIds: string[];
|
|
81
|
+
uncertaintyParameterIds: string[];
|
|
82
|
+
}[];
|
|
83
|
+
records: {
|
|
84
|
+
recordSha256: string;
|
|
85
|
+
planSha256: string;
|
|
86
|
+
parentAmendmentSha256: string | null;
|
|
87
|
+
design: {
|
|
88
|
+
sha256: string;
|
|
89
|
+
objectLocator: string;
|
|
90
|
+
};
|
|
91
|
+
reason: string;
|
|
92
|
+
affectedTaskRequirementIds: string[];
|
|
93
|
+
}[];
|
|
94
|
+
gates: Record<import("./types.js").ScientificReviewRole, {
|
|
95
|
+
status: import("./types.js").ScientificGateStatus;
|
|
96
|
+
packetSha256: string | null;
|
|
97
|
+
assessmentSha256: string | null;
|
|
98
|
+
reviewSha256: string | null;
|
|
99
|
+
reviewerSessionSha256: string | null;
|
|
100
|
+
}> | undefined;
|
|
101
|
+
nextAction: string;
|
|
102
|
+
}>;
|
|
103
|
+
export {};
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2
|
+
import { lstat, mkdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { CliError } from "../../errors.js";
|
|
5
|
+
import { appendJournalEvent, readVerifiedJournal } from "./journal.js";
|
|
6
|
+
import { assertProjectAuthority, projectAuthorityIndex } from "./project-authority.js";
|
|
7
|
+
import { loadProject } from "./projects.js";
|
|
8
|
+
import { assertResearchPolicyBinding } from "./research-policy.js";
|
|
9
|
+
import { configuredResearchSecrets, sanitizeResearchValue } from "./sanitization.js";
|
|
10
|
+
import { evaluateScientificDesign } from "./scientific-design.js";
|
|
11
|
+
import { applyScientificFulfillmentRecord, loadScientificFulfillmentView, } from "./scientific-fulfillment.js";
|
|
12
|
+
import { canonicalJson, isObject, pathExists, resolveContained, sha256Text, workspacePaths, writeTextAtomic, } from "./storage.js";
|
|
13
|
+
import { beginProjectMutation, prepareProjectMutation, projectMutationBinding, settleProjectMutation, } from "./project-mutations.js";
|
|
14
|
+
import { withWorkspaceLock } from "./workspace.js";
|
|
15
|
+
const HASH = /^[a-f0-9]{64}$/;
|
|
16
|
+
const PLAN_NEXT_ACTION = "Review the exact changes and their reason. Apply requires confirmation of this plan hash and its supplied authorization source; neither proves authenticated authorship or scientific approval.";
|
|
17
|
+
/** An operation-local dependency index over the already verified design history. */
|
|
18
|
+
export function scientificAmendmentImpact(records, design) {
|
|
19
|
+
const impact = { claims: new Map(), coverage: new Map() };
|
|
20
|
+
for (const [ordinal, record] of records.entries()) {
|
|
21
|
+
const binding = { sha256: record.recordSha256, ordinal };
|
|
22
|
+
const scope = amendmentChangeScope(record.plan.changes, design);
|
|
23
|
+
for (const id of scope.claims)
|
|
24
|
+
impact.claims.set(id, binding);
|
|
25
|
+
for (const id of scope.coverage)
|
|
26
|
+
impact.coverage.set(id, binding);
|
|
27
|
+
}
|
|
28
|
+
return impact;
|
|
29
|
+
}
|
|
30
|
+
function amendmentChangeScope(changes, design) {
|
|
31
|
+
const claims = new Set();
|
|
32
|
+
const coverage = new Set();
|
|
33
|
+
const roles = new Map(design.evidenceRoles.map((role) => [role.id, role.coverageDimensionIds]));
|
|
34
|
+
for (const change of changes)
|
|
35
|
+
for (const rule of [change.before, change.after]) {
|
|
36
|
+
for (const id of rule.claimIds)
|
|
37
|
+
claims.add(id);
|
|
38
|
+
for (const id of rule.evidenceRoleIds)
|
|
39
|
+
for (const dimension of roles.get(id) ?? [])
|
|
40
|
+
coverage.add(dimension);
|
|
41
|
+
}
|
|
42
|
+
return { claims, coverage };
|
|
43
|
+
}
|
|
44
|
+
export function requirementAmendmentBinding(requirement, impact) {
|
|
45
|
+
let latest;
|
|
46
|
+
for (const binding of [
|
|
47
|
+
...requirement.designClaimIds.map((id) => impact?.claims.get(id)),
|
|
48
|
+
...requirement.coverageDimensionIds.map((id) => impact?.coverage.get(id)),
|
|
49
|
+
])
|
|
50
|
+
if (binding && (!latest || binding.ordinal > latest.ordinal))
|
|
51
|
+
latest = binding;
|
|
52
|
+
return latest?.sha256 ?? null;
|
|
53
|
+
}
|
|
54
|
+
export async function loadScientificAmendmentImpact(root, project, events) {
|
|
55
|
+
if (!project.scientificDesign?.amendmentSha256 &&
|
|
56
|
+
!events.some((event) => event.scope === project.id && event.type === "scientific.amendment.recorded"))
|
|
57
|
+
return undefined;
|
|
58
|
+
const view = await loadScientificFulfillmentView(root, project, undefined, events);
|
|
59
|
+
return scientificAmendmentImpact(view.amendments, view.base);
|
|
60
|
+
}
|
|
61
|
+
const ids = {
|
|
62
|
+
type: "array",
|
|
63
|
+
uniqueItems: true,
|
|
64
|
+
maxItems: 128,
|
|
65
|
+
items: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" },
|
|
66
|
+
};
|
|
67
|
+
const inputSchema = {
|
|
68
|
+
type: "object",
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
required: ["schemaVersion", "reason", "changes"],
|
|
71
|
+
properties: {
|
|
72
|
+
schemaVersion: { const: 1 },
|
|
73
|
+
reason: { type: "string", minLength: 8, maxLength: 4000 },
|
|
74
|
+
changes: {
|
|
75
|
+
type: "array",
|
|
76
|
+
minItems: 1,
|
|
77
|
+
maxItems: 128,
|
|
78
|
+
items: {
|
|
79
|
+
type: "object",
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
required: [
|
|
82
|
+
"ruleId",
|
|
83
|
+
"dueGate",
|
|
84
|
+
"rationale",
|
|
85
|
+
"modelStructureIds",
|
|
86
|
+
"uncertaintyParameterIds",
|
|
87
|
+
],
|
|
88
|
+
properties: {
|
|
89
|
+
ruleId: { type: "string", minLength: 1, maxLength: 128 },
|
|
90
|
+
dueGate: {
|
|
91
|
+
enum: ["research-design", "evidence-construct", "pilot-methods", "publication-freeze"],
|
|
92
|
+
},
|
|
93
|
+
rationale: { type: "string", minLength: 8, maxLength: 4000 },
|
|
94
|
+
modelStructureIds: ids,
|
|
95
|
+
uncertaintyParameterIds: ids,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const validate = new Ajv2020({ strict: false, allErrors: true }).compile(inputSchema);
|
|
102
|
+
export function scientificAmendmentSchema() {
|
|
103
|
+
return structuredClone(inputSchema);
|
|
104
|
+
}
|
|
105
|
+
function invalid(message, code = "RESEARCH_SCIENTIFIC_AMENDMENT_INVALID") {
|
|
106
|
+
return new CliError(message, { code, exitCode: code.endsWith("INVALID") ? 2 : 3 });
|
|
107
|
+
}
|
|
108
|
+
/** Derive a reviewable proposal without changing frozen bytes or authority. */
|
|
109
|
+
export async function planScientificAmendment(root, projectId, value) {
|
|
110
|
+
if (!validate(value) ||
|
|
111
|
+
canonicalJson(sanitizeResearchValue(value, configuredResearchSecrets(process.env))) !==
|
|
112
|
+
canonicalJson(value))
|
|
113
|
+
throw invalid("Amendment must match the closed, secret-free lifecycle/binding schema.");
|
|
114
|
+
const input = value;
|
|
115
|
+
if (input.reason.trim().length < 8 ||
|
|
116
|
+
input.changes.some((change) => change.rationale.trim().length < 8) ||
|
|
117
|
+
new Set(input.changes.map((change) => change.ruleId)).size !== input.changes.length)
|
|
118
|
+
throw invalid("Amendment requires a reason, substantive rationales and unique existing rules.");
|
|
119
|
+
const paths = workspacePaths(root);
|
|
120
|
+
const project = await loadProject(root, projectId);
|
|
121
|
+
const events = await readVerifiedJournal(paths.journal);
|
|
122
|
+
assertProjectAuthority(project, projectAuthorityIndex(events));
|
|
123
|
+
const analyze = project.packages.find((item) => item.stage === "analyze");
|
|
124
|
+
if (!project.scientificDesign ||
|
|
125
|
+
project.status === "complete" ||
|
|
126
|
+
project.handoff.state !== "agent-actionable" ||
|
|
127
|
+
!analyze ||
|
|
128
|
+
analyze.attempts > 0 ||
|
|
129
|
+
analyze.startedAt ||
|
|
130
|
+
!["ready", "pending"].includes(analyze.status) ||
|
|
131
|
+
(await pathExists(join(paths.projects, project.id, "native/active.json"))) ||
|
|
132
|
+
(await pathExists(join(paths.projects, project.id, "outputs/inference-snapshot.json"))))
|
|
133
|
+
throw invalid("Amend only an idle authoritative pre-analysis project. Resolve the active session or handoff; substantive or post-analysis changes require a reviewed successor.", "RESEARCH_SCIENTIFIC_AMENDMENT_UNAVAILABLE");
|
|
134
|
+
if (!project.publicationPolicy)
|
|
135
|
+
throw conflict();
|
|
136
|
+
await assertResearchPolicyBinding(root, project.publicationPolicy);
|
|
137
|
+
const view = await loadScientificFulfillmentView(root, project, undefined, events);
|
|
138
|
+
const effective = structuredClone(view.contract);
|
|
139
|
+
const declaredRules = new Set(project.publicationPolicy?.resolvedRules ?? []);
|
|
140
|
+
const models = new Set(effective.identity.modelStructures.map((model) => model.id));
|
|
141
|
+
const parameters = new Set(effective.uncertaintyParameters.map((parameter) => parameter.id));
|
|
142
|
+
const changes = input.changes.map((change) => {
|
|
143
|
+
const rule = effective.policyRuleDispositions.find((item) => item.ruleId === change.ruleId);
|
|
144
|
+
if (!rule ||
|
|
145
|
+
!declaredRules.has(change.ruleId) ||
|
|
146
|
+
rule.status !== "planned" ||
|
|
147
|
+
change.modelStructureIds.some((id) => !models.has(id)) ||
|
|
148
|
+
change.uncertaintyParameterIds.some((id) => !parameters.has(id)))
|
|
149
|
+
throw invalid("Amend only planned Policy rules and their existing declared model/parameter IDs.");
|
|
150
|
+
const before = structuredClone(rule);
|
|
151
|
+
Object.assign(rule, change);
|
|
152
|
+
const after = structuredClone(rule);
|
|
153
|
+
if (canonicalJson(before) === canonicalJson(after))
|
|
154
|
+
throw invalid("Each proposed rule amendment must change its lifecycle or bindings.");
|
|
155
|
+
return { ruleId: rule.ruleId, before, after };
|
|
156
|
+
});
|
|
157
|
+
const introducedIssues = evaluateScientificDesign(effective).issueCodes;
|
|
158
|
+
if (introducedIssues.length)
|
|
159
|
+
throw new CliError("Amendment introduces blocking scientific-design issues.", {
|
|
160
|
+
code: "RESEARCH_SCIENTIFIC_AMENDMENT_INVALID",
|
|
161
|
+
exitCode: 2,
|
|
162
|
+
details: { issueCodes: introducedIssues },
|
|
163
|
+
});
|
|
164
|
+
const { loadProjectTask } = await import("./task-contract.js");
|
|
165
|
+
const task = await loadProjectTask(root, projectId, events);
|
|
166
|
+
const scope = amendmentChangeScope(changes, view.base);
|
|
167
|
+
const affectedTaskRequirementIds = (task?.current.requirements ?? [])
|
|
168
|
+
.filter((requirement) => requirement.designClaimIds.some((id) => scope.claims.has(id)) ||
|
|
169
|
+
requirement.coverageDimensionIds.some((id) => scope.coverage.has(id)))
|
|
170
|
+
.map((requirement) => requirement.id)
|
|
171
|
+
.sort();
|
|
172
|
+
const core = {
|
|
173
|
+
schemaVersion: 1,
|
|
174
|
+
kind: "tiangong-scientific-amendment-plan",
|
|
175
|
+
projectId: project.id,
|
|
176
|
+
parentDesignSha256: project.scientificDesign.designSha256,
|
|
177
|
+
parentFulfillmentSha256: view.headSha256,
|
|
178
|
+
parentAmendmentSha256: project.scientificDesign.amendmentSha256 ?? null,
|
|
179
|
+
parentEffectiveDesignSha256: view.effectiveSha256,
|
|
180
|
+
parentProjectSha256: sha256Text(canonicalJson(project)),
|
|
181
|
+
reason: input.reason,
|
|
182
|
+
changes,
|
|
183
|
+
proposedEffectiveDesignSha256: sha256Text(canonicalJson(effective)),
|
|
184
|
+
invalidatedScientificRoles: ["research-design", "evidence-construct", "pilot-methods"],
|
|
185
|
+
affectedTaskRequirementIds,
|
|
186
|
+
preservedAcquisitionSnapshotSha256: project.evidenceState.currentSnapshotSha256,
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
...core,
|
|
190
|
+
planSha256: sha256Text(canonicalJson(core)),
|
|
191
|
+
nextAction: PLAN_NEXT_ACTION,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function conflict() {
|
|
195
|
+
return invalid("Amendment no longer matches its exact parent, authorized changes or immutable history. Inspect amendment status and prepare a new plan; do not edit frozen records.", "RESEARCH_SCIENTIFIC_AMENDMENT_CONFLICT");
|
|
196
|
+
}
|
|
197
|
+
function planInput(plan) {
|
|
198
|
+
return {
|
|
199
|
+
schemaVersion: 1,
|
|
200
|
+
reason: plan.reason,
|
|
201
|
+
changes: plan.changes.map(({ ruleId, after }) => ({
|
|
202
|
+
ruleId,
|
|
203
|
+
dueGate: after.dueGate,
|
|
204
|
+
rationale: after.rationale,
|
|
205
|
+
modelStructureIds: after.modelStructureIds,
|
|
206
|
+
uncertaintyParameterIds: after.uncertaintyParameterIds,
|
|
207
|
+
})),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function validatePlan(value) {
|
|
211
|
+
if (!isObject(value))
|
|
212
|
+
throw conflict();
|
|
213
|
+
const { planSha256, nextAction, ...core } = value;
|
|
214
|
+
if (Object.keys(value).sort().join(",") !==
|
|
215
|
+
"affectedTaskRequirementIds,changes,invalidatedScientificRoles,kind,nextAction,parentAmendmentSha256,parentDesignSha256,parentEffectiveDesignSha256,parentFulfillmentSha256,parentProjectSha256,planSha256,preservedAcquisitionSnapshotSha256,projectId,proposedEffectiveDesignSha256,reason,schemaVersion" ||
|
|
216
|
+
value.schemaVersion !== 1 ||
|
|
217
|
+
value.kind !== "tiangong-scientific-amendment-plan" ||
|
|
218
|
+
typeof value.projectId !== "string" ||
|
|
219
|
+
!/^[a-z0-9][a-z0-9-]{2,63}$/.test(value.projectId) ||
|
|
220
|
+
nextAction !== PLAN_NEXT_ACTION ||
|
|
221
|
+
planSha256 !== sha256Text(canonicalJson(core)) ||
|
|
222
|
+
[
|
|
223
|
+
value.parentDesignSha256,
|
|
224
|
+
value.parentEffectiveDesignSha256,
|
|
225
|
+
value.parentProjectSha256,
|
|
226
|
+
value.proposedEffectiveDesignSha256,
|
|
227
|
+
].some((hash) => typeof hash !== "string" || !HASH.test(hash)) ||
|
|
228
|
+
[
|
|
229
|
+
value.parentAmendmentSha256,
|
|
230
|
+
value.parentFulfillmentSha256,
|
|
231
|
+
value.preservedAcquisitionSnapshotSha256,
|
|
232
|
+
].some((hash) => hash !== null && (typeof hash !== "string" || !HASH.test(hash))) ||
|
|
233
|
+
canonicalJson(value.invalidatedScientificRoles) !==
|
|
234
|
+
canonicalJson(["research-design", "evidence-construct", "pilot-methods"]) ||
|
|
235
|
+
!Array.isArray(value.affectedTaskRequirementIds) ||
|
|
236
|
+
value.affectedTaskRequirementIds.some((id) => typeof id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id)) ||
|
|
237
|
+
new Set(value.affectedTaskRequirementIds).size !== value.affectedTaskRequirementIds.length ||
|
|
238
|
+
!Array.isArray(value.changes) ||
|
|
239
|
+
value.changes.some((change) => !isObject(change) ||
|
|
240
|
+
!isObject(change.before) ||
|
|
241
|
+
!isObject(change.after) ||
|
|
242
|
+
Object.keys(change).sort().join(",") !== "after,before,ruleId"))
|
|
243
|
+
throw conflict();
|
|
244
|
+
const plan = value;
|
|
245
|
+
if (!validate(planInput(plan)) ||
|
|
246
|
+
new Set(plan.changes.map((item) => item.ruleId)).size !== plan.changes.length)
|
|
247
|
+
throw conflict();
|
|
248
|
+
return plan;
|
|
249
|
+
}
|
|
250
|
+
export function scientificAmendmentLocator(projectId, hash) {
|
|
251
|
+
if (!/^[a-z0-9][a-z0-9-]{2,63}$/.test(projectId) || !HASH.test(hash))
|
|
252
|
+
throw conflict();
|
|
253
|
+
return `projects/${projectId}/scientific/amendments/${hash}.json`;
|
|
254
|
+
}
|
|
255
|
+
export function validateScientificAmendmentRecord(value, projectId, hash) {
|
|
256
|
+
if (!isObject(value) ||
|
|
257
|
+
Object.keys(value).sort().join(",") !==
|
|
258
|
+
"amendmentAuthorization,design,kind,plan,projectId,recordSha256,schemaVersion")
|
|
259
|
+
throw conflict();
|
|
260
|
+
const { recordSha256, ...core } = value;
|
|
261
|
+
if (value.schemaVersion !== 1 ||
|
|
262
|
+
value.kind !== "tiangong-scientific-amendment" ||
|
|
263
|
+
value.projectId !== projectId ||
|
|
264
|
+
recordSha256 !== hash ||
|
|
265
|
+
sha256Text(canonicalJson(core)) !== hash)
|
|
266
|
+
throw conflict();
|
|
267
|
+
const plan = validatePlan(value.plan);
|
|
268
|
+
const authorization = value.amendmentAuthorization;
|
|
269
|
+
const design = value.design;
|
|
270
|
+
if (plan.projectId !== projectId ||
|
|
271
|
+
!isObject(authorization) ||
|
|
272
|
+
!isObject(design) ||
|
|
273
|
+
Object.keys(authorization).sort().join(",") !==
|
|
274
|
+
"kind,planSha256,sourceBytes,sourceLocator,sourceSha256" ||
|
|
275
|
+
authorization.kind !== "operator-confirmation" ||
|
|
276
|
+
authorization.planSha256 !== plan.planSha256 ||
|
|
277
|
+
typeof authorization.sourceSha256 !== "string" ||
|
|
278
|
+
!HASH.test(authorization.sourceSha256) ||
|
|
279
|
+
authorization.sourceLocator !==
|
|
280
|
+
`projects/${projectId}/scientific/authorization/${authorization.sourceSha256}.txt` ||
|
|
281
|
+
!Number.isSafeInteger(authorization.sourceBytes) ||
|
|
282
|
+
Number(authorization.sourceBytes) < 8 ||
|
|
283
|
+
Number(authorization.sourceBytes) > 65536 ||
|
|
284
|
+
Object.keys(design).sort().join(",") !== "objectLocator,sha256" ||
|
|
285
|
+
typeof design.sha256 !== "string" ||
|
|
286
|
+
!HASH.test(design.sha256) ||
|
|
287
|
+
design.objectLocator !== `projects/${projectId}/scientific/design/objects/${design.sha256}.json`)
|
|
288
|
+
throw conflict();
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
/** Shared by the live loader and portable verifier; rule patches commute with slot fulfillment. */
|
|
292
|
+
export function projectScientificAmendments(base, baseSha256, fulfillments, records) {
|
|
293
|
+
const design = structuredClone(base);
|
|
294
|
+
let parent = null;
|
|
295
|
+
for (const record of records) {
|
|
296
|
+
const plan = record.plan;
|
|
297
|
+
if (plan.parentAmendmentSha256 !== parent ||
|
|
298
|
+
plan.parentDesignSha256 !== baseSha256 ||
|
|
299
|
+
plan.projectId !== base.projectId)
|
|
300
|
+
throw conflict();
|
|
301
|
+
const prefix = plan.parentFulfillmentSha256 === null
|
|
302
|
+
? 0
|
|
303
|
+
: fulfillments.findIndex((item) => item.recordSha256 === plan.parentFulfillmentSha256) + 1;
|
|
304
|
+
if (plan.parentFulfillmentSha256 !== null && prefix === 0)
|
|
305
|
+
throw conflict();
|
|
306
|
+
const previous = structuredClone(design);
|
|
307
|
+
for (const item of fulfillments.slice(0, prefix))
|
|
308
|
+
applyScientificFulfillmentRecord(previous, item);
|
|
309
|
+
if (sha256Text(canonicalJson(previous)) !== plan.parentEffectiveDesignSha256)
|
|
310
|
+
throw conflict();
|
|
311
|
+
const models = new Set(design.identity.modelStructures.map((item) => item.id));
|
|
312
|
+
const parameters = new Set(design.uncertaintyParameters.map((item) => item.id));
|
|
313
|
+
for (const change of plan.changes) {
|
|
314
|
+
const rule = design.policyRuleDispositions.find((item) => item.ruleId === change.ruleId);
|
|
315
|
+
if (!rule ||
|
|
316
|
+
rule.status !== "planned" ||
|
|
317
|
+
canonicalJson(rule) !== canonicalJson(change.before))
|
|
318
|
+
throw conflict();
|
|
319
|
+
const expected = { ...rule, ...planInput({ ...plan, changes: [change] }).changes[0] };
|
|
320
|
+
if (canonicalJson(expected) !== canonicalJson(change.after) ||
|
|
321
|
+
canonicalJson(rule) === canonicalJson(expected) ||
|
|
322
|
+
expected.modelStructureIds.some((id) => !models.has(id)) ||
|
|
323
|
+
expected.uncertaintyParameterIds.some((id) => !parameters.has(id)))
|
|
324
|
+
throw conflict();
|
|
325
|
+
Object.assign(rule, expected);
|
|
326
|
+
}
|
|
327
|
+
const effective = structuredClone(design);
|
|
328
|
+
for (const item of fulfillments.slice(0, prefix))
|
|
329
|
+
applyScientificFulfillmentRecord(effective, item);
|
|
330
|
+
if (sha256Text(canonicalJson(design)) !== record.design.sha256 ||
|
|
331
|
+
sha256Text(canonicalJson(effective)) !== plan.proposedEffectiveDesignSha256 ||
|
|
332
|
+
evaluateScientificDesign(effective).issueCodes.length)
|
|
333
|
+
throw conflict();
|
|
334
|
+
parent = record.recordSha256;
|
|
335
|
+
}
|
|
336
|
+
return design;
|
|
337
|
+
}
|
|
338
|
+
async function controlledPath(root, locator, create = false) {
|
|
339
|
+
const destination = resolveContained(workspacePaths(root).control, locator);
|
|
340
|
+
let directory = workspacePaths(root).control;
|
|
341
|
+
for (const part of locator.split("/").slice(0, -1)) {
|
|
342
|
+
directory = join(directory, part);
|
|
343
|
+
if (create && !(await pathExists(directory)))
|
|
344
|
+
await mkdir(directory, { mode: 0o700 });
|
|
345
|
+
const info = await lstat(directory).catch(() => null);
|
|
346
|
+
if (!info?.isDirectory() || info.isSymbolicLink())
|
|
347
|
+
throw conflict();
|
|
348
|
+
}
|
|
349
|
+
return destination;
|
|
350
|
+
}
|
|
351
|
+
async function readImmutable(root, locator, maxBytes = 16 * 1024 * 1024) {
|
|
352
|
+
const path = await controlledPath(root, locator);
|
|
353
|
+
const info = await lstat(path).catch(() => null);
|
|
354
|
+
if (!info?.isFile() || info.isSymbolicLink() || info.size > maxBytes)
|
|
355
|
+
throw conflict();
|
|
356
|
+
const bytes = await readFile(path);
|
|
357
|
+
if (bytes.length > maxBytes)
|
|
358
|
+
throw conflict();
|
|
359
|
+
try {
|
|
360
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
throw conflict();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function writeImmutable(root, locator, text) {
|
|
367
|
+
const path = await controlledPath(root, locator, true);
|
|
368
|
+
const existing = await lstat(path).catch((error) => {
|
|
369
|
+
if (error.code === "ENOENT")
|
|
370
|
+
return null;
|
|
371
|
+
throw conflict();
|
|
372
|
+
});
|
|
373
|
+
if (existing) {
|
|
374
|
+
if (!existing.isFile() || existing.isSymbolicLink())
|
|
375
|
+
throw conflict();
|
|
376
|
+
if ((await readImmutable(root, locator)) !== text)
|
|
377
|
+
throw conflict();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
await writeTextAtomic(path, text, 0o444);
|
|
381
|
+
}
|
|
382
|
+
export async function loadScientificAmendments(root, project, events) {
|
|
383
|
+
const entries = events.filter((event) => event.scope === project.id && event.type === "scientific.amendment.recorded");
|
|
384
|
+
if ((entries.at(-1)?.payload.recordSha256 ?? null) !==
|
|
385
|
+
(project.scientificDesign?.amendmentSha256 ?? null))
|
|
386
|
+
throw conflict();
|
|
387
|
+
const records = [];
|
|
388
|
+
for (const event of entries) {
|
|
389
|
+
const hash = String(event.payload.recordSha256);
|
|
390
|
+
let value;
|
|
391
|
+
try {
|
|
392
|
+
value = JSON.parse(await readImmutable(root, scientificAmendmentLocator(project.id, hash)));
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
throw conflict();
|
|
396
|
+
}
|
|
397
|
+
const record = validateScientificAmendmentRecord(value, project.id, hash);
|
|
398
|
+
if (event.payload.planSha256 !== record.plan.planSha256 ||
|
|
399
|
+
!isObject(event.payload.mutation) ||
|
|
400
|
+
event.payload.mutation.requestSha256 !== hash ||
|
|
401
|
+
event.payload.parentAmendmentSha256 !== record.plan.parentAmendmentSha256)
|
|
402
|
+
throw conflict();
|
|
403
|
+
const source = await readImmutable(root, record.amendmentAuthorization.sourceLocator, 65536);
|
|
404
|
+
if (sha256Text(source) !== record.amendmentAuthorization.sourceSha256 ||
|
|
405
|
+
Buffer.byteLength(source) !== record.amendmentAuthorization.sourceBytes)
|
|
406
|
+
throw conflict();
|
|
407
|
+
const version = await readImmutable(root, record.design.objectLocator);
|
|
408
|
+
if (sha256Text(version) !== record.design.sha256)
|
|
409
|
+
throw conflict();
|
|
410
|
+
records.push(record);
|
|
411
|
+
}
|
|
412
|
+
return records;
|
|
413
|
+
}
|
|
414
|
+
export async function applyScientificAmendment(root, projectId, value, confirmation, sourcePath) {
|
|
415
|
+
const plan = validatePlan(value);
|
|
416
|
+
if (confirmation !== plan.planSha256 || !sourcePath || !isAbsolute(sourcePath))
|
|
417
|
+
throw invalid("Apply requires --confirm with the exact reviewed plan hash and an absolute --authorization-source file.");
|
|
418
|
+
const info = await lstat(sourcePath).catch(() => null);
|
|
419
|
+
if (!info?.isFile() || info.isSymbolicLink() || info.size > 65536)
|
|
420
|
+
throw invalid("Authorization source must be a bounded regular UTF-8 file.");
|
|
421
|
+
const bytes = await readFile(sourcePath);
|
|
422
|
+
let source;
|
|
423
|
+
try {
|
|
424
|
+
source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
throw invalid("Authorization source must be UTF-8 text.");
|
|
428
|
+
}
|
|
429
|
+
if (bytes.length > 65536 ||
|
|
430
|
+
source.trim().length < 8 ||
|
|
431
|
+
sanitizeResearchValue(source, configuredResearchSecrets(process.env)) !== source)
|
|
432
|
+
throw invalid("Authorization source must contain bounded, non-secret confirmation text.");
|
|
433
|
+
const sourceSha256 = sha256Text(source);
|
|
434
|
+
return withWorkspaceLock(root, "research.scientific.amendment", async () => {
|
|
435
|
+
const project = await loadProject(root, projectId);
|
|
436
|
+
const events = await readVerifiedJournal(workspacePaths(root).journal);
|
|
437
|
+
assertProjectAuthority(project, projectAuthorityIndex(events));
|
|
438
|
+
const view = await loadScientificFulfillmentView(root, project, undefined, events);
|
|
439
|
+
const replay = view.amendments.find((record) => record.plan.planSha256 === plan.planSha256);
|
|
440
|
+
if (replay) {
|
|
441
|
+
if (canonicalJson(replay.plan) !== canonicalJson(plan) ||
|
|
442
|
+
replay.amendmentAuthorization.sourceSha256 !== sourceSha256)
|
|
443
|
+
throw conflict();
|
|
444
|
+
return replay;
|
|
445
|
+
}
|
|
446
|
+
if (plan.projectId !== projectId ||
|
|
447
|
+
canonicalJson(await planScientificAmendment(root, projectId, planInput(plan))) !==
|
|
448
|
+
canonicalJson(plan))
|
|
449
|
+
throw conflict();
|
|
450
|
+
const design = structuredClone(view.amendedBase);
|
|
451
|
+
for (const change of plan.changes)
|
|
452
|
+
Object.assign(design.policyRuleDispositions.find((rule) => rule.ruleId === change.ruleId), change.after);
|
|
453
|
+
const designText = canonicalJson(design);
|
|
454
|
+
const designSha256 = sha256Text(designText);
|
|
455
|
+
const core = {
|
|
456
|
+
schemaVersion: 1,
|
|
457
|
+
kind: "tiangong-scientific-amendment",
|
|
458
|
+
projectId,
|
|
459
|
+
plan,
|
|
460
|
+
amendmentAuthorization: {
|
|
461
|
+
kind: "operator-confirmation",
|
|
462
|
+
planSha256: plan.planSha256,
|
|
463
|
+
sourceSha256,
|
|
464
|
+
sourceLocator: `projects/${projectId}/scientific/authorization/${sourceSha256}.txt`,
|
|
465
|
+
sourceBytes: Buffer.byteLength(source),
|
|
466
|
+
},
|
|
467
|
+
design: {
|
|
468
|
+
sha256: designSha256,
|
|
469
|
+
objectLocator: `projects/${projectId}/scientific/design/objects/${designSha256}.json`,
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
const record = {
|
|
473
|
+
...core,
|
|
474
|
+
recordSha256: sha256Text(canonicalJson(core)),
|
|
475
|
+
};
|
|
476
|
+
await writeImmutable(root, record.amendmentAuthorization.sourceLocator, source);
|
|
477
|
+
await writeImmutable(root, record.design.objectLocator, designText);
|
|
478
|
+
await writeImmutable(root, scientificAmendmentLocator(projectId, record.recordSha256), canonicalJson(record));
|
|
479
|
+
let mutation = await beginProjectMutation(root, "scientific-amendment", project, record.recordSha256);
|
|
480
|
+
try {
|
|
481
|
+
project.scientificDesign.amendmentSha256 = record.recordSha256;
|
|
482
|
+
for (const role of ["research-design", "evidence-construct", "pilot-methods"])
|
|
483
|
+
project.scientificDesign.gates[role] = {
|
|
484
|
+
status: "pending",
|
|
485
|
+
packetSha256: null,
|
|
486
|
+
assessmentSha256: null,
|
|
487
|
+
reviewSha256: null,
|
|
488
|
+
reviewerSessionSha256: null,
|
|
489
|
+
};
|
|
490
|
+
project.updatedAt = new Date().toISOString();
|
|
491
|
+
mutation = await prepareProjectMutation(root, mutation, project);
|
|
492
|
+
await appendJournalEvent(workspacePaths(root).journal, "scientific.amendment.recorded", projectId, {
|
|
493
|
+
projectId,
|
|
494
|
+
recordSha256: record.recordSha256,
|
|
495
|
+
planSha256: plan.planSha256,
|
|
496
|
+
parentAmendmentSha256: plan.parentAmendmentSha256,
|
|
497
|
+
mutation: projectMutationBinding(mutation),
|
|
498
|
+
});
|
|
499
|
+
await settleProjectMutation(root, mutation);
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
await settleProjectMutation(root, mutation);
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
return record;
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
export async function inspectScientificAmendment(root, projectId) {
|
|
509
|
+
const project = await loadProject(root, projectId);
|
|
510
|
+
const view = await loadScientificFulfillmentView(root, project);
|
|
511
|
+
return {
|
|
512
|
+
projectId,
|
|
513
|
+
amendmentSha256: project.scientificDesign?.amendmentSha256 ?? null,
|
|
514
|
+
originalDesignSha256: project.scientificDesign?.designSha256,
|
|
515
|
+
effectiveDesignSha256: view.effectiveSha256,
|
|
516
|
+
plannedRules: view.contract.policyRuleDispositions
|
|
517
|
+
.filter((rule) => rule.status === "planned")
|
|
518
|
+
.map(({ ruleId, dueGate, rationale, modelStructureIds, uncertaintyParameterIds }) => ({
|
|
519
|
+
ruleId,
|
|
520
|
+
dueGate,
|
|
521
|
+
rationale,
|
|
522
|
+
modelStructureIds,
|
|
523
|
+
uncertaintyParameterIds,
|
|
524
|
+
})),
|
|
525
|
+
records: view.amendments.map((record) => ({
|
|
526
|
+
recordSha256: record.recordSha256,
|
|
527
|
+
planSha256: record.plan.planSha256,
|
|
528
|
+
parentAmendmentSha256: record.plan.parentAmendmentSha256,
|
|
529
|
+
design: record.design,
|
|
530
|
+
reason: record.plan.reason,
|
|
531
|
+
affectedTaskRequirementIds: record.plan.affectedTaskRequirementIds,
|
|
532
|
+
})),
|
|
533
|
+
gates: project.scientificDesign?.gates,
|
|
534
|
+
nextAction: "Complete the pending scientific gates for this design version. Reuse still-bound acquisition artifacts and typed evidence; authorization is not scientific approval.",
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
//# sourceMappingURL=scientific-amendment.js.map
|