@event4u/agent-config 9.13.0 → 9.15.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 +113 -67
- package/README.md +9 -1
- package/dist/agent-src/commands/agents/audit.md +7 -7
- package/dist/agent-src/commands/agents/optimize.md +1 -1
- package/dist/agent-src/commands/analyze/reference-repo.md +1 -1
- package/dist/agent-src/commands/chat-history.md +1 -0
- package/dist/agent-src/commands/context.md +1 -0
- package/dist/agent-src/commands/council.md +1 -1
- package/dist/agent-src/commands/ghostwriter.md +1 -1
- package/dist/agent-src/commands/memory.md +2 -1
- package/dist/agent-src/commands/module.md +1 -0
- package/dist/agent-src/commands/optimize/agents-dir.md +1 -1
- package/dist/agent-src/commands/optimize/deep.md +150 -0
- package/dist/agent-src/commands/optimize/skills.md +5 -5
- package/dist/agent-src/commands/optimize.md +12 -6
- package/dist/agent-src/commands/override.md +1 -0
- package/dist/agent-src/commands/prediction-pool.md +4 -4
- package/dist/agent-src/commands/research/deep.md +1 -1
- package/dist/agent-src/commands/roadmap.md +2 -1
- package/dist/agent-src/commands/rule-compliance-audit.md +1 -1
- package/dist/agent-src/commands/tdd.md +1 -0
- package/dist/agent-src/commands/tests.md +1 -0
- package/dist/agent-src/commands/upstream-contribute.md +5 -5
- package/dist/agent-src/commands/video/from-script.md +1 -1
- package/dist/agent-src/commands/video/from-song.md +1 -1
- package/dist/agent-src/commands/video/scene.md +1 -1
- package/dist/agent-src/commands/video/stitch.md +1 -1
- package/dist/agent-src/commands/video/storyboard.md +1 -1
- package/dist/agent-src/commands/worktree/create.md +2 -1
- package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
- package/dist/agent-src/contexts/contracts/artifact-engagement-flow.md +5 -5
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +3 -3
- package/dist/agent-src/contexts/contracts/emergency-triage-block.md +2 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +4 -3
- package/dist/agent-src/contexts/execution/subagent-routing.md +19 -0
- package/dist/agent-src/contexts/override-system.md +1 -1
- package/dist/agent-src/ghostwriter/fictional-fixture-v1.md +1 -1
- package/dist/agent-src/rules/augment-edit-discipline.md +1 -1
- package/dist/agent-src/rules/autonomous-execution.md +2 -1
- package/dist/agent-src/rules/brand-consistency.md +1 -0
- package/dist/agent-src/rules/brand-source-of-truth.md +1 -0
- package/dist/agent-src/rules/code-comment-discipline.md +2 -0
- package/dist/agent-src/rules/communication-through-line.md +1 -0
- package/dist/agent-src/rules/content-quoting-floor.md +1 -0
- package/dist/agent-src/rules/cross-source-consistency.md +0 -1
- package/dist/agent-src/rules/downstream-changes.md +1 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +1 -0
- package/dist/agent-src/rules/external-reference-deep-dive.md +1 -0
- package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
- package/dist/agent-src/rules/finance-safety-floor.md +0 -1
- package/dist/agent-src/rules/icon-consistency.md +1 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
- package/dist/agent-src/rules/improve-before-implement.md +1 -1
- package/dist/agent-src/rules/invite-challenge.md +1 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +1 -0
- package/dist/agent-src/rules/media-governance-routing.md +4 -3
- package/dist/agent-src/rules/media-sync-ground-truth.md +1 -0
- package/dist/agent-src/rules/no-attribution-footers.md +1 -0
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +1 -0
- package/dist/agent-src/rules/no-pr-progress-comments.md +1 -0
- package/dist/agent-src/rules/onboarding-gate.md +1 -0
- package/dist/agent-src/rules/output-discipline.md +1 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +1 -0
- package/dist/agent-src/rules/preservation-guard.md +1 -0
- package/dist/agent-src/rules/question-not-instruction.md +1 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +1 -0
- package/dist/agent-src/rules/role-mode-adherence.md +1 -0
- package/dist/agent-src/rules/runtime-safety.md +1 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +1 -0
- package/dist/agent-src/rules/session-canary.md +9 -2
- package/dist/agent-src/rules/source-confidentiality.md +1 -0
- package/dist/agent-src/rules/source-of-truth.md +1 -0
- package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -0
- package/dist/agent-src/rules/think-before-action.md +1 -0
- package/dist/agent-src/rules/token-budget-discipline.md +1 -0
- package/dist/agent-src/rules/token-efficiency.md +1 -0
- package/dist/agent-src/rules/token-optimizer-maintenance.md +5 -5
- package/dist/agent-src/rules/tool-safety.md +1 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
- package/dist/agent-src/rules/user-interaction.md +1 -0
- package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
- package/dist/agent-src/skills/adr-create/SKILL.md +1 -1
- package/dist/agent-src/skills/check-refs/SKILL.md +1 -1
- package/dist/agent-src/skills/competitive-positioning/SKILL.md +1 -1
- package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
- package/dist/agent-src/skills/context-authoring/SKILL.md +1 -1
- package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +1 -1
- package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
- package/dist/agent-src/skills/github-ci/SKILL.md +1 -0
- package/dist/agent-src/skills/guideline-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/lint-skills/SKILL.md +1 -1
- package/dist/agent-src/skills/readme-reviewer/SKILL.md +1 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +8 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +4 -4
- package/dist/agent-src/skills/token-optimizer/SKILL.md +4 -4
- package/dist/agent-src/skills/upstream-contribute/SKILL.md +6 -6
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -0
- package/dist/agent-src/templates/AGENTS.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/command.md +1 -1
- package/dist/agent-src/templates/copilot-instructions.md +3 -3
- package/dist/agent-src/templates/rule.md +2 -2
- package/dist/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
- package/dist/agent-src/templates/skill-archive-note.md +5 -5
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/agent-config.js +66 -19
- package/dist/cli/agent-config.js.map +1 -1
- package/dist/cli/main.js +9 -4
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/python/workspace_drive.js +2 -0
- package/dist/cli/python/workspace_drive.js.map +1 -1
- package/dist/cli/python/workspace_drive_health.js +2 -0
- package/dist/cli/python/workspace_drive_health.js.map +1 -1
- package/dist/cli/python/workspace_hosts.js +2 -0
- package/dist/cli/python/workspace_hosts.js.map +1 -1
- package/dist/cli/python/workspace_sessions.js +2 -0
- package/dist/cli/python/workspace_sessions.js.map +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-A3HJ6NTI.js → chunk-63WSP6YZ.js} +2 -1
- package/dist/cli-delegate/{chunk-EJYIGZ3T.js → chunk-BKO2AZ7Z.js} +1 -1
- package/dist/cli-delegate/{chunk-QFKQLQ7X.js → chunk-H574KOUH.js} +340 -462
- package/dist/cli-delegate/chunk-NXFWFAYV.js +339 -0
- package/dist/cli-delegate/{chunk-TEGJU2JX.js → chunk-ZXXJLL5K.js} +2 -2
- package/dist/cli-delegate/cmd_conformance.js +29 -23
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_preflight.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +163 -103
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +729 -430
- package/dist/install/install.mjs +700 -625
- package/dist/install/rule_scope.js +18 -2
- package/dist/install/rule_scope.js.map +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +564 -424
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/claude_settings_hooks.js +30 -5
- package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
- package/dist/scripts/_lib/scoped_projection.js +223 -0
- package/dist/scripts/_lib/scoped_projection.js.map +1 -0
- package/dist/scripts/_lib/surface_tiers.js +125 -0
- package/dist/scripts/_lib/surface_tiers.js.map +1 -0
- package/dist/scripts/_lib/token_count.js +166 -0
- package/dist/scripts/_lib/token_count.js.map +1 -0
- package/dist/scripts/_lib/trigger_eval_floors.js +8 -0
- package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -1
- package/dist/scripts/condense.js +139 -9
- package/dist/scripts/condense.js.map +1 -1
- package/dist/scripts/project_thin_rules.js +450 -0
- package/dist/scripts/project_thin_rules.js.map +1 -0
- package/dist/server/routes/workspace.js +2 -0
- package/dist/server/routes/workspace.js.map +1 -1
- package/dist/server/schemas/settings.js +9 -3
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/index-DJY_vQVn.js +40 -0
- package/dist/ui/assets/index-DJY_vQVn.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/package.json +11 -11
- package/src/agent-src/templates/AGENTS.md +1 -1
- package/src/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
- package/src/config/agent-settings.template.yml +59 -11
- package/src/config/ci-local-parity.yml +8 -0
- package/src/config/discovery/unassigned-artefacts.yml +23 -25
- package/src/config/evaluator-budgets.json +35 -5
- package/src/config/gate-coverage.yml +147 -0
- package/src/config/gate-violation-baselines.json +25 -0
- package/src/install/rule_scope.ts +27 -2
- package/src/scripts/_cli/cmd_conformance.ts +13 -24
- package/src/scripts/_dispatch.bash +10 -0
- package/src/scripts/_lib/claude_settings_hooks.ts +30 -4
- package/src/scripts/_lib/exec_evidence.ts +5 -0
- package/src/scripts/_lib/gate_baseline.ts +213 -0
- package/src/scripts/_lib/scoped_projection.ts +262 -0
- package/src/scripts/_lib/tier_budget_routing.ts +234 -0
- package/src/scripts/_lib/tool_adapter_registry.ts +8 -1
- package/src/scripts/_lib/trigger_eval_floors.ts +9 -0
- package/src/scripts/_lib/trigger_routers.ts +17 -5
- package/src/scripts/adr/regenerate_index.ts +4 -3
- package/src/scripts/ai-video/lib/parse-blueprint.sh +1 -1
- package/src/scripts/ai_council/compile_corpus.ts +1 -1
- package/src/scripts/audit_cloud_compatibility.ts +37 -2
- package/src/scripts/audit_initial_context.ts +53 -19
- package/src/scripts/audit_likelihood.ts +31 -2
- package/src/scripts/audit_user_type_axis.ts +58 -11
- package/src/scripts/bench_hook_latency.ts +172 -41
- package/src/scripts/build_mcp_catalog.ts +12 -1
- package/src/scripts/check_always_budget.ts +122 -11
- package/src/scripts/check_artefact_count_messaging.ts +128 -15
- package/src/scripts/check_augment_description_cap.ts +24 -3
- package/src/scripts/check_ci_strict_superset.ts +150 -0
- package/src/scripts/check_cluster_patterns.ts +100 -0
- package/src/scripts/check_consumer_scope_flip.ts +26 -4
- package/src/scripts/check_evaluator_budgets.ts +198 -19
- package/src/scripts/check_gate_coverage.ts +337 -1
- package/src/scripts/check_generator_output_coverage.ts +2 -1
- package/src/scripts/check_iron_law_prominence.ts +1 -1
- package/src/scripts/check_kernel_rule_bundle.ts +1 -1
- package/src/scripts/check_no_new_legacy_path.ts +167 -3
- package/src/scripts/check_pack_size.ts +18 -4
- package/src/scripts/check_portability.ts +37 -7
- package/src/scripts/check_references.ts +19 -3
- package/src/scripts/check_release_pr_shape.ts +13 -0
- package/src/scripts/check_token_regression.ts +9 -2
- package/src/scripts/condense.ts +155 -12
- package/src/scripts/cost/budget.mjs +63 -2
- package/src/scripts/count_scoped_projection.ts +104 -0
- package/src/scripts/evaluator_umbrella.sh +34 -1
- package/src/scripts/generate_capability_matrix.ts +8 -0
- package/src/scripts/generate_router_coverage_from_matrix.ts +155 -0
- package/src/scripts/hook_manifest.yaml +17 -3
- package/src/scripts/hooks/block_config_weakening.ts +337 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/probe_throttle.ts +75 -0
- package/src/scripts/install-hooks.sh +24 -2
- package/src/scripts/install.ts +15 -92
- package/src/scripts/lint_agents_md.ts +9 -3
- package/src/scripts/lint_archived_skills.ts +1 -1
- package/src/scripts/lint_artefact_frontmatter.ts +185 -37
- package/src/scripts/lint_command_routing.ts +21 -52
- package/src/scripts/lint_command_verbs.ts +47 -28
- package/src/scripts/lint_context_spine_usage.ts +38 -12
- package/src/scripts/lint_framework_leakage.ts +25 -6
- package/src/scripts/lint_handoffs.ts +49 -2
- package/src/scripts/lint_media_policy_linkage.ts +62 -30
- package/src/scripts/lint_namespace.ts +92 -7
- package/src/scripts/lint_new_skill_gate.ts +39 -31
- package/src/scripts/lint_originality_shingles.ts +7 -1
- package/src/scripts/lint_override_kernel_guard.ts +21 -3
- package/src/scripts/lint_pack_boundaries.ts +461 -91
- package/src/scripts/lint_pack_dependencies.ts +24 -57
- package/src/scripts/lint_persona_governance.ts +1 -1
- package/src/scripts/lint_role_experiences.ts +34 -5
- package/src/scripts/lint_trigger_precision.ts +142 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +12 -201
- package/src/scripts/mcp_server/tools.ts +1 -1
- package/src/scripts/prepack-check.mjs +40 -1
- package/src/scripts/prepack_router_targets.d.mts +7 -0
- package/src/scripts/prepack_router_targets.mjs +93 -0
- package/src/scripts/print_required_checks.ts +72 -18
- package/src/scripts/profile_staleness_hook.ts +12 -0
- package/src/scripts/record_evaluator_measurements.mjs +105 -0
- package/src/scripts/recruit_preflight.sh +1 -1
- package/src/scripts/release.ts +105 -1
- package/src/scripts/router_target_paths.d.mts +4 -0
- package/src/scripts/router_target_paths.mjs +69 -0
- package/src/scripts/router_telemetry.ts +52 -1
- package/src/scripts/routing_doctor.ts +485 -0
- package/src/scripts/rule_trigger_eval.ts +403 -0
- package/src/scripts/schemas/rule.schema.json +5 -1
- package/src/scripts/session_canary_hook.ts +96 -5
- package/src/scripts/skill_linter.ts +90 -42
- package/src/scripts/smoke/skills.sh +1 -1
- package/src/scripts/sweep_dead_scan_roots.ts +356 -10
- package/src/scripts/trigger_coverage.ts +13 -2
- package/src/scripts/update_counts.ts +91 -12
- package/src/scripts/validate_telegraph_carveouts.ts +1 -1
- package/src/scripts/wrapper_freshness_hook.ts +14 -0
- package/src/server/schemas/settings.ts +19 -5
- package/dist/cli-delegate/chunk-TJIAM5FW.js +0 -133
- package/dist/ui/assets/index-BpSQt5vm.js +0 -40
- package/dist/ui/assets/index-BpSQt5vm.js.map +0 -1
|
@@ -794,30 +794,6 @@ function extractFrontmatterField(frontmatter: string, pattern: RegExp): string |
|
|
|
794
794
|
|
|
795
795
|
type YamlListItem = string | Record<string, string>;
|
|
796
796
|
|
|
797
|
-
function _parseTrustLevel(frontmatter: string): string | null {
|
|
798
|
-
const lines = splitlines(frontmatter);
|
|
799
|
-
let inBlock = false;
|
|
800
|
-
for (const line of lines) {
|
|
801
|
-
if (!inBlock) {
|
|
802
|
-
if (line.startsWith('trust:')) {
|
|
803
|
-
const rhs = line.slice('trust:'.length).trim();
|
|
804
|
-
if (rhs === '') {
|
|
805
|
-
inBlock = true;
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
continue;
|
|
809
|
-
}
|
|
810
|
-
if (line.startsWith(' level:')) {
|
|
811
|
-
return stripQuotes(line.slice(' level:'.length).trim());
|
|
812
|
-
}
|
|
813
|
-
if (line.startsWith(' ')) {
|
|
814
|
-
continue;
|
|
815
|
-
}
|
|
816
|
-
break;
|
|
817
|
-
}
|
|
818
|
-
return null;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
797
|
function stripQuotes(s: string): string {
|
|
822
798
|
return s.replace(/^["']/, '').replace(/["']$/, '');
|
|
823
799
|
}
|
|
@@ -932,13 +908,18 @@ function lint_router_frontmatter(
|
|
|
932
908
|
}
|
|
933
909
|
|
|
934
910
|
if (routesTo === null) {
|
|
935
|
-
|
|
936
|
-
|
|
911
|
+
// Self-contained carve-out (ADR-210): a rule whose body IS the
|
|
912
|
+
// constraint (prohibition / gate / format law — no procedure to
|
|
913
|
+
// delegate) certifies itself via `self_contained: true` instead of
|
|
914
|
+
// routing. Neither declared → error: the contract's "routes_to ≥ 1
|
|
915
|
+
// unless self-contained" is unenforceable without a marker.
|
|
916
|
+
const selfContained = /^self_contained:[ \t]*true[ \t]*$/m.test(frontmatter);
|
|
917
|
+
if (!selfContained) {
|
|
937
918
|
issues.push(
|
|
938
919
|
new Issue(
|
|
939
|
-
'
|
|
920
|
+
'error',
|
|
940
921
|
'router_routes_to_missing',
|
|
941
|
-
|
|
922
|
+
"Non-kernel rule declares neither routes_to: nor self_contained: true — route the body to its skill/guideline, or certify it self-contained per docs/contracts/rule-router.md",
|
|
942
923
|
),
|
|
943
924
|
);
|
|
944
925
|
}
|
|
@@ -2439,6 +2420,16 @@ export function gather_candidate_files_under(srcRoot: string): string[] {
|
|
|
2439
2420
|
*/
|
|
2440
2421
|
export let last_changed_scope_reason = '';
|
|
2441
2422
|
|
|
2423
|
+
/**
|
|
2424
|
+
* Whether the base-span diff (`base...HEAD`) itself ran cleanly.
|
|
2425
|
+
*
|
|
2426
|
+
* The verified-empty release outcome (see `main`) may only rest on an empty
|
|
2427
|
+
* change set that was actually COMPUTED — a base diff that errored (missing
|
|
2428
|
+
* tag object, corrupt ref) degrades to the worktree fallbacks and would
|
|
2429
|
+
* otherwise be indistinguishable from a genuinely empty span.
|
|
2430
|
+
*/
|
|
2431
|
+
export let last_changed_base_diff_ok = false;
|
|
2432
|
+
|
|
2442
2433
|
/**
|
|
2443
2434
|
* Candidate files changed against the resolved base.
|
|
2444
2435
|
*
|
|
@@ -2454,6 +2445,7 @@ export let last_changed_scope_reason = '';
|
|
|
2454
2445
|
export function gather_changed_candidate_files(root: string, since?: string | null): string[] {
|
|
2455
2446
|
const scope = resolveContentLintScope({ cwd: root, since: since ?? null });
|
|
2456
2447
|
last_changed_scope_reason = scope.reason;
|
|
2448
|
+
last_changed_base_diff_ok = false;
|
|
2457
2449
|
const diffCommands = [
|
|
2458
2450
|
['git', 'diff', '--name-only', `${scope.base}...HEAD`],
|
|
2459
2451
|
['git', 'diff', '--name-only', '--cached', 'HEAD'],
|
|
@@ -2461,10 +2453,15 @@ export function gather_changed_candidate_files(root: string, since?: string | nu
|
|
|
2461
2453
|
];
|
|
2462
2454
|
try {
|
|
2463
2455
|
let rawLines: string[] = [];
|
|
2456
|
+
let first = true;
|
|
2464
2457
|
for (const cmd of diffCommands) {
|
|
2465
2458
|
// A truncated diff would silently shrink the change set — the same
|
|
2466
2459
|
// degrade-to-zero shape this roadmap closes elsewhere.
|
|
2467
2460
|
const result = runCountedProbe(cmd[0] as string, cmd.slice(1), { cwd: root });
|
|
2461
|
+
if (first) {
|
|
2462
|
+
last_changed_base_diff_ok = result.ok;
|
|
2463
|
+
first = false;
|
|
2464
|
+
}
|
|
2468
2465
|
if (result.ok && result.stdout.trim()) {
|
|
2469
2466
|
rawLines = splitlines(result.stdout);
|
|
2470
2467
|
break;
|
|
@@ -4202,6 +4199,43 @@ function _release_scope_requires_findings(root: string, since?: string | null):
|
|
|
4202
4199
|
return scope.isRelease && scope.previousTag !== null;
|
|
4203
4200
|
}
|
|
4204
4201
|
|
|
4202
|
+
/**
|
|
4203
|
+
* True when the `--changed` corpus filter still matches tracked files at HEAD.
|
|
4204
|
+
*
|
|
4205
|
+
* The discriminator between a DEAD scope and a VERIFIED-EMPTY release span.
|
|
4206
|
+
* A release that touches no skill/rule file is a legitimate outcome — but only
|
|
4207
|
+
* provably so when the exact predicate `gather_changed_candidate_files`
|
|
4208
|
+
* applies to diff lines demonstrably still sees the corpus. Probing
|
|
4209
|
+
* `git ls-files` with that same predicate means a moved projection root
|
|
4210
|
+
* (the ADR-051 blindness class) fails this probe exactly as it would blind
|
|
4211
|
+
* the diff — so "empty because the root moved" cannot masquerade as
|
|
4212
|
+
* "empty because nothing changed".
|
|
4213
|
+
*/
|
|
4214
|
+
function _changed_filter_matches_head(root: string): boolean {
|
|
4215
|
+
// Probes the canonical projection root only (ADR-051): a corpus that
|
|
4216
|
+
// exists solely under a legacy path counts as dead, not alive.
|
|
4217
|
+
const result = runCountedProbe('git', ['ls-files', '--', 'dist/agent-src'], { cwd: root });
|
|
4218
|
+
if (!result.ok) {
|
|
4219
|
+
return false;
|
|
4220
|
+
}
|
|
4221
|
+
for (const raw of splitlines(result.stdout)) {
|
|
4222
|
+
const norm = raw.trim().replace(/\\/g, '/');
|
|
4223
|
+
if (!norm || norm.includes('/evals/')) {
|
|
4224
|
+
continue;
|
|
4225
|
+
}
|
|
4226
|
+
if (!norm.startsWith('dist/agent-src/') && !norm.includes('/dist/agent-src/')) {
|
|
4227
|
+
continue;
|
|
4228
|
+
}
|
|
4229
|
+
if (
|
|
4230
|
+
norm.endsWith('/SKILL.md') ||
|
|
4231
|
+
(norm.endsWith('.md') && (norm.includes('/rules/') || norm.includes('/commands/')))
|
|
4232
|
+
) {
|
|
4233
|
+
return true;
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
return false;
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4205
4239
|
export function main(argv: string[]): number {
|
|
4206
4240
|
let args: Args;
|
|
4207
4241
|
try {
|
|
@@ -4244,25 +4278,39 @@ export function main(argv: string[]): number {
|
|
|
4244
4278
|
process.stdout.write(`${format_json([])}\n`);
|
|
4245
4279
|
}
|
|
4246
4280
|
// A release PR that resolved a real tag span and still matched
|
|
4247
|
-
// nothing
|
|
4248
|
-
//
|
|
4281
|
+
// nothing needs a discriminator: DEAD scope (the diff errored, or
|
|
4282
|
+
// the corpus filter no longer matches anything at HEAD — the
|
|
4283
|
+
// ADR-051 moved-root blindness) fails; VERIFIED-EMPTY (the base
|
|
4284
|
+
// diff ran cleanly AND the filter provably still sees tracked
|
|
4285
|
+
// corpus files) is a legitimate release that simply changed no
|
|
4286
|
+
// skill/rule — failing it would red every scripts-/docs-only
|
|
4287
|
+
// release forever. Mirrors the scan-scope regime
|
|
4249
4288
|
// (`_lib/scan_scope.ts`; exit 2, as `check_iron_law_prominence`).
|
|
4250
4289
|
// Ordinary PRs keep the historical exit 0: a change set with no
|
|
4251
4290
|
// skill or rule in it is a legitimate empty scope, not a dead one.
|
|
4252
4291
|
if (args.changed && _release_scope_requires_findings(root, args.since)) {
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
}
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4292
|
+
if (last_changed_base_diff_ok && _changed_filter_matches_head(root)) {
|
|
4293
|
+
process.stderr.write(
|
|
4294
|
+
'✅ verified-empty release scope: the base-span diff ran cleanly and ' +
|
|
4295
|
+
'the corpus filter still matches tracked files at HEAD — this ' +
|
|
4296
|
+
'release genuinely changes no skill/rule file. The gate is alive, ' +
|
|
4297
|
+
'not blind.\n',
|
|
4298
|
+
);
|
|
4299
|
+
} else {
|
|
4300
|
+
try {
|
|
4301
|
+
assertScanned({
|
|
4302
|
+
gate: 'skill_linter --changed (release PR)',
|
|
4303
|
+
scanned: 0,
|
|
4304
|
+
units: 'skill/rule files',
|
|
4305
|
+
roots: [last_changed_scope_reason],
|
|
4306
|
+
});
|
|
4307
|
+
} catch (exc) {
|
|
4308
|
+
if (exc instanceof DeadScopeError) {
|
|
4309
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
4310
|
+
return 2;
|
|
4311
|
+
}
|
|
4312
|
+
throw exc;
|
|
4264
4313
|
}
|
|
4265
|
-
throw exc;
|
|
4266
4314
|
}
|
|
4267
4315
|
}
|
|
4268
4316
|
process.stderr.write('No matching skill/rule files found.\n');
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# scripts/smoke/skills.sh — skills-tier smoke (step-11 Phase 3 Step 5).
|
|
3
3
|
#
|
|
4
4
|
# Picks 5 random skills (deterministic seed = epoch day) from
|
|
5
|
-
#
|
|
5
|
+
# src/skills/*/SKILL.md and asserts:
|
|
6
6
|
# 1. SKILL.md exists.
|
|
7
7
|
# 2. Frontmatter parses + validates against scripts/schemas/skill.schema.json.
|
|
8
8
|
# 3. `name:` field matches the parent directory name.
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* sweep_dead_scan_roots — which gate scripts walk a root that does not exist?
|
|
4
4
|
*
|
|
5
5
|
* Population expansion for `road-to-gates-that-can-fail` Phase 1. The committed
|
|
6
|
-
* census (`agents/evidence/reports/gate-scope-census.md`)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* census (`agents/evidence/reports/gate-scope-census.md`) began as a manual audit
|
|
7
|
+
* of 14 gates; since 2026-08-02 this script GENERATES it over the full population
|
|
8
|
+
* via `--census`, so it is re-runnable and a stale row shows up in a diff.
|
|
9
9
|
*
|
|
10
10
|
* WHY IT EXISTS: two more dead gates were found OUTSIDE the censused 14 while
|
|
11
11
|
* doing thematically unrelated work — `audit_skill_overlap` (rooted at a
|
|
@@ -73,7 +73,9 @@
|
|
|
73
73
|
* ── EXIT CONTRACT (one meaning per code, so red stays informative) ─────────
|
|
74
74
|
*
|
|
75
75
|
* 0 clean
|
|
76
|
-
* 1
|
|
76
|
+
* 1 class-A finding(s) ABOVE the recorded ratchet baseline
|
|
77
|
+
* (`src/config/gate-violation-baselines.json`). With no baseline entry this
|
|
78
|
+
* is any class-A finding at all. Nothing else returns 1.
|
|
77
79
|
* 2 self-test failure — the extractor is blind; output is not trustworthy.
|
|
78
80
|
* 3 ledger hygiene — stale entry or over cap. The sweep ran fine; the LIST
|
|
79
81
|
* needs work.
|
|
@@ -85,7 +87,13 @@
|
|
|
85
87
|
*
|
|
86
88
|
* Usage:
|
|
87
89
|
* ./scripts-run src/scripts/sweep_dead_scan_roots [--quiet] [--json <path>]
|
|
88
|
-
* [--
|
|
90
|
+
* [--census <path>] [--root <dir>]
|
|
91
|
+
* [--ledger <path>]
|
|
92
|
+
*
|
|
93
|
+
* `--census` writes the full scan-scope census — every gate, every root it
|
|
94
|
+
* reads, and the unit count under that root today. The sweep answers "which
|
|
95
|
+
* roots are dead"; the census answers "what does each gate read, and how much",
|
|
96
|
+
* so the NEXT container move shows up as a count dropping in a diff.
|
|
89
97
|
*/
|
|
90
98
|
|
|
91
99
|
import * as fs from 'node:fs';
|
|
@@ -93,6 +101,9 @@ import * as path from 'node:path';
|
|
|
93
101
|
import process from 'node:process';
|
|
94
102
|
import { fileURLToPath } from 'node:url';
|
|
95
103
|
|
|
104
|
+
import * as agentSrc from './_lib/agent_src.js';
|
|
105
|
+
import { checkRatchet } from './_lib/gate_baseline.js';
|
|
106
|
+
|
|
96
107
|
const _HERE = fileURLToPath(import.meta.url);
|
|
97
108
|
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
98
109
|
const DEFAULT_SCRIPTS = path.join(REPO, 'src', 'scripts');
|
|
@@ -135,9 +146,24 @@ export interface Finding {
|
|
|
135
146
|
evidence: string;
|
|
136
147
|
}
|
|
137
148
|
|
|
149
|
+
/** Every root the extractor saw for one gate, dead or alive — the census row source. */
|
|
150
|
+
export interface RootRef {
|
|
151
|
+
rel: string;
|
|
152
|
+
/** Declaring identifier(s), or `(inline)` / `(array)` / `(spec-table)`. */
|
|
153
|
+
names: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
138
156
|
export interface Analysis {
|
|
139
157
|
confirmed: Finding[];
|
|
140
158
|
unproven: Finding[];
|
|
159
|
+
/**
|
|
160
|
+
* Census input: ALL extracted roots, not only the missing ones the sweep
|
|
161
|
+
* reports as findings. The sweep answers "which roots are dead"; the census
|
|
162
|
+
* answers "what does every gate read, and how much" — a root+count pair per
|
|
163
|
+
* gate is what makes the NEXT container move visible in a diff instead of
|
|
164
|
+
* silently disarming a gate for weeks.
|
|
165
|
+
*/
|
|
166
|
+
roots: RootRef[];
|
|
141
167
|
}
|
|
142
168
|
|
|
143
169
|
export interface Disposition {
|
|
@@ -345,9 +371,11 @@ export function analyze(src: string, repoRoot: string): Analysis {
|
|
|
345
371
|
|
|
346
372
|
const confirmed: Finding[] = [];
|
|
347
373
|
const unproven: Finding[] = [];
|
|
374
|
+
const roots = new Map<string, string>();
|
|
348
375
|
const missing = (rel: string): boolean => !fs.existsSync(path.join(repoRoot, rel));
|
|
349
376
|
|
|
350
377
|
for (const [rel, e] of found) {
|
|
378
|
+
roots.set(rel, [...e.names].join(',') || '(inline)');
|
|
351
379
|
if (!missing(rel)) continue;
|
|
352
380
|
let evidence: string | null =
|
|
353
381
|
e.inlineRead ? 'inline-read' : (arrayHits.get(rel) ?? specHits.get(rel) ?? null);
|
|
@@ -376,7 +404,30 @@ export function analyze(src: string, repoRoot: string): Analysis {
|
|
|
376
404
|
for (const [rel, ev] of arrayHits) push(confirmed, [], rel, '(array)', ev);
|
|
377
405
|
for (const rel of arrayUnproven) push(unproven, unproven, rel, '(array)', '—');
|
|
378
406
|
|
|
379
|
-
|
|
407
|
+
// Resolver-reached roots: a gate calling `SRC_SKILLS()` reads `src/skills`
|
|
408
|
+
// just as surely as one joining the literal, and the census must see both.
|
|
409
|
+
for (const [getter, rel] of resolverRoots(repoRoot)) {
|
|
410
|
+
if (new RegExp(`\\b${getter}\\s*\\(\\s*\\)`).test(src) && !roots.has(rel)) {
|
|
411
|
+
roots.set(rel, `${getter}()`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Census-only permissive pass — see `censusOnlyRoots` for why it may never
|
|
416
|
+
// feed the finding path.
|
|
417
|
+
for (const [rel, name] of censusOnlyRoots(src, repoRoot)) {
|
|
418
|
+
if (!roots.has(rel)) roots.set(rel, name);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
for (const rel of specHits.keys()) if (!roots.has(rel)) roots.set(rel, '(spec-table)');
|
|
422
|
+
for (const rel of specUnproven) if (!roots.has(rel)) roots.set(rel, '(spec-table)');
|
|
423
|
+
for (const rel of arrayHits.keys()) if (!roots.has(rel)) roots.set(rel, '(array)');
|
|
424
|
+
for (const rel of arrayUnproven) if (!roots.has(rel)) roots.set(rel, '(array)');
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
confirmed,
|
|
428
|
+
unproven,
|
|
429
|
+
roots: [...roots].sort(([a], [b]) => a.localeCompare(b)).map(([rel, names]) => ({ rel, names })),
|
|
430
|
+
};
|
|
380
431
|
}
|
|
381
432
|
|
|
382
433
|
/**
|
|
@@ -411,19 +462,279 @@ export function loadLedger(file: string): Disposition[] {
|
|
|
411
462
|
return Array.isArray(data) ? data : [];
|
|
412
463
|
}
|
|
413
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Roots reached through the shared resolver rather than a path literal.
|
|
467
|
+
*
|
|
468
|
+
* Without this, the census is blind to exactly the gates that did the right
|
|
469
|
+
* thing: a gate repaired from `path.join(ROOT, '.agent-src.uncondensed')` to
|
|
470
|
+
* `SRC_SKILLS()` would VANISH from the census, so adopting the resolver — which
|
|
471
|
+
* the roadmap explicitly prefers over new literals — would look like losing
|
|
472
|
+
* coverage. The map is built by calling the real getters, not by copying their
|
|
473
|
+
* values, so it cannot drift from `_lib/agent_src.ts`.
|
|
474
|
+
*/
|
|
475
|
+
function resolverRoots(repoRoot: string): ReadonlyMap<string, string> {
|
|
476
|
+
const getters: Record<string, () => string> = {
|
|
477
|
+
SRC: agentSrc.SRC,
|
|
478
|
+
SRC_SKILLS: agentSrc.SRC_SKILLS,
|
|
479
|
+
SRC_RULES: agentSrc.SRC_RULES,
|
|
480
|
+
SRC_AGENT: agentSrc.SRC_AGENT,
|
|
481
|
+
SRC_DOMAINS: agentSrc.SRC_DOMAINS,
|
|
482
|
+
LEGACY_SRC: agentSrc.LEGACY_SRC,
|
|
483
|
+
PACKAGES: agentSrc.PACKAGES,
|
|
484
|
+
};
|
|
485
|
+
const out = new Map<string, string>();
|
|
486
|
+
for (const [name, fn] of Object.entries(getters)) {
|
|
487
|
+
let abs: string;
|
|
488
|
+
try {
|
|
489
|
+
abs = fn();
|
|
490
|
+
} catch {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const rel = path.relative(repoRoot, abs).split(path.sep).join('/');
|
|
494
|
+
if (rel && !rel.startsWith('..')) out.set(name, rel);
|
|
495
|
+
}
|
|
496
|
+
return out;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Permissive root extraction — CENSUS ONLY, never the finding path.
|
|
501
|
+
*
|
|
502
|
+
* The finding extractor is deliberately strict: it reports a dead root only
|
|
503
|
+
* with positive read evidence, because a false "this gate is dead" turns CI red
|
|
504
|
+
* on a lie. That precision costs recall — a gate writing
|
|
505
|
+
* `const SOURCE_DIR = 'src'` and later `path.join(REPO, SOURCE_DIR)` is
|
|
506
|
+
* invisible to it, and roughly half the population is shaped that way.
|
|
507
|
+
*
|
|
508
|
+
* The census has the opposite trade: a missed root is a coverage hole, and a
|
|
509
|
+
* spurious row is cheap. So this pass substitutes single-literal string consts
|
|
510
|
+
* into `path.join(...)` calls — and then keeps ONLY roots that EXIST on disk.
|
|
511
|
+
* That filter is what makes it safe: a pass that cannot yield a missing path
|
|
512
|
+
* cannot manufacture a dead-root finding, no matter how loose its parsing.
|
|
513
|
+
* Precision stays where red exits are decided; recall goes where the record is.
|
|
514
|
+
*/
|
|
515
|
+
export function censusOnlyRoots(src: string, repoRoot: string): Map<string, string> {
|
|
516
|
+
const consts = new Map<string, string>();
|
|
517
|
+
const cre = /(?:const|let)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*'([^'\n]+)'/g;
|
|
518
|
+
let cm: RegExpExecArray | null;
|
|
519
|
+
while ((cm = cre.exec(src)) !== null) {
|
|
520
|
+
const [, name, value] = cm;
|
|
521
|
+
if (value !== undefined && !value.includes(' ') && value !== '') consts.set(name as string, value);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const out = new Map<string, string>();
|
|
525
|
+
const jre = /path\.join\(([^)]*)\)/g;
|
|
526
|
+
let jm: RegExpExecArray | null;
|
|
527
|
+
while ((jm = jre.exec(src)) !== null) {
|
|
528
|
+
const parts = (jm[1] as string).split(',').map((p) => p.trim()).filter((p) => p !== '');
|
|
529
|
+
if (parts.length < 2) continue;
|
|
530
|
+
const base = parts[0] as string;
|
|
531
|
+
if (!BASE_IDS.has(base)) continue;
|
|
532
|
+
|
|
533
|
+
const segs: string[] = [];
|
|
534
|
+
for (const raw of parts.slice(1)) {
|
|
535
|
+
const lit = /^'([^']*)'$/.exec(raw);
|
|
536
|
+
if (lit) {
|
|
537
|
+
segs.push(lit[1] as string);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const viaConst = consts.get(raw);
|
|
541
|
+
if (viaConst !== undefined) {
|
|
542
|
+
segs.push(viaConst);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
break; // a segment we cannot resolve ends the path — a prefix is still a real root
|
|
546
|
+
}
|
|
547
|
+
if (segs.length === 0) continue;
|
|
548
|
+
const rel = segs.join('/').replace(/\/+$/, '');
|
|
549
|
+
if (rel === '' || rel.startsWith('..')) continue;
|
|
550
|
+
// `node_modules/.bin/tsx` is a tool path a gate SPAWNS, not a corpus it reads.
|
|
551
|
+
if (rel === 'node_modules' || rel.startsWith('node_modules/')) continue;
|
|
552
|
+
if (!fs.existsSync(path.join(repoRoot, rel))) continue; // the safety filter — see the doc comment
|
|
553
|
+
if (!out.has(rel)) out.set(rel, '(census-only)');
|
|
554
|
+
}
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Directories never worth walking for a unit count. */
|
|
559
|
+
const COUNT_SKIP: ReadonlySet<string> = new Set(['node_modules', '.git', '.venv', '__pycache__']);
|
|
560
|
+
|
|
561
|
+
/** Depth cap for the unit count — deep enough for every artefact tree here, cheap enough to re-run. */
|
|
562
|
+
const COUNT_MAX_DEPTH = 12;
|
|
563
|
+
|
|
564
|
+
export interface RootCount {
|
|
565
|
+
/** `dir` · `file` · `absent` — absent is the dead-root case the sweep reports separately. */
|
|
566
|
+
kind: 'dir' | 'file' | 'absent';
|
|
567
|
+
/** Files under the root (recursive), 1 for a file, 0 for absent. */
|
|
568
|
+
units: number;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Count what a root actually contains, on this tree, right now.
|
|
573
|
+
*
|
|
574
|
+
* The number is deliberately generic — files under the root — not the gate's
|
|
575
|
+
* own notion of a unit (rules, skills, declarers). A gate-specific count would
|
|
576
|
+
* mean reimplementing 200 gates' filters here and would rot the moment one
|
|
577
|
+
* changed. What the census needs is a value that MOVES when the tree moves, so
|
|
578
|
+
* a container migration shows up as a diff instead of as silence.
|
|
579
|
+
*/
|
|
580
|
+
export function countUnits(repoRoot: string, rel: string): RootCount {
|
|
581
|
+
const abs = path.join(repoRoot, rel);
|
|
582
|
+
let st: fs.Stats;
|
|
583
|
+
try {
|
|
584
|
+
st = fs.statSync(abs);
|
|
585
|
+
} catch {
|
|
586
|
+
return { kind: 'absent', units: 0 };
|
|
587
|
+
}
|
|
588
|
+
if (st.isFile()) return { kind: 'file', units: 1 };
|
|
589
|
+
if (!st.isDirectory()) return { kind: 'absent', units: 0 };
|
|
590
|
+
|
|
591
|
+
let units = 0;
|
|
592
|
+
const walk = (dir: string, depth: number): void => {
|
|
593
|
+
if (depth > COUNT_MAX_DEPTH) return;
|
|
594
|
+
let entries: fs.Dirent[];
|
|
595
|
+
try {
|
|
596
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
597
|
+
} catch {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
for (const e of entries) {
|
|
601
|
+
if (e.isDirectory()) {
|
|
602
|
+
if (!COUNT_SKIP.has(e.name)) walk(path.join(dir, e.name), depth + 1);
|
|
603
|
+
} else if (e.isFile()) {
|
|
604
|
+
units += 1;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
walk(abs, 0);
|
|
609
|
+
return { kind: 'dir', units };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export interface CensusRow {
|
|
613
|
+
gate: string;
|
|
614
|
+
rel: string;
|
|
615
|
+
names: string;
|
|
616
|
+
kind: RootCount['kind'];
|
|
617
|
+
units: number;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Build the census: one row per (gate, extracted root), plus an explicit row
|
|
622
|
+
* for every gate the extractor found NO root in.
|
|
623
|
+
*
|
|
624
|
+
* The no-root rows are the honest half. A census that silently omits the gates
|
|
625
|
+
* it could not read would claim coverage it does not have — the same
|
|
626
|
+
* green-on-nothing shape this whole sweep exists to end.
|
|
627
|
+
*/
|
|
628
|
+
export function buildCensus(population: readonly string[], results: ReadonlyMap<string, Analysis>, repoRoot: string): CensusRow[] {
|
|
629
|
+
const rows: CensusRow[] = [];
|
|
630
|
+
for (const file of population) {
|
|
631
|
+
const gate = file.replace(/\.ts$/, '');
|
|
632
|
+
const analysis = results.get(file);
|
|
633
|
+
const refs = analysis?.roots ?? [];
|
|
634
|
+
if (refs.length === 0) {
|
|
635
|
+
rows.push({ gate, rel: '(no literal root extracted)', names: '—', kind: 'absent', units: 0 });
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
for (const ref of refs) {
|
|
639
|
+
const c = countUnits(repoRoot, ref.rel);
|
|
640
|
+
rows.push({ gate, rel: ref.rel, names: ref.names, kind: c.kind, units: c.units });
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return rows;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function renderCensus(rows: readonly CensusRow[], population: number, today: string): string {
|
|
647
|
+
const dead = rows.filter((r) => r.kind === 'absent' && r.rel !== '(no literal root extracted)');
|
|
648
|
+
const noRoot = rows.filter((r) => r.rel === '(no literal root extracted)');
|
|
649
|
+
const live = rows.filter((r) => r.kind !== 'absent');
|
|
650
|
+
const gatesWithRoots = new Set(live.map((r) => r.gate)).size;
|
|
651
|
+
|
|
652
|
+
const out: string[] = [];
|
|
653
|
+
out.push('# Gate scan-scope census');
|
|
654
|
+
out.push('');
|
|
655
|
+
out.push(`> Generated by \`./scripts-run src/scripts/sweep_dead_scan_roots --census <path>\` on ${today}.`);
|
|
656
|
+
out.push('> Do not hand-edit — re-run the command. A row that disagrees with a fresh');
|
|
657
|
+
out.push('> run is the signal this file exists to produce.');
|
|
658
|
+
out.push('');
|
|
659
|
+
out.push('## Why');
|
|
660
|
+
out.push('');
|
|
661
|
+
out.push('ADR-051 moved the source container. Gates carrying a hardcoded literal root');
|
|
662
|
+
out.push('kept exiting 0 while scanning zero files, for weeks, because nothing recorded');
|
|
663
|
+
out.push('what each gate was supposed to be reading. This census is that record: every');
|
|
664
|
+
out.push('gate, every root it reads, and how much is under that root **today**. The next');
|
|
665
|
+
out.push('container move shows up here as a count dropping to zero in a diff, instead of');
|
|
666
|
+
out.push('as a green checkmark over an empty directory.');
|
|
667
|
+
out.push('');
|
|
668
|
+
out.push('## Headline');
|
|
669
|
+
out.push('');
|
|
670
|
+
out.push('| Metric | Value |');
|
|
671
|
+
out.push('|---|---|');
|
|
672
|
+
out.push(`| Gate scripts in population | ${population} |`);
|
|
673
|
+
out.push(`| Gates with at least one resolvable root | ${gatesWithRoots} |`);
|
|
674
|
+
out.push(`| Gates with no literal root the extractor can see | ${noRoot.length} |`);
|
|
675
|
+
out.push(`| Roots resolved and counted | ${live.length} |`);
|
|
676
|
+
out.push(`| Roots that do not exist on this tree | ${dead.length} |`);
|
|
677
|
+
out.push('');
|
|
678
|
+
out.push('## Scope — what this census does NOT cover');
|
|
679
|
+
out.push('');
|
|
680
|
+
out.push('- **The unit count is files under the root, not the gate\'s own unit.** A gate');
|
|
681
|
+
out.push(' filtering to `*.md` reports fewer units than the row shows. The number is a');
|
|
682
|
+
out.push(' movement detector, not a gate-internal assertion — reimplementing 200 filters');
|
|
683
|
+
out.push(' here would rot on the first filter change.');
|
|
684
|
+
out.push('- **Roots the static extractor cannot see** are listed as');
|
|
685
|
+
out.push(' `(no literal root extracted)`, not omitted. Known blind spots: roots read from');
|
|
686
|
+
out.push(' a config file, bases outside the accepted repo-root identifier set, glob-library');
|
|
687
|
+
out.push(' walks, and template-literal paths. Those gates are uncovered, and the row says so.');
|
|
688
|
+
out.push('- **Nested helper directories** under `src/scripts/` are outside the population —');
|
|
689
|
+
out.push(' the sweep reads the top level only.');
|
|
690
|
+
out.push('- A dead root here is a *finding*, not a repair. Repairs are the roadmap\'s job;');
|
|
691
|
+
out.push(' this file only makes them visible.');
|
|
692
|
+
out.push('');
|
|
693
|
+
out.push('## Roots that do not exist on this tree');
|
|
694
|
+
out.push('');
|
|
695
|
+
if (dead.length === 0) {
|
|
696
|
+
out.push('None. Every extracted root resolves.');
|
|
697
|
+
} else {
|
|
698
|
+
out.push('| Gate | Declared root | Declared by |');
|
|
699
|
+
out.push('|---|---|---|');
|
|
700
|
+
for (const r of dead) out.push(`| \`${r.gate}\` | \`${r.rel}\` | \`${r.names}\` |`);
|
|
701
|
+
}
|
|
702
|
+
out.push('');
|
|
703
|
+
out.push('## Full census');
|
|
704
|
+
out.push('');
|
|
705
|
+
out.push('| Gate | Root | Kind | Units | Declared by |');
|
|
706
|
+
out.push('|---|---|---|---|---|');
|
|
707
|
+
for (const r of rows) {
|
|
708
|
+
const rootCell = r.rel === '(no literal root extracted)' ? '_(none extracted)_' : `\`${r.rel}\``;
|
|
709
|
+
const units = r.kind === 'absent' ? '**0**' : String(r.units);
|
|
710
|
+
out.push(`| \`${r.gate}\` | ${rootCell} | ${r.kind} | ${units} | \`${r.names}\` |`);
|
|
711
|
+
}
|
|
712
|
+
out.push('');
|
|
713
|
+
out.push('## Reproducing');
|
|
714
|
+
out.push('');
|
|
715
|
+
out.push('```bash');
|
|
716
|
+
out.push('./scripts-run src/scripts/sweep_dead_scan_roots --census agents/evidence/reports/gate-scope-census.md');
|
|
717
|
+
out.push('git diff --stat agents/evidence/reports/gate-scope-census.md # empty = the tree has not moved');
|
|
718
|
+
out.push('```');
|
|
719
|
+
out.push('');
|
|
720
|
+
return `${out.join('\n')}\n`;
|
|
721
|
+
}
|
|
722
|
+
|
|
414
723
|
interface Args {
|
|
415
724
|
quiet: boolean;
|
|
416
725
|
json: string | null;
|
|
726
|
+
census: string | null;
|
|
417
727
|
scripts: string;
|
|
418
728
|
ledger: string;
|
|
419
729
|
}
|
|
420
730
|
|
|
421
731
|
export function parseArgs(argv: string[]): Args {
|
|
422
|
-
const a: Args = { quiet: false, json: null, scripts: DEFAULT_SCRIPTS, ledger: DEFAULT_LEDGER };
|
|
732
|
+
const a: Args = { quiet: false, json: null, census: null, scripts: DEFAULT_SCRIPTS, ledger: DEFAULT_LEDGER };
|
|
423
733
|
for (let i = 0; i < argv.length; i += 1) {
|
|
424
734
|
const arg = argv[i];
|
|
425
735
|
if (arg === '--quiet') a.quiet = true;
|
|
426
736
|
else if (arg === '--json') a.json = argv[(i += 1)] ?? '';
|
|
737
|
+
else if (arg === '--census') a.census = argv[(i += 1)] ?? '';
|
|
427
738
|
else if (arg === '--root') a.scripts = path.resolve(REPO, argv[(i += 1)] ?? DEFAULT_SCRIPTS);
|
|
428
739
|
else if (arg === '--ledger') a.ledger = path.resolve(REPO, argv[(i += 1)] ?? DEFAULT_LEDGER);
|
|
429
740
|
else {
|
|
@@ -463,9 +774,14 @@ export function main(argv: string[]): number {
|
|
|
463
774
|
const pop = fs.existsSync(args.scripts)
|
|
464
775
|
? fs.readdirSync(args.scripts).filter((f) => GATE_RE.test(f)).sort()
|
|
465
776
|
: [];
|
|
777
|
+
// Census needs every gate's roots; the finding report needs only the ones
|
|
778
|
+
// with hits. Keep both, and filter at the point of use rather than dropping
|
|
779
|
+
// data here — `census` is the superset.
|
|
780
|
+
const census = new Map<string, Analysis>();
|
|
466
781
|
const results = new Map<string, Analysis>();
|
|
467
782
|
for (const f of pop) {
|
|
468
783
|
const r = analyze(fs.readFileSync(path.join(args.scripts, f), 'utf-8'), repoRoot);
|
|
784
|
+
census.set(f, r);
|
|
469
785
|
if (r.confirmed.length || r.unproven.length) results.set(f, r);
|
|
470
786
|
}
|
|
471
787
|
|
|
@@ -514,7 +830,11 @@ export function main(argv: string[]): number {
|
|
|
514
830
|
unproven: unprovenCount,
|
|
515
831
|
class_a: classA,
|
|
516
832
|
stale: stale.map((s) => `${s.script}:${s.rel}`),
|
|
517
|
-
findings: [...results].map(([script, r]) => ({
|
|
833
|
+
findings: [...results].map(([script, r]) => ({
|
|
834
|
+
script,
|
|
835
|
+
confirmed: r.confirmed,
|
|
836
|
+
unproven: r.unproven,
|
|
837
|
+
})),
|
|
518
838
|
},
|
|
519
839
|
null,
|
|
520
840
|
2,
|
|
@@ -522,6 +842,18 @@ export function main(argv: string[]): number {
|
|
|
522
842
|
);
|
|
523
843
|
}
|
|
524
844
|
|
|
845
|
+
if (args.census) {
|
|
846
|
+
const rows = buildCensus(pop, census, repoRoot);
|
|
847
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
848
|
+
fs.mkdirSync(path.dirname(args.census), { recursive: true });
|
|
849
|
+
fs.writeFileSync(args.census, renderCensus(rows, pop.length, today));
|
|
850
|
+
if (!args.quiet) {
|
|
851
|
+
process.stdout.write(
|
|
852
|
+
`sweep_dead_scan_roots: census → ${args.census} (${rows.length} row(s) over ${pop.length} gate(s))\n`,
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
525
857
|
if (!args.quiet) {
|
|
526
858
|
process.stdout.write(`sweep_dead_scan_roots: ${pop.length} gate script(s) scanned\n`);
|
|
527
859
|
for (const l of lines) process.stdout.write(`${l}\n`);
|
|
@@ -534,14 +866,28 @@ export function main(argv: string[]): number {
|
|
|
534
866
|
);
|
|
535
867
|
}
|
|
536
868
|
|
|
537
|
-
//
|
|
538
|
-
|
|
869
|
+
// Class-A findings are judged against the ratchet, not against zero.
|
|
870
|
+
//
|
|
871
|
+
// This gate shipped deliberately OUTSIDE `task ci` because it exits 1 on
|
|
872
|
+
// pre-existing class-A debt, and "a gate that turns CI red on debt it did
|
|
873
|
+
// not create" is the exact failure this track exists to avoid — its own
|
|
874
|
+
// Taskfile entry says so, and defers the wiring to the
|
|
875
|
+
// `dead-gate-finding-triage` disposition. That disposition is now decided
|
|
876
|
+
// (repair + ratchet, council 2026-08-02), so the deferral is spent: a count
|
|
877
|
+
// at or under the recorded baseline passes and the sweep can run in CI,
|
|
878
|
+
// while any RISE still exits 1. A tree with no baseline file behaves exactly
|
|
879
|
+
// as before — any class-A finding fails.
|
|
880
|
+
const ratchet = checkRatchet({ gate: 'sweep_dead_scan_roots', actual: classA, repoRoot });
|
|
881
|
+
const exit = !ratchet.ok ? 1 : stale.length > 0 ? 3 : 0;
|
|
539
882
|
if (!args.quiet || exit !== 0) {
|
|
540
883
|
process.stdout.write(
|
|
541
884
|
`sweep_dead_scan_roots: ${confirmedCount} confirmed, ${unprovenCount} unproven, ` +
|
|
542
885
|
`${classA} class-A, ${stale.length} stale → exit ${exit}\n`,
|
|
543
886
|
);
|
|
544
887
|
}
|
|
888
|
+
if (classA > 0 || ratchet.status !== 'unbaselined') {
|
|
889
|
+
(ratchet.ok ? process.stdout : process.stderr).write(`${ratchet.message}\n`);
|
|
890
|
+
}
|
|
545
891
|
return exit;
|
|
546
892
|
}
|
|
547
893
|
|