@event4u/agent-config 9.11.0 → 9.13.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 +80 -204
- package/README.md +8 -1
- package/dist/agent-src/commands/fix/ci.md +29 -0
- package/dist/agent-src/commands/pr/create.md +60 -0
- package/dist/agent-src/contexts/authority/kernel-rule-edits.md +18 -2
- package/dist/agent-src/rules/active-remediation.md +0 -2
- package/dist/agent-src/rules/architecture.md +0 -1
- package/dist/agent-src/rules/artifact-drafting-protocol.md +0 -4
- package/dist/agent-src/rules/autonomous-execution.md +1 -2
- package/dist/agent-src/rules/broken-access-control.md +0 -2
- package/dist/agent-src/rules/code-comment-discipline.md +0 -3
- package/dist/agent-src/rules/code-provenance.md +0 -3
- package/dist/agent-src/rules/communication-through-line.md +2 -3
- package/dist/agent-src/rules/content-quoting-floor.md +0 -2
- package/dist/agent-src/rules/context-hygiene.md +1 -3
- package/dist/agent-src/rules/decision-revisit-gate.md +0 -2
- package/dist/agent-src/rules/design-fidelity.md +30 -1
- package/dist/agent-src/rules/domain-adoption-policy.md +0 -3
- package/dist/agent-src/rules/downstream-changes.md +10 -1
- package/dist/agent-src/rules/external-reference-deep-dive.md +0 -7
- package/dist/agent-src/rules/fast-path-marker-visibility.md +0 -1
- package/dist/agent-src/rules/git-history-discipline.md +0 -7
- package/dist/agent-src/rules/guidelines.md +0 -2
- package/dist/agent-src/rules/improve-before-implement.md +4 -2
- package/dist/agent-src/rules/invite-challenge.md +0 -4
- package/dist/agent-src/rules/linked-projects-onboarding-gate.md +0 -2
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +0 -1
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -2
- package/dist/agent-src/rules/missing-tool-handling.md +0 -1
- package/dist/agent-src/rules/no-attribution-footers.md +0 -3
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +8 -5
- package/dist/agent-src/rules/no-pr-progress-comments.md +1 -3
- package/dist/agent-src/rules/no-roadmap-references.md +0 -2
- package/dist/agent-src/rules/output-discipline.md +0 -1
- package/dist/agent-src/rules/prefer-enums-over-literals.md +0 -2
- package/dist/agent-src/rules/preservation-guard.md +0 -3
- package/dist/agent-src/rules/role-mode-adherence.md +0 -1
- package/dist/agent-src/rules/secret-vcs-guard.md +0 -2
- package/dist/agent-src/rules/senior-engineering-discipline.md +0 -2
- package/dist/agent-src/rules/session-canary.md +0 -2
- package/dist/agent-src/rules/size-enforcement.md +0 -5
- package/dist/agent-src/rules/source-confidentiality.md +0 -3
- package/dist/agent-src/rules/source-discovery-gate.md +0 -2
- package/dist/agent-src/rules/think-before-action.md +3 -3
- package/dist/agent-src/rules/token-budget-discipline.md +0 -1
- package/dist/agent-src/rules/token-efficiency.md +1 -2
- package/dist/agent-src/rules/tool-safety.md +0 -1
- package/dist/agent-src/rules/user-interaction.md +0 -3
- package/dist/agent-src/rules/user-interrupt-priority.md +0 -1
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +8 -0
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +1 -0
- package/dist/agent-src/skills/analysis-skill-router/SKILL.md +55 -86
- package/dist/agent-src/skills/character-consistency/SKILL.md +7 -4
- package/dist/agent-src/skills/decision-record/SKILL.md +8 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +27 -1
- package/dist/agent-src/skills/design-intelligence/data/font-pairings-reference.csv +74 -74
- package/dist/agent-src/skills/design-intelligence/data/manifest.json +2 -0
- package/dist/agent-src/skills/design-review/SKILL.md +11 -0
- package/dist/agent-src/skills/design-system-capture/reference/design-system-json.md +8 -0
- package/dist/agent-src/skills/design-tokens/scripts/tokens.ts +0 -7
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +34 -3
- package/dist/agent-src/skills/fe-design/SKILL.md +10 -0
- package/dist/agent-src/skills/motion-choreographer/SKILL.md +7 -4
- package/dist/agent-src/skills/overbuild-review-lens/SKILL.md +201 -0
- package/dist/agent-src/skills/performance/SKILL.md +14 -0
- package/dist/agent-src/skills/pixar-storyteller/SKILL.md +8 -5
- package/dist/agent-src/skills/scene-expander/SKILL.md +9 -6
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +10 -0
- package/dist/agent-src/skills/typography-system/SKILL.md +55 -11
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +180 -0
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +14 -37
- package/dist/agent-src/skills/video-director/SKILL.md +9 -6
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +162 -17
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +49 -6
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/review.ts +9 -5
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +182 -29
- package/dist/agent-src/templates/scripts/work_engine/stack/detect.ts +311 -24
- package/dist/agent-src/templates/scripts/work_engine/state.ts +55 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
- package/dist/cli-delegate/{chunk-IR7P7JWA.js → chunk-A3HJ6NTI.js} +271 -59
- package/dist/cli-delegate/chunk-EJYIGZ3T.js +125 -0
- package/dist/cli-delegate/{chunk-RDGRHAAB.js → chunk-HYWSRNUX.js} +6 -86
- package/dist/cli-delegate/{chunk-YMFO6FDN.js → chunk-QFKQLQ7X.js} +313 -81
- package/dist/cli-delegate/{chunk-4SV3XGFA.js → chunk-TEGJU2JX.js} +7 -9
- package/dist/cli-delegate/chunk-TJIAM5FW.js +133 -0
- package/dist/cli-delegate/cmd_conformance.js +4 -7
- package/dist/cli-delegate/cmd_doctor.js +3 -5
- package/dist/cli-delegate/cmd_explain.js +153 -85
- package/dist/cli-delegate/cmd_export.js +5 -3
- package/dist/cli-delegate/cmd_fleet.js +2 -1
- package/dist/cli-delegate/cmd_preflight.js +6 -123
- package/dist/cli-delegate/cmd_prune.js +5 -3
- package/dist/cli-delegate/cmd_settings_migrate.js +5 -3
- package/dist/cli-delegate/cmd_sync.js +5 -3
- package/dist/cli-delegate/cmd_uninstall.js +6 -5
- package/dist/cli-delegate/cmd_validate.js +5 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +116 -72
- 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 +7 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +174 -13
- package/dist/install/install.mjs +605 -412
- package/dist/install/ruleInScope.js +123 -0
- package/dist/install/ruleInScope.js.map +1 -0
- package/dist/install/rule_scope.js +2 -2
- package/dist/install/rule_scope.js.map +1 -1
- package/dist/install/wizard-plan.js +10 -1
- package/dist/install/wizard-plan.js.map +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +710 -2491
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +8 -24
- package/dist/scripts/condense.js.map +1 -1
- package/docs/MIGRATION.md +1 -1
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +71 -0
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +24 -1
- package/docs/guidelines/code-clarity.md +9 -2
- package/docs/guidelines/design-fidelity-mechanics.md +58 -0
- package/llms.txt +63 -27
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +162 -17
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +49 -6
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +9 -5
- package/src/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +182 -29
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +311 -24
- package/src/agent-src/templates/scripts/work_engine/state.ts +55 -0
- package/src/config/agent-settings.template.yml +8 -0
- package/src/install/ruleInScope.ts +133 -0
- package/src/install/rule_scope.ts +2 -2
- package/src/install/wizard-plan.ts +11 -1
- package/src/scripts/_lib/counted_probe.ts +142 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +38 -2
- package/src/scripts/_lib/overbuild_lens_contract.ts +196 -0
- package/src/scripts/_lib/release_scope.ts +207 -0
- package/src/scripts/ai_council/stance_tally.ts +37 -2
- package/src/scripts/audit_auto_rules.ts +9 -9
- package/src/scripts/audit_likelihood.ts +3 -3
- package/src/scripts/audit_overlap.ts +2 -2
- package/src/scripts/audit_skill_overlap.ts +233 -95
- package/src/scripts/audit_skill_overlap_allowlist.json +4 -0
- package/src/scripts/build_proof.ts +15 -1
- package/src/scripts/check_dependency_floors.ts +16 -6
- package/src/scripts/check_gate_coverage.ts +6 -6
- package/src/scripts/check_installer_import_purity.ts +295 -0
- package/src/scripts/check_kernel_rule_bundle.ts +5 -4
- package/src/scripts/check_token_quality_golden.ts +0 -18
- package/src/scripts/code_graph/loader.ts +35 -6
- package/src/scripts/compile_router.ts +3 -8
- package/src/scripts/condense.ts +9 -30
- package/src/scripts/explain_run.ts +0 -1
- package/src/scripts/generate_pack_manifests.ts +3 -20
- package/src/scripts/hooks/block_kernel_rule_writes.ts +145 -10
- package/src/scripts/hooks/block_no_verify.ts +101 -0
- package/src/scripts/install-hooks.sh +36 -4
- package/src/scripts/install.ts +210 -5
- package/src/scripts/lint_agent_security.ts +5 -4
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_regression.ts +92 -1
- package/src/scripts/lint_skill_descriptions.ts +93 -5
- package/src/scripts/lint_ui_stack_bundles.ts +31 -0
- package/src/scripts/project_thin_rules.ts +2 -2
- package/src/scripts/release.ts +107 -3
- package/src/scripts/resolve_lint_scope.ts +53 -0
- package/src/scripts/router_telemetry.ts +0 -5
- package/src/scripts/runtime_dispatcher.ts +40 -0
- package/src/scripts/schemas/pack.schema.json +1 -5
- package/src/scripts/schemas/rule.schema.json +1 -2
- package/src/scripts/self_review_gate.ts +15 -73
- package/src/scripts/skill_linter.ts +93 -11
- package/src/scripts/sweep_dead_scan_roots.ts +550 -0
- package/src/scripts/trigger_coverage.ts +11 -37
- package/dist/cli-delegate/chunk-7DSAMZFU.js +0 -356
- package/dist/cli-delegate/chunk-BJGXZNPM.js +0 -266
- package/dist/cli-delegate/chunk-TFG57JJM.js +0 -1969
|
@@ -51,7 +51,14 @@ export interface StanceTallyResult {
|
|
|
51
51
|
consensus: OptionTally | null; // the option clearing the threshold, or null
|
|
52
52
|
split: boolean; // true when no option clears the threshold
|
|
53
53
|
abstain_count: number;
|
|
54
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Members whose stance line was missing or unparseable. They COUNT toward
|
|
56
|
+
* `w_total` (they responded) and back no option — see the
|
|
57
|
+
* refusal-preservation invariant in `tally_stances`. Surfaced by
|
|
58
|
+
* `render_vote_tally` so a shrunken-signal quorum is visible rather than
|
|
59
|
+
* merely computed.
|
|
60
|
+
*/
|
|
61
|
+
needs_repair: string[];
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
// Tolerant of whitespace and case; requires the three explicit fields in order.
|
|
@@ -150,7 +157,23 @@ export function tally_stances(
|
|
|
150
157
|
parsed.push({ member, ...p });
|
|
151
158
|
}
|
|
152
159
|
|
|
153
|
-
|
|
160
|
+
// ── Refusal-preservation invariant (road-to-governance-invariants P2) ──
|
|
161
|
+
//
|
|
162
|
+
// Selection may never rank an option higher *because* a member refused
|
|
163
|
+
// less. A safety refusal is not a scored-down property, and — measured by
|
|
164
|
+
// spike S0.1 — the way this engine could be steered was not a weight but a
|
|
165
|
+
// DENOMINATOR: `w_total` once counted only members whose stance line
|
|
166
|
+
// parsed, so a refusal phrased as prose ("I will not answer this") vanished
|
|
167
|
+
// from the quorum and made consensus EASIER. Same two backers, margin
|
|
168
|
+
// −0.25 and no consensus when the refusal parsed as an abstention;
|
|
169
|
+
// +0.4167 and consensus when it did not parse. Δ 0.6667, outcome flipped.
|
|
170
|
+
//
|
|
171
|
+
// The fix is structural rather than a new gate: a member who RESPONDED
|
|
172
|
+
// counts toward the quorum whether or not its stance line parsed. It backs
|
|
173
|
+
// no option either way, so an unparseable response now behaves exactly like
|
|
174
|
+
// an abstention — the fail-safe direction. Members who never responded are
|
|
175
|
+
// dropped upstream (`!r.error`) and are correctly absent from `members`.
|
|
176
|
+
const w_total = parsed.length + needs_repair.length; // responded, abstain + unparseable included
|
|
154
177
|
const threshold = CONSENSUS_FRACTION * w_total;
|
|
155
178
|
let abstain_count = 0;
|
|
156
179
|
|
|
@@ -208,6 +231,18 @@ export function render_vote_tally(result: StanceTallyResult): string {
|
|
|
208
231
|
if (result.abstain_count > 0) {
|
|
209
232
|
lines.push(`- abstain — ${result.abstain_count} member(s) (raises the bar)`);
|
|
210
233
|
}
|
|
234
|
+
// Refusal divergence, as an OBSERVATION and never a selection input
|
|
235
|
+
// (road-to-governance-invariants P2). A member whose stance did not parse
|
|
236
|
+
// still counts toward the quorum; saying so here is what makes a shrunken
|
|
237
|
+
// signal visible to a reader instead of merely present in a struct field
|
|
238
|
+
// no caller reads. Nothing in the tally consumes this string.
|
|
239
|
+
if (result.needs_repair.length > 0) {
|
|
240
|
+
lines.push(
|
|
241
|
+
`- unparsed — ${result.needs_repair.length} member(s) responded without a ` +
|
|
242
|
+
`readable stance (${result.needs_repair.join(', ')}); counted in the ` +
|
|
243
|
+
'quorum, backing nothing',
|
|
244
|
+
);
|
|
245
|
+
}
|
|
211
246
|
lines.push(`Threshold: ⅔ × ${result.w_total} = ${result.threshold.toFixed(2)}`);
|
|
212
247
|
lines.push(
|
|
213
248
|
result.consensus !== null
|
|
@@ -136,14 +136,14 @@ export function _split_frontmatter(text: string): [Record<string, Json>, string]
|
|
|
136
136
|
interface TriggerSummary {
|
|
137
137
|
path_prefixes: string[];
|
|
138
138
|
keywords: string[];
|
|
139
|
-
|
|
139
|
+
phrases: string[];
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/** Mirror `_trigger_summary`. */
|
|
143
143
|
export function _trigger_summary(triggers: Json): TriggerSummary {
|
|
144
144
|
const paths: string[] = [];
|
|
145
145
|
const keywords: string[] = [];
|
|
146
|
-
const
|
|
146
|
+
const phrases: string[] = [];
|
|
147
147
|
const list: Json[] = Array.isArray(triggers) ? triggers : [];
|
|
148
148
|
for (const entry of list) {
|
|
149
149
|
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
@@ -156,11 +156,11 @@ export function _trigger_summary(triggers: Json): TriggerSummary {
|
|
|
156
156
|
if ('keyword' in e) {
|
|
157
157
|
keywords.push(String(e['keyword']));
|
|
158
158
|
}
|
|
159
|
-
if ('
|
|
160
|
-
|
|
159
|
+
if ('phrase' in e) {
|
|
160
|
+
phrases.push(String(e['phrase']));
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
-
return { path_prefixes: paths, keywords,
|
|
163
|
+
return { path_prefixes: paths, keywords, phrases };
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
interface RuleEntry {
|
|
@@ -208,7 +208,7 @@ export function collect(): RuleEntry[] {
|
|
|
208
208
|
description_chars: pyLen(desc),
|
|
209
209
|
triggers,
|
|
210
210
|
trigger_count:
|
|
211
|
-
triggers.path_prefixes.length + triggers.keywords.length + triggers.
|
|
211
|
+
triggers.path_prefixes.length + triggers.keywords.length + triggers.phrases.length,
|
|
212
212
|
routes_to,
|
|
213
213
|
body_chars: pyLen(body),
|
|
214
214
|
file_chars: pyLen(text),
|
|
@@ -292,7 +292,7 @@ export function render_markdown(rules: RuleEntry[]): string {
|
|
|
292
292
|
const triggers =
|
|
293
293
|
`${r.triggers.path_prefixes.length}p / ` +
|
|
294
294
|
`${r.triggers.keywords.length}k / ` +
|
|
295
|
-
`${r.triggers.
|
|
295
|
+
`${r.triggers.phrases.length}ph`;
|
|
296
296
|
const routes = r.routes_to.map((x) => String(x)).join(', ') || '—';
|
|
297
297
|
lines.push(
|
|
298
298
|
`| ${i} | \`${r.name}\` | ${_pyTruthy(r.tier) ? String(r.tier) : '—'} | ` +
|
|
@@ -302,7 +302,7 @@ export function render_markdown(rules: RuleEntry[]): string {
|
|
|
302
302
|
i++;
|
|
303
303
|
}
|
|
304
304
|
lines.push('');
|
|
305
|
-
lines.push('Trigger key: `Np` = path-prefix, `Nk` = keyword, `
|
|
305
|
+
lines.push('Trigger key: `Np` = path-prefix, `Nk` = keyword, `Nph` = phrase.');
|
|
306
306
|
lines.push('');
|
|
307
307
|
return lines.join('\n');
|
|
308
308
|
}
|
|
@@ -393,7 +393,7 @@ function _ruleToJson(r: RuleEntry): Record<string, Json> {
|
|
|
393
393
|
triggers: {
|
|
394
394
|
path_prefixes: r.triggers.path_prefixes,
|
|
395
395
|
keywords: r.triggers.keywords,
|
|
396
|
-
|
|
396
|
+
phrases: r.triggers.phrases,
|
|
397
397
|
},
|
|
398
398
|
trigger_count: r.trigger_count,
|
|
399
399
|
routes_to: r.routes_to,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* For every auto-rule from `agents/reports/auto-rules-audit.json`:
|
|
17
17
|
*
|
|
18
18
|
* 1. Build a token set from `description`, `triggers[].keyword`,
|
|
19
|
-
* `triggers[].
|
|
19
|
+
* `triggers[].phrase`, and the rule name itself.
|
|
20
20
|
* 2. Index a corpus of skills (`SKILL.md`), contexts, guidelines, and
|
|
21
21
|
* command files.
|
|
22
22
|
* 3. Score `corpus_hits = sum(1 for token in tokens if token in corpus)`.
|
|
@@ -197,9 +197,9 @@ function _ruleTokens(rule: Json): Set<string> {
|
|
|
197
197
|
add(tokens(String(rule['name'] ?? '').replaceAll('-', ' ')));
|
|
198
198
|
const trig = rule['triggers'] ?? {};
|
|
199
199
|
const kw: Json[] = Array.isArray(trig['keywords']) ? trig['keywords'] : [];
|
|
200
|
-
const
|
|
200
|
+
const phrases: Json[] = Array.isArray(trig['phrases']) ? trig['phrases'] : [];
|
|
201
201
|
add(tokens(kw.map((x) => String(x)).join(' ')));
|
|
202
|
-
add(tokens(
|
|
202
|
+
add(tokens(phrases.map((x) => String(x)).join(' ')));
|
|
203
203
|
return out;
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -54,7 +54,7 @@ const _ALL_DIGITS = /^[0-9]+$/;
|
|
|
54
54
|
interface RuleTriggers {
|
|
55
55
|
path_prefixes: string[];
|
|
56
56
|
keywords: string[];
|
|
57
|
-
|
|
57
|
+
phrases: string[];
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
interface RuleEntry {
|
|
@@ -194,7 +194,7 @@ function analyse(rules: RuleEntry[]): Pair[] {
|
|
|
194
194
|
for (const x of keyword_set(r.triggers.keywords.join(' '))) {
|
|
195
195
|
kw.add(x);
|
|
196
196
|
}
|
|
197
|
-
for (const x of keyword_set(r.triggers.
|
|
197
|
+
for (const x of keyword_set(r.triggers.phrases.join(' '))) {
|
|
198
198
|
kw.add(x);
|
|
199
199
|
}
|
|
200
200
|
keywords.push(kw);
|
|
@@ -3,35 +3,51 @@
|
|
|
3
3
|
* Skill-family content-overlap analysis (6.0.0-C Phase 4 Step 8).
|
|
4
4
|
*
|
|
5
5
|
* Ported from the retired Python `src/scripts/audit_skill_overlap.py` (ADR-200 —
|
|
6
|
-
* Python→TS migration, Phase 8 / Wave 8c).
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Python→TS migration, Phase 8 / Wave 8c). The numeric contract — keyword
|
|
7
|
+
* vector, cosine, banker's rounding, pair ordering, `json.dumps(..., indent=2)`
|
|
8
|
+
* byte shape, and the Markdown renderer — is preserved so historical reports
|
|
9
|
+
* stay comparable. The CLI grew a `--root` flag; the exit codes are unchanged
|
|
10
|
+
* (0 ok / 3 dead scan scope).
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* `_skill_roots()`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* SCAN ROOT (repaired 2026-08-02, road-to-overlap-truth-and-skill-cut Phase 1):
|
|
13
|
+
* the ported `_skill_roots()` resolved the pre-ADR-051 source containers, both
|
|
14
|
+
* of which were deleted when `src/skills` became the source of truth. The tool
|
|
15
|
+
* therefore walked a directory that does not exist and could never report an
|
|
16
|
+
* overlap for the 287-skill corpus. That resolution is GONE, not kept as a
|
|
17
|
+
* fallback branch: the default root is the shared `SRC_SKILLS()` resolver, and
|
|
18
|
+
* an empty scan is a hard failure via {@link assertScanned} rather than an
|
|
19
|
+
* empty result.
|
|
16
20
|
*
|
|
17
21
|
* Walks every `SKILL.md`, builds a keyword vector from the body
|
|
18
22
|
* (frontmatter stripped), and flags pairs whose content cosine-similarity
|
|
19
23
|
* is >= OVERLAP_THRESHOLD. Same-domain (shared `packs:`) >= threshold pairs
|
|
20
24
|
* are the merge candidates. THIS SCRIPT MERGES NOTHING.
|
|
21
25
|
*
|
|
22
|
-
*
|
|
26
|
+
* `--strict` makes a same-pack pair at or above threshold a BUILD FAILURE
|
|
27
|
+
* (exit 1) unless the pair carries a reviewed justification in
|
|
28
|
+
* `audit_skill_overlap_allowlist.json`, capped at 20 entries. The cap is the
|
|
29
|
+
* point: it forces periodic re-litigation instead of an allowlist that grows
|
|
30
|
+
* quietly until the gate means nothing. Cross-pack pairs never block — a
|
|
31
|
+
* cross-pack merge changes install shape and is a different decision.
|
|
23
32
|
*/
|
|
24
33
|
import * as fs from 'node:fs';
|
|
25
34
|
import * as path from 'node:path';
|
|
26
35
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
36
|
import { parse as parseYaml } from 'yaml';
|
|
28
37
|
|
|
38
|
+
import { SRC_SKILLS } from './_lib/agent_src.js';
|
|
39
|
+
import { DeadScopeError, assertScanned } from './_lib/scan_scope.js';
|
|
40
|
+
|
|
29
41
|
const _HERE = fileURLToPath(import.meta.url);
|
|
30
42
|
// src/scripts/audit_skill_overlap.py → parent.parent.parent == repo root.
|
|
31
43
|
export const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
32
44
|
export const REPORT_DIR = path.join(ROOT, 'agents', 'reports');
|
|
33
|
-
export const
|
|
34
|
-
export const
|
|
45
|
+
export const OUT_JSON_NAME = 'skill-overlap.json';
|
|
46
|
+
export const OUT_MD_NAME = 'skill-overlap.md';
|
|
47
|
+
export const OUT_JSON = path.join(REPORT_DIR, OUT_JSON_NAME);
|
|
48
|
+
export const OUT_MD = path.join(REPORT_DIR, OUT_MD_NAME);
|
|
49
|
+
export const ALLOWLIST = path.join(ROOT, 'src', 'scripts', 'audit_skill_overlap_allowlist.json');
|
|
50
|
+
export const ALLOWLIST_CAP = 20;
|
|
35
51
|
// re.compile(r"^---\n(.*?)\n---", re.DOTALL) — used with .search (anywhere).
|
|
36
52
|
const FM_RE = /^---\n([\s\S]*?)\n---/m;
|
|
37
53
|
export const OVERLAP_THRESHOLD = 0.7;
|
|
@@ -70,49 +86,16 @@ function _isFile(p: string): boolean {
|
|
|
70
86
|
return false;
|
|
71
87
|
}
|
|
72
88
|
}
|
|
73
|
-
|
|
74
|
-
/** Sorted immediate child entries (mirrors `sorted(p.iterdir())` / `p.glob`). */
|
|
75
|
-
function _iterdirSorted(p: string): string[] {
|
|
76
|
-
let names: string[];
|
|
77
|
-
try {
|
|
78
|
-
names = fs.readdirSync(p);
|
|
79
|
-
} catch {
|
|
80
|
-
return [];
|
|
81
|
-
}
|
|
82
|
-
names.sort();
|
|
83
|
-
return names.map((n) => path.join(p, n));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
89
|
/**
|
|
87
|
-
*
|
|
88
|
-
* roots = [d for d in (ROOT/"packages").glob("*\/.agent-src.uncondensed/skills")
|
|
89
|
-
* if d.is_dir()] if (ROOT/"packages").is_dir() else []
|
|
90
|
-
* legacy = ROOT/".agent-src.uncondensed"/"skills"
|
|
91
|
-
* if not roots and legacy.is_dir(): roots = [legacy]
|
|
90
|
+
* The one scan root: `src/skills`, via the shared ADR-051 resolver.
|
|
92
91
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* children; the resulting root order only affects first-win dedup, and no
|
|
98
|
-
* skill dir name collides across packages in practice.)
|
|
92
|
+
* There is deliberately no fallback. A second, silently-tried root is how the
|
|
93
|
+
* previous resolution stayed green while reading nothing — the caller passes an
|
|
94
|
+
* explicit `--root` when it wants a different tree (fixtures, tests), and any
|
|
95
|
+
* root that yields zero skills fails loudly in {@link main}.
|
|
99
96
|
*/
|
|
100
|
-
export function
|
|
101
|
-
|
|
102
|
-
let roots: string[] = [];
|
|
103
|
-
if (_isDir(pkgs)) {
|
|
104
|
-
for (const child of _iterdirSorted(pkgs)) {
|
|
105
|
-
const cand = path.join(child, '.agent-src.uncondensed', 'skills');
|
|
106
|
-
if (_isDir(cand)) {
|
|
107
|
-
roots.push(cand);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
const legacy = path.join(ROOT, '.agent-src.uncondensed', 'skills');
|
|
112
|
-
if (roots.length === 0 && _isDir(legacy)) {
|
|
113
|
-
roots = [legacy];
|
|
114
|
-
}
|
|
115
|
-
return roots;
|
|
97
|
+
export function _default_skill_root(): string {
|
|
98
|
+
return SRC_SKILLS();
|
|
116
99
|
}
|
|
117
100
|
|
|
118
101
|
/**
|
|
@@ -162,8 +145,19 @@ export function _cosine(a: Map<string, number>, b: Map<string, number>): number
|
|
|
162
145
|
return da && db ? num / (da * db) : 0.0;
|
|
163
146
|
}
|
|
164
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Which text the similarity vector is built from.
|
|
150
|
+
*
|
|
151
|
+
* `body` is the CANONICAL metric — the one every historical report, the 0.70
|
|
152
|
+
* threshold, and the merge decisions are calibrated against. `description` is
|
|
153
|
+
* an explicitly non-canonical second view: routing happens on descriptions, and
|
|
154
|
+
* body similarity does not measure it, so the two answer different questions
|
|
155
|
+
* and their scores are never mixed or compared against the same threshold.
|
|
156
|
+
*/
|
|
157
|
+
export type Field = 'body' | 'description';
|
|
158
|
+
|
|
165
159
|
/** Mirror `_parse`: yaml.safe_load frontmatter, body after the FM match. */
|
|
166
|
-
export function _parse(md: string): Skill {
|
|
160
|
+
export function _parse(md: string, field: Field = 'body'): Skill {
|
|
167
161
|
const text = fs.readFileSync(md, 'utf-8');
|
|
168
162
|
let fm: Json = {};
|
|
169
163
|
let body = text;
|
|
@@ -179,11 +173,13 @@ export function _parse(md: string): Skill {
|
|
|
179
173
|
const isObj = fm !== null && typeof fm === 'object' && !Array.isArray(fm);
|
|
180
174
|
const name = isObj ? (fm as Record<string, Json>)['name'] : null;
|
|
181
175
|
const packsVal = isObj ? (fm as Record<string, Json>)['packs'] : null;
|
|
176
|
+
const descVal = isObj ? (fm as Record<string, Json>)['description'] : null;
|
|
177
|
+
const source = field === 'description' ? (descVal ? String(descVal) : '') : body;
|
|
182
178
|
return {
|
|
183
179
|
name: name ? String(name) : path.basename(path.dirname(md)),
|
|
184
180
|
relpath: _relPosix(md, ROOT),
|
|
185
181
|
packs: Array.isArray(packsVal) ? new Set(packsVal.map((x) => String(x))) : new Set(),
|
|
186
|
-
vector: _keyword_vector(
|
|
182
|
+
vector: _keyword_vector(source),
|
|
187
183
|
};
|
|
188
184
|
}
|
|
189
185
|
|
|
@@ -247,22 +243,20 @@ function _pathlibCompare(a: string, b: string): number {
|
|
|
247
243
|
return pa.length - pb.length;
|
|
248
244
|
}
|
|
249
245
|
|
|
250
|
-
export function collect(): Skill[] {
|
|
246
|
+
export function collect(root: string = _default_skill_root(), field: Field = 'body'): Skill[] {
|
|
251
247
|
const skills: Skill[] = [];
|
|
252
248
|
const seen = new Set<string>();
|
|
253
|
-
for (const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
const key = path.basename(path.dirname(md));
|
|
260
|
-
if (seen.has(key)) {
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
seen.add(key);
|
|
264
|
-
skills.push(_parse(md));
|
|
249
|
+
for (const md of _rglobSkillMd(root)) {
|
|
250
|
+
// if "_archive" in md.parts: continue
|
|
251
|
+
if (md.split(path.sep).includes('_archive')) {
|
|
252
|
+
continue;
|
|
265
253
|
}
|
|
254
|
+
const key = path.basename(path.dirname(md));
|
|
255
|
+
if (seen.has(key)) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
seen.add(key);
|
|
259
|
+
skills.push(_parse(md, field));
|
|
266
260
|
}
|
|
267
261
|
return skills;
|
|
268
262
|
}
|
|
@@ -354,17 +348,30 @@ function _pct(x: number): string {
|
|
|
354
348
|
return `${pct}%`;
|
|
355
349
|
}
|
|
356
350
|
|
|
357
|
-
export function render_md(
|
|
351
|
+
export function render_md(
|
|
352
|
+
skills: Skill[],
|
|
353
|
+
pairs: Pair[],
|
|
354
|
+
threshold: number,
|
|
355
|
+
field: Field = 'body',
|
|
356
|
+
): string {
|
|
358
357
|
const merge = pairs.filter((p) => p.same_domain);
|
|
359
358
|
const cross = pairs.filter((p) => !p.same_domain);
|
|
360
359
|
const L: string[] = [
|
|
361
360
|
'# Skill-family overlap report (6.0.0-C Phase 4 Step 8)\n',
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
361
|
+
field === 'description'
|
|
362
|
+
? `> **NON-CANONICAL measurement.** Cosine over the \`description:\` ` +
|
|
363
|
+
`frontmatter only, across ${skills.length} skills, at ≥ ${_pct(threshold)}. ` +
|
|
364
|
+
`Routing happens on descriptions, so this answers a question the ` +
|
|
365
|
+
`canonical body metric does not — but its scores are NOT comparable ` +
|
|
366
|
+
`to the canonical 0.70 merge threshold and must never be mixed into ` +
|
|
367
|
+
`a merge decision. The canonical run is the one without ` +
|
|
368
|
+
`\`--descriptions\`.\n`
|
|
369
|
+
: `> Content cosine-similarity over ${skills.length} skills; pairs at ` +
|
|
370
|
+
`≥ ${_pct(threshold)}. **Same-domain pairs (shared \`packs:\`) are the ` +
|
|
371
|
+
`merge candidates** consumed by ` +
|
|
372
|
+
`[\`evidence-based-pruning.md\`](../../docs/contracts/evidence-based-pruning.md). ` +
|
|
373
|
+
`This report merges NOTHING — it is input to a future, human-driven ` +
|
|
374
|
+
`consolidation roadmap.\n`,
|
|
368
375
|
`\n- Skills scanned: **${skills.length}**`,
|
|
369
376
|
`\n- Overlap pairs ≥ ${_pct(threshold)}: **${pairs.length}** ` +
|
|
370
377
|
`(${merge.length} same-domain merge candidates, ${cross.length} cross-domain)\n`,
|
|
@@ -476,30 +483,126 @@ class FloatTag {
|
|
|
476
483
|
|
|
477
484
|
export function main(argv: string[] | null = null): number {
|
|
478
485
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
486
|
+
const root = args.root ?? _default_skill_root();
|
|
487
|
+
const skills = collect(root, args.field);
|
|
488
|
+
try {
|
|
489
|
+
assertScanned({
|
|
490
|
+
gate: 'audit_skill_overlap',
|
|
491
|
+
scanned: skills.length,
|
|
492
|
+
units: 'skill(s)',
|
|
493
|
+
roots: [_relPosix(root, ROOT)],
|
|
494
|
+
});
|
|
495
|
+
} catch (exc) {
|
|
496
|
+
if (!(exc instanceof DeadScopeError)) {
|
|
497
|
+
throw exc;
|
|
498
|
+
}
|
|
499
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
482
500
|
return 3;
|
|
483
501
|
}
|
|
484
502
|
const pairs = find_pairs(skills, args.threshold);
|
|
485
|
-
|
|
503
|
+
const outDir = args.outDir ?? REPORT_DIR;
|
|
504
|
+
const outJson = path.join(outDir, OUT_JSON_NAME);
|
|
505
|
+
const outMd = path.join(outDir, OUT_MD_NAME);
|
|
506
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
486
507
|
const jsonObj = {
|
|
487
508
|
threshold: new FloatTag(args.threshold),
|
|
488
509
|
skills_scanned: skills.length,
|
|
489
510
|
pairs: pairs.map(_pairToJson),
|
|
490
511
|
};
|
|
491
|
-
fs.writeFileSync(
|
|
492
|
-
fs.writeFileSync(
|
|
512
|
+
fs.writeFileSync(outJson, _dumpsWithFloats(jsonObj), 'utf-8');
|
|
513
|
+
fs.writeFileSync(outMd, render_md(skills, pairs, args.threshold, args.field), 'utf-8');
|
|
514
|
+
const merge = pairs.filter((p) => p.same_domain);
|
|
493
515
|
if (!args.quiet) {
|
|
494
|
-
const merge = pairs.filter((p) => p.same_domain).length;
|
|
495
516
|
process.stdout.write(
|
|
496
517
|
`✅ Skill overlap: ${skills.length} skills, ${pairs.length} pair(s) ` +
|
|
497
|
-
`≥ ${_pct(args.threshold)} (${merge} same-domain merge candidate(s)).\n`,
|
|
518
|
+
`≥ ${_pct(args.threshold)} (${merge.length} same-domain merge candidate(s)).\n`,
|
|
519
|
+
);
|
|
520
|
+
process.stdout.write(` JSON: ${_relPosix(outJson, ROOT)}\n`);
|
|
521
|
+
process.stdout.write(` MD: ${_relPosix(outMd, ROOT)}\n`);
|
|
522
|
+
}
|
|
523
|
+
// Deliberately NOT gated on --quiet: an advisory CI step whose only output
|
|
524
|
+
// is suppressed by the pipeline's default --quiet is a report nobody reads,
|
|
525
|
+
// which is one step away from the silent-green defect this tool just came
|
|
526
|
+
// back from. The counts line always ships when the table is requested.
|
|
527
|
+
if (args.printTable) {
|
|
528
|
+
process.stdout.write(
|
|
529
|
+
`ℹ️ skill overlap${args.strict ? '' : ' (advisory)'}: ${skills.length} skills scanned, ` +
|
|
530
|
+
`${merge.length} same-domain pair(s) ≥ ${_pct(args.threshold)}\n`,
|
|
531
|
+
);
|
|
532
|
+
for (const p of merge) {
|
|
533
|
+
process.stdout.write(
|
|
534
|
+
` ${p.similarity.toFixed(3)} ${p.a} ↔ ${p.b} [${p.shared_packs.join(', ')}]\n`,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (!args.strict) {
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
let allow: Set<string>;
|
|
543
|
+
try {
|
|
544
|
+
allow = _loadAllowlist(args.allowlist ?? ALLOWLIST);
|
|
545
|
+
} catch (exc) {
|
|
546
|
+
process.stderr.write(`❌ ${exc instanceof Error ? exc.message : String(exc)}\n`);
|
|
547
|
+
return 2;
|
|
548
|
+
}
|
|
549
|
+
const blocking = merge.filter((p) => !allow.has(_pairKey(p.a, p.b)));
|
|
550
|
+
if (blocking.length === 0) {
|
|
551
|
+
return 0;
|
|
552
|
+
}
|
|
553
|
+
process.stderr.write(
|
|
554
|
+
`❌ audit_skill_overlap: ${blocking.length} same-pack pair(s) at or above ` +
|
|
555
|
+
`${_pct(args.threshold)} with no reviewed justification:\n`,
|
|
556
|
+
);
|
|
557
|
+
for (const p of blocking) {
|
|
558
|
+
process.stderr.write(
|
|
559
|
+
` ${p.similarity.toFixed(3)} ${p.a} ↔ ${p.b} [${p.shared_packs.join(', ')}]\n`,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
process.stderr.write(
|
|
563
|
+
` Merge the pair, or add {"pair": "${_pairKey(blocking[0]!.a, blocking[0]!.b)}", "reason": "…"} ` +
|
|
564
|
+
`to ${_relPosix(args.allowlist ?? ALLOWLIST, ROOT)} (cap ${ALLOWLIST_CAP}).\n`,
|
|
565
|
+
);
|
|
566
|
+
return 1;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Order-independent key for an unordered pair. */
|
|
570
|
+
export function _pairKey(a: string, b: string): string {
|
|
571
|
+
return a < b ? `${a}::${b}` : `${b}::${a}`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Reviewed same-pack overlaps, capped. Over-cap throws rather than warning:
|
|
576
|
+
* per the autonomous-execution allowlist-growth antipattern, an allowlist that
|
|
577
|
+
* outgrows its cap means the threshold is wrong, not that the corpus needs
|
|
578
|
+
* twenty-one exceptions.
|
|
579
|
+
*/
|
|
580
|
+
export function _loadAllowlist(file: string): Set<string> {
|
|
581
|
+
if (!_isFile(file)) {
|
|
582
|
+
return new Set();
|
|
583
|
+
}
|
|
584
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf-8')) as {
|
|
585
|
+
entries?: Array<{ pair: string; reason: string }>;
|
|
586
|
+
};
|
|
587
|
+
const entries = Array.isArray(data.entries) ? data.entries : [];
|
|
588
|
+
if (entries.length > ALLOWLIST_CAP) {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`audit_skill_overlap: allowlist has ${entries.length} entries (> ${ALLOWLIST_CAP}). ` +
|
|
591
|
+
'Per the autonomous-execution allowlist-growth antipattern, the threshold is wrong, ' +
|
|
592
|
+
'not the corpus — re-litigate the bar, do not grow the list.',
|
|
498
593
|
);
|
|
499
|
-
process.stdout.write(` JSON: ${_relPosix(OUT_JSON, ROOT)}\n`);
|
|
500
|
-
process.stdout.write(` MD: ${_relPosix(OUT_MD, ROOT)}\n`);
|
|
501
594
|
}
|
|
502
|
-
|
|
595
|
+
const out = new Set<string>();
|
|
596
|
+
for (const e of entries) {
|
|
597
|
+
const [a, b] = String(e.pair).split('::');
|
|
598
|
+
if (a === undefined || b === undefined || (e.reason ?? '').trim() === '') {
|
|
599
|
+
throw new Error(
|
|
600
|
+
`audit_skill_overlap: allowlist entry ${JSON.stringify(e)} needs "pair": "a::b" and a non-empty "reason".`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
out.add(_pairKey(a, b));
|
|
604
|
+
}
|
|
605
|
+
return out;
|
|
503
606
|
}
|
|
504
607
|
|
|
505
608
|
/** json.dumps(indent=2) with FloatTag → Python-float repr. */
|
|
@@ -545,23 +648,58 @@ function _dumpsWithFloats(obj: Json, level = 0): string {
|
|
|
545
648
|
interface Args {
|
|
546
649
|
threshold: number;
|
|
547
650
|
quiet: boolean;
|
|
651
|
+
/** Absolute scan root; `undefined` = the `src/skills` default. */
|
|
652
|
+
root?: string;
|
|
653
|
+
/** Absolute report directory; `undefined` = `agents/reports`. */
|
|
654
|
+
outDir?: string;
|
|
655
|
+
/** Print the same-domain pair table to stdout even under `--quiet`. */
|
|
656
|
+
printTable: boolean;
|
|
657
|
+
/** Fail the build on an unjustified same-pack pair at or above threshold. */
|
|
658
|
+
strict: boolean;
|
|
659
|
+
/** Allowlist path override (tests); `undefined` = the shipped file. */
|
|
660
|
+
allowlist?: string;
|
|
661
|
+
/** Which text the vector is built from. `description` is NON-canonical. */
|
|
662
|
+
field: Field;
|
|
548
663
|
}
|
|
549
664
|
|
|
550
665
|
export function parse_args(argv: string[]): Args {
|
|
551
|
-
const args: Args = {
|
|
666
|
+
const args: Args = {
|
|
667
|
+
threshold: OVERLAP_THRESHOLD,
|
|
668
|
+
quiet: false,
|
|
669
|
+
printTable: false,
|
|
670
|
+
strict: false,
|
|
671
|
+
field: 'body',
|
|
672
|
+
};
|
|
673
|
+
const takeValue = (flag: string, inline: string | undefined, next: () => string | undefined): string => {
|
|
674
|
+
const v = inline ?? next();
|
|
675
|
+
if (v === undefined) {
|
|
676
|
+
process.stderr.write(`argument ${flag}: expected one argument\n`);
|
|
677
|
+
process.exit(2);
|
|
678
|
+
}
|
|
679
|
+
return v;
|
|
680
|
+
};
|
|
552
681
|
for (let i = 0; i < argv.length; i++) {
|
|
553
682
|
const a = argv[i] as string;
|
|
554
683
|
if (a === '--quiet') {
|
|
555
684
|
args.quiet = true;
|
|
556
|
-
} else if (a === '--
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
args.
|
|
563
|
-
} else if (a
|
|
564
|
-
args.
|
|
685
|
+
} else if (a === '--strict') {
|
|
686
|
+
args.strict = true;
|
|
687
|
+
} else if (a === '--allowlist' || a.startsWith('--allowlist=')) {
|
|
688
|
+
const inline = a.startsWith('--allowlist=') ? a.slice('--allowlist='.length) : undefined;
|
|
689
|
+
args.allowlist = path.resolve(ROOT, takeValue('--allowlist', inline, () => argv[++i]));
|
|
690
|
+
} else if (a === '--descriptions') {
|
|
691
|
+
args.field = 'description';
|
|
692
|
+
} else if (a === '--print-table') {
|
|
693
|
+
args.printTable = true;
|
|
694
|
+
} else if (a === '--threshold' || a.startsWith('--threshold=')) {
|
|
695
|
+
const inline = a.startsWith('--threshold=') ? a.slice('--threshold='.length) : undefined;
|
|
696
|
+
args.threshold = Number(takeValue('--threshold', inline, () => argv[++i]));
|
|
697
|
+
} else if (a === '--root' || a.startsWith('--root=')) {
|
|
698
|
+
const inline = a.startsWith('--root=') ? a.slice('--root='.length) : undefined;
|
|
699
|
+
args.root = path.resolve(ROOT, takeValue('--root', inline, () => argv[++i]));
|
|
700
|
+
} else if (a === '--out-dir' || a.startsWith('--out-dir=')) {
|
|
701
|
+
const inline = a.startsWith('--out-dir=') ? a.slice('--out-dir='.length) : undefined;
|
|
702
|
+
args.outDir = path.resolve(ROOT, takeValue('--out-dir', inline, () => argv[++i]));
|
|
565
703
|
} else {
|
|
566
704
|
process.stderr.write(`unrecognized arguments: ${a}\n`);
|
|
567
705
|
process.exit(2);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Reviewed same-pack overlaps at or above the canonical 0.70 threshold. Each entry needs a real reason — a pair is here because a human read both skills and decided the overlap is structural, not because the gate was noisy. Capped at 20 by audit_skill_overlap.ts: crossing the cap means the threshold is wrong, not that the corpus needs a twenty-first exception. Empty is the healthy state.",
|
|
3
|
+
"entries": []
|
|
4
|
+
}
|
|
@@ -146,6 +146,11 @@ function render(): string {
|
|
|
146
146
|
const entries = [...ledger.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
147
147
|
const backed = entries.filter((e) => e.status === 'backed');
|
|
148
148
|
const unbacked = entries.filter((e) => e.status === 'unbacked');
|
|
149
|
+
// `resolved-null` is neither backed nor debt: the question was asked, the
|
|
150
|
+
// answer came back null, and the entry is closed. Counting it as debt
|
|
151
|
+
// would inflate the inventory with items that can never shrink; counting
|
|
152
|
+
// it as backed would claim a win that was never measured.
|
|
153
|
+
const resolvedNull = entries.filter((e) => e.status === 'resolved-null');
|
|
149
154
|
|
|
150
155
|
const L: string[] = [];
|
|
151
156
|
L.push('<!-- GENERATED by src/scripts/build_proof.ts — do NOT hand-edit.');
|
|
@@ -232,6 +237,13 @@ function render(): string {
|
|
|
232
237
|
L.push('allowed to carry a marker in public prose. Hiding them would be the');
|
|
233
238
|
L.push('opposite of the point.');
|
|
234
239
|
}
|
|
240
|
+
if (resolvedNull.length > 0) {
|
|
241
|
+
L.push('');
|
|
242
|
+
L.push(`And our **nulls**: ${resolvedNull.length} claim(s) are \`resolved-null\` —`);
|
|
243
|
+
L.push('measured, the threshold was missed, and the entry is closed rather than');
|
|
244
|
+
L.push('left open forever. A null that stays filed as pending debt is a claim');
|
|
245
|
+
L.push('quietly waiting to be re-argued.');
|
|
246
|
+
}
|
|
235
247
|
L.push('');
|
|
236
248
|
L.push('## 2. We publish honest nulls');
|
|
237
249
|
L.push('');
|
|
@@ -419,9 +431,11 @@ function render(): string {
|
|
|
419
431
|
{
|
|
420
432
|
const ledger = [...load_ledger().values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
421
433
|
const backed = ledger.filter((e) => e.status === 'backed').length;
|
|
434
|
+
const nulls = ledger.filter((e) => e.status === 'resolved-null').length;
|
|
422
435
|
L.push(
|
|
423
436
|
`**Axis 2 — evidence form per public claim.** ${ledger.length} ledger entries · ` +
|
|
424
|
-
`${backed} backed · ${ledger.length - backed} unbacked inventory
|
|
437
|
+
`${backed} backed · ${ledger.length - backed - nulls} unbacked inventory · ` +
|
|
438
|
+
`${nulls} resolved-null.`,
|
|
425
439
|
);
|
|
426
440
|
L.push('');
|
|
427
441
|
L.push('| Claim id | Kind | Status | Evidence pointer |');
|