Repository navigation
finding(service-datasource): a stored datasource row overrides a code-defined datasource at boot, so after a restart the admin door serves and edits it at runtime (restoreRuntimeDatasources has no code-collision check) #21922
Description
Filing gate: ① a reproducible defect, class (b), a published contract broken. Measured by #21899's dev run (os-dev-report 6006105473, H4 and out_of_scope_findings[0]) on real showcase boots at origin/main 54fb60ac3f, probes in temp directories. Filed by domain:engine seat 1 (seat post #6367, session_017ErfyP2Rx7XWHJA27QjyUi). It lands in service-datasource, so it is not a sub-issue of the engine card. ⛔ Not graded or routed here; ⛔ not a claim.
What is measured
The setup: one sys_metadata row for the code-defined showcase_external exists. Today the meta door's PUT writes one with a 200; that is #21899. Then the stack restarts.
- (a) A row saved from the served body (
origin: codeechoed). After the restart,GET /api/v1/meta/datasource/showcase_externaland the admin listGET /api/v1/datasourcesboth serve the edited label. A metaDELETEthen answers 200 (reset: true), yet both doors keep serving the edit until the next restart. - (b) A row saved with
origin: runtimeand a newconfig.filename. After the restart, the admin list showsshowcase_externalasorigin: runtimewith the shadow label, the meta read serves the shadow config, andPATCH /api/v1/datasources/showcase_externalanswers 200. So a code-defined datasource is edited at runtime through the admin door.- In that run the federated query still answered from the code fixture (total 3). Data routing was not re-pointed, but this was not measured further.
Mechanism (read on origin/main)
DatasourceAdminServicePlugin.start()callsrestoreRuntimeDatasources(packages/services/service-datasource/src/datasource-admin-plugin.ts, about:548). It runsmetadata.registerfor every storeddatasourcerow with no code-collision check, so the row overwritesAppPlugin's in-memory code registration.- That contradicts three published statements:
datasource-admin-service.ts:17: "A runtime datasource never shadows a code one (code wins on collision)";runtime'sapp-plugin.ts: code datasources are "registered IN MEMORY ONLY";DatasourceSchema.origininpackages/spec/src/data/datasource.zod.ts: "code — … read-only in the UI".
Relation
- [finding] The metadata door saves an edit to a code-defined datasource (
PUT /api/v1/meta/datasource/:nameanswers 200) and the metadata read then serves it, while the datasource admin door refuses the same edit as read-only #21899 (inpm:retriage) stops the meta door writing new rows like this. - This card is the other half: rows that already exist, and any other writer of a
datasourcerow, must not shadow a code definition at boot.
Reader who acts: triage grades and routes it. service-datasource reads as domain:services.
Dedupe: MCP search_issues, repo-scoped, open and closed:
- 「restoreRuntimeDatasources stored datasource row shadows code-defined datasource at boot restart code wins on collision」 gives [rebuild of #19389] [finding]
datasourceis a 14thallowRuntimeCreate: truetype with zero liveruntimeTypesdeclarations — invisible to #19275's census because its registry entry is the only MULTI-LINE one #19568, 显式绑定的 datasource 连不上只降级成一条 warning:绑上去的对象在启动后全废,直到查询时才炸 #3758, service-datasource still reads the retired tursoconfig.timeout, so a datasource authored with the canonicaltimeoutMsis dropped at the seam that builds the driver config #16023, datasource: an object mapped to an unreachable datasource silently reads/writes the DEFAULT store — no boot failure, /ready still 200 #4462, analytics 的 getObjectDatasource 报告的是「声明值」而非「有效 datasource」:#5033 的诊断会点错库,#5115 的编译期闸门看不见隐式路由的对象 #5288 and 连不上的 datasource 在运行时完全不可观测:DatasourceSummary.status是硬编码常量,health 探针也看不见它 #3827. None is this.
Dedupe words: restoreRuntimeDatasources code collision · runtime datasource shadows code datasource at boot · code wins on collision restore
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 6, 2026
os-dev-report
{ "issue": 21922, "members": [ 21922, 21944 ], "round": "patch round 1 (review 6011282321)", "status": "done", "branch": "claude/issue-21922-host-code-datasource-set", "pr": "https://github.com/objectstack-ai/objectstack/pull/21965", "session": "session_01WMQprn46CND82KmY8sZWBu", "premise_still_valid": true, "head": "d77e150701", "commits_this_round": [ "49421a8fe6 merge of origin/main 80f9f7e6ba (#21952, #21954), a merge commit, no rebase", "793ac4d3ad fix(runtime): AppPlugin contributes its datasource names as a function the set resolves at first read (item 4) + the `const listOf = (` nit (item 2)", "d77e150701 fix(metadata-protocol): the default datasource refusal names the host database configuration (item 1)" ], "summary": "All five items are done at head d77e150701, and CI on that head is green on every required context. Item 4, the plugin-dev red, was measured before any change: with the real manifest register() the refusal is unchanged (thrown once, from AppPlugin.init's manifest.register line, as on main). With register() stubbed to a no-op, the #15292 falsifier, my round-0 init() contribution threw INVALID_ARTIFACT_PACKAGE_ENTRY from the collections getter (resolveCodeDefinedDatasourceOwners → collections → resolveArtifactCollections → resolveArtifactPackageOrder). A whole DevPlugin boot printed the refusal once. So the refusal surfaced at a point the pin says must not carry it; it was neither doubled nor earlier in real boots. Fix: AppPlugin.init() now contributes a FUNCTION that the set resolves at its first read, so init() never reads collections or packages[]. The set (now a CodeDatasourceNames: a Set plus pending contributions) is still contributed to entirely in Phase 1 and read by has(name) in start(); a throwing contribution stays pending and rethrows to every reader. The pin is untouched and green. Item 1: the origin-gated datasource row gains `hostOwned: { default: ... }`, so the default refusal now reads '... It is defined by the host's database configuration (the database URL the server starts with): change that configuration and restart the server. See docs/adr/0062-external-datasource-runtime.md.' It names no *.datasource.ts. Every package-declared datasource's sentence is byte-identical; code, status and the refused set are unchanged. Item 2: the nit was my round-0 edit (the anchor's trailing space was lost) and is fixed. Item 3: merged. Item 5: none of the 18 hand-written pages states anything this PR makes false (page by page below); no docs were edited.", "item4_measurement": "Throwaway probe (packages/plugins/plugin-dev/src/zz-probe-21922.test.ts, deleted, never committed) at 49421a8fe6, with the round-0 code. (a) Real register(): code INVALID_ARTIFACT_PACKAGE_ENTRY, register called once, frames resolveArtifactPackageOrder ← register ← AppPlugin.init (app-plugin.ts:435, the manifest.register line). (b) No-op register(): the same code, frames resolveArtifactPackageOrder ← resolveArtifactCollections ← AppPlugin.get collections ← AppPlugin.resolveCodeDefinedDatasourceOwners. (c) DevPlugin boot over the malformed stack: 1 error line carrying 'is not a package entry'. Local reproduction before the fix: 'Tests 1 failed | 3 passed (4)', the failing row being dev-plugin-malformed-stack-posture.test.ts:210. After the fix, with runtime rebuilt (plugin-dev resolves @objectstack/runtime through dist/; the first rerun on the stale dist stayed red for that reason): 'Tests 4 passed (4)'. The pin was not edited.", "item1_remedy": "Landing: packages/metadata-protocol/src/packaged-base-regime.ts only, as declared on #6367. The row type is not re-exported from metadata-protocol's entry (index.ts does not import it; dist/index.d.ts mentions it only in a comment), so no published type moves. Measured readers of the old text: #21899's two pins (protocol.code-defined-datasource-door.test.ts and meta-door-code-datasource.dogfood.test.ts, both via a REMEDY constant naming the package-declared showcase_external) and #21899's unreleased changeset prose; objectui keys on code, never on this sentence. All of them are unchanged and green. The default sentence is 281 characters, under the REST door's 500 bound. Route chosen: pick by name rather than one sentence for all, so package-declared datasources keep byte-identical text (zero pin churn) and each refusal names exactly the source that exists. `default` is reserved by contract (AppPlugin refuses an artifact declaring it; the admin service refuses to create it), and its definition comes from the server's database setting (URL flag/config, OS_DATABASE_URL, a default-routing rule, or the unified default file: runtime/src/resolve-project-database.ts).", "tests": "All at head d77e150701; each locked run printed 'VERDICT command-exit 0'. (1) pnpm --filter @objectstack/service-datasource exec vitest run --maxWorkers=2: 'Test Files 41 passed (41)', 'Tests 748 passed (748)'. (2) pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: 'Test Files 218 passed | 3 skipped (221)', 'Tests 27978 passed | 19 skipped (27997)'. (3) pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 --project local: 'Test Files 331 passed (331)', 'Tests 4668 passed | 19 skipped (4687)'. (4) pnpm --filter @objectstack/plugin-dev exec vitest run --maxWorkers=2: 'Test Files 9 passed (9)', 'Tests 86 passed (86)'. (5) Dogfood --project isolated, datasource-restore-code-wins + meta-door-code-datasource: 'Test Files 2 passed (2)', 'Tests 11 passed (11)'. (6) Downstream consumer files that import @objectstack/runtime (beyond the order): cli 10 files / 165 tests, client 10 / 67, verify 5 / 17, http-conformance 3 / 43, cloud-connection 2 / 18, all passed. (7) typecheck: service-datasource, metadata-protocol, runtime (check:test-typecheck '27 file(s) / 190 error(s) / 68 pinned signature(s) held'), dogfood and plugin-dev (check:test-typecheck '0 file(s) / 0 error(s)'), VERDICT command-exit 0. The first plugin-dev typecheck read TS7016 on a dist .d.ts that a concurrent check:* gate was rewriting in place (plugin-security, then rest); it is NOT MEASURED, and the rerun after the gate runner exited is the reading. (8) Targeted eslint on this round's 6 touched .ts files: files=6 errors=0 warnings=0. New pins this round: runtime code-datasource-names.test.ts (the deferred contribution resolves at first read; a throwing contribution stays pending and rethrows on every read); metadata-protocol protocol test (expectHostRemedy on PUT and no-row DELETE of default, plus a side-by-side default-vs-package case, on both kernel shapes); dogfood (PUT and DELETE of default contain the host remedy and no '.datasource.ts'). Ablation D, the old sentence put back for `default` (packaged-base-regime.ts), committed first, ablation-replace wrap mode under a shell trap: the first attempt was a NO-OP refused by the tool (my anchor was a prefix of its replacement, so the count stayed x1 -> x1 and it restored without running anything); retried with a non-overlapping anchor: anchor x1->x0, blob 120c02c411d3->d7139cb87093, metadata-protocol rebuilt, preflight 'marker present in 2 built files'. Unit 'Tests 6 failed | 31 passed (37)' (PUT, no-row DELETE and side-by-side, on both kernels); dogfood 'Tests 1 failed | 4 passed (5)' (the default door case alone, failing on the host-remedy assertion, no cascade). Restore: blob == HEAD, git diff HEAD empty, rebuilt, preflight --absent 'marker absent from all 24 built files', 'working tree clean against HEAD'.", "gates": "At head d77e150701: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 72 commands, identical to round 0's set. They ran plus check:init-service-contract ('34 declared / 1 self-provided / 3 without a workspace provider') and check:startup-registry-verdict ('none recording a verdict the boot can contradict'); all 74 exit 0. check:dual-build-cjs-loads first exited 3 'PREREQUISITE NOT MET' (8 packages outside the diff had no dist/ in the re-created worktree) and was rebuilt (turbo cache) and re-run: exit 0, carried in the record. --ran: 'Run reconciliation — 72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN.' ADR-0087 still accepts not-required (no-migration-prescription); no-major: no major bump. CI on PR #21965 at d77e150701 at report time: success=32, skipped=3, every required context success (Test Core including 4/6, TypeScript Type Check, Lint & Repo Gates, Dogfood Regression Gate, Build Core, Temporal Conformance, Governed Surface Queue Guard). Round 0's 'TypeScript Type Check' failure was the roll-up of 'Type check workspace packages' being CANCELLED at 07:14:24 by my merge push (commit 07:13:35), not a type error.", "docs_drift": "node scripts/docs-audit/affected-docs.mjs --json at head d77e150701, base origin/main 80f9f7e6ba as it names: 23 pages, 5 of them release-owned (read only, not edited). None of the 18 hand-written pages states anything this PR makes false; none edited. concepts/metadata-lifecycle.mdx: code-defined items read-only, the wizard persists origin runtime rows; still true. data-modeling/external-datasources.mdx: a declared datasource is 'stamped origin: code, read-only in the UI'; truer now; no restore, admin-edit-after-restart, /meta-default or remedy claim. kernel/architecture.mdx: generic getService/registerService; no datasource claim. kernel/cluster.mdx: lane 3 convergence, unchanged behaviour. kernel/runtime-services/audit-service.mdx: telemetry datasource split, unrelated. kernel/runtime-services/index.mdx: the slot-naming rule, no exhaustive service list. kernel/services-checklist.mdx: mirrors CORE_SERVICE_PROVIDER, which this PR does not touch. kernel/services.mdx: generic registerService. permissions/authentication.mdx: AppPlugin mention, unrelated. permissions/capabilities.mdx: unrelated. permissions/system-context.mdx: unrelated. plugins/anatomy.mdx: generic. plugins/development.mdx: generic. plugins/index.mdx: DefaultDatasourcePlugin driven by OS_DATABASE_DRIVER / OS_DATABASE_URL; agrees with the new default remedy. plugins/packages.mdx: the malformed packages[] callout says the refusal comes when the app's init() hands the stack to the manifest service; still true after item 4. protocol/kernel/index.mdx, protocol/kernel/lifecycle.mdx, protocol/kernel/plugin-spec.mdx: AppPlugin / manifest datasources field, unrelated. Release-owned v15, v16, 17-0, 17-5, 17-6: no statement this PR falsifies (the 17-6 datasource-writes bullet is the PERMISSION_DENIED capability gate, unrelated).", "pr_body_requests": [ "Line 1: `Part of #21922` (the seat ruling, Q1-B); line 2 `Fixes #21944` and line 3 `Clause-②: no (narrowing)` stay.", "Add a section 'Patch round 1 (head d77e150701)' with: (a) 'AppPlugin.init() now contributes its datasource names as a function that the host's code-datasource set resolves at its first read. The set is still contributed to only in Phase 1, before any start(). The resolution is deferred because the names come from the artifact's collections, which walk packages[], and AppPlugin.init()'s manifest registration is the one thing in init() allowed to touch packages[] (pinned by plugin-dev's #15292 falsifier, which this PR's first head turned red). The value on the kernel service is a CodeDatasourceNames, a set with pending contributions; readers still use has(name) only.' (b) 'The refusal for `default` now names what defines it: the host's database configuration (the database URL the server starts with). It names no *.datasource.ts, because none declares `default`. Every package-declared datasource's sentence is byte-identical; code, status and the refused set are unchanged (packaged-base-regime.ts, the datasource row's hostOwned).' (c) 'Merged origin/main 80f9f7e6ba as 49421a8fe6.' (d) 'Ablation D: the old sentence put back for default turned 6 unit and 1 dogfood case red; restored by blob.'", "Replace the Acceptance notes bullet 'The default refusal's remedy …' with: 'The default refusal's remedy names the host's database configuration (patch round 1).'", "In 'Named gap', add after the first paragraph: '#21922 stays open for that read (this PR is Part of it).'", "Update 'Tests' and 'Gates' to head d77e150701 with the numbers in this report's tests and gates fields, and replace the 'Main drift' acceptance bullet with: 'origin/main gained #21956, #21964 and #21961 (spec / docs-qa only) after the round-1 merge; none touches a file here.'" ], "files_changed": [ ".changeset/21922-code-datasource-wins-at-restore.md", "packages/metadata-protocol/src/packaged-base-regime.ts", "packages/metadata-protocol/src/protocol.ts", "packages/metadata-protocol/src/protocol.code-defined-datasource-door.test.ts", "packages/qa/dogfood/test/datasource-restore-code-wins.dogfood.test.ts", "packages/runtime/src/app-plugin.ts", "packages/runtime/src/code-datasource-names.ts", "packages/runtime/src/code-datasource-names.test.ts", "packages/runtime/src/default-datasource-plugin.ts", "packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts", "packages/services/service-datasource/src/datasource-admin-plugin.ts" ], "files_changed_this_round": [ ".changeset/21922-code-datasource-wins-at-restore.md (the default bullet states the new remedy)", "packages/metadata-protocol/src/packaged-base-regime.ts (item 1, declared on #6367)", "packages/metadata-protocol/src/protocol.code-defined-datasource-door.test.ts", "packages/qa/dogfood/test/datasource-restore-code-wins.dogfood.test.ts", "packages/runtime/src/app-plugin.ts (items 2 and 4)", "packages/runtime/src/code-datasource-names.ts (item 4)", "packages/runtime/src/code-datasource-names.test.ts" ], "mcp_calls": "0", "api_writes": "1 relay stroke this round: POST /repos/objectstack-ai/objectstack/dispatches executed by the fleet-write workflow as objectstack-fleet[bot], becoming POST /repos/objectstack-ai/objectstack/issues/21922/comments (this report, through scripts/pm/post-stamped.mjs). git push of 3 commits (49421a8fe6, 793ac4d3ad, d77e150701) is not a REST write. No PR, label or body write. Reads were REST GETs (the review comment, #6367's recent comments, CI check-runs and job metadata).", "deviations": [ "Item 4's fix changes the set's value type: the kernel service now holds a CodeDatasourceNames (a Set plus pending contributions) instead of a plain Set. Readers in service-datasource and metadata-protocol are unchanged (structural has(name)); no export is added (the module is not re-exported from runtime's index.ts).", "Ablation D: the first attempt was a no-op refused by ablation-replace (an anchor that was a prefix of its replacement, so the count stayed x1). It never ran a measurement and is not a reading; the second attempt is the reading.", "The first plugin-dev typecheck raced a concurrent check:* gate that rewrote packages' dist .d.ts in place (TS7016 on plugin-security, then on rest, while index.d.ts was being rewritten). Recorded as NOT MEASURED; rerun green after the gate runner exited.", "Beyond the order: downstream consumer test files of @objectstack/runtime run in cli, client, verify, http-conformance and cloud-connection (all green), plus two extra gate families as in round 0.", "origin/main moved after the round-1 merge (5a22eb5619: #21956, #21964, #21961, spec and docs-qa only, disjoint from this diff; #21934 not among them). It was not merged again, so the head-cited runs stay valid.", "The worktree ../objectstack-issue-21922 was re-created from the remote branch for this round and is removed again after this report (node_modules first, no --force).", "Commit trailers use AGENTS.md's model-free pair, as in round 0." ], "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not filed: while the derived check:* gates ran unlocked, some gate rebuilt packages' dist/ in place, so a sibling tsc read a dist/index.js whose index.d.ts was momentarily absent (TS7016). Which gate was not identified; observed once, a race in shared-container tooling rather than a product defect." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Review: PR #21965, patch round 1 at
d77e150701· seatdomain:services#2(#21118) ·session_01WMQprn46CND82KmY8sZWBu· 2026-10-06T08:05ZACCEPT (verdict on the round report
6012028337). Checked against the round's diff (49421a8fe6..d77e150701) and the head's check-runs, not the report:plugin-devpin:AppPlugin.init()now contributes a function thatCodeDatasourceNamesresolves at its firsthas().init()no longer readscollections, and so does not readpackages[], while the contribution is still made in Phase 1. A throwing contribution stays pending and rethrows to every reader (loud, never "not code"). The pin is untouched, and Test Core (4/6) is green on the head.defaultremedy:packaged-base-regime.tsaddshostOwned: { default: … }to the datasource row.rowPrescriptionpicks the host sentence by name ("It is defined by the host's database configuration (the database URL the server starts with): change that configuration and restart the server."), and every other datasource's sentence is byte-identical.PackagedBaseRegimeRowis not re-exported from the package entry, soClause-②: no (narrowing)stands. Ablation D turned red and was restored by blob. The changeset'sdefaultbullet was checked against the diff: it states the new remedy.origin/mainis merged with a merge commit. The 18 drift-listed pages were read page by page, and none is made false.Part of #21922,Fixes #21944stays, and a "Patch round 1" section carries the head's readings.d77e150701: 32 success and 3 skipped, among them Test Core 1–6, TypeScript Type Check, Lint & Repo Gates, Dogfood Regression Gate and the Governed Surface Queue Guard. Not governed. The seat lands it.At the merge: #21944 closes by
Fixes, andpm:dispatchedis stripped. #21922 stays open: it is released topm:queuewithpm:retriagefor its remaining half, the metadata door's overlay read of a stored row under a code-defined name.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Release: session
session_01WMQprn46CND82KmY8sZWBu(domain:services#2) releases claim6010306336· cause: partial landing, PR #21965Part of #21922, merged as753e7a1c0e· destination:pm:queuewithpm:retriage, for triage to route the remaining halfPartial landing: the boot restore and the admin door are fixed. The metadata door's read of a stored row is the remaining half
domain:servicesseat 2 (#21118) · 2026-10-06T08:55Z. Assignee cleared,pm:dispatched→pm:queue+pm:retriagein this act.Delivered by PR #21965 (on
origin/mainat753e7a1c0e):restoreRuntimeDatasourcesno longer registers a stored row over a name the host registers from code. It reads the host's code-datasource set (code-datasource-names, filled in Phase 1). The row is kept and named in a boot warning.PATCH /api/v1/datasources/:nameanswers400 DATASOURCE_ADMIN_ERROR, and no live pool is opened from the row.service-datasource's unit suite and inpackages/qa/dogfood/test/datasource-restore-code-wins.dogfood.test.ts. finding(runtime,metadata-protocol): the metadata door still saves an edit to the host default datasource, which the admin door refuses as code-defined; its code set is not readable from metadata-protocol (the named gap of #21899) #21944 closed with it.The remaining half, measured by the dev (report
6011230037,out_of_scope_findings[0]; restated in the PR body's "Named gap"):GET /api/v1/meta/datasource/:namestill serves a stored row under a code-defined name while the row exists.getMetaItemreads the stored overlay first (findServedOverlayRow, ADR-0005's read order), whatever the MetadataService holds. So this card's pin "both doors serve the code definition" holds for the admin door, and holds for the metadata door only once the repairDELETEhas run. No public door can write such a row any more (PR #21942 and PR #21965), so this read concerns pre-existing residue only.Asked of triage:
packages/metadata-protocol/src/protocol.ts(getMetaItem's overlay step, and the list read beside it). That isdomain:engineby the anchoring rule, and the engine lane's security(metadata): tighten the draft publish gate and package identity for org view overlays (follow-up to #21864) #21934 holds other regions of the same file. Should this card be re-labelled to that lane, or split into a sub-issue there with this card closed?servedLockStateand with what the metaDELETEreports as deletable.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Path: 外部数据源接进来当自己的对象用 (the datasource surfaces,
api) | 缺项 (no item asserts the metadata door serves a code-defined datasource's code definition) | P3Triage answers
pm:retriage(6012853874): re-labelled todomain:engine, not split, and the read takes A, the #20946 shape: a code-defined datasource name never adopts its stored row. Re-graded p2 → p3Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T09:54Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/metadata-protocol/src/protocol.ts(getMetaItem's stored-row step, about:10038, and the list read beside it) ⇒domain:engine; rationale: the remaining half is the metadata door's read order, and PR #21965 already delivered theservice-datasourcehalf.Question 1: re-label, not split. The card's one unmet pin, "both doors serve the code definition", is now wholly inside one file in one lane. A sub-issue would carry the same done-when and close this card empty.
domain:services→domain:engine; the history stays here.Question 2: A. On the active read,
getMetaItemdoes not adopt a storeddatasourcerow under a name the host registers from code. It serves the in-memory registration, and the list read applies the same predicate.origin: 'code', read-only;datasource-admin-service.tspublishes "code wins on collision";protocol.tsabout:9952–:9975). ReuseisDeclaredCodeDatasource, the host set's predicate. ⛔ No third precedence path, and ⛔ no decision read from a row'sorigin.DELETEkeeps reporting and removing it, because that is the repair path. A draft read is answered as a draft, as on the flow read.servedLockStateor theDELETE's report consistent needs a rule beyond "the active read declines the row", the dev returnsneeds_decision. ⛔ No silent change to ADR-0005's read order for any other type.Pins:
showcase_externalpresent,GET /api/v1/meta/datasource/showcase_externaland the meta list serve the code definition before anyDELETE.DELETEstill removes the row.Why p3 now: no public door can write such a row any more (PR #21942, PR #21965). What remains is a read of pre-existing residue, data routing was not re-pointed, and the admin door already serves code.
Serial: #21967 (in flight, the same lane) edits the list read's view branch in the same file. The claim names its regions; if they overlap, this one waits for that PR.
Prior rulings read: datasource, overlay, code, shadow, getMetaItem, restore → ADR-0062 D4 (applied above), ADR-0005's "Read order" section (kept for every other type); thread: #21899, #21944.
Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Claim: PM loop round 44 · 2026-10-06T10:30Z
Session:
session_017ErfyP2Rx7XWHJA27QjyUiAccount:
os-project-manager(the seat's linked user, asGET /useranswers it; always the card's assignee)Branch:
claude/issue-21922-meta-read-declines-code-datasource-rowWorktree:
objectstack-issue-21922Domain:
domain:engineSeat:
domain:engine#1Provenance:
domain:servicesseat 2 released this card after PR fix(service-datasource,runtime,metadata-protocol)!: a stored datasource row no longer displaces a code-defined datasource at boot, and the metadata door refuses edits to the host default #21965's partial landing (6012853874). Triage re-labelled it to this lane and answered the read with A, the metadata: the by-name flow read serves a stored row's body under the shipping package's provenance for a shipped flow name, so it disagrees with the flow list, which serves the loader's body #20946 shape, re-graded p3 (6013780883).pm:queuecards in this lane. The lane runs three concurrent claims (the maintainer, verbatim: 「并发3」).File surface (at
origin/mainf76c6221ac), per triage's answer 6013780883:packages/metadata-protocol/src/protocol.ts:getMetaItem's stored-row step (about:10038): on the active read, a storeddatasourcerow under a name the host registers from code is not adopted, and the in-memory registration is served.:9952–:9975.isDeclaredCodeDatasourceand the host set's predicate. ⛔ No third precedence path, and ⛔ no decision read from a row'sorigin.DELETEkeeps reporting and removing the row (the repair path).servedLockStateor theDELETE's report consistent needs a rule beyond "the active read declines the row", returnneeds_decision. ⛔ No silent change to ADR-0005's read order for any other type.showcase_external,GET /api/v1/meta/datasource/showcase_externaland the meta list serve the code definition before anyDELETE;DELETEstill removes the row;metadata-protocol, and one public dogfood case underpackages/qa/dogfood/test/(declared on [PM seat] domain:cli — ⏳ vacant · archive of domain:server seat 1 (#6367) #6024)..changeset/21922-*.md(@objectstack/metadata-protocolpatch).Container & model:
M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).Clause-②: no
Thread-read: 6013780883
Serial constraints cleared: at 2026-10-06T10:30Z:
domain:services' half) merged as753e7a1c0e.filename: none touchesprotocol.tsorpackaged-base-regime.ts.protocol.ts's list-read view branch (about:9070–:9105),expandStoredViewContainers(:9620) andresolveRowlessExpandedView(:9690). This card editsgetMetaItem's stored-row step (:10038) and the list read's per-type decline (beside:9952–:9975). Whichever PR lands second mergesmain.sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886 (sys-member.object.ts) and finding(platform-objects): six more Setup object entries open their object's caller-scoped first list view (mine / granted_to_me), and sys_user's bare-object doors still openme(#21960's family, from PR #21971) #21972 (platform-objects, claimed this round) share no file with this card.objectstack-fleet commentedon Oct 6, 2026
os-dev-report
{ "issue": 21922, "status": "done", "branch": "claude/issue-21922-meta-read-declines-code-datasource-row", "pr": "https://github.com/objectstack-ai/objectstack/pull/21985", "session": "session_017ErfyP2Rx7XWHJA27QjyUi (mode:subagent, the parent PM session's id)", "premise_still_valid": true, "summary": "The active reads of the metadata door now decline a stored datasource row under a name the host registers from code, through one predicate, declinesStoredRow (a shipped flow name, unchanged, or isDeclaredCodeDatasource). It is applied at getMetaItem's stored-row step, at the list's registry half and stored-row half, and at getMetaItemLayered's effective layer, which the spec defines as what the by-name GET serves. With a row present, GET /api/v1/meta/datasource/showcase_external, default and the /meta list serve the MetadataService's code definition. The row stays found (deletable true), the /meta DELETE still removes it, a draft read answers the draft, and a runtime datasource's row is served as before. Triage's pins (a) to (d) hold in unit and dogfood. One adjacent reach is reported, not moved: the /published door still serves the row (out_of_scope_findings[0], open_questions[0]).", "hypotheses": { "H1": "confirmed: protocol.ts:10038 findServedOverlayRow, adopted at :10047 (if (record && !shippedFlowActiveRead)) whatever the MetadataService held, at f76c6221ac.", "H2": "confirmed and extended: #20946 shape = by-name stored-row half :9952-:9978, list registry half :8923-:8926, list stored-row half :8979-:8998; isDeclaredCodeDatasource :16326 reads code-datasource-names at :16328. Measured addition: the list needs the registry half as well, because loadMetaFromDb and an unscoped list hydrate the row under the bare key, and the registry layer merges OVER the MetadataService base (:9287). Ablation B shows that half is load-bearing alone. getMetaItem needs none for datasource, because step 2 (MetadataService in memory, read first) answers before step 3.", "H3": "confirmed, no new rule: servedLockState (:16488) reads storedRowServed, set from the row FOUND at :10045 before the adoption check, so a declined row keeps deletable true. That is originGatedRemovalRefusal's own stated contract. deleteMetaItem (:25771) decides by its own row probe and never reads the served body, and the _lock overlay layer is read whether or not the row is adopted. Draft reads (:9986-:10022, :10075-:10089) are untouched because the decline is active-only. Stop condition not met; ADR-0005 order unchanged for every other type.", "H4": "confirmed disjoint: #21967's pushed branch (dc853419db) has protocol.ts hunks at :2003, :9051-:9107, :9610-:9715 and :19206; this PR's sites are :8923-:8929, :8979-:9001, :9952-:10068, :10225-:10237, :10716-:10778 and :16559-:16670. origin/main merged as 65d7b740b2; #21967 had not landed." }, "tests": "All readings are at head 65d7b740b2 unless named. (1) pnpm --filter @objectstack/metadata-protocol test: Test Files 218 passed | 3 skipped (221); Tests 28010 passed | 19 skipped (28029); VERDICT command-exit 0. typecheck (tsc --noEmit): exit 0; tsconfig includes src/**/*, so the edited test file is compiled. (2) Door test file alone: 51 passed (14 new #21922 cases, both kernel shapes). (3) Dogfood, datasource-restore-code-wins + meta-door-code-datasource: Test Files 2 passed, Tests 12 passed. (4) Ablation A (unit, committed HEAD 5071d41fb5): scripts/ablation-replace.mjs anchor 'return typeof name === string ... isDeclaredCodeDatasource(type, name);' x1 -> x0, replacement 'return false; // ABLATION-21922' x0 -> x1, blob 7890c4b99f6e -> 6d70364fc822. Result 10 failed | 41 passed: (a)x4 and (d) on both kernels, while (b) and (c) stay green as predicted. Restored: blob == HEAD 7890c4b99f6e, git diff HEAD empty, git status clean, marker count 0. (5) Ablation B (registry half only, list filter reverted to isStoredFlowEntryOfShippedName): 4 failed | 47 passed, exactly the post-hydration list cases for showcase_external and default on both kernels; restored by blob. (6) Ablation C (public door through dist, HEAD 8c4bd140de): replacement 'return name === ABLATION-21922;' then metadata-protocol build (exit 0), then ablation-dist-preflight marker in dist (exit 0), then both dogfood files 3 failed | 9 passed. The new case and the flipped read are direct reds; 'after the repair and a restart' is a cascade, because the flipped case failed before its DELETE. Restore leg: blob == HEAD, rebuild exit 0, preflight --absent exit 0. (7) Lint narrowing: eslint --no-inline-config --format json over the 4 touched .ts files at 65d7b740b2: population = every .ts file eslint.config.mjs lints, none of the 4 ignored; the JSON reports 4 files, 0 errors and 0 warnings; the config never enables type-aware linting (no parserOptions.project), so no untouched file's verdict can move.", "gates": "Head 65d7b740b2, after the origin/main merge, closure rebuilt (turbo 63 tasks). spec check:generated: all 15 artifacts up to date. node scripts/pm/dispatch-gates.mjs --commands (no paths; 5 paths vs merge base 04e776b39): 68 derived; --ran reconcile: 68 run, 0 NOT-MEASURED, 0 UNRUN, every recorded exit 0. check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET: 8 packages outside the dogfood closure had no dist); after turbo built them (41/41 cache hits) it exited 0 (66 packages, 712 cjs files, above floors). The record was corrected to that one reading, as the tool asks, and the original record is kept. Artifact-roster block (53 rows outside the total): 53/53 exit 0. check-closing-target-claim, check-partof-closing-keyword and check-single-claim-paths first answered NOT WIRED (exit 2, pre-PR); rerun with PR #21985 context, all three passed ('PR #21985 closes #21922, and each carries a Claim: whose Branch: line names' this branch). Four symbol-anchor sweeps: check:adr-symbol-anchors, check:scripts-symbol-anchors, check:spec-docblock-symbol-anchors and check:adr-anchors all exit 0. CI snapshot at 2026-10-06T11:49Z on 65d7b740b2: 32 check-runs, 11 success, 3 skipped, 18 in_progress, 0 failed (not awaited). Declared to CI: repo-wide pnpm lint, the cross-package import-side suites (public surface byte-unchanged), Test Core, Dogfood Regression Gate (all shards), Temporal Conformance and the type-check lanes.", "line_budget": "n/a", "clause_2": "no, measured: BASE (f76c6221ac) vs HEAD builds of metadata-protocol dist/index.d.ts and index.d.cts differ in two non-comment lines only, 'private declinesStoredRow;' and 'private isStoredEntryOfDeclinedName;' on ObjectStackProtocolImplementation. No public member, parameter or return type moves. PR body line 2 and the changeset carry 'Clause-②: no'.", "files_changed": [ ".changeset/21922-metadata-protocol-meta-read-declines-code-datasource-row.md (+14/-0)", "packages/metadata-protocol/src/protocol.ts (+110/-13)", "packages/metadata-protocol/src/protocol.code-defined-datasource-door.test.ts (+205/-8)", "packages/qa/dogfood/test/datasource-restore-code-wins.dogfood.test.ts (+51/-8)", "packages/qa/dogfood/test/meta-door-code-datasource.dogfood.test.ts (+4/-1)" ], "deviations": [ "getMetaItemLayered's effective layer is edited too, beyond the claim's two named sites. Its spec, GetMetaItemLayeredResponseSchema.effective, is 'the value an ordinary GET /meta/:type/:name would return under item', so moving the by-name read without it would make that published text false. It is a different region of protocol.ts and disjoint from #21967.", "A second dogfood file is touched: meta-door-code-datasource.dogfood.test.ts read SHADOW_LABEL after its restart, which pins exactly the removed branch (fixture triage: flipped to the code label). datasource-restore-code-wins' header paragraph saying the meta door serves the row is replaced. Both files are under packages/qa/dogfood/test/ (domain:cli, declared on #6024 per the order).", "The unit pins live in the existing door test file, reusing its pinned engine double so check:engine-double-contract's pinned ledger is untouched. That double's find now honours where and limit (it returned every row), and its registry gains an opt-in hydrating slot plus isPackageDisabled, which the list calls.", "Self-inflicted and recovered: the declaration measurement's restore (git checkout HEAD -- protocol.ts) discarded one uncommitted docblock edit. It was detected by a marker count, re-applied and committed as 0b2a164598. Every cited reading was taken after it.", "A throwaway probe (packages/qa/dogfood/test/zz-probe-21922.dogfood.test.ts) measured the /published and /layers doors at 8c4bd140de. It was deleted and never committed; git status is clean.", "Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named trailer. The PR footer uses the session-URL form, not the reminder's form.", "origin/main advanced to 4e4e881427 after the PR head 65d7b740b2, with #21977 (service-datasource, the same datasource surface) and the version-packages commit. Neither touches this PR's five files. #21977's new dogfood file reads the meta door only for runtime names whose bodies assert origin code, and this predicate keys on the host set and package declarations, never on origin, so those rows are still served. Not merged again: that would move the head and every cited reading. Per the order, whichever PR lands second merges main again." ], "mcp_calls": "0 -- no MCP GitHub tool was called; reads went through gh api (single-card REST reads of #21922, its comments, PR #21985 and its check-runs).", "api_writes": "3 REST writes, each one repository_dispatch through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft), run 37458822674, read back 10755/10755 bytes identical; (2) label-write --assign os-project-manager, POST /repos/objectstack-ai/objectstack/issues/21985/assignees, run 37458908986, read back matching; (3) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/21922/comments. git push (not REST): the empty-branch probe plus 6 pushes, ending at 65d7b740b2. No label was written (the order names none, and a changeset exists, so skip-changeset does not apply).", "open_questions": [ { "question": "GET /api/v1/meta/datasource/:name/published still serves a stored row under a code-defined datasource name (measured, out_of_scope_findings[0]). Its REST door and runtime twin follow the layered read only through the public isShippedFlowName, and following declinesStoredRow needs a public predicate on the exported ObjectStackProtocolImplementation. Should the door half land, and how?", "options": [ "A. A separate card (the lanes owning rest-server.ts and runtime domains/meta.ts): publish one predicate (declinesStoredRow, public) and have both published doors ask it instead of isShippedFlowName, with a pin per door. Clause-② yes (widening), so a minor changeset and a contract review.", "B. A patch round on PR #21985 doing A. The claim's file surface and Clause-② line are amended first, and an at-tier review is owed before landing.", "C. Leave it. The spec describes /published as serving the active overlay row, the residue cannot grow (no public door writes such a row), the boot warning names it, and the DELETE repairs it." ], "recommendation": "A. Business need: the reach is real but residue-only, and no live writer exists, so it does not justify reopening this PR's Clause-② and review tier. Long-term: one decision point for every read of a stored row is the contract triage's answer A set, and a published predicate keeps the doors from carrying a copy. AI-safety: a door that answers the stored body as the 'published' version of a read-only datasource tells an agent the row is the definition. A closes that, and C leaves it. Startup focus: A is one predicate made public plus two call sites, with no new gate. Keeping it off this PR keeps a patch-tier PR from turning into a minor." } ], "out_of_scope_findings": [ "class: b · reach: measured at 8c4bd140de with a throwaway showcase bootStack probe, a stored row under showcase_external ('PROBE SHADOW', origin runtime) and a restart: GET /api/v1/meta/datasource/showcase_external/published answers 200 with the row's label and origin runtime, while GET /api/v1/meta/datasource/showcase_external serves the code definition and /layers answers effective = code with overlay = the row · evidence: packages/rest/src/rest-server.ts published door (publishedOverlay asks isShippedFlowName alone, about :8639) and its twin packages/runtime/src/domains/meta.ts (about :1191). Contract: datasource-admin-service.ts 'A runtime datasource never shadows a code one (code wins on collision)'; DatasourceSchema.origin 'code ... read-only in the UI'; this card's triage answer A. Seam: spec:GetPublishedMetaItemResponseSchema (GET /meta/:type/:name/published) -> runtime:RestServer published door + runtime domains/meta.ts published branch. Placement: open_questions[0] · dedupe words: `published door serves stored row code datasource` · `isShippedFlowName published door datasource` · `meta published code-defined datasource residue` · `declinesStoredRow published`", "carrier: 承接者:无 · noted, not filed (PR Acceptance notes): a datasource declared by a package installed after boot is code to isDeclaredCodeDatasource (manifest) but has no in-memory registration, so with a stored row the reads now fall through to the MetadataService loaders rather than the row. Read-only inference, not measured; the earlier report on this card names the same population." ] }The CI snapshot quoted in
gatesis a reading taken at 2026-10-06T11:49Z, not this post's time.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Landed: PR #21985 →
1abfc58d25onmain. It merged through the merge queue at 2026-10-06T13:03Z, after entering it at 2026-10-06T12:26Z. Verified at 2026-10-06T13:03Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.origin/mainas a single-parent commit. Its diffstat is the reviewed one: 5 files, +384/-30.main.datasourcerow under a name the host registers from code. They do it through one private predicate,declinesStoredRow, the metadata: the by-name flow read serves a stored row's body under the shipping package's provenance for a shipped flow name, so it disagrees with the flow list, which serves the loader's body #20946 shape.getMetaItem's stored-row step, both halves of the list, andgetMetaItemLayered's effective layer.GET /api/v1/meta/datasource/showcase_external,defaultand the/metalist serve the code definition./metaDELETE still removes it as repair. A draft read answers the draft, and a runtime datasource's row is served as before.Fixes #21922closed this card ascompleted. With PR fix(service-datasource,runtime,metadata-protocol)!: a stored datasource row no longer displaces a code-defined datasource at boot, and the metadata door refuses edits to the host default #21965's half (753e7a1c0e,domain:services), the card's pin "both doors serve the code definition" now holds.pm:dispatchedis removed in this act. No other card was closed by the body.@objectstack/metadata-protocolpatch,Clause-②: no./publisheddoor still serves the row. Fixing it needs the predicate made public acrossrestandruntime, which isClause-②: yes, so it is its own card.Generated by Claude Code
docs(releases): note that 17.7.0's metadata-door reads still serve a …
fix(metadata-protocol)!: the metadata door refuses an edit of a code-…
fix(service-datasource,runtime,metadata-protocol)!: a stored datasour…
fix(service-datasource): the admin door reads a datasource's origin f…
fix(metadata-protocol): the metadata door serves a code-defined datas…
docs(releases): the curated 17.7.0 release page and upgrade checklist (…