Skip to content

fix(runtime): collapse the published-seed read to the single env-wide read its gate produces - #15592

Draft
os-litant wants to merge 5 commits into
mainfrom
claude/issue-15068-seed-read-dead-org-rung
Draft

fix(runtime): collapse the published-seed read to the single env-wide read its gate produces#15592
os-litant wants to merge 5 commits into
mainfrom
claude/issue-15068-seed-read-dead-org-rung

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes #15068

applyPublishedSeeds — the route-level seed apply behind POST /packages/:id/publish-drafts — read each just-published seed body twice whenever the session had an active organization: once naming the organization, then once env-wide. This collapses that ladder to the single read the gate already produces, and rewrites the comment to say the scope is decided by the registry flag, not by this call site.

The fence this card came with, and how it was cleared

Triage was explicit that a registry reading alone was not licence to delete anything: the original comment says resolving the wrong scope "is what silently produced 0 rows loaded", so the ladder was written for a real outage. The deletion is licensed by measurement instead.

The pinned publish-then-read path (packages/runtime/src/domains/packages-seed-apply-org-scope.test.ts). The publish is real — a state:'draft' seed row promoted by the shipping publishPackageDrafts on a real ObjectStackProtocolImplementation. The read-back is real — the same protocol instance serving getMetaItem over the same engine, reading the row publish just wrote. Exactly one thing is doubled, and only to reach the code under test at all: the route-level apply runs only for protocols that do not self-apply seeds inside publishPackageDrafts, so the second call presents a publishPackageDrafts that reports the published seed without a seedApplied field — the population this fallback documents itself as existing for.

The positive control (§0) — without it, "nothing reddened" is indistinguishable from "nothing ran":

  • the publish really promoted the draft (an active row lands in sys_metadata);
  • the read-back really reached sys_metadata and resolved the row publish had just written (item.object === 'project', two records back);
  • and the session's organization really reached this request — publishPackageDrafts was handed organizationId: 'org_acme', and one resolveActiveOrganizationId call serves both it and applyPublishedSeeds. That is what put the ladder on its two-rung branch.

The ablation, direction predicted before running: §0/§1 stay green (the rung is dead) and §2/§3 flip green. The second rung was neutered on disk, proven by counting both the removed text (1 → 0) and an injected OS_ABLATION_15068 marker (0 → 1) plus a non-empty git diff --stat, under a trap … EXIT INT TERM restoring through an absolute path.

leg packages.ts result
A — baseline unmodified 7 passed, 3 failed — §0 (2) and §1 (5) green, §2 (2) and §3 (1) red
B — ablation second rung neutered 10 passed, 0 failed
C — restore git checkout HEAD -- ABSOLUTE_PATH HEAD blob 1c0349a734d… == worktree git hash-object (both non-empty), git diff HEAD empty, marker count back to 0, removed text back to 1

⇒ Neutering the rung reddened nothing on a path proven to reach it. The rung is dead.

Resolution path, stated because a source-only mutation can measure nothing. The mutated file is reached by relative source (./http-dispatcher.js./domains/packages.js), so no dist/ hop is involved for it and ablation-dist-preflight does not apply — and the ablation's own §2/§3 flip is the proof the edit reached the running code. The gate under test (@objectstack/metadata-protocol, @objectstack/metadata-core) resolves through exports to dist/, so the dependency closure was built before any leg was read (pnpm --filter '@objectstack/runtime^...' build, exit 0).

The mechanism, and the case analysis

