@event4u/agent-config 16.0.0 → 16.2.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 +289 -140
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +74 -27
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/commands/work.md +4 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +47 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +19 -23
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/rules/ui-audit-gate.md +1 -2
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +80 -4
- package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +30 -5
- package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
- package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +218 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -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 +260 -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/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-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-7XGBX6ZH.js} +973 -555
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-FP36SWJD.js} +587 -141
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-IGD6ERX6.js} +62 -7
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-JNMVLWLR.js} +99 -14
- package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
- package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
- 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 +985 -831
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +7 -7
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +3 -3
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +90 -43
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +7682 -2772
- package/dist/hooks/dispatch.sha256 +1 -0
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2200 -1559
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3066 -1470
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +40 -5
- package/dist/shared/missionExecution.js +97 -0
- 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/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +28 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- 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 +260 -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/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 +168 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +36 -5
- package/src/config/continuity-surface.json +23 -2
- package/src/config/gate-coverage.yml +377 -13
- package/src/config/gate-violation-baselines.json +10 -6
- package/src/config/hook-latency-budget.json +21 -0
- package/src/config/hook-token-budget.json +4 -2
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/metric-registry.yml +114 -0
- package/src/config/placeholder-drift-budget.json +10 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +33 -5
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +7 -20
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +173 -0
- package/src/scripts/_cli/doctor_network_posture.ts +226 -0
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/body_portable.ts +102 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/cc_transcript.ts +132 -1
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/concern_sla_window.ts +200 -0
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/forge_protection.ts +381 -0
- package/src/scripts/_lib/forge_reader.ts +558 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/host_env_write.ts +111 -0
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +513 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/probe_inputs.ts +271 -0
- package/src/scripts/_lib/read_surface_scan.ts +322 -0
- package/src/scripts/_lib/release_highlights.ts +201 -11
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/shell_write_shapes.ts +108 -0
- package/src/scripts/_lib/structural_hiding.ts +622 -0
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/turn_end_refusals.ts +525 -4
- package/src/scripts/_lib/turn_end_transcript.ts +216 -0
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -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/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ai_team/team_dispatch.ts +4 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/before_complete_hook.ts +340 -23
- package/src/scripts/bench_hook_latency.ts +412 -5
- 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 +866 -0
- package/src/scripts/check_finding_dispositions.ts +22 -3
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +1025 -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 +47 -5
- package/src/scripts/check_kill_switch_table.ts +294 -0
- package/src/scripts/check_placeholder_drift.ts +221 -0
- package/src/scripts/check_preamble_payload_budget.ts +75 -2
- package/src/scripts/check_read_surface_coverage.ts +515 -0
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +568 -0
- package/src/scripts/check_routing_coverage.ts +378 -11
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_static_parity.ts +17 -0
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/ci_settle.ts +145 -19
- package/src/scripts/closure_scan.ts +447 -0
- package/src/scripts/compile_hook_manifest.ts +102 -18
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/external_sources_denylist.json +29 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +127 -11
- package/src/scripts/hooks/block_config_weakening.ts +229 -4
- package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
- package/src/scripts/hooks/block_plumbing_writes.ts +314 -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 +1 -2
- package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
- package/src/scripts/hooks/concern_registry.ts +6 -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 +221 -5
- package/src/scripts/hooks/design_slop_hook.ts +1 -2
- package/src/scripts/hooks/dispatch_hook.ts +25 -19
- 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 +97 -2
- package/src/scripts/hooks/exit_codes.ts +128 -0
- package/src/scripts/hooks/host_lowering.json +1 -0
- package/src/scripts/hooks/host_lowering.ts +128 -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 +539 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +328 -0
- package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
- package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
- 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 +55 -4
- package/src/scripts/hooks/run_continuation_hook.ts +30 -35
- 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 +49 -0
- package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
- package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
- package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
- package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
- package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
- package/src/scripts/hooks_doctor.ts +85 -0
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +109 -210
- package/src/scripts/janitor.ts +7 -0
- package/src/scripts/lint_decision_classes.ts +464 -0
- 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 +66 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +139 -12
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/lint_skill_descriptions.ts +1 -1
- package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
- package/src/scripts/mcp_server/prompts.ts +142 -1
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
- package/src/scripts/measure_turn_end_gate.ts +193 -0
- package/src/scripts/memory_lookup.ts +1 -1
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/prepack-check.mjs +53 -0
- package/src/scripts/release.ts +14 -98
- package/src/scripts/release_drill.ts +267 -1
- package/src/scripts/release_publication.ts +64 -1
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_invocation_surface.ts +383 -0
- package/src/scripts/report_obligation_carriers.ts +699 -0
- package/src/scripts/report_obligation_writer_split.ts +236 -0
- package/src/scripts/report_skill_menu_census.ts +208 -0
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/roadmap_verify_share.ts +213 -0
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +48 -0
- package/src/scripts/schemas/review-findings.schema.json +4 -0
- package/src/scripts/schemas/skill.schema.json +44 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/self_review_gate.ts +359 -10
- package/src/scripts/semantic_parity_census.ts +314 -0
- package/src/scripts/session_eol_report.ts +54 -1
- package/src/scripts/sweep_dead_scan_roots.ts +1 -1
- package/src/scripts/trigger_eval_grandfather.json +0 -3
- package/src/scripts/trigger_eval_rotation.ts +122 -27
- package/src/scripts/ui_conformance_probe.ts +774 -0
- package/src/scripts/update_prices.ts +85 -3
- package/src/scripts/write_bundle_digest.mjs +74 -0
- package/src/server/schemas/settings.ts +62 -7
- package/src/shared/missionExecution.ts +126 -0
- package/src/templates/consumer-settings/ONBOARDING.md +1 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
- package/dist/ui/assets/index-D0jaCRPM.js +0 -29
- package/dist/ui/assets/index-jSfMW8_t.css +0 -1
- /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* check_enforcement_matrix.ts — generate the per-slot enforcement table in
|
|
4
|
+
* `docs/enforcement-by-host.md` from `src/scripts/hooks/host_lowering.yaml`,
|
|
5
|
+
* and fail when the committed table and the configuration disagree.
|
|
6
|
+
*
|
|
7
|
+
* WHAT THIS PROVES, AND WHAT IT DOES NOT
|
|
8
|
+
* --------------------------------------
|
|
9
|
+
* It proves the published table agrees with the YAML. It proves NOTHING about
|
|
10
|
+
* any host's runtime behavior. A mistaken lowering rule produces a perfectly
|
|
11
|
+
* synchronised and perfectly false document, and this gate would be green over
|
|
12
|
+
* it. That is why the generated region is titled CONFIGURED behavior rather
|
|
13
|
+
* than ENFORCED behavior, even though the file it lives in is called
|
|
14
|
+
* `enforcement-by-host.md` — the honest claim is a claim about a configuration
|
|
15
|
+
* file, and the region says so in its own first line rather than leaving the
|
|
16
|
+
* distinction to this docstring. Establishing the other half needs a runtime
|
|
17
|
+
* conformance test against a real host, which this tree does not have for any
|
|
18
|
+
* host and does not pretend to.
|
|
19
|
+
*
|
|
20
|
+
* WHY THE TABLE IS GENERATED AT ALL
|
|
21
|
+
* ---------------------------------
|
|
22
|
+
* A host-level binary cell has to summarise a column of slot values, and no
|
|
23
|
+
* binary value is faithful when the column disagrees with itself — `claude`
|
|
24
|
+
* blocks on 3 of its 9 lowerable slots. Generating the projection at slot
|
|
25
|
+
* granularity removes the summary step rather than correcting it once.
|
|
26
|
+
*
|
|
27
|
+
* ONE PROJECTION, IMPORTED
|
|
28
|
+
* ------------------------
|
|
29
|
+
* The YAML-to-per-slot reading is `report_enforcement_drift.readSlots`, and it
|
|
30
|
+
* is imported rather than reimplemented. Two copies of a projection are what
|
|
31
|
+
* this document already suffered from at the prose layer; writing a second copy
|
|
32
|
+
* in the gate that fixes it would be the same defect one level down.
|
|
33
|
+
*
|
|
34
|
+
* THE CLOSED VOCABULARY, AND THE ONE VALUE NOTHING EMITS
|
|
35
|
+
* -----------------------------------------------------
|
|
36
|
+
* A cell carries one of `refusal` · `halt-by-state` · `warning` · `unenforced`.
|
|
37
|
+
* {@link outcomeFor} can return three of those four. `halt-by-state` has no
|
|
38
|
+
* backing field in the lowering configuration and therefore no cell can
|
|
39
|
+
* legitimately carry it today — it is defined in the document's hand-written
|
|
40
|
+
* taxonomy and marked unused so that a reader who meets it later does not meet
|
|
41
|
+
* an unexplained category. {@link unbackedOutcomes} is what keeps the
|
|
42
|
+
* definition from becoming a claim: a committed cell carrying a value the
|
|
43
|
+
* configuration cannot produce is a finding with its own message, which is the
|
|
44
|
+
* check the dissenting council seat asked for when it argued the value should
|
|
45
|
+
* not exist at all.
|
|
46
|
+
*
|
|
47
|
+
* That guard is honest about its own reach. It catches a value the derivation
|
|
48
|
+
* can never return, which is exactly the phantom-value case. It cannot catch a
|
|
49
|
+
* value that is reachable but wrong for the row — that is the drift comparison
|
|
50
|
+
* above, and behind that, the configured-not-enforced caveat.
|
|
51
|
+
*
|
|
52
|
+
* Exit codes: 0 in sync · 1 drift or an unbacked cell · 2 a source is missing,
|
|
53
|
+
* unreadable, or has lost its markers.
|
|
54
|
+
*/
|
|
55
|
+
import * as fs from 'node:fs';
|
|
56
|
+
import * as os from 'node:os';
|
|
57
|
+
import * as path from 'node:path';
|
|
58
|
+
|
|
59
|
+
import * as yaml from 'js-yaml';
|
|
60
|
+
import process from 'node:process';
|
|
61
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
62
|
+
|
|
63
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
64
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
65
|
+
import { splitMarkdownRow } from './_lib/md_table.js';
|
|
66
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
67
|
+
import { DEFAULT_SURFACE, type HostLowering, parseHostLowering } from './hooks/host_lowering.js';
|
|
68
|
+
import { type SlotReading, deriveDeny, readSlots } from './report_enforcement_drift.js';
|
|
69
|
+
|
|
70
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
71
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
72
|
+
|
|
73
|
+
export const GATE = 'check_enforcement_matrix';
|
|
74
|
+
export const DOC_REL = path.join('docs', 'enforcement-by-host.md');
|
|
75
|
+
export const LOWERING_REL = path.join('src', 'scripts', 'hooks', 'host_lowering.yaml');
|
|
76
|
+
|
|
77
|
+
export const BEGIN_MARKER = '<!-- BEGIN GENERATED: enforcement-configured-by-slot -->';
|
|
78
|
+
export const END_MARKER = '<!-- END GENERATED: enforcement-configured-by-slot -->';
|
|
79
|
+
|
|
80
|
+
/** The command a drift failure tells the reader to run. Printed, never guessed at. */
|
|
81
|
+
export const REGEN_COMMAND = `./scripts-run src/scripts/${GATE} --write`;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The closed set a configured-outcome cell may carry.
|
|
85
|
+
*
|
|
86
|
+
* Order is the ladder's own, strongest first, and the document's hand-written
|
|
87
|
+
* taxonomy lists them in the same order so the two read together.
|
|
88
|
+
*/
|
|
89
|
+
export const CONFIGURED_OUTCOMES = ['refusal', 'halt-by-state', 'warning', 'unenforced'] as const;
|
|
90
|
+
|
|
91
|
+
export type ConfiguredOutcome = (typeof CONFIGURED_OUTCOMES)[number];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* What the `Answered` column prints for a pair the configuration never dated.
|
|
95
|
+
*
|
|
96
|
+
* A word rather than an empty cell, because an empty cell in a rendered table
|
|
97
|
+
* is indistinguishable from a rendering bug, and this one is the finding.
|
|
98
|
+
* `lint_hook_manifest._check_slot_answers` is what actually refuses it; this
|
|
99
|
+
* gate's job is to make sure the published document cannot hide it.
|
|
100
|
+
*/
|
|
101
|
+
export const UNDATED = '`undated`';
|
|
102
|
+
|
|
103
|
+
export interface MatrixRow {
|
|
104
|
+
host: string;
|
|
105
|
+
slot: string;
|
|
106
|
+
outcome: ConfiguredOutcome;
|
|
107
|
+
/** The configuration values the outcome was read off, quoted for the table. */
|
|
108
|
+
backing: string;
|
|
109
|
+
/**
|
|
110
|
+
* `answered_at` for this pair, or `undated` when the configuration carries
|
|
111
|
+
* none. Printed LAST so the outcome stays in column three and
|
|
112
|
+
* {@link committedCells} keeps reading the same index it always did.
|
|
113
|
+
*/
|
|
114
|
+
answered: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface BuildResult {
|
|
118
|
+
rows: MatrixRow[];
|
|
119
|
+
/** Hosts the configuration models with no lowerable slot at all. */
|
|
120
|
+
slotlessHosts: string[];
|
|
121
|
+
/**
|
|
122
|
+
* Rows whose configuration cannot be projected into the vocabulary — a
|
|
123
|
+
* `fail_policy` this mapping does not know, or a blocking exit under a
|
|
124
|
+
* policy that discards it. Reported, never resolved by picking a side.
|
|
125
|
+
*/
|
|
126
|
+
contradictions: string[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The configured outcome for one slot reading.
|
|
131
|
+
*
|
|
132
|
+
* REFUSAL IS READ OFF `effective`, NOT `literal`, which is the same rule
|
|
133
|
+
* `report_enforcement_drift` applies and the reason the two cannot disagree: a
|
|
134
|
+
* row whose `verified` block has expired carries no blocking binding whatever
|
|
135
|
+
* its slot literals say.
|
|
136
|
+
*
|
|
137
|
+
* `fail_policy` only splits the non-refusal case. `propagate` means the
|
|
138
|
+
* dispatcher's exit code reaches the host, so a concern bound there can report
|
|
139
|
+
* and the run continues — `warning`. `discard` means the trampoline exits 0
|
|
140
|
+
* regardless, so the verdict reaches nothing — `unenforced`.
|
|
141
|
+
*
|
|
142
|
+
* Returns `null` for a `fail_policy` this mapping does not know. A gate that
|
|
143
|
+
* guessed at an unknown input would publish the guess in a table, which is the
|
|
144
|
+
* class of defect this whole file exists to remove.
|
|
145
|
+
*/
|
|
146
|
+
export function outcomeFor(reading: SlotReading, failPolicy: string): ConfiguredOutcome | null {
|
|
147
|
+
if (reading.effective !== null) return 'refusal';
|
|
148
|
+
if (failPolicy === 'propagate') return 'warning';
|
|
149
|
+
if (failPolicy === 'discard') return 'unenforced';
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The configuration values a cell was read off, as the table prints them. */
|
|
154
|
+
export function backingFor(reading: SlotReading, failPolicy: string): string {
|
|
155
|
+
const parts: string[] = [];
|
|
156
|
+
if (reading.effective !== null) {
|
|
157
|
+
parts.push(`\`block_exit: ${String(reading.effective)}\``);
|
|
158
|
+
} else if (reading.literal !== null) {
|
|
159
|
+
// The expiry case. Printing the literal beside the effective null is
|
|
160
|
+
// what makes a silently disarmed host visible as a difference rather
|
|
161
|
+
// than as a value that quietly turned into `null`.
|
|
162
|
+
parts.push(`\`block_exit: ${String(reading.literal)}\` disarmed by an expired \`verified\``);
|
|
163
|
+
} else {
|
|
164
|
+
parts.push('`block_exit: null`');
|
|
165
|
+
}
|
|
166
|
+
parts.push(`\`fail_policy: ${failPolicy}\``);
|
|
167
|
+
return parts.join(' · ');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Every (host, slot) pair the configuration models, in the file's own order.
|
|
172
|
+
*
|
|
173
|
+
* NOT SORTED, DELIBERATELY. `host_lowering.yaml` states that its `slots:` order
|
|
174
|
+
* is part of the generated bridge output and must not be sorted; keeping the
|
|
175
|
+
* same order here means a diff of this table corresponds line for line to a
|
|
176
|
+
* diff of the YAML, which is the review property the table is for.
|
|
177
|
+
*/
|
|
178
|
+
export function buildRows(lowering: HostLowering): BuildResult {
|
|
179
|
+
const rows: MatrixRow[] = [];
|
|
180
|
+
const slotlessHosts: string[] = [];
|
|
181
|
+
const contradictions: string[] = [];
|
|
182
|
+
for (const [host, surfaces] of lowering) {
|
|
183
|
+
const surface = surfaces.get(DEFAULT_SURFACE);
|
|
184
|
+
if (surface === undefined) continue;
|
|
185
|
+
const { slots } = readSlots(lowering, host);
|
|
186
|
+
if (slots.length === 0) {
|
|
187
|
+
slotlessHosts.push(host);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
for (const reading of slots) {
|
|
191
|
+
const outcome = outcomeFor(reading, surface.fail_policy);
|
|
192
|
+
if (outcome === null) {
|
|
193
|
+
contradictions.push(
|
|
194
|
+
`${host}/${reading.slot}: \`fail_policy: ${surface.fail_policy}\` has no ` +
|
|
195
|
+
'outcome in the closed vocabulary. Extend the mapping deliberately, in ' +
|
|
196
|
+
`\`src/scripts/${GATE}.ts\`, rather than letting a cell be guessed.`,
|
|
197
|
+
);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (reading.effective !== null && surface.fail_policy === 'discard') {
|
|
201
|
+
contradictions.push(
|
|
202
|
+
`${host}/${reading.slot}: a blocking \`block_exit\` under ` +
|
|
203
|
+
'`fail_policy: discard`. The configuration says both that the host ' +
|
|
204
|
+
'honours a refusal here and that the trampoline drops it. Fix the YAML; ' +
|
|
205
|
+
'this gate will not pick one.',
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
rows.push({
|
|
209
|
+
host,
|
|
210
|
+
slot: reading.slot,
|
|
211
|
+
outcome,
|
|
212
|
+
backing: backingFor(reading, surface.fail_policy),
|
|
213
|
+
answered: surface.slots.get(reading.slot)?.answered_at ?? UNDATED,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { rows, slotlessHosts, contradictions };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The outcome values the configuration can express on this tree.
|
|
222
|
+
*
|
|
223
|
+
* DERIVED FROM THE RAW CONFIGURATION FIELDS, NOT FROM {@link outcomeFor}'s
|
|
224
|
+
* output, and the distinction is the whole value of this function. The first
|
|
225
|
+
* version of it read the generated rows and collected the outcomes it found —
|
|
226
|
+
* which made a broken mapping self-backing: sabotaging `outcomeFor` to return
|
|
227
|
+
* `halt-by-state` for every propagating slot produced a region full of a value
|
|
228
|
+
* nothing emits, and the check passed, because the check's idea of "backed"
|
|
229
|
+
* was the same broken output. Measured, on this gate, before it shipped.
|
|
230
|
+
*
|
|
231
|
+
* So this is a deliberate SECOND expression of what the configuration carries,
|
|
232
|
+
* written against the fields themselves. `refusal` is backed when some slot
|
|
233
|
+
* carries an unexpired blocking exit; `warning` and `unenforced` are backed by
|
|
234
|
+
* the presence of the two `fail_policy` values. `halt-by-state` has no field to
|
|
235
|
+
* be backed by, on purpose — no schema element expresses it — so it can never
|
|
236
|
+
* enter this set while that stays true.
|
|
237
|
+
*
|
|
238
|
+
* The residual, stated rather than implied: a bug in BOTH this function and
|
|
239
|
+
* the mapping would agree, and this cannot catch a value that is reachable but
|
|
240
|
+
* wrong for its row. The drift comparison covers the second; nothing covers
|
|
241
|
+
* the first except that the two are written against different inputs.
|
|
242
|
+
*/
|
|
243
|
+
export function backedOutcomes(lowering: HostLowering): Set<ConfiguredOutcome> {
|
|
244
|
+
const backed = new Set<ConfiguredOutcome>();
|
|
245
|
+
for (const [host, surfaces] of lowering) {
|
|
246
|
+
const surface = surfaces.get(DEFAULT_SURFACE);
|
|
247
|
+
if (surface === undefined) continue;
|
|
248
|
+
const { slots } = readSlots(lowering, host);
|
|
249
|
+
if (slots.length === 0) continue;
|
|
250
|
+
if (slots.some((s) => s.effective !== null)) backed.add('refusal');
|
|
251
|
+
if (surface.fail_policy === 'propagate') backed.add('warning');
|
|
252
|
+
if (surface.fail_policy === 'discard') backed.add('unenforced');
|
|
253
|
+
}
|
|
254
|
+
return backed;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The derived one-line summary.
|
|
259
|
+
*
|
|
260
|
+
* Generated from the same rows as the table so the short answer a reader takes
|
|
261
|
+
* away is derived rather than remembered. `deriveDeny` is the host-level fold
|
|
262
|
+
* `report_enforcement_drift` already owns, reused here rather than recounted.
|
|
263
|
+
*/
|
|
264
|
+
export function summaryLine(lowering: HostLowering, rows: readonly MatrixRow[]): string {
|
|
265
|
+
const count = (o: ConfiguredOutcome): number => rows.filter((r) => r.outcome === o).length;
|
|
266
|
+
const refusals = rows.filter((r) => r.outcome === 'refusal');
|
|
267
|
+
const refusalHosts = [...new Set(refusals.map((r) => r.host))];
|
|
268
|
+
let hostsAll = 0;
|
|
269
|
+
let hostsAny = 0;
|
|
270
|
+
let modelled = 0;
|
|
271
|
+
for (const [host, surfaces] of lowering) {
|
|
272
|
+
if (surfaces.get(DEFAULT_SURFACE) === undefined) continue;
|
|
273
|
+
modelled += 1;
|
|
274
|
+
const derived = deriveDeny(readSlots(lowering, host).slots, true);
|
|
275
|
+
if (derived === 'all') hostsAll += 1;
|
|
276
|
+
if (derived === 'all' || derived === 'partial') hostsAny += 1;
|
|
277
|
+
}
|
|
278
|
+
const where =
|
|
279
|
+
refusalHosts.length === 0
|
|
280
|
+
? 'nowhere'
|
|
281
|
+
: refusalHosts
|
|
282
|
+
.map((h) => {
|
|
283
|
+
const slots = refusals
|
|
284
|
+
.filter((r) => r.host === h)
|
|
285
|
+
.map((r) => `\`${r.slot}\``)
|
|
286
|
+
.join(', ');
|
|
287
|
+
const total = rows.filter((r) => r.host === h).length;
|
|
288
|
+
const n = refusals.filter((r) => r.host === h).length;
|
|
289
|
+
return `\`${h}\` on ${String(n)} of its ${String(total)} (${slots})`;
|
|
290
|
+
})
|
|
291
|
+
.join('; ');
|
|
292
|
+
return (
|
|
293
|
+
`**Configured: ${String(count('refusal'))} of ${String(rows.length)} host-slot pairs ` +
|
|
294
|
+
`configure a refusal — ${where}. ` +
|
|
295
|
+
`${String(count('warning'))} are \`warning\`, ${String(count('unenforced'))} are ` +
|
|
296
|
+
`\`unenforced\`, ${String(count('halt-by-state'))} are \`halt-by-state\`. ` +
|
|
297
|
+
`${String(hostsAny)} of ${String(modelled)} modelled hosts ` +
|
|
298
|
+
`${hostsAny === 1 ? 'configures' : 'configure'} a refusal on any slot; ` +
|
|
299
|
+
`${String(hostsAll)} ${hostsAll === 1 ? 'configures' : 'configure'} one on every slot ` +
|
|
300
|
+
'it binds.**'
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The whole region body, between the markers and excluding them. */
|
|
305
|
+
export function renderRegion(lowering: HostLowering): string {
|
|
306
|
+
const { rows, slotlessHosts } = buildRows(lowering);
|
|
307
|
+
const out: string[] = [];
|
|
308
|
+
out.push(
|
|
309
|
+
`Projected from \`${LOWERING_REL}\` — **configured behavior, not observed behavior.**`,
|
|
310
|
+
);
|
|
311
|
+
out.push(
|
|
312
|
+
'A cell says what this package has written down about a host, never what the host does.',
|
|
313
|
+
);
|
|
314
|
+
out.push('');
|
|
315
|
+
out.push(summaryLine(lowering, rows));
|
|
316
|
+
out.push('');
|
|
317
|
+
out.push('| Host | Slot | Configured outcome | Backing | Answered |');
|
|
318
|
+
out.push('|---|---|---|---|---|');
|
|
319
|
+
for (const r of rows) {
|
|
320
|
+
out.push(
|
|
321
|
+
`| \`${r.host}\` | \`${r.slot}\` | \`${r.outcome}\` | ${r.backing} | ${r.answered} |`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
out.push('');
|
|
325
|
+
if (slotlessHosts.length === 0) {
|
|
326
|
+
out.push('Every modelled host carries at least one lowerable slot.');
|
|
327
|
+
} else {
|
|
328
|
+
out.push(
|
|
329
|
+
`**No row above for ${slotlessHosts.map((h) => `\`${h}\``).join(', ')}** — ` +
|
|
330
|
+
'modelled in the configuration with an empty `slots:` map, so there is no ' +
|
|
331
|
+
'host-slot pair to carry an outcome. That is an absence of bindings, not an ' +
|
|
332
|
+
'outcome of `unenforced`.',
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
return out.join('\n');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Replace the block between the markers. Idempotent by construction. */
|
|
339
|
+
export function spliceDoc(text: string, region: string): string {
|
|
340
|
+
const b = text.indexOf(BEGIN_MARKER);
|
|
341
|
+
const e = text.indexOf(END_MARKER);
|
|
342
|
+
if (b === -1 || e === -1 || e < b) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`${GATE}: ${DOC_REL} is missing the generated-region markers ` +
|
|
345
|
+
`(${BEGIN_MARKER} … ${END_MARKER}). Restore them rather than hand-writing the ` +
|
|
346
|
+
'table — the markers are what makes the region regenerable.',
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return `${text.slice(0, b + BEGIN_MARKER.length)}\n${region}\n${text.slice(e)}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** The region body a committed document carries, or null when the markers are gone. */
|
|
353
|
+
export function extractRegion(text: string): string | null {
|
|
354
|
+
const b = text.indexOf(BEGIN_MARKER);
|
|
355
|
+
const e = text.indexOf(END_MARKER);
|
|
356
|
+
if (b === -1 || e === -1 || e < b) return null;
|
|
357
|
+
return text
|
|
358
|
+
.slice(b + BEGIN_MARKER.length, e)
|
|
359
|
+
.replace(/^\n/, '')
|
|
360
|
+
.replace(/\n$/, '');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Outcome cells a committed region carries, with their row label. */
|
|
364
|
+
export function committedCells(region: string): { label: string; value: string }[] {
|
|
365
|
+
const out: { label: string; value: string }[] = [];
|
|
366
|
+
for (const line of region.split('\n')) {
|
|
367
|
+
if (!line.trim().startsWith('|')) continue;
|
|
368
|
+
const cells = splitMarkdownRow(line);
|
|
369
|
+
if (cells.length < 4) continue;
|
|
370
|
+
if (cells[0] === 'Host' || /^-+$/.test((cells[0] ?? '').trim())) continue;
|
|
371
|
+
const strip = (s: string): string => s.trim().replace(/^`|`$/g, '');
|
|
372
|
+
out.push({
|
|
373
|
+
label: `${strip(cells[0] ?? '')}/${strip(cells[1] ?? '')}`,
|
|
374
|
+
value: strip(cells[2] ?? ''),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Committed cells carrying a value the configuration cannot produce.
|
|
382
|
+
*
|
|
383
|
+
* Two ways to fail, reported apart because they need different fixes: a value
|
|
384
|
+
* outside the closed set is a typo or an invented category, and a value inside
|
|
385
|
+
* the set that nothing on this tree emits is the phantom `halt-by-state` case.
|
|
386
|
+
*/
|
|
387
|
+
export function unbackedOutcomes(region: string, backed: ReadonlySet<ConfiguredOutcome>): string[] {
|
|
388
|
+
const findings: string[] = [];
|
|
389
|
+
for (const cell of committedCells(region)) {
|
|
390
|
+
const known = (CONFIGURED_OUTCOMES as readonly string[]).includes(cell.value);
|
|
391
|
+
if (!known) {
|
|
392
|
+
findings.push(
|
|
393
|
+
`${cell.label}: \`${cell.value}\` is not in the closed vocabulary ` +
|
|
394
|
+
`(${CONFIGURED_OUTCOMES.map((o) => `\`${o}\``).join(' · ')}).`,
|
|
395
|
+
);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (!backed.has(cell.value as ConfiguredOutcome)) {
|
|
399
|
+
findings.push(
|
|
400
|
+
`${cell.label}: \`${cell.value}\` is in the vocabulary but NO line in ` +
|
|
401
|
+
`\`${LOWERING_REL}\` produces it on this tree. The value is defined so a ` +
|
|
402
|
+
'reader who meets it has a definition; it may appear in a cell only once ' +
|
|
403
|
+
'the configuration carries it.',
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return findings;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function _read(root: string, rel: string): string | null {
|
|
411
|
+
const abs = path.join(root, rel);
|
|
412
|
+
return fs.existsSync(abs) ? fs.readFileSync(abs, 'utf-8') : null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Flags this gate understands. `--root` is the only one taking a value. */
|
|
416
|
+
const KNOWN_FLAGS: ReadonlySet<string> = new Set(['--self-test', '--root', '--write', '--quiet']);
|
|
417
|
+
|
|
418
|
+
/** Where the concern→slot bindings and per-concern severities live. */
|
|
419
|
+
export const MANIFEST_REL = path.join('src', 'scripts', 'hook_manifest.yaml');
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Why a bound concern's verdict cannot leave its slot.
|
|
423
|
+
*
|
|
424
|
+
* Three states, not two, and the third is the one an earlier version of this
|
|
425
|
+
* audit got wrong by skipping it. They are ordered worst-first:
|
|
426
|
+
*
|
|
427
|
+
* - `unlowerable` — the lowering table carries NO row for this slot on this
|
|
428
|
+
* host, so the installer writes no native binding at all.
|
|
429
|
+
* `cowork` is the worked case: `slots: {}` against sixty-odd
|
|
430
|
+
* declared bindings. Skipping it printed `0/0 · 0 blocking`,
|
|
431
|
+
* byte-identical to a host that binds nothing — a false
|
|
432
|
+
* green on the host with the largest gap.
|
|
433
|
+
* - `null-block` — a row exists and its literal `block_exit` is null. The
|
|
434
|
+
* slot is wired and a refusal has nowhere to go.
|
|
435
|
+
* - `stale-proof` — the literal `block_exit` CAN deny, but the row's
|
|
436
|
+
* `verified` block has expired, so `effective` reads null.
|
|
437
|
+
* This is a statement about this package's provenance, NOT
|
|
438
|
+
* about the host: `host_lowering.yaml` says in its own
|
|
439
|
+
* header that an absent `verified` "does NOT mean the host
|
|
440
|
+
* cannot enforce". Printing "cannot deny" here would
|
|
441
|
+
* assert exactly the host fact that file disclaims, so it
|
|
442
|
+
* is reported as its own class and worded differently.
|
|
443
|
+
*/
|
|
444
|
+
export type NoDenyReason = 'unlowerable' | 'null-block' | 'stale-proof';
|
|
445
|
+
|
|
446
|
+
/** One concern bound where its verdict cannot produce a refusal. */
|
|
447
|
+
export interface NullBlockBinding {
|
|
448
|
+
readonly host: string;
|
|
449
|
+
readonly slot: string;
|
|
450
|
+
readonly concern: string;
|
|
451
|
+
/** The concern's declared `severity`, or `(undeclared)`. */
|
|
452
|
+
readonly severity: string;
|
|
453
|
+
readonly reason: NoDenyReason;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** What {@link concernSlotAudit} establishes for one host. */
|
|
457
|
+
export interface HostConcernAudit {
|
|
458
|
+
readonly host: string;
|
|
459
|
+
/** Lowerable slots whose literal `block_exit` can deny. */
|
|
460
|
+
readonly denySlots: number;
|
|
461
|
+
/** Lowerable slots in total. */
|
|
462
|
+
readonly lowerableSlots: number;
|
|
463
|
+
/** Distinct slots this host binds a concern to, whether lowerable or not. */
|
|
464
|
+
readonly boundSlots: number;
|
|
465
|
+
/**
|
|
466
|
+
* Bindings whose verdict cannot become a refusal, worst reason first and
|
|
467
|
+
* `blocking` severity ahead of the rest within a reason.
|
|
468
|
+
*
|
|
469
|
+
* An `advisory` concern here is CONSISTENT, not a defect — most concerns are
|
|
470
|
+
* advisory and belong on a reporting slot. The row that matters is a
|
|
471
|
+
* `blocking` severity: a concern declared to refuse, bound where a refusal
|
|
472
|
+
* has nowhere to go.
|
|
473
|
+
*/
|
|
474
|
+
readonly noDeny: readonly NullBlockBinding[];
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Slots that carry a dispatcher verdict. `ask` is a UI affordance, not one. */
|
|
478
|
+
const NON_VERDICT_SLOTS: ReadonlySet<string> = new Set(['ask']);
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Cross-reference the manifest's concern bindings against the lowering table's
|
|
482
|
+
* per-slot `block_exit`.
|
|
483
|
+
*
|
|
484
|
+
* road-to-a-content-scanner-on-a-slot-that-can-refuse 2.1. The gate above
|
|
485
|
+
* compares published host-SLOT rows against the configuration and is green
|
|
486
|
+
* whenever those agree — it never asks whether a CONCERN is bound to a slot
|
|
487
|
+
* that can carry its severity. That question was answered once, by an external
|
|
488
|
+
* reader comparing two files by hand, which is why it drifted back: a hand
|
|
489
|
+
* comparison leaves no number behind. This makes it a printed count.
|
|
490
|
+
*
|
|
491
|
+
* DELIBERATELY NOT A FAILURE. It prints; the exit code is unchanged. Failing
|
|
492
|
+
* would red the tree on bindings that predate this check — the same reason the
|
|
493
|
+
* continuity and estate ratchets report distance-to-target instead of gating on
|
|
494
|
+
* it. What a reader gets is a number that moves when a binding or a slot moves.
|
|
495
|
+
*
|
|
496
|
+
* NOTHING BOUND IS SILENTLY DROPPED. Every bound slot lands in exactly one of
|
|
497
|
+
* {can deny, one of the three {@link NoDenyReason} classes, non-verdict}, and
|
|
498
|
+
* `boundSlots` publishes the denominator so a reader can check the arithmetic
|
|
499
|
+
* rather than trust it. That is the correction the first version needed: it
|
|
500
|
+
* skipped any slot missing from the lowering table, which is precisely the
|
|
501
|
+
* largest gap in the tree.
|
|
502
|
+
*/
|
|
503
|
+
export function concernSlotAudit(root: string, lowering: HostLowering): HostConcernAudit[] {
|
|
504
|
+
const manifestText = _read(root, MANIFEST_REL);
|
|
505
|
+
if (manifestText === null) return [];
|
|
506
|
+
let manifest: unknown;
|
|
507
|
+
try {
|
|
508
|
+
manifest = yaml.load(manifestText);
|
|
509
|
+
} catch {
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
if (manifest === null || typeof manifest !== 'object') return [];
|
|
513
|
+
const m = manifest as Record<string, unknown>;
|
|
514
|
+
|
|
515
|
+
const severities = new Map<string, string>();
|
|
516
|
+
const concerns = m['concerns'];
|
|
517
|
+
if (concerns !== null && typeof concerns === 'object') {
|
|
518
|
+
for (const [name, spec] of Object.entries(concerns as Record<string, unknown>)) {
|
|
519
|
+
const sev =
|
|
520
|
+
spec !== null && typeof spec === 'object'
|
|
521
|
+
? (spec as Record<string, unknown>)['severity']
|
|
522
|
+
: undefined;
|
|
523
|
+
severities.set(name, typeof sev === 'string' ? sev : '(undeclared)');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const platforms = m['platforms'];
|
|
528
|
+
if (platforms === null || typeof platforms !== 'object') return [];
|
|
529
|
+
|
|
530
|
+
const out: HostConcernAudit[] = [];
|
|
531
|
+
for (const [host, blockRaw] of Object.entries(platforms as Record<string, unknown>)) {
|
|
532
|
+
const reading = readSlots(lowering, host);
|
|
533
|
+
const rows = new Map<string, SlotReading>(reading.slots.map((s: SlotReading) => [s.slot, s]));
|
|
534
|
+
const denySlots = reading.slots.filter((s: SlotReading) => s.literal !== null).length;
|
|
535
|
+
|
|
536
|
+
const noDeny: NullBlockBinding[] = [];
|
|
537
|
+
let boundSlots = 0;
|
|
538
|
+
if (blockRaw !== null && typeof blockRaw === 'object') {
|
|
539
|
+
for (const [slot, value] of Object.entries(blockRaw as Record<string, unknown>)) {
|
|
540
|
+
if (!Array.isArray(value)) continue;
|
|
541
|
+
if (NON_VERDICT_SLOTS.has(slot)) continue;
|
|
542
|
+
boundSlots += 1;
|
|
543
|
+
const row = rows.get(slot);
|
|
544
|
+
// The literal decides whether a refusal can leave the slot; the
|
|
545
|
+
// `verified` currency decides only whether this package has
|
|
546
|
+
// cited its proof. Reading `effective` here would turn an
|
|
547
|
+
// expired citation into a claim that the host cannot enforce.
|
|
548
|
+
const reason: NoDenyReason | null =
|
|
549
|
+
row === undefined
|
|
550
|
+
? 'unlowerable'
|
|
551
|
+
: row.literal === null
|
|
552
|
+
? 'null-block'
|
|
553
|
+
: row.effective === null
|
|
554
|
+
? 'stale-proof'
|
|
555
|
+
: null;
|
|
556
|
+
if (reason === null) continue;
|
|
557
|
+
for (const concern of value) {
|
|
558
|
+
if (typeof concern !== 'string') continue;
|
|
559
|
+
noDeny.push({
|
|
560
|
+
host,
|
|
561
|
+
slot,
|
|
562
|
+
concern,
|
|
563
|
+
severity: severities.get(concern) ?? '(unknown concern)',
|
|
564
|
+
reason,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const reasonRank = (r: NoDenyReason): number =>
|
|
570
|
+
r === 'unlowerable' ? 0 : r === 'null-block' ? 1 : 2;
|
|
571
|
+
const sevRank = (s: string): number => (s === 'blocking' ? 0 : s === 'advisory' ? 2 : 1);
|
|
572
|
+
noDeny.sort(
|
|
573
|
+
(a, b) =>
|
|
574
|
+
reasonRank(a.reason) - reasonRank(b.reason) ||
|
|
575
|
+
sevRank(a.severity) - sevRank(b.severity) ||
|
|
576
|
+
a.slot.localeCompare(b.slot) ||
|
|
577
|
+
a.concern.localeCompare(b.concern),
|
|
578
|
+
);
|
|
579
|
+
out.push({
|
|
580
|
+
host,
|
|
581
|
+
denySlots,
|
|
582
|
+
lowerableSlots: reading.slots.length,
|
|
583
|
+
boundSlots,
|
|
584
|
+
noDeny,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return out;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** How each {@link NoDenyReason} is worded. `stale-proof` never says "cannot". */
|
|
591
|
+
const REASON_TEXT: Readonly<Record<NoDenyReason, string>> = {
|
|
592
|
+
unlowerable: 'no lowering row for this slot — nothing is bound natively at all',
|
|
593
|
+
'null-block': 'block_exit is null — a refusal has nowhere to go',
|
|
594
|
+
'stale-proof': 'block_exit can deny, but this row’s `verified` proof has expired',
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
/** Render the audit as the lines the reporter prints. */
|
|
598
|
+
export function renderConcernAudit(audit: readonly HostConcernAudit[]): string[] {
|
|
599
|
+
const lines: string[] = [];
|
|
600
|
+
lines.push('');
|
|
601
|
+
lines.push(`${GATE} · concerns bound where a verdict cannot become a refusal`);
|
|
602
|
+
lines.push(
|
|
603
|
+
' A blocking severity here is a concern declared to refuse, bound where a refusal',
|
|
604
|
+
);
|
|
605
|
+
lines.push(' cannot happen. An advisory one is consistent and is counted, not flagged.');
|
|
606
|
+
lines.push(' `stale-proof` is a statement about this package’s citation, never about the host.');
|
|
607
|
+
for (const h of audit) {
|
|
608
|
+
const blocking = h.noDeny.filter((b) => b.severity === 'blocking');
|
|
609
|
+
const others = h.noDeny.length - blocking.length;
|
|
610
|
+
lines.push(
|
|
611
|
+
` ${h.host.padEnd(10)} ${String(h.denySlots)}/${String(h.lowerableSlots)} lowerable slot(s) can deny · ` +
|
|
612
|
+
`${String(h.boundSlots)} verdict-bearing slot(s) bound · ` +
|
|
613
|
+
`${String(blocking.length)} blocking + ${String(others)} non-blocking binding(s) cannot refuse`,
|
|
614
|
+
);
|
|
615
|
+
// One line per (reason, slot) rather than per concern: a host with an
|
|
616
|
+
// empty `slots:` map has sixty-odd bindings and the reason is the fact,
|
|
617
|
+
// not the roster.
|
|
618
|
+
const seen = new Set<string>();
|
|
619
|
+
for (const b of h.noDeny) {
|
|
620
|
+
if (b.severity !== 'blocking') continue;
|
|
621
|
+
const key = `${b.reason}/${b.slot}`;
|
|
622
|
+
if (!seen.has(key)) {
|
|
623
|
+
seen.add(key);
|
|
624
|
+
lines.push(` ⚠️ ${b.slot} — ${REASON_TEXT[b.reason]}`);
|
|
625
|
+
}
|
|
626
|
+
lines.push(` ${b.concern} (${b.severity})`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return lines;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function main(argv?: readonly string[]): number {
|
|
633
|
+
const args = argv ?? process.argv.slice(2);
|
|
634
|
+
// An unrecognised flag is refused rather than ignored. Both ratification
|
|
635
|
+
// seats named the silent-ignore form a fail-open interface defect: this
|
|
636
|
+
// gate is registered in a required CI job, and a mistyped flag that still
|
|
637
|
+
// exits 0 hands back a green nobody earned. Refusing costs a typo its own
|
|
638
|
+
// error message instead of a false pass.
|
|
639
|
+
const unknown = args.filter(
|
|
640
|
+
(a, i) => a.startsWith('-') && !KNOWN_FLAGS.has(a) && args[i - 1] !== '--root',
|
|
641
|
+
);
|
|
642
|
+
if (unknown.length > 0) {
|
|
643
|
+
process.stderr.write(
|
|
644
|
+
`${GATE}: unrecognised flag(s): ${unknown.join(', ')}\n` +
|
|
645
|
+
` known: ${[...KNOWN_FLAGS].join(', ')}\n`,
|
|
646
|
+
);
|
|
647
|
+
return 2;
|
|
648
|
+
}
|
|
649
|
+
if (args.includes('--self-test')) return selfTest();
|
|
650
|
+
const rootIdx = args.indexOf('--root');
|
|
651
|
+
const root = rootIdx === -1 ? ROOT : (args[rootIdx + 1] ?? ROOT);
|
|
652
|
+
const write = args.includes('--write');
|
|
653
|
+
|
|
654
|
+
const loweringText = _read(root, LOWERING_REL);
|
|
655
|
+
const docText = _read(root, DOC_REL);
|
|
656
|
+
if (loweringText === null || docText === null) {
|
|
657
|
+
process.stderr.write(
|
|
658
|
+
`❌ ${GATE}: ${loweringText === null ? LOWERING_REL : DOC_REL} not found under ${root}\n`,
|
|
659
|
+
);
|
|
660
|
+
return 2;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
let lowering: HostLowering;
|
|
664
|
+
try {
|
|
665
|
+
lowering = parseHostLowering(loweringText);
|
|
666
|
+
} catch (exc) {
|
|
667
|
+
process.stderr.write(`❌ ${GATE}: ${LOWERING_REL} did not parse — ${String(exc)}\n`);
|
|
668
|
+
return 2;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const built = buildRows(lowering);
|
|
672
|
+
const ledger = new GateLedger(GATE);
|
|
673
|
+
ledger.plan(built.rows.map((r) => `${r.host}/${r.slot}`));
|
|
674
|
+
const region = renderRegion(lowering);
|
|
675
|
+
|
|
676
|
+
if (write) {
|
|
677
|
+
let next: string;
|
|
678
|
+
try {
|
|
679
|
+
next = spliceDoc(docText, region);
|
|
680
|
+
} catch (exc) {
|
|
681
|
+
process.stderr.write(`❌ ${String(exc)}\n`);
|
|
682
|
+
return 2;
|
|
683
|
+
}
|
|
684
|
+
// The backing check runs BEFORE the write, so a generator bug cannot
|
|
685
|
+
// put a phantom value into the document and then be caught by the
|
|
686
|
+
// check that same generator's output feeds.
|
|
687
|
+
const phantom = unbackedOutcomes(region, backedOutcomes(lowering));
|
|
688
|
+
if (phantom.length > 0) {
|
|
689
|
+
for (const f of phantom) process.stderr.write(`❌ ${GATE}: ${f}\n`);
|
|
690
|
+
process.stderr.write(
|
|
691
|
+
`${GATE}: refusing to write a region the configuration does not back.\n`,
|
|
692
|
+
);
|
|
693
|
+
return 1;
|
|
694
|
+
}
|
|
695
|
+
for (const r of built.rows) ledger.complete(`${r.host}/${r.slot}`);
|
|
696
|
+
if (next !== docText) fs.writeFileSync(path.join(root, DOC_REL), next, 'utf-8');
|
|
697
|
+
reportScanned({
|
|
698
|
+
gate: GATE,
|
|
699
|
+
scanned: built.rows.length,
|
|
700
|
+
units: 'host-slot pair(s)',
|
|
701
|
+
roots: [LOWERING_REL],
|
|
702
|
+
});
|
|
703
|
+
ledger.report();
|
|
704
|
+
process.stdout.write(
|
|
705
|
+
`✅ ${GATE}: wrote ${String(built.rows.length)} host-slot row(s) into ${DOC_REL}` +
|
|
706
|
+
`${next === docText ? ' (already current)' : ''}.\n`,
|
|
707
|
+
);
|
|
708
|
+
return 0;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const committed = extractRegion(docText);
|
|
712
|
+
if (committed === null) {
|
|
713
|
+
process.stderr.write(
|
|
714
|
+
`❌ ${GATE}: ${DOC_REL} has no generated region. The markers ` +
|
|
715
|
+
`${BEGIN_MARKER} … ${END_MARKER} are what make the table regenerable; restore ` +
|
|
716
|
+
`them and run \`${REGEN_COMMAND}\`.\n`,
|
|
717
|
+
);
|
|
718
|
+
return 2;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const findings: string[] = [];
|
|
722
|
+
findings.push(...unbackedOutcomes(committed, backedOutcomes(lowering)));
|
|
723
|
+
findings.push(...built.contradictions);
|
|
724
|
+
if (committed !== region) {
|
|
725
|
+
findings.push(
|
|
726
|
+
`${DOC_REL} § the generated region differs from what \`${LOWERING_REL}\` produces.`,
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// A pair is `fail`ed when the committed cell for it is absent or carries a
|
|
731
|
+
// different outcome, and `complete`d otherwise. The denominator is the
|
|
732
|
+
// configuration's pairs, not the document's — a pair the document dropped
|
|
733
|
+
// must count as unchecked work rather than vanish from the accounting.
|
|
734
|
+
const byLabel = new Map(committedCells(committed).map((c) => [c.label, c.value]));
|
|
735
|
+
for (const r of built.rows) {
|
|
736
|
+
const label = `${r.host}/${r.slot}`;
|
|
737
|
+
const got = byLabel.get(label);
|
|
738
|
+
if (got === r.outcome) ledger.complete(label);
|
|
739
|
+
else {
|
|
740
|
+
ledger.fail(
|
|
741
|
+
label,
|
|
742
|
+
`published \`${got ?? '(absent)'}\`, configuration says \`${r.outcome}\``,
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
reportScanned({
|
|
748
|
+
gate: GATE,
|
|
749
|
+
scanned: built.rows.length,
|
|
750
|
+
units: 'host-slot pair(s)',
|
|
751
|
+
roots: [LOWERING_REL],
|
|
752
|
+
});
|
|
753
|
+
ledger.report();
|
|
754
|
+
|
|
755
|
+
// The concern-vs-slot audit prints on every read-only run, before the
|
|
756
|
+
// verdict, so it is visible whether the row comparison passed or failed.
|
|
757
|
+
// It never changes the exit code — see `concernSlotAudit`.
|
|
758
|
+
for (const line of renderConcernAudit(concernSlotAudit(root, lowering))) {
|
|
759
|
+
process.stdout.write(`${line}\n`);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (findings.length > 0) {
|
|
763
|
+
for (const f of findings) process.stdout.write(`❌ ${GATE}: ${f}\n`);
|
|
764
|
+
process.stdout.write(
|
|
765
|
+
`\n${String(findings.length)} finding(s). Regenerate with:\n` +
|
|
766
|
+
` ${REGEN_COMMAND}\n` +
|
|
767
|
+
`Corrections belong in \`${LOWERING_REL}\` or in the generator, never in the ` +
|
|
768
|
+
'table — a cell edited by hand is overwritten by the next regeneration, and ' +
|
|
769
|
+
'reported by this gate in between.\n',
|
|
770
|
+
);
|
|
771
|
+
return 1;
|
|
772
|
+
}
|
|
773
|
+
process.stdout.write(
|
|
774
|
+
`✅ ${GATE}: ${String(built.rows.length)} host-slot row(s) in ${DOC_REL} match ` +
|
|
775
|
+
`${LOWERING_REL}.\n`,
|
|
776
|
+
);
|
|
777
|
+
return 0;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* One rejecting case per way the region and the configuration can part company.
|
|
782
|
+
*
|
|
783
|
+
* The `halt-by-state` case is the one that matters most: a cell claiming a
|
|
784
|
+
* value the configuration does not carry must red the gate. Its fixture is
|
|
785
|
+
* built by hand rather than generated, because a fixture the generator produced
|
|
786
|
+
* could not demonstrate a value the generator cannot emit.
|
|
787
|
+
*/
|
|
788
|
+
function selfTest(): number {
|
|
789
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'enforcement-matrix-'));
|
|
790
|
+
const lowering = _read(ROOT, LOWERING_REL) ?? '';
|
|
791
|
+
const plant = (name: string, mutate: (region: string) => string): string => {
|
|
792
|
+
const dir = path.join(tmp, name);
|
|
793
|
+
fs.mkdirSync(path.join(dir, path.dirname(LOWERING_REL)), { recursive: true });
|
|
794
|
+
fs.mkdirSync(path.join(dir, path.dirname(DOC_REL)), { recursive: true });
|
|
795
|
+
fs.writeFileSync(path.join(dir, LOWERING_REL), lowering, 'utf-8');
|
|
796
|
+
const region = renderRegion(parseHostLowering(lowering));
|
|
797
|
+
fs.writeFileSync(
|
|
798
|
+
path.join(dir, DOC_REL),
|
|
799
|
+
`# fixture\n\n${BEGIN_MARKER}\n${mutate(region)}\n${END_MARKER}\n`,
|
|
800
|
+
'utf-8',
|
|
801
|
+
);
|
|
802
|
+
return dir;
|
|
803
|
+
};
|
|
804
|
+
const run = (root: string): number =>
|
|
805
|
+
runGateCli(ROOT, `src/scripts/${GATE}.ts`, ['--root', root], root);
|
|
806
|
+
|
|
807
|
+
try {
|
|
808
|
+
return runSelfTest({
|
|
809
|
+
gate: GATE,
|
|
810
|
+
minCases: 5,
|
|
811
|
+
minRejectCases: 4,
|
|
812
|
+
cases: [
|
|
813
|
+
{
|
|
814
|
+
name: 'a cell claiming `halt-by-state`, which nothing emits, is rejected',
|
|
815
|
+
expect: 'reject',
|
|
816
|
+
run: () =>
|
|
817
|
+
run(plant('phantom', (r) => r.replace('| `warning` |', '| `halt-by-state` |'))),
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
name: 'a cell outside the closed vocabulary is rejected',
|
|
821
|
+
expect: 'reject',
|
|
822
|
+
run: () => run(plant('vocab', (r) => r.replace('| `refusal` |', '| `blocked` |'))),
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
name: 'a hand-flipped outcome is rejected',
|
|
826
|
+
expect: 'reject',
|
|
827
|
+
run: () => run(plant('flip', (r) => r.replace('| `refusal` |', '| `unenforced` |'))),
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
name: 'a deleted row is rejected',
|
|
831
|
+
expect: 'reject',
|
|
832
|
+
run: () =>
|
|
833
|
+
run(
|
|
834
|
+
plant('deleted', (r) =>
|
|
835
|
+
r
|
|
836
|
+
.split('\n')
|
|
837
|
+
.filter((l) => !l.includes('| `refusal` |'))
|
|
838
|
+
.join('\n'),
|
|
839
|
+
),
|
|
840
|
+
),
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
name: 'the regenerated region passes',
|
|
844
|
+
expect: 'accept',
|
|
845
|
+
run: () => run(plant('good', (r) => r)),
|
|
846
|
+
},
|
|
847
|
+
],
|
|
848
|
+
});
|
|
849
|
+
} finally {
|
|
850
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function _isCliEntry(): boolean {
|
|
855
|
+
if (process.argv[1] === undefined) return false;
|
|
856
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
857
|
+
try {
|
|
858
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
859
|
+
} catch {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (_isCliEntry()) {
|
|
865
|
+
process.exit(main());
|
|
866
|
+
}
|