SPEC-064 — Schema Registry Completion
Self-describing schema_registry table; meta-schema bootstrap and validator wiring.
SPEC-064: Schema registry completion
| Field | Value |
|---|---|
| Status | Shipped |
| Owner | kris.ganjam@gmail.com |
| Date | 2026-05-01 |
| Pillar | Cross-cutting |
| Implementation | #443 |
Target repos: scidex-substrate (this doc, all schema JSON files, bootstrap loader, codegen). scidex-prism (consumes generated TypeScript types — separate PR). |
|
| Masters: SPEC-001 §6 (schema registry as artifact type), SPEC-009 (versioning rules), SPEC-031 (validator that depends on schemas being available). |
Adjacent context:
-
src/scidex_substrate/schema_registry/default_schemas/ships onlymessage.jsonandskill.jsontoday. Every other type relies on the v1 database having been seeded — substrate boots degraded if v1 DB is unavailable or empty. -
22+ artifact types exist across the codebase: hypothesis, paper, wiki_page, comment, signal, mission, dataset, debate_session, pantheon_debate, persona, agent, market, collider, arena, gap, peer_review (SPEC-055), notebook (SPEC-058), reproducibility_attestation (SPEC-056), curation_decision (SPEC-054), evidence_link (SPEC-053), flag/moderation_decision/appeal (SPEC-068).
-
scripts/export_typescript_types.pyexists and consumes only the two shipped schemas. Prism’sArtifact*types are partly hand-written. -
north-star §6 “Empty tables created ‘for later’”: v1 anti-pattern was registering empty tables. v2 corollary: don’t register a schema for a type with no producer. SPEC-064 ships schemas only for types whose owning SPEC reaches implementation in this release train.
TL;DR
Anchored against north-star.md §4 “World model = ALL artifacts”: if every persistable thing is an artifact, every artifact type needs a published, machine-readable schema. SPEC-064 lands all canonical schemas in default_schemas/ as JSON Schema draft-07, makes startup upsert the registry from those files (degraded → graceful), enforces validation at every scidex.create, and extends the codegen script to generate Prism types from the full set. Net effect: substrate is self-describing and Prism types are 1:1 with substrate reality, eliminating an entire class of drift bugs.
1. Problem statement
Three interlocking gaps:
-
Substrate boots degraded. On a fresh DB,
schema_registryis empty.scidex.create(type='hypothesis', …)either silently skips validation or 500s. SPEC-001 §6 designed the registry as an artifact type but did not pre-populate it. -
No source of truth for type contracts. When a SPEC declares a new type (peer_review in SPEC-055, notebook in SPEC-058), the contract lives in prose. Two readers can disagree on whether
peer_review.evidence_linksis required. Without a JSON Schema in the repo, the SPEC is literally the only spec. -
Prism types drift.
scidex-prism/src/lib/types/Artifact.tsis partly hand-typed and partly generated. New types will be hand-written, badly, and silently. The codegen exists but only feeds two schemas through.
These gaps are doctrine-load-bearing: north-star §4 makes the polymorphic substrate the model of the world. A model with no published schema is not a model; it’s a guess.
2. Decision
2.1 Ship a JSON Schema for every artifact type with a producer
Each schema lives at default_schemas/<type>.json (JSON Schema draft-07, matching existing message.json and skill.json). Single object envelope:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://scidex.ai/schemas/hypothesis@v1",
"title": "hypothesis",
"type": "object",
"version": "v1",
"mutability": "mutable_with_history", // SPEC-001 §5.3
"id_strategy": "h-<rand12>", // SPEC-001 §5.1
"search": { // SPEC-005 §5
"searchable": true,
"embedding_recipe": "title || '\\n' || content.mechanism || '\\n' || content.target_gene",
"tsvector_recipe": "title || ' ' || content.mechanism || ' ' || content.target_gene",
"boosts": {"title": 2.0, "content.target_gene": 1.5}
},
"lifecycle": { // SPEC-001 §11
"states": ["proposed","investigating","supported","contradicted","promoted","archived"],
"initial": "proposed"
},
"properties": {
"title": {"type":"string","minLength":1,"maxLength":300},
"content": {
"type": "object",
"properties": {
"mechanism": {"type":"string","maxLength":4000},
"target_gene": {"type":"string","maxLength":80},
"evidence_for": {"type":"array","items":{"$ref":"#/$defs/Ref"}}
},
"required": ["mechanism","target_gene"]
}
},
"required": ["title","content"]
}
The full list of schemas this SPEC ships:
| Type | Mutability | Owner |
|---|---|---|
hypothesis.json |
mutable_with_history | hypothesis pipeline |
paper.json |
immutable | corpus ETL (SPEC-029) |
wiki_page.json |
mutable_with_history | wiki |
comment.json |
append_only | comments |
signal.json |
append_only | signals |
mission.json |
mutable_with_history | SPEC-027 |
dataset.json |
superseded_only | datasets |
debate_session.json |
mutable_with_history | classic debates |
pantheon_debate.json |
mutable_with_history | SPEC-028, SPEC-051 |
persona.json |
mutable_with_history | SPEC-017, SPEC-051 |
agent.json |
mutable_with_history | SPEC-017, SPEC-051 |
market.json |
mutable_with_history | SPEC-018 |
collider.json |
mutable_with_history | SPEC-022 |
arena.json |
mutable_with_history | SPEC-032 |
gap.json |
mutable_with_history | knowledge gaps |
peer_review.json |
append_only | SPEC-055 |
notebook.json |
mutable_with_history | SPEC-058 |
reproducibility_attestation.json |
append_only | SPEC-056 |
curation_decision.json |
append_only | SPEC-054 |
evidence_link.json |
append_only | SPEC-053 |
flag.json, moderation_decision.json, appeal.json |
append_only | SPEC-068 |
Total: 23 schemas. Existing message.json and skill.json are upgraded to the same envelope.
The “no schema without a producer” rule from north-star §6 is honoured: all referenced SPECs are landing in the same release train. Schemas for types whose owning SPEC is not yet drafted (e.g. team from SPEC-051 §8) wait.
2.2 Bootstrap: upsert on substrate startup
# src/scidex_substrate/schema_registry/bootstrap.py
def bootstrap_schema_registry(db):
for path in DEFAULT_SCHEMAS_DIR.glob("*.json"):
schema = json.loads(path.read_text())
validate_schema_envelope(schema)
db.execute("""
INSERT INTO schema_registry (artifact_type, version, schema_json, source, updated_at)
VALUES (%s, %s, %s::jsonb, 'default_schemas', NOW())
ON CONFLICT (artifact_type, version)
DO UPDATE SET schema_json = EXCLUDED.schema_json, updated_at = NOW()
WHERE schema_registry.source = 'default_schemas'
""", (schema["title"], schema["version"], json.dumps(schema)))
The WHERE source='default_schemas' clause means operator-curated overrides (source='operator') are not clobbered. Idempotent and safe to run on every restart.
/health/deep reports schema_registry: { count: 23, missing: [] }.
2.3 Validation enforced at every scidex.create
scidex.create already looks up the schema; today it returns schema_unknown for missing types. After SPEC-064:
-
All 23 types have a schema → no
schema_unknownfor in-tree write paths. -
Validator runs
jsonschema.validate(content, schema['properties']['content'])and returns SPEC-001 §10 structured errors withpath/constraint/valueon failure. -
Failures are 400, not retryable.
scidex.batch(SPEC-062) surfaces them in per-operror.
Legacy rows that no longer satisfy strict validation stay readable; only writes are gated. A drift-scanner cron (SPEC-048) lists drifted rows for the curation loop.
2.4 Codegen: TypeScript types for Prism
Extend scripts/export_typescript_types.py to walk every schema, run json-schema-to-typescript over each, and write to scidex-prism/src/lib/types/generated/. One .ts per schema. An index.ts re-exports a discriminated union Artifact = Hypothesis | Paper | … keyed on type.
Generator runs as a CI step in scidex-substrate; output committed to scidex-prism via the substrate-pull pipeline (SPEC-014). Drift becomes a CI failure, not a runtime bug.
3. Schema / verb changes (concrete)
-
New files: 21 schemas under
default_schemas/(23 total minus 2 pre-existing). -
Updated files:
message.json,skill.jsonupgraded to the new envelope. -
Bootstrap loader: new
schema_registry/bootstrap.pyinvoked fromapi/app.pystartup. -
Codegen:
scripts/export_typescript_types.pyextended (~100 LOC) to iterate the directory. -
Tests:
tests/schema_registry/test_default_schemas_load.py— every schema parses and registers. Per-typetest_<type>_create_validates.pyround-trips a fixture. -
No new tables.
schema_registryalready exists per SPEC-001 §6. -
Health:
/health/deepgains aschema_registryblock.
4. Acceptance
-
Boot from empty DB. Drop
schema_registry, restart. Repopulated from disk;/health/deepreportscount = 23, missing = []. -
Validation engaged for every type.
scidex.create(type='peer_review', content={…valid…})succeeds; missing required field returns 400 withpath='/content/verdict', constraint='required'. -
Operator override preserved. Insert a
schema_registryrow withsource='operator'forhypothesis. Restart. The operator row is not overwritten. -
Codegen produces compilable TypeScript. Output passes
tsc --noEmit.Artifactunion includes all 23 variants. -
Round-trip fixture per type. Each type ships
tests/fixtures/<type>_valid.json; create + get round-trip succeeds. -
Drift detection. A type referenced in code but missing from the registry surfaces under
schema_registry.missingin/health/deep. -
No silent skips. No
scidex.createpath bypasses validation. Static check: every call site either passes through the validator or is tagged# noqa: SCH001with inline justification.
5. Rollout
Phase 1 (1 large PR): all 23 schemas + bootstrap loader + tests. Strict validation off by default behind SCIDEX_STRICT_SCHEMA_VALIDATION=0. One week soak with warnings logged but writes accepted, allowing time to fix legacy data.
Phase 2 (1 PR, S): flip default to =1. Document the flip in SPEC-019 cutover runbook.
Phase 3 (1 PR, M): codegen integrated into substrate-pull pipeline. Prism PR consumes generated types; hand-typed Artifact*.ts files deleted in the same Prism PR.
Phase 4 (per type): when a future SPEC adds a new type, the SPEC must include the schema file in the same PR as the first producer. Reviewer checklist enforces.
Compatible with cutover-runbook: existing writes against prod continue because Phase 1 doesn’t enforce. Phase 2’s flip is a planned date with a rollback flag.
6. Open questions
-
JSON Schema vs Pydantic-as-source. Several types have Pydantic models. Should those be the source and JSON Schema be derived? Decision: JSON Schema is the source (language-neutral; Prism reads directly). Where Pydantic exists, CI auto-checks against the JSON Schema.
-
Type aliasing across cutover. v1’s
actor_typestrings differ from v2’sagent.kindper SPEC-051.agent.jsondocuments both until the back-compat alias column is removed (SPEC-051 §4.2 timeline). -
Per-type lifecycle vocabulary.
lifecycle.statesmust match what curation SPECs expect. SPEC-054 owns the state vocabulary; coordinate during PR review. -
Multilingual content. All schemas English-only today. If non-English papers arrive, do we add
langper type or universalcontent.lang? Defer; not required for cutover. -
Schema versioning across upgrades. SPEC-009 owns
v1 → v2evolution. SPEC-064 ships onlyv1. -
External consumers. Once published, oxa.dev mirrors (SPEC-021) and federated readers may consume them.
$idalready points athttps://scidex.ai/schemas/<type>@<v>; serving it is a small follow-up.
Work Log
2026-05-14 — SPEC-064 §5 Phase 1 implementation
Commit: 27ce9c5 — [Cross-cutting][SPEC-064 §5 Phase 1] SCIDEX_STRICT_SCHEMA_VALIDATION=0 + test expansion
What landed:
-
SCIDEX_STRICT_SCHEMA_VALIDATIONflag wired intoscidex.create(handler.validators + TypedCreateValidator paths). Default0— failures log a warning but allow writes through (Phase 1 soak mode). Set to1to enforce and block. -
tests/service/test_schema_registry_bootstrap.pyexpanded from 10 hardcoded SPEC-064 types to all 29 canonical types currently indefault_schemas/(after filteringgap_mission— themission.jsonfilename declarestype: gap_missioninternally, a pre-existing mismatch tracked in SPEC-064 §6 open questions).
What was already done (prior commits on main):
-
All 30 schema JSON files present in
default_schemas/(ca04e7b, #443) -
seed_default_schemas()idempotent upsert inschema_registry/__init__.py -
Bootstrap loader invoked from
api/app.pyat startup (SCIDEX_AUTO_SEED_SCHEMAS=1) -
/health/deepreports schema seed status (613b09f) -
Meta-schema, envelope validation, and lifecycle block tests
Still open (not in this commit):
-
Phase 2: flip
SCIDEX_STRICT_SCHEMA_VALIDATIONdefault to=1+ SPEC-019 cutover doc -
Phase 3: codegen integration into substrate-pull pipeline
-
Phase 4: per-type schema-in-PR rule for future SPECs
-
mission.jsonfilename↔typegap_missionmismatch resolution