getMetaItem opens with organizationIdForMetaRead(request.type, request.organizationId) (#14908) and spends that binding — never the raw argument — on every read beneath it; request.organizationId appears exactly once in the method. seed declares allowOrgOverride: false, so the predicate answers undefined whatever organization arrives.

§1 measures that directly rather than restating it: both rungs, run against one store, produce byte-identical engine predicates and byte-identical answers on the hit branch and the miss branch. The control that makes it a reading rather than a tautology: the same comparison on view (allowOrgOverride: true) does separate the two rungs, and the org-first one names a partition the env-wide one never touches.

Measured here and not claimed by the card: getMetaItem answers a wrapper with no item — not a falsy value — for a name it cannot resolve. So if (item) break fires on the first attempt even on a miss, and the only branch on which the second rung ever executed is the throw branch, where it repeated the identical failing read.

The one observable that changes

On the throw branch, the duplicated rung appended the same sentence to the client-facing seedApplied.errors[] twice — a payload an author reads as two distinct failures. A failed read-back is now reported once (§3, asserting the ADR-0112 declaration on code and status; never a bare toThrow(), since this door reports rather than throws). Nothing about which row a publish resolves, or whether its rows load, moves.

Clause ② — no

The two attempts cannot differ, so removing one moves no contract: no export changes, no accepted key or value is added, and applyPublishedSeeds is module-private. The bump argument against the Check Changeset rule text ("a purely additive widening … takes at least minor"): this is a removal that widens nothing, so patch is what the act requires, and the fix commit type does not raise it.

The two sentences in this file now agree

packages/runtime/src/domains/packages.ts:692 (the app flip, repaired by #15063) and the new sentence say the same thing: the predicate answers undefined for every non-overridable type, the read is env-wide by construction, and ⛔ dropping the organization is the repair, not an oversight to undo. One narrowing worth recording against the dispatch's reading: :692 states that of the plural getMetaItems on app, citing #14683 — not of getMetaItem on seed. The file's two sentences were about different verbs, so a reader could believe both were true of their own; they now agree on both verbs.

The class is closed in this package, re-verified with the card's own method: every non-test metadata read call site under ':(glob)packages/runtime/src/**/*.ts' gives 9 (positive control: 18 with test files included), and exactly two comment lines under that pathspec stated the pre-gate rule — both at this site. #14771 / #15063 repaired the other member.

One collateral edit, declared

packages/metadata-protocol/src/protocol.ts — the read gate's own docblock enumerates the runtime callers that hand it a raw active organization and counts them: "FOUR, across two files", with applyPublishedSeeds as one bullet. This change makes that count and that bullet false, so leaving them would author a new member of the very class this card closes. The enumeration now reads THREE, all in one file, with the fourth recorded as having moved into the "names no organization at all" bucket and why. Comment-only, no behaviour, no changeset entry (it publishes nothing). Serial re-measured for this second file: 35/35 open PR heads fetched per-ref at --depth=80 with a computed merge-base on every row — 0 hits on packages/runtime/src/domains/packages.ts, one hit on protocol.ts (#15395) whose hunks are at lines 78/1736/10571+ and do not reach this region.

Verification — all on a400e6ed9e4, the final commit

  • pnpm --filter @objectstack/runtime --filter @objectstack/metadata-protocol test — runtime 3253 passed / 227 files; metadata-protocol 2379 passed, 10 skipped / 164 files.
  • pnpm --filter @objectstack/runtime --filter @objectstack/metadata-protocol typecheck — exit 0. Both script names echoed (a zero-match --filter exits 0 having run nothing). The new test file is really in the program: tsc -p tsconfig.test.json --listFiles names it, and check:test-typecheck reports the ledger held.
  • Gate union, re-derived from the real changed set with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack on a tree the tool does not call stale: 59 families, 59 green. Two were re-run after the full workspace build cleared their PREREQUISITE NOT MET (check:dual-build-cjs-loads, check:type-check-debt — exit 3, recorded as NOT MEASURED, never as a pass). Two were genuinely red on the first sweep and are fixed here: check:engine-double-contract (the new double needed its ledger seams, --write) and check:objectql-double-limit (the find double was limit-blind; it now applies the caller's bound by presence). Re-deriving after the ledger file joined the changed set added 6 more families — all run, all green.
  • ESLint — a declared narrowing, not a skip. Targeted run over the three changed source files: 0 errors, 0 warnings, file count 3 read from --format json, population read from eslint's own --print-config rather than guessed. The invariance that makes the narrowing a measurement: this repo runs one eslint.config.mjs which never enables type-aware linting for any file (eslint.config.mjs:327-329, with its own recorded positive control), so this diff cannot move the verdict on a file it did not touch.

Not done, with reasons


Generated by Claude Code

… org rung

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
… read its gate produces

`applyPublishedSeeds` read each just-published seed body twice when a session
had an active organization — once naming the org, then env-wide. `seed` is
`allowOrgOverride: false` and `getMetaItem` resolves
`organizationIdForMetaRead` once at its top, so both rungs asked the engine the
same predicates and served the same answer.

Measured, not reasoned: an ablation neutering the second rung reddens nothing on
a pinned publish-then-read path, with a positive control that the ladder is
reached and a `view` control proving the comparison does separate the rungs for
an org-overridable type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…ble and pin its seams

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
@github-actions github-actions Bot added the size/l label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/runtime, touching 3 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/runtime/src/domains/packages.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/data-api.mdx (via /:object/export (route, bridged from symbol getMetaItem — its route source's handler names it))
  • content/docs/data-modeling/drivers.mdx (via getMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/data-modeling/schema-design.mdx (via /:object/export (route, bridged from symbol getMetaItem — its route source's handler names it))
  • content/docs/kernel/services-checklist.mdx (via getMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/permissions/permission-sets.mdx (via /:object/export (route, bridged from symbol getMetaItem — its route source's handler names it))
  • content/docs/protocol/objectui/actions.mdx (via /:object/export (route, bridged from symbol getMetaItem — its route source's handler names it))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/runtime/src/domains/packages.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 28 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 ca46f8f128784987f13b3e89dc549e9f4203994epackageMentionDocs.

Which tree this was computed on

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

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

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

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/l tests tooling

Projects

None yet

2 participants