Skip to content

internal-api, trigger-queue, store: bind the substrate value instead of writing it in SQL - #383

Merged
ac-pplx merged 1 commit into
mainfrom
ac/substrate-kind-sql-internal-api
Oct 15, 2026
Merged

ac-pplx merged 1 commit into
mainfrom
ac/substrate-kind-sql-internal-api

Conversation

@ac-pplx

@ac-pplx ac-pplx commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Based on main, now that #373 has merged (fc47b31). This PR is one commit.

What merging does

Control-plane Postgres sandboxes.managed_by and nodes.managed_by hold the substrate that owns a row, 'k8s' or 'space'. #373 binds that value in the cluster controller. This PR does the same in the internal API, the trigger queue and the sandbox store. They scoped on the value with raw string literals in 11 production SQL statements and one Rust comparison. Their production code now binds SubstrateKind as a parameter (managed_by = $n with .bind(SubstrateKind::Kubernetes) / ::Space) and matches the parsed variant. Each statement stays syntactically whole. This is phase 2, PR 2 of containing the Direct/Kubernetes substrate split.

There are no schema, wire format, metric or configuration changes, and no behavior changes. SubstrateKind encodes as the column's text, so a bound SubstrateKind::Kubernetes matches exactly the rows 'k8s' did.

This revision replaces the previous one, which spliced the now-removed sql_substrate_*! macros into sql!(concat!(…)).

Rebase history. The commit was written on #373's 14e5b7b. It was rebased cleanly onto #373's bbe7ae1, which changed only tests, docs and comments. After #373 merged, it was rebased onto main (fc47b31), dropping #373's commit. That rebase had one conflict, in crates/libs/internal/api/server/src/tests.rs: main (#435) added mod reconcile_claim_origins; on the line where this PR adds mod substrate_scope;. Both declarations are kept, and nothing else in the file changed. git range-diff shows the commit is otherwise identical. I re-checked every converted statement on the new base. main's changes in these crates (adapter.rs, metadata.rs, a new test module) add no substrate literal. The only 'k8s'/'space' left in production code in scope is the routable_execution_or_non_k8s fragment (see below).

internal-api (crates/libs/internal/api/server/src/)

Each new placeholder is numbered after the statement's existing parameters, and its .bind comes last.

