Skip to content

Feat/portable design standards - #77

Merged
earthshiner merged 24 commits into
mainfrom
feat/portable-design-standards
Oct 14, 2026
Merged

earthshiner merged 24 commits into
mainfrom
feat/portable-design-standards

Conversation

@NathanG-TD

Copy link
Copy Markdown
Collaborator

Portable design standards: one specification, three engines

Summary

A build now has exactly three inputs: the design specification (the design agent's output, which names no platform, container or organisation), an organisation profile (one per organisation per platform), and the platform binding. tooling/build/build_context.py resolves them into one build context, and every binding's Jinja templates read only that context. They derive no names and hold no product literals.

PostgreSQL, DuckDB and Teradata are all on the build context. Both reference specifications (IT Service Desk and Customer Orders) deploy on all three engines, and every check the build renders passes. One specification gives every binding the same relations and the same checks, and a test holds them to that.

This PR includes PR #74 (Jinja-only bindings), which was merged into this branch rather than main.

What changes

Standards (design/)

  • Platform Implementation Authoring Standard: the three build inputs; the build context; binding declarations (Binding:, Types:, Functions:) that bring a platform's SQL into the context as data; a new Gaps field for gaps no check can detect; and conformance status that follows generated evidence (§8.1).
  • Design Specification Standard and Organisation Profile Standard: what each input carries, in tables the linters and the resolver read as the rule.
  • Settled where the bindings disagreed, each now enforced by a test:
    • Semantic §5 bounds path discovery at four hops (PostgreSQL and DuckDB walked four, Teradata three). The resolver passes the bound to every binding.
    • Validation §10: a legacy run's derived PRODUCT trust entry is graded by §4.3 and capped at partial. PostgreSQL and DuckDB had shown partial even with ERROR failures.
    • Validation defines ValidationRun and ValidationArea as entities. The standard-owned discovery and trust-map views are consumer views placed by the profile. base_view is an optional role that a binding declares it emits.
  • No platform names in design/: a new platform-name lint rule, with no ignore-file escape. The Design Language's Appendix A, a hand-kept copy of the bindings' physical types that had drifted (native VECTOR on Teradata, pgvector, vss), now points at the bindings' declarations.

Tooling

  • tooling/build/: the profile checker, the binding-declaration loader and the resolver. tooling/evals/expressions.py renders the expression vocabulary through each binding's declared SQL fragments, and reports any function a binding lacks rather than approximating it.
  • tooling/bindings/render.py renders a binding's declared phases, deploy script and checks into a manifest that lists every gap. validate.py runs the checks and publishes wire-schema 2.1 evidence through a driver per engine (engines.py), with no platform == ... branches.
  • tooling/bindings/conformance.py generates each binding's CONFORMANCE.md from the tests, the reference builds and the binding's declared gaps, replacing the hand-written files. --check fails on an out-of-date report and names any gap it adds.
  • tooling/skill/verify_skill.py --package checks what an install actually receives (git archive HEAD). {platform} routes are now checked on every binding; they were hard-coded to Teradata.

Bindings

  • PostgreSQL and DuckDB: rewritten as templates over the build context; the old per-platform inputs (TEMPLATE_INPUTS.md, entities.json, context.json, placement.json) are gone.

  • Teradata: moved onto the build context and engine-tested on a ClearScape system. The real engine found and fixed:

    • no VECTOR type on 17.20 or 20.00.24, so a Vector[n] is VARBYTE(n × 8) of little-endian 8-byte floats, checked to that length. TD_VECTORDISTANCE reads VARBYTE as 8-byte floats; with 4-byte floats every distance was wrong, and no error was raised;
    • GRANT ALL cannot go to a role;
    • grants ran before the objects they grant on existed;
    • interval literals take no precision;
    • an untyped NULL in a union branch fails only when the view is queried;
    • hours_between and days_between are now exact.

    Similarity search on Teradata is now rendered: one stored procedure per Embedding: block runs TD_VECTORDISTANCE over the store's current vectors, as dynamic SQL because the operator takes no parameters. It is engine-tested for ranking, threshold, limit and a wrong-dimension query. A new Deploying user setting grants CREATE PROCEDURE, which a database's creator does not get, and the read and agent tiers get EXECUTE PROCEDURE. Types may be sized as {dim*n}, and the deploy splitter keeps a procedure as one statement.

    test_teradata_dialect.py holds every rendered statement to PLATFORM_PROFILE §7. Teradata now grants its governed history views to the read and agent tiers, as PostgreSQL grants its point-in-time functions.

  • Adoption, on every binding: an engine test deploys over an existing table. It found every binding treating an adopted container as its own: changing its grants, flagging the organisation's other tables as unregistered, and publishing their metadata. All three are fixed.

Examples

  • examples/it-service-desk-data-product/profiles/ holds the organisation profiles: PostgreSQL, DuckDB and two contrasting Teradata ones.
  • The generated customer360-duckdb example and the binding-specific inputs under the ITSD example are removed: examples hold inputs, not generated output (Authoring §7).

CI

.github/workflows/validate.yml runs the validation suite, then conformance.py --check on DuckDB, PostgreSQL (service container) and Teradata (TERADATA_TEST_DSN secret), and fails on any skip. docs/ci-setup.md covers the setup.

Before merging

  1. Decide on the graph-lineage and catalogue-interface templates (port or retire). They still read the old flat context, and no phase renders them. Two test_binding_package tests fail on them alone, so CI cannot go green until this is settled. They are left untouched pending that decision.
  2. Add the TERADATA_TEST_DSN repository secret (docs/ci-setup.md). Until then the Teradata tests skip and CI fails, by design: a missing engine is a gap, never a pass. Then make validate a required check.
  3. Delete REMAINING_WORK.md. It is the handoff note for this branch, not part of the corpus.
  4. PR feat: declare product layout so evaluators stop assuming names #75 (layout standard): update it or withdraw it. The organisation profile now covers layout and naming.

For review

  • History for consumers. Every binding now grants full history to the read and agent tiers, including the past values of personal-data columns. The access-layer grant matrix (§5) never names the governed full-contract surface. Confirm the intent and add the row.
  • Tables apart from views. Teradata best practice keeps them in separate databases, so grants can be database-level. That is planned as a separate PR, applied to PostgreSQL and DuckDB too, so every binding has the same layout.
  • In-database encoding. mldb.ONNXEmbeddings returns VARBYTE as 4-byte floats, so vectors encoded in Teradata must be converted to 8-byte floats before they are stored. Whether the function can emit 8-byte floats directly is untested.
  • Adoption modes. Only the reference mode is implemented. Authoring §4.3 says the modes are "defined separately", but they are not defined yet.

Evidence

PostgreSQL DuckDB Teradata
Engine 16.4 (psycopg 3.3.6) 1.5.6 (Python driver) 20.00.24.60 (teradatasql 20.0.0.71)
Tests passed 36 36 39
Tests failed 0 0 2 (the untouched templates above)
Tests skipped 0 0 0
Gaps the builds report 13 14 17
Gaps the binding declares 8 11 13

From the last full conformance.py run, on all three engines; each binding's CONFORMANCE.md has the detail.

  • Compositions: both reference specifications, each with all six modules. Smaller compositions are not shown to deploy (a declared gap).
  • Portability: the IT Service Desk specification builds under four example profiles (PostgreSQL, DuckDB and two contrasting Teradata ones), and under a renamed variant of each platform's profile that changes every container name and no template. Customer Orders builds under each platform's profiles. No output carries another case's names.
  • Profile settings: Teradata declares Container space; the profiles' mask protection and object-storage placement are not rendered by any binding and are reported as gaps.
  • Validation suite: 253 of 255 pass, with the same two failures. Design lint, catalogue and skill-package checks are clean.
  • Every binding's status is draft. None is gap-free, so none may be standard (§8.1), and a package test now enforces that.

How to verify

python -m unittest discover -s tooling/validation/tests
pip install -r tooling/bindings/requirements.txt duckdb pytz "psycopg[binary]" teradatasql
POSTGRES_TEST_DSN="..." TERADATA_TEST_DSN='{"host": "...", "user": "...", "password": "..."}' \
    python tooling/bindings/conformance.py --check
python tooling/skill/verify_skill.py --package

A full Teradata run takes 17 to 55 minutes, depending on the system's load.

earthshiner and others added 24 commits October 9, 2026 14:24
Run the validation and binding suites on every pull request to main,
with PostgreSQL available so engine tests execute, and fail on any
skipped test since a skip is a conformance gap, not a pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The five validation templates become *.sql.j2 reading db and semantic_db from
the build context. The SQLite behavioural harness now renders them under
StrictUndefined through td_context instead of reading plain files.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The registry and catalogue-interface templates read governance_db, semantic_db
and stage from the build context; the shared governance container is no longer
a literal.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The temporal maintenance, view, statistics and conformance specimens take the
table, keys and views from the context. The access layer renders Search and
Prediction grants only when those modules are in the composition.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every Teradata template now has a render case, and a hygiene test asserts no
plain .sql, no substitution convention other than Jinja, no governance
container literal, StrictUndefined failure on a missing value, composition
honoured, and that TEMPLATE_INPUTS.md documents every input. The memory capture
protocol becomes a data-driven Jinja loop and documentation tokens in earlier
templates are reworded. Reference docs follow the renamed files.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolve the allocator sequence name and column pii/classification defaults in
the build tooling so templates read them instead of defaulting. Reject a roles
setting on a platform without roles. Make validation summaries severity aware
and accept an explicit run id. Add template-coverage, CLI, trust-case, mutation
and lineage tests shared by every Jinja binding.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replace the hand-written SQL, the Python model/render/checks/temporal/validate
compiler and the generated Customer360 example with *.sql.j2 templates and
entities.json declarations driven by tooling/bindings. Add TEMPLATE_INPUTS,
rewrite the platform profile, conformance scope (draft, with declared gaps) and
module and pattern bindings, add the ITSD DuckDB placement input and walkthrough,
DuckDB engine tests and a binding hygiene test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PostgreSQL refuses an overlapping row through its exclusion constraint; DuckDB cannot, so its check must detect it. Both outcomes are non-silent.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
refactor: make Jinja the only template language across all bindings
The PostgreSQL and DuckDB bindings took their facts from per-platform
inputs (TEMPLATE_INPUTS.md, context.json, placement.json) and from
Python defaults. This adds those facts to the shared pipeline, sourced
from the design specification, the organisation profile and the
binding's own declarations, so bindings can move onto the build
context.

- Authoring standard §5.1: each PLATFORM_PROFILE.md declares Binding
  (quoting, principals, emitted roles, settings), Types (logical to
  physical) and Functions (expression vocabulary to SQL fragments, plus
  duration, map case, literals and flag test). Declarations added for
  Teradata, PostgreSQL and DuckDB; tooling/build/binding.py loads and
  validates them.
- tooling/evals/expressions.py: parses the expression vocabulary to a
  tree and renders it through a binding's fragments; a function the
  binding does not declare is reported, never approximated.
- Design Specification Standard: every entity has a Description section
  and every attribute a // description; spec_lint enforces both, and
  both reference specifications carry them.
- Organisation profile: optional sequence role; optional roles a
  binding emits need templates.
- Resolver: entity and attribute descriptions, physical types, enum
  members, vector dimensions, the canonical temporal columns each
  profile carries (mapped names, physical types, meanings), deletion
  support, relationships derived from references, standard-owned
  relations shaped from their module's entity definitions, emitted
  sequences and functions placed by the profile, expressions with
  rendered SQL (derivations, embeddings, metrics with inlined metric
  references, quality rules, filters), the orientation entrypoint
  container, product version (--version), and the binding's facts.
- 19 new tests; schema updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion relations, optional governed views

Rewriting the PostgreSQL and DuckDB bindings onto the build context
showed objects the bindings had named and shaped themselves.

- Standard-owned views: Semantic's ColumnCatalogue, DataProductManifest
  and RelationshipPaths, and Observability's TrustMap, join LineageGraph
  and LineageRunLatest. They are consumer views placed and named through
  the organisation profile. Every standard relation outside the Memory
  runtime facet has a consumer view.
- The validation pattern defines ValidationRun and ValidationArea as
  entities, so a build creates them from the design; standard relations
  are shaped from pattern as well as module definitions.
- base_view is an optional role, placed only where the binding declares
  it emits a governed tier (Teradata does; PostgreSQL and DuckDB do not).
- A standard relation's own placement rule and mapped name apply to the
  relation itself (its table, or the view that is the relation); its
  other views follow their role rules.
- Bindings declare Phases and Checks, and may emit a similarity function
  per Embedding block, placed by the profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both bindings now render only from the resolved build context: shared macros,
module tables from the standards, registration, discovery, capture, similarity,
trust map, checks and grants, ordered by the Phases, Deploy and Checks each
PLATFORM_PROFILE declares. render.py takes a specification and an organisation
profile; validate.py reads physical names from the manifest and publishes wire
2.1 evidence. Both reference specifications deploy on PostgreSQL 16 and DuckDB
1.5 with every rendered check passing.

Standards and tooling made consistent along the way: Decision modules,
embedding shape and the Memory capture floor are linted; outside principals,
Runtime scope, binding settings with defaults and procedure similarity are
declared; the product registry stays in its own Semantic store; the derived
trust-map entry is limited to legacy wire versions. Old per-binding inputs
(TEMPLATE_INPUTS, entities.json, context.json, placement.json) are retired.

The Teradata migration is not in this commit; REMAINING_WORK.md records it and
the other open tasks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_WORK.md)

