feat(dynseal): the server asserts its schema instead of inventing one #11

Merged
peterswimm merged 1 commit from fix/dynseal-postgres into main 2026-09-28 20:45:13 +00:00
Member

Stage 3 of reviving dynseal, on top of #10 (merged). Needs spel#901's tables to
actually run, but nothing here applies anything.

The split this closes

Two modules disagreed about who owns dynseal's schema:

dynseal-pattern-publisher.py asserted the tables were present, refused to create them
dynseal_server.py created them itself, every startup, inline, in SQLite

Each was internally consistent, which is why nothing caught it.

There is now one answer — dynseal/forge_db.py — holding the connection and the
check. It reads information_schema rather than probing each table with a
SELECT, because a probe cannot tell no such table from no permission to read
it
and those want different fixes. It creates nothing.

start-dynseal-server.sh's inline CREATE TABLE block goes too. That block
wrote ./data/foundation.db while the server opened a different file beside
itself — so it had never initialised the database it claimed to.

The hardcoded paths — why none of this could run

Three modules carried an absolute path to a directory that no longer exists.
forge_config's own rule, at the top of that file, is "no hardcoded defaults";
spel_root() is that rule applied — env override, then manifest, like every
other value there.

What is deliberately NOT converted

The part worth arguing with. Three modules keep SQLite:

module also writes
dynseal-pattern-publisher.py artifacts
dynseal-p2p-sync.py trust_log
actor_manifest_integration.py actors, trust_log

None of those tables is defined by the dynseal migration. trust_log exists
in no database here — the forge has core.trust_ledger, a different shape and a
different decision. artifacts exists only as ux.artifacts, something else
entirely, which would not resolve unqualified.

So converting them is not a mechanical port; it needs a schema decision about
what dynseal's attestations have to do with the forge's trust ledger. Converting
half of a module — its supply_chain_log read but not its trust_log write —
would move the split rather than close it, which is the mistake this PR undoes.

Each says so at the line where it matters. The publisher's error message is
corrected while it waits: it used to tell you to apply a Postgres migration to
fix a SQLite problem.

Testing

Against the live database, read-only. All three tables are absent today, so the
refusal path is real rather than simulated:

dynseal schema missing: pattern_attestations, information_provenance,
supply_chain_log. Apply migrations/20260928_2014_dynseal_provenance.sql ...

The other two paths were exercised by pointing REQUIRED_TABLES at tables that
do exist:

case result
all three present passes
one present, one missing names only the missing one
all missing (real) names all three, points at the migration

