@skill-harness/core 0.12.0 → 0.13.0
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/dist/adoption.d.ts +128 -0
- package/dist/adoption.js +102 -0
- package/dist/command-cost.d.ts +1 -1
- package/dist/command-cost.js +1 -1
- package/dist/factory-calibration.d.ts +63 -0
- package/dist/factory-calibration.js +115 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/intervention-results.d.ts +6 -0
- package/dist/intervention-results.js +52 -0
- package/dist/intervention.d.ts +123 -0
- package/dist/intervention.js +218 -0
- package/dist/investigation.d.ts +94 -0
- package/dist/investigation.js +148 -0
- package/dist/qualification-process.js +5 -3
- package/dist/trajectory-gates.d.ts +0 -12
- package/dist/trajectory-gates.js +0 -36
- package/dist/trusted-host-supervision.d.ts +25 -0
- package/dist/trusted-host-supervision.js +94 -0
- package/dist/work-capture-schema.d.ts +38 -0
- package/dist/work-capture-schema.js +33 -0
- package/dist/work-capture.d.ts +43 -0
- package/dist/work-capture.js +81 -0
- package/dist/work-signals.d.ts +55 -0
- package/dist/work-signals.js +71 -0
- package/package.json +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
const SHA = /^[a-f0-9]{64}$/;
|
|
3
|
+
const text = (x, max = 512) => typeof x === "string" && x.length > 0 && x.length <= max && !/[\u0000-\u001f\u007f]/.test(x);
|
|
4
|
+
const closed = (x, names) => !!x && typeof x === "object" && !Array.isArray(x) && Object.keys(x).sort().join() === [...names].sort().join();
|
|
5
|
+
function canonical(x, depth = 0) {
|
|
6
|
+
if (depth > 16)
|
|
7
|
+
throw new Error("intervention data exceeds depth");
|
|
8
|
+
if (x === null || typeof x === "string" || typeof x === "boolean" || (typeof x === "number" && Number.isFinite(x)))
|
|
9
|
+
return JSON.stringify(x);
|
|
10
|
+
if (!x || typeof x !== "object" || (!Array.isArray(x) && ![Object.prototype, null].includes(Object.getPrototypeOf(x))))
|
|
11
|
+
throw new Error("plain intervention JSON required");
|
|
12
|
+
const d = Object.getOwnPropertyDescriptors(x);
|
|
13
|
+
if (Array.isArray(x)) {
|
|
14
|
+
if (x.length > 4096 || Reflect.ownKeys(x).length !== x.length + 1)
|
|
15
|
+
throw new Error("bounded dense array required");
|
|
16
|
+
return `[${Array.from({ length: x.length }, (_, i) => { if (!d[i] || !Object.hasOwn(d[i], "value"))
|
|
17
|
+
throw new Error("plain array required"); return canonical(d[i].value, depth + 1); }).join(",")}]`;
|
|
18
|
+
}
|
|
19
|
+
return `{${Reflect.ownKeys(d).sort((a, b) => String(a) < String(b) ? -1 : 1).map(k => { if (typeof k !== "string" || !d[k].enumerable || !Object.hasOwn(d[k], "value"))
|
|
20
|
+
throw new Error("plain object required"); return `${JSON.stringify(k)}:${canonical(d[k].value, depth + 1)}`; }).join(",")}}`;
|
|
21
|
+
}
|
|
22
|
+
/** Plain, bounded JSON encoding for retained intervention inputs; not semantic validation. */
|
|
23
|
+
export function interventionCanonicalJson(value) {
|
|
24
|
+
const bytes = canonical(value);
|
|
25
|
+
if (Buffer.byteLength(bytes) > 2 * 1024 * 1024)
|
|
26
|
+
throw new Error("intervention data exceeds byte bound");
|
|
27
|
+
return bytes;
|
|
28
|
+
}
|
|
29
|
+
const digest = (value) => createHash("sha256").update(interventionCanonicalJson(value)).digest("hex");
|
|
30
|
+
function frozen(value) { if (value && typeof value === "object") {
|
|
31
|
+
Object.values(value).forEach(frozen);
|
|
32
|
+
Object.freeze(value);
|
|
33
|
+
} return value; }
|
|
34
|
+
const clone = (value) => JSON.parse(canonical(value));
|
|
35
|
+
const axes = ["model", "effort", "skill", "prompt", "configuration"];
|
|
36
|
+
export function freezeIntervention(input) {
|
|
37
|
+
const d = clone(input);
|
|
38
|
+
if (!closed(d, ["family", "investigationSha256", "resourceMetric", "axes", "common", "proposer", "judge", "cases", "arms"]) || d.family !== "intervention" || !SHA.test(d.investigationSha256)
|
|
39
|
+
|| !["usd", "wall_ms", "tool_calls"].includes(d.resourceMetric)
|
|
40
|
+
|| !Array.isArray(d.axes) || !d.axes.length || new Set(d.axes).size !== d.axes.length || d.axes.some(a => !axes.includes(a))
|
|
41
|
+
|| !closed(d.common, ["mode", "scenarioSha256", "rubricSha256", "fixtureSha256", "heldoutSha256", "harnessSha256", "judgePolicySha256"])
|
|
42
|
+
|| !["green", "force"].includes(d.common.mode) || Object.entries(d.common).some(([k, v]) => k !== "mode" && !SHA.test(v))
|
|
43
|
+
|| !text(d.proposer) || !text(d.judge) || !Array.isArray(d.arms) || d.arms.length < 2 || d.arms.length > 8
|
|
44
|
+
|| !Array.isArray(d.cases) || !d.cases.length || d.cases.length > 128)
|
|
45
|
+
throw new Error("invalid intervention manifest");
|
|
46
|
+
for (const arm of d.arms)
|
|
47
|
+
if (!closed(arm, ["id", "configuration"]) || !text(arm.id, 128) || !closed(arm.configuration, axes)
|
|
48
|
+
|| !text(arm.configuration.model) || !text(arm.configuration.effort) || ["skill", "prompt", "configuration"].some(k => !SHA.test(arm.configuration[k])))
|
|
49
|
+
throw new Error("invalid intervention arm");
|
|
50
|
+
if (new Set(d.arms.map(a => a.id)).size !== d.arms.length || new Set(d.cases.map(c => c.id)).size !== d.cases.length)
|
|
51
|
+
throw new Error("duplicate intervention identity");
|
|
52
|
+
for (const c of d.cases)
|
|
53
|
+
if (!closed(c, ["id", "criteria", "reps", "threshold", "critical"]) || !text(c.id, 128)
|
|
54
|
+
|| !Number.isSafeInteger(c.criteria) || c.criteria < 1 || c.criteria > 128 || !Number.isSafeInteger(c.reps) || c.reps < 1 || c.reps > 20
|
|
55
|
+
|| !Number.isFinite(c.threshold) || c.threshold <= 0 || c.threshold > 1 || typeof c.critical !== "boolean"
|
|
56
|
+
|| ((c.critical || c.id.startsWith("B")) && c.threshold !== 1))
|
|
57
|
+
throw new Error("invalid frozen case policy");
|
|
58
|
+
const changedAxes = axes.filter(axis => d.arms.some(a => a.configuration[axis] !== d.arms[0].configuration[axis]));
|
|
59
|
+
if (changedAxes.some(axis => !d.axes.includes(axis)))
|
|
60
|
+
throw new Error("undeclared intervention axis");
|
|
61
|
+
const body = { ...d, version: "intervention-comparison-v1", inputDigest: digest({ common: d.common, cases: d.cases }), changedAxes, deterministicSampling: false };
|
|
62
|
+
return frozen({ ...body, id: digest(body) });
|
|
63
|
+
}
|
|
64
|
+
export const interventionEvidenceDigest = (value) => digest(value);
|
|
65
|
+
const assessments = new WeakSet();
|
|
66
|
+
const assessmentArtifacts = new WeakMap();
|
|
67
|
+
function manifestValid(m) {
|
|
68
|
+
const detached = clone(m);
|
|
69
|
+
const { version, id: _id, inputDigest: _inputs, changedAxes: _axes, deterministicSampling, ...draft } = detached;
|
|
70
|
+
if (version !== "intervention-comparison-v1" || deterministicSampling !== false || canonical(freezeIntervention(draft)) !== canonical(detached))
|
|
71
|
+
throw new Error("frozen intervention changed");
|
|
72
|
+
}
|
|
73
|
+
/** Pre-spend role gate: consumes independently resolved host facts, never guesses aliases or calls a provider. */
|
|
74
|
+
export function assertInterventionRoles(manifest, qualification) {
|
|
75
|
+
manifestValid(manifest);
|
|
76
|
+
if (!qualification || !qualification.proposer || !qualification.judge || !qualification.subjects || qualification.manifestId !== manifest.id || qualification.proposer.requested !== manifest.proposer || qualification.judge.requested !== manifest.judge
|
|
77
|
+
|| !text(qualification.proposer.canonical) || !text(qualification.judge.canonical))
|
|
78
|
+
throw new Error("independent role qualification required");
|
|
79
|
+
const reserved = new Set([qualification.proposer.canonical, qualification.judge.canonical]);
|
|
80
|
+
if (reserved.size !== 2 || manifest.arms.some(a => !Object.hasOwn(qualification.subjects, a.id) || qualification.subjects[a.id].requested !== a.configuration.model
|
|
81
|
+
|| !text(qualification.subjects[a.id].canonical) || reserved.has(qualification.subjects[a.id].canonical)))
|
|
82
|
+
throw new Error("role identity conflict or unresolved role");
|
|
83
|
+
}
|
|
84
|
+
/** Inspects separately host-qualified evidence; does not obtain qualification or execute a model. */
|
|
85
|
+
export function assessIntervention(manifest, evidence, qualification) {
|
|
86
|
+
assertInterventionRoles(manifest, qualification);
|
|
87
|
+
if (evidence.length > manifest.arms.length || new Set(evidence.map(e => e.armId)).size !== evidence.length || evidence.some(e => !manifest.arms.some(a => a.id === e.armId)))
|
|
88
|
+
throw new Error("unexpected intervention evidence identity");
|
|
89
|
+
const retained = new Map();
|
|
90
|
+
let retainedBytes = 0;
|
|
91
|
+
const arms = manifest.arms.map(arm => {
|
|
92
|
+
const e = evidence.find(e => e.armId === arm.id);
|
|
93
|
+
let admitted = false;
|
|
94
|
+
const result = (state) => ({ armId: arm.id, state, eligible: state === "MEASURED", artifactDigests: admitted ? [...e.artifactDigests] : [], cost: admitted ? e.cost : null, costUnit: admitted ? e.costUnit : "unknown" });
|
|
95
|
+
if (!e)
|
|
96
|
+
return result("MISSING");
|
|
97
|
+
if (qualification.evidenceDigests[arm.id] !== interventionEvidenceDigest(e) || !closed(e, ["armId", "inputDigest", "artifactDigests", "cells", "cost", "costUnit"])
|
|
98
|
+
|| e.inputDigest !== manifest.inputDigest || !Array.isArray(e.artifactDigests) || e.artifactDigests.length > 4096 || e.artifactDigests.some(h => !SHA.test(h))
|
|
99
|
+
|| !(e.cost === null || (Number.isFinite(e.cost) && e.cost >= 0)) || e.costUnit !== manifest.resourceMetric || !Array.isArray(e.cells)
|
|
100
|
+
|| new Set(e.artifactDigests).size !== e.artifactDigests.length || e.cells.length !== manifest.cases.reduce((n, c) => n + c.reps, 0))
|
|
101
|
+
return result("ERROR");
|
|
102
|
+
admitted = true;
|
|
103
|
+
const cells = new Map();
|
|
104
|
+
let error = false, unmeasured = false, unresolved = false, missingOutput = false;
|
|
105
|
+
for (const c of e.cells) {
|
|
106
|
+
const spec = manifest.cases.find(s => s.id === c.caseId), id = `${c.caseId}:${c.repetition}`;
|
|
107
|
+
if (!closed(c, ["caseId", "repetition", "delivery", "objective", "criteria", "suspect", "artifactSha256"]) || !spec || !Number.isSafeInteger(c.repetition) || c.repetition < 0 || c.repetition >= spec.reps || cells.has(id)
|
|
108
|
+
|| !["PASS", "NOT-MEASURED", "ERROR"].includes(c.delivery) || !["PASS", "FAIL", "ERROR", "NOT-MEASURED"].includes(c.objective) || !Array.isArray(c.criteria) || typeof c.suspect !== "boolean") {
|
|
109
|
+
error = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
cells.set(id, c);
|
|
113
|
+
if (c.artifactSha256 !== null && (typeof c.artifactSha256 !== "string" || !SHA.test(c.artifactSha256) || !e.artifactDigests.includes(c.artifactSha256)))
|
|
114
|
+
error = true;
|
|
115
|
+
if (c.delivery === "PASS" && c.objective === "PASS" && c.artifactSha256 === null)
|
|
116
|
+
missingOutput = true;
|
|
117
|
+
if (c.delivery !== "PASS" || c.objective !== "PASS") {
|
|
118
|
+
if (c.criteria.length)
|
|
119
|
+
error = true;
|
|
120
|
+
if (c.delivery === "NOT-MEASURED")
|
|
121
|
+
unmeasured = true;
|
|
122
|
+
if (c.delivery === "ERROR" || c.objective === "ERROR" || (c.objective === "NOT-MEASURED" && c.delivery !== "NOT-MEASURED"))
|
|
123
|
+
error = true;
|
|
124
|
+
}
|
|
125
|
+
else if (c.criteria.length !== spec.criteria || c.criteria.some(v => !["PASS", "FAIL", "UNKNOWN"].includes(v)))
|
|
126
|
+
error = true;
|
|
127
|
+
else if (c.suspect || c.criteria.includes("UNKNOWN"))
|
|
128
|
+
unresolved = true;
|
|
129
|
+
}
|
|
130
|
+
if (error)
|
|
131
|
+
return result("ERROR");
|
|
132
|
+
if (unmeasured)
|
|
133
|
+
return result("NOT-MEASURED");
|
|
134
|
+
if (unresolved)
|
|
135
|
+
return result("UNRESOLVED");
|
|
136
|
+
if (missingOutput)
|
|
137
|
+
return result("MISSING");
|
|
138
|
+
const passed = manifest.cases.every(spec => Array.from(cells.values()).filter(c => c.caseId === spec.id && c.objective === "PASS" && c.criteria.every(v => v === "PASS")).length / spec.reps >= spec.threshold);
|
|
139
|
+
if (!passed)
|
|
140
|
+
return result("FAILED");
|
|
141
|
+
if (!e.artifactDigests.length)
|
|
142
|
+
return result("MISSING");
|
|
143
|
+
for (const hash of e.artifactDigests) {
|
|
144
|
+
const bytes = qualification.artifacts?.get(hash);
|
|
145
|
+
if (bytes === undefined)
|
|
146
|
+
return result("MISSING");
|
|
147
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength > 8 * 1024 * 1024)
|
|
148
|
+
return result("ERROR");
|
|
149
|
+
const stable = Buffer.from(bytes);
|
|
150
|
+
if (createHash("sha256").update(stable).digest("hex") !== hash)
|
|
151
|
+
return result("ERROR");
|
|
152
|
+
if (!retained.has(hash)) {
|
|
153
|
+
if (retainedBytes + bytes.byteLength > 64 * 1024 * 1024)
|
|
154
|
+
return result("ERROR");
|
|
155
|
+
retained.set(hash, stable);
|
|
156
|
+
retainedBytes += stable.byteLength;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return result("MEASURED");
|
|
160
|
+
});
|
|
161
|
+
const eligible = arms.filter(a => a.eligible), knownCosts = eligible.length && eligible.every(a => a.cost !== null && a.costUnit === eligible[0].costUnit);
|
|
162
|
+
const cheapestEligible = knownCosts ? [...eligible].sort((a, b) => a.cost - b.cost || (a.armId < b.armId ? -1 : 1))[0].armId : null;
|
|
163
|
+
const result = frozen({ manifestId: manifest.id, complete: arms.every(a => a.state === "MEASURED" || a.state === "FAILED"), arms, cheapestEligible, routingDefault: null, adoptionAuthorized: false });
|
|
164
|
+
assessments.add(result);
|
|
165
|
+
assessmentArtifacts.set(result, retained);
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
/** Only this view omits identity/cost; arbitrary artifact content can still leak clues and must be reviewed. */
|
|
169
|
+
export function createBlindComparison(manifest, assessment, fixtureOrPersistedSeed) {
|
|
170
|
+
manifestValid(manifest);
|
|
171
|
+
if (!assessments.has(assessment) || assessment.manifestId !== manifest.id || !assessment.complete)
|
|
172
|
+
throw new Error("complete recomputed intervention assessment required");
|
|
173
|
+
const seed = fixtureOrPersistedSeed ?? randomBytes(32).toString("hex");
|
|
174
|
+
if (!SHA.test(seed))
|
|
175
|
+
throw new Error("invalid private blinding seed");
|
|
176
|
+
const assessmentId = digest(assessment);
|
|
177
|
+
const eligible = assessment.arms.filter(a => a.eligible).sort((a, b) => {
|
|
178
|
+
const left = digest([seed, manifest.id, assessmentId, a.armId]), right = digest([seed, manifest.id, assessmentId, b.armId]);
|
|
179
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
180
|
+
});
|
|
181
|
+
const mapping = new Map(eligible.map((a, i) => [`variant-${digest([seed, manifest.id, assessmentId, i]).slice(0, 16)}`, a.armId]));
|
|
182
|
+
if (mapping.size !== eligible.length)
|
|
183
|
+
throw new Error("opaque label collision");
|
|
184
|
+
const cards = [...mapping].map(([label, armId]) => ({ label, artifactDigests: assessment.arms.find(a => a.armId === armId).artifactDigests }));
|
|
185
|
+
let choice = null, revealed = false;
|
|
186
|
+
return Object.freeze({
|
|
187
|
+
view: () => frozen({ version: "blind-intervention-view-v1", cards, limitations: ["artifact-content-may-disclose-identity"] }),
|
|
188
|
+
readArtifact(label, hash) {
|
|
189
|
+
const armId = mapping.get(label), arm = assessment.arms.find(a => a.armId === armId);
|
|
190
|
+
if (!arm || !arm.artifactDigests.includes(hash))
|
|
191
|
+
throw new Error("artifact outside blind view");
|
|
192
|
+
const bytes = assessmentArtifacts.get(assessment)?.get(hash);
|
|
193
|
+
if (!bytes)
|
|
194
|
+
throw new Error("retained blind artifact missing");
|
|
195
|
+
return Buffer.from(bytes);
|
|
196
|
+
},
|
|
197
|
+
choose(input) {
|
|
198
|
+
const value = clone(input);
|
|
199
|
+
if (!closed(value, ["kind", "labels"]) || !["one", "tie", "none", "insufficient"].includes(value.kind) || !Array.isArray(value.labels)
|
|
200
|
+
|| new Set(value.labels).size !== value.labels.length || value.labels.some(l => !mapping.has(l))
|
|
201
|
+
|| (value.kind === "one" ? value.labels.length !== 1 : value.kind === "tie" ? value.labels.length < 2 : value.labels.length !== 0))
|
|
202
|
+
throw new Error("invalid blind quality choice");
|
|
203
|
+
value.labels.sort();
|
|
204
|
+
if (revealed && canonical(choice) !== canonical(value))
|
|
205
|
+
throw new Error("quality choice locked after reveal");
|
|
206
|
+
choice = frozen(value);
|
|
207
|
+
return choice;
|
|
208
|
+
},
|
|
209
|
+
reveal() {
|
|
210
|
+
if (!choice)
|
|
211
|
+
throw new Error("quality choice required before reveal");
|
|
212
|
+
revealed = true;
|
|
213
|
+
return frozen({ manifestId: manifest.id, choice, arms: [...mapping].map(([label, armId]) => ({ label, armId, configuration: manifest.arms.find(a => a.id === armId).configuration,
|
|
214
|
+
cost: assessment.arms.find(a => a.armId === armId).cost, costUnit: assessment.arms.find(a => a.armId === armId).costUnit })), routingDefault: null });
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=intervention.js.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Bounded operator envelope; duplicate keys and over-expanded/cyclic objects are refused before dispatch. */
|
|
2
|
+
export declare function parseLearningRequest(text: string): unknown;
|
|
3
|
+
export interface HypothesisProposal {
|
|
4
|
+
archiveSnapshot: string;
|
|
5
|
+
caseIds: string[];
|
|
6
|
+
population: string;
|
|
7
|
+
intervention: string;
|
|
8
|
+
alternatives: string[];
|
|
9
|
+
prediction: string;
|
|
10
|
+
downside: string;
|
|
11
|
+
disproof: string;
|
|
12
|
+
rollback: string;
|
|
13
|
+
limits: {
|
|
14
|
+
subjectCalls: number;
|
|
15
|
+
judgeCalls: number;
|
|
16
|
+
wallMs: number;
|
|
17
|
+
};
|
|
18
|
+
effectProfile: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface Hypothesis {
|
|
21
|
+
version: "factory-hypothesis-v1";
|
|
22
|
+
id: string;
|
|
23
|
+
proposal: HypothesisProposal;
|
|
24
|
+
status: "proposed";
|
|
25
|
+
}
|
|
26
|
+
export declare function buildHypothesis(input: HypothesisProposal): Hypothesis;
|
|
27
|
+
export interface InvestigationAuthority {
|
|
28
|
+
id: string;
|
|
29
|
+
investigations: readonly string[];
|
|
30
|
+
promotions: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
/** Independent host declarations, not approval fields on a proposal and never adoption/activation authority. */
|
|
33
|
+
export declare function authorizeInvestigation(h: Hypothesis, authority: InvestigationAuthority | null): {
|
|
34
|
+
version: string;
|
|
35
|
+
hypothesisId: string;
|
|
36
|
+
authorityId: string;
|
|
37
|
+
scope: "investigation-only";
|
|
38
|
+
};
|
|
39
|
+
export interface InvestigationScenarioPreview {
|
|
40
|
+
version: "investigation-scenario-preview-v1";
|
|
41
|
+
hypothesisId: string;
|
|
42
|
+
specPath: string;
|
|
43
|
+
baseSha256: string;
|
|
44
|
+
afterSha256: string;
|
|
45
|
+
scenario: Record<string, unknown>;
|
|
46
|
+
block: string;
|
|
47
|
+
digest: string;
|
|
48
|
+
}
|
|
49
|
+
export declare function previewInvestigationScenario(h: Hypothesis, specPath: string, scenario: Record<string, unknown>): InvestigationScenarioPreview;
|
|
50
|
+
/** Exact preview consent includes destination, existing spec and complete scenario bytes. No automatic sanitization claim. */
|
|
51
|
+
export declare function applyInvestigationScenario(h: Hypothesis, preview: InvestigationScenarioPreview, authority: InvestigationAuthority | null): {
|
|
52
|
+
scenarioId: string;
|
|
53
|
+
replayed: boolean;
|
|
54
|
+
sha256: string;
|
|
55
|
+
scope: "scenario-only";
|
|
56
|
+
};
|
|
57
|
+
export interface FrozenInvestigationInputs {
|
|
58
|
+
specSha256: string;
|
|
59
|
+
rubricSha256: string;
|
|
60
|
+
judgePolicySha256: string;
|
|
61
|
+
heldoutSha256: string;
|
|
62
|
+
configurationSha256: string;
|
|
63
|
+
}
|
|
64
|
+
export declare function freezeInvestigation(h: Hypothesis, inputs: FrozenInvestigationInputs, authority: InvestigationAuthority | null): {
|
|
65
|
+
digest: string;
|
|
66
|
+
version: string;
|
|
67
|
+
hypothesisId: string;
|
|
68
|
+
authorityId: string;
|
|
69
|
+
inputs: FrozenInvestigationInputs;
|
|
70
|
+
executionReady: false;
|
|
71
|
+
};
|
|
72
|
+
export declare function assertFrozenInvestigation(frozen: ReturnType<typeof freezeInvestigation>, current: FrozenInvestigationInputs): void;
|
|
73
|
+
export interface WeeklyInvestigationInput {
|
|
74
|
+
week: string;
|
|
75
|
+
population: string;
|
|
76
|
+
policyDigest: string;
|
|
77
|
+
archiveSnapshot: string;
|
|
78
|
+
eligibleCaseIds: string[];
|
|
79
|
+
maxCases: number;
|
|
80
|
+
}
|
|
81
|
+
export interface WeeklyInvestigationSelection {
|
|
82
|
+
version: "weekly-investigation-selection-v1";
|
|
83
|
+
id: string;
|
|
84
|
+
key: string;
|
|
85
|
+
inputDigest: string;
|
|
86
|
+
week: string;
|
|
87
|
+
population: string;
|
|
88
|
+
policyDigest: string;
|
|
89
|
+
archiveSnapshot: string;
|
|
90
|
+
selectedCaseIds: string[];
|
|
91
|
+
sampling: "exploratory-sha256-order-v1";
|
|
92
|
+
}
|
|
93
|
+
/** The host persists these immutable selections; this function neither schedules a process nor spends tokens. */
|
|
94
|
+
export declare function selectWeeklyInvestigation(history: readonly WeeklyInvestigationSelection[], input: WeeklyInvestigationInput): WeeklyInvestigationSelection;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import yaml from "js-yaml";
|
|
3
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
4
|
+
import { appendScenario, renderScenarioBlock, specSha256 } from "./spec-write.js";
|
|
5
|
+
import { parseSpec } from "./spec.js";
|
|
6
|
+
const SHA = /^[a-f0-9]{64}$/;
|
|
7
|
+
const keys = (x, expected) => Object.keys(x).sort().join() === [...expected].sort().join();
|
|
8
|
+
const text = (x, max = 4000) => typeof x === "string" && x.length > 0 && x.length <= max;
|
|
9
|
+
function json(input) {
|
|
10
|
+
let nodes = 0;
|
|
11
|
+
const visit = (x, depth) => {
|
|
12
|
+
if (++nodes > 4096 || depth > 16)
|
|
13
|
+
throw new Error("investigation JSON exceeds bounds");
|
|
14
|
+
if (x === null || typeof x === "boolean" || typeof x === "string" || (typeof x === "number" && Number.isFinite(x)))
|
|
15
|
+
return x;
|
|
16
|
+
if (!x || typeof x !== "object" || (!Array.isArray(x) && ![Object.prototype, null].includes(Object.getPrototypeOf(x))))
|
|
17
|
+
throw new Error("plain investigation JSON required");
|
|
18
|
+
const descriptors = Object.getOwnPropertyDescriptors(x);
|
|
19
|
+
if (Array.isArray(x)) {
|
|
20
|
+
if (Object.getPrototypeOf(x) !== Array.prototype || x.length > 256 || Reflect.ownKeys(x).length !== x.length + 1)
|
|
21
|
+
throw new Error("bounded dense JSON array required");
|
|
22
|
+
return Array.from({ length: x.length }, (_, i) => { const d = descriptors[String(i)]; if (!d || !('value' in d))
|
|
23
|
+
throw new Error("plain JSON array required"); return visit(d.value, depth + 1); });
|
|
24
|
+
}
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const key of Reflect.ownKeys(descriptors).sort((a, b) => String(a) < String(b) ? -1 : 1)) {
|
|
27
|
+
const d = descriptors[key];
|
|
28
|
+
if (typeof key !== "string" || !d.enumerable || !('value' in d))
|
|
29
|
+
throw new Error("plain JSON properties required");
|
|
30
|
+
Object.defineProperty(out, key, { value: visit(d.value, depth + 1), enumerable: true });
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
};
|
|
34
|
+
const result = visit(input, 0);
|
|
35
|
+
if (Buffer.byteLength(JSON.stringify(result)) > 65536)
|
|
36
|
+
throw new Error("investigation JSON exceeds byte bound");
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
/** Bounded operator envelope; duplicate keys and over-expanded/cyclic objects are refused before dispatch. */
|
|
40
|
+
export function parseLearningRequest(text) {
|
|
41
|
+
if (typeof text !== "string" || Buffer.byteLength(text) > 65536)
|
|
42
|
+
throw new Error("learning request exceeds bound");
|
|
43
|
+
let value;
|
|
44
|
+
try {
|
|
45
|
+
value = yaml.load(text, { schema: yaml.JSON_SCHEMA, json: false });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error("invalid learning request");
|
|
49
|
+
}
|
|
50
|
+
return json(value);
|
|
51
|
+
}
|
|
52
|
+
const digest = (x) => createHash("sha256").update(JSON.stringify(json(x))).digest("hex");
|
|
53
|
+
function freeze(value) { if (value && typeof value === "object") {
|
|
54
|
+
Object.values(value).forEach(freeze);
|
|
55
|
+
Object.freeze(value);
|
|
56
|
+
} return value; }
|
|
57
|
+
export function buildHypothesis(input) {
|
|
58
|
+
const p = json(input);
|
|
59
|
+
if (!keys(p, ["archiveSnapshot", "caseIds", "population", "intervention", "alternatives", "prediction", "downside", "disproof", "rollback", "limits", "effectProfile"])
|
|
60
|
+
|| !SHA.test(p.archiveSnapshot) || !Array.isArray(p.caseIds) || !p.caseIds.length || p.caseIds.some(id => typeof id !== "string" || !SHA.test(id))
|
|
61
|
+
|| ![p.population, p.intervention, p.prediction, p.downside, p.disproof, p.rollback].every(s => text(s))
|
|
62
|
+
|| !Array.isArray(p.alternatives) || !p.alternatives.length || p.alternatives.some(a => !text(a))
|
|
63
|
+
|| !p.limits || !keys(p.limits, ["subjectCalls", "judgeCalls", "wallMs"])
|
|
64
|
+
|| !Object.values(p.limits).every(n => Number.isSafeInteger(n) && n >= 0 && n <= 1000000000)
|
|
65
|
+
|| !(p.effectProfile === null || text(p.effectProfile, 512)))
|
|
66
|
+
throw new Error("invalid bounded hypothesis");
|
|
67
|
+
return freeze({ version: "factory-hypothesis-v1", id: digest({ version: "factory-hypothesis-v1", proposal: p }), proposal: p, status: "proposed" });
|
|
68
|
+
}
|
|
69
|
+
function authorize(h, authority) {
|
|
70
|
+
const value = json(h);
|
|
71
|
+
if (!keys(value, ["version", "id", "proposal", "status"]) || value.version !== "factory-hypothesis-v1" || value.status !== "proposed" || buildHypothesis(value.proposal).id !== value.id)
|
|
72
|
+
throw new Error("hypothesis identity changed");
|
|
73
|
+
if (!authority || !text(authority.id, 512) || !Array.isArray(authority.investigations) || !Array.isArray(authority.promotions)
|
|
74
|
+
|| authority.investigations.length > 256 || authority.promotions.length > 256
|
|
75
|
+
|| [...authority.investigations, ...authority.promotions].some(id => typeof id !== "string" || !SHA.test(id))
|
|
76
|
+
|| !authority.investigations.includes(value.id))
|
|
77
|
+
throw new Error("independent investigation authority required");
|
|
78
|
+
return authority;
|
|
79
|
+
}
|
|
80
|
+
/** Independent host declarations, not approval fields on a proposal and never adoption/activation authority. */
|
|
81
|
+
export function authorizeInvestigation(h, authority) {
|
|
82
|
+
const selected = authorize(h, authority);
|
|
83
|
+
return freeze({ version: "investigation-approval-v1", hypothesisId: h.id, authorityId: selected.id, scope: "investigation-only" });
|
|
84
|
+
}
|
|
85
|
+
export function previewInvestigationScenario(h, specPath, scenario) {
|
|
86
|
+
if (buildHypothesis(h.proposal).id !== h.id)
|
|
87
|
+
throw new Error("hypothesis identity changed");
|
|
88
|
+
const path = realpathSync(specPath), current = readFileSync(path, "utf8"), safe = json(scenario), block = renderScenarioBlock(safe);
|
|
89
|
+
parseSpec(current + block, path);
|
|
90
|
+
const value = { version: "investigation-scenario-preview-v1", hypothesisId: h.id, specPath: path, baseSha256: specSha256(current), afterSha256: specSha256(current + block), scenario: safe, block };
|
|
91
|
+
return freeze({ ...value, digest: digest(value) });
|
|
92
|
+
}
|
|
93
|
+
/** Exact preview consent includes destination, existing spec and complete scenario bytes. No automatic sanitization claim. */
|
|
94
|
+
export function applyInvestigationScenario(h, preview, authority) {
|
|
95
|
+
const selected = authorize(h, authority), copy = json(preview), { digest: claimed, ...body } = copy;
|
|
96
|
+
if (!keys(copy, ["version", "hypothesisId", "specPath", "baseSha256", "afterSha256", "scenario", "block", "digest"])
|
|
97
|
+
|| body.version !== "investigation-scenario-preview-v1" || body.hypothesisId !== h.id || digest(body) !== claimed
|
|
98
|
+
|| renderScenarioBlock(body.scenario) !== body.block || !selected.promotions.includes(claimed))
|
|
99
|
+
throw new Error("exact privacy/promotion preview authority required");
|
|
100
|
+
if (realpathSync(body.specPath) !== body.specPath)
|
|
101
|
+
throw new Error("promotion destination changed");
|
|
102
|
+
const current = readFileSync(body.specPath, "utf8");
|
|
103
|
+
if (specSha256(current) === body.afterSha256)
|
|
104
|
+
return { scenarioId: String(body.scenario.id), replayed: true, sha256: body.afterSha256, scope: "scenario-only" };
|
|
105
|
+
const result = appendScenario({ specPath: body.specPath, scenario: body.scenario, baseSha256: body.baseSha256 });
|
|
106
|
+
return { scenarioId: result.id, replayed: false, sha256: result.sha256, scope: "scenario-only" };
|
|
107
|
+
}
|
|
108
|
+
function frozenInputs(input) {
|
|
109
|
+
const value = json(input);
|
|
110
|
+
if (!keys(value, ["specSha256", "rubricSha256", "judgePolicySha256", "heldoutSha256", "configurationSha256"]) || Object.values(value).some(v => typeof v !== "string" || !SHA.test(v)))
|
|
111
|
+
throw new Error("invalid frozen evaluation inputs");
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
export function freezeInvestigation(h, inputs, authority) {
|
|
115
|
+
const approved = authorizeInvestigation(h, authority), value = { version: "frozen-investigation-v1", hypothesisId: h.id, authorityId: approved.authorityId, inputs: frozenInputs(inputs), executionReady: false };
|
|
116
|
+
return freeze({ ...value, digest: digest(value) });
|
|
117
|
+
}
|
|
118
|
+
export function assertFrozenInvestigation(frozen, current) {
|
|
119
|
+
const { digest: recorded, ...value } = json(frozen);
|
|
120
|
+
if (!keys(value, ["version", "hypothesisId", "authorityId", "inputs", "executionReady"]) || !SHA.test(value.hypothesisId) || !text(value.authorityId, 512)
|
|
121
|
+
|| value.version !== "frozen-investigation-v1" || value.executionReady !== false || digest(value) !== recorded || digest(value.inputs) !== digest(frozenInputs(current)))
|
|
122
|
+
throw new Error("frozen investigation changed or invalid");
|
|
123
|
+
}
|
|
124
|
+
/** The host persists these immutable selections; this function neither schedules a process nor spends tokens. */
|
|
125
|
+
export function selectWeeklyInvestigation(history, input) {
|
|
126
|
+
const value = json(input);
|
|
127
|
+
if (!keys(value, ["week", "population", "policyDigest", "archiveSnapshot", "eligibleCaseIds", "maxCases"])
|
|
128
|
+
|| !/^\d{4}-W(?:0[1-9]|[1-4]\d|5[0-3])$/.test(value.week) || !text(value.population, 512)
|
|
129
|
+
|| !SHA.test(value.policyDigest) || !SHA.test(value.archiveSnapshot) || !Array.isArray(value.eligibleCaseIds)
|
|
130
|
+
|| !value.eligibleCaseIds.length || value.eligibleCaseIds.some(id => typeof id !== "string" || !SHA.test(id))
|
|
131
|
+
|| !Number.isSafeInteger(value.maxCases) || value.maxCases < 1 || value.maxCases > 32 || history.length > 4096)
|
|
132
|
+
throw new Error("invalid weekly investigation selection");
|
|
133
|
+
const eligible = [...new Set(value.eligibleCaseIds)].sort();
|
|
134
|
+
const key = digest({ week: value.week, population: value.population, policyDigest: value.policyDigest });
|
|
135
|
+
const inputDigest = digest({ ...value, eligibleCaseIds: eligible });
|
|
136
|
+
const selectedCaseIds = eligible.sort((a, b) => { const ka = digest([key, value.archiveSnapshot, a]), kb = digest([key, value.archiveSnapshot, b]); return ka < kb ? -1 : ka > kb ? 1 : 0; }).slice(0, value.maxCases);
|
|
137
|
+
const body = { version: "weekly-investigation-selection-v1", key, inputDigest, week: value.week, population: value.population,
|
|
138
|
+
policyDigest: value.policyDigest, archiveSnapshot: value.archiveSnapshot, selectedCaseIds, sampling: "exploratory-sha256-order-v1" };
|
|
139
|
+
const result = freeze({ ...body, id: digest(body) });
|
|
140
|
+
const previous = history.filter(record => record.key === key);
|
|
141
|
+
for (const record of previous) {
|
|
142
|
+
const { id, ...old } = json(record);
|
|
143
|
+
if (digest(old) !== id || id !== result.id)
|
|
144
|
+
throw new Error("weekly investigation already frozen or history invalid");
|
|
145
|
+
}
|
|
146
|
+
return previous.length ? freeze(json(previous[0])) : result;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=investigation.js.map
|
|
@@ -154,10 +154,12 @@ function qualificationLinuxProcessGroupMembers(processGroupId) {
|
|
|
154
154
|
if (process.platform !== "linux")
|
|
155
155
|
return [];
|
|
156
156
|
const members = [];
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
// Dirent construction may lstat an unrelated disappearing PID before our
|
|
158
|
+
// per-entry guard runs. Names need no metadata; validate each stat below.
|
|
159
|
+
for (const name of readdirSync("/proc")) {
|
|
160
|
+
if (!/^\d+$/.test(name))
|
|
159
161
|
continue;
|
|
160
|
-
const pid = Number(
|
|
162
|
+
const pid = Number(name);
|
|
161
163
|
try {
|
|
162
164
|
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
163
165
|
const close = stat.lastIndexOf(")");
|
|
@@ -135,15 +135,3 @@ export declare function deserializeTrajectoryEvents(text: string): TrajectoryEve
|
|
|
135
135
|
/** Evaluate a closed, declarative assertion set over saved normalized events. */
|
|
136
136
|
export declare function evaluateTrajectoryGates(assert: TrajectoryAssert, input: TrajectoryEventV1[]): TrajectoryGateResult;
|
|
137
137
|
export declare function parseTrajectoryAssert(raw: unknown, ctx: string): TrajectoryAssert;
|
|
138
|
-
export interface MutationSelfTestCase {
|
|
139
|
-
id: string;
|
|
140
|
-
detected: boolean;
|
|
141
|
-
status: TrajectoryAssertionStatus;
|
|
142
|
-
detail: string;
|
|
143
|
-
}
|
|
144
|
-
export interface MutationSelfTestReport {
|
|
145
|
-
baseline: "PASS";
|
|
146
|
-
cases: MutationSelfTestCase[];
|
|
147
|
-
}
|
|
148
|
-
/** Fast, deterministic proof that every trajectory assertion class can turn red. */
|
|
149
|
-
export declare function runTrajectoryMutationSelfTest(): MutationSelfTestReport;
|
package/dist/trajectory-gates.js
CHANGED
|
@@ -429,42 +429,6 @@ function parseCount(raw, ctx) {
|
|
|
429
429
|
throw new Error(`${ctx}.min exceeds max`);
|
|
430
430
|
return out;
|
|
431
431
|
}
|
|
432
|
-
/** Fast, deterministic proof that every trajectory assertion class can turn red. */
|
|
433
|
-
export function runTrajectoryMutationSelfTest() {
|
|
434
|
-
const e = (seq, type, extra = {}) => ({ event_version: TRAJECTORY_EVENT_VERSION, seq, type, source: "mutation-self-test", at: `2026-01-01T00:00:${String(seq).padStart(2, "0")}Z`, ...extra });
|
|
435
|
-
const h = "a".repeat(40), t = "b".repeat(40), p = "1".repeat(64);
|
|
436
|
-
const cases = [
|
|
437
|
-
{ id: "remove-required-event", assertion: { version: "1.0", require: [{ event: "risk_classified" }] }, good: [e(1, "risk_classified")], mutate: () => [e(1, "unrelated")] },
|
|
438
|
-
{ id: "add-forbidden-tool-side-effect-approval", assertion: { version: "1.0", forbid: [{ event: "tool_called", where: { tool: { equals: "rm" } } }, { event: "side_effect_approved" }] }, good: [e(1, "tool_called", { tool: "read" })], mutate: (events) => [...events, e(2, "side_effect_approved", { attributes: { action: "migration" } })] },
|
|
439
|
-
{ id: "reorder-transition", assertion: { version: "1.0", ordered: [[{ event: "phase_started" }, { event: "phase_completed" }]] }, good: [e(1, "phase_started"), e(2, "phase_completed")], mutate: () => [e(2, "phase_started"), e(1, "phase_completed")] },
|
|
440
|
-
{ id: "substitute-workspace-id", assertion: { version: "1.0", correlate: [{ left: { event: "code_changed" }, right: { event: "evidence_recorded" }, same: ["workspace_id"] }] }, good: [e(1, "code_changed", { workspace_id: "ws-1" }), e(2, "evidence_recorded", { workspace_id: "ws-1" })], mutate: (events) => events.map((x) => x.type === "evidence_recorded" ? { ...x, workspace_id: "ws-2" } : x) },
|
|
441
|
-
{ id: "concurrent-writer", assertion: { version: "1.0", forbid: [{ event: "writer_lease_conflict" }] }, good: [e(1, "writer_lease_acquired", { workspace_id: "ws-1" })], mutate: (events) => [...events, e(2, "writer_lease_conflict", { workspace_id: "ws-1" })] },
|
|
442
|
-
{ id: "approval-expired-or-mismatched", assertion: { version: "1.0", approvals: [{ grant: { event: "approval_granted" }, use: { event: "approval_used" }, same: ["approval.id", "approval.capability"], unexpired: true }] }, good: [e(1, "approval_granted", { approval: { id: "a1", capability: "push", expires_at: "2026-01-01T01:00:00Z" } }), e(2, "approval_used", { approval: { id: "a1", capability: "push", used_at: "2026-01-01T00:01:00Z" } })], mutate: (events) => events.map((x) => x.type === "approval_used" ? { ...x, approval: { ...x.approval, used_at: "2026-01-01T02:00:00Z" } } : x) },
|
|
443
|
-
{ id: "evidence-before-change", assertion: { version: "1.0", freshness: [{ subject: { event: "evidence_recorded" }, after: [{ event: "code_changed" }] }] }, good: [e(1, "code_changed"), e(2, "evidence_recorded")], mutate: () => [e(2, "code_changed"), e(1, "evidence_recorded")] },
|
|
444
|
-
{ id: "evidence-before-authority", assertion: { version: "1.0", freshness: [{ subject: { event: "evidence_recorded" }, after: [{ event: "plan_recorded" }] }] }, good: [e(1, "plan_recorded", { digests: { plan: p } }), e(2, "evidence_recorded")], mutate: () => [e(2, "plan_recorded", { digests: { plan: p } }), e(1, "evidence_recorded")] },
|
|
445
|
-
{ id: "evidence-before-build-completion", assertion: { version: "1.0", freshness: [{ subject: { event: "evidence_recorded" }, after: [{ event: "phase_completed", where: { phase: { equals: "build" } } }] }] }, good: [e(1, "phase_completed", { phase: "build" }), e(2, "evidence_recorded")], mutate: () => [e(2, "phase_completed", { phase: "build" }), e(1, "evidence_recorded")] },
|
|
446
|
-
{ id: "head-equal-tree-different", assertion: { version: "1.0", correlate: [{ left: { event: "code_changed" }, right: { event: "evidence_recorded" }, same: ["digests.head", "digests.tree"] }] }, good: [e(1, "code_changed", { digests: { head: h, tree: t } }), e(2, "evidence_recorded", { digests: { head: h, tree: t } })], mutate: (events) => events.map((x) => x.type === "evidence_recorded" ? { ...x, digests: { head: h, tree: "c".repeat(40) } } : x) },
|
|
447
|
-
{ id: "command-receipt-nonzero", assertion: { version: "1.0", require: [{ event: "evidence_recorded", where: { exit_code: { equals: 0 } } }] }, good: [e(1, "evidence_recorded", { exit_code: 0 })], mutate: (events) => events.map((x) => ({ ...x, exit_code: 1 })) },
|
|
448
|
-
{ id: "remove-requirement-coverage", assertion: { version: "1.0", coverage: [{ requirements: ["AUTH-7"], events: { event: "evidence_recorded" } }] }, good: [e(1, "evidence_recorded", { requirements: ["AUTH-7"] })], mutate: (events) => events.map((x) => ({ ...x, requirements: [] })) },
|
|
449
|
-
{ id: "mutate-superseded-task", assertion: { version: "1.0", forbid_after: [{ anchor: { event: "task_packet_superseded" }, forbidden: [{ event: "code_changed" }, { event: "repair_started" }], same: ["task_id"] }] }, good: [e(1, "task_packet_superseded", { task_id: "old" }), e(2, "code_changed", { task_id: "new" })], mutate: (events) => events.map((x) => x.type === "code_changed" ? { ...x, task_id: "old" } : x) },
|
|
450
|
-
{ id: "reuse-context-id", assertion: { version: "1.0", unique: [{ events: { event: "review_recorded" }, fields: ["context_id"] }] }, good: [e(1, "review_recorded", { context_id: "c1" }), e(2, "review_recorded", { context_id: "c2" })], mutate: (events) => events.map((x) => x.seq === 2 ? { ...x, context_id: "c1" } : x) },
|
|
451
|
-
{ id: "mismatch-finalization-identity", assertion: { version: "1.0", correlate: [{ left: { event: "gate_evaluated", where: { "attributes.gate": { equals: "finalize" } } }, right: { event: "finalization_completed" }, same: ["digests.head", "digests.tree"], order: "before" }] }, good: [e(1, "gate_evaluated", { digests: { head: h, tree: t }, attributes: { gate: "finalize", code: "OK" } }), e(2, "finalization_completed", { digests: { head: h, tree: t } })], mutate: (events) => events.map((x) => x.type === "finalization_completed" ? { ...x, digests: { head: h, tree: "d".repeat(40) } } : x) },
|
|
452
|
-
{ id: "v3-blocked-critical-code", assertion: { version: "1.0", require: [{ event: "gate_evaluated", where: { "attributes.code": { equals: "BLOCKED_CRITICAL_ASSURANCE" } } }] }, good: [e(1, "gate_evaluated", { attributes: { gate: "finalize", code: "BLOCKED_CRITICAL_ASSURANCE", missing_count: 2 } })], mutate: (events) => events.map((x) => ({ ...x, attributes: { ...x.attributes, code: "OK" } })) },
|
|
453
|
-
{ id: "v3-stale-gate-must-block", assertion: { version: "1.0", require: [{ event: "gate_evaluated", where: { "attributes.code": { matches: "^BLOCKED_" } } }] }, good: [e(1, "gate_evaluated", { attributes: { gate: "finalize", code: "BLOCKED_ASSURANCE" } })], mutate: (events) => events.map((x) => ({ ...x, attributes: { ...x.attributes, code: "OK" } })) },
|
|
454
|
-
{ id: "v3-finalize-gate-must-be-ok", assertion: { version: "1.0", require: [{ event: "gate_evaluated", where: { "attributes.gate": { equals: "finalize" }, "attributes.code": { equals: "OK" } } }] }, good: [e(1, "gate_evaluated", { attributes: { gate: "finalize", code: "OK" } })], mutate: (events) => events.map((x) => ({ ...x, attributes: { ...x.attributes, code: "BLOCKED_ASSURANCE" } })) },
|
|
455
|
-
{ id: "v3-discard-requires-explicit-request", assertion: { version: "1.0", forbid: [{ event: "finish_selected", where: { "attributes.choice": { equals: "discard" }, "attributes.explicit_request": { equals: false } } }] }, good: [e(1, "finish_selected", { attributes: { choice: "discard", explicit_request: true } })], mutate: (events) => events.map((x) => ({ ...x, attributes: { ...x.attributes, explicit_request: false } })) },
|
|
456
|
-
{ id: "v3-side-effect-approval-and-gate", assertion: { version: "1.0", ordered: [[{ event: "side_effect_approved", where: { "attributes.action": { equals: "migration" } } }, { event: "gate_evaluated", where: { "attributes.gate": { equals: "side-effect" }, "attributes.code": { equals: "OK" } } }]] }, good: [e(1, "side_effect_approved", { attributes: { action: "migration" } }), e(2, "gate_evaluated", { attributes: { gate: "side-effect", code: "OK" } })], mutate: (events) => events.map((x) => x.type === "gate_evaluated" ? { ...x, attributes: { ...x.attributes, code: "BLOCKED_ASSURANCE" } } : x) },
|
|
457
|
-
{ id: "v3-governed-spawn-started", assertion: { version: "1.0", require: [{ event: "child_started", where: { source: { equals: "pi-daddy-v3" }, "attributes.state": { equals: "starting" } } }] }, good: [e(1, "child_started", { source: "pi-daddy-v3", attributes: { state: "starting" } })], mutate: (events) => events.map((x) => ({ ...x, attributes: { ...x.attributes, state: "failed" } })) },
|
|
458
|
-
];
|
|
459
|
-
const results = cases.map((testCase) => {
|
|
460
|
-
const baseline = evaluateTrajectoryGates(testCase.assertion, testCase.good);
|
|
461
|
-
if (baseline.status !== "PASS")
|
|
462
|
-
throw new Error(`mutation self-test baseline ${testCase.id} is ${baseline.status}`);
|
|
463
|
-
const mutated = evaluateTrajectoryGates(testCase.assertion, testCase.mutate(structuredClone(testCase.good)));
|
|
464
|
-
return { id: testCase.id, detected: mutated.status !== "PASS", status: mutated.status, detail: mutated.assertions.find((result) => result.status !== "PASS")?.detail ?? "mutation was not detected" };
|
|
465
|
-
});
|
|
466
|
-
return { baseline: "PASS", cases: results };
|
|
467
|
-
}
|
|
468
432
|
// ---------------------------------------------------------------------------
|
|
469
433
|
// Internals
|
|
470
434
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import { type QualificationProcessIdentity } from './qualification-process.js';
|
|
3
|
+
export interface TrustedHostSupervisionLimits {
|
|
4
|
+
wallMs: number;
|
|
5
|
+
settlementMs: number;
|
|
6
|
+
maxOutputBytes: number;
|
|
7
|
+
signal: AbortSignal;
|
|
8
|
+
}
|
|
9
|
+
export interface TrustedHostProcessObservation {
|
|
10
|
+
outcome: 'completed' | 'failed' | 'aborted' | 'timed-out' | 'output-limit' | 'unknown';
|
|
11
|
+
identity: QualificationProcessIdentity;
|
|
12
|
+
exitCode: number | null;
|
|
13
|
+
signal: NodeJS.Signals | null;
|
|
14
|
+
stdoutBytes: number;
|
|
15
|
+
stderrBytes: number;
|
|
16
|
+
truncated: boolean;
|
|
17
|
+
cleanupAttempted: boolean;
|
|
18
|
+
acceptance: 'not-assessed';
|
|
19
|
+
}
|
|
20
|
+
/** Observe an ALREADY admitted, caller-owned detached Linux child, after its spawn event.
|
|
21
|
+
* No spawn/authorization/accounting/retry is performed here. The parent event loop must
|
|
22
|
+
* remain responsive. Occurrence-safe same-group cleanup is not hostile-code containment,
|
|
23
|
+
* survivor/crash recovery, hard aggregate RAM/CPU, or proof of remote provider termination.
|
|
24
|
+
* Bytes are discarded, not retained or hashed; the existing worker journal owns artifacts. */
|
|
25
|
+
export declare function superviseTrustedHost(child: ChildProcess, limits: TrustedHostSupervisionLimits): Promise<TrustedHostProcessObservation>;
|