@davesheffer/hunch 1.7.0 → 1.8.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/README.md +242 -0
- package/bench/constitution-exp03-v1.json +70 -0
- package/dist/cli/index.js +1630 -107
- package/dist/constitution/adapters.js +487 -0
- package/dist/constitution/behaviorAttestationBinding.js +17 -0
- package/dist/constitution/behaviorEvaluator.js +220 -0
- package/dist/constitution/behaviorProof.js +205 -0
- package/dist/constitution/behaviorWorkspace.js +124 -0
- package/dist/constitution/bootstrap.js +133 -0
- package/dist/constitution/canonical.js +51 -0
- package/dist/constitution/card.js +133 -0
- package/dist/constitution/compiler.js +176 -0
- package/dist/constitution/composition.js +101 -0
- package/dist/constitution/corpus.js +58 -0
- package/dist/constitution/delta.js +154 -0
- package/dist/constitution/disposition.js +141 -0
- package/dist/constitution/evaluator.js +435 -0
- package/dist/constitution/experiment.js +1007 -0
- package/dist/constitution/experimentRunner.js +344 -0
- package/dist/constitution/g2.js +291 -0
- package/dist/constitution/g2BehaviorAttestation.js +209 -0
- package/dist/constitution/g2BehaviorCandidates.js +703 -0
- package/dist/constitution/g2BehaviorDependencies.js +379 -0
- package/dist/constitution/g2BehaviorMaterialization.js +171 -0
- package/dist/constitution/g2BehaviorPolicyMaterializer.js +241 -0
- package/dist/constitution/g2CandidateAttestation.js +179 -0
- package/dist/constitution/g2Candidates.js +195 -0
- package/dist/constitution/g2Drills.js +122 -0
- package/dist/constitution/g3.js +511 -0
- package/dist/constitution/g3Conformance.js +132 -0
- package/dist/constitution/lifecycle.js +224 -0
- package/dist/constitution/mutation.js +262 -0
- package/dist/constitution/nodeTestEvidence.js +47 -0
- package/dist/constitution/plan.js +172 -0
- package/dist/constitution/policyRuntime.js +8 -0
- package/dist/constitution/proof.js +166 -0
- package/dist/constitution/repairPolicies.js +78 -0
- package/dist/constitution/replay.js +361 -0
- package/dist/constitution/replayCache.js +89 -0
- package/dist/constitution/replayWorker.js +34 -0
- package/dist/constitution/repository.js +533 -0
- package/dist/constitution/schema.js +545 -0
- package/dist/constitution/scorecard.js +106 -0
- package/dist/constitution/service.js +1211 -0
- package/dist/constitution/shadow.js +235 -0
- package/dist/constitution/sourceMutation.js +316 -0
- package/dist/constitution/structural.js +601 -0
- package/dist/core/autoreview.js +27 -3
- package/dist/core/dupdetect.js +10 -3
- package/dist/core/escalations.js +65 -0
- package/dist/core/events.js +61 -0
- package/dist/core/externalImports.js +24 -0
- package/dist/core/hookpolicy.js +3 -0
- package/dist/core/memorylog.js +69 -0
- package/dist/core/relativeImports.js +33 -0
- package/dist/core/repair.js +71 -0
- package/dist/core/reviewqueue.js +11 -0
- package/dist/core/stats.js +115 -0
- package/dist/extractors/git.js +120 -0
- package/dist/extractors/indexer.js +39 -38
- package/dist/extractors/nativeTreeSitter.js +108 -0
- package/dist/extractors/parse.js +5 -15
- package/dist/integrations/claudemd.js +8 -1
- package/dist/integrations/gitignore.js +8 -0
- package/dist/integrations/providers.js +32 -10
- package/dist/integrations/sync.js +16 -1
- package/dist/mcp/server.js +317 -1
- package/dist/synthesis/synthesize.js +8 -1
- package/dist/wiki/graph.js +301 -0
- package/dist/wiki/wiki.js +31 -3
- package/package.json +5 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { shortHash } from "../core/ids.js";
|
|
7
|
+
import { canonicalHash } from "./canonical.js";
|
|
8
|
+
import { NODE_TEST_REPORTER_SOURCE, exactNodeTestPattern, nodeTestIsolationFlag, nodeTestReporterEvents } from "./nodeTestEvidence.js";
|
|
9
|
+
export const G2_OPERATIONAL_DRILLS = {
|
|
10
|
+
evaluator_error: {
|
|
11
|
+
file: "test/constitution.test.ts",
|
|
12
|
+
name: "neutral result algebra keeps not_applicable, unknown, and error distinct",
|
|
13
|
+
},
|
|
14
|
+
false_positive: {
|
|
15
|
+
file: "test/constitution.test.ts",
|
|
16
|
+
name: "Phase 2N shadow outcomes are append-only, disposition-bound, and precision-only",
|
|
17
|
+
},
|
|
18
|
+
private_leak: {
|
|
19
|
+
file: "test/constitution.test.ts",
|
|
20
|
+
name: "private evidence produces private policy/proof and public-only evaluation leaks no identifier",
|
|
21
|
+
},
|
|
22
|
+
stale_policy: {
|
|
23
|
+
file: "test/constitution.test.ts",
|
|
24
|
+
name: "proof is bound to policy semantics and cannot authorize a changed assertion",
|
|
25
|
+
},
|
|
26
|
+
provider_outage: {
|
|
27
|
+
file: "test/cliproviders.test.ts",
|
|
28
|
+
name: "HUNCH_SYNTH_PROVIDER=deterministic forces the offline heuristic",
|
|
29
|
+
},
|
|
30
|
+
corrupt_graph: {
|
|
31
|
+
file: "test/constitution.test.ts",
|
|
32
|
+
name: "corrupt policy JSON is a visible error, never a skipped false pass",
|
|
33
|
+
},
|
|
34
|
+
adapter_break: {
|
|
35
|
+
file: "test/constitution.test.ts",
|
|
36
|
+
name: "Gate G1 adapter contract: CLI, MCP, and strict CI expose the identical receipt",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
export function executeG2OperationalDrill(root, plan, runbook, category, timeoutMs = 120_000) {
|
|
40
|
+
if (plan.runbooks[category] !== runbook.id)
|
|
41
|
+
throw new Error(`G2 ${category} drill does not bind plan runbook ${runbook.id}`);
|
|
42
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300_000)
|
|
43
|
+
throw new Error("G2 drill timeout must be a positive integer no greater than 300000");
|
|
44
|
+
const spec = G2_OPERATIONAL_DRILLS[category];
|
|
45
|
+
const source = readFileSync(join(root, spec.file), "utf8");
|
|
46
|
+
const session = mkdtempSync(join(tmpdir(), "hunch-g2-drill-"));
|
|
47
|
+
const reporter = join(session, "reporter.mjs");
|
|
48
|
+
writeFileSync(reporter, NODE_TEST_REPORTER_SOURCE);
|
|
49
|
+
const isolationFlag = nodeTestIsolationFlag();
|
|
50
|
+
const tsx = join(root, "node_modules/tsx/dist/cli.mjs");
|
|
51
|
+
const env = {};
|
|
52
|
+
for (const key of ["PATH", "SystemRoot", "WINDIR", "TMPDIR", "TMP", "TEMP"]) {
|
|
53
|
+
if (process.env[key])
|
|
54
|
+
env[key] = process.env[key];
|
|
55
|
+
}
|
|
56
|
+
Object.assign(env, {
|
|
57
|
+
HOME: session,
|
|
58
|
+
HUNCH_PRIVATE_DIR: "",
|
|
59
|
+
HUNCH_SYNTH_PROVIDER: "deterministic",
|
|
60
|
+
CI: "1",
|
|
61
|
+
NO_COLOR: "1",
|
|
62
|
+
FORCE_COLOR: "0",
|
|
63
|
+
});
|
|
64
|
+
const run = spawnSync(process.execPath, [
|
|
65
|
+
tsx,
|
|
66
|
+
"--test",
|
|
67
|
+
isolationFlag,
|
|
68
|
+
`--test-name-pattern=${exactNodeTestPattern(spec.name)}`,
|
|
69
|
+
`--test-reporter=${pathToFileURL(reporter).href}`,
|
|
70
|
+
"--test-reporter-destination=stdout",
|
|
71
|
+
spec.file,
|
|
72
|
+
], {
|
|
73
|
+
cwd: root,
|
|
74
|
+
env,
|
|
75
|
+
encoding: "utf8",
|
|
76
|
+
timeout: timeoutMs,
|
|
77
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
78
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
79
|
+
shell: false,
|
|
80
|
+
});
|
|
81
|
+
rmSync(session, { recursive: true, force: true });
|
|
82
|
+
const stdout = run.stdout ?? "";
|
|
83
|
+
const stderr = run.stderr ?? "";
|
|
84
|
+
const events = nodeTestReporterEvents(stdout).filter((event) => event.name === spec.name && !event.skip && !event.todo);
|
|
85
|
+
const selectedEvent = events.length === 1 ? (events[0].type === "test:pass" ? "passed" : "failed") : null;
|
|
86
|
+
let result = "error";
|
|
87
|
+
let errorCode;
|
|
88
|
+
if (run.error)
|
|
89
|
+
errorCode = run.error.code === "ETIMEDOUT" ? "timeout" : "runner-failed";
|
|
90
|
+
else if (run.signal)
|
|
91
|
+
errorCode = "runner-signaled";
|
|
92
|
+
else if (events.length === 0)
|
|
93
|
+
errorCode = "selected-test-not-executed";
|
|
94
|
+
else if (events.length > 1)
|
|
95
|
+
errorCode = "selected-test-ambiguous";
|
|
96
|
+
else if (selectedEvent === "passed" && run.status === 0)
|
|
97
|
+
result = "passed";
|
|
98
|
+
else if (selectedEvent === "failed" && run.status !== 0)
|
|
99
|
+
result = "failed";
|
|
100
|
+
else
|
|
101
|
+
errorCode = "runner-outcome-inconsistent";
|
|
102
|
+
const body = {
|
|
103
|
+
category,
|
|
104
|
+
plan_id: plan.id,
|
|
105
|
+
runbook_id: runbook.id,
|
|
106
|
+
runbook_hash: canonicalHash(runbook),
|
|
107
|
+
test: { ...spec, source_hash: canonicalHash(source) },
|
|
108
|
+
runner: { name: "node-test-tsx", isolation_flag: isolationFlag },
|
|
109
|
+
result,
|
|
110
|
+
exit_code: run.status ?? null,
|
|
111
|
+
selected_event: selectedEvent,
|
|
112
|
+
...(errorCode ? { error_code: errorCode } : {}),
|
|
113
|
+
log_hash: canonicalHash({ stdout, stderr }),
|
|
114
|
+
data_class: "private",
|
|
115
|
+
authority: "none",
|
|
116
|
+
effects: "diagnostic_only",
|
|
117
|
+
writes: "none",
|
|
118
|
+
};
|
|
119
|
+
const contentHash = canonicalHash(body);
|
|
120
|
+
return { id: `g2drill_${shortHash(contentHash)}`, content_hash: contentHash, ...body };
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=g2Drills.js.map
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { writeFileAtomic } from "../core/io.js";
|
|
5
|
+
import { shortHash } from "../core/ids.js";
|
|
6
|
+
import { canonicalHash } from "./canonical.js";
|
|
7
|
+
import { currentAppendOnly, loadPrivateRecords } from "./g2.js";
|
|
8
|
+
const HASH = /^sha1:[a-f0-9]{40}$/;
|
|
9
|
+
const HUMAN_ACTOR = /^(human|github|git):[^\s]+$/i;
|
|
10
|
+
const POLICY_ID = /^pol_[a-f0-9]{10}$/;
|
|
11
|
+
const CLIENT_ID = /^[a-z][a-z0-9_-]{1,63}$/;
|
|
12
|
+
const encode = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
13
|
+
export const G3_REQUIRED_EXPERIMENTS = ["EXP-01", "EXP-03"];
|
|
14
|
+
const ExperimentBindingsSchema = z.object({
|
|
15
|
+
retrieval: z.string().regex(/^expreg_[a-f0-9]{10}$/),
|
|
16
|
+
compiler: z.string().regex(/^expreg_[a-f0-9]{10}$/),
|
|
17
|
+
}).strict();
|
|
18
|
+
export const G3PlanSchema = z.object({
|
|
19
|
+
id: z.string().regex(/^g3plan_[a-f0-9]{10}$/),
|
|
20
|
+
content_hash: z.string().regex(HASH),
|
|
21
|
+
gate: z.literal("G3"),
|
|
22
|
+
g2_readiness_hash: z.string().regex(HASH),
|
|
23
|
+
policy_ids: z.array(z.string().regex(POLICY_ID)).min(3, "G3 requires at least 3 selected policies"),
|
|
24
|
+
clients: z.array(z.string().regex(CLIENT_ID)).min(3, "G3 requires at least 3 selected clients"),
|
|
25
|
+
experiments: ExperimentBindingsSchema,
|
|
26
|
+
actor: z.string().regex(HUMAN_ACTOR, "G3 plan requires an explicit human actor (human:, github:, or git:)"),
|
|
27
|
+
reason: z.string().trim().min(1).max(2000),
|
|
28
|
+
supersedes: z.string().regex(/^g3plan_[a-f0-9]{10}$/).nullable(),
|
|
29
|
+
data_class: z.literal("private"),
|
|
30
|
+
authority: z.literal("none"),
|
|
31
|
+
created_at: z.string().datetime({ offset: true }),
|
|
32
|
+
}).strict().superRefine((plan, ctx) => {
|
|
33
|
+
if (new Set(plan.policy_ids).size !== plan.policy_ids.length) {
|
|
34
|
+
ctx.addIssue({ code: "custom", path: ["policy_ids"], message: "G3 policy ids must be unique" });
|
|
35
|
+
}
|
|
36
|
+
if (new Set(plan.clients).size !== plan.clients.length) {
|
|
37
|
+
ctx.addIssue({ code: "custom", path: ["clients"], message: "G3 client ids must be unique" });
|
|
38
|
+
}
|
|
39
|
+
if (plan.experiments.retrieval === plan.experiments.compiler) {
|
|
40
|
+
ctx.addIssue({ code: "custom", path: ["experiments"], message: "G3 retrieval and compiler experiments must be distinct records" });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
export function g3PlanContentHash(plan) {
|
|
44
|
+
const { id: _id, content_hash: _hash, ...body } = plan;
|
|
45
|
+
return canonicalHash(body);
|
|
46
|
+
}
|
|
47
|
+
export function compileG3Plan(input, opts = {}) {
|
|
48
|
+
const body = {
|
|
49
|
+
gate: "G3",
|
|
50
|
+
g2_readiness_hash: input.g2_readiness_hash,
|
|
51
|
+
policy_ids: [...input.policy_ids].sort(),
|
|
52
|
+
clients: [...input.clients].sort(),
|
|
53
|
+
experiments: { retrieval: input.experiments.retrieval, compiler: input.experiments.compiler },
|
|
54
|
+
actor: input.actor,
|
|
55
|
+
reason: input.reason.trim(),
|
|
56
|
+
supersedes: input.supersedes ?? null,
|
|
57
|
+
data_class: "private",
|
|
58
|
+
authority: "none",
|
|
59
|
+
created_at: opts.now ?? new Date().toISOString(),
|
|
60
|
+
};
|
|
61
|
+
const contentHash = canonicalHash(body);
|
|
62
|
+
return G3PlanSchema.parse({ id: `g3plan_${shortHash(contentHash)}`, content_hash: contentHash, ...body });
|
|
63
|
+
}
|
|
64
|
+
const ExperimentArmSchema = z.object({
|
|
65
|
+
id: z.string().regex(/^[A-Z][A-Z0-9_-]{0,15}$/),
|
|
66
|
+
description: z.string().trim().min(1).max(2000),
|
|
67
|
+
}).strict();
|
|
68
|
+
const ExperimentAssignmentSchema = z.object({
|
|
69
|
+
method: z.enum(["randomized", "blocked_randomized", "paired_randomized"]),
|
|
70
|
+
unit: z.enum(["task", "pull_request", "repository", "developer", "policy_candidate"]),
|
|
71
|
+
seed: z.string().trim().min(1).max(256),
|
|
72
|
+
}).strict();
|
|
73
|
+
const ExperimentSamplePlanSchema = z.object({
|
|
74
|
+
minimum_per_arm: z.number().int().min(1).max(100000),
|
|
75
|
+
target_per_arm: z.number().int().min(1).max(100000),
|
|
76
|
+
maximum_total: z.number().int().min(2).max(1000000),
|
|
77
|
+
rationale: z.string().trim().min(1).max(4000),
|
|
78
|
+
}).strict().superRefine((plan, ctx) => {
|
|
79
|
+
if (plan.target_per_arm < plan.minimum_per_arm) {
|
|
80
|
+
ctx.addIssue({ code: "custom", path: ["target_per_arm"], message: "target_per_arm cannot be below minimum_per_arm" });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const ExperimentAnalysisPlanSchema = z.object({
|
|
84
|
+
primary_estimator: z.string().trim().min(1).max(2000),
|
|
85
|
+
effect_size: z.string().trim().min(1).max(2000),
|
|
86
|
+
uncertainty: z.string().trim().min(1).max(2000),
|
|
87
|
+
missing_data: z.string().trim().min(1).max(2000),
|
|
88
|
+
multiple_metrics: z.string().trim().min(1).max(2000),
|
|
89
|
+
}).strict();
|
|
90
|
+
export const ExperimentPreregistrationSchema = z.object({
|
|
91
|
+
id: z.string().regex(/^expreg_[a-f0-9]{10}$/),
|
|
92
|
+
content_hash: z.string().regex(HASH),
|
|
93
|
+
experiment: z.enum(G3_REQUIRED_EXPERIMENTS),
|
|
94
|
+
revision: z.number().int().min(1),
|
|
95
|
+
hypothesis: z.string().trim().min(1).max(4000),
|
|
96
|
+
primary_metric: z.string().trim().min(1).max(2000),
|
|
97
|
+
secondary_metrics: z.array(z.string().trim().min(1).max(2000)).min(1),
|
|
98
|
+
unit: z.enum(["task", "pull_request", "repository", "developer", "policy_candidate"]),
|
|
99
|
+
arms: z.array(ExperimentArmSchema).min(2).max(16),
|
|
100
|
+
assignment: ExperimentAssignmentSchema,
|
|
101
|
+
strata: z.array(z.string().trim().min(1).max(500)).min(1),
|
|
102
|
+
inclusion: z.array(z.string().trim().min(1).max(1000)).min(1),
|
|
103
|
+
exclusion: z.array(z.string().trim().min(1).max(1000)).min(1),
|
|
104
|
+
sample_plan: ExperimentSamplePlanSchema,
|
|
105
|
+
analysis_plan: ExperimentAnalysisPlanSchema,
|
|
106
|
+
stopping_rule: z.string().trim().min(1).max(4000),
|
|
107
|
+
guardrails: z.array(z.string().trim().min(1).max(1000)).min(1),
|
|
108
|
+
privacy_class: z.literal("private"),
|
|
109
|
+
status: z.literal("registered"),
|
|
110
|
+
actor: z.string().regex(HUMAN_ACTOR, "experiment preregistration requires an explicit human actor"),
|
|
111
|
+
reason: z.string().trim().min(1).max(2000),
|
|
112
|
+
supersedes: z.string().regex(/^expreg_[a-f0-9]{10}$/).nullable(),
|
|
113
|
+
authority: z.literal("none"),
|
|
114
|
+
registered_at: z.string().datetime({ offset: true }),
|
|
115
|
+
}).strict().superRefine((spec, ctx) => {
|
|
116
|
+
const armIds = spec.arms.map((arm) => arm.id);
|
|
117
|
+
if (new Set(armIds).size !== armIds.length)
|
|
118
|
+
ctx.addIssue({ code: "custom", path: ["arms"], message: "experiment arm ids must be unique" });
|
|
119
|
+
if (spec.assignment.unit !== spec.unit)
|
|
120
|
+
ctx.addIssue({ code: "custom", path: ["assignment", "unit"], message: "assignment unit must equal the experiment unit" });
|
|
121
|
+
if (spec.sample_plan.maximum_total < spec.sample_plan.target_per_arm * spec.arms.length) {
|
|
122
|
+
ctx.addIssue({ code: "custom", path: ["sample_plan", "maximum_total"], message: "maximum_total cannot be below target_per_arm multiplied by arm count" });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
export function experimentPreregistrationContentHash(spec) {
|
|
126
|
+
const { id: _id, content_hash: _hash, ...body } = spec;
|
|
127
|
+
return canonicalHash(body);
|
|
128
|
+
}
|
|
129
|
+
export function compileExperimentPreregistration(input, opts = {}) {
|
|
130
|
+
const body = {
|
|
131
|
+
experiment: input.experiment,
|
|
132
|
+
revision: input.revision ?? 1,
|
|
133
|
+
hypothesis: input.hypothesis.trim(),
|
|
134
|
+
primary_metric: input.primary_metric.trim(),
|
|
135
|
+
secondary_metrics: [...input.secondary_metrics],
|
|
136
|
+
unit: input.unit,
|
|
137
|
+
arms: input.arms.map((arm) => ({ id: arm.id, description: arm.description.trim() })),
|
|
138
|
+
assignment: { ...input.assignment },
|
|
139
|
+
strata: [...input.strata],
|
|
140
|
+
inclusion: [...input.inclusion],
|
|
141
|
+
exclusion: [...input.exclusion],
|
|
142
|
+
sample_plan: { ...input.sample_plan },
|
|
143
|
+
analysis_plan: { ...input.analysis_plan },
|
|
144
|
+
stopping_rule: input.stopping_rule.trim(),
|
|
145
|
+
guardrails: [...input.guardrails],
|
|
146
|
+
privacy_class: "private",
|
|
147
|
+
status: "registered",
|
|
148
|
+
actor: input.actor,
|
|
149
|
+
reason: input.reason.trim(),
|
|
150
|
+
supersedes: input.supersedes ?? null,
|
|
151
|
+
authority: "none",
|
|
152
|
+
registered_at: opts.now ?? new Date().toISOString(),
|
|
153
|
+
};
|
|
154
|
+
const contentHash = canonicalHash(body);
|
|
155
|
+
return ExperimentPreregistrationSchema.parse({ id: `expreg_${shortHash(contentHash)}`, content_hash: contentHash, ...body });
|
|
156
|
+
}
|
|
157
|
+
const ComprehensionSchema = z.object({
|
|
158
|
+
requirement: z.enum(["correct", "incorrect"]),
|
|
159
|
+
limitations: z.enum(["correct", "incorrect"]),
|
|
160
|
+
authority: z.enum(["correct", "incorrect"]),
|
|
161
|
+
}).strict();
|
|
162
|
+
export const ProofReviewMeasurementSchema = z.object({
|
|
163
|
+
id: z.string().regex(/^proofreview_[a-f0-9]{10}$/),
|
|
164
|
+
content_hash: z.string().regex(HASH),
|
|
165
|
+
plan_id: z.string().regex(/^g3plan_[a-f0-9]{10}$/),
|
|
166
|
+
policy_id: z.string().regex(POLICY_ID),
|
|
167
|
+
policy_hash: z.string().regex(HASH),
|
|
168
|
+
proof_id: z.string().min(1),
|
|
169
|
+
proof_hash: z.string().regex(HASH),
|
|
170
|
+
card_hash: z.string().regex(HASH),
|
|
171
|
+
reviewer: z.string().regex(HUMAN_ACTOR, "proof review requires an explicit human reviewer"),
|
|
172
|
+
duration_ms: z.number().int().min(1000).max(14_400_000),
|
|
173
|
+
comprehension: ComprehensionSchema,
|
|
174
|
+
notes: z.string().trim().min(1).max(4000),
|
|
175
|
+
supersedes: z.string().regex(/^proofreview_[a-f0-9]{10}$/).nullable(),
|
|
176
|
+
data_class: z.literal("private"),
|
|
177
|
+
authority: z.literal("none"),
|
|
178
|
+
measured_at: z.string().datetime({ offset: true }),
|
|
179
|
+
}).strict();
|
|
180
|
+
export function proofReviewMeasurementContentHash(record) {
|
|
181
|
+
const { id: _id, content_hash: _hash, ...body } = record;
|
|
182
|
+
return canonicalHash(body);
|
|
183
|
+
}
|
|
184
|
+
export function compileProofReviewMeasurement(input, opts = {}) {
|
|
185
|
+
const body = {
|
|
186
|
+
plan_id: input.plan_id,
|
|
187
|
+
policy_id: input.policy_id,
|
|
188
|
+
policy_hash: input.policy_hash,
|
|
189
|
+
proof_id: input.proof_id,
|
|
190
|
+
proof_hash: input.proof_hash,
|
|
191
|
+
card_hash: input.card_hash,
|
|
192
|
+
reviewer: input.reviewer,
|
|
193
|
+
duration_ms: input.duration_ms,
|
|
194
|
+
comprehension: { ...input.comprehension },
|
|
195
|
+
notes: input.notes.trim(),
|
|
196
|
+
supersedes: input.supersedes ?? null,
|
|
197
|
+
data_class: "private",
|
|
198
|
+
authority: "none",
|
|
199
|
+
measured_at: opts.now ?? new Date().toISOString(),
|
|
200
|
+
};
|
|
201
|
+
const contentHash = canonicalHash(body);
|
|
202
|
+
return ProofReviewMeasurementSchema.parse({ id: `proofreview_${shortHash(contentHash)}`, content_hash: contentHash, ...body });
|
|
203
|
+
}
|
|
204
|
+
export const AdapterConformanceReceiptSchema = z.object({
|
|
205
|
+
id: z.string().regex(/^g3conformance_[a-f0-9]{10}$/),
|
|
206
|
+
content_hash: z.string().regex(HASH),
|
|
207
|
+
plan_id: z.string().regex(/^g3plan_[a-f0-9]{10}$/),
|
|
208
|
+
clients: z.array(z.string().regex(CLIENT_ID)).min(3),
|
|
209
|
+
test: z.object({ file: z.string().min(1), name: z.string().min(1), source_hash: z.string().regex(HASH) }).strict(),
|
|
210
|
+
runner: z.object({ name: z.literal("node-test-tsx"), isolation_flag: z.string().min(1) }).strict(),
|
|
211
|
+
result: z.enum(["passed", "failed", "error"]),
|
|
212
|
+
exit_code: z.number().int().nullable(),
|
|
213
|
+
selected_event: z.enum(["passed", "failed"]).nullable(),
|
|
214
|
+
verdict_agreement: z.number().min(0).max(1).nullable(),
|
|
215
|
+
confirmed_private_leaks: z.number().int().min(0).nullable(),
|
|
216
|
+
error_code: z.string().min(1).optional(),
|
|
217
|
+
log_hash: z.string().regex(HASH),
|
|
218
|
+
supersedes: z.string().regex(/^g3conformance_[a-f0-9]{10}$/).nullable(),
|
|
219
|
+
data_class: z.literal("private"),
|
|
220
|
+
authority: z.literal("none"),
|
|
221
|
+
effects: z.literal("diagnostic_only"),
|
|
222
|
+
writes: z.literal("evidence_receipt_only"),
|
|
223
|
+
executed_at: z.string().datetime({ offset: true }),
|
|
224
|
+
}).strict().superRefine((receipt, ctx) => {
|
|
225
|
+
if (new Set(receipt.clients).size !== receipt.clients.length)
|
|
226
|
+
ctx.addIssue({ code: "custom", path: ["clients"], message: "conformance client ids must be unique" });
|
|
227
|
+
if (receipt.result === "passed" && (receipt.verdict_agreement !== 1 || receipt.confirmed_private_leaks !== 0)) {
|
|
228
|
+
ctx.addIssue({ code: "custom", message: "passed conformance requires exact verdict agreement and zero confirmed private leaks" });
|
|
229
|
+
}
|
|
230
|
+
if (receipt.result !== "passed" && (receipt.verdict_agreement !== null || receipt.confirmed_private_leaks !== null)) {
|
|
231
|
+
ctx.addIssue({ code: "custom", message: "failed/error conformance cannot claim agreement or privacy results" });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
export function adapterConformanceContentHash(receipt) {
|
|
235
|
+
const { id: _id, content_hash: _hash, ...body } = receipt;
|
|
236
|
+
return canonicalHash(body);
|
|
237
|
+
}
|
|
238
|
+
export function compileAdapterConformanceReceipt(input, opts = {}) {
|
|
239
|
+
const body = {
|
|
240
|
+
plan_id: input.plan_id,
|
|
241
|
+
clients: [...input.clients].sort(),
|
|
242
|
+
test: { ...input.test },
|
|
243
|
+
runner: { ...input.runner },
|
|
244
|
+
result: input.result,
|
|
245
|
+
exit_code: input.exit_code,
|
|
246
|
+
selected_event: input.selected_event,
|
|
247
|
+
verdict_agreement: input.verdict_agreement,
|
|
248
|
+
confirmed_private_leaks: input.confirmed_private_leaks,
|
|
249
|
+
...(input.error_code ? { error_code: input.error_code } : {}),
|
|
250
|
+
log_hash: input.log_hash,
|
|
251
|
+
supersedes: input.supersedes,
|
|
252
|
+
data_class: "private",
|
|
253
|
+
authority: "none",
|
|
254
|
+
effects: "diagnostic_only",
|
|
255
|
+
writes: "evidence_receipt_only",
|
|
256
|
+
executed_at: opts.now ?? new Date().toISOString(),
|
|
257
|
+
};
|
|
258
|
+
const contentHash = canonicalHash(body);
|
|
259
|
+
return AdapterConformanceReceiptSchema.parse({ id: `g3conformance_${shortHash(contentHash)}`, content_hash: contentHash, ...body });
|
|
260
|
+
}
|
|
261
|
+
export function currentG3Plans(records) {
|
|
262
|
+
return currentAppendOnly(records.map((record) => G3PlanSchema.parse(record)), "G3 plan", () => "G3", true);
|
|
263
|
+
}
|
|
264
|
+
export function currentExperimentPreregistrations(records) {
|
|
265
|
+
return currentAppendOnly(records.map((record) => ExperimentPreregistrationSchema.parse(record)), "experiment preregistration", (record) => record.experiment);
|
|
266
|
+
}
|
|
267
|
+
export function currentProofReviewMeasurements(records) {
|
|
268
|
+
return currentAppendOnly(records.map((record) => ProofReviewMeasurementSchema.parse(record)), "proof review measurement", (record) => `${record.plan_id}:${record.policy_id}:${record.reviewer}`);
|
|
269
|
+
}
|
|
270
|
+
export function currentAdapterConformanceReceipts(records) {
|
|
271
|
+
return currentAppendOnly(records.map((record) => AdapterConformanceReceiptSchema.parse(record)), "adapter conformance receipt", (record) => record.plan_id);
|
|
272
|
+
}
|
|
273
|
+
function median(values) {
|
|
274
|
+
if (!values.length)
|
|
275
|
+
return null;
|
|
276
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
277
|
+
const middle = Math.floor(sorted.length / 2);
|
|
278
|
+
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
279
|
+
}
|
|
280
|
+
function overallBand(bands) {
|
|
281
|
+
if (bands.includes("red"))
|
|
282
|
+
return "red";
|
|
283
|
+
if (bands.includes("unmeasured"))
|
|
284
|
+
return "unmeasured";
|
|
285
|
+
if (bands.includes("yellow"))
|
|
286
|
+
return "yellow";
|
|
287
|
+
return "green";
|
|
288
|
+
}
|
|
289
|
+
export function scoreG3Readiness(input) {
|
|
290
|
+
const blockers = [];
|
|
291
|
+
if (!input.manifest)
|
|
292
|
+
blockers.push("No current private G3 evidence plan exists; a human must select the exact G2 policies, three or more clients, and preregistrations.");
|
|
293
|
+
if (input.g2_readiness.recommendation !== "eligible_for_human_g2_signoff")
|
|
294
|
+
blockers.push(`G2 evidence is not currently eligible (${input.g2_readiness.id}).`);
|
|
295
|
+
if (input.manifest && input.manifest.g2_readiness_hash !== input.g2_readiness.content_hash)
|
|
296
|
+
blockers.push("G3 plan does not bind the exact current G2 readiness receipt.");
|
|
297
|
+
if (input.manifest && input.policy_evidence.length !== input.manifest.policy_ids.length)
|
|
298
|
+
blockers.push(`Selected G3 policy evidence is incomplete (${input.policy_evidence.length}/${input.manifest.policy_ids.length}).`);
|
|
299
|
+
for (const evidence of input.policy_evidence)
|
|
300
|
+
for (const reason of evidence.reasons)
|
|
301
|
+
blockers.push(`${evidence.policy_id}: ${reason}`);
|
|
302
|
+
for (const experiment of input.experiments)
|
|
303
|
+
for (const reason of experiment.reasons)
|
|
304
|
+
blockers.push(`${experiment.experiment}: ${reason}`);
|
|
305
|
+
if (input.experiments.length !== G3_REQUIRED_EXPERIMENTS.length)
|
|
306
|
+
blockers.push(`Experiment evidence is incomplete (${input.experiments.length}/${G3_REQUIRED_EXPERIMENTS.length}).`);
|
|
307
|
+
if (!input.conformance)
|
|
308
|
+
blockers.push("The exact current G3 plan has no adapter-conformance receipt.");
|
|
309
|
+
else {
|
|
310
|
+
if (input.conformance.result !== "passed")
|
|
311
|
+
blockers.push(`Adapter conformance result is ${input.conformance.result}.`);
|
|
312
|
+
if (input.manifest && input.conformance.plan_id !== input.manifest.id)
|
|
313
|
+
blockers.push("Adapter conformance is not bound to the current G3 plan.");
|
|
314
|
+
if (input.manifest && canonicalHash([...input.conformance.clients].sort()) !== canonicalHash([...input.manifest.clients].sort()))
|
|
315
|
+
blockers.push("Adapter conformance client set differs from the human-selected G3 client set.");
|
|
316
|
+
}
|
|
317
|
+
const activeBlocking = [...new Set(input.active_blocking_policy_ids)].sort();
|
|
318
|
+
if (activeBlocking.length)
|
|
319
|
+
blockers.push(`Blocking behavior must remain disabled for G3; active blocking policies: ${activeBlocking.join(", ")}.`);
|
|
320
|
+
const durations = input.policy_evidence.flatMap((item) => item.review_duration_ms == null ? [] : [item.review_duration_ms]);
|
|
321
|
+
const medianMs = median(durations);
|
|
322
|
+
const reviewBand = medianMs == null ? "unmeasured" : medianMs <= 300_000 ? "green" : medianMs <= 900_000 ? "yellow" : "red";
|
|
323
|
+
const comprehension = input.policy_evidence.flatMap((item) => item.comprehension_correct == null ? [] : [item.comprehension_correct]);
|
|
324
|
+
const comprehensionCorrect = comprehension.filter(Boolean).length;
|
|
325
|
+
const comprehensionRate = comprehension.length ? comprehensionCorrect / comprehension.length : null;
|
|
326
|
+
const comprehensionBand = comprehensionRate == null ? "unmeasured" : comprehensionRate === 1 ? "green" : "red";
|
|
327
|
+
const agreement = input.conformance?.verdict_agreement ?? null;
|
|
328
|
+
const agreementBand = agreement == null ? "unmeasured" : agreement === 1 ? "green" : agreement >= 0.99 ? "yellow" : "red";
|
|
329
|
+
const currentProofs = input.policy_evidence.filter((item) => item.proof_current).length;
|
|
330
|
+
const proofRate = input.policy_evidence.length ? currentProofs / input.policy_evidence.length : null;
|
|
331
|
+
const proofBand = proofRate == null ? "unmeasured" : proofRate >= 0.95 ? "green" : proofRate >= 0.85 ? "yellow" : "red";
|
|
332
|
+
const leaks = input.conformance?.confirmed_private_leaks ?? null;
|
|
333
|
+
const privacyBand = leaks == null ? "unmeasured" : leaks === 0 ? "green" : "red";
|
|
334
|
+
const overall = overallBand([reviewBand, comprehensionBand, agreementBand, proofBand, privacyBand]);
|
|
335
|
+
if (reviewBand === "red")
|
|
336
|
+
blockers.push("Median proof-card review burden exceeds the 15-minute red threshold.");
|
|
337
|
+
if (reviewBand === "unmeasured")
|
|
338
|
+
blockers.push("Proof-card review burden is not measured for the selected policy set.");
|
|
339
|
+
if (comprehensionBand === "red")
|
|
340
|
+
blockers.push("At least one proof-card comprehension measurement is incorrect.");
|
|
341
|
+
if (comprehensionBand === "unmeasured")
|
|
342
|
+
blockers.push("Proof-card comprehension is not measured for the selected policy set.");
|
|
343
|
+
if (agreementBand === "red" || agreementBand === "unmeasured")
|
|
344
|
+
blockers.push("Required-client verdict agreement is not measured at the G3 threshold.");
|
|
345
|
+
if (proofBand === "red" || proofBand === "unmeasured")
|
|
346
|
+
blockers.push("Current-proof coverage is below the G3 scorecard threshold.");
|
|
347
|
+
if (privacyBand !== "green")
|
|
348
|
+
blockers.push("The privacy red line is not cleared by an exact zero-leak conformance result.");
|
|
349
|
+
if (overall === "red" || overall === "unmeasured")
|
|
350
|
+
blockers.push(`G3 scorecard is ${overall}; only green/yellow is eligible.`);
|
|
351
|
+
const uniqueBlockers = [...new Set(blockers)];
|
|
352
|
+
const body = {
|
|
353
|
+
gate: "G3",
|
|
354
|
+
manifest: input.manifest,
|
|
355
|
+
g2_readiness: { id: input.g2_readiness.id, content_hash: input.g2_readiness.content_hash, recommendation: input.g2_readiness.recommendation },
|
|
356
|
+
policy_evidence: [...input.policy_evidence].sort((a, b) => a.policy_id.localeCompare(b.policy_id)),
|
|
357
|
+
experiments: [...input.experiments].sort((a, b) => a.experiment.localeCompare(b.experiment)),
|
|
358
|
+
conformance: input.conformance,
|
|
359
|
+
scorecard: {
|
|
360
|
+
median_review_burden_ms: { value: medianMs, band: reviewBand },
|
|
361
|
+
proof_comprehension: { correct: comprehensionCorrect, total: comprehension.length, rate: comprehensionRate, band: comprehensionBand },
|
|
362
|
+
required_client_verdict_agreement: { value: agreement, band: agreementBand },
|
|
363
|
+
current_proof_rate: { current: currentProofs, selected: input.policy_evidence.length, value: proofRate, band: proofBand },
|
|
364
|
+
confirmed_private_leaks: { value: leaks, band: privacyBand },
|
|
365
|
+
overall,
|
|
366
|
+
},
|
|
367
|
+
active_blocking_policy_ids: activeBlocking,
|
|
368
|
+
blockers: uniqueBlockers,
|
|
369
|
+
recommendation: uniqueBlockers.length ? "not_ready" : "eligible_for_human_g3_signoff",
|
|
370
|
+
authority: "none",
|
|
371
|
+
g3_passed: false,
|
|
372
|
+
};
|
|
373
|
+
const contentHash = canonicalHash(body);
|
|
374
|
+
return { id: `g3readiness_${shortHash(contentHash)}`, content_hash: contentHash, ...body };
|
|
375
|
+
}
|
|
376
|
+
/** Private-only append-only storage for G3 plans, preregistrations, measurements, and conformance. */
|
|
377
|
+
export class G3EvidenceRepository {
|
|
378
|
+
store;
|
|
379
|
+
constructor(store) {
|
|
380
|
+
this.store = store;
|
|
381
|
+
}
|
|
382
|
+
requirePrivate() {
|
|
383
|
+
if (!this.store.privateDir)
|
|
384
|
+
throw new Error("No private Hunch overlay is configured; refusing to write G3 evidence.");
|
|
385
|
+
return this.store.privateDir;
|
|
386
|
+
}
|
|
387
|
+
listPlans() {
|
|
388
|
+
const records = loadPrivateRecords(this.store.privateDir ? join(this.store.privateDir, "gates") : undefined, "g3plan_", (raw) => {
|
|
389
|
+
const parsed = G3PlanSchema.parse(raw);
|
|
390
|
+
if (parsed.content_hash !== g3PlanContentHash(parsed) || parsed.id !== `g3plan_${shortHash(parsed.content_hash)}`)
|
|
391
|
+
throw new Error(`G3 plan ${parsed.id} content hash mismatch`);
|
|
392
|
+
return parsed;
|
|
393
|
+
}, "gates");
|
|
394
|
+
currentG3Plans(records);
|
|
395
|
+
return records.sort((a, b) => a.id.localeCompare(b.id));
|
|
396
|
+
}
|
|
397
|
+
currentPlan() { return currentG3Plans(this.listPlans())[0] ?? null; }
|
|
398
|
+
putPlan(plan) {
|
|
399
|
+
const parsed = G3PlanSchema.parse(plan);
|
|
400
|
+
if (parsed.content_hash !== g3PlanContentHash(parsed) || parsed.id !== `g3plan_${shortHash(parsed.content_hash)}`)
|
|
401
|
+
throw new Error(`G3 plan ${parsed.id} content hash mismatch`);
|
|
402
|
+
const records = this.listPlans();
|
|
403
|
+
const existing = records.find((record) => record.id === parsed.id);
|
|
404
|
+
if (existing)
|
|
405
|
+
return existing;
|
|
406
|
+
const current = currentG3Plans(records)[0];
|
|
407
|
+
if (current && parsed.supersedes !== current.id)
|
|
408
|
+
throw new Error(`G3 plan ${current.id} is current; pass supersedes:${current.id} to append a correction`);
|
|
409
|
+
if (!current && parsed.supersedes)
|
|
410
|
+
throw new Error(`G3 plan ${parsed.id} supersedes no current plan`);
|
|
411
|
+
currentG3Plans([...records, parsed]);
|
|
412
|
+
const dir = join(this.requirePrivate(), "gates");
|
|
413
|
+
mkdirSync(dir, { recursive: true });
|
|
414
|
+
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
415
|
+
return parsed;
|
|
416
|
+
}
|
|
417
|
+
listExperiments() {
|
|
418
|
+
const records = loadPrivateRecords(this.store.privateDir ? join(this.store.privateDir, "experiments") : undefined, "expreg_", (raw) => {
|
|
419
|
+
const parsed = ExperimentPreregistrationSchema.parse(raw);
|
|
420
|
+
if (parsed.content_hash !== experimentPreregistrationContentHash(parsed) || parsed.id !== `expreg_${shortHash(parsed.content_hash)}`)
|
|
421
|
+
throw new Error(`experiment preregistration ${parsed.id} content hash mismatch`);
|
|
422
|
+
return parsed;
|
|
423
|
+
}, "experiments");
|
|
424
|
+
currentExperimentPreregistrations(records);
|
|
425
|
+
return records.sort((a, b) => a.id.localeCompare(b.id));
|
|
426
|
+
}
|
|
427
|
+
currentExperiments() { return currentExperimentPreregistrations(this.listExperiments()); }
|
|
428
|
+
putExperiment(spec) {
|
|
429
|
+
const parsed = ExperimentPreregistrationSchema.parse(spec);
|
|
430
|
+
if (parsed.content_hash !== experimentPreregistrationContentHash(parsed) || parsed.id !== `expreg_${shortHash(parsed.content_hash)}`)
|
|
431
|
+
throw new Error(`experiment preregistration ${parsed.id} content hash mismatch`);
|
|
432
|
+
const records = this.listExperiments();
|
|
433
|
+
const existing = records.find((record) => record.id === parsed.id);
|
|
434
|
+
if (existing)
|
|
435
|
+
return existing;
|
|
436
|
+
const current = currentExperimentPreregistrations(records).find((record) => record.experiment === parsed.experiment);
|
|
437
|
+
if (current && parsed.supersedes !== current.id)
|
|
438
|
+
throw new Error(`${parsed.experiment} preregistration ${current.id} is current; pass supersedes:${current.id} to append a correction`);
|
|
439
|
+
if (!current && parsed.supersedes)
|
|
440
|
+
throw new Error(`${parsed.experiment} preregistration ${parsed.id} supersedes no current record`);
|
|
441
|
+
if (current && parsed.revision !== current.revision + 1)
|
|
442
|
+
throw new Error(`${parsed.experiment} correction revision must be ${current.revision + 1}`);
|
|
443
|
+
currentExperimentPreregistrations([...records, parsed]);
|
|
444
|
+
const dir = join(this.requirePrivate(), "experiments");
|
|
445
|
+
mkdirSync(dir, { recursive: true });
|
|
446
|
+
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
447
|
+
return parsed;
|
|
448
|
+
}
|
|
449
|
+
listReviews() {
|
|
450
|
+
const records = loadPrivateRecords(this.store.privateDir ? join(this.store.privateDir, "proof-reviews") : undefined, "proofreview_", (raw) => {
|
|
451
|
+
const parsed = ProofReviewMeasurementSchema.parse(raw);
|
|
452
|
+
if (parsed.content_hash !== proofReviewMeasurementContentHash(parsed) || parsed.id !== `proofreview_${shortHash(parsed.content_hash)}`)
|
|
453
|
+
throw new Error(`proof review ${parsed.id} content hash mismatch`);
|
|
454
|
+
return parsed;
|
|
455
|
+
}, "proof-reviews");
|
|
456
|
+
currentProofReviewMeasurements(records);
|
|
457
|
+
return records.sort((a, b) => a.id.localeCompare(b.id));
|
|
458
|
+
}
|
|
459
|
+
currentReviews() { return currentProofReviewMeasurements(this.listReviews()); }
|
|
460
|
+
putReview(review) {
|
|
461
|
+
const parsed = ProofReviewMeasurementSchema.parse(review);
|
|
462
|
+
if (parsed.content_hash !== proofReviewMeasurementContentHash(parsed) || parsed.id !== `proofreview_${shortHash(parsed.content_hash)}`)
|
|
463
|
+
throw new Error(`proof review ${parsed.id} content hash mismatch`);
|
|
464
|
+
const records = this.listReviews();
|
|
465
|
+
const existing = records.find((record) => record.id === parsed.id);
|
|
466
|
+
if (existing)
|
|
467
|
+
return existing;
|
|
468
|
+
const identity = `${parsed.plan_id}:${parsed.policy_id}:${parsed.reviewer}`;
|
|
469
|
+
const current = currentProofReviewMeasurements(records).find((record) => `${record.plan_id}:${record.policy_id}:${record.reviewer}` === identity);
|
|
470
|
+
if (current && parsed.supersedes !== current.id)
|
|
471
|
+
throw new Error(`proof review ${current.id} is current for this plan, policy, and reviewer; pass supersedes:${current.id} to append a correction`);
|
|
472
|
+
if (!current && parsed.supersedes)
|
|
473
|
+
throw new Error(`proof review ${parsed.id} supersedes no current measurement`);
|
|
474
|
+
currentProofReviewMeasurements([...records, parsed]);
|
|
475
|
+
const dir = join(this.requirePrivate(), "proof-reviews");
|
|
476
|
+
mkdirSync(dir, { recursive: true });
|
|
477
|
+
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
478
|
+
return parsed;
|
|
479
|
+
}
|
|
480
|
+
listConformance() {
|
|
481
|
+
const records = loadPrivateRecords(this.store.privateDir ? join(this.store.privateDir, "adapter-conformance") : undefined, "g3conformance_", (raw) => {
|
|
482
|
+
const parsed = AdapterConformanceReceiptSchema.parse(raw);
|
|
483
|
+
if (parsed.content_hash !== adapterConformanceContentHash(parsed) || parsed.id !== `g3conformance_${shortHash(parsed.content_hash)}`)
|
|
484
|
+
throw new Error(`adapter conformance ${parsed.id} content hash mismatch`);
|
|
485
|
+
return parsed;
|
|
486
|
+
}, "adapter-conformance");
|
|
487
|
+
currentAdapterConformanceReceipts(records);
|
|
488
|
+
return records.sort((a, b) => a.id.localeCompare(b.id));
|
|
489
|
+
}
|
|
490
|
+
currentConformance() { return currentAdapterConformanceReceipts(this.listConformance()); }
|
|
491
|
+
putConformance(receipt) {
|
|
492
|
+
const parsed = AdapterConformanceReceiptSchema.parse(receipt);
|
|
493
|
+
if (parsed.content_hash !== adapterConformanceContentHash(parsed) || parsed.id !== `g3conformance_${shortHash(parsed.content_hash)}`)
|
|
494
|
+
throw new Error(`adapter conformance ${parsed.id} content hash mismatch`);
|
|
495
|
+
const records = this.listConformance();
|
|
496
|
+
const existing = records.find((record) => record.id === parsed.id);
|
|
497
|
+
if (existing)
|
|
498
|
+
return existing;
|
|
499
|
+
const current = currentAdapterConformanceReceipts(records).find((record) => record.plan_id === parsed.plan_id);
|
|
500
|
+
if (current && parsed.supersedes !== current.id)
|
|
501
|
+
throw new Error(`adapter conformance ${current.id} is current for plan ${parsed.plan_id}; pass supersedes:${current.id} to append a correction`);
|
|
502
|
+
if (!current && parsed.supersedes)
|
|
503
|
+
throw new Error(`adapter conformance ${parsed.id} supersedes no current receipt`);
|
|
504
|
+
currentAdapterConformanceReceipts([...records, parsed]);
|
|
505
|
+
const dir = join(this.requirePrivate(), "adapter-conformance");
|
|
506
|
+
mkdirSync(dir, { recursive: true });
|
|
507
|
+
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
508
|
+
return parsed;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
//# sourceMappingURL=g3.js.map
|