Also: every dynseal/*.py parses, bash -n start-dynseal-server.sh is clean, and
tools/check_schema_registry.py still reports the registry consistent.

Impact Statement

Breaking changes: the server now refuses to start without its schema,
where it previously created one. That is the intended change, and until spel#901
merges it means the server will not start at all. It could not usefully start
before either — it was writing to a SQLite file nothing else read.

Behaviour change: dynseal_server.py talks to the forge Postgres database
via forge_config.database_url() — DATABASE_URL, then the manifest, then a
RuntimeError. No silent local fallback, deliberately: a silent fallback is how
it ended up writing somewhere nobody read.

Risk. The two converted query sites are untested against real tables, because
the tables do not exist yet. They are a parameter-style change (? → %s) and a
connection swap; the SQL is otherwise unchanged. Once spel#901 lands, ./start-dynseal-server.sh
plus ./test-dynseal-api.sh exercises both.

What this does not do: give dynseal a caller. It still has none. That is the
next stage, and it is the one that may conclude there isn't an honest one.

Stage 3 of reviving dynseal, on top of #10 (merged). Needs spel#901's tables to actually run, but nothing here applies anything. ## The split this closes Two modules disagreed about who owns dynseal's schema: | | | |---|---| | `dynseal-pattern-publisher.py` | asserted the tables were present, refused to create them | | `dynseal_server.py` | created them itself, every startup, inline, in SQLite | Each was internally consistent, which is why nothing caught it. There is now one answer — `dynseal/forge_db.py` — holding the connection and the check. It reads `information_schema` rather than probing each table with a `SELECT`, because a probe cannot tell *no such table* from *no permission to read it* and those want different fixes. It creates nothing. `start-dynseal-server.sh`'s inline `CREATE TABLE` block goes too. That block wrote `./data/foundation.db` while the server opened a different file beside itself — so it had never initialised the database it claimed to. ## The hardcoded paths — why none of this could run **Three** modules carried an absolute path to a directory that no longer exists. `forge_config`'s own rule, at the top of that file, is *"no hardcoded defaults"*; `spel_root()` is that rule applied — env override, then manifest, like every other value there. ## What is deliberately NOT converted The part worth arguing with. Three modules keep SQLite: | module | also writes | |---|---| | `dynseal-pattern-publisher.py` | `artifacts` | | `dynseal-p2p-sync.py` | `trust_log` | | `actor_manifest_integration.py` | `actors`, `trust_log` | **None of those tables is defined by the dynseal migration.** `trust_log` exists in no database here — the forge has `core.trust_ledger`, a different shape and a different decision. `artifacts` exists only as `ux.artifacts`, something else entirely, which would not resolve unqualified. So converting them is not a mechanical port; it needs a schema decision about what dynseal's attestations have to do with the forge's trust ledger. Converting *half* of a module — its `supply_chain_log` read but not its `trust_log` write — would move the split rather than close it, which is the mistake this PR undoes. Each says so at the line where it matters. The publisher's error message is corrected while it waits: it used to tell you to apply a Postgres migration to fix a SQLite problem. ## Testing Against the live database, read-only. All three tables are absent today, so the refusal path is real rather than simulated: ``` dynseal schema missing: pattern_attestations, information_provenance, supply_chain_log. Apply migrations/20260928_2014_dynseal_provenance.sql ... ``` The other two paths were exercised by pointing `REQUIRED_TABLES` at tables that do exist: | case | result | |---|---| | all three present | passes | | one present, one missing | names only the missing one | | all missing (real) | names all three, points at the migration | Also: every `dynseal/*.py` parses, `bash -n start-dynseal-server.sh` is clean, and `tools/check_schema_registry.py` still reports the registry consistent. ## Impact Statement **Breaking changes:** the server now **refuses to start** without its schema, where it previously created one. That is the intended change, and until spel#901 merges it means the server will not start at all. It could not usefully start before either — it was writing to a SQLite file nothing else read. **Behaviour change:** `dynseal_server.py` talks to the forge Postgres database via `forge_config.database_url()` — `DATABASE_URL`, then the manifest, then a `RuntimeError`. No silent local fallback, deliberately: a silent fallback is how it ended up writing somewhere nobody read. **Risk.** The two converted query sites are untested against real tables, because the tables do not exist yet. They are a parameter-style change (`?` → `%s`) and a connection swap; the SQL is otherwise unchanged. Once spel#901 lands, `./start-dynseal-server.sh` plus `./test-dynseal-api.sh` exercises both. **What this does not do:** give dynseal a caller. It still has none. That is the next stage, and it is the one that may conclude there isn't an honest one.
feat(dynseal): the server asserts its schema instead of inventing one
Some checks failed
PR policy check / policy-gate (pull_request) Failing after 0s
CI / schema-registry (pull_request) Successful in 5s
0dd96e998e
Two modules disagreed about who owns dynseal's schema. The publisher asserted
the tables were present and refused to create them — the schema-first rule, DDL
belongs in migrations. The server created them itself on every startup, inline,
in SQLite. Each was internally consistent, which is why nothing caught it.

There is now one answer, dynseal/forge_db.py, holding the connection and the
check. It reads information_schema rather than probing each table with a SELECT,
because a probe cannot tell "no such table" from "no permission to read it" and
those want different fixes. It creates nothing: a missing table produces a named
error pointing at the migration, which is the behaviour the whole revival exists
to establish.

dynseal_server.py is converted. init_database() is gone, the two query sites use
psycopg2 with %s rather than SQLite's ?, and startup refuses rather than
proceeds. start-dynseal-server.sh's inline CREATE TABLE block is gone with it —
that block wrote ./data/foundation.db while the server opened a different file
beside itself, so it had never initialised the database it claimed to.

THE HARDCODED PATHS, which are why none of this could run at all. Three modules
carried an absolute path to a directory that no longer exists. forge_config's
own rule, at the top of that file, is "no hardcoded defaults"; spel_root() is
that rule applied, env override then manifest, like every other value there.

WHAT IS DELIBERATELY NOT CONVERTED, and why — this is the part worth arguing
with. Three modules still use SQLite:

  dynseal-pattern-publisher.py  also writes `artifacts`
  dynseal-p2p-sync.py           also writes `trust_log`
  actor_manifest_integration.py also writes `actors` and `trust_log`

None of those tables is defined by the dynseal migration. `trust_log` exists in
no database here — the forge has core.trust_ledger, a different shape and a
different decision. `artifacts` exists only as ux.artifacts, which is something
else entirely and would not resolve unqualified.

So converting them is not a mechanical port; it needs a schema decision about
what dynseal's attestations have to do with the forge's trust ledger. Converting
half of a module — its supply_chain_log read but not its trust_log write — would
move the split rather than close it, which is the mistake this commit is
undoing. They keep SQLite, and each says so at the line where it matters.

The publisher's error message is corrected while it waits: it used to tell you
to apply a Postgres migration to fix a SQLite problem.

Verified against the live database, read-only. All three tables absent today, so
the refusal path is real rather than simulated:

  dynseal schema missing: pattern_attestations, information_provenance,
  supply_chain_log. Apply migrations/20260928_2014_dynseal_provenance.sql ...

The positive and partial paths were exercised by pointing REQUIRED_TABLES at
tables that do exist: all-present passes, and a partial set names only the one
missing. Every dynseal/*.py parses, the start script passes bash -n, and
tools/check_schema_registry.py still reports the registry consistent.
peterswimm deleted branch fix/dynseal-postgres 2026-09-28 20:45:13 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Toilville/rituals-api-spec!11
No description provided.