Schema Audit Contracts¶
truthound.schema_audit is an in-development, separately imported metadata and audit A/B namespace.
It includes a bounded PostgreSQL14/17 catalog adapter, not an ERD editor/importer,
connection authorization, approval or release workflow. Existing
th.check() and table-schema validators are unchanged. No new package extra is needed;
the namespace itself uses only the Python standard library. The root package retains
its usual dependencies. Unreleased source functionality is not a PyPI availability claim.
Design-to-Catalog Audit B¶
from truthound.schema_audit import audit_implementation
from truthound.schema_audit.postgresql import collect_postgresql
# connection is a dedicated, caller-authenticated asyncpg.Connection.
# The caller must authorize the destination and configure verified TLS.
snapshot = await collect_postgresql(
connection, schemas=("application",), binding_ref="approved-binding",
snapshot_id="snapshot-1", timeout_ms=3000, attempts=2,
)
report = audit_implementation(design, snapshot)
result = report.as_dict()
The caller installs asyncpg and closes the connection. The adapter does not resolve
credentials or accept SQL/URLs. It rejects an already active transaction. It uses
read-only transactions, a repeatable-read catalog snapshot, bounded ACCESS SHARE
locks acquired before catalog reads, and explicit catalog scope. It never selects
application rows or expands scope to follow foreign keys. Source database schemas
must have USAGE and tables SELECT privileges; SELECT is required for locks, not row
collection. System schemas are rejected. Missing/inaccessible schemas and hidden
tables lower coverage rather than producing a false missing-table failure.
Limits: 16 schemas, 500 table-like objects, 10,000 combined tables/columns/constraints,
4,096 characters per metadata value. timeout_ms is 100..10,000 and attempts 1..3.
Each attempt has a wall-time budget of four times the statement timeout. Only
catalog-change and timeout failures retry. CollectionError.code never contains
driver messages, connection details or SQL values. Cancellation propagates and
transactions are rolled back. Collection is not a guarantee against future DDL.
Ordinary PostgreSQL14/17 tables are supported. Versions 15/16/18 are not implicitly enabled. The design and snapshot must have the same major version. PG14 uses its own catalog projection rather than referencing PG15+ constraint columns. Negative numeric scale and scale greater than precision remain unsupported in the PG14 normalizer. The implementation rule pack is version 2; old results must not be relabeled. Partitioned/inherited tables, views, foreign tables, unvalidated constraints, initially deferred constraints, nondefault FK match semantics, subset SET NULL/DEFAULT and NULLS NOT DISTINCT unique semantics lower coverage. Cross-scope FK targets are not fetched. Standalone unique indexes also lower constraint coverage. Custom types/domains/enums, array dimensionality and timestamp precision zero remain conservative UNKNOWN where the v1 model cannot represent them exactly. Character lengths are character counts, not byte counts.
Comparison uses exact dialect-aware namespace/table/column/constraint names, ordered keys and referenced columns, FK update/delete actions, type facets, nullability and ordinal positions. Renames are not inferred. Logical names use the entire COMMENT value with NFC normalization; absent comments or external-mapping claims remain UNKNOWN. Default/check expressions are never executed: equal trimmed strings match; different simple literals differ; other expression differences are UNKNOWN, not asserted SQL semantic equivalence or inequality. Default absence versus presence is a difference. Unsupported names cannot produce trustworthy missing/extra findings.
Every result pins semantic input digests, rule-pack version/digest, dual provenance
and stable check fingerprints. ImplementationReport.as_dict() produces the
truthound.implementation-audit/1 output contract, not an input accepted by
load_document(). Any FAIL yields NONCONFORMANT while UNKNOWN counts remain visible;
incomplete coverage or empty targets cannot yield CONFORMANT. No schema is modified.
Research basis: PostgreSQL17 catalogs define ordered keys/actions and validation flags; visibility rules explain why filtered metadata is not proof of absence. Transaction consistency informs bounded locking and snapshot boundaries. Rahm and Bernstein motivates keeping inferred correspondences separate from exact matching. The DDIA author overview informs transaction and immutable evidence boundaries; this is not a claim to have read the full paid book.
Loading and Identity¶
from truthound.schema_audit import (
load_document, dump_document, raw_digest, semantic_digest, json_schema,
)
payload = b'{"contract_version":"truthound.schema-audit/1","kind":"DESIGN","id":"design1","revision":1,"dialect":{"name":"postgresql","major":17},"tables":[],"sources":[]}'
model = load_document(payload)
assert load_document(dump_document(model)) == model
original_sha256 = raw_digest(payload)
content_sha256 = semantic_digest(model)
structural_schema = json_schema()
An empty model is representable; loading it is not an audit PASS.
load_document(bytes) -> StandardRevision | DesignRevision | CatalogSnapshot
accepts strict UTF-8 JSON. All defined fields must be present; unknown fields,
duplicate JSON keys, invalid versions, duplicate IDs and dangling graph references
are rejected. Constructor arguments are also checked without type coercion.
Models use frozen dataclasses and nested tuples, not mutable input dictionaries.
ModelError provides code and pointer. Known-field validation uses JSON pointers;
duplicate-key and pre-decoding resource errors use structural numeric/container
locations, never unknown property names or payload values. Do not log the input on
failure. JSON syntax errors have a root pointer, not a raw parser excerpt.
Limits: 20 MiB input/output, nesting 32, 4,096 Unicode code points per string, 50,000 elements per array, 1,000 tables, 50,000 total columns and 1,000,000 decoded nodes. Numbers are integers within ±(2^53−1); bool is not an integer. NaN, Infinity, floating point, NUL and lone surrogates are rejected. These are contract safety limits, not measured production throughput promises.
The packaged schemas/document-v1.json is JSON Schema 2020-12 and matches
json_schema(). It covers structure and primitive types. Cross-reference integrity,
resource limits and observation/constraint invariants additionally require
load_document; a generic JSON Schema validator alone is insufficient.
Models¶
| Value | Responsibility |
|---|---|
StandardRevision |
Words, ordered term word IDs, domains, naming policy, revision identity, source locations |
DesignRevision |
Dialect, names, tables, columns, constraints, revision identity, source locations |
CatalogSnapshot |
Same object graph plus opaque binding, adapter version, UTC acquisition time, explicit scope and facet coverage |
TypeDescriptor |
Original native token, type schema, length/unit, precision/scale, timezone, array rank, domain/enum references, collation |
Attribute |
VALUE, ABSENT, UNKNOWN, NOT_APPLICABLE, typed scalar and closed reason code |
Identifier |
Original decoded identifier text and UNQUOTED, QUOTED or CATALOG origin |
LogicalName |
Observed name and explicit DESIGN/COMMENT/EXTERNAL_MAPPING/NONE source |
SourceLocation |
Object ID, source SHA-256 and source JSON pointer, not a filesystem location |
Finding, FindingSet |
Stable result IDs, rule version, verdict/reason and deterministic ordering; no evaluator |
known(value), absent() and unknown(reason=Reason.NOT_COLLECTED) construct
observations. Null payloads mean no observed value and require a non-VALUE state;
SQL DEFAULT NULL, when observed, is an expression string, not JSON null.
Unknown observations require a reason; absence is not lack of permission.
Catalog coverage explicitly lists TABLES, COLUMNS, CONSTRAINTS, LOGICAL_NAMES,
TYPES and DEFAULTS. Incomplete catalogs must omit unresolved relationship edges
and mark coverage partial rather than claiming a closed complete graph. Domain/enum
type references are opaque adapter references, not automatically resolved objects.
Approval evidence and tenant authorization remain the consuming application's job.
Normalization and Digests¶
normalize_identifier(identifier, dialect) and normalize_type(type, dialect)
return an Attribute, not an equivalence verdict. Unsupported input is UNKNOWN.
PostgreSQL majors 14 and 17 are built in. An immutable NormalizationRegistry supports
explicit local injection; no uploaded code or automatic version fallback is used.
Injected normalizers do not alter the built-in versioned semantic digest policy.
- Decoded quoted/catalog names are preserved byte-for-byte. Unquoted ASCII names are folded to lowercase. Non-ASCII unquoted input and names exceeding the default PostgreSQL UTF-8 63-byte identifier limit are UNKNOWN, never truncated or guessed.
- Resolved built-in type tokens require
type_schema=known("pg_catalog")and no domain/enum override. Exact aliases such as int4/integer and decimal/numeric are normalized. Qualified tokens, SQL declarations such asnumeric(10,2), serial pseudo-types and custom types are not parsed or guessed. - All facets remain present. Length units, arrays, numeric negative scale (including scale greater than precision), timezone and collation are not erased. Contradictory temporal timezone observations are UNKNOWN. No SQL/default expression is executed.
- Logical labels use NFC with Python's fixed Unicode 3.2 database for reproducibility across supported Python versions. Later Unicode additions are preserved, not fully normalized. Physical names, abbreviations and SQL expressions are never NFC-folded.
raw_digest(bytes) hashes exact original bytes. semantic_bytes(model) uses the
truthound.schema-audit.semantic/1 profile and pg17-ascii-nfc32-logical-v1 policy;
semantic_digest(model) is its SHA-256. This is not RFC 8785/JCS. Object keys
are sorted, ID-addressed collections are sorted by ID, and coverage/scope order is
canonicalized. Composite-key columns, FK pairs, word IDs and column ordinals retain
their semantics. Root ID/revision, sources, capture time, adapter version and binding
are excluded; object IDs, dialect and coverage are retained. Envelope identity and
authorization must therefore be bound separately by callers. Different object IDs
need not hash equally even if a human considers two diagrams equivalent.
Standards-to-Design Audit A¶
from truthound.schema_audit import audit_standards, load_document
# Both arguments are previously validated immutable models, not DB connections.
def audit_bytes(standard_json: bytes, design_json: bytes) -> dict:
return audit_standards(
load_document(standard_json), load_document(design_json)
).as_dict()
audit_standards(StandardRevision, DesignRevision, *, bindings=()) returns a frozen
StandardsReport. The standards-v1 pack checks ASCII naming syntax/case/byte
length, exact NFC logical dictionary lookup, ordered abbreviation composition,
deprecated terms/words, domain type and all explicit facets, and physical-name
uniqueness. Table labels resolve exact words or terms; columns require terms.
No substring, translation, morphology or fuzzy match can produce a PASS. Prefixes
and suffixes must be explicit dictionary words; there is no arbitrary regex policy.
Domain assignment is inherited from the resolved term's domain_id. A descriptor's
domain_ref represents a DB user-defined type, not a standard-domain identifier.
Missing logical names produce FAIL; unknown or ambiguous labels produce UNKNOWN.
Unresolved terms leave domain checks UNKNOWN. Dangling domain/FK references are
rejected by the common model before execution, not silently repaired by this pack.
Unsupported dialects or custom types remain UNKNOWN; known facet differences still
produce independent FAIL findings. Character length and byte length are not equated.
Every check records rule/version, target, expected/observed observations, both input
source locations, standard IDs and a deterministic fingerprint. Suggestions show an
expected physical name but never rewrite the model or erase a violation. Trusted
callers may supply TermBinding values with approval and both semantic input digests;
these values do not authenticate approval themselves. HTTP consumers must resolve
approval from their own authorized repository. Workflow v1 does not accept caller bindings.
The report pins input IDs/revisions/semantic digests, mapping digest and rule-pack
digest. Policy changes require a new rule version. Verdict is NONCONFORMANT if any
check fails, INDETERMINATE if any required check is unknown or no targets exist,
otherwise CONFORMANT. Coverage retains FAIL and UNKNOWN independently by rule;
worker completion is not an audit PASS. The report JSON is a separate output contract
truthound.standards-audit/1, not an input to load_document or json_schema().
Execution is bounded to 10,000 tables plus columns and 32 source locations per object. Reports permit at most 400,000 checks without relaxing the 50,000-item input array limit. These are Core computation budgets, not an end-to-end Workflow capacity promise: Workflow applies separate 20 MiB input, 80 MiB raw-result and 16 MiB result-envelope budgets and rejects oversized results. The result envelope may use bounded lossless compression. Abbreviation combinations exceeding 4,096 code points are UNKNOWN without building an unbounded concatenation. Repeated term resolutions are cached within one run. These are safety limits, not a throughput claim. This engine does not execute SQL, inspect row values, approve a revision or operate a Release gate.
Audit A References¶
The public ISO 11179-31 abstract informed
separate term/domain concepts. SHACL validation reports
informed target/path/constraint evidence, not RDF/SHACL compatibility or its verdict
semantics. Rahm and Bernstein, On Matching Schemas Automatically
informed the boundary between a match candidate and trusted correspondence.
Helland, Immutability Changes Everything
informed fixed-input replay. Kleppmann's DDIA author overview
was consulted for architecture trade-offs, not treated as a fully read textbook.
These are design references, not certification. Rule acceptance uses exact and
metamorphic synthetic tests in tests/schema_audit/test_standards.py.
Foundation Research Basis¶
These references inform design, not certification or proof of correctness. Accessed 2026-09-26; paid books/standards were not read in full.
| Reference / reading scope | Applied decision |
|---|---|
| ISO/IEC 11179-3:2023, official abstract | Separate registry identities, labels and mappings |
| PostgreSQL 17 identifiers, identifier section | Preserve quoted/catalog names and explicit origin |
| PostgreSQL 17 numeric types, numeric section | Preserve negative scale and precision independently |
| RFC 8785, canonicalization section | Separate raw bytes from domain normalization; do not claim JCS compliance |
| JSON Schema 2020-12, vocabulary/validation overview | Publish structural schema; retain semantic validation |
| Unicode normalization, normalization/stability sections | Explicit, version-pinned logical normalization only |
| Cupid, Madhavan et al., research abstract | Do not equate name similarity with exact conformance |
| Barr et al., Oracle Problem, abstract | Fixed independent byte vector and metamorphic tests |
| Specification by Example, Adzic, publisher overview | Concrete normal/invalid acceptance examples |
| Database System Concepts, 7th ed., author chapter index | Keep relational structure distinct from future semantic design auditing; no claim of full book review |
Verification Scope¶
The portable suite is tests/schema_audit/test_contract.py. It exercises malformed
input, immutable graphs, unknowns, normalization and deterministic digest vectors.
Package resource checks run against wheel/sdist artifacts; existing validation and
public-surface regressions remain separate. Actual eXERD import, live database
comparison and Console end-to-end validation are later-phase gates.