Crosswake. Bridge. CatalogGuard
(crosswake v0.2.3)
View Source
Merge-blocking structural guard for the Phase 154 catalog line (CTRL-04, PROOF-04).
This module is a plain support module (NO use ExUnit.Case) callable from the
proof lane and from mix crosswake.doctor. It lives in lib/, not test/,
for the reason D-43 gives: deleting the test does not delete the rule, and
doctor can call the same predicates a developer's CI run does. The guard
travels with the code.
It reads the attestation file that already exists — Manifest.Builder's
capability catalog — rather than minting a second one. A second catalog would
recreate exactly the drift problem it claims to solve, and here it would be
five-way drift: new file, catalog, command list, capability-command map, and
two native enums (D-42).
The six criteria, labelled honestly (D-44)
This labelling is not decoration. D-45 records that PROOF-04 does not stop a maintainer adding forty controls one string at a time. Stating which criteria are mechanical, which is mechanical only in the negative, and which is hybrid with a later phase is what keeps the requirement from quietly overclaiming.
- (a) route-local and declarable — MECHANICAL. Universally quantified over
every command in the vocabulary, never spot-checked: every shipped command
resolves to a declared capability family, and every bounded-bridge catalog
entry resolves back to a shipped command (
check_attestation/3). - (b) low-frequency — MECHANICAL ONLY IN THE NEGATIVE. The guard can prove
no streaming seam exists in the bridge tree (
check_no_streaming_seam/1). It cannot prove nobody calls a control in a loop. Absence of a seam is provable; discipline in calling is not. - (c) zero external SDK — MECHANICAL. An AST allowlist walk over every
alias/import/require/usedeclaration in the bridge tree (check_no_external_sdk/1). - (d) semantically bounded — MECHANICAL. Six sub-assertions: the command list is a literal, no dynamic-registration function, no runtime function application, no atom minting outside the frozen allowlist, and native command enum parity against BOTH native sources in BOTH directions.
- (e) fails closed — HYBRID. This phase asserts the declaration. Phase 155's PROOF-01 route tour asserts it renders. Nothing here proves the denial surface actually appears to a user.
- (f) backend-authoritative — MECHANICAL BY PROXY.
Crosswake.Bridge.Reply's field set is frozen (Plan 03), so a reply has nowhere to put an authority-carrying key. The guard does not re-derive this; it inherits it.
Non-mechanical exclusion — the unbounded host-supplied denial reason
Separately labelled, because it is NOT one of the six and it is NOT covered by
the mechanical set above. Five delegate seams accept a bare String denial
reason from an adopter host, so no static enumeration can bound them:
packages/crosswake-shell-core-android/src/main/java/dev/crosswake/shell/core/CrosswakeDelegates.kt:39—data class Denied(val reason: String, ...)on the notification-token delegate.packages/crosswake-shell-core-android/src/main/java/dev/crosswake/shell/core/FilesPickResult.kt:7—data class Denied(val reason: String, ...)on the files-pick result.packages/crosswake-shell-core-ios/Sources/CrosswakeShellCore/BridgeChannel.swift:128—case unavailable(reason: String, detail: [String: String]).packages/crosswake-shell-core-ios/Sources/CrosswakeShellCore/BridgeChannel.swift:133—case deny(reason: String, message: String, hint: String).examples/android_shell_host/app/src/main/java/dev/crosswake/shell/CrosswakeDelegates.kt:39— the duplicated example-host copy of seam 1.
Any adopter host can mint an arbitrary reason at any of those five seams. This
sub-assertion is NOT mechanically enforceable and is carried by
.planning/seeds/SEED-008-native-denial-vocabulary.md, not by this guard. Do
not read the six-criteria block above as covering it. The runtime answer
already shipped: Crosswake.Bridge's reply decoder resolves an unknown reason
string to :unavailable_capability and preserves the raw value at
details.raw_reason, so an unbounded reason can neither crash the server nor
launder itself into the closed vocabulary.
Known extractor limitation
extract_native_denial_reasons/1 recognises a reason literal only when it is
lowercase and contains at least one underscore. Every one of the 14 closed
vocabulary reasons and all 8 seeded allowlist entries have that shape, and the
underscore requirement is what keeps neighbouring prose and detail-key literals
("unconfigured", "connecting") from being mistaken for reasons. A
single-word reason literal would slip past. Named here rather than hidden.
AST mechanism
Code.string_to_quoted/2 + Macro.prewalk/3 only — stdlib, no new dependency,
no cross-reference tooling. Mirrors Crosswake.CompanionGuard exactly, including
its documented child-module prefix-match pitfall when matching alias node parts.
The injection seam, and why it is not a loophole
assert_catalog_closed!/1 accepts an optional root: plus the three compiled
inputs it would otherwise read from Contract, Registry, and
Manifest.Builder. Every default is the real shipped value, so the zero-argument
call — the one CI and mix crosswake.doctor make — is byte-for-byte the gate it
was before the seam existed. Nothing is relaxed and no violation is skippable.
The seam exists so
test/crosswake/proof/phase154_recipe_followable_test.exs can EXECUTE the
six-step recipe this module's failure message prints, against a synthetic control
in a temp tree, and prove the gate goes red-to-green across the steps and red
again when any single step is omitted. Before the seam, that test could only
re-compose the individual predicates and hope the composition matched the
raiser's; now it drives the raiser itself. A gate whose documented path to yes is
never executed is a gate nobody has checked the exit door on.
Summary
Functions
Walks the real shipped sources and raises on the first violated criterion, with the six-step recipe for legitimately adding the next control.
The bounded-bridge capability families declared in Manifest.Builder's
capability catalog — the attestation file that already exists (D-42).
The lib/ sources the catalog line is enforced over.
Criterion (a): every catalog entry maps to a shipped command and every shipped command maps to a catalog entry.
Criterion (d): the command vocabulary must be a compile-time literal.
Asserts every allowlist entry is still emitted by at least one of its declared sites. An entry whose string has been fixed or deleted is rot: it makes the allowlist look larger than the real debt and quietly widens the gate.
Asserts every statically extractable native denial reason is either in the
closed Crosswake.Shell.Denial vocabulary or on the eight-entry seeded
allowlist. A NINTH out-of-vocabulary string is a violation.
Criterion (d): native command enum parity, asserted in BOTH directions.
Criterion (d): no atom minting. String.to_atom/1 and List.to_atom/1 grow the
atom table from wire input. String.to_existing_atom/1 is on the frozen
allowlist — it cannot mint, only resolve.
Criterion (d): no dynamic-registration seam.
Criterion (c): zero external SDK. An allowlist walk over every
alias/import/require/use declaration; anything rooted outside
[:Crosswake, :Phoenix, :Logger, :Jason, :Kernel] is an external SDK reaching into the
bounded bridge.
Criterion (d): no runtime function application. apply/2, apply/3, and
Kernel.apply/3 all let a command name become a call target.
Criterion (b), in the negative only: no streaming or back-pressure seam
([:Stream, :GenStage, :Flow, :Broadway]) in the bridge tree.
Runs every source-level mechanical sub-assertion over source_string and
returns the COMPLETE SET of violations.
The closed denial vocabulary as wire strings.
Extracts the wire values from a native BridgeCommand enum block.
Extracts denial reason string literals from native (Swift/Kotlin) source.
The two native command-enum sources checked for bidirectional parity.
The native sources whose emitted denial reason strings are checked against the closed vocabulary. Enumerated, not globbed: a source that has moved must be noticed, not silently skipped.
The eight enumerated out-of-vocabulary native denial reason strings, each with its shipping sites, an individual justification, and the SEED-008 id (D-16, resolved as option-b with amendment).
Native enum cases exempt from orphan detection because they are outbound server -> shell pushes with no inbound bounded-bridge request seam.
The shipped command -> capability family map, read from Crosswake.Bridge.Registry.
Functions
@spec assert_catalog_closed!(keyword()) :: :ok
Walks the real shipped sources and raises on the first violated criterion, with the six-step recipe for legitimately adding the next control.
Returns :ok when the catalog line holds.
Options — all defaulting to the real shipped values
:root— the tree the source, native-enum, and native-denial walks read from. Defaults toFile.cwd!().:commands— defaults toCrosswake.Bridge.Contract.commands/0.:command_capability_map— defaults toshipped_command_capability_map/0.:catalog_capability_ids— defaults tobounded_bridge_capability_ids/0.
assert_catalog_closed!() with no options is the merge-blocking gate, unchanged.
See the moduledoc's "injection seam" section for why the options exist.
@spec bounded_bridge_capability_ids() :: [String.t()]
The bounded-bridge capability families declared in Manifest.Builder's
capability catalog — the attestation file that already exists (D-42).
The lib/ sources the catalog line is enforced over.
root defaults to File.cwd!() — the shipped tree. It is parameterised only so
the recipe-followability proof can point the same walk at a temp fixture tree.
@spec check_attestation([String.t()], %{required(String.t()) => String.t() | nil}, [ String.t() ]) :: :ok | {:violation, list()}
Criterion (a): every catalog entry maps to a shipped command and every shipped command maps to a catalog entry.
Rejecting only gaps would let a command ship with no owner, no rebuild cost, and no declared denial — an undeclared control wearing a declared one's badge. Rejecting only orphans would let a catalog entry claim a control that does not exist. Both directions or neither.
Criterion (d): the command vocabulary must be a compile-time literal.
A ~w sigil or a plain list of string literals is a literal. A list built with
++, a comprehension, an Enum call, or string interpolation is not — that is
a runtime-constructed vocabulary, which is the plugin-catalog road under a
different name. A source with no @commands attribute is clean; not every file
declares a vocabulary.
Asserts every allowlist entry is still emitted by at least one of its declared sites. An entry whose string has been fixed or deleted is rot: it makes the allowlist look larger than the real debt and quietly widens the gate.
Asserts every statically extractable native denial reason is either in the
closed Crosswake.Shell.Denial vocabulary or on the eight-entry seeded
allowlist. A NINTH out-of-vocabulary string is a violation.
Criterion (d): native command enum parity, asserted in BOTH directions.
A GAP is an Elixir command absent from the native enum. An ORPHAN is a native enum case with no Elixir command, excluding the enumerated outbound-only pushes. Checking one direction only would let a native shell ship a command the server has never heard of — which is the same hole as a dynamic registration seam, opened from the other end.
An unlocatable enum block is a violation, never a vacuous pass.
Criterion (d): no atom minting. String.to_atom/1 and List.to_atom/1 grow the
atom table from wire input. String.to_existing_atom/1 is on the frozen
allowlist — it cannot mint, only resolve.
Criterion (d): no dynamic-registration seam.
Any def/defp/defmacro whose name starts with register_ is
a violation. Prefix match, not substring: registry_lookup/1 is fine.
Criterion (c): zero external SDK. An allowlist walk over every
alias/import/require/use declaration; anything rooted outside
[:Crosswake, :Phoenix, :Logger, :Jason, :Kernel] is an external SDK reaching into the
bounded bridge.
Criterion (d): no runtime function application. apply/2, apply/3, and
Kernel.apply/3 all let a command name become a call target.
Criterion (b), in the negative only: no streaming or back-pressure seam
([:Stream, :GenStage, :Flow, :Broadway]) in the bridge tree.
This proves no streaming seam EXISTS. It does not prove nobody calls a control in a loop — see the moduledoc's honest labelling.
Runs every source-level mechanical sub-assertion over source_string and
returns the COMPLETE SET of violations.
A source violating five criteria reports all five. Short-circuiting on the first would make a five-violation control look like a one-violation control, and the fix-one-rerun loop is exactly how a structural gate degrades into a nuisance.
Returns :ok, or {:violation, [{criterion_atom, node_or_detail}]}. An empty
or trivially small source is clean. An UNPARSEABLE source is a violation, not
a pass — the guard fails closed.
@spec closed_vocabulary() :: [String.t()]
The closed denial vocabulary as wire strings.
Extracts the wire values from a native BridgeCommand enum block.
Returns {:ok, wire_values} or :error. :error when the enum block cannot
be located at all — carrying forward Phase 134's guard that a job not found is
a FAILURE, not a pass. An extractor that silently returns [] for a renamed
enum turns the parity check green at exactly the moment it should be red.
Extracts denial reason string literals from native (Swift/Kotlin) source.
Recognises three emission shapes, each anchored on a real call site rather than a bare regex over every literal in the file:
- The FIRST reason-shaped literal inside a balanced
deny(/Denied(/unavailable(call — covers both the Swift labelled-argument form (reason: "x") and the Kotlin positional form. - Every reason-shaped literal in a
let/val/var reason =assignment expression — covers ternary-assigned reasons. - Nothing at all when the reason argument is a VARIABLE. That is not a miss; it is the unbounded host-supplied seam, which is not statically bounded and is carried by SEED-008 (see the moduledoc's non-mechanical exclusion).
"Reason-shaped" means lowercase with at least one underscore — see the moduledoc's known extractor limitation.
The two native command-enum sources checked for bidirectional parity.
The native sources whose emitted denial reason strings are checked against the closed vocabulary. Enumerated, not globbed: a source that has moved must be noticed, not silently skipped.
@spec out_of_vocabulary_denial_allowlist() :: [map()]
The eight enumerated out-of-vocabulary native denial reason strings, each with its shipping sites, an individual justification, and the SEED-008 id (D-16, resolved as option-b with amendment).
A NINTH string turns check_native_denial_reasons/1 red. Padding this list is
the failure mode the individual justifications exist to make visible: adding an
entry means writing down, in review, why the closed vocabulary could not answer.
@spec outbound_only_native_commands() :: [String.t()]
Native enum cases exempt from orphan detection because they are outbound server -> shell pushes with no inbound bounded-bridge request seam.
The shipped command -> capability family map, read from Crosswake.Bridge.Registry.