Partial port of the Teradata templates to the build context. Old templates are
not yet retired and the old Teradata tests still target them. Predates the
review fixes on feat/portable-design-standards (registration guard,
discriminator-aware reference check, container-grant rule, Runtime scope).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Teradata now renders only from the resolved build context, like PostgreSQL and
DuckDB, and is engine-tested on Teradata 20.00.24 (ClearScape): both reference
specifications deploy and every rendered check passes. The shared engine tests
run unchanged on all three engines; an engine says only how it writes a flag,
an open interval end and a vector, and how it reads history.

Teradata fixes the engine found: vectors are LATIN JSON-array text with a
dimension check (no VECTOR type on 17.20 or 20.00.24); the admin tier gets an
explicit list of rights, since ALL cannot go to a role; implied grants run
before the views and are narrowed to what each view reads, tier grants run
last; interval literals and NULLs in union branches are typed; maintenance
casts its instants. hours_between and days_between are declared, exact to the
microsecond.

Conflicts between bindings, fixed in the standard and every binding, each with
a test: Semantic section 5 now bounds path discovery at four hops, carried as
RelationshipPaths.max_hops; a legacy run's PRODUCT trust entry is graded by
Validation section 4.3 and capped at partial (PostgreSQL and DuckDB said
partial even with ERROR failures). render.py now reports classification
protection, object-storage placement and missing consumer point-in-time
surfaces as gaps.

