Skip to content

fix(core)!: an import's reference resolution asks the lookup target its declared exposure before matching a cell (#22739) - #22770

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-22739-import-ref-exposure
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-22739-import-ref-exposure

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #22739
Clause-②: yes (narrowing)

Census row 7 of #22661: the import door's reference resolution matched a lookup cell against a TARGET object nobody addressed, without asking that target its declared exposure. A match stored the target record's id and a miss answered reference_not_found per row, so the row report told the two apart even for a target every data route refuses.

What changed

The decision takes no caller, so an administrator, a member and a system context (the connector pull, which drives runImport through the real protocol) are answered the same, as #22661's two reads answer every caller the same.

Measured on a real stack (fixture objects only)

@objectstack/verify boot with the real SecurityPlugin, ObjectQL, SQL driver and REST layers; the #22661 fixture targets plus a row-scoped one; an administrator and a member; the synchronous import door and the async jobs door; three cells per target (naming the record, naming none, the record's id).

target declaration before (base bf515e724d) after
off switch / whitelist without list (create-only, get-only); deny-all pinned in the core unit test only match and id stored, miss reference_not_found all three reference_not_found
no enable block / whitelist granting list match and id stored, miss reference_not_found unchanged
PRECEDENT: member without read on the target, or record hidden by row scope all three reference_not_found unchanged

