@davesheffer/hunch 1.8.2 → 1.9.2
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 +96 -1
- package/dist/cli/index.js +1238 -396
- package/dist/constitution/adapters.js +31 -14
- package/dist/constitution/behaviorEvaluator.js +20 -7
- package/dist/constitution/behaviorProof.js +3 -2
- package/dist/constitution/canonical.js +7 -1
- package/dist/constitution/card.js +7 -2
- package/dist/constitution/compiler.js +71 -1
- package/dist/constitution/correctionPolicyMaterializer.js +496 -0
- package/dist/constitution/delta.js +3 -2
- package/dist/constitution/evaluator.js +29 -3
- package/dist/constitution/experiment.js +96 -5
- package/dist/constitution/experimentRunner.js +43 -14
- package/dist/constitution/g2BehaviorCandidates.js +49 -26
- package/dist/constitution/g2BehaviorDependencies.js +203 -14
- package/dist/constitution/g2Candidates.js +1 -1
- package/dist/constitution/lifecycle.js +17 -0
- package/dist/constitution/plan.js +26 -9
- package/dist/constitution/replacementFreeGit.js +67 -0
- package/dist/constitution/replay.js +6 -0
- package/dist/constitution/replayCache.js +1 -1
- package/dist/constitution/replayWorker.js +1 -1
- package/dist/constitution/repository.js +141 -5
- package/dist/constitution/safeCheckout.js +75 -0
- package/dist/constitution/schema.js +30 -5
- package/dist/constitution/service.js +74 -14
- package/dist/constitution/sourceMutation.js +65 -12
- package/dist/constitution/staticGraphBaseline.js +44 -0
- package/dist/constitution/structural.js +60 -4
- package/dist/core/autoreview.js +1 -1
- package/dist/core/canonicalOrder.js +6 -0
- package/dist/core/conformance.js +68 -27
- package/dist/core/docscan.js +2 -1
- package/dist/core/escalations.js +11 -0
- package/dist/core/io.js +44 -9
- package/dist/core/overlaySafety.js +178 -0
- package/dist/core/paths.js +13 -2
- package/dist/core/safeRepoFile.js +74 -0
- package/dist/extractors/comments.js +6 -8
- package/dist/extractors/git.js +1631 -82
- package/dist/extractors/indexer.js +86 -47
- package/dist/extractors/repoSource.js +390 -0
- package/dist/integrations/ciAction.js +10 -2
- package/dist/integrations/gitignore.js +44 -5
- package/dist/integrations/mergeDriver.js +23 -5
- package/dist/integrations/sync.js +61 -5
- package/dist/integrations/team.js +666 -23
- package/dist/mcp/server.js +261 -34
- package/dist/store/db.js +57 -7
- package/dist/store/hunchStore.js +92 -11
- package/dist/store/jsonStore.js +350 -63
- package/dist/store/schema.js +27 -11
- package/dist/synthesis/provider.js +13 -4
- package/dist/synthesis/synthesize.js +56 -19
- package/dist/wiki/graph.js +5 -4
- package/dist/wiki/wiki.js +16 -10
- package/package.json +15 -3
- package/tooling/competitive-watch.mjs +108 -0
- package/tooling/md1-benchmark.mjs +628 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { toPosixTarget } from "../core/paths.js";
|
|
5
|
+
import { isHumanConfirmed } from "../core/strictgate.js";
|
|
6
|
+
import { isGitCleanPath, stableRepositoryName } from "../extractors/git.js";
|
|
7
|
+
import { correctionEvidenceEvent } from "./adapters.js";
|
|
8
|
+
import { canonicalHash, canonicalJson } from "./canonical.js";
|
|
9
|
+
import { compileCorrectionPolicy } from "./compiler.js";
|
|
10
|
+
import { policyProofHash } from "./composition.js";
|
|
11
|
+
import { blockingEvidenceError, proposeProvedPolicy } from "./lifecycle.js";
|
|
12
|
+
import { createProofPlan } from "./plan.js";
|
|
13
|
+
import { evaluatorForPolicy, mutationEngineForPolicy } from "./policyRuntime.js";
|
|
14
|
+
import { provePolicy } from "./proof.js";
|
|
15
|
+
import { canonicalStaticGraphBaseline } from "./staticGraphBaseline.js";
|
|
16
|
+
import { replacementFreeGitEnvironment } from "./replacementFreeGit.js";
|
|
17
|
+
import { EvidenceEventSchema, } from "./schema.js";
|
|
18
|
+
import { directConflict, inspectExternalImportBoundary, structuralKey, } from "./structural.js";
|
|
19
|
+
const LIVE_CONFLICT_STATES = new Set([
|
|
20
|
+
"compiled",
|
|
21
|
+
"validating",
|
|
22
|
+
"proposed",
|
|
23
|
+
"active_advisory",
|
|
24
|
+
"active_blocking",
|
|
25
|
+
"stale",
|
|
26
|
+
"repaired",
|
|
27
|
+
]);
|
|
28
|
+
function exactCorrection(store, id, opts) {
|
|
29
|
+
if (opts.publicOnly && opts.privateOnly)
|
|
30
|
+
throw new Error("choose only one of publicOnly or privateOnly");
|
|
31
|
+
if (opts.privateOnly && !store.hasPrivate)
|
|
32
|
+
throw new Error("private correction upgrade needs a configured Hunch private overlay");
|
|
33
|
+
if (opts.publicOnly) {
|
|
34
|
+
const correction = store.json.get("constraints", id);
|
|
35
|
+
if (!correction)
|
|
36
|
+
throw new Error(`public correction ${id} not found`);
|
|
37
|
+
return { correction, home: "public", dataClass: "public" };
|
|
38
|
+
}
|
|
39
|
+
if (opts.privateOnly) {
|
|
40
|
+
const correction = store.getPrivateRec("constraints", id);
|
|
41
|
+
if (!correction)
|
|
42
|
+
throw new Error(`private correction ${id} not found`);
|
|
43
|
+
return { correction, home: "private", dataClass: "private" };
|
|
44
|
+
}
|
|
45
|
+
const privateCorrection = store.getPrivateRec("constraints", id);
|
|
46
|
+
if (privateCorrection)
|
|
47
|
+
return { correction: privateCorrection, home: "private", dataClass: "private" };
|
|
48
|
+
const correction = store.json.get("constraints", id);
|
|
49
|
+
if (!correction)
|
|
50
|
+
throw new Error(`correction ${id} not found`);
|
|
51
|
+
return { correction, home: "public", dataClass: "public" };
|
|
52
|
+
}
|
|
53
|
+
function concreteScope(correction) {
|
|
54
|
+
if (correction.scope.length !== 1) {
|
|
55
|
+
return { file: null, reason: `Correction has ${correction.scope.length} scopes; exactly one concrete file is required.` };
|
|
56
|
+
}
|
|
57
|
+
const file = toPosixTarget(correction.scope[0].trim());
|
|
58
|
+
const parts = file.split("/");
|
|
59
|
+
if (!file || file === "." || file === "**" || file.startsWith("/") || /^[A-Za-z]:\//.test(file)
|
|
60
|
+
|| parts.some((part) => !part || part === "." || part === "..") || /[*?\[\]{}!\x00-\x1f\x7f]/.test(file)) {
|
|
61
|
+
return { file: null, reason: "Correction scope is not one safe, concrete repository-relative file." };
|
|
62
|
+
}
|
|
63
|
+
return { file, reason: null };
|
|
64
|
+
}
|
|
65
|
+
function unsafeSourceScopeReason(root, file) {
|
|
66
|
+
const target = join(root, file);
|
|
67
|
+
try {
|
|
68
|
+
const stat = lstatSync(target);
|
|
69
|
+
if (stat.isSymbolicLink()) {
|
|
70
|
+
return `${file} is a symbolic link; MD-1a only proves regular committed source files`;
|
|
71
|
+
}
|
|
72
|
+
if (!stat.isFile())
|
|
73
|
+
return `${file} is not a regular source file`;
|
|
74
|
+
const canonicalRoot = realpathSync(root);
|
|
75
|
+
const canonicalTarget = realpathSync(target);
|
|
76
|
+
const fromRoot = relative(canonicalRoot, canonicalTarget);
|
|
77
|
+
if (!fromRoot || fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)
|
|
78
|
+
|| canonicalTarget !== resolve(canonicalRoot, file)) {
|
|
79
|
+
return `${file} resolves through or outside the repository; MD-1a only proves regular committed source files`;
|
|
80
|
+
}
|
|
81
|
+
const entry = execFileSync("git", ["-C", root, "ls-tree", "-z", "HEAD", "--", file], {
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
env: replacementFreeGitEnvironment(),
|
|
84
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
85
|
+
});
|
|
86
|
+
if (!/^(?:100644|100755) blob [a-f0-9]{40,64}\t/.test(entry) || !entry.endsWith(`\t${file}\0`)) {
|
|
87
|
+
return `${file} is not a regular source blob at committed HEAD; symbolic links and special entries are unsupported`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return `${file} could not be verified as a regular committed source file`;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function exactForbiddenDependency(correction) {
|
|
96
|
+
if (correction.match || !correction.forbids) {
|
|
97
|
+
return { dependency: null, reason: "Correction has no exact structured forbidden-package meaning." };
|
|
98
|
+
}
|
|
99
|
+
if (correction.forbids.deps.length !== 1 || correction.forbids.symbols.length || correction.forbids.patterns.length) {
|
|
100
|
+
return { dependency: null, reason: "Correction must contain exactly one forbidden package and no symbol, pattern, or regex fallback." };
|
|
101
|
+
}
|
|
102
|
+
return { dependency: correction.forbids.deps[0], reason: null };
|
|
103
|
+
}
|
|
104
|
+
function candidateContext(candidate, conflicts = []) {
|
|
105
|
+
return {
|
|
106
|
+
alternatives: [{
|
|
107
|
+
id: candidate.id,
|
|
108
|
+
basis: candidate.basis,
|
|
109
|
+
reason: candidate.reason,
|
|
110
|
+
assertion_hash: canonicalHash(candidate.assertion),
|
|
111
|
+
}],
|
|
112
|
+
uncertainty: [],
|
|
113
|
+
conflicts,
|
|
114
|
+
incumbent: null,
|
|
115
|
+
scope_suggestion: null,
|
|
116
|
+
counterexamples: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function classifiedEvidence(base, status, reason, policy, context) {
|
|
120
|
+
return EvidenceEventSchema.parse({
|
|
121
|
+
...base,
|
|
122
|
+
related_records: [...new Set([...base.related_records, ...(policy ? [policy] : [])])].sort(),
|
|
123
|
+
compiler: {
|
|
124
|
+
status,
|
|
125
|
+
policy,
|
|
126
|
+
reason,
|
|
127
|
+
...(context ?? {}),
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function putEvidenceIfChanged(repository, event, home) {
|
|
132
|
+
const view = home === "public" ? { publicOnly: true } : { privateOnly: true };
|
|
133
|
+
const existing = repository.getEvidence(event.id, view);
|
|
134
|
+
return existing && canonicalJson(existing) === canonicalJson(event)
|
|
135
|
+
? existing
|
|
136
|
+
: repository.putEvidence(event, { private: home === "private", public: home === "public" });
|
|
137
|
+
}
|
|
138
|
+
function reviewFor(correction, candidate, policy) {
|
|
139
|
+
const file = candidate.scope.paths[0];
|
|
140
|
+
const assertion = candidate.assertion;
|
|
141
|
+
const dependency = assertion.kind === "not-reaches"
|
|
142
|
+
? assertion.object.selector.slice("external:".length)
|
|
143
|
+
: "the selected package";
|
|
144
|
+
return {
|
|
145
|
+
status: "ready_for_review",
|
|
146
|
+
rule: correction.statement,
|
|
147
|
+
meaning: `${file} must not directly import ${dependency} through a supported static ESM import declaration.`,
|
|
148
|
+
why: correction.rationale,
|
|
149
|
+
catches: `A static TypeScript/JavaScript import of ${dependency} in ${file}.`,
|
|
150
|
+
does_not_catch: "re-exports, require(), dynamic import(), aliases, runtime loading, or anchor-symbol rename/removal until the proposal is repaired.",
|
|
151
|
+
authority: policy?.authority ? "existing_human" : "none",
|
|
152
|
+
next_action: policy?.authority
|
|
153
|
+
? "This policy is already human-activated; the correction upgrade changed no authority."
|
|
154
|
+
: "Review this proposal; keep it non-active until source-currentness safety is in place.",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function result(status, correction, reason, evidence, policy = null, plan = null, proof = null, review = null) {
|
|
158
|
+
return {
|
|
159
|
+
status,
|
|
160
|
+
correction_id: correction.id,
|
|
161
|
+
reason,
|
|
162
|
+
evidence,
|
|
163
|
+
policy,
|
|
164
|
+
plan,
|
|
165
|
+
proof,
|
|
166
|
+
review,
|
|
167
|
+
authority: "none",
|
|
168
|
+
effects: "proposal_only",
|
|
169
|
+
activation: "not_available_in_this_operation",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function proofPayloadHash(proof) {
|
|
173
|
+
const { generated_at: _generatedAt, ...payload } = proof;
|
|
174
|
+
return canonicalHash(payload);
|
|
175
|
+
}
|
|
176
|
+
function reusableProofPacket(store, root, repository, policy, homeView) {
|
|
177
|
+
if (!["proposed", "active_advisory", "active_blocking"].includes(policy.state)) {
|
|
178
|
+
return { reason: `policy lifecycle is ${policy.state}, not a reusable proved state` };
|
|
179
|
+
}
|
|
180
|
+
const policies = repository.listPolicies(homeView);
|
|
181
|
+
if (policy.exception_of || policies.some((candidate) => candidate.exception_of === policy.id)) {
|
|
182
|
+
return { reason: "policy participates in an exception composition that this correction bridge cannot reuse" };
|
|
183
|
+
}
|
|
184
|
+
if (!policy.proof)
|
|
185
|
+
return { reason: "policy has no linked proof" };
|
|
186
|
+
const proof = repository.getProof(policy.proof, homeView);
|
|
187
|
+
if (!proof)
|
|
188
|
+
return { reason: `linked proof ${policy.proof} is missing` };
|
|
189
|
+
const plan = repository.listPlans(homeView).find((candidate) => candidate.policy_id === policy.id && candidate.content_hash === proof.plan_hash);
|
|
190
|
+
if (!plan)
|
|
191
|
+
return { reason: `proof ${proof.id} has no exact bound plan` };
|
|
192
|
+
const expectedPolicyHash = policyProofHash(policy);
|
|
193
|
+
if (plan.policy_candidate_hash !== expectedPolicyHash || proof.policy_hash !== expectedPolicyHash) {
|
|
194
|
+
return { reason: "proof or plan does not bind the current policy semantics" };
|
|
195
|
+
}
|
|
196
|
+
const evaluator = evaluatorForPolicy(policy);
|
|
197
|
+
const mutation = mutationEngineForPolicy(policy);
|
|
198
|
+
if (plan.policy_id !== policy.id || plan.repository !== stableRepositoryName(root)
|
|
199
|
+
|| plan.data_class !== policy.data_class || proof.data_class !== policy.data_class) {
|
|
200
|
+
return { reason: "proof packet does not match the policy repository or data class" };
|
|
201
|
+
}
|
|
202
|
+
if (plan.evaluator.name !== evaluator.name || plan.evaluator.version !== evaluator.version) {
|
|
203
|
+
return { reason: "proof plan evaluator version is stale" };
|
|
204
|
+
}
|
|
205
|
+
if (plan.budgets.max_commits !== 0 || plan.corpus.accepted_history.max_commits !== 0
|
|
206
|
+
|| plan.budgets.max_mutations > 3 || plan.mutations.length > 3 || plan.budgets.max_minutes > 1
|
|
207
|
+
|| plan.corpus.known_bad.length !== 0 || plan.corpus.known_good.length !== 1
|
|
208
|
+
|| plan.corpus.known_good[0]?.ref !== plan.corpus.current_baseline.ref) {
|
|
209
|
+
return { reason: "proof plan exceeds the MD-1a automatic replay budget" };
|
|
210
|
+
}
|
|
211
|
+
if (plan.mutation_engine?.name !== mutation.name || plan.mutation_engine.version !== mutation.version) {
|
|
212
|
+
return { reason: "proof plan mutation engine version is stale" };
|
|
213
|
+
}
|
|
214
|
+
if (proof.evaluator.name !== evaluator.name || proof.evaluator.version !== evaluator.version) {
|
|
215
|
+
return { reason: "proof evaluator version is stale" };
|
|
216
|
+
}
|
|
217
|
+
if (proof.mutation_engine?.name !== mutation.name || proof.mutation_engine.version !== mutation.version) {
|
|
218
|
+
return { reason: "proof mutation engine version is stale" };
|
|
219
|
+
}
|
|
220
|
+
if (!["P3", "P4", "P5"].includes(proof.proof_class))
|
|
221
|
+
return { reason: `proof class ${proof.proof_class} is below P3` };
|
|
222
|
+
if (proof.current.total !== 1 || proof.current.satisfied !== 1 || proof.current.violated
|
|
223
|
+
|| proof.current.not_applicable || proof.current.unknown || proof.current.error) {
|
|
224
|
+
return { reason: "proof has no exact clean current baseline" };
|
|
225
|
+
}
|
|
226
|
+
const baselineHead = plan.corpus.current_baseline.ref;
|
|
227
|
+
if (plan.corpus.accepted_history.to !== baselineHead || baselineHead !== canonicalStaticGraphBaseline(root)) {
|
|
228
|
+
return { reason: "proof baseline is not the current source-equivalent HEAD" };
|
|
229
|
+
}
|
|
230
|
+
const dispositions = repository.listDispositions(homeView).filter((record) => record.policy_id === policy.id && record.proof_id === proof.id);
|
|
231
|
+
const evidenceError = blockingEvidenceError(proof, dispositions);
|
|
232
|
+
if (evidenceError)
|
|
233
|
+
return { reason: evidenceError };
|
|
234
|
+
try {
|
|
235
|
+
const regenerated = provePolicy(store, root, policy, {
|
|
236
|
+
publicOnly: "publicOnly" in homeView,
|
|
237
|
+
plan,
|
|
238
|
+
now: proof.generated_at,
|
|
239
|
+
});
|
|
240
|
+
if (proofPayloadHash(regenerated) !== proofPayloadHash(proof)) {
|
|
241
|
+
return { reason: "proof receipts do not reproduce from the bound plan and current evaluator" };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return { reason: `proof receipts could not be reproduced: ${error.message}` };
|
|
246
|
+
}
|
|
247
|
+
return { plan, proof };
|
|
248
|
+
}
|
|
249
|
+
function privateOnlySourceDecisionError(store, correction, home) {
|
|
250
|
+
const source = correction.source_decision;
|
|
251
|
+
if (home === "public" && source && !store.json.get("decisions", source)) {
|
|
252
|
+
const location = store.getPrivateRec("decisions", source) ? "private-only" : "missing from the public home";
|
|
253
|
+
return `public correction ${correction.id} references decision ${source}, which is ${location}; refusing to write any public correction-policy artifact`;
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
function publicationInputError(store, root, correction, home, expectedHead, sourceFile) {
|
|
258
|
+
if (canonicalStaticGraphBaseline(root) !== expectedHead) {
|
|
259
|
+
return { status: "pending", reason: `Repository HEAD changed while proving correction ${correction.id}; the stale packet was not published.` };
|
|
260
|
+
}
|
|
261
|
+
const current = home === "public"
|
|
262
|
+
? store.json.get("constraints", correction.id)
|
|
263
|
+
: store.getPrivateRec("constraints", correction.id);
|
|
264
|
+
if (!current || canonicalJson(current) !== canonicalJson(correction)) {
|
|
265
|
+
return { status: "conflicted", reason: `Correction ${correction.id} changed or left its ${home} home while proof was running; the old meaning was not published.` };
|
|
266
|
+
}
|
|
267
|
+
if (!isGitCleanPath(root, sourceFile)) {
|
|
268
|
+
return { status: "pending", reason: `${sourceFile} changed while proving correction ${correction.id}; only a clean committed source baseline can be published.` };
|
|
269
|
+
}
|
|
270
|
+
const sourceError = privateOnlySourceDecisionError(store, current, home);
|
|
271
|
+
return sourceError ? { status: "conflicted", reason: sourceError } : null;
|
|
272
|
+
}
|
|
273
|
+
function liveConflicts(repository, candidate, home) {
|
|
274
|
+
const visible = home === "private"
|
|
275
|
+
? [...repository.listPolicies({ publicOnly: true }), ...repository.listPolicies({ privateOnly: true })]
|
|
276
|
+
: repository.listPolicies({ publicOnly: true });
|
|
277
|
+
return [...new Set(visible
|
|
278
|
+
.filter((policy) => LIVE_CONFLICT_STATES.has(policy.state) && directConflict(candidate, policy))
|
|
279
|
+
.map((policy) => policy.id))]
|
|
280
|
+
.sort();
|
|
281
|
+
}
|
|
282
|
+
/** Upgrade one safe correction projection into a proved proposal. This operation
|
|
283
|
+
* cannot activate, warn, or block; unsupported corrections retain only their
|
|
284
|
+
* immediate legacy Constraint. */
|
|
285
|
+
export function materializeCorrectionPolicy(store, root, repository, correctionId, opts = {}) {
|
|
286
|
+
const { correction, home, dataClass } = exactCorrection(store, correctionId, opts);
|
|
287
|
+
const now = opts.now ?? new Date().toISOString();
|
|
288
|
+
const homeView = home === "public" ? { publicOnly: true } : { privateOnly: true };
|
|
289
|
+
const sourceError = privateOnlySourceDecisionError(store, correction, home);
|
|
290
|
+
if (sourceError)
|
|
291
|
+
throw new Error(sourceError);
|
|
292
|
+
const base = correctionEvidenceEvent(root, correction, dataClass);
|
|
293
|
+
if (!base)
|
|
294
|
+
throw new Error(`correction ${correction.id} has no valid occurrence time`);
|
|
295
|
+
const prior = repository.getEvidence(base.id, homeView);
|
|
296
|
+
const evidence = prior ?? repository.putEvidence(base, { private: home === "private", public: home === "public" });
|
|
297
|
+
if (correction.status !== "active" || correction.valid_to || !isHumanConfirmed(correction.provenance.source)) {
|
|
298
|
+
return result("legacy_only", correction, "Only an active human-confirmed correction can be upgraded.", evidence);
|
|
299
|
+
}
|
|
300
|
+
const scope = concreteScope(correction);
|
|
301
|
+
if (scope.file === null) {
|
|
302
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, "uncompilable", `${scope.reason} The legacy Constraint remains active.`, null), home);
|
|
303
|
+
return result("legacy_only", correction, scope.reason, classified);
|
|
304
|
+
}
|
|
305
|
+
const unsafeScope = unsafeSourceScopeReason(root, scope.file);
|
|
306
|
+
if (unsafeScope) {
|
|
307
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, "uncompilable", `${unsafeScope}. The legacy Constraint remains active.`, null), home);
|
|
308
|
+
return result("legacy_only", correction, unsafeScope, classified);
|
|
309
|
+
}
|
|
310
|
+
if (!isGitCleanPath(root, scope.file)) {
|
|
311
|
+
const reason = `${scope.file} is not a clean committed source baseline; commit the fix before Hunch builds a proof packet`;
|
|
312
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, "eligible", `${reason}. The legacy Constraint remains active.`, null), home);
|
|
313
|
+
return result("pending", correction, reason, classified);
|
|
314
|
+
}
|
|
315
|
+
const forbidden = exactForbiddenDependency(correction);
|
|
316
|
+
if (forbidden.dependency === null) {
|
|
317
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, "uncompilable", `${forbidden.reason} The legacy Constraint remains active.`, null), home);
|
|
318
|
+
return result("legacy_only", correction, forbidden.reason, classified);
|
|
319
|
+
}
|
|
320
|
+
const inspection = inspectExternalImportBoundary(store, scope.file, forbidden.dependency, { publicOnly: home === "public" });
|
|
321
|
+
if (!inspection.candidate) {
|
|
322
|
+
const pending = inspection.code === "baseline_violated";
|
|
323
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, pending ? "eligible" : "uncompilable", `${inspection.reason}. The legacy Constraint remains active.`, null), home);
|
|
324
|
+
return result(pending ? "pending" : "legacy_only", correction, inspection.reason, classified);
|
|
325
|
+
}
|
|
326
|
+
const candidate = inspection.candidate;
|
|
327
|
+
let context = candidateContext(candidate);
|
|
328
|
+
const compiled = compileCorrectionPolicy(store, {
|
|
329
|
+
source: correction,
|
|
330
|
+
evidenceId: base.id,
|
|
331
|
+
assertion: candidate.assertion,
|
|
332
|
+
scope: candidate.scope,
|
|
333
|
+
dataClass,
|
|
334
|
+
candidate: context,
|
|
335
|
+
now,
|
|
336
|
+
});
|
|
337
|
+
const conflicts = liveConflicts(repository, compiled.policy, home);
|
|
338
|
+
if (conflicts.length) {
|
|
339
|
+
context = candidateContext(candidate, conflicts);
|
|
340
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, "conflicted", `The exact supported correction projection conflicts with ${conflicts.join(", ")}; no policy or authority changed.`, null, context), home);
|
|
341
|
+
return result("conflicted", correction, classified.compiler.reason, classified);
|
|
342
|
+
}
|
|
343
|
+
const key = structuralKey(compiled.policy);
|
|
344
|
+
const handleIncumbent = (policy) => {
|
|
345
|
+
if (structuralKey(policy) !== key) {
|
|
346
|
+
const reason = `Policy id ${policy.id} is occupied by different semantics; no lifecycle or authority changed.`;
|
|
347
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, "conflicted", reason, null, context), home);
|
|
348
|
+
return result("conflicted", correction, reason, classified);
|
|
349
|
+
}
|
|
350
|
+
const packet = reusableProofPacket(store, root, repository, policy, homeView);
|
|
351
|
+
if (!("reason" in packet)) {
|
|
352
|
+
const compiledHere = policy.audit.some((event) => event.action === "compiled"
|
|
353
|
+
&& event.actor === "hunch:correction-policy-materializer");
|
|
354
|
+
const status = prior?.compiler?.status === "compiled" || compiledHere ? "compiled" : "covered";
|
|
355
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, status, status === "compiled"
|
|
356
|
+
? `One exact supported correction projection was proved without granting authority: ${candidate.reason}.`
|
|
357
|
+
: "An equivalent proved policy already covers this supported projection; lifecycle and authority were preserved.", policy.id, context), home);
|
|
358
|
+
return result("already_proved", correction, classified.compiler.reason, classified, policy, packet.plan, packet.proof, reviewFor(correction, candidate, policy));
|
|
359
|
+
}
|
|
360
|
+
const retryable = ["compiled", "validating", "proposed"].includes(policy.state);
|
|
361
|
+
const status = retryable ? "eligible" : "conflicted";
|
|
362
|
+
const reason = retryable
|
|
363
|
+
? `Equivalent policy ${policy.id} is preserved, but its proof packet is not reusable (${packet.reason}); automatic retry will not rewrite the incumbent.`
|
|
364
|
+
: `Equivalent policy ${policy.id} is ${policy.state} without a reusable proof packet (${packet.reason}); lifecycle and authority were preserved.`;
|
|
365
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, status, reason, null, context), home);
|
|
366
|
+
return result(retryable ? "pending" : "conflicted", correction, reason, classified);
|
|
367
|
+
};
|
|
368
|
+
const incumbent = repository.getPolicy(compiled.policy.id, homeView)
|
|
369
|
+
?? repository.listPolicies(homeView).find((policy) => structuralKey(policy) === key);
|
|
370
|
+
if (incumbent)
|
|
371
|
+
return handleIncumbent(incumbent);
|
|
372
|
+
const generatedPlan = createProofPlan(store, root, repository, compiled.policy, {
|
|
373
|
+
...homeView,
|
|
374
|
+
maxCommits: 0,
|
|
375
|
+
maxMutations: 3,
|
|
376
|
+
maxMinutes: 1,
|
|
377
|
+
repositoryName: stableRepositoryName(root),
|
|
378
|
+
now,
|
|
379
|
+
});
|
|
380
|
+
const generatedProof = provePolicy(store, root, compiled.policy, {
|
|
381
|
+
publicOnly: home === "public",
|
|
382
|
+
plan: generatedPlan,
|
|
383
|
+
now,
|
|
384
|
+
});
|
|
385
|
+
if (generatedProof.proof_class !== "P3") {
|
|
386
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, "eligible", `The supported projection is exact, but the bounded proof reached ${generatedProof.proof_class}; no policy was written and the legacy Constraint remains active.`, null, context), home);
|
|
387
|
+
return result("pending", correction, classified.compiler.reason, classified);
|
|
388
|
+
}
|
|
389
|
+
// Proof execution is deliberately outside the publication critical section.
|
|
390
|
+
// Re-read every mutable lifecycle input before writing deterministic artifacts.
|
|
391
|
+
const expectedHead = generatedPlan.corpus.current_baseline.ref;
|
|
392
|
+
const inputError = publicationInputError(store, root, correction, home, expectedHead, scope.file);
|
|
393
|
+
if (inputError) {
|
|
394
|
+
const compilerStatus = inputError.status === "pending" ? "eligible" : "conflicted";
|
|
395
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, compilerStatus, inputError.reason, null, context), home);
|
|
396
|
+
return result(inputError.status, correction, inputError.reason, classified);
|
|
397
|
+
}
|
|
398
|
+
const postProofConflicts = liveConflicts(repository, compiled.policy, home);
|
|
399
|
+
if (postProofConflicts.length) {
|
|
400
|
+
context = candidateContext(candidate, postProofConflicts);
|
|
401
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, "conflicted", `A live conflicting policy appeared during proof (${postProofConflicts.join(", ")}); no policy or authority changed.`, null, context), home);
|
|
402
|
+
return result("conflicted", correction, classified.compiler.reason, classified);
|
|
403
|
+
}
|
|
404
|
+
const postProofIncumbent = repository.getPolicy(compiled.policy.id, homeView)
|
|
405
|
+
?? repository.listPolicies(homeView).find((policy) => structuralKey(policy) === key);
|
|
406
|
+
if (postProofIncumbent)
|
|
407
|
+
return handleIncumbent(postProofIncumbent);
|
|
408
|
+
const planClaim = repository.putPlanIfAbsent(generatedPlan, compiled.policy.id, {
|
|
409
|
+
private: home === "private",
|
|
410
|
+
public: home === "public",
|
|
411
|
+
});
|
|
412
|
+
if (planClaim.plan.content_hash !== generatedPlan.content_hash) {
|
|
413
|
+
throw new Error(`proof plan ${generatedPlan.id} already exists with different canonical content`);
|
|
414
|
+
}
|
|
415
|
+
const plan = planClaim.plan;
|
|
416
|
+
const proofClaim = repository.putProofIfAbsent(generatedProof, compiled.policy.id, {
|
|
417
|
+
private: home === "private",
|
|
418
|
+
public: home === "public",
|
|
419
|
+
});
|
|
420
|
+
if (proofPayloadHash(proofClaim.proof) !== proofPayloadHash(generatedProof)) {
|
|
421
|
+
throw new Error(`proof ${generatedProof.id} already exists with different deterministic content`);
|
|
422
|
+
}
|
|
423
|
+
const proof = proofClaim.proof;
|
|
424
|
+
const prePublishError = publicationInputError(store, root, correction, home, expectedHead, scope.file);
|
|
425
|
+
if (prePublishError) {
|
|
426
|
+
const compilerStatus = prePublishError.status === "pending" ? "eligible" : "conflicted";
|
|
427
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, compilerStatus, prePublishError.reason, null, context), home);
|
|
428
|
+
return result(prePublishError.status, correction, prePublishError.reason, classified);
|
|
429
|
+
}
|
|
430
|
+
const proposed = proposeProvedPolicy(compiled.policy, proof, now);
|
|
431
|
+
const published = repository.putPolicyIfAbsent(proposed, { private: home === "private", public: home === "public" });
|
|
432
|
+
if (!published.created)
|
|
433
|
+
return handleIncumbent(published.policy);
|
|
434
|
+
const policy = published.policy;
|
|
435
|
+
const finalInputError = publicationInputError(store, root, correction, home, expectedHead, scope.file);
|
|
436
|
+
const finalConflicts = liveConflicts(repository, compiled.policy, home);
|
|
437
|
+
if (finalInputError || finalConflicts.length) {
|
|
438
|
+
const reason = finalInputError?.reason
|
|
439
|
+
?? `A live conflicting policy appeared during publication (${finalConflicts.join(", ")}); the persisted proposal remains non-authoritative and activation-blocked.`;
|
|
440
|
+
context = candidateContext(candidate, finalConflicts);
|
|
441
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, "conflicted", reason, policy.id, context), home);
|
|
442
|
+
return result("conflicted", correction, reason, classified, policy, plan, proof);
|
|
443
|
+
}
|
|
444
|
+
const classified = putEvidenceIfChanged(repository, classifiedEvidence(base, "compiled", `One exact supported correction projection was proved without granting authority: ${candidate.reason}.`, policy.id, context), home);
|
|
445
|
+
return result("proved", correction, classified.compiler.reason, classified, policy, plan, proof, reviewFor(correction, candidate));
|
|
446
|
+
}
|
|
447
|
+
/** Durable retry queue: the captured Constraints are the source of truth, so a
|
|
448
|
+
* crashed process needs no in-memory job record. Normal indexing and post-commit
|
|
449
|
+
* sync can safely rescan both exact homes; every artifact is deterministic and
|
|
450
|
+
* no result grants authority. */
|
|
451
|
+
export function materializeCorrectionPolicies(store, root, repository, opts = {}) {
|
|
452
|
+
if (opts.publicOnly && opts.privateOnly)
|
|
453
|
+
throw new Error("choose only one of publicOnly or privateOnly");
|
|
454
|
+
if (opts.privateOnly && !store.hasPrivate)
|
|
455
|
+
throw new Error("private correction upgrade needs a configured Hunch private overlay");
|
|
456
|
+
const homes = opts.publicOnly
|
|
457
|
+
? ["public"]
|
|
458
|
+
: opts.privateOnly
|
|
459
|
+
? ["private"]
|
|
460
|
+
: store.hasPrivate ? ["public", "private"] : ["public"];
|
|
461
|
+
const now = opts.now ?? new Date().toISOString();
|
|
462
|
+
const upgrades = [];
|
|
463
|
+
const failed = [];
|
|
464
|
+
for (const home of homes) {
|
|
465
|
+
const corrections = store.recsInHome("constraints", home)
|
|
466
|
+
.filter((correction) => correction.status === "active"
|
|
467
|
+
&& !correction.valid_to
|
|
468
|
+
&& isHumanConfirmed(correction.provenance.source))
|
|
469
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
470
|
+
for (const correction of corrections) {
|
|
471
|
+
try {
|
|
472
|
+
upgrades.push(materializeCorrectionPolicy(store, root, repository, correction.id, {
|
|
473
|
+
publicOnly: home === "public",
|
|
474
|
+
privateOnly: home === "private",
|
|
475
|
+
now,
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
failed.push({ correction_id: correction.id, home, error: error.message });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const count = (status) => upgrades.filter((upgrade) => upgrade.status === status).length;
|
|
484
|
+
return {
|
|
485
|
+
scanned: upgrades.length + failed.length,
|
|
486
|
+
proved: count("proved"),
|
|
487
|
+
already_proved: count("already_proved"),
|
|
488
|
+
legacy_only: count("legacy_only"),
|
|
489
|
+
pending: count("pending"),
|
|
490
|
+
conflicted: count("conflicted"),
|
|
491
|
+
failed,
|
|
492
|
+
upgrades,
|
|
493
|
+
authority: "none",
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
//# sourceMappingURL=correctionPolicyMaterializer.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { shortHash } from "../core/ids.js";
|
|
2
|
+
import { compareCodeUnits } from "../core/canonicalOrder.js";
|
|
2
3
|
import { commitChanges, fileAtRef, firstParent, revParse } from "../extractors/git.js";
|
|
3
4
|
import { attributeCalls, parseSource } from "../extractors/parse.js";
|
|
4
5
|
import { canonicalHash } from "./canonical.js";
|
|
@@ -49,7 +50,7 @@ function importRef(file, specifier) {
|
|
|
49
50
|
return { file, specifier };
|
|
50
51
|
}
|
|
51
52
|
function sortByKey(items, key) {
|
|
52
|
-
return items.sort((a, b) => key(a)
|
|
53
|
+
return items.sort((a, b) => compareCodeUnits(key(a), key(b)));
|
|
53
54
|
}
|
|
54
55
|
/** Compare exact git blobs at a commit and its first parent. No checkout,
|
|
55
56
|
* worktree, hook, or model/provider is involved. */
|
|
@@ -129,7 +130,7 @@ export function extractStructuralDelta(root, commit) {
|
|
|
129
130
|
const body = {
|
|
130
131
|
before_commit: before,
|
|
131
132
|
after_commit: after,
|
|
132
|
-
files: [...files].sort(),
|
|
133
|
+
files: [...files].sort(compareCodeUnits),
|
|
133
134
|
symbols: {
|
|
134
135
|
added: sortByKey(addedSymbols, (s) => `${s.file}\0${s.kind}\0${s.name}`),
|
|
135
136
|
removed: sortByKey(removedSymbols, (s) => `${s.file}\0${s.kind}\0${s.name}`),
|
|
@@ -5,6 +5,8 @@ import { headSha } from "../extractors/git.js";
|
|
|
5
5
|
import { canonicalHash, proofEvaluationHash } from "./canonical.js";
|
|
6
6
|
import { evaluateExecutableBehaviorPolicy } from "./behaviorEvaluator.js";
|
|
7
7
|
import { policyCompositionBinding } from "./composition.js";
|
|
8
|
+
import { activationGateError } from "./lifecycle.js";
|
|
9
|
+
import { canonicalStaticGraphBaseline } from "./staticGraphBaseline.js";
|
|
8
10
|
import { POLICY_EVALUATOR, PolicyEvaluationSchema, } from "./schema.js";
|
|
9
11
|
function snapshotHash(symbols, edges, components) {
|
|
10
12
|
return canonicalHash({
|
|
@@ -16,11 +18,31 @@ function snapshotHash(symbols, edges, components) {
|
|
|
16
18
|
export function graphSnapshotFromRecords(root, head, symbols, edges, components = []) {
|
|
17
19
|
return { root, head, symbols, edges, components, graph_hash: snapshotHash(symbols, edges, components) };
|
|
18
20
|
}
|
|
21
|
+
/** Bind a filesystem-derived graph to its content, never to a Git commit whose
|
|
22
|
+
* tree may not contain the scanned bytes. The graph hash is independent of the
|
|
23
|
+
* receipt head, so it is safe to use as the explicit checkout identity. */
|
|
24
|
+
export function checkoutGraphSnapshot(root, symbols, edges, components = []) {
|
|
25
|
+
const snapshot = graphSnapshotFromRecords(root, "checkout", symbols, edges, components);
|
|
26
|
+
return { ...snapshot, head: `checkout:${snapshot.graph_hash}` };
|
|
27
|
+
}
|
|
28
|
+
/** Bind a semantic check receipt to both the selected source surface and the
|
|
29
|
+
* exact raw source bytes. The graph hash remains explicit because two source
|
|
30
|
+
* bodies can differ while producing the same topology. */
|
|
31
|
+
export function sourceGraphSnapshot(root, source, symbols, edges, components = []) {
|
|
32
|
+
const snapshot = graphSnapshotFromRecords(root, source.kind, symbols, edges, components);
|
|
33
|
+
const revision = source.revision ? `:${source.revision}` : "";
|
|
34
|
+
return {
|
|
35
|
+
...snapshot,
|
|
36
|
+
head: `${source.kind}${revision}:${source.content_hash}:${snapshot.graph_hash}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
19
39
|
export function graphSnapshot(store, root, opts = {}) {
|
|
20
40
|
const symbols = opts.publicOnly ? store.json.loadAll("symbols") : store.recs("symbols");
|
|
21
41
|
const edges = opts.publicOnly ? store.json.loadAll("edges") : store.recs("edges");
|
|
22
42
|
const components = opts.publicOnly ? store.json.loadAll("components") : store.recs("components");
|
|
23
|
-
|
|
43
|
+
const repositoryHead = headSha(root);
|
|
44
|
+
const head = opts.head ?? (repositoryHead ? canonicalStaticGraphBaseline(root, repositoryHead) : "working-tree");
|
|
45
|
+
return graphSnapshotFromRecords(root, head, symbols, edges, components);
|
|
24
46
|
}
|
|
25
47
|
function resolveSelector(snapshot, selector) {
|
|
26
48
|
const raw = selector.selector;
|
|
@@ -352,19 +374,23 @@ export function evaluatePolicy(store, root, policy, opts = {}) {
|
|
|
352
374
|
throw new Error("executable-behavior policies cannot participate in parent/exception composition");
|
|
353
375
|
return evaluateExecutableBehaviorPolicy(root, policy, opts.behavior);
|
|
354
376
|
}
|
|
355
|
-
|
|
377
|
+
// Read-only gates can supply an ephemeral scan of changed source. Executable
|
|
378
|
+
// behavior deliberately ignores it and keeps using its isolated checkout path.
|
|
379
|
+
const snapshot = opts.snapshot ?? graphSnapshot(store, root, opts);
|
|
356
380
|
return opts.composition?.length
|
|
357
381
|
? evaluateCompositePolicyOnSnapshot(policy, opts.composition, snapshot)
|
|
358
382
|
: evaluatePolicyOnSnapshot(policy, snapshot);
|
|
359
383
|
}
|
|
360
384
|
export function policyIsActive(policy) {
|
|
361
|
-
return policy.state === "active_advisory" || policy.state === "active_blocking"
|
|
385
|
+
return (policy.state === "active_advisory" || policy.state === "active_blocking")
|
|
386
|
+
&& !activationGateError(policy);
|
|
362
387
|
}
|
|
363
388
|
export function policyBlocks(policy, evaluation) {
|
|
364
389
|
return policy.state === "active_blocking"
|
|
365
390
|
&& !policy.exception_of
|
|
366
391
|
&& policy.severity === "blocking"
|
|
367
392
|
&& policy.authority?.kind === "human"
|
|
393
|
+
&& !activationGateError(policy)
|
|
368
394
|
&& evaluation.result === "violated";
|
|
369
395
|
}
|
|
370
396
|
export function mutationOperatorForPolicy(policy) {
|