The old flat-context templates and their tests are retired, except the
graph-lineage and catalogue-interface templates, which await a decision.
test_teradata_dialect.py holds every rendered statement to PLATFORM_PROFILE
section 7. validate.py and the tests use one driver per engine (engines.py).
Teradata docs, CONFORMANCE.md and statuses (draft) updated; the skill's roles
point at the build manifest's checks; CI installs teradatasql and reads
TERADATA_TEST_DSN from a secret.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Teradata now grants the governed base view of every versioned relation to the
read and agent tiers, one view at a time, as PostgreSQL grants its
point-in-time functions (temporal-lifecycle-metadata section 9). Base views can
share a database with tables, so these are never database grants. The engine
test reads the read role's rights from DBC.AllRoleRightsV: it reaches the
consumer and history views and holds nothing on the table or its database.

render.py counts a governed base view as a history surface, so the history gap
no longer fires on Teradata. The example profiles' prose no longer says the
governed views are withheld. Separating tables from views on every binding,
and naming the history surface in the access-layer grant matrix, are left to
their own change (REMAINING_WORK.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…duct

design_lint gains platform-name: a document under design/ may name no
platform, neither one with a binding under implementation/ nor one the linter
lists as known. Unlike the SQL rules it has no ignore-file escape. The list of
known platforms moves out of the Design Specification Standard into the
linter, beside the SQL tokens, and spec_lint reads it from there, so stating
the rule no longer means breaking it.

design/ is made to pass: the Design Language's Appendix A, a hand-kept copy of
the bindings' physical types and capabilities that had already drifted (it
claimed a native VECTOR on Teradata, pgvector and vss), now points at each
binding's declarations; the boundary test, the frontmatter example, the
placement pattern's platform terms and two change-log entries no longer name a
platform.

