@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
|
@@ -4,9 +4,11 @@ import { basename, resolve } from "node:path";
|
|
|
4
4
|
import { shortHash } from "../core/ids.js";
|
|
5
5
|
import { parseDocAnchors } from "../core/docanchors.js";
|
|
6
6
|
import { toPosixTarget } from "../core/paths.js";
|
|
7
|
-
import {
|
|
7
|
+
import { isHumanConfirmed } from "../core/strictgate.js";
|
|
8
|
+
import { stableRepositoryName } from "../extractors/git.js";
|
|
8
9
|
import { durationCutoff } from "./bootstrap.js";
|
|
9
10
|
import { canonicalHash } from "./canonical.js";
|
|
11
|
+
import { replacementFreeCommitMeta, replacementFreeExactCommit, replacementFreeGitEnvironment, } from "./replacementFreeGit.js";
|
|
10
12
|
import { EvidenceEventSchema, EvidenceImportSchema, } from "./schema.js";
|
|
11
13
|
const MAX_INSTRUCTION_FILES = 64;
|
|
12
14
|
const MAX_INSTRUCTION_FILE_BYTES = 512 * 1024;
|
|
@@ -55,6 +57,7 @@ function committedInstructionFiles(root) {
|
|
|
55
57
|
try {
|
|
56
58
|
const raw = execFileSync("git", ["-C", root, "ls-tree", "-r", "-z", "--name-only", "HEAD"], {
|
|
57
59
|
encoding: "buffer",
|
|
60
|
+
env: replacementFreeGitEnvironment(),
|
|
58
61
|
maxBuffer: 16 * 1024 * 1024,
|
|
59
62
|
stdio: ["ignore", "pipe", "ignore"],
|
|
60
63
|
});
|
|
@@ -69,12 +72,14 @@ function committedFileAt(root, revision, file, maxBytes) {
|
|
|
69
72
|
const object = `${revision}:${file}`;
|
|
70
73
|
const size = Number(execFileSync("git", ["-C", root, "cat-file", "-s", object], {
|
|
71
74
|
encoding: "utf8",
|
|
75
|
+
env: replacementFreeGitEnvironment(),
|
|
72
76
|
stdio: ["ignore", "pipe", "ignore"],
|
|
73
77
|
}).trim());
|
|
74
78
|
if (!Number.isFinite(size) || size < 0 || size > maxBytes)
|
|
75
79
|
return null;
|
|
76
80
|
return execFileSync("git", ["-C", root, "cat-file", "blob", object], {
|
|
77
81
|
encoding: "utf8",
|
|
82
|
+
env: replacementFreeGitEnvironment(),
|
|
78
83
|
maxBuffer: maxBytes + 1,
|
|
79
84
|
stdio: ["ignore", "pipe", "ignore"],
|
|
80
85
|
});
|
|
@@ -102,6 +107,7 @@ function authoredInstructionCommit(root, file, sourceHash) {
|
|
|
102
107
|
file,
|
|
103
108
|
], {
|
|
104
109
|
encoding: "utf8",
|
|
110
|
+
env: replacementFreeGitEnvironment(),
|
|
105
111
|
maxBuffer: 1024 * 1024,
|
|
106
112
|
stdio: ["ignore", "pipe", "ignore"],
|
|
107
113
|
}).split(/\r?\n/).filter(Boolean);
|
|
@@ -135,12 +141,12 @@ function instructionEvents(root, dataClass, policies, constraints) {
|
|
|
135
141
|
totalBytes += Buffer.byteLength(source, "utf8");
|
|
136
142
|
const sourceHash = canonicalHash(source);
|
|
137
143
|
const introducedAt = authoredInstructionCommit(root, file, sourceHash);
|
|
138
|
-
|
|
144
|
+
const commit = introducedAt ? replacementFreeExactCommit(root, introducedAt) : null;
|
|
145
|
+
if (!commit) {
|
|
139
146
|
excluded++;
|
|
140
147
|
continue;
|
|
141
148
|
}
|
|
142
|
-
const
|
|
143
|
-
const meta = commitMeta(commit, root);
|
|
149
|
+
const meta = replacementFreeCommitMeta(root, commit);
|
|
144
150
|
if (!meta || !Number.isFinite(Date.parse(meta.date))) {
|
|
145
151
|
excluded++;
|
|
146
152
|
continue;
|
|
@@ -213,9 +219,10 @@ function importEvents(root, file, store, repository, opts, policies, constraints
|
|
|
213
219
|
}
|
|
214
220
|
let commit;
|
|
215
221
|
if (item.commit) {
|
|
216
|
-
|
|
222
|
+
const exactCommit = replacementFreeExactCommit(root, item.commit);
|
|
223
|
+
if (!exactCommit)
|
|
217
224
|
throw new Error(`evidence import item ${item.id} commit ${item.commit} does not resolve`);
|
|
218
|
-
commit =
|
|
225
|
+
commit = exactCommit;
|
|
219
226
|
}
|
|
220
227
|
const files = [...new Set(item.files.map(normalizeRepoFile))].sort();
|
|
221
228
|
const symbols = [...new Set(item.symbols)].sort();
|
|
@@ -294,7 +301,9 @@ function isPrivateRecord(store, kind, id, opts) {
|
|
|
294
301
|
return false;
|
|
295
302
|
return !!store.getPrivateRec(kind, id);
|
|
296
303
|
}
|
|
297
|
-
|
|
304
|
+
/** Canonical evidence projection for one captured correction. Materializers may
|
|
305
|
+
* reuse this projection, but ingestion itself remains evidence-only. */
|
|
306
|
+
export function correctionEvidenceEvent(root, constraint, dataClass) {
|
|
298
307
|
const occurredAt = constraint.valid_from ?? constraint.provenance.last_verified;
|
|
299
308
|
if (!occurredAt || !Number.isFinite(Date.parse(occurredAt)))
|
|
300
309
|
return null;
|
|
@@ -306,16 +315,16 @@ function correctionEvent(root, constraint, isPrivate) {
|
|
|
306
315
|
match: constraint.match,
|
|
307
316
|
source_decision: constraint.source_decision,
|
|
308
317
|
});
|
|
309
|
-
|
|
318
|
+
return EvidenceEventSchema.parse({
|
|
310
319
|
id: `ev_${shortHash(`correction:${contentHash}`)}`,
|
|
311
320
|
kind: "correction",
|
|
312
321
|
occurred_at: occurredAt,
|
|
313
|
-
repository:
|
|
322
|
+
repository: stableRepositoryName(root),
|
|
314
323
|
files: constraint.scope.filter((scope) => scope !== "**"),
|
|
315
324
|
symbols: constraint.forbids?.symbols ?? [],
|
|
316
325
|
text_ref: constraint.id,
|
|
317
326
|
related_records: [constraint.id, ...(constraint.source_decision ? [constraint.source_decision] : [])],
|
|
318
|
-
data_class:
|
|
327
|
+
data_class: dataClass,
|
|
319
328
|
content_hash: contentHash,
|
|
320
329
|
compiler: {
|
|
321
330
|
status: "covered",
|
|
@@ -329,11 +338,19 @@ function correctionEvent(root, constraint, isPrivate) {
|
|
|
329
338
|
last_verified: constraint.provenance.last_verified,
|
|
330
339
|
},
|
|
331
340
|
});
|
|
332
|
-
|
|
341
|
+
}
|
|
342
|
+
function correctionEvent(store, root, constraint, isPrivate) {
|
|
343
|
+
// A public evidence event may only name a source decision that is itself
|
|
344
|
+
// public. Missing/private-only ids are excluded before serialization so a
|
|
345
|
+
// machine without the overlay cannot accidentally declassify the reference.
|
|
346
|
+
if (!isPrivate && constraint.source_decision && !store.json.get("decisions", constraint.source_decision))
|
|
347
|
+
return null;
|
|
348
|
+
const event = correctionEvidenceEvent(root, constraint, isPrivate ? "private" : "public");
|
|
349
|
+
return event ? { occurredAt: event.occurred_at, event, private: isPrivate } : null;
|
|
333
350
|
}
|
|
334
351
|
function bugEvent(root, store, repository, bug, isPrivate, opts) {
|
|
335
352
|
const commit = bug.lineage.fixed_commit ?? bug.lineage.introduced_commit;
|
|
336
|
-
const meta = commit ?
|
|
353
|
+
const meta = commit ? replacementFreeCommitMeta(root, commit) : null;
|
|
337
354
|
const occurredAt = meta?.date ?? bug.provenance.last_verified;
|
|
338
355
|
if (!occurredAt || !Number.isFinite(Date.parse(occurredAt)))
|
|
339
356
|
return null;
|
|
@@ -411,11 +428,11 @@ export function ingestLocalEvidence(store, root, repository, opts = {}) {
|
|
|
411
428
|
let scanned = records.constraints.length + records.bugs.length;
|
|
412
429
|
let excluded = 0;
|
|
413
430
|
for (const constraint of records.constraints) {
|
|
414
|
-
if (constraint.status !== "active" || !constraint.provenance.source
|
|
431
|
+
if (constraint.status !== "active" || !isHumanConfirmed(constraint.provenance.source)) {
|
|
415
432
|
excluded++;
|
|
416
433
|
continue;
|
|
417
434
|
}
|
|
418
|
-
const item = correctionEvent(root, constraint, isPrivateRecord(store, "constraints", constraint.id, opts));
|
|
435
|
+
const item = correctionEvent(store, root, constraint, isPrivateRecord(store, "constraints", constraint.id, opts));
|
|
419
436
|
if (!item || Date.parse(item.occurredAt) < minDate)
|
|
420
437
|
excluded++;
|
|
421
438
|
else
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { mkdirSync, mkdtempSync, rmSync,
|
|
4
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { headSha
|
|
6
|
+
import { headSha } from "../extractors/git.js";
|
|
7
7
|
import { canonicalHash } from "./canonical.js";
|
|
8
8
|
import { nodeTestInfrastructureError } from "./g2BehaviorCandidates.js";
|
|
9
|
-
import { dependencySnapshotForCommit } from "./g2BehaviorDependencies.js";
|
|
9
|
+
import { dependencySnapshotForCommit, materializeDependencySnapshot } from "./g2BehaviorDependencies.js";
|
|
10
10
|
import { cleanupReplayWorktree, hasUnsafeReplayFilter, replayGitArgs } from "./replay.js";
|
|
11
|
+
import { hasUnsafeCheckoutAttributes } from "./safeCheckout.js";
|
|
12
|
+
import { replacementFreeExactCommit, replacementFreeGitEnvironment } from "./replacementFreeGit.js";
|
|
11
13
|
import { BEHAVIOR_POLICY_EVALUATOR, PolicyEvaluationSchema, } from "./schema.js";
|
|
12
14
|
import { NODE_TEST_REPORTER_SOURCE, exactNodeTestPattern, nodeTestIsolationFlag, nodeTestReporterEvents, } from "./nodeTestEvidence.js";
|
|
13
15
|
import { applyBehaviorWorkspace, BehaviorWorkspaceError, captureBehaviorWorkspace, } from "./behaviorWorkspace.js";
|
|
@@ -22,6 +24,7 @@ function behaviorEnvironment(home, gitConfig) {
|
|
|
22
24
|
HOME: home,
|
|
23
25
|
GIT_CONFIG_GLOBAL: gitConfig,
|
|
24
26
|
GIT_CONFIG_NOSYSTEM: "1",
|
|
27
|
+
GIT_NO_REPLACE_OBJECTS: "1",
|
|
25
28
|
GIT_TERMINAL_PROMPT: "0",
|
|
26
29
|
GIT_LFS_SKIP_SMUDGE: "1",
|
|
27
30
|
HUNCH_PRIVATE_DIR: "",
|
|
@@ -31,10 +34,17 @@ function behaviorEnvironment(home, gitConfig) {
|
|
|
31
34
|
FORCE_COLOR: "0",
|
|
32
35
|
};
|
|
33
36
|
}
|
|
37
|
+
function behaviorWorkspaceCaptureEnvironment(home, gitConfig) {
|
|
38
|
+
const env = behaviorEnvironment(home, gitConfig);
|
|
39
|
+
if (process.env.GIT_INDEX_FILE)
|
|
40
|
+
env.GIT_INDEX_FILE = process.env.GIT_INDEX_FILE;
|
|
41
|
+
return env;
|
|
42
|
+
}
|
|
34
43
|
function sourceAt(root, commit, file) {
|
|
35
44
|
try {
|
|
36
45
|
return execFileSync("git", ["-C", root, "show", `${commit}:${file}`], {
|
|
37
46
|
encoding: "utf8",
|
|
47
|
+
env: replacementFreeGitEnvironment(),
|
|
38
48
|
maxBuffer: 10 * 1024 * 1024,
|
|
39
49
|
stdio: ["ignore", "pipe", "ignore"],
|
|
40
50
|
});
|
|
@@ -74,7 +84,7 @@ export function evaluateExecutableBehaviorPolicy(root, policy, opts = {}) {
|
|
|
74
84
|
let workspace;
|
|
75
85
|
if (opts.workspace && /^[a-f0-9]{40}$/.test(commit)) {
|
|
76
86
|
const probe = mkdtempSync(join(tmpdir(), "hunch-behavior-workspace-probe-"));
|
|
77
|
-
const env =
|
|
87
|
+
const env = behaviorWorkspaceCaptureEnvironment(probe, join(probe, "global.gitconfig"));
|
|
78
88
|
try {
|
|
79
89
|
writeFileSync(join(probe, "global.gitconfig"), "");
|
|
80
90
|
workspace = captureBehaviorWorkspace(root, commit, opts.workspace, env);
|
|
@@ -103,7 +113,7 @@ export function evaluateExecutableBehaviorPolicy(root, policy, opts = {}) {
|
|
|
103
113
|
exit_code: null,
|
|
104
114
|
selected_event: null,
|
|
105
115
|
};
|
|
106
|
-
if (!/^[a-f0-9]{40}$/.test(commit) ||
|
|
116
|
+
if (!/^[a-f0-9]{40}$/.test(commit) || replacementFreeExactCommit(root, commit) !== commit) {
|
|
107
117
|
return evaluation(policy, baseExecution.commit, { ...baseExecution, error_code: "commit-ref-unresolved" }, "error", "executable behavior commit does not resolve");
|
|
108
118
|
}
|
|
109
119
|
const source = sourceAt(root, assertion.test.source_commit, assertion.test.file);
|
|
@@ -132,6 +142,9 @@ export function evaluateExecutableBehaviorPolicy(root, policy, opts = {}) {
|
|
|
132
142
|
if (hasUnsafeReplayFilter(root, env)) {
|
|
133
143
|
result = evaluation(policy, commit, { ...baseExecution, commit, dependency_snapshot_id: dependency.snapshot.id, error_code: "unsafe-local-filter-config" }, "error", "repository has an unsafe local Git content filter");
|
|
134
144
|
}
|
|
145
|
+
else if (hasUnsafeCheckoutAttributes(root, commit, env, { allowDisabledLfs: true })) {
|
|
146
|
+
result = evaluation(policy, commit, { ...baseExecution, commit, dependency_snapshot_id: dependency.snapshot.id, error_code: "unsafe-checkout-attributes" }, "error", "repository has checkout attributes that could transform or merge exact source bytes");
|
|
147
|
+
}
|
|
135
148
|
else {
|
|
136
149
|
execFileSync("git", replayGitArgs(root, hooks, ["worktree", "add", "--detach", "--force", checkout, commit]), {
|
|
137
150
|
env,
|
|
@@ -141,7 +154,7 @@ export function evaluateExecutableBehaviorPolicy(root, policy, opts = {}) {
|
|
|
141
154
|
added = true;
|
|
142
155
|
if (workspace)
|
|
143
156
|
applyBehaviorWorkspace(root, checkout, workspace, env);
|
|
144
|
-
|
|
157
|
+
materializeDependencySnapshot(dependency, join(checkout, "node_modules"));
|
|
145
158
|
const testFile = join(checkout, assertion.test.file);
|
|
146
159
|
mkdirSync(dirname(testFile), { recursive: true });
|
|
147
160
|
writeFileSync(testFile, source);
|
|
@@ -155,7 +168,7 @@ export function evaluateExecutableBehaviorPolicy(root, policy, opts = {}) {
|
|
|
155
168
|
];
|
|
156
169
|
const args = assertion.runner === "node-test"
|
|
157
170
|
? common
|
|
158
|
-
: [join(
|
|
171
|
+
: [join(checkout, "node_modules", "tsx", "dist", "cli.mjs"), ...common];
|
|
159
172
|
const run = spawnSync(process.execPath, args, {
|
|
160
173
|
cwd: checkout,
|
|
161
174
|
env,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
1
|
import { shortHash } from "../core/ids.js";
|
|
3
2
|
import { headSha } from "../extractors/git.js";
|
|
4
3
|
import { evaluateExecutableBehaviorPolicy } from "./behaviorEvaluator.js";
|
|
@@ -81,7 +80,9 @@ export function createExecutableBehaviorProofPlan(root, repository, policy, opts
|
|
|
81
80
|
const body = {
|
|
82
81
|
policy_id: policy.id,
|
|
83
82
|
policy_candidate_hash: policyHash,
|
|
84
|
-
|
|
83
|
+
// Bind to the immutable corpus label so a teammate in a differently named
|
|
84
|
+
// clone reuses the same executable-behavior plan instead of minting a fork.
|
|
85
|
+
repository: corpus.repository,
|
|
85
86
|
data_class: policy.data_class,
|
|
86
87
|
source_commit: policy.assertion.test.source_commit,
|
|
87
88
|
valid_from_commit: policy.assertion.test.source_commit,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { sha1, shortHash } from "../core/ids.js";
|
|
2
|
+
import { compareCodeUnits } from "../core/canonicalOrder.js";
|
|
2
3
|
function canonicalValue(value) {
|
|
3
4
|
if (Array.isArray(value))
|
|
4
5
|
return value.map(canonicalValue);
|
|
5
6
|
if (value && typeof value === "object") {
|
|
6
7
|
return Object.fromEntries(Object.entries(value)
|
|
7
8
|
.filter(([, v]) => v !== undefined)
|
|
8
|
-
.sort(([a], [b]) => a
|
|
9
|
+
.sort(([a], [b]) => compareCodeUnits(a, b))
|
|
9
10
|
.map(([k, v]) => [k, canonicalValue(v)]));
|
|
10
11
|
}
|
|
11
12
|
return value;
|
|
@@ -23,6 +24,11 @@ export function canonicalHash(value) {
|
|
|
23
24
|
export function policySemanticHash(policy) {
|
|
24
25
|
return canonicalHash({
|
|
25
26
|
id: policy.id,
|
|
27
|
+
// Preserve existing generic-policy hashes while binding the correction
|
|
28
|
+
// compiler's provenance and permanent MD-1a activation gate into every
|
|
29
|
+
// correction proof. Removing either makes that proof stale.
|
|
30
|
+
...(policy.origin !== "generic" ? { origin: policy.origin } : {}),
|
|
31
|
+
...(policy.activation_gate ? { activation_gate: policy.activation_gate } : {}),
|
|
26
32
|
ir_version: policy.ir_version,
|
|
27
33
|
statement: policy.statement,
|
|
28
34
|
scope: policy.scope,
|
|
@@ -14,8 +14,9 @@ export function buildProofCard(policy, proof, dispositions = [], composition = [
|
|
|
14
14
|
const baselineClean = proof.current.total === 1 && proof.current.satisfied === 1 && proof.current.unknown === 0 && proof.current.error === 0;
|
|
15
15
|
const dispositionAssessment = assessHistoryDispositions(proof, dispositions);
|
|
16
16
|
const evidenceError = blockingEvidenceError(proof, dispositions);
|
|
17
|
+
const activationError = policy.activation_gate?.status === "blocked" ? policy.activation_gate.reason : null;
|
|
17
18
|
const proofStrongEnough = proofRank[proof.proof_class] >= proofRank.P3;
|
|
18
|
-
const eligible = semanticMatch && baselineClean && proofStrongEnough && !evidenceError;
|
|
19
|
+
const eligible = semanticMatch && baselineClean && proofStrongEnough && !evidenceError && !activationError;
|
|
19
20
|
const canBlock = eligible && policy.state === "active_blocking" && policy.severity === "blocking" && policy.authority?.kind === "human";
|
|
20
21
|
const unknownResults = proof.current.unknown + proof.known_bad.unknown + proof.known_good.unknown + proof.accepted_history.unknown + proof.mutations.unknown;
|
|
21
22
|
const errorResults = proof.current.error + proof.known_bad.error + proof.known_good.error + proof.accepted_history.error + proof.mutations.error;
|
|
@@ -36,6 +37,8 @@ export function buildProofCard(policy, proof, dispositions = [], composition = [
|
|
|
36
37
|
actions.push("Repair every failed required mutation control before considering blocking approval.");
|
|
37
38
|
if (policy.candidate.conflicts.length)
|
|
38
39
|
actions.push("Resolve every direct candidate conflict with a human disposition before lifecycle promotion.");
|
|
40
|
+
if (activationError)
|
|
41
|
+
actions.push(`Activation is mechanically blocked: ${activationError}`);
|
|
39
42
|
if (shadowPrecision?.recommendation === "eligible_for_p4_review")
|
|
40
43
|
actions.push("Shadow thresholds are met; a human may review P4 evidence, but measurement grants no authority.");
|
|
41
44
|
else if (shadowPrecision)
|
|
@@ -57,6 +60,7 @@ export function buildProofCard(policy, proof, dispositions = [], composition = [
|
|
|
57
60
|
evidence: [...policy.evidence],
|
|
58
61
|
candidate: policy.candidate,
|
|
59
62
|
exception_of: policy.exception_of,
|
|
63
|
+
activation_gate: policy.activation_gate,
|
|
60
64
|
},
|
|
61
65
|
proof: { id: proof.id, proof_class: proof.proof_class, plan_hash: proof.plan_hash, generated_at: proof.generated_at },
|
|
62
66
|
evidence_vector: {
|
|
@@ -88,7 +92,7 @@ export function buildProofCard(policy, proof, dispositions = [], composition = [
|
|
|
88
92
|
current: policy.authority,
|
|
89
93
|
eligible_for_human_blocking_approval: eligible,
|
|
90
94
|
can_block_now: canBlock,
|
|
91
|
-
blocking_evidence_error: evidenceError,
|
|
95
|
+
blocking_evidence_error: activationError ?? evidenceError,
|
|
92
96
|
},
|
|
93
97
|
actions,
|
|
94
98
|
};
|
|
@@ -108,6 +112,7 @@ export function renderProofCard(card) {
|
|
|
108
112
|
` candidate alternatives: ${card.policy.candidate.alternatives.length} · conflicts: ${card.policy.candidate.conflicts.length} · incumbent: ${card.policy.candidate.incumbent ?? "none"}`,
|
|
109
113
|
` scope suggestion: ${card.policy.candidate.scope_suggestion ? JSON.stringify(card.policy.candidate.scope_suggestion) : "none — narrow compiled scope retained"}`,
|
|
110
114
|
` exception parent: ${card.policy.exception_of ?? "none"}`,
|
|
115
|
+
...(card.policy.activation_gate ? [` activation gate: ${card.policy.activation_gate.status} — ${card.policy.activation_gate.reason}`] : []),
|
|
111
116
|
` ${line("current", card.evidence_vector.current)}`,
|
|
112
117
|
` ${line("known bad", card.evidence_vector.known_bad)}`,
|
|
113
118
|
` ${line("known good", card.evidence_vector.known_good)}`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { policyId } from "./canonical.js";
|
|
2
|
-
import { POLICY_IR_VERSION, PolicySpecSchema } from "./schema.js";
|
|
2
|
+
import { CORRECTION_POLICY_IR_VERSION, POLICY_IR_VERSION, PolicySpecSchema } from "./schema.js";
|
|
3
3
|
function selector(ref) {
|
|
4
4
|
if (ref.startsWith("symbol-id:") || ref.startsWith("symbol:"))
|
|
5
5
|
return { selector: ref };
|
|
@@ -173,4 +173,74 @@ export function compileStructuralPolicy(store, input) {
|
|
|
173
173
|
});
|
|
174
174
|
return { policy, private: isPrivate };
|
|
175
175
|
}
|
|
176
|
+
/** Compile one already-bound correction projection. The caller must first prove
|
|
177
|
+
* that the projection has exactly one supported structural interpretation. */
|
|
178
|
+
export function compileCorrectionPolicy(store, input) {
|
|
179
|
+
const isPrivate = input.dataClass !== "public";
|
|
180
|
+
if (isPrivate && !store.hasPrivate)
|
|
181
|
+
throw new Error("private correction compilation needs a configured Hunch private overlay");
|
|
182
|
+
const now = input.now ?? new Date().toISOString();
|
|
183
|
+
const id = policyId({ assertion: input.assertion, scope: input.scope, data_class: input.dataClass });
|
|
184
|
+
const refs = [...new Set([
|
|
185
|
+
input.source.id,
|
|
186
|
+
input.evidenceId,
|
|
187
|
+
...(input.source.source_decision ? [input.source.source_decision] : []),
|
|
188
|
+
])].sort();
|
|
189
|
+
const policy = PolicySpecSchema.parse({
|
|
190
|
+
id,
|
|
191
|
+
topic: `correction.${input.source.id}`,
|
|
192
|
+
origin: "correction_md1a",
|
|
193
|
+
ir_version: CORRECTION_POLICY_IR_VERSION,
|
|
194
|
+
revision: 1,
|
|
195
|
+
state: "compiled",
|
|
196
|
+
statement: input.source.statement,
|
|
197
|
+
rationale: input.source.rationale,
|
|
198
|
+
scope: input.scope,
|
|
199
|
+
assertion: input.assertion,
|
|
200
|
+
severity: "warning",
|
|
201
|
+
surfaces: ["cli", "mcp", "ci"],
|
|
202
|
+
authority: null,
|
|
203
|
+
activation_gate: {
|
|
204
|
+
kind: "source_currentness",
|
|
205
|
+
status: "blocked",
|
|
206
|
+
reason: "MD-1a correction projections cannot activate until MD-2 proves that every source correction and decision is still current.",
|
|
207
|
+
},
|
|
208
|
+
evidence: refs,
|
|
209
|
+
proof: null,
|
|
210
|
+
reversal_conditions: [
|
|
211
|
+
`Source correction ${input.source.id} is retired or its exact structural interpretation is rejected.`,
|
|
212
|
+
...(input.source.source_decision ? [`Source decision ${input.source.source_decision} is superseded or explicitly closed.`] : []),
|
|
213
|
+
],
|
|
214
|
+
supersedes: null,
|
|
215
|
+
superseded_by: null,
|
|
216
|
+
exception_of: null,
|
|
217
|
+
valid_from: null,
|
|
218
|
+
valid_to: null,
|
|
219
|
+
data_class: input.dataClass,
|
|
220
|
+
limitations: [
|
|
221
|
+
"Scope is intentionally limited to one exact file and anchored to one stable symbol in that file.",
|
|
222
|
+
"TypeScript/JavaScript static ESM import declarations only; re-exports, require(), dynamic import(), package aliases, and runtime loading are not covered.",
|
|
223
|
+
"The immediate legacy Constraint remains the fast guard; this MD-1a policy cannot activate until the source-currentness gate is implemented and cleared.",
|
|
224
|
+
],
|
|
225
|
+
candidate: input.candidate,
|
|
226
|
+
legacy_refs: [input.source.id, ...(input.source.source_decision ? [input.source.source_decision] : [])],
|
|
227
|
+
audit: [{
|
|
228
|
+
action: "compiled",
|
|
229
|
+
actor_kind: "system",
|
|
230
|
+
actor: "hunch:correction-policy-materializer",
|
|
231
|
+
at: now,
|
|
232
|
+
reason: `Compiled from one deterministic supported projection bound to correction ${input.source.id}.`,
|
|
233
|
+
proof: null,
|
|
234
|
+
}],
|
|
235
|
+
created_at: now,
|
|
236
|
+
updated_at: now,
|
|
237
|
+
provenance: {
|
|
238
|
+
source: "derived",
|
|
239
|
+
confidence: 1,
|
|
240
|
+
evidence: refs,
|
|
241
|
+
last_verified: now,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
return { policy, private: isPrivate };
|
|
245
|
+
}
|
|
176
246
|
//# sourceMappingURL=compiler.js.map
|