What DataHub can carry, and the measure it cannot¶
Status: accepted as a finding, and now acted on. The finding was the deliverable; the adapter it
scheduled has since landed as sutura-catalog-datahub - a declaring SemanticCatalog that reads
recorded entity aspects and declares it provides structure, descriptions and the join columns and no
metric layer. Everything it decides is tested against a fake reader over recorded aspects; the HTTP
client over the versioned OpenAPI entity surface is still owed, and the read-path cost this record
left open has since been measured against a provisioned instance. Issue #202 changed the no metric
layer half of that sentence, and the Amendment, 2026-09-02 at the foot of this page is where the
change is recorded - read it before citing provides no measure above, read the Addendum under it
before citing decision 7 or the Required filters argument, both of which it narrows, and read
Revision, 2026-09-04 below that before citing anything as unmeasured against a live instance.
Reframed after review, and the reframe is recorded rather than smoothed over. The measurements
below are unchanged and were independently verified. What changed is the conclusion drawn from one of
them: the first version read the catalog conformance matrix as every adapter's contract and concluded
that DataHub needs a composition step before it can be useful. It does not. The oracle test is the
golden adapters' contract, and everything else conforms or declares - which is what
pluggable by declaration already decided and what
sutura-catalog-local's own documentation already says about itself. The withdrawn reading is written
out in Alternatives considered rather than deleted, because the reasoning that produced it is the
reasoning somebody will produce again.
AGENTS.md's crate table lists a -datahub crate as planned. Four accepted records name DataHub as a
metadata connector -
pluggable by declaration,
conformance packs,
federating across different data systems and
the plan - and until this record
docs/implementation-plan.md's stack table had no row for it at all. This
record closes that gap, and it puts the question before the row because the answer decides the row's
dependencies.
The question¶
Can DataHub express a complete semantic model as this repository defines one, or only part of one?
Three answers were possible and each implies different work:
| If DataHub supplies | It is | What it costs |
|---|---|---|
complete Definitions |
a second adapter held to the golden adapters' own contract | low - a registration, as the invariant promises |
| descriptions and a glossary only | a Knowledge source, which this repository confines to descriptive content with the prompt as its only consumer |
a different feature, arguably a different port |
| part - the physical model and the prose, the measure still authored | a declaring adapter: it says which kinds it provides and which it does not, and is tested against that | a required capability declaration on SemanticCatalog, which does not exist yet |
It is the third. The reason is not the one that was expected, and the difference matters enough to
be the substance of this record: the expectation was that DataHub has no metric at all. As of
1.7.0 it has one, it is category: core, and what it holds a measure as is a raw expression
string tagged with a dialect.
The third row's cost is written as it is now understood, and it is smaller than the first version of this record said. That version priced it as a merge story - an assembler over several sources plus a contribution manifest that moves every committed digest. The declaring reading prices it as a required associated item on one port. Both are real work; only one of them is in the way.
What was measured against¶
Read out of the code rather than out of the prose, because the prose is a summary and a port is a
signature. SemanticCatalog::load returns a PinnedDefinitions; its two halves are a Definitions
and a Knowledge; and Definitions::assemble is the only constructor of the first. So can a source
express our model is really can it fill these fields, and survive these checks.
| What has to be filled | The type in sutura-domain |
|---|---|
| a physical table and the column set it exposes | Model - ModelName, SourceName, TableName, BTreeSet<ColumnName>, Description |
| a join | Relationship - a name, two (ModelName, ColumnName) endpoints, and a JoinType of OneToOne, ManyToOne or OneToMany |
| what a metric measures | Measure - Simple(Term) or Ratio { numerator, denominator, zero_denominator }, over a Term of Aggregate(AggregatedColumn) or CountIf { column }, where Aggregate is the closed set sum, count, count_distinct, avg, min, max |
| a predicate that is part of what the metric MEANS | Vec<RequiredFilter> - Equals, NotEquals, IsTrue, IsNotNull, values typed as DimensionValue |
| when, and at what resolution | ColumnName plus a BTreeSet<Grain> |
| what it may be broken down by | Dimension - a column, optionally via one relationship, optionally an allowlist of at most MAX_VALUES_PER_DIMENSION values |
| the number it produced when it was certified | Option<Anchor> - a TimeRange and a value as text |
| what a reader has to know | Knowledge - phrases, caveats, reviewed absences and worked examples, each carrying a Referent that names a metric, a dimension of one, or a declared value of one |
Two of those are worth restating because they are the checks rather than the fields.
Definitions::assemble refuses a dimension reached through a relationship whose declared
cardinality may duplicate rows, as JoinWouldDuplicateRows. And every field above is a type with no
free-text SQL anywhere in it: Measure has no expression: key and no Option<String> at any depth,
which is the load-bearing half of
first-party semantic models.
What DataHub 1.7.0 actually has¶
Everything below was fetched on 2026-08-29 from DataHub's public documentation and from the
public datahub-project/datahub repository. Where a schema is quoted it is the .pdl file rather
than the rendered page, because the page paraphrases and the schema does not. Nothing here comes
from any deployment.
Metrics and semantic models are new, in Beta, and off by default. The feature guide says so in its
own words: "Metrics & Semantic Models is currently in Beta. The underlying entity model is stable,
but the UI experience, lineage visualization, and ingestion coverage are actively evolving." Core
deployments enable it by setting METRICS_ENABLED=true on the GMS service; a managed deployment needs
version 2.1.0 or later and a per-tenant flag. Both halves of that are published
(feature guide,
read 2026-08-29). The release is v1.7.0, published 2026-08-04, and the entity index at
docs.datahub.com reports 1.7.0 as the
current version. metric and semanticmodel both appear in that index, both are category: core in
metadata-models/src/main/resources/entity-registry.yml at the v1.7.0 tag, and the whole
com/linkedin/metric package is absent at the v1.6.0.1 tag - checked by asking the contents API
for that directory at both refs and getting a listing at one and a 404 at the other. So this is one
release old, which is the single most important fact about it.
And DataHub says plainly what it is. From the same guide: "DataHub Metrics is a catalog for metric definitions", and its FAQ answers "Can DataHub query my metric values?" with "No. DataHub is a catalog for metric definitions - the calculation, dimensional context, lineage, and governance. Value computation and visualization stay in your BI tool or semantic layer." That is not a limitation being conceded; it is the product boundary, and it is the same boundary this repository sits on the other side of.
The shapes, verbatim¶
| Schema | Fields, as read |
|---|---|
metric/MetricInfo.pdl |
name: string, description: optional string, created, lastModified, semanticModel: optional Urn, expression: optional MetricExpression. That is the entire calculation surface. |
metric/MetricExpression.pdl |
one field: dialects: array[DialectExpression] |
metric/DialectExpression.pdl |
dialect: Dialect, expression: string - doc comment "The raw expression string." |
metric/Dialect.pdl |
ANSI_SQL, SNOWFLAKE, MDX, TABLEAU, DATABRICKS, MAQL, OTHER |
semanticmodel/SemanticFieldAnnotation.pdl |
type: SemanticFieldType, expression: MetricExpression, aggregationFunction: optional string, dimension: optional Dimension |
semanticmodel/SemanticFieldType.pdl |
DIMENSION, MEASURE, FILTER, OTHER |
semanticmodel/Dimension.pdl |
one field: isTime: boolean = false, and the record documents itself as intentionally minimal |
semanticmodel/SemanticModelRelationship.pdl |
name, from, fromColumns, to, toColumns, aiContext, cardinality: optional ERModelRelationshipCardinality |
ermodelrelation/ERModelRelationshipCardinality.pdl |
ONE_ONE, ONE_N, N_ONE, N_N |
schema/SchemaMetadata.pdl |
fields: array[SchemaField], primaryKeys: optional array[SchemaFieldPath], foreignKeys: optional array[ForeignKeyConstraint], plus a deprecated foreignKeysSpecs |
schema/ForeignKeyConstraint.pdl |
name, sourceFields: array[Urn], foreignFields: array[Urn], foreignDataset: Urn. No cardinality field. |
glossary/GlossaryTermInfo.pdl |
id, name, definition: string, parentNode, termSource, sourceRef, sourceUrl, a deprecated rawSchema, plus custom properties. No synonyms field. |
Each of those was read at
https://raw.githubusercontent.com/datahub-project/datahub/master/metadata-models/src/main/pegasus/com/linkedin/<path>
on 2026-08-29, and the rendered entity pages for metric, semanticmodel, dataset, glossaryterm,
businessattribute and structuredproperty were read the same day under version 1.7.0.
The ref each fact came from, because it is not one ref and pretending otherwise is how this record
would go stale invisibly. The field lists above are the development branch. What was checked at
the v1.7.0 tag is narrower and deliberate: that the com/linkedin/metric package exists there,
that it does not exist at v1.6.0.1, and that both entities are registered category: core there. The
field lists were not re-read per tag, so read them as the model as it is being developed rather than
as the model 1.7.0 shipped - and the Beta caveat below is the reason that distinction is not
pedantic: one aspect's schema version already differs between the tag and the branch.
Field by field¶
| What we need | What DataHub has | Verdict |
|---|---|---|
Model.table, .columns |
a dataset entity with schemaMetadata.fields |
faithful |
Model.source |
the dataPlatform URN, and dataPlatformInstance |
faithful, with a name mapping a deployment decides |
Model.description |
datasetProperties, editableDatasetProperties, documentation, institutionalMemory |
faithful, and richer than ours - separate ingested and human-edited prose, and multiple documentation sources |
Relationship endpoints |
SemanticModelRelationship.from/fromColumns/to/toColumns, or schemaMetadata.foreignKeys |
faithful, and WIDER: both are arrays, so a multi-column join is expressible where ours is one column each |
Relationship.join_type |
SemanticModelRelationship.cardinality, and ERModelRelationshipProperties.cardinality |
present, and declared unsupported anyway - see below |
Measure |
MetricInfo.expression, or a MEASURE-annotated field's aggregationFunction |
NOT faithful - see below |
Metric.required_filters |
nothing first-class | absent - see below |
Metric.time_column |
Dimension.isTime marks a dimension as temporal |
partly: which dimension is time, not which column a metric measures time on |
Metric.grains |
nothing | absent. No grain, no resolution, no time-bucket vocabulary anywhere in the two packages |
Dimension.allowed_values |
nothing on a field. glossaryRelatedTerms.values is a term-to-term HasValue edge; structuredProperty has an allowedValues array bounding its OWN value |
absent for a column allowlist. Both near-misses are enumerations over something else |
Anchor |
nothing, and by design - see the FAQ quoted above | absent |
Knowledge glossary |
glossaryTerm with a required definition, glossaryNode for hierarchy, glossaryRelatedTerms for IsA/HasA/HasValue/IsRelatedTo |
partly - rich, and no synonym field; a second spelling has to be a second term related to the first |
| lineage | upstreamLineage, fineGrainedLineages, metricUpstreams with dataset and field edges |
we have no shape at all - see below |
The four sticking points¶
The measure, which is the answer¶
DataHub's measure is a string, and the closed vocabulary has no counterpart in it.
MetricInfo.expression resolves, in two hops, to { dialect, expression: string } per dialect. There
is no numerator, no denominator, no aggregate, no column, and no operator. The official example in
DataHub's own SDK library - metadata-ingestion/examples/library/semantic_model_create.py, read
2026-08-29 - writes a whole metric as expression="SUM(ORDERS.amount)" with
dialect=DialectClass.SNOWFLAKE and nothing else about the calculation. That is precisely the
shape a closed vocabulary for measures exists not to have,
and first-party semantic models states why in one line: "A
string field is an escape hatch, and an escape hatch on the query path is the thing being defended
against."
There is one structured signal, and taking it would be worse than refusing it.
SemanticFieldAnnotation carries aggregationFunction: optional string beside expression, so a
MEASURE-annotated field can say SUM next to the column it annotates - which looks like exactly
AggregatedColumn::new(Aggregate::Sum, column). Three reasons not to harvest it, and the third is the
one that decides:
- It is an open string, not an enum. Parsing it against our
Aggregateset is cheap and is the pattern this repository already uses, so this alone is not an obstacle - it is aparsethat refuses what it does not recognise. optional, and a search of the public ingestion sources suggests it is mostly absent. Of the connectors that emit these entities at all, the one present in the 1.7.0 release populates no aggregation function; the one that does populate it is on the development branch and not in the release. That is a code search rather than a measurement of any deployment, so read it as nothing found rather than nothing exists - a deployment authoring annotations through the SDK would populate the field by hand, exactly as the published example does.- The two fields are independently authored and nothing reconciles them. The published example
writes
aggregation_function="SUM"andexpression="SUM(amount)"on adjacent lines of one field definition, as two values a human typed twice. Harvesting the first and discarding the second means certifying a number from half of a definition whose other half we did not read - and where the halves disagree, the answer is wrong under a certified name, arrived at by omission. That is the exact failure mode this repository is arranged against, andrecurring_revenuein the example catalog is the worked instance of it: the whole definition issum(mrr_cents)and a filter onstatus, and either one alone is a different number.
And the dialect sets do not intersect, which closes the other route. The obvious alternative is to
take expression as authored SQL through the hatch
a named escape hatch for authored SQL decides. It does
not reach: sutura_sql::dialect::ALL is DuckDb, Postgres and ClickHouse, and DataHub's Dialect
is ANSI_SQL, SNOWFLAKE, MDX, TABLEAU, DATABRICKS, MAQL, OTHER. Not one value is in
both sets, and three of DataHub's are not SQL at all - MDX, Tableau's calculation language and MAQL
would not parse in a SQL parser and are not meant to. Getting from ANSI_SQL to our three would be a
translation, and we never translate SQL we did not generate is an invariant with a mechanism behind
it. Even setting that aside, the hatch is in AGENTS.md's Built And Not Wired section: no shipped
binary can execute an authored expression, because the engine generates no SQL and the adapter that
renders is a dev-dependency. So reading metricInfo.expression as a computation would move a refusal
from load time to query time rather than answer anything.
Cardinality: declared, optional, and one value we cannot represent¶
DataHub does declare cardinality, which was the open question, and the news is better than
expected and smaller than it looks. SemanticModelRelationship.cardinality is an
ERModelRelationshipCardinality of ONE_ONE, ONE_N, N_ONE, N_N. Three of those map onto
JoinType directly. N_N has no JoinType variant, so a relationship declaring it has to be
refused by the adapter - and there is no InconsistentDefinitions variant for a cardinality we
cannot represent, because until now no source could state one. That refusal would be the adapter's
own typed error, which is where a source's vocabulary is supposed to be mapped.
Four limits, each stated next to the claim, and the second one is the reason this section reads as a warning rather than as good news:
- It is
optional. A relationship with no cardinality declared cannot be used to reach a dimension at all, becauseDefinitions::assembleneeds aJoinTypeand the safe default does not exist: assumingManyToOneis assuming the fan-out away, and assumingOneToManyrefuses every dimension. So absent means refused, and absent is the ordinary state of ingested metadata. - On the PHYSICAL relationship it is not optional - it defaults to
N_N. The other consumer of that enum isERModelRelationshipProperties, and the field there readscardinality: ERModelRelationshipCardinality = "N_N"- verified at thev1.7.0tag. A default of many-to-many is a default of the most permissive value, which means a relationship nobody thought about carries the same declaration as one somebody decided was many-to-many, and the two are indistinguishable. For us that is the harmless direction only by accident:N_Nmaps to noJoinType, so both get refused. The direction to be careful about is a future temptation to "read the cardinality and use it" - that would be reading a default as a decision, which is the precise failure mode 0011's evidence argument is written to avoid.AGENTS.md's trusted-precondition caveat gets weaker on this input, not stronger, and this record refuses to describe it as an improvement. - It is a declaration, exactly as trusted as ours.
AGENTS.mdrecords that catalog cardinality is a trusted precondition: nothing checks the declaration against the data. Reading it from DataHub does not close that - it moves the person who is trusted from whoever wrote a markdown file to whoever authored the semantic view, and DataHub validates nothing about the rows. That is a governance improvement in some deployments and a governance question in others; it is not a check. - The join columns are unpaired arrays.
fromColumnsandtoColumnsare twoarray[string]fields with nothing binding index i of one to index i of the other. Ours is a single column each precisely so that no such pairing can be got wrong; a source that carries the wider shape carries the ambiguity with it, and an adapter reading a two-column join is guessing an order. - The evidence path 0011 describes does not come through this aspect.
Pluggable by declaration argues that a primary-key or unique
constraint is evidence, in one direction, for the side being joined to being unique.
schemaMetadata.primaryKeysexists and gives that.ForeignKeyConstraintcarries no cardinality field at all - verified by reading it - so on an ordinary warehouse dataset the join and the uniqueness evidence come from two different places and the cardinality declaration comes from neither. The one-direction argument in 0011 survives intact; what it does not get is a free ride on the new relationship shape.
Required filters: absent, and absent in the way that matters¶
required_filters is the field that makes a metric mean what its name says, and DataHub has no
counterpart.
The metric entity's registered aspect list, read out of entity-registry.yml at the v1.7.0 tag, is
metricInfo, metricRelationships, metricUpstreams, ownership, domains, globalTags,
glossaryTerms, institutionalMemory, structuredProperties, status, deprecation,
dataPlatformInstance, subTypes, documentation, browsePathsV2, applications, aiContext.
No aspect on that list carries a predicate, and none carries a grain either - which is the same
listing that settles two of the absent rows above.
SemanticFieldTypehas aFILTERsymbol, documented as "A named boolean predicate used to filter results, distinct from a dimension". Its body would be aMetricExpression, so it is a SQL string again, and the research found no code path in the public repository that emits it.- There is a record called
DatasetFilter, and it is the nearest thing in the model. It carries no@Aspectannotation, so nothing can hold it as metadata about a dataset; itstypeis an enum with exactly one symbol,SQL, documented as "The partition is represented as a an opaque, raw SQL clause"; and its own comment says "In the future, we'll likely add support for more structured predicates." Its one consumer is a volume assertion's monitoring scope. So the nearest thing to a predicate in DataHub is a monitoring detail, is opaque by its own description, and is not attachable to a dataset at all. - The metric entity's own documentation puts
filters- along withadditivityandmetricKind- intostructuredProperties, explicitly as platform-specific metadata that does not yet warrant a first-class field. A structured property is genuinely typed - five value types, an optionalallowedValueslist,SINGLEorMULTIPLEcardinality, an immutability flag, and a server-side validator that enforces all of it on write. And it is scalar-only.PrimitivePropertyValueistyperef PrimitivePropertyValue = union [ string, double ], verified at thev1.7.0tag, so a structured property value is a string or a number and there is no nested or record-typed form. A four-operator predicate over a named column put there is a JSON string DataHub will validate for length and never interpret. What a structured property cannot carry is therefore not only the obligation - it cannot carry the shape either. Which is the point:
Nothing in DataHub says a filter is REQUIRED. RequiredFilter is not a predicate a caller may
apply - it is one applied to every question about the metric, which a caller cannot see, choose or
remove. That is a statement about how a definition must be used, and a catalog that records a
predicate as a property records a fact, not a duty. So even a deployment that populated a structured
property called filters would be handing us a value we would have to decide to enforce, and
deciding that on a source's behalf is exactly the substitution of our reading for the author's that
0001 refuses.
What DataHub has that we have no shape for¶
Lineage, and it is out of scope rather than useful. upstreamLineage, fineGrainedLineages and
metricUpstreams model dataset-level and column-level derivation. There is no lineage type anywhere
in crates/ - grep -rni lineage crates/ returns nothing - and none is wanted on the query path: a
plan reads at most two data systems, a measure reads columns a model declares, and knowing where a
column came from upstream changes none of it. It is genuinely valuable metadata and it is somebody
else's product. Where it could matter later is provenance for a human reviewing a promotion candidate,
which is a different feature from anything in this repository today.
AiContext is the one that is not out of scope, and it is the closest thing in DataHub to work
this branch has already done. aiContext is a registered aspect on metric and on semanticModel
themselves - read out of entity-registry.yml at the v1.7.0 tag - as well as attaching to a schema
field and to a semantic-model relationship, and it carries synonyms, instructions, examples and
customInstructions - which is very nearly the shape of Knowledge: a glossary phrase, a caveat, a
worked example, attached to the metric it is about. Two things follow. It is the field
a DataHub Knowledge contribution would come from, rather than the glossary alone, because
GlossaryTermInfo has no synonyms field and Phrase is what our glossary is made of. And it arrives
as unbounded free text over a network - four fields, no length cap, no normalisation, no uniqueness
rule - which makes Phrase::parse, NoteBody::parse and MAX_KNOWLEDGE_BYTES load-time gates on a
real input rather than bounds on a file somebody in this repository wrote. That is the first time those
bounds would be doing the job they were designed for. It is also on metric and semanticModel and
not on glossaryTerm, which is worth knowing before assuming the glossary is where a DataHub
Knowledge contribution comes from.
And here is the one place the two designs point in opposite directions, named rather than elided.
DataHub's published roadmap for this feature commits to natural-language questions being resolved
against catalogued metric definitions using those synonyms - which is server-side phrase
resolution. This repository refuses that by construction: RefusalReason has no PhraseNotDefined,
the glossary renders into the agent-facing prompt, and the agent states which metric it chose in its
own transcript, which is what makes the choice auditable. Both designs put a synonym list next to a
metric; one resolves it inside the service and one hands it to the caller to resolve out loud. That
is a real fork and not a gap on either side. For an adapter it means one specific thing: harvesting
synonyms into Phrase is legitimate, and letting anything server-side match on them is the
architecture decision AGENTS.md says a second consumer of knowledge would be.
Also worth recording, because it is the interchange story nobody asked for: DataHub's Dialect
enum documents itself as aligned one-to-one with the Open Semantic Interchange specification. If a
neutral wire format for a metric definition ever matters here, that is where two vendors have
converged, and it is a better place to look than at any one tool's model.
The answer¶
Case three. DataHub supplies part of a semantic model as this repository defines one.
Precisely: it supplies the physical model, the descriptions, the joins with a declared cardinality, and a glossary that needs a metric to point at. It does not supply a measure this repository will execute, a definitional filter in any form, a grain, a value allowlist, or an anchor.
There is a sharp consequence in the test suite, and it is worth stating before the decision because the first version of this record drew the wrong conclusion from it.
crates/sutura-app/tests/golden/catalogs.rs expands seven behaviours over every registered catalog,
and its central one is agrees_with_the_oracle: every registered SemanticCatalog must produce
the same Definitions as the hand-written oracle over examples/single-player. That corpus
contains measure: { simple: { aggregate: sum, column: mrr_cents } } and
required_filters: [{ equals: { column: status, value: active } }] and grains: [month] and value
allowlists and an anchor. A source that cannot express five of those cannot pass that test, and no
amount of adapter code changes it - the missing thing is in the source. That finding stands, and it has
been verified independently rather than taken on this record's word.
What it MEANS is the part this record had to correct. The first version concluded that DataHub therefore needs a composition step in front of it - something to supply the measures so that a combined bundle could satisfy the oracle. That is one reading. It is not the one this repository had already decided, and the difference is not cosmetic: composition-first makes a narrow source unusable until unbuilt work lands, and the reading below makes it usable now.
The reading that matches pluggable by declaration is that the
oracle test is the GOLDEN adapters' contract and not every adapter's. There are two golden adapters.
sutura-catalog-local is the metadata reference - wren-style, where the model and the structures are
defined here, so it can be held to producing all of them - and sutura-exec-duckdb is the data-system
reference. Everything else conforms, or declares what it does not provide. A declaring adapter is
then tested against its own declaration rather than against the reference bundle: what it says it
provides must be exactly what it provides, and what it says it does not provide must be visibly
absent rather than silently missing.
That is not this record's idea to invent. It is in the tree already, in sutura-catalog-local's own
words, and it predicted this case before anybody measured it:
This adapter declares every knowledge capability there is, and that is a statement about the ADAPTER rather than about the directory it read. [...] A metadata-service adapter is the other case: it has glossary terms with synonyms and no way at all to record an absence, so it will declare the two it can represent and never the other two. [...]
KnowledgeCapabilities::allrather than a list of the four, deliberately [...] which is what makes this the reference adapter [...] An adapter mapping a fixed external schema gets the opposite treatment -of([..]), so a new kind leaves its declaration alone.
That prediction is now measured, and it is nearly right and wrong in one checkable place. DataHub
does have glossary terms and does have no way to record an absence, exactly as written. What it does
not have is synonyms on the glossary term: GlossaryTermInfo has no such field, and the synonym list
lives on AiContext instead. The shape of the conclusion survives; the field it names moved.
So the invariant "Adding a metadata provider or a data system is a registration, not a test edit" is true as written, and what this record adds is WHICH test. For a golden adapter it is the oracle. For a declaring adapter it is fidelity to its declaration - still a registration plus a declaration rather than a test edit, because the declaration is what selects the assertions.
And a narrow source is usable on its own, which is the claim the composition-first reading quietly
denied. Checked rather than assumed: Definitions::assemble has no minimum-metric refusal -
there is no NoMetrics variant and no such check anywhere - so a bundle of models, relationships and
zero metrics assembles, pins and validates, because zero metrics is zero anchors. 0011 already says the
prompt can state this deployment carries no certified metric layer as a fact derived from what it was
handed rather than from something a source claimed. A DataHub-only deployment therefore gets a bundle,
a prompt that tells the truth about it, and a path to add metrics. It does not get a refusal at load
for being narrow.
The honest limit on that, stated next to the claim, and it is not about DataHub. What a zero-metric
bundle cannot do on its own is open a data system, and the two composition roots differ - which is
worth getting right because the mechanism moved recently - twice, and the second move dates this
paragraph's own original wording, which said the command-line tool reads no source registry. It does,
since telekom/sutura#121. Both roots read the sources: tree: every source a catalog names is looked
up there, and a missing entry is "this catalog reads from <source>, and no sources.<source> entry
declares where that ...". They differ in what an ABSENT entry means. sutura-serve refuses, full
stop. sutura-cli falls back to its own built-in declaration - a files source called local over
the directory on the command line - so an undeclared source must still be named local there, because
nothing declared it and an engine wearing another name over the caller's own files would answer that
catalog's certified numbers out of them. Both refuse a catalog declaring no models - "this catalog
declares no models, so there is nothing to open" - which a DataHub bundle passes. So a
DataHub bundle whose models carry platform URNs is servable exactly when a deployment has declared a
source per platform it wants to read. That is configuration, not a refusal of DataHub, and it is
the concrete thing the guidance below has to mention. It is written here so nobody reads usable alone
as answers questions alone.
The decision¶
1. There are two golden adapters, and they are the reference everything else is measured against.
sutura-catalog-local on the metadata side and sutura-exec-duckdb on the data side. The reason a
golden adapter can be held to the whole model is that the model is defined here: a wren-style directory
of markdown with YAML frontmatter has a document shape for every field Definitions::assemble needs,
so "produce the same Definitions as the hand-written oracle" is a contract it can meet. That
contract stays exactly as it is and is not weakened by anything in this record.
2. Every other adapter conforms, or DECLARES what it does not provide - and the negatives are the point.
An absence must be declared, never inferred from silence. 0011 decided this and already carries the three-state distinction that makes it work: declared, not declared, and declared-and-empty, with the note that not declared means the prompt must not imply the absence list is complete. A map with no entries cannot tell those apart, which is why the declaration exists at all.
The mechanical shape to reuse is already in the tree, and it should be copied rather than reinvented.
Warehouse::IMPERSONATION is an associated constant with no default, so an adapter that omits it
does not compile - pinned by a compile_fail doctest whose struct is literally named Undeclared,
beside a compiling twin named Declared differing in exactly the one line. Its own documentation gives
the argument in full: "A defaulted capability would mean an adapter that said nothing got the benefit
of the doubt in whichever direction the default pointed - and both directions are wrong." And the
negative has a name rather than being an absence: ImpersonationCapability::NoPlaceForASubject,
whose doc says saying so explicitly is the point.
The contrast next door is the other half of the vocabulary. dry_run is defaulted, and its doc
explains why: "an adapter for which it is not cheaper has no way to say so if the port demands an
implementation, and the honest thing for it to do is nothing." So the rule this record adopts is that
one: a capability whose absence changes what a caller may believe is required with no default; a
capability whose absence is merely a missed optimisation may be defaulted, and the default says why.
The metadata-side analogue does not exist yet. SemanticCatalog today is an associated Error and
load, and nothing on it declares anything. That is the gap this record schedules, and it is the
one thing here that is a change to a port rather than a new adapter.
3. What DataHub declares, concretely - and this is where the research lands.
Not a list of blockers. The content of a declaration, with the negatives named because naming them is what makes the source safe to use. Every does NOT provide below that a deployment-defined structured property can supply is narrowed by the Amendment, 2026-09-02 to a declared-and-empty may-provide, and the adapter's own capability declaration - not this table - is the authority for which those are:
| Kind | DataHub declares | On the evidence of |
|---|---|---|
| Structure - tables, columns, types | provides | dataset + schemaMetadata.fields |
| Descriptions | provides, and richer than ours | four aspects, ingested and human-edited kept apart |
| Relationships - the join columns | provides, with a caveat below | SemanticModelRelationship, or schemaMetadata.foreignKeys |
| Relationship CARDINALITY | does NOT provide | optional on the semantic join; on the physical relationship it defaults to N_N, so a default is indistinguishable from a decision |
| Metrics and measures | does NOT provide | MetricInfo.expression is a raw string in a dialect set that does not intersect ours; aggregationFunction contradicts it with nothing reconciling the two |
| Definitional filters | does NOT provide | no aspect carries a predicate; the nearest record has no @Aspect; structured properties are scalars |
| Grains | does NOT provide | nothing in either package; Dimension is one boolean |
| Value allowlists | does NOT provide | no field-level enumeration; the two near-misses enumerate something else |
| Anchors | does NOT provide | DataHub's own FAQ: value computation stays in the BI tool |
| Glossary phrases | provides, conditionally | AiContext.synonyms plus glossaryTermInfo.definition - and only where the bundle already declares a metric for a Referent to name |
| Caveats | provides, conditionally | institutionalMemory, documentation, deprecation.note - same Referent condition |
| Reviewed absences | does NOT provide | there is no deliberately undefined concept; deprecation and status.removed are different claims |
| Worked examples | does NOT provide | AiContext.examples is free text, and an Example must carry a Query that validates against the metric's grains, dimensions and allowlists |
Two of those are "does not provide" where the field EXISTS, and that is the interesting kind.
Cardinality and the measure are both present in DataHub and both declared unsupported here - not
because reading them is hard, but because reading them would be reading something the source does not
guarantee. The N_N default means an unconsidered relationship and a considered many-to-many are the
same value. The aggregationFunction/expression pair means taking either one alone certifies half a
definition. Declaring those two unsupported is a better outcome than harvesting them, and it is a
better outcome than the composition step the first version of this record proposed: it is one line of
declaration each, it is visible in a diff, and it fails nothing.
4. metricInfo.expression is READ and REPORTED, never executed and never converted.
A DataHub metric is a promotion candidate: a name, a description, an owner, a semantic model, a set
of dimensional fields, and a string somebody wrote in a dialect this repository does not render. What
an adapter may do with the string is show it to a person deciding whether to author a Measure. What
it may not do is compile it, translate it, or infer a Measure from the aggregationFunction beside
it. Unbuilt - a decision about what the adapter will be permitted to do, not a description of
anything that exists.
5. A relationship reaches a dimension only where cardinality is declared and representable.
Absent or N_N is refused by the adapter, naming the relationship, rather than defaulted in either
direction: ManyToOne as a default assumes the fan-out away, and OneToMany refuses every dimension,
so one is unsafe and the other is a silent feature removal. Under decision 3 this is not a
degradation - the adapter declares that it does not provide cardinality, so a deployment reads that
in the declaration rather than discovering it when a dimension is missing. The relationship still
arrives; what it cannot do on its own is license a join.
6. agrees_with_the_oracle is NOT weakened, and a declaring adapter needs a different assertion.
Status: implemented. What this decision specified was the split in the conformance matrix - the
part that "deliberately not written" left open - and the catalog-matrix diff writes it: a required
SemanticCatalog::KIND (CatalogKind, golden or declaring) with no default, a GoldenCatalog marker
that routes the golden-only cells, and a registration whose declaring/golden tag is a compile-time
assertion against that constant - so a registration that disagrees with its own declaration does not
build, and agrees_with_the_oracle (and the three example-corpus cells) cannot be expanded for a
declaring adapter at all. The AGENTS.md registration row states the same split. The oracle test itself
is untouched, held by the golden adapters only, exactly as this decision says it must be. The golden
adapters keep the oracle test.
A declaring adapter gets declaration fidelity, which is two assertions rather than one:
- everything it declared, it produced - for each declared kind, the bundle carries content of that kind, so a declaration is not aspirational;
- everything it did not declare is absent - nothing of an undeclared kind appears in the bundle,
which is the direction
Knowledge::assemble'sUndeclaredContentguard already covers for knowledge and which nothing covers for definitions.
Plus the negative-capability shape 0012 already decided: where a declared absence has something to
try, the pack tries it and the absence must hold, and where there is nothing to perform, no test is
written - because "a green test named ..._is_declared_unsupported_... over nothing is
coverage-shaped and measures nothing". A metadata adapter that declares it provides no grains has
nothing to perform, so what holds there is the fidelity assertion above and not an invented action.
7. Guidance, not requirement - and it is the easiest thing here to overshoot.
A deployment that already runs DataHub gets value from the model it already has: that is decision 3,
and nothing in the docs may turn it into a precondition. So the guidance that lands with the connector
states what a user MAY populate and what each thing buys, once, with the benefit next to it - and
never that DataHub must be configured a particular way for this to work. Concretely, the shape it may
take: a semantic model with cardinality set on its relationships lets those relationships license a
join; AiContext.synonyms on a metric a deployment has also certified lets a glossary phrase render;
and a sources.<alias> entry per platform a DataHub model names is what lets that model's data system
be opened at all. Each of those is an option with a payoff, and the absence of all three is a
supported configuration - the bundle still loads, and the prompt still tells the truth about it. The two sentences that may not be written are "configure DataHub like this" and
"DataHub is not usable without X", because the second one is false and the first one is not ours to
say.
8. Lineage is out of scope, and docs/architecture.md overstated it. That page said metrics,
dimensions, the glossary and lineage arrive through SemanticCatalog. There is no lineage type in the
workspace and none is planned, so the sentence claimed an input that does not exist. Corrected on this
branch, in the same diff as this record, because an overstated claim is itself the defect.
Consequences¶
- The stack table gains a capability-declaration row and a connector row. The declaration row can start now, because it is a port change plus a conformance shape and needs no live service. The connector depends on it. Neither depends on composition, which is the substantive change from the first version of this record.
SemanticCataloggains a required declaration, and that is the only port change here. Today the trait is an associatedErrorandload. What it needs is theWarehouse::IMPERSONATIONshape: an associated item with no default, so an adapter cannot be silent about what it cannot supply, with acompile_faildoctest and its compiling twin to pin that the omission does not build.dry_runis the counter-example to imitate deliberately, not accidentally - it is defaulted, and its doc says why.docs/implementation-plan-identity-and-services.mdsaid the remaining metadata connectors are all "feat/source-registry-shaped once the packs exist: a registration, a declaration, and fixtures." That is closer to right than the first version of this record allowed, and it is still not right: a registration and a declaration is exactly the cost, once the declaration exists to make - and it does not exist yet. So the correction is narrower than the one this branch first wrote. It is corrected on this branch to say that.- 0011's metadata half stops being wholly dormant, but only the part this needs. That record's metadata work is three things: the capability declaration, the assembler over N sources, and the contribution manifest. The declaration is scheduled here. The assembler and the manifest are not, and this record no longer claims they block anything - they become valuable when a deployment wants DataHub's structure and certified metrics in one bundle, which is a real want and a separate step. 0011's own sentence still applies to those two: "a decision whose record is accepted and whose branch does not exist is a decision, not progress."
- No committed digest moves from the narrow declaration, and a correction to the claim it made.
The first version of this record priced a manifest that would have moved every pin. Under the
narrow declaration nothing the reference adapters hash is hashed differently, because the physical
content is unchanged - and the declaration ITSELF now travels in each contributor's
ContributionManifestunder the digest (0011's later amendment), which means an adapter that WIDENS its declaration moves its own pins, and one that does not keeps them. Thedatahubamendment's widening is exactly such a move;sutura-catalog-local's example digest moving would be the failure signal. The knowledge capabilities already under the digest are unaffected; they travel with the bundle as they do now. - A narrow source is servable, and the prompt has to say so honestly. A bundle with zero metrics loads. What the prompt says about it is derived from the bundle rather than authored, which 0011 already decided, so a deployment reading only DataHub is told there is no certified metric layer rather than being told nothing. Nothing in this record adds prose to the prompt.
- The Beta flag is a real risk to price, and the declaration is what contains it.
MetricInfois at schema version 4 in the release and 5 on the development branch;SemanticModelInfo.datasetsis deprecated on the branch and not in the release; membership moved from the model side to the member side within one cycle. An adapter built against the metric entity this quarter would be revised. Under decision 3 the adapter does not read the metric entity at all - a statement thesuturaamendment narrows, because the certified path readsMetricInfo.expressionBESIDE the deployment's own property. The moving part of DataHub's model is outside what this connector depends on -schemaMetadata, the dataset property aspects and the glossary are the long-established part, andschemaMetadataeven carries a deprecated foreign-key field superseded by a newer one, which is what an aspect that has been through a migration looks like. That is a durability argument for the narrow declaration, and it is the second one after correctness.
What could not be determined¶
Stated rather than reasoned around, because an overstated claim here would send the implementation the wrong way.
- Whether a managed DataHub offering carries a structured metric shape the open-source model does not. Nothing public says so, and no cloud-only schema documentation was found. The published difference is enablement - a version floor and a per-tenant flag - not a different schema.
- Which lineage shape 1.7.0 intends for a metric that has a semantic model. The aspect schema and the SDK tutorial say different things about whether the semantic model is a lineage hop. It does not bear on anything decided here, and it is a live contradiction in the upstream documentation.
- Whether any
FILTER-annotated field is emitted by anything. The symbol is in the enum; a search of the public repository found no emitter. No emitter found is weaker than no emitter exists, and a deployment authoring annotations through the SDK could populate it by hand. - How much of a real DataHub instance's metric content is populated in practice. That is a question
about deployments and cannot be answered from a schema. It matters, because a
semanticModelwith no relationships and noMEASUREannotations contributes descriptions and nothing else - which would make DataHub the canonical narrow source 0011 describes rather than the rich one its entity list suggests. - The read path's cost, though its shape is now known and is the better news in this record.
DataHub publishes Python and Java SDKs and no Rust client - its integration tree holds one
language and it is not this one - so the adapter is an HTTP client written here. Which surface is
settled by DataHub's own guidance rather than by preference: its API overview says the GraphQL API
assumes frontend callers, comes with caching and "operations are intentionally limited in scope",
and that "care should be taken when used programmatically". The OpenAPI v3 entity surface is the
one to build against - versioned paths under
/openapi/v3/entity/{entityName}[/{urn}[/{aspect}]]with batch and scroll forms, anIf-Version-Matchheader for reading a specific aspect version, and a spec the deployment serves for itself at/openapi/v3/api-docs, generated from the handlers rather than hand-written. Authentication is a personal access token as a bearer. What was not evaluated is the cost: how many requests a bundle takes, whether the scroll surface is usable for a whole catalog, and what keeps a generated client from drifting. That is the adapter's first engineering question and not this record's. - One thing worth knowing rather than deciding: DataHub ships its own agent-facing package exposing catalog search and lineage traversal over the Model Context Protocol, and it can be configured to expose metadata mutations. It is not a competitor to the certified surface here - it answers what metadata exists, not what is this number - but a deployment that runs both is giving an agent two tools with very different guarantees, and which one an agent reaches for is a deployment question this repository cannot answer for it.
Alternatives considered¶
Composition first: block the connector on an assembler and a contribution manifest. This is what
the first version of this record decided, and it is written out rather than deleted because the
reasoning that produced it is the reasoning somebody will produce again. It follows from reading
agrees_with_the_oracle as every adapter's contract: if a registered catalog must match the oracle,
and DataHub cannot, then something must supply the difference. Rejected because the premise is
wrong - the oracle is the golden adapter's contract, 0011 already decided that adapters declare, and
sutura-catalog-local's own documentation already calls itself the reference and already prescribes
of([..]) for an adapter mapping a fixed external schema. The cost of the mistake was not small: it
would have made a deployment that already runs DataHub wait for an assembler and a digest migration
before getting anything, and it would have put a "one source may provide a given kind" precedence
argument in front of a source that provides kinds nothing else does. Composition remains decided and
unbuilt in 0011, and it remains genuinely wanted - for the deployment that has DataHub's structure and
certified metrics elsewhere - but it is not this connector's precondition.
Treat DataHub as a Knowledge source only. The second of the three cases. Rejected because it
throws away the half of DataHub that is genuinely load-bearing: schemaMetadata is the authoritative
column list for a great many warehouses. Declaring descriptions only would be flattening a source to
its least interesting capability, which
pluggable by declaration argues against by name. It is also
self-defeating under decision 3: a glossary phrase needs a Referent naming a metric, so a
knowledge-only DataHub with no metrics anywhere contributes nothing at all, whereas a
structure-and-descriptions DataHub contributes a working bundle.
Read metricInfo.expression through the authored-SQL hatch. Rejected on two independent grounds,
either of which is sufficient: the dialect sets do not intersect and closing the gap is translation;
and no shipped binary can execute an authored expression, so the load would succeed and the question
would still be refused, one stage later and less clearly.
Derive a Measure from aggregationFunction plus the annotated field. Rejected because the
expression beside it is authored independently and nothing reconciles the two, so the derivation
certifies half a definition. This is the alternative that looks cheapest and is the one whose failure
mode is a wrong number under a certified name. Declaring the capability unsupported is the cheaper
outcome and the honest one - it costs one line and it tells a deployment the truth.
Read the cardinality and use it. Rejected, and it is the closest call in this record, because
cardinality is the one thing 0011 hoped a rich source would improve. On the semantic-model join it is
optional; on the physical relationship it defaults to N_N. Using it would mean treating a default
as a decision on the input where the default is many-to-many - so a deployment that never thought
about a relationship would get the same treatment as one that decided. Both happen to be refused here,
because N_N maps to no JoinType, which is why this is a decision about what the adapter may
declare rather than a live hazard. Declaring cardinality unsupported means a deployment reads that
fact in the declaration instead of discovering it when a dimension goes missing.
Add an N_N variant to JoinType. Not now, and not as part of a connector. A many-to-many
relationship cannot reach a dimension without changing a measure, so the variant's only behaviour
would be to be refused - and AGENTS.md records that a variant no test can provoke is one an enum
refuses to carry. If a source needs to record many-to-many for a human to read, that is a different
field from the one the join planner reads.
Weaken agrees_with_the_oracle so a narrow source can pass it. Rejected outright, and named so
that nobody proposes it as the cheap version of decision 6. That test is the reason two independently
written statements of one catalog can be compared at all, and a version of it that tolerated missing
measures would pass a golden adapter that had silently stopped reading them. The golden adapters keep
the strict test; a declaring adapter gets a different one.
Amendment, 2026-09-02 (revised): a deployment-defined sutura structured property carries a certified metric, and it carries the WHOLE of one¶
Status of the amendment: accepted, and this revision corrects its own first version. The finding -
DataHub's own measure surface is a raw expression string in a dialect that does not intersect ours -
is unchanged and was re-verified. The first version of this amendment put the certified content in a
plurality of scalar structured properties under a sutura.* namespace carrying just the measure, and
argued from the shape that a measure is flat-expressible where a filter is not. Both halves of that
were wrong, and a review measured them wrong:
- The transport is one string-valued property, not a namespace of granules.
DataHub'sstructuredPropertyhas no nested or record value type, so a deployment cannot define a nested object undersutura.*at all. What it CAN define is one structured property - under a name of its own - whose single scalar value is a JSON document.document::SuturaPropertyis that scalar,suturais the field it arrives under on this adapter's own canonical shape rather than a urn this repository dictates (see the addendum's decision-7 bullet), andSuturaProperty::assembleis the scalar-to-nested decode - the issue #202 mechanism, implemented and exercised rather than described. This also collapses the old "a structured property cannot carry the shape of a predicate" argument: the scalar is a string, a JSON string carries anything closed, and the namespace is closed bydeny_unknown_fieldsover the decoded document, not by what a scalar can hold. - The scope is the whole metric, not the measure. The first version deferred
required_filters,dimensions,anchorand a value allowlist to "the composition story in 0011". That route is CLOSED by 0011 itself: all four are fields on the oneMetric, and 0011'sMetricCollisionrule means a second source can never attach them to a metric DataHub defines - "for metrics there is no precedence at all, declared or otherwise". So thesuturaproperty is the ONLY channel, and issue #202's scope is closed past the measure:required_filters,dimensions(withviaandallowed_values),anchoranddescriptionall ride it, over the same closed vocabularies a markdown metric uses.
The transport, as defined¶
A deployment defines, on a metric entity, one string-valued structured property - under whatever
name it chooses - whose
value is the canonical SuturaContent document as JSON text: a model, a measure (the domain
Measure type, written exactly as a markdown metric writes its measure: key), a time_column,
non-empty grains, and optionally description, required_filters, dimensions and anchor.
The measure, filter operators, grains and allowed values are the domain's closed vocabularies
verbatim, and deny_unknown_fields - on the decoded document, on the measure and on the term inside
it, on a filter, on a dimension, on the anchor and on the range inside the anchor - refuses a
property this adapter does not define rather than guessing, naming the key.
The range was the one level at which at every depth was one depth short, and it is closed rather
than recorded as a limit. An anchor's range decodes through the domain's TimeRangeInput, which
carried no deny_unknown_fields, so a key written inside the range object was discarded in silence
instead of named - and because a question's range and a markdown metric's anchor.range decode
that same one type, it was the same hole on all three paths. The attribute is on that shape now, held
by a_key_inside_an_anchor_range_is_refused_through_the_load_path here and by
a_key_inside_a_range_is_an_error_and_not_a_dropped_field over the YAML question path, each red
without it.
A metric that carries the property becomes a certified Metric; a metric that does not
stays the promotion candidate decision 4 describes, read and never converted. The two halves are
the same MetricAspect and the distinction is an Option - the deployment's declaration, not an
adapter's inference.
What this does to the decision¶
- Decision 3's rows change from does NOT provide to a conditional provide. The adapter's
capability declaration provides
Structure,DescriptionsandRelationshipsunconditionally, and declaresMetrics,Grains,RequiredFilters,AllowedValues,Anchors- andCardinality- as declared-and-empty may-provide kinds (DefinitionCapabilities::of_may_provide, 0011's declared-and-empty state, built for this).Cardinalitybelongs among them because it is observed only as a dimension reached through a relationship, which happens exactly when a deployment declares a dimension with avia. Because undeclared-and-empty marks absence lawful, a DataHub deployment that defined no metric content still loads - models, prose and joins, no metrics - which is decision 3's original narrow deployment rather than the boot failure an unconditional declaration would have produced. That is what made the first version of this amendment a correctness defect: it widened the declaration unconditionally, so any DataHub deployment without the namespace (the ordinary one) failed its own fidelity check. - Decision 4's never converted is narrowed to never converted where the property is absent.
The raw
expressionstring is still never executed, never translated and never certified against; the certified content comes from the structured property, not from the string. The two are both carried and are not reconciled - the string remains the promotion-candidate half, which is 0016's "reconcile, never assume" applied rather than abandoned. - The declaration moves a digest, and the Consequences bullet that said otherwise is corrected.
The first version of this amendment claimed no committed digest moves. Since 0011's manifest
amendment the declaration travels in the
ContributionManifestunder the digest, so widening it moves EVERY digest this adapter produces - which is what happened and is expected; the definitions and digest pins for thedatahubcell moved with it. Adapters whose declaration is unchanged keep their pins, which is why the test of the preservation is thatsutura-catalog-local's example digest did not move.
What still has no source¶
A real AspectReader and a served composition. The adapter is a dev-dependency of sutura-app,
no composition root links it, and sutura-serve refuses catalog.kind: datahub by name. The only
reader is the recorded fixture source, so no library code shapes a request or maps a response, and
.agents/skills/sutura/query-surface/SKILL.md's Built and not wired register records that nothing
serves it. The read path's COST is no longer the open measurement this record leaves - see
Revision, 2026-09-04 below - but a reader is still owed.
How it is proven¶
- The unit half is a fake reader over recorded documents carrying the property in its flat,
scalar form - the simple aggregate, a ratio and a
count_ifall load as domainMeasureshapes; a definitional filter, a dimension with its allowlist, an anchor and prose all load as the certified metric's fields; a metric without the property stays read-only; a metric that does not hold together is refused by the exact inner defect (UnknownModel,UnknownMeasureColumn,NoGrains); an unknown key at the content's top level is refused BY NAME; and a relationship this adapter cannot vouch for is refused.crates/sutura-catalog-datahub's suite. - The conformance matrix's
datahubcell - already adeclaringregistration - expands the universal cells over the richer bundle: the pinned digest and definitions moved with it and the declaration-fidelity cell holds, over a declaration that mixes unconditional and may-provide kinds. - The read path against a provisioned instance was the open measurement this record left, and
Revision, 2026-09-04 below closes the platform's half of it: the document is accepted under a
property the deployment names, served back, and decoded into a certified
Metric. What is still open is the READER -sutura-catalog-datahubhas oneAspectReaderimplementor outside a test, the recorded fixture source, so nothing in the library reaches a network.
Addendum to the amendment: which half of decision 7 the deployment still owns¶
Status: accepted. The amendment above answered issue #202's feasibility question and left two of its own arguments standing unreconciled. Both are answered here rather than in place, because an accepted decision's body is not edited to agree with a later one - the amendment is the record.
Decision 7 says the sentence "configure DataHub like this" is not ours to say, and the amendment writes one property name and one document grammar. Read together they contradict, so the boundary is drawn rather than left to a reader:
- The grammar of the decoded document is this repository's, and there was no version of this that
left it open. A decoder over a scalar has no way to negotiate the meaning of what it decodes:
the closedness IS
deny_unknown_fieldsat every depth over one known key set, and a key set the deployment chose would be no closedness at all - it would be the free-text escape hatchdocs/adr/0002exists to refuse, reached by a longer route. So the document is dictated, and that is the price of the closed vocabulary rather than a preference. - Whether any metric carries it stays the deployment's, and that half is held by a mechanism.
The metric kinds are declared-and-empty may-provide (
DefinitionCapabilities::of_may_provide) andDefinitions::assemblehas no minimum-metric refusal, so a deployment that defined nothing loads models, prose and joins. Decision 7's "the absence of all of them is a supported configuration" therefore survives intact for the property too, as a type rather than as a sentence. - What the property is called IN DataHub is still the deployment's, and nothing here names it.
The
suturakey is a field on this adapter's OWN canonical shape (document::MetricAspect), not a DataHub structured-property urn -document.rs's header is explicit that these shapes are the adapter's statement of aspect CONTENT and not DataHub's envelope, and no urn appears anywhere in the crate. Mapping a registered structured property - its namespace, itsSINGLEcardinality, its string value type, the entity types it binds to - onto that field is the unbuilt HTTPAspectReader's job. So "under which name" is genuinely unanswered rather than answered assutura, and no urn is written here. What the revision below adds is that this is now a MEASUREMENT: a name that shares nothing with the field name carried the document through a real registry, so the independence is checked rather than asserted.
And the Required filters: absent, and absent in the way that matters section makes TWO arguments,
where the amendment collapsed one. The amendment answered the SHAPE argument - a scalar cannot
carry a predicate - and the second argument is untouched by it: "a catalog that records a predicate
as a property records a fact, not a duty", so enforcing one would substitute our reading for the
author's, which docs/adr/0001 refuses. That argument stands, and it is what still forbids reading
DataHub's own filters structured property or its aggregationFunction. What passes it is narrower
than a structured property in general: a deployment writing a predicate under the key
required_filters, inside a document written in this repository's grammar, is not recording a fact
for us to interpret - it is stating the duty in the one vocabulary where that key already MEANS
applied to every question about this metric. The substitution 0001 refuses is inferring a duty from
a field DataHub defines; reading one from a field the deployment wrote in our own grammar is the
author speaking.
What this addendum does NOT establish¶
- Nothing here has ever written or read the property through DataHub. The venue exists and is
reachable -
just datahub-acceptancegets a2xxoff the surface a reader would call - and the instance is empty, so DataHub's server-side structured-property validator has never seen one of these documents..agents/skills/sutura/query-surface/SKILL.md's Built and not wired register is where that limit is read from. Superseded by the revision below. - The document's size ceiling is unmeasured. It grows with the metric - dimensions, allowed values, prose - and a deployment's DataHub validates the scalar for length. This crate adds no bound of its own, and no measurement here says what the ceiling is or what a metric that exceeds it does, because writing one through the platform is the same absent step as above. Superseded by the revision below.
- Whether a per-deployment property is acceptable to MAINTAIN is still not decided by this record. Issue #202 said that was not its job; it is not this addendum's either. What is decided is what the shape is, who owns which half of it, and which of the original arguments against it survive.
Revision, 2026-09-04: the platform's half, measured¶
Status: accepted. The first two bullets above were true when written and are not now.
a_document_served_by_a_real_datahub_decodes_into_a_certified_metric, behind
just datahub-acceptance, asks the provisioned instance rather than its schema. What that run
records:
- A deployment can define the property, under a name of its own, and the platform accepts this
document as its value. One string-valued,
SINGLE,metric-bound property; the value written is the recorded corpus's own document, read throughfixture::FixtureReaderso it cannot drift from the one the unit half decodes. The cell registers it asdeployment_metric_document, which shares nothing with thesuturafield ondocument::MetricAspect- that difference is the measurement, because a cell registeringsuturawould pass equally whether the name were the deployment's choice or a constant this repository requires. Decision 7's "not ours to say" holds as a measurement rather than as a sentence. - What the instance serves DECODES into a certified metric. The served aspect is mapped onto
document::MetricAspectand is EQUAL to the one the recorded fixture carries, which is the strongest available statement that the fixture is faithful to the platform rather than to itself;DataHubCatalog::loadthen produces the closed-vocabularyMeasure. That is issue #202's feasibility question answered against a running instance. - The read path's cost is a page per entity type, and it is eventually consistent. ONE
GET /openapi/v3/entity/metric?aspects=structuredProperties&aspects=metricInforeturns the metric with both aspects inline - the certified half and the promotion candidate's raw half in the same response - so a reader pages rather than fetching an entity per metric. But that surface is search-backed, and it lagged a synchronous write by ~2.2 s, whereGET /openapi/v3/entity/metric/{urn}answered immediately. A reader that pages does not get read-your-writes, which is the limit this measurement adds to the cost answer rather than a detail of it: the version of the cell that paged once, immediately after writing, was red. - The ceiling is the deployment's Elasticsearch keyword length. The platform's own refusal names
it: value is 131072 bytes which exceeds the maximum of 32766 UTF-8 bytes for structured property
values indexed as Elasticsearch keywords (
structuredProperties.keywordMaxLength). So what bounds a metric's document is an index setting rather than a constant in this repository. What was measured is that the refusal names the setting - nothing raised it and retried, so a deployment's ability to move it is DataHub's own documentation and not a finding here - which is why the cell asserts an order of magnitude of headroom against the number the refusal states rather than pinning the number. SINGLEcardinality and the declared value type are enforced server-side, each refused with its own reason (has cardinality 1, but multiple values were assigned; should be a string). "One string-valued property" is therefore the platform's rule and not this adapter's reading of it.
What this revision does NOT reach, and the distinction is the whole of it. There is still no
HTTP AspectReader: the requests and the mapping from the response shape
(structuredProperties.properties[].values[].string) onto document::MetricAspect are written in
that test file and nowhere in src/, so A real AspectReader and a served composition above
stands unaltered, and only the METRIC half of that snapshot came off the wire - the models and the
relationship are still the corpus's. Nothing is authenticated either: the tier runs with
metadata-service auth off. And the cell is #[ignore]d with no CI venue, because the nix sandbox has
no docker socket - so it is evidence of whatever the last just datahub-acceptance run reported.