A new rendering test shows one specification, under each platform's example
profile, places the same relations and renders the same checks, with the same
ids and scopes, on every binding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tooling/bindings/conformance.py runs the binding and package tests once,
attributes every outcome to the binding it evidences (an engine class's
platform, or a subtest's platform parameter), renders both reference
specifications under every example profile per platform, and writes each
implementation/*/CONFORMANCE.md: outcomes, failures, skips, every gap the
builds report, and the gaps the binding declares in a new Gaps field of its
Binding: block (Authoring Standard section 5.1). The hand-kept reports are
replaced. --check fails when a committed report is out of date and names any
gap it does not carry; CI runs it in place of the plain binding suite and
uploads the regenerated reports. A package test requires each report to be
generated and no binding document to claim standard while its report is draft.
A new engine test records the engine and driver versions the evidence comes
from.

verify_skill.py --package builds what an install receives (git archive HEAD)
and checks the entry files, every route, and that nothing .gitignore excludes
is shipped. {platform} routes must resolve on every binding, and the platforms
are discovered: they were hard-coded to Teradata.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heck

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new engine test, run on DuckDB, PostgreSQL and Teradata, binds ITSD's
Category to an existing table the organisation already has, beside an
unrelated table of its own. The build must create nothing in the table's place
and change nothing on it, serve its rows through the product's views, register
it as the product's own, pass every check, and leave the unrelated table alone
(Authoring Standard section 4.3, reference mode).

It found every binding treating an adopted container as its own. PostgreSQL
revoked and re-granted privileges on an adopted schema, GRANT ALL ON ALL
TABLES to the admin tier included, and Teradata granted admin rights on an
adopted database; both now grant only on containers the product owns. The
module semantic check flagged the organisation's other tables in an adopted
container as unregistered, and ColumnCatalogue published their metadata; both
now use an in_product macro per binding: every object in an owned container,
and only the adopted tables in an adopted one.

Wrap and migrate modes, canonical names over column aliases, and Teradata's
implied grant on an adopted database are declared as gaps. The conformance
reports are regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Vector[n] binds to VARBYTE(n x 8), little-endian float64 checked to that
length, the encoding TD_VECTORDISTANCE reads. Similarity is one stored
procedure per Embedding block, running TD_VECTORDISTANCE as dynamic SQL
over the store's consumer view, since the operator takes no parameters.
A Deploying user setting grants CREATE PROCEDURE where it is needed; the
access layer grants EXECUTE PROCEDURE to the read and agent tiers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earthshiner

Copy link
Copy Markdown
Collaborator

Heads-up from #76, which is based on this PR.

The two failing test_binding_package tests are the catalogue-interface and graph-lineage templates listed under "Before merging" item 1. #76 does not change those templates or the tests, so it inherits exactly these two failures and does not mask them.

We would rather not delete the templates, and we would not exempt them from the gate. Our suggestion is a follow-up PR after this one merges that settles where the governance, stage and graph containers are placed (nothing in the profile standard places them today) and then ports both sets onto the build context. A tracking issue is open for it: #78.

Question for you: do you want to settle port-or-retire in this PR, or are you happy for it to be handled separately?

@earthshiner earthshiner 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.

See my latest comments about the 2 Teradata test failures and how we might deal with them. I raised an issue #78

@earthshiner
earthshiner merged commit 9c0e5fb into main Oct 14, 2026
1 check failed
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