Skip to content

findReferencesToMeta answers {references: []} for every target type absent from REFERENCE_PATHS — the "Used by" panel is silent by construction for 20+ declared types #9190

Closed

Description

@os-zhuang

Surfaced while implementing #9157 (step ① of the #9180 ruling). ⚠️ Filed plainly and unassigned: the behaviour is documented in the method's own TSDoc, so this may well be triaged as working-as-declared. It is filed because the declaration and the consumer's reading of the answer disagree, not because I measured a live incident.

What was measured

packages/metadata-protocol/src/protocol.ts on origin/main @ 50900479f. findReferencesToMeta is driven entirely by the hand-curated REFERENCE_PATHS registry, which has exactly seven target keys:

object · view · tool · skill · flow · dashboard · page

Any other target type misses the registry and takes this limb:

const matchers = REFERENCE_PATHS[singularTarget];
if (!matchers || matchers.length === 0) {
    return { references: [] };
}

So GET /api/v1/meta/permission/sales_admin/references, .../field/..., .../translation/..., .../seed/..., .../external_catalog/..., .../agent/..., .../report/..., .../email_template/... — every declared type outside those seven — answer 200 with an empty list, always, on every deployment, whether or not anything depends on the item.

Why this is filed rather than shrugged at

The method's TSDoc calls it legitimate:

> A target type not present in the registry, and a SOURCE type this deployment does not declare, both simply produce no hits — neither is an error.

That is true as a statement about the registry. It is not how the answer is consumed. This response drives the admin UI's "Used by" panel, which is rendered before a rename / delete / type-narrowing, and #8896 already named the harm shape for this exact endpoint: an empty or short list reads as "nothing depends on this — safe to remove". #8896 closed the half where a read FAILURE was swallowed into that shape; this is the half where the registry simply has no entry, and the two are indistinguishable to the caller — same status, same body.

⛔ Explicitly not the same defect as #9157. That card was about the type KEY being spelled wrong (a plural reaching a lookup keyed on singulars), and it is fixed. This is about the lookup being empty for the key even when the key is perfectly canonical. #9157's pin the manifest-ABSENT class is NOT closed here, and that is stated rather than implied asserts this residue deliberately so it could not be quietly read as closed.

Dispositions worth weighing at triage (not chosen here)

  1. Leave it — accept an incomplete registry, and make the RESPONSE honest instead: distinguish "no dependents" from "this platform cannot answer that question for this type" (a coverage: 'none' discriminator, or a 501). Cheapest, and it converts a silent wrong answer into a visible gap.
  2. Extend REFERENCE_PATHS to the remaining declared types. Real work per type, and it is hand-curated by construction, so it will drift again.
  3. Derive the matchers from the spec's own reference-bearing declarations rather than hand-curating them, so a newly declared type arrives covered — the shape meta-plural-url-bypass: PUT /meta/fields/<name> walks around the whole two-tier registry gate — 4 registry types have no entry in PLURAL_TO_SINGULAR #7894 used to make the URL-spelling map non-recurring.

⚠️ Whether option 1's discriminator is worth a wire change is a product call, and options 2/3 are materially different sizes. Not chosen here — triage's field.

Backlinks: #9157 (surfaced it; its pin asserts the residue) · #8896 (the empty-accumulator harm shape on this same endpoint) · #9180 (the ruling whose step ① #9157 implements) · ADR-0110 D3.

