@lannguyensi/harness 0.42.0 → 0.43.0
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/CHANGELOG.md +27 -0
- package/dist/cli/approve/branch-protection.js +1 -1
- package/dist/cli/approve/branch-protection.js.map +1 -1
- package/dist/cli/dry-run.js +13 -1
- package/dist/cli/dry-run.js.map +1 -1
- package/dist/cli/init/composer.js +6 -6
- package/dist/cli/init/composer.js.map +1 -1
- package/dist/cli/init/profiles.d.ts +1 -1
- package/dist/cli/init/profiles.js +1 -1
- package/dist/cli/init/templates.d.ts +1 -1
- package/dist/cli/init/templates.js +47 -27
- package/dist/cli/init/templates.js.map +1 -1
- package/dist/cli/pack/hook-branch-protection.js +1 -1
- package/dist/cli/pack/hook-branch-protection.js.map +1 -1
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js +1 -1
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js.map +1 -1
- package/dist/cli/pack/hook-solution-acceptance.js +1 -1
- package/dist/cli/pack/hook-solution-acceptance.js.map +1 -1
- package/dist/cli/policy/intercept.js +77 -10
- package/dist/cli/policy/intercept.js.map +1 -1
- package/dist/cli/validate/checks.js +2 -2
- package/dist/cli/validate/checks.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +1 -1
- package/dist/policy-packs/builtin/branch-protection-runtime.js +1 -1
- package/dist/policy-packs/builtin/branch-protection.js +2 -2
- package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
- package/dist/policy-packs/builtin/post-merge-gate.js +1 -1
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js +1 -1
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/solution-acceptance.js +1 -1
- package/dist/runtime/bash-match-facts.d.ts +58 -0
- package/dist/runtime/bash-match-facts.js +194 -0
- package/dist/runtime/bash-match-facts.js.map +1 -0
- package/dist/runtime/bash-match-registry.d.ts +75 -0
- package/dist/runtime/bash-match-registry.js +359 -0
- package/dist/runtime/bash-match-registry.js.map +1 -0
- package/dist/runtime/bash-prefix-parse.js +23 -0
- package/dist/runtime/bash-prefix-parse.js.map +1 -1
- package/dist/runtime/command-normalize.d.ts +92 -0
- package/dist/runtime/command-normalize.js +1182 -0
- package/dist/runtime/command-normalize.js.map +1 -0
- package/dist/runtime/intercept.d.ts +39 -1
- package/dist/runtime/intercept.js +33 -4
- package/dist/runtime/intercept.js.map +1 -1
- package/dist/runtime/read-only-bash.d.ts +43 -0
- package/dist/runtime/read-only-bash.js +15 -3
- package/dist/runtime/read-only-bash.js.map +1 -1
- package/dist/schema/policies.js +1 -1
- package/dist/schema/policies.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Live facts about FULL_TEMPLATE's shipped `bash_match` policies (task
|
|
2
|
+
// `074acf5d`, run `2026-07-28-manifest-facts-drift-guard`).
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS MODULE EXISTS: two consecutive runs shipped/attempted a defect
|
|
5
|
+
// of the exact same CLASS, one day and one file apart.
|
|
6
|
+
// - Run `432db3d3` (merged 2026-07-27), HIGH: `NON_GIT_HEAD_TOKENS`
|
|
7
|
+
// (`command-normalize.ts`) covered 4 of the 8 head tokens a shipped
|
|
8
|
+
// `bash_match` policy actually gates. Closed by
|
|
9
|
+
// `tests/runtime/bash-match-head-token-drift.test.ts`, but scoped to
|
|
10
|
+
// that one set in that one file.
|
|
11
|
+
// - Run `dbc6d303` (halted 2026-07-28), CRITICAL: a NEW module's
|
|
12
|
+
// `INERT_CONSUMER_HEADS` contained `harness` — itself the gated head
|
|
13
|
+
// token of `deny-kill-switch-bypass`. The prior day's guard
|
|
14
|
+
// structurally could not see it: it only ever compared ONE named set
|
|
15
|
+
// (`NON_GIT_HEAD_TOKENS`) against the manifest, never a set someone
|
|
16
|
+
// adds tomorrow.
|
|
17
|
+
// This module answers, ONE call, read live (never hand-copied): "which
|
|
18
|
+
// policies declare a `bash_match`, which head token(s) each anchors on,
|
|
19
|
+
// which verbs, and is token X gated by ANY of them?" — the question both
|
|
20
|
+
// incidents got wrong. `src/runtime/bash-match-registry.ts` builds the
|
|
21
|
+
// generalised, EVERY-registered-set guard on top of the facts this module
|
|
22
|
+
// produces.
|
|
23
|
+
//
|
|
24
|
+
// DESIGN: literal head-token extraction straight out of an arbitrary
|
|
25
|
+
// regex string is fragile (alternations, nested groups, nothing
|
|
26
|
+
// guarantees a token is even a bare word) — the same reasoning the
|
|
27
|
+
// migrated guard's own header carried. A CURATED classification map is
|
|
28
|
+
// used instead, but it is never trusted blind: `extractBashMatchFacts`
|
|
29
|
+
// verifies it against the LIVE policies passed in (call it with
|
|
30
|
+
// `parseManifest(parseYaml(FULL_TEMPLATE)).policies` to get the shipped
|
|
31
|
+
// ground truth) and THROWS descriptively the moment the curated map and
|
|
32
|
+
// the live manifest disagree — a policy added/removed/renamed, or a
|
|
33
|
+
// classified token no longer present in its policy's live pattern. A
|
|
34
|
+
// caller that gets a `BashMatchFacts` back without an exception is
|
|
35
|
+
// holding verified-fresh facts, not a stale copy.
|
|
36
|
+
//
|
|
37
|
+
// LAYERING: this module takes policies as a plain parameter and does not
|
|
38
|
+
// import `FULL_TEMPLATE` itself (that lives in `src/cli/init/templates.ts`,
|
|
39
|
+
// the `cli` layer — `.dependency-cruiser.cjs`'s `runtime-no-upward-imports`
|
|
40
|
+
// rule forbids `runtime/` importing `cli/`). The live wiring (parsing
|
|
41
|
+
// FULL_TEMPLATE and calling this module) happens in test code, which the
|
|
42
|
+
// boundary check does not scan.
|
|
43
|
+
//
|
|
44
|
+
// WHY src/runtime/, NOT tests/ (F8, fix round): this module and
|
|
45
|
+
// `bash-match-registry.ts` are a single shared source of truth that TWO
|
|
46
|
+
// independent test files (`bash-match-head-token-drift.test.ts`,
|
|
47
|
+
// `bash-match-registry.test.ts`) import symmetrically — a module peer
|
|
48
|
+
// test files both depend on belongs beside the engine code it inspects,
|
|
49
|
+
// not inside the test tree that merely happens to hold its only callers
|
|
50
|
+
// today.
|
|
51
|
+
/**
|
|
52
|
+
* The curated ground truth, verified fresh against FULL_TEMPLATE's live
|
|
53
|
+
* policies by `extractBashMatchFacts` every time it runs — see the module
|
|
54
|
+
* header. Keyed by policy name.
|
|
55
|
+
*
|
|
56
|
+
* "documented-uncovered" tokens (fix round 2, finding F1 on the migrated
|
|
57
|
+
* guard — see `command-normalize.ts`'s "SHIPPED BUT NOT COVERED"
|
|
58
|
+
* paragraph for why each is genuinely out of reach today, not merely
|
|
59
|
+
* unimplemented): `env`, `unset`, `tee`, `cp`.
|
|
60
|
+
*/
|
|
61
|
+
const CURATED_BASH_MATCH_FACTS = {
|
|
62
|
+
"review-before-merge-bash": {
|
|
63
|
+
headTokens: [{ token: "gh", class: "non-git-set" }],
|
|
64
|
+
verbs: ["merge"],
|
|
65
|
+
},
|
|
66
|
+
"dogfood-before-release": {
|
|
67
|
+
headTokens: [
|
|
68
|
+
{ token: "npm", class: "non-git-set" },
|
|
69
|
+
{ token: "git", class: "git" },
|
|
70
|
+
],
|
|
71
|
+
verbs: ["publish", "tag"],
|
|
72
|
+
},
|
|
73
|
+
"preflight-before-investigation": {
|
|
74
|
+
headTokens: [{ token: "git", class: "git" }],
|
|
75
|
+
verbs: ["status", "log", "diff", "branch"],
|
|
76
|
+
},
|
|
77
|
+
"review-subagent-before-pr-create-bash": {
|
|
78
|
+
headTokens: [{ token: "gh", class: "non-git-set" }],
|
|
79
|
+
verbs: ["create"],
|
|
80
|
+
},
|
|
81
|
+
"preflight-before-push": {
|
|
82
|
+
headTokens: [{ token: "git", class: "git" }],
|
|
83
|
+
verbs: ["push"],
|
|
84
|
+
},
|
|
85
|
+
"deny-kill-switch-bypass": {
|
|
86
|
+
headTokens: [{ token: "harness", class: "non-git-set" }],
|
|
87
|
+
verbs: ["pause", "resume", "gate", "disable", "enable"],
|
|
88
|
+
},
|
|
89
|
+
"deny-session-env-strip": {
|
|
90
|
+
headTokens: [
|
|
91
|
+
{ token: "env", class: "documented-uncovered" },
|
|
92
|
+
{ token: "unset", class: "documented-uncovered" },
|
|
93
|
+
],
|
|
94
|
+
// Headless trigger alternatives (the bare `<SESSION_VAR>=` form) and
|
|
95
|
+
// multiple independent alternations make a flat verb list misleading
|
|
96
|
+
// rather than merely incomplete — left empty rather than overclaim.
|
|
97
|
+
verbs: [],
|
|
98
|
+
},
|
|
99
|
+
"deny-pause-sentinel-forgery": {
|
|
100
|
+
headTokens: [
|
|
101
|
+
{ token: "tee", class: "documented-uncovered" },
|
|
102
|
+
{ token: "cp", class: "documented-uncovered" },
|
|
103
|
+
],
|
|
104
|
+
// No separate "verb" beyond the head tokens themselves here.
|
|
105
|
+
verbs: [],
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* `documented-uncovered` head tokens are collected from the curated map
|
|
110
|
+
* itself (single source of truth) rather than hand-copied a second time.
|
|
111
|
+
*/
|
|
112
|
+
function documentedUncoveredTokens() {
|
|
113
|
+
const out = new Set();
|
|
114
|
+
for (const fact of Object.values(CURATED_BASH_MATCH_FACTS)) {
|
|
115
|
+
for (const h of fact.headTokens) {
|
|
116
|
+
if (h.class === "documented-uncovered")
|
|
117
|
+
out.add(h.token);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/** Exposed for callers that need the full "genuinely not covered today" list (see `bash-match-registry.ts`). */
|
|
123
|
+
export const DOCUMENTED_UNCOVERED_HEAD_TOKENS = documentedUncoveredTokens();
|
|
124
|
+
/**
|
|
125
|
+
* Compute verified-fresh `BashMatchFacts` from a LIVE list of parsed
|
|
126
|
+
* policies. Call with `parseManifest(parseYaml(FULL_TEMPLATE)).policies`
|
|
127
|
+
* to get the shipped ground truth (never hand-copy the patterns).
|
|
128
|
+
*
|
|
129
|
+
* Throws descriptively — the caller should let this propagate as a
|
|
130
|
+
* failing test, not swallow it — when:
|
|
131
|
+
* 1. The exact SET of policy names carrying a `bash_match` trigger in
|
|
132
|
+
* the live `policies` differs from the curated map's key set
|
|
133
|
+
* (a policy was added, removed, or renamed).
|
|
134
|
+
* 2. A curated head token or verb is no longer present as a standalone
|
|
135
|
+
* word (`\bTOKEN\b`) in its policy's live pattern (renamed/removed
|
|
136
|
+
* within an otherwise still-existing policy).
|
|
137
|
+
*/
|
|
138
|
+
export function extractBashMatchFacts(policies) {
|
|
139
|
+
const live = policies.filter((p) => p.trigger.bash_match !== undefined);
|
|
140
|
+
const actualNames = [...live.map((p) => p.name)].sort();
|
|
141
|
+
const curatedNames = Object.keys(CURATED_BASH_MATCH_FACTS).sort();
|
|
142
|
+
if (JSON.stringify(actualNames) !== JSON.stringify(curatedNames)) {
|
|
143
|
+
throw new Error(`bash_match facts drift: the live policies declaring a bash_match trigger (${JSON.stringify(actualNames)}) no longer match CURATED_BASH_MATCH_FACTS's classified set (${JSON.stringify(curatedNames)}) in src/runtime/bash-match-facts.ts. A policy was added, removed, or renamed — update the curated map consciously.`);
|
|
144
|
+
}
|
|
145
|
+
const byName = new Map(live.map((p) => [p.name, p]));
|
|
146
|
+
const gatedHeadTokens = new Set();
|
|
147
|
+
const classification = new Map();
|
|
148
|
+
const policyFacts = [];
|
|
149
|
+
for (const policyName of curatedNames) {
|
|
150
|
+
const curated = CURATED_BASH_MATCH_FACTS[policyName];
|
|
151
|
+
const policy = byName.get(policyName);
|
|
152
|
+
const pattern = policy.trigger.bash_match;
|
|
153
|
+
for (const { token, class: cls } of curated.headTokens) {
|
|
154
|
+
assertStandaloneWord(pattern, token, policyName);
|
|
155
|
+
gatedHeadTokens.add(token);
|
|
156
|
+
classification.set(token, cls);
|
|
157
|
+
}
|
|
158
|
+
for (const verb of curated.verbs) {
|
|
159
|
+
assertStandaloneWord(pattern, verb, policyName);
|
|
160
|
+
}
|
|
161
|
+
policyFacts.push({
|
|
162
|
+
policyName,
|
|
163
|
+
pattern,
|
|
164
|
+
headTokens: curated.headTokens,
|
|
165
|
+
verbs: curated.verbs,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
policyNames: actualNames,
|
|
170
|
+
policies: policyFacts,
|
|
171
|
+
gatedHeadTokens,
|
|
172
|
+
classify: (token) => classification.get(token),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function assertStandaloneWord(pattern, word, policyName) {
|
|
176
|
+
const standaloneWordRe = new RegExp(`\\b${word}\\b`);
|
|
177
|
+
if (!standaloneWordRe.test(pattern)) {
|
|
178
|
+
throw new Error(`bash_match facts drift: policy "${policyName}"'s live bash_match pattern no longer contains the classified word "${word}" as a standalone word (pattern: ${pattern}). It was renamed or removed — update CURATED_BASH_MATCH_FACTS in src/runtime/bash-match-facts.ts.`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* "Is `token` gated by ANY shipped `bash_match` policy?" — the one-call
|
|
183
|
+
* question both incidents got wrong. Real caller (F8, fix round):
|
|
184
|
+
* `bash-match-registry.ts`'s `checkRegisteredSets` covers-purity check,
|
|
185
|
+
* which asks this once per declared `member` of every
|
|
186
|
+
* "covers-gated-head-tokens" registration. Takes only the `gatedHeadTokens`
|
|
187
|
+
* slice of `BashMatchFacts` (not the full shape) so a caller holding the
|
|
188
|
+
* same narrowed `Pick` `checkRegisteredSets` accepts can call this directly
|
|
189
|
+
* without widening its own parameter type.
|
|
190
|
+
*/
|
|
191
|
+
export function isHeadTokenGated(facts, token) {
|
|
192
|
+
return facts.gatedHeadTokens.has(token);
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=bash-match-facts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bash-match-facts.js","sourceRoot":"","sources":["../../src/runtime/bash-match-facts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { BashMatchFacts } from "./bash-match-facts.js";
|
|
2
|
+
export type RegisteredSetIntent = "covers-gated-head-tokens" | "must-not-contain-gated-head-token";
|
|
3
|
+
interface RegisteredHeadTokenSetCommon {
|
|
4
|
+
/** Stable identifier, used in failure messages and by the unregistered-set scan's cross-reference. */
|
|
5
|
+
readonly id: string;
|
|
6
|
+
/** File the set is actually declared in, for failure messages. */
|
|
7
|
+
readonly module: string;
|
|
8
|
+
/** Membership test — `Set.has` or `RegExp.test`, see the module header. */
|
|
9
|
+
readonly has: (token: string) => boolean;
|
|
10
|
+
readonly description: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A "covers-gated-head-tokens" registration — the set exists so the engine
|
|
14
|
+
* RECOGNISES these head tokens. `members` is NON-OPTIONAL (fix round 2,
|
|
15
|
+
* S1): the round-1 shape had this field OPTIONAL, and
|
|
16
|
+
* `checkRegisteredSets`'s covers-purity check (c) simply `continue`d past
|
|
17
|
+
* any registration that omitted it — a new module registered as
|
|
18
|
+
* "covers-gated-head-tokens" WITHOUT `members` measurably passed the full
|
|
19
|
+
* suite (167 files / 3980 tests) even with a gated head token laundered
|
|
20
|
+
* inside its `has()` predicate. Making `members` mandatory on this arm
|
|
21
|
+
* turns that escape into a TYPE ERROR: a covers registration that omits
|
|
22
|
+
* `members` now fails `tsc`, not merely a test. See `checkRegisteredSets`
|
|
23
|
+
* checks (c), (d), (e) below for the three independent things `members`
|
|
24
|
+
* (and `has()`) are cross-checked against.
|
|
25
|
+
*/
|
|
26
|
+
export interface CoversHeadTokenSet extends RegisteredHeadTokenSetCommon {
|
|
27
|
+
readonly intent: "covers-gated-head-tokens";
|
|
28
|
+
readonly members: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A "must-not-contain-gated-head-token" registration — the set exists for
|
|
32
|
+
* some OTHER purpose and must never contain a token the manifest itself
|
|
33
|
+
* gates (the dbc6d303 CRITICAL shape). `members` is optional here: no
|
|
34
|
+
* purity/redundancy/predicate-purity semantics apply to this arm — only
|
|
35
|
+
* `has()` is ever asked anything (check (a)).
|
|
36
|
+
*/
|
|
37
|
+
export interface MustNotContainHeadTokenSet extends RegisteredHeadTokenSetCommon {
|
|
38
|
+
readonly intent: "must-not-contain-gated-head-token";
|
|
39
|
+
readonly members?: readonly string[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Discriminated union on `intent` (fix round 2, S1) — see
|
|
43
|
+
* `CoversHeadTokenSet`'s doc for why the split exists.
|
|
44
|
+
*/
|
|
45
|
+
export type RegisteredHeadTokenSet = CoversHeadTokenSet | MustNotContainHeadTokenSet;
|
|
46
|
+
export declare const REGISTERED_HEAD_TOKEN_SETS: readonly RegisteredHeadTokenSet[];
|
|
47
|
+
/**
|
|
48
|
+
* Which of the five independent checks flagged this violation (fix round
|
|
49
|
+
* 2): a fixture test asserting on one check's behaviour must not be
|
|
50
|
+
* silently polluted by another check's unrelated finding on the same
|
|
51
|
+
* (setId, token) pair — see `tests/runtime/bash-match-registry.test.ts`.
|
|
52
|
+
*/
|
|
53
|
+
export type RegistrationCheck = "must-not-contain" | "covers-completeness" | "covers-purity" | "covers-redundancy" | "covers-predicate-purity";
|
|
54
|
+
export interface RegistrationViolation {
|
|
55
|
+
readonly setId: string;
|
|
56
|
+
readonly intent: RegisteredSetIntent;
|
|
57
|
+
readonly token: string;
|
|
58
|
+
readonly check: RegistrationCheck;
|
|
59
|
+
readonly reason: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Check every registered set against the live facts. Returns an EMPTY
|
|
63
|
+
* array when clean. `registrations` and `documentedUncovered` default to
|
|
64
|
+
* the real, shipped values (F5 fix-round correction: the
|
|
65
|
+
* `documentedUncovered` default previously silently fell back to an empty
|
|
66
|
+
* set, which raised 4 FALSE violations — tee, cp, env, unset — when called
|
|
67
|
+
* with only `facts`; pinned by
|
|
68
|
+
* `tests/runtime/bash-match-head-token-drift.test.ts`'s
|
|
69
|
+
* "checkRegisteredSets(liveFacts())" defaults test) but accept fixtures —
|
|
70
|
+
* see `tests/runtime/bash-match-registry.test.ts`'s regression tests for
|
|
71
|
+
* the historical incidents, which pass deliberately-broken fixtures to
|
|
72
|
+
* prove this function catches their exact shape.
|
|
73
|
+
*/
|
|
74
|
+
export declare function checkRegisteredSets(facts: Pick<BashMatchFacts, "gatedHeadTokens" | "classify">, registrations?: readonly RegisteredHeadTokenSet[], documentedUncovered?: ReadonlySet<string>): RegistrationViolation[];
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// Registration point for every engine-side constant set that mirrors
|
|
2
|
+
// FULL_TEMPLATE's `bash_match` facts (task `074acf5d`, run
|
|
3
|
+
// `2026-07-28-manifest-facts-drift-guard`).
|
|
4
|
+
//
|
|
5
|
+
// WHY A REGISTRY, NOT ONE HARD-CODED COMPARISON: the guard this replaces
|
|
6
|
+
// (`tests/runtime/bash-match-head-token-drift.test.ts`, pre-migration)
|
|
7
|
+
// checked exactly one named set (`NON_GIT_HEAD_TOKENS`) against the
|
|
8
|
+
// manifest. That guard could not see run `dbc6d303`'s `INERT_CONSUMER_HEADS`
|
|
9
|
+
// (a NEW module, CRITICAL: contained `harness`, the kill-switch's own
|
|
10
|
+
// gated head token) at all — it simply never looked at any set but the
|
|
11
|
+
// one it was told about. `REGISTERED_HEAD_TOKEN_SETS` below is the list
|
|
12
|
+
// every such set must join; `checkRegisteredSets` walks the WHOLE list
|
|
13
|
+
// against the live facts, so a set registered tomorrow is checked
|
|
14
|
+
// automatically, and (paired with the unregistered-set scan in
|
|
15
|
+
// `tests/runtime/bash-match-head-token-drift.test.ts`) a set that is
|
|
16
|
+
// NOT registered is itself a finding.
|
|
17
|
+
//
|
|
18
|
+
// TWO INTENTS (minimum required — see the two historical incidents this
|
|
19
|
+
// run exists to close):
|
|
20
|
+
// - "covers-gated-head-tokens": the set exists so the engine recognises
|
|
21
|
+
// these head tokens (`NON_GIT_HEAD_TOKENS`'s shape). Checked for
|
|
22
|
+
// COMPLETENESS: every head token the manifest actually gates (barring
|
|
23
|
+
// a consciously `documented-uncovered` token) must be covered by AT
|
|
24
|
+
// LEAST ONE registered set with this intent — the run `432db3d3` HIGH
|
|
25
|
+
// (a covers-set spanning only 4 of 8 gated tokens) as a standing
|
|
26
|
+
// regression class.
|
|
27
|
+
// - "must-not-contain-gated-head-token": the set exists for some OTHER
|
|
28
|
+
// purpose entirely and must NEVER contain a token the manifest itself
|
|
29
|
+
// gates — the run `dbc6d303` CRITICAL's shape (an "inert consumer"
|
|
30
|
+
// allowlist that accidentally absorbed the kill-switch's own gated
|
|
31
|
+
// head token). Checked by testing every LIVE gated head token against
|
|
32
|
+
// every such registered set's `has()`.
|
|
33
|
+
//
|
|
34
|
+
// FIX ROUND 2 (task `074acf5d`, second fix round, findings S1/S2/S4 —
|
|
35
|
+
// see `.ai/runs/2026-07-28-manifest-facts-drift-guard/03-decisions.md`
|
|
36
|
+
// D-002/D-004): review round 2 MEASURED that the round-1 guard's central
|
|
37
|
+
// defence — a registration's self-declared `intent` — was still
|
|
38
|
+
// escapable two ways, and that the guard reproduced its own defect class
|
|
39
|
+
// inside itself. Three changes close what can be made STRUCTURALLY
|
|
40
|
+
// impossible (D-002/D-003 deliberately draws the line here, not at every
|
|
41
|
+
// enumerable escape — see the decisions file):
|
|
42
|
+
// (S1) `RegisteredHeadTokenSet` is now a DISCRIMINATED UNION on `intent`:
|
|
43
|
+
// the "covers-gated-head-tokens" arm's `members` is NON-OPTIONAL.
|
|
44
|
+
// Omitting `members` on a covers registration — the round-1 escape,
|
|
45
|
+
// "declare no members and check (c) below never runs" — is now a
|
|
46
|
+
// COMPILE ERROR, not a test failure. The "must-not-contain" arm keeps
|
|
47
|
+
// `members` optional (no purity semantics apply to it).
|
|
48
|
+
// (S2) check (d), COVERS-REDUNDANCY, below: NARROWS — does not close —
|
|
49
|
+
// the escape where a laundered set's declared `members` are all
|
|
50
|
+
// HONESTLY gated (so check (c) is satisfied) but the set's true
|
|
51
|
+
// purpose is something else. It works only for a gated token that some
|
|
52
|
+
// covers-set legitimately covers, and is STRUCTURALLY SILENT for the
|
|
53
|
+
// `documented-uncovered` subset. MEASURED (review round 3): a
|
|
54
|
+
// registration declaring `members: ["tee"]` — or any subset of
|
|
55
|
+
// `env`/`unset`/`tee`/`cp`, four of the eight live gated head tokens —
|
|
56
|
+
// passes checks (a) through (e) with an entirely honest, live-coupled
|
|
57
|
+
// `has()` and `members`, end to end on the real tree. That is the
|
|
58
|
+
// `dbc6d303` shape (an inert allowlist absorbing a gated head token)
|
|
59
|
+
// and this registry does not see it. Redesign filed as a follow-up;
|
|
60
|
+
// see the LIMITS paragraph at the end of this header.
|
|
61
|
+
// (S4) check (e), COVERS-PREDICATE-PURITY, below: closes the escape
|
|
62
|
+
// where a registration's `has()` PREDICATE accepts more than its
|
|
63
|
+
// declared `members` claim (measured: widening `GIT_TOKEN_RE` to also
|
|
64
|
+
// match "docker" while leaving `members: ["git"]` untouched left the
|
|
65
|
+
// full suite green) — this guard reproducing, inside itself, the exact
|
|
66
|
+
// "engine-side constant mirrors manifest content with no coupling"
|
|
67
|
+
// defect class it exists to eliminate elsewhere.
|
|
68
|
+
// Each violation now carries a `check` field (see `RegistrationViolation`)
|
|
69
|
+
// naming exactly which of the five checks fired, so a fixture test can
|
|
70
|
+
// assert on ONE check's output without an unrelated check's finding on the
|
|
71
|
+
// same (setId, token) pair silently changing what the array contains.
|
|
72
|
+
//
|
|
73
|
+
// COVERS-PURITY (fix round, F1+F2): a THIRD check, over a "covers"
|
|
74
|
+
// registration's OWN declared `members` (see `RegisteredHeadTokenSet.members`
|
|
75
|
+
// below), closes two escapes with one rule — every declared member must
|
|
76
|
+
// itself be a live gated head token:
|
|
77
|
+
// (F1) the REVERSE direction, ENGINE->MANIFEST: the pre-migration guard
|
|
78
|
+
// asserted this (every member of `NON_GIT_HEAD_TOKENS` must be a token
|
|
79
|
+
// some shipped policy gates) and the migration to this registry
|
|
80
|
+
// silently dropped it — completeness-checking (b) only ever walks
|
|
81
|
+
// `facts.gatedHeadTokens` outward to registered sets, never a
|
|
82
|
+
// registered set's own members inward to the facts. An engine set
|
|
83
|
+
// gaining an ungated member (e.g. "docker" added to
|
|
84
|
+
// `NON_GIT_HEAD_TOKENS`) is real DEAD CODE at best and a sign the
|
|
85
|
+
// manifest and the engine have silently diverged at worst; either way
|
|
86
|
+
// it is a regression this registry exists to catch.
|
|
87
|
+
// (F2) MIS-DECLARED INTENT: a `must-not-contain-gated-head-token` set
|
|
88
|
+
// relabeled `covers-gated-head-tokens` to dodge check (a) — cheap,
|
|
89
|
+
// because `intent` is self-declared and was, before this check,
|
|
90
|
+
// cross-checked against nothing. A set that exists for any purpose
|
|
91
|
+
// OTHER than recognising gated head tokens (an inert-consumer
|
|
92
|
+
// allowlist, say) necessarily contains members that are not
|
|
93
|
+
// themselves gated head tokens, so it fails THIS check the moment it
|
|
94
|
+
// declares `members` — the decision rule is: pick the intent that
|
|
95
|
+
// describes the set's PURPOSE; a set whose purpose is anything other
|
|
96
|
+
// than recognising gated head tokens is
|
|
97
|
+
// "must-not-contain-gated-head-token", never "covers-gated-head-tokens".
|
|
98
|
+
//
|
|
99
|
+
// AXIS SCOPE (F6, fix round): this registry models the HEAD-TOKEN axis
|
|
100
|
+
// only — which literal head token a `bash_match` anchors on. A shipped
|
|
101
|
+
// policy's `bash_match` can equally anchor on a VERB with no head-token
|
|
102
|
+
// significance of its own (`pause` in `deny-kill-switch-bypass`), checked
|
|
103
|
+
// today only by `HARNESS_READ_ONLY_SUBS` in `read-only-bash.ts`, a set this
|
|
104
|
+
// registry does not see and does not check. The VERB axis is explicitly
|
|
105
|
+
// OUT OF SCOPE for this run: the exact same defect shape (an engine-side
|
|
106
|
+
// allowlist silently absorbing a gated token) is unmodellable here today,
|
|
107
|
+
// one axis over. Do not assume verb coverage exists because head-token
|
|
108
|
+
// coverage does.
|
|
109
|
+
//
|
|
110
|
+
// WHY src/runtime/, NOT tests/ (F8, fix round): see `bash-match-facts.ts`'s
|
|
111
|
+
// module header — the same reasoning applies here.
|
|
112
|
+
//
|
|
113
|
+
// LIMITS — READ THIS BEFORE TRUSTING A REGISTRATION. Three independent
|
|
114
|
+
// adversarial review passes each found a way to register a set that this
|
|
115
|
+
// registry accepts but should not. Two rounds of added checks narrowed the
|
|
116
|
+
// family without closing it, so the checks were frozen here deliberately
|
|
117
|
+
// and the registration SURFACE is being redesigned instead (follow-up
|
|
118
|
+
// task; the shape under consideration is that a registration hands the
|
|
119
|
+
// registry the constant itself rather than a self-description, so a stale
|
|
120
|
+
// or lying mirror cannot be expressed at all). What this registry does NOT
|
|
121
|
+
// catch today, each measured end to end, not theorised:
|
|
122
|
+
// 1. A covers registration whose declared `members` are all
|
|
123
|
+
// `documented-uncovered` gated tokens (`env`, `unset`, `tee`, `cp` —
|
|
124
|
+
// four of eight). Passes every check with an honest, live-coupled
|
|
125
|
+
// registration. This is the `dbc6d303` CRITICAL shape.
|
|
126
|
+
// 2. A `has()` predicate that does not reflect the constant its
|
|
127
|
+
// `id`/`module` names: a stale hand-copied snapshot, or `() => false`.
|
|
128
|
+
// Nothing couples the two. The `must-not-contain` arm has no
|
|
129
|
+
// predicate check at all, and that is the arm the CRITICAL shape
|
|
130
|
+
// lives in.
|
|
131
|
+
// 3. `members` declaring coverage the predicate rejects (the two are
|
|
132
|
+
// each walked in one direction, never against each other), and an
|
|
133
|
+
// empty `members: []`, which typechecks.
|
|
134
|
+
// 4. A registration naming a `module`/`id` that no exported constant
|
|
135
|
+
// matches.
|
|
136
|
+
// What it DOES catch is the bar it was built for and still meets: both
|
|
137
|
+
// historical incidents (`432db3d3`'s covers-completeness gap and
|
|
138
|
+
// `dbc6d303`'s `harness` in an inert allowlist), plus an unregistered set
|
|
139
|
+
// in the runtime layer. Treat it as a tripwire for the accidental case,
|
|
140
|
+
// never as a boundary against a determined one.
|
|
141
|
+
//
|
|
142
|
+
// TODAY'S REAL REGISTRATIONS are both "covers" sets — `GIT_TOKEN_RE` and
|
|
143
|
+
// `NON_GIT_HEAD_TOKENS`, both from `command-normalize.ts`, the two sets
|
|
144
|
+
// the pre-migration guard already knew about. No real
|
|
145
|
+
// "must-not-contain-gated-head-token" consumer exists in this tree today
|
|
146
|
+
// (run `dbc6d303`'s own module is explicit non-goal / future work for
|
|
147
|
+
// THIS run) — the intent is fully implemented and covered by the fixture
|
|
148
|
+
// regression tests in `tests/runtime/bash-match-registry.test.ts`
|
|
149
|
+
// (reproducing the CRITICAL's exact shape) and by a live mutation probe
|
|
150
|
+
// against this file's own `REGISTERED_HEAD_TOKEN_SETS` array, not against
|
|
151
|
+
// a second production consumer: temporarily registering a fixture
|
|
152
|
+
// (`intent: "must-not-contain-gated-head-token"`, `has` containing
|
|
153
|
+
// "harness") reddens exactly "every registered engine-side set is clean
|
|
154
|
+
// against the live facts ..." and the F5 defaults test in
|
|
155
|
+
// `tests/runtime/bash-match-head-token-drift.test.ts`, with the violation
|
|
156
|
+
// naming the fixture's own `setId` and the offending token — the probe was
|
|
157
|
+
// reverted immediately after.
|
|
158
|
+
//
|
|
159
|
+
// `has()` rather than an enumerable `ReadonlySet<string>` uniformly for
|
|
160
|
+
// checks (a) and (b): the live gated-head-token vocabulary is small and
|
|
161
|
+
// fully known from `BashMatchFacts.gatedHeadTokens` (never more than ~8
|
|
162
|
+
// entries), so both checks only ever need to ask "does this registered set
|
|
163
|
+
// answer YES for token X", which a `RegExp.test` (`GIT_TOKEN_RE`) and a
|
|
164
|
+
// `Set.has` (`NON_GIT_HEAD_TOKENS`) can equally answer. Check (c)
|
|
165
|
+
// (COVERS-PURITY, above) is different in kind — it asks "does this
|
|
166
|
+
// registered set contain something the facts DON'T know about", which no
|
|
167
|
+
// predicate over the ~8 known tokens can answer; that is why `members` is
|
|
168
|
+
// a separate, enumerable field rather than folded into `has()` — NON-
|
|
169
|
+
// OPTIONAL on the "covers-gated-head-tokens" arm as of fix round 2 (S1;
|
|
170
|
+
// see `CoversHeadTokenSet`'s doc), optional on "must-not-contain", which
|
|
171
|
+
// this field's checks never apply to.
|
|
172
|
+
import { GIT_TOKEN_RE, NON_GIT_HEAD_TOKENS } from "./command-normalize.js";
|
|
173
|
+
import { DOCUMENTED_UNCOVERED_HEAD_TOKENS, isHeadTokenGated } from "./bash-match-facts.js";
|
|
174
|
+
export const REGISTERED_HEAD_TOKEN_SETS = [
|
|
175
|
+
{
|
|
176
|
+
id: "GIT_TOKEN_RE",
|
|
177
|
+
module: "src/runtime/command-normalize.ts",
|
|
178
|
+
intent: "covers-gated-head-tokens",
|
|
179
|
+
has: (token) => GIT_TOKEN_RE.test(token),
|
|
180
|
+
members: ["git"],
|
|
181
|
+
description: "canonicalizeSegment's git-basename recognizer (matches `git`, and any `\\S*/git` path-qualified spelling, by basename)",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "NON_GIT_HEAD_TOKENS",
|
|
185
|
+
module: "src/runtime/command-normalize.ts",
|
|
186
|
+
intent: "covers-gated-head-tokens",
|
|
187
|
+
has: (token) => NON_GIT_HEAD_TOKENS.has(token),
|
|
188
|
+
members: [...NON_GIT_HEAD_TOKENS],
|
|
189
|
+
description: "canonicalizeSegment's closed non-git head-token set (today: gh, npm, harness)",
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
/**
|
|
193
|
+
* Fixed, deliberately small vocabulary of plausible NON-gated head tokens
|
|
194
|
+
* (fix round 2, S4/D-004) — commands a reasonable engine-side set might
|
|
195
|
+
* plausibly mirror or accidentally absorb, but that no shipped
|
|
196
|
+
* `bash_match` policy gates today. Combined with the live
|
|
197
|
+
* `facts.gatedHeadTokens` (every token that IS gated, verified fresh —
|
|
198
|
+
* see `bash-match-facts.ts`) this gives check (e) below a concrete probe
|
|
199
|
+
* domain to test every registered "covers" predicate against, instead of
|
|
200
|
+
* trusting a registration's declared `members` to be an honest reflection
|
|
201
|
+
* of what its `has()` actually accepts.
|
|
202
|
+
*/
|
|
203
|
+
const PLAUSIBLE_NON_GATED_HEAD_TOKENS = [
|
|
204
|
+
"docker",
|
|
205
|
+
"kubectl",
|
|
206
|
+
"ls",
|
|
207
|
+
"cat",
|
|
208
|
+
"node",
|
|
209
|
+
"python",
|
|
210
|
+
"make",
|
|
211
|
+
"ssh",
|
|
212
|
+
];
|
|
213
|
+
/**
|
|
214
|
+
* Check every registered set against the live facts. Returns an EMPTY
|
|
215
|
+
* array when clean. `registrations` and `documentedUncovered` default to
|
|
216
|
+
* the real, shipped values (F5 fix-round correction: the
|
|
217
|
+
* `documentedUncovered` default previously silently fell back to an empty
|
|
218
|
+
* set, which raised 4 FALSE violations — tee, cp, env, unset — when called
|
|
219
|
+
* with only `facts`; pinned by
|
|
220
|
+
* `tests/runtime/bash-match-head-token-drift.test.ts`'s
|
|
221
|
+
* "checkRegisteredSets(liveFacts())" defaults test) but accept fixtures —
|
|
222
|
+
* see `tests/runtime/bash-match-registry.test.ts`'s regression tests for
|
|
223
|
+
* the historical incidents, which pass deliberately-broken fixtures to
|
|
224
|
+
* prove this function catches their exact shape.
|
|
225
|
+
*/
|
|
226
|
+
export function checkRegisteredSets(facts, registrations = REGISTERED_HEAD_TOKEN_SETS, documentedUncovered = DOCUMENTED_UNCOVERED_HEAD_TOKENS) {
|
|
227
|
+
const violations = [];
|
|
228
|
+
// (a) must-not-contain: no registered set with this intent may answer
|
|
229
|
+
// `true` for ANY live gated head token — the dbc6d303 CRITICAL shape.
|
|
230
|
+
for (const reg of registrations) {
|
|
231
|
+
if (reg.intent !== "must-not-contain-gated-head-token")
|
|
232
|
+
continue;
|
|
233
|
+
for (const token of facts.gatedHeadTokens) {
|
|
234
|
+
if (reg.has(token)) {
|
|
235
|
+
violations.push({
|
|
236
|
+
setId: reg.id,
|
|
237
|
+
intent: reg.intent,
|
|
238
|
+
token,
|
|
239
|
+
check: "must-not-contain",
|
|
240
|
+
reason: `"${reg.id}" (${reg.module} — ${reg.description}) declares intent "must-not-contain-gated-head-token" but contains "${token}", which IS gated by a shipped bash_match policy`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// (b) covers-completeness: every gated head token NOT consciously
|
|
246
|
+
// documented as uncovered must be answered `true` by at least one
|
|
247
|
+
// registered "covers" set — the 432db3d3 HIGH shape.
|
|
248
|
+
for (const token of facts.gatedHeadTokens) {
|
|
249
|
+
if (documentedUncovered.has(token))
|
|
250
|
+
continue;
|
|
251
|
+
const covered = registrations.some((reg) => reg.intent === "covers-gated-head-tokens" && reg.has(token));
|
|
252
|
+
if (!covered) {
|
|
253
|
+
const cls = facts.classify(token);
|
|
254
|
+
violations.push({
|
|
255
|
+
setId: "(none)",
|
|
256
|
+
intent: "covers-gated-head-tokens",
|
|
257
|
+
token,
|
|
258
|
+
check: "covers-completeness",
|
|
259
|
+
reason: `gated head token "${token}" (class: ${cls ?? "unknown"}) is not covered by ANY registered "covers-gated-head-tokens" set, and is not in the documented-uncovered list either`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// (c) covers-purity (fix round, F1+F2): every member a "covers" set
|
|
264
|
+
// DECLARES (see RegisteredHeadTokenSet.members) must itself be a live
|
|
265
|
+
// gated head token. Closes two escapes with one check: (1) the reverse
|
|
266
|
+
// ENGINE->MANIFEST direction the pre-migration guard checked and the
|
|
267
|
+
// migration dropped (a covers set gaining a member — e.g. "docker" —
|
|
268
|
+
// that no shipped policy gates at all); (2) the mis-declared-intent
|
|
269
|
+
// laundering escape, where a `must-not-contain-gated-head-token` set is
|
|
270
|
+
// relabeled `covers-gated-head-tokens` to dodge check (a) — a set that
|
|
271
|
+
// exists for any purpose OTHER than recognising gated head tokens
|
|
272
|
+
// necessarily contains a member that fails this check. `members` is
|
|
273
|
+
// non-optional on this arm as of fix round 2 (S1) — every registration
|
|
274
|
+
// that reaches here has one to walk.
|
|
275
|
+
for (const reg of registrations) {
|
|
276
|
+
if (reg.intent !== "covers-gated-head-tokens")
|
|
277
|
+
continue;
|
|
278
|
+
for (const member of reg.members) {
|
|
279
|
+
if (!isHeadTokenGated(facts, member)) {
|
|
280
|
+
violations.push({
|
|
281
|
+
setId: reg.id,
|
|
282
|
+
intent: reg.intent,
|
|
283
|
+
token: member,
|
|
284
|
+
check: "covers-purity",
|
|
285
|
+
reason: `"${reg.id}" (${reg.module} — ${reg.description}) declares intent "covers-gated-head-tokens" and lists "${member}" among its members, but "${member}" is not a live gated head token (no shipped bash_match policy gates it) — pick the intent that describes the set's PURPOSE: a set that exists for any purpose OTHER than recognising gated head tokens is "must-not-contain-gated-head-token", never "covers-gated-head-tokens"`,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// (d) covers-redundancy (fix round 2, S2): a gated head token answered
|
|
291
|
+
// `true` by MORE THAN ONE registered "covers-gated-head-tokens" set is a
|
|
292
|
+
// violation. Closes the escape check (c) cannot: a laundered set (its
|
|
293
|
+
// TRUE purpose is something other than recognising gated head tokens,
|
|
294
|
+
// relabeled "covers-gated-head-tokens" to dodge check (a)) whose
|
|
295
|
+
// declared `members` are HONESTLY all gated — e.g. `new
|
|
296
|
+
// Set(["harness"])` or `new Set(["harness", "gh"])` — passes check (c)
|
|
297
|
+
// cleanly (every declared member IS a live gated head token). The tell
|
|
298
|
+
// is redundancy: a gated head token legitimately has exactly ONE
|
|
299
|
+
// registered "covers" set responsible for it.
|
|
300
|
+
//
|
|
301
|
+
// SCOPE OF THIS CHECK, measured, not assumed (review round 3): the tell
|
|
302
|
+
// only exists for a token some covers-set ALREADY covers. For the four
|
|
303
|
+
// `documented-uncovered` tokens (`env`, `unset`, `tee`, `cp`) no
|
|
304
|
+
// covers-set covers them by definition, so `coveringSets.length` is 1
|
|
305
|
+
// and this check is structurally silent — while check (b) skips them and
|
|
306
|
+
// checks (c)/(e) pass because they ARE gated. A laundered set declaring
|
|
307
|
+
// only those tokens therefore passes everything. Do not read this check
|
|
308
|
+
// as a completeness proof; an earlier version of this comment did, and
|
|
309
|
+
// it was false for half the live vocabulary.
|
|
310
|
+
for (const token of facts.gatedHeadTokens) {
|
|
311
|
+
const coveringSets = registrations.filter((reg) => reg.intent === "covers-gated-head-tokens" && reg.has(token));
|
|
312
|
+
if (coveringSets.length > 1) {
|
|
313
|
+
for (const reg of coveringSets) {
|
|
314
|
+
const others = coveringSets.filter((r) => r.id !== reg.id).map((r) => r.id);
|
|
315
|
+
violations.push({
|
|
316
|
+
setId: reg.id,
|
|
317
|
+
intent: reg.intent,
|
|
318
|
+
token,
|
|
319
|
+
check: "covers-redundancy",
|
|
320
|
+
reason: `"${reg.id}" (${reg.module} — ${reg.description}) declares intent "covers-gated-head-tokens" and answers true for "${token}", but "${token}" is ALSO covered by ${others.length} other registered covers-set(s) (${others.join(", ")}) — a gated head token must be covered by exactly one registered covers-set; redundant coverage is the signature of an intent-laundered set whose declared members happen to already be genuinely gated elsewhere`,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// (e) covers-predicate-purity (fix round 2, S4/D-004): unlike check (c),
|
|
326
|
+
// which only inspects a registration's DECLARED `members`, this probes
|
|
327
|
+
// the registration's ACTUAL `has()` predicate directly against a fixed
|
|
328
|
+
// vocabulary — every live gated head token plus
|
|
329
|
+
// `PLAUSIBLE_NON_GATED_HEAD_TOKENS` — and demands `has(token)` answer
|
|
330
|
+
// `true` ONLY for tokens that ARE live gated head tokens. Closes the
|
|
331
|
+
// exact defect class this run exists to eliminate, reproduced INSIDE
|
|
332
|
+
// this guard: a predicate-based registration (`GIT_TOKEN_RE.test`)
|
|
333
|
+
// paired with a hand-written `members` list that check (c) verifies but
|
|
334
|
+
// the predicate ITSELF is never re-checked against — measured (D-004):
|
|
335
|
+
// widening `GIT_TOKEN_RE` to also match "docker" while leaving
|
|
336
|
+
// `members: ["git"]` untouched left the full suite green before this
|
|
337
|
+
// check existed.
|
|
338
|
+
const probeVocabulary = new Set([
|
|
339
|
+
...facts.gatedHeadTokens,
|
|
340
|
+
...PLAUSIBLE_NON_GATED_HEAD_TOKENS,
|
|
341
|
+
]);
|
|
342
|
+
for (const reg of registrations) {
|
|
343
|
+
if (reg.intent !== "covers-gated-head-tokens")
|
|
344
|
+
continue;
|
|
345
|
+
for (const token of probeVocabulary) {
|
|
346
|
+
if (reg.has(token) && !isHeadTokenGated(facts, token)) {
|
|
347
|
+
violations.push({
|
|
348
|
+
setId: reg.id,
|
|
349
|
+
intent: reg.intent,
|
|
350
|
+
token,
|
|
351
|
+
check: "covers-predicate-purity",
|
|
352
|
+
reason: `"${reg.id}" (${reg.module} — ${reg.description}) declares intent "covers-gated-head-tokens" and its has() predicate answers true for "${token}", but "${token}" is not a live gated head token — the predicate accepts more than its declared members claim, reproducing this run's own defect class (an engine-side predicate mirroring manifest content with no coupling) inside the guard itself`,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return violations;
|
|
358
|
+
}
|
|
359
|
+
//# sourceMappingURL=bash-match-registry.js.map
|