Identical for both personas and both doors (the member's job report was read with a read grant on its own import jobs added to the fixture member). The refused rows carry the precedent's exact sentence shape (field label, then the cell). Before-readings reproduce #22661's census row 7.

Pins and ablation

  • packages/core/src/utils/import-runner-reference-exposure.test.ts (14): per persona, four refusing declarations answer match, miss and id alike and never call findData; two serving declarations resolve (controls); one fail-closed case; and the user-field control (a user field without reference has no target and its cell reaches the write unresolved, as on main).
  • packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts (11): per persona, an ARMED leg (each target's own list answer read off the data door), then per door (sync and jobs) a withheld leg and a served control; plus the precedent leg.
  • packages/core/src/security/second-object-read-exposure.pin.test.ts (4): the new decided row.

Ablation, each through scripts/ablation-replace.mjs on committed 22bfc2770b, anchors 1 to 0, each restore proven (blob equals HEAD, git diff HEAD empty):

  • A, wiring bypassed in resolveRef: core pin 9 red / 4 green (every refusing leg plus fail-closed).
  • A2, operation get: 4 red / 9 green (get-only refused legs and list-only controls flip).
  • A3, the catch fails open: 1 red (fail-closed).
  • D, import-field-meta.ts back on the raw carrier: enumeration pin 2 red (the stale classification and the matcher leg).
  • G (rework, on committed f06912e442), the guard removed (reference: referenceTargetOf(f)): the user-field control 1 red / 13 green.
  • C, A on the built artifact: core rebuilt, ablation-dist-preflight marker present (exit 0), dogfood pin 4 red / 7 green (the withheld leg of both doors for both personas); restore rebuilt, preflight --absent exit 0, rerun 11 / 11.

Verification

Each reading names the commit it was taken on. The final head is c06ec74a87. The rework merged origin/main twice: 0d326bfb12, then c06ec74a87, because #22766 edited the enumeration pin. The decided lists were united: servesExpansionTarget, servesLabelTarget, servesPayloadDisplayTarget, servesReferenceTarget, servesSummaryTitleTarget.

  • @objectstack/core at c06ec74a87, after a full workspace build: test 93 files / 2349 passed; test:repo 5 files / 55 passed; typecheck exit 0, including check:test-typecheck.
  • @objectstack/dogfood at c06ec74a87: this pin 11 / 11 and security(data, analytics): a lookup target's exposure declaration is not judged when the data door's $expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661's second-object-exposure pin 25 / 25 (36 / 36); typecheck exit 0.
  • Importers at c06ec74a87: @objectstack/rest typecheck exit 0, and its 38 test files that reach the import, export or template doors 38 / 38; @objectstack/plugin-auth admin-import-users 2 files / 50; @objectstack/service-automation connector pull 3 files / 22.
  • Lint, narrowed and proven: eslint --no-inline-config on the 6 changed .ts files; population read from --print-config; --format json 6 files, 0 errors, 0 warnings. eslint.config.mjs enables no type-aware linting. Repo-wide pnpm lint is CI's.
  • Gates at c06ec74a87: dispatch-gates --commands derived 69 (the dispatch's 51 are a subset); all 69 exit 0; --ran: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN.

File surface

Acceptance notes

  • Clause-②: yes (narrowing) (edited by the seat after the contract review 6106439614): no accept set widens, but ImportProtocolLike, exported from @objectstack/core, gains one optional member (getMetaItem?). That is an additive, type-level public-surface change, spelled yes by the fleet's practice. The changeset's line follows on this PR's next head.

  • user fields written without reference. The field set the import, export and template doors treat as references is exactly main's, so such a field is unchanged here, and pinned. The door disagrees with the spec's arbiter, which gives it sys_user. That gap is measured on main and carried by import: a user field written without reference fails reference_not_found on import, while the spec's arbiter gives it sys_user #22785, because fixing it is a Clause-② widening.

  • The residual narrowing: a non-reference type that declares reference. Read through the arbiter, it names no target. The only population whose answer moves is the legacy, schema-refused type: 'reference' spelling, which is still listed in the doors' own type tables. An AST census over every git-tracked non-test source under packages/ and examples/ (3,636 files at 0d326bfb12, the same at c06ec74a87) found 0 shipped field declaring it. Controls: 30 reference-typed literals and 80 Field.lookup/masterDetail/user/tree calls. Positive control: three planted shapes were all found. The changeset states it in FROM → TO.

  • Measured producers. On origin/main bf515e724d, twelve in-repo objects refuse list by declaration and exactly one lookup points into any of them, from an object that is itself apiEnabled: false; control: 75 lookups into sys_user. No shipped object's import answer changes.

  • toFailedResult is untouched; [finding] import: a sandbox's own fault (CPU budget, wall-clock ceiling) reaches an import row as its debug wrapper, where the data doors answer Internal server error #22741 follows in this file.


Generated by Claude Code

…s declared exposure before matching a cell (#22739)

Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T
Co-authored-by: Claude <noreply@anthropic.com>
…sure on both doors; classify it in the enumeration pin (#22739)

Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T
Co-authored-by: Claude <noreply@anthropic.com>
…since the target is read through referenceTargetOf (#22739)

Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 11, 2026
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/core, touching 12 documentable anchor(s).

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/api/wire-format.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/data-modeling/drivers.mdx (via getMetaItem (symbol, a method of interface ImportProtocolLike))
  • content/docs/data-modeling/fields.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/data-modeling/import-mappings.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it), /:object/import/jobs (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/kernel/services-checklist.mdx (via getMetaItem (symbol, a method of interface ImportProtocolLike))
  • content/docs/protocol/objectql/state-machine.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it), /:object/import/jobs (route, bridged from symbol runImport — its route source's handler names it))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/releases/v17/17-6.mdx (via runImport (symbol, a top-level function), /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/releases/v17/index.mdx (via ExportFieldMeta (symbol, a top-level interface))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: /data/{target} (route, 58 pages)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e84aeb36ce14169a633670f14ce8280fc998e2b9 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 25ebc0607bcc94fd6196a9873ac3abc62bdb9980 — the merge of head c06ec74a8799c72ffed17d51466de12860dcc285 into base e84aeb36ce14169a633670f14ce8280fc998e2b9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 25ebc0607bcc94fd6196a9873ac3abc62bdb9980 && git checkout 25ebc0607bcc94fd6196a9873ac3abc62bdb9980
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e84aeb36ce14169a633670f14ce8280fc998e2b9 c06ec74a8799c72ffed17d51466de12860dcc285 && git checkout -B drift-repro e84aeb36ce14169a633670f14ce8280fc998e2b9 && git merge --no-ff c06ec74a8799c72ffed17d51466de12860dcc285

node scripts/docs-audit/affected-docs.mjs --json e84aeb36ce14169a633670f14ce8280fc998e2b9

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs e84aeb36ce14169a633670f14ce8280fc998e2b9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…s one, reading its target through referenceTargetOf; pin the user-field control (#22739)

Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T
Co-authored-by: Claude <noreply@anthropic.com>
… and its producer census (#22739)

Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T
Co-authored-by: Claude <noreply@anthropic.com>
…port-ref-exposure

# Conflicts:
#	packages/core/src/security/second-object-read-exposure.pin.test.ts
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c06ec74a8799c72ffed17d51466de12860dcc285
Local-runs: none

Inputs, and nothing else: card #22739 (body; comments 6104521404 claim, 6105593563 round-1 dev report, 6105610630 REWORK, 6106294041 round-2 dev report), parent card #22661 (body; comments 6096329009 triage, 6101260211 claim, 6102815147 dev report, 6102873261 census split, 6102951996 ACCEPT, 6104471901 landing) with its contract review 6102920614 on PR #22735 and #22737's contract review 6106147584 on PR #22768, PR #22770 (body, its 7-file list, the net diff from merge-base e84aeb36ce to the head: 7 files, +367/−21, byte-matching the file list; the head is a merge of origin/main), the tree at the head where a judgment needed a caller or a definition read (git show against the fetched object, no checkout), card #22785's title and body for the one question ③ asks of it, and the head's check-runs, read 2026-10-11T06:55Z. Of 42 check-runs (the push's workflows at 05:57Z and the PR Automation re-run on the body edit at 06:38Z): 37 concluded success (Build Core, Test Core and its six shards, Dogfood Regression Gate and its three shards, Dogfood Verify CLI, Temporal Conformance, Lint & Repo Gates, Type Check · consumer gates / debt ledger / source gates / workspace, TypeScript Type Check, Governed Surface Queue Guard, Spec property liveness, Check Documentation Links, Flag docs affected, filter, and Check Changeset, Auto Label, Check PR Size and the four claim / branch / part-of guards on the push run, with Check Changeset and the four guards again on the re-run), 5 skipped by their own triggers (Build Docs, Console Pin Gate, Packed-tarball smoke; Auto Label and Check PR Size on the body-edit re-run only, both success on the push run); none in progress. Security card: classes, positions and functions only.

① Derived judgments

  1. @objectstack/core — accept-set narrowing in runImport's resolveRef (import-runner.ts). Before any candidate-field read, the resolver asks servesReferenceTarget(p, referenceObject) once per target per import (the promise is cached in servedTargets, ahead of refCache) and answers {} when the target is not served. Reach, read at the head: every runImport caller — the synchronous import door and the async jobs door in rest-server.ts (both hand the environment-resolved protocol), the connector pull in service-automation (ConnectorPullProtocol requires getMetaItem), and plugin-auth's identity import (judged under ①.7). A dry run is judged too, so the dry-run report leaks nothing a real run withholds. Right.

  2. Operation list (REFERENCE_TARGET_OPERATION). The resolver's read is findData with a where on one candidate field at a time (id, the display field, then the human identifiers): a predicate read over the target, the runtime find that DATA_ACTION_TO_API_OPERATION maps to list, and the read the target's own list route with a field filter performs. It is not bounded by ids the caller holds (a name cell holds none; a pasted id is one candidate among seven). So this is security(data): a nested-relation filter condition on a lookup target is evaluated without asking the target's exposure (census row 4 of #22661) #22737's reading, not fix(metadata-protocol,service-analytics)!: a read that follows a lookup asks the target object its declared exposure — $expand and the dataset label passes (#22661) #22735's: a get-only target is refused here and served to $expand, a list-only target the reverse — one decision answering two different reads. Right. The dogfood pin arms each target on the data door's own list answer before believing the import's, so the operation is measured, not asserted.

  3. The refused answer is the measured precedent, by construction. resolveRef returns {} before any read; coerceRow (import-coerce.ts) turns that into the same coerceError(…, 'reference_not_found', 'import_reference_not_found', …) call a miss takes, and the call the precedent takes on main (a target the member cannot read throws inside the candidate loop, is caught per candidate, and yields {}). A cell naming a record, a cell naming none and a pasted id are therefore one answer with one sentence, and nothing of the target is read (the core pin asserts findData never called). No new error code, no status change; the dogfood PRECEDENT leg pins the member-unreadable case unchanged. Right. Disclosure judged: the sentence is the one the miss already carried, and the target's name is already in the source object's served declaration; no new disclosure class.

  4. One decision, no second rule. servesReferenceTarget asks canServeApiOperation(enable, 'list') of found.item.enable as getMetaItem serves it — the boolean face of apiExposureDenialReason, which puts apiEnabled: false ahead of the whitelist, reads the deny-all apiMethods: [] and a non-array as closed, and a missing block as open. The enumeration pin holds the function body to DECISION_CALL and the file to the spec import. The decision takes no caller — no context is passed — so an administrator, a member and the connector pull's job context are answered alike, as security(data): a nested-relation filter condition on a lookup target is evaluated without asking the target's exposure (census row 4 of #22661) #22737 pinned a member and a system context alike at the data door. Right, with the bound named: the connector pull reads through the door-level protocol, not the engine, so it is a door caller in the family's sense and not one of the privileged engine callers fix(metadata-protocol,service-analytics)!: a read that follows a lookup asks the target object its declared exposure — $expand and the dataset label passes (#22661) #22735 left untouched; the in-repo producer census (twelve list-refusing objects; one lookup into them, from an object itself apiEnabled: false) bounds the effect to zero shipped objects; a deployed mapping whose target refuses list is NOT MEASURED, as the changeset says.

  5. Fail-closed. A getMetaItem throw returns false; the only awaited call is inside the try, so a rejected promise never reaches the cache. The catch logs nothing — the runner carries no logger — so an operator whose metadata read fails sees every reference cell of that target refuse with the ordinary sentence; closed is the right direction and the once-per-import ask keeps the cost to one read. Right; the silent catch is named, not a defect. A target with no declaration (an unknown name) is default-open to the decision and then fails in the candidate loop exactly as on main.

  6. The target's declaration as the real protocol serves it. getMetaItem on ObjectStackProtocolImplementation takes only the request (no caller context; state omitted reads the active row) and answers the { type, name, item, … } envelope the prepare step already reads at the same door (import-prepare.ts probes typeof p.getMetaItem === 'function' on the same object); item.enable is the block apiAccessDenialFromEnable judges for the addressed object. The dogfood pin's withheld legs could not pass if item.enable arrived absent (default-open), so the shape is measured, not assumed. Right.

  7. ImportProtocolLike gains getMetaItem? — the public surface, and the absent-member stand-down. ImportProtocolLike is exported from @objectstack/core's index (export * of import-runner.js) and re-exported by @objectstack/rest's, so the published declaration gains one optional method: a public-surface widening, type-level and additive — no implementor must add it, no accept set of any door grows, no served answer widens, and the member's shape is the getMetaItem the real protocol and ConnectorPullProtocol already carry. Where the member is absent, servesReferenceTarget answers true: the capability-gate shape validateData? on the same interface takes, and the one fix(metadata-protocol,service-analytics)!: a read that follows a lookup asks the target object its declared exposure — $expand and the dataset label passes (#22661) #22735's review judged on the label face ("absent probe, no gate") as right with the dependency named. Named here: (a) the one caller without it is plugin-auth's identity import (admin-import-users.ts), whose hand-written run protocol omits getMetaItem although the same function holds deps.getMetaItem and hands it to prepareImportRequest a few lines earlier — "no declaration to judge" is a choice at that door, not a fact of it; its field map is sys_user's own, so its targets are platform objects (sys_user serves list by the dev's control count; the others NOT MEASURED), behind a platform-admin door; carried to ③. (b) A future runImport caller that hands a protocol without the member is unjudged and nothing goes red for it: the enumeration pin holds the decision function and its wiring, not the capability gate. Outside this card's surface; named.

  8. The rework's guard — the reference field set is exactly main's. buildFieldMetaMap sets reference only when f.reference is a string, as on main; the value is now referenceTargetOf(f). For every type in REFERENCE_VALUE_TYPES (lookup, master_detail, user, tree) the arbiter returns the declared string (a declared target wins over the implicit sys_user), so those fields' targets are byte-identical to main's. A user field without reference has no target on main and none here (the guard precedes the arbiter), pinned in the control and ablated (G: 1 red). A non-string carrier never reaches the arbiter, so its throw is unreachable from these doors — the dev's round-2 correction (d) is right. Right.

  9. The residual narrowing, measured and stated. A string reference on a type outside the four now names no target. Which door read it before: coerceRow resolves only when IMPORT_REFERENCE_TYPES has the type, the export's referenceFieldNames expands only when its local REFERENCE_TYPES has it, and the template's reference branch sits under IMPORT_REFERENCE_TYPES — and both tables are the spec's four plus the legacy spelling 'reference'. So the only population whose answer moves is a field typed 'reference' with a reference string (import stops matching it, export stops expanding it), exactly the changeset's FROM → TO; every other non-reference type, and a type-less field, was never resolved by these doors. The dev's AST census (0 shipped producers; 30 + 80 controls; three planted shapes found) is the measurement and was not re-run here. Right. One wording in the PR body is off: FieldSchema carries no per-type refinement on reference (the only one near it requires the key on lookup / master_detail), so the packages/rest fixture's number field with reference parsed — the key was inert on that type, not refused. The fixture change is right for the right reason (the test asserts presentation keys, and the arbiter now answers none for that type); "never spec-legal" should read "inert" — ③.

  10. Enumeration pin. import-field-meta.ts moves from the declared blind spot to decided (list, servesReferenceTarget, wiring servesReferenceTarget(p, referenceObject, its pin); the operation type widens to 'get' | 'list'; the decided list is the union with fix(plugin-approvals, plugin-audit)!: a lookup title is served only for a target whose declared exposure serves get #22766's two rows; the header's import-door sentence goes. Read at the head, the wiring string is present, the function body carries the decision call and the file the spec import. Right.

  11. Pins. Core unit pin: per persona, four refusing declarations × match / miss / id with findData never called, two serving controls, one fail-closed leg asserting a single getMetaItem call, and the field-set control (owner / assignee / amount → none / sys_user / none, the unresolved cell reaching the write as written, one declaration read and for sys_user only). Dogfood pin, real stack, both personas, both doors: ARMED on each target's own list status, withheld legs, served controls read back off the engine, the precedent leg. The jobs door's member leg needs a read grant on the member's own import jobs, added in the new file over the untouched security(data, analytics): a lookup target's exposure declaration is not judged when the data door's $expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661 fixture — right. The ablation ledger (A, A2, A3, D, G, C) is the dev's measurement, not re-run here; what this record judges is the pins' construction, which cannot pass with the decision bypassed (never-called assertions plus the enumeration pin's decision-call check), and whose shapes cover the decision's whole distinction set for list.

  12. Reach. Stated on a real stack at base bf515e724d over fixture objects, both personas, both doors; in-repo producers censused at bf515e724d; deployed and cloud-held definitions NOT MEASURED. The bound is stated, not hidden.

② Semver level

  • .changeset/22739-import-reference-target-exposure.md: @objectstack/core minor with the ! summary, the **BREAKING** banner, FROM → TO for both moved populations (the unserved target; the legacy spelling), a one-line fix, and exactly one ADR-0087 marker (not-required (no-migration-prescription) — no spec key, spelling, export or stored shape moves; the fix(metadata-protocol,service-analytics)!: a read that follows a lookup asks the target object its declared exposure — $expand and the dataset label passes (#22661) #22735 / security(data): a nested-relation filter condition on a lookup target is evaluated without asking the target's exposure (census row 4 of #22661) #22737 shape). Right: an accept-set narrowing is breaking and the launch window ships breaking as minor (check-changeset-no-major.mjs); Check Changeset concluded success on both runs. The bindings paragraph matches the diff sentence by sentence (every caller whose protocol reads declarations; no new code; the field set does not widen). The other touched packages publish nothing from this diff: rest (one test), dogfood (private). @objectstack/rest re-exports the type by reference and changes no source, so it owes no changeset of its own.
  • Clause-②: no (narrowing) on line 2 of the PR body and in the changeset. Arm right — the spelling clause2-line.mjs reads, and the one check-adr-0087-registration.mjs takes the breaking-ness from. Value understated, by the fleet's own recorded practice: an optional member added to a published declaration is spelled yes — in the tree at the head, 22663-analytics-unexposed-cube-registration.md, a narrowing of this same family whose CubeRegistry gained one optional constructor parameter, reads yes (narrowing), and three pending changesets that add one optional interface member (22434, 22575, 22754) read yes (widening). Here ImportProtocolLike gains getMetaItem? on @objectstack/core's published index (①.7), which the changeset's own marker and bindings state in full. So does any widening survive the rework? Of an accept set, none — the field-set widening is gone and pinned gone. Of the public face, this one optional member, additive and type-level. The level is unaffected (yes needs at least minor: it is), the ADR-0087 disposition is unaffected (the arm is narrowing: the marker is present), so no gate reads differently and nothing ships differently. Judged a declaration gap of the fix(metadata-protocol,service-analytics)!: a read that follows a lookup asks the target object its declared exposure — $expand and the dataset label passes (#22661) #22735 class, not a semver mismatch. Prescription, non-blocking: the owning seat edits the PR body's line to yes (narrowing) now (a body edit spends no head), and the changeset's line on the next head this PR takes (③ names the one it is likely to take anyway).

③ Boundary flags

Implemented-by: claude/issue-22739-import-ref-exposure
Reviewed-by: session_01JfJfBUC3cQ6hhgm9MQK76T

VERDICT: PASS

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661)

2 participants