Activity

  1. added theissue type on Aug 17, 2026
  2. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor

    First-touch grading (triage seat): promoted finding → pm:queue, type Bug, routed domain:metadata (lands in packages/metadata-protocol/src/protocol.ts — findReferencesToMeta's lookup REFERENCE_PATHS[singularTarget], the hand-curated table at :2973, verified on origin/main).

    Grading rationale: a read API that answers a well-formed {references: []} for a target type it has simply never heard of makes the "Used by" panel indistinguishable from "no references" — silent-empty on a consumer surface. Dispatch note: the honest minimal fix is making uncovered types loud (or extending the table with real matchers); ⚠️ if the chosen shape changes the wire response (a new covered/uncovered indicator on /meta references), that fragment is a contract-face change — stop and route it to the spec seat rather than riding it here. Size/model suggestion: M, opus.


    Generated by Claude Code

  3. self-assigned this
    on Aug 17, 2026
  4. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop — batch 5
    Session: session_01NTKPDRoynY8i3HmdSFUxFj · Branch claude/issue-9190-reference-paths-coverage · Worktree objectstack-issue-9190
    Domain: domain:metadata · File surface: packages/metadata-protocol/src/protocol.ts — REFERENCE_PATHS and findReferencesToMeta
    Model: opus
    ⚠️ Clause-② suspicion, declared at claim time (#9211): borderline, judged NOT triggered. This changes what a read returns (real references where it returned []), not what the door accepts or refuses; no packages/spec/**, no ledger row, no envelope change. ⛔ If your design drifts toward changing the accept face — a new refusal, a new error code, a response-shape change — STOP and report. The seat dispatching you breached clause ② twice this shift, so this fence is drawn tighter than usual on purpose.
    Serial: ⚠️ hot file. PR #9173 (#9111) is open on protocol.ts at 11398-11496 / 11650-11666; #9196 is dispatched into its JSDoc this round. Declare your actual changed ranges from git diff --unified=0.

    裁决(不可重裁)

    REFERENCE_PATHS being hand-curated is the defect, not its current contents. Seven keys against 20+ declared types means the "Used by" panel is silent by construction — and silence here is the #8896 harm shape at its worst: "nothing depends on this" is what an operator reads immediately before a rename or a delete.

    ⛔ Adding a few more keys by hand is NOT the fix. That reproduces the defect one generation later, and #8908 already measured what hand-written lists do here — a "four types" list that shipped two members short. The close is derivation: the reference graph exists in the metadata already (field types, reference_to, master-detail, lookups). Derive the paths, or state with measurement why derivation is impossible and what the honest fallback is.

    ⭐ If full derivation is out of reach, the acceptable partial is one that CANNOT silently under-report: a response that distinguishes "no references" from "this type's references are not computable". ⛔ An empty array must stop meaning both.

    PM 机制假设(须实测,鼓励证伪)

    1. "7 keys, 20+ declared types" comes from the Three read-side /meta verbs (history, audit, references) never reach canonicalizeMetaRequestType — they still derive their key from the MANIFEST map #9157 dev's measurement. ⭐ Re-derive both numbers from the code; a number handed to you is a hypothesis. If either moved, explain the delta rather than reconciling to mine.
    2. The panel is the consumer. Confirm the admin "Used by" surface actually reads this — if a different path feeds it, the severity argument changes and I want that before the fix.
    3. Three read-side /meta verbs (history, audit, references) never reach canonicalizeMetaRequestType — they still derive their key from the MANIFEST map #9157 landed the spelling fix (PR fix(metadata): route the three read-side /meta verbs through the canonical type boundary (#9157) #9191, the three read verbs now fold at the boundary). Its pin asserts this residue deliberately so it could not be read as closed — expect that pin to move, and say what it becomes.
    4. ⚠️ Every REFERENCE_PATHS key is manifest-present today, so a manifest-absent target answers [] even after Three read-side /meta verbs (history, audit, references) never reach canonicalizeMetaRequestType — they still derive their key from the MANIFEST map #9157's fold. That is precisely this card. Do not re-measure it as if it were new.

    Gates

    Derive after the final commit and read CI's own job list too (#9171, #9192 — the derivation missed five gates on a sibling this round). Predict reverse-verification direction in writing first; report it as observed even if the shape was wrong.

    Fixes #9190.


    Generated by Claude Code

  5. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 9190,
      "status": "needs_decision",
      "branch": "claude/issue-9190-reference-paths-derivation",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9324",
      "premise_still_valid": true,
      "summary": "Full derivation was REACHABLE, so this is not the negative result the brief allowed for. REFERENCE_PATHS is deleted, not extended; packages/metadata-protocol/src/reference-sites.ts derives the index at module load from DEFAULT_METADATA_TYPE_REGISTRY joined to getMetadataTypeSchema(), in the #7894 shape, so a newly declared type arrives covered. The premise held and UNDERSTATED the defect: 34 of the 40 curated paths name properties no schema declares, so only 6 were live and they reached 2 target types, not 7 — five advertised keys answered [] unconditionally while looking covered. Coverage is now 17 target types. status is needs_decision (not done) for one residual class the tier fence keeps out of scope: reference properties whose NAME does not spell their target need a producer-side annotation in packages/spec, and I carried exactly one bridge entry rather than curating a list. PR is Part of, not Fixes, so merging leaves #9190 open for that decision.",
      "tests": "All at the final commit 4e5989b9a (git rev-parse --short HEAD, quoted from the run). pnpm --filter @objectstack/metadata-protocol test --run: 'Test Files 118 passed (118)'. pnpm --filter @objectstack/objectql test --run: 'Test Files 213 passed (213)'. pnpm --filter @objectstack/rest test --run: 'Test Files 122 passed (122)' (first rest run was a false red -- 28 suites failed to RESOLVE @objectstack/service-analytics because the closure was unbuilt; pnpm --filter '@objectstack/rest^...' build fixed it, consumer/downstream direction). Gate union re-derived against my 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 run AFTER the final commit and after the full closure build (turbo run build --filter=./packages/* --filter=./packages/*/*): 'check-type-check-coverage --re-measure: OK -- 33 ledger entr(ies) re-measured in 301.1s, 1926 raw tsc error(s) total, none above its recorded number. surplus: none'. REVERSE VERIFICATION, direction predicted in writing BEFORE running (scratchpad prediction.md): ablating the SHAPE half of the naming rule would go red on exactly the anti-guesser pins and nothing else. Observed exactly that -- 'x an ENUM-constrained value that shares a type name is NOT a reference / AssertionError: expected true to be false / Test Files 1 failed | 117 passed (118)'. Fix was committed BEFORE the ablation, restored via git checkout of the branch, and proved byte-identical by diff against git show HEAD:. No dogfood ablation in this card, so no dist rebuild leg applies.",
      "open_questions": [
        {
          "question": "Reference properties whose NAME does not spell their target are not derivable from the schema as it stands. FieldSchema.reference names an object in PROSE ('Target object name (snake_case) for lookup/master_detail fields'); AppSchema.homePageId names a page; AppSchema.defaultAgent names an agent. I carried ONE bridge entry (field.reference) because dropping it would regress the highest-value edge in the graph, pinned it at one, and deliberately EXCLUDED the other measured members so the incompleteness stays visible. How should this class be closed?",
          "options": [
            "A - Producer-side annotation in packages/spec, e.g. .meta({ xRefTo: 'object' }) on the property, and derive a fourth limb from it. Same channel flow-node-expression-paths.ts already uses for .meta({ xExpression }) through z.toJSONSchema. Requires packages/spec + *.zod.ts edits, which this card's tier fence explicitly forbids without your sign-off.",
            "B - Grow SEMANTIC_REFERENCE_SITES by hand to cover the measured members. Cheap now; it is the #8908 shape one layer over, and the map would need re-auditing on every schema change.",
            "C - Leave the class open and rely on the pin. The bridge stays at one entry, the panel silently under-reports homePageId and defaultAgent edges, and the gap is documented but not closed."
          ],
          "recommendation": "A. Business need: measured, not speculative -- homePageId and defaultAgent are live app-authoring keys, and field.reference is THE object-to-object edge an admin consults before a delete, so the consumer exists and is the destructive-action gate itself. Long-term soundness: it puts the binding where the property is declared, so derivation reads it like any other schema fact and the reference graph stops having a second, hand-maintained half -- B reintroduces exactly the maintenance debt this card removed, and C leaves a known-wrong 'safe to delete' on real edges. AI-authored-metadata safety: A is the declared-equals-enforced side of ADR-0049 -- a new reference-bearing property is annotated at authoring time or it is not a reference, which is structural prevention, whereas B and C both depend on a human remembering. Trade-off stated honestly: A is the largest of the three and touches the spec surface, so it wants its own card with a spec-seat tier judgement rather than riding here."
        },
        {
          "question": "Wire-shape clause. The ruling's fallback said an acceptable partial must distinguish 'no references' from 'not computable', but both the triage note and the dispatch fence forbid a wire-level coverage discriminator without spec-seat routing. I resolved this 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-not-computable type (external_catalog, resolves no schema) is named in unwalkableSourceTypes and pinned by a test, so the set cannot grow silently. Is that the intended reading of the clause, or do you still want a response-level discriminator routed to the spec seat?",
          "options": [
            "A - Accept the build-time discriminator as the close; no wire change, fence untouched.",
            "B - Additionally route a response-level coverage discriminator (a coverage field or a 501) to the spec seat as a separate card."
          ],
          "recommendation": "A for this card, because it satisfies the clause's intent -- an empty array has stopped meaning two things -- without crossing the fence you drew tighter on purpose. B is worth considering only if a client needs to RENDER the distinction; measured, the panel does not, since it renders From type / From name / Path and never reads kind."
        }
      ],
      "out_of_scope_findings": [
        "NOT FILED -- GitHub API rate limit exceeded (search_issues and list_issues both returned 'API rate limit already exceeded for user ID 277994282'), so I could not run the dedup search that filing requires. Recording them here rather than filing blind or dropping them; both are mine to file once the limit resets, or yours to re-dispatch.",
        "FINDING 1 (metadata-protocol, same harm shape as this card): the `field` metadata type can never match as a reference TARGET. Field items are keyed '<object>.<field>' (per the #7893 comment on DEFAULT_METADATA_TYPE_REGISTRY), while every derived field reference site -- view.config.columns[].field, dataset.dimensions[].field, object.validations[].field and ~30 more -- holds a BARE field name. So GET /meta/field/account.owner/references scans 10 source types and answers [] on every deployment regardless of content. Derivation made this visible rather than causing it; it is the silent-empty defect for one more type and needs a name-form decision, not a path.",
        "FINDING 2 (packages/rest/src/rest-server.ts:4141-4143): when the resolved protocol has no findReferencesToMeta method, the route answers `res.json({ references: [] })` -- a capability gap rendered as 'nothing depends on this item', the exact ADR-0110 D3 / #8896 harm shape one layer above the one this card closed. A missing capability and an empty dependency list are different facts and this limb collapses them."
      ]
    }

    Notes the JSON cannot carry cleanly

    PM assumption 1 — re-derived, one number moved. Not "20+" declared types: exactly 27 in DEFAULT_METADATA_TYPE_REGISTRY, 26 with a resolvable schema (external_catalog has none). The "7 keys" figure is confirmed. The delta that matters is the one neither of us had: those 7 keys served 2 target types, because 34 of their 40 paths do not exist in the source type's schema.

    PM assumption 2 — confirmed, with a harder artifact than expected. The panel is the consumer (objectui ResourceEditPage.tsx → client.references() → ReferencesPanel), and its empty state is not neutral phrasing:

    'engine.edit.refsEmptyDesc': 'Nothing in the metadata graph points at this item. Safe to delete.'
    

    PM assumption 3 — the #9157 pin moved; here is what it became. It asserted translation answers [] and named that a coverage question for a later card. DocSchema.translations is a real, schema-declared site, so the pin now asserts that both spellings fold to the same non-empty result — a strictly stronger assertion than the empty one it replaced. #9157's own contract is untouched.

    PM assumption 4 — held, and is now moot by construction. I did not re-measure it as new. Derivation removes the manifest-present/absent question from this verb entirely: coverage no longer depends on a key existing in a table.

    Branch name. The claim comment says claude/issue-9190-reference-paths-coverage; my dispatch prompt said ...-derivation. I used the dispatch prompt's spelling — flagging the divergence rather than picking a side silently.

    Serial / hot file. My actual ranges in protocol.ts from git diff --unified=0: line 80 (import), 2955-3069 (registry ⇒ walker), 17843-17997 (the method). No overlap with #9173 (11398-11496 / 11650-11666) or #9196's JSDoc region.

    Fence. Not tripped, and I checked at the point the design could have drifted: no packages/spec edit, no *.zod.ts, no ledger row, no new refusal, no response-shape change. The one design pressure toward the fence is open question 1, which I stopped at rather than wrote.


    Generated by Claude Code

  6. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    PM adjudication — both open questions answered, card closed. This was never a dead dispatch; it was waiting on me. PM seat, session session_01NYgmGheCzM6NrHZN436Cxf.

    Reached during a maintainer-authorised sweep of stale pm:dispatched cards. The sweep's premise was "the seat died". Here the seat did the opposite: it delivered PR #9324 (merged), reported status: needs_decision, deliberately used Part of rather than Fixes so the card would stay open for adjudication, and then waited 18 hours for an answer that never came because the dispatching PM session did not return.

    That is a distinct failure from the one the sweep was looking for, and it matters more: pm:dispatched has no state for "delivered, awaiting the PM", so a card blocked on me is indistinguishable from a card being worked on. Recorded separately below.

    Q1 — reference properties whose NAME does not spell their target → option A, as its own card

    Ruled as recommended. The reasoning stands on its own and I am adopting it rather than restating it:

    it puts the binding where the property is declared, so derivation reads it like any other schema fact and the reference graph stops having a second, hand-maintained half — B reintroduces exactly the maintenance debt this card removed, and C leaves a known-wrong "safe to delete" on real edges

    B is the #8908 shape one layer over, and this card exists because hand-curation failed — choosing it here would be re-committing the error while closing the ticket for it. C is worse than it sounds: homePageId and defaultAgent are live app-authoring keys, so C means the panel keeps printing "Safe to delete." over real edges.

    A goes to a new card with a spec-seat tier judgement, exactly as you said it should — .meta({ xRefTo: … }) through the same z.toJSONSchema channel flow-node-expression-paths.ts already uses for xExpression. It does not ride here.

    Carrying the single field.reference bridge entry, pinning it at one, and deliberately excluding the other measured members so the incompleteness stays visible was the right call. A bridge that quietly grew to three would have hidden the very gap the card is about.

    Q2 — the wire-shape clause → A, and the clause does not actually apply

    Accept the build-time discriminator. But the sharper answer is that the question dissolves: the ruling's fallback read "if full derivation is out of reach, the acceptable partial is one that CANNOT silently under-report." Derivation was not out of reach — you achieved it — so the fallback's precondition never triggered. You are asking whether a substitute satisfies a clause that was conditional on failing, and you did not fail.

    The clause's intent — an empty array must stop meaning two things — is satisfied anyway, and by the stronger mechanism: for 26 of 27 declared types an empty answer is now a derived statement, and the one genuinely-uncomputable type is named in unwalkableSourceTypes and pinned so the set cannot grow silently. Moving a discriminator from the wire into the build, where it is checkable, is better than the wire change the clause imagined.

    No spec-seat routing. The fence held and you stopped at the one place it could have drifted, which is what the tighter fence was drawn for.

    On the premise being understated

    34 of the 40 curated paths name properties no schema declares, so only 6 were live and they reached 2 target types, not 7 — five advertised keys answered [] unconditionally while looking covered

    That is worth more than the fix. The card said seven types were covered and 20+ were not; the truth was two covered and 25 not, with five keys in the table actively lying about coverage. And the consumer-side artifact you pulled makes the harm concrete rather than argued:

    'engine.edit.refsEmptyDesc': 'Nothing in the metadata graph points at this item. Safe to delete.'
    

    Re-deriving the numbers instead of reconciling to the PM's was correct, and it is the second time tonight a seat has improved a card by refusing to trust a figure handed to it.

    Your two unfiled findings

    Both were blocked by the shared rate limit, correctly recorded here rather than filed blind or dropped. I am filing them — you should not have to carry them.

    FINDING 2 is the more serious and I want it stated plainly: rest-server.ts:4141-4143 answers {references: []} when the resolved protocol has no findReferencesToMeta at all. That renders a capability gap as "nothing depends on this item" — the exact ADR-0110 D3 / #8896 harm shape, one layer above the one you just closed, and it prints the "Safe to delete." string above over a question the platform never asked.

    Closing this card. Its own scope — derivation replacing hand-curation — is delivered and merged. The residue is Q1's class, which becomes its own card rather than holding this one open.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions