Skip to content

fix(rest)!: an async import job for an unregistered object is refused with 404 OBJECT_NOT_FOUND before any job is created - #27711

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-27501-import-job-unknown-object
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-27501-import-job-unknown-object

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Requested by Sales · project thread
Fixes #27501

Scope: ask (a) only, per triage 6498786930. Ask (b), "a job whose every row failed does not read succeeded", was split to the decision card #27676 and is not built here: the job's terminal status is unchanged.

Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none (no published skill names the async import job door's object handling)

维护者速读

  • 改了什么:POST /data/OBJ/import/jobs 在对象未注册时直接返回 404 OBJECT_NOT_FOUND,与 POST /data/OBJ 的应答信封完全一致;不再建 sys_import_job,也不启动后台任务。
  • 为什么改:之前拼错对象名会得到 201、一个任务 id,随后任务显示 succeeded,只有逐行错误里写着找不到对象;自动化导入方会把这当成成功。
  • 风险与回滚:只收窄一个入口;已注册对象行为不变。对象元数据读取本身出错时不拦截(与 API 暴露闸门同样放行),逐行写入仍按数据入口应答。回滚即还原 data-action-routes.ts 的一处调用。
  • 先看:packages/rest/src/data-action-routes.ts 的 assertImportObjectExists 及其调用点;packages/rest/src/import-job-integration.test.ts 末尾新增的 describe。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/import-job-integration.test.ts,预期 15/15 通过。
  • 需要你做的:无,席位落地。

What changes

  • POST /data/:object/import/jobs now reads the object by name, the same read the API-exposure gate makes (the protocol's execution face where it has one, else getMetaItem), right after that gate and before the body is parsed. If no object is behind the name, it throws objectNotFoundError(objectName) from @objectstack/core. That is the one factory the protocol's existence gate and the engine's verbs throw, and the route's handleRouteError classifies it. The answer is 404 OBJECT_NOT_FOUND with the same body POST /data/:object gives the same name. No new code is minted.
  • No sys_import_job row is written and no worker is scheduled for a refused request.
  • A read refused OBJECT_PACKAGE_DISABLED propagates (the protocol's by-name read documents that refusal; this was not measured here). Any other thrown read fails open and logs at warn, as the exposure gate's loadObjectItem does: such a read says nothing about existence, and each row's data call still answers for the object. Two existing real-protocol harnesses whose metadata store is not provisioned (import-row-schema-drift-20701, import-row-dropped-fields-20701) need this; with the fault propagated they answered 503.
  • Changeset: @objectstack/rest patch, with a BREAKING note and the ADR-0087 not-required (no-migration-prescription) marker.

Tests

Two cases in packages/rest/src/import-job-integration.test.ts. They use the real RestServer route, the real ObjectQL engine with ObjectStackProtocolImplementation, and SQLite :memory:.

  • An unregistered object → 404 and OBJECT_NOT_FOUND. The body is toEqual the data door's (POST /api/v1/data/:object) body for the same name. sys_import_job holds 0 rows, counted at the engine before and after.
  • Control: a registered object → 201, {object:'task', status:'pending', total:1}, and exactly one sys_import_job row once the job finishes.

Acceptance notes

  • Out of scope: the synchronous door POST /data/:object/import has the same gap. It runs no existence check, answers 200, and reports OBJECT_NOT_FOUND per row (read from the handler; the card's reproduction step 3 measured it). Triage did not name it, so it is not changed here. It is reported to the seat.
  • The pins drive the route handler registered on a real RestServer, not an HTTP transport. This is the harness the file's existing cases use.

Verification

Final head bb1e1d2f (merged with origin/main).

Red before the fix (commit fcde8d5, test only)
× answers 404 OBJECT_NOT_FOUND, the data door's answer, and persists no job row
AssertionError: expected 201 to be 404 // Object.is equality
Tests  1 failed | 14 passed (15)
Ablation on the final head (scripts/ablation-replace.mjs)
ablation-replace: ok mutation landed: anchor 1 -> 0, blob e436474a91b7 -> 01857e27771d
× answers 404 OBJECT_NOT_FOUND, the data door's answer, and persists no job row
AssertionError: expected 201 to be 404 // Object.is equality
Tests  1 failed | 14 passed (15)
ablation-replace: ok restored: blob == HEAD (e436474a91b7) and `git diff HEAD` is empty

The ablation removed the await assertImportObjectExists(...) call. The suite imports ./rest-server from source, so no dist rebuild was involved.

Package suites and gates

Read at head bb1e1d2f, except where a line names another sha.

  • pnpm --filter @objectstack/rest test: exit 0, 340/340 files, 6668 passed, 326 skipped. Run at 06dd5e1; the commit after it changes only the new test's row-count helper, and that file re-ran 15/15 at bb1e1d2.
  • pnpm --filter @objectstack/rest typecheck: exit 0 (06dd5e1, after a declaration build of the @objectstack/rest... closure).
  • eslint packages/rest --no-inline-config: exit 0, 0 errors. The 2 warnings are in an untouched file.
  • dispatch-gates --commands derived 59 families. 57 exited 0. check:query-options-erasure was red at first (the new test's as any options cast grew the test surface 236 → 237); it was fixed in bb1e1d2 and re-ran green at 236. check:dual-build-cjs-loads exited 3, PREREQUISITE NOT MET: it reads every package's dist and needs a full pnpm build, so it is NOT MEASURED locally and the CI Lint & Repo Gates job carries it. --ran: 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN.
  • The test-reading gates re-ran green at bb1e1d2: cross-package-test-inputs, test-source-alias, engine-double-contract, nul-bytes, rest-log-spy-declared and doc-authoring.
  • Dogfood battery ((narrowing) arm) at bb1e1d2: os-verify-lock VERDICT command-exit 0 (held 36m11s), closure build exit 0, 352 files passed and 3 skipped, 2844 tests passed and 20 skipped, no failures (os-dev-report 6508232970 on import: POST /data/OBJ/import/jobs accepts an unregistered object with 201 and the job then reads succeeded, where the data door answers 404 #27501).
  • No open PR touches data-action-routes.ts or import-job-integration.test.ts (REST read of all 29 open PRs' files).

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F

… 404 before persisting it

Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F
Co-authored-by: Claude <noreply@anthropic.com>
…s the exposure gate's does

Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F
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 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

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

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

  • content/docs/releases/v18/18-1.mdx (via OBJECT_PACKAGE_DISABLED (literal, a string literal in assertImportObjectExists))

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 d93d9be598e6b5ba407836e9d53d3a7d37ee6194 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 d93d9be598e6b5ba407836e9d53d3a7d37ee6194 → 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#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract-review-claim: session_01L6bhgUCizoYWQVJWjhdR6F · head bb1e1d2 · 2026-10-29T21:03Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bb1e1d2f5a4408afc35b5b4113891e6125b8186a
Local-runs: none
Scope: packages/rest/src/data-action-routes.ts, packages/rest/src/import-job-integration.test.ts, .changeset/27501-import-job-unknown-object.md + packages/rest/src/rest-server.ts (objectItemReader, enforceApiAccess), packages/rest/src/meta-item-read-gate.ts (loadObjectItem), packages/runtime/src/api-exposure.ts (the fail-open decision), packages/core/src/utils/object-not-found.ts, packages/rest/src/error-response.ts (handleRouteError, resolveErrorResponse), packages/types/src/data-error-classification.ts (mapDataError), packages/rest/src/crud-routes.ts (POST /data/:object), packages/metadata-protocol/src/protocol.ts (getMetaItem, getMetaItemForExecution, readMetaItem, objectPackageDisabledError, rethrowUnlessMetadataStoreUnprovisioned), packages/metadata-protocol/src/protocol-meta-item-face.ts, packages/metadata-protocol/src/protocol-query-validation.ts (assertObjectRegistered), packages/objectql/src/registry.ts (getItem, getObject, resolveObjectKey)

Read at 2026-10-29T21:15Z against the net diff origin/main...pr27711 (3 files, +114/−1), card #27501 (body, triage 6495291748 and 6498786930, the claim 6504711785, the os-dev-report 6508232970) and the head's check-runs. Ask (b) is on #27676 and is not judged here. Adversarial read: the seat's own dispatch and its report conclusions were inputs to check, not findings to adopt.

① Derived judgments

  1. The job door's existence oracle is the data door's. The data door refuses in createData → assertObjectRegistered (protocol-query-validation.ts:293-338): registry.getObject(name) truthy, else objectNotFoundError; registered but in a disabled package → objectPackageDisabledError. The new assertImportObjectExists (data-action-routes.ts:330-347) reads getMetaItemForExecution → readMetaItemFace (protocol-meta-item-face.ts:51-70), which throws the same objectPackageDisabledError by the same predicate before any read, then readMetaItem: stored row (step 1) → MetadataService (step 2) → registry.getItem('object', name) (step 3, protocol.ts:11115-11120), and getItem for object IS getObject(name) (registry.ts:5896-5900). So every object assertObjectRegistered accepts is found here, including objects registered after boot (steps 2 and 3 read live state, not a boot snapshot) and system objects (registry). No false 404 on a registered object was found.
  2. One residual difference, declared in the helper and pre-existing at the exposure gate: the equality item.name === objectName (data-action-routes.ts:345, the same test as meta-item-read-gate.ts:509). registry.getObject resolves a legacy ns__short key from its short name (resolveObjectKey, registry.ts:4981-5000), so the data door serves such an object under short while this door answers 404 for it. computeFQN has not prefixed names since the FQN retirement (registry.ts:468) and no shipped object source carries a __ name (one legacy example in a doc comment of system-names.ts); the exposure gate already does not see such a name. Not a live caller; see ③/④.
  3. Ordering equals the data door's. enforceAuth → enforceApiAccess('import') → existence (data-action-routes.ts:625-639) mirrors enforceAuth → enforceApiAccess('create') → createData (crud-routes.ts:258-298). No existence oracle runs before the exposure gate; a hidden object keeps the gate's 404; an unauthenticated caller keeps 401. The env-scoped request is built exactly as the gate's reader builds it (rest-server.ts:2617-2628).
  4. Same envelope. handleRouteError excludes OBJECT_NOT_FOUND from its status pass-through (error-response.ts:365-366) so the answer is mapDataError's canonical arm (data-error-classification.ts:1310-1320), the arm the data door reaches directly; the new pin holds the two bodies toEqual in one harness.
  5. Fail-open on a thrown read is the cited posture, read at its lines. loadObjectItem (meta-item-read-gate.ts:502-518): thrown read → logWarn → undefined → the gate passes; decision recorded at api-exposure.ts:31-58 (元数据不可解析时的 fail-open 残余风险评估(api-exposure / rest-server) #3545). Here a thrown read that is not OBJECT_PACKAGE_DISABLED → logWarn → return (data-action-routes.ts:338-343). What can throw in production: a stored-row read fault other than an unprovisioned table (rethrowUnlessMetadataStoreUnprovisioned, protocol.ts:8858-8872) or a degraded MetadataService with no item (protocol.ts:11158-11159). In that window a job for an unknown name is created as before, with every row refused by assertObjectRegistered in the worker; the warn names it. Acceptable: a narrow, logged window; failing closed would 503 legitimate imports during a metadata-store blip when the registry could still answer. warn is the right level (a read, not a durability seam; AGENTS.md § Degradation log levels). The new test observes no log, so it owes no OS_REST_LOG declaration (check-rest-log-spy-declared.mjs rules 2 and 6); Lint & Repo Gates carries that gate green.
  6. OBJECT_PACKAGE_DISABLED reaches the client with the data door's status and code, by reading: the same factory (protocol.ts:646-656: code, status 404, object), rethrown at data-action-routes.ts:339, classified by the declared-4xx pass-through (error-response.ts) whose key set {error, code, object} was converged with mapDataError's generic arm (door-parity suite §1/§3). It is UNMEASURED on this door, and it is a second narrowing: before this PR the gate read that refusal as "no object" and the job was created. The changeset's Before/Now does not name it. Should-fix, not must-fix: the behaviour is the data door's own and better than a succeeded empty job.
  7. A protocol with no by-name read is not asked (returns, old behaviour), where the exposure gate falls back to the list read. The real protocol has both faces; only a double without either keeps the gap. Noted, no fix owed.
  8. Ablation and red-first are on record in the PR body (test-only commit fcde8d5 red; ablation-replace on the final head red with blob readings and verified restore). Taken as the dev's evidence, consistent with the diff.

② Semver level

@objectstack/rest patch with a BREAKING banner: correct under AGENTS.md Post-Task Checklist step 3 (a non-protocol narrowing ships patch and keeps the banner and disposition). Clause-②: no (narrowing) and Metadata-protocol: none are line-initial in the PR body and the changeset and match the claim; nothing an author writes changes. ADR-0087 not-required (no-migration-prescription): one marker, the body carries no FROM→TO prescription (the remedy sentence names the registered name, not a rewrite); Check Changeset and Lint & Repo Gates re-validate it green. Title carries !. Skills-docs: line present (none, with a reason); spelled none (REASON) where SKILL.md:313 spells none — REASON; no gate reads it, true on its merits (no published skill teaches this door's object handling). Face: record-recognisers --paths declines on this file list and hands the decision to the changeset's Clause-② arm → narrowing → CONTRACT_REVIEW_TIER, the tier this record serves.

③ Boundary flags

  • Gates on this head (check-runs, read twice this act): Lint & Repo Gates, TypeScript Type Check, Test Core and its 8 shards, Build Core, Governed Surface Queue Guard, Test UI, Check Changeset (both runs, the second started by the body edit that added the battery reading) success; Lint UI, Type Check UI, Build & E2E UI, Live E2E UI skipped (no ui/ path). No failure, no cancellation, nothing in progress at the second read. No governed surface in the file list; size/m.
  • Narrowing dogfood battery at the final head: on record (os-dev-report 6508232970 dogfood_battery: bb1e1d2, lock VERDICT command-exit 0, Test Files 352 passed / 3 skipped, Tests 2844 passed / 20 skipped, no failures). PR body cites it.
  • Dev deviations, each accepted: (i) pins drive the handler, not an HTTP transport: the file's existing harness, and both doors' classifiers write at the handler, so the equality pin loses nothing there; (ii) report signature line first, marker second: post-stamped requires it; (iii) "battery queued" in the body: superseded, the body now carries the reading; (iv) changeset outside the claim's file surface: required by the repo, correct.
  • open_questions: [] — none owed.
  • Out-of-scope finding, sync door POST /data/:object/import: same gap, reported with class, reach, evidence and dedupe words; carrier none. Leaving it is acceptable for this PR: triage 6498786930 scoped (a) to the async door and the claim's file surface is that handler; Prime Directive chore: version packages #10 says note, never expand. The seat owes the card; it is filed by this seat, not by this record.
  • Two narrowings the changeset does not name (from ① items 2 and 6): the disabled-package refusal up front, and the legacy short-name alias. Both are should-fix on the text, below.
  • The job door's helper says "counts only under its own name" and the exposure gate's reader already does; so the alias residual widens no existing hole, it moves one door to the gate's view. Accepted at this tier with the should-fix.

④ Design judgment

packages/rest/src/data-action-routes.ts — holds against the code it cites. The reader is byte-for-byte the gate's objectItemReader selection (rest-server.ts:2617-2619); the thrown factory is the one the protocol's gate and the engine's verbs throw (object-not-found.ts); the call sits after the exposure gate and before body parse and any sys_import_job write, so a refused request writes no row and schedules no worker (verified in the handler at 625-710). Fail-open posture and log level match the precedent, cited above.

  • should-fix: pin the disabled-package case on this door (404 OBJECT_PACKAGE_DISABLED, body toEqual the data door's) or drop the claim "propagates" from the PR body; today it is read, not measured.
  • should-fix (lesser): either make the oracle exactly assertObjectRegistered's (item returned non-null, no name equality) so a legacy ns__short object addressed by short is served as the data door serves it, or name that residual in the changeset. No shipped object is affected; the choice is the seat's.

packages/rest/src/import-job-integration.test.ts — the negative pin asserts status, nested-or-flat code, the whole body against the data door's in the same harness, and the sys_import_job count at the engine before and after; the control asserts 201 and exactly one row after terminal. Meets the review checklist (refusal pins carry code and status). The harness never provisions sys_metadata (family registered after DDL), so step 1 answers via isMissingTableError and the pins exercise the registry step, the step a production object without a stored row resolves through; adequate. No must-fix.

  • should-fix: add the disabled-package case named above.

.changeset/27501-import-job-unknown-object.md — frontmatter, both declaration lines, one ADR-0087 marker, BREAKING banner, Before / Now / Unchanged / Who sees it / Remedy: the shape the gates and the CHANGELOG reader need. Sentences checked against the diff: all true.

  • should-fix: one line under "Who sees it" naming the two refusals this door now also makes up front: an object of a disabled package (OBJECT_PACKAGE_DISABLED, remedy: re-enable the package) and, if the equality is kept, a legacy ns__short object addressed by its short name.

No must-fix. The narrowing takes away only an accept that imported nothing, and makes the data door's refusal real at this door.

Implemented-by: claude/issue-27501-import-job-unknown-object
Reviewed-by: session_01L6bhgUCizoYWQVJWjhdR6F

VERDICT: PASS

…f the import job door

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

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 004cbb14df2d48a6c6d4adec0d180b7d6f849bf0
Local-runs: none
Scope: .changeset/27501-import-job-unknown-object.md, packages/rest/src/data-action-routes.ts, packages/rest/src/import-job-integration.test.ts + packages/metadata-protocol/src/protocol-meta-item-face.ts (readMetaItemFace), packages/metadata-protocol/src/protocol.ts (objectPackageDisabledError, readMetaItem step 3), packages/metadata-protocol/src/protocol-query-validation.ts (assertObjectRegistered), packages/objectql/src/registry.ts (getItem, getObject, resolveObjectKey), packages/rest/src/meta-item-read-gate.ts (loadObjectItem), packages/rest/src/error-response.ts (resolveErrorResponse), packages/types/src/data-error-classification.ts (mapDataError), packages/core/src/utils/object-not-found.ts

Delta record on the head that followed the reviewed head bb1e1d2f (record 6508524284, PASS). Read at 2026-10-29T21:38Z: git diff bb1e1d2f 004cbb14, card #27729, the PR body and the new head's check-runs. The full ①–④ of 6508524284 stands for everything this delta does not re-judge.

① Derived judgments

  1. The delta is the one file. git diff bb1e1d2f 004cbb14 touches .changeset/27501-import-job-unknown-object.md alone, +1 line (the bullet "Also refused up front."), one commit 004cbb14 chore(changeset): …. Nothing else moved.
  2. The code is byte-identical to the reviewed head. git diff bb1e1d2f 004cbb14 -- packages/ is empty; blob oids at both heads: data-action-routes.ts e436474a91b707cc161080c46fddd4e4af1f1186, import-job-integration.test.ts bec74b227eb9aa9f1ce30d1e97dd5f79349233d6. The judgments of 6508524284 on those files carry unchanged.
  3. The new bullet, sentence by sentence, against the code it describes:
    • "An object of a disabled package now gets the data door's 404 OBJECT_PACKAGE_DISABLED instead of a job (re-enable the package)." Accurate. readMetaItemFace throws objectPackageDisabledError(name) before any read (protocol-meta-item-face.ts:59-61); the factory sets code: 'OBJECT_PACKAGE_DISABLED', status: 404, object (protocol.ts:646-656); the door rethrows exactly that code (data-action-routes.ts:339) and handleRouteError's declared-4xx arm keeps the status and the code. The data door's assertObjectRegistered throws the same factory (protocol-query-validation.ts:332), so "the data door's" holds for status and code; the remedy phrase is the factory's own sentence. "Instead of a job" is the before-state: the exposure gate read that refusal as "no object" and the job was created. Body-level toEqual remains unmeasured on this door, which is 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 item 1, not this changeset's claim.
    • "A legacy ns__short object addressed by its short name alone gets 404 OBJECT_NOT_FOUND, because the door counts an object only under its own name, as the API-exposure gate does (send the full registered name)." Accurate. registry.getObject('short') resolves the ns__short key through the short-name index (registry.ts:4981-4996) and readMetaItem step 3 is that call (getItem('object', …) → getObject, registry.ts:5896-5900), so the item comes back named ns__short; item.name === objectName fails (data-action-routes.ts:345) and the door throws objectNotFoundError('short') — code: 'OBJECT_NOT_FOUND', status: 404 (object-not-found.ts), answered through mapDataError's canonical arm. The exposure gate applies the same equality (meta-item-read-gate.ts:509). The remedy is the one that works: the full key resolves by objectContributors and the item's name is that key.
  4. SF1 and SF3 are carried by 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, read on its body: item 1 is the disabled-package pin (status, body toEqual the data door's, zero job rows); item 2 is the name-equality decision (drop it, or record the residual; triage decides, and whether the gate follows). It cites record 6508524284 by id, names fix(rest)!: an async import job for an unregistered object is refused with 404 OBJECT_NOT_FOUND before any job is created #27711, and says the synchronous door's card 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 should take the same read (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 not read here). Neither is on this PR, as 6508524284 allowed.
  5. SF2 is closed by this head. The changeset now names both refusals the door makes up front beyond the typo case. The PR body's own sentence ("this was not measured here") still stands and is true; the changeset's new bullet claims status and code, which are read, not a measured body.

② Semver level

Unchanged and still correct: @objectstack/rest patch, BREAKING banner, Clause-②: no (narrowing), Metadata-protocol: none, one ADR-0087 not-required (no-migration-prescription) marker. The new bullet adds no FROM→TO prescription (two remedy phrases, like the existing "The remedy" line), so the category still holds; Check Changeset and Lint & Repo Gates re-validated it green on this head. No declaration line moved.

③ Boundary flags

④ Design judgment

.changeset/27501-import-job-unknown-object.md — the added bullet holds against the cited code for both refusals (status and code as read above), names the before-state truthfully, and gives each its remedy. Placed under "Who sees it" and before "The remedy", so the CHANGELOG reader meets the two extra refusals beside the typo case. No must-fix, no should-fix.

packages/rest/src/data-action-routes.ts, packages/rest/src/import-job-integration.test.ts — unchanged by oid; 6508524284's ④ stands, its should-fixes 1 and 3 now carried by #27729 and not owed on this PR.

Implemented-by: claude/issue-27501-import-job-unknown-object
Reviewed-by: session_01L6bhgUCizoYWQVJWjhdR6F

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

维护者速读(席位终稿)

  • 改了什么:POST /data/OBJ/import/jobs 遇到未注册的对象名时直接回 404 OBJECT_NOT_FOUND,应答与 POST /data/OBJ 完全一致;不再建 sys_import_job,也不启动后台任务。已停用包的对象同样在入口被拒(OBJECT_PACKAGE_DISABLED)。
  • 为什么改:拼错对象名原先会得到 201 和任务 id,任务随后显示 succeeded,只有逐行错误里写着找不到对象;自动化导入方会当成成功。
  • 风险与回滚:只收窄一个入口,已注册对象行为不变。对象元数据读取本身出错时放行并记 warn,与 API 暴露闸门同样处理;逐行写入仍按数据入口应答。回滚 = revert 本 PR。
  • 先看:packages/rest/src/data-action-routes.ts 的 assertImportObjectExists 及其调用点;import-job-integration.test.ts 末尾新增的 describe。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/import-job-integration.test.ts,预期 15/15 通过。
  • 需要你做的:无。非受管面,契约审查 PASS,席位落地。

席位意见: ACCEPT。按分诊只做 (a),与数据入口同一工厂、同一应答;(b) 任务终态在决策卡 #27676 等裁决。同步导入入口的同类缺口和两条复核建议另开卡。

席位核对

ACCEPT · head 004cbb14df2d48a6c6d4adec0d180b7d6f849bf0 · 2026-10-29T21:39Z

  • 形态: 3 files, +115/−1: the route helper and its call site, one test describe, one changeset. No packages/spec, no new code, no governed path; Tier S.
  • Clause-②: no (narrowing), Metadata-protocol: none, patch + BREAKING, ADR-0087 not-required (no-migration-prescription). Contract review at CONTRACT_REVIEW_TIER (claim on this PR): record 6508524284 on bb1e1d2f PASS, no must-fix, 3 should-fix; delta record 6508933124 on 004cbb14 PASS, no findings (code blobs identical to the reviewed head).
  • 前提实测: the new pin red at the test-only commit fcde8d59 (201, not 404); ablation of the assertImportObjectExists call on the final code red, restored to the HEAD blob.
  • 抽查读数: the seat read the helper: same by-name read as the exposure gate, objectNotFoundError from @objectstack/core, OBJECT_PACKAGE_DISABLED rethrown, any other read fault fails open with warn; it runs after the exposure gate, so it is no existence oracle.
  • 门禁 check-runs: 48 on this head: 35 success, 13 skipped, 0 other (read 2026-10-29T21:39Z); mergeable_state: clean. Derived gates 59/59 (check:dual-build-cjs-loads carried by CI Lint & Repo Gates, green); rest 340/340 files; dogfood battery at bb1e1d2f 352 passed / 3 skipped, 2844 tests, carried to 004cbb14 on identical packages/ blobs.
  • 接受的偏差: the dev's four (harness through the registered route handler, report signature line, battery recorded in the report, changeset outside the claim's file surface) and the changeset-only respin for should-fix 2.
  • 范围外去向: the synchronous door POST /data/:object/import with the same gap → 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; should-fix 1 (pin the disabled-package refusal) and 3 (name equality vs. the data door's legacy ns__short resolution) → 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; ask (b) → decision card rest: an async import job whose every row failed ends failed, not succeeded (ruled B; split from #27501 (b)) #27676.
  • 散文面: PR body and changeset checked against the diff by both review records.
  • 落地条款: relay pr_ready + automerge_enable through the merge queue.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 21:41
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 29, 2026 21:41
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 81d00a4 Oct 29, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-27501-import-job-unknown-object branch October 29, 2026 22:01
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.

import: POST /data/OBJ/import/jobs accepts an unregistered object with 201 and the job then reads succeeded, where the data door answers 404

1 participant