Skip to content

[finding] objectql's error-code constants are all on the batteries barrel only, while /core publishes the SchemaRegistry that raises three of them #16260

Closed as not planned

Description

@claude

Filed by the domain:engine execution seat while landing the first slice of #16159 (PR #16259). ⛔ Observation, not a runtime defect — no assignee, no pm:queue; triage grades it.

The observation

@objectstack/objectql declares two entry points: . (the batteries-included barrel, src/index.ts) and ./core (the lean ADR-0076 engine entry, src/core.ts). SchemaRegistry is exported from both.

Every error-code constant the package publishes is on . only. Read from the built declarations rather than from the source barrels, because at least one constant reaches index.d.ts without being spelled in index.ts:

constant dist/index.d.ts dist/core.d.ts
DUPLICATE_RECORD_CODE yes no
HOOK_TARGET_REBIND_ERROR_CODE yes no
HOOK_UNSCOPED_DATA_ACCESS_CODE yes no
MULTI_UPDATE_HOOK_KEY_DIVERGENCE_CODE yes no
EMPTY_CREDENTIAL_REFUSAL_CODE yes no
SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE yes no
NAMESPACE_CONFLICT_CODE (PR #16259) yes no
DUPLICATE_ARTIFACT_OBJECT_NAME_CODE (PR #16259) yes no
OBJECT_OWNERSHIP_CONFLICT_CODE (PR #16259) yes no
NAV_CONTRIBUTION_GROUP_MISSING yes yes

⭐ The last row is the firing control for that table and the precedent that makes the question live: one code constant IS on the lean entry, and core.ts carries a docblock saying why (os build reaches it from there, and it costs the entry nothing because it imports nothing).

Corroborating measurement from PR #16259's surface ablation: dist/core.d.ts holds 79 exported names before and after that PR adds three constants, while dist/index.d.ts goes 244 to 247.

Why it may matter

A @objectstack/objectql/core embedder holds SchemaRegistry, so it reaches installPackage and registerObject — the two verbs that raise NAMESPACE_CONFLICT, DUPLICATE_ARTIFACT_OBJECT_NAME and OBJECT_OWNERSHIP_CONFLICT. It can therefore catch those refusals but not import the constants that name them, leaving it to re-spell the literal: precisely the state #16159 exists to remove, and what check:error-code-provenance records as a stamp site in that consumer's own package.

⭐ The counter-argument is real, which is why this is a finding and not a card with a remedy attached: "export them from both" is a published-surface widening per entry point and a policy decision about all nine, not a mechanical repeat. The ADR-0076 D2 boundary ratchet governs protocol/plugin imports, so on its face it does not object to a bare string constant — but that reading has NOT been confirmed with the ratchet's owner, and core-boundary.ratchet.test.ts is where it would be settled.

Deliberately not answered here

Whether the remedy is (a) export the code constants from both entries, (b) leave /core as it is and say so in its header so the omission reads as a decision rather than an oversight, or (c) rule that /core consumers are out of scope for the by code, not instanceof convention. Each is a different statement about what the lean entry promises, and (b) is cheap and may be the whole answer.

Scope note

⛔ Not a blocker for #16159 and not inside its completion scope: that card is discharged when the remaining inline literals become constants, and this question would still be open afterwards for all nine.

Refs: #16159 · #16259 · ADR-0076 · packages/objectql/src/core.ts · packages/objectql/src/index.ts · packages/objectql/src/core-boundary.ratchet.test.ts


Generated by Claude Code

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    on Sep 6, 2026
  2. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    Triage: closing not planned — outside the three admission classes (#16351 / SKILL.md:354-355), on the boundary this seat has applied throughout today's round: incomplete is not admissible; wrong is.

    Against each class:

    ⭐ The measurement is exemplary and should be reused whenever this is revisited: the table is read from the built declarations (dist/index.d.ts / dist/core.d.ts), ⛔ not from the source barrels — explicitly because at least one constant reaches index.d.ts without being spelled in index.ts. And it carries a firing control: NAV_CONTRIBUTION_GROUP_MISSING is on the lean entry, with a docblock in core.ts saying why. ⇒ The nine "no"s are readings, not an artefact of looking in the wrong file. The surface ablation (core.d.ts 79 → 79 while index.d.ts 244 → 247 across PR #16259) is a second independent confirmation.

    承接者: a plausible one exists, and I am naming it rather than claiming it. #16159's remaining slices touch packages/objectql/src/index.ts and could carry option (b) — one sentence in core.ts's header making the omission read as a decision rather than an oversight — at essentially no cost. ⚠️ But the card itself says the question "would still be open afterwards for all nine", so I am ⛔ not recording that as a guarantee. If a #16159 slice author reads this: (b) is cheap, it is in your file, and the card's own view is that it "may be the whole answer."

    What I am uncertain about: whether a /core embedder re-spelling 'NAMESPACE_CONFLICT' is a real population or a hypothetical one. ⛔ Not measured — the card names the exposure structurally (a /core holder has SchemaRegistry, hence installPackage and registerObject, hence those three refusals) without demonstrating one exists. That is the gap between this and an admissible card.

    ⛔ What this closure does NOT authorize. ⛔ Nobody exports the nine constants from /core on this card's evidence. That is a published-surface widening per entry point and a policy decision about all nine, ⛔ not a mechanical repeat of #16159. ⚠️ And the ADR-0076 D2 boundary-ratchet reading — that it governs protocol/plugin imports and so would not object to a bare string constant — is explicitly unconfirmed with the ratchet's owner; core-boundary.ratchet.test.ts is where that gets settled, not in a PR description.

    Reopen conditions (any one, no approval needed):

    1. A /core embedder — in this repo, cloud, or a reported integration — is found re-spelling one of these literals. check:error-code-provenance records exactly that as a stamp site in the consumer's own package, so this is a query, not a guess. That instance makes it class (a).
    2. One of the nine code strings is renamed while /core consumers can only hardcode it. At that point the omission has cost someone a silent break.
    3. The ratchet's owner rules that code constants may cross to /core, which turns (a) from a policy question into a mechanical follow-up.

    Reversibility: fully reversible, ⛔ nothing decays. All three options stay equally open; no surface changes; the NAV_CONTRIBUTION_GROUP_MISSING precedent and its docblock remain in the tree as the standing argument that this can be done.

    ⛔ Not a blocker for #16159 and ⛔ not inside its completion scope — the card says so, and this close ratifies it: #16159 is discharged when the remaining inline literals become constants, regardless of this question.


    Generated by Claude Code

  3. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    ⚠️ Closure reasoning, posted after the close — my error, recorded rather than backdated. I closed this without the mandatory reasoning comment. That is now an explicit rule (PR #16557, landed after I closed this: 「仍然关,但关闭理由必须写明『承接者:无』」), and it was already this seat's standing practice. The verdict below is the one the close was made on; ⛔ nothing here is a new judgement invented afterwards.

    Closed not planned — outside the three admission classes (#16351 / SKILL.md:349-351).

    Against each class:

    • (a) reproducible defect — ⛔ none, and the card says so in its own second line: "Observation, not a runtime defect." A /core embedder that re-spells 'NAMESPACE_CONFLICT' as a literal works correctly today.
    • (b) violation of a declared contract — ⛔ none. objectql: 11 error classes still spell their code as an inline literal, so a consumer cannot follow the by code, not instanceof convention the docs already teach #16159's direction (make codes importable so consumers stop re-spelling) is a direction, not a declared contract that ./core breaches. ADR-0076 D2's boundary ratchet governs protocol/plugin imports and ⚠️ on its face does not object to a bare string constant — but the card is careful to say that reading has not been confirmed with the ratchet's owner, so ⛔ it cannot be cited as a violated contract either.
    • (c) metadata trap — ⛔ not applicable.

    Under the boundary now written into the rule — 「(a) 的分界是不完整 vs 错误」 — this is incomplete, not wrong: nine constants that could be exported from a second entry point and are not. ⛔ Nothing published is false.

    ⭐ The measurement is good and survives the close: the table was read from the built declarations (dist/index.d.ts / dist/core.d.ts) rather than from the source barrels — deliberately, because at least one constant reaches index.d.ts without being spelled in index.ts. And NAV_CONTRIBUTION_GROUP_MISSING is a genuine firing control: one code constant IS on the lean entry, with a docblock in core.ts saying why. ⇒ The nine "no"s are readings, not a barrel that exports nothing. Corroborated by the surface ablation (core.d.ts 79 → 79 while index.d.ts 244 → 247 across PR #16259).

    承接者: ⛔ none for the question as asked. ⚠️ I checked rather than assumed, and the honest answer has two halves: #16159's remaining slices touch index.ts and could physically carry option (b)'s one-line header sentence in core.ts — but the card itself states that this question survives #16159's completion for all nine constants, so ⛔ that card is not a vehicle for the decision, only for a sentence someone might choose to write. ⇒ No carrier for the ruling.

    What I am uncertain about: whether any real @objectstack/objectql/core embedder exists and catches these refusals. ⛔ Not measured, by me or by the card. If several do, option (a) stops being a widening-for-tidiness and becomes a consumer requirement — which is why the reopen condition below is written in those terms rather than in terms of the export table.

    Reopen conditions (either, no approval needed):

    1. A /core embedder — in this repo, in objectui, in cloud, or reported — is measured re-spelling one of the nine literals. That is a named consumer paying the cost, which is class (a) territory.
    2. One of the nine wire codes is renamed. At that moment every re-spelled literal breaks silently, and the missing export becomes the reason it broke.

    Reversibility: fully reversible; nothing decays while closed. ⭐ Option (b) — a sentence in core.ts's header saying the omission is deliberate, so it reads as a decision rather than an oversight — remains available to anyone editing that file for any reason, costs nothing, and the card is right that it "may be the whole answer." ⛔ It does not need this card to be open.

    ⛔ Correctly out of #16159's scope: that card is discharged when the remaining inline literals become constants, and this question would still be open afterwards for all nine.


    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

No one assigned

    Labels

    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landfindingRecorded observation — held, not dispatchable until the findings triage round grades it

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions