@event4u/agent-config 16.0.0 → 16.1.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 +297 -137
- 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/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 +15 -15
- 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/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 +50 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- 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/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 +188 -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/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- 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-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
- 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-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
- 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/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +90 -56
- 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 +4 -4
- package/dist/cli-delegate/cmd_mcp_available.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 +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- 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 +85 -38
- 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 +1832 -1007
- 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 +2007 -1481
- 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 +398 -39
- 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 +17 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +26 -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 +1 -1
- 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/config/agent-settings.template.yml +139 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +18 -5
- package/src/config/continuity-surface.json +21 -0
- package/src/config/gate-coverage.yml +146 -13
- package/src/config/gate-violation-baselines.json +6 -6
- package/src/config/hook-token-budget.json +2 -0
- package/src/config/loop-surfaces.yaml +215 -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 +18 -4
- 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 +4 -3
- 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 +90 -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/cascade_base.ts +94 -0
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- 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 +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -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/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 +402 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/release_highlights.ts +199 -10
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -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/ask_block_census.ts +285 -15
- package/src/scripts/check_enforcement_matrix.ts +624 -0
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_preamble_payload_budget.ts +75 -2
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/closure_scan.ts +333 -0
- package/src/scripts/compile_hook_manifest.ts +43 -3
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +88 -9
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- package/src/scripts/hooks/evidence_independence.ts +96 -0
- package/src/scripts/hooks/memory_recall_hook.ts +540 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
- package/src/scripts/hooks/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks_doctor.ts +85 -0
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +106 -206
- package/src/scripts/janitor.ts +7 -0
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_hook_manifest.ts +11 -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 +61 -0
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +14 -98
- package/src/scripts/release_drill.ts +267 -1
- package/src/scripts/release_publication.ts +51 -0
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -0
- package/src/server/schemas/settings.ts +31 -7
- package/src/shared/missionExecution.ts +126 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
|
@@ -0,0 +1,624 @@
|
|
|
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
|
+
import process from 'node:process';
|
|
59
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
60
|
+
|
|
61
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
62
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
63
|
+
import { splitMarkdownRow } from './_lib/md_table.js';
|
|
64
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
65
|
+
import { DEFAULT_SURFACE, type HostLowering, parseHostLowering } from './hooks/host_lowering.js';
|
|
66
|
+
import { type SlotReading, deriveDeny, readSlots } from './report_enforcement_drift.js';
|
|
67
|
+
|
|
68
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
69
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
70
|
+
|
|
71
|
+
export const GATE = 'check_enforcement_matrix';
|
|
72
|
+
export const DOC_REL = path.join('docs', 'enforcement-by-host.md');
|
|
73
|
+
export const LOWERING_REL = path.join('src', 'scripts', 'hooks', 'host_lowering.yaml');
|
|
74
|
+
|
|
75
|
+
export const BEGIN_MARKER = '<!-- BEGIN GENERATED: enforcement-configured-by-slot -->';
|
|
76
|
+
export const END_MARKER = '<!-- END GENERATED: enforcement-configured-by-slot -->';
|
|
77
|
+
|
|
78
|
+
/** The command a drift failure tells the reader to run. Printed, never guessed at. */
|
|
79
|
+
export const REGEN_COMMAND = `./scripts-run src/scripts/${GATE} --write`;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The closed set a configured-outcome cell may carry.
|
|
83
|
+
*
|
|
84
|
+
* Order is the ladder's own, strongest first, and the document's hand-written
|
|
85
|
+
* taxonomy lists them in the same order so the two read together.
|
|
86
|
+
*/
|
|
87
|
+
export const CONFIGURED_OUTCOMES = ['refusal', 'halt-by-state', 'warning', 'unenforced'] as const;
|
|
88
|
+
|
|
89
|
+
export type ConfiguredOutcome = (typeof CONFIGURED_OUTCOMES)[number];
|
|
90
|
+
|
|
91
|
+
export interface MatrixRow {
|
|
92
|
+
host: string;
|
|
93
|
+
slot: string;
|
|
94
|
+
outcome: ConfiguredOutcome;
|
|
95
|
+
/** The configuration values the outcome was read off, quoted for the table. */
|
|
96
|
+
backing: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface BuildResult {
|
|
100
|
+
rows: MatrixRow[];
|
|
101
|
+
/** Hosts the configuration models with no lowerable slot at all. */
|
|
102
|
+
slotlessHosts: string[];
|
|
103
|
+
/**
|
|
104
|
+
* Rows whose configuration cannot be projected into the vocabulary — a
|
|
105
|
+
* `fail_policy` this mapping does not know, or a blocking exit under a
|
|
106
|
+
* policy that discards it. Reported, never resolved by picking a side.
|
|
107
|
+
*/
|
|
108
|
+
contradictions: string[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The configured outcome for one slot reading.
|
|
113
|
+
*
|
|
114
|
+
* REFUSAL IS READ OFF `effective`, NOT `literal`, which is the same rule
|
|
115
|
+
* `report_enforcement_drift` applies and the reason the two cannot disagree: a
|
|
116
|
+
* row whose `verified` block has expired carries no blocking binding whatever
|
|
117
|
+
* its slot literals say.
|
|
118
|
+
*
|
|
119
|
+
* `fail_policy` only splits the non-refusal case. `propagate` means the
|
|
120
|
+
* dispatcher's exit code reaches the host, so a concern bound there can report
|
|
121
|
+
* and the run continues — `warning`. `discard` means the trampoline exits 0
|
|
122
|
+
* regardless, so the verdict reaches nothing — `unenforced`.
|
|
123
|
+
*
|
|
124
|
+
* Returns `null` for a `fail_policy` this mapping does not know. A gate that
|
|
125
|
+
* guessed at an unknown input would publish the guess in a table, which is the
|
|
126
|
+
* class of defect this whole file exists to remove.
|
|
127
|
+
*/
|
|
128
|
+
export function outcomeFor(reading: SlotReading, failPolicy: string): ConfiguredOutcome | null {
|
|
129
|
+
if (reading.effective !== null) return 'refusal';
|
|
130
|
+
if (failPolicy === 'propagate') return 'warning';
|
|
131
|
+
if (failPolicy === 'discard') return 'unenforced';
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The configuration values a cell was read off, as the table prints them. */
|
|
136
|
+
export function backingFor(reading: SlotReading, failPolicy: string): string {
|
|
137
|
+
const parts: string[] = [];
|
|
138
|
+
if (reading.effective !== null) {
|
|
139
|
+
parts.push(`\`block_exit: ${String(reading.effective)}\``);
|
|
140
|
+
} else if (reading.literal !== null) {
|
|
141
|
+
// The expiry case. Printing the literal beside the effective null is
|
|
142
|
+
// what makes a silently disarmed host visible as a difference rather
|
|
143
|
+
// than as a value that quietly turned into `null`.
|
|
144
|
+
parts.push(`\`block_exit: ${String(reading.literal)}\` disarmed by an expired \`verified\``);
|
|
145
|
+
} else {
|
|
146
|
+
parts.push('`block_exit: null`');
|
|
147
|
+
}
|
|
148
|
+
parts.push(`\`fail_policy: ${failPolicy}\``);
|
|
149
|
+
return parts.join(' · ');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Every (host, slot) pair the configuration models, in the file's own order.
|
|
154
|
+
*
|
|
155
|
+
* NOT SORTED, DELIBERATELY. `host_lowering.yaml` states that its `slots:` order
|
|
156
|
+
* is part of the generated bridge output and must not be sorted; keeping the
|
|
157
|
+
* same order here means a diff of this table corresponds line for line to a
|
|
158
|
+
* diff of the YAML, which is the review property the table is for.
|
|
159
|
+
*/
|
|
160
|
+
export function buildRows(lowering: HostLowering): BuildResult {
|
|
161
|
+
const rows: MatrixRow[] = [];
|
|
162
|
+
const slotlessHosts: string[] = [];
|
|
163
|
+
const contradictions: string[] = [];
|
|
164
|
+
for (const [host, surfaces] of lowering) {
|
|
165
|
+
const surface = surfaces.get(DEFAULT_SURFACE);
|
|
166
|
+
if (surface === undefined) continue;
|
|
167
|
+
const { slots } = readSlots(lowering, host);
|
|
168
|
+
if (slots.length === 0) {
|
|
169
|
+
slotlessHosts.push(host);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
for (const reading of slots) {
|
|
173
|
+
const outcome = outcomeFor(reading, surface.fail_policy);
|
|
174
|
+
if (outcome === null) {
|
|
175
|
+
contradictions.push(
|
|
176
|
+
`${host}/${reading.slot}: \`fail_policy: ${surface.fail_policy}\` has no ` +
|
|
177
|
+
'outcome in the closed vocabulary. Extend the mapping deliberately, in ' +
|
|
178
|
+
`\`src/scripts/${GATE}.ts\`, rather than letting a cell be guessed.`,
|
|
179
|
+
);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (reading.effective !== null && surface.fail_policy === 'discard') {
|
|
183
|
+
contradictions.push(
|
|
184
|
+
`${host}/${reading.slot}: a blocking \`block_exit\` under ` +
|
|
185
|
+
'`fail_policy: discard`. The configuration says both that the host ' +
|
|
186
|
+
'honours a refusal here and that the trampoline drops it. Fix the YAML; ' +
|
|
187
|
+
'this gate will not pick one.',
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
rows.push({
|
|
191
|
+
host,
|
|
192
|
+
slot: reading.slot,
|
|
193
|
+
outcome,
|
|
194
|
+
backing: backingFor(reading, surface.fail_policy),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { rows, slotlessHosts, contradictions };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The outcome values the configuration can express on this tree.
|
|
203
|
+
*
|
|
204
|
+
* DERIVED FROM THE RAW CONFIGURATION FIELDS, NOT FROM {@link outcomeFor}'s
|
|
205
|
+
* output, and the distinction is the whole value of this function. The first
|
|
206
|
+
* version of it read the generated rows and collected the outcomes it found —
|
|
207
|
+
* which made a broken mapping self-backing: sabotaging `outcomeFor` to return
|
|
208
|
+
* `halt-by-state` for every propagating slot produced a region full of a value
|
|
209
|
+
* nothing emits, and the check passed, because the check's idea of "backed"
|
|
210
|
+
* was the same broken output. Measured, on this gate, before it shipped.
|
|
211
|
+
*
|
|
212
|
+
* So this is a deliberate SECOND expression of what the configuration carries,
|
|
213
|
+
* written against the fields themselves. `refusal` is backed when some slot
|
|
214
|
+
* carries an unexpired blocking exit; `warning` and `unenforced` are backed by
|
|
215
|
+
* the presence of the two `fail_policy` values. `halt-by-state` has no field to
|
|
216
|
+
* be backed by, on purpose — no schema element expresses it — so it can never
|
|
217
|
+
* enter this set while that stays true.
|
|
218
|
+
*
|
|
219
|
+
* The residual, stated rather than implied: a bug in BOTH this function and
|
|
220
|
+
* the mapping would agree, and this cannot catch a value that is reachable but
|
|
221
|
+
* wrong for its row. The drift comparison covers the second; nothing covers
|
|
222
|
+
* the first except that the two are written against different inputs.
|
|
223
|
+
*/
|
|
224
|
+
export function backedOutcomes(lowering: HostLowering): Set<ConfiguredOutcome> {
|
|
225
|
+
const backed = new Set<ConfiguredOutcome>();
|
|
226
|
+
for (const [host, surfaces] of lowering) {
|
|
227
|
+
const surface = surfaces.get(DEFAULT_SURFACE);
|
|
228
|
+
if (surface === undefined) continue;
|
|
229
|
+
const { slots } = readSlots(lowering, host);
|
|
230
|
+
if (slots.length === 0) continue;
|
|
231
|
+
if (slots.some((s) => s.effective !== null)) backed.add('refusal');
|
|
232
|
+
if (surface.fail_policy === 'propagate') backed.add('warning');
|
|
233
|
+
if (surface.fail_policy === 'discard') backed.add('unenforced');
|
|
234
|
+
}
|
|
235
|
+
return backed;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The derived one-line summary.
|
|
240
|
+
*
|
|
241
|
+
* Generated from the same rows as the table so the short answer a reader takes
|
|
242
|
+
* away is derived rather than remembered. `deriveDeny` is the host-level fold
|
|
243
|
+
* `report_enforcement_drift` already owns, reused here rather than recounted.
|
|
244
|
+
*/
|
|
245
|
+
export function summaryLine(lowering: HostLowering, rows: readonly MatrixRow[]): string {
|
|
246
|
+
const count = (o: ConfiguredOutcome): number => rows.filter((r) => r.outcome === o).length;
|
|
247
|
+
const refusals = rows.filter((r) => r.outcome === 'refusal');
|
|
248
|
+
const refusalHosts = [...new Set(refusals.map((r) => r.host))];
|
|
249
|
+
let hostsAll = 0;
|
|
250
|
+
let hostsAny = 0;
|
|
251
|
+
let modelled = 0;
|
|
252
|
+
for (const [host, surfaces] of lowering) {
|
|
253
|
+
if (surfaces.get(DEFAULT_SURFACE) === undefined) continue;
|
|
254
|
+
modelled += 1;
|
|
255
|
+
const derived = deriveDeny(readSlots(lowering, host).slots, true);
|
|
256
|
+
if (derived === 'all') hostsAll += 1;
|
|
257
|
+
if (derived === 'all' || derived === 'partial') hostsAny += 1;
|
|
258
|
+
}
|
|
259
|
+
const where =
|
|
260
|
+
refusalHosts.length === 0
|
|
261
|
+
? 'nowhere'
|
|
262
|
+
: refusalHosts
|
|
263
|
+
.map((h) => {
|
|
264
|
+
const slots = refusals
|
|
265
|
+
.filter((r) => r.host === h)
|
|
266
|
+
.map((r) => `\`${r.slot}\``)
|
|
267
|
+
.join(', ');
|
|
268
|
+
const total = rows.filter((r) => r.host === h).length;
|
|
269
|
+
const n = refusals.filter((r) => r.host === h).length;
|
|
270
|
+
return `\`${h}\` on ${String(n)} of its ${String(total)} (${slots})`;
|
|
271
|
+
})
|
|
272
|
+
.join('; ');
|
|
273
|
+
return (
|
|
274
|
+
`**Configured: ${String(count('refusal'))} of ${String(rows.length)} host-slot pairs ` +
|
|
275
|
+
`configure a refusal — ${where}. ` +
|
|
276
|
+
`${String(count('warning'))} are \`warning\`, ${String(count('unenforced'))} are ` +
|
|
277
|
+
`\`unenforced\`, ${String(count('halt-by-state'))} are \`halt-by-state\`. ` +
|
|
278
|
+
`${String(hostsAny)} of ${String(modelled)} modelled hosts ` +
|
|
279
|
+
`${hostsAny === 1 ? 'configures' : 'configure'} a refusal on any slot; ` +
|
|
280
|
+
`${String(hostsAll)} ${hostsAll === 1 ? 'configures' : 'configure'} one on every slot ` +
|
|
281
|
+
'it binds.**'
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The whole region body, between the markers and excluding them. */
|
|
286
|
+
export function renderRegion(lowering: HostLowering): string {
|
|
287
|
+
const { rows, slotlessHosts } = buildRows(lowering);
|
|
288
|
+
const out: string[] = [];
|
|
289
|
+
out.push(
|
|
290
|
+
`Projected from \`${LOWERING_REL}\` — **configured behavior, not observed behavior.**`,
|
|
291
|
+
);
|
|
292
|
+
out.push(
|
|
293
|
+
'A cell says what this package has written down about a host, never what the host does.',
|
|
294
|
+
);
|
|
295
|
+
out.push('');
|
|
296
|
+
out.push(summaryLine(lowering, rows));
|
|
297
|
+
out.push('');
|
|
298
|
+
out.push('| Host | Slot | Configured outcome | Backing |');
|
|
299
|
+
out.push('|---|---|---|---|');
|
|
300
|
+
for (const r of rows) {
|
|
301
|
+
out.push(`| \`${r.host}\` | \`${r.slot}\` | \`${r.outcome}\` | ${r.backing} |`);
|
|
302
|
+
}
|
|
303
|
+
out.push('');
|
|
304
|
+
if (slotlessHosts.length === 0) {
|
|
305
|
+
out.push('Every modelled host carries at least one lowerable slot.');
|
|
306
|
+
} else {
|
|
307
|
+
out.push(
|
|
308
|
+
`**No row above for ${slotlessHosts.map((h) => `\`${h}\``).join(', ')}** — ` +
|
|
309
|
+
'modelled in the configuration with an empty `slots:` map, so there is no ' +
|
|
310
|
+
'host-slot pair to carry an outcome. That is an absence of bindings, not an ' +
|
|
311
|
+
'outcome of `unenforced`.',
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return out.join('\n');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Replace the block between the markers. Idempotent by construction. */
|
|
318
|
+
export function spliceDoc(text: string, region: string): string {
|
|
319
|
+
const b = text.indexOf(BEGIN_MARKER);
|
|
320
|
+
const e = text.indexOf(END_MARKER);
|
|
321
|
+
if (b === -1 || e === -1 || e < b) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`${GATE}: ${DOC_REL} is missing the generated-region markers ` +
|
|
324
|
+
`(${BEGIN_MARKER} … ${END_MARKER}). Restore them rather than hand-writing the ` +
|
|
325
|
+
'table — the markers are what makes the region regenerable.',
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
return `${text.slice(0, b + BEGIN_MARKER.length)}\n${region}\n${text.slice(e)}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** The region body a committed document carries, or null when the markers are gone. */
|
|
332
|
+
export function extractRegion(text: string): string | null {
|
|
333
|
+
const b = text.indexOf(BEGIN_MARKER);
|
|
334
|
+
const e = text.indexOf(END_MARKER);
|
|
335
|
+
if (b === -1 || e === -1 || e < b) return null;
|
|
336
|
+
return text
|
|
337
|
+
.slice(b + BEGIN_MARKER.length, e)
|
|
338
|
+
.replace(/^\n/, '')
|
|
339
|
+
.replace(/\n$/, '');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Outcome cells a committed region carries, with their row label. */
|
|
343
|
+
export function committedCells(region: string): { label: string; value: string }[] {
|
|
344
|
+
const out: { label: string; value: string }[] = [];
|
|
345
|
+
for (const line of region.split('\n')) {
|
|
346
|
+
if (!line.trim().startsWith('|')) continue;
|
|
347
|
+
const cells = splitMarkdownRow(line);
|
|
348
|
+
if (cells.length < 4) continue;
|
|
349
|
+
if (cells[0] === 'Host' || /^-+$/.test((cells[0] ?? '').trim())) continue;
|
|
350
|
+
const strip = (s: string): string => s.trim().replace(/^`|`$/g, '');
|
|
351
|
+
out.push({
|
|
352
|
+
label: `${strip(cells[0] ?? '')}/${strip(cells[1] ?? '')}`,
|
|
353
|
+
value: strip(cells[2] ?? ''),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Committed cells carrying a value the configuration cannot produce.
|
|
361
|
+
*
|
|
362
|
+
* Two ways to fail, reported apart because they need different fixes: a value
|
|
363
|
+
* outside the closed set is a typo or an invented category, and a value inside
|
|
364
|
+
* the set that nothing on this tree emits is the phantom `halt-by-state` case.
|
|
365
|
+
*/
|
|
366
|
+
export function unbackedOutcomes(region: string, backed: ReadonlySet<ConfiguredOutcome>): string[] {
|
|
367
|
+
const findings: string[] = [];
|
|
368
|
+
for (const cell of committedCells(region)) {
|
|
369
|
+
const known = (CONFIGURED_OUTCOMES as readonly string[]).includes(cell.value);
|
|
370
|
+
if (!known) {
|
|
371
|
+
findings.push(
|
|
372
|
+
`${cell.label}: \`${cell.value}\` is not in the closed vocabulary ` +
|
|
373
|
+
`(${CONFIGURED_OUTCOMES.map((o) => `\`${o}\``).join(' · ')}).`,
|
|
374
|
+
);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (!backed.has(cell.value as ConfiguredOutcome)) {
|
|
378
|
+
findings.push(
|
|
379
|
+
`${cell.label}: \`${cell.value}\` is in the vocabulary but NO line in ` +
|
|
380
|
+
`\`${LOWERING_REL}\` produces it on this tree. The value is defined so a ` +
|
|
381
|
+
'reader who meets it has a definition; it may appear in a cell only once ' +
|
|
382
|
+
'the configuration carries it.',
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return findings;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function _read(root: string, rel: string): string | null {
|
|
390
|
+
const abs = path.join(root, rel);
|
|
391
|
+
return fs.existsSync(abs) ? fs.readFileSync(abs, 'utf-8') : null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Flags this gate understands. `--root` is the only one taking a value. */
|
|
395
|
+
const KNOWN_FLAGS: ReadonlySet<string> = new Set(['--self-test', '--root', '--write', '--quiet']);
|
|
396
|
+
|
|
397
|
+
export function main(argv?: readonly string[]): number {
|
|
398
|
+
const args = argv ?? process.argv.slice(2);
|
|
399
|
+
// An unrecognised flag is refused rather than ignored. Both ratification
|
|
400
|
+
// seats named the silent-ignore form a fail-open interface defect: this
|
|
401
|
+
// gate is registered in a required CI job, and a mistyped flag that still
|
|
402
|
+
// exits 0 hands back a green nobody earned. Refusing costs a typo its own
|
|
403
|
+
// error message instead of a false pass.
|
|
404
|
+
const unknown = args.filter(
|
|
405
|
+
(a, i) => a.startsWith('-') && !KNOWN_FLAGS.has(a) && args[i - 1] !== '--root',
|
|
406
|
+
);
|
|
407
|
+
if (unknown.length > 0) {
|
|
408
|
+
process.stderr.write(
|
|
409
|
+
`${GATE}: unrecognised flag(s): ${unknown.join(', ')}\n` +
|
|
410
|
+
` known: ${[...KNOWN_FLAGS].join(', ')}\n`,
|
|
411
|
+
);
|
|
412
|
+
return 2;
|
|
413
|
+
}
|
|
414
|
+
if (args.includes('--self-test')) return selfTest();
|
|
415
|
+
const rootIdx = args.indexOf('--root');
|
|
416
|
+
const root = rootIdx === -1 ? ROOT : (args[rootIdx + 1] ?? ROOT);
|
|
417
|
+
const write = args.includes('--write');
|
|
418
|
+
|
|
419
|
+
const loweringText = _read(root, LOWERING_REL);
|
|
420
|
+
const docText = _read(root, DOC_REL);
|
|
421
|
+
if (loweringText === null || docText === null) {
|
|
422
|
+
process.stderr.write(
|
|
423
|
+
`❌ ${GATE}: ${loweringText === null ? LOWERING_REL : DOC_REL} not found under ${root}\n`,
|
|
424
|
+
);
|
|
425
|
+
return 2;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let lowering: HostLowering;
|
|
429
|
+
try {
|
|
430
|
+
lowering = parseHostLowering(loweringText);
|
|
431
|
+
} catch (exc) {
|
|
432
|
+
process.stderr.write(`❌ ${GATE}: ${LOWERING_REL} did not parse — ${String(exc)}\n`);
|
|
433
|
+
return 2;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const built = buildRows(lowering);
|
|
437
|
+
const ledger = new GateLedger(GATE);
|
|
438
|
+
ledger.plan(built.rows.map((r) => `${r.host}/${r.slot}`));
|
|
439
|
+
const region = renderRegion(lowering);
|
|
440
|
+
|
|
441
|
+
if (write) {
|
|
442
|
+
let next: string;
|
|
443
|
+
try {
|
|
444
|
+
next = spliceDoc(docText, region);
|
|
445
|
+
} catch (exc) {
|
|
446
|
+
process.stderr.write(`❌ ${String(exc)}\n`);
|
|
447
|
+
return 2;
|
|
448
|
+
}
|
|
449
|
+
// The backing check runs BEFORE the write, so a generator bug cannot
|
|
450
|
+
// put a phantom value into the document and then be caught by the
|
|
451
|
+
// check that same generator's output feeds.
|
|
452
|
+
const phantom = unbackedOutcomes(region, backedOutcomes(lowering));
|
|
453
|
+
if (phantom.length > 0) {
|
|
454
|
+
for (const f of phantom) process.stderr.write(`❌ ${GATE}: ${f}\n`);
|
|
455
|
+
process.stderr.write(
|
|
456
|
+
`${GATE}: refusing to write a region the configuration does not back.\n`,
|
|
457
|
+
);
|
|
458
|
+
return 1;
|
|
459
|
+
}
|
|
460
|
+
for (const r of built.rows) ledger.complete(`${r.host}/${r.slot}`);
|
|
461
|
+
if (next !== docText) fs.writeFileSync(path.join(root, DOC_REL), next, 'utf-8');
|
|
462
|
+
reportScanned({
|
|
463
|
+
gate: GATE,
|
|
464
|
+
scanned: built.rows.length,
|
|
465
|
+
units: 'host-slot pair(s)',
|
|
466
|
+
roots: [LOWERING_REL],
|
|
467
|
+
});
|
|
468
|
+
ledger.report();
|
|
469
|
+
process.stdout.write(
|
|
470
|
+
`✅ ${GATE}: wrote ${String(built.rows.length)} host-slot row(s) into ${DOC_REL}` +
|
|
471
|
+
`${next === docText ? ' (already current)' : ''}.\n`,
|
|
472
|
+
);
|
|
473
|
+
return 0;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const committed = extractRegion(docText);
|
|
477
|
+
if (committed === null) {
|
|
478
|
+
process.stderr.write(
|
|
479
|
+
`❌ ${GATE}: ${DOC_REL} has no generated region. The markers ` +
|
|
480
|
+
`${BEGIN_MARKER} … ${END_MARKER} are what make the table regenerable; restore ` +
|
|
481
|
+
`them and run \`${REGEN_COMMAND}\`.\n`,
|
|
482
|
+
);
|
|
483
|
+
return 2;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const findings: string[] = [];
|
|
487
|
+
findings.push(...unbackedOutcomes(committed, backedOutcomes(lowering)));
|
|
488
|
+
findings.push(...built.contradictions);
|
|
489
|
+
if (committed !== region) {
|
|
490
|
+
findings.push(
|
|
491
|
+
`${DOC_REL} § the generated region differs from what \`${LOWERING_REL}\` produces.`,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// A pair is `fail`ed when the committed cell for it is absent or carries a
|
|
496
|
+
// different outcome, and `complete`d otherwise. The denominator is the
|
|
497
|
+
// configuration's pairs, not the document's — a pair the document dropped
|
|
498
|
+
// must count as unchecked work rather than vanish from the accounting.
|
|
499
|
+
const byLabel = new Map(committedCells(committed).map((c) => [c.label, c.value]));
|
|
500
|
+
for (const r of built.rows) {
|
|
501
|
+
const label = `${r.host}/${r.slot}`;
|
|
502
|
+
const got = byLabel.get(label);
|
|
503
|
+
if (got === r.outcome) ledger.complete(label);
|
|
504
|
+
else {
|
|
505
|
+
ledger.fail(
|
|
506
|
+
label,
|
|
507
|
+
`published \`${got ?? '(absent)'}\`, configuration says \`${r.outcome}\``,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
reportScanned({
|
|
513
|
+
gate: GATE,
|
|
514
|
+
scanned: built.rows.length,
|
|
515
|
+
units: 'host-slot pair(s)',
|
|
516
|
+
roots: [LOWERING_REL],
|
|
517
|
+
});
|
|
518
|
+
ledger.report();
|
|
519
|
+
|
|
520
|
+
if (findings.length > 0) {
|
|
521
|
+
for (const f of findings) process.stdout.write(`❌ ${GATE}: ${f}\n`);
|
|
522
|
+
process.stdout.write(
|
|
523
|
+
`\n${String(findings.length)} finding(s). Regenerate with:\n` +
|
|
524
|
+
` ${REGEN_COMMAND}\n` +
|
|
525
|
+
`Corrections belong in \`${LOWERING_REL}\` or in the generator, never in the ` +
|
|
526
|
+
'table — a cell edited by hand is overwritten by the next regeneration, and ' +
|
|
527
|
+
'reported by this gate in between.\n',
|
|
528
|
+
);
|
|
529
|
+
return 1;
|
|
530
|
+
}
|
|
531
|
+
process.stdout.write(
|
|
532
|
+
`✅ ${GATE}: ${String(built.rows.length)} host-slot row(s) in ${DOC_REL} match ` +
|
|
533
|
+
`${LOWERING_REL}.\n`,
|
|
534
|
+
);
|
|
535
|
+
return 0;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* One rejecting case per way the region and the configuration can part company.
|
|
540
|
+
*
|
|
541
|
+
* The `halt-by-state` case is the one that matters most: a cell claiming a
|
|
542
|
+
* value the configuration does not carry must red the gate. Its fixture is
|
|
543
|
+
* built by hand rather than generated, because a fixture the generator produced
|
|
544
|
+
* could not demonstrate a value the generator cannot emit.
|
|
545
|
+
*/
|
|
546
|
+
function selfTest(): number {
|
|
547
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'enforcement-matrix-'));
|
|
548
|
+
const lowering = _read(ROOT, LOWERING_REL) ?? '';
|
|
549
|
+
const plant = (name: string, mutate: (region: string) => string): string => {
|
|
550
|
+
const dir = path.join(tmp, name);
|
|
551
|
+
fs.mkdirSync(path.join(dir, path.dirname(LOWERING_REL)), { recursive: true });
|
|
552
|
+
fs.mkdirSync(path.join(dir, path.dirname(DOC_REL)), { recursive: true });
|
|
553
|
+
fs.writeFileSync(path.join(dir, LOWERING_REL), lowering, 'utf-8');
|
|
554
|
+
const region = renderRegion(parseHostLowering(lowering));
|
|
555
|
+
fs.writeFileSync(
|
|
556
|
+
path.join(dir, DOC_REL),
|
|
557
|
+
`# fixture\n\n${BEGIN_MARKER}\n${mutate(region)}\n${END_MARKER}\n`,
|
|
558
|
+
'utf-8',
|
|
559
|
+
);
|
|
560
|
+
return dir;
|
|
561
|
+
};
|
|
562
|
+
const run = (root: string): number =>
|
|
563
|
+
runGateCli(ROOT, `src/scripts/${GATE}.ts`, ['--root', root], root);
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
return runSelfTest({
|
|
567
|
+
gate: GATE,
|
|
568
|
+
minCases: 5,
|
|
569
|
+
minRejectCases: 4,
|
|
570
|
+
cases: [
|
|
571
|
+
{
|
|
572
|
+
name: 'a cell claiming `halt-by-state`, which nothing emits, is rejected',
|
|
573
|
+
expect: 'reject',
|
|
574
|
+
run: () =>
|
|
575
|
+
run(plant('phantom', (r) => r.replace('| `warning` |', '| `halt-by-state` |'))),
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: 'a cell outside the closed vocabulary is rejected',
|
|
579
|
+
expect: 'reject',
|
|
580
|
+
run: () => run(plant('vocab', (r) => r.replace('| `refusal` |', '| `blocked` |'))),
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
name: 'a hand-flipped outcome is rejected',
|
|
584
|
+
expect: 'reject',
|
|
585
|
+
run: () => run(plant('flip', (r) => r.replace('| `refusal` |', '| `unenforced` |'))),
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: 'a deleted row is rejected',
|
|
589
|
+
expect: 'reject',
|
|
590
|
+
run: () =>
|
|
591
|
+
run(
|
|
592
|
+
plant('deleted', (r) =>
|
|
593
|
+
r
|
|
594
|
+
.split('\n')
|
|
595
|
+
.filter((l) => !l.includes('| `refusal` |'))
|
|
596
|
+
.join('\n'),
|
|
597
|
+
),
|
|
598
|
+
),
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
name: 'the regenerated region passes',
|
|
602
|
+
expect: 'accept',
|
|
603
|
+
run: () => run(plant('good', (r) => r)),
|
|
604
|
+
},
|
|
605
|
+
],
|
|
606
|
+
});
|
|
607
|
+
} finally {
|
|
608
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function _isCliEntry(): boolean {
|
|
613
|
+
if (process.argv[1] === undefined) return false;
|
|
614
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
615
|
+
try {
|
|
616
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
617
|
+
} catch {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (_isCliEntry()) {
|
|
623
|
+
process.exit(main());
|
|
624
|
+
}
|