@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,533 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { writeFileAtomic } from "../core/io.js";
|
|
4
|
+
import { shortHash } from "../core/ids.js";
|
|
5
|
+
import { policySemanticHash, proofPlanContentHash } from "./canonical.js";
|
|
6
|
+
import { proofCorpusContentHash } from "./corpus.js";
|
|
7
|
+
import { currentHistoryDispositions, historyDispositionContentHash, historyDispositionJudgmentHash } from "./disposition.js";
|
|
8
|
+
import { assertCompositionBinding, compositionDescendants, policyProofHash } from "./composition.js";
|
|
9
|
+
import { currentShadowDispositions, policyEvaluationContentHash, shadowDispositionContentHash, shadowDispositionJudgmentHash, shadowEvaluationContentHash, shadowEvaluationIdentityHash, } from "./shadow.js";
|
|
10
|
+
import { HistoryDispositionSchema, ProofCorpusSchema, PolicyProofSchema, ProofPlanSchema, PolicySpecSchema, ShadowRecordSchema, EvidenceEventSchema, } from "./schema.js";
|
|
11
|
+
const encode = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
12
|
+
function loadRecords(dir, parse, label) {
|
|
13
|
+
if (!existsSync(dir))
|
|
14
|
+
return [];
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const name of readdirSync(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
17
|
+
try {
|
|
18
|
+
out.push(parse(JSON.parse(readFileSync(join(dir, name), "utf8"))));
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
// A policy store can control CI. Skipping a corrupt record would turn an
|
|
22
|
+
// enforcement failure into a false pass, so fail visibly instead.
|
|
23
|
+
throw new Error(`invalid ${label}/${name}: ${e.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
/** Git-native policy/proof source of truth. Kept separate from the legacy entity
|
|
29
|
+
* registry for the first slice so existing schema-v2 repositories and SQLite
|
|
30
|
+
* compatibility remain unchanged. */
|
|
31
|
+
export class PolicyRepository {
|
|
32
|
+
store;
|
|
33
|
+
publicHome;
|
|
34
|
+
privateHome;
|
|
35
|
+
constructor(root, store) {
|
|
36
|
+
this.store = store;
|
|
37
|
+
this.publicHome = join(root, ".hunch");
|
|
38
|
+
this.privateHome = store.privateDir;
|
|
39
|
+
}
|
|
40
|
+
dir(home, kind) {
|
|
41
|
+
const base = home === "private" ? this.privateHome : this.publicHome;
|
|
42
|
+
if (!base)
|
|
43
|
+
throw new Error("No private Hunch overlay is configured; refusing to write a private policy.");
|
|
44
|
+
return join(base, kind);
|
|
45
|
+
}
|
|
46
|
+
policiesIn(home) {
|
|
47
|
+
if (home === "private" && !this.privateHome)
|
|
48
|
+
return [];
|
|
49
|
+
return loadRecords(this.dir(home, "policies"), (v) => PolicySpecSchema.parse(v), "policies");
|
|
50
|
+
}
|
|
51
|
+
proofsIn(home) {
|
|
52
|
+
if (home === "private" && !this.privateHome)
|
|
53
|
+
return [];
|
|
54
|
+
return loadRecords(this.dir(home, "proofs"), (v) => PolicyProofSchema.parse(v), "proofs");
|
|
55
|
+
}
|
|
56
|
+
evidenceIn(home) {
|
|
57
|
+
if (home === "private" && !this.privateHome)
|
|
58
|
+
return [];
|
|
59
|
+
return loadRecords(this.dir(home, "evidence"), (v) => EvidenceEventSchema.parse(v), "evidence");
|
|
60
|
+
}
|
|
61
|
+
plansIn(home) {
|
|
62
|
+
if (home === "private" && !this.privateHome)
|
|
63
|
+
return [];
|
|
64
|
+
return loadRecords(this.dir(home, "plans"), validatePlan, "plans");
|
|
65
|
+
}
|
|
66
|
+
corporaIn(home) {
|
|
67
|
+
if (home === "private" && !this.privateHome)
|
|
68
|
+
return [];
|
|
69
|
+
return loadRecords(this.dir(home, "corpora"), validateCorpus, "corpora");
|
|
70
|
+
}
|
|
71
|
+
dispositionsIn(home) {
|
|
72
|
+
if (home === "private" && !this.privateHome)
|
|
73
|
+
return [];
|
|
74
|
+
return loadRecords(this.dir(home, "dispositions"), validateDisposition, "dispositions");
|
|
75
|
+
}
|
|
76
|
+
shadowIn(home) {
|
|
77
|
+
if (home === "private" && !this.privateHome)
|
|
78
|
+
return [];
|
|
79
|
+
return loadRecords(this.dir(home, "shadow"), validateShadowRecord, "shadow");
|
|
80
|
+
}
|
|
81
|
+
listPolicies(opts = {}) {
|
|
82
|
+
if (opts.publicOnly && opts.privateOnly)
|
|
83
|
+
throw new Error("choose only one of publicOnly or privateOnly");
|
|
84
|
+
if (opts.privateOnly)
|
|
85
|
+
return this.policiesIn("private").sort((a, b) => a.id.localeCompare(b.id));
|
|
86
|
+
const byId = new Map(this.policiesIn("public").map((p) => [p.id, p]));
|
|
87
|
+
if (!opts.publicOnly)
|
|
88
|
+
for (const p of this.policiesIn("private"))
|
|
89
|
+
byId.set(p.id, p);
|
|
90
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
91
|
+
}
|
|
92
|
+
getPolicy(id, opts = {}) {
|
|
93
|
+
return this.listPolicies(opts).find((p) => p.id === id);
|
|
94
|
+
}
|
|
95
|
+
getProof(id, opts = {}) {
|
|
96
|
+
if (opts.publicOnly && opts.privateOnly)
|
|
97
|
+
throw new Error("choose only one of publicOnly or privateOnly");
|
|
98
|
+
if (opts.privateOnly)
|
|
99
|
+
return this.proofsIn("private").find((proof) => proof.id === id);
|
|
100
|
+
const byId = new Map(this.proofsIn("public").map((p) => [p.id, p]));
|
|
101
|
+
if (!opts.publicOnly)
|
|
102
|
+
for (const p of this.proofsIn("private"))
|
|
103
|
+
byId.set(p.id, p);
|
|
104
|
+
return byId.get(id);
|
|
105
|
+
}
|
|
106
|
+
listProofs(opts = {}) {
|
|
107
|
+
if (opts.publicOnly && opts.privateOnly)
|
|
108
|
+
throw new Error("choose only one of publicOnly or privateOnly");
|
|
109
|
+
if (opts.privateOnly)
|
|
110
|
+
return this.proofsIn("private").sort((a, b) => a.id.localeCompare(b.id));
|
|
111
|
+
const byId = new Map(this.proofsIn("public").map((proof) => [proof.id, proof]));
|
|
112
|
+
if (!opts.publicOnly)
|
|
113
|
+
for (const proof of this.proofsIn("private"))
|
|
114
|
+
byId.set(proof.id, proof);
|
|
115
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
116
|
+
}
|
|
117
|
+
listPlans(opts = {}) {
|
|
118
|
+
if (opts.publicOnly && opts.privateOnly)
|
|
119
|
+
throw new Error("choose only one of publicOnly or privateOnly");
|
|
120
|
+
if (opts.privateOnly)
|
|
121
|
+
return this.plansIn("private").sort((a, b) => a.id.localeCompare(b.id));
|
|
122
|
+
const byId = new Map(this.plansIn("public").map((p) => [p.id, p]));
|
|
123
|
+
if (!opts.publicOnly)
|
|
124
|
+
for (const p of this.plansIn("private"))
|
|
125
|
+
byId.set(p.id, p);
|
|
126
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
127
|
+
}
|
|
128
|
+
getPlan(id, opts = {}) {
|
|
129
|
+
return this.listPlans(opts).find((p) => p.id === id);
|
|
130
|
+
}
|
|
131
|
+
getCorpus(policyId, opts = {}) {
|
|
132
|
+
if (opts.publicOnly && opts.privateOnly)
|
|
133
|
+
throw new Error("choose only one of publicOnly or privateOnly");
|
|
134
|
+
if (opts.privateOnly)
|
|
135
|
+
return this.corporaIn("private").find((corpus) => corpus.policy_id === policyId);
|
|
136
|
+
const publicCorpus = this.corporaIn("public").find((corpus) => corpus.policy_id === policyId);
|
|
137
|
+
if (opts.publicOnly)
|
|
138
|
+
return publicCorpus;
|
|
139
|
+
return this.corporaIn("private").find((corpus) => corpus.policy_id === policyId) ?? publicCorpus;
|
|
140
|
+
}
|
|
141
|
+
listCorpora(opts = {}) {
|
|
142
|
+
if (opts.publicOnly && opts.privateOnly)
|
|
143
|
+
throw new Error("choose only one of publicOnly or privateOnly");
|
|
144
|
+
if (opts.privateOnly)
|
|
145
|
+
return this.corporaIn("private").sort((a, b) => a.policy_id.localeCompare(b.policy_id));
|
|
146
|
+
const byPolicy = new Map(this.corporaIn("public").map((corpus) => [corpus.policy_id, corpus]));
|
|
147
|
+
if (!opts.publicOnly)
|
|
148
|
+
for (const corpus of this.corporaIn("private"))
|
|
149
|
+
byPolicy.set(corpus.policy_id, corpus);
|
|
150
|
+
return [...byPolicy.values()].sort((a, b) => a.policy_id.localeCompare(b.policy_id));
|
|
151
|
+
}
|
|
152
|
+
listEvidence(opts = {}) {
|
|
153
|
+
if (opts.publicOnly && opts.privateOnly)
|
|
154
|
+
throw new Error("choose only one of publicOnly or privateOnly");
|
|
155
|
+
if (opts.privateOnly)
|
|
156
|
+
return this.evidenceIn("private").sort((a, b) => a.id.localeCompare(b.id));
|
|
157
|
+
const byId = new Map(this.evidenceIn("public").map((e) => [e.id, e]));
|
|
158
|
+
if (!opts.publicOnly)
|
|
159
|
+
for (const e of this.evidenceIn("private"))
|
|
160
|
+
byId.set(e.id, e);
|
|
161
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
162
|
+
}
|
|
163
|
+
getEvidence(id, opts = {}) {
|
|
164
|
+
return this.listEvidence(opts).find((e) => e.id === id);
|
|
165
|
+
}
|
|
166
|
+
listDispositions(opts = {}) {
|
|
167
|
+
if (opts.publicOnly && opts.privateOnly)
|
|
168
|
+
throw new Error("choose only one of publicOnly or privateOnly");
|
|
169
|
+
if (opts.privateOnly) {
|
|
170
|
+
const records = this.dispositionsIn("private");
|
|
171
|
+
currentHistoryDispositions(records);
|
|
172
|
+
return records.sort((left, right) => left.id.localeCompare(right.id));
|
|
173
|
+
}
|
|
174
|
+
const byId = new Map(this.dispositionsIn("public").map((record) => [record.id, record]));
|
|
175
|
+
if (!opts.publicOnly)
|
|
176
|
+
for (const record of this.dispositionsIn("private"))
|
|
177
|
+
byId.set(record.id, record);
|
|
178
|
+
const records = [...byId.values()];
|
|
179
|
+
currentHistoryDispositions(records);
|
|
180
|
+
return records.sort((left, right) => left.id.localeCompare(right.id));
|
|
181
|
+
}
|
|
182
|
+
listShadowEvaluations(opts = {}) {
|
|
183
|
+
if (opts.publicOnly && opts.privateOnly)
|
|
184
|
+
throw new Error("choose only one of publicOnly or privateOnly");
|
|
185
|
+
const records = opts.privateOnly
|
|
186
|
+
? this.shadowIn("private")
|
|
187
|
+
: [...this.shadowIn("public"), ...(opts.publicOnly ? [] : this.shadowIn("private"))];
|
|
188
|
+
const byId = new Map(records.filter((record) => record.record_type === "evaluation").map((record) => [record.id, record]));
|
|
189
|
+
return [...byId.values()].sort((left, right) => left.observed_at.localeCompare(right.observed_at) || left.id.localeCompare(right.id));
|
|
190
|
+
}
|
|
191
|
+
listShadowDispositions(opts = {}) {
|
|
192
|
+
if (opts.publicOnly && opts.privateOnly)
|
|
193
|
+
throw new Error("choose only one of publicOnly or privateOnly");
|
|
194
|
+
const records = opts.privateOnly
|
|
195
|
+
? this.shadowIn("private")
|
|
196
|
+
: [...this.shadowIn("public"), ...(opts.publicOnly ? [] : this.shadowIn("private"))];
|
|
197
|
+
const byId = new Map(records.filter((record) => record.record_type === "disposition").map((record) => [record.id, record]));
|
|
198
|
+
const dispositions = [...byId.values()];
|
|
199
|
+
currentShadowDispositions(dispositions);
|
|
200
|
+
return dispositions.sort((left, right) => left.id.localeCompare(right.id));
|
|
201
|
+
}
|
|
202
|
+
homeOfPolicy(id) {
|
|
203
|
+
if (this.privateHome && existsSync(join(this.dir("private", "policies"), `${id}.json`)))
|
|
204
|
+
return "private";
|
|
205
|
+
if (existsSync(join(this.dir("public", "policies"), `${id}.json`)))
|
|
206
|
+
return "public";
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
putPolicy(policy, opts = {}) {
|
|
210
|
+
const parsed = PolicySpecSchema.parse(policy);
|
|
211
|
+
const existing = this.homeOfPolicy(parsed.id);
|
|
212
|
+
if (existing === "public" && parsed.data_class !== "public" && !opts.private) {
|
|
213
|
+
throw new Error(`refusing to write ${parsed.data_class} policy ${parsed.id} into its existing public home; migrate it to the private overlay first`);
|
|
214
|
+
}
|
|
215
|
+
const home = opts.private ? "private" : existing ?? (parsed.data_class !== "public" || this.store.unified ? "private" : "public");
|
|
216
|
+
const dir = this.dir(home, "policies");
|
|
217
|
+
mkdirSync(dir, { recursive: true });
|
|
218
|
+
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
219
|
+
return parsed;
|
|
220
|
+
}
|
|
221
|
+
putProof(proof, policyId) {
|
|
222
|
+
const parsed = PolicyProofSchema.parse(proof);
|
|
223
|
+
const home = this.homeOfPolicy(policyId) ?? (parsed.data_class === "public" && !this.store.unified ? "public" : "private");
|
|
224
|
+
const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
|
|
225
|
+
const policy = this.getPolicy(policyId, homeOpts);
|
|
226
|
+
if (policy) {
|
|
227
|
+
const composition = compositionDescendants(policy, this.listPolicies(homeOpts));
|
|
228
|
+
assertCompositionBinding(policy, composition, parsed.composition);
|
|
229
|
+
if (parsed.policy_hash !== policyProofHash(policy, composition))
|
|
230
|
+
throw new Error(`composite proof ${parsed.id} policy hash mismatch`);
|
|
231
|
+
if (composition.length) {
|
|
232
|
+
const plan = this.listPlans(homeOpts).find((candidate) => candidate.content_hash === parsed.plan_hash);
|
|
233
|
+
if (!plan || plan.policy_candidate_hash !== parsed.policy_hash)
|
|
234
|
+
throw new Error(`composite proof ${parsed.id} has no exact bound proof plan`);
|
|
235
|
+
assertCompositionBinding(policy, composition, plan.composition);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const dir = this.dir(home, "proofs");
|
|
239
|
+
mkdirSync(dir, { recursive: true });
|
|
240
|
+
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
241
|
+
return parsed;
|
|
242
|
+
}
|
|
243
|
+
putPlan(plan, policyId, opts = {}) {
|
|
244
|
+
const parsed = validatePlan(plan);
|
|
245
|
+
if (opts.private && opts.public)
|
|
246
|
+
throw new Error("choose only one proof-plan home");
|
|
247
|
+
const home = opts.private
|
|
248
|
+
? "private"
|
|
249
|
+
: opts.public
|
|
250
|
+
? "public"
|
|
251
|
+
: this.homeOfPolicy(policyId) ?? (parsed.data_class === "public" && !this.store.unified ? "public" : "private");
|
|
252
|
+
const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
|
|
253
|
+
const policy = this.getPolicy(policyId, homeOpts);
|
|
254
|
+
if (policy) {
|
|
255
|
+
const composition = compositionDescendants(policy, this.listPolicies(homeOpts));
|
|
256
|
+
assertCompositionBinding(policy, composition, parsed.composition);
|
|
257
|
+
if (parsed.policy_candidate_hash !== policyProofHash(policy, composition))
|
|
258
|
+
throw new Error(`composite plan ${parsed.id} policy hash mismatch`);
|
|
259
|
+
}
|
|
260
|
+
const dir = this.dir(home, "plans");
|
|
261
|
+
mkdirSync(dir, { recursive: true });
|
|
262
|
+
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
263
|
+
return parsed;
|
|
264
|
+
}
|
|
265
|
+
putCorpus(corpus, policyId) {
|
|
266
|
+
const parsed = validateCorpus(corpus);
|
|
267
|
+
if (parsed.policy_id !== policyId)
|
|
268
|
+
throw new Error(`corpus ${parsed.id} does not belong to policy ${policyId}`);
|
|
269
|
+
const home = this.homeOfPolicy(policyId);
|
|
270
|
+
if (!home)
|
|
271
|
+
throw new Error(`cannot write corpus ${parsed.id}: policy ${policyId} has no exact storage home`);
|
|
272
|
+
const policy = this.getPolicy(policyId, home === "public" ? { publicOnly: true } : { privateOnly: true });
|
|
273
|
+
if (!policy || parsed.data_class !== policy.data_class || parsed.policy_hash !== policySemanticHash(policy)) {
|
|
274
|
+
throw new Error(`corpus ${parsed.id} does not match policy ${policyId} semantics/data class`);
|
|
275
|
+
}
|
|
276
|
+
const dir = this.dir(home, "corpora");
|
|
277
|
+
mkdirSync(dir, { recursive: true });
|
|
278
|
+
writeFileAtomic(join(dir, `${policyId}.json`), encode(parsed));
|
|
279
|
+
return parsed;
|
|
280
|
+
}
|
|
281
|
+
putEvidence(event, opts = {}) {
|
|
282
|
+
const parsed = EvidenceEventSchema.parse(event);
|
|
283
|
+
const home = opts.private || parsed.data_class !== "public" || this.store.unified ? "private" : "public";
|
|
284
|
+
const dir = this.dir(home, "evidence");
|
|
285
|
+
mkdirSync(dir, { recursive: true });
|
|
286
|
+
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
287
|
+
return parsed;
|
|
288
|
+
}
|
|
289
|
+
putDisposition(disposition, policyId) {
|
|
290
|
+
const parsed = validateDisposition(disposition);
|
|
291
|
+
if (parsed.policy_id !== policyId)
|
|
292
|
+
throw new Error(`history disposition ${parsed.id} does not belong to policy ${policyId}`);
|
|
293
|
+
const home = this.homeOfPolicy(policyId);
|
|
294
|
+
if (!home)
|
|
295
|
+
throw new Error(`cannot write history disposition ${parsed.id}: policy ${policyId} has no exact storage home`);
|
|
296
|
+
const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
|
|
297
|
+
const policy = this.getPolicy(policyId, homeOpts);
|
|
298
|
+
const composition = policy ? compositionDescendants(policy, this.listPolicies(homeOpts)) : [];
|
|
299
|
+
if (!policy || policy.data_class !== parsed.data_class || policy.proof !== parsed.proof_id || policyProofHash(policy, composition) !== parsed.policy_hash) {
|
|
300
|
+
throw new Error(`history disposition ${parsed.id} does not match policy ${policyId} semantics/proof/data class`);
|
|
301
|
+
}
|
|
302
|
+
const proof = this.getProof(parsed.proof_id, homeOpts);
|
|
303
|
+
const receipt = proof?.replay_receipts.find((candidate) => candidate.leg === "accepted_history" && candidate.result === "violated" && candidate.commit === parsed.commit && candidate.deterministic_hash === parsed.receipt_hash);
|
|
304
|
+
if (!proof || proof.policy_hash !== parsed.policy_hash || proof.plan_hash !== parsed.plan_hash || proof.data_class !== parsed.data_class || !receipt) {
|
|
305
|
+
throw new Error(`history disposition ${parsed.id} does not match an exact violated accepted-history receipt in proof ${parsed.proof_id}`);
|
|
306
|
+
}
|
|
307
|
+
const records = this.listDispositions(homeOpts);
|
|
308
|
+
const existing = records.find((record) => record.id === parsed.id);
|
|
309
|
+
if (existing)
|
|
310
|
+
return existing;
|
|
311
|
+
const sameJudgment = records.find((record) => historyDispositionJudgmentHash(record) === historyDispositionJudgmentHash(parsed));
|
|
312
|
+
if (sameJudgment)
|
|
313
|
+
return sameJudgment;
|
|
314
|
+
const hitRecords = records.filter((record) => record.policy_id === policyId && record.proof_id === parsed.proof_id && record.commit === parsed.commit);
|
|
315
|
+
const current = currentHistoryDispositions(hitRecords)[0];
|
|
316
|
+
if (current && parsed.supersedes !== current.id) {
|
|
317
|
+
throw new Error(`history hit ${parsed.commit} already has current disposition ${current.id}; pass --supersedes ${current.id} to append a correction`);
|
|
318
|
+
}
|
|
319
|
+
if (!current && parsed.supersedes)
|
|
320
|
+
throw new Error(`history disposition ${parsed.id} supersedes no current disposition for this proof hit`);
|
|
321
|
+
currentHistoryDispositions([...records, parsed]);
|
|
322
|
+
const dir = this.dir(home, "dispositions");
|
|
323
|
+
mkdirSync(dir, { recursive: true });
|
|
324
|
+
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
325
|
+
return parsed;
|
|
326
|
+
}
|
|
327
|
+
putShadowEvaluation(evaluation, policyId) {
|
|
328
|
+
const parsed = validateShadowEvaluation(evaluation);
|
|
329
|
+
if (parsed.policy_id !== policyId)
|
|
330
|
+
throw new Error(`shadow evaluation ${parsed.id} does not belong to policy ${policyId}`);
|
|
331
|
+
const home = this.homeOfPolicy(policyId);
|
|
332
|
+
if (!home)
|
|
333
|
+
throw new Error(`cannot write shadow evaluation ${parsed.id}: policy ${policyId} has no exact storage home`);
|
|
334
|
+
const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
|
|
335
|
+
const policy = this.getPolicy(policyId, homeOpts);
|
|
336
|
+
const proof = policy?.proof ? this.getProof(policy.proof, homeOpts) : undefined;
|
|
337
|
+
const composition = policy ? compositionDescendants(policy, this.listPolicies(homeOpts)) : [];
|
|
338
|
+
if (!policy || !proof
|
|
339
|
+
|| policy.proof !== parsed.proof_id
|
|
340
|
+
|| proof.id !== parsed.proof_id
|
|
341
|
+
|| proof.plan_hash !== parsed.plan_hash
|
|
342
|
+
|| policyProofHash(policy, composition) !== parsed.policy_hash
|
|
343
|
+
|| proof.policy_hash !== parsed.policy_hash
|
|
344
|
+
|| policy.data_class !== parsed.data_class
|
|
345
|
+
|| parsed.evaluation.policy_id !== policyId
|
|
346
|
+
|| parsed.evaluation.evaluator.name !== proof.evaluator.name
|
|
347
|
+
|| parsed.evaluation.evaluator.version !== proof.evaluator.version
|
|
348
|
+
|| parsed.evaluation.deterministic_hash !== policyEvaluationContentHash(parsed.evaluation)) {
|
|
349
|
+
throw new Error(`shadow evaluation ${parsed.id} does not match the current policy/proof/evaluation binding`);
|
|
350
|
+
}
|
|
351
|
+
const existing = this.listShadowEvaluations(homeOpts).find((record) => shadowEvaluationIdentityHash(record) === shadowEvaluationIdentityHash(parsed));
|
|
352
|
+
if (existing)
|
|
353
|
+
return existing;
|
|
354
|
+
const dir = this.dir(home, "shadow");
|
|
355
|
+
mkdirSync(dir, { recursive: true });
|
|
356
|
+
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
357
|
+
return parsed;
|
|
358
|
+
}
|
|
359
|
+
putShadowDisposition(disposition, policyId) {
|
|
360
|
+
const parsed = validateShadowDisposition(disposition);
|
|
361
|
+
if (parsed.policy_id !== policyId)
|
|
362
|
+
throw new Error(`shadow disposition ${parsed.id} does not belong to policy ${policyId}`);
|
|
363
|
+
const home = this.homeOfPolicy(policyId);
|
|
364
|
+
if (!home)
|
|
365
|
+
throw new Error(`cannot write shadow disposition ${parsed.id}: policy ${policyId} has no exact storage home`);
|
|
366
|
+
const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
|
|
367
|
+
const evaluation = this.listShadowEvaluations(homeOpts).find((record) => record.id === parsed.shadow_id);
|
|
368
|
+
if (!evaluation
|
|
369
|
+
|| evaluation.evaluation.result !== "violated"
|
|
370
|
+
|| evaluation.policy_id !== parsed.policy_id
|
|
371
|
+
|| evaluation.proof_id !== parsed.proof_id
|
|
372
|
+
|| evaluation.policy_hash !== parsed.policy_hash
|
|
373
|
+
|| evaluation.evaluation.deterministic_hash !== parsed.evaluation_hash
|
|
374
|
+
|| evaluation.data_class !== parsed.data_class) {
|
|
375
|
+
throw new Error(`shadow disposition ${parsed.id} does not match an exact violated shadow evaluation`);
|
|
376
|
+
}
|
|
377
|
+
const records = this.listShadowDispositions(homeOpts);
|
|
378
|
+
const existing = records.find((record) => record.id === parsed.id);
|
|
379
|
+
if (existing)
|
|
380
|
+
return existing;
|
|
381
|
+
const sameJudgment = records.find((record) => shadowDispositionJudgmentHash(record) === shadowDispositionJudgmentHash(parsed));
|
|
382
|
+
if (sameJudgment)
|
|
383
|
+
return sameJudgment;
|
|
384
|
+
const evaluationRecords = records.filter((record) => record.shadow_id === parsed.shadow_id);
|
|
385
|
+
const current = currentShadowDispositions(evaluationRecords)[0];
|
|
386
|
+
if (current && parsed.supersedes !== current.id) {
|
|
387
|
+
throw new Error(`shadow evaluation ${parsed.shadow_id} already has current disposition ${current.id}; pass --supersedes ${current.id} to append a correction`);
|
|
388
|
+
}
|
|
389
|
+
if (!current && parsed.supersedes)
|
|
390
|
+
throw new Error(`shadow disposition ${parsed.id} supersedes no current disposition for this evaluation`);
|
|
391
|
+
currentShadowDispositions([...records, parsed]);
|
|
392
|
+
const dir = this.dir(home, "shadow");
|
|
393
|
+
mkdirSync(dir, { recursive: true });
|
|
394
|
+
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
395
|
+
return parsed;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function validatePlan(raw) {
|
|
399
|
+
const plan = ProofPlanSchema.parse(raw);
|
|
400
|
+
const hash = proofPlanContentHash(plan);
|
|
401
|
+
if (plan.content_hash !== hash)
|
|
402
|
+
throw new Error(`proof plan ${plan.id} content hash mismatch`);
|
|
403
|
+
if (plan.id !== `plan_${shortHash(hash)}`)
|
|
404
|
+
throw new Error(`proof plan ${plan.id} id does not match its canonical content`);
|
|
405
|
+
return plan;
|
|
406
|
+
}
|
|
407
|
+
function validateCorpus(raw) {
|
|
408
|
+
const corpus = ProofCorpusSchema.parse(raw);
|
|
409
|
+
const hash = proofCorpusContentHash(corpus);
|
|
410
|
+
if (corpus.content_hash !== hash)
|
|
411
|
+
throw new Error(`proof corpus ${corpus.id} content hash mismatch`);
|
|
412
|
+
if (corpus.id !== `corpus_${shortHash(hash)}`)
|
|
413
|
+
throw new Error(`proof corpus ${corpus.id} id does not match its canonical content`);
|
|
414
|
+
return corpus;
|
|
415
|
+
}
|
|
416
|
+
function validateDisposition(raw) {
|
|
417
|
+
const disposition = HistoryDispositionSchema.parse(raw);
|
|
418
|
+
const hash = historyDispositionContentHash(disposition);
|
|
419
|
+
if (disposition.content_hash !== hash)
|
|
420
|
+
throw new Error(`history disposition ${disposition.id} content hash mismatch`);
|
|
421
|
+
if (disposition.id !== `disp_${shortHash(hash)}`)
|
|
422
|
+
throw new Error(`history disposition ${disposition.id} id does not match its canonical content`);
|
|
423
|
+
return disposition;
|
|
424
|
+
}
|
|
425
|
+
function validateShadowRecord(raw) {
|
|
426
|
+
const record = ShadowRecordSchema.parse(raw);
|
|
427
|
+
return record.record_type === "evaluation" ? validateShadowEvaluation(record) : validateShadowDisposition(record);
|
|
428
|
+
}
|
|
429
|
+
function validateShadowEvaluation(raw) {
|
|
430
|
+
const record = ShadowRecordSchema.parse(raw);
|
|
431
|
+
if (record.record_type !== "evaluation")
|
|
432
|
+
throw new Error("expected a shadow evaluation record");
|
|
433
|
+
const hash = shadowEvaluationContentHash(record);
|
|
434
|
+
if (record.content_hash !== hash)
|
|
435
|
+
throw new Error(`shadow evaluation ${record.id} content hash mismatch`);
|
|
436
|
+
if (record.id !== `shadow_${shortHash(hash)}`)
|
|
437
|
+
throw new Error(`shadow evaluation ${record.id} id does not match its canonical content`);
|
|
438
|
+
if (record.evaluation.deterministic_hash !== policyEvaluationContentHash(record.evaluation))
|
|
439
|
+
throw new Error(`shadow evaluation ${record.id} receipt hash mismatch`);
|
|
440
|
+
return record;
|
|
441
|
+
}
|
|
442
|
+
function validateShadowDisposition(raw) {
|
|
443
|
+
const record = ShadowRecordSchema.parse(raw);
|
|
444
|
+
if (record.record_type !== "disposition")
|
|
445
|
+
throw new Error("expected a shadow disposition record");
|
|
446
|
+
const hash = shadowDispositionContentHash(record);
|
|
447
|
+
if (record.content_hash !== hash)
|
|
448
|
+
throw new Error(`shadow disposition ${record.id} content hash mismatch`);
|
|
449
|
+
if (record.id !== `sdisp_${shortHash(hash)}`)
|
|
450
|
+
throw new Error(`shadow disposition ${record.id} id does not match its canonical content`);
|
|
451
|
+
return record;
|
|
452
|
+
}
|
|
453
|
+
/** One-time private migration for the Constitution's standalone Git-native
|
|
454
|
+
* records. Copy/validate everything before deleting the public directories;
|
|
455
|
+
* private records win on an id collision. */
|
|
456
|
+
export function movePolicyArtifactsToPrivate(publicHunchDir, privateHunchDir) {
|
|
457
|
+
const counts = { policies: 0, proofs: 0, plans: 0, evidence: 0, corpora: 0, dispositions: 0, shadow: 0 };
|
|
458
|
+
const specs = [
|
|
459
|
+
{ kind: "policies", parse: (value) => PolicySpecSchema.parse(value) },
|
|
460
|
+
{ kind: "proofs", parse: (value) => PolicyProofSchema.parse(value) },
|
|
461
|
+
{ kind: "plans", parse: validatePlan },
|
|
462
|
+
{ kind: "evidence", parse: (value) => EvidenceEventSchema.parse(value) },
|
|
463
|
+
{ kind: "corpora", parse: validateCorpus },
|
|
464
|
+
{ kind: "dispositions", parse: validateDisposition },
|
|
465
|
+
{ kind: "shadow", parse: validateShadowRecord },
|
|
466
|
+
];
|
|
467
|
+
const staged = specs.map(({ kind, parse }) => {
|
|
468
|
+
const from = join(publicHunchDir, kind);
|
|
469
|
+
const to = join(privateHunchDir, kind);
|
|
470
|
+
const pub = existsSync(from) ? loadRecords(from, parse, kind) : [];
|
|
471
|
+
const keyFor = (record) => kind === "corpora" ? record.policy_id : record.id;
|
|
472
|
+
const priv = new Map(loadRecords(to, parse, kind).map((r) => [keyFor(r), r]));
|
|
473
|
+
return { kind, from, to, pub, priv, keyFor };
|
|
474
|
+
});
|
|
475
|
+
// All public and private records are parsed before the first write or delete.
|
|
476
|
+
// A corrupt late category therefore cannot partially migrate earlier ones.
|
|
477
|
+
const dispositions = staged.find((entry) => entry.kind === "dispositions");
|
|
478
|
+
if (dispositions) {
|
|
479
|
+
const union = new Map([...dispositions.pub, ...dispositions.priv.values()].map((record) => [record.id, record]));
|
|
480
|
+
currentHistoryDispositions([...union.values()]);
|
|
481
|
+
}
|
|
482
|
+
const shadow = staged.find((entry) => entry.kind === "shadow");
|
|
483
|
+
if (shadow) {
|
|
484
|
+
const union = new Map([...shadow.pub, ...shadow.priv.values()].map((record) => [record.id, record]));
|
|
485
|
+
const shadowRecords = [...union.values()];
|
|
486
|
+
const shadowEvaluations = shadowRecords.filter((record) => record.record_type === "evaluation");
|
|
487
|
+
const shadowDispositions = shadowRecords.filter((record) => record.record_type === "disposition");
|
|
488
|
+
currentShadowDispositions(shadowDispositions);
|
|
489
|
+
const policyStage = staged.find((entry) => entry.kind === "policies");
|
|
490
|
+
const proofStage = staged.find((entry) => entry.kind === "proofs");
|
|
491
|
+
const policies = [...new Map([...policyStage.pub, ...policyStage.priv.values()].map((record) => [record.id, record])).values()];
|
|
492
|
+
const proofs = new Map([...proofStage.pub, ...proofStage.priv.values()].map((record) => [record.id, record]));
|
|
493
|
+
for (const evaluation of shadowEvaluations) {
|
|
494
|
+
const policy = policies.find((record) => record.id === evaluation.policy_id);
|
|
495
|
+
const proof = proofs.get(evaluation.proof_id);
|
|
496
|
+
const composition = policy ? compositionDescendants(policy, policies) : [];
|
|
497
|
+
if (!policy || !proof
|
|
498
|
+
|| policy.proof !== proof.id
|
|
499
|
+
|| proof.policy_hash !== evaluation.policy_hash
|
|
500
|
+
|| proof.plan_hash !== evaluation.plan_hash
|
|
501
|
+
|| policyProofHash(policy, composition) !== evaluation.policy_hash
|
|
502
|
+
|| evaluation.evaluation.policy_id !== policy.id
|
|
503
|
+
|| evaluation.evaluation.deterministic_hash !== policyEvaluationContentHash(evaluation.evaluation)) {
|
|
504
|
+
throw new Error(`shadow evaluation ${evaluation.id} does not match migrated policy/proof semantics`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
for (const disposition of shadowDispositions) {
|
|
508
|
+
const evaluation = shadowEvaluations.find((record) => record.id === disposition.shadow_id);
|
|
509
|
+
if (!evaluation
|
|
510
|
+
|| evaluation.evaluation.result !== "violated"
|
|
511
|
+
|| evaluation.policy_id !== disposition.policy_id
|
|
512
|
+
|| evaluation.proof_id !== disposition.proof_id
|
|
513
|
+
|| evaluation.policy_hash !== disposition.policy_hash
|
|
514
|
+
|| evaluation.evaluation.deterministic_hash !== disposition.evaluation_hash) {
|
|
515
|
+
throw new Error(`shadow disposition ${disposition.id} does not match migrated evaluation`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
for (const { kind, from, to, pub, priv, keyFor } of staged) {
|
|
520
|
+
if (!pub.length && !existsSync(from))
|
|
521
|
+
continue;
|
|
522
|
+
mkdirSync(to, { recursive: true });
|
|
523
|
+
for (const rec of pub) {
|
|
524
|
+
const key = keyFor(rec);
|
|
525
|
+
if (!priv.has(key))
|
|
526
|
+
writeFileAtomic(join(to, `${key}.json`), encode(rec));
|
|
527
|
+
}
|
|
528
|
+
rmSync(from, { recursive: true, force: true });
|
|
529
|
+
counts[kind] = pub.length;
|
|
530
|
+
}
|
|
531
|
+
return counts;
|
|
532
|
+
}
|
|
533
|
+
//# sourceMappingURL=repository.js.map
|