@event4u/agent-config 9.13.0 → 9.14.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 +48 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/optimize/deep.md +147 -0
- package/dist/agent-src/commands/optimize.md +12 -6
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-QFKQLQ7X.js → chunk-GCVFLBP2.js} +399 -365
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +39 -11
- 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 +3 -3
- package/dist/discovery/workspaces.json +5 -4
- package/dist/install/install.mjs +648 -614
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +46 -13
- package/package.json +1 -1
- package/src/scripts/_lib/exec_evidence.ts +5 -0
- package/src/scripts/_lib/scoped_projection.ts +212 -0
- package/src/scripts/check_artefact_count_messaging.ts +128 -15
- package/src/scripts/count_scoped_projection.ts +104 -0
- package/src/scripts/install.ts +13 -90
- package/src/scripts/skill_linter.ts +81 -14
- package/src/scripts/update_counts.ts +90 -11
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"discovery": {
|
|
3
|
-
"artefact_count":
|
|
3
|
+
"artefact_count": 594,
|
|
4
4
|
"scanner_version": "c24312e116f8"
|
|
5
5
|
},
|
|
6
6
|
"generated_at": "2026-08-02",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"homepage": "https://event4u-app.github.io/agent-config",
|
|
10
10
|
"name": "@event4u/agent-config",
|
|
11
11
|
"repository": "https://github.com/event4u-app/agent-config",
|
|
12
|
-
"version": "9.
|
|
12
|
+
"version": "9.14.0"
|
|
13
13
|
},
|
|
14
14
|
"registries": [
|
|
15
15
|
{
|
package/dist/mcp/server.json
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"transport": {
|
|
10
10
|
"type": "stdio"
|
|
11
11
|
},
|
|
12
|
-
"version": "9.
|
|
12
|
+
"version": "9.14.0"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"repository": {
|
|
16
16
|
"source": "github",
|
|
17
17
|
"url": "https://github.com/event4u-app/agent-config"
|
|
18
18
|
},
|
|
19
|
-
"version": "9.
|
|
19
|
+
"version": "9.14.0"
|
|
20
20
|
}
|
package/dist/mcp/server.mjs
CHANGED
|
@@ -14036,6 +14036,7 @@ __export(skill_linter_exports, {
|
|
|
14036
14036
|
gather_all_candidate_files: () => gather_all_candidate_files,
|
|
14037
14037
|
gather_candidate_files_under: () => gather_candidate_files_under,
|
|
14038
14038
|
gather_changed_candidate_files: () => gather_changed_candidate_files,
|
|
14039
|
+
last_changed_base_diff_ok: () => last_changed_base_diff_ok,
|
|
14039
14040
|
last_changed_scope_reason: () => last_changed_scope_reason,
|
|
14040
14041
|
lint_command: () => lint_command,
|
|
14041
14042
|
lint_file: () => lint_file,
|
|
@@ -15984,6 +15985,7 @@ function gather_candidate_files_under(srcRoot) {
|
|
|
15984
15985
|
function gather_changed_candidate_files(root, since) {
|
|
15985
15986
|
const scope = resolveContentLintScope({ cwd: root, since: since ?? null });
|
|
15986
15987
|
last_changed_scope_reason = scope.reason;
|
|
15988
|
+
last_changed_base_diff_ok = false;
|
|
15987
15989
|
const diffCommands = [
|
|
15988
15990
|
["git", "diff", "--name-only", `${scope.base}...HEAD`],
|
|
15989
15991
|
["git", "diff", "--name-only", "--cached", "HEAD"],
|
|
@@ -15991,8 +15993,13 @@ function gather_changed_candidate_files(root, since) {
|
|
|
15991
15993
|
];
|
|
15992
15994
|
try {
|
|
15993
15995
|
let rawLines = [];
|
|
15996
|
+
let first = true;
|
|
15994
15997
|
for (const cmd of diffCommands) {
|
|
15995
15998
|
const result = runCountedProbe(cmd[0], cmd.slice(1), { cwd: root });
|
|
15999
|
+
if (first) {
|
|
16000
|
+
last_changed_base_diff_ok = result.ok;
|
|
16001
|
+
first = false;
|
|
16002
|
+
}
|
|
15996
16003
|
if (result.ok && result.stdout.trim()) {
|
|
15997
16004
|
rawLines = splitlines(result.stdout);
|
|
15998
16005
|
break;
|
|
@@ -17398,6 +17405,25 @@ function _release_scope_requires_findings(root, since) {
|
|
|
17398
17405
|
const scope = resolveContentLintScope({ cwd: root, since: since ?? null });
|
|
17399
17406
|
return scope.isRelease && scope.previousTag !== null;
|
|
17400
17407
|
}
|
|
17408
|
+
function _changed_filter_matches_head(root) {
|
|
17409
|
+
const result = runCountedProbe("git", ["ls-files", "--", "dist/agent-src"], { cwd: root });
|
|
17410
|
+
if (!result.ok) {
|
|
17411
|
+
return false;
|
|
17412
|
+
}
|
|
17413
|
+
for (const raw of splitlines(result.stdout)) {
|
|
17414
|
+
const norm = raw.trim().replace(/\\/g, "/");
|
|
17415
|
+
if (!norm || norm.includes("/evals/")) {
|
|
17416
|
+
continue;
|
|
17417
|
+
}
|
|
17418
|
+
if (!norm.startsWith("dist/agent-src/") && !norm.includes("/dist/agent-src/")) {
|
|
17419
|
+
continue;
|
|
17420
|
+
}
|
|
17421
|
+
if (norm.endsWith("/SKILL.md") || norm.endsWith(".md") && (norm.includes("/rules/") || norm.includes("/commands/"))) {
|
|
17422
|
+
return true;
|
|
17423
|
+
}
|
|
17424
|
+
}
|
|
17425
|
+
return false;
|
|
17426
|
+
}
|
|
17401
17427
|
function main7(argv) {
|
|
17402
17428
|
let args;
|
|
17403
17429
|
try {
|
|
@@ -17442,20 +17468,26 @@ function main7(argv) {
|
|
|
17442
17468
|
`);
|
|
17443
17469
|
}
|
|
17444
17470
|
if (args.changed && _release_scope_requires_findings(root, args.since)) {
|
|
17445
|
-
|
|
17446
|
-
|
|
17447
|
-
|
|
17448
|
-
|
|
17449
|
-
|
|
17450
|
-
|
|
17451
|
-
|
|
17452
|
-
|
|
17453
|
-
|
|
17454
|
-
|
|
17471
|
+
if (last_changed_base_diff_ok && _changed_filter_matches_head(root)) {
|
|
17472
|
+
process7.stderr.write(
|
|
17473
|
+
"\u2705 verified-empty release scope: the base-span diff ran cleanly and the corpus filter still matches tracked files at HEAD \u2014 this release genuinely changes no skill/rule file. The gate is alive, not blind.\n"
|
|
17474
|
+
);
|
|
17475
|
+
} else {
|
|
17476
|
+
try {
|
|
17477
|
+
assertScanned({
|
|
17478
|
+
gate: "skill_linter --changed (release PR)",
|
|
17479
|
+
scanned: 0,
|
|
17480
|
+
units: "skill/rule files",
|
|
17481
|
+
roots: [last_changed_scope_reason]
|
|
17482
|
+
});
|
|
17483
|
+
} catch (exc) {
|
|
17484
|
+
if (exc instanceof DeadScopeError) {
|
|
17485
|
+
process7.stderr.write(`\u274C ${exc.message}
|
|
17455
17486
|
`);
|
|
17456
|
-
|
|
17487
|
+
return 2;
|
|
17488
|
+
}
|
|
17489
|
+
throw exc;
|
|
17457
17490
|
}
|
|
17458
|
-
throw exc;
|
|
17459
17491
|
}
|
|
17460
17492
|
}
|
|
17461
17493
|
process7.stderr.write("No matching skill/rule files found.\n");
|
|
@@ -17635,7 +17667,7 @@ function _isCliEntry7() {
|
|
|
17635
17667
|
return false;
|
|
17636
17668
|
}
|
|
17637
17669
|
}
|
|
17638
|
-
var _HERE4, REQUIRED_PERSONA_SECTIONS_CORE, REQUIRED_PERSONA_SECTIONS_SPECIALIST, VALID_PERSONA_TIERS, PERSONA_LINE_BUDGETS, REQUIRED_USERTYPE_SECTIONS, USERTYPE_LINE_BUDGET, VALID_PERSONA_WINGS, PERSONA_LINE_BUDGETS_BY_WING, REQUIRED_SKILL_SECTIONS, SECTION_ALIASES, RECOMMENDED_SKILL_SECTIONS, RULE_BAD_SIGNS, FRUGALITY_WRITER_SKILLS, FRUGALITY_CHARTER_RELPATH, FRUGALITY_CHARTER_INDEX_RULES, VAGUE_VALIDATION_PATTERNS, TRIGGER_WARNING_PATTERNS, ORDERED_STEP_PATTERN, SECTION_PATTERN, FRONTMATTER_PATTERN, DESCRIPTION_PATTERN, STATUS_PATTERN, REPLACED_BY_PATTERN, TIER_PATTERN, TYPE_PATTERN, SOURCE_PATTERN, SENIOR_RELATED_SKILLS_PATTERN, SENIOR_RELATED_WHEN_PATTERN, SENIOR_RELATED_WHEN_NOT_PATTERN, SENIOR_PROACTIVE_PATTERN, SENIOR_OUTPUT_PATTERN, H1_PATTERN, DOUBLE_BLANK_PATTERN, VALID_RULE_TYPES, VALID_RULE_SOURCES, ROUTER_ALLOWED_TRIGGER_KEYS, KERNEL_RULE_IDS, VALID_EXECUTION_TYPES, VALID_EXECUTION_HANDLERS, VALID_EXECUTION_SAFETY_MODES, VALID_EXECUTION_FIELDS, WING3_SPINE_SLOTS, CONTEXT_SPINE_INLINE_PATTERN, WING3_SAAS_URL_PATTERN, WING3_VENDOR_BLACKLIST, WING3_STACK_LOCKED_PATTERN, WING3_CHANNEL_TACTIC_PATTERN, WING4_SPINE_SLOTS, WING4_SAAS_URL_PATTERN, WING4_VENDOR_BLACKLIST, WING4_STAGE_AGNOSTIC_PATTERN, Issue, LintResult, _ROLE_CONTRACT_CANDIDATES, _ROLE_CONTRACT_SLUGS_CACHE, _ROLE_CONTRACT_REF_PATTERN, PROCEDURE_HEADING_PATTERN, COMMAND_FRONTMATTER_DELEGATION_KEYS, MD_LINK_PATTERN_G, NAME_PATTERN, DISABLE_MODEL_PATTERN, _INSPECT_VERB_PATTERN, last_changed_scope_reason, _INTERACTION_NAME_PATTERNS, _INTERACTION_CONTENT_KEYWORDS, _EXEC_FILE_SIGNALS, _EXEC_CONTENT_SIGNALS, _TASK_TYPE_SIGNALS, _VERIFICATION_TOOLS, _FRUGALITY_STANDARDS_PATTERN, _FRUGALITY_CHARTER_LINK_PATTERN, _MD_LINK_PATTERN, _MALICE_CRED_EXFIL, _MALICE_REMOTE_EXEC, _MALICE_FORCE_PUSH, _MALICE_CHMOD_SECRETS, _MALICE_SHELL_INJECT, _MALICE_PATTERNS, _NAG_UPDATE_CHECK, _NAG_STAY_CURRENT, _NAG_SELF_PROMO, _NAG_BEST_SKILL, _NAG_MUST_USE, _NAG_ALWAYS_USE, _NAG_PATTERNS, _OUTPUT_SCHEMA_KEY_PATTERN, _SCHEMA_ARTEFACT_TYPES, ArgError, isCliEntry2;
|
|
17670
|
+
var _HERE4, REQUIRED_PERSONA_SECTIONS_CORE, REQUIRED_PERSONA_SECTIONS_SPECIALIST, VALID_PERSONA_TIERS, PERSONA_LINE_BUDGETS, REQUIRED_USERTYPE_SECTIONS, USERTYPE_LINE_BUDGET, VALID_PERSONA_WINGS, PERSONA_LINE_BUDGETS_BY_WING, REQUIRED_SKILL_SECTIONS, SECTION_ALIASES, RECOMMENDED_SKILL_SECTIONS, RULE_BAD_SIGNS, FRUGALITY_WRITER_SKILLS, FRUGALITY_CHARTER_RELPATH, FRUGALITY_CHARTER_INDEX_RULES, VAGUE_VALIDATION_PATTERNS, TRIGGER_WARNING_PATTERNS, ORDERED_STEP_PATTERN, SECTION_PATTERN, FRONTMATTER_PATTERN, DESCRIPTION_PATTERN, STATUS_PATTERN, REPLACED_BY_PATTERN, TIER_PATTERN, TYPE_PATTERN, SOURCE_PATTERN, SENIOR_RELATED_SKILLS_PATTERN, SENIOR_RELATED_WHEN_PATTERN, SENIOR_RELATED_WHEN_NOT_PATTERN, SENIOR_PROACTIVE_PATTERN, SENIOR_OUTPUT_PATTERN, H1_PATTERN, DOUBLE_BLANK_PATTERN, VALID_RULE_TYPES, VALID_RULE_SOURCES, ROUTER_ALLOWED_TRIGGER_KEYS, KERNEL_RULE_IDS, VALID_EXECUTION_TYPES, VALID_EXECUTION_HANDLERS, VALID_EXECUTION_SAFETY_MODES, VALID_EXECUTION_FIELDS, WING3_SPINE_SLOTS, CONTEXT_SPINE_INLINE_PATTERN, WING3_SAAS_URL_PATTERN, WING3_VENDOR_BLACKLIST, WING3_STACK_LOCKED_PATTERN, WING3_CHANNEL_TACTIC_PATTERN, WING4_SPINE_SLOTS, WING4_SAAS_URL_PATTERN, WING4_VENDOR_BLACKLIST, WING4_STAGE_AGNOSTIC_PATTERN, Issue, LintResult, _ROLE_CONTRACT_CANDIDATES, _ROLE_CONTRACT_SLUGS_CACHE, _ROLE_CONTRACT_REF_PATTERN, PROCEDURE_HEADING_PATTERN, COMMAND_FRONTMATTER_DELEGATION_KEYS, MD_LINK_PATTERN_G, NAME_PATTERN, DISABLE_MODEL_PATTERN, _INSPECT_VERB_PATTERN, last_changed_scope_reason, last_changed_base_diff_ok, _INTERACTION_NAME_PATTERNS, _INTERACTION_CONTENT_KEYWORDS, _EXEC_FILE_SIGNALS, _EXEC_CONTENT_SIGNALS, _TASK_TYPE_SIGNALS, _VERIFICATION_TOOLS, _FRUGALITY_STANDARDS_PATTERN, _FRUGALITY_CHARTER_LINK_PATTERN, _MD_LINK_PATTERN, _MALICE_CRED_EXFIL, _MALICE_REMOTE_EXEC, _MALICE_FORCE_PUSH, _MALICE_CHMOD_SECRETS, _MALICE_SHELL_INJECT, _MALICE_PATTERNS, _NAG_UPDATE_CHECK, _NAG_STAY_CURRENT, _NAG_SELF_PROMO, _NAG_BEST_SKILL, _NAG_MUST_USE, _NAG_ALWAYS_USE, _NAG_PATTERNS, _OUTPUT_SCHEMA_KEY_PATTERN, _SCHEMA_ARTEFACT_TYPES, ArgError, isCliEntry2;
|
|
17639
17671
|
var init_skill_linter = __esm({
|
|
17640
17672
|
"src/scripts/skill_linter.ts"() {
|
|
17641
17673
|
"use strict";
|
|
@@ -17811,6 +17843,7 @@ var init_skill_linter = __esm({
|
|
|
17811
17843
|
DISABLE_MODEL_PATTERN = /^disable-model-invocation:\s*"?(true|false)"?\s*$/m;
|
|
17812
17844
|
_INSPECT_VERB_PATTERN = /\b(?:inspect|examine|audit|survey|read|look\s+at|check|review|understand|identify|analyze|analyse|detect|gather|discover)\b/i;
|
|
17813
17845
|
last_changed_scope_reason = "";
|
|
17846
|
+
last_changed_base_diff_ok = false;
|
|
17814
17847
|
_INTERACTION_NAME_PATTERNS = /skill-router|handoff|analysis-skill|skill-writing|skill-reviewer|model-recommendation|developer-like-execution|universal-project-analysis|interaction|autonomous-mode|feature-planning/i;
|
|
17815
17848
|
_INTERACTION_CONTENT_KEYWORDS = [
|
|
17816
17849
|
"handoff",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
3
|
"mcpName": "io.github.event4u-app/agent-config",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.14.0",
|
|
5
5
|
"description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including \"zero runtime daemon\".",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
@@ -47,6 +47,11 @@ export const ALLOWLIST_PREFIXES: readonly (readonly string[])[] = [
|
|
|
47
47
|
['skill_eval_coverage'],
|
|
48
48
|
['domain_soundness_status'],
|
|
49
49
|
['check_artefact_count_messaging'],
|
|
50
|
+
// `update_counts --check` re-derives every anchored number in the docs and
|
|
51
|
+
// exits non-zero on any disagreement — so its exit code IS the verdict for
|
|
52
|
+
// a published count, not merely evidence that some report exists. Read-only
|
|
53
|
+
// under `--check`; same deterministic, model-free class as its neighbours.
|
|
54
|
+
['update_counts'],
|
|
50
55
|
['check_enforcement_coverage'],
|
|
51
56
|
['check_token_regression'],
|
|
52
57
|
['lint_agent_security'],
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scoped-projection predicate — ONE implementation, two callers.
|
|
3
|
+
*
|
|
4
|
+
* `projection.mode: scoped` narrows a GLOBAL deploy to the active profile's
|
|
5
|
+
* packs. Two places need to know exactly which skills survive that prune:
|
|
6
|
+
*
|
|
7
|
+
* 1. `install.ts` — which artefacts it actually writes to the user's tree.
|
|
8
|
+
* 2. `count_scoped_projection.ts` — the number the claims ledger publishes.
|
|
9
|
+
*
|
|
10
|
+
* Before this module those were the same rule written once and *typed* a
|
|
11
|
+
* second time into `docs/CLAIMS.md`. The published pair drifted from the
|
|
12
|
+
* benchmark doc it cited as its own method (217/288 vs 215/286) and grew by
|
|
13
|
+
* one on every skill added, because nothing could compare them. The fix is
|
|
14
|
+
* not a second counter — a second counting path that disagrees with the
|
|
15
|
+
* first is the failure this replaces. It is this module: the installer and
|
|
16
|
+
* the counter call the same functions, so a divergence is impossible rather
|
|
17
|
+
* than merely unlikely.
|
|
18
|
+
*
|
|
19
|
+
* Consumers: `install.ts` (the prune itself) and `count_scoped_projection.ts`
|
|
20
|
+
* (the published number).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
import type * as YamlModule from 'yaml';
|
|
27
|
+
|
|
28
|
+
import * as surface_tiers from './surface_tiers.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ESM-safe `require`. A bare `require('yaml')` is `undefined` under tsx (the
|
|
32
|
+
* runner every `./scripts-run` gate uses), so the older lazy-require pattern
|
|
33
|
+
* elsewhere in this tree degrades to "no yaml" silently. `createRequire`
|
|
34
|
+
* keeps the load lazy — `yaml` stays out of the installer bundle's static
|
|
35
|
+
* graph — while actually resolving.
|
|
36
|
+
*/
|
|
37
|
+
const _require = createRequire(import.meta.url);
|
|
38
|
+
|
|
39
|
+
/** `workspaces` values whose packs are ALWAYS active under `scoped` mode. */
|
|
40
|
+
export const SCOPED_ACTIVE_WORKSPACES: ReadonlySet<string> = new Set([
|
|
41
|
+
'engineering',
|
|
42
|
+
'agent-config-maintainer',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export interface PackRecord {
|
|
46
|
+
id: string;
|
|
47
|
+
workspaces: string[];
|
|
48
|
+
requires: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
52
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Lazy YAML safe_load mirroring PyYAML (version 1.1); `null` when absent. */
|
|
56
|
+
function yamlSafeLoad(text: string): unknown {
|
|
57
|
+
let YAML: typeof YamlModule;
|
|
58
|
+
try {
|
|
59
|
+
YAML = _require('yaml') as typeof YamlModule;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return YAML.parse(text, { version: '1.1' }) ?? null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Repo-relative location of the pack vocabulary. */
|
|
71
|
+
export const PACKS_YML_REL = path.join('src', 'config', 'discovery', 'packs.yml');
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse `src/config/discovery/packs.yml` into `{id, workspaces, requires}`
|
|
75
|
+
* records. `requires` falls back to the legacy `requires_hint` field name
|
|
76
|
+
* when `requires` is absent.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately THROWS on a missing file or a parse result that is not a
|
|
79
|
+
* list — an empty active-pack set would be the LEAST safe fallback here
|
|
80
|
+
* (it would prune every tagged artefact, the opposite of the fail-safe
|
|
81
|
+
* contract). Callers wrap this in a try/catch that restores the full tree
|
|
82
|
+
* on any error instead.
|
|
83
|
+
*/
|
|
84
|
+
export function load_packs_registry(package_root: string): PackRecord[] {
|
|
85
|
+
const vocab_path = path.join(package_root, PACKS_YML_REL);
|
|
86
|
+
const data = yamlSafeLoad(fs.readFileSync(vocab_path, 'utf-8'));
|
|
87
|
+
if (!Array.isArray(data)) {
|
|
88
|
+
throw new Error(`packs.yml did not parse to a list: ${vocab_path}`);
|
|
89
|
+
}
|
|
90
|
+
const out: PackRecord[] = [];
|
|
91
|
+
for (const entry of data) {
|
|
92
|
+
if (!isPlainObject(entry)) continue;
|
|
93
|
+
const id = entry['id'];
|
|
94
|
+
if (typeof id !== 'string' || id === '') continue;
|
|
95
|
+
const workspaces_raw = entry['workspaces'];
|
|
96
|
+
const workspaces = Array.isArray(workspaces_raw)
|
|
97
|
+
? workspaces_raw.filter((w): w is string => typeof w === 'string')
|
|
98
|
+
: [];
|
|
99
|
+
const requires_raw = entry['requires'] ?? entry['requires_hint'];
|
|
100
|
+
const requires = Array.isArray(requires_raw)
|
|
101
|
+
? requires_raw.filter((r): r is string => typeof r === 'string')
|
|
102
|
+
: [];
|
|
103
|
+
out.push({ id, workspaces, requires });
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Active pack id set for `scoped` mode: every pack whose `workspaces`
|
|
110
|
+
* intersects `SCOPED_ACTIVE_WORKSPACES`, unioned with `runtime_active_packs`,
|
|
111
|
+
* expanded over the `requires` graph (transitive closure).
|
|
112
|
+
*/
|
|
113
|
+
export function compute_active_pack_ids(
|
|
114
|
+
packs: readonly PackRecord[],
|
|
115
|
+
runtime_active_packs: readonly string[],
|
|
116
|
+
): Set<string> {
|
|
117
|
+
const by_id = new Map<string, PackRecord>();
|
|
118
|
+
for (const p of packs) by_id.set(p.id, p);
|
|
119
|
+
|
|
120
|
+
const active = new Set<string>();
|
|
121
|
+
for (const p of packs) {
|
|
122
|
+
if (p.workspaces.some((w) => SCOPED_ACTIVE_WORKSPACES.has(w))) {
|
|
123
|
+
active.add(p.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const id of runtime_active_packs) {
|
|
127
|
+
active.add(id);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let frontier = [...active];
|
|
131
|
+
while (frontier.length > 0) {
|
|
132
|
+
const next: string[] = [];
|
|
133
|
+
for (const id of frontier) {
|
|
134
|
+
const rec = by_id.get(id);
|
|
135
|
+
if (rec === undefined) continue;
|
|
136
|
+
for (const dep of rec.requires) {
|
|
137
|
+
if (!active.has(dep)) {
|
|
138
|
+
active.add(dep);
|
|
139
|
+
next.push(dep);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
frontier = next;
|
|
144
|
+
}
|
|
145
|
+
return active;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The prune predicate itself: `true` when this artefact is DROPPED under
|
|
150
|
+
* `scoped`. Untagged artefacts (empty `packs:` frontmatter) are core/shared
|
|
151
|
+
* and always kept; a tagged artefact survives iff its tags intersect the
|
|
152
|
+
* active set.
|
|
153
|
+
*/
|
|
154
|
+
export function is_pruned_under_scoped(md_path: string, active_ids: ReadonlySet<string>): boolean {
|
|
155
|
+
const packs = surface_tiers.frontmatter_packs(md_path);
|
|
156
|
+
if (packs.size === 0) return false;
|
|
157
|
+
for (const id of packs) {
|
|
158
|
+
if (active_ids.has(id)) return false;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface ScopedProjectionStats {
|
|
164
|
+
/** Skills a default `projection.mode: scoped` install deploys. */
|
|
165
|
+
projected: number;
|
|
166
|
+
/** Every skill in the catalog — identical to canonical `count('skills')`. */
|
|
167
|
+
total: number;
|
|
168
|
+
/** `total - projected`; the reduction the claim reports. */
|
|
169
|
+
pruned: number;
|
|
170
|
+
/** Pack ids active by default, sorted — the requires-closure result. */
|
|
171
|
+
active_packs: string[];
|
|
172
|
+
/** The workspaces whose packs are always on under `scoped`. */
|
|
173
|
+
workspaces: string[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Partition a skill catalog with the installer's own predicate.
|
|
178
|
+
*
|
|
179
|
+
* The caller supplies the file set — in practice always
|
|
180
|
+
* `update_counts.iter_skills()`, the walk behind the canonical
|
|
181
|
+
* `count('skills')`, so `projected + pruned === total` holds by
|
|
182
|
+
* construction. Taking it as a parameter (rather than importing
|
|
183
|
+
* `update_counts` here) keeps this module free of a cycle: `update_counts`
|
|
184
|
+
* imports it, not the other way round.
|
|
185
|
+
*
|
|
186
|
+
* `runtime_active_packs` defaults to empty: the published claim is about the
|
|
187
|
+
* DEFAULT for a new install, and a fresh machine has no overlay.
|
|
188
|
+
*/
|
|
189
|
+
export function scoped_projection_stats(
|
|
190
|
+
package_root: string,
|
|
191
|
+
skills: Iterable<string>,
|
|
192
|
+
runtime_active_packs: readonly string[] = [],
|
|
193
|
+
): ScopedProjectionStats {
|
|
194
|
+
const active = compute_active_pack_ids(load_packs_registry(package_root), runtime_active_packs);
|
|
195
|
+
|
|
196
|
+
let projected = 0;
|
|
197
|
+
let total = 0;
|
|
198
|
+
for (const skill of skills) {
|
|
199
|
+
total += 1;
|
|
200
|
+
if (!is_pruned_under_scoped(skill, active)) {
|
|
201
|
+
projected += 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
projected,
|
|
207
|
+
total,
|
|
208
|
+
pruned: total - projected,
|
|
209
|
+
active_packs: [...active].sort(),
|
|
210
|
+
workspaces: [...SCOPED_ACTIVE_WORKSPACES].sort(),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -33,7 +33,7 @@ import * as path from 'node:path';
|
|
|
33
33
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
34
34
|
|
|
35
35
|
import { canonical_counts } from './check_command_count_messaging.js';
|
|
36
|
-
import { count } from './update_counts.js';
|
|
36
|
+
import { count, TARGETS } from './update_counts.js';
|
|
37
37
|
|
|
38
38
|
const _HERE = fileURLToPath(import.meta.url);
|
|
39
39
|
export const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -164,6 +164,93 @@ export function scan_structured(
|
|
|
164
164
|
return { findings, seen };
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// --- Anchor-coverage pass (road-to-reproducible-artefact-counts) ----------
|
|
168
|
+
//
|
|
169
|
+
// The two gates used to overlap only by accident. This scanner flags any
|
|
170
|
+
// count-shaped prose; `update_counts` rewrites a hand-written list of
|
|
171
|
+
// anchors. Nothing asserted the second list covered the first, so positions
|
|
172
|
+
// existed that the scanner could turn red and the generator could not fix —
|
|
173
|
+
// the README Commands badge and the getting-started browse line drifted to
|
|
174
|
+
// 191 against a canonical 192 exactly there, each explicitly left unanchored
|
|
175
|
+
// to "avoid double-ownership". The reconciliation was a human's job, and the
|
|
176
|
+
// gate's own advice said so ("run update_counts … OR correct the prose").
|
|
177
|
+
//
|
|
178
|
+
// This pass removes the human: every position this scanner can flag must be
|
|
179
|
+
// rewritable by the generator, or be a generated file whose source is. A new
|
|
180
|
+
// count-shaped sentence with no anchor now fails CI as a coverage gap.
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Scanner kind → the generator kinds that legitimately write it.
|
|
184
|
+
*
|
|
185
|
+
* One prose kind can have several canonical numbers and they are NOT
|
|
186
|
+
* interchangeable: "N commands" in prose means ACTIVE commands (total minus
|
|
187
|
+
* deprecation shims), and a scoped-projection sentence carries the projected
|
|
188
|
+
* skill count beside the catalog total. Anchoring an active-count position
|
|
189
|
+
* to the raw `commands` total would silently write the wrong number the day
|
|
190
|
+
* a command is superseded — which is why this is a mapping and not equality.
|
|
191
|
+
*/
|
|
192
|
+
const ANCHOR_KINDS: Readonly<Record<string, readonly string[]>> = {
|
|
193
|
+
skills: ['skills', 'skills_scoped'],
|
|
194
|
+
commands: ['commands', 'commands_active'],
|
|
195
|
+
rules: ['rules'],
|
|
196
|
+
guidelines: ['guidelines'],
|
|
197
|
+
personas: ['personas'],
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Surfaces regenerated from an anchored source — exempt because fixing the
|
|
202
|
+
* source fixes them. Each entry names the generator, so the exemption stays
|
|
203
|
+
* falsifiable rather than becoming a place to hide drift.
|
|
204
|
+
*/
|
|
205
|
+
export const GENERATED_DOWNSTREAM: Readonly<Record<string, string>> = {
|
|
206
|
+
'docs/proof.md': 'generated from docs/CLAIMS.md by src/scripts/build_proof.ts',
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export interface CoverageGap {
|
|
210
|
+
file: string;
|
|
211
|
+
line: number;
|
|
212
|
+
kind: string;
|
|
213
|
+
text: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Positions this scanner can flag that `update_counts` cannot rewrite.
|
|
218
|
+
*
|
|
219
|
+
* A position counts as anchored when some `TARGETS` pattern for the same
|
|
220
|
+
* file, bound to a compatible kind, matches the same line.
|
|
221
|
+
*/
|
|
222
|
+
export function anchor_coverage_gaps(root: string = ROOT): CoverageGap[] {
|
|
223
|
+
const anchorsByFile = new Map<string, ReadonlyArray<[string, string]>>();
|
|
224
|
+
for (const [rel, patterns] of TARGETS) {
|
|
225
|
+
anchorsByFile.set(rel, patterns);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const gaps: CoverageGap[] = [];
|
|
229
|
+
for (const rel of SURFACES) {
|
|
230
|
+
if (rel in GENERATED_DOWNSTREAM) continue;
|
|
231
|
+
const p = path.join(root, rel);
|
|
232
|
+
if (!fs.existsSync(p)) continue;
|
|
233
|
+
const lines = fs.readFileSync(p, 'utf-8').split('\n');
|
|
234
|
+
const anchors = anchorsByFile.get(rel) ?? [];
|
|
235
|
+
for (const [kind, pattern] of KIND_PATTERNS) {
|
|
236
|
+
const allowed = ANCHOR_KINDS[kind] ?? [kind];
|
|
237
|
+
for (let i = 0; i < lines.length; i++) {
|
|
238
|
+
const line = lines[i]!;
|
|
239
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
240
|
+
if (!re.test(line)) continue;
|
|
241
|
+
const anchored = anchors.some(
|
|
242
|
+
([raw, anchorKind]) =>
|
|
243
|
+
allowed.includes(anchorKind) && new RegExp(raw).test(line),
|
|
244
|
+
);
|
|
245
|
+
if (!anchored) {
|
|
246
|
+
gaps.push({ file: rel, line: i + 1, kind, text: line.trim().slice(0, 100) });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return gaps;
|
|
252
|
+
}
|
|
253
|
+
|
|
167
254
|
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
168
255
|
const QUIET = argv.includes('--quiet');
|
|
169
256
|
const expected: Record<string, number> = {};
|
|
@@ -213,29 +300,55 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
213
300
|
}
|
|
214
301
|
}
|
|
215
302
|
|
|
216
|
-
|
|
303
|
+
// Anchor coverage — every position this gate can flag must be a position
|
|
304
|
+
// `update_counts` can rewrite, so the two never need a human to reconcile.
|
|
305
|
+
const gaps = anchor_coverage_gaps();
|
|
306
|
+
|
|
307
|
+
if (allFindings.length === 0 && inconsistent.length === 0 && gaps.length === 0) {
|
|
217
308
|
if (!QUIET) {
|
|
218
|
-
process.stdout.write(
|
|
309
|
+
process.stdout.write(
|
|
310
|
+
'✅ All artefact-count prose in sync with source, every position anchored.\n',
|
|
311
|
+
);
|
|
219
312
|
}
|
|
220
313
|
return 0;
|
|
221
314
|
}
|
|
222
315
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
316
|
+
if (allFindings.length > 0 || inconsistent.length > 0) {
|
|
317
|
+
process.stdout.write(
|
|
318
|
+
`❌ Artefact-count messaging drift — ${allFindings.length} mismatch(es):\n`,
|
|
319
|
+
);
|
|
320
|
+
for (const f of allFindings) {
|
|
321
|
+
const tag = f.approx ? ' (approximation "~" not allowed on flagship surfaces)' : '';
|
|
322
|
+
process.stdout.write(
|
|
323
|
+
` ${f.file}:${f.line}: ${f.kind} says ${f.found}, expected ${f.expected}${tag}\n`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
for (const line of inconsistent) {
|
|
327
|
+
process.stdout.write(` internal inconsistency — ${line}\n`);
|
|
328
|
+
}
|
|
228
329
|
process.stdout.write(
|
|
229
|
-
|
|
330
|
+
'\nFix: run `./scripts-run src/scripts/update_counts` — every checked position\n' +
|
|
331
|
+
'is anchored, so the generator can write it. Never hand-type a count.\n',
|
|
230
332
|
);
|
|
231
333
|
}
|
|
232
|
-
|
|
233
|
-
|
|
334
|
+
|
|
335
|
+
if (gaps.length > 0) {
|
|
336
|
+
process.stdout.write(
|
|
337
|
+
`❌ Anchor-coverage gap — ${gaps.length} position(s) this gate checks but\n` +
|
|
338
|
+
' `update_counts` cannot rewrite:\n',
|
|
339
|
+
);
|
|
340
|
+
for (const g of gaps) {
|
|
341
|
+
process.stdout.write(` ${g.file}:${g.line}: ${g.kind} — ${g.text}\n`);
|
|
342
|
+
}
|
|
343
|
+
process.stdout.write(
|
|
344
|
+
'\nFix: add an anchor for each position to `TARGETS` in\n' +
|
|
345
|
+
'`src/scripts/update_counts.ts`, bound to the kind that position\n' +
|
|
346
|
+
'actually carries (`commands_active` for active-command mentions,\n' +
|
|
347
|
+
'`skills_scoped` for the projected-skill figure). If the file is\n' +
|
|
348
|
+
'generated from an anchored source, register it in\n' +
|
|
349
|
+
'`GENERATED_DOWNSTREAM` with the generator that produces it.\n',
|
|
350
|
+
);
|
|
234
351
|
}
|
|
235
|
-
process.stdout.write(
|
|
236
|
-
'\nFix: run `./scripts-run src/scripts/update_counts` for anchored positions,\n' +
|
|
237
|
-
'or correct the prose to the canonical number (never hand-type a new one).\n',
|
|
238
|
-
);
|
|
239
352
|
return 1;
|
|
240
353
|
}
|
|
241
354
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Emit the scoped-projection skill count — the number `docs/CLAIMS.md`
|
|
4
|
+
* publishes for the default install
|
|
5
|
+
* (road-to-reproducible-artefact-counts).
|
|
6
|
+
*
|
|
7
|
+
* The claim used to read "217 of 288 skills … with the counting method
|
|
8
|
+
* pinned in the benchmark doc", while that benchmark doc said 215 of 286.
|
|
9
|
+
* Both numbers were hand-typed, only one of them tracked the catalog, and
|
|
10
|
+
* the gap grew by one on every skill added because no gate could compare
|
|
11
|
+
* them. This script is the single derivation both now come from.
|
|
12
|
+
*
|
|
13
|
+
* It adds no counting mechanism of its own — that would be a second path
|
|
14
|
+
* free to disagree with the first, which is the defect, not the fix:
|
|
15
|
+
*
|
|
16
|
+
* - the file set is `update_counts.iter_skills()`, the exact walk behind
|
|
17
|
+
* the canonical `count('skills')`, so `projected + pruned === total`
|
|
18
|
+
* holds by construction;
|
|
19
|
+
* - the in/out predicate is `_lib/scoped_projection.is_pruned_under_scoped`,
|
|
20
|
+
* the exact function `install.ts` applies when it prunes a real user's
|
|
21
|
+
* tree, so the published number cannot describe a projection the
|
|
22
|
+
* installer does not perform.
|
|
23
|
+
*
|
|
24
|
+
* `runtime.active_packs` is deliberately empty here: the claim is about the
|
|
25
|
+
* DEFAULT for a new install, and a fresh machine has no overlay. An operator
|
|
26
|
+
* who activates extra packs gets more skills than the claim states — the
|
|
27
|
+
* claim is a floor, and `--json` exposes `active_packs` so a reader can see
|
|
28
|
+
* the assumption rather than infer it.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* ./scripts-run src/scripts/count_scoped_projection # "217 of 288"
|
|
32
|
+
* ./scripts-run src/scripts/count_scoped_projection --json
|
|
33
|
+
* ./scripts-run src/scripts/count_scoped_projection --projected
|
|
34
|
+
*
|
|
35
|
+
* Exit codes: 0 ok · 1 the pack registry could not be read.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import * as fs from 'node:fs';
|
|
39
|
+
import * as path from 'node:path';
|
|
40
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
scoped_projection_stats,
|
|
44
|
+
type ScopedProjectionStats,
|
|
45
|
+
} from './_lib/scoped_projection.js';
|
|
46
|
+
import { iter_skills, REPO_ROOT } from './update_counts.js';
|
|
47
|
+
|
|
48
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
49
|
+
|
|
50
|
+
export type { ScopedProjectionStats };
|
|
51
|
+
|
|
52
|
+
/** Partition the canonical skill catalog with the installer's own predicate. */
|
|
53
|
+
export function stats(package_root: string = REPO_ROOT): ScopedProjectionStats {
|
|
54
|
+
return scoped_projection_stats(package_root, iter_skills());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
58
|
+
let s: ScopedProjectionStats;
|
|
59
|
+
try {
|
|
60
|
+
s = stats();
|
|
61
|
+
} catch (err) {
|
|
62
|
+
process.stderr.write(
|
|
63
|
+
`❌ scoped-projection count unavailable: ${(err as Error).message}\n` +
|
|
64
|
+
' (src/config/discovery/packs.yml must exist and parse to a list)\n',
|
|
65
|
+
);
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (argv.includes('--json')) {
|
|
70
|
+
process.stdout.write(`${JSON.stringify(s, null, 2)}\n`);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
if (argv.includes('--projected')) {
|
|
74
|
+
process.stdout.write(`${s.projected}\n`);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
if (argv.includes('--total')) {
|
|
78
|
+
process.stdout.write(`${s.total}\n`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
process.stdout.write(`${s.projected} of ${s.total}\n`);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _isCliEntry(): boolean {
|
|
86
|
+
if (process.argv[1] === undefined) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
90
|
+
if (import.meta.url === argvUrl) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
95
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
96
|
+
return here === argv;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
103
|
+
process.exit(main(process.argv.slice(2)));
|
|
104
|
+
}
|