Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-protocol): derive the reference graph from the type schemas instead of curating it - #9324
Conversation
…mas (#9190) `findReferencesToMeta` — the admin "Used by" panel behind `GET /api/v1/meta/:type/:name/references` — was driven by a hand-curated table of 7 target types and 40 dotted paths. Measured against the schemas it claimed to describe, 34 of the 40 named properties no metadata type declares, leaving 5 of its 7 target keys answering `{ references: [] }` unconditionally while appearing covered. The panel's empty state reads "Nothing in the metadata graph points at this item. Safe to delete." Coverage is now derived at boot from `DEFAULT_METADATA_TYPE_REGISTRY` and each type's schema, in the shape #7894 used for the URL-spelling map, so a newly declared type arrives covered. The unit of derivation is a PROPERTY, not a path: recursive containers (app navigation) make an exhaustive path list unbounded, and the walk reports where the name was actually found. No wire change: response shape, status codes and error envelope untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop):
|
os-zhuang
commented
Aug 17, 2026
PM review — the card was filed cautiously and the defect was far worse than filed⛔ Not flipping: CI started 13:50Z. Holding until each job's own conclusion is green. ⭐ The headline: the registry was not incomplete, it was largely fictional#9190 was filed with a careful hedge — "the behaviour is documented in the method's own TSDoc, so this may well be triaged as working-as-declared." Measured: 34 of the 40 curated paths do not exist against the schemas they claim to describe. Only 6 were live, reaching 2 target types. Five of the seven advertised keys answered And the harm is not abstract. You quoted the consuming string verbatim: A lookup that could not answer was rendering "Safe to delete." That is the sentence that makes this a defect rather than a documentation gap, and finding it in ⭐ The insight that makes the old design unfixable, not just unfixed
That is the load-bearing observation and it retroactively justifies the ⛔ in my brief. Adding keys by hand was never merely tedious — for a self-recursive schema it cannot be completed even in principle, so a view named from a third-level nav group was invisible even where the curated path was spelled correctly. Any close that stayed inside the path grammar would have shipped a new incompleteness with a fresh coat of paint. The fixtures were confirming the fiction — that is the ninth instanceFour tests failed, every one pinning a phantom path. ⭐ "the list and the fixtures were confirming each other." A hand-written list validated by fixtures written from the same hand is a closed loop that cannot detect its own error, and it is why this survived so long with tests passing. That makes nine instances this shift of a derived or hand-curated artefact consumed as authoritative and wrong: #9171 (twice — a missing path→gate edge, and a gate no path can express), #9182, #9192, the What the review credits beyond that
The one thing now owed to the maintainerThe durable close for Holding for CI. Generated by Claude Code |
os-zhuang
commented
Aug 17, 2026
PM rulings on the two open questions, and the two findings are filedQ2 — the wire-shape clause: A. Ruled, and your reading is the intended one.My brief said an acceptable partial "must distinguish 'no references' from 'not computable'." You satisfied that without a wire change, by moving the discriminator off the response and into the build: after derivation an empty answer is a derived statement for 26 of 27 declared types, and the one genuinely-uncomputable type is named in That is the clause's intent met exactly. ⭐ The clause was never about a response field — it was about an empty array having stopped meaning two things, and it has. Doing it inside the fence rather than by widening the fence is the better answer, not a lesser one. ⛔ Not routing B. You measured that the panel renders From type / From name / Path and never reads Q1 — the semantic-reference class: not mine. Carried to the maintainer, unchanged.⛔ I am not ruling this and I am not sizing it. It needs What I am doing is making sure it does not evaporate inside a PR body: I am carrying it to the maintainer as a decision, with your recommendation intact (A — a producer-side ⭐ And the reason your interim handling is right regardless of which way it is ruled: you carried exactly one bridge entry, pinned it at one, and deliberately excluded Your two unfiled findings — filed, with the dedup search you were blocked from runningYou were right to report rather than file blind or drop them when the API rate limit blocked the duplicate search. Both are now filed:
Two smaller things
Holding the flip for CI, which now runs the full suite as a required queue check (maintainer ruled #9283 Option A at ~11:59Z) — so expect it to take materially longer than earlier today. Generated by Claude Code |
os-zhuang
commented
Aug 17, 2026
docs-drift advisory — checked, no doc change belongs in this PR
The entire mention is one line in a code sample: constrefs=awaitclient.meta.getReferences('object','account');No sample response, no field table, no prose describing The other three: dismissals confirmed, and one is stronger than "noise".
⛔ The three No commit results from this round, so the head stays Generated by Claude Code |
os-zhuang
commented
Aug 17, 2026
|
Uh oh!
There was an error while loading. Please reload this page.
Part of #9190.
Part of, notFixes: the derivation half is complete, but one residual class needs apackages/specannotation this card's tier fence keeps out of scope, and it is named at the bottom. #9190 remains open for that decision.The premise held, and it understated the defect
The card says
REFERENCE_PATHScovers 7 target types against 20+ declared ones. Re-derived from the code rather than reconciled to the handed number: 27 declared types (DEFAULT_METADATA_TYPE_REGISTRY), 26 with a resolvable schema, 7 curated keys.Then I checked the curated paths against the schemas they claim to describe. 34 of the 40 do not exist:
app.navItems[],app.tabs[](14 paths, 6 targets)AppSchemadeclaresnavigationandareasagent.tools[],agent.tools[].name@objectstack/spec17 (#3894); converts to{ not: {} }permission.objects[].name,objects[].objectobjectsis a name-keyed RECORD, not an arrayobject.fields{}.referenceToreferencedashboard.widgets[].object/.view/.objectName/.viewNameDashboardWidgethas no such property at any depthpage.viewName,view.objectName,report.object,flow.object/.context.object/.trigger.object/.targetObjectOnly 6 paths were live, reaching 2 target types (
object,skill). Because theapprow was the only row forflow,dashboardandpage, and the removedagent.toolsthe only one fortool, five of the seven advertised keys answered{ references: [] }unconditionally — indistinguishable from a key that was never there.That answer drives the admin "Used by" panel, whose empty state reads, verbatim from
objectuimetadata-admin/i18n.ts:The close is derivation, not more keys
REFERENCE_PATHSis deleted.packages/metadata-protocol/src/reference-sites.tsderives the index at module load fromDEFAULT_METADATA_TYPE_REGISTRYjoined togetMetadataTypeSchema()— the shape #7894 used to make the URL-spelling map non-recurring, so a newly declared type arrives covered instead of waiting for someone to remember it.A property is a reference site for target T when both hold: its name spells T (T, camelCase,
Name-suffixed,target-prefixed, or a plural), and its value is name-shaped (unconstrained string, array/record of those, or objects carrying aname). Three limbs feed it: every declared type's schema;SCHEMALESS_NODE_CONFIG_SCHEMASattributed toflow(becauseFlowSchemadeclaresnodes[].configasadditionalProperties: {}, so limb 1 sees nothing inside it); and the residue below.The unit is a PROPERTY, not a path.
AppSchema.navigationis self-recursive, so an exhaustive path list is unbounded — a view named from a third-level nav group was invisible even where the curated path was spelled correctly. The walk finds the property wherever the document puts it and reports where it was actually found.Result: 17 target types resolve real sites, up from 2 served. New coverage includes
permission-to-object grants through the record key (which the old path grammar could not express at all),translation,dataset,action,report,doc,datasource, andflow-to-flowvia subflow node config.The rejected rule, pinned
A suffix rule (
endsWith(Cap(T))) would have caughthomePageIdanddefaultAgent. Measured against the real schemas it is ~15% signal: it readsdisplayField,nameField,startDateField,stageFieldand ~30 siblings as references to thefieldMETADATA TYPE when every one names a field inside an object, plusfieldMapping/inputMappingasmappingreferences andtabPositionas apositionreference. Rejected, and pinned inreference-sites.derivation.test.tsso it cannot return by accident.Fixture triage — the list and the fixtures were confirming each other
Four tests failed, every one on a fixture that pinned a phantom path. Re-spelled to the schemas, with the reason recorded in each:
objectql/protocol-references.test.ts—referenceTo⇒reference;objects: [{name}]⇒ record; dashboard-widget-to-view ⇒app.navigation[].viewName; agent-to-tool ⇒skill.tools[](ADR-0064).protocol.read-seam-empty-accumulator.test.ts—page.viewName⇒page.slots.header.dataSource.view; the failing source type moved fromdashboardtoapp. Measured set: five read seams answer a failed read from an empty accumulator with no log and no field saying the answer is incomplete #8896's assertions are untouched in substance.protocol.read-verb-canonical-fold.test.ts— the Three read-side/metaverbs (history, audit, references) never reachcanonicalizeMetaRequestType— they still derive their key from the MANIFEST map #9157 residue pin moved, deliberately. It assertedtranslationanswers[]and called that a coverage question for a later card. This is that card:DocSchema.translationsis a real site, so the pin now proves the same fold on a non-trivial result — a strictly stronger assertion than the empty one it replaces.⛔
referenceTowas dropped rather than tolerated: a consumer that accepts both spellings is how the wrong one survives.What is still not computable, and where it now lives
The ruling requires that "no references" stop meaning "not computable". After derivation, an empty answer is a derived statement for 26 of 27 declared types. The residual honest gap moved off the response and into the build, so no wire change was needed:
unwalkableSourceTypesis pinned to exactly['external_catalog'](resolves no schema, ADR-0062/0088). If that set grows, a source type stopped being readable and every panel silently got shorter — now a red test.SEMANTIC_REFERENCE_SITESholds exactly one entry:FieldSchema.reference, which names an object in prose ("Target object name (snake_case) for lookup/master_detail fields") and nowhere machine-readable. It is carried only because dropping it regresses the highest-value edge in the graph, and pinned at one. Other measured members of the class —AppSchema.homePageId,AppSchema.defaultAgent— are deliberately excluded so the incompleteness stays visible rather than looking handled..meta({ xRefTo: 'object' })on the property — the channelflow-node-expression-paths.tsalready uses forxExpression, and the "declared = enforced" side of ADR-0049. That is apackages/spec/*.zod.tschange and sits behind this card's tier fence, so it is flagged, not written.Tier fence
Not tripped. No
packages/specedit, no*.zod.ts, no error-code ledger row, no new refusal, no response-shape change — response shape, status codes and error envelope are byte-identical.pathandkindnow describe where the reference was found rather than which table row matched.Serial
Changed ranges in
protocol.tsfromgit diff --unified=0: line 80 (import), 2955-3069 (registry ⇒ walker), 17843-17997 (the method). No overlap with PR #9173 (11398-11496 / 11650-11666) or #9196's JSDoc region.Verification — all at
4e5989b9a, the final commitGate families derived from the actual changed paths via
scripts/pm/dispatch-gates.mjs, all PASS:check:cross-package-test-inputs,check:durability-log-level,check:filter-alias-parity,check:objectui-changeset,check:query-options-erasure,check:engine-double-contract,check:where-matcher,check:type-check-coverage,check-empty-changeset,check-changeset-no-major,check-adr-0087-registration,check-nul-bytes.Ratchet family at the final head, after the full closure build:
Reverse verification — direction predicted in writing first
Predicted: ablating the shape half of the rule (accept any name-spelled property regardless of its value schema) goes red on exactly the anti-guesser pins and nothing else. Observed, exactly that:
chartConfig.xAxis.position('left' | 'right') andflow.nodes[].position({x, y}) were both admitted aspositionreferences. Fix restored and proved byte-identical to the committed state (diffagainstgit show HEAD:..., empty).Generated by Claude Code