@event4u/agent-config 16.1.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 +98 -257
- 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 +5 -5
- 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/commands/work.md +4 -0
- 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/autonomy-mechanics.md +4 -8
- 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/ui-audit-gate.md +1 -2
- 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/accessibility-auditor/data/aria-patterns.csv +1 -1
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
- 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-review/SKILL.md +30 -4
- package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
- package/dist/agent-src/skills/design-system-capture/references/import-procedure.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +18 -3
- package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
- 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/markitdown/SKILL.md +4 -0
- package/dist/agent-src/skills/monorepo-workspace/SKILL.md +1 -1
- package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
- 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 +25 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +30 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +302 -21
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
- package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/dist/cli/python/knowledge_ingest.js +1 -1
- package/dist/cli/python/workspace_drive.js +1 -1
- 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-UNCRJBT3.js → chunk-JQVYKIZA.js} +41 -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-YUM5KURG.js → chunk-KR6EM23A.js} +235 -1455
- package/dist/cli-delegate/{chunk-LTYVIY76.js → chunk-LIYTLH6O.js} +1 -1
- package/dist/cli-delegate/chunk-NQ7WT43P.js +2729 -0
- 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-HDVVGPQW.js → chunk-RQNNW7IF.js} +1405 -567
- package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-S4UMZNES.js} +1 -1
- package/dist/cli-delegate/{chunk-6KM6ORRY.js → chunk-S4YWNRKO.js} +24 -8
- package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-TRI6DQZ6.js} +3054 -325
- 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 +1862 -809
- 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 +132 -80
- 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 +7972 -5186
- package/dist/hooks/dispatch.sha256 +1 -0
- 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 +2226 -1055
- 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 +40913 -34865
- 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 +29 -0
- package/dist/shared/skillRanking.js +74 -13
- package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
- package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/docs/contracts/settings-classes.md +10 -6
- package/package.json +4 -4
- 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/app_spec.ts +10 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +302 -21
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
- package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/src/config/agent-settings.template.yml +91 -22
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/continuity-surface.json +9 -2
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +327 -3
- package/src/config/gate-violation-baselines.json +21 -9
- package/src/config/hook-bundle-budget.json +25 -0
- package/src/config/hook-latency-budget.json +21 -0
- package/src/config/hook-token-budget.json +8 -3
- package/src/config/host-instruction-limits.json +33 -0
- package/src/config/metric-registry.yml +114 -0
- package/src/config/placeholder-drift-budget.json +10 -0
- package/src/config/routing-coverage-seed.json +38 -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 +28 -90
- package/src/scripts/_cli/cmd_doctor_neighbours.ts +293 -0
- package/src/scripts/_cli/doctor_execution.ts +100 -17
- package/src/scripts/_cli/doctor_network_posture.ts +226 -0
- package/src/scripts/_dispatch.bash +16 -2
- package/src/scripts/_lib/agent_settings.ts +11 -0
- package/src/scripts/_lib/body_portable.ts +102 -0
- package/src/scripts/_lib/cc_transcript.ts +132 -1
- package/src/scripts/_lib/claude_settings_hooks.ts +10 -8
- package/src/scripts/_lib/concern_sla_window.ts +200 -0
- 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/forge_protection.ts +114 -0
- package/src/scripts/_lib/forge_reader.ts +558 -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_env_write.ts +111 -0
- 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/llm_proposer_transport.ts +14 -4
- 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/obligations.ts +119 -8
- package/src/scripts/_lib/probe_inputs.ts +271 -0
- package/src/scripts/_lib/prompt_shape.ts +46 -0
- package/src/scripts/_lib/read_surface_scan.ts +322 -0
- package/src/scripts/_lib/release_highlights.ts +2 -1
- package/src/scripts/_lib/release_migration_gate.ts +19 -3
- package/src/scripts/_lib/reserved_name_sweep.ts +73 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
- 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/shell_write_shapes.ts +108 -0
- package/src/scripts/_lib/skill_origin.ts +254 -0
- package/src/scripts/_lib/structural_hiding.ts +622 -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 +600 -4
- package/src/scripts/_lib/turn_end_transcript.ts +216 -0
- package/src/scripts/_lib/verification_command.ts +324 -0
- package/src/scripts/_lib/verification_evidence.ts +647 -0
- package/src/scripts/_lib/verify_clause.ts +280 -0
- 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/ai_team/team_dispatch.ts +4 -0
- package/src/scripts/audit_skill_overlap.ts +261 -0
- package/src/scripts/before_complete_hook.ts +573 -25
- package/src/scripts/bench_hook_latency.ts +412 -5
- package/src/scripts/bench_touched_file_quality.ts +238 -0
- package/src/scripts/cache_realization_report.ts +61 -1
- package/src/scripts/check_argument_hint.ts +287 -0
- package/src/scripts/check_claims.ts +125 -1
- package/src/scripts/check_enforcement_coverage.ts +38 -9
- package/src/scripts/check_enforcement_matrix.ts +621 -22
- package/src/scripts/check_estate_count.ts +130 -153
- package/src/scripts/check_finding_dispositions.ts +138 -7
- package/src/scripts/check_generator_sync.ts +71 -0
- package/src/scripts/check_hook_bundle_composition.ts +370 -0
- package/src/scripts/check_host_docs_digest.ts +786 -0
- package/src/scripts/check_host_format_column.ts +559 -0
- package/src/scripts/check_input_references.ts +228 -0
- package/src/scripts/check_inputs_parity.ts +235 -0
- package/src/scripts/check_kernel_edit_ratified.ts +89 -9
- package/src/scripts/check_kill_switch_table.ts +294 -0
- package/src/scripts/check_memory.ts +7 -1
- package/src/scripts/check_placeholder_drift.ts +221 -0
- package/src/scripts/check_read_surface_coverage.ts +515 -0
- package/src/scripts/check_release_highlights.ts +10 -0
- package/src/scripts/check_release_holds.ts +2 -1
- package/src/scripts/check_release_pr_shape.ts +38 -15
- package/src/scripts/check_routing_coverage.ts +378 -11
- package/src/scripts/check_standing_rule_delivery.ts +33 -13
- package/src/scripts/check_static_parity.ts +17 -0
- package/src/scripts/check_verify_expectation_delta.ts +521 -0
- package/src/scripts/ci_settle.ts +145 -19
- package/src/scripts/closure_scan.ts +115 -1
- 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 +90 -14
- package/src/scripts/condense.ts +14 -27
- package/src/scripts/estate_exemption_shape.ts +202 -0
- package/src/scripts/external_sources_denylist.json +29 -4
- package/src/scripts/generate_host_cost_table.ts +1 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +192 -14
- package/src/scripts/hooks/block_config_weakening.ts +294 -4
- package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
- package/src/scripts/hooks/block_plumbing_writes.ts +342 -0
- package/src/scripts/hooks/bundle_integrity.ts +291 -0
- package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
- package/src/scripts/hooks/code_graph_context_hook.ts +253 -25
- package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
- package/src/scripts/hooks/concern_failure_policy.ts +280 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/concern_timings.ts +94 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
- package/src/scripts/hooks/design_pass_hook.ts +100 -9
- package/src/scripts/hooks/design_slop_hook.ts +1 -2
- package/src/scripts/hooks/dispatch_hook.ts +77 -71
- package/src/scripts/hooks/edit_shape_hook.ts +1 -2
- package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
- package/src/scripts/hooks/evidence_independence.ts +1 -2
- package/src/scripts/hooks/exit_codes.ts +129 -0
- package/src/scripts/hooks/host_lowering.json +1 -0
- package/src/scripts/hooks/host_lowering.ts +146 -3
- package/src/scripts/hooks/host_lowering.yaml +262 -46
- package/src/scripts/hooks/host_semantics.ts +30 -2
- package/src/scripts/hooks/injection_budget.ts +9 -2
- package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
- package/src/scripts/hooks/journal_record_hook.ts +1 -2
- package/src/scripts/hooks/memory_recall_hook.ts +1 -2
- package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
- package/src/scripts/hooks/one_question_per_ask_hook.ts +43 -6
- package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
- package/src/scripts/hooks/payload_stub.ts +10 -1
- package/src/scripts/hooks/permission_gate.ts +1 -2
- package/src/scripts/hooks/push_settle_hook.ts +1 -3
- package/src/scripts/hooks/replay_hook.ts +2 -1
- package/src/scripts/hooks/reread_guard_hook.ts +1 -2
- package/src/scripts/hooks/review_baseline_hook.ts +1 -2
- package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
- package/src/scripts/hooks/rule_inject_hook.ts +765 -68
- package/src/scripts/hooks/run_continuation_hook.ts +13 -15
- package/src/scripts/hooks/skill_route_hook.ts +1 -2
- package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
- package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
- package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
- package/src/scripts/hooks/table_fingerprint.ts +70 -0
- package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
- package/src/scripts/hooks/telemetry_usage_hook.ts +65 -10
- package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
- package/src/scripts/hooks/turn_end_gate_hook.ts +421 -324
- package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
- package/src/scripts/hooks_status.ts +112 -0
- package/src/scripts/injection_scan_hook.ts +7 -0
- package/src/scripts/install.ts +75 -189
- 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_deny_text.ts +419 -0
- package/src/scripts/lint_exit_codes.ts +262 -0
- package/src/scripts/lint_hook_manifest.ts +108 -0
- package/src/scripts/lint_major_migration_sections.ts +156 -26
- package/src/scripts/lint_pack_boundaries.ts +63 -5
- package/src/scripts/lint_roadmap_blockers.ts +78 -12
- package/src/scripts/lint_rule_law_section.ts +517 -0
- package/src/scripts/lint_skill_descriptions.ts +1 -1
- package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
- 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/mcp_server/prompts.ts +142 -1
- package/src/scripts/measure_routing_signal.ts +54 -16
- package/src/scripts/measure_skill_ranker_baseline.ts +515 -53
- package/src/scripts/measure_turn_end_gate.ts +193 -0
- package/src/scripts/memory_learn_hook.ts +91 -6
- package/src/scripts/memory_lookup.ts +1 -1
- package/src/scripts/model_rule_injection.ts +52 -5
- package/src/scripts/prepack-check.mjs +53 -0
- package/src/scripts/project_thin_rules.ts +34 -327
- package/src/scripts/release_publication.ts +13 -1
- package/src/scripts/report_evidence_temperature.ts +657 -0
- package/src/scripts/report_installed_rule_links.ts +157 -0
- package/src/scripts/report_invocation_surface.ts +383 -0
- package/src/scripts/report_label_agreement.ts +233 -0
- package/src/scripts/report_obligation_carriers.ts +699 -0
- package/src/scripts/report_obligation_mechanism.ts +230 -0
- package/src/scripts/report_obligation_writer_split.ts +236 -0
- package/src/scripts/report_single_token_triggers.ts +277 -0
- package/src/scripts/report_skill_menu_census.ts +208 -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/roadmap_verify_share.ts +213 -0
- package/src/scripts/rule_activation_census.ts +5 -2
- package/src/scripts/rule_trigger_eval.ts +9 -9
- package/src/scripts/schemas/command.schema.json +44 -0
- package/src/scripts/schemas/review-findings.schema.json +7 -2
- package/src/scripts/schemas/rule.schema.json +5 -0
- package/src/scripts/schemas/skill.schema.json +44 -0
- package/src/scripts/self_review_gate.ts +421 -17
- package/src/scripts/semantic_parity_census.ts +314 -0
- package/src/scripts/session_eol_report.ts +54 -1
- package/src/scripts/skill_tools/score_skill_relevance.ts +290 -25
- package/src/scripts/sweep_dead_scan_roots.ts +1 -1
- 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 -7
- package/src/scripts/trigger_eval_rotation.ts +122 -27
- package/src/scripts/ui_conformance_probe.ts +156 -4
- package/src/scripts/update_prices.ts +85 -3
- package/src/scripts/write_bundle_digest.mjs +74 -0
- package/src/server/schemas/settings.ts +41 -0
- package/src/shared/skillRanking.ts +117 -11
- package/src/templates/consumer-settings/ONBOARDING.md +1 -0
- package/dist/cli-delegate/chunk-FXYUYU4N.js +0 -4416
- package/dist/cli-delegate/chunk-QJWAUMJW.js +0 -1072
- package/dist/cli-delegate/chunk-YDPILCOL.js +0 -152
- package/dist/ui/assets/index-DH1xc62v.js +0 -29
- package/dist/ui/assets/index-jSfMW8_t.css +0 -1
|
@@ -7,14 +7,24 @@
|
|
|
7
7
|
* none, and a gate that blocks unobserved is the shape that discipline exists to
|
|
8
8
|
* prevent.
|
|
9
9
|
*
|
|
10
|
-
* This module is the reader. It owns
|
|
10
|
+
* This module is the reader. It owns five things and deliberately not a sixth:
|
|
11
11
|
*
|
|
12
12
|
* 1. the on-disk record shape, extended so a session's refusals are COUNTED
|
|
13
13
|
* rather than overwritten (§ Phase 1 step 1.1);
|
|
14
14
|
* 2. a TTL prune, which the gate's own header admits is missing (step 1.2);
|
|
15
15
|
* 3. the split by recorded package version, so claim 10's prediction — that
|
|
16
16
|
* refusals correlate with the local 12.1 install — is TESTED rather than
|
|
17
|
-
* assumed (step 1.3)
|
|
17
|
+
* assumed (step 1.3);
|
|
18
|
+
* 4. the SHADOW record — what a retry would have been refused for, written on
|
|
19
|
+
* an allow path (`road-to-a-stop-that-holds` step 2.1);
|
|
20
|
+
* 5. the rollup over those records (step 2.2). Read `ShadowLayerStats` before
|
|
21
|
+
* quoting anything it produces: the share it computes is NOT the Q1 the
|
|
22
|
+
* demotion contract registers bars against, and the difference is named
|
|
23
|
+
* there rather than left to a reader of the number.
|
|
24
|
+
*
|
|
25
|
+
* (This list read "three things and deliberately not a fourth" until 2026-10-01.
|
|
26
|
+
* It was accurate when written and silently false for the two steps after it —
|
|
27
|
+
* an independent review caught it. Items 4 and 5 are the two that landed.)
|
|
18
28
|
*
|
|
19
29
|
* The fourth thing it does not own is a **rate over all sessions**, and the
|
|
20
30
|
* reason is structural rather than an omission. A record is written only when a
|
|
@@ -335,6 +345,318 @@ export function readInstallBoundary(): InstallBoundary {
|
|
|
335
345
|
}
|
|
336
346
|
}
|
|
337
347
|
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// The shadow read — road-to-a-stop-that-holds step 2.1
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* One observation of "this retry would still have been refused".
|
|
354
|
+
*
|
|
355
|
+
* `turn-end-detector-demotion.md` § Q1 asks what share of the turns the gate
|
|
356
|
+
* allows on a re-entrancy layer would have been refused again. Both layers
|
|
357
|
+
* return `EXIT_ALLOW` unconditionally, so that share is not derivable from the
|
|
358
|
+
* refusal record: a retry leaves no trace at all, and a detector that fired
|
|
359
|
+
* twice is indistinguishable from one that was satisfied on the second pass.
|
|
360
|
+
* This row is that trace.
|
|
361
|
+
*
|
|
362
|
+
* `layer` is which allow path produced it, because the two are not the same
|
|
363
|
+
* measurement. `stop_hook_active` is the HOST's answer and is set on any stop
|
|
364
|
+
* hook's block, including another concern's; `refused_turn` is this gate's own
|
|
365
|
+
* marker and therefore always follows a refusal by this gate. Pooling them
|
|
366
|
+
* would let another concern's retries inflate Q1's numerator against a
|
|
367
|
+
* denominator that only counts this gate's refusals.
|
|
368
|
+
*/
|
|
369
|
+
/**
|
|
370
|
+
* Who set the `stop_hook_active` flag this row was recorded under.
|
|
371
|
+
*
|
|
372
|
+
* `layer` already says WHICH allow path produced the row, and the doc above
|
|
373
|
+
* says the host sets `stop_hook_active` on ANY stop hook's block — including a
|
|
374
|
+
* neighbour's. What it could not say is which: a `stop_hook_active` row and a
|
|
375
|
+
* `refused_turn` row were the only two things the record distinguished, so a
|
|
376
|
+
* retry caused by a foreign concern and a retry caused by this gate were
|
|
377
|
+
* indistinguishable once the layer was `stop_hook_active`.
|
|
378
|
+
*
|
|
379
|
+
* - `ours` — this session's refusal record exists and parses, so this gate
|
|
380
|
+
* refused at some point before this stop.
|
|
381
|
+
* - `foreign` — the record is ABSENT. This gate never refused in this session,
|
|
382
|
+
* and the host set the flag anyway, so something else did.
|
|
383
|
+
* - `unknown` — the record exists and could not be read. The distinction from
|
|
384
|
+
* `foreign` is the whole point: "we did not refuse" and "we
|
|
385
|
+
* cannot tell whether we refused" are different findings, and
|
|
386
|
+
* collapsing them would let an unreadable state directory read
|
|
387
|
+
* as evidence of a neighbour.
|
|
388
|
+
*
|
|
389
|
+
* On the `refused_turn` layer the answer is `ours` by construction — that
|
|
390
|
+
* marker is this gate's own and no other writer sets it.
|
|
391
|
+
*/
|
|
392
|
+
export type ShadowSetBy = 'ours' | 'foreign' | 'unknown';
|
|
393
|
+
|
|
394
|
+
export interface WouldRefuseAgainRow {
|
|
395
|
+
detector: RefusalDetectorId;
|
|
396
|
+
/** The turn ordinal the shadow ran on — joins to `refused_turn`. */
|
|
397
|
+
turn: number;
|
|
398
|
+
at: string;
|
|
399
|
+
layer: ShadowLayer;
|
|
400
|
+
/**
|
|
401
|
+
* Absent on rows written before this field existed, which parse as
|
|
402
|
+
* `unknown` rather than being dropped — an old row is still a real
|
|
403
|
+
* observation of a retry, and discarding it would silently lower Q1's
|
|
404
|
+
* numerator.
|
|
405
|
+
*/
|
|
406
|
+
set_by: ShadowSetBy;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* The two allow paths a shadow read can run on, in the order `main()` reaches
|
|
411
|
+
* them.
|
|
412
|
+
*
|
|
413
|
+
* A LIST rather than a bare union, and the counter below is derived from it,
|
|
414
|
+
* for the reason `emptyCounts` gives one screen up: a hand-written counter
|
|
415
|
+
* object satisfied `Record<Union, number>` on the day it was typed and read
|
|
416
|
+
* back `undefined` the moment a member was added, so every `+=` over it
|
|
417
|
+
* produced `NaN` and the whole rollup went quiet. Adding a third layer here
|
|
418
|
+
* cannot repeat that.
|
|
419
|
+
*/
|
|
420
|
+
export const SHADOW_LAYERS = ['stop_hook_active', 'refused_turn'] as const;
|
|
421
|
+
|
|
422
|
+
export type ShadowLayer = (typeof SHADOW_LAYERS)[number];
|
|
423
|
+
|
|
424
|
+
/** A zeroed retry counter per layer, DERIVED from `SHADOW_LAYERS`. */
|
|
425
|
+
export function emptyRetryCounts(): Record<ShadowLayer, number> {
|
|
426
|
+
const out = {} as Record<ShadowLayer, number>;
|
|
427
|
+
for (const l of SHADOW_LAYERS) out[l] = 0;
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Per-session shadow state, in its own file beside the refusal record.
|
|
433
|
+
*
|
|
434
|
+
* SEPARATE FILE, deliberately, and this is the one design decision in the step
|
|
435
|
+
* worth defending. The plan says "into the session state" and the refusal
|
|
436
|
+
* record is the obvious place — but `refused_turn` in that record is the
|
|
437
|
+
* re-entrancy wedge guard, `parseRecord` rejects any record lacking it, and a
|
|
438
|
+
* Layer-1 retry can occur with no refusal by this gate at all (another stop
|
|
439
|
+
* concern blocked). Writing a shadow row there would mean either synthesising
|
|
440
|
+
* a `refused_turn` this gate never wrote — corrupting the one field a wedge
|
|
441
|
+
* depends on — or loosening the parser that protects it. A sibling file costs
|
|
442
|
+
* one more path and touches neither.
|
|
443
|
+
*
|
|
444
|
+
* It lives in this module rather than in the gate for the reason the header
|
|
445
|
+
* already gives about the pruner: a directory with two writers each holding
|
|
446
|
+
* their own idea of where the files are is how a reader and a writer end up
|
|
447
|
+
* disagreeing.
|
|
448
|
+
*
|
|
449
|
+
* Two limits bound what a reading of this record may claim.
|
|
450
|
+
*
|
|
451
|
+
* **It counts retries with a READABLE TRANSCRIPT, not all retries.** A retry
|
|
452
|
+
* whose transcript is absent, over `TRANSCRIPT_READ_MAX_BYTES`, or carries no
|
|
453
|
+
* assistant text records nothing at all — the gate's input assembly returns
|
|
454
|
+
* null and there is no detector verdict to shadow. The numerator is unaffected
|
|
455
|
+
* and the denominator shrinks, so the bias is UPWARD: a Q1 read off this
|
|
456
|
+
* record is an upper bound, never a point estimate.
|
|
457
|
+
*
|
|
458
|
+
* **On a host that sends no `session_id` it pools across sessions.** The key
|
|
459
|
+
* is `deriveSessionKey(rawSessionId || 'unknown-session')`, so every such
|
|
460
|
+
* session shares one file and one counter. The gate documents the same
|
|
461
|
+
* collision for the re-entrancy marker and argues it degrades safely, toward
|
|
462
|
+
* under-refusing. That argument does NOT transfer here: a merged measurement
|
|
463
|
+
* does not under-report, it reports one number for several sessions and a
|
|
464
|
+
* reader takes it for one. Named rather than inherited, because the two
|
|
465
|
+
* failures are opposite in kind.
|
|
466
|
+
*/
|
|
467
|
+
export interface ShadowRecord {
|
|
468
|
+
/** Newest last. Bounded by `SHADOW_MAX_ROWS`. */
|
|
469
|
+
would_refuse_again: WouldRefuseAgainRow[];
|
|
470
|
+
/**
|
|
471
|
+
* Retries observed on an allow path in this session, refusing or NOT,
|
|
472
|
+
* SPLIT BY LAYER.
|
|
473
|
+
*
|
|
474
|
+
* Kept because a shadow row array alone cannot distinguish "no retry
|
|
475
|
+
* happened" from "every retry came back clean", and those are opposite
|
|
476
|
+
* readings of the same empty list.
|
|
477
|
+
*
|
|
478
|
+
* Split because pooling it made a per-layer Q1 uncomputable, which the
|
|
479
|
+
* first version shipped and an independent review caught before any row
|
|
480
|
+
* accumulated. `layer` is on every ROW for a stated reason — a
|
|
481
|
+
* `stop_hook_active` retry follows ANY stop concern's block, not only this
|
|
482
|
+
* gate's — so a pooled numerator would read another concern's retries
|
|
483
|
+
* against this gate's refusals. A pooled DENOMINATOR has the same defect
|
|
484
|
+
* one level down, and worse: a CLEAN retry adds no row at all, so it left
|
|
485
|
+
* no layer trace anywhere. The numerator could be split and the
|
|
486
|
+
* denominator could not, which is not a sanity check on anything.
|
|
487
|
+
*
|
|
488
|
+
* Keyed rather than two fields so a third layer, if one is ever added,
|
|
489
|
+
* cannot silently land in neither bucket.
|
|
490
|
+
*/
|
|
491
|
+
retries_observed: Record<ShadowLayer, number>;
|
|
492
|
+
/** Rows dropped to the cap. A non-zero value makes the array a sample. */
|
|
493
|
+
dropped: number;
|
|
494
|
+
first_at: string;
|
|
495
|
+
last_at: string;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Row cap per session.
|
|
500
|
+
*
|
|
501
|
+
* A stated bound, not a measured optimum. A row is ~90 bytes and a session
|
|
502
|
+
* with 200 retries is already pathological, so the cap exists to keep an
|
|
503
|
+
* unbounded write off the Stop path rather than to fit a distribution. Oldest
|
|
504
|
+
* rows are dropped first: Q1 is a rate, and the most recent observations are
|
|
505
|
+
* the ones a reader is deciding on. A non-zero `dropped` on a real session is
|
|
506
|
+
* the signal that the number was chosen too low.
|
|
507
|
+
*/
|
|
508
|
+
export const SHADOW_MAX_ROWS = 200;
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* The shadow file's suffix, named rather than inlined because TWO readers need
|
|
512
|
+
* to agree on it: the writer below and `pruneAgedRefusalState`, which scans the
|
|
513
|
+
* same directory for `*.json` and would otherwise keep every shadow record
|
|
514
|
+
* forever.
|
|
515
|
+
*/
|
|
516
|
+
export const SHADOW_SUFFIX = '.shadow.json';
|
|
517
|
+
|
|
518
|
+
export function sessionShadowFile(workspaceRoot: string, sessionKey: string): string {
|
|
519
|
+
return path.join(refusalStateDir(workspaceRoot), `${sessionKey}${SHADOW_SUFFIX}`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Parse one shadow record. `null` for anything unreadable — same fail-open
|
|
524
|
+
* contract as `parseRecord`, and for the same reason: this is the Stop path.
|
|
525
|
+
*/
|
|
526
|
+
export function parseShadowRecord(raw: string): ShadowRecord | null {
|
|
527
|
+
let decoded: unknown;
|
|
528
|
+
try {
|
|
529
|
+
decoded = JSON.parse(raw);
|
|
530
|
+
} catch {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) return null;
|
|
534
|
+
const o = decoded as Record<string, unknown>;
|
|
535
|
+
const rowsRaw = o['would_refuse_again'];
|
|
536
|
+
if (!Array.isArray(rowsRaw)) return null;
|
|
537
|
+
const rows: WouldRefuseAgainRow[] = [];
|
|
538
|
+
for (const r of rowsRaw) {
|
|
539
|
+
if (typeof r !== 'object' || r === null || Array.isArray(r)) continue;
|
|
540
|
+
const ro = r as Record<string, unknown>;
|
|
541
|
+
const det = ro['detector'];
|
|
542
|
+
const turn = ro['turn'];
|
|
543
|
+
const at = ro['at'];
|
|
544
|
+
const layer = ro['layer'];
|
|
545
|
+
if (!isDetector(det)) continue;
|
|
546
|
+
if (typeof turn !== 'number' || !Number.isFinite(turn)) continue;
|
|
547
|
+
if (typeof at !== 'string') continue;
|
|
548
|
+
if (layer !== 'stop_hook_active' && layer !== 'refused_turn') continue;
|
|
549
|
+
const setByRaw = ro['set_by'];
|
|
550
|
+
const set_by: ShadowSetBy =
|
|
551
|
+
setByRaw === 'ours' || setByRaw === 'foreign' || setByRaw === 'unknown'
|
|
552
|
+
? setByRaw
|
|
553
|
+
: 'unknown';
|
|
554
|
+
rows.push({ detector: det, turn, at, layer, set_by });
|
|
555
|
+
}
|
|
556
|
+
const num = (v: unknown): number =>
|
|
557
|
+
typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : 0;
|
|
558
|
+
const observed = o['retries_observed'];
|
|
559
|
+
const byLayer = emptyRetryCounts();
|
|
560
|
+
if (typeof observed === 'object' && observed !== null && !Array.isArray(observed)) {
|
|
561
|
+
for (const l of SHADOW_LAYERS) {
|
|
562
|
+
byLayer[l] = num((observed as Record<string, unknown>)[l]);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
would_refuse_again: rows,
|
|
567
|
+
retries_observed: byLayer,
|
|
568
|
+
dropped: num(o['dropped']),
|
|
569
|
+
first_at: typeof o['first_at'] === 'string' ? o['first_at'] : '',
|
|
570
|
+
last_at: typeof o['last_at'] === 'string' ? o['last_at'] : '',
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Fold one retry observation into a shadow record. Pure — the caller writes.
|
|
576
|
+
*
|
|
577
|
+
* `detectors` EMPTY is a real and load-bearing input: a retry whose detectors
|
|
578
|
+
* all came back silent raises `retries_observed` for its layer and adds no
|
|
579
|
+
* row, which is the only way Q1 can ever read below 1.
|
|
580
|
+
*/
|
|
581
|
+
export function foldShadow(
|
|
582
|
+
prev: ShadowRecord | null,
|
|
583
|
+
input: {
|
|
584
|
+
detectors: readonly RefusalDetectorId[];
|
|
585
|
+
turnOrdinal: number;
|
|
586
|
+
at: string;
|
|
587
|
+
layer: ShadowLayer;
|
|
588
|
+
/**
|
|
589
|
+
* Omitted by a caller that cannot establish it, which is `unknown` —
|
|
590
|
+
* never `foreign`, because a missing answer is not evidence of a
|
|
591
|
+
* neighbour.
|
|
592
|
+
*/
|
|
593
|
+
setBy?: ShadowSetBy;
|
|
594
|
+
},
|
|
595
|
+
): ShadowRecord {
|
|
596
|
+
const rows = [...(prev?.would_refuse_again ?? [])];
|
|
597
|
+
const set_by: ShadowSetBy = input.setBy ?? 'unknown';
|
|
598
|
+
for (const d of input.detectors) {
|
|
599
|
+
rows.push({
|
|
600
|
+
detector: d,
|
|
601
|
+
turn: input.turnOrdinal,
|
|
602
|
+
at: input.at,
|
|
603
|
+
layer: input.layer,
|
|
604
|
+
set_by,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
let dropped = prev?.dropped ?? 0;
|
|
608
|
+
if (rows.length > SHADOW_MAX_ROWS) {
|
|
609
|
+
dropped += rows.length - SHADOW_MAX_ROWS;
|
|
610
|
+
rows.splice(0, rows.length - SHADOW_MAX_ROWS);
|
|
611
|
+
}
|
|
612
|
+
const retries = emptyRetryCounts();
|
|
613
|
+
for (const l of SHADOW_LAYERS) retries[l] = prev?.retries_observed[l] ?? 0;
|
|
614
|
+
retries[input.layer] += 1;
|
|
615
|
+
return {
|
|
616
|
+
would_refuse_again: rows,
|
|
617
|
+
retries_observed: retries,
|
|
618
|
+
dropped,
|
|
619
|
+
first_at: prev?.first_at !== undefined && prev.first_at !== '' ? prev.first_at : input.at,
|
|
620
|
+
last_at: input.at,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Who set `stop_hook_active`, decided from this gate's OWN refusal record.
|
|
626
|
+
*
|
|
627
|
+
* Three states, and the third is not a hedge. `ENOENT` means this gate never
|
|
628
|
+
* refused in this session, so a flag the host set anyway came from somewhere
|
|
629
|
+
* else — `foreign`. Any OTHER read failure means the answer is unavailable, and
|
|
630
|
+
* reporting that as `foreign` would turn a broken state directory into evidence
|
|
631
|
+
* that a neighbour acted. The lane's own framing is the reason: what this suite
|
|
632
|
+
* can say about a neighbour is only what it observes, and an unreadable file is
|
|
633
|
+
* not an observation.
|
|
634
|
+
*
|
|
635
|
+
* `parseRecord` returning `null` on a present file is also `unknown`: the file
|
|
636
|
+
* exists, so something wrote it, and we cannot say what.
|
|
637
|
+
*/
|
|
638
|
+
export function shadowSetBy(workspaceRoot: string, sessionKey: string): ShadowSetBy {
|
|
639
|
+
let raw: string;
|
|
640
|
+
try {
|
|
641
|
+
raw = fs.readFileSync(sessionRefusalFile(workspaceRoot, sessionKey), 'utf-8');
|
|
642
|
+
} catch (err) {
|
|
643
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
644
|
+
return code === 'ENOENT' ? 'foreign' : 'unknown';
|
|
645
|
+
}
|
|
646
|
+
return parseRecord(raw) === null ? 'unknown' : 'ours';
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** This session's shadow record, or `null` when it has never retried. */
|
|
650
|
+
export function readShadowRecord(workspaceRoot: string, sessionKey: string): ShadowRecord | null {
|
|
651
|
+
try {
|
|
652
|
+
return parseShadowRecord(
|
|
653
|
+
fs.readFileSync(sessionShadowFile(workspaceRoot, sessionKey), 'utf-8'),
|
|
654
|
+
);
|
|
655
|
+
} catch {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
338
660
|
// ---------------------------------------------------------------------------
|
|
339
661
|
// TTL — step 1.2
|
|
340
662
|
// ---------------------------------------------------------------------------
|
|
@@ -350,8 +672,9 @@ export function readInstallBoundary(): InstallBoundary {
|
|
|
350
672
|
* LIVENESS and this bounds EVIDENCE — pruning evidence on a liveness clock would
|
|
351
673
|
* delete the corpus this roadmap exists to read.
|
|
352
674
|
*
|
|
353
|
-
*
|
|
354
|
-
* the directory's file count
|
|
675
|
+
* Two things would show 90 is the wrong number: a rollup window longer than it
|
|
676
|
+
* being needed to read a rate, or the directory's file count becoming a
|
|
677
|
+
* measured cost rather than a projected one.
|
|
355
678
|
*/
|
|
356
679
|
export const REFUSAL_STATE_MAX_AGE_DAYS = 90;
|
|
357
680
|
|
|
@@ -389,6 +712,53 @@ export function pruneAgedRefusalState(
|
|
|
389
712
|
if (!name.endsWith('.json')) continue;
|
|
390
713
|
result.scanned += 1;
|
|
391
714
|
const file = path.join(dir, name);
|
|
715
|
+
// A shadow record is a sibling in the same directory whose name also
|
|
716
|
+
// ends `.json`, and `parseRecord` rejects it for lacking `refused_at`.
|
|
717
|
+
// Left to the branch below it would land in `kept` FOREVER — the
|
|
718
|
+
// unbounded growth this pruner exists to stop, reintroduced by the
|
|
719
|
+
// instrument step 2.1 added. Its own clock is `last_at`.
|
|
720
|
+
if (name.endsWith(SHADOW_SUFFIX)) {
|
|
721
|
+
let shadow: ShadowRecord | null = null;
|
|
722
|
+
try {
|
|
723
|
+
shadow = parseShadowRecord(fs.readFileSync(file, 'utf-8'));
|
|
724
|
+
} catch {
|
|
725
|
+
shadow = null;
|
|
726
|
+
}
|
|
727
|
+
const shadowAt = shadow === null ? NaN : Date.parse(shadow.last_at);
|
|
728
|
+
// KEEP-ON-UNPARSEABLE IS NOT INHERITED, and the asymmetry with the
|
|
729
|
+
// branch below is the point. A refusal record that will not parse
|
|
730
|
+
// may still carry the wedge marker, so deleting it is the one
|
|
731
|
+
// irreversible move available and it is refused. A shadow record
|
|
732
|
+
// is pure measurement with nothing to protect — keeping one whose
|
|
733
|
+
// `last_at` cannot be read means it never ages out at all, which
|
|
734
|
+
// is the unbounded growth this pruner exists to stop, surviving
|
|
735
|
+
// for exactly the subset nobody can read. An unreadable one is
|
|
736
|
+
// pruned on the filesystem mtime, which is the weaker clock this
|
|
737
|
+
// function refuses for refusal records precisely because they are
|
|
738
|
+
// evidence; for a corrupt measurement it is the only clock left.
|
|
739
|
+
if (!Number.isFinite(shadowAt)) {
|
|
740
|
+
let mtime = NaN;
|
|
741
|
+
try {
|
|
742
|
+
mtime = fs.statSync(file).mtimeMs;
|
|
743
|
+
} catch {
|
|
744
|
+
mtime = NaN;
|
|
745
|
+
}
|
|
746
|
+
if (!Number.isFinite(mtime) || mtime >= cutoffMs) {
|
|
747
|
+
result.kept += 1;
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
} else if (shadowAt >= cutoffMs) {
|
|
751
|
+
result.kept += 1;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
fs.unlinkSync(file);
|
|
756
|
+
result.pruned += 1;
|
|
757
|
+
} catch {
|
|
758
|
+
result.kept += 1;
|
|
759
|
+
}
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
392
762
|
let rec: RefusalRecord | null = null;
|
|
393
763
|
try {
|
|
394
764
|
rec = parseRecord(fs.readFileSync(file, 'utf-8'));
|
|
@@ -492,6 +862,12 @@ export function collectRefusalStats(workspaceRoot: string): RefusalStats {
|
|
|
492
862
|
const versions = new Map<string, VersionBucket>();
|
|
493
863
|
for (const name of entries) {
|
|
494
864
|
if (!name.endsWith('.json')) continue;
|
|
865
|
+
// Shadow records share the directory and the extension. `parseRecord`
|
|
866
|
+
// would reject each one anyway, so this line changes no number — it is
|
|
867
|
+
// here because relying on another function's strictness for a
|
|
868
|
+
// correctness property is how the property disappears when that
|
|
869
|
+
// function is loosened.
|
|
870
|
+
if (name.endsWith(SHADOW_SUFFIX)) continue;
|
|
495
871
|
let rec: RefusalRecord | null = null;
|
|
496
872
|
try {
|
|
497
873
|
rec = parseRecord(fs.readFileSync(path.join(dir, name), 'utf-8'));
|
|
@@ -535,3 +911,223 @@ export function collectRefusalStats(workspaceRoot: string): RefusalStats {
|
|
|
535
911
|
stats.byVersion = [...versions.values()].sort((a, b) => (a.version < b.version ? 1 : -1));
|
|
536
912
|
return stats;
|
|
537
913
|
}
|
|
914
|
+
|
|
915
|
+
// ---------------------------------------------------------------------------
|
|
916
|
+
// Q1 — the shadow rollup (step 2.2)
|
|
917
|
+
// ---------------------------------------------------------------------------
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* One layer's Q1 inputs, kept as counts so the ratio is computed where it is
|
|
921
|
+
* read rather than stored.
|
|
922
|
+
*
|
|
923
|
+
* SPLIT BY LAYER BEFORE DETECTOR, and that order is the shape's whole point
|
|
924
|
+
* rather than a presentation choice. A `stop_hook_active` retry follows ANY
|
|
925
|
+
* stop concern's block — not only this gate's — so pooling the two layers
|
|
926
|
+
* divides another concern's retries into this gate's refusals and calls the
|
|
927
|
+
* result a re-refusal share. `ShadowRecord.retries_observed` is keyed the same
|
|
928
|
+
* way so the denominator splits with the numerator; an independent review
|
|
929
|
+
* found the first version pooling the denominator, which left the per-layer
|
|
930
|
+
* ratio uncomputable even though every row already carried its `layer`.
|
|
931
|
+
*/
|
|
932
|
+
export interface ShadowLayerStats {
|
|
933
|
+
layer: ShadowLayer;
|
|
934
|
+
/**
|
|
935
|
+
* Retries observed on this layer across every readable record — the
|
|
936
|
+
* denominator, counting retries that came back CLEAN as well as refusing
|
|
937
|
+
* ones. A clean retry writes no row, so without this a zero numerator and
|
|
938
|
+
* "no retry happened" are the same empty list.
|
|
939
|
+
*/
|
|
940
|
+
retries: number;
|
|
941
|
+
/** Shadow rows on this layer, by detector — the numerators. */
|
|
942
|
+
byDetector: DetectorCounts;
|
|
943
|
+
/**
|
|
944
|
+
* Rows on this layer, summed.
|
|
945
|
+
*
|
|
946
|
+
* MAY EXCEED `retries`, legitimately: one retry that would have been
|
|
947
|
+
* refused by two detectors writes two rows, and the demotion contract's
|
|
948
|
+
* attribution rule counts it once for each. A per-detector ratio stays
|
|
949
|
+
* within [0, 1]; this total is not a share and is printed as a count.
|
|
950
|
+
*/
|
|
951
|
+
rows: number;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Rollup over every session's shadow record under a workspace.
|
|
956
|
+
*
|
|
957
|
+
* Two bounds travel with every number this carries. Both are properties of the
|
|
958
|
+
* instrument rather than of a particular reading, both are stated on
|
|
959
|
+
* `ShadowRecord`, and both are repeated here because a consumer reads this
|
|
960
|
+
* type and not that comment:
|
|
961
|
+
*
|
|
962
|
+
* - A retry whose transcript is unreadable or oversized records NOTHING —
|
|
963
|
+
* no row and no retry. The denominator shrinks with the numerator, so a
|
|
964
|
+
* Q1 read off this is an UPPER bound rather than a point estimate.
|
|
965
|
+
* - On a host that sends no `session_id`, every session shares one record
|
|
966
|
+
* and one counter. `files` below then counts FILES and not sessions, and
|
|
967
|
+
* a reader taking one for the other over-states the sample.
|
|
968
|
+
*/
|
|
969
|
+
export interface ShadowStats {
|
|
970
|
+
/**
|
|
971
|
+
* Shadow FILES parsed. Deliberately not named `sessions`: one file is one
|
|
972
|
+
* session only on a host that sends a `session_id`.
|
|
973
|
+
*/
|
|
974
|
+
files: number;
|
|
975
|
+
/** Files present but unparseable. A non-zero value makes every rate a floor. */
|
|
976
|
+
unreadable: number;
|
|
977
|
+
/**
|
|
978
|
+
* Rows dropped to `SHADOW_MAX_ROWS`, summed. Non-zero means the row arrays
|
|
979
|
+
* are samples of their own sessions and every numerator is a floor.
|
|
980
|
+
*/
|
|
981
|
+
dropped: number;
|
|
982
|
+
byLayer: ShadowLayerStats[];
|
|
983
|
+
earliest: string | null;
|
|
984
|
+
latest: string | null;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** A zeroed rollup, DERIVED from `SHADOW_LAYERS` for the reason `emptyCounts` gives. */
|
|
988
|
+
export function emptyShadowStats(): ShadowStats {
|
|
989
|
+
return {
|
|
990
|
+
files: 0,
|
|
991
|
+
unreadable: 0,
|
|
992
|
+
dropped: 0,
|
|
993
|
+
byLayer: SHADOW_LAYERS.map((layer) => ({
|
|
994
|
+
layer,
|
|
995
|
+
retries: 0,
|
|
996
|
+
byDetector: emptyCounts(),
|
|
997
|
+
rows: 0,
|
|
998
|
+
})),
|
|
999
|
+
earliest: null,
|
|
1000
|
+
latest: null,
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Aggregate every shadow record under a workspace.
|
|
1006
|
+
*
|
|
1007
|
+
* Reads through `parseShadowRecord`, never by hand: the gate writes these
|
|
1008
|
+
* files, and a reader carrying its own idea of their shape is how a producer
|
|
1009
|
+
* and a consumer drift apart — the exact seam the 2026-09-30 review round
|
|
1010
|
+
* found uncrossed between the verification recorder and its classifier.
|
|
1011
|
+
*/
|
|
1012
|
+
export function collectShadowStats(workspaceRoot: string): ShadowStats {
|
|
1013
|
+
const dir = refusalStateDir(workspaceRoot);
|
|
1014
|
+
const stats = emptyShadowStats();
|
|
1015
|
+
const byLayer = new Map(stats.byLayer.map((b) => [b.layer, b]));
|
|
1016
|
+
let entries: string[];
|
|
1017
|
+
try {
|
|
1018
|
+
entries = fs.readdirSync(dir);
|
|
1019
|
+
} catch {
|
|
1020
|
+
return stats;
|
|
1021
|
+
}
|
|
1022
|
+
for (const name of entries) {
|
|
1023
|
+
if (!name.endsWith(SHADOW_SUFFIX)) continue;
|
|
1024
|
+
let rec: ShadowRecord | null = null;
|
|
1025
|
+
try {
|
|
1026
|
+
rec = parseShadowRecord(fs.readFileSync(path.join(dir, name), 'utf-8'));
|
|
1027
|
+
} catch {
|
|
1028
|
+
rec = null;
|
|
1029
|
+
}
|
|
1030
|
+
if (rec === null) {
|
|
1031
|
+
stats.unreadable += 1;
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
stats.files += 1;
|
|
1035
|
+
stats.dropped += rec.dropped;
|
|
1036
|
+
for (const layer of SHADOW_LAYERS) {
|
|
1037
|
+
const bucket = byLayer.get(layer);
|
|
1038
|
+
if (bucket !== undefined) bucket.retries += rec.retries_observed[layer] ?? 0;
|
|
1039
|
+
}
|
|
1040
|
+
for (const row of rec.would_refuse_again) {
|
|
1041
|
+
const bucket = byLayer.get(row.layer);
|
|
1042
|
+
if (bucket === undefined) continue;
|
|
1043
|
+
bucket.byDetector[row.detector] += 1;
|
|
1044
|
+
bucket.rows += 1;
|
|
1045
|
+
}
|
|
1046
|
+
// EMPTY STRINGS ARE SKIPPED, NOT COMPARED. `parseShadowRecord` coerces a
|
|
1047
|
+
// missing `first_at` to `''`, and `'' < <any ISO stamp>` is true, so a
|
|
1048
|
+
// record lacking the field would win the earliest comparison and the
|
|
1049
|
+
// report would print an empty window start beside a real end. An absent
|
|
1050
|
+
// timestamp is not an earlier one.
|
|
1051
|
+
if (rec.first_at !== '' && (stats.earliest === null || rec.first_at < stats.earliest)) {
|
|
1052
|
+
stats.earliest = rec.first_at;
|
|
1053
|
+
}
|
|
1054
|
+
if (rec.last_at !== '' && (stats.latest === null || rec.last_at > stats.latest)) {
|
|
1055
|
+
stats.latest = rec.last_at;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return stats;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Detectors whose share may NOT be reported off a shadow rollup, because the
|
|
1063
|
+
* record cannot tell a dispatch-suppressed retry from a clean one.
|
|
1064
|
+
*
|
|
1065
|
+
* `runDetectors` in `turn_end_gate_hook.ts` skips A, D and F when a subagent
|
|
1066
|
+
* dispatch is open — they did not run, so the turn is not an observation of
|
|
1067
|
+
* them staying silent. `recordShadow` then increments `retries_observed`
|
|
1068
|
+
* unconditionally and writes no dispatch flag, so every dispatch-suppressed
|
|
1069
|
+
* retry lands in those three denominators as a "would not have refused" that
|
|
1070
|
+
* nobody observed. In a suite whose delegation policy dispatches by default
|
|
1071
|
+
* that is not a rare branch.
|
|
1072
|
+
*
|
|
1073
|
+
* The demotion contract already forbids publishing it: *"A rollup that cannot
|
|
1074
|
+
* tell a dispatch-suppressed turn from a clean one may not report Q2 for A or
|
|
1075
|
+
* D at all."* This set is that clause applied to the shadow rollup, extended to
|
|
1076
|
+
* F — the contract's attribution paragraph names only A and D because it
|
|
1077
|
+
* predates F, while its own § The six detectors table and the gate both gate
|
|
1078
|
+
* all three. The wider set is the safe reading of a censoring rule.
|
|
1079
|
+
*
|
|
1080
|
+
* Closing this needs a `dispatch_open` flag on the shadow row, which is a
|
|
1081
|
+
* PRODUCER change. Until then these three are censored rather than estimated.
|
|
1082
|
+
*/
|
|
1083
|
+
export const DISPATCH_CENSORED_DETECTORS: ReadonlySet<RefusalDetectorId> =
|
|
1084
|
+
new Set<RefusalDetectorId>(['promissory', 'completion', 'untested']);
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* The **retry-conditioned re-refusal share** for one detector on one layer, or
|
|
1088
|
+
* `null` when the layer observed no retry.
|
|
1089
|
+
*
|
|
1090
|
+
* ```
|
|
1091
|
+
* THIS IS NOT THE Q1 `turn-end-detector-demotion.md` REGISTERS BARS AGAINST.
|
|
1092
|
+
* NEVER COMPARE IT TO A BAR IN THAT TABLE.
|
|
1093
|
+
* ```
|
|
1094
|
+
*
|
|
1095
|
+
* The contract defines Q1 as *"of **a detector's** eligible initial refusals,
|
|
1096
|
+
* the share whose immediate retry is refused again by the same detector"*. Both
|
|
1097
|
+
* halves of that are conditioned on the detector. This function conditions
|
|
1098
|
+
* NEITHER: the denominator is every retry observed on the layer, whatever
|
|
1099
|
+
* refusal produced it, and the numerator is every row where this detector would
|
|
1100
|
+
* have fired, whatever refusal produced the retry.
|
|
1101
|
+
*
|
|
1102
|
+
* **They disagree in direction, so this is not a bound either.** Take ten
|
|
1103
|
+
* retries on a layer, nine following `verification` refusals and one following
|
|
1104
|
+
* a `language` refusal, with one `language` row. The contract's Q1 for B is
|
|
1105
|
+
* 1/1 = 100 % — bar crossed, a demotion study authorised. This function returns
|
|
1106
|
+
* 1/10 = 10 % — bar not crossed. Opposite verdicts on identical evidence. The
|
|
1107
|
+
* numerator can also run the other way, since a `language` row on a retry that
|
|
1108
|
+
* followed a `verification` refusal counts here and does not count there.
|
|
1109
|
+
*
|
|
1110
|
+
* **Why it is shaped this way, which is a producer limit rather than a choice.**
|
|
1111
|
+
* `ShadowRecord` carries no originating detector: `retries_observed` is keyed by
|
|
1112
|
+
* layer alone and a row records only what WOULD fire, never what did. The
|
|
1113
|
+
* session's `RefusalRecord` holds per-detector COUNTS for the whole session and
|
|
1114
|
+
* `refused_turn` for the last refused turn, so the per-turn origin needed to
|
|
1115
|
+
* condition this denominator is not on disk anywhere. Computing the registered
|
|
1116
|
+
* Q1 needs the producer to record it; that is named as the open residue on
|
|
1117
|
+
* `road-to-a-stop-that-holds` step 2.2 rather than papered over here.
|
|
1118
|
+
*
|
|
1119
|
+
* `null` RATHER THAN ZERO on an empty denominator, and that distinction is why
|
|
1120
|
+
* `retries_observed` exists at all: zero means "retries happened and this
|
|
1121
|
+
* detector would not have refused any of them", a finding about the detector,
|
|
1122
|
+
* while null means "nothing was observed", a finding about the sample.
|
|
1123
|
+
* Collapsing them publishes the second as the first.
|
|
1124
|
+
*/
|
|
1125
|
+
export function retryConditionedShare(
|
|
1126
|
+
stats: ShadowStats,
|
|
1127
|
+
layer: ShadowLayer,
|
|
1128
|
+
detector: RefusalDetectorId,
|
|
1129
|
+
): number | null {
|
|
1130
|
+
const bucket = stats.byLayer.find((b) => b.layer === layer);
|
|
1131
|
+
if (bucket === undefined || bucket.retries === 0) return null;
|
|
1132
|
+
return bucket.byDetector[detector] / bucket.retries;
|
|
1133
|
+
}
|