@event4u/agent-config 14.17.0 → 14.19.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +207 -77
- package/README.md +72 -150
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +2 -2
- package/dist/agent-src/commands/pr/merge.md +35 -44
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
- package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +3 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
- package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +749 -669
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.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 -38
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +2764 -2439
- package/dist/install/install.mjs +588 -527
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1381 -1138
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/shared/settingsConsent.js +1 -1
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/conformance-claim-baseline.json +9 -0
- package/src/config/gate-coverage.yml +170 -0
- package/src/config/gate-violation-baselines.json +4 -4
- package/src/config/release-gate-locality.yml +141 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/release_highlights.ts +265 -1
- package/src/scripts/_lib/release_material.ts +129 -0
- package/src/scripts/_lib/repeated_run.ts +77 -0
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/_lib/unified_diff.ts +121 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +239 -6
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_installed_hooks_fresh.ts +381 -0
- package/src/scripts/check_release_highlights.ts +99 -14
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_suppression_hygiene.ts +11 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/code_graph/build.ts +230 -33
- package/src/scripts/code_graph/extract.ts +168 -33
- package/src/scripts/code_graph/types.ts +37 -2
- package/src/scripts/code_graph/validate.ts +12 -1
- package/src/scripts/conformance_scan.ts +45 -4
- package/src/scripts/detect_silent_catch.ts +403 -0
- package/src/scripts/detect_verification_tampering.ts +538 -0
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +228 -18
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +47 -30
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/block_no_verify.ts +3 -3
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/evidence_independence.ts +21 -2
- package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/push_settle_hook.ts +250 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install-hooks.sh +134 -1
- package/src/scripts/install.ts +53 -82
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/language_mirror_hook.ts +27 -5
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +31 -143
- package/src/scripts/release_drill.ts +71 -1
- package/src/scripts/release_publication.ts +243 -3
- package/src/scripts/release_verify.ts +220 -0
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +6 -1
- package/src/scripts/schemas/skill.schema.json +12 -2
- package/src/scripts/self_review_gate.ts +10 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/src/shared/settingsConsent.ts +1 -1
|
@@ -63,28 +63,34 @@ interface LapsedBaseline {
|
|
|
63
63
|
contracts: string[];
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
// Keyed by root: an unkeyed cache returns the REPO baseline for a fixture root
|
|
67
|
+
// whenever the repo was read first, which silently judges a fixture against
|
|
68
|
+
// production data and is invisible in the result.
|
|
69
|
+
const _baselineCache = new Map<string, Set<string>>();
|
|
67
70
|
|
|
68
71
|
export function loadLapsedBaseline(root: string = ROOT): Set<string> {
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
const hit = _baselineCache.get(root);
|
|
73
|
+
if (hit !== undefined) {
|
|
74
|
+
return hit;
|
|
71
75
|
}
|
|
72
76
|
const p = path.join(root, BASELINE_REL);
|
|
73
77
|
if (!fs.existsSync(p)) {
|
|
74
78
|
// The baseline reaching zero is the SUCCESS state: the file is deleted
|
|
75
79
|
// in the same change that flips the severity. An absent file therefore
|
|
76
80
|
// means "no inherited debt", i.e. every lapse is fresh and errors.
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
const empty = new Set<string>();
|
|
82
|
+
_baselineCache.set(root, empty);
|
|
83
|
+
return empty;
|
|
79
84
|
}
|
|
80
85
|
const parsed = JSON.parse(fs.readFileSync(p, 'utf-8')) as LapsedBaseline;
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
const loaded = new Set(parsed.contracts);
|
|
87
|
+
_baselineCache.set(root, loaded);
|
|
88
|
+
return loaded;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
/** Test seam — the module-level cache would otherwise outlive a fixture. */
|
|
86
92
|
export function _resetLapsedBaseline(): void {
|
|
87
|
-
_baselineCache
|
|
93
|
+
_baselineCache.clear();
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
/**
|
|
@@ -115,12 +121,43 @@ const SUPERSEDED_RE = /^superseded-by:\s*\S+\s*$/m;
|
|
|
115
121
|
|
|
116
122
|
const MAX_REVIEW_WINDOW_DAYS = 90;
|
|
117
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Forward horizon for the upcoming-lapse report.
|
|
126
|
+
*
|
|
127
|
+
* The gate reports a contract on the day it lapses, which is the day it is
|
|
128
|
+
* already too late to decide anything: a FRESH lapse errors, and every pull
|
|
129
|
+
* request in the repository reds until someone edits a contract under time
|
|
130
|
+
* pressure. The horizon names the set that is ABOUT to enter that branch.
|
|
131
|
+
*
|
|
132
|
+
* The number is bounded on both sides: too wide and the report names most of
|
|
133
|
+
* the forward-dated corpus on every run, which is the same as naming none; too
|
|
134
|
+
* narrow and it arrives after the decision needed to be made.
|
|
135
|
+
*/
|
|
136
|
+
const HORIZON_DAYS = 14;
|
|
137
|
+
|
|
118
138
|
interface Violation {
|
|
119
139
|
file: string;
|
|
120
140
|
reason: string;
|
|
121
141
|
severity: 'error' | 'warning';
|
|
122
142
|
}
|
|
123
143
|
|
|
144
|
+
/**
|
|
145
|
+
* A contract whose window closes within {@link HORIZON_DAYS} and which is NOT
|
|
146
|
+
* in the frozen baseline — i.e. one that will enter the ERROR branch, not the
|
|
147
|
+
* inherited-warning one.
|
|
148
|
+
*
|
|
149
|
+
* Deliberately NOT a {@link Violation}: it carries no severity and never moves
|
|
150
|
+
* the exit code. A third severity class printed into the same stream as the 84
|
|
151
|
+
* standing warnings is the way this work changes nothing (risk 1 on the
|
|
152
|
+
* roadmap's own register), so the horizon is a separate, clearly-labelled
|
|
153
|
+
* section and the exit code stays a function of real violations alone.
|
|
154
|
+
*/
|
|
155
|
+
interface Upcoming {
|
|
156
|
+
file: string;
|
|
157
|
+
date: string;
|
|
158
|
+
daysOut: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
124
161
|
function _exists(p: string): boolean {
|
|
125
162
|
try {
|
|
126
163
|
fs.statSync(p);
|
|
@@ -184,7 +221,7 @@ function _ordinalToISO(ordinal: number): string {
|
|
|
184
221
|
return `${y}-${m}-${d}`;
|
|
185
222
|
}
|
|
186
223
|
|
|
187
|
-
function check_one(p: string, todayOrdinal: number): Violation[] {
|
|
224
|
+
function check_one(p: string, todayOrdinal: number, root: string = ROOT): Violation[] {
|
|
188
225
|
const fm = read_frontmatter(p);
|
|
189
226
|
if (fm === null) {
|
|
190
227
|
return [];
|
|
@@ -199,7 +236,7 @@ function check_one(p: string, todayOrdinal: number): Violation[] {
|
|
|
199
236
|
['superseded-by', SUPERSEDED_RE.test(fm)],
|
|
200
237
|
];
|
|
201
238
|
const setMarkers = markers.filter(([, present]) => present).map(([name]) => name);
|
|
202
|
-
const rel = _relPosix(p,
|
|
239
|
+
const rel = _relPosix(p, root);
|
|
203
240
|
if (setMarkers.length === 0) {
|
|
204
241
|
return [
|
|
205
242
|
{
|
|
@@ -231,7 +268,7 @@ function check_one(p: string, todayOrdinal: number): Violation[] {
|
|
|
231
268
|
// bound because a lapsed date can never also exceed the forward window,
|
|
232
269
|
// so the order is a readability choice rather than a precedence one.
|
|
233
270
|
if (reviewOrdinal < todayOrdinal) {
|
|
234
|
-
const inherited = loadLapsedBaseline().has(rel);
|
|
271
|
+
const inherited = loadLapsedBaseline(root).has(rel);
|
|
235
272
|
const age = todayOrdinal - reviewOrdinal;
|
|
236
273
|
return [
|
|
237
274
|
{
|
|
@@ -263,6 +300,34 @@ function check_one(p: string, todayOrdinal: number): Violation[] {
|
|
|
263
300
|
return [];
|
|
264
301
|
}
|
|
265
302
|
|
|
303
|
+
/**
|
|
304
|
+
* The upcoming-lapse finding for one contract, or `null`.
|
|
305
|
+
*
|
|
306
|
+
* Baselined contracts are excluded EXPLICITLY rather than by relying on the
|
|
307
|
+
* fact that a baselined contract is already lapsed and so cannot also carry a
|
|
308
|
+
* future date. That implication holds today and is not a property anything
|
|
309
|
+
* enforces; a guard that is merely vacuous cannot be distinguished from a
|
|
310
|
+
* guard that is absent, and the difference shows up the first time a baseline
|
|
311
|
+
* entry is re-dated forward.
|
|
312
|
+
*/
|
|
313
|
+
export function upcoming_one(
|
|
314
|
+
p: string,
|
|
315
|
+
todayOrdinal: number,
|
|
316
|
+
horizonDays: number = HORIZON_DAYS,
|
|
317
|
+
root: string = ROOT,
|
|
318
|
+
): Upcoming | null {
|
|
319
|
+
const fm = read_frontmatter(p);
|
|
320
|
+
if (fm === null || !/^stability:\s*beta\s*$/m.test(fm)) return null;
|
|
321
|
+
const km = KEEP_RE.exec(fm);
|
|
322
|
+
if (!km) return null;
|
|
323
|
+
const rel = _relPosix(p, root);
|
|
324
|
+
if (loadLapsedBaseline(root).has(rel)) return null;
|
|
325
|
+
const [ry, rm, rd] = _parseISODate(km[1]!);
|
|
326
|
+
const reviewOrdinal = _dateOrdinal(ry, rm, rd);
|
|
327
|
+
const daysOut = reviewOrdinal - todayOrdinal;
|
|
328
|
+
if (daysOut < 0 || daysOut > horizonDays) return null;
|
|
329
|
+
return { file: rel, date: _ordinalToISO(reviewOrdinal), daysOut };
|
|
330
|
+
}
|
|
266
331
|
|
|
267
332
|
/**
|
|
268
333
|
* STABILITY.md's own re-audit condition, as a command instead of as prose.
|
|
@@ -302,10 +367,16 @@ interface ParsedArgs {
|
|
|
302
367
|
trigger: boolean;
|
|
303
368
|
/** Fixture root, for `--self-test`. A fixture is never judged against the repo baseline. */
|
|
304
369
|
root: string | null;
|
|
370
|
+
horizon: number;
|
|
305
371
|
}
|
|
306
372
|
|
|
307
373
|
function parse_args(argv: readonly string[]): ParsedArgs {
|
|
308
|
-
const args: ParsedArgs = {
|
|
374
|
+
const args: ParsedArgs = {
|
|
375
|
+
json: false,
|
|
376
|
+
trigger: false,
|
|
377
|
+
root: null,
|
|
378
|
+
horizon: HORIZON_DAYS,
|
|
379
|
+
};
|
|
309
380
|
for (let i = 0; i < argv.length; i += 1) {
|
|
310
381
|
const arg = argv[i] as string;
|
|
311
382
|
if (arg === '--json') {
|
|
@@ -313,14 +384,27 @@ function parse_args(argv: readonly string[]): ParsedArgs {
|
|
|
313
384
|
} else if (arg === '--root') {
|
|
314
385
|
args.root = argv[i + 1] ?? null;
|
|
315
386
|
i += 1;
|
|
387
|
+
} else if (arg === '--horizon') {
|
|
388
|
+
const raw = argv[i + 1] ?? '';
|
|
389
|
+
const n = Number.parseInt(raw, 10);
|
|
390
|
+
if (!/^\d+$/.test(raw) || Number.isNaN(n)) {
|
|
391
|
+
process.stderr.write(
|
|
392
|
+
`check_beta_review_markers: error: --horizon needs a non-negative integer, got: ${raw || '(nothing)'}\n`,
|
|
393
|
+
);
|
|
394
|
+
process.exit(2);
|
|
395
|
+
}
|
|
396
|
+
args.horizon = n;
|
|
397
|
+
i += 1;
|
|
316
398
|
} else if (arg === '--trigger') {
|
|
317
399
|
args.trigger = true;
|
|
318
400
|
} else if (arg === '-h' || arg === '--help') {
|
|
319
401
|
process.stdout.write(
|
|
320
|
-
'usage: check_beta_review_markers [-h] [--json] [--trigger]\n' +
|
|
402
|
+
'usage: check_beta_review_markers [-h] [--json] [--trigger] [--horizon N]\n' +
|
|
321
403
|
' --trigger print the lapsed percentage and whether STABILITY.md\n' +
|
|
322
404
|
' re-audit condition (>= 25 %) has fired. Reports; never fails.\n' +
|
|
323
|
-
' --root DIR judge a fixture tree instead of the repo (self-test seam).\n'
|
|
405
|
+
' --root DIR judge a fixture tree instead of the repo (self-test seam).\n' +
|
|
406
|
+
` --horizon N days ahead to report upcoming fresh lapses (default ${String(HORIZON_DAYS)}).\n` +
|
|
407
|
+
' Advisory: never changes the exit code.\n',
|
|
324
408
|
);
|
|
325
409
|
process.exit(0);
|
|
326
410
|
} else {
|
|
@@ -400,11 +484,15 @@ function main(): number {
|
|
|
400
484
|
const scanRoot = args.root ?? ROOT;
|
|
401
485
|
const contracts = _globMdSorted(path.join(scanRoot, CONTRACTS_DIR));
|
|
402
486
|
let betaContracts = 0;
|
|
487
|
+
const upcoming: Upcoming[] = [];
|
|
403
488
|
for (const p of contracts) {
|
|
404
489
|
const fm = read_frontmatter(p);
|
|
405
490
|
if (fm !== null && /^stability:\s*beta\s*$/m.test(fm)) betaContracts += 1;
|
|
406
|
-
violations.push(...check_one(p, todayOrdinal));
|
|
491
|
+
violations.push(...check_one(p, todayOrdinal, scanRoot));
|
|
492
|
+
const due = upcoming_one(p, todayOrdinal, args.horizon, scanRoot);
|
|
493
|
+
if (due !== null) upcoming.push(due);
|
|
407
494
|
}
|
|
495
|
+
upcoming.sort((a, b) => a.daysOut - b.daysOut || a.file.localeCompare(b.file));
|
|
408
496
|
// Count every contract read, not the `stability: beta` subset: over a moved
|
|
409
497
|
// `docs/contracts/` "no beta contracts" and "no contracts at all" produce
|
|
410
498
|
// the same clean line, and only the second is a dead gate. Exit 1 is the
|
|
@@ -446,7 +534,9 @@ function main(): number {
|
|
|
446
534
|
return 0;
|
|
447
535
|
}
|
|
448
536
|
if (args.json) {
|
|
449
|
-
process.stdout.write(
|
|
537
|
+
process.stdout.write(
|
|
538
|
+
JSON.stringify({ violations, upcoming, horizonDays: args.horizon }, null, 2) + '\n',
|
|
539
|
+
);
|
|
450
540
|
} else {
|
|
451
541
|
if (violations.length === 0) {
|
|
452
542
|
process.stdout.write('✅ All beta contracts carry a valid review marker.\n');
|
|
@@ -455,7 +545,39 @@ function main(): number {
|
|
|
455
545
|
const icon = v.severity === 'error' ? '❌' : '⚠️ ';
|
|
456
546
|
process.stdout.write(`${icon} ${v.file}: ${v.reason}\n`);
|
|
457
547
|
}
|
|
458
|
-
|
|
548
|
+
// Both counts, and the relationship between them, because a
|
|
549
|
+
// violation count alone cannot answer "how many contracts is this".
|
|
550
|
+
// Today they are equal — check_one returns at most one finding per
|
|
551
|
+
// contract — and printing only one of them hides the fact that it
|
|
552
|
+
// is the size of the decision, not just the size of the output.
|
|
553
|
+
const distinct = new Set(violations.map((v) => v.file)).size;
|
|
554
|
+
const baselineSize = loadLapsedBaseline(scanRoot).size;
|
|
555
|
+
const reportedInherited = violations.filter((v) =>
|
|
556
|
+
loadLapsedBaseline(scanRoot).has(v.file),
|
|
557
|
+
).length;
|
|
558
|
+
process.stdout.write(
|
|
559
|
+
`\n${String(violations.length)} violation(s) across ` +
|
|
560
|
+
`${String(distinct)} distinct contract(s) — one line per finding, so the ` +
|
|
561
|
+
'two differ only where a contract carries more than one.\n',
|
|
562
|
+
);
|
|
563
|
+
process.stdout.write(
|
|
564
|
+
`Frozen baseline: ${String(baselineSize)} entries · ` +
|
|
565
|
+
`${String(reportedInherited)} still reported as lapsed · ` +
|
|
566
|
+
`${String(baselineSize - reportedInherited)} inert ` +
|
|
567
|
+
'(left the lapsed set by promotion, supersession, or re-dating).\n',
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
if (upcoming.length > 0) {
|
|
571
|
+
process.stdout.write(
|
|
572
|
+
`\nUpcoming FRESH lapses within ${String(args.horizon)} day(s) — advisory, ` +
|
|
573
|
+
'exit code unchanged. These are absent from the frozen baseline, so each\n' +
|
|
574
|
+
'becomes an ERROR on its date rather than an inherited warning:\n',
|
|
575
|
+
);
|
|
576
|
+
for (const u of upcoming) {
|
|
577
|
+
process.stdout.write(
|
|
578
|
+
` ${u.file}: keep-beta-until=${u.date} in ${String(u.daysOut)} day(s) [fresh]\n`,
|
|
579
|
+
);
|
|
580
|
+
}
|
|
459
581
|
}
|
|
460
582
|
}
|
|
461
583
|
return violations.some((v) => v.severity === 'error') ? 1 : 0;
|
|
@@ -495,9 +617,11 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
|
495
617
|
|
|
496
618
|
export {
|
|
497
619
|
type Violation,
|
|
620
|
+
type Upcoming,
|
|
498
621
|
ROOT,
|
|
499
622
|
CONTRACTS_DIR,
|
|
500
623
|
MAX_REVIEW_WINDOW_DAYS,
|
|
624
|
+
HORIZON_DAYS,
|
|
501
625
|
read_frontmatter,
|
|
502
626
|
check_one,
|
|
503
627
|
main,
|
|
@@ -118,7 +118,7 @@ export interface RuleCoverage {
|
|
|
118
118
|
/**
|
|
119
119
|
* Carrier period per hook-capable platform — never a single scalar.
|
|
120
120
|
*
|
|
121
|
-
* A scalar is wrong twice over:
|
|
121
|
+
* A scalar is wrong twice over: this package binds nothing on copilot, so a
|
|
122
122
|
* weakest-platform collapse turns one platform property into a finding on
|
|
123
123
|
* every hook-carried rule; and cline maps `stop` from `TaskCancel`, so the
|
|
124
124
|
* same slot means per-turn on six platforms and per-interruption there. Both
|
|
@@ -39,9 +39,26 @@
|
|
|
39
39
|
* Escape hatch: `<!-- enforcement-count-ok: <reason> -->` on the line, reason
|
|
40
40
|
* mandatory. A bare marker stays a finding.
|
|
41
41
|
*
|
|
42
|
+
* THE SECOND HALF — the projection owes the split. Exempting the generated file
|
|
43
|
+
* settles WHERE the number may be published; it says nothing about whether the
|
|
44
|
+
* published number is readable. `undeclared` quoted bare reads as a backlog
|
|
45
|
+
* somebody forgot to wire, and for nine of them that is not what it is:
|
|
46
|
+
* `block_kernel_rule_writes` refuses an `enforced_by` write on a kernel rule
|
|
47
|
+
* with no agent-accessible override, so those cannot declare one at all. The
|
|
48
|
+
* resolver has always computed `kernel_denied`; the published line dropped it
|
|
49
|
+
* and appended the word `yet`, promising a future that does not exist for that
|
|
50
|
+
* nine. So a line in the sanctioned projection that states an `undeclared`
|
|
51
|
+
* figure without its kernel-denied split is the SAME class of failure as a
|
|
52
|
+
* hand-written count elsewhere: a figure the reader cannot resolve.
|
|
53
|
+
*
|
|
54
|
+
* This deliberately does NOT check the split's value, for the same reason the
|
|
55
|
+
* first half does not check the count's — the resolver is the only source, and
|
|
56
|
+
* a value comparison here would be a second place the number lives.
|
|
57
|
+
*
|
|
42
58
|
* Exit codes:
|
|
43
|
-
* 0 — no hand-written enforcement count in the published corpus
|
|
44
|
-
*
|
|
59
|
+
* 0 — no hand-written enforcement count in the published corpus, and every
|
|
60
|
+
* published `undeclared` figure carries its kernel-denied split
|
|
61
|
+
* 1 — at least one (or a marker with no reason, or a split-less figure)
|
|
45
62
|
* 2 — the gate could not run (dead scan scope, bad args)
|
|
46
63
|
*/
|
|
47
64
|
import * as fs from 'node:fs';
|
|
@@ -57,8 +74,8 @@ const _HERE = fileURLToPath(import.meta.url);
|
|
|
57
74
|
const DEFAULT_ROOT = path.join(path.dirname(_HERE), '..', '..');
|
|
58
75
|
const SELF = 'src/scripts/check_enforcement_denominator.ts';
|
|
59
76
|
|
|
60
|
-
const SELF_TEST_MIN_CASES =
|
|
61
|
-
const SELF_TEST_MIN_REJECT =
|
|
77
|
+
const SELF_TEST_MIN_CASES = 10;
|
|
78
|
+
const SELF_TEST_MIN_REJECT = 5;
|
|
62
79
|
|
|
63
80
|
/** Roots walked, repo-relative. */
|
|
64
81
|
const SCAN_ROOTS = ['docs', 'README.md'] as const;
|
|
@@ -82,6 +99,19 @@ const MARKER = 'enforcement-count-ok:';
|
|
|
82
99
|
const ENFORCEMENT_KEYWORDS =
|
|
83
100
|
/undeclared|enforced_by|enforcement coverage|carry a backstop|backstop that fails/;
|
|
84
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Shapes that state the `undeclared` figure itself. Narrower than
|
|
104
|
+
* {@link COUNT_SHAPES} on purpose: this is the ONE figure whose split is
|
|
105
|
+
* load-bearing, and a wider net would demand the split beside `blocking`.
|
|
106
|
+
*/
|
|
107
|
+
const UNDECLARED_FIGURE_SHAPES: readonly RegExp[] = [
|
|
108
|
+
/\b\d+\s+undeclared\b/i,
|
|
109
|
+
/\bundeclared\s+rules?\s*\(\s*\d+/i,
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
/** The split that makes the figure readable. Presence is checked, never value. */
|
|
113
|
+
const SPLIT_TOKEN = /kernel[-_]denied/i;
|
|
114
|
+
|
|
85
115
|
const COUNT_SHAPES: readonly RegExp[] = [
|
|
86
116
|
/\b\d+\s+of\s+\d+\s+(?:governed\s+)?rules?\b/,
|
|
87
117
|
/\b\d+\s+(?:governed\s+)?rules?\b/,
|
|
@@ -93,8 +123,12 @@ export interface Finding {
|
|
|
93
123
|
file: string;
|
|
94
124
|
line: number;
|
|
95
125
|
excerpt: string;
|
|
96
|
-
/**
|
|
97
|
-
|
|
126
|
+
/**
|
|
127
|
+
* `raw` — an unmarked count. `bare-marker` — a marker with no reason.
|
|
128
|
+
* `missing-split` — the projection states `undeclared` without its
|
|
129
|
+
* kernel-denied split.
|
|
130
|
+
*/
|
|
131
|
+
kind: 'raw' | 'bare-marker' | 'missing-split';
|
|
98
132
|
}
|
|
99
133
|
|
|
100
134
|
function markerOn(line: string): 'none' | 'bare' | 'reasoned' {
|
|
@@ -123,6 +157,30 @@ export function scanText(relPath: string, text: string): Finding[] {
|
|
|
123
157
|
return out;
|
|
124
158
|
}
|
|
125
159
|
|
|
160
|
+
/**
|
|
161
|
+
* The projection's own obligation: every line stating an `undeclared` figure
|
|
162
|
+
* carries the kernel-denied split. Run INSTEAD of {@link scanText} on a
|
|
163
|
+
* generated file — that file is allowed to state the number, and is the only
|
|
164
|
+
* one that owes the split.
|
|
165
|
+
*/
|
|
166
|
+
export function scanGenerated(relPath: string, text: string): Finding[] {
|
|
167
|
+
const out: Finding[] = [];
|
|
168
|
+
const lines = text.split('\n');
|
|
169
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
170
|
+
const line = lines[i] ?? '';
|
|
171
|
+
if (!UNDECLARED_FIGURE_SHAPES.some((re) => re.test(line))) continue;
|
|
172
|
+
if (SPLIT_TOKEN.test(line)) continue;
|
|
173
|
+
if (markerOn(line) === 'reasoned') continue;
|
|
174
|
+
out.push({
|
|
175
|
+
file: relPath,
|
|
176
|
+
line: i + 1,
|
|
177
|
+
excerpt: line.trim().slice(0, 120),
|
|
178
|
+
kind: 'missing-split',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
126
184
|
/** `true` when the file declares itself generated — i.e. it IS the projection. */
|
|
127
185
|
export function isGenerated(text: string): boolean {
|
|
128
186
|
return text.split('\n', GENERATED_HEAD_LINES).join('\n').includes(GENERATED_MARKER);
|
|
@@ -177,7 +235,13 @@ export function check(root: string): { code: number; findings: Finding[]; scanne
|
|
|
177
235
|
for (const rel of files) {
|
|
178
236
|
const text = fs.readFileSync(path.join(root, rel), 'utf-8');
|
|
179
237
|
if (isGenerated(text)) {
|
|
180
|
-
|
|
238
|
+
const split = scanGenerated(rel, text);
|
|
239
|
+
if (split.length > 0) {
|
|
240
|
+
findings.push(...split);
|
|
241
|
+
ledger.fail(rel, `${String(split.length)} undeclared figure(s) without the split`);
|
|
242
|
+
} else {
|
|
243
|
+
ledger.complete(rel);
|
|
244
|
+
}
|
|
181
245
|
continue;
|
|
182
246
|
}
|
|
183
247
|
const found = scanText(rel, text);
|
|
@@ -259,8 +323,21 @@ export function selfTest(): number {
|
|
|
259
323
|
}),
|
|
260
324
|
},
|
|
261
325
|
{
|
|
262
|
-
name: "accepts the resolver's own generated projection",
|
|
326
|
+
name: "accepts the resolver's own generated projection when it carries the split",
|
|
263
327
|
expect: 'accept',
|
|
328
|
+
run: () =>
|
|
329
|
+
run({
|
|
330
|
+
'docs/proof.md':
|
|
331
|
+
`<!-- ${GENERATED_MARKER} src/scripts/build_proof.ts -->\n\n` +
|
|
332
|
+
'120 rules · 15 blocking · 86 undeclared — of which 9 kernel-denied.\n',
|
|
333
|
+
}),
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
// DENIAL POLARITY for the split half. Same file, same exemption, the
|
|
337
|
+
// split removed: the projection is allowed to state the number and is
|
|
338
|
+
// still a finding when it states it bare.
|
|
339
|
+
name: 'rejects the generated projection when the kernel-denied split is removed',
|
|
340
|
+
expect: 'reject',
|
|
264
341
|
run: () =>
|
|
265
342
|
run({
|
|
266
343
|
'docs/proof.md':
|
|
@@ -268,6 +345,26 @@ export function selfTest(): number {
|
|
|
268
345
|
'120 rules · 15 blocking · 86 undeclared (no enforced_by yet).\n',
|
|
269
346
|
}),
|
|
270
347
|
},
|
|
348
|
+
{
|
|
349
|
+
name: 'rejects a bare "Undeclared rules (N)" restatement in the projection',
|
|
350
|
+
expect: 'reject',
|
|
351
|
+
run: () =>
|
|
352
|
+
run({
|
|
353
|
+
'docs/proof.md':
|
|
354
|
+
`<!-- ${GENERATED_MARKER} src/scripts/build_proof.ts -->\n\n` +
|
|
355
|
+
'Undeclared rules (86) carry no row.\n',
|
|
356
|
+
}),
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: 'accepts a generated line about enforcement that states no undeclared figure',
|
|
360
|
+
expect: 'accept',
|
|
361
|
+
run: () =>
|
|
362
|
+
run({
|
|
363
|
+
'docs/proof.md':
|
|
364
|
+
`<!-- ${GENERATED_MARKER} src/scripts/build_proof.ts -->\n\n` +
|
|
365
|
+
'120 rules · 15 blocking · the resolver prints its own frame.\n',
|
|
366
|
+
}),
|
|
367
|
+
},
|
|
271
368
|
{
|
|
272
369
|
name: 'accepts a dated ADR record (docs/decisions/ is excluded)',
|
|
273
370
|
expect: 'accept',
|
|
@@ -318,12 +415,25 @@ export function main(argv?: readonly string[]): number {
|
|
|
318
415
|
if (code === 2) return 2;
|
|
319
416
|
|
|
320
417
|
if (findings.length > 0) {
|
|
321
|
-
process.stdout.write('❌
|
|
418
|
+
process.stdout.write('❌ unresolvable enforcement figure(s) in a published doc:\n\n');
|
|
322
419
|
for (const f of findings) {
|
|
323
420
|
const why =
|
|
324
|
-
f.kind === 'bare-marker'
|
|
421
|
+
f.kind === 'bare-marker'
|
|
422
|
+
? `\`${MARKER}\` with no reason`
|
|
423
|
+
: f.kind === 'missing-split'
|
|
424
|
+
? 'published `undeclared` figure with no kernel-denied split'
|
|
425
|
+
: 'hand-written count';
|
|
325
426
|
process.stdout.write(` ${f.file}:${String(f.line)} — ${why}\n ${f.excerpt}\n`);
|
|
326
427
|
}
|
|
428
|
+
if (findings.some((f) => f.kind === 'missing-split')) {
|
|
429
|
+
process.stdout.write(
|
|
430
|
+
'\nThe projection may state the `undeclared` figure — it is the only surface\n' +
|
|
431
|
+
'that may — but not bare. Nine of those rules are kernel-denied:\n' +
|
|
432
|
+
'`block_kernel_rule_writes` refuses an `enforced_by` write with no\n' +
|
|
433
|
+
'agent-accessible override, so for them there is no *yet*. Print\n' +
|
|
434
|
+
"`summary.kernel_denied` on the same line (`build_proof.ts` § 4b does).\n",
|
|
435
|
+
);
|
|
436
|
+
}
|
|
327
437
|
process.stdout.write(
|
|
328
438
|
'\nExactly one enforcement denominator is quotable, and it comes from\n' +
|
|
329
439
|
'`check_enforcement_coverage` — which resolves rather than counts, and prints\n' +
|
|
@@ -362,4 +472,4 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
|
362
472
|
process.exit(main());
|
|
363
473
|
}
|
|
364
474
|
|
|
365
|
-
export { SCAN_ROOTS, EXCLUDED_PREFIXES, MARKER };
|
|
475
|
+
export { SCAN_ROOTS, EXCLUDED_PREFIXES, MARKER, SPLIT_TOKEN, UNDECLARED_FIGURE_SHAPES };
|