@davesheffer/hunch 1.8.3 → 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 +92 -1
- package/dist/cli/index.js +1222 -397
- 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 +6 -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 +53 -10
- 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 +7 -2
- package/tooling/md1-benchmark.mjs +628 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
1
|
import { shortHash } from "../core/ids.js";
|
|
3
|
-
import {
|
|
2
|
+
import { stableRepositoryName } from "../extractors/git.js";
|
|
4
3
|
import { canonicalHash, policySemanticHash } from "./canonical.js";
|
|
5
4
|
import { policyCompositionBinding, policyProofHash } from "./composition.js";
|
|
6
5
|
import { graphSnapshot, mutationOperatorForPolicy, selectedPolicyForComposition } from "./evaluator.js";
|
|
7
6
|
import { createExecutableBehaviorProofPlan } from "./behaviorProof.js";
|
|
7
|
+
import { canonicalStaticGraphBaseline, isAncestorOrSame } from "./staticGraphBaseline.js";
|
|
8
|
+
import { replacementFreeExactCommit, replacementFreeFirstCommitForFile } from "./replacementFreeGit.js";
|
|
9
|
+
export { canonicalStaticGraphBaseline } from "./staticGraphBaseline.js";
|
|
8
10
|
import { POLICY_EVALUATOR, MUTATION_ENGINE, ProofPlanSchema, } from "./schema.js";
|
|
9
11
|
function clamp(value, fallback, min, max) {
|
|
10
12
|
if (value == null || !Number.isFinite(value))
|
|
@@ -46,19 +48,26 @@ export function createProofPlan(store, root, repository, policy, opts = {}) {
|
|
|
46
48
|
throw new Error("executable-behavior policies cannot have exception composition");
|
|
47
49
|
return createExecutableBehaviorProofPlan(root, repository, policy, { now: opts.now, privateOnly: true });
|
|
48
50
|
}
|
|
49
|
-
const
|
|
50
|
-
if (!
|
|
51
|
+
const repositoryHead = replacementFreeExactCommit(root, "HEAD");
|
|
52
|
+
if (!repositoryHead)
|
|
51
53
|
throw new Error("proof planning needs a Git repository with a current HEAD");
|
|
54
|
+
const head = canonicalStaticGraphBaseline(root, repositoryHead);
|
|
52
55
|
const composition = opts.composition ?? [];
|
|
53
56
|
const parentHash = policySemanticHash(policy);
|
|
54
57
|
const policyHash = policyProofHash(policy, composition);
|
|
55
58
|
const compositionBinding = policyCompositionBinding(policy, composition);
|
|
56
59
|
const corpus = repository.getCorpus(policy.id, opts);
|
|
60
|
+
// Proof plans are shared memory. A clone-local directory basename would mint
|
|
61
|
+
// different plan IDs for Architect, Developer, CI, and linked worktrees even
|
|
62
|
+
// when every one of them is looking at the same repository and policy. An
|
|
63
|
+
// existing immutable corpus remains the compatibility authority for artifacts
|
|
64
|
+
// created before stable repository identities were introduced.
|
|
65
|
+
const repositoryName = opts.repositoryName ?? corpus?.repository ?? stableRepositoryName(root);
|
|
57
66
|
if (corpus) {
|
|
58
67
|
if (corpus.policy_hash !== parentHash) {
|
|
59
68
|
throw new Error(`proof corpus ${corpus.id} is stale for policy ${policy.id}; re-import it after the policy semantic change`);
|
|
60
69
|
}
|
|
61
|
-
if (corpus.repository !==
|
|
70
|
+
if (corpus.repository !== repositoryName || corpus.data_class !== policy.data_class) {
|
|
62
71
|
throw new Error(`proof corpus ${corpus.id} does not match repository/data class for policy ${policy.id}`);
|
|
63
72
|
}
|
|
64
73
|
}
|
|
@@ -73,11 +82,19 @@ export function createProofPlan(store, root, repository, policy, opts = {}) {
|
|
|
73
82
|
.filter((ref) => ref.startsWith("dec_"))
|
|
74
83
|
.map(readDecision)
|
|
75
84
|
.find((record) => !!record);
|
|
76
|
-
const policyCommit =
|
|
85
|
+
const policyCommit = replacementFreeFirstCommitForFile(root, `.hunch/policies/${policy.id}.json`);
|
|
77
86
|
const sourceRef = sourceEvent?.commit ?? decision?.commit ?? (policyCommit || head);
|
|
78
|
-
|
|
87
|
+
const rawSource = replacementFreeExactCommit(root, sourceRef);
|
|
88
|
+
if (!rawSource)
|
|
79
89
|
throw new Error(`proof-plan source commit ${sourceRef} does not resolve in this repository`);
|
|
80
|
-
|
|
90
|
+
// Canonicalize the source independently of current HEAD. A policy introduced
|
|
91
|
+
// by a Hunch-only publication commit remains anchored to the indexed-code (or
|
|
92
|
+
// merge) boundary immediately before that publication, even after later code
|
|
93
|
+
// commits advance the current baseline.
|
|
94
|
+
const sourceCommit = canonicalStaticGraphBaseline(root, rawSource);
|
|
95
|
+
if (!isAncestorOrSame(root, sourceCommit, head)) {
|
|
96
|
+
throw new Error(`proof-plan source commit ${sourceCommit} is not an ancestor of canonical graph baseline ${head}`);
|
|
97
|
+
}
|
|
81
98
|
const structural = events.find((event) => event.structural_delta && (event.kind === "bug_fix" || event.kind === "revert" || event.kind === "decision"));
|
|
82
99
|
const structuralKnownBad = structural?.structural_delta
|
|
83
100
|
? [{
|
|
@@ -112,7 +129,7 @@ export function createProofPlan(store, root, repository, policy, opts = {}) {
|
|
|
112
129
|
const body = {
|
|
113
130
|
policy_id: policy.id,
|
|
114
131
|
policy_candidate_hash: policyHash,
|
|
115
|
-
repository:
|
|
132
|
+
repository: repositoryName,
|
|
116
133
|
data_class: policy.data_class,
|
|
117
134
|
source_commit: sourceCommit,
|
|
118
135
|
valid_from_commit: sourceCommit,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import { foreignRepoEnv } from "../extractors/git.js";
|
|
3
|
+
/** Constitution proof identity is the repository's real object graph, never a
|
|
4
|
+
* clone-local `refs/replace/*` or legacy graft view. Keep this environment
|
|
5
|
+
* private to proof planning so every traversal and ancestry check agrees with
|
|
6
|
+
* replay, which uses the same Git invariant. */
|
|
7
|
+
export function replacementFreeGitEnvironment(source = process.env) {
|
|
8
|
+
return {
|
|
9
|
+
...foreignRepoEnv(source),
|
|
10
|
+
GIT_NO_REPLACE_OBJECTS: "1",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function gitText(root, args, maxBuffer = 64 * 1024 * 1024) {
|
|
14
|
+
return execFileSync("git", ["-C", root, ...args], {
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
env: replacementFreeGitEnvironment(),
|
|
17
|
+
maxBuffer,
|
|
18
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
19
|
+
}).trim();
|
|
20
|
+
}
|
|
21
|
+
function gitTextSafe(root, args, maxBuffer) {
|
|
22
|
+
try {
|
|
23
|
+
return gitText(root, args, maxBuffer);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Resolve one commit-ish through the real object graph. */
|
|
30
|
+
export function replacementFreeExactCommit(root, ref) {
|
|
31
|
+
const oid = gitTextSafe(root, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).toLowerCase();
|
|
32
|
+
return /^[0-9a-f]{40,64}$/.test(oid) ? oid : null;
|
|
33
|
+
}
|
|
34
|
+
/** Files changed by an exact commit, ignoring local replacement objects. */
|
|
35
|
+
export function replacementFreeCommitFiles(root, commit) {
|
|
36
|
+
const out = gitTextSafe(root, ["diff-tree", "--no-commit-id", "--name-only", "-r", "--root", commit]);
|
|
37
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
38
|
+
}
|
|
39
|
+
export function replacementFreeCommitMeta(root, commit) {
|
|
40
|
+
const raw = gitTextSafe(root, ["show", "-s", "--format=%H%x1f%h%x1f%s%x1f%b%x1f%an%x1f%aI", commit]);
|
|
41
|
+
if (!raw)
|
|
42
|
+
return null;
|
|
43
|
+
const [sha = "", shortSha = "", subject = "", body = "", author = "", date = ""] = raw.split("\x1f");
|
|
44
|
+
if (!/^[0-9a-f]{40,64}$/i.test(sha))
|
|
45
|
+
return null;
|
|
46
|
+
return { sha, shortSha, subject, body, author, date, files: replacementFreeCommitFiles(root, sha) };
|
|
47
|
+
}
|
|
48
|
+
/** Exact introducing commit for a Git-native policy record. */
|
|
49
|
+
export function replacementFreeFirstCommitForFile(root, file) {
|
|
50
|
+
const added = gitTextSafe(root, ["log", "--diff-filter=A", "--format=%H", "--", file])
|
|
51
|
+
.split("\n")
|
|
52
|
+
.find(Boolean);
|
|
53
|
+
if (added)
|
|
54
|
+
return added;
|
|
55
|
+
return gitTextSafe(root, ["log", "--reverse", "--format=%H", "--", file])
|
|
56
|
+
.split("\n")
|
|
57
|
+
.find(Boolean) ?? "";
|
|
58
|
+
}
|
|
59
|
+
export function replacementFreeIsAncestorOrSame(root, ancestor, descendant) {
|
|
60
|
+
if (ancestor === descendant)
|
|
61
|
+
return true;
|
|
62
|
+
return spawnSync("git", ["-C", root, "merge-base", "--is-ancestor", ancestor, descendant], {
|
|
63
|
+
env: replacementFreeGitEnvironment(),
|
|
64
|
+
stdio: "ignore",
|
|
65
|
+
}).status === 0;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=replacementFreeGit.js.map
|
|
@@ -7,6 +7,7 @@ import { canonicalHash, proofEvaluationHash, proofPlanContentHash } from "./cano
|
|
|
7
7
|
import { assertCompositionBinding, policyProofHash } from "./composition.js";
|
|
8
8
|
import { evaluateCompositePolicyOnSnapshot, evaluatePolicyOnSnapshot } from "./evaluator.js";
|
|
9
9
|
import { loadReplaySnapshot, putReplaySnapshot } from "./replayCache.js";
|
|
10
|
+
import { hasUnsafeCheckoutAttributes } from "./safeCheckout.js";
|
|
10
11
|
import { POLICY_EVALUATOR, ProofPlanSchema, ReplayReceiptSchema, } from "./schema.js";
|
|
11
12
|
const ZERO_SHA = "0".repeat(40);
|
|
12
13
|
export const DEFAULT_REPLAY_WORKERS = 4;
|
|
@@ -26,6 +27,7 @@ export function replaySafeEnvironment(home, gitConfig) {
|
|
|
26
27
|
HOME: home,
|
|
27
28
|
GIT_CONFIG_GLOBAL: gitConfig,
|
|
28
29
|
GIT_CONFIG_NOSYSTEM: "1",
|
|
30
|
+
GIT_NO_REPLACE_OBJECTS: "1",
|
|
29
31
|
GIT_TERMINAL_PROMPT: "0",
|
|
30
32
|
GIT_LFS_SKIP_SMUDGE: "1",
|
|
31
33
|
HUNCH_PRIVATE_DIR: "",
|
|
@@ -204,6 +206,10 @@ export function replayProofPlan(root, policy, inputPlan, opts = {}) {
|
|
|
204
206
|
outcomes.set(commit, { commit, error_code: "timeout" });
|
|
205
207
|
continue;
|
|
206
208
|
}
|
|
209
|
+
if (hasUnsafeCheckoutAttributes(root, commit, env, { allowDisabledLfs: true })) {
|
|
210
|
+
outcomes.set(commit, { commit, error_code: "unsafe-checkout-attributes" });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
207
213
|
const cached = loadReplaySnapshot(root, commit, plan.data_class);
|
|
208
214
|
if (cached.status === "hit" && cached.snapshot) {
|
|
209
215
|
cacheStats.hits++;
|
|
@@ -6,7 +6,7 @@ import { writeFileAtomic } from "../core/io.js";
|
|
|
6
6
|
import { canonicalHash } from "./canonical.js";
|
|
7
7
|
import { graphSnapshotFromRecords } from "./evaluator.js";
|
|
8
8
|
import { DataClassSchema, POLICY_EVALUATOR } from "./schema.js";
|
|
9
|
-
export const REPLAY_CACHE_ENGINE = { name: "hunch-tsjs-static-index", version: "
|
|
9
|
+
export const REPLAY_CACHE_ENGINE = { name: "hunch-tsjs-static-index", version: "4" };
|
|
10
10
|
const ReplayGraphCacheSchema = z.object({
|
|
11
11
|
version: z.literal(1),
|
|
12
12
|
engine: z.object({ name: z.string().min(1), version: z.string().min(1) }),
|
|
@@ -13,7 +13,7 @@ let message;
|
|
|
13
13
|
try {
|
|
14
14
|
store = new HunchStore(hunchPathsForDir(input.graph));
|
|
15
15
|
store.json.ensureDirs();
|
|
16
|
-
indexRepo(store, input.checkout, { churn: false });
|
|
16
|
+
indexRepo(store, input.checkout, { churn: false, requireComplete: true });
|
|
17
17
|
message = {
|
|
18
18
|
commit: input.commit,
|
|
19
19
|
snapshot: graphSnapshot(store, input.root, { publicOnly: true, head: input.commit }),
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { writeFileAtomic } from "../core/io.js";
|
|
3
|
+
import { writeFileAtomic, writeFileAtomicIfAbsent } from "../core/io.js";
|
|
4
4
|
import { shortHash } from "../core/ids.js";
|
|
5
|
-
import { policySemanticHash, proofPlanContentHash } from "./canonical.js";
|
|
5
|
+
import { canonicalHash, policySemanticHash, proofPlanContentHash } from "./canonical.js";
|
|
6
6
|
import { proofCorpusContentHash } from "./corpus.js";
|
|
7
7
|
import { currentHistoryDispositions, historyDispositionContentHash, historyDispositionJudgmentHash } from "./disposition.js";
|
|
8
8
|
import { assertCompositionBinding, compositionDescendants, policyProofHash } from "./composition.js";
|
|
@@ -218,9 +218,51 @@ export class PolicyRepository {
|
|
|
218
218
|
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
219
219
|
return parsed;
|
|
220
220
|
}
|
|
221
|
-
|
|
221
|
+
/** Publish a new policy lifecycle record without overwriting a concurrent
|
|
222
|
+
* writer. Used by automated proposal materializers so human authority always
|
|
223
|
+
* wins a race. */
|
|
224
|
+
putPolicyIfAbsent(policy, opts = {}) {
|
|
225
|
+
const parsed = PolicySpecSchema.parse(policy);
|
|
226
|
+
if (opts.private && opts.public)
|
|
227
|
+
throw new Error("choose only one policy home");
|
|
228
|
+
const home = opts.private ? "private" : opts.public ? "public" : parsed.data_class !== "public" || this.store.unified ? "private" : "public";
|
|
229
|
+
if (home === "public" && parsed.data_class !== "public") {
|
|
230
|
+
throw new Error(`refusing to write ${parsed.data_class} policy ${parsed.id} into the public home`);
|
|
231
|
+
}
|
|
232
|
+
const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
|
|
233
|
+
const existing = this.getPolicy(parsed.id, homeOpts);
|
|
234
|
+
const otherHome = this.getPolicy(parsed.id, home === "public" ? { privateOnly: true } : { publicOnly: true });
|
|
235
|
+
if (existing && otherHome)
|
|
236
|
+
throw new Error(`policy ${parsed.id} exists in both public and private homes`);
|
|
237
|
+
if (existing)
|
|
238
|
+
return { policy: existing, created: false };
|
|
239
|
+
if (otherHome)
|
|
240
|
+
throw new Error(`policy ${parsed.id} already exists in the ${home === "public" ? "private" : "public"} home`);
|
|
241
|
+
const dir = this.dir(home, "policies");
|
|
242
|
+
mkdirSync(dir, { recursive: true });
|
|
243
|
+
if (writeFileAtomicIfAbsent(join(dir, `${parsed.id}.json`), encode(parsed))) {
|
|
244
|
+
const racedOtherHome = this.getPolicy(parsed.id, home === "public" ? { privateOnly: true } : { publicOnly: true });
|
|
245
|
+
if (racedOtherHome)
|
|
246
|
+
throw new Error(`policy ${parsed.id} was published concurrently in both public and private homes`);
|
|
247
|
+
return { policy: parsed, created: true };
|
|
248
|
+
}
|
|
249
|
+
const winner = this.getPolicy(parsed.id, homeOpts);
|
|
250
|
+
if (!winner)
|
|
251
|
+
throw new Error(`policy ${parsed.id} appeared concurrently but could not be read`);
|
|
252
|
+
return { policy: winner, created: false };
|
|
253
|
+
}
|
|
254
|
+
putProof(proof, policyId, opts = {}) {
|
|
222
255
|
const parsed = PolicyProofSchema.parse(proof);
|
|
223
|
-
|
|
256
|
+
if (opts.private && opts.public)
|
|
257
|
+
throw new Error("choose only one proof home");
|
|
258
|
+
const home = opts.private
|
|
259
|
+
? "private"
|
|
260
|
+
: opts.public
|
|
261
|
+
? "public"
|
|
262
|
+
: this.homeOfPolicy(policyId) ?? (parsed.data_class === "public" && !this.store.unified ? "public" : "private");
|
|
263
|
+
if (home === "public" && parsed.data_class !== "public") {
|
|
264
|
+
throw new Error(`refusing to write ${parsed.data_class} proof ${parsed.id} into the public home`);
|
|
265
|
+
}
|
|
224
266
|
const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
|
|
225
267
|
const policy = this.getPolicy(policyId, homeOpts);
|
|
226
268
|
if (policy) {
|
|
@@ -240,6 +282,50 @@ export class PolicyRepository {
|
|
|
240
282
|
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
241
283
|
return parsed;
|
|
242
284
|
}
|
|
285
|
+
/** Publish an immutable proof without replacing a concurrent writer. */
|
|
286
|
+
putProofIfAbsent(proof, policyId, opts = {}) {
|
|
287
|
+
const parsed = PolicyProofSchema.parse(proof);
|
|
288
|
+
if (opts.private && opts.public)
|
|
289
|
+
throw new Error("choose only one proof home");
|
|
290
|
+
const home = opts.private
|
|
291
|
+
? "private"
|
|
292
|
+
: opts.public
|
|
293
|
+
? "public"
|
|
294
|
+
: this.homeOfPolicy(policyId) ?? (parsed.data_class === "public" && !this.store.unified ? "public" : "private");
|
|
295
|
+
if (home === "public" && parsed.data_class !== "public") {
|
|
296
|
+
throw new Error(`refusing to write ${parsed.data_class} proof ${parsed.id} into the public home`);
|
|
297
|
+
}
|
|
298
|
+
const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
|
|
299
|
+
const policy = this.getPolicy(policyId, homeOpts);
|
|
300
|
+
if (policy) {
|
|
301
|
+
const composition = compositionDescendants(policy, this.listPolicies(homeOpts));
|
|
302
|
+
assertCompositionBinding(policy, composition, parsed.composition);
|
|
303
|
+
if (parsed.policy_hash !== policyProofHash(policy, composition))
|
|
304
|
+
throw new Error(`composite proof ${parsed.id} policy hash mismatch`);
|
|
305
|
+
if (composition.length) {
|
|
306
|
+
const plan = this.listPlans(homeOpts).find((candidate) => candidate.content_hash === parsed.plan_hash);
|
|
307
|
+
if (!plan || plan.policy_candidate_hash !== parsed.policy_hash)
|
|
308
|
+
throw new Error(`composite proof ${parsed.id} has no exact bound proof plan`);
|
|
309
|
+
assertCompositionBinding(policy, composition, plan.composition);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const existing = this.getProof(parsed.id, homeOpts);
|
|
313
|
+
if (existing) {
|
|
314
|
+
if (immutableProofHash(existing) !== immutableProofHash(parsed))
|
|
315
|
+
throw new Error(`proof ${parsed.id} already exists with different immutable content`);
|
|
316
|
+
return { proof: existing, created: false };
|
|
317
|
+
}
|
|
318
|
+
const dir = this.dir(home, "proofs");
|
|
319
|
+
mkdirSync(dir, { recursive: true });
|
|
320
|
+
if (writeFileAtomicIfAbsent(join(dir, `${parsed.id}.json`), encode(parsed)))
|
|
321
|
+
return { proof: parsed, created: true };
|
|
322
|
+
const winner = this.getProof(parsed.id, homeOpts);
|
|
323
|
+
if (!winner)
|
|
324
|
+
throw new Error(`proof ${parsed.id} appeared concurrently but could not be read`);
|
|
325
|
+
if (immutableProofHash(winner) !== immutableProofHash(parsed))
|
|
326
|
+
throw new Error(`proof ${parsed.id} appeared concurrently with different immutable content`);
|
|
327
|
+
return { proof: winner, created: false };
|
|
328
|
+
}
|
|
243
329
|
putPlan(plan, policyId, opts = {}) {
|
|
244
330
|
const parsed = validatePlan(plan);
|
|
245
331
|
if (opts.private && opts.public)
|
|
@@ -249,6 +335,9 @@ export class PolicyRepository {
|
|
|
249
335
|
: opts.public
|
|
250
336
|
? "public"
|
|
251
337
|
: this.homeOfPolicy(policyId) ?? (parsed.data_class === "public" && !this.store.unified ? "public" : "private");
|
|
338
|
+
if (home === "public" && parsed.data_class !== "public") {
|
|
339
|
+
throw new Error(`refusing to write ${parsed.data_class} proof plan ${parsed.id} into the public home`);
|
|
340
|
+
}
|
|
252
341
|
const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
|
|
253
342
|
const policy = this.getPolicy(policyId, homeOpts);
|
|
254
343
|
if (policy) {
|
|
@@ -262,6 +351,44 @@ export class PolicyRepository {
|
|
|
262
351
|
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
263
352
|
return parsed;
|
|
264
353
|
}
|
|
354
|
+
/** Publish an immutable proof plan without replacing a concurrent writer. */
|
|
355
|
+
putPlanIfAbsent(plan, policyId, opts = {}) {
|
|
356
|
+
const parsed = validatePlan(plan);
|
|
357
|
+
if (opts.private && opts.public)
|
|
358
|
+
throw new Error("choose only one proof-plan home");
|
|
359
|
+
const home = opts.private
|
|
360
|
+
? "private"
|
|
361
|
+
: opts.public
|
|
362
|
+
? "public"
|
|
363
|
+
: this.homeOfPolicy(policyId) ?? (parsed.data_class === "public" && !this.store.unified ? "public" : "private");
|
|
364
|
+
if (home === "public" && parsed.data_class !== "public") {
|
|
365
|
+
throw new Error(`refusing to write ${parsed.data_class} proof plan ${parsed.id} into the public home`);
|
|
366
|
+
}
|
|
367
|
+
const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
|
|
368
|
+
const policy = this.getPolicy(policyId, homeOpts);
|
|
369
|
+
if (policy) {
|
|
370
|
+
const composition = compositionDescendants(policy, this.listPolicies(homeOpts));
|
|
371
|
+
assertCompositionBinding(policy, composition, parsed.composition);
|
|
372
|
+
if (parsed.policy_candidate_hash !== policyProofHash(policy, composition))
|
|
373
|
+
throw new Error(`composite plan ${parsed.id} policy hash mismatch`);
|
|
374
|
+
}
|
|
375
|
+
const existing = this.getPlan(parsed.id, homeOpts);
|
|
376
|
+
if (existing) {
|
|
377
|
+
if (existing.content_hash !== parsed.content_hash)
|
|
378
|
+
throw new Error(`proof plan ${parsed.id} already exists with different immutable content`);
|
|
379
|
+
return { plan: existing, created: false };
|
|
380
|
+
}
|
|
381
|
+
const dir = this.dir(home, "plans");
|
|
382
|
+
mkdirSync(dir, { recursive: true });
|
|
383
|
+
if (writeFileAtomicIfAbsent(join(dir, `${parsed.id}.json`), encode(parsed)))
|
|
384
|
+
return { plan: parsed, created: true };
|
|
385
|
+
const winner = this.getPlan(parsed.id, homeOpts);
|
|
386
|
+
if (!winner)
|
|
387
|
+
throw new Error(`proof plan ${parsed.id} appeared concurrently but could not be read`);
|
|
388
|
+
if (winner.content_hash !== parsed.content_hash)
|
|
389
|
+
throw new Error(`proof plan ${parsed.id} appeared concurrently with different immutable content`);
|
|
390
|
+
return { plan: winner, created: false };
|
|
391
|
+
}
|
|
265
392
|
putCorpus(corpus, policyId) {
|
|
266
393
|
const parsed = validateCorpus(corpus);
|
|
267
394
|
if (parsed.policy_id !== policyId)
|
|
@@ -280,7 +407,12 @@ export class PolicyRepository {
|
|
|
280
407
|
}
|
|
281
408
|
putEvidence(event, opts = {}) {
|
|
282
409
|
const parsed = EvidenceEventSchema.parse(event);
|
|
283
|
-
|
|
410
|
+
if (opts.private && opts.public)
|
|
411
|
+
throw new Error("choose only one evidence home");
|
|
412
|
+
const home = opts.private ? "private" : opts.public ? "public" : parsed.data_class !== "public" || this.store.unified ? "private" : "public";
|
|
413
|
+
if (home === "public" && parsed.data_class !== "public") {
|
|
414
|
+
throw new Error(`refusing to write ${parsed.data_class} evidence ${parsed.id} into the public home`);
|
|
415
|
+
}
|
|
284
416
|
const dir = this.dir(home, "evidence");
|
|
285
417
|
mkdirSync(dir, { recursive: true });
|
|
286
418
|
writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
|
|
@@ -395,6 +527,10 @@ export class PolicyRepository {
|
|
|
395
527
|
return parsed;
|
|
396
528
|
}
|
|
397
529
|
}
|
|
530
|
+
function immutableProofHash(proof) {
|
|
531
|
+
const { generated_at: _generatedAt, ...payload } = proof;
|
|
532
|
+
return canonicalHash(payload);
|
|
533
|
+
}
|
|
398
534
|
function validatePlan(raw) {
|
|
399
535
|
const plan = ProofPlanSchema.parse(raw);
|
|
400
536
|
const hash = proofPlanContentHash(plan);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
const CHECKOUT_TRANSFORM_ATTRIBUTES = ["filter", "working-tree-encoding", "ident", "eol", "text", "crlf"];
|
|
6
|
+
const MAX_ATTRIBUTE_BYTES = 64 * 1024 * 1024;
|
|
7
|
+
function nulFields(bytes) {
|
|
8
|
+
const fields = [];
|
|
9
|
+
let start = 0;
|
|
10
|
+
for (let end = bytes.indexOf(0, start); end !== -1; end = bytes.indexOf(0, start)) {
|
|
11
|
+
fields.push(bytes.subarray(start, end));
|
|
12
|
+
start = end + 1;
|
|
13
|
+
}
|
|
14
|
+
if (start < bytes.length)
|
|
15
|
+
fields.push(bytes.subarray(start));
|
|
16
|
+
return fields;
|
|
17
|
+
}
|
|
18
|
+
/** Inspect the exact target tree plus repository-local info attributes without
|
|
19
|
+
* checking it out. Any checkout transform could execute code or make the worktree
|
|
20
|
+
* bytes diverge from the raw blobs bound by receipts, so fail closed. Merge
|
|
21
|
+
* attributes are intentionally excluded: worktree materialization never invokes a
|
|
22
|
+
* merge driver. LFS is allowed only when callers explicitly disable its
|
|
23
|
+
* smudge/process hooks. */
|
|
24
|
+
export function hasUnsafeCheckoutAttributes(root, commit, env, opts = {}) {
|
|
25
|
+
const session = mkdtempSync(join(tmpdir(), "hunch-attr-index-"));
|
|
26
|
+
const index = join(session, "index");
|
|
27
|
+
const exactEnv = { ...env, GIT_INDEX_FILE: index, GIT_NO_REPLACE_OBJECTS: "1", GIT_ATTR_NOSYSTEM: "1" };
|
|
28
|
+
try {
|
|
29
|
+
execFileSync("git", ["-C", root, "read-tree", commit], {
|
|
30
|
+
env: exactEnv,
|
|
31
|
+
timeout: 10_000,
|
|
32
|
+
maxBuffer: MAX_ATTRIBUTE_BYTES,
|
|
33
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
34
|
+
});
|
|
35
|
+
const paths = execFileSync("git", ["-C", root, "ls-files", "-z"], {
|
|
36
|
+
env: exactEnv,
|
|
37
|
+
timeout: 10_000,
|
|
38
|
+
maxBuffer: MAX_ATTRIBUTE_BYTES,
|
|
39
|
+
encoding: "buffer",
|
|
40
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
41
|
+
});
|
|
42
|
+
if (!paths.length)
|
|
43
|
+
return false;
|
|
44
|
+
const raw = execFileSync("git", ["-C", root, "check-attr", "--cached", "-z", "--stdin", ...CHECKOUT_TRANSFORM_ATTRIBUTES], {
|
|
45
|
+
env: exactEnv,
|
|
46
|
+
input: paths,
|
|
47
|
+
timeout: 10_000,
|
|
48
|
+
maxBuffer: MAX_ATTRIBUTE_BYTES,
|
|
49
|
+
encoding: "buffer",
|
|
50
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
51
|
+
});
|
|
52
|
+
const fields = nulFields(raw);
|
|
53
|
+
if (fields.length % 3 !== 0)
|
|
54
|
+
return true;
|
|
55
|
+
for (let index = 0; index < fields.length; index += 3) {
|
|
56
|
+
const attribute = fields[index + 1].toString("utf8");
|
|
57
|
+
const value = fields[index + 2].toString("utf8");
|
|
58
|
+
if (!CHECKOUT_TRANSFORM_ATTRIBUTES.includes(attribute))
|
|
59
|
+
return true;
|
|
60
|
+
if (value === "unspecified" || value === "unset")
|
|
61
|
+
continue;
|
|
62
|
+
if (opts.allowDisabledLfs && attribute === "filter" && value === "lfs")
|
|
63
|
+
continue;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
rmSync(session, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=safeCheckout.js.map
|
|
@@ -4,6 +4,10 @@ export const POLICY_IR_VERSION = 1;
|
|
|
4
4
|
export const POLICY_EVALUATOR = { name: "hunch-graph-policy", version: "1.3.0" };
|
|
5
5
|
export const MUTATION_ENGINE = { name: "hunch-static-graph-controls", version: "5" };
|
|
6
6
|
export const EXECUTABLE_BEHAVIOR_IR_VERSION = 2;
|
|
7
|
+
/** Source-gated correction policies deliberately use an IR unknown to pre-Matrix
|
|
8
|
+
* clients. Those clients then reject the artifact instead of loading it without
|
|
9
|
+
* understanding (and potentially bypassing) its activation gate. */
|
|
10
|
+
export const CORRECTION_POLICY_IR_VERSION = 3;
|
|
7
11
|
export const BEHAVIOR_POLICY_EVALUATOR = { name: "hunch-executable-behavior", version: "1.0.0" };
|
|
8
12
|
export const BEHAVIOR_MUTATION_ENGINE = { name: "hunch-behavior-controls", version: "1" };
|
|
9
13
|
export const DataClassSchema = z.enum(["public", "private", "secret"]);
|
|
@@ -195,10 +199,20 @@ export const PolicyAuthoritySchema = z.object({
|
|
|
195
199
|
event: z.string().min(1),
|
|
196
200
|
at: z.string().datetime({ offset: true }),
|
|
197
201
|
});
|
|
202
|
+
export const PolicyActivationGateSchema = z.object({
|
|
203
|
+
kind: z.literal("source_currentness"),
|
|
204
|
+
status: z.literal("blocked"),
|
|
205
|
+
reason: z.string().min(1),
|
|
206
|
+
}).strict();
|
|
198
207
|
export const PolicySpecSchema = z.object({
|
|
199
208
|
id: z.string().regex(/^pol_[a-f0-9]{10}$/),
|
|
200
209
|
topic: z.string().min(1),
|
|
201
|
-
|
|
210
|
+
origin: z.enum(["generic", "correction_md1a"]).default("generic"),
|
|
211
|
+
ir_version: z.union([
|
|
212
|
+
z.literal(POLICY_IR_VERSION),
|
|
213
|
+
z.literal(EXECUTABLE_BEHAVIOR_IR_VERSION),
|
|
214
|
+
z.literal(CORRECTION_POLICY_IR_VERSION),
|
|
215
|
+
]),
|
|
202
216
|
revision: z.number().int().min(1),
|
|
203
217
|
state: PolicyStateSchema,
|
|
204
218
|
statement: z.string().min(1),
|
|
@@ -208,6 +222,7 @@ export const PolicySpecSchema = z.object({
|
|
|
208
222
|
severity: z.enum(["advisory", "warning", "blocking"]).default("warning"),
|
|
209
223
|
surfaces: z.array(z.enum(["pre_edit", "pre_commit", "ci", "mcp", "cli"])).default(["cli", "mcp"]),
|
|
210
224
|
authority: PolicyAuthoritySchema.nullable().default(null),
|
|
225
|
+
activation_gate: PolicyActivationGateSchema.nullable().default(null),
|
|
211
226
|
evidence: z.array(z.string()).default([]),
|
|
212
227
|
proof: z.string().nullable().default(null),
|
|
213
228
|
reversal_conditions: z.array(z.string()).default([]),
|
|
@@ -225,11 +240,21 @@ export const PolicySpecSchema = z.object({
|
|
|
225
240
|
updated_at: z.string().datetime({ offset: true }),
|
|
226
241
|
provenance: ProvenanceSchema,
|
|
227
242
|
}).passthrough().superRefine((policy, context) => {
|
|
228
|
-
if (policy.
|
|
229
|
-
|
|
243
|
+
if (policy.origin === "correction_md1a") {
|
|
244
|
+
if (policy.assertion.kind === "executable-behavior") {
|
|
245
|
+
context.addIssue({ code: "custom", path: ["assertion", "kind"], message: "source-gated correction policies require a graph assertion" });
|
|
246
|
+
}
|
|
247
|
+
if (policy.ir_version !== CORRECTION_POLICY_IR_VERSION) {
|
|
248
|
+
context.addIssue({ code: "custom", path: ["ir_version"], message: `source-gated correction policies require Policy IR v${CORRECTION_POLICY_IR_VERSION}` });
|
|
249
|
+
}
|
|
230
250
|
}
|
|
231
|
-
|
|
232
|
-
|
|
251
|
+
else {
|
|
252
|
+
if (policy.assertion.kind === "executable-behavior" && policy.ir_version !== EXECUTABLE_BEHAVIOR_IR_VERSION) {
|
|
253
|
+
context.addIssue({ code: "custom", path: ["ir_version"], message: `executable-behavior requires Policy IR v${EXECUTABLE_BEHAVIOR_IR_VERSION}` });
|
|
254
|
+
}
|
|
255
|
+
if (policy.assertion.kind !== "executable-behavior" && policy.ir_version !== POLICY_IR_VERSION) {
|
|
256
|
+
context.addIssue({ code: "custom", path: ["ir_version"], message: `graph assertions require Policy IR v${POLICY_IR_VERSION}` });
|
|
257
|
+
}
|
|
233
258
|
}
|
|
234
259
|
});
|
|
235
260
|
export const PolicyCompositionMemberSchema = z.object({
|