ID |
|
|---|---|
Status |
Ready |
Bucket |
cleanup |
Priority |
5 |
Theme |
model-cleanup |
Created |
2026-08-19 |
Updated |
2026-08-19 |
The stated key-column match spends its ambiguity silently; make it state its arity
One view in the fact schema still folds case per row. intent_node_metadata_defect, in its
KEY_COLUMN_UNRESOLVED arm, matches sql_column.jooq_name and sql_column.column_name against
sql_node_key_column.column_name under UPPER on both sides, four per-row calls. The match is
buried inside a NOT EXISTS, so it answers with a boolean and has nowhere to put an ambiguity: on a
table carrying two columns that differ only by case, the entry is reported resolved and which column
resolved it is decided by whichever the join reached. This item mints the folded column the ordinary
rule owes this crossing, lifts the match out into a relation that states its own arity the way every
other folded resolution in the schema does, and lets the arity decide, exact spelling winning where
more than one column answers and an irreducible ambiguity becoming a stated defect instead of a
silent pick.
What the tree settles about the open question
The Backlog body left one question open: whether the key-columns constant’s spelling is
catalog-canonical in the same sense the crawler’s own column reading is. If it were, the fix would be
deleting four UPPER calls; if it were not, sql_node_key_column.column_name would earn a folded
companion under the ordinary rule. The tree answers it, and the answer is the second branch.
JooqCatalog.reduceNodeMetadata fills an entry name with field.getName() on the entries of the
Field<?>[] the constant holds, and does not resolve that field against the table it is standing on.
JooqCatalog.columnFactsOf fills sql_column.column_name with col.getName() off the table’s own
generated field constants and jooq_name with the reflective Java field name. So the two strings are
byte-identical exactly when the constant references the table’s own field constants, which is what
Sikt’s KjerneJooqGenerator emits, and are unrelated otherwise: an entry holding a DSL.field("id")
or a field belonging to another table yields whatever spelling that field carries. The store records
the entry as stated precisely so it can hold a name belonging to no column, which is the whole reason
sql_node_key_column declines a foreign key to sql_column.
The argument form matters, because the common case points the other way. Almost always the constant does reference the table’s own field constants and the two strings are identical, and reasoning from that would settle the question wrongly. The case where the derivation fails is what decides it, and one example is enough. The schema reached the same answer before the question was asked: that foreign key is declined because "the constant spells a column by name and may spell one the table does not have", which puts the column on the spelled-reference side of the structural-versus-spelled split. A spelled reference is not a catalog reading.
An entry name is therefore a stated name, sql_node_key_column.column_name earns a column_name_upper
companion under the ordinary rule, and no amendment to where folds are minted is needed.
The Backlog title, "the store folds two catalog-produced names as a hedge; make the comparison exact", is wrong in both halves as a result: the operands are not both catalog-produced, and exactness is what the match falls back to rather than what it becomes. A third word went with them in this revision. The fold is not a hedge at all. A hedge is a fold with no crossing under it, which is R702’s subject; this one bridges a spelled reference and a catalog reading and is the semantic the rule exists to serve. It stays, unconditionally. What is wrong is that the match spends an ambiguity without saying so, and the title and slug now name that instead.
One doc hazard this creates is handled in the Implementation below rather than discovered later.
After the column lands, both operands of the match live in the sql_ family, and seven shipped
_upper comments carry the same sentence: "Two values of one family are compared exactly, and a
comparison that does want a fold on both sides reaches this column by joining … on its key rather
than by having it forwarded through a derived view." They sit on sql_table (two), sql_column
(two) and sql_constraint (three), which is every _upper column the sql_ family has. A reader
arriving at the new column with that rule in hand deletes it.
The rule is wrong as stated rather than merely incomplete, and the word doing the damage is family.
The discriminator was never the relation prefix: it is whether a value is a reading of the thing it
names or a name spelled at it. Both operands here are sql_ and one of them is a spelled reference,
which is the split the missing foreign key already stands on. The family holds facts about generated
Java classes beside facts a database produced, sql_node_metadata sitting under sql_ because it
shares a refresh unit with sql_table rather than because a database produced it. So the fix is to say
catalog readings where those comments say values of one family: one phrase in seven places, after
which the new column is obviously not an instance of the rule rather than an exception to be argued.
Two further facts the design leans on. FactSink.claim keys its dedupe set on a plain HashSet of
the key values, so "Id" and "ID" are two distinct sql_column rows and the store can hold the
collision at all. And the reading side this view mirrors, JooqCatalog.findColumn, matches the Java
name case-insensitively and then the SQL name case-insensitively, each with findFirst, so on a
colliding table it silently picks by reflective field order. That is the behaviour the store is meant
to replace, not the behaviour it is meant to reproduce.
Design
This is not a new axis for the fold rule, and framing it as one would be both wrong and a weaker
argument. The fold stays entirely unconditional: every folded match becomes a row, exactly as
intent_bound_table makes every candidate table a row. What is conditional is a verdict a consumer
computes from arity, which is the discipline the schema already applies wherever ambiguity has a
relation to sit on. Ambiguity is rows, never a decline, and the count says so;
intent_bound_table.candidates and intent_name_matched_key_pair.unmatched_columns are that rule
written down. This comparison never got the arity column because it was buried in a NOT EXISTS
where arity had nowhere to surface, and that is the whole defect. No general statement about
conditional folds is needed, and none should be added.
The schema has done this shape before at another site, which is the precedent to follow rather than
invent against: intent_field_reference_step_hop already encodes SQL-name-wins precedence over the
jOOQ name in SQL, through a NOT EXISTS, and carries key_matched_by as a closed-vocabulary column.
Three pieces:
The pairing relation. A new view intent_stated_key_column_match at grain one row per (key-column
entry, sql_column row it could be). The grain is per column and not per (column, spelling) pairing:
a column whose SQL name and Java name are both the sought name must be one row and not two. The join
condition being a boolean rather than a row source gives that for free, so no DISTINCT is involved.
Every exact match is also a folded match, so the folded predicate is the join and exactness is a
projection over it, never a second arm.
Arity is stated as columns, not left to the reader: candidates (how many columns answer at all) and
exact_candidates (how many answer on an exact spelling), both window counts over the entry’s
partition, with the inherited-window caution the schema’s other arity comments carry. A boolean would
not be enough, and the case that proves it is reachable: on a quoted-identifier catalog, column A can
be exactly Id on its jooq_name while column B is exactly Id on its column_name. A
some-row-matched-exactly test calls that resolved and hands the pick back to whoever reaches a column
first, which is precisely the failure this item exists to stop. Two counts express it; one flag
cannot.
The relation deliberately does not carry which spelling matched, and deliberately replaces
findColumn’s Java-name-first precedence with exactness rather than reproducing it. What that costs
has to be stated exactly, because the tempting claim is the wrong one. The two rules agree on every
table whose column spellings, Java and SQL names taken together, are case-insensitively distinct:
exactly one column can answer at all there, so both rules return it. They diverge only where two
columns collide under the fold, which is the population this item exists for. They do not diverge
only on the inputs the arity columns call malformed: a table where one column answers exactly and a
second answers only folded has two candidates and one exact, resolves with no defect, and resolves to
the exactly-spelled column where `findColumn would have taken the Java-name match. That is a
deliberate behaviour change on a colliding catalog rather than a divergence to apologise for, and the
statement of it belongs in the view comment; it is the load-bearing half of the relation’s
justification. It exposes no _upper column either, so the rule that a derived view never forwards a
fold holds.
What earns the relation, given that it will have one view reading it. An earlier revision argued two readers, R668’s tier being the second; R668 has since settled and does not read it, so that argument is dead and is replaced here rather than quietly left standing. One reader is the shape this schema otherwise answers with a CTE, so the question is real.
The load-bearing answer is that a CTE would state the arity where nobody can read it. This schema’s
rule for an ambiguity is not that a count exists but that the witnesses are rows;
intent_name_matched_key_pair.unmatched_columns says so outright, a consumer explaining a refusal
reading the rows behind a number above it, and intent_bound_table offering every candidate to an
editor is the same rule. A KEY_COLUMN_CASE_AMBIGUOUS row says a table is malformed and says nothing
about which columns collided, which is the question anyone who hits it asks first. Shipping a verdict
whose witnesses are sealed inside the view that reached it would reproduce, one level up, exactly the
defect this item exists to remove: an answer with nowhere to put what it knows.
The resolution being spelled more than once is a second argument and no longer the main one. It stays
spelled in Java by JooqCatalog.findColumn, which is R729’s, and
intent_resolved_node_key_column.column_name forwards the stated spelling onward to
intent_resolved_node_key_projection, which R668 has landed, and to an emitter that eventually needs
a real column; that column’s comment explicitly declines to perform the match, "settled convention
rather than this relation’s rule". Lifting the defect view’s own copy out takes the SQL count from two
to one rather than to zero, which is worth stating plainly rather than claiming a sweep.
The gated arm. intent_node_metadata_defect’s entry arm stops reading columns and reads the counts:
an entry resolves when `exact_candidates = 1, or when exact_candidates = 0 AND candidates = 1.
KEY_COLUMN_UNRESOLVED keeps its meaning exactly, no column answers at all.
The new defect value. KEY_COLUMN_CASE_AMBIGUOUS, the complement: more than one exact candidate, or
none exact and more than one folded. One arm covers both shapes because the arity columns make them
one condition. It is a distinct value rather than a reading of KEY_COLUMN_UNRESOLVED because that
value’s meaning is already fixed by the message the live probe composes from it, references column 'X'
which does not belong to this table, and on a colliding table that sentence is false: case-variant
columns do exist and the entry names none of them exactly.
That value changes what well-formed metadata means, and that has to be stated rather than treated as a
message refinement, because well-formedness has consumers. intent_resolved_node_key_column’s
`JOOQ_METADATA tier gates on a table having no defect row at all, so a case-ambiguous entry stops that
tier resolving and falls through rather than resolving against a column picked by field order. That is
the payoff, and it needs no edit to that view. intent_inferred_node_type shipped while this item was
in Spec and stands on the same conjunction, so a case-ambiguous key column also stops the type being
an inferred node. Both are the right answers and both are semantic changes this item owns.
Neither consumer needs editing, and the reason is worth stating so nobody goes looking. Both gate on a table having no defect row at all, with no filter on which value, so a new value reaches both by construction. The schema also deliberately accepts two spellings of that conjunction rather than extracting it, each view’s comment naming the other as its sibling spelling, so the count of value-agnostic gates is already two and would keep growing. That is what makes the propagation worth a pinned case rather than an observation: a gate that inherits a new value silently is exactly the kind that nobody notices has changed.
The Java enforcer. The store’s verdict needs one or it is a fact with no teeth.
JooqCatalog.validateLookup resolves entries through findColumn, which folds and takes findFirst
over the reflective field order, so without a Java-side change the store would report a table
malformed while the shipped generator quietly picks a column and emits against it. A store verdict the
build does not enforce is the drift this project treats as a smell.
The enforcer mirrors the relation rather than inventing a second rule, which is what stops the two
sides drifting apart the way the folded predicate already did. validateLookup’s `columnLookup
argument stops returning one column and starts returning every column that answers the name, and
validateLookup computes the verdict from that list exactly as the defect arm computes it from the
counts: one exact candidate wins, a single folded candidate wins where none is exact, anything else is
Malformed. The list has the relation’s grain, one entry per column and not per spelling, which falls
out of walking the table’s fields once. An Optional cannot carry this, and that is the real reason
the signature moves rather than a convenience: the existing empty case already means no column answers
and composes the sentence that says so, and telling an ambiguity apart from it is what stops that
sentence being printed about a table whose columns do exist.
findColumn is not touched and neither are its other callers. They carry the same silent pick, and
that is R729, filed off this item’s analysis. This item leaves one worked example of the rule rather
than a sweep, and depends on nothing.
Implementation
-
graphitron-model/src/main/resources/no/sikt/graphitron/model/graphitron-model.sql-
sql_node_key_columngainscolumn_name_upper VARCHAR GENERATED ALWAYS AS (UPPER(column_name)), null exactly wherecolumn_nameis. Its comment takes the shape the twenty-nine_uppercolumns across twelve relations already share (what the fold is for, that nothing writes it and nothing can) and names this crossing as a spelled reference meeting a catalog reading, which is the sentence that stops a reader deleting it as a within-family fold. -
CREATE VIEW intent_stated_key_column_matchwith the columns(source_name, table_schema, table_name, position, column_name, case_exact, exact_candidates, candidates), the first four keying the entry andcolumn_namebeing the matched column’s SQL name rather than the entry’s stated spelling, which the column comment has to say outright since the parent relation spells the other thing under that name. Joiningsql_node_key_columntosql_columnon the table key and(c.column_name_upper = k.column_name_upper OR c.jooq_name_upper = k.column_name_upper), guarded byk.column_name IS NOT NULL.case_exactis aCASE WHEN ... THEN TRUE ELSE FALSE ENDoverc.column_name = k.column_name OR c.jooq_name = k.column_name, the established idiom for a boolean view column; all three operands are non-null under that guard, so no arm evaluates to unknown. The two counts are windows overPARTITION BY source_name, table_schema, table_name, position,exact_candidatesaCOUNT(CASE WHEN <exact> THEN 1 END). The view must be defined ahead ofintent_node_metadata_defect, which reads it. -
The
KEY_COLUMN_UNRESOLVEDarm becomes aNOT EXISTSover the new relation, and aKEY_COLUMN_CASE_AMBIGUOUSarm is added reading the two counts. Both arms keep thek.column_name IS NOT NULLguard the entry arm carries today. The relation guards itself, so the guard looks redundant and is not: an arm that dropped it would report a null entry unresolved besideKEY_COLUMN_ENTRY_NULL, andaTableExhibitingSeveralDefectsGetsARowForEachOfThemis what catches it. The two arms are mutually exclusive by construction, which keeps the no-short-circuit grain the view comment promises. The fourUPPERcalls go. -
Comments. The new relation and its columns get theirs, including the sentence about declining
findColumn’s precedence on purpose. `sql_node_key_column.column_name’s comment currently ends "it matches the reading side: case-insensitively, against the generated Java name or the SQL name", which stops being true, and so do two neighbours that assert the same mirroring: `intent_resolved_node_key_column.column_name’s "settled convention" sentence, and the javadoc on `NodeMetadataDefectTest.anEntryResolvesCaseInsensitivelyThroughEitherName. All three are rewritten in the same commit, and the honest replacement is that fidelity to the predecessor is evidence rather than a specification: the store diverges fromfindColumnon the collision case deliberately, and the Java change below is what stops that being a divergence at all. Onintent_resolved_node_key_column.column_namethe edit is that one sentence only. R668 appended a paragraph to that comment explaining why the column exposes no fold, and is rewriting its reason under R731; leave the paragraph alone either way, whichever wording is standing when this lands. -
The seven
sql_-family_uppercomments (sql_tabletwo,sql_columntwo,sql_constraintthree) say "Two values of one family are compared exactly". Replace values of one family with catalog readings in each. It is the same claim said accurately: those columns' comparisons are between two readings, the new column’s is not, and a reader who reaches the new column with the corrected rule in hand keeps it instead of deleting it. The sentence’s second half, that a comparison wanting a fold on both sides joins the owning relation rather than having one forwarded, is untouched and still true. The page these seven got the word from is edited below, so the schema and the docs land the correction together. -
intent_node_metadata_defect’s `defectcomment enumerates "a closed vocabulary of ten" and itspositioncomment says "the eight that are about a whole constant". One added arm stales both, off one edit, which is the argument for dropping the numbers rather than bumping them: two hand-maintained counts over one vocabulary, neither pinned by a test. Rewrite to enumerate without the number. Where the schema does carry a count it either pins it or restates it from one place, `intent_argmapping_pair’s vocabulary of eight being cited by name at three other columns rather than recounted; this view does neither, so the number is inventory.
-
-
graphitron/src/main/java/no/sikt/graphitron/rewrite/capture/FactWrites.java: asqlNodeKeyColumnwrite function naming the five writable columns, registered in the writers map. A relation carrying a computed column cannot go through the generic every-column arm, which is why the column above forces this;WrittenStatementCoverageTestalready gates it with no roster to update. The parents-first ordering inFactSink.flushalready spans written and generic relations, so nothing there changes. Conflict behaviour is stated per relation rather than inferred from the name, so state it:onDuplicateKeyIgnore, matchingsqlColumnbeside it and the generic arm’s behaviour for this relation today,FactSink.claimhaving already deduped the key. -
graphitron/src/main/java/no/sikt/graphitron/rewrite/JooqCatalog.java: the node-metadata lookup stops picking on an ambiguous fold.columnLookupchanges fromFunction<String, Optional<ColumnEntry>>to a function returning every column that answers the name;validateLookupderives the verdict from that list the way the defect arm derives it from the counts;doLookuppasses a private matcher over the standing table’s fields, and is still the only production call site touched. The ambiguity gets a newMalformedreason naming it, distinct from the existing "does not belong to this table", which is the distinction theOptionalcould not carry. It matches the store’s new defect value in meaning without either side citing the other.findColumnis untouched and so are its other callers; that residue is R729.validateNodeIdMetadata’s signature moves with `columnLookup’s, which reaches `JooqCatalogNodeIdMetadataTest’s nine call sites through the one `RESOLVE_ID_COLUMNSconstant they share, so the test churn is that constant plus the new cases. -
docs/architecture/explanation/fact-model.adoc, the fold-rule paragraph. Its last two sentences carve out this comparison as "the one comparison the rule declines to serve" and say "it goes away by becoming exact rather than by being stored". Both become false and both are deleted: the reason the page gives for declining this crossing is the reasoning the first section of this item refutes, so the carve-out goes rather than being reworded. Two further corrections to the same paragraph. Its crossing statement widens, from an author typing "a GraphQL or SDL identifier" to any hand-authored spelling, since the new fold’s authored side is a Java expression in a generated or hand-written table class. And its second consequence, "a comparison between two values of one family mints nothing", is where the seven column comments got the word: it becomes a statement about two catalog readings, so the page and the comments carry the corrected rule in the same terms. -
The same paragraph, and the reason the deletion above is not the end of it. The page promises "views that hold no per-row case fold on any comparison the rule reaches", and treats the defect view as the sole survivor. That stopped being true before this item is implemented: R668 landed
intent_resolved_node_key_projection, which foldsintent_resolved_node_key_column.column_nameper row againstgraphitron_argument_path_segment.segment_name_upper, and did not touch this page. So removing this item’s survivor leaves the page asserting a clean sweep that is false, which is worse than the carve-out it replaces. State the surviving site, and state it as an open question rather than as a settled exception: the rule’s second consequence forbids a derived view from exposing a fold, andintent_resolved_node_key_columnhands out a spelling rather than a resolved column, so a consumer matching against it has nowhere to reach and folds at the crossing. Whether that relation should hand out a spelling at all is R731’s, and the page should say so. Do not write the reason as a three-tier pick having no base relation to reach a fold through:intent_spelled_tablereads arm folds across as many arms without trouble, and R668 has withdrawn that wording from its own body. The wording of the site is R668’s; not leaving the paragraph false is this item’s, being the item that edits it.
Nothing generated needs regenerating by hand. The schema reference under
docs/architecture/reference/schema/ is rendered from the DDL and its comments by the docs module’s
render-schema-reference execution, so the new relation documents itself.
Tests
graphitron-model/src/test/java/no/sikt/graphitron/model/intent/NodeMetadataDefectTest.java is the
home; it seeds rows directly, which is what lets a colliding catalog be stated in two lines rather
than shipped as a broken generated class.
-
Every existing case in that class passes unchanged, and that is an assertion about the design rather than a hope: the file’s
anEntryResolvesCaseInsensitivelyThroughEitherNameresolvesFILM_IDexactly on the Java name,release_yearexactly on the SQL name, andReLeAsEyEaRon a fold that exactly one column answers. Confirm this by running the class before touching it and after, not by reading the diff. -
A colliding table where the entry matches one column exactly: two columns differing only by case, the entry spelled as one of them. No defect,
candidatestwo andexact_candidatesone, and the exactly-spelled column is the one row carryingcase_exact, which is the column a tier reading this relation takes. That last assertion is where the deliberate override of `findColumn’s Java-name-first precedence is pinned rather than only described. This is the case the item exists for, and the one the old arm got right by accident rather than by verdict. -
A colliding table where the entry matches neither exactly:
KEY_COLUMN_CASE_AMBIGUOUSat that position, and noKEY_COLUMN_UNRESOLVEDbeside it, so the two arms are pinned as exclusive. -
Two exact candidates: column A exact on its
jooq_name, column B exact on itscolumn_name, the same sought name.exact_candidatestwo, ambiguous. This is the case a boolean could not express and the reason the relation carries counts. -
An entry naming no column at all still yields
KEY_COLUMN_UNRESOLVED, which pins that the new arm did not absorb the old one. -
The collision across the two spellings rather than within one: column A’s
jooq_namefolds onto the same value as column B’scolumn_name, neither exactly. Two candidates, none exact, ambiguous. -
One case on the grain itself: a column whose SQL name and Java name are both the sought name is one row and
candidatesone, so a single column answering twice cannot trip the ambiguity arm. -
A case pinning that a case-ambiguous entry suppresses
intent_resolved_node_key_column’s `JOOQ_METADATAtier, which is where the defect value earns its keep. Belongs inResolvedNodeKeyColumnTestbeside the tier’s other cases rather than in this class. -
The same for nodehood: a case-ambiguous key column takes the type out of
intent_inferred_node_type, beside that relation’s own cases. That relation is on trunk and the item that built it is Done, so the case is written unconditionally. -
The Java side, beside
JooqCatalogNodeIdMetadataTest’s other `validateNodeIdMetadatacases. Three, matching the store’s: an ambiguous fold reportsMalformedwith the new reason rather than resolving; an exact candidate beside a folded one resolves to the exact column, which is where the Java and the store agree on overriding `findColumn’s precedence; and a name no column answers still reports the existing "does not belong to this table" reason, pinning that the two messages stayed apart. The helper already takes a synthetic column lookup, so a collision is two entries the stub hands back together and needs no fixture class.
All the store cases are seeded rows, not crawler fixtures. The class javadoc already argues why: reaching these states through a crawler would mean shipping a broken generated class into the fixture tree for each of them, where stating them takes a line. Said here so the implementer does not try to manufacture a quoted-identifier collision inside the sakila catalog.
WrittenStatementCoverageTest covers the write function without a new case: it enumerates the
relations the catalog reports a computed column for and round-trips a distinct value per writable
column.
Deferral
If the exact-first decision turns out hard, what defers is that predicate plus the new defect value plus
the Java enforcer, together, and what lands is the folded column, the pairing relation with both arity
columns, and the entry arm rewritten to read it with today’s semantics: a defect exactly when
candidates = 0. That intermediate is coherent on its own terms. There is one spelling of the
resolution instead of two, no vocabulary change, no consumer impact on the metadata tier or on
intent_inferred_node_type, the arity is already stated so a collision is visible to anyone who
looks, and the remaining work is a one-line predicate change plus its tests.
What must not be the fallback is landing the relation while leaving the defect view’s own NOT EXISTS
in place. That would leave the fold spelled twice with nothing binding the two, which is the drift this
item exists to remove, arriving by the back door.
Deferring is a judgement about difficulty encountered, not a default. If it happens, the deferred half is filed as its own Backlog item with the design above carried across intact, and this item’s body records what made it hard so the next session does not rediscover it.
Relationship to R668, which has settled
R668 has answered the question this section used to hold open, and the answer removes an obligation
rather than adding one. Its earlier body stated the constraint that the JOOQ_METADATA tier of
intent_resolved_node_key_column must resolve an entry through the same predicate as the arm deciding
the entry is well-formed, "or the two can disagree about which entries resolved". Its landed stages
hold that agreement the way it was always held: the tier reads nothing but the defect view’s verdict
and never resolves an entry itself.
What R668 needed was a different match, a trailing argMapping segment against a resolved key column,
and it closed that one without spelling a predicate twice either: the authored side is folded on its
own base relation as graphitron_argument_path_segment.segment_name_upper, the key-column side is
folded at the crossing in intent_resolved_node_key_projection, and the unknown-column defect is
stated as the absence of a projection row rather than as a repeated predicate. R668’s body records
that it shipped a column_name_upper on intent_resolved_node_key_column and then reverted it, on
the rule that a derived view never forwards a fold, and `intent_resolved_node_key_column.column_name’s
comment now carries the reason so nobody mints one again.
Two consequences for this item, one of them uncomfortable and stated rather than buried. R668 will not
read intent_stated_key_column_match, so the two-readers argument this item first made for the
relation is gone; what earns it now is argued above, on witnesses. And R668’s landed DDL left this
item’s arm alone exactly as its body promises: the four UPPER calls are in place and
sql_node_key_column.column_name_upper is unminted, both deliberately deferred here. The implementer
still re-reads the arm as it then stands rather than trusting this paragraph, R668 not yet being Done.
The depends-on field stays empty deliberately: this item is not blocked by R668, it just must not be
written against a stale copy of the arm.
Follow-ups not in scope
-
The parked extraction of the well-formed-stated-metadata conjunction into its own relation on `sql_table’s key. Not filed as an item; it is recorded where it cannot rot, in the comments of the two views that carry the conjunction twice, each calling it the follow-on neither performs unilaterally. A different relation at a different grain from the pairing relation here, but a reader will pair them, so this item’s view comment points at it rather than leaving the adjacency to look like an oversight.
-
A schema-wide census of case-colliding columns on
sql_column, which would answer whether any real consumer catalog exhibits the pathology at all and would serve R729’s per-site reachability question. Deliberately not added here: the pairing relation already carries the witnesses for this view’s own question, and a census with no reader today belongs with the item that needs it. -
The per-row fold in
intent_resolved_node_key_projection, which is R731’s and not this item’s. R668 landed the authored side asgraphitron_argument_path_segment.segment_name_upperand folded the key-column side at the crossing, having shipped an_upperon the reduction and reverted it; no view in this schema exposes one. R731 asks the layer below that, whetherintent_resolved_node_key_columnshould hand out a spelling rather than a resolved column at all. This item’s new column does not serve that site and must not be pointed at it, the value there being whichever tier won rather than the stated entry name; it is a precondition for one tier of R731 and nothing more. What this item owes the site is the page edit above. -
R729, `findColumn’s silent pick at its other seventeen call sites, filed off this item’s analysis. This item installs the rule at one of those sites because a store verdict about malformed metadata needs an enforcer or it has no teeth; generalising it needs a per-site reachability call this item has no reason to make.
Two neighbouring items must not settle a convention opposite to this one, and they are not the same item as each other. R729 is the Java-side sibling over this crossing, a spelled name meeting a catalog reading, where the fold stays and only the silent pick goes. R702 is over the other one, two catalog readings compared under a fold that bridges nothing, where the fold itself goes. Neither is a dependency. This item’s convention, stated so both can agree or argue with it: a fold is minted where a spelled reference meets a reading and nowhere else, exactness is what a fold falls back to when more than one thing answers, and an ambiguity a fold would have spent silently is a stated row.