Skip to content

fix(rest)!: both import doors answer for the object as the data door does (#27728, #27729) - #27897

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-trba3v
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-trba3v

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Fixes #27728
Fixes #27729

domain:server#3 · os-elon-musk · session_016WV4cxENxbA8ZpRgwafjvs

Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no hand-written docs page or published skill states what either import door answers for an unregistered object, a disabled package's object, or a short name (git grep of content/docs outside references//releases/ and of skills/ for the import routes with OBJECT_NOT_FOUND, OBJECT_PACKAGE_DISABLED, 404 or "not found": 0 hits; wire-format.mdx names the sync route only as the bulk door, import-mappings.mdx documents per-row outcomes).

维护者速读

  • 改了什么:同步导入 POST /data/OBJ/import 现在先问对象在不在:没注册的对象回 404 OBJECT_NOT_FOUND,停用包里的对象回 404 OBJECT_PACKAGE_DISABLED,和数据门 POST /data/OBJ 一字不差(原来回 200,错误藏在每行结果里)。两个导入门与 API 暴露闸都改用同一种读法:旧式 ns__short 对象用短名访问时,认的就是数据门认的那个对象。
  • 为什么改:按分诊 6509235875(rest: POST /data/:object/import (synchronous import) on an unregistered object answers 200 with a per-row OBJECT_NOT_FOUND, where POST /data/:object answers 404 #27728)与 6509225810(rest: pin the async import job door's disabled-package refusal, and decide whether it should resolve a legacy ns__short name as the data door does #27729):只看状态码的导入客户端,拼错对象名时会以为成功;作业门对短名比数据门更严;暴露闸对短名放行,apiEnabled: false 的 ns__short 对象用短名能被读写(实测)。
  • 风险与回滚:收窄,已写 BREAKING 说明;只影响拼错/未注册/停用包的对象名和带 __ 的旧对象名,已发运的对象都不带 __。回滚即删 data-action-routes.ts 里同步门那一行调用,再把两处 item.name === name 比较加回去。
  • 先看:packages/rest/src/data-action-routes.ts(同步门新增的 assertImportObjectExists 调用与函数里去掉的名字比较);packages/rest/src/meta-item-read-gate.ts 的 loadObjectItem;新测试 packages/rest/src/import-door-object-existence.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/import-door-object-existence.test.ts,预期 11/11 通过。
  • 需要你做的:无,席位落地(合同复审由席位安排)。

What changes

Tests

packages/rest/src/import-door-object-existence.test.ts (new), real RestServer routes on a real ObjectQL engine + ObjectStackProtocolImplementation, SQLite :memory:, real sys_import_job, real disablePackage:

Ablation (both source files reverted to main, test kept): 6 of 11 red (the two #27728 cases, the sync disabled-package case, and three short-name cases); the 5 that stay green are the controls, the data-door reference reading, and the job-door disabled-package pin (already true on the base). Restored: 11/11.

Acceptance notes

  • Triage rulings followed as written; nothing re-ruled.
  • Out of scope, read from the code and not measured: the cross-object POST /batch per-op exposure gate (batch-routes.ts) keys the object LIST by name, so it keeps the old short-name pass-through. This harness's runtime answers 501 for a transactional batch, so it could not be measured here. Noted for the seat; not filed.

Verification

Head c0b19922, base main 30f8fdd3 (#27795 and #27711 already in the base).

  • pnpm --filter @objectstack/rest exec vitest run (whole package): Test Files 353 passed (353), Tests 7031 passed, 350 skipped.
  • pnpm --filter @objectstack/rest run typecheck: exit 0. pnpm lint: exit 0.
  • dispatch-gates: 60 derived families, 59 run with exit 0, 1 NOT MEASURED: pnpm check:dual-build-cjs-loads exits 3 (needs every package's dist; CI builds it). check-plugin-teardown-shape --self-test first exited 3 in the shallow clone and passed (0) after fetching its pinned commit. --ran reconciliation: "60 derived famil(ies) accounted for — 59 run, 1 NOT-MEASURED".
Dogfood battery (narrowing arm), full, under the verify lock, at c0b1992

pnpm --filter @objectstack/dogfood test after the @objectstack/dogfood^... closure build. os-verify-lock: VERDICT command-exit 1. Test Files 3 failed, 353 passed, 3 skipped (359); Tests 4 failed, 2867 passed, 20 skipped (2891). None is this diff's (none reads an import door, the exposure gate or a __ name):

The dogfood shards do not run on the PR or in the merge queue for this diff (their if: excludes both events), so the reading above is the only one before the merge.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Not touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request. (Two cards carried together on the seat's order: one file, one read.)
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cxENxbA8ZpRgwafjvs

…does

The synchronous import door (POST /data/:object/import) now asks the
same up-front existence read as the async job door, right after the
API-exposure gate and before the body is parsed: an unregistered object
answers the data door's 404 OBJECT_NOT_FOUND and an object of a disabled
package its 404 OBJECT_PACKAGE_DISABLED, instead of a 200 with the
refusal repeated in every row.

The existence read and the exposure gate both take the item the by-name
read serves, under whichever name it carries, so a legacy ns__short
object addressed by its short name is the object behind that name at
every door, as registry.getObject resolves it for the data door.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxENxbA8ZpRgwafjvs
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract-review-claim: session_016WV4czuTWNgKR22fKv7jia · head c0b19922f981aaf11736034af1d0353c5c58880e · 2026-10-30T02:56Z

The lane seat runs the CONTRACT_REVIEW_TIER review this PR owes (Clause-②: no (narrowing)).

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

6 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 assertImportObjectExists — its route source's handler names it))
  • content/docs/api/wire-format.mdx (via /:object/import (route, bridged from symbol assertImportObjectExists — its route source's handler names it))
  • content/docs/data-modeling/fields.mdx (via /:object/import (route, bridged from symbol assertImportObjectExists — its route source's handler names it))
  • content/docs/data-modeling/import-mappings.mdx (via /:object/import (route, bridged from symbol assertImportObjectExists — its route source's handler names it), /:object/import/jobs (route, bridged from symbol assertImportObjectExists — its route source's handler names it))
  • content/docs/permissions/system-context.mdx (via /:object/import (route, bridged from symbol assertImportObjectExists — its route source's handler names it), /:object/import/jobs (route, bridged from symbol assertImportObjectExists — its route source's handler names it))
  • content/docs/protocol/objectql/state-machine.mdx (via /:object/import (route, bridged from symbol assertImportObjectExists — its route source's handler names it), /:object/import/jobs (route, bridged from symbol assertImportObjectExists — its route source's handler names it))

⛔ 4 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 assertImportObjectExists — its route source's handler names it))
  • content/docs/releases/v17/17-6.mdx (via /:object/import (route, bridged from symbol assertImportObjectExists — its route source's handler names it))
  • content/docs/releases/v18/18-1.mdx (via /:object/import (route, bridged from symbol assertImportObjectExists — its route source's handler names it))
  • content/docs/releases/v18/18-2.mdx (via /:object/import (route, bridged from symbol assertImportObjectExists — its route source's handler names it))

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
  • the SDK route bridge reached 58 of 210 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 — 19 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 e65536193a5cd493d1cbf6ad7ac78ae34909e37a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4371c1709770486de3f9a8f2eb11fe41979903c8 — the merge of head 274ccc20b44dcf2009f8edcc2827932b859af9a4 into base e65536193a5cd493d1cbf6ad7ac78ae34909e37a, 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 4371c1709770486de3f9a8f2eb11fe41979903c8 && git checkout 4371c1709770486de3f9a8f2eb11fe41979903c8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e65536193a5cd493d1cbf6ad7ac78ae34909e37a 274ccc20b44dcf2009f8edcc2827932b859af9a4 && git checkout -B drift-repro e65536193a5cd493d1cbf6ad7ac78ae34909e37a && git merge --no-ff 274ccc20b44dcf2009f8edcc2827932b859af9a4

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

⚠️ 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 e65536193a5cd493d1cbf6ad7ac78ae34909e37a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c0b19922f981aaf11736034af1d0353c5c58880e
Local-runs: none
Scope: packages/rest/src/data-action-routes.ts, packages/rest/src/meta-item-read-gate.ts, packages/rest/src/import-door-object-existence.test.ts, .changeset/27728-27729-import-doors-object-existence.md + packages/rest/src/rest-server.ts (objectItemReader, enforceApiAccess, enforceImportTemplateGates, apiAccessDenialFromEnable), packages/rest/src/crud-routes.ts and packages/rest/src/batch-routes.ts (the gate's callers; the cross-object /batch per-op gate), packages/metadata-protocol/src/protocol-meta-item-face.ts (readMetaItemFace), packages/metadata-protocol/src/protocol.ts (readMetaItem step 3, isObjectOfDisabledPackage), packages/metadata-protocol/src/protocol-query-validation.ts (assertObjectRegistered), packages/objectql/src/registry.ts (getObject, getItem, resolveObjectKey, isObjectPackageDisabled)

Read at 2026-10-30 against the net diff merge-base(origin/main, pr-27897)...c0b19922 (4 files, +276/−21), cards #27728 and #27729 (bodies, triage 6509235875 and 6509225810, the os-dev-report 6513826657), PR #27711 and its record 6508524284 (the model read), and the head's check-runs. Face: record-recognisers --paths declines on this file list and hands the decision to the changeset's Clause-② arm → (narrowing) → this tier. Adversarial read: the dev's report and the PR body were inputs to check, not findings to adopt. Rulings taken as triage gave them, not re-ruled.

① Derived judgments

  1. Sync door: the 404 sits after the exposure gate, before the body — no existence oracle. POST /data/:object/import (data-action-routes.ts:510-529): enforceAuth → enforceApiAccess('import') → assertImportObjectExists → opt-out gate → prepareImportRequest. Byte-for-byte the job door's order (652-662) and the data door's (enforceAuth → enforceApiAccess('create') → createData → assertObjectRegistered). A hidden object (apiEnabled:false) keeps the gate's 404 OBJECT_API_DISABLED before existence is asked; an unregistered name gets 404 OBJECT_NOT_FOUND from the same objectNotFoundError factory through the route's handleRouteError. Unregistered vs. not-exposed are two 404s with different codes, which is exactly the data door's split; neither reveals more than the data door does. Correct.
  2. The existence read equals the data door's for every name. Data door: assertObjectRegistered → registry.getObject(name) → resolveObjectKey (short-name index first, then FQN) → disabled-package check via isObjectPackageDisabled(name), which resolves the same way. Import doors: getMetaItemForExecution → readMetaItemFace throws objectPackageDisabledError(request.name) by the same isObjectPackageDisabled(name) predicate; then readMetaItem step 3 is registry.getItem('object', name) = getObject(name). With item.name === objectName dropped, the import doors' accept set is getObject's: the only item the by-name read serves under a different name is the registry's short-name alias (stored row and MetadataService are keyed by the requested name), so the accept set grows by exactly the alias set and nothing else. fix(rest)!: an async import job for an unregistered object is refused with 404 OBJECT_NOT_FOUND before any job is created #27711's residual (② item 2 there) is closed, as rest: pin the async import job door's disabled-package refusal, and decide whether it should resolve a legacy ns__short name as the data door does #27729 ruled.
  3. All three doors give one answer per name (the new suite pins it in one harness): unregistered → 404 OBJECT_NOT_FOUND, body toEqual; disabled package → 404 OBJECT_PACKAGE_DISABLED, body toEqual, zero sys_import_job and zero object rows; apiEnabled:false under short name → 404 OBJECT_API_DISABLED at data, sync and jobs; registered short name → served at all three. Two pre-existing, declared postures from fix(rest)!: an async import job for an unregistered object is refused with 404 OBJECT_NOT_FOUND before any job is created #27711 remain the only divergences: a thrown read other than OBJECT_PACKAGE_DISABLED fails open and logs (the per-row write then answers), and a protocol with no by-name read is not asked. Both are logged or non-shipped; no fix owed.
  4. loadObjectItem is shared, and the change narrows more doors than the two import doors. Its one non-test caller is enforceApiAccess (rest-server.ts:2697), which every /data/:object route asks: GET/POST /data/:object, POST /query, GET/PATCH/DELETE /:id, POST /:id/clone, GET /:id/field-access, POST /import, POST /import/jobs, GET /import/template (via enforceImportTemplateGates), POST /export, and the per-object POST /batch, /createMany, /updateMany, /deleteMany. Rule change on each: a legacy ns__short object addressed by its short name is now judged by its own enable (apiEnabled:false → 404 OBJECT_API_DISABLED; an apiMethods whitelist → 405 OBJECT_API_METHOD_NOT_ALLOWED), where before the short name was "no object" to the gate and the data plane served the object (getObject resolves it). That is the measured base case the dev reports (POST /data/vault 201). It takes nothing away from the spec: it makes the spec's own refusal (ADR-0049, enable.apiEnabled) real under the alias the data plane already honours. Triage 6509225810 ordered it ("the exposure gate reads the same way"). Declared: the changeset's "One read for existence and exposure" paragraph names the gate, apiEnabled:false and apiMethods, and "refuses the same way it refuses the full name"; the PR body names it with the measurement. Under-enumerated: the Clause-②: parenthetical, the BREAKING headline and "Who sees it" name only the sync import door. Population: no shipped object carries a __ name (fix(rest)!: an async import job for an unregistered object is refused with 404 OBJECT_NOT_FOUND before any job is created #27711 record ② item 2: computeFQN no longer prefixes; one legacy example in a doc comment). Should-fix on the text, below; not a must-fix.
  5. The list fallback keeps the name match, correctly. loadObjectItem without getObjectItem searches loadObjectItems by o.name === name; the list carries no alias resolution, so there is nothing to follow; the docblock says so. Only a non-shipped protocol double reaches it.
  6. Envelope parity holds by the same classifiers as fix(rest)!: an async import job for an unregistered object is refused with 404 OBJECT_NOT_FOUND before any job is created #27711: objectNotFoundError → handleRouteError → mapDataError's canonical arm; objectPackageDisabledError → declared-4xx pass-through, keys converged with the data door's. The new test holds both toEqual across doors.
  7. Test adequacy. 11 cases: real ObjectQL + ObjectStackProtocolImplementation + sqlite, real disablePackage, real sys_import_job; refusal pins carry status and code and the whole body against the data door's reference in the same harness; counts at the engine. Controls present for the sync door and the short-name premise (registry.getObject('widget') → exl__widget). Ablation on record: both sources reverted, 6/11 red, the 5 green named and each explained (controls, reference reading, the job-door pin already true on the base). Consistent with the diff.
  8. Nothing is widened for the gate's purpose that was closed before. Dropping the equality cannot pass an object the data plane would refuse: the data plane resolves the same alias, so a name the gate now judges is a name the data plane would have served.

② Semver level

@objectstack/rest patch with a BREAKING banner: correct under AGENTS.md Post-Task Checklist step 3 (a non-protocol narrowing ships patch, keeps the banner and the ADR-0087 disposition). Clause-②: no (narrowing) and Metadata-protocol: none are line-initial in the PR body and the changeset and match the diff. Metadata-protocol: none is right: no key, value or type an author can write is added, refused or re-meant; enable.apiEnabled/apiMethods keep their documented meaning and are now enforced under an alias the data plane already resolved — enforcement, not semantics. ADR-0087 not-required (no-migration-prescription): one marker, no FROM → TO prescription owed (no authorable thing removed or renamed); same disposition as #27711's changeset, which Check Changeset validated; Check Changeset is green on this head. Title carries !. Skills-docs: none — REASON present and well-formed, with the git grep evidence; true on its merits.

③ Boundary flags

  • Check-runs on this head, read at 2026-10-30: Governed Surface Queue Guard, Check Changeset, Check PR Size, PR-body guards, Lint · PM dispatch-gates, Type Check · source gates, Lint · docs & code guards, Test UI, Dogfood Regression Gate success; Test Core (8 shards), Build Core, Type Check · workspace/consumer/debt, Lint · ESLint, Lint · repo & tooling, Lint · scoped · query-options erasure, Live E2E UI still in progress; nothing failed or cancelled. The seat reads them green before any landing act. No governed surface in the file list; PR author is objectstack-fleet[bot], so no author note.
  • Narrowing dogfood battery at c0b1992: on record but command-exit 1. 3 failed files / 4 tests, each named as a main-side red with its card (main red (Dogfood Regression Gate 1/3) since 21c4bdd8 (#27667): two dogfood pins still expect a code-less hook refusal after HOOK_ABORTED landed #27806 HOOK_ABORTED, fix in test(dogfood): flip the three hook-abort pins to the ruled HOOK_ABORTED wire #27878; 4-hourly full run: red on main (CI) #27813 red 2 from 194e66c0). None reads an import door, the exposure gate or a __ name. Accepted under the merge-base-signature rule on the dev's reading; the seat verifies the signatures against the base before ACCEPT, since the dogfood shards do not run on the PR or in the queue for this diff.
  • Dev deviations, each accepted: (i) meta-item-read-gate.ts edited outside the claim's named file: triage 6509225810 ordered the gate to read the same way; (ii) no local test:repo run: CI's Test Core carries it (in progress at this read). open_questions: [] — none owed.
  • Under-enumerated narrowing (from ① item 4): should-fix on the changeset text, item for the seat's ACCEPT. The Clause-②: parenthetical and "Who sees it" should name the second class: a client addressing an enable-restricted legacy ns__short object by its short name on any /data/:object route now gets that object's 404/405; remedy: address it by its full name, or lift the enable restriction. Empty live population; no gate reads the prose; not a must-fix.
  • Out-of-scope finding, cross-object POST /batch per-op gate (batch-routes.ts:285-300): keys loadObjectItems by o.name, so an ns__short object's enable is not judged under its short name there and the op inside the transaction is served by the resolving data plane. Pre-existing; this PR does not worsen it, it leaves /batch as the one data door with the old read (plus loadObjectItem's list fallback, non-shipped). The dev noted it in the PR body and did not file; the seat owes the card (report-level, below).

④ Design judgment

packages/rest/src/data-action-routes.ts — holds against the code it cites and the scenario it is accepted against. The call sits where the job door's does and where the data door's existence refusal sits relative to the gate; dry run and an unparseable CSV body are refused before parsing (pinned). The helper's docblock now states the alias rule and cites registry.getObject; the fail-open and warn posture are unchanged from #27711's accepted read. The log line's wording changed (import job: → import:); no test observes it, so no OS_REST_LOG declaration is owed. No must-fix.

packages/rest/src/meta-item-read-gate.ts — holds against ADR-0049's rule (a non-enforcing apiEnabled is false security) and the data plane's own resolution: the gate now judges the object the data call will serve. The docblock records the before/after and why the list fallback keeps its match. The narrowing takes no declared capability away at the consumer; it makes the spec's refusal real under an alias. No must-fix. The declaration's under-enumeration is a text should-fix (③), carried by the seat's ACCEPT.

packages/rest/src/import-door-object-existence.test.ts — the pins are the review checklist's shape (status, code, whole body against the reference door, engine-counted rows); the short-name premise is asserted before it is used; as any casts are confined to the harness and the dev reports check:query-options-erasure green locally (CI in progress). No must-fix.

.changeset/27728-27729-import-doors-object-existence.md — frontmatter, both declaration lines, one ADR-0087 marker, BREAKING banner, Before / Now / One read / Who sees it / Remedy; every sentence checked against the diff is true. Should-fix (③): name the gate-side class under "Who sees it" and the remedy.

No must-fix. The narrowing takes away only answers that were wrong (a 200 that imported nothing; a short-name pass-through of a declared apiEnabled:false), and makes the data door's refusals real at every door that reads through the gate.

Implemented-by: claude/project-thread-trba3v
Reviewed-by: session_016WV4czuTWNgKR22fKv7jia (seat subagent)

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4cxENxbA8ZpRgwafjvs

Changeset should-fix pushed (text only): head 274ccc20. The CR PASS 6513915701 was on c0b19922. .changeset/27728-27729-import-doors-object-existence.md now names the exposure-gate class in the Clause-②: parenthetical, the BREAKING headline, "Who sees it" and the remedy. That class is a legacy ns__short object, addressed by its short name on any /data/:object route, whose enable.apiEnabled: false or enable.apiMethods now answers 404 OBJECT_API_DISABLED / 405 OBJECT_API_METHOD_NOT_ALLOWED there, as it already did under the full name. No code change. check-changeset-no-major, check-adr-0087-registration, check-empty-changeset, check:nul-bytes and check:issue-citations all exit 0 locally on the new head.

Dogfood battery reds match main's signatures (battery at c0b19922; the new commit touches only the changeset, so the battery's code is unchanged):

Red in this battery Main-side anchor Match
error-sentences-live.dogfood.test.ts:79, acceptance[1], POST /data/OBJECT and /createMany: "the hook declared no code, so the /data door carries none", body carries "code":"HOOK_ABORTED" #27806 (red 1 of #27813): same file, same line 79, same two doors, same HOOK_ABORTED body identical
object-hook-lifecycle-scratch.dogfood.test.ts:134, acceptance[6] negative[0]: deep-equal fails on an extra "code": "HOOK_ABORTED" #27806: same file, same line 134, same case identical
platform-app-object-entry-views.test.ts:371: "named objects neither source declares: expected [ 'sys_exchange_rate' ] to deeply equal []" #27813 red 2 (triage 6512235460; reproduced at base in the #27813 os-dev-report 6513471487): same file, same line 371, same message identical; stop-bleed PR #27873 merged at e65536193a, after this branch's base

None of the three reads an import door, the exposure gate or a __ name.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 274ccc20b44dcf2009f8edcc2827932b859af9a4
Local-runs: none
Scope: delta on the record at comment 6513915701 + .changeset/27728-27729-import-doors-object-existence.md

Delta read at 2026-10-30 of git diff c0b19922..274ccc20: one commit (docs(changeset): name the exposure-gate class the narrowing reaches), one file, 4+/4−, all in the changeset. No source, test or config line moves, so every code judgment in the PASS record 6513915701 (① items 1–8, ④ per file) stands unchanged on this head and is not re-ruled here.

① Derived judgments

  1. The should-fix of 6513915701 (③ "under-enumerated narrowing") is met in all four places named. Clause-②: parenthetical now adds "on every /data/:object route, a legacy ns__short object addressed by its short name is now held to its own enable settings (404 or 405)"; the BREAKING headline adds "the API-exposure gate of every /data/:object route now judges a legacy ns__short object addressed by its short name"; "Who sees it" adds the client-facing sentence with both codes; the remedy adds the ns__short sentence. Each is true against the unchanged diff: enforceApiAccess → apiAccessDenialFromEnable answers 404 OBJECT_API_DISABLED for apiEnabled: false and 405 OBJECT_API_METHOD_NOT_ALLOWED for an apiMethods list that leaves the operation out (rest-server.ts:548-571), and the gate now reads through loadObjectItem without the item.name === objectName match, so the short name is judged as the full name is.
  2. The remedy is stated more precisely than the record suggested. The record proposed "address it by its full name, or lift the enable restriction"; the changeset says "the short name now answers as the full name does: lift the object's enable restriction if the API should serve it." The full-name half would not help (the full name was already refused), so dropping it is correct, not a gap.
  3. Rule level held. Every new sentence states a rule (door, name class, setting, status + code); no command transcript, test output, fixture or log line is reproduced. "which it already got under the full name" is the one before-state clause and is the record's own ① item 4 finding.
  4. Nothing in the new prose over-claims. The population statement ("No shipped object carries a __ name") is unchanged; the headline keeps the scope to @objectstack/rest; the cross-object POST /batch per-op gate (record ③, out-of-scope, seat-owed card) is correctly still not claimed as narrowed.

② Semver level

Unchanged and still correct: frontmatter '@objectstack/rest': patch; Clause-②: no (narrowing) and Metadata-protocol: none each line-initial, exactly once; **BREAKING** line-initial once; exactly one adr-0087 marker (not-required (no-migration-prescription)), its justification sentence untouched and still true (no key added, removed, renamed or re-shaped; the gate judges what the data plane already resolves). Title keeps !. The wider Clause-② parenthetical adds a second narrowing class, not a widening, so no (narrowing) remains the right arm. Dev reports check-changeset-no-major, check-adr-0087-registration, check-empty-changeset, check:nul-bytes, check:issue-citations exit 0 on this head (comment 6513950052); the seat reads Check Changeset on 274ccc20 before any landing act.

③ Boundary flags

④ Design judgment

.changeset/27728-27729-import-doors-object-existence.md — the one should-fix is closed in exactly the four places the record named, with the remedy sharpened rather than copied; declarations intact and line-initial; rule-level prose throughout. No must-fix, no residual should-fix.

Implemented-by: claude/project-thread-trba3v
Reviewed-by: session_016WV4czuTWNgKR22fKv7jia (seat subagent)

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Seat ACCEPT · PR #27897 · head 274ccc20b44dcf2009f8edcc2827932b859af9a4 · 2026-10-30T03:27Z

维护者速读

Both import doors and the data door now give one answer about an object's existence:

  • Synchronous import (POST /data/:object/import) refuses an unregistered object with the data door's 404 OBJECT_NOT_FOUND before any row. The refusal comes after the exposure gate, so it reveals nothing about objects the caller cannot see.
  • Async import job door reads existence exactly as the data door does. Its disabled-package case is pinned.
  • The /data/:object exposure gate now reads an object the same way. A legacy ns__short object whose enable settings restrict it, addressed by its short name, is now refused (404 / 405) on every /data/:object route. Before, it passed. The changeset names this, with the remedy.

席位核对

ACCEPT

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

2 participants