@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
|
@@ -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({
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
2
|
import { headSha } from "../extractors/git.js";
|
|
3
3
|
import { canonicalHash, canonicalJson } from "./canonical.js";
|
|
4
|
+
import { replacementFreeGitEnvironment } from "./replacementFreeGit.js";
|
|
4
5
|
import { shortHash } from "../core/ids.js";
|
|
5
6
|
import { compileDecisionPolicy } from "./compiler.js";
|
|
6
|
-
import { evaluatePolicy, policyBlocks
|
|
7
|
-
import { approvePolicy, blockingProofError, demotePolicy, linkPolicyException, proposeProvedPolicy, retirePolicy, withdrawPolicy } from "./lifecycle.js";
|
|
7
|
+
import { evaluatePolicy, policyBlocks } from "./evaluator.js";
|
|
8
|
+
import { activationGateError, approvePolicy, blockingProofError, demotePolicy, linkPolicyException, proposeProvedPolicy, retirePolicy, withdrawPolicy } from "./lifecycle.js";
|
|
8
9
|
import { provePolicy } from "./proof.js";
|
|
9
10
|
import { PolicyRepository } from "./repository.js";
|
|
10
11
|
import { bootstrapPolicies } from "./bootstrap.js";
|
|
@@ -31,8 +32,19 @@ import { evaluateExecutableBehaviorPolicy } from "./behaviorEvaluator.js";
|
|
|
31
32
|
import { executeG2OperationalDrill } from "./g2Drills.js";
|
|
32
33
|
import { G3_REQUIRED_EXPERIMENTS, G3EvidenceRepository, compileExperimentPreregistration, compileG3Plan, compileProofReviewMeasurement, scoreG3Readiness, } from "./g3.js";
|
|
33
34
|
import { executeG3AdapterConformance, g3ConformanceSourceHash } from "./g3Conformance.js";
|
|
34
|
-
import { ExperimentRepository, assignmentTreatment, buildExperimentReport, compileExperimentCaseBank, compileExperimentFollowup, compileExperimentOutcome, compileExperimentReviewStart, compileExperimentRun, compileExperimentStop, compileExp03ReviewResponse, currentExperimentOutcomes, normalizedEditDistance, } from "./experiment.js";
|
|
35
|
+
import { ExperimentRepository, assignmentTreatment, buildExperimentReport, compileExperimentCaseBank, compileExperimentFollowup, compileExperimentOutcome, compileExperimentReviewStart, compileExperimentReviewerQualification, compileExperimentRun, compileExperimentStop, compileExp03ReviewResponse, currentExperimentOutcomes, experimentReviewGuide, normalizedEditDistance, } from "./experiment.js";
|
|
35
36
|
import { executeExp01Assignment } from "./experimentRunner.js";
|
|
37
|
+
import { materializeCorrectionPolicies, materializeCorrectionPolicy, } from "./correctionPolicyMaterializer.js";
|
|
38
|
+
export function policyEvaluationEnvelope(result) {
|
|
39
|
+
return {
|
|
40
|
+
...result.evaluation,
|
|
41
|
+
enforcement: {
|
|
42
|
+
blocks: result.blocks,
|
|
43
|
+
strict_error: result.strict_error,
|
|
44
|
+
gate_error: result.gate_error ?? null,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
36
48
|
function relationSummary(policy) {
|
|
37
49
|
return {
|
|
38
50
|
id: policy.id,
|
|
@@ -76,6 +88,7 @@ export function shadowCommitEligible(root, policy, commit) {
|
|
|
76
88
|
const sourceCommit = policy.assertion.test.source_commit;
|
|
77
89
|
const check = spawnSync("git", ["-C", root, "merge-base", "--is-ancestor", sourceCommit, commit], {
|
|
78
90
|
encoding: "utf8",
|
|
91
|
+
env: replacementFreeGitEnvironment(),
|
|
79
92
|
stdio: ["ignore", "ignore", "pipe"],
|
|
80
93
|
});
|
|
81
94
|
if (check.status === 0)
|
|
@@ -492,11 +505,22 @@ export class ConstitutionService {
|
|
|
492
505
|
const bank = this.experimentRepository.listCaseBanks().find((item) => item.id === run.case_bank_id);
|
|
493
506
|
if (!bank)
|
|
494
507
|
throw new Error(`run ${run.id} is missing exact case bank ${run.case_bank_id}`);
|
|
508
|
+
const preregistration = this.g3Repository.listExperiments().find((item) => item.id === run.preregistration_id && item.content_hash === run.preregistration_hash);
|
|
509
|
+
const usesPlainLanguageReview = bank.cases.some((item) => "required_relationship" in item);
|
|
510
|
+
if (usesPlainLanguageReview && !preregistration)
|
|
511
|
+
throw new Error(`run ${run.id} is missing exact preregistration ${run.preregistration_id}`);
|
|
512
|
+
if (preregistration && preregistration.revision >= 2) {
|
|
513
|
+
const qualification = this.experimentRepository.listReviewerQualifications().find((item) => item.preregistration_id === preregistration.id && item.preregistration_hash === preregistration.content_hash && item.reviewer === reviewer);
|
|
514
|
+
if (!qualification)
|
|
515
|
+
throw new Error(`${reviewer} must pass the excluded plain-language comprehension check before a revision-${preregistration.revision} timed review`);
|
|
516
|
+
const targetReviewers = new Set(bank.cases.map((item) => item.strata.target_reviewer).filter(Boolean));
|
|
517
|
+
if (targetReviewers.has(reviewer) || targetReviewers.has(reviewer.replace(/^human:/i, "")))
|
|
518
|
+
throw new Error(`${reviewer} cannot perform timed reviews because the same actor labeled revision-${preregistration.revision} targets`);
|
|
519
|
+
}
|
|
495
520
|
// Single-operator mitigation (expreg_9c9617cd13, revision >= 3): at least 48 hours
|
|
496
521
|
// must separate the case-bank lock from the FIRST review start — enforced, not
|
|
497
522
|
// merely auditable, so a violation is impossible rather than post-hoc visible.
|
|
498
|
-
|
|
499
|
-
if (prereg && prereg.revision >= 3) {
|
|
523
|
+
if (preregistration && preregistration.revision >= 3) {
|
|
500
524
|
const elapsed = Date.parse(opts.now ?? new Date().toISOString()) - Date.parse(bank.locked_at);
|
|
501
525
|
const hasStart = this.experimentRepository.listReviewStarts().some((item) => item.run_id === run.id);
|
|
502
526
|
if (!hasStart && elapsed < 48 * 3_600_000) {
|
|
@@ -518,7 +542,13 @@ export class ConstitutionService {
|
|
|
518
542
|
if (!assignment)
|
|
519
543
|
throw new Error(`no unreviewed EXP-03 assignment is available for ${reviewer}`);
|
|
520
544
|
const start = existing ?? this.experimentRepository.putReviewStart(compileExperimentReviewStart(run, assignment, reviewer, opts));
|
|
521
|
-
return { start, assignment, treatment: assignmentTreatment(bank, run, assignment) };
|
|
545
|
+
return { start, assignment, treatment: assignmentTreatment(bank, run, assignment), review_guide: experimentReviewGuide(assignment.arm) };
|
|
546
|
+
}
|
|
547
|
+
qualifyExperimentReviewer(input, opts = {}) {
|
|
548
|
+
const preregistration = this.g3Repository.currentExperiments().find((item) => item.experiment === "EXP-03");
|
|
549
|
+
if (!preregistration)
|
|
550
|
+
throw new Error("no current EXP-03 preregistration");
|
|
551
|
+
return this.experimentRepository.putReviewerQualification(compileExperimentReviewerQualification(input, preregistration, opts));
|
|
522
552
|
}
|
|
523
553
|
/** Resolve an EXP-03 run/assignment/case triple (the shared lookup for both
|
|
524
554
|
* review-submission dialects). */
|
|
@@ -676,6 +706,7 @@ export class ConstitutionService {
|
|
|
676
706
|
const manifest = this.g2Repository.currentPlan();
|
|
677
707
|
const commits = manifest ? execFileSync("git", ["-C", this.root, "rev-list", "--first-parent", `--max-count=${maxCommits}`, "HEAD"], {
|
|
678
708
|
encoding: "utf8",
|
|
709
|
+
env: replacementFreeGitEnvironment(),
|
|
679
710
|
stdio: ["ignore", "pipe", "ignore"],
|
|
680
711
|
}).trim().split("\n").filter((commit) => /^[a-f0-9]{40}$/.test(commit)) : [];
|
|
681
712
|
const attempted = (manifest?.policy_ids.length ?? 0) * commits.length;
|
|
@@ -927,6 +958,12 @@ export class ConstitutionService {
|
|
|
927
958
|
ingest(opts = {}) {
|
|
928
959
|
return ingestLocalEvidence(this.store, this.root, this.repository, opts);
|
|
929
960
|
}
|
|
961
|
+
upgradeCorrection(id, opts = {}) {
|
|
962
|
+
return materializeCorrectionPolicy(this.store, this.root, this.repository, id, opts);
|
|
963
|
+
}
|
|
964
|
+
upgradeCorrections(opts = {}) {
|
|
965
|
+
return materializeCorrectionPolicies(this.store, this.root, this.repository, opts);
|
|
966
|
+
}
|
|
930
967
|
plan(id, opts = {}) {
|
|
931
968
|
const policy = this.get(id, opts);
|
|
932
969
|
const home = opts.publicOnly ? "public" : opts.privateOnly ? "private" : this.repository.homeOfPolicy(id);
|
|
@@ -944,7 +981,7 @@ export class ConstitutionService {
|
|
|
944
981
|
const publicOnly = policy.data_class === "public" && this.repository.homeOfPolicy(id) === "public";
|
|
945
982
|
const homeOpts = publicOnly ? { publicOnly: true } : { privateOnly: true };
|
|
946
983
|
const composition = this.composition(policy, homeOpts);
|
|
947
|
-
const plan = this.plan(id, { publicOnly, now: opts.now });
|
|
984
|
+
const plan = this.plan(id, { publicOnly, now: opts.now, repositoryName: opts.repositoryName });
|
|
948
985
|
const proof = provePolicy(this.store, this.root, policy, { publicOnly, now: opts.now, plan, composition });
|
|
949
986
|
this.repository.putProof(proof, policy.id);
|
|
950
987
|
const proposed = proposeProvedPolicy(policy, proof, opts.now ?? proof.generated_at, composition, this.g2BehaviorAttestationRepository.current());
|
|
@@ -1020,13 +1057,18 @@ export class ConstitutionService {
|
|
|
1020
1057
|
throw new Error(`historical shadow backfill supports executable-behavior policies only; ${policy.id} is ${policy.assertion.kind}`);
|
|
1021
1058
|
}
|
|
1022
1059
|
else {
|
|
1023
|
-
evaluation = evaluatePolicy(this.store, this.root, policy, {
|
|
1060
|
+
evaluation = evaluatePolicy(this.store, this.root, policy, {
|
|
1061
|
+
publicOnly: home === "public",
|
|
1062
|
+
composition,
|
|
1063
|
+
snapshot: opts.snapshot,
|
|
1064
|
+
behavior: opts.behavior,
|
|
1065
|
+
});
|
|
1024
1066
|
}
|
|
1025
1067
|
const latencyMs = opts.latencyMs ?? performance.now() - started;
|
|
1026
1068
|
const sameMatches = canonicalJson(evaluation.matches);
|
|
1027
1069
|
const alsoDetectedBy = evaluation.result === "violated"
|
|
1028
1070
|
&& (!opts.commit || opts.commit === currentHead)
|
|
1029
|
-
? this.evaluate({ activeOnly: true, publicOnly: home === "public" })
|
|
1071
|
+
? this.evaluate({ activeOnly: true, publicOnly: home === "public", snapshot: opts.snapshot, behavior: opts.behavior })
|
|
1030
1072
|
.filter((candidate) => candidate.policy.id !== id
|
|
1031
1073
|
&& candidate.evaluation.result === "violated"
|
|
1032
1074
|
&& canonicalJson(candidate.evaluation.matches) === sameMatches)
|
|
@@ -1057,7 +1099,10 @@ export class ConstitutionService {
|
|
|
1057
1099
|
const audit = this.repository.listShadowDispositions(opts).filter((record) => record.policy_id === id);
|
|
1058
1100
|
const current = currentShadowDispositions(audit);
|
|
1059
1101
|
const history = this.repository.listDispositions(opts).filter((record) => record.policy_id === id && record.proof_id === proof.id);
|
|
1060
|
-
const scoringRecords = records.filter((record) => shadowCommitEligible(this.root, policy,
|
|
1102
|
+
const scoringRecords = records.filter((record) => shadowCommitEligible(this.root, policy,
|
|
1103
|
+
// Workspace receipts retain their content-addressed pseudo-head for
|
|
1104
|
+
// dedupe, but ancestry eligibility is anchored to the real base commit.
|
|
1105
|
+
record.evaluation.repository.base ?? record.evaluation.repository.head));
|
|
1061
1106
|
const report = scoreShadowPrecision(policy, proof, scoringRecords, audit, history, thresholds);
|
|
1062
1107
|
return {
|
|
1063
1108
|
...report,
|
|
@@ -1183,11 +1228,21 @@ export class ConstitutionService {
|
|
|
1183
1228
|
}
|
|
1184
1229
|
evaluate(opts = {}) {
|
|
1185
1230
|
let policies = opts.id ? [this.get(opts.id, opts)] : this.list(opts);
|
|
1186
|
-
|
|
1187
|
-
|
|
1231
|
+
// Select the persisted active lifecycle states, including invalid/tampered
|
|
1232
|
+
// ones. The evaluator below must surface their gate/proof error so strict CI
|
|
1233
|
+
// fails visibly; filtering through policyIsActive would silently erase the
|
|
1234
|
+
// exact fail-closed configuration errors this seam is responsible for.
|
|
1235
|
+
if (opts.activeOnly) {
|
|
1236
|
+
policies = policies.filter((policy) => policy.state === "active_advisory" || policy.state === "active_blocking");
|
|
1237
|
+
}
|
|
1188
1238
|
return policies.map((policy) => {
|
|
1189
1239
|
const composition = this.composition(policy, opts);
|
|
1190
|
-
const evaluation = evaluatePolicy(this.store, this.root, policy, {
|
|
1240
|
+
const evaluation = evaluatePolicy(this.store, this.root, policy, {
|
|
1241
|
+
publicOnly: opts.publicOnly,
|
|
1242
|
+
composition,
|
|
1243
|
+
behavior: opts.behavior,
|
|
1244
|
+
snapshot: opts.snapshot,
|
|
1245
|
+
});
|
|
1191
1246
|
let proof;
|
|
1192
1247
|
let dispositions = [];
|
|
1193
1248
|
if (policy.state === "active_blocking" && policy.proof) {
|
|
@@ -1197,7 +1252,12 @@ export class ConstitutionService {
|
|
|
1197
1252
|
dispositions = this.repository.listDispositions(homeOpts).filter((record) => record.policy_id === policy.id && record.proof_id === policy.proof);
|
|
1198
1253
|
}
|
|
1199
1254
|
const behaviorBindingError = executableBehaviorAttestationError(policy, this.g2BehaviorAttestationRepository.current());
|
|
1200
|
-
const
|
|
1255
|
+
const activeActivationError = policy.state === "active_advisory" || policy.state === "active_blocking"
|
|
1256
|
+
? activationGateError(policy)
|
|
1257
|
+
: null;
|
|
1258
|
+
const gateError = behaviorBindingError
|
|
1259
|
+
?? activeActivationError
|
|
1260
|
+
?? blockingProofError(policy, proof, dispositions, composition, this.g2BehaviorAttestationRepository.current());
|
|
1201
1261
|
return {
|
|
1202
1262
|
policy,
|
|
1203
1263
|
evaluation,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join, posix, relative } from "node:path";
|
|
2
|
+
import { closeSync, constants, fstatSync, ftruncateSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, realpathSync, rmSync, writeFileSync, writeSync, } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { hunchPathsForDir } from "../core/paths.js";
|
|
5
5
|
import { symbolId } from "../core/ids.js";
|
|
6
6
|
import { externalPackage } from "../core/externalImports.js";
|
|
@@ -11,6 +11,7 @@ import { parseSource } from "../extractors/parse.js";
|
|
|
11
11
|
import { HunchStore } from "../store/hunchStore.js";
|
|
12
12
|
import { canonicalHash } from "./canonical.js";
|
|
13
13
|
import { evaluatePolicyOnSnapshot, graphSnapshot } from "./evaluator.js";
|
|
14
|
+
import { hasUnsafeCheckoutAttributes } from "./safeCheckout.js";
|
|
14
15
|
function safeEnvironment(home, gitConfig) {
|
|
15
16
|
const env = {};
|
|
16
17
|
for (const key of ["PATH", "SystemRoot", "WINDIR", "TMPDIR", "TMP", "TEMP"]) {
|
|
@@ -22,6 +23,7 @@ function safeEnvironment(home, gitConfig) {
|
|
|
22
23
|
HOME: home,
|
|
23
24
|
GIT_CONFIG_GLOBAL: gitConfig,
|
|
24
25
|
GIT_CONFIG_NOSYSTEM: "1",
|
|
26
|
+
GIT_NO_REPLACE_OBJECTS: "1",
|
|
25
27
|
GIT_TERMINAL_PROMPT: "0",
|
|
26
28
|
GIT_LFS_SKIP_SMUDGE: "1",
|
|
27
29
|
HUNCH_PRIVATE_DIR: "",
|
|
@@ -232,6 +234,37 @@ function removeWorktree(root, hooks, env, checkout) {
|
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
}
|
|
237
|
+
function openRegularSourceNoFollow(checkout, sourceFile) {
|
|
238
|
+
const file = join(checkout, sourceFile);
|
|
239
|
+
const before = lstatSync(file);
|
|
240
|
+
if (before.isSymbolicLink())
|
|
241
|
+
throw new Error("mutation-source-symlink-unsupported");
|
|
242
|
+
if (!before.isFile())
|
|
243
|
+
throw new Error("mutation-source-not-regular");
|
|
244
|
+
const canonicalCheckout = realpathSync(checkout);
|
|
245
|
+
const canonicalFile = realpathSync(file);
|
|
246
|
+
const fromCheckout = relative(canonicalCheckout, canonicalFile);
|
|
247
|
+
if (!fromCheckout || fromCheckout === ".." || fromCheckout.startsWith(`..${sep}`) || isAbsolute(fromCheckout)
|
|
248
|
+
|| canonicalFile !== resolve(canonicalCheckout, sourceFile)) {
|
|
249
|
+
throw new Error("mutation-source-outside-checkout");
|
|
250
|
+
}
|
|
251
|
+
let descriptor;
|
|
252
|
+
try {
|
|
253
|
+
descriptor = openSync(file, constants.O_RDWR | constants.O_NOFOLLOW);
|
|
254
|
+
const opened = fstatSync(descriptor);
|
|
255
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino) {
|
|
256
|
+
throw new Error("mutation-source-changed-before-open");
|
|
257
|
+
}
|
|
258
|
+
return descriptor;
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
if (descriptor !== undefined)
|
|
262
|
+
closeSync(descriptor);
|
|
263
|
+
if (error.code === "ELOOP")
|
|
264
|
+
throw new Error("mutation-source-symlink-unsupported");
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
235
268
|
/** Apply one primary mutation to an immutable disposable source checkout. No
|
|
236
269
|
* project script, build, test, provider, model, or repository hook executes. */
|
|
237
270
|
export function runSourceMutation(root, policy, base) {
|
|
@@ -255,6 +288,9 @@ export function runSourceMutation(root, policy, base) {
|
|
|
255
288
|
try {
|
|
256
289
|
if (unsafeLocalFilter(root, env))
|
|
257
290
|
throw new Error("unsafe-local-filter-config");
|
|
291
|
+
if (hasUnsafeCheckoutAttributes(root, base.head, env, { allowDisabledLfs: true })) {
|
|
292
|
+
throw new Error("unsafe-checkout-attributes");
|
|
293
|
+
}
|
|
258
294
|
execFileSync("git", gitArgs(root, hooks, ["worktree", "add", "--detach", "--force", checkout, base.head]), {
|
|
259
295
|
env,
|
|
260
296
|
timeout: 30_000,
|
|
@@ -266,15 +302,32 @@ export function runSourceMutation(root, policy, base) {
|
|
|
266
302
|
const sourceFile = subject?.file ?? (subjectComponent ? componentFiles(base, subjectComponent)[0] : undefined);
|
|
267
303
|
if (!sourceFile)
|
|
268
304
|
throw new Error("mutation-subject-unresolved");
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
305
|
+
const descriptor = openRegularSourceNoFollow(checkout, sourceFile);
|
|
306
|
+
let mutation;
|
|
307
|
+
try {
|
|
308
|
+
const attempted = mutateSource(policy, base, sourceFile, readFileSync(descriptor, "utf8"));
|
|
309
|
+
if ("error" in attempted)
|
|
310
|
+
throw new Error(attempted.error);
|
|
311
|
+
mutation = attempted;
|
|
312
|
+
if (mutation.file !== sourceFile)
|
|
313
|
+
throw new Error("mutation-source-target-changed");
|
|
314
|
+
const parsed = parseSource(mutation.file, mutation.source);
|
|
315
|
+
if (!parsed?.parseable)
|
|
316
|
+
throw new Error("mutation-source-unparseable");
|
|
317
|
+
ftruncateSync(descriptor, 0);
|
|
318
|
+
const bytes = Buffer.from(mutation.source, "utf8");
|
|
319
|
+
let written = 0;
|
|
320
|
+
while (written < bytes.length) {
|
|
321
|
+
const count = writeSync(descriptor, bytes, written, bytes.length - written, written);
|
|
322
|
+
if (!count)
|
|
323
|
+
throw new Error("mutation-source-write-incomplete");
|
|
324
|
+
written += count;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
closeSync(descriptor);
|
|
329
|
+
}
|
|
330
|
+
const diff = execFileSync("git", gitArgs(checkout, hooks, ["diff", "--no-ext-diff", "--no-textconv", "--", mutation.file]), {
|
|
278
331
|
env,
|
|
279
332
|
encoding: "utf8",
|
|
280
333
|
timeout: 10_000,
|
|
@@ -286,7 +339,7 @@ export function runSourceMutation(root, policy, base) {
|
|
|
286
339
|
throw new Error("mutation-source-diff-too-large");
|
|
287
340
|
store = new HunchStore(hunchPathsForDir(graph));
|
|
288
341
|
store.json.ensureDirs();
|
|
289
|
-
indexRepo(store, checkout, { churn: false });
|
|
342
|
+
indexRepo(store, checkout, { churn: false, requireComplete: true });
|
|
290
343
|
const snapshot = graphSnapshot(store, root, { publicOnly: true, head: base.head });
|
|
291
344
|
outcome = {
|
|
292
345
|
snapshot,
|