@davesheffer/hunch 1.7.0 → 1.7.1

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.
Files changed (63) hide show
  1. package/README.md +214 -0
  2. package/bench/constitution-exp03-v1.json +70 -0
  3. package/dist/cli/index.js +1203 -24
  4. package/dist/constitution/adapters.js +487 -0
  5. package/dist/constitution/behaviorAttestationBinding.js +17 -0
  6. package/dist/constitution/behaviorEvaluator.js +220 -0
  7. package/dist/constitution/behaviorProof.js +205 -0
  8. package/dist/constitution/behaviorWorkspace.js +124 -0
  9. package/dist/constitution/bootstrap.js +133 -0
  10. package/dist/constitution/canonical.js +51 -0
  11. package/dist/constitution/card.js +133 -0
  12. package/dist/constitution/compiler.js +176 -0
  13. package/dist/constitution/composition.js +101 -0
  14. package/dist/constitution/corpus.js +58 -0
  15. package/dist/constitution/delta.js +154 -0
  16. package/dist/constitution/disposition.js +141 -0
  17. package/dist/constitution/evaluator.js +435 -0
  18. package/dist/constitution/experiment.js +948 -0
  19. package/dist/constitution/experimentRunner.js +344 -0
  20. package/dist/constitution/g2.js +291 -0
  21. package/dist/constitution/g2BehaviorAttestation.js +209 -0
  22. package/dist/constitution/g2BehaviorCandidates.js +703 -0
  23. package/dist/constitution/g2BehaviorDependencies.js +379 -0
  24. package/dist/constitution/g2BehaviorMaterialization.js +171 -0
  25. package/dist/constitution/g2BehaviorPolicyMaterializer.js +241 -0
  26. package/dist/constitution/g2CandidateAttestation.js +179 -0
  27. package/dist/constitution/g2Candidates.js +195 -0
  28. package/dist/constitution/g2Drills.js +122 -0
  29. package/dist/constitution/g3.js +511 -0
  30. package/dist/constitution/g3Conformance.js +115 -0
  31. package/dist/constitution/lifecycle.js +189 -0
  32. package/dist/constitution/mutation.js +262 -0
  33. package/dist/constitution/nodeTestEvidence.js +47 -0
  34. package/dist/constitution/plan.js +172 -0
  35. package/dist/constitution/policyRuntime.js +8 -0
  36. package/dist/constitution/proof.js +166 -0
  37. package/dist/constitution/replay.js +361 -0
  38. package/dist/constitution/replayCache.js +89 -0
  39. package/dist/constitution/replayWorker.js +34 -0
  40. package/dist/constitution/repository.js +533 -0
  41. package/dist/constitution/schema.js +545 -0
  42. package/dist/constitution/scorecard.js +106 -0
  43. package/dist/constitution/service.js +1149 -0
  44. package/dist/constitution/shadow.js +235 -0
  45. package/dist/constitution/sourceMutation.js +316 -0
  46. package/dist/constitution/structural.js +601 -0
  47. package/dist/core/autoreview.js +27 -3
  48. package/dist/core/dupdetect.js +10 -3
  49. package/dist/core/events.js +61 -0
  50. package/dist/core/externalImports.js +24 -0
  51. package/dist/core/hookpolicy.js +3 -0
  52. package/dist/core/relativeImports.js +33 -0
  53. package/dist/core/stats.js +115 -0
  54. package/dist/extractors/git.js +81 -0
  55. package/dist/extractors/indexer.js +39 -38
  56. package/dist/extractors/nativeTreeSitter.js +108 -0
  57. package/dist/extractors/parse.js +5 -15
  58. package/dist/integrations/claudemd.js +8 -1
  59. package/dist/integrations/gitignore.js +8 -0
  60. package/dist/integrations/providers.js +32 -10
  61. package/dist/integrations/sync.js +16 -1
  62. package/dist/mcp/server.js +284 -0
  63. 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