@event4u/agent-config 9.23.0 → 9.25.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +82 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/agent-handoff.md +10 -3
- package/dist/agent-src/commands/analyze/conformance.md +182 -0
- package/dist/agent-src/commands/analyze/inbox.md +221 -0
- package/dist/agent-src/commands/analyze.md +4 -3
- package/dist/agent-src/commands/pr/create.md +6 -6
- package/dist/agent-src/rules/context-hygiene.md +3 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +2 -0
- package/dist/agent-src/rules/evaluator-independence.md +105 -0
- package/dist/agent-src/rules/session-canary.md +13 -1
- package/dist/agent-src/rules/user-interaction.md +22 -0
- package/dist/agent-src/skills/finishing-a-development-branch/SKILL.md +1 -1
- package/dist/agent-src/skills/rule-writing/SKILL.md +45 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +114 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +96 -22
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +11 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +9 -6
- package/dist/hooks/dispatch.js +1026 -328
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/router.json +1 -1
- package/package.json +1 -1
- package/src/config/evaluator-budgets.json +68 -68
- package/src/config/gate-coverage.yml +103 -5
- package/src/config/gate-violation-baselines.json +11 -6
- package/src/scripts/_dispatch.bash +12 -0
- package/src/scripts/before_complete_hook.ts +127 -5
- package/src/scripts/check_depth_budget.ts +295 -0
- package/src/scripts/conformance_scan.ts +392 -0
- package/src/scripts/git_authorization_hook.ts +263 -0
- package/src/scripts/hook_manifest.yaml +50 -9
- package/src/scripts/hooks/block_unauthorized_git.ts +254 -0
- package/src/scripts/hooks/concern_registry.ts +8 -0
- package/src/scripts/hooks/evidence_independence.ts +334 -0
- package/src/scripts/language_mirror_hook.ts +267 -0
- package/src/scripts/lint_decision_impact.ts +246 -0
- package/src/scripts/lint_example_fences.ts +609 -0
- package/src/scripts/lint_rule_interactions.ts +90 -0
- package/src/scripts/lint_rule_migration_ledger.ts +480 -0
- package/src/scripts/lint_skill_descriptions.ts +86 -2
- package/src/scripts/measure_rule_absoluta.ts +231 -0
- package/src/scripts/schemas/rule.schema.json +10 -0
|
@@ -44,6 +44,17 @@
|
|
|
44
44
|
* construction and wake up exactly when a new skill lands close to an existing
|
|
45
45
|
* one. That is the point.
|
|
46
46
|
*
|
|
47
|
+
* A seventh check runs on EVERY skill and is neither absence nor presence — it
|
|
48
|
+
* is a prohibition:
|
|
49
|
+
*
|
|
50
|
+
* (g) preemption-phrase — the description claims priority over a sibling, or
|
|
51
|
+
* instructs activation regardless of the request.
|
|
52
|
+
* Unlike (a)-(f), this defect harms OTHER skills: it
|
|
53
|
+
* wins turns that belonged to them, and the loss is
|
|
54
|
+
* invisible at the source. See PREEMPTION_PATTERNS
|
|
55
|
+
* for why every pattern is anchored on the directive
|
|
56
|
+
* rather than on a vocabulary word.
|
|
57
|
+
*
|
|
47
58
|
* Deterministic, static-config only. Allowlist mirrors lint_skill_originality:
|
|
48
59
|
* `lint_skill_descriptions_allowlist.json`, capped at 20 (autonomous-execution
|
|
49
60
|
* allowlist-growth antipattern — over-cap means the linter is wrong).
|
|
@@ -78,6 +89,62 @@ const CONDITION_RE =
|
|
|
78
89
|
const IMPERATIVE_LEAD_RE =
|
|
79
90
|
/^["'`]*\s*(write|author|convert|review|run|add|build|generate|create|fix|wire|detect|analy[sz]e|optimi[sz]e|draft|extract|route|turn|produce|score|consolidate|rate|model|estimate|plan|check|audit|ship|emit|render|guide|poll|capture|explain|map|prepare|handle|manage|track|compare|validate|verify|enforce|choose|pick|enumerate|investigate|format|scaffold|design|define|describe|gate|block|refactor|migrate|transcribe|summari[sz]e|orchestrate|surface|propose|walk|stress-test|red-team|challenge|bootstrap|seed|install|deploy|package|split|merge|condense|harvest|evaluate|assess|diagnose|reproduce|trace|inspect|lint|test)/i;
|
|
80
91
|
|
|
92
|
+
/**
|
|
93
|
+
* (g) Preemption phrases — a description that argues for itself against the
|
|
94
|
+
* rest of the catalogue.
|
|
95
|
+
*
|
|
96
|
+
* Every other check here asks whether a description carries ENOUGH signal.
|
|
97
|
+
* This one is the opposite: a description claiming priority over a sibling, or
|
|
98
|
+
* instructing activation regardless of the request, degrades routing **globally
|
|
99
|
+
* and invisibly**. It does not misroute its own skill — it wins turns that
|
|
100
|
+
* belonged to other skills, and the loss surfaces as those skills mysteriously
|
|
101
|
+
* under-firing. The sweep behind this check found the class live in a shipped
|
|
102
|
+
* third-party description and in a second suite's own catalogue.
|
|
103
|
+
*
|
|
104
|
+
* Every pattern is anchored on the **directive**, never on a vocabulary word,
|
|
105
|
+
* and that is the whole design. A first pass keyed on the words alone
|
|
106
|
+
* (`overrides`, `supersedes`, `for every …`) returned three hits on this corpus
|
|
107
|
+
* and **all three were false positives**: `override-management` manages
|
|
108
|
+
* overrides, `decision-review` asks whether an ADR should be superseded, and
|
|
109
|
+
* `prediction-pool-optimizer` answers "every question" in a prediction pool.
|
|
110
|
+
* Those are subjects, not claims. The patterns below therefore require the
|
|
111
|
+
* shape of an instruction to the router (`always use`, `use this instead of`,
|
|
112
|
+
* `this skill takes precedence`), which no domain noun produces.
|
|
113
|
+
*/
|
|
114
|
+
const PREEMPTION_PATTERNS: ReadonlyArray<readonly [string, RegExp]> = [
|
|
115
|
+
[
|
|
116
|
+
'unconditional activation',
|
|
117
|
+
/\b(?:always|invariably|unconditionally)\s+(?:use|load|apply|run|activate|invoke|prefer)\b/i,
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
'activation regardless of the request',
|
|
121
|
+
/\b(?:regardless of|no matter)\s+(?:the|what|which)\b|\b(?:for|on)\s+(?:every|all|any)\s+(?:request|turn|prompt|conversation|session)s?\b|\beven if not (?:asked|requested)\b/i,
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
'priority over a sibling',
|
|
125
|
+
/\b(?:use|load|apply|invoke|prefer)\s+(?:this|these|it)\b[^.;]{0,60}?\b(?:instead of|rather than|over|in preference to)\b/i,
|
|
126
|
+
],
|
|
127
|
+
[
|
|
128
|
+
'authority claim over the catalogue',
|
|
129
|
+
/\bthis (?:skill|one|artifact)\s+(?:takes precedence|outranks|supersedes|overrides|wins)\b|\b(?:takes precedence over|outranks)\s+(?:all|any|every)\b/i,
|
|
130
|
+
],
|
|
131
|
+
[
|
|
132
|
+
'load-order claim',
|
|
133
|
+
/\b(?:before|prior to)\s+(?:any|all)\s+other\s+(?:skill|tool|rule)s?\b|\bload (?:this )?first\b/i,
|
|
134
|
+
],
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/** The first preemption claim in a description, or null. */
|
|
138
|
+
function preemptionPhrase(desc: string): { kind: string; match: string } | null {
|
|
139
|
+
for (const [kind, rx] of PREEMPTION_PATTERNS) {
|
|
140
|
+
const m = rx.exec(desc);
|
|
141
|
+
if (m) {
|
|
142
|
+
return { kind: kind as string, match: m[0] };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
81
148
|
interface Violation {
|
|
82
149
|
slug: string;
|
|
83
150
|
code:
|
|
@@ -86,7 +153,8 @@ interface Violation {
|
|
|
86
153
|
| 'triggers-are-name'
|
|
87
154
|
| 'no-routing-signal'
|
|
88
155
|
| 'clustered-no-sibling-routing'
|
|
89
|
-
| 'clustered-no-quoted-phrase'
|
|
156
|
+
| 'clustered-no-quoted-phrase'
|
|
157
|
+
| 'preemption-phrase';
|
|
90
158
|
detail: string;
|
|
91
159
|
}
|
|
92
160
|
|
|
@@ -204,6 +272,22 @@ function analyseSkill(slug: string, fm: Record<string, string>, siblings: readon
|
|
|
204
272
|
});
|
|
205
273
|
}
|
|
206
274
|
|
|
275
|
+
// (g) a claim of priority over the rest of the catalogue. Unlike (a)-(d),
|
|
276
|
+
// this defect harms OTHER skills, so it is checked on every skill and is
|
|
277
|
+
// never scoped to a cluster — a preemption phrase costs turns regardless of
|
|
278
|
+
// whether the router already had a near-neighbour to confuse it with.
|
|
279
|
+
const preemption = preemptionPhrase(desc);
|
|
280
|
+
if (preemption) {
|
|
281
|
+
out.push({
|
|
282
|
+
slug,
|
|
283
|
+
code: 'preemption-phrase',
|
|
284
|
+
detail:
|
|
285
|
+
`description claims ${preemption.kind} ("${preemption.match}") — a description ` +
|
|
286
|
+
'argues for its own routing conditions, never against its siblings; this wins ' +
|
|
287
|
+
'turns that belonged to other skills and the loss is invisible at the source',
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
207
291
|
// (e) + (f) fire ONLY for skills the canonical tool put in a cluster.
|
|
208
292
|
if (siblings.length > 0) {
|
|
209
293
|
const unrouted = siblings.filter((s) => !desc.includes(s));
|
|
@@ -332,4 +416,4 @@ if (path.resolve(process.argv[1] ?? '') === path.resolve(_HERE)) {
|
|
|
332
416
|
process.exit(main(process.argv.slice(2)));
|
|
333
417
|
}
|
|
334
418
|
|
|
335
|
-
export { analyseSkill, normalize, triggerPhrases, main };
|
|
419
|
+
export { analyseSkill, normalize, triggerPhrases, preemptionPhrase, main };
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Measure how many rules carry an imperative ABSOLUTE, and which terms.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. `road-to-rule-coherence` was designed around the figure
|
|
6
|
+
* "17 rules carry competing absolutes". A scratch census during that roadmap
|
|
7
|
+
* refuted it and reported 97 of 111 — and that refutation is what killed the
|
|
8
|
+
* precedence lattice and the coverage ratchet: a governance mechanism over 17
|
|
9
|
+
* outliers is proportionate, the same mechanism over ~90% of the corpus is
|
|
10
|
+
* declaration debt across almost all of it.
|
|
11
|
+
*
|
|
12
|
+
* But the 97 came from a throwaway script in a scratch directory, so it was an
|
|
13
|
+
* unverified number replacing an unverified number — the exact failure class it
|
|
14
|
+
* had just exposed. Re-deriving it here shows BOTH prior figures were lexicon
|
|
15
|
+
* artifacts:
|
|
16
|
+
*
|
|
17
|
+
* strict (ALL-CAPS imperatives only) 79 / 111 (71.2%)
|
|
18
|
+
* same lexicon, case-insensitive 97 / 111 (87.4%) <- the prior 97
|
|
19
|
+
* carries an Iron Law block (structural) 94 / 111 (84.7%)
|
|
20
|
+
*
|
|
21
|
+
* The 97 is reproducible — it is the case-insensitive reading, which counts
|
|
22
|
+
* lowercase "must"/"never" in explanatory prose as mandates. The strict reading
|
|
23
|
+
* is the better proxy for an imperative, but it has false negatives of its own:
|
|
24
|
+
* `downstream-changes` carries a full Iron Law block ("EVERY EDIT IS INCOMPLETE
|
|
25
|
+
* ... IS A CRITICAL FAILURE") using none of the lexicon terms, and scores 0.
|
|
26
|
+
*
|
|
27
|
+
* So no single number is defensible, and the third row is why that does not
|
|
28
|
+
* matter: the structural count needs no lexicon at all, and all three readings
|
|
29
|
+
* land at 71-87%. The conclusion the roadmap actually rests on — absolutes are
|
|
30
|
+
* the house style, not 17 outliers — is invariant to the method. Cite the range
|
|
31
|
+
* and the structural figure; do not cite a point estimate.
|
|
32
|
+
*
|
|
33
|
+
* NOT A GATE, deliberately. The `measure_` prefix keeps it out of
|
|
34
|
+
* `_lib/gate_population`'s gate set, and it emits no `scanned:` contract line,
|
|
35
|
+
* so it carries no `gate-coverage.yml` registration duty. There is nothing here
|
|
36
|
+
* to enforce: "how many rules speak in absolutes" is a property of the house
|
|
37
|
+
* writing style, not a defect with a threshold. Adding a threshold would
|
|
38
|
+
* recreate the ratchet the measurement itself argued against.
|
|
39
|
+
*
|
|
40
|
+
* Frontmatter is excluded — `description:` routinely contains trigger words
|
|
41
|
+
* that are not imperatives addressed to the agent.
|
|
42
|
+
*
|
|
43
|
+
* Output: default table, `--json` for the deterministic record.
|
|
44
|
+
*
|
|
45
|
+
* SCOPE: this is a CLOSED decision input, not ongoing instrumentation — see
|
|
46
|
+
* docs/decisions/ADR-218-absoluta-census-is-a-closed-decision-input.md. Nothing
|
|
47
|
+
* consumes the number; it exists so the figures in that record can be
|
|
48
|
+
* re-derived instead of cited from a commit message. A future "did this PR
|
|
49
|
+
* introduce a new absolute?" check is a DIFFERENT tool that watches diffs.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import * as fs from 'node:fs';
|
|
53
|
+
import * as path from 'node:path';
|
|
54
|
+
import { pathToFileURL } from 'node:url';
|
|
55
|
+
|
|
56
|
+
import { SRC_RULES } from './_lib/agent_src.js';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The lexicon IS the measurement — enumerated here rather than described, so a
|
|
60
|
+
* re-run reproduces the number exactly and a disagreement about the number is a
|
|
61
|
+
* disagreement about this list.
|
|
62
|
+
*
|
|
63
|
+
* Case-sensitive where the term is an ALL-CAPS imperative (`NEVER`, `ALWAYS`,
|
|
64
|
+
* `STOP`, `MUST`, `FORBIDDEN`): lowercase "never" appears in ordinary prose
|
|
65
|
+
* ("never mind that") and is not a mandate. Case-insensitive where the phrase
|
|
66
|
+
* is a mandate in any casing (`non-bypassable`, `Hard Floor`, `no exceptions`)
|
|
67
|
+
* or a verb stem (`refuse`, `abort`).
|
|
68
|
+
*/
|
|
69
|
+
export const ABSOLUTA_LEXICON: ReadonlyArray<{
|
|
70
|
+
term: string;
|
|
71
|
+
pattern: RegExp;
|
|
72
|
+
note: string;
|
|
73
|
+
}> = [
|
|
74
|
+
{ term: 'NEVER', pattern: /\bNEVER\b/g, note: 'ALL-CAPS only — lowercase "never" is ordinary prose' },
|
|
75
|
+
{ term: 'ALWAYS', pattern: /\bALWAYS\b/g, note: 'ALL-CAPS only' },
|
|
76
|
+
{ term: 'STOP', pattern: /\bSTOP\b/g, note: 'ALL-CAPS only' },
|
|
77
|
+
{ term: 'MUST', pattern: /\bMUST\b/g, note: 'ALL-CAPS only — lowercase "must" is common in explanatory prose' },
|
|
78
|
+
{ term: 'FORBIDDEN', pattern: /\bFORBIDDEN\b/g, note: 'ALL-CAPS only' },
|
|
79
|
+
{ term: 'non-bypassable', pattern: /non-bypassable/gi, note: 'any casing — always a mandate' },
|
|
80
|
+
{ term: 'Hard Floor', pattern: /hard floor/gi, note: 'any casing — names the supremacy tier' },
|
|
81
|
+
{ term: 'no exceptions', pattern: /no exceptions/gi, note: 'any casing' },
|
|
82
|
+
{ term: 'refuse', pattern: /\brefuses?\b|\brefusal\b/gi, note: 'verb stem, any casing' },
|
|
83
|
+
{ term: 'abort', pattern: /\baborts?\b/gi, note: 'verb stem, any casing' },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
export interface RuleAbsoluta {
|
|
87
|
+
rule: string;
|
|
88
|
+
total: number;
|
|
89
|
+
perTerm: Record<string, number>;
|
|
90
|
+
/** True when the file has a body but no absolute — the pointer-stub class. */
|
|
91
|
+
stub: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface AbsolutaCensus {
|
|
95
|
+
rules_scanned: number;
|
|
96
|
+
rules_with_absolute: number;
|
|
97
|
+
rules_without_absolute: number;
|
|
98
|
+
share_with_absolute: number;
|
|
99
|
+
occurrences_total: number;
|
|
100
|
+
per_term: Record<string, { occurrences: number; rules: number }>;
|
|
101
|
+
without_absolute: string[];
|
|
102
|
+
per_rule: RuleAbsoluta[];
|
|
103
|
+
/** Case-insensitive reading of the same lexicon — the upper bound. */
|
|
104
|
+
rules_with_absolute_loose: number;
|
|
105
|
+
/**
|
|
106
|
+
* Structural, lexicon-independent signal: rules carrying an `Iron Law`
|
|
107
|
+
* heading or a `**Iron Law` marker. This is the number that does NOT move
|
|
108
|
+
* when the lexicon is argued about, which is why it is reported alongside.
|
|
109
|
+
*/
|
|
110
|
+
rules_with_iron_law: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** `## Iron Law`, `### Iron Laws`, `**Iron Law.**`, `Iron Law 1`, … */
|
|
114
|
+
const IRON_LAW_MARKER = /^#{1,6}\s+.*Iron Law|^\*\*Iron Law/im;
|
|
115
|
+
|
|
116
|
+
/** Strip YAML frontmatter; return the body only. */
|
|
117
|
+
export function stripFrontmatter(text: string): string {
|
|
118
|
+
if (!text.startsWith('---\n')) return text;
|
|
119
|
+
const end = text.indexOf('\n---\n', 4);
|
|
120
|
+
if (end === -1) return text;
|
|
121
|
+
return text.slice(end + '\n---\n'.length);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function censusAbsoluta(rulesDir: string): AbsolutaCensus {
|
|
125
|
+
const files = fs
|
|
126
|
+
.readdirSync(rulesDir)
|
|
127
|
+
.filter((f) => f.endsWith('.md'))
|
|
128
|
+
.sort();
|
|
129
|
+
|
|
130
|
+
const perRule: RuleAbsoluta[] = [];
|
|
131
|
+
const perTerm: Record<string, { occurrences: number; rules: number }> = {};
|
|
132
|
+
for (const { term } of ABSOLUTA_LEXICON) perTerm[term] = { occurrences: 0, rules: 0 };
|
|
133
|
+
|
|
134
|
+
let occurrences = 0;
|
|
135
|
+
let looseHits = 0;
|
|
136
|
+
let ironLawHits = 0;
|
|
137
|
+
for (const file of files) {
|
|
138
|
+
const body = stripFrontmatter(fs.readFileSync(path.join(rulesDir, file), 'utf-8'));
|
|
139
|
+
const counts: Record<string, number> = {};
|
|
140
|
+
let total = 0;
|
|
141
|
+
let looseTotal = 0;
|
|
142
|
+
for (const { term, pattern } of ABSOLUTA_LEXICON) {
|
|
143
|
+
// Fresh regex per file: /g patterns carry lastIndex across calls.
|
|
144
|
+
const n = (body.match(new RegExp(pattern.source, pattern.flags)) ?? []).length;
|
|
145
|
+
counts[term] = n;
|
|
146
|
+
total += n;
|
|
147
|
+
if (n > 0) {
|
|
148
|
+
perTerm[term]!.occurrences += n;
|
|
149
|
+
perTerm[term]!.rules += 1;
|
|
150
|
+
}
|
|
151
|
+
// Loose reading: same lexicon, case-insensitive throughout. This is
|
|
152
|
+
// the upper bound, and it is the reading a prior scratch census
|
|
153
|
+
// used — reproducing it here is what identifies that method.
|
|
154
|
+
looseTotal += (body.match(new RegExp(pattern.source, 'gi')) ?? []).length;
|
|
155
|
+
}
|
|
156
|
+
occurrences += total;
|
|
157
|
+
if (looseTotal > 0) looseHits += 1;
|
|
158
|
+
if (IRON_LAW_MARKER.test(body)) ironLawHits += 1;
|
|
159
|
+
perRule.push({ rule: path.basename(file, '.md'), total, perTerm: counts, stub: total === 0 });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const withAbs = perRule.filter((r) => r.total > 0).length;
|
|
163
|
+
return {
|
|
164
|
+
rules_scanned: perRule.length,
|
|
165
|
+
rules_with_absolute: withAbs,
|
|
166
|
+
rules_without_absolute: perRule.length - withAbs,
|
|
167
|
+
share_with_absolute: perRule.length === 0 ? 0 : Number((withAbs / perRule.length).toFixed(4)),
|
|
168
|
+
occurrences_total: occurrences,
|
|
169
|
+
per_term: perTerm,
|
|
170
|
+
without_absolute: perRule.filter((r) => r.stub).map((r) => r.rule),
|
|
171
|
+
per_rule: perRule,
|
|
172
|
+
rules_with_absolute_loose: looseHits,
|
|
173
|
+
rules_with_iron_law: ironLawHits,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function render(c: AbsolutaCensus): string {
|
|
178
|
+
const lines: string[] = [];
|
|
179
|
+
lines.push('Rule absoluta census');
|
|
180
|
+
lines.push('');
|
|
181
|
+
lines.push(
|
|
182
|
+
` rules scanned ${c.rules_scanned}`,
|
|
183
|
+
);
|
|
184
|
+
lines.push(
|
|
185
|
+
` carry >=1 absolute ${c.rules_with_absolute} (${(c.share_with_absolute * 100).toFixed(1)}%)`,
|
|
186
|
+
);
|
|
187
|
+
lines.push(` carry none ${c.rules_without_absolute}`);
|
|
188
|
+
lines.push(
|
|
189
|
+
` same lexicon, loose ${c.rules_with_absolute_loose} (${((c.rules_with_absolute_loose / c.rules_scanned) * 100).toFixed(1)}%) <- case-insensitive upper bound`,
|
|
190
|
+
);
|
|
191
|
+
lines.push(
|
|
192
|
+
` carry an Iron Law ${c.rules_with_iron_law} (${((c.rules_with_iron_law / c.rules_scanned) * 100).toFixed(1)}%) <- structural, lexicon-free`,
|
|
193
|
+
);
|
|
194
|
+
lines.push(` total occurrences ${c.occurrences_total} (strict)`);
|
|
195
|
+
lines.push('');
|
|
196
|
+
lines.push(' per term (occurrences / rules):');
|
|
197
|
+
for (const { term } of ABSOLUTA_LEXICON) {
|
|
198
|
+
const e = c.per_term[term]!;
|
|
199
|
+
lines.push(` ${term.padEnd(16)} ${String(e.occurrences).padStart(4)} / ${e.rules}`);
|
|
200
|
+
}
|
|
201
|
+
if (c.without_absolute.length > 0) {
|
|
202
|
+
lines.push('');
|
|
203
|
+
lines.push(' rules with NO absolute (expected: migrated pointer stubs):');
|
|
204
|
+
for (const r of c.without_absolute) lines.push(` ${r}`);
|
|
205
|
+
}
|
|
206
|
+
return lines.join('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function main(argv: string[]): number {
|
|
210
|
+
const json = argv.includes('--json');
|
|
211
|
+
const dir = SRC_RULES();
|
|
212
|
+
if (!fs.existsSync(dir)) {
|
|
213
|
+
process.stderr.write(`measure_rule_absoluta: rules dir missing: ${dir}\n`);
|
|
214
|
+
return 1;
|
|
215
|
+
}
|
|
216
|
+
const c = censusAbsoluta(dir);
|
|
217
|
+
if (c.rules_scanned === 0) {
|
|
218
|
+
// A census over zero files would print a confident 0% — the
|
|
219
|
+
// scan-nothing-and-look-green shape this repo has been burned by.
|
|
220
|
+
process.stderr.write(`measure_rule_absoluta: scanned 0 rules under ${dir}\n`);
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
223
|
+
process.stdout.write((json ? JSON.stringify(c, null, 2) : render(c)) + '\n');
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* c8 ignore start */
|
|
228
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
229
|
+
process.exitCode = main(process.argv.slice(2));
|
|
230
|
+
}
|
|
231
|
+
/* c8 ignore stop */
|
|
@@ -18,6 +18,16 @@
|
|
|
18
18
|
"enum": ["package", "project"],
|
|
19
19
|
"default": "package"
|
|
20
20
|
},
|
|
21
|
+
"decision_impact": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"enum": ["overrides-model-default", "encodes-house-choice", "already-complied-with"],
|
|
24
|
+
"description": "What is at stake when this rule fires — distinct from `type`, which is a DELIVERY class (how the rule reaches the model). `overrides-model-default`: the model's base behaviour is wrong here, so the rule is load-bearing. `encodes-house-choice`: the model would make *a* defensible choice but not *our* choice. `already-complied-with`: the model does this unprompted — the one value that permits removal, and therefore the one carrying an evidence bar (`decision_impact_evidence`, enforced by lint_decision_impact). OPTIONAL by design: classifying all 111 rules in one batch is exactly the shape that trips check_kernel_prefix_stability, so it lands on new rules and on rules touched for another reason."
|
|
25
|
+
},
|
|
26
|
+
"decision_impact_evidence": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"minLength": 20,
|
|
29
|
+
"description": "Required when `decision_impact: already-complied-with`, forbidden otherwise. A pointer a reviewer can follow to the observation that the model complies without this rule — an eval report, a PR discussion, a trajectory sample. The bar exists because that classification is what permits deleting a rule, and 'the model probably does this anyway' is the cheapest possible way to remove a floor that was working precisely because nothing had crossed it."
|
|
30
|
+
},
|
|
21
31
|
"description": {
|
|
22
32
|
"type": "string",
|
|
23
33
|
"minLength": 1,
|