@event4u/agent-config 10.2.0 → 10.4.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 +168 -256
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/reference-repo.md +62 -0
- package/dist/agent-src/commands/analyze.md +2 -1
- package/dist/agent-src/commands/design-system/capture.md +44 -0
- package/dist/agent-src/commands/design-system/generate.md +53 -0
- package/dist/agent-src/commands/design-system/import.md +63 -0
- package/dist/agent-src/commands/design-system.md +66 -0
- package/dist/agent-src/commands/roadmap/create.md +4 -1
- package/dist/agent-src/personas/advisors/first-principles.md +6 -0
- package/dist/agent-src/rules/active-remediation.md +53 -2
- package/dist/agent-src/rules/code-provenance.md +66 -4
- package/dist/agent-src/rules/content-quoting-floor.md +6 -0
- package/dist/agent-src/rules/design-fidelity.md +49 -3
- package/dist/agent-src/skills/design-review/references/verification-automation.md +23 -1
- package/dist/agent-src/skills/design-system-capture/SKILL.md +9 -0
- package/dist/agent-src/skills/design-system-capture/references/design-system-json.md +70 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +27 -3
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +30 -0
- package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +11 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +8 -8
- package/dist/agent-src/skills/skill-writing/SKILL.md +4 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/persona.md +26 -0
- package/dist/agent-src/templates/roadmaps.md +27 -7
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +4 -1
- package/dist/agent-src/templates/skill.md +35 -1
- package/dist/cli/commands/commands.candidates.test.js +257 -0
- package/dist/cli/commands/commands.candidates.test.js.map +1 -0
- package/dist/cli/commands/commands.js +199 -0
- package/dist/cli/commands/commands.js.map +1 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/registry.js +1 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-E4RUZKXB.js → chunk-FV34EGEX.js} +1 -1
- package/dist/cli-delegate/chunk-RLN73PR5.js +157 -0
- package/dist/cli-delegate/{chunk-I5EAFRII.js → chunk-ZPSXJTLI.js} +2 -154
- package/dist/cli-delegate/cmd_conformance.js +672 -214
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +8 -6
- package/dist/cli-delegate/cmd_session_recycle.js +5 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +127 -22
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +9 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +8 -4
- package/dist/hooks/dispatch.js +1056 -411
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1103 -593
- package/dist/router.json +1 -1
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +54 -0
- package/docs/guidelines/design-antipatterns.md +79 -10
- package/docs/guidelines/design-fidelity-mechanics.md +120 -2
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +4 -1
- package/src/config/gate-coverage.yml +65 -10
- package/src/config/gate-violation-baselines.json +6 -0
- package/src/config/review-prompt-binding-baseline.json +19 -0
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/scripts/_dispatch.bash +1 -0
- package/src/scripts/_lib/design_system_import.ts +987 -0
- package/src/scripts/ai_council/clients.ts +15 -1
- package/src/scripts/check_ci_local_parity.ts +60 -6
- package/src/scripts/check_review_prompt_binding.ts +553 -0
- package/src/scripts/check_suppression_hygiene.ts +19 -3
- package/src/scripts/design_slop_rules.test.ts +37 -6
- package/src/scripts/design_slop_rules.ts +150 -22
- package/src/scripts/design_system_import.ts +264 -0
- package/src/scripts/dispatch_r2_reviewer.ts +15 -1
- package/src/scripts/hook_manifest.yaml +71 -2
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +10 -0
- package/src/scripts/hooks/host_semantics.ts +2 -0
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +212 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +723 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +39 -2
- package/src/scripts/hooks/ui_route_nudge_hook.ts +55 -2
- package/src/scripts/lint_design_antipattern_parity.ts +201 -0
- package/src/scripts/lint_harvest_provenance.ts +483 -0
- package/src/scripts/lint_hook_manifest.ts +2 -0
- package/src/scripts/lint_rule_skill_pack_reach.ts +194 -13
- package/src/scripts/lint_skill_router_head.ts +254 -0
- package/src/scripts/report_consultation_rate.ts +126 -1
- package/src/scripts/run_skill_evals.ts +10 -5
- package/src/scripts/schemas/persona.schema.json +8 -0
- package/src/scripts/schemas/provenance-harvest.schema.json +39 -0
- package/src/scripts/sweep_dead_scan_roots.ts +51 -0
- package/tests/fixtures/hooks/subagent_start.json +7 -0
- package/tests/fixtures/hooks/subagent_stop.json +8 -0
|
@@ -129,6 +129,7 @@ import { isSafeTranscriptPath } from './end_review_nudge_hook.js';
|
|
|
129
129
|
import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
|
|
130
130
|
import { readHookStdin } from './hook_stdin.js';
|
|
131
131
|
import { atomic_write_json, is_replay_mode } from './state_io.js';
|
|
132
|
+
import { openRecordStats } from './subagent_ledger_hook.js';
|
|
132
133
|
|
|
133
134
|
/** Dispatcher-internal block code. Pinned to 1 by `concern_block_exit_parity`. */
|
|
134
135
|
const EXIT_BLOCK = 1;
|
|
@@ -842,6 +843,39 @@ export function main(): number {
|
|
|
842
843
|
|
|
843
844
|
const workspaceRoot = str(envelope['workspace_root'] as JsonValue | undefined) || process.cwd();
|
|
844
845
|
|
|
846
|
+
// Layer 1b — an open subagent dispatch is an EXPLICIT allow
|
|
847
|
+
// (road-to-subagent-lifecycle-integrity Phase 3 Step 2).
|
|
848
|
+
//
|
|
849
|
+
// A turn waiting on an async subagent has, by construction, outstanding
|
|
850
|
+
// work the model cannot finish in this turn. Refusing it is the upstream
|
|
851
|
+
// Stop-hook x async-subagent loop shape (anthropics/claude-code#55754):
|
|
852
|
+
// the gate refuses, the model still cannot proceed because the dispatch
|
|
853
|
+
// has not returned, and the refusal repeats.
|
|
854
|
+
//
|
|
855
|
+
// This is an allow path and can never become a deny path — it only ever
|
|
856
|
+
// lets a turn END that would otherwise have been refused. It reads the
|
|
857
|
+
// Phase-1 ledger; a ledger that is absent, empty or unreadable yields zero
|
|
858
|
+
// open records and changes nothing, so the gate degrades to exactly its
|
|
859
|
+
// previous behaviour rather than failing open in the dangerous direction.
|
|
860
|
+
//
|
|
861
|
+
// R2 round 2, finding 2: it used to be an early `return EXIT_ALLOW`, which
|
|
862
|
+
// suppressed ALL FOUR detectors. A pending dispatch explains a promissory
|
|
863
|
+
// closing (A) and an unsettled completion claim (D); it explains nothing
|
|
864
|
+
// about a language mismatch (B) or an unverified edit (C), and silencing
|
|
865
|
+
// those was scope Step 2 never asked for. Applied per detector below.
|
|
866
|
+
//
|
|
867
|
+
// R2 round 2, finding 1: the count is TTL-filtered inside
|
|
868
|
+
// `openRecordStats`. A leaked record from a dispatch that never returned
|
|
869
|
+
// would otherwise read as "a dispatch is open" forever and, because this
|
|
870
|
+
// branch is an ALLOW, disable those two detectors indefinitely with no
|
|
871
|
+
// signal — the ledger's own leak inherited with the opposite polarity.
|
|
872
|
+
let dispatchOpen = false;
|
|
873
|
+
try {
|
|
874
|
+
dispatchOpen = openRecordStats(workspaceRoot).open_count > 0;
|
|
875
|
+
} catch {
|
|
876
|
+
// An unreadable ledger is not a reason to change the verdict.
|
|
877
|
+
}
|
|
878
|
+
|
|
845
879
|
const transcriptPath = str(
|
|
846
880
|
(payload['transcript_path'] ?? payload['transcriptPath']) as JsonValue | undefined,
|
|
847
881
|
);
|
|
@@ -881,7 +915,10 @@ export function main(): number {
|
|
|
881
915
|
// second, configurable notion of warranted layered on top of it.
|
|
882
916
|
const findings: Finding[] = [];
|
|
883
917
|
for (const f of [
|
|
884
|
-
|
|
918
|
+
// A and D are the completion-adjacent pair a pending dispatch excuses
|
|
919
|
+
// (Phase 3 Step 2, narrowed by R2 round 2 finding 2). B and C are not
|
|
920
|
+
// excused by anything about a dispatch and run unchanged.
|
|
921
|
+
dispatchOpen ? null : detectPromissory(lastAssistant),
|
|
885
922
|
detectLanguage(lastAssistant, readLanguagePin(workspaceRoot)),
|
|
886
923
|
detectUnverifiedEdit(toolCalls),
|
|
887
924
|
// Round 7 § Phase 1 — detector D, in the same unconditional list as the
|
|
@@ -891,7 +928,7 @@ export function main(): number {
|
|
|
891
928
|
// gating is where the comment above says gating belongs — inside the
|
|
892
929
|
// detector: no CI observed, or a settled read, or no completion claim
|
|
893
930
|
// ⇒ no finding.
|
|
894
|
-
detectCompletionClaim(lastAssistant, readCiSettled(workspaceRoot)),
|
|
931
|
+
dispatchOpen ? null : detectCompletionClaim(lastAssistant, readCiSettled(workspaceRoot)),
|
|
895
932
|
]) {
|
|
896
933
|
if (f) findings.push(f);
|
|
897
934
|
}
|
|
@@ -123,6 +123,45 @@ export function isUiWrite(event: ToolEvent): boolean {
|
|
|
123
123
|
return isUiPath(event.file) || isUiTreePath(event.file);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* The two handover shapes `design-fidelity` routes on a FILE rather than on a
|
|
128
|
+
* word: its `file_pattern: *design.html` and its
|
|
129
|
+
* `path_prefix: .claude/design-system/`. Reading one is the agent reaching
|
|
130
|
+
* rung 1 of the data-basis ladder before it writes.
|
|
131
|
+
*
|
|
132
|
+
* A SHARED PREDICATE, NOT A DECISION INPUT HERE. `decide` does not branch on
|
|
133
|
+
* it: the nudge's behaviour is unchanged by an artifact read, and the phase
|
|
134
|
+
* that introduced this states `nothing behavioural`. The one consumer is
|
|
135
|
+
* `report_consultation_rate`, which measures over transcripts. Keeping the
|
|
136
|
+
* predicate beside `isUiWrite` / `isConsultation` is what stops the analyzer
|
|
137
|
+
* and the nudge from classifying the same event differently.
|
|
138
|
+
*
|
|
139
|
+
* THIS IS A COPY OF THE RULE'S TRIGGERS, NOT A READ OF THEM — same honesty
|
|
140
|
+
* boundary the header states for `ui_surface.ts`: nothing here parses
|
|
141
|
+
* `src/rules/*.md`. `ui_route_nudge_artifact_read.test.ts` pins the two against
|
|
142
|
+
* the rule's own frontmatter so the copy cannot drift silently.
|
|
143
|
+
*
|
|
144
|
+
* **Not disjoint from `isConsultation` by construction** —
|
|
145
|
+
* `src/skills/design-review/references/design.html` satisfies both. Callers
|
|
146
|
+
* must evaluate the two independently rather than branching on the first hit;
|
|
147
|
+
* an early return on this predicate silently removes such an event from the
|
|
148
|
+
* consultation numerator.
|
|
149
|
+
*
|
|
150
|
+
* **Known over-count, not fixed here.** A `Grep` / `Glob` whose `path` argument
|
|
151
|
+
* happens to be the artifact satisfies this predicate although nothing was
|
|
152
|
+
* read — the same search-is-not-a-read class `isConsultation` was narrowed to
|
|
153
|
+
* exclude. It is not excluded here because `ToolEvent` carries no tool name,
|
|
154
|
+
* and adding one would change the shared shape `isConsultation` and `isUiWrite`
|
|
155
|
+
* are measured through, moving a published rate this branch does not own. The
|
|
156
|
+
* consequence is stated where it matters: the rate is a CEILING on
|
|
157
|
+
* read-before-write, and `report_consultation_rate`'s output says so.
|
|
158
|
+
*/
|
|
159
|
+
export function isArtifactRead(event: ToolEvent): boolean {
|
|
160
|
+
if (event.isWrite || !event.file) return false;
|
|
161
|
+
const normalized = event.file.replace(/\\/g, '/').toLowerCase();
|
|
162
|
+
return normalized.endsWith('design.html') || normalized.includes('.claude/design-system/');
|
|
163
|
+
}
|
|
164
|
+
|
|
126
165
|
export interface SessionState {
|
|
127
166
|
consulted: boolean;
|
|
128
167
|
nudges: number;
|
|
@@ -203,8 +242,22 @@ export function decide(
|
|
|
203
242
|
return { state: { ...state, nudges: state.nudges + 1 }, warn: true };
|
|
204
243
|
}
|
|
205
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Did anything worth persisting change?
|
|
247
|
+
*
|
|
248
|
+
* Enumerated per field rather than deep-compared. **A new field must be added
|
|
249
|
+
* here or it does not persist** — a review on this branch caught exactly that
|
|
250
|
+
* shape: two session fields were computed on every event and written on none,
|
|
251
|
+
* because the guard tested the two older fields by name. Those fields are gone
|
|
252
|
+
* (the measurement lives in the analyzer, one implementation instead of two),
|
|
253
|
+
* but the guard stays enumerated and this note stays with it.
|
|
254
|
+
*/
|
|
255
|
+
export function stateChanged(before: SessionState, after: SessionState): boolean {
|
|
256
|
+
return before.consulted !== after.consulted || before.nudges !== after.nudges;
|
|
257
|
+
}
|
|
258
|
+
|
|
206
259
|
export function nudgeReason(file: string): string {
|
|
207
|
-
return `Non-trivial UI write to ${file} with no design consultation this session. Run \`existing-ui-audit\` first, then the fe-design loop (brief → build → review). Skip only if this is ui-trivial: <=1 file, <=5 lines, no new component, no new state, no new dependency.`;
|
|
260
|
+
return `Non-trivial UI write to ${file} with no design consultation this session. Run \`existing-ui-audit\` first, then the fe-design loop (brief → inventory, if an artifact was provided → build → review). Skip only if this is ui-trivial: <=1 file, <=5 lines, no new component, no new state, no new dependency.`;
|
|
208
261
|
}
|
|
209
262
|
|
|
210
263
|
export function main(): number {
|
|
@@ -230,7 +283,7 @@ export function main(): number {
|
|
|
230
283
|
|
|
231
284
|
const before = readState(root, session);
|
|
232
285
|
const { state, warn } = decide(event, before);
|
|
233
|
-
if (
|
|
286
|
+
if (stateChanged(before, state)) {
|
|
234
287
|
writeState(root, session, state);
|
|
235
288
|
}
|
|
236
289
|
if (!warn) return EXIT_ALLOW;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate: catalog ↔ slop-rule parity (road-to-design-detector-evidence Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* `src/scripts/design_slop_rules.ts` states in its header that every rule cites
|
|
5
|
+
* its catalog id "so prose ↔ rule stay traceable". Nothing enforced that. The
|
|
6
|
+
* unit test validates the *shape* `/^[A-Z]+\d+$/` and never opens
|
|
7
|
+
* `docs/guidelines/design-antipatterns.md`, so a rule citing `Z99` passed, and a
|
|
8
|
+
* catalog entry could silently lose or gain a detector with no signal anywhere.
|
|
9
|
+
* The tree happened to be consistent; the defect was that consistency was
|
|
10
|
+
* unguarded, not that it was absent.
|
|
11
|
+
*
|
|
12
|
+
* This gate makes the claim true, in both directions:
|
|
13
|
+
*
|
|
14
|
+
* A. every non-`Q*` catalog entry appears exactly once in § Detector status
|
|
15
|
+
* B. every § Detector status row names a catalog entry that exists
|
|
16
|
+
* C. the set of `backed` rows equals the registry's `catalogId` set
|
|
17
|
+
* D. every status is one of the five allowed values
|
|
18
|
+
* E. every non-`backed` row carries a reason
|
|
19
|
+
*
|
|
20
|
+
* `Q*` entries are out of scope by construction: they are objective floors owned
|
|
21
|
+
* by `lint_design_quality`, not aesthetic tells, and the status table says so.
|
|
22
|
+
*
|
|
23
|
+
* Pure core (`parityFindings`) takes both documents as text, so the failure
|
|
24
|
+
* paths are unit-testable without the filesystem. Exit 0 = clean, 1 = at least
|
|
25
|
+
* one finding.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import * as fs from 'node:fs';
|
|
29
|
+
import * as path from 'node:path';
|
|
30
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
|
+
|
|
32
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
33
|
+
|
|
34
|
+
const QUIET = process.argv.includes('--quiet');
|
|
35
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
36
|
+
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
37
|
+
const CATALOG = path.join(REPO, 'docs', 'guidelines', 'design-antipatterns.md');
|
|
38
|
+
const REGISTRY = path.join(REPO, 'src', 'scripts', 'design_slop_rules.ts');
|
|
39
|
+
|
|
40
|
+
/** The five statuses § Detector status may use. */
|
|
41
|
+
export const STATUSES = ['backed', 'floor', 'judgment-only', 'deferred', 'candidate'] as const;
|
|
42
|
+
export type Status = (typeof STATUSES)[number];
|
|
43
|
+
|
|
44
|
+
export interface ParityFinding {
|
|
45
|
+
kind: string;
|
|
46
|
+
msg: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ENTRY_ROW = /^\|\s*((?:V|C|T|L|M|CP)\d+)\s*\|/gm;
|
|
50
|
+
const STATUS_ROW = /^\|\s*((?:V|C|T|L|M|CP)\d+)\s*\|\s*([a-z-]*)\s*\|([^|]*)\|/gm;
|
|
51
|
+
const STATUS_SECTION = /^## Detector status$([\s\S]*?)(?=^## )/m;
|
|
52
|
+
const CATALOG_ID = /^\s+catalogId:\s*"([A-Z]+\d+)"/gm;
|
|
53
|
+
|
|
54
|
+
/** Catalog entry ids outside the § Detector status section. */
|
|
55
|
+
export function catalogEntries(catalogMd: string): string[] {
|
|
56
|
+
const section = STATUS_SECTION.exec(catalogMd)?.[0] ?? '';
|
|
57
|
+
const outside = section === '' ? catalogMd : catalogMd.replace(section, '');
|
|
58
|
+
return [...outside.matchAll(ENTRY_ROW)].map((m) => m[1] as string);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `id -> {status, note}` parsed from the § Detector status table. */
|
|
62
|
+
export function statusRows(catalogMd: string): Map<string, { status: string; note: string }> {
|
|
63
|
+
const section = STATUS_SECTION.exec(catalogMd)?.[1] ?? '';
|
|
64
|
+
const out = new Map<string, { status: string; note: string }>();
|
|
65
|
+
for (const m of section.matchAll(STATUS_ROW)) {
|
|
66
|
+
out.set(m[1] as string, { status: (m[2] ?? '').trim(), note: (m[3] ?? '').trim() });
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** `catalogId` values declared by the rule registry. */
|
|
72
|
+
export function registryCatalogIds(registryTs: string): string[] {
|
|
73
|
+
return [...registryTs.matchAll(CATALOG_ID)].map((m) => m[1] as string);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Pure invariant check over both documents. */
|
|
77
|
+
export function parityFindings(catalogMd: string, registryTs: string): ParityFinding[] {
|
|
78
|
+
const out: ParityFinding[] = [];
|
|
79
|
+
|
|
80
|
+
if (STATUS_SECTION.exec(catalogMd) === null) {
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
kind: 'no-status-section',
|
|
84
|
+
msg: 'design-antipatterns.md has no `## Detector status` section — the parity claim has nothing to check against',
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const entries = catalogEntries(catalogMd);
|
|
90
|
+
const seen = new Set<string>();
|
|
91
|
+
for (const id of entries) {
|
|
92
|
+
if (seen.has(id)) out.push({ kind: 'duplicate-entry', msg: `${id}: appears more than once in the catalog tables` });
|
|
93
|
+
seen.add(id);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const rows = statusRows(catalogMd);
|
|
97
|
+
|
|
98
|
+
// A — every catalog entry is classified.
|
|
99
|
+
for (const id of seen) {
|
|
100
|
+
if (!rows.has(id)) {
|
|
101
|
+
out.push({ kind: 'unclassified', msg: `${id}: catalog entry missing from § Detector status` });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// B — every classified row names a real entry.
|
|
105
|
+
for (const id of rows.keys()) {
|
|
106
|
+
if (!seen.has(id)) {
|
|
107
|
+
out.push({ kind: 'orphan-status', msg: `${id}: § Detector status row names an entry that no catalog table defines` });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// D + E — status vocabulary and reasons.
|
|
111
|
+
for (const [id, row] of rows) {
|
|
112
|
+
if (!(STATUSES as readonly string[]).includes(row.status)) {
|
|
113
|
+
out.push({
|
|
114
|
+
kind: 'bad-status',
|
|
115
|
+
msg: `${id}: status '${row.status || '(empty)'}' is not one of ${STATUSES.join(', ')}`,
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (row.status !== 'backed' && row.note === '') {
|
|
120
|
+
out.push({ kind: 'missing-reason', msg: `${id}: status '${row.status}' carries no reason` });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// C — backed set equals the registry set, both directions.
|
|
125
|
+
const backed = new Set([...rows].filter(([, r]) => r.status === 'backed').map(([id]) => id));
|
|
126
|
+
const registry = new Set(registryCatalogIds(registryTs));
|
|
127
|
+
for (const id of backed) {
|
|
128
|
+
if (!registry.has(id)) {
|
|
129
|
+
out.push({ kind: 'backed-without-rule', msg: `${id}: marked backed but no registry rule declares that catalogId` });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const id of registry) {
|
|
133
|
+
if (!backed.has(id)) {
|
|
134
|
+
const known = rows.get(id);
|
|
135
|
+
out.push({
|
|
136
|
+
kind: 'rule-without-backed',
|
|
137
|
+
msg: known
|
|
138
|
+
? `${id}: a registry rule declares this catalogId but § Detector status marks it '${known.status}'`
|
|
139
|
+
: `${id}: a registry rule declares this catalogId and no catalog entry carries it`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _print(s: string): void {
|
|
148
|
+
if (!QUIET) process.stdout.write(`${s}\n`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function main(): number {
|
|
152
|
+
try {
|
|
153
|
+
assertWatchlistResolves({
|
|
154
|
+
gate: 'lint_design_antipattern_parity',
|
|
155
|
+
candidates: [path.relative(REPO, CATALOG), path.relative(REPO, REGISTRY)],
|
|
156
|
+
repoRoot: REPO,
|
|
157
|
+
});
|
|
158
|
+
} catch (exc) {
|
|
159
|
+
if (exc instanceof DeadScopeError) {
|
|
160
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
throw exc;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const catalogMd = fs.readFileSync(CATALOG, 'utf-8');
|
|
167
|
+
const registryTs = fs.readFileSync(REGISTRY, 'utf-8');
|
|
168
|
+
const findings = parityFindings(catalogMd, registryTs);
|
|
169
|
+
|
|
170
|
+
const entries = catalogEntries(catalogMd).length;
|
|
171
|
+
const backed = [...statusRows(catalogMd).values()].filter((r) => r.status === 'backed').length;
|
|
172
|
+
|
|
173
|
+
if (findings.length === 0) {
|
|
174
|
+
_print(`✅ catalog ↔ rule parity OK — ${entries} entries classified, ${backed} detector-backed`);
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
_print(`❌ catalog ↔ rule parity — ${findings.length} finding(s):`);
|
|
178
|
+
for (const f of findings) _print(` - ${f.msg}`);
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function _isCliEntry(): boolean {
|
|
183
|
+
if (process.argv[1] === undefined) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
187
|
+
if (import.meta.url === argvUrl) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
192
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
193
|
+
return here === argv;
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (_isCliEntry()) {
|
|
200
|
+
process.exit(main());
|
|
201
|
+
}
|