@event4u/agent-config 9.20.0 → 9.22.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 +86 -0
- package/dist/agent-src/commands/worktree/cleanup.md +17 -0
- package/dist/agent-src/commands/worktree/status.md +6 -0
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +22 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-XGA2NTU4.js → chunk-3EWYC3YK.js} +40 -0
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +5 -5
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.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 +58 -2
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +47 -29
- package/package.json +1 -1
- package/src/config/gate-coverage.yml +22 -4
- package/src/config/gate-violation-baselines.json +3 -8
- package/src/scripts/_lib/bench_ab_activation.ts +368 -0
- package/src/scripts/_lib/gate_population.ts +70 -0
- package/src/scripts/_lib/roadmap_granularity.ts +157 -0
- package/src/scripts/_lib/scan_scope.ts +44 -0
- package/src/scripts/_lib/stdin.ts +82 -0
- package/src/scripts/audit_adr_coverage.ts +38 -0
- package/src/scripts/audit_auto_rules.ts +17 -3
- package/src/scripts/audit_command_surface.ts +51 -0
- package/src/scripts/audit_initial_context.ts +39 -3
- package/src/scripts/audit_mcp_tools.ts +25 -0
- package/src/scripts/audit_overlap.ts +24 -1
- package/src/scripts/audit_skill_descriptions.ts +19 -0
- package/src/scripts/{verify_before_complete_hook.ts → before_complete_hook.ts} +16 -3
- package/src/scripts/bench_ab_task_runner.ts +18 -0
- package/src/scripts/bench_ab_v2_run.ts +147 -6
- package/src/scripts/bench_ab_v2_stats.ts +74 -2
- package/src/scripts/check_adr_frontmatter.ts +23 -3
- package/src/scripts/check_always_budget.ts +24 -0
- package/src/scripts/check_artefact_checksums.ts +24 -1
- package/src/scripts/check_artefact_count_messaging.ts +20 -0
- package/src/scripts/check_augmentignore.ts +22 -15
- package/src/scripts/check_backstop_debt.ts +22 -0
- package/src/scripts/check_beta_review_markers.ts +22 -1
- package/src/scripts/check_bridge_derivation.ts +22 -1
- package/src/scripts/check_bundle_path_leakage.ts +26 -0
- package/src/scripts/check_ci_strict_superset.ts +21 -3
- package/src/scripts/check_claims.ts +22 -0
- package/src/scripts/check_cli_registry_budget_sync.ts +33 -4
- package/src/scripts/check_cluster_patterns.ts +26 -4
- package/src/scripts/check_command_count_messaging.ts +20 -4
- package/src/scripts/check_comparison.ts +18 -0
- package/src/scripts/check_condensed_paths.ts +23 -2
- package/src/scripts/check_consumer_scope_flip.ts +21 -2
- package/src/scripts/check_council_config_location.ts +20 -0
- package/src/scripts/check_council_layout.ts +22 -2
- package/src/scripts/check_council_references.ts +23 -1
- package/src/scripts/check_dependency_floors.ts +19 -3
- package/src/scripts/check_discovery_determinism.ts +26 -0
- package/src/scripts/check_enforcement_coverage.ts +20 -0
- package/src/scripts/check_evaluator_budgets.ts +23 -0
- package/src/scripts/check_finding_dispositions.ts +29 -0
- package/src/scripts/check_gate_coverage.ts +63 -22
- package/src/scripts/check_gate_paths.ts +30 -5
- package/src/scripts/check_generated_artefact_headers.ts +21 -0
- package/src/scripts/check_generator_output_coverage.ts +23 -0
- package/src/scripts/check_gitignore_freshness.ts +23 -0
- package/src/scripts/check_host_loadability.ts +34 -8
- package/src/scripts/check_installer_import_purity.ts +22 -8
- package/src/scripts/check_kernel_prefix_stability.ts +19 -1
- package/src/scripts/check_kernel_rule_bundle.ts +23 -0
- package/src/scripts/check_knowledge_cards.ts +13 -0
- package/src/scripts/check_knowledge_pages.ts +23 -0
- package/src/scripts/check_knowledge_sharing.ts +27 -0
- package/src/scripts/check_md_language.ts +24 -0
- package/src/scripts/check_media_deps.ts +23 -0
- package/src/scripts/check_memory.ts +26 -1
- package/src/scripts/check_memory_contradiction.ts +16 -1
- package/src/scripts/check_memory_proposal.ts +15 -0
- package/src/scripts/check_memory_similarity.ts +16 -0
- package/src/scripts/check_module_management_neutral.ts +19 -11
- package/src/scripts/check_no_conflict_markers.ts +22 -1
- package/src/scripts/check_no_external_sources.ts +26 -1
- package/src/scripts/check_no_local_settings_committed.ts +37 -10
- package/src/scripts/check_no_new_legacy_path.ts +30 -1
- package/src/scripts/check_one_off_location.ts +43 -0
- package/src/scripts/check_originality_freshness.ts +25 -1
- package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
- package/src/scripts/check_pack_size.ts +22 -0
- package/src/scripts/check_preamble_payload_budget.ts +35 -5
- package/src/scripts/check_proposal.ts +24 -1
- package/src/scripts/check_public_catalog_links.ts +27 -0
- package/src/scripts/check_public_links.ts +19 -0
- package/src/scripts/check_quality_regression.ts +19 -0
- package/src/scripts/check_reach_channels.ts +44 -0
- package/src/scripts/check_reach_staleness.ts +27 -0
- package/src/scripts/check_release_adjacent_health.ts +28 -0
- package/src/scripts/check_release_highlights.ts +34 -0
- package/src/scripts/check_release_includes_discovery.ts +25 -6
- package/src/scripts/check_release_pr_shape.ts +18 -1
- package/src/scripts/check_release_published.ts +21 -0
- package/src/scripts/check_release_surface_equality.ts +22 -0
- package/src/scripts/check_release_trunk_sync.ts +19 -0
- package/src/scripts/check_reply_consistency.ts +22 -0
- package/src/scripts/check_roadmap_trackable.ts +20 -0
- package/src/scripts/check_role_doc_links.ts +23 -0
- package/src/scripts/check_rule_invariants.ts +14 -0
- package/src/scripts/check_secret_leak.ts +47 -0
- package/src/scripts/check_skill_gaps.ts +32 -0
- package/src/scripts/check_skill_requires.ts +19 -0
- package/src/scripts/check_source_pointer_freshness.ts +20 -0
- package/src/scripts/check_structural_breaking.ts +29 -2
- package/src/scripts/check_surface_tiers.ts +34 -0
- package/src/scripts/check_template_pin_drift.ts +23 -1
- package/src/scripts/check_test_coverage_diff.ts +17 -1
- package/src/scripts/check_token_optimizer_freshness.ts +18 -3
- package/src/scripts/check_token_quality_golden.ts +21 -0
- package/src/scripts/check_token_regression.ts +25 -0
- package/src/scripts/check_tracked_but_ignored.ts +26 -0
- package/src/scripts/check_trigger_eval_presence.ts +23 -1
- package/src/scripts/check_trigger_evals.ts +36 -0
- package/src/scripts/check_trunk_drift.ts +23 -0
- package/src/scripts/check_update_banner.ts +24 -0
- package/src/scripts/gate_scope_classify.ts +513 -0
- package/src/scripts/hook_manifest.yaml +1 -1
- package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/lint_agent_security.ts +19 -0
- package/src/scripts/lint_agent_skill_names.ts +30 -0
- package/src/scripts/lint_agents_layout.ts +20 -0
- package/src/scripts/lint_agents_md.ts +20 -0
- package/src/scripts/lint_archived_skills.ts +24 -3
- package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
- package/src/scripts/lint_bench_ab.ts +25 -0
- package/src/scripts/lint_bench_corpus.ts +20 -4
- package/src/scripts/lint_breaking_changes_index.ts +24 -0
- package/src/scripts/lint_budget_ownership.ts +20 -3
- package/src/scripts/lint_changelog_rollback.ts +24 -0
- package/src/scripts/lint_command_flow_coverage.ts +22 -0
- package/src/scripts/lint_command_tiers.ts +29 -5
- package/src/scripts/lint_commit_subjects.ts +15 -0
- package/src/scripts/lint_confusables.ts +24 -0
- package/src/scripts/lint_consumer_internal_refs.ts +24 -0
- package/src/scripts/lint_design_quality.ts +22 -0
- package/src/scripts/lint_design_slop.ts +23 -0
- package/src/scripts/lint_discovery_manifest.ts +23 -0
- package/src/scripts/lint_discovery_vocabulary.ts +19 -0
- package/src/scripts/lint_documented_commands.ts +23 -0
- package/src/scripts/lint_empty_roadmaps.ts +23 -0
- package/src/scripts/lint_eval_fixture_citations.ts +19 -0
- package/src/scripts/lint_eval_freshness.ts +35 -0
- package/src/scripts/lint_examples.ts +18 -3
- package/src/scripts/lint_explain_trace.ts +23 -0
- package/src/scripts/lint_featured_skills.ts +26 -1
- package/src/scripts/lint_flows.ts +19 -0
- package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
- package/src/scripts/lint_ghostwriter_source.ts +26 -6
- package/src/scripts/lint_glama_drift.ts +24 -0
- package/src/scripts/lint_global_paths.ts +24 -0
- package/src/scripts/lint_governed_writes.ts +20 -0
- package/src/scripts/lint_hidden_unicode.ts +25 -0
- package/src/scripts/lint_hook_concern_budget.ts +39 -6
- package/src/scripts/lint_hook_manifest.ts +22 -0
- package/src/scripts/lint_instruction_smuggling.ts +23 -0
- package/src/scripts/lint_knowledge_scale.ts +32 -2
- package/src/scripts/lint_legal_pack.ts +31 -0
- package/src/scripts/lint_marketplace.ts +26 -0
- package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
- package/src/scripts/lint_mcp_config_security.ts +24 -0
- package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
- package/src/scripts/lint_memory_tripwire.ts +35 -0
- package/src/scripts/lint_missions.ts +23 -7
- package/src/scripts/lint_model_tier_coverage.ts +18 -0
- package/src/scripts/lint_namespace_collisions.ts +19 -0
- package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
- package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
- package/src/scripts/lint_one_off_age.ts +38 -2
- package/src/scripts/lint_orchestration_dsl.ts +23 -3
- package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
- package/src/scripts/lint_originality.ts +22 -4
- package/src/scripts/lint_originality_shingles.ts +28 -3
- package/src/scripts/lint_output_slop.ts +19 -0
- package/src/scripts/lint_override_kernel_guard.ts +30 -0
- package/src/scripts/lint_pack_first_win.ts +23 -0
- package/src/scripts/lint_pack_risk_class.ts +19 -3
- package/src/scripts/lint_persistence.ts +19 -0
- package/src/scripts/lint_persona_governance.ts +18 -3
- package/src/scripts/lint_positioning.ts +23 -0
- package/src/scripts/lint_pre_migration_refs.ts +24 -0
- package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
- package/src/scripts/lint_provenance.ts +21 -0
- package/src/scripts/lint_provenance_vocabulary.ts +24 -1
- package/src/scripts/lint_readme_jargon.ts +21 -3
- package/src/scripts/lint_readme_serial_comma.ts +21 -3
- package/src/scripts/lint_readme_size.ts +21 -3
- package/src/scripts/lint_regression.ts +28 -0
- package/src/scripts/lint_roadmap_blockers.ts +43 -0
- package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
- package/src/scripts/lint_roadmap_complexity.ts +47 -0
- package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
- package/src/scripts/lint_rule_interactions.ts +16 -2
- package/src/scripts/lint_rule_tiers.ts +19 -6
- package/src/scripts/lint_showcase_sessions.ts +19 -3
- package/src/scripts/lint_skill_descriptions.ts +19 -0
- package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
- package/src/scripts/lint_skill_originality.ts +18 -3
- package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
- package/src/scripts/lint_skill_tools.ts +84 -145
- package/src/scripts/lint_spawn_payload.ts +27 -1
- package/src/scripts/lint_store_boundary.ts +19 -0
- package/src/scripts/lint_subagent_determinism.ts +23 -1
- package/src/scripts/lint_supported_tools_matrix.ts +18 -0
- package/src/scripts/lint_surface_matrix.ts +19 -3
- package/src/scripts/lint_ticket_buildable.ts +23 -0
- package/src/scripts/lint_topics_yaml.ts +19 -12
- package/src/scripts/lint_trigger_precision.ts +27 -0
- package/src/scripts/lint_trust_coherence.ts +21 -0
- package/src/scripts/lint_ui_stack_bundles.ts +17 -3
- package/src/scripts/lint_value_dashboard.ts +19 -3
- package/src/scripts/lint_versioned_cache.ts +19 -1
- package/src/scripts/lint_workflow_security.ts +22 -14
- package/src/scripts/pr_url_reminder_hook.ts +1 -1
- package/src/scripts/skill_collision_clusters.ts +37 -3
- package/src/scripts/skill_discovery.ts +20 -0
- package/src/scripts/skill_eval_coverage.ts +22 -0
- package/src/scripts/skill_overlap.ts +32 -6
- package/src/scripts/skill_preview.ts +31 -0
- package/src/scripts/skill_trigger_eval.ts +18 -0
- package/src/scripts/skill_usage_collect.ts +21 -0
- package/src/scripts/skill_usage_report.ts +21 -0
- package/src/scripts/sweep_dead_scan_roots.ts +11 -3
- package/src/scripts/verify_physical_move.ts +24 -0
- package/src/scripts/worktree_cleanup_check.ts +319 -4
- package/src/scripts/check_bite_sized_granularity.ts +0 -116
- package/src/scripts/lint_workspace_boundary.ts +0 -267
|
@@ -19,6 +19,8 @@ import * as path from 'node:path';
|
|
|
19
19
|
import * as fs from 'node:fs';
|
|
20
20
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
21
21
|
|
|
22
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
23
|
+
|
|
22
24
|
const _HERE = fileURLToPath(import.meta.url);
|
|
23
25
|
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
24
26
|
const SCANNER = path.join(ROOT, 'src', 'scripts', 'build_discovery_manifest.ts');
|
|
@@ -76,6 +78,30 @@ class ExitError extends Error {}
|
|
|
76
78
|
function main(): number {
|
|
77
79
|
const a = _normalise(_run());
|
|
78
80
|
const b = _normalise(_run());
|
|
81
|
+
|
|
82
|
+
// Determinism over an EMPTY manifest is vacuously true: if the scanner's
|
|
83
|
+
// artefact roots moved, both runs emit zero artefacts, the two payloads
|
|
84
|
+
// compare equal, and this gate reports "OK: deterministic" having compared
|
|
85
|
+
// nothing. Assert the corpus the comparison ran over, not the comparison.
|
|
86
|
+
// Deletion test: wiping the artefact tree would give 0 — which is blindness
|
|
87
|
+
// here, never success — so no `allowEmpty`. Exit 1 is this pinned CLI's
|
|
88
|
+
// only failure code (the scanner-failed path also exits 1, via ExitError).
|
|
89
|
+
const artefacts = a['artefacts'];
|
|
90
|
+
try {
|
|
91
|
+
assertScanned({
|
|
92
|
+
gate: 'check_discovery_determinism',
|
|
93
|
+
scanned: Array.isArray(artefacts) ? artefacts.length : 0,
|
|
94
|
+
units: 'manifest artefact(s)',
|
|
95
|
+
roots: ['src/scripts/build_discovery_manifest.ts → manifest.artefacts'],
|
|
96
|
+
});
|
|
97
|
+
} catch (exc) {
|
|
98
|
+
if (exc instanceof DeadScopeError) {
|
|
99
|
+
process.stderr.write(`${exc.message}\n`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
throw exc;
|
|
103
|
+
}
|
|
104
|
+
|
|
79
105
|
const sa = _dumps(a);
|
|
80
106
|
const sb = _dumps(b);
|
|
81
107
|
if (sa !== sb) {
|
|
@@ -37,6 +37,8 @@ import * as fs from 'node:fs';
|
|
|
37
37
|
import * as path from 'node:path';
|
|
38
38
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
39
39
|
|
|
40
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
41
|
+
|
|
40
42
|
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
41
43
|
const REPO_ROOT = path.resolve(_HERE, '..', '..');
|
|
42
44
|
const RULES_DIR = path.join(REPO_ROOT, 'src', 'rules');
|
|
@@ -478,6 +480,24 @@ function main(argv: string[]): number {
|
|
|
478
480
|
}
|
|
479
481
|
|
|
480
482
|
const rows = collect();
|
|
483
|
+
// Count every rule read, not the declared subset: with an empty rules dir
|
|
484
|
+
// the report renders "0/0 rules (0%)" and exits green, and `summarise`
|
|
485
|
+
// special-cases the zero denominator rather than rejecting it. Exit 2 is
|
|
486
|
+
// the code the missing-rules-dir guard above already uses for this class.
|
|
487
|
+
try {
|
|
488
|
+
assertScanned({
|
|
489
|
+
gate: 'check_enforcement_coverage',
|
|
490
|
+
scanned: rows.length,
|
|
491
|
+
units: 'rule file(s)',
|
|
492
|
+
roots: ['src/rules'],
|
|
493
|
+
});
|
|
494
|
+
} catch (exc) {
|
|
495
|
+
if (exc instanceof DeadScopeError) {
|
|
496
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
497
|
+
return 2;
|
|
498
|
+
}
|
|
499
|
+
throw exc;
|
|
500
|
+
}
|
|
481
501
|
const summary = summarise(rows);
|
|
482
502
|
|
|
483
503
|
if (as_json) {
|
|
@@ -40,6 +40,8 @@ import * as fs from 'node:fs';
|
|
|
40
40
|
import * as path from 'node:path';
|
|
41
41
|
import { fileURLToPath } from 'node:url';
|
|
42
42
|
|
|
43
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
44
|
+
|
|
43
45
|
const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
|
|
44
46
|
const BUDGETS_PATH = path.join(REPO_ROOT, 'src', 'config', 'evaluator-budgets.json');
|
|
45
47
|
|
|
@@ -230,6 +232,27 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
230
232
|
return 2;
|
|
231
233
|
}
|
|
232
234
|
|
|
235
|
+
// The budget document IS the corpus: every finding below is produced by
|
|
236
|
+
// iterating it, so a budgets file that parses to no entries reports
|
|
237
|
+
// "✅ evaluator budgets met (0 metrics)" — green over nothing, the exact
|
|
238
|
+
// shape this harness exists to end. A missing `budgets` key counts as 0 for
|
|
239
|
+
// the same reason. Exit 2 (documented internal error — the gate could not
|
|
240
|
+
// run), never 1, which means a metric actually breached.
|
|
241
|
+
try {
|
|
242
|
+
assertScanned({
|
|
243
|
+
gate: 'check_evaluator_budgets',
|
|
244
|
+
scanned: Object.keys(budgetsDoc.budgets ?? {}).length,
|
|
245
|
+
units: 'budgeted metric(s)',
|
|
246
|
+
roots: ['src/config/evaluator-budgets.json (budgets)'],
|
|
247
|
+
});
|
|
248
|
+
} catch (e) {
|
|
249
|
+
if (e instanceof DeadScopeError) {
|
|
250
|
+
process.stderr.write(`❌ ${e.message}\n`);
|
|
251
|
+
return 2;
|
|
252
|
+
}
|
|
253
|
+
throw e;
|
|
254
|
+
}
|
|
255
|
+
|
|
233
256
|
// The committed record is optional: a fresh checkout before the first
|
|
234
257
|
// nightly has none, and a missing record must not fake a green drift check.
|
|
235
258
|
const recordPath = argValue(argv, '--recorded') ?? path.join(REPO_ROOT, RECORD_REL);
|
|
@@ -34,6 +34,8 @@ import * as path from 'node:path';
|
|
|
34
34
|
import process from 'node:process';
|
|
35
35
|
import { fileURLToPath } from 'node:url';
|
|
36
36
|
|
|
37
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
38
|
+
|
|
37
39
|
const _HERE = fileURLToPath(import.meta.url);
|
|
38
40
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
39
41
|
|
|
@@ -235,6 +237,33 @@ function main(argv: readonly string[]): number {
|
|
|
235
237
|
}
|
|
236
238
|
}
|
|
237
239
|
|
|
240
|
+
// Scope declaration, not a scope guard. The ledger is written per release by
|
|
241
|
+
// `--ingest`, so "no <release>.json" is the normal state of a release whose
|
|
242
|
+
// self-review reported nothing — the gate's own success line says so. That
|
|
243
|
+
// makes an absent file indistinguishable from a moved ledger dir here, and
|
|
244
|
+
// the honest reading is that the corpus count cannot carry that signal: the
|
|
245
|
+
// durable trigger for an un-ingested finding is `--pr` mode, which compares
|
|
246
|
+
// the ledger against what the self-review actually reported.
|
|
247
|
+
try {
|
|
248
|
+
assertScanned({
|
|
249
|
+
gate: 'check_finding_dispositions',
|
|
250
|
+
scanned: ledger.findings.length,
|
|
251
|
+
units: 'recorded finding(s)',
|
|
252
|
+
roots: [path.relative(REPO_ROOT, ledgerPath)],
|
|
253
|
+
allowEmpty:
|
|
254
|
+
'EMPTY_VALID: zero recorded findings IS the pass state for a release whose '
|
|
255
|
+
+ 'self-review reported none — the ledger file is created on first --ingest, so a '
|
|
256
|
+
+ 'clean release legitimately has no <version>.json. Un-ingested findings are '
|
|
257
|
+
+ 'caught by --pr (unrecorded_findings), never by this count.',
|
|
258
|
+
});
|
|
259
|
+
} catch (exc) {
|
|
260
|
+
if (exc instanceof DeadScopeError) {
|
|
261
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
262
|
+
return 1;
|
|
263
|
+
}
|
|
264
|
+
throw exc;
|
|
265
|
+
}
|
|
266
|
+
|
|
238
267
|
const problems = missing_dispositions(ledger.findings);
|
|
239
268
|
if (pr) {
|
|
240
269
|
const reported = parse_comment_findings(_gh_comment_bodies(pr));
|
|
@@ -51,6 +51,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
51
51
|
import * as yaml from 'js-yaml';
|
|
52
52
|
import { checkRatchet } from './_lib/gate_baseline.js';
|
|
53
53
|
import { runCountedProbe } from './_lib/counted_probe.js';
|
|
54
|
+
import { listGateScripts } from './_lib/gate_population.js';
|
|
55
|
+
import { assertScanned } from './_lib/scan_scope.js';
|
|
54
56
|
|
|
55
57
|
const _HERE = fileURLToPath(import.meta.url);
|
|
56
58
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -260,6 +262,18 @@ export function main(argv: readonly string[]): number {
|
|
|
260
262
|
let specs: GateSpec[];
|
|
261
263
|
try {
|
|
262
264
|
specs = load_manifest();
|
|
265
|
+
// This guard's own scan scope is the manifest. It already refused an empty
|
|
266
|
+
// gate list ("a coverage guard over an empty set is vacuous"), but as a
|
|
267
|
+
// bespoke throw — invisible to the very population test below, which is how
|
|
268
|
+
// the meta-gate ended up outside its own definition of hardened. Routing the
|
|
269
|
+
// same condition through the shared assertion is the honest fix: no new
|
|
270
|
+
// behaviour, one shape for "this gate read nothing".
|
|
271
|
+
assertScanned({
|
|
272
|
+
gate: 'check_gate_coverage',
|
|
273
|
+
scanned: specs.length,
|
|
274
|
+
units: 'manifest gate(s)',
|
|
275
|
+
roots: [path.relative(REPO_ROOT, MANIFEST)],
|
|
276
|
+
});
|
|
263
277
|
} catch (e) {
|
|
264
278
|
process.stderr.write(`❌ gate-coverage manifest: ${(e as Error).message}\n`);
|
|
265
279
|
return 2;
|
|
@@ -358,33 +372,66 @@ export function report_hardening_ratchet(): number {
|
|
|
358
372
|
}
|
|
359
373
|
|
|
360
374
|
/**
|
|
361
|
-
*
|
|
362
|
-
* population still able to report success over an empty corpus.
|
|
375
|
+
* Manifest ids whose `scanned:` line is actually ENFORCED against a floor.
|
|
363
376
|
*
|
|
364
|
-
* A
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
377
|
+
* A `pending` entry, or an enforced one with `min_scanned: 0`, reads the line
|
|
378
|
+
* without being able to fail on it — so it cannot make a gate hardened either.
|
|
379
|
+
* Read best-effort: an unreadable manifest yields an empty set, which pushes
|
|
380
|
+
* every emit-only gate back into the unhardened count. That direction is
|
|
381
|
+
* deliberate — a coverage guard that cannot read its own manifest should
|
|
382
|
+
* over-report exposure, never under-report it.
|
|
368
383
|
*/
|
|
369
|
-
export function
|
|
370
|
-
let entries: string[];
|
|
384
|
+
export function enforced_manifest_ids(file = MANIFEST): Set<string> {
|
|
371
385
|
try {
|
|
372
|
-
|
|
386
|
+
return new Set(
|
|
387
|
+
load_manifest(file)
|
|
388
|
+
.filter((s) => s.status === 'enforced' && s.min_scanned >= 1)
|
|
389
|
+
.map((s) => s.id),
|
|
390
|
+
);
|
|
373
391
|
} catch {
|
|
374
|
-
return
|
|
392
|
+
return new Set<string>();
|
|
375
393
|
}
|
|
376
|
-
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Gate scripts that neither assert their scan scope nor publish an ENFORCED
|
|
398
|
+
* count — the population still able to report success over an empty corpus.
|
|
399
|
+
*
|
|
400
|
+
* ```
|
|
401
|
+
* hardened ⇔ routes through _lib/scan_scope
|
|
402
|
+
* ∨ (emits `scanned:` ∧ registered enforced in gate-coverage.yml with a floor)
|
|
403
|
+
* ```
|
|
404
|
+
*
|
|
405
|
+
* The second clause used to be a bare "emits `scanned:`". Measured 2026-08-04
|
|
406
|
+
* while chartering the conversion of the remaining 189: a printed count is only
|
|
407
|
+
* a *guard* for the gates this file actually runs against a floor. For an
|
|
408
|
+
* unregistered gate the line is decoration — it can print `scanned: 0` out of a
|
|
409
|
+
* deleted root and still exit 0, with nobody reading it. That made the cheapest
|
|
410
|
+
* route to a green ratchet (add one write per gate) also the route that changes
|
|
411
|
+
* nothing, while "pad the manifest" is an explicit non-goal of the conversion
|
|
412
|
+
* roadmap. Requiring registration closes the loop: to harden by emitting you
|
|
413
|
+
* must accept a floor, and everything else asserts.
|
|
414
|
+
*
|
|
415
|
+
* The tightening cost the existing population nothing — all 14 emit-only gates
|
|
416
|
+
* were already registered enforced — which is why it is a drift repair against
|
|
417
|
+
* this guard's own charter ("asserts its scan scope … or carries a justified
|
|
418
|
+
* `allowEmpty`") rather than a moved goalpost.
|
|
419
|
+
*/
|
|
420
|
+
export function list_unhardened_gates(
|
|
421
|
+
dir = path.join(REPO_ROOT, 'src/scripts'),
|
|
422
|
+
registered: ReadonlySet<string> = enforced_manifest_ids(),
|
|
423
|
+
): string[] {
|
|
377
424
|
const out: string[] = [];
|
|
378
|
-
for (const
|
|
425
|
+
for (const id of listGateScripts(dir, (d) => fs.readdirSync(d))) {
|
|
379
426
|
let src: string;
|
|
380
427
|
try {
|
|
381
|
-
src = fs.readFileSync(path.join(dir,
|
|
428
|
+
src = fs.readFileSync(path.join(dir, `${id}.ts`), 'utf8');
|
|
382
429
|
} catch {
|
|
383
430
|
continue;
|
|
384
431
|
}
|
|
385
|
-
const asserts = /assertScanned\(|assertWatchlistResolves\(/.test(src);
|
|
432
|
+
const asserts = /assertScanned\(|assertWatchlistResolves\(|reportScanned\(/.test(src);
|
|
386
433
|
const emits = /(?:process\.(?:stdout|stderr)\.write|lines\.push)\(\s*`scanned: /.test(src);
|
|
387
|
-
if (!asserts && !emits) out.push(
|
|
434
|
+
if (!asserts && !(emits && registered.has(id))) out.push(id);
|
|
388
435
|
}
|
|
389
436
|
return out.sort();
|
|
390
437
|
}
|
|
@@ -637,13 +684,7 @@ export function render_ledger(
|
|
|
637
684
|
|
|
638
685
|
/** Count of gate-shaped scripts, for the honest denominator in the ledger. */
|
|
639
686
|
export function count_gate_scripts(dir = path.join(REPO_ROOT, 'src/scripts')): number {
|
|
640
|
-
|
|
641
|
-
return fs
|
|
642
|
-
.readdirSync(dir)
|
|
643
|
-
.filter((f) => /^(lint|check|audit)_.*\.ts$/.test(f) && !f.endsWith('.d.ts')).length;
|
|
644
|
-
} catch {
|
|
645
|
-
return 0;
|
|
646
|
-
}
|
|
687
|
+
return listGateScripts(dir, (d) => fs.readdirSync(d)).length;
|
|
647
688
|
}
|
|
648
689
|
|
|
649
690
|
function run_canary_mode(specs: readonly GateSpec[], argv: readonly string[]): number {
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* synchronous. No behaviour changes.
|
|
21
21
|
*
|
|
22
22
|
* Exit codes: 0 = all enforced targets resolve under the source tree ·
|
|
23
|
-
* 1 = at least one missing / out-of-tree target · 2 = a gate failed to import
|
|
23
|
+
* 1 = at least one missing / out-of-tree target · 2 = a gate failed to import
|
|
24
|
+
* (which now includes a dead scan scope — zero enforced targets across the
|
|
25
|
+
* whole GATES list — reported through `_lib/scan_scope`).
|
|
24
26
|
*/
|
|
25
27
|
|
|
26
28
|
import { spawnSync } from 'node:child_process';
|
|
@@ -28,6 +30,8 @@ import * as fs from 'node:fs';
|
|
|
28
30
|
import * as path from 'node:path';
|
|
29
31
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
30
32
|
|
|
33
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
34
|
+
|
|
31
35
|
const _HERE = fileURLToPath(import.meta.url);
|
|
32
36
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
33
37
|
|
|
@@ -190,6 +194,31 @@ function main(): number {
|
|
|
190
194
|
process.stderr.write(`❌ check-gate-paths: ${msg}\n`);
|
|
191
195
|
return 2;
|
|
192
196
|
}
|
|
197
|
+
let total = 0;
|
|
198
|
+
for (const v of named.values()) {
|
|
199
|
+
total += v.length;
|
|
200
|
+
}
|
|
201
|
+
// The enforced targets ARE the corpus. A per-gate empty GATE_CORE_PATHS is
|
|
202
|
+
// already exit 2 inside the subprocess, but an emptied GATES list is not:
|
|
203
|
+
// it yields zero targets, zero failures, and a green "0 enforced target(s)
|
|
204
|
+
// … resolve" — the path-integrity gate certifying that nothing is enforced.
|
|
205
|
+
try {
|
|
206
|
+
assertScanned({
|
|
207
|
+
gate: 'check_gate_paths',
|
|
208
|
+
scanned: total,
|
|
209
|
+
units: 'enforced target(s)',
|
|
210
|
+
roots: ['src/scripts (GATE_CORE_PATHS of the GATES list)'],
|
|
211
|
+
});
|
|
212
|
+
} catch (exc) {
|
|
213
|
+
if (exc instanceof DeadScopeError) {
|
|
214
|
+
// Exit 2, not 1: 1 means "a declared target failed to resolve" (a
|
|
215
|
+
// finding), while 2 is this file's existing "a gate cannot be
|
|
216
|
+
// checked" code — which is exactly what an empty target set is.
|
|
217
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
218
|
+
return 2;
|
|
219
|
+
}
|
|
220
|
+
throw exc;
|
|
221
|
+
}
|
|
193
222
|
const failures = check_paths(named);
|
|
194
223
|
if (failures.length > 0) {
|
|
195
224
|
process.stdout.write(
|
|
@@ -202,10 +231,6 @@ function main(): number {
|
|
|
202
231
|
process.stdout.write(' GATE_CORE_PATHS or the move.\n');
|
|
203
232
|
return 1;
|
|
204
233
|
}
|
|
205
|
-
let total = 0;
|
|
206
|
-
for (const v of named.values()) {
|
|
207
|
-
total += v.length;
|
|
208
|
-
}
|
|
209
234
|
process.stdout.write(
|
|
210
235
|
`✅ check-gate-paths: ${total} enforced target(s) across ` +
|
|
211
236
|
`${named.size} gate(s) resolve under the source tree.\n`,
|
|
@@ -15,6 +15,8 @@ import * as fs from 'node:fs';
|
|
|
15
15
|
import * as path from 'node:path';
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
17
|
|
|
18
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
19
|
+
|
|
18
20
|
const _HERE = fileURLToPath(import.meta.url);
|
|
19
21
|
const REPO_ROOT = path.join(path.dirname(_HERE), '..', '..');
|
|
20
22
|
|
|
@@ -55,6 +57,25 @@ function main(argv?: readonly string[]): number {
|
|
|
55
57
|
const args = argv ?? process.argv.slice(2);
|
|
56
58
|
const quiet = args.includes('--quiet');
|
|
57
59
|
|
|
60
|
+
// Every artefact is skipped when absent, so all three moving at once prints
|
|
61
|
+
// "✅ All 3 generated artefacts carry the freshness marker" over zero reads.
|
|
62
|
+
// The per-file skip below stays — a single gitignored artefact is a real
|
|
63
|
+
// state; none of them existing is a moved root. 1 is the only failure code
|
|
64
|
+
// this gate has, so "could not run" and "found something" share it.
|
|
65
|
+
try {
|
|
66
|
+
assertWatchlistResolves({
|
|
67
|
+
gate: 'check_generated_artefact_headers',
|
|
68
|
+
candidates: GENERATED_ARTEFACTS.map((e) => e.path),
|
|
69
|
+
repoRoot: REPO_ROOT,
|
|
70
|
+
});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (err instanceof DeadScopeError) {
|
|
73
|
+
process.stderr.write(`❌ ${err.message}\n`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
|
|
58
79
|
const errors: string[] = [];
|
|
59
80
|
const warnings: string[] = [];
|
|
60
81
|
|
|
@@ -18,6 +18,8 @@ import * as fs from 'node:fs';
|
|
|
18
18
|
import * as path from 'node:path';
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
20
|
|
|
21
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
22
|
+
|
|
21
23
|
const _HERE = fileURLToPath(import.meta.url);
|
|
22
24
|
const REPO_ROOT = path.join(path.dirname(_HERE), '..', '..');
|
|
23
25
|
|
|
@@ -103,6 +105,27 @@ function main(argv?: readonly string[]): number {
|
|
|
103
105
|
.filter((l) => Boolean(l) && !l.startsWith('#')),
|
|
104
106
|
);
|
|
105
107
|
|
|
108
|
+
// The registry below is the corpus — the manifest and .gitignore are only
|
|
109
|
+
// what each root is checked AGAINST, and both already fail closed above. An
|
|
110
|
+
// emptied registry (a refactor that moves the list, a bad merge) leaves the
|
|
111
|
+
// loop with nothing to classify and prints "✅ All 0 generator output roots
|
|
112
|
+
// are classified". Exit 2, the code this gate already uses for "a required
|
|
113
|
+
// input is not there", never 1, which means a root is unclassified.
|
|
114
|
+
try {
|
|
115
|
+
assertScanned({
|
|
116
|
+
gate: 'check_generator_output_coverage',
|
|
117
|
+
scanned: GENERATOR_OUTPUT_ROOTS.length,
|
|
118
|
+
units: 'generator output root(s)',
|
|
119
|
+
roots: ['src/scripts/check_generator_output_coverage.ts (GENERATOR_OUTPUT_ROOTS)'],
|
|
120
|
+
});
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err instanceof DeadScopeError) {
|
|
123
|
+
process.stderr.write(`❌ ${err.message}\n`);
|
|
124
|
+
return 2;
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
|
|
106
129
|
const unclassified: typeof GENERATOR_OUTPUT_ROOTS[number][] = [];
|
|
107
130
|
for (const entry of GENERATOR_OUTPUT_ROOTS) {
|
|
108
131
|
if (!isClassified(entry.root, manifestPaths, gitignoreLines)) {
|
|
@@ -17,6 +17,8 @@ import * as fs from 'node:fs';
|
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
19
|
|
|
20
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
21
|
+
|
|
20
22
|
const _HERE = fileURLToPath(import.meta.url);
|
|
21
23
|
const REPO_ROOT = path.join(path.dirname(_HERE), '..', '..');
|
|
22
24
|
|
|
@@ -83,6 +85,27 @@ function main(argv?: readonly string[]): number {
|
|
|
83
85
|
const blockLines = new Set(blockText.split('\n').map((l) => l.trim()).filter(Boolean));
|
|
84
86
|
|
|
85
87
|
const entries = parseManifest(manifestText);
|
|
88
|
+
// Counted on the whole parse, not on the consumer-scoped subset the loop
|
|
89
|
+
// judges: `_isFile` above only proves the manifest exists, and a shape change
|
|
90
|
+
// that makes `- path:` stop matching yields zero entries, zero missing paths,
|
|
91
|
+
// and "✅ covers all 0 consumer-scoped ignored paths". Exit 2 (documented
|
|
92
|
+
// "invalid args or missing file" — the gate could not run), not 1, which
|
|
93
|
+
// means the block is genuinely missing a path.
|
|
94
|
+
try {
|
|
95
|
+
assertScanned({
|
|
96
|
+
gate: 'check_gitignore_freshness',
|
|
97
|
+
scanned: entries.length,
|
|
98
|
+
units: 'manifest entr(y|ies)',
|
|
99
|
+
roots: ['src/config/agents-paths.yml'],
|
|
100
|
+
});
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err instanceof DeadScopeError) {
|
|
103
|
+
process.stderr.write(`❌ ${err.message}\n`);
|
|
104
|
+
return 2;
|
|
105
|
+
}
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
|
|
86
109
|
const missing: string[] = [];
|
|
87
110
|
|
|
88
111
|
for (const e of entries) {
|
|
@@ -16,10 +16,14 @@ import * as path from 'node:path';
|
|
|
16
16
|
|
|
17
17
|
import { parse as parseYaml } from 'yaml';
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
20
|
+
|
|
21
|
+
/** @returns how many skill directories were examined. */
|
|
22
|
+
export function check_claude_skills(root: string, errors: string[]): number {
|
|
20
23
|
const dir = path.join(root, '.claude', 'skills');
|
|
21
|
-
if (!fs.existsSync(dir)) return;
|
|
22
|
-
|
|
24
|
+
if (!fs.existsSync(dir)) return 0;
|
|
25
|
+
const names = fs.readdirSync(dir);
|
|
26
|
+
for (const name of names) {
|
|
23
27
|
const skillMd = path.join(dir, name, 'SKILL.md');
|
|
24
28
|
if (!fs.existsSync(skillMd)) {
|
|
25
29
|
errors.push(`${skillMd}: missing SKILL.md`);
|
|
@@ -35,13 +39,17 @@ export function check_claude_skills(root: string, errors: string[]): void {
|
|
|
35
39
|
else if (meta['name'] !== name) errors.push(`${skillMd}: name '${meta['name']}' != dir '${name}'`);
|
|
36
40
|
if (typeof meta['description'] !== 'string' || !meta['description']) errors.push(`${skillMd}: missing description`);
|
|
37
41
|
}
|
|
42
|
+
return names.length;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
/** @returns how many `.mdc` rule files were examined. */
|
|
46
|
+
export function check_cursor_rules(root: string, errors: string[]): number {
|
|
41
47
|
const dir = path.join(root, '.cursor', 'rules');
|
|
42
|
-
if (!fs.existsSync(dir)) return;
|
|
48
|
+
if (!fs.existsSync(dir)) return 0;
|
|
49
|
+
let scanned = 0;
|
|
43
50
|
for (const name of fs.readdirSync(dir)) {
|
|
44
51
|
if (!name.endsWith('.mdc')) continue;
|
|
52
|
+
scanned += 1;
|
|
45
53
|
const p = path.join(dir, name);
|
|
46
54
|
const src = fs.readFileSync(p, 'utf-8');
|
|
47
55
|
const m = src.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -57,12 +65,22 @@ export function check_cursor_rules(root: string, errors: string[]): void {
|
|
|
57
65
|
try { parseYaml(lenient); }
|
|
58
66
|
catch (e) { errors.push(`${p}: frontmatter YAML invalid (${(e as Error).message})`); }
|
|
59
67
|
}
|
|
68
|
+
return scanned;
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
export function run(root: string): string[] {
|
|
63
72
|
const errors: string[] = [];
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
// Both host trees are generated and gitignored, and each checker returns
|
|
74
|
+
// early when its tree is absent — so "no malformed artefacts" and "no
|
|
75
|
+
// artefacts" are the same green. Thrown, not returned: `errors` names
|
|
76
|
+
// malformed files, and an unprojected tree is not one.
|
|
77
|
+
const scanned = check_claude_skills(root, errors) + check_cursor_rules(root, errors);
|
|
78
|
+
assertScanned({
|
|
79
|
+
gate: 'check_host_loadability',
|
|
80
|
+
scanned,
|
|
81
|
+
units: 'host artefact(s)',
|
|
82
|
+
roots: ['.claude/skills', '.cursor/rules'],
|
|
83
|
+
});
|
|
66
84
|
return errors;
|
|
67
85
|
}
|
|
68
86
|
|
|
@@ -71,7 +89,15 @@ const isMain = process.argv[1] !== undefined
|
|
|
71
89
|
if (isMain) {
|
|
72
90
|
const rootIdx = process.argv.indexOf('--root');
|
|
73
91
|
const root = rootIdx !== -1 ? process.argv[rootIdx + 1] ?? '.' : '.';
|
|
74
|
-
|
|
92
|
+
let errors: string[];
|
|
93
|
+
try {
|
|
94
|
+
errors = run(root);
|
|
95
|
+
} catch (exc) {
|
|
96
|
+
if (!(exc instanceof DeadScopeError)) throw exc;
|
|
97
|
+
// Exit 1 is this gate's only failure code.
|
|
98
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
75
101
|
if (errors.length > 0) {
|
|
76
102
|
for (const e of errors) process.stderr.write(`❌ ${e}\n`);
|
|
77
103
|
process.exit(1);
|
|
@@ -59,6 +59,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
59
59
|
|
|
60
60
|
import ts from 'typescript';
|
|
61
61
|
|
|
62
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
63
|
+
|
|
62
64
|
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
63
65
|
const REPO_ROOT = path.resolve(_HERE, '..', '..');
|
|
64
66
|
|
|
@@ -208,14 +210,6 @@ function parse(file: string): ts.SourceFile | null {
|
|
|
208
210
|
*/
|
|
209
211
|
function auditEntry(entryRel: string, root: string = REPO_ROOT): Finding[] {
|
|
210
212
|
const entryAbs = path.join(root, entryRel);
|
|
211
|
-
if (!fs.existsSync(entryAbs)) {
|
|
212
|
-
process.stderr.write(
|
|
213
|
-
`check-installer-import-purity: entry ${entryRel} does not exist — ` +
|
|
214
|
-
'update BUNDLE_ENTRIES\n',
|
|
215
|
-
);
|
|
216
|
-
process.exitCode = 1;
|
|
217
|
-
return [];
|
|
218
|
-
}
|
|
219
213
|
const findings: Finding[] = [];
|
|
220
214
|
const seen = new Set<string>([entryAbs]);
|
|
221
215
|
// BFS so the reported chain is a shortest path — the least confusing one.
|
|
@@ -245,6 +239,26 @@ function auditEntry(entryRel: string, root: string = REPO_ROOT): Finding[] {
|
|
|
245
239
|
}
|
|
246
240
|
|
|
247
241
|
function main(): number {
|
|
242
|
+
// BUNDLE_ENTRIES is the whole scan scope — every module examined is reached
|
|
243
|
+
// from one of them. This REPLACES the per-entry existence guard in
|
|
244
|
+
// `auditEntry`, which could not fail: it set `process.exitCode = 1` and
|
|
245
|
+
// returned no findings, and `process.exit(main())` then overwrote that with
|
|
246
|
+
// main's 0, so a moved entry printed "✅ … reach no module-level
|
|
247
|
+
// process.exit()". 1 is the only failure code this gate has.
|
|
248
|
+
try {
|
|
249
|
+
assertWatchlistResolves({
|
|
250
|
+
gate: 'check_installer_import_purity',
|
|
251
|
+
candidates: BUNDLE_ENTRIES,
|
|
252
|
+
repoRoot: REPO_ROOT,
|
|
253
|
+
});
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (err instanceof DeadScopeError) {
|
|
256
|
+
process.stderr.write(`❌ ${err.message}\n`);
|
|
257
|
+
return 1;
|
|
258
|
+
}
|
|
259
|
+
throw err;
|
|
260
|
+
}
|
|
261
|
+
|
|
248
262
|
const all: Finding[] = [];
|
|
249
263
|
for (const entry of BUNDLE_ENTRIES) {
|
|
250
264
|
all.push(...auditEntry(entry));
|
|
@@ -37,6 +37,8 @@ import * as path from 'node:path';
|
|
|
37
37
|
import process from 'node:process';
|
|
38
38
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
39
39
|
|
|
40
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
41
|
+
|
|
40
42
|
const _HERE = fileURLToPath(import.meta.url);
|
|
41
43
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
42
44
|
const ROUTER = path.join(REPO_ROOT, 'dist/router.json');
|
|
@@ -96,7 +98,17 @@ function _live_prefix(): KernelPrefix | null {
|
|
|
96
98
|
const router = _readJson(ROUTER);
|
|
97
99
|
if (router === null) return null;
|
|
98
100
|
const ids = kernel_ids(router);
|
|
99
|
-
|
|
101
|
+
// Scope only — the baseline and the comparison are untouched. An empty kernel
|
|
102
|
+
// list used to collapse into the same `null` as an unreadable router, so a
|
|
103
|
+
// router that compiled to zero kernel rules was reported as a missing file;
|
|
104
|
+
// it is a dead scan scope, and a digest over no bodies is a constant that
|
|
105
|
+
// would then look byte-stable forever.
|
|
106
|
+
assertScanned({
|
|
107
|
+
gate: 'check_kernel_prefix_stability',
|
|
108
|
+
scanned: ids.length,
|
|
109
|
+
units: 'kernel rule(s)',
|
|
110
|
+
roots: ['dist/router.json (kernel)', 'dist/agent-src/rules'],
|
|
111
|
+
});
|
|
100
112
|
return compute_prefix(ids, (id) => fs.readFileSync(path.join(RULES_DIR, `${id}.md`), 'utf-8'));
|
|
101
113
|
}
|
|
102
114
|
|
|
@@ -118,6 +130,12 @@ function main(argv: string[]): number {
|
|
|
118
130
|
try {
|
|
119
131
|
current = _live_prefix();
|
|
120
132
|
} catch (e) {
|
|
133
|
+
// 1 is the documented "file error" code — the gate could not run. 2 is
|
|
134
|
+
// reserved for drift, i.e. a prefix that was read and did change.
|
|
135
|
+
if (e instanceof DeadScopeError) {
|
|
136
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
121
139
|
process.stderr.write(`error: cannot read kernel bodies: ${(e as Error).message}\n`);
|
|
122
140
|
return 1;
|
|
123
141
|
}
|
|
@@ -37,6 +37,7 @@ import * as path from 'node:path';
|
|
|
37
37
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
38
38
|
import { KERNEL_RULE_FILENAMES } from './_lib/kernel_rules.js';
|
|
39
39
|
import { runCountedProbe } from './_lib/counted_probe.js';
|
|
40
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
40
41
|
|
|
41
42
|
const KERNEL_RULES: ReadonlySet<string> = KERNEL_RULE_FILENAMES;
|
|
42
43
|
|
|
@@ -55,6 +56,8 @@ const KERNEL_RULES: ReadonlySet<string> = KERNEL_RULE_FILENAMES;
|
|
|
55
56
|
*/
|
|
56
57
|
const KERNEL_DIR = 'src/rules';
|
|
57
58
|
const DEFAULT_LABEL = 'bundled-always-rules-acknowledged';
|
|
59
|
+
// src/scripts/check_kernel_rule_bundle.ts → two levels up is the repo root.
|
|
60
|
+
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
58
61
|
|
|
59
62
|
function _git_changed_files(base_ref: string): string[] {
|
|
60
63
|
// Bounded read: a truncated diff would shrink the change set and let the
|
|
@@ -188,6 +191,26 @@ function parse_args(argv: readonly string[]): Args {
|
|
|
188
191
|
export function main(argv?: readonly string[]): number {
|
|
189
192
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
190
193
|
|
|
194
|
+
// This gate has no corpus — it matches changed paths against a fixed set of
|
|
195
|
+
// 9 kernel rules, so its scope IS that watch list. It has already been
|
|
196
|
+
// inert once for exactly this reason (see KERNEL_DIR above): a prefix that
|
|
197
|
+
// no longer exists never matches, and every kernel-rule edit scored
|
|
198
|
+
// "no kernel rule touched". Exit 3 (internal error) over 1 (bundle
|
|
199
|
+
// violated) — a phantom watch list means the gate could not run.
|
|
200
|
+
try {
|
|
201
|
+
assertWatchlistResolves({
|
|
202
|
+
gate: 'check_kernel_rule_bundle',
|
|
203
|
+
candidates: [...KERNEL_RULES].sort().map((name) => `${KERNEL_DIR}/${name}`),
|
|
204
|
+
repoRoot: REPO,
|
|
205
|
+
});
|
|
206
|
+
} catch (exc) {
|
|
207
|
+
if (exc instanceof DeadScopeError) {
|
|
208
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
209
|
+
return 3;
|
|
210
|
+
}
|
|
211
|
+
throw exc;
|
|
212
|
+
}
|
|
213
|
+
|
|
191
214
|
// Python: args.files or _git_changed_files(...). An empty list from
|
|
192
215
|
// `--files` (no values) is falsy in Python → falls through to git diff.
|
|
193
216
|
const files =
|