@davesheffer/hunch 1.10.2 → 1.10.3
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/dist/cli/index.js
CHANGED
|
@@ -68,7 +68,7 @@ import { computeDrift } from "../core/drift.js";
|
|
|
68
68
|
import { renderCompilerScorecard, scoreCompilerCaseBank } from "../constitution/scorecard.js";
|
|
69
69
|
import { generateWiki, wikiStatus, wikiPrompt, publicHome, privateHome, readWikiManifestAt, nowData } from "../wiki/wiki.js";
|
|
70
70
|
import { adoptProsePrompt } from "../wiki/adopt.js";
|
|
71
|
-
import { topicCollisions, renderGrounding } from "../core/topics.js";
|
|
71
|
+
import { topicCollisions, renderGrounding, isInForce } from "../core/topics.js";
|
|
72
72
|
import { pendingEscalations, policyEscalations } from "../core/escalations.js";
|
|
73
73
|
import { parseDocAnchors, renderDocGrounding } from "../core/docanchors.js";
|
|
74
74
|
import { compareCandidates } from "../core/compare.js";
|
|
@@ -3420,7 +3420,7 @@ vetoCmd
|
|
|
3420
3420
|
let drafted = 0;
|
|
3421
3421
|
let touched = 0;
|
|
3422
3422
|
for (const d of store.json.loadAll("decisions")) {
|
|
3423
|
-
if (d
|
|
3423
|
+
if (!isInForce(d))
|
|
3424
3424
|
continue;
|
|
3425
3425
|
if (!d.alternatives_rejected.length)
|
|
3426
3426
|
continue;
|
|
@@ -30,9 +30,16 @@ export function planPolicyRepair(renames, policies) {
|
|
|
30
30
|
rewrites.push({ id: p.id, field: "scope.paths", from: path, to });
|
|
31
31
|
}
|
|
32
32
|
if (p.assertion.kind !== "executable-behavior") {
|
|
33
|
+
// must-pass-through carries a THIRD selector (`via`). Omitting it meant an
|
|
34
|
+
// ordinary rename left the via binding pointing at the old symbol: the policy
|
|
35
|
+
// either bricked (its semantic hash moved, invalidating the proof, while the
|
|
36
|
+
// stale via could never be re-proved) or silently stopped enforcing. This runs
|
|
37
|
+
// automatically from the post-commit hook, so neither outcome was visible.
|
|
33
38
|
const selectors = p.assertion.kind === "exists"
|
|
34
39
|
? [p.assertion.subject.selector]
|
|
35
|
-
:
|
|
40
|
+
: p.assertion.kind === "must-pass-through"
|
|
41
|
+
? [p.assertion.subject.selector, p.assertion.via.selector, p.assertion.object.selector]
|
|
42
|
+
: [p.assertion.subject.selector, p.assertion.object.selector];
|
|
36
43
|
for (const raw of selectors) {
|
|
37
44
|
const healed = repairSelector(raw, map);
|
|
38
45
|
if (healed !== raw)
|
|
@@ -54,11 +61,18 @@ export function repairPolicySpec(policy, rewrites, at) {
|
|
|
54
61
|
? policy.assertion
|
|
55
62
|
: policy.assertion.kind === "exists"
|
|
56
63
|
? { ...policy.assertion, subject: { selector: subSelector(policy.assertion.subject.selector) } }
|
|
57
|
-
:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
: policy.assertion.kind === "must-pass-through"
|
|
65
|
+
? {
|
|
66
|
+
...policy.assertion,
|
|
67
|
+
subject: { selector: subSelector(policy.assertion.subject.selector) },
|
|
68
|
+
via: { selector: subSelector(policy.assertion.via.selector) },
|
|
69
|
+
object: { selector: subSelector(policy.assertion.object.selector) },
|
|
70
|
+
}
|
|
71
|
+
: {
|
|
72
|
+
...policy.assertion,
|
|
73
|
+
subject: { selector: subSelector(policy.assertion.subject.selector) },
|
|
74
|
+
object: { selector: subSelector(policy.assertion.object.selector) },
|
|
75
|
+
};
|
|
62
76
|
return {
|
|
63
77
|
...policy,
|
|
64
78
|
revision: policy.revision + 1,
|
package/dist/core/conformance.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { externalImportNodeId } from "./externalImports.js";
|
|
2
|
+
import { isInForce } from "./topics.js";
|
|
2
3
|
function resolveSymbols(graph, ref) {
|
|
3
4
|
const syms = graph.symbols;
|
|
4
5
|
if (ref.startsWith("sym_"))
|
|
@@ -102,8 +103,8 @@ export function checkConformance(store, opts = {}) {
|
|
|
102
103
|
const graph = opts.graph ?? { symbols: load("symbols"), edges: load("edges") };
|
|
103
104
|
const out = [];
|
|
104
105
|
for (const d of load("decisions")) {
|
|
105
|
-
if (d
|
|
106
|
-
continue; // in-force
|
|
106
|
+
if (!isInForce(d))
|
|
107
|
+
continue; // in-force only — a REJECTED intent is not an intent
|
|
107
108
|
for (const p of d.conformance ?? [])
|
|
108
109
|
out.push(evalPredicate(graph, d, p));
|
|
109
110
|
}
|
package/dist/core/strictgate.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* weaker signals still print, as advisory. This makes strict mode safe to enable
|
|
9
9
|
* on a shared repo: a false positive downgrades to a warning instead of wrongly
|
|
10
10
|
* failing a teammate's commit. */
|
|
11
|
+
import { isInForce } from "./topics.js";
|
|
11
12
|
export const STRICT_MIN_CONFIDENCE = 0.8;
|
|
12
13
|
/** Is a provenance source HUMAN-CONFIRMED? Token-aware, so a composite source like
|
|
13
14
|
* "llm_draft+human_confirmed" counts, but a lookalike ("not_human_confirmed",
|
|
@@ -34,8 +35,10 @@ export function isStrictBlocker(c, stale) {
|
|
|
34
35
|
* warns. Semantic similarity never blocks. In-force + non-stale gates run first.
|
|
35
36
|
* This is what makes the "day-one is advisory" DX true (dec_a466655539). */
|
|
36
37
|
export function isVetoBlocker(d, tw, tier, stale) {
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
// Shared in-force predicate (core/topics.ts): superseded, REJECTED and
|
|
39
|
+
// window-closed decisions all lose their teeth here, not just superseded ones.
|
|
40
|
+
if (!isInForce(d))
|
|
41
|
+
return false;
|
|
39
42
|
if (stale)
|
|
40
43
|
return false; // freshness gate
|
|
41
44
|
if (tier === "semantic")
|
package/dist/core/topics.js
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
export function isLive(d) {
|
|
5
5
|
return d.status === "accepted" && d.superseded_by === null && d.valid_to === null;
|
|
6
6
|
}
|
|
7
|
+
/** In force for ENFORCEMENT — the one predicate every guard must share.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately BROADER than `isLive`: a `proposed` draft still contributes ADVISORY
|
|
10
|
+
* signal (its tripwires are `llm_draft`, which `isVetoBlocker` can never promote to a
|
|
11
|
+
* block), and that is the curate loop working as designed. What must never contribute
|
|
12
|
+
* is a decision the team formally REJECTED, or one whose valid-time window was closed.
|
|
13
|
+
*
|
|
14
|
+
* The guards previously each inlined a weaker `superseded`-only copy of this test, so a
|
|
15
|
+
* REJECTED decision kept driving the veto, regression, retired-symbol and conformance
|
|
16
|
+
* guards — it went on blocking commits and injecting "don't re-add this" into the
|
|
17
|
+
* pre-edit hook, with no way to un-stick it short of hand-editing the JSON. Structural
|
|
18
|
+
* parameter so the strict gate (which sees only a partial record) shares it too. */
|
|
19
|
+
export function isInForce(d) {
|
|
20
|
+
return d.status !== "superseded" && d.status !== "rejected" && !d.superseded_by && !d.valid_to;
|
|
21
|
+
}
|
|
7
22
|
/** Every live decision anchored to `topic`. In a healthy graph this is length 0 or 1;
|
|
8
23
|
* length > 1 is a topic collision the §4 resolution must settle. */
|
|
9
24
|
export function liveForTopic(decisions, topic) {
|
package/dist/mcp/server.js
CHANGED
|
@@ -821,10 +821,14 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
821
821
|
// Route the write to its ONE home: an explicit private:true goes to the overlay
|
|
822
822
|
// (putPrivate throws rather than silently falling public); in unified ("shared")
|
|
823
823
|
// mode EVERY capture goes to the overlay; else the public store.
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
824
|
+
// Through putCapture, NOT the raw per-home writers: it carries the cross-home
|
|
825
|
+
// twin guard. Branching on `home` here bypassed that guard, so one id could
|
|
826
|
+
// exist in BOTH stores — after which the merged/private-first read makes the
|
|
827
|
+
// topic-uniqueness check see one record while a later public `supersedes:`
|
|
828
|
+
// closes the other, leaving two live decisions on one topic and grounding
|
|
829
|
+
// silently injecting nothing for it. The guard throws; the surrounding catch
|
|
830
|
+
// turns that into a clean tool error instead of a silent twin.
|
|
831
|
+
store.putCapture("decisions", rec, !!decision.private);
|
|
828
832
|
// Invalidate, don't delete: closing the superseded decision's valid-time window
|
|
829
833
|
// (+ a supersedes edge) preserves the why-it-changed trail. Route the close to the
|
|
830
834
|
// same store the new record landed in — a private decision supersedes within the
|
|
@@ -890,10 +894,8 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
890
894
|
return err(`Refusing to record public correction ${rec.id}: source decision ${rec.source_decision} ${location}.`);
|
|
891
895
|
}
|
|
892
896
|
const existing = home === "private" ? store.getPrivateRec("constraints", rec.id) : store.json.get("constraints", rec.id);
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
else
|
|
896
|
-
store.json.put("constraints", rec);
|
|
897
|
+
// Same cross-home twin guard as the decision path above.
|
|
898
|
+
store.putCapture("constraints", rec, !!input.private);
|
|
897
899
|
store.reindex();
|
|
898
900
|
// Propagate the new rule to EVERY assistant's ambient grounding (Cursor/Copilot/
|
|
899
901
|
// Windsurf/AGENTS.md/CLAUDE.md), so a correction captured in one assistant is held
|
package/dist/store/hunchStore.js
CHANGED
|
@@ -20,7 +20,7 @@ import { selectEmbedder } from "./embedder.js";
|
|
|
20
20
|
import { JsonStore } from "./jsonStore.js";
|
|
21
21
|
import { gitCommonDir, gitWorktreeRoot, sameGitPublication } from "../extractors/git.js";
|
|
22
22
|
import { pathMatchesGlob } from "../core/glob.js";
|
|
23
|
-
import { currentForTopic } from "../core/topics.js";
|
|
23
|
+
import { currentForTopic, isInForce } from "../core/topics.js";
|
|
24
24
|
import { edgeId } from "../core/ids.js";
|
|
25
25
|
import { isStrictBlocker, isVetoBlocker } from "../core/strictgate.js";
|
|
26
26
|
import { effectiveForbids, matchForbids } from "../core/constraintmatch.js";
|
|
@@ -1254,9 +1254,9 @@ export class HunchStore {
|
|
|
1254
1254
|
// attribution (the strict guard fails on `blocking`).
|
|
1255
1255
|
const ordered = [...decisions].sort((a, b) => Number(blockingDec.has(b.id)) - Number(blockingDec.has(a.id)));
|
|
1256
1256
|
for (const d of ordered) {
|
|
1257
|
-
// Only IN-FORCE decisions: re-adding what an OUTDATED (superseded)
|
|
1258
|
-
// removed is not a regression against the current design.
|
|
1259
|
-
if (d
|
|
1257
|
+
// Only IN-FORCE decisions: re-adding what an OUTDATED (superseded), REJECTED, or
|
|
1258
|
+
// window-closed decision removed is not a regression against the current design.
|
|
1259
|
+
if (!isInForce(d))
|
|
1260
1260
|
continue;
|
|
1261
1261
|
if (!d.retired.symbols.length && !d.retired.deps.length)
|
|
1262
1262
|
continue;
|
|
@@ -1283,8 +1283,8 @@ export class HunchStore {
|
|
|
1283
1283
|
const out = [];
|
|
1284
1284
|
const seen = new Set(); // dedup: one hit per decision+alternative
|
|
1285
1285
|
for (const d of this.recs("decisions")) {
|
|
1286
|
-
if (d
|
|
1287
|
-
continue; // in-force only
|
|
1286
|
+
if (!isInForce(d))
|
|
1287
|
+
continue; // in-force only (excludes rejected + window-closed)
|
|
1288
1288
|
const tripwires = d.rejected_tripwires ?? [];
|
|
1289
1289
|
if (!tripwires.length)
|
|
1290
1290
|
continue;
|
|
@@ -1353,7 +1353,7 @@ export class HunchStore {
|
|
|
1353
1353
|
retiredForFile(file) {
|
|
1354
1354
|
const out = [];
|
|
1355
1355
|
for (const d of this.recs("decisions")) {
|
|
1356
|
-
if (d
|
|
1356
|
+
if (!isInForce(d))
|
|
1357
1357
|
continue;
|
|
1358
1358
|
if (!d.retired.symbols.length && !d.retired.deps.length)
|
|
1359
1359
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.3",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
6
6
|
"description": "Engineering memory and a deterministic Change Gate for AI-assisted codebases: decisions, rejected approaches, constraints, and bug lineage become portable context and opt-in enforcement for every MCP assistant.",
|