# File Statement Before → after
1 lib.rs fetch_nodes (Direct node listing) n.managed_by = 'space' → $3 = Space
2 lib.rs resolve_origin_node_id (Kubernetes node-name lookup) n.managed_by = 'k8s' → $3 = Kubernetes
3 lib.rs bootstrap_k8s_origin_node INSERT … ON CONFLICT VALUES (…, 'k8s', …) and WHERE nodes.managed_by = 'k8s' → one $4 = Kubernetes for both
4 lib.rs registered_origin_node n.managed_by IN ('space', 'k8s') → IN ($3, $4) = Space, Kubernetes
5 node_inventory.rs list_kubernetes n.managed_by = 'k8s' → $5 = Kubernetes
6 published_state.rs publish_sandbox_states: delete reported-absent stopped Direct sandboxes s.managed_by = 'space' → $3 = Space
7 published_state.rs publish_sandbox_states: local suspend handoff observation s.managed_by = 'k8s' → $5 = Kubernetes
8 published_state.rs project_node_published_failed_sandboxes (#337) s.managed_by = 'k8s' → $3 = Kubernetes. The rest of #337's statement, including FOR NO KEY UPDATE OF s, is unchanged.
9 status.rs prune_absent_node_sandboxes: marked_deleted CTE s.managed_by = 'space' → $4 = Space
10 template_releases_store.rs node_scope_for_address managed_by IN ('space', 'k8s') → IN ($2, $3) = Space, Kubernetes

Trigger queue (crates/libs/cluster/trigger-queue/src/lib.rs)

11 fetch_trigger_targets n.managed_by != 'space' → $4 = Space

Sandbox store (crates/libs/sandbox/store/src/lib.rs)

  • AutoLifecycleExecution::proves_no_execution changes from self.managed_by == "k8s" to matches!(self.managed_by.parse::<SubstrateKind>(), Ok(SubstrateKind::Kubernetes)) (Share the 4 MiB message cap and state native write limits in the API docs #12 below).
  • managed_by stays a String field. AutoLifecycleExecution is a public FromRow row type that the api crate reads, so typing the field changes that crate's interface, which is outside this PR. (sandboxes_managed_by_check already limits the column to 'space'/'k8s', so the database can't return an unrecognized value.)
  • AutoLifecycleExecution.managed_by and SandboxRecord.managed_by (both single-row reads) remain text, and the conventions doc lists them as debt.

common-svc (crates/libs/common/svc/src/sql/sandbox_routability.rs): not converted, documented as debt

  • routable_execution_or_non_k8s(alias) returns a fragment that callers splice into their own statements, each with its own parameter numbering. Binding its 'k8s' means each caller passes a placeholder, as cluster-controller: bind the substrate value instead of writing it in SQL #373 did for row_state_guards.
  • One caller is the api crate's wake_completion.rs, which this PR may not edit. The others are the cluster controller's transient sweeper and internal-api's restored-state convergence.
  • So the literal stays. The function's doc comment now says why. docs/rust/space-conventions.md (from cluster-controller: bind the substrate value instead of writing it in SQL #373) already names this fragment as remaining debt, and this PR updates that paragraph: internal-api and the trigger queue now follow the convention apart from the fragment, alongside the cluster controller. The paragraph also lists the sandbox store's two text managed_by fields as debt. The space-rust-development skill's sentence ("Remaining raw-literal sites, including the shared routable_execution_or_non_k8s fragment, are existing debt") is still accurate, so the skill and the plugin version don't change.

No change needed or left alone

  • crates/libs/cluster/pool/: its managed_by is the warm-pool config owner ('manual'/'autoscaler'), not a substrate.
  • crates/libs/sandbox/portability/: its Kubernetes finalization helpers take no substrate value.
  • data_plane_sessions.rs and route_identity.rs read managed_by as data and hold no literal.
  • The trigger queue's typed target.managed_by == NodeManagedBy::Kubernetes already compares variants.
  • Test modules and #[cfg(test)] support modules: fixtures keep writing literal managed_by values into the rows they set up. This includes internal-api: characterize node publication's lifecycle writers (tests only) #324's published_state_tests/lifecycle_writers.rs and internal-api: recheck the failed projection's guards under the row lock #337's published_state_lifecycle_race_tests.rs, both unchanged.
  • SQL text in // / /// comments.

Evidence

A Postgres-backed test per converted statement

Each test seeds a row the statement would act on, then shows the 'k8s' row and an otherwise identical 'space' row on the expected sides of the predicate. For the Direct-scoped statements (#1, #6, #9, #11) the 'space' row is the included one.

New tests:

  • tests::substrate_scope::node_lookups_select_only_their_substrate (src/tests/substrate_scope.rs). It gives each node both a node_direct and a node_k8s row. The existing listing test seeds only the substrate's own registration, so its joins did the excluding and managed_by never decided anything.
  • tests::substrate_scope::bootstrap_k8s_origin_node_writes_and_updates_only_kubernetes_rows. A fresh bootstrap writes 'k8s'. An existing 'k8s' row at the derived id is refreshed. A 'space' row there is refused with FailedPrecondition and left unchanged. No test reached the ON CONFLICT branch before.
  • publishing_local_suspend_handoff_records_only_kubernetes_rows (tests/published_state.rs). No internal-api test covered this statement; the api crate's Kubernetes tests seed only 'k8s'.
  • sandbox-store tests::proves_no_execution_only_for_kubernetes_rows, a unit table: "k8s" → true; "space", "kubernetes" and "" → false.

Extended tests, with an otherwise identical 'k8s' row added beside the existing 'space' one:

  • status::status_tests::complete_inventory_marks_missing_space_sandbox_deleted. The 'k8s' row stays started / on_node.
  • publishing_absent_stopped_space_sandbox_deletes_central_row. Both rows get the same bound pool-slot receipt. The 'k8s' row stays stopped and its receipt stays bound. The 'space' row and its receipt are gone.

Predicate-removal check. For each statement I replaced the converted predicate with (… OR TRUE), ran the owning package's whole suite (--all-targets --all-features --run-ignored all --retries 0 --no-fail-fast), and reverted.

# Statement Test that fails (after) Predicate dropped: before → after
1 fetch_nodes substrate_scope::node_lookups_select_only_their_substrate passed → fails
2 resolve_origin_node_id same passed → fails
3 bootstrap ON CONFLICT guard substrate_scope::bootstrap_k8s_origin_node_writes_and_updates_only_kubernetes_rows (the 'space' row is adopted as 'k8s') passed → fails
4 registered_origin_node IN — (CHECK constraint) OR TRUE: passed → passed. IN ($3, $3) (Space only): 65 → 66 tests fail. IN ($4, $4) (Kubernetes only): 2 → 3 fail (existing base_snapshot_policy_state_prefers_requesting_node_inventory, template_desired_state_projects_snapshot_pool_shape, plus node_lookups_select_only_their_substrate)
5 list_kubernetes substrate_scope::node_lookups_select_only_their_substrate passed → fails
6 absent stopped Direct delete publishing_absent_stopped_space_sandbox_deletes_central_row passed → fails
7 local suspend handoff observation publishing_local_suspend_handoff_records_only_kubernetes_rows passed → fails
8 project_node_published_failed_sandboxes existing lifecycle_writers::failed_report_moves_every_live_k8s_lifecycle_to_failed (#324) and publishing_direct_failed_sandbox_does_not_mark_central_lifecycle_failed fails → fails
9 prune_absent_node_sandboxes marked_deleted status_tests::complete_inventory_marks_missing_space_sandbox_deleted passed → fails
10 node_scope_for_address IN — (CHECK constraint) OR TRUE: passed → passed. IN ($2, $2) (Space only): 11 → 12 fail. IN ($3, $3) (Kubernetes only): passed → fails (node_lookups_select_only_their_substrate; every earlier scope test used 'k8s' nodes only)
11 fetch_trigger_targets existing tests::fetch_targets_requires_live_direct_row_for_space_nodes fails → fails
12 proves_no_execution sandbox-store tests::proves_no_execution_only_for_kubernetes_rows; also the api crate's existing routes::e2b::tests::timeout_route_distinguishes_dormant_intent_from_unreachable_execution store package: passed → fails. Api test: fails with the mutation, passes without it.

Before this PR's tests, 3 of the 10 discriminable statements (#8, #11, #12 via the api crate) failed a test when their predicate was dropped. After it, all 10 do. #4 and #10 can't be told apart by OR TRUE, but dropping either bound value fails a test.

Generic plans: prune_absent_node_sandboxes

Why this statement. It runs in every node publication that carries a complete sandbox census (publish.rs), for every node. Its marked_deleted CTE scans the publishing node's whole resident set, not rows keyed by reported ids. It is also the only converted sandboxes scan whose literal is the rare value ('space') on a column with an index (idx_sandboxes_managed_by). So it's the statement where losing the literal's statistics could matter most. The other converted sandboxes statements look rows up by reported id or primary key.

Setup. A scratch database migrated from the test template on the repository's test image (PostgreSQL 16.15), with synthetic, deliberately skewed data: 400 nodes (10% 'space'), 200,000 sandboxes (5% 'space', mixed lifecycles), an active execution per started 'k8s' sandbox, then ANALYZE. I ran EXPLAIN (GENERIC_PLAN) on the statement text from #373's head (literal) and from this commit (bound). The statement is the same on main. Only the marked_deleted CTE differs; the other CTEs' plans are identical.

-- before (literal 'space')
CTE marked_deleted
  ->  Update on sandboxes s  (cost=121.47..232.99 rows=26)
        ->  Bitmap Heap Scan on sandboxes s
              Recheck Cond: ((active_node_id = $1) AND (managed_by = 'space'::text))
              Filter: ((lifecycle <> ALL ('{deleted,deleting}'::text[])) AND (id <> ALL ($2)))
              ->  BitmapAnd  (rows=29)
                    ->  Bitmap Index Scan on idx_sandboxes_active_node  (rows=576)
                          Index Cond: (active_node_id = $1)
                    ->  Bitmap Index Scan on idx_sandboxes_managed_by  (rows=10173)
                          Index Cond: (managed_by = 'space'::text)

-- after (bound $4)
CTE marked_deleted
  ->  Update on sandboxes s  (cost=8.68..1781.00 rows=253)
        ->  Bitmap Heap Scan on sandboxes s
              Recheck Cond: (active_node_id = $1)
              Filter: ((lifecycle <> ALL ('{deleted,deleting}'::text[])) AND (managed_by = $4) AND (id <> ALL ($2)))
              ->  Bitmap Index Scan on idx_sandboxes_active_node  (rows=576)
                    Index Cond: (active_node_id = $1)

The generic plan does differ. Without the value, Postgres estimates managed_by = $4 at 1/ndistinct (50%) instead of the literal's 5%. It drops the BitmapAnd with idx_sandboxes_managed_by and filters the node's rows instead. Both plans start from the node's idx_sandboxes_active_node entries; neither scans the table. Custom plans are identical to the literal plan by construction. The question is what plan_cache_mode = auto (the default) does with the statement over repeated executions on one connection.

That first database had no sandbox_observed_states or facet rows. So the other four CTEs (the removed delete of the node's observed states and the facet deletes) cost almost nothing, and the marked_deleted difference dominated. On that database auto kept building custom plans for the bound statement (generic estimate 1781 vs about 233 for the custom plans). It used the cached generic plan for the literal one.

Plan caching with realistic rows (review round 1). I rebuilt the scratch database. It has 400 nodes (10% 'space') and 200,000 sandboxes (5% 'space'). 125,000 of them are placed; most nodes hold about 90–150, a few about 1,000, and three about 5,100. Every placed sandbox has a sandbox_observed_states row and a network-policy facet status row. A third have a facet condition and half a snapshot inventory row. A further 60 'k8s' nodes hold 0–9 sandboxes each.

Each sequence ran on one connection: PREPARE once, then 30 × EXPLAIN (ANALYZE, TIMING OFF) EXECUTE. Each execution passes a complete census: $2 lists the node's present sandboxes, so the data stays stable. generic_plans / custom_plans are read from pg_prepared_statements at the end. "Before" is the statement text on main (literal); "after" is this commit (bound).

Sequence (30 executions, one connection) Literal: generic / custom plans Literal: planning / execution, ms (sum; median) Bound: generic / custom plans Bound: planning / execution, ms (sum; median)
Mixed: 24 'k8s' and 6 Direct nodes, 91–1,034 sandboxes, shuffled 24 / 6 13.6; 0.111 / 100.3; 2.89 24 / 6 13.4; 0.103 / 91.5; 2.67
Large first: three 'k8s' nodes with about 5,100 and Direct nodes with 853 and 126, then 25 'k8s' nodes with 99–146 25 / 5 24.9; 0.102 / 92.0; 2.75 25 / 5 25.3; 0.103 / 90.2; 2.66
Small nodes: 30 'k8s' nodes with 0–9 sandboxes 0 / 30 executions 6–30: 6.1; 0.238 / 58.8; 2.34 0 / 30 executions 6–30: 6.0; 0.229 / 58.5; 2.32

Estimated costs explain the table. The whole statement's generic plan costs 3311.2 with the literal and 3358.2 bound, a premium of 47 units (1.4%). Custom plans cost 2574 for a 'k8s' node with 107 sandboxes, 5046 for a Direct node with 853, and 8143 for a 'k8s' node with 5,155. With realistic rows, the CTEs this PR doesn't touch dominate both numbers.

  • (a) Does every execution now replan? No, in every measured regime. Literal and bound made the same choice in all three sequences. On ordinary and large nodes, both switched to the cached generic plan after the 5th execution. On very small nodes, both kept custom plans, so the literal statement already replanned there: about 0.23 ms planning against about 2.3 ms execution, the same for both. Planning cost and execution time are equal within noise. The only place I saw the two diverge was the first database, which had empty observed-state and facet tables. On a database with a narrower margin between the custom-plan average and the two generic costs, the bound statement could keep replanning where the literal latched. The margin here was 47 units on about 3,300.
  • (b) Can the generic plan latch per connection? Yes, for both statements, as auto decides from each connection's first five custom plans. Both "large first" sequences latched after their large-node executions. The bound statement's latched plan reads the node's resident rows in marked_deleted where the literal's used the BitmapAnd. I compared them with force_generic_plan and EXPLAIN (ANALYZE, BUFFERS):
Node marked_deleted buffers, literal → bound Whole statement buffers, literal → bound Execution, literal → bound
'k8s', 107 sandboxes 11 → 9 1094 → 1092 3.5 → 2.5 ms
Direct, 853 sandboxes 75 → 66 1168 → 1159 5.0 → 4.7 ms
'k8s', 5,155 sandboxes 15 → 296 1168 → 1449 41.5 → 42.2 ms

On the largest node the latched bound plan reads 281 more buffers (+24% for the statement). Execution grows 1.8%, because the generic plan's other CTEs cost about 40 ms for that node either way. That cost predates this PR. The extra read is bounded by the node's resident set through idx_sandboxes_active_node.

Trade-off. On these measurements the planning cost and plan choice are unchanged, and a latched generic plan costs at most a bounded per-node read (+281 buffers at 5,155 sandboxes). I don't consider that material, so I kept the binding. If the owner prefers, this one statement could keep its fixed 'space' as a documented literal exception. The conventions doc would then name it, and the predicate-removal test (#9) would apply unchanged.

These are synthetic numbers on one PostgreSQL 16.15 instance. A production database's statistics and node sizes decide the actual margins.

For comparison, the local suspend handoff observation (#7, also run on every publication) has an identical generic plan. The only difference is its filter line, (managed_by = 'k8s'::text) vs (managed_by = $5), under the same id-driven join.

There are no partial indexes on managed_by, so no generic plan can lose one.

Characterization tests pass unchanged

#324's 9 published_state_tests::lifecycle_writers tests and #337's published_state_lifecycle_race_tests::failed_projection_decides_on_the_row_committed_while_it_waits pass in every run below. Their files are unchanged.

Decisions made unattended

Decision Rationale
Leave routable_execution_or_non_k8s's literal Binding it changes the parameter numbering of every statement that splices it, including the api crate's wake_completion.rs, which is outside this PR's scope. The task allows leaving it as documented debt. The doc comment and the conventions paragraph say so.
Bind the constant variant everywhere, not substrate.managed_by() Every converted statement scopes to a fixed substrate regardless of the running one: Direct listings, Kubernetes registration, Direct census pruning. None scopes to "the substrate I run on".
One $4 for the bootstrap INSERT's value and its ON CONFLICT guard The row it writes and the row it may overwrite are both Kubernetes registrations. One parameter keeps them the same value by construction.
Keep the two IN (…) filters, bound The CHECK constraint makes them always true, and deleting them would change the statements' shape. Whether to drop a dead filter is a separate, reviewable change. The new scope test pins that both substrates stay included.
proves_no_execution parses a String instead of typing the field AutoLifecycleExecution is a public FromRow row type the api crate reads; typing its field changes that crate's interface, outside this PR's scope. The CHECK constraint means the database can't return an unrecognized value, so this isn't about robustness. The field and SandboxRecord.managed_by are listed as debt.
Keep the binding in prune_absent_node_sandboxes Measured plan-cache behavior is the same as with the literal (see Generic plans). The latched generic plan's extra read is bounded and small next to the statement's other CTEs. The literal exception is noted as an option for the owner.
New node-registry tests in src/tests/substrate_scope.rs src/tests.rs is already large. A sibling module beside prefetch_catalog and publication reuses its cluster_fixture.
Update one paragraph of docs/rust/space-conventions.md, not the skill The paragraph named only the cluster controller as following the convention. The skill's sentence stays true after this PR, so the plugin version doesn't change.
Disk: Docker-mode Rust build abandoned See Validation. The incremental default ran once and kept everything within the disks.

Review round 1 → changes

Finding Change
MINOR: the generic-plan risk statement for prune_absent_node_sandboxes omits per-execution replanning (a) and per-connection latching (b) Measured on one connection with realistic observed-state and facet rows, three sequences, literal vs bound: generic_plans/custom_plans, planning vs execution time, costs and buffers. The "Generic plans" section now states (a) and (b) with those numbers and the trade-off. The binding is kept, and the literal exception is offered as an option.
NIT: the sandbox store's reason for keeping String cited unrecognized values The doc comment, the store bullet, the decisions row, the conventions paragraph and the commit message now give the real reason (a public FromRow type the api crate reads). The conventions paragraph lists SandboxRecord.managed_by and AutoLifecycleExecution.managed_by as remaining debt instead of saying the store follows the convention.
NIT: the absent-delete test's pair differed by the pool-slot receipt The 'k8s' row gets the same bound receipt, and the test asserts it stays bound. The predicate-removal mutation (#6) still fails the test.

Locator note

Converted statements' text changes (a placeholder replaces the literal). Their /*module_path:line*/ locators, sqlx statement-cache keys and DBM query signatures change accordingly. Statements below an added .bind line move to new locators. Module paths don't change.

Overlaps with open PRs

#283 adds two raw literals in internal-api: n.managed_by IN ('space', 'k8s') in a new origin-node lookup, and owner.managed_by <> 'k8s' in a backup-admission statement. Whichever of #283 and this PR lands second should bind them. #165 no longer adds a substrate literal.

Validation

Heads: 422d26a (this commit on #373's 14e5b7b) → 5e846a2 (on #373's bbe7ae1) → 180d2a1 (on main fc47b31) → 0135f69 (review round 1: a doc comment, docs, one test and evidence; this head). The code checks were rerun on each head. Kind ran on 422d26a and wasn't rerun for round 1, which changed a doc comment, docs, one test and evidence. After that, this commit's production code is unchanged, but main has since merged other production changes (#450, #237, #435), so the Kind results describe this commit on #373's base, not on today's main.

Check Result
cargo fmt --all -- --check (and ci/scripts/run-rust-checks.sh fmt: env contract, Rust error policy) pass (422d26a, 5e846a2, 180d2a1, 0135f69)
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings pass (422d26a, 5e846a2, 180d2a1, 0135f69)
pre-commit (Gitleaks) at commit, and pre-commit run gitleaks --from-ref origin/main --to-ref HEAD on 180d2a1 and 0135f69 pass
space-public-review on the staged change and this text no findings in the reviewed scope (synthetic names; RFC 5737 TEST-NET addresses)
cargo nextest run --locked -p pplx-space-internal-api-server -p pplx-space-cluster-trigger-queue -p pplx-space-sandbox-store -p pplx-space-common-svc --all-targets --all-features --run-ignored all --retries 0, SPACE_CI_REQUIRE_TEST_DEPS=1 0135f69: 628/628 passed, 0 skipped (once, per the round-1 instructions). 180d2a1: 628/628 in each of 3 runs. That's main's new reconcile_claim_origins test plus the 627 below. 422d26a: 627/627 in each of 3 runs. 5e846a2: 627/627 once. Every run includes the characterization tests above and the 4 new tests.
cargo test --locked --doc on the four packages; RUSTDOCFLAGS=-D warnings cargo doc -p pplx-space-sandbox-store on 0135f69 (the new intra-doc link) pass
scripts/source_guards/*.py pass, except thin_service_mains.py, which flags only crates/apps/*/main.rs files this PR doesn't touch: PRE-EXISTING
scripts/check_no_inline_rust_test_modules.py flags only crates/libs/observability/src/{otlp_endpoint,selection}.rs, untouched: PRE-EXISTING
cargo arch exit 0 on 180d2a1. 69 advisory and 0 required findings, the same set as main. Growth: internal/api/server/src/lib.rs goes from 1298 to 1303 code lines and published_state.rs from 3762 to 3765 (the added .bind lines). Both are advisory listed_no_growth caps (1222, 3750) the files already exceeded.
Predicate-removal check see Evidence
Kind: disk (before bring-up) Docker data device 46 GB free, home volume 38 GB free; no cleanup needed.
Kind: first bring-up attempt ENVIRONMENTAL. With SPACE_KIND_RUST_ARTIFACTS=docker (chosen to keep the build off the home volume), several runtime images each ran a BuildKit cook at once. The Docker data device filled (49 GB of 49 GB) and the build failed. I tore the cluster down, then ran cleanup step 1: docker builder prune -af freed 36.43 GB and docker image prune -f freed 0 B. I reran with the default incremental mode, which serializes one cargo build.
Kind: fresh cluster, build provenance No cluster existed. smoke-kind-e2e.sh (BOOTSTRAP_TIMEOUT=1800, DATA_PLANE_URL, as CI sets) created space-local and started Tilt from this checkout. The api, internal-api and cluster-controller services log version: 422d26a51afc. Their deployed binaries are byte-identical (sha256) to the artifact image's, before and after the suites. The internal-api binary contains the bound n.managed_by IN ($3, $4), VALUES ($1, $2::INET, $3, $4, 'registered', NOW()) and #337's FOR NO KEY UPDATE OF s, and no AND s.managed_by = 'k8s', AND s.managed_by = 'space' or IN ('space', 'k8s'). The cluster controller contains the trigger queue's n.managed_by != $4 and not != 'space'.
smoke-kind-e2e.sh (start) pass (runtime-manager checks and 7/7)
smoke-kind-typed.sh all (unsharded, CI's env: admin token file, cold-receipts dir, host dm targets loaded) ECW 77/78, native 23/23, none quarantined. The non-pass is below.
smoke-kind-e2e.sh (end) pass (runtime-manager checks and 7/7)
Cleanup teardown.sh on the task-owned cluster; scratch checkouts and the scratch Postgres container removed

Typed-suite non-pass, rerun once:

  • local.rate_limiters_enforce_and_fail_open: FLAKY. On the first run, the preflight ECW create timed out after 90 s without a response, right after the scenario rolled the public API to enable its limiters. The scenario's own rerun command passed every phase: cluster admission, per-client create and connect concurrency, fail-open with Redis unreachable, and the env restore. This PR doesn't touch the API's limiters. The same scenario's preflight timed out in cluster-controller: bind the substrate value instead of writing it in SQL #373's validation too.

🤖 Generated with Claude Code

@ac-pplx
ac-pplx force-pushed the ac/substrate-kind-sql-internal-api branch from 1347b08 to ff497ef Compare October 13, 2026 17:34
@ac-pplx ac-pplx changed the title internal-api, pool, store: name the substrate value once in Rust and SQL internal-api, trigger-queue, store, common-svc: name the substrate value once in Rust and SQL Oct 13, 2026
@ac-pplx
ac-pplx added this pull request to stack #397 October 13, 2026 19:29
@ac-pplx
ac-pplx force-pushed the ac/substrate-kind-sql-internal-api branch from ff497ef to 422d26a Compare October 14, 2026 17:40
@ac-pplx
ac-pplx force-pushed the ac/substrate-kind-sql-internal-api branch from 422d26a to 5e846a2 Compare October 14, 2026 18:29
Base automatically changed from ac/substrate-kind-sql-cluster-controller to main October 14, 2026 19:13
@ac-pplx
ac-pplx force-pushed the ac/substrate-kind-sql-internal-api branch from 5e846a2 to 180d2a1 Compare October 14, 2026 19:22
@ac-pplx ac-pplx changed the title internal-api, trigger-queue, store, common-svc: name the substrate value once in Rust and SQL internal-api, trigger-queue, store: bind the substrate value instead of writing it in SQL Oct 14, 2026
…of writing it in SQL

Control-plane Postgres `sandboxes.managed_by` and `nodes.managed_by` hold
the substrate that owns a row, 'k8s' or 'space'. The internal API, the
trigger queue and the sandbox store scoped on that value with raw string
literals in 11 production SQL statements and one Rust comparison. Their
production code now binds `SubstrateKind` as a parameter and matches the
parsed variant, as the cluster controller does, so each statement stays
syntactically whole.

internal-api:
- `managed_by = 'k8s'` / `'space'` becomes `managed_by = $n`, bound to
  `SubstrateKind::Kubernetes` or `::Space` and numbered after the
  statement's existing parameters: the Direct and Kubernetes node
  listings, the Kubernetes node-name lookup, the absent stopped Direct
  sandbox delete, the local suspend handoff observation, the failed-report
  projection (its `FOR NO KEY UPDATE OF s` claim is unchanged) and the
  census prune.
- Kubernetes node bootstrap binds one `$4` for both the inserted
  `managed_by` and the ON CONFLICT guard on the existing row.
- The two `managed_by IN ('space', 'k8s')` filters bind both variants.
  They are kept, though the column's CHECK constraint already allows only
  those values.

trigger queue: `fetch_trigger_targets` binds `SubstrateKind::Space` for its
`n.managed_by != $4` scope.

sandbox store: `AutoLifecycleExecution::proves_no_execution` parses
`managed_by` and matches `SubstrateKind::Kubernetes`. The field stays the
column's text, because the struct is a public `FromRow` row type the api
crate reads; it and `SandboxRecord.managed_by` remain listed as debt.

common-svc: `routable_execution_or_non_k8s` keeps its `'k8s'` literal and
says why. It is spliced into statements with their own parameter
numbering, one of them in the api crate, so binding it changes every
caller.

docs/rust/space-conventions.md now names internal-api and the trigger
queue, with the cluster controller, as following the convention apart from
that fragment, and lists the store's two text `managed_by` fields as debt.

Tests: each converted statement has a Postgres-backed test with a 'k8s'
row and an otherwise identical 'space' row on the expected sides of the
predicate. New: node lookups and bootstrap (`tests/substrate_scope.rs`),
the local suspend handoff, and a store unit test that includes an
unparseable value. The census-prune and absent-delete tests gain an
otherwise identical 'k8s' row (including the absent-delete test's pool-slot
receipt, which stays bound). Dropping any one converted predicate fails a test, except the two IN
filters, which the CHECK constraint makes always true; dropping a value
from either fails one.

No schema, wire format, metric or configuration changes. Statement text
changes, so the converted statements' `/*module_path:line*/` locators,
sqlx statement-cache keys and DBM query signatures change.
@ac-pplx
ac-pplx force-pushed the ac/substrate-kind-sql-internal-api branch from 180d2a1 to 0135f69 Compare October 14, 2026 19:43
@ac-pplx
ac-pplx marked this pull request as ready for review October 14, 2026 19:59

@nickdujay-pplx nickdujay-pplx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed commit 0135f69. No blocking findings: the 11 SQL conversions preserve parameter ordering and substrate scope, and the Rust comparison preserves behavior. The added tests cover wrong-substrate exclusion, bootstrap conflicts, and cleanup preserving Kubernetes rows. Locally, 621 tests passed with PostgreSQL dependencies required; CI is green on this commit. The documented generic-plan trade-off for inventory cleanup is nonblocking; the synthetic benchmark does not establish unchanged production performance.

@ac-pplx
ac-pplx added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit 310f4e6 Oct 15, 2026
20 checks passed
@ac-pplx
ac-pplx deleted the ac/substrate-kind-sql-internal-api branch October 15, 2026 14:09
ac-pplx added a commit that referenced this pull request Oct 16, 2026
Control-plane Postgres `sandboxes.managed_by` and `nodes.managed_by` hold the
substrate that owns a row, 'k8s' or 'space'. After #373, #383 and #506 the
remaining production literals were the shared routability fragment, the
Kubernetes adapter's builder lookup and the Direct route-identity match. They
now bind `SubstrateKind` or match its parsed variant, and a new archcheck
instance keeps new literals out of production code. No schema, wire format,
metric or configuration changes, and no behavior change.

- common-svc: `routable_execution_or_non_k8s(alias, kubernetes_placeholder)`
  writes `{alias}.managed_by <> {placeholder}` instead of `<> 'k8s'`. Each
  caller binds `SubstrateKind::Kubernetes` at the placeholder it numbers
  after its own parameters: the transient sweeper's aged-transient list and
  restoring converge (`$5`), internal-api's restored-state convergence (`$4`)
  and the api's wake-completion CAS (`$2`).
- api Kubernetes adapter: `builder_client_for_node` binds
  `n.managed_by = $2`.
- api Direct: the route identity's `substrate_kind` text is parsed as
  `SubstrateKind` in `route_identity_substrate_kind`, which maps exactly
  'space' and 'k8s' as before and rejects any other text with the same
  error.
- ARCHITECTURE.toml: `pattern_ratchet.substrate_literals` matches SQL
  comparisons of `managed_by` with a 'k8s'/'space' literal (=, <>, !=,
  IS [NOT] DISTINCT FROM, [NOT] IN; any case and alias) and Rust comparisons
  or match arms on "k8s"/"space". It allows test-support modules and
  fixtures that archcheck's filename-based test rule counts as production.
  On the parent commit it matches exactly the four converted lines; on this
  commit it matches nothing, so it has no baseline table and every behaviour
  is required.
- Tests: Postgres-backed twins for each converted statement. A new
  Kubernetes builder-lookup test, a unit table for the route-identity
  labels, and an otherwise identical 'space' row added to the sweeper's
  converge, internal-api's restored convergence and the wake-completion
  redrive tests. Test callers of the sweeper's shared builders bind the
  extra parameter.
- Docs: `docs/rust/space-conventions.md`, the substrate AGENTS.md and the
  space-rust-development skill say every crate's production SQL binds the
  value; Rust parses it and matches the variant (or calls the `SubstrateKind`
  method that names the question) and never compares the label string; and
  the archcheck rule enforces it. Plugin version 0.2.33 -> 0.2.34.

Converted statements' text changes, so their sqlx statement-cache keys and
query signatures change; statements below an added `.bind` move to new
`sql!` locators.
ac-pplx added a commit that referenced this pull request Oct 17, 2026
Control-plane Postgres `sandboxes.managed_by` and `nodes.managed_by` hold the
substrate that owns a row, 'k8s' or 'space'. After #373, #383 and #506 the
remaining production literals were the shared routability fragment, the
Kubernetes adapter's builder lookup and the Direct route-identity match, plus
three statements that landed on main since: internal-api's publisher-node
lookup and Direct's create-target claim and release. They now bind
`SubstrateKind` or match its parsed variant, and a new archcheck instance
keeps new literals out of production code. No schema, wire format, metric or
configuration changes, and no behavior change.

- common-svc: `routable_execution_or_non_k8s(alias, kubernetes_placeholder)`
  writes `{alias}.managed_by <> {placeholder}` instead of `<> 'k8s'`. Each
  caller binds `SubstrateKind::Kubernetes` at the placeholder it numbers
  after its own parameters: the transient sweeper's aged-transient list and
  restoring converge (`$5`), internal-api's restored-state convergence (`$4`)
  and the api's wake-completion CAS (`$2`).
- api Kubernetes adapter: `builder_client_for_node` binds
  `n.managed_by = $2`.
- internal-api: `publisher_node_for_identity` binds `n.managed_by = $3`
  (`Kubernetes`).
- api Direct: `claim_create_target` and `release_create_target` bind
  `managed_by = $3` (`Space`).
- api Direct: the route identity's `substrate_kind` text is parsed as
  `SubstrateKind` in `route_identity_substrate_kind`, which maps exactly
  'space' and 'k8s' as before and rejects any other text with the same
  error.
- ARCHITECTURE.toml: `pattern_ratchet.substrate_literals` matches SQL
  comparisons of `managed_by` with a 'k8s'/'space' literal (=, <>, !=,
  IS [NOT] DISTINCT FROM, [NOT] IN; any case and alias) and Rust comparisons
  or match arms on "k8s"/"space". It allows test-support modules and
  fixtures that archcheck's filename-based test rule counts as production.
  On the parent commit it matches exactly the seven converted lines; on
  this commit it matches nothing, so it has no baseline table and every
  behaviour is required.
- Tests: Postgres-backed twins for each converted statement. New
  Kubernetes builder-lookup and Direct create-target tests, a unit table for
  the route-identity labels, a relabelled-builder case in the tenant
  publication table, and an otherwise identical 'space' row added to the
  sweeper's converge, internal-api's restored convergence and the
  wake-completion redrive tests. Test callers of the sweeper's shared builders bind the
  extra parameter.
- Docs: `docs/rust/space-conventions.md`, the substrate AGENTS.md and the
  space-rust-development skill say every crate's production SQL binds the
  value; Rust parses it and matches the variant (or calls the `SubstrateKind`
  method that names the question) and never compares the label string; and
  the archcheck rule enforces it. Plugin version 0.2.33 -> 0.2.34.

Converted statements' text changes, so their sqlx statement-cache keys and
query signatures change; statements below an added `.bind` move to new
`sql!` locators.
ac-pplx added a commit that referenced this pull request Oct 17, 2026
Control-plane Postgres `sandboxes.managed_by` and `nodes.managed_by` hold the
substrate that owns a row, 'k8s' or 'space'. After #373, #383 and #506 the
remaining production literals were the shared routability fragment, the
Kubernetes adapter's builder lookup and the Direct route-identity match, plus
Direct's create-target claim and release, which landed on main since. They
now bind `SubstrateKind` or match its parsed variant, and a new archcheck
instance keeps new literals out of production code. No schema, wire format,
metric or configuration changes, and no behavior change.

- common-svc: `routable_execution_or_non_k8s(alias, kubernetes_placeholder)`
  writes `{alias}.managed_by <> {placeholder}` instead of `<> 'k8s'`. Each
  caller binds `SubstrateKind::Kubernetes` at the placeholder it numbers
  after its own parameters: the transient sweeper's aged-transient list and
  restoring converge (`$5`), internal-api's restored-state convergence (`$4`)
  and the api's wake-completion CAS (`$2`).
- api Kubernetes adapter: `builder_client_for_node` binds
  `n.managed_by = $2`.
- api Direct: `claim_create_target` and `release_create_target` bind
  `managed_by = $3` (`Space`).
- api Direct: the route identity's `substrate_kind` text is parsed as
  `SubstrateKind` in `route_identity_substrate_kind`, which maps exactly
  'space' and 'k8s' as before and rejects any other text with the same
  error.
- ARCHITECTURE.toml: `pattern_ratchet.substrate_literals` matches SQL
  comparisons of `managed_by` with a 'k8s'/'space' literal (=, <>, !=,
  IS [NOT] DISTINCT FROM, [NOT] IN; any case and alias) and Rust comparisons
  or match arms on "k8s"/"space". It allows test-support modules and
  fixtures that archcheck's filename-based test rule counts as production.
  On the parent commit it matches exactly the six converted lines; on
  this commit it matches nothing, so it has no baseline table and every
  behaviour is required.
- Tests: Postgres-backed twins for each converted statement. New
  Kubernetes builder-lookup and Direct create-target tests, a unit table for
  the route-identity labels, and an otherwise identical 'space' row added to
  the sweeper's converge, internal-api's restored convergence and the
  wake-completion redrive tests. Test callers of the sweeper's shared
  builders bind the extra parameter.
- Docs: `docs/rust/space-conventions.md`, the substrate AGENTS.md and the
  space-rust-development skill say every crate's production SQL binds the
  value; Rust parses it and matches the variant (or calls the `SubstrateKind`
  method that names the question) and never compares the label string; and
  the archcheck rule enforces it. Plugin version 0.2.33 -> 0.2.34.

Converted statements' text changes, so their sqlx statement-cache keys and
query signatures change; statements below an added `.bind` move to new
`sql!` locators.
ac-pplx added a commit that referenced this pull request Oct 17, 2026
Control-plane Postgres `sandboxes.managed_by` and `nodes.managed_by` hold the
substrate that owns a row, 'k8s' or 'space'. After #373, #383 and #506 the
remaining production literals were the shared routability fragment, the
Kubernetes adapter's builder lookup and the Direct route-identity match, plus
Direct's create-target claim and release, which landed on main since. They
now bind `SubstrateKind` or match its parsed variant, and a new archcheck
instance keeps new literals out of production code. No schema, wire format,
metric or configuration changes, and no behavior change.

- common-svc: `routable_execution_or_non_k8s(alias, kubernetes_placeholder)`
  writes `{alias}.managed_by <> {placeholder}` instead of `<> 'k8s'`. Each
  caller binds `SubstrateKind::Kubernetes` at the placeholder it numbers
  after its own parameters: the transient sweeper's aged-transient list and
  restoring converge (`$5`), internal-api's restored-state convergence (`$4`)
  and the api's wake-completion CAS (`$2`).
- api Kubernetes adapter: `builder_client_for_node` binds
  `n.managed_by = $2`.
- api Direct: `claim_create_target` and `release_create_target` bind
  `managed_by = $3` (`Space`).
- api Direct: the route identity's `substrate_kind` text is parsed as
  `SubstrateKind` in `route_identity_substrate_kind`, which maps exactly
  'space' and 'k8s' as before and rejects any other text with the same
  error.
- ARCHITECTURE.toml: `pattern_ratchet.substrate_literals` matches SQL
  comparisons of `managed_by` with a 'k8s'/'space' literal (=, <>, !=,
  IS [NOT] DISTINCT FROM, [NOT] IN; any case and alias) and Rust comparisons
  or match arms on "k8s"/"space". It allows test-support modules and
  fixtures that archcheck's filename-based test rule counts as production.
  On the parent commit it matches exactly the six converted lines; on
  this commit it matches nothing, so it has no baseline table and every
  behaviour is required.
- Tests: Postgres-backed twins for each converted statement. New
  Kubernetes builder-lookup and Direct create-target tests, a unit table for
  the route-identity labels, and an otherwise identical 'space' row added to
  the sweeper's converge, internal-api's restored convergence and the
  wake-completion redrive tests. Test callers of the sweeper's shared
  builders bind the extra parameter.
- Docs: `docs/rust/space-conventions.md`, the substrate AGENTS.md and the
  space-rust-development skill say every crate's production SQL binds the
  value; Rust parses it and matches the variant (or calls the `SubstrateKind`
  method that names the question) and never compares the label string; and
  the archcheck rule enforces it. Plugin version 0.2.33 -> 0.2.34.

Converted statements' text changes, so their sqlx statement-cache keys and
query signatures change; statements below an added `.bind` move to new
`sql!` locators.
ac-pplx added a commit that referenced this pull request Oct 17, 2026
Control-plane Postgres `sandboxes.managed_by` and `nodes.managed_by` hold the
substrate that owns a row, 'k8s' or 'space'. After #373, #383 and #506 the
remaining production literals were the shared routability fragment, the
Kubernetes adapter's builder lookup and the Direct route-identity match, plus
Direct's create-target claim and release, which landed on main since. They
now bind `SubstrateKind` or match its parsed variant, and a new archcheck
instance keeps new literals out of production code. No schema, wire format,
metric or configuration changes, and no behavior change.

- common-svc: `routable_execution_or_non_k8s(alias, kubernetes_placeholder)`
  writes `{alias}.managed_by <> {placeholder}` instead of `<> 'k8s'`. Each
  caller binds `SubstrateKind::Kubernetes` at the placeholder it numbers
  after its own parameters: the transient sweeper's aged-transient list and
  restoring converge (`$5`), internal-api's restored-state convergence (`$4`)
  and the api's wake-completion CAS (`$2`).
- api Kubernetes adapter: `builder_client_for_node` binds
  `n.managed_by = $2`.
- api Direct: `claim_create_target` and `release_create_target` bind
  `managed_by = $3` (`Space`).
- api Direct: the route identity's `substrate_kind` text is parsed as
  `SubstrateKind` in `route_identity_substrate_kind`, which maps exactly
  'space' and 'k8s' as before and rejects any other text with the same
  error.
- ARCHITECTURE.toml: `pattern_ratchet.substrate_literals` matches SQL
  comparisons of `managed_by` with a 'k8s'/'space' literal (=, <>, !=,
  IS [NOT] DISTINCT FROM, [NOT] IN; any case and alias) and Rust comparisons
  or match arms on "k8s"/"space". It allows test-support modules and
  fixtures that archcheck's filename-based test rule counts as production.
  On the parent commit it matches exactly the six converted lines; on
  this commit it matches nothing, so it has no baseline table and every
  behaviour is required.
- Tests: Postgres-backed twins for each converted statement. New
  Kubernetes builder-lookup and Direct create-target tests, a unit table for
  the route-identity labels, and an otherwise identical 'space' row added to
  the sweeper's converge, internal-api's restored convergence and the
  wake-completion redrive tests. Test callers of the sweeper's shared
  builders bind the extra parameter.
- Docs: `docs/rust/space-conventions.md`, the substrate AGENTS.md and the
  space-rust-development skill say every crate's production SQL binds the
  value; Rust parses it and matches the variant (or calls the `SubstrateKind`
  method that names the question) and never compares the label string; and
  the archcheck rule enforces it. Plugin version 0.2.33 -> 0.2.34.

Converted statements' text changes, so their sqlx statement-cache keys and
query signatures change; statements below an added `.bind` move to new
`sql!` locators.
ac-pplx added a commit that referenced this pull request Oct 17, 2026
Control-plane Postgres `sandboxes.managed_by` and `nodes.managed_by` hold the
substrate that owns a row, 'k8s' or 'space'. After #373, #383 and #506 the
remaining production literals were the shared routability fragment, the
Kubernetes adapter's builder lookup and the Direct route-identity match, plus
Direct's create-target claim and release, which landed on main since. They
now bind `SubstrateKind` or match its parsed variant, and a new archcheck
instance keeps new literals out of production code. No schema, wire format,
metric or configuration changes, and no behavior change.

- common-svc: `routable_execution_or_non_k8s(alias, kubernetes_placeholder)`
  writes `{alias}.managed_by <> {placeholder}` instead of `<> 'k8s'`. Each
  caller binds `SubstrateKind::Kubernetes` at the placeholder it numbers
  after its own parameters: the transient sweeper's aged-transient list and
  restoring converge (`$5`), internal-api's restored-state convergence (`$4`)
  and the api's wake-completion CAS (`$2`).
- api Kubernetes adapter: `builder_client_for_node` binds
  `n.managed_by = $2`.
- api Direct: `claim_create_target` and `release_create_target` bind
  `managed_by = $3` (`Space`).
- api Direct: the route identity's `substrate_kind` text is parsed as
  `SubstrateKind` in `route_identity_substrate_kind`, which maps exactly
  'space' and 'k8s' as before and rejects any other text with the same
  error.
- ARCHITECTURE.toml: `pattern_ratchet.substrate_literals` catches the
  common literal forms in production code: SQL comparisons of `managed_by`
  with 'k8s'/'space' (=, <>, !=, IS [NOT] DISTINCT FROM, [NOT] IN, = ANY,
  same-line CASE ... WHEN; any case, alias, prefix, cast or wrapping call),
  and Rust comparisons, label-string matches and match arms on "k8s"/"space"
  beside a substrate name. Its comment lists what it misses and its false
  positives. It allows test-support modules and fixtures that archcheck's
  filename-based test rule counts as production. On the parent commit it
  matches the seven lines this commit converts; on this commit it matches
  nothing, so it has no baseline table. no_crossing, listed_no_growth and
  stale_entries are required; no_new_entries and no_raised_caps are
  advisory, so a deliberate exception is a reviewed baseline entry.
- Tests: Postgres-backed twins for each converted statement. New
  Kubernetes builder-lookup and Direct create-target tests, a unit table for
  the route-identity labels, and an otherwise identical 'space' row added to
  the sweeper's converge, internal-api's restored convergence and the
  wake-completion redrive tests. Test callers of the sweeper's shared
  builders bind the extra parameter.
- Docs: `docs/rust/space-conventions.md`, the substrate AGENTS.md and the
  space-rust-development skill say every crate's production SQL binds the
  value; in Rust a decision about a row's substrate calls a `SubstrateKind`
  method named for the question, a fact about the running substrate reads
  `capabilities()`, and code never compares the label string; and the
  archcheck rule catches the common literal forms. Plugin version
  0.2.33 -> 0.2.34.

Converted statements' text changes, so their sqlx statement-cache keys and
query signatures change; statements below an added `.bind` move to new
`sql!` locators.
ac-pplx added a commit that referenced this pull request Oct 17, 2026
Control-plane Postgres `sandboxes.managed_by` and `nodes.managed_by` hold the
substrate that owns a row, 'k8s' or 'space'. After #373, #383 and #506 the
remaining production literals were the shared routability fragment, the
Kubernetes adapter's builder lookup and the Direct route-identity match, plus
Direct's create-target claim and release, which landed on main since. They
now bind `SubstrateKind` or match its parsed variant, and a new archcheck
instance keeps new literals out of production code. No schema, wire format,
metric or configuration changes, and no behavior change.

- common-svc: `routable_execution_or_non_k8s(alias, kubernetes_placeholder)`
  writes `{alias}.managed_by <> {placeholder}` instead of `<> 'k8s'`. Each
  caller binds `SubstrateKind::Kubernetes` at the placeholder it numbers
  after its own parameters: the transient sweeper's aged-transient list and
  restoring converge (`$5`), internal-api's restored-state convergence (`$4`)
  and the api's wake-completion CAS (`$2`).
- api Kubernetes adapter: `builder_client_for_node` binds
  `n.managed_by = $2`.
- api Direct: `claim_create_target` and `release_create_target` bind
  `managed_by = $3` (`Space`).
- api Direct: the route identity's `substrate_kind` text is parsed as
  `SubstrateKind` in `route_identity_substrate_kind`, which maps exactly
  'space' and 'k8s' as before and rejects any other text with the same
  error.
- ARCHITECTURE.toml: `pattern_ratchet.substrate_literals` catches the
  common literal forms in production code: SQL comparisons of `managed_by`
  with 'k8s'/'space' (=, <>, !=, IS [NOT] DISTINCT FROM, [NOT] IN, = ANY,
  same-line CASE ... WHEN; any case, alias, prefix, quoting, cast or wrapping
  call), and Rust comparisons with the label (raw strings included), a
  `NODE_MANAGED_BY_*` const or a variant's `.as_str()`/`.to_string()`,
  matches on the label string, and match arms on "k8s"/"space" beside a
  substrate name. Its comment lists what it misses and its false positives. It allows test-support modules and fixtures that archcheck's
  filename-based test rule counts as production. On the parent commit it
  matches the seven lines this commit converts; on this commit it matches
  nothing, so it has no baseline table. no_crossing, listed_no_growth and
  stale_entries are required; no_new_entries and no_raised_caps are
  advisory, so a deliberate exception is a reviewed baseline entry.
- Tests: Postgres-backed twins for each converted statement. New
  Kubernetes builder-lookup and Direct create-target tests, a unit table for
  the route-identity labels, and an otherwise identical 'space' row added to
  the sweeper's converge, internal-api's restored convergence and the
  wake-completion redrive tests. Test callers of the sweeper's shared
  builders bind the extra parameter.
- Docs: `docs/rust/space-conventions.md`, the substrate AGENTS.md and the
  space-rust-development skill say every crate's production SQL binds the
  value; in Rust a decision about a row's substrate calls a `SubstrateKind`
  method named for the question, a fact about the running substrate reads
  `capabilities()`, and code never compares the label string; and the
  archcheck rule catches the common literal forms. Plugin version
  0.2.33 -> 0.2.34.

Converted statements' text changes, so their sqlx statement-cache keys and
query signatures change; statements below an added `.bind` move to new
`sql!` locators.
ac-pplx added a commit that referenced this pull request Oct 17, 2026
Control-plane Postgres `sandboxes.managed_by` and `nodes.managed_by` hold the
substrate that owns a row, 'k8s' or 'space'. After #373, #383 and #506 the
remaining production literals were the shared routability fragment, the
Kubernetes adapter's builder lookup and the Direct route-identity match, plus
Direct's create-target claim and release, which landed on main since. They
now bind `SubstrateKind` or match its parsed variant, and a new archcheck
instance keeps new literals out of production code. No schema, wire format,
metric or configuration changes, and no behavior change.

- common-svc: `routable_execution_or_non_k8s(alias, kubernetes_placeholder)`
  writes `{alias}.managed_by <> {placeholder}` instead of `<> 'k8s'`. Each
  caller binds `SubstrateKind::Kubernetes` at the placeholder it numbers
  after its own parameters: the transient sweeper's aged-transient list and
  restoring converge (`$5`), internal-api's restored-state convergence (`$4`)
  and the api's wake-completion CAS (`$2`).
- api Kubernetes adapter: `builder_client_for_node` binds
  `n.managed_by = $2`.
- api Direct: `claim_create_target` and `release_create_target` bind
  `managed_by = $3` (`Space`).
- api Direct: the route identity's `substrate_kind` text is parsed as
  `SubstrateKind` in `route_identity_substrate_kind`, which maps exactly
  'space' and 'k8s' as before and rejects any other text with the same
  error.
- ARCHITECTURE.toml: `pattern_ratchet.substrate_literals` catches the common
  literal forms in production code: SQL comparisons of `managed_by` with
  'k8s'/'space' (=, <>, !=, IS [NOT] DISTINCT FROM, [NOT] IN, = ANY,
  same-line CASE ... WHEN; any case, alias, prefix, quoting, cast or wrapping
  call), and Rust comparisons with the label (raw strings included), a
  `NODE_MANAGED_BY_*` const or a variant's `.as_str()`/`.to_string()`,
  matches on the label string, and match arms on "k8s"/"space" beside a
  substrate name. Its comment lists what it misses and its false positives.
  It allows test-support modules and fixtures that archcheck's filename-based
  test rule counts as production. On the parent commit it matches the seven
  lines this commit converts; on this commit it matches nothing, so it has no
  baseline table. no_crossing, listed_no_growth and stale_entries are
  required; no_new_entries and no_raised_caps are advisory, so a deliberate
  exception is a reviewed baseline entry.
- Tests: Postgres-backed twins for each converted statement. New
  Kubernetes builder-lookup and Direct create-target tests, a unit table for
  the route-identity labels, and an otherwise identical 'space' row added to
  the sweeper's converge, internal-api's restored convergence and the
  wake-completion redrive tests. Test callers of the sweeper's shared
  builders bind the extra parameter.
- Docs: `docs/rust/space-conventions.md`, the substrate AGENTS.md and the
  space-rust-development skill say every crate's production SQL binds the
  value; in Rust a decision about a row's substrate calls a `SubstrateKind`
  method named for the question, a fact about the running substrate reads
  `capabilities()`, and code never compares the label string; and the
  archcheck rule catches the common literal forms. Plugin version
  0.2.33 -> 0.2.34.

Converted statements' text changes, so their sqlx statement-cache keys and
query signatures change; statements below an added `.bind` move to new
`sql!` locators.
meefs pushed a commit to meefs/SPACE that referenced this pull request Oct 18, 2026
…erplexityai#611)

Control-plane Postgres `sandboxes.managed_by` and `nodes.managed_by` hold the
substrate that owns a row, 'k8s' or 'space'. After perplexityai#373, perplexityai#383 and perplexityai#506 the
remaining production literals were the shared routability fragment, the
Kubernetes adapter's builder lookup and the Direct route-identity match, plus
Direct's create-target claim and release, which landed on main since. They
now bind `SubstrateKind` or match its parsed variant, and a new archcheck
instance keeps new literals out of production code. No schema, wire format,
metric or configuration changes, and no behavior change.

- common-svc: `routable_execution_or_non_k8s(alias, kubernetes_placeholder)`
  writes `{alias}.managed_by <> {placeholder}` instead of `<> 'k8s'`. Each
  caller binds `SubstrateKind::Kubernetes` at the placeholder it numbers
  after its own parameters: the transient sweeper's aged-transient list and
  restoring converge (`$5`), internal-api's restored-state convergence (`$4`)
  and the api's wake-completion CAS (`$2`).
- api Kubernetes adapter: `builder_client_for_node` binds
  `n.managed_by = $2`.
- api Direct: `claim_create_target` and `release_create_target` bind
  `managed_by = $3` (`Space`).
- api Direct: the route identity's `substrate_kind` text is parsed as
  `SubstrateKind` in `route_identity_substrate_kind`, which maps exactly
  'space' and 'k8s' as before and rejects any other text with the same
  error.
- ARCHITECTURE.toml: `pattern_ratchet.substrate_literals` catches the common
  literal forms in production code: SQL comparisons of `managed_by` with
  'k8s'/'space' (=, <>, !=, IS [NOT] DISTINCT FROM, [NOT] IN, = ANY,
  same-line CASE ... WHEN; any case, alias, prefix, quoting, cast or wrapping
  call), and Rust comparisons with the label (raw strings included), a
  `NODE_MANAGED_BY_*` const or a variant's `.as_str()`/`.to_string()`,
  matches on the label string, and match arms on "k8s"/"space" beside a
  substrate name. Its comment lists what it misses and its false positives.
  It allows test-support modules and fixtures that archcheck's filename-based
  test rule counts as production. On the parent commit it matches the seven
  lines this commit converts; on this commit it matches nothing, so it has no
  baseline table. no_crossing, listed_no_growth and stale_entries are
  required; no_new_entries and no_raised_caps are advisory, so a deliberate
  exception is a reviewed baseline entry.
- Tests: Postgres-backed twins for each converted statement. New
  Kubernetes builder-lookup and Direct create-target tests, a unit table for
  the route-identity labels, and an otherwise identical 'space' row added to
  the sweeper's converge, internal-api's restored convergence and the
  wake-completion redrive tests. Test callers of the sweeper's shared
  builders bind the extra parameter.
- Docs: `docs/rust/space-conventions.md`, the substrate AGENTS.md and the
  space-rust-development skill say every crate's production SQL binds the
  value; in Rust a decision about a row's substrate calls a `SubstrateKind`
  method named for the question, a fact about the running substrate reads
  `capabilities()`, and code never compares the label string; and the
  archcheck rule catches the common literal forms. Plugin version
  0.2.33 -> 0.2.34.

Converted statements' text changes, so their sqlx statement-cache keys and
query signatures change; statements below an added `.bind` move to new
`sql!` locators.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants