@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,566 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* report_enforcement_drift.ts — the published enforcement claim, per host,
|
|
4
|
+
* beside the per-slot blocking values the runtime resolver actually reads.
|
|
5
|
+
*
|
|
6
|
+
* READ-ONLY AND OFFLINE. It opens two files for reading, prints, and exits. It
|
|
7
|
+
* creates no directory, opens no socket, and has no code path that puts a byte
|
|
8
|
+
* on disk. That is a property a reader can check with one grep rather than
|
|
9
|
+
* trust, and it is the reason this is a report and not a generator: deciding
|
|
10
|
+
* whether the published document may become partly build output is a
|
|
11
|
+
* documentation-policy call that has not been made.
|
|
12
|
+
*
|
|
13
|
+
* WHY IT EXISTS
|
|
14
|
+
* -------------
|
|
15
|
+
* `docs/enforcement-by-host.md` states, per HOST, whether a deny is honoured.
|
|
16
|
+
* `src/scripts/hooks/host_lowering.yaml` records, per (host, surface, SLOT),
|
|
17
|
+
* the exit code the host honours as a refusal there — `null` meaning this tree
|
|
18
|
+
* has established nothing. Those are different shapes. A host-level cell has to
|
|
19
|
+
* summarise a column of slot values, and nothing checked that the summary was
|
|
20
|
+
* faithful. This prints both, side by side, and names the rows where they
|
|
21
|
+
* disagree.
|
|
22
|
+
*
|
|
23
|
+
* THE COMPARISON RULE, AND WHY THE COUNT DEPENDS ON IT
|
|
24
|
+
* ---------------------------------------------------
|
|
25
|
+
* Two readings of a host-level cell are defensible, and they do not return the
|
|
26
|
+
* same number, so the rule is a flag rather than a hidden constant.
|
|
27
|
+
*
|
|
28
|
+
* strict (default) A binary host-level cell is accurate only when the slot
|
|
29
|
+
* values under it are UNANIMOUS. `honoured` requires every
|
|
30
|
+
* lowerable slot to carry a block exit; `not-honoured`
|
|
31
|
+
* requires every one to carry none.
|
|
32
|
+
* lenient `honoured` requires only that SOME slot carries a block
|
|
33
|
+
* exit.
|
|
34
|
+
*
|
|
35
|
+
* Strict is the default because an unqualified host-level "refuses on a deny"
|
|
36
|
+
* is what a reader reconstructs as "a guard bound on this host is honoured",
|
|
37
|
+
* and a guard bound to a non-blocking slot on that same host runs and is
|
|
38
|
+
* ignored. The lenient rule cannot distinguish those two states — it reports a
|
|
39
|
+
* host with one blocking slot out of nine exactly as it reports a host with
|
|
40
|
+
* nine out of nine — so choosing it would make the drift this report exists to
|
|
41
|
+
* measure invisible by construction. Both counts are printed either way; the
|
|
42
|
+
* flag only chooses which one is the headline.
|
|
43
|
+
*
|
|
44
|
+
* WHAT IS NOT COUNTED AS A MISMATCH, AND WHY
|
|
45
|
+
* ------------------------------------------
|
|
46
|
+
* Hosts absent from the lowering table. A published row for a host the
|
|
47
|
+
* lowering configuration does not model has no second value to compare
|
|
48
|
+
* against. It is reported as unverifiable-from-this-file, never as a
|
|
49
|
+
* disagreement — a missing row is silence, not a contradiction, and counting
|
|
50
|
+
* it would inflate the finding with a claim sourced from a different file.
|
|
51
|
+
*
|
|
52
|
+
* The bound-slot count column. The published column counts DECLARED bindings
|
|
53
|
+
* (`hook_manifest.yaml`); the lowering table records what an install can
|
|
54
|
+
* actually emit. Those are two senses the published document itself keeps
|
|
55
|
+
* apart, and it already records one disagreement between them in prose. The
|
|
56
|
+
* lowerable count is printed beside the published number as context and has
|
|
57
|
+
* no effect on the mismatch count, because comparing the two columns would
|
|
58
|
+
* be comparing two different questions.
|
|
59
|
+
*
|
|
60
|
+
* VERIFICATION CURRENCY IS APPLIED, NOT IGNORED. The effective blocking value
|
|
61
|
+
* is `blockExitFor`'s: a row whose `verified` block is absent or expired cannot
|
|
62
|
+
* carry a blocking binding, whatever its slot literals say. The raw literal is
|
|
63
|
+
* printed too, so an expiry that has silently disarmed a host is visible as a
|
|
64
|
+
* difference between the two columns rather than as a value that quietly turned
|
|
65
|
+
* into `null`.
|
|
66
|
+
*
|
|
67
|
+
* Exit: 0 always. A usage error is 1. It gates on nothing and must not acquire
|
|
68
|
+
* a threshold — the drift it measures is a documentation defect whose fix is a
|
|
69
|
+
* human edit or a generator, and a report that fails a build before either
|
|
70
|
+
* exists would only teach people to skip it.
|
|
71
|
+
*/
|
|
72
|
+
import * as fs from 'node:fs';
|
|
73
|
+
import * as path from 'node:path';
|
|
74
|
+
import process from 'node:process';
|
|
75
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
76
|
+
|
|
77
|
+
import { splitMarkdownRow } from './_lib/md_table.js';
|
|
78
|
+
import {
|
|
79
|
+
DEFAULT_SURFACE,
|
|
80
|
+
type HostLowering,
|
|
81
|
+
type SurfaceRow,
|
|
82
|
+
isVerifiedNow,
|
|
83
|
+
parseHostLowering,
|
|
84
|
+
} from './hooks/host_lowering.js';
|
|
85
|
+
|
|
86
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
87
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
88
|
+
|
|
89
|
+
export const DEFAULT_LOWERING = path.join('src', 'scripts', 'hooks', 'host_lowering.yaml');
|
|
90
|
+
export const DEFAULT_DOC = path.join('docs', 'enforcement-by-host.md');
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The header that identifies the enforcement matrix inside the published
|
|
94
|
+
* document.
|
|
95
|
+
*
|
|
96
|
+
* Matched on its cells rather than on position, because the document carries
|
|
97
|
+
* more than one table and gained another one recently. A positional read would
|
|
98
|
+
* have silently retargeted onto whichever table happened to come first.
|
|
99
|
+
*/
|
|
100
|
+
const MATRIX_HEADER = ['Host', 'Compile-time rules', 'Lifecycle slots bound', 'Deny honoured'];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The opening marker of the generated per-slot region.
|
|
104
|
+
*
|
|
105
|
+
* Held as a literal rather than imported, because the module that owns it —
|
|
106
|
+
* `check_enforcement_matrix` — imports this one, and an import back would be a
|
|
107
|
+
* cycle. `tests/scripts/check_enforcement_matrix.test.ts` asserts the two
|
|
108
|
+
* spellings are equal, so the duplication cannot drift silently.
|
|
109
|
+
*/
|
|
110
|
+
const GENERATED_REGION_MARKER = '<!-- BEGIN GENERATED: enforcement-configured-by-slot -->';
|
|
111
|
+
|
|
112
|
+
/** What the published cell asserts. */
|
|
113
|
+
export type ClaimedDeny = 'honoured' | 'not-honoured' | 'not-applicable' | 'unparsed';
|
|
114
|
+
|
|
115
|
+
/** What the lowering configuration says about a host's slot column. */
|
|
116
|
+
export type DerivedDeny = 'all' | 'partial' | 'none' | 'no-slots' | 'unmodelled';
|
|
117
|
+
|
|
118
|
+
export type ComparisonRule = 'strict' | 'lenient';
|
|
119
|
+
|
|
120
|
+
export interface SlotReading {
|
|
121
|
+
slot: string;
|
|
122
|
+
/** The literal in the table. */
|
|
123
|
+
literal: number | null;
|
|
124
|
+
/** The literal after the row's verification currency is applied. */
|
|
125
|
+
effective: number | null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** What {@link readSlots} establishes about one host. */
|
|
129
|
+
export interface HostReading {
|
|
130
|
+
/** The host's `any` surface row, or null when the configuration has none. */
|
|
131
|
+
row: SurfaceRow | null;
|
|
132
|
+
/** The row exists and its `verified` block has not expired. */
|
|
133
|
+
verifiedNow: boolean;
|
|
134
|
+
/** Lowerable slots, in the configuration file's own order. */
|
|
135
|
+
slots: SlotReading[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface PublishedRow {
|
|
139
|
+
/** The label exactly as the document writes it. */
|
|
140
|
+
label: string;
|
|
141
|
+
/** The lowering key it resolves to. */
|
|
142
|
+
host: string;
|
|
143
|
+
/** The bound-slot count the document publishes, or null when unparsable. */
|
|
144
|
+
publishedSlots: number | null;
|
|
145
|
+
claimed: ClaimedDeny;
|
|
146
|
+
/** The whole cell, so a reason clause is quotable in the output. */
|
|
147
|
+
claimCell: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface HostComparison extends PublishedRow {
|
|
151
|
+
derived: DerivedDeny;
|
|
152
|
+
slots: SlotReading[];
|
|
153
|
+
/** Lowerable slots, which is not the published count's sense. */
|
|
154
|
+
lowerableSlots: number;
|
|
155
|
+
blockingSlots: string[];
|
|
156
|
+
nonBlockingSlots: string[];
|
|
157
|
+
/** Present in the lowering table with an unexpired `verified` block. */
|
|
158
|
+
verifiedNow: boolean;
|
|
159
|
+
mismatchStrict: boolean;
|
|
160
|
+
mismatchLenient: boolean;
|
|
161
|
+
/** No lowering row, so nothing to compare — never a mismatch. */
|
|
162
|
+
unverifiable: boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface DriftReport {
|
|
166
|
+
loweringPath: string;
|
|
167
|
+
docPath: string;
|
|
168
|
+
rule: ComparisonRule;
|
|
169
|
+
/**
|
|
170
|
+
* The document carries the generated per-slot region.
|
|
171
|
+
*
|
|
172
|
+
* Distinguishes the two reasons this report can find no host-level matrix:
|
|
173
|
+
* the header moved (a parse failure), or the hand-maintained column this
|
|
174
|
+
* report was written against was replaced by a generated one (the fix
|
|
175
|
+
* landing). Reporting both as "header not found" would make a repaired
|
|
176
|
+
* document read like a broken one.
|
|
177
|
+
*/
|
|
178
|
+
generatedRegionPresent: boolean;
|
|
179
|
+
rows: HostComparison[];
|
|
180
|
+
/** Rows under the chosen rule. */
|
|
181
|
+
mismatches: HostComparison[];
|
|
182
|
+
/** Rows the other rule would have flagged, so the choice is visible. */
|
|
183
|
+
mismatchesStrict: HostComparison[];
|
|
184
|
+
mismatchesLenient: HostComparison[];
|
|
185
|
+
unverifiable: HostComparison[];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The lowering key a published label names.
|
|
190
|
+
*
|
|
191
|
+
* The first word, lower-cased, resolves every row the document carries today
|
|
192
|
+
* (`Claude Code (plugin)` to `claude`, and eight one-word labels to themselves).
|
|
193
|
+
* A label that does not resolve is reported as unmodelled rather than guessed
|
|
194
|
+
* at, which is the same treatment a host genuinely absent from the table gets —
|
|
195
|
+
* both mean "no value to compare", and inventing a mapping would turn a naming
|
|
196
|
+
* change into a false agreement.
|
|
197
|
+
*/
|
|
198
|
+
export function loweringKeyFor(label: string): string {
|
|
199
|
+
const first = label.trim().split(/\s+/)[0] ?? '';
|
|
200
|
+
return first.toLowerCase().replace(/[^a-z]/g, '');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Read the published deny cell without inferring anything from its prose. */
|
|
204
|
+
export function parseClaim(cell: string): ClaimedDeny {
|
|
205
|
+
const c = cell.trim();
|
|
206
|
+
if (c.startsWith('✅')) return 'honoured';
|
|
207
|
+
if (c.startsWith('❌')) return 'not-honoured';
|
|
208
|
+
if (c.startsWith('—') || c.startsWith('-')) return 'not-applicable';
|
|
209
|
+
return 'unparsed';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Rows of the enforcement matrix, in document order.
|
|
214
|
+
*
|
|
215
|
+
* Returns an empty list when the header is absent. An empty list is reported as
|
|
216
|
+
* such by the renderer and never silently treated as "no drift" — a report that
|
|
217
|
+
* cannot find its subject must not read as a clean bill of health.
|
|
218
|
+
*/
|
|
219
|
+
export function parsePublishedMatrix(markdown: string): PublishedRow[] {
|
|
220
|
+
const lines = markdown.split('\n');
|
|
221
|
+
let start = -1;
|
|
222
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
223
|
+
const line = lines[i] ?? '';
|
|
224
|
+
if (!line.trim().startsWith('|')) continue;
|
|
225
|
+
const cells = splitMarkdownRow(line);
|
|
226
|
+
if (cells.length === MATRIX_HEADER.length && MATRIX_HEADER.every((h, j) => cells[j] === h)) {
|
|
227
|
+
start = i + 2;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (start < 0) return [];
|
|
232
|
+
const out: PublishedRow[] = [];
|
|
233
|
+
for (let i = start; i < lines.length; i += 1) {
|
|
234
|
+
const line = lines[i] ?? '';
|
|
235
|
+
if (!line.trim().startsWith('|')) break;
|
|
236
|
+
const cells = splitMarkdownRow(line);
|
|
237
|
+
if (cells.length < MATRIX_HEADER.length) break;
|
|
238
|
+
const label = cells[0] ?? '';
|
|
239
|
+
const slotCell = cells[2] ?? '';
|
|
240
|
+
const m = /^(\d+)$/.exec(slotCell.trim());
|
|
241
|
+
out.push({
|
|
242
|
+
label,
|
|
243
|
+
host: loweringKeyFor(label),
|
|
244
|
+
publishedSlots: m === null ? null : Number(m[1]),
|
|
245
|
+
claimed: parseClaim(cells[3] ?? ''),
|
|
246
|
+
claimCell: (cells[3] ?? '').trim(),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* One host's slot column, with verification currency already applied.
|
|
254
|
+
*
|
|
255
|
+
* THE PROJECTION, AND THE ONE PLACE IT LIVES. Everything downstream of this
|
|
256
|
+
* file that needs "what does the configuration say about (host, slot)" calls
|
|
257
|
+
* this rather than re-walking the parsed table. `check_enforcement_matrix`
|
|
258
|
+
* generates the published per-slot table from exactly this return value, so the
|
|
259
|
+
* generator and this reporter cannot drift into two readings of the same YAML —
|
|
260
|
+
* which is the failure the document they both describe already suffered once,
|
|
261
|
+
* at a different layer.
|
|
262
|
+
*
|
|
263
|
+
* `effective` applies the row's `verified` currency: a row whose block has
|
|
264
|
+
* expired cannot carry a blocking binding whatever its slot literals say.
|
|
265
|
+
* `literal` is returned beside it so an expiry that silently disarmed a host is
|
|
266
|
+
* visible as a difference between two columns rather than as a value that
|
|
267
|
+
* quietly turned into `null`.
|
|
268
|
+
*/
|
|
269
|
+
export function readSlots(lowering: HostLowering, host: string): HostReading {
|
|
270
|
+
const row = lowering.get(host)?.get(DEFAULT_SURFACE) ?? null;
|
|
271
|
+
const verifiedNow = isVerifiedNow(row);
|
|
272
|
+
const slots: SlotReading[] = [];
|
|
273
|
+
if (row) {
|
|
274
|
+
for (const [slot, sr] of row.slots) {
|
|
275
|
+
slots.push({
|
|
276
|
+
slot,
|
|
277
|
+
literal: sr.block_exit,
|
|
278
|
+
effective: verifiedNow ? sr.block_exit : null,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { row, verifiedNow, slots };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Fold a host's slot column into the vocabulary a host-level cell can carry. */
|
|
286
|
+
export function deriveDeny(slots: SlotReading[], present: boolean): DerivedDeny {
|
|
287
|
+
if (!present) return 'unmodelled';
|
|
288
|
+
if (slots.length === 0) return 'no-slots';
|
|
289
|
+
const blocking = slots.filter((s) => s.effective !== null).length;
|
|
290
|
+
if (blocking === 0) return 'none';
|
|
291
|
+
return blocking === slots.length ? 'all' : 'partial';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Does the published cell agree with the derived value under `rule`?
|
|
296
|
+
*
|
|
297
|
+
* `not-applicable` is accepted against `no-slots` and `unmodelled` only: a
|
|
298
|
+
* dash means the question does not arise, which is true exactly when there is
|
|
299
|
+
* nothing bound to ask it of.
|
|
300
|
+
*/
|
|
301
|
+
export function agrees(claimed: ClaimedDeny, derived: DerivedDeny, rule: ComparisonRule): boolean {
|
|
302
|
+
if (derived === 'unmodelled') return true;
|
|
303
|
+
switch (claimed) {
|
|
304
|
+
case 'honoured':
|
|
305
|
+
return rule === 'strict' ? derived === 'all' : derived === 'all' || derived === 'partial';
|
|
306
|
+
case 'not-honoured':
|
|
307
|
+
return derived === 'none' || derived === 'no-slots';
|
|
308
|
+
case 'not-applicable':
|
|
309
|
+
return derived === 'no-slots';
|
|
310
|
+
case 'unparsed':
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function compareEnforcement(
|
|
316
|
+
lowering: HostLowering,
|
|
317
|
+
markdown: string,
|
|
318
|
+
rule: ComparisonRule,
|
|
319
|
+
loweringPath: string,
|
|
320
|
+
docPath: string,
|
|
321
|
+
): DriftReport {
|
|
322
|
+
const rows: HostComparison[] = [];
|
|
323
|
+
for (const published of parsePublishedMatrix(markdown)) {
|
|
324
|
+
const { row, verifiedNow, slots } = readSlots(lowering, published.host);
|
|
325
|
+
const derived = deriveDeny(slots, row !== null);
|
|
326
|
+
rows.push({
|
|
327
|
+
...published,
|
|
328
|
+
derived,
|
|
329
|
+
slots,
|
|
330
|
+
lowerableSlots: slots.length,
|
|
331
|
+
blockingSlots: slots.filter((s) => s.effective !== null).map((s) => s.slot),
|
|
332
|
+
nonBlockingSlots: slots.filter((s) => s.effective === null).map((s) => s.slot),
|
|
333
|
+
verifiedNow,
|
|
334
|
+
mismatchStrict: !agrees(published.claimed, derived, 'strict'),
|
|
335
|
+
mismatchLenient: !agrees(published.claimed, derived, 'lenient'),
|
|
336
|
+
unverifiable: derived === 'unmodelled',
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
const mismatchesStrict = rows.filter((r) => r.mismatchStrict);
|
|
340
|
+
const mismatchesLenient = rows.filter((r) => r.mismatchLenient);
|
|
341
|
+
return {
|
|
342
|
+
loweringPath,
|
|
343
|
+
docPath,
|
|
344
|
+
rule,
|
|
345
|
+
generatedRegionPresent: markdown.includes(GENERATED_REGION_MARKER),
|
|
346
|
+
rows,
|
|
347
|
+
mismatches: rule === 'strict' ? mismatchesStrict : mismatchesLenient,
|
|
348
|
+
mismatchesStrict,
|
|
349
|
+
mismatchesLenient,
|
|
350
|
+
unverifiable: rows.filter((r) => r.unverifiable),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function _derivedLabel(r: HostComparison): string {
|
|
355
|
+
switch (r.derived) {
|
|
356
|
+
case 'all':
|
|
357
|
+
return `all ${String(r.lowerableSlots)} lowerable slot(s) block`;
|
|
358
|
+
case 'partial':
|
|
359
|
+
return `${String(r.blockingSlots.length)} of ${String(r.lowerableSlots)} lowerable slots block`;
|
|
360
|
+
case 'none':
|
|
361
|
+
return `0 of ${String(r.lowerableSlots)} lowerable slots block`;
|
|
362
|
+
case 'no-slots':
|
|
363
|
+
return 'no lowerable slots';
|
|
364
|
+
case 'unmodelled':
|
|
365
|
+
return 'no row in the lowering configuration';
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function render(d: DriftReport): string {
|
|
370
|
+
const lines: string[] = [];
|
|
371
|
+
lines.push('enforcement drift — published host claim vs per-slot lowering values');
|
|
372
|
+
lines.push(` lowering: ${d.loweringPath}`);
|
|
373
|
+
lines.push(` document: ${d.docPath}`);
|
|
374
|
+
lines.push(` rule: ${d.rule}`);
|
|
375
|
+
lines.push('');
|
|
376
|
+
|
|
377
|
+
if (d.rows.length === 0 && d.generatedRegionPresent) {
|
|
378
|
+
lines.push(' There is no hand-maintained host-level enforcement column left to drift.');
|
|
379
|
+
lines.push(' The document carries the generated per-slot region instead, and the check');
|
|
380
|
+
lines.push(' that keeps it current is `check_enforcement_matrix`, which runs in CI.');
|
|
381
|
+
lines.push('');
|
|
382
|
+
lines.push(' This report measured the column that preceded it, and that measurement is');
|
|
383
|
+
lines.push(' what the generated region replaced: a binary host-level cell has to');
|
|
384
|
+
lines.push(' summarise a column of slot values, and no binary value is faithful when the');
|
|
385
|
+
lines.push(' column disagrees with itself. Nothing here is a parse failure: a document');
|
|
386
|
+
lines.push(' whose header had genuinely moved, with no generated region to replace it,');
|
|
387
|
+
lines.push(' says so instead and names the header it looked for.');
|
|
388
|
+
return lines.join('\n');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (d.rows.length === 0) {
|
|
392
|
+
lines.push(' The enforcement matrix header was not found in the document. Nothing was');
|
|
393
|
+
lines.push(' compared. This is a parse failure, NOT an empty mismatch list — do not read');
|
|
394
|
+
lines.push(' it as the table being accurate. Check that the header still reads:');
|
|
395
|
+
lines.push(` | ${MATRIX_HEADER.join(' | ')} |`);
|
|
396
|
+
return lines.join('\n');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
lines.push(' PER HOST, PER SLOT');
|
|
400
|
+
for (const r of d.rows) {
|
|
401
|
+
const mark = r.unverifiable ? '--' : d.mismatches.includes(r) ? '❌' : '✅';
|
|
402
|
+
lines.push(` ${mark} ${r.label} [${r.host}]`);
|
|
403
|
+
lines.push(` published: ${r.claimCell || '(empty)'}`);
|
|
404
|
+
lines.push(` derived: ${_derivedLabel(r)}`);
|
|
405
|
+
// Only when currency actually CHANGED a value. A host whose literals are
|
|
406
|
+
// all null already has nothing for an expiry to disarm, and printing the
|
|
407
|
+
// note there would read as a host that used to block and quietly stopped.
|
|
408
|
+
if (!r.verifiedNow && r.slots.some((s) => s.literal !== null)) {
|
|
409
|
+
lines.push(' note: no unexpired `verified` block, so every effective value');
|
|
410
|
+
lines.push(' is null whatever the slot literals say');
|
|
411
|
+
}
|
|
412
|
+
for (const s of r.slots) {
|
|
413
|
+
const eff = s.effective === null ? 'null' : String(s.effective);
|
|
414
|
+
const lit = s.literal === null ? 'null' : String(s.literal);
|
|
415
|
+
const drift = lit === eff ? '' : ` (literal ${lit}, disarmed by expiry)`;
|
|
416
|
+
lines.push(` ${s.slot.padEnd(20)} block_exit ${eff}${drift}`);
|
|
417
|
+
}
|
|
418
|
+
if (r.lowerableSlots === 0) {
|
|
419
|
+
lines.push(' (none)');
|
|
420
|
+
}
|
|
421
|
+
lines.push(
|
|
422
|
+
` slot count: published ${r.publishedSlots === null ? '?' : String(r.publishedSlots)}` +
|
|
423
|
+
` · lowerable ${String(r.lowerableSlots)}` +
|
|
424
|
+
(r.publishedSlots !== null && r.publishedSlots !== r.lowerableSlots
|
|
425
|
+
? ' (different senses — see the note below; not a mismatch here)'
|
|
426
|
+
: ''),
|
|
427
|
+
);
|
|
428
|
+
lines.push('');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
lines.push(` MISMATCHES under the ${d.rule} rule: ${String(d.mismatches.length)}`);
|
|
432
|
+
if (d.mismatches.length === 0) {
|
|
433
|
+
lines.push(' None. Every published host cell agrees with the slot values under it at');
|
|
434
|
+
lines.push(' slot granularity. That is a finding, not a skipped check — the table was');
|
|
435
|
+
lines.push(' measured against its data source and matched.');
|
|
436
|
+
}
|
|
437
|
+
for (const r of d.mismatches) {
|
|
438
|
+
lines.push(` - ${r.label} [${r.host}]`);
|
|
439
|
+
lines.push(` published claims ${r.claimed}, configuration says ${_derivedLabel(r)}`);
|
|
440
|
+
if (r.blockingSlots.length > 0) {
|
|
441
|
+
lines.push(` blocking: ${r.blockingSlots.join(', ')}`);
|
|
442
|
+
}
|
|
443
|
+
if (r.nonBlockingSlots.length > 0) {
|
|
444
|
+
lines.push(` NOT blocking: ${r.nonBlockingSlots.join(', ')}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
lines.push('');
|
|
448
|
+
|
|
449
|
+
const other: ComparisonRule = d.rule === 'strict' ? 'lenient' : 'strict';
|
|
450
|
+
const otherCount = other === 'strict' ? d.mismatchesStrict.length : d.mismatchesLenient.length;
|
|
451
|
+
lines.push(` Under the ${other} rule the count would be ${String(otherCount)}.`);
|
|
452
|
+
if (otherCount !== d.mismatches.length) {
|
|
453
|
+
lines.push(' The two rules disagree, so the headline number is a consequence of the rule');
|
|
454
|
+
lines.push(' and not of the data alone. The strict rule reads a binary host-level cell as');
|
|
455
|
+
lines.push(' a claim about every slot under it; the lenient rule reads it as a claim about');
|
|
456
|
+
lines.push(' at least one. Re-run with --rule to see the other reading rather than');
|
|
457
|
+
lines.push(' taking this sentence for it.');
|
|
458
|
+
}
|
|
459
|
+
lines.push('');
|
|
460
|
+
|
|
461
|
+
if (d.unverifiable.length > 0) {
|
|
462
|
+
lines.push(` NOT COMPARED (${String(d.unverifiable.length)}) — published, but absent from the lowering`);
|
|
463
|
+
lines.push(' configuration, so there is no second value. Reported rather than counted: a');
|
|
464
|
+
lines.push(' missing row is silence, and a claim sourced from another file is not this');
|
|
465
|
+
lines.push(" file's to contradict.");
|
|
466
|
+
for (const r of d.unverifiable) {
|
|
467
|
+
lines.push(` - ${r.label} [${r.host}]: ${r.claimCell || '(empty)'}`);
|
|
468
|
+
}
|
|
469
|
+
lines.push('');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const countGap = d.rows.filter(
|
|
473
|
+
(r) => !r.unverifiable && r.publishedSlots !== null && r.publishedSlots !== r.lowerableSlots,
|
|
474
|
+
);
|
|
475
|
+
if (countGap.length > 0) {
|
|
476
|
+
lines.push(` SLOT-COUNT COLUMN (${String(countGap.length)} row(s) differ) — advisory, deliberately not a`);
|
|
477
|
+
lines.push(' mismatch. The published column counts DECLARED bindings; this file records');
|
|
478
|
+
lines.push(' what an install can emit. Two senses the document itself keeps apart, so a');
|
|
479
|
+
lines.push(' difference here is an axis difference and not necessarily an error.');
|
|
480
|
+
for (const r of countGap) {
|
|
481
|
+
lines.push(
|
|
482
|
+
` - ${r.label}: published ${String(r.publishedSlots)} · lowerable ${String(r.lowerableSlots)}`,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
lines.push('');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
lines.push(`scanned: ${String(d.rows.length)}`);
|
|
489
|
+
return lines.join('\n');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function main(argv?: readonly string[]): number {
|
|
493
|
+
const args = argv ?? process.argv.slice(2);
|
|
494
|
+
let loweringPath = path.join(REPO_ROOT, DEFAULT_LOWERING);
|
|
495
|
+
let docPath = path.join(REPO_ROOT, DEFAULT_DOC);
|
|
496
|
+
let rule: ComparisonRule = 'strict';
|
|
497
|
+
let asJson = false;
|
|
498
|
+
// A value must not itself be a flag: `--doc --rule strict` would otherwise
|
|
499
|
+
// resolve a file literally named `--rule` and silently drop `strict`.
|
|
500
|
+
const value = (i: number): string | null => {
|
|
501
|
+
const v = args[i + 1];
|
|
502
|
+
return v === undefined || v.startsWith('-') ? null : v;
|
|
503
|
+
};
|
|
504
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
505
|
+
const a = args[i];
|
|
506
|
+
if (a === '--lowering' || a === '--doc' || a === '--rule') {
|
|
507
|
+
const v = value(i);
|
|
508
|
+
if (v === null) {
|
|
509
|
+
process.stderr.write(`report_enforcement_drift: ${a} needs a value\n`);
|
|
510
|
+
return 1;
|
|
511
|
+
}
|
|
512
|
+
if (a === '--rule') {
|
|
513
|
+
if (v !== 'strict' && v !== 'lenient') {
|
|
514
|
+
process.stderr.write(`report_enforcement_drift: --rule must be strict or lenient\n`);
|
|
515
|
+
return 1;
|
|
516
|
+
}
|
|
517
|
+
rule = v;
|
|
518
|
+
} else if (a === '--lowering') loweringPath = path.resolve(v);
|
|
519
|
+
else docPath = path.resolve(v);
|
|
520
|
+
i += 1;
|
|
521
|
+
} else if (a === '--json') {
|
|
522
|
+
asJson = true;
|
|
523
|
+
} else if (a === '--help' || a === '-h') {
|
|
524
|
+
process.stdout.write(
|
|
525
|
+
'usage: report_enforcement_drift [--lowering FILE] [--doc FILE] ' +
|
|
526
|
+
'[--rule strict|lenient] [--json]\n',
|
|
527
|
+
);
|
|
528
|
+
return 0;
|
|
529
|
+
} else if (a !== undefined && a.startsWith('-')) {
|
|
530
|
+
process.stderr.write(`report_enforcement_drift: unknown flag ${a}\n`);
|
|
531
|
+
return 1;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
let lowering: HostLowering;
|
|
536
|
+
let markdown: string;
|
|
537
|
+
try {
|
|
538
|
+
lowering = parseHostLowering(fs.readFileSync(loweringPath, 'utf-8'));
|
|
539
|
+
markdown = fs.readFileSync(docPath, 'utf-8');
|
|
540
|
+
} catch (exc) {
|
|
541
|
+
process.stderr.write(`❌ report_enforcement_drift: ${String(exc)}\n`);
|
|
542
|
+
return 1;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const report = compareEnforcement(lowering, markdown, rule, loweringPath, docPath);
|
|
546
|
+
if (asJson) {
|
|
547
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
process.stdout.write(`${render(report)}\n`);
|
|
551
|
+
return 0;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function _isCliEntry(): boolean {
|
|
555
|
+
if (process.argv[1] === undefined) return false;
|
|
556
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
557
|
+
try {
|
|
558
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
559
|
+
} catch {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (_isCliEntry()) {
|
|
565
|
+
process.exit(main());
|
|
566
|
+
}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
32
|
+
import { PLATFORM_METADATA_KEYS } from './_lib/hook_platform_keys.js';
|
|
32
33
|
import * as fs from 'node:fs';
|
|
33
34
|
import * as path from 'node:path';
|
|
34
35
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -64,17 +65,8 @@ export function loadPlatforms(root: string): Record<string, Record<string, strin
|
|
|
64
65
|
return JSON.parse(out.stdout) as Record<string, Record<string, string[]>>;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
*
|
|
70
|
-
* `fallback_only` is a marker saying this package binds nothing here, and
|
|
71
|
-
* counting it as a slot made copilot read 1 against the 0 that
|
|
72
|
-
* `docs/enforcement-by-host.md` records — a one-row disagreement with the host
|
|
73
|
-
* table for a nameable reason, which is the drift this repository gates
|
|
74
|
-
* elsewhere. Caught by comparing the generated table against that doc rather
|
|
75
|
-
* than by a test, so it is named here.
|
|
76
|
-
*/
|
|
77
|
-
const NON_SLOT_KEYS: ReadonlySet<string> = new Set(['fallback_only']);
|
|
68
|
+
/** Non-slot keys, from the one definition — see `_lib/hook_platform_keys.ts`. */
|
|
69
|
+
const NON_SLOT_KEYS = PLATFORM_METADATA_KEYS;
|
|
78
70
|
|
|
79
71
|
/** Lifecycle slots bound for a host, on the host table's own definition. */
|
|
80
72
|
export function boundSlotCount(platforms: Readonly<Record<string, Readonly<Record<string, readonly string[]>>>>, host: string): number {
|