@event4u/agent-config 9.22.0 → 9.24.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 +84 -238
- package/README.md +2 -2
- package/dist/agent-src/commands/agent-handoff.md +10 -3
- package/dist/agent-src/commands/analyze/inbox.md +190 -0
- package/dist/agent-src/commands/analyze.md +4 -3
- package/dist/agent-src/commands/optimize/deep.md +13 -0
- package/dist/agent-src/commands/pr/create.md +6 -6
- package/dist/agent-src/contexts/execution/cheap-question-mechanics.md +12 -2
- package/dist/agent-src/contexts/execution/mandated-lines.md +180 -0
- package/dist/agent-src/rules/context-hygiene.md +20 -0
- package/dist/agent-src/rules/downstream-changes.md +26 -0
- package/dist/agent-src/rules/think-before-action.md +25 -0
- package/dist/agent-src/rules/token-budget-discipline.md +46 -3
- package/dist/agent-src/rules/token-efficiency.md +13 -0
- package/dist/agent-src/rules/ui-audit-gate.md +33 -2
- package/dist/agent-src/skills/adversarial-review/SKILL.md +18 -4
- package/dist/agent-src/skills/decision-review/SKILL.md +49 -0
- package/dist/agent-src/skills/judge-artifact-completeness/SKILL.md +5 -1
- package/dist/agent-src/skills/judge-bug-hunter/SKILL.md +16 -0
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +14 -1
- package/dist/agent-src/skills/rule-writing/SKILL.md +33 -0
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +31 -0
- package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
- 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/cli-delegate/chunk-DVRS2KVR.js +4602 -0
- package/dist/cli-delegate/{chunk-ELMFOQPW.js → chunk-SJQMJI7M.js} +8 -4597
- package/dist/cli-delegate/cmd_export.js +2 -1
- package/dist/cli-delegate/cmd_prune.js +2 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -1
- package/dist/cli-delegate/cmd_settings_set.js +3370 -0
- package/dist/cli-delegate/cmd_sync.js +2 -1
- package/dist/cli-delegate/cmd_uninstall.js +2 -1
- package/dist/cli-delegate/cmd_validate.js +2 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +54 -27
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +79 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/scripts/condense.js +47 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/routes/settings.js +82 -0
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/shared/settingsClasses.js +191 -0
- package/dist/shared/settingsClasses.js.map +1 -0
- package/docs/contracts/settings-classes.md +342 -0
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +23 -2
- package/docs/guidelines/agent-infra/false-green.md +187 -0
- package/docs/guidelines/agent-infra/gate-authoring.md +177 -0
- package/package.json +3 -1
- package/src/config/ci-local-parity.yml +12 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +54 -12
- package/src/config/gate-violation-baselines.json +6 -0
- package/src/config/pack-size-budget.json +4 -3
- package/src/scripts/_cli/cmd_settings_set.ts +509 -0
- package/src/scripts/_dispatch.bash +16 -0
- package/src/scripts/_lib/gate_ledger.ts +295 -0
- package/src/scripts/_lib/gate_result.ts +101 -0
- package/src/scripts/_lib/gate_self_test.ts +129 -0
- package/src/scripts/_lib/measured_render.ts +97 -0
- package/src/scripts/_lib/ratchet_base_ref.ts +373 -0
- package/src/scripts/check_gate_completeness.ts +244 -0
- package/src/scripts/check_gate_coverage.ts +35 -6
- package/src/scripts/check_references.ts +125 -15
- package/src/scripts/check_review_dispositions.ts +14 -1
- package/src/scripts/check_suppression_hygiene.ts +503 -0
- package/src/scripts/condense.ts +47 -2
- package/src/scripts/hook_manifest.yaml +23 -0
- package/src/scripts/hooks/dispatch_hook.ts +65 -1
- package/src/scripts/hooks/host_semantics.ts +140 -0
- package/src/scripts/lint_framework_leakage.ts +147 -15
- package/src/scripts/lint_mandated_lines.ts +339 -0
- package/src/scripts/lint_roadmap_complexity.ts +102 -11
- package/src/scripts/lint_roadmap_family_cap.ts +191 -0
- package/src/scripts/lint_settings_classes.ts +397 -0
- package/src/scripts/lint_token_budget_discipline.ts +109 -1
- package/src/scripts/measure_rule_absoluta.ts +231 -0
- package/src/scripts/report_imperative_density.ts +229 -0
- package/src/scripts/verify_physical_move.ts +10 -1
- package/src/server/io/yamlIO.ts +173 -0
- package/src/shared/settingsClasses.ts +235 -0
|
@@ -10,6 +10,27 @@
|
|
|
10
10
|
* 2. JUSTIFICATION — every `rich`-tagged skill carries a
|
|
11
11
|
* `## Why this skill is rich` section explaining the irreducible
|
|
12
12
|
* complexity.
|
|
13
|
+
* 3. SIZE — a `rich`-tagged skill stays under the declared ceiling
|
|
14
|
+
* (3,500 tokens, ADR-217). Measured with the exact BPE tokenizer
|
|
15
|
+
* where `js-tiktoken` resolves and with the character proxy where it does
|
|
16
|
+
* not; the gate says which. A proxy measurement sitting within its own
|
|
17
|
+
* error margin of the ceiling is reported UNRESOLVED rather than silently
|
|
18
|
+
* classified — the proxy runs ~5 % off per file IN EITHER DIRECTION, and a
|
|
19
|
+
* reading whose error band straddles the ceiling has not decided anything.
|
|
20
|
+
*
|
|
21
|
+
* The band was documentation until ADR-217: the ceiling described no artifact
|
|
22
|
+
* that existed (measured max 3,331 of a declared 5,000), so nothing ever
|
|
23
|
+
* noticed that its top half was unused. An unused permission costs nothing
|
|
24
|
+
* until someone uses it.
|
|
25
|
+
*
|
|
26
|
+
* The band's FLOOR is documented and deliberately NOT gated. Running the check
|
|
27
|
+
* once found `accessibility-auditor` at 1,931 tokens — under the 2,000 floor
|
|
28
|
+
* while legitimately holding the class, because `rich` buys exemption from
|
|
29
|
+
* condensation and that is a claim about what compression would LOSE, not
|
|
30
|
+
* about how large the file is. The published measurement supplies a
|
|
31
|
+
* degradation threshold, which is a ceiling; nothing measures a minimum. A
|
|
32
|
+
* floor gate would have forced a WCAG-criteria skill out of the exemption on a
|
|
33
|
+
* number with no evidence behind it.
|
|
13
34
|
*
|
|
14
35
|
* Additionally, any `token_budget_class` value outside
|
|
15
36
|
* `lean | standard | rich` is a finding (schema drift).
|
|
@@ -20,6 +41,8 @@ import * as fs from 'node:fs';
|
|
|
20
41
|
import * as path from 'node:path';
|
|
21
42
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
22
43
|
|
|
44
|
+
import { gpt_tokens } from './_lib/token_count.js';
|
|
45
|
+
|
|
23
46
|
const _HERE = fileURLToPath(import.meta.url);
|
|
24
47
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
25
48
|
|
|
@@ -32,6 +55,17 @@ export const VALID_CLASSES = ['lean', 'standard', 'rich'] as const;
|
|
|
32
55
|
|
|
33
56
|
export const RICH_SECTION_HEADING = '## Why this skill is rich';
|
|
34
57
|
|
|
58
|
+
/** Declared band for the rich class (ADR-217). Floor unchanged; ceiling 5,000 → 3,500. */
|
|
59
|
+
export const RICH_MIN_TOKENS = 2000;
|
|
60
|
+
export const RICH_MAX_TOKENS = 3500;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Worst-case per-file error of the character proxy, measured over the four
|
|
64
|
+
* rich artifacts on 2026-08-06 (−5.3 % on the largest). A proxy measurement
|
|
65
|
+
* inside this margin of a boundary is not a verdict.
|
|
66
|
+
*/
|
|
67
|
+
export const PROXY_ERROR_MARGIN = 0.06;
|
|
68
|
+
|
|
35
69
|
export interface Finding {
|
|
36
70
|
/** repo-relative path */
|
|
37
71
|
file: string;
|
|
@@ -44,6 +78,45 @@ export interface ScanResult {
|
|
|
44
78
|
scanned: number;
|
|
45
79
|
/** Skills declaring token_budget_class: rich. */
|
|
46
80
|
richCount: number;
|
|
81
|
+
/** Per-rich-skill size measurements, for the report line. */
|
|
82
|
+
sizes: RichSize[];
|
|
83
|
+
/** True when every size came from the exact BPE tokenizer. */
|
|
84
|
+
exactThroughout: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface RichSize {
|
|
88
|
+
file: string;
|
|
89
|
+
tokens: number;
|
|
90
|
+
exact: boolean;
|
|
91
|
+
/** Set when a proxy measurement lands inside its own error margin of a boundary. */
|
|
92
|
+
unresolved: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Measure one artifact, exact where the tokenizer resolved.
|
|
97
|
+
*
|
|
98
|
+
* Split out so the size rule is testable without a filesystem: the boundary
|
|
99
|
+
* cases that matter (exactly at the cap, one token over, a proxy reading a
|
|
100
|
+
* hair under) are the ones a fixture tree makes awkward and a function call
|
|
101
|
+
* makes trivial.
|
|
102
|
+
*/
|
|
103
|
+
export function classify_size(tokens: number, exact: boolean): { over: boolean; unresolved: boolean } {
|
|
104
|
+
if (exact) {
|
|
105
|
+
return { over: tokens > RICH_MAX_TOKENS, unresolved: false };
|
|
106
|
+
}
|
|
107
|
+
// A proxy reading is a band around the truth, and the band is SYMMETRIC.
|
|
108
|
+
// The first version applied the margin upward only — guarding a proxy that
|
|
109
|
+
// reads low — while the measured error on the largest artifact runs the
|
|
110
|
+
// other way: 3,518 by proxy against 3,331 exact, reading 5.3 % HIGH. That
|
|
111
|
+
// version therefore hard-failed the one artifact ADR-217 rules in-band,
|
|
112
|
+
// on any machine without the tokenizer (`js-tiktoken` is a devDependency).
|
|
113
|
+
//
|
|
114
|
+
// So: a proxy verdict counts only when the WHOLE band falls on one side of
|
|
115
|
+
// the ceiling. A band straddling it is `unresolved` — which is a finding,
|
|
116
|
+
// but one that says "measure properly" rather than "this is too big".
|
|
117
|
+
const margin = Math.ceil(tokens * PROXY_ERROR_MARGIN);
|
|
118
|
+
const straddles = tokens - margin <= RICH_MAX_TOKENS && tokens + margin > RICH_MAX_TOKENS;
|
|
119
|
+
return { over: !straddles && tokens - margin > RICH_MAX_TOKENS, unresolved: straddles };
|
|
47
120
|
}
|
|
48
121
|
|
|
49
122
|
/** `token_budget_class` value from a SKILL.md's YAML frontmatter, or null. */
|
|
@@ -63,6 +136,7 @@ export function frontmatter_class(content: string): string | null {
|
|
|
63
136
|
/** Scan one skills root (directory of `<skill>/SKILL.md`). */
|
|
64
137
|
export function scan_skills(skillsRoot: string): ScanResult {
|
|
65
138
|
const findings: Finding[] = [];
|
|
139
|
+
const sizes: RichSize[] = [];
|
|
66
140
|
let scanned = 0;
|
|
67
141
|
let richCount = 0;
|
|
68
142
|
const entries = fs
|
|
@@ -97,6 +171,28 @@ export function scan_skills(skillsRoot: string): ScanResult {
|
|
|
97
171
|
message: `rich-tagged skill missing the '${RICH_SECTION_HEADING}' justification section`,
|
|
98
172
|
});
|
|
99
173
|
}
|
|
174
|
+
const measured = gpt_tokens(content);
|
|
175
|
+
const verdict = classify_size(measured.tokens, measured.exact);
|
|
176
|
+
sizes.push({ file: rel, tokens: measured.tokens, exact: measured.exact, unresolved: verdict.unresolved });
|
|
177
|
+
const how = measured.exact ? 'exact BPE' : 'chars/4 proxy';
|
|
178
|
+
if (verdict.over) {
|
|
179
|
+
findings.push({
|
|
180
|
+
file: rel,
|
|
181
|
+
message:
|
|
182
|
+
`rich-class size band exceeded: ${String(measured.tokens)} tokens (${how}) > ` +
|
|
183
|
+
`${String(RICH_MAX_TOKENS)} (ADR-217). Split by responsibility, or argue the ` +
|
|
184
|
+
'ceiling in a decision record — not in this file.',
|
|
185
|
+
});
|
|
186
|
+
} else if (verdict.unresolved) {
|
|
187
|
+
findings.push({
|
|
188
|
+
file: rel,
|
|
189
|
+
message:
|
|
190
|
+
`rich-class size UNRESOLVED: ${String(measured.tokens)} tokens (${how}) sits within the ` +
|
|
191
|
+
`${String(PROXY_ERROR_MARGIN * 100)} % proxy margin of a band boundary. Install the exact ` +
|
|
192
|
+
'tokenizer (js-tiktoken is a declared dependency) and re-run — a proxy reading this ' +
|
|
193
|
+
'close has not decided anything.',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
100
196
|
}
|
|
101
197
|
}
|
|
102
198
|
if (scanned > 0 && richCount > scanned * RICH_RATIO_CAP) {
|
|
@@ -108,7 +204,7 @@ export function scan_skills(skillsRoot: string): ScanResult {
|
|
|
108
204
|
`max ${String(Math.floor(scanned * RICH_RATIO_CAP))})`,
|
|
109
205
|
});
|
|
110
206
|
}
|
|
111
|
-
return { findings, scanned, richCount };
|
|
207
|
+
return { findings, scanned, richCount, sizes, exactThroughout: sizes.every((s) => s.exact) };
|
|
112
208
|
}
|
|
113
209
|
|
|
114
210
|
export function main(argv?: readonly string[]): number {
|
|
@@ -134,9 +230,21 @@ export function main(argv?: readonly string[]): number {
|
|
|
134
230
|
process.stdout.write(`❌ ${f.file} ${f.message}\n`);
|
|
135
231
|
}
|
|
136
232
|
if (result.findings.length === 0 && !quiet) {
|
|
233
|
+
const how = result.exactThroughout ? 'exact BPE' : 'chars/4 proxy (js-tiktoken did not resolve)';
|
|
234
|
+
const largest = result.sizes.reduce((max, s) => (s.tokens > max ? s.tokens : max), 0);
|
|
137
235
|
process.stdout.write(
|
|
138
236
|
`✅ token-budget discipline: ${String(result.richCount)} rich of ${String(result.scanned)} skills within the ${String(RICH_RATIO_CAP * 100)} % cap; every rich skill justifies itself\n`,
|
|
139
237
|
);
|
|
238
|
+
// Publish the sizes on the GREEN path too. A band nobody sees the
|
|
239
|
+
// numbers for is how this one spent months describing no artifact
|
|
240
|
+
// that existed.
|
|
241
|
+
process.stdout.write(
|
|
242
|
+
` size ceiling ${String(RICH_MAX_TOKENS)} (ADR-217; the ${String(RICH_MIN_TOKENS)} floor is documented, not gated), ` +
|
|
243
|
+
`measured ${how}; largest ${String(largest)}\n`,
|
|
244
|
+
);
|
|
245
|
+
for (const s of result.sizes) {
|
|
246
|
+
process.stdout.write(` ${String(s.tokens).padStart(5)} ${s.file}\n`);
|
|
247
|
+
}
|
|
140
248
|
}
|
|
141
249
|
// gate-coverage contract (src/config/gate-coverage.yml): files inspected.
|
|
142
250
|
process.stdout.write(`scanned: ${String(result.scanned)}\n`);
|
|
@@ -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 */
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* report_imperative_density.ts — an advisory number whose direction is unknown.
|
|
4
|
+
*
|
|
5
|
+
* Prints the ratio of imperative-marker lines to prose lines across the
|
|
6
|
+
* authored rules and skills. **It gates on nothing, and it must not acquire a
|
|
7
|
+
* threshold without the measurement that would justify one.**
|
|
8
|
+
*
|
|
9
|
+
* WHY THIS IS NOT A GATE, AND MUST NOT BECOME ONE
|
|
10
|
+
* -----------------------------------------------
|
|
11
|
+
* Three external sources disagree about which direction is better. One asserts
|
|
12
|
+
* reasoning-based phrasing outperforms rigid directives. One ships a metric
|
|
13
|
+
* that scores instruction files HIGHER for imperative-marker density — the
|
|
14
|
+
* opposite ranking from the same observation. A third sits between them and
|
|
15
|
+
* supplies the only part all three can live with: a rule the agent consistently
|
|
16
|
+
* fails to follow needs structural enforcement or deletion, not a louder rule.
|
|
17
|
+
*
|
|
18
|
+
* This suite's house style sits at the imperative end and has no measurement
|
|
19
|
+
* either way. A gate would prejudge exactly the question the sweep left open
|
|
20
|
+
* (`skill-ecosystem-sweep-2026-08.md` § R2). An advisory number is the input a
|
|
21
|
+
* future decision needs; a target is the thing that would corrupt it, because
|
|
22
|
+
* the moment a number is optimised it stops measuring what it measured.
|
|
23
|
+
*
|
|
24
|
+
* So: no exit-2, no baseline entry, no `gate-coverage.yml` row, and the header
|
|
25
|
+
* of every run says the direction is unsettled. Read it as a description, not
|
|
26
|
+
* as a score.
|
|
27
|
+
*
|
|
28
|
+
* Exit: 0 always (except a usage/IO error, which is 1). That is deliberate.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as fs from 'node:fs';
|
|
32
|
+
import * as path from 'node:path';
|
|
33
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
34
|
+
|
|
35
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
36
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
37
|
+
|
|
38
|
+
export const RULES_ROOT = 'src/rules';
|
|
39
|
+
export const SKILLS_ROOT = 'src/skills';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Lines counted as imperative markers.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately narrow and stated: an all-caps directive line, a bullet opening
|
|
45
|
+
* with a modal or a bare imperative verb, and the fenced Iron-Law blocks this
|
|
46
|
+
* suite uses. A broader net would inflate the number without making it mean
|
|
47
|
+
* more, and the number's meaning is already the open question.
|
|
48
|
+
*/
|
|
49
|
+
const IMPERATIVE_PATTERNS: ReadonlyArray<readonly [string, RegExp]> = [
|
|
50
|
+
['all-caps directive', /^[^a-z]*\b(NEVER|ALWAYS|MUST|DO NOT|STOP|REFUSE|FORBIDDEN)\b[^a-z]*$/],
|
|
51
|
+
['inline modal', /\b(MUST|NEVER|ALWAYS|DO NOT|SHALL)\b/],
|
|
52
|
+
['imperative bullet', /^\s*[-*]\s+(Always|Never|Do not|Don't|Use|Run|Check|Verify|Refuse|Stop|Add|Remove|Keep|Prefer|Avoid)\b/],
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export interface FileDensity {
|
|
56
|
+
file: string;
|
|
57
|
+
prose: number;
|
|
58
|
+
imperative: number;
|
|
59
|
+
ratio: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DensityReport {
|
|
63
|
+
files: FileDensity[];
|
|
64
|
+
totalProse: number;
|
|
65
|
+
totalImperative: number;
|
|
66
|
+
/** Ratio over the whole corpus, or null when nothing was counted. */
|
|
67
|
+
ratio: number | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Lines that carry prose. Fenced code, frontmatter, table rows, and blanks are
|
|
72
|
+
* excluded — counting them would make the ratio a function of how much YAML a
|
|
73
|
+
* file happens to carry.
|
|
74
|
+
*/
|
|
75
|
+
export function proseLines(text: string): string[] {
|
|
76
|
+
const lines = text.split('\n');
|
|
77
|
+
|
|
78
|
+
// Frontmatter only when the opening `---` actually CLOSES. A file whose
|
|
79
|
+
// first line is a horizontal rule used to swallow everything to the next
|
|
80
|
+
// `---`, and a file with an unterminated opener swallowed all of it.
|
|
81
|
+
let start = 0;
|
|
82
|
+
if (lines[0]?.trim() === '---') {
|
|
83
|
+
const close = lines.findIndex((l, i) => i > 0 && l.trim() === '---');
|
|
84
|
+
// …and only when the block LOOKS like YAML. `---` opening a document
|
|
85
|
+
// that then reads as prose is a horizontal rule, and treating it as
|
|
86
|
+
// frontmatter silently ate everything up to the next one.
|
|
87
|
+
const looksYaml = close > 0 && lines.slice(1, close).some((l) => /^\s*[\w.-]+:\s/.test(l));
|
|
88
|
+
if (close !== -1 && looksYaml) start = close + 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fences are matched BY CHARACTER, so `~~~` cannot close a ```-opened
|
|
92
|
+
// block, and an opener that never closes is treated as though it never
|
|
93
|
+
// opened — dropping the rest of a file silently is worse than counting a
|
|
94
|
+
// few code lines as prose, because the denominator is the thing this
|
|
95
|
+
// report publishes.
|
|
96
|
+
const openers: Array<{ index: number; marker: string }> = [];
|
|
97
|
+
const fenced = new Set<number>();
|
|
98
|
+
let open: { index: number; marker: string } | null = null;
|
|
99
|
+
for (let i = start; i < lines.length; i++) {
|
|
100
|
+
const m = /^\s*(`{3,}|~{3,})/.exec(lines[i] ?? '');
|
|
101
|
+
if (m === null) continue;
|
|
102
|
+
const marker = (m[1] as string)[0] as string;
|
|
103
|
+
if (open === null) {
|
|
104
|
+
open = { index: i, marker };
|
|
105
|
+
openers.push(open);
|
|
106
|
+
} else if (open.marker === marker) {
|
|
107
|
+
for (let j = open.index; j <= i; j++) fenced.add(j);
|
|
108
|
+
open = null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const out: string[] = [];
|
|
113
|
+
for (let i = start; i < lines.length; i++) {
|
|
114
|
+
if (fenced.has(i)) continue;
|
|
115
|
+
const line = (lines[i] ?? '').trimEnd();
|
|
116
|
+
if (line.trim() === '') continue;
|
|
117
|
+
if (/^\s*\|/.test(line)) continue;
|
|
118
|
+
out.push(line);
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function countImperative(lines: readonly string[]): number {
|
|
124
|
+
let n = 0;
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
if (IMPERATIVE_PATTERNS.some(([, re]) => re.test(line))) n += 1;
|
|
127
|
+
}
|
|
128
|
+
return n;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _walkMarkdown(dir: string): string[] {
|
|
132
|
+
let entries: fs.Dirent[];
|
|
133
|
+
try {
|
|
134
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const out: string[] = [];
|
|
139
|
+
for (const e of entries) {
|
|
140
|
+
const full = path.join(dir, e.name);
|
|
141
|
+
if (e.isDirectory()) out.push(..._walkMarkdown(full));
|
|
142
|
+
else if (e.name.endsWith('.md')) out.push(full);
|
|
143
|
+
}
|
|
144
|
+
return out.sort();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function buildReport(root: string): DensityReport {
|
|
148
|
+
const files: FileDensity[] = [];
|
|
149
|
+
let totalProse = 0;
|
|
150
|
+
let totalImperative = 0;
|
|
151
|
+
for (const dir of [RULES_ROOT, SKILLS_ROOT]) {
|
|
152
|
+
for (const abs of _walkMarkdown(path.join(root, dir))) {
|
|
153
|
+
const lines = proseLines(fs.readFileSync(abs, 'utf-8'));
|
|
154
|
+
if (lines.length === 0) continue;
|
|
155
|
+
const imperative = countImperative(lines);
|
|
156
|
+
totalProse += lines.length;
|
|
157
|
+
totalImperative += imperative;
|
|
158
|
+
files.push({
|
|
159
|
+
file: path.relative(root, abs).split(path.sep).join('/'),
|
|
160
|
+
prose: lines.length,
|
|
161
|
+
imperative,
|
|
162
|
+
ratio: imperative / lines.length,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
files,
|
|
168
|
+
totalProse,
|
|
169
|
+
totalImperative,
|
|
170
|
+
ratio: totalProse === 0 ? null : totalImperative / totalProse,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const HEADER = [
|
|
175
|
+
'imperative-density report — ADVISORY, and the direction of this number is UNSETTLED.',
|
|
176
|
+
'',
|
|
177
|
+
'Three sources disagree on whether higher is better. One prefers reasoning-based',
|
|
178
|
+
'phrasing; one scores instruction files higher for imperative density; the third',
|
|
179
|
+
'declines the question and supplies the operational fork instead. This suite has',
|
|
180
|
+
'no measurement either way — see skill-ecosystem-sweep-2026-08.md § R2.',
|
|
181
|
+
'',
|
|
182
|
+
'Do not read this as a score, and do not optimise it. It gates nothing on purpose:',
|
|
183
|
+
'a threshold here would prejudge the open question, and a number that gets',
|
|
184
|
+
'optimised stops measuring what it measured.',
|
|
185
|
+
].join('\n');
|
|
186
|
+
|
|
187
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
188
|
+
const top = argv.includes('--top') ? Number.parseInt(argv[argv.indexOf('--top') + 1] ?? '10', 10) : 10;
|
|
189
|
+
const json = argv.includes('--json');
|
|
190
|
+
|
|
191
|
+
const report = buildReport(REPO_ROOT);
|
|
192
|
+
if (json) {
|
|
193
|
+
process.stdout.write(`${JSON.stringify({ note: 'advisory; direction unsettled (§ R2)', ...report }, null, 2)}\n`);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
process.stdout.write(`${HEADER}\n\n`);
|
|
198
|
+
if (report.ratio === null) {
|
|
199
|
+
// `renderShare`'s rule, applied by hand: an empty denominator renders as
|
|
200
|
+
// "not measured", never as 0.0 %.
|
|
201
|
+
process.stdout.write('corpus ratio: not measured — zero prose lines found under the scanned roots\n');
|
|
202
|
+
process.stdout.write(`files: ${String(report.files.length)}\n`);
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
process.stdout.write(
|
|
206
|
+
`corpus ratio: ${(report.ratio * 100).toFixed(1)} % ` +
|
|
207
|
+
`(${String(report.totalImperative)} imperative lines of ${String(report.totalProse)} prose lines, ` +
|
|
208
|
+
`${String(report.files.length)} files)\n\n`,
|
|
209
|
+
);
|
|
210
|
+
const ranked = [...report.files].sort((a, b) => b.ratio - a.ratio).slice(0, top);
|
|
211
|
+
process.stdout.write(`densest ${String(ranked.length)}:\n`);
|
|
212
|
+
for (const f of ranked) {
|
|
213
|
+
process.stdout.write(
|
|
214
|
+
` ${(f.ratio * 100).toFixed(1).padStart(5)} % ${String(f.imperative).padStart(4)}/${String(f.prose).padEnd(5)} ${f.file}\n`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
// `files:` and not `scanned:` on purpose. The latter is the machine-read
|
|
218
|
+
// coverage signal `check_gate_coverage` parses, and emitting it would
|
|
219
|
+
// enlist an advisory report as a gate with a coverage floor — the exact
|
|
220
|
+
// promotion this report must not undergo.
|
|
221
|
+
process.stdout.write(`\nfiles: ${String(report.files.length)}\n`);
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
226
|
+
process.exit(main());
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export { REPO_ROOT };
|
|
@@ -179,7 +179,16 @@ export function _diff_tree(
|
|
|
179
179
|
|
|
180
180
|
export function _diff_manifest(before: Obj | null, after: Obj | null): string[] {
|
|
181
181
|
if (before === null && after === null) {
|
|
182
|
-
|
|
182
|
+
// Equality without validity — the literal `diff a b` where NEITHER side
|
|
183
|
+
// exists and the answer comes back "equal". An empty issue list means
|
|
184
|
+
// clean to every caller, so both-absent used to certify a move that was
|
|
185
|
+
// never compared. Two absent sides are the absence of a comparison, not
|
|
186
|
+
// the presence of a match. (Audited estate-wide in
|
|
187
|
+
// `docs/guidelines/agent-infra/false-green.md § The audited sync/parity gates`.)
|
|
188
|
+
return [
|
|
189
|
+
' manifest: BOTH the pre-move snapshot and the post-move manifest are missing — ' +
|
|
190
|
+
'nothing was compared, so nothing is verified',
|
|
191
|
+
];
|
|
183
192
|
}
|
|
184
193
|
if (before === null) {
|
|
185
194
|
return [' manifest: pre-move snapshot missing'];
|