@event4u/agent-config 16.2.0 → 16.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +89 -237
- package/README.md +14 -12
- package/dist/agent-src/commands/analyze/inbox.md +1 -1
- package/dist/agent-src/commands/analyze/roadmap-repos.md +1 -1
- package/dist/agent-src/commands/bug/fix.md +17 -2
- package/dist/agent-src/commands/feature/plan.md +1 -1
- package/dist/agent-src/commands/feature/roadmap.md +1 -1
- package/dist/agent-src/commands/fix/ci.md +1 -1
- package/dist/agent-src/commands/implement-ticket.md +1 -1
- package/dist/agent-src/commands/jira-ticket.md +1 -1
- package/dist/agent-src/commands/roadmap/create.md +1 -1
- package/dist/agent-src/commands/roadmap/materialize.md +1 -1
- package/dist/agent-src/commands/roadmap/process-full.md +4 -4
- package/dist/agent-src/commands/roadmap/resolve-blockers.md +361 -0
- package/dist/agent-src/commands/roadmap/triage-parked.md +325 -0
- package/dist/agent-src/commands/roadmap.md +18 -4
- package/dist/agent-src/commands/tests/create.md +17 -3
- package/dist/agent-src/commands/tests/execute.md +22 -5
- package/dist/agent-src/contexts/authority/decision-revisit-gate-enforcement.md +43 -0
- package/dist/agent-src/contexts/communication/rules-auto/design-fidelity-enforcement.md +24 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +6 -6
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +32 -2
- package/dist/agent-src/contexts/execution/session-canary-enforcement-history.md +67 -0
- package/dist/agent-src/contexts/execution/toolchain-resolver.md +101 -7
- package/dist/agent-src/rules/active-remediation.md +1 -1
- package/dist/agent-src/rules/autonomous-execution.md +1 -0
- package/dist/agent-src/rules/broken-access-control.md +1 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +1 -0
- package/dist/agent-src/rules/context-hygiene.md +1 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +16 -33
- package/dist/agent-src/rules/design-fidelity.md +13 -16
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +1 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +1 -0
- package/dist/agent-src/rules/domain-safety-pii.md +1 -0
- package/dist/agent-src/rules/domain-safety-retention.md +1 -0
- package/dist/agent-src/rules/engineering-safety-floor.md +1 -0
- package/dist/agent-src/rules/evaluator-independence.md +3 -2
- package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
- package/dist/agent-src/rules/finance-safety-floor.md +1 -0
- package/dist/agent-src/rules/git-history-discipline.md +1 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
- package/dist/agent-src/rules/legal-safety-floor.md +3 -2
- 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/media-governance-routing.md +14 -17
- package/dist/agent-src/rules/persona-governance.md +2 -2
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +1 -1
- package/dist/agent-src/rules/question-not-instruction.md +1 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
- package/dist/agent-src/rules/runtime-safety.md +1 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +1 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +3 -2
- package/dist/agent-src/rules/session-canary.md +8 -62
- package/dist/agent-src/rules/settings-ask-protocol.md +1 -0
- package/dist/agent-src/rules/source-confidentiality.md +1 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +1 -0
- package/dist/agent-src/rules/tool-safety.md +1 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +1 -0
- package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
- package/dist/agent-src/scripts/deferral_carry.ts +339 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +4 -1
- package/dist/agent-src/skills/api-design/data/manifest.json +2 -2
- package/dist/agent-src/skills/api-testing/SKILL.md +17 -0
- package/dist/agent-src/skills/code-intelligence/SKILL.md +14 -2
- package/dist/agent-src/skills/design-system-capture/references/import-procedure.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +2 -2
- package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
- package/dist/agent-src/skills/iconography/SKILL.md +1 -1
- package/dist/agent-src/skills/js-library-packaging/SKILL.md +2 -2
- package/dist/agent-src/skills/monorepo-workspace/SKILL.md +1 -1
- package/dist/agent-src/skills/quality-tools/SKILL.md +11 -1
- package/dist/agent-src/skills/quality-tools/references/go-tools.md +76 -0
- package/dist/agent-src/skills/quality-tools/references/python-tools.md +104 -0
- package/dist/agent-src/skills/review-routing/SKILL.md +2 -2
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +3 -3
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +7 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +17 -0
- package/dist/agent-src/skills/test-performance/SKILL.md +19 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +2 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
- package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
- package/dist/cli/registry.js +1 -1
- package/dist/cli-delegate/archive_completed_roadmaps.js +92 -12
- package/dist/cli-delegate/check_update_banner.js +12 -1
- package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
- package/dist/cli-delegate/{chunk-M2DPGFZ4.js → chunk-6V6TA25H.js} +22 -152
- package/dist/cli-delegate/chunk-7IQE3D5S.js +191 -0
- package/dist/cli-delegate/{chunk-6KFEC3G7.js → chunk-CQWX336L.js} +1 -1
- package/dist/cli-delegate/chunk-DEIV7XP7.js +108 -0
- package/dist/cli-delegate/chunk-EEHDXSYD.js +2040 -0
- package/dist/cli-delegate/{chunk-P6ZHP5W2.js → chunk-GKE7ZVFA.js} +4 -4
- package/dist/cli-delegate/chunk-JQOM7TTM.js +1201 -0
- package/dist/cli-delegate/{chunk-IGD6ERX6.js → chunk-JQVYKIZA.js} +10 -0
- package/dist/cli-delegate/{chunk-FC75PGJE.js → chunk-JY2I2DXV.js} +12 -1
- package/dist/cli-delegate/chunk-KB5FZ655.js +208 -0
- package/dist/cli-delegate/{chunk-7RQP4UDH.js → chunk-KR6EM23A.js} +235 -1455
- package/dist/cli-delegate/{chunk-LTYVIY76.js → chunk-LIYTLH6O.js} +1 -1
- package/dist/cli-delegate/{chunk-YJ72UIAK.js → chunk-NQ7WT43P.js} +1186 -244
- package/dist/cli-delegate/chunk-NXEILF2K.js +2160 -0
- package/dist/cli-delegate/{chunk-5R7HOLAF.js → chunk-NYX3L2PZ.js} +1 -1
- package/dist/cli-delegate/{chunk-4E2W63DA.js → chunk-OKJUIAPS.js} +62 -1
- package/dist/cli-delegate/{chunk-QAJS5YWY.js → chunk-PHD7OOLE.js} +2 -1
- package/dist/cli-delegate/{chunk-RGNIWTEA.js → chunk-QLDHIPWR.js} +1 -1
- package/dist/cli-delegate/{chunk-7XGBX6ZH.js → chunk-RQNNW7IF.js} +1314 -560
- package/dist/cli-delegate/{chunk-4XFP2Y3E.js → chunk-S4UMZNES.js} +1 -1
- package/dist/cli-delegate/{chunk-6KM6ORRY.js → chunk-S4YWNRKO.js} +24 -8
- package/dist/cli-delegate/{chunk-FP36SWJD.js → chunk-TRI6DQZ6.js} +2781 -340
- package/dist/cli-delegate/chunk-ULQRUR23.js +208 -0
- package/dist/cli-delegate/{chunk-NUHLI2RX.js → chunk-YCU3TIXH.js} +2 -0
- package/dist/cli-delegate/chunk-ZBZV2G5R.js +210 -0
- package/dist/cli-delegate/{chunk-WGXTVKCQ.js → chunk-ZUZKZYCN.js} +5 -3
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +1167 -234
- package/dist/cli-delegate/cmd_doctor.js +13 -6
- package/dist/cli-delegate/cmd_doctor_neighbours.js +23 -0
- package/dist/cli-delegate/cmd_explain.js +5 -5
- package/dist/cli-delegate/cmd_export.js +11 -8
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +10 -7
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_memory_get.js +3 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +3 -3
- package/dist/cli-delegate/cmd_preflight.js +6 -6
- package/dist/cli-delegate/cmd_prune.js +11 -8
- package/dist/cli-delegate/cmd_refresh.js +2 -1
- package/dist/cli-delegate/cmd_route_audit.js +8 -6
- package/dist/cli-delegate/cmd_route_explain.js +4 -4
- package/dist/cli-delegate/cmd_session_recycle.js +4 -3
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +11 -8
- package/dist/cli-delegate/cmd_settings_set.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +11 -8
- package/dist/cli-delegate/cmd_uninstall.js +11 -8
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +3 -2
- package/dist/cli-delegate/cmd_validate.js +11 -8
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/cli-delegate/deferral_carry.js +16 -0
- package/dist/cli-delegate/gate_execute.js +2 -2
- package/dist/cli-delegate/roadmap_gates.js +5 -5
- package/dist/cli-delegate/update_roadmap_progress.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +127 -75
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +5357 -6656
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/install/claudePathsPlan.js +47 -6
- package/dist/install/claudeRuleRewrite.js +7 -0
- package/dist/install/emit_host_rules_cli.js +39 -36
- package/dist/install/globalRuleScope.js +87 -0
- package/dist/install/host_rule_ownership.js +160 -0
- package/dist/install/install.mjs +2080 -1024
- package/dist/install/installThinLayer.js +261 -0
- package/dist/install/installedRuleLinks.js +176 -0
- package/dist/install/wizard-plan.js +24 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +31965 -27154
- package/dist/scripts/_lib/agent_settings.js +11 -0
- package/dist/scripts/_lib/claude_settings_hooks.js +10 -8
- package/dist/scripts/_lib/installed_layer.js +378 -0
- package/dist/scripts/_lib/lean_projection_mode.js +218 -15
- package/dist/scripts/_lib/rule_consequence_class.js +45 -0
- package/dist/scripts/_lib/rule_injection.js +67 -10
- package/dist/scripts/_lib/rule_law_section.js +122 -0
- package/dist/scripts/_lib/thin_rules.js +458 -0
- package/dist/scripts/_lib/windsurf_render.js +60 -0
- package/dist/scripts/condense.js +14 -26
- package/dist/scripts/project_thin_rules.js +14 -294
- package/dist/server/schemas/settings.js +6 -0
- package/dist/shared/skillRanking.js +74 -13
- package/docs/contracts/settings-classes.md +7 -5
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
- package/src/agent-src/scripts/deferral_carry.ts +339 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +4 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
- package/src/config/agent-settings.template.yml +62 -22
- package/src/config/continuity-surface.json +7 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +96 -3
- package/src/config/gate-violation-baselines.json +17 -9
- package/src/config/hook-bundle-budget.json +25 -0
- package/src/config/hook-token-budget.json +6 -1
- package/src/config/host-instruction-limits.json +33 -0
- package/src/config/routing-coverage-seed.json +24 -3
- package/src/config/rule-activation-census.json +7 -3
- package/src/config/rule-consequence-class.json +161 -0
- package/src/config/rule-law-ceilings.json +99 -0
- package/src/install/claudePathsPlan.ts +46 -4
- package/src/install/claudeRuleRewrite.ts +10 -1
- package/src/install/emit_host_rules_cli.ts +43 -34
- package/src/install/globalRuleScope.ts +109 -0
- package/src/install/host_rule_ownership.ts +170 -0
- package/src/install/installThinLayer.ts +312 -0
- package/src/install/installedRuleLinks.ts +220 -0
- package/src/install/wizard-plan.ts +24 -0
- package/src/scripts/_cli/cmd_doctor.ts +23 -71
- package/src/scripts/_cli/cmd_doctor_neighbours.ts +293 -0
- package/src/scripts/_dispatch.bash +16 -2
- package/src/scripts/_lib/agent_settings.ts +11 -0
- package/src/scripts/_lib/claude_settings_hooks.ts +10 -8
- package/src/scripts/_lib/estate_offsets.ts +241 -0
- package/src/scripts/_lib/exemption_shape.ts +143 -0
- package/src/scripts/_lib/fnv.ts +23 -0
- package/src/scripts/_lib/foreign_scan.ts +106 -0
- package/src/scripts/_lib/gitignore_block.ts +314 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +32 -0
- package/src/scripts/_lib/graph_feeder_record.ts +214 -0
- package/src/scripts/_lib/hook_settings.ts +73 -0
- package/src/scripts/_lib/host_capability.ts +40 -19
- package/src/scripts/_lib/host_hook_merge.ts +165 -0
- package/src/scripts/_lib/installed_layer.ts +538 -0
- package/src/scripts/_lib/json_merge.ts +82 -0
- package/src/scripts/_lib/json_pointers.ts +93 -11
- package/src/scripts/_lib/lean_projection_mode.ts +275 -15
- package/src/scripts/_lib/machine_wake.ts +16 -3
- package/src/scripts/_lib/neighbour_census.ts +760 -0
- package/src/scripts/_lib/neighbour_scan.ts +122 -0
- package/src/scripts/_lib/neighbour_tool_use.ts +189 -0
- package/src/scripts/_lib/prompt_shape.ts +46 -0
- package/src/scripts/_lib/release_migration_gate.ts +19 -3
- package/src/scripts/_lib/reserved_name_sweep.ts +73 -0
- package/src/scripts/_lib/review_coverage.ts +134 -0
- package/src/scripts/_lib/rule_consequence_class.ts +71 -0
- package/src/scripts/_lib/rule_injection.ts +130 -10
- package/src/scripts/_lib/rule_law_section.ts +147 -0
- package/src/scripts/_lib/rule_layer_overlap.ts +58 -0
- package/src/scripts/_lib/skill_origin.ts +254 -0
- package/src/scripts/_lib/structured_ask.ts +64 -21
- package/src/scripts/_lib/thin_rules.ts +540 -0
- package/src/scripts/_lib/touched_file_quality.ts +434 -0
- package/src/scripts/_lib/turn_end_refusals.ts +77 -2
- package/src/scripts/_lib/windsurf_render.ts +63 -0
- package/src/scripts/ai_council/cli_call_budget.ts +13 -5
- package/src/scripts/ai_council/clients.ts +26 -57
- package/src/scripts/ai_council/config.ts +1 -1
- package/src/scripts/ai_council/vendor_defaults.ts +45 -0
- package/src/scripts/audit_skill_overlap.ts +261 -0
- package/src/scripts/before_complete_hook.ts +243 -12
- package/src/scripts/bench_touched_file_quality.ts +238 -0
- package/src/scripts/check_enforcement_matrix.ts +377 -20
- package/src/scripts/check_estate_count.ts +130 -153
- package/src/scripts/check_finding_dispositions.ts +116 -4
- package/src/scripts/check_hook_bundle_composition.ts +370 -0
- package/src/scripts/check_kernel_edit_ratified.ts +49 -11
- package/src/scripts/check_memory.ts +7 -1
- package/src/scripts/check_release_highlights.ts +10 -0
- package/src/scripts/check_release_pr_shape.ts +38 -15
- package/src/scripts/check_standing_rule_delivery.ts +33 -13
- package/src/scripts/check_verify_expectation_delta.ts +521 -0
- package/src/scripts/code_graph/detect.ts +127 -7
- package/src/scripts/code_graph/foreign.ts +194 -0
- package/src/scripts/code_graph/query.ts +217 -4
- package/src/scripts/code_graph/types.ts +26 -2
- package/src/scripts/code_graph/validate.ts +3 -1
- package/src/scripts/code_graph/verbs.ts +103 -0
- package/src/scripts/compile_hook_manifest.ts +33 -1
- package/src/scripts/condense.ts +14 -27
- package/src/scripts/estate_exemption_shape.ts +202 -0
- package/src/scripts/generate_host_cost_table.ts +1 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +150 -9
- package/src/scripts/hooks/block_config_weakening.ts +67 -2
- package/src/scripts/hooks/block_plumbing_writes.ts +29 -1
- package/src/scripts/hooks/code_graph_context_hook.ts +252 -23
- package/src/scripts/hooks/concern_failure_policy.ts +280 -0
- package/src/scripts/hooks/dispatch_hook.ts +52 -52
- package/src/scripts/hooks/exit_codes.ts +2 -1
- package/src/scripts/hooks/host_lowering.json +1 -1
- package/src/scripts/hooks/host_lowering.ts +19 -1
- package/src/scripts/hooks/one_question_per_ask_hook.ts +41 -25
- package/src/scripts/hooks/payload_stub.ts +10 -1
- package/src/scripts/hooks/rule_inject_hook.ts +757 -63
- package/src/scripts/hooks/table_fingerprint.ts +21 -0
- package/src/scripts/hooks/telemetry_usage_hook.ts +64 -8
- package/src/scripts/hooks/turn_end_gate_hook.ts +84 -0
- package/src/scripts/hooks_status.ts +112 -0
- package/src/scripts/injection_scan_hook.ts +7 -0
- package/src/scripts/install.ts +72 -185
- package/src/scripts/installed_layer_report.ts +103 -0
- package/src/scripts/language_mirror_hook.ts +15 -6
- package/src/scripts/learning_sidecar.ts +119 -8
- package/src/scripts/lint_hook_manifest.ts +53 -0
- package/src/scripts/lint_major_migration_sections.ts +156 -26
- package/src/scripts/lint_pack_boundaries.ts +63 -5
- package/src/scripts/lint_rule_law_section.ts +517 -0
- package/src/scripts/lint_workflow_security.ts +161 -5
- package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -0
- package/src/scripts/mcp_server/graph_tools.ts +72 -3
- package/src/scripts/measure_routing_signal.ts +54 -16
- package/src/scripts/measure_skill_ranker_baseline.ts +216 -8
- package/src/scripts/memory_learn_hook.ts +91 -6
- package/src/scripts/model_rule_injection.ts +52 -5
- package/src/scripts/project_thin_rules.ts +34 -327
- package/src/scripts/report_evidence_temperature.ts +657 -0
- package/src/scripts/report_installed_rule_links.ts +157 -0
- package/src/scripts/report_label_agreement.ts +233 -0
- package/src/scripts/report_obligation_mechanism.ts +230 -0
- package/src/scripts/report_single_token_triggers.ts +277 -0
- package/src/scripts/report_skill_ranker_confusion.ts +442 -0
- package/src/scripts/report_standing_payload_by_host.ts +188 -15
- package/src/scripts/report_verification_record_coverage.ts +306 -0
- package/src/scripts/rule_activation_census.ts +5 -2
- package/src/scripts/rule_trigger_eval.ts +9 -9
- package/src/scripts/schemas/review-findings.schema.json +3 -2
- package/src/scripts/schemas/rule.schema.json +5 -0
- package/src/scripts/self_review_gate.ts +62 -7
- package/src/scripts/skill_tools/score_skill_relevance.ts +290 -25
- package/src/scripts/sweep_skill_ranker_signals.ts +129 -0
- package/src/scripts/sync_gitignore.ts +29 -235
- package/src/scripts/trigger_eval_grandfather.json +0 -5
- package/src/server/schemas/settings.ts +10 -0
- package/src/shared/skillRanking.ts +117 -11
- package/dist/cli-delegate/chunk-JNMVLWLR.js +0 -4457
- package/dist/cli-delegate/chunk-YDPILCOL.js +0 -152
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Which links inside an installed rule file do NOT resolve, per host.
|
|
4
|
+
*
|
|
5
|
+
* A rule body is authored against the package tree, where every sibling
|
|
6
|
+
* directory is present. The installer copies a subset
|
|
7
|
+
* (`GLOBAL_DEPLOY_SOURCES`), and nothing joined the two, so a rule could ship
|
|
8
|
+
* a link into a directory no consumer has.
|
|
9
|
+
*
|
|
10
|
+
* This reporter reads the real deploy plan and the real rule bodies, so its
|
|
11
|
+
* numbers are the installer's rather than a restatement of it. It also prices
|
|
12
|
+
* the two repairs step 1.2 of `road-to-rule-triggers-and-links-that-hold`
|
|
13
|
+
* weighs against each other:
|
|
14
|
+
*
|
|
15
|
+
* · **deploy** — add the directory to the host's plan. Nothing is written
|
|
16
|
+
* into a rule body, so the standing text a host loads as instructions does
|
|
17
|
+
* not move; the cost is files on disk.
|
|
18
|
+
* · **rewrite** — leave the plan alone and make each link an absolute
|
|
19
|
+
* package path. Every character lands INSIDE a rule body, which is exactly
|
|
20
|
+
* the text the host loads.
|
|
21
|
+
*
|
|
22
|
+
* The rewrite price depends on the installed package root's own length, which
|
|
23
|
+
* is a property of the machine, so `--prefix-chars` is explicit and the report
|
|
24
|
+
* states the value it used.
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from 'node:fs';
|
|
27
|
+
import * as path from 'node:path';
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
type LinkAuditReport,
|
|
32
|
+
auditInstalledRuleLinks,
|
|
33
|
+
deployPlanFrom,
|
|
34
|
+
rewriteOptionCost,
|
|
35
|
+
} from '../install/installedRuleLinks.js';
|
|
36
|
+
import { GLOBAL_DEPLOY_SOURCES } from './install.js';
|
|
37
|
+
|
|
38
|
+
// ledger-exempt: a REPORTER, not a gate. It has no verdict for a per-target
|
|
39
|
+
// ledger to account for; it prints what the deploy plan and the rule bodies
|
|
40
|
+
// already say.
|
|
41
|
+
|
|
42
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
43
|
+
|
|
44
|
+
/** Hosts whose plan carries a rules directory — the only ones with an answer. */
|
|
45
|
+
function ruleHosts(): [string, ReadonlyArray<readonly [string, string]>][] {
|
|
46
|
+
return Object.entries(GLOBAL_DEPLOY_SOURCES).filter(([, pairs]) =>
|
|
47
|
+
pairs.some(([source]) => source.endsWith('/rules')),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface HostLinkReading {
|
|
52
|
+
host: string;
|
|
53
|
+
links: number;
|
|
54
|
+
resolved: number;
|
|
55
|
+
unresolved: number;
|
|
56
|
+
rewrite_cost_chars: number;
|
|
57
|
+
by_directory: LinkAuditReport['by_directory'];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function readings(packageRoot: string, prefixChars: number): HostLinkReading[] {
|
|
61
|
+
const out: HostLinkReading[] = [];
|
|
62
|
+
for (const [host, pairs] of ruleHosts()) {
|
|
63
|
+
const report = auditInstalledRuleLinks(deployPlanFrom(pairs), packageRoot);
|
|
64
|
+
out.push({
|
|
65
|
+
host,
|
|
66
|
+
links: report.audits.length,
|
|
67
|
+
resolved: report.counts.resolved,
|
|
68
|
+
unresolved: report.audits.length - report.counts.resolved,
|
|
69
|
+
rewrite_cost_chars: rewriteOptionCost(report, prefixChars),
|
|
70
|
+
by_directory: report.by_directory,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function flagValue(flag: string): string | null {
|
|
77
|
+
const argv = process.argv.slice(2);
|
|
78
|
+
const i = argv.indexOf(flag);
|
|
79
|
+
return i >= 0 && i + 1 < argv.length ? (argv[i + 1] as string) : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const USAGE =
|
|
83
|
+
'usage: report_installed_rule_links [--root DIR] [--prefix-chars N] [--json]\n' +
|
|
84
|
+
' --root package checkout the installer copies FROM (default: cwd)\n' +
|
|
85
|
+
' --prefix-chars length of the installed package root, for the rewrite price\n' +
|
|
86
|
+
' (default 48, a typical `~/.claude/agent-config` expansion)\n' +
|
|
87
|
+
' --json machine-readable readings\n';
|
|
88
|
+
|
|
89
|
+
export function main(): number {
|
|
90
|
+
const argv = process.argv.slice(2);
|
|
91
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
92
|
+
process.stdout.write(USAGE);
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
for (const a of argv) {
|
|
96
|
+
if (a.startsWith('-') && !['--root', '--prefix-chars', '--json'].includes(a)) {
|
|
97
|
+
process.stderr.write(`unknown argument: ${a}\n${USAGE}`);
|
|
98
|
+
return 2;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const root = flagValue('--root') ?? process.cwd();
|
|
103
|
+
const rawPrefix = flagValue('--prefix-chars');
|
|
104
|
+
const prefixChars = rawPrefix === null ? 48 : Number.parseInt(rawPrefix, 10);
|
|
105
|
+
if (!Number.isInteger(prefixChars) || prefixChars <= 0) {
|
|
106
|
+
process.stderr.write(`--prefix-chars must be a positive integer\n`);
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
if (!fs.existsSync(path.join(root, 'dist', 'agent-src', 'rules'))) {
|
|
110
|
+
process.stderr.write(`no dist/agent-src/rules under ${root} — nothing to read\n`);
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const rows = readings(root, prefixChars);
|
|
115
|
+
|
|
116
|
+
if (argv.includes('--json')) {
|
|
117
|
+
process.stdout.write(`${JSON.stringify({ prefix_chars: prefixChars, hosts: rows }, null, 2)}\n`);
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
process.stdout.write(`installed rule links, per host (rewrite priced at ${String(prefixChars)} prefix chars)\n\n`);
|
|
122
|
+
for (const r of rows) {
|
|
123
|
+
process.stdout.write(
|
|
124
|
+
` ${r.host}: ${String(r.links)} link(s), ${String(r.resolved)} resolved, ${String(r.unresolved)} unresolved\n`,
|
|
125
|
+
);
|
|
126
|
+
if (r.unresolved > 0) {
|
|
127
|
+
// Priced over EVERY unresolved link, including the ones a rewrite
|
|
128
|
+
// to an absolute package path cannot reach either (a target the
|
|
129
|
+
// projection does not ship). Comparing it against a deploy option
|
|
130
|
+
// that repairs only a subset overstates the rewrite; the caveat is
|
|
131
|
+
// printed rather than left for the reader to rediscover.
|
|
132
|
+
process.stdout.write(
|
|
133
|
+
` rewrite option: +${String(r.rewrite_cost_chars)} standing characters ` +
|
|
134
|
+
`(all ${String(r.unresolved)} unresolved; a like-for-like figure prices only ` +
|
|
135
|
+
`the subset the option being compared against repairs)\n`,
|
|
136
|
+
);
|
|
137
|
+
for (const d of r.by_directory) {
|
|
138
|
+
process.stdout.write(` ${String(d.count).padStart(4)} ${d.directory} (${d.verdict})\n`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _isCliEntry(): boolean {
|
|
146
|
+
if (process.argv[1] === undefined) return false;
|
|
147
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
148
|
+
try {
|
|
149
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (_isCliEntry()) {
|
|
156
|
+
process.exit(main());
|
|
157
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* report_label_agreement — ranker error, separated from label noise.
|
|
4
|
+
*
|
|
5
|
+
* `road-to-a-ranker-that-routes` 1.4. A top-1 of 0.21 is a statement about two
|
|
6
|
+
* things at once: how often the ranker is wrong, and how often "wrong" is one
|
|
7
|
+
* reasonable person disagreeing with another about which skill a prompt wants.
|
|
8
|
+
* The second is bounded by how far two independent labelling seats agree, and
|
|
9
|
+
* that bound was published as a single aggregate (39 / 55 exact-set agreement)
|
|
10
|
+
* with no per-row record — so nobody could ask the only question that matters
|
|
11
|
+
* here: what does the ranker score on the rows the seats actually AGREE on?
|
|
12
|
+
*
|
|
13
|
+
* This script answers it, and it needs a second seat's labels per row to do so.
|
|
14
|
+
* The store it reads is {@link SECOND_SEAT_FILE}: a flat `{ case id: [skill
|
|
15
|
+
* ids] }` map written by seats that had not read this ranker's scoring, under
|
|
16
|
+
* the protocol in `tests/eval/routing-matrix/README.md`. The first seat's labels
|
|
17
|
+
* are the `expected_skills` lists in the matrix itself.
|
|
18
|
+
*
|
|
19
|
+
* THE DISAGREEMENTS ARE NOT RECONCILED, and that is the point. Overwriting one
|
|
20
|
+
* seat with the other would replace the honest width of the ground truth with a
|
|
21
|
+
* narrower number that looks better and means less.
|
|
22
|
+
*
|
|
23
|
+
* THREE AGREEMENT DEFINITIONS, reported side by side because they answer
|
|
24
|
+
* different questions and the gap between them IS the finding:
|
|
25
|
+
*
|
|
26
|
+
* - `exact` — the two label sets are identical. The strictest, and the one
|
|
27
|
+
* the published 70.9 % figure used.
|
|
28
|
+
* - `overlap` — the two sets share at least one skill, or are both empty. A
|
|
29
|
+
* hit is scored when ANY labelled skill appears in the top-k,
|
|
30
|
+
* so this is the definition the MEASUREMENT actually cares
|
|
31
|
+
* about: on an overlap row the two seats would score the same
|
|
32
|
+
* top-1 answer the same way.
|
|
33
|
+
* - `polarity` — the two seats agree only on whether a skill answer exists at
|
|
34
|
+
* all. The weakest, and the floor under the other two.
|
|
35
|
+
*
|
|
36
|
+
* **`polarity` IS ONE-SIDED ON THIS DENOMINATOR and must not be compared with a
|
|
37
|
+
* two-sided figure.** The first seat's labels arrive through
|
|
38
|
+
* `readMatrixLabelledPrompts`, which keeps only NON-EMPTY lists, so "seat one
|
|
39
|
+
* thinks a skill answer exists" is true for every row here by construction and
|
|
40
|
+
* the `agrees` both-empty branch is unreachable from this caller. The number
|
|
41
|
+
* therefore reads "the second seat also wrote a non-empty label", not "the two
|
|
42
|
+
* seats agree about whether an answer exists". It is still worth reporting — it
|
|
43
|
+
* counts the rows the second seat declined — and it is NOT the published
|
|
44
|
+
* two-sided agreement figure wearing the same name.
|
|
45
|
+
*
|
|
46
|
+
* `second_seat_rows` and `relabelled_and_labelled` differ by exactly the rows
|
|
47
|
+
* the second seat relabelled that the first seat had left empty: those are
|
|
48
|
+
* absent from `first` and so cannot be paired.
|
|
49
|
+
*
|
|
50
|
+
* Usage:
|
|
51
|
+
* ./scripts-run src/scripts/report_label_agreement
|
|
52
|
+
* ./scripts-run src/scripts/report_label_agreement --ranker idf
|
|
53
|
+
*
|
|
54
|
+
* Output is JSON on stdout, always — there is no second format and therefore no
|
|
55
|
+
* flag to pick one.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
import * as fs from 'node:fs';
|
|
59
|
+
import * as path from 'node:path';
|
|
60
|
+
import { fileURLToPath } from 'node:url';
|
|
61
|
+
|
|
62
|
+
import { wilsonInterval } from './_lib/capture_rate.js';
|
|
63
|
+
import {
|
|
64
|
+
REPO,
|
|
65
|
+
SKILLS_DIR,
|
|
66
|
+
type LabelledPrompt,
|
|
67
|
+
parseRanker,
|
|
68
|
+
rankOptionsFor,
|
|
69
|
+
readMatrixLabelledPrompts,
|
|
70
|
+
sliceForId,
|
|
71
|
+
} from './measure_skill_ranker_baseline.js';
|
|
72
|
+
import { rank } from './skill_tools/score_skill_relevance.js';
|
|
73
|
+
|
|
74
|
+
/** Where the second seat's per-row labels live, relative to the repo root. */
|
|
75
|
+
export const SECOND_SEAT_FILE = path.join('tests', 'eval', 'routing-matrix', 'second-seat-2026-10-01.json');
|
|
76
|
+
|
|
77
|
+
export type AgreementKind = 'exact' | 'overlap' | 'polarity';
|
|
78
|
+
export const AGREEMENT_KINDS: readonly AgreementKind[] = ['exact', 'overlap', 'polarity'];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The store, or `{}` when nobody has relabelled.
|
|
82
|
+
*
|
|
83
|
+
* A MALFORMED store is a third state and must not look like either of the other
|
|
84
|
+
* two: an empty object would read as "nobody has relabelled" and a raw
|
|
85
|
+
* `SyntaxError` out of `main` would bypass the message that distinguishes them.
|
|
86
|
+
*/
|
|
87
|
+
export function readSecondSeat(repo = REPO): Record<string, string[]> {
|
|
88
|
+
const file = path.join(repo, SECOND_SEAT_FILE);
|
|
89
|
+
if (!fs.existsSync(file)) return {};
|
|
90
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(raw) as Record<string, string[]>;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`report_label_agreement: ${SECOND_SEAT_FILE} is not valid JSON (${(err as Error).message}). ` +
|
|
96
|
+
'That is a corrupt store, NOT an empty one — the two must not be confused.',
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function agrees(a: readonly string[], b: readonly string[], kind: AgreementKind): boolean {
|
|
102
|
+
if (kind === 'polarity') return (a.length > 0) === (b.length > 0);
|
|
103
|
+
if (kind === 'overlap') {
|
|
104
|
+
if (a.length === 0 || b.length === 0) return a.length === b.length;
|
|
105
|
+
return a.some((s) => b.includes(s));
|
|
106
|
+
}
|
|
107
|
+
if (a.length !== b.length) return false;
|
|
108
|
+
const sa = [...a].sort();
|
|
109
|
+
const sb = [...b].sort();
|
|
110
|
+
return sa.every((s, i) => s === sb[i]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface AgreementArm {
|
|
114
|
+
kind: AgreementKind | 'all-relabelled';
|
|
115
|
+
n: number;
|
|
116
|
+
top1: number;
|
|
117
|
+
top1_ci95: { lower: number; upper: number };
|
|
118
|
+
top3: number;
|
|
119
|
+
top3_ci95: { lower: number; upper: number };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function round3(n: number): number {
|
|
123
|
+
return Math.round(n * 1000) / 1000;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function arm(
|
|
127
|
+
kind: AgreementArm['kind'],
|
|
128
|
+
rows: readonly { prompt: LabelledPrompt; ranked: string[] }[],
|
|
129
|
+
): AgreementArm {
|
|
130
|
+
let t1 = 0;
|
|
131
|
+
let t3 = 0;
|
|
132
|
+
for (const r of rows) {
|
|
133
|
+
if (r.prompt.expected.some((e) => r.ranked.slice(0, 1).includes(e))) t1 += 1;
|
|
134
|
+
if (r.prompt.expected.some((e) => r.ranked.slice(0, 3).includes(e))) t3 += 1;
|
|
135
|
+
}
|
|
136
|
+
const n = rows.length;
|
|
137
|
+
const ci = (s: number): { lower: number; upper: number } => {
|
|
138
|
+
const raw = wilsonInterval(s, n);
|
|
139
|
+
return { lower: round3(raw.lower), upper: round3(raw.upper) };
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
kind,
|
|
143
|
+
n,
|
|
144
|
+
top1: n ? round3(t1 / n) : 0,
|
|
145
|
+
top1_ci95: ci(t1),
|
|
146
|
+
top3: n ? round3(t3 / n) : 0,
|
|
147
|
+
top3_ci95: ci(t3),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface AgreementReport {
|
|
152
|
+
ranker: string;
|
|
153
|
+
second_seat_rows: number;
|
|
154
|
+
relabelled_and_labelled: number;
|
|
155
|
+
agreement: Record<AgreementKind, { agreed: number; share: number }>;
|
|
156
|
+
arms: AgreementArm[];
|
|
157
|
+
/** Rows where the two seats share no skill at all — the honest width. */
|
|
158
|
+
disjoint: { id: string; first: string[]; second: string[] }[];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function measureAgreement(opts: {
|
|
162
|
+
repo?: string;
|
|
163
|
+
skillsDir?: string;
|
|
164
|
+
ranker: string;
|
|
165
|
+
}): AgreementReport {
|
|
166
|
+
const repo = opts.repo ?? REPO;
|
|
167
|
+
const skillsDir = opts.skillsDir ?? SKILLS_DIR;
|
|
168
|
+
const second = readSecondSeat(repo);
|
|
169
|
+
const first = readMatrixLabelledPrompts(repo);
|
|
170
|
+
const rankOpts = rankOptionsFor(opts.ranker);
|
|
171
|
+
|
|
172
|
+
const paired = first.filter((p) => second[p.id] !== undefined);
|
|
173
|
+
const ranked = new Map<string, string[]>();
|
|
174
|
+
for (const p of paired) ranked.set(p.id, rank(p.prompt, skillsDir, rankOpts).map((r) => r[0]));
|
|
175
|
+
|
|
176
|
+
const rows = paired.map((prompt) => ({ prompt, ranked: ranked.get(prompt.id) ?? [] }));
|
|
177
|
+
const agreement = {} as Record<AgreementKind, { agreed: number; share: number }>;
|
|
178
|
+
const arms: AgreementArm[] = [arm('all-relabelled', rows)];
|
|
179
|
+
for (const kind of AGREEMENT_KINDS) {
|
|
180
|
+
const kept = rows.filter((r) => agrees(r.prompt.expected, second[r.prompt.id] ?? [], kind));
|
|
181
|
+
agreement[kind] = {
|
|
182
|
+
agreed: kept.length,
|
|
183
|
+
share: rows.length ? round3(kept.length / rows.length) : 0,
|
|
184
|
+
};
|
|
185
|
+
arms.push(arm(kind, kept));
|
|
186
|
+
}
|
|
187
|
+
const disjoint = paired
|
|
188
|
+
.filter((p) => !agrees(p.expected, second[p.id] ?? [], 'overlap'))
|
|
189
|
+
.map((p) => ({ id: p.id, first: p.expected, second: second[p.id] ?? [] }));
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
ranker: opts.ranker,
|
|
193
|
+
second_seat_rows: Object.keys(second).length,
|
|
194
|
+
relabelled_and_labelled: paired.length,
|
|
195
|
+
agreement,
|
|
196
|
+
arms,
|
|
197
|
+
disjoint,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Every relabelled row is expected to sit in the tuning slice; this says so. */
|
|
202
|
+
export function relabelledSliceCensus(repo = REPO): Record<string, number> {
|
|
203
|
+
const out: Record<string, number> = { tuning: 0, sealed: 0 };
|
|
204
|
+
for (const id of Object.keys(readSecondSeat(repo))) out[sliceForId(id)] = (out[sliceForId(id)] ?? 0) + 1;
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function main(argv: readonly string[]): number {
|
|
209
|
+
let report: AgreementReport;
|
|
210
|
+
try {
|
|
211
|
+
// Same check as `--slice` elsewhere: a present flag with no value is an
|
|
212
|
+
// error, never a silent substitution of the baseline.
|
|
213
|
+
report = measureAgreement({ ranker: parseRanker(argv) });
|
|
214
|
+
} catch (err) {
|
|
215
|
+
process.stderr.write(`${(err as Error).message}\n`);
|
|
216
|
+
return 2;
|
|
217
|
+
}
|
|
218
|
+
if (report.second_seat_rows === 0) {
|
|
219
|
+
process.stderr.write(
|
|
220
|
+
`report_label_agreement: no second-seat labels at ${SECOND_SEAT_FILE} — ` +
|
|
221
|
+
'this is "nobody has relabelled", not "the seats disagree about nothing".\n',
|
|
222
|
+
);
|
|
223
|
+
return 2;
|
|
224
|
+
}
|
|
225
|
+
process.stdout.write(
|
|
226
|
+
`${JSON.stringify({ ...report, relabelled_slice_census: relabelledSliceCensus() }, null, 2)}\n`,
|
|
227
|
+
);
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
232
|
+
process.exit(main(process.argv.slice(2)));
|
|
233
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Per rule: is the obligation already carried by a mechanism, and is there a
|
|
4
|
+
* slot that could carry it?
|
|
5
|
+
*
|
|
6
|
+
* Step 1.5 of `road-to-rule-triggers-and-links-that-hold`. It joins three
|
|
7
|
+
* instruments that already exist and had never been read together:
|
|
8
|
+
*
|
|
9
|
+
* · `check_enforcement_coverage --json` — the only place `enforced_by` and
|
|
10
|
+
* `obligation_frequency` are already resolved per rule, including what the
|
|
11
|
+
* declaration resolves TO (a wired validator, a bound hook, nothing).
|
|
12
|
+
* · `report_obligation_carriers` — how many artifacts restate the same
|
|
13
|
+
* obligation, which is a different question from whether one enforces it.
|
|
14
|
+
* · The `# obligation: line N` frontmatter marker — 107 of 121 rules carry
|
|
15
|
+
* one and nothing in the tree reads it, so this is its first reader.
|
|
16
|
+
*
|
|
17
|
+
* **Two columns, and conflating them would be the error worth avoiding.**
|
|
18
|
+
* `carried` is measured: a gate either refuses or it does not.
|
|
19
|
+
* `slot_exists` is NECESSARY, NEVER SUFFICIENT — it says a host event fires at
|
|
20
|
+
* the obligation's own frequency, so a carrier could at least be bound. It
|
|
21
|
+
* does not say the obligation is decidable from what that event sees, which is
|
|
22
|
+
* the actual question and is not answerable by a join. A rule whose body says
|
|
23
|
+
* `instruction-only` has usually already answered it in the other direction,
|
|
24
|
+
* and that answer is reproduced rather than overridden.
|
|
25
|
+
*
|
|
26
|
+
* Report only. Moving an obligation out of prose is a per-rule change with its
|
|
27
|
+
* own review, which this roadmap's scope section says in as many words.
|
|
28
|
+
*/
|
|
29
|
+
import { execFileSync } from 'node:child_process';
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
33
|
+
|
|
34
|
+
// ledger-exempt: a REPORTER, not a gate. Its only non-zero exit says an input
|
|
35
|
+
// instrument could not be run, which is a statement about the walk.
|
|
36
|
+
|
|
37
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
38
|
+
|
|
39
|
+
/** Frequencies a host event is known to fire at — the `slot_exists` test. */
|
|
40
|
+
const EVENT_BOUND_FREQUENCIES: ReadonlySet<string> = new Set([
|
|
41
|
+
'per-edit',
|
|
42
|
+
'per-file-write',
|
|
43
|
+
'per-commit',
|
|
44
|
+
'per-turn',
|
|
45
|
+
'per-session',
|
|
46
|
+
'per-event',
|
|
47
|
+
'per-task',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/** Resolutions where something can actually refuse. */
|
|
51
|
+
const REFUSING: ReadonlySet<string> = new Set(['validator', 'validator-local', 'test', 'hook']);
|
|
52
|
+
|
|
53
|
+
export interface MechanismRow {
|
|
54
|
+
rule: string;
|
|
55
|
+
tier: string;
|
|
56
|
+
marker_line: number | null;
|
|
57
|
+
frequency: string;
|
|
58
|
+
declared: number;
|
|
59
|
+
effective: string;
|
|
60
|
+
/** `gate` · `observer` · `declared-gap` · `undeclared`. */
|
|
61
|
+
carried: string;
|
|
62
|
+
/** A host event is known to fire at this frequency. Necessary, not sufficient. */
|
|
63
|
+
slot_exists: boolean;
|
|
64
|
+
/** Artifacts restating this rule's obligations, per the carrier census. */
|
|
65
|
+
restatements: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** `# obligation: line N` inside the frontmatter block. */
|
|
69
|
+
export function markerLine(content: string): number | null {
|
|
70
|
+
if (!content.startsWith('---')) return null;
|
|
71
|
+
const end = content.indexOf('\n---', 3);
|
|
72
|
+
const block = end === -1 ? content : content.slice(0, end);
|
|
73
|
+
const m = /^#\s*obligation:\s*line\s+(\d+)\s*$/m.exec(block);
|
|
74
|
+
return m ? Number.parseInt(m[1] as string, 10) : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function run(root: string, script: string, args: readonly string[]): string {
|
|
78
|
+
return execFileSync(path.join(root, 'scripts-run'), [script, ...args], {
|
|
79
|
+
cwd: root,
|
|
80
|
+
encoding: 'utf8',
|
|
81
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
82
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface CoverageRule {
|
|
87
|
+
id: string;
|
|
88
|
+
tier?: string;
|
|
89
|
+
declared?: string[];
|
|
90
|
+
effective?: string;
|
|
91
|
+
obligation_frequency?: string | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function rows(root: string): MechanismRow[] {
|
|
95
|
+
const coverage = JSON.parse(run(root, 'src/scripts/check_enforcement_coverage', ['--json'])) as {
|
|
96
|
+
rules: CoverageRule[];
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
run(root, 'src/scripts/report_obligation_carriers', ['--top', '1']);
|
|
100
|
+
const carriersPath = path.join(root, 'agents', 'runtime', 'reports', 'obligation-carriers.json');
|
|
101
|
+
const restatements = new Map<string, number>();
|
|
102
|
+
if (!fs.existsSync(carriersPath)) {
|
|
103
|
+
// Not a degradation to absorb: `agents/runtime/` is gitignored, so a
|
|
104
|
+
// fresh checkout reaches this, and a silent fallback would publish a
|
|
105
|
+
// uniformly-zero Restatements column with nothing saying it is empty.
|
|
106
|
+
// Every other input failure in this script exits 1; so does this one.
|
|
107
|
+
throw new Error(
|
|
108
|
+
`report_obligation_carriers wrote no census at ${carriersPath} — the ` +
|
|
109
|
+
'Restatements column would read 0 for every rule without saying so',
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
{
|
|
113
|
+
const census = JSON.parse(fs.readFileSync(carriersPath, 'utf8')) as {
|
|
114
|
+
rows: { carriers: { path: string; cls: string }[] }[];
|
|
115
|
+
};
|
|
116
|
+
for (const r of census.rows) {
|
|
117
|
+
// A row is one obligation; it counts for every RULE that carries it.
|
|
118
|
+
for (const c of r.carriers) {
|
|
119
|
+
if (c.cls !== 'rule') continue;
|
|
120
|
+
const id = path.basename(c.path, '.md');
|
|
121
|
+
restatements.set(id, (restatements.get(id) ?? 0) + r.carriers.length - 1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const out: MechanismRow[] = [];
|
|
127
|
+
for (const r of coverage.rules) {
|
|
128
|
+
const file = path.join(root, 'src', 'rules', `${r.id}.md`);
|
|
129
|
+
const content = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
130
|
+
const declared = r.declared ?? [];
|
|
131
|
+
const effective = r.effective ?? 'none';
|
|
132
|
+
const frequency = r.obligation_frequency ?? '—';
|
|
133
|
+
|
|
134
|
+
let carried: string;
|
|
135
|
+
if (REFUSING.has(effective)) carried = 'gate';
|
|
136
|
+
else if (effective === 'observer') carried = 'observer';
|
|
137
|
+
else if (declared.length > 0) carried = 'declared-gap';
|
|
138
|
+
else carried = 'undeclared';
|
|
139
|
+
|
|
140
|
+
out.push({
|
|
141
|
+
rule: r.id,
|
|
142
|
+
tier: r.tier ?? '—',
|
|
143
|
+
marker_line: markerLine(content),
|
|
144
|
+
frequency,
|
|
145
|
+
declared: declared.length,
|
|
146
|
+
effective,
|
|
147
|
+
carried,
|
|
148
|
+
slot_exists: EVENT_BOUND_FREQUENCIES.has(frequency),
|
|
149
|
+
restatements: restatements.get(r.id) ?? 0,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
out.sort((a, b) => a.rule.localeCompare(b.rule));
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function renderTable(data: readonly MechanismRow[]): string[] {
|
|
157
|
+
const L: string[] = [];
|
|
158
|
+
L.push('| Rule | Tier | Marker | Frequency | `enforced_by` | Resolves to | Carried | Slot exists | Restatements |');
|
|
159
|
+
L.push('|---|---|---:|---|---:|---|---|---|---:|');
|
|
160
|
+
for (const r of data) {
|
|
161
|
+
L.push(
|
|
162
|
+
`| \`${r.rule}\` | ${r.tier} | ${r.marker_line === null ? '—' : String(r.marker_line)} | ` +
|
|
163
|
+
`${r.frequency} | ${String(r.declared)} | \`${r.effective}\` | ${r.carried} | ` +
|
|
164
|
+
`${r.slot_exists ? 'yes' : 'no'} | ${String(r.restatements)} |`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return L;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const USAGE = 'usage: report_obligation_mechanism [--root DIR] [--json | --table]\n';
|
|
171
|
+
|
|
172
|
+
export function main(): number {
|
|
173
|
+
const argv = process.argv.slice(2);
|
|
174
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
175
|
+
process.stdout.write(USAGE);
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
for (const a of argv) {
|
|
179
|
+
if (a.startsWith('-') && !['--root', '--json', '--table'].includes(a)) {
|
|
180
|
+
process.stderr.write(`unknown argument: ${a}\n${USAGE}`);
|
|
181
|
+
return 2;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const i = argv.indexOf('--root');
|
|
185
|
+
const root = i >= 0 && i + 1 < argv.length ? (argv[i + 1] as string) : process.cwd();
|
|
186
|
+
|
|
187
|
+
let data: MechanismRow[];
|
|
188
|
+
try {
|
|
189
|
+
data = rows(root);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
process.stderr.write(`could not read an input instrument: ${String(e)}\n`);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (argv.includes('--json')) {
|
|
196
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
if (argv.includes('--table')) {
|
|
200
|
+
process.stdout.write(`${renderTable(data).join('\n')}\n`);
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const by = (k: string): number => data.filter((r) => r.carried === k).length;
|
|
205
|
+
const slotNoGate = data.filter((r) => r.carried !== 'gate' && r.slot_exists).length;
|
|
206
|
+
process.stdout.write(
|
|
207
|
+
`${String(data.length)} rule(s)\n` +
|
|
208
|
+
` carried by a gate that can refuse: ${String(by('gate'))}\n` +
|
|
209
|
+
` carried by an observer (fires, never refuses): ${String(by('observer'))}\n` +
|
|
210
|
+
` declared gap (the rule says instruction-only / none): ${String(by('declared-gap'))}\n` +
|
|
211
|
+
` no enforcement declaration at all: ${String(by('undeclared'))}\n` +
|
|
212
|
+
` not gated, but a host event fires at the obligation's frequency: ${String(slotNoGate)}\n` +
|
|
213
|
+
` carry no \`# obligation: line N\` marker: ${String(data.filter((r) => r.marker_line === null).length)}\n`,
|
|
214
|
+
);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function _isCliEntry(): boolean {
|
|
219
|
+
if (process.argv[1] === undefined) return false;
|
|
220
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
221
|
+
try {
|
|
222
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
223
|
+
} catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (_isCliEntry()) {
|
|
229
|
+
process.exit(main());
|
|
230
|
+
}
|