Skip to content

fix(service-settings)!: a host that binds its own engine and states no organization source reads and writes as the organization D9 derives - #25350

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-c4vx2u
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-c4vx2u

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #25249

Clause-②: no (narrowing)
Metadata-protocol: none

Ruling-ref: 6355249103 (letter A). Why Clause-②: no (narrowing): nothing is added to any accept set. SettingsServiceOptions and the bindEngine extras are unchanged. What narrows is which organization a host that binds its own engine, and states no organization source, reads and writes as.

What changed

SettingsService.organizationReachOf (packages/services/service-settings/src/settings-service.ts) now takes ADR-0131 D9's answer from organizationDecision():

  1. the stated organization source, when the host bound one (the plugin always does);
  2. no engine at all (the in-memory store): no-organization-object, as before;
  3. otherwise resolveGuestOrganization(this.engine, posture), the same call the plugin's source makes, through the engine the service holds.

There is no second predicate, no registry-sniffing arm, and no catch around the probe. An engine that exposes no registry (the plugin's own wrapEngineAsSettingsEngine, any hand-written SettingsEngine) cannot say the organization object is absent, so D9 reads it as registered and asks sys_organization. A lean self-binding host declares the composition at its bind site: bindEngine(engine, audit, { organization: async () => ({ kind: 'no-organization-object' }) }), or hands over an engine whose registry.getObject answers.

The retired docblock sentence ("Unset … reads as a composition that registers no organization object") is rewritten on the source type, its field and the bindEngine extra. The index.ts host paragraph gains the declaration line and names the symptom of omitting it. No docs/adr edit.

Also: a changeset (@objectstack/service-settings patch, BREAKING, FROM/TO, the one-line fix).

Branch

The harness assigned the branch claude/project-thread-c4vx2u, not the repo-convention claude/issue-25249-…. This note is for the Duplicate Fix Guard. The card's Claim: (6354964172) names this branch.

Tests

Premises P1–P4 of the ruling were re-read on the branch before the migration: registryHoldsObject answers true with no registry.getObject (core resolve-authz-context.ts:1235); wrapEngineAsSettingsEngine returns find / insert / update only and forwards find verbatim; the plugin's only bindEngine call states a source; the plugin is the only non-test constructor in the repo. All four hold.

Migration (45 tests in 7 files, red on the route alone). Each suite now states its composition truthfully, through a shared testkit (src/__tests__/settings-organization.testkit.ts):

  • a suite over a double states LEAN_ORGANIZATION_SOURCE at its bind: settings-getmany, settings-user-rung-owner.pin, settings-system-context.pin (its constructor-engine case moves to bindEngine with the same crypto provider and secret store);
  • a suite running the real ObjectQL behind the production adapter forwards that engine's registry (withEngineRegistry), so D9 answers no-organization-object itself: settings-secret-rotation, sys-secret-orphan-report, settings-global-rung, settings-user-reference.pin.

No default in the service, no stub getObject, no new input. The four construction sites outside the package pass { env } and no engine; they are untouched.

Pins (settings-organization-reach.pin.test.ts, new section):

  • (a) a registry-less adapter over a real engine holding one sys_organization row, no source: the tenant and user rungs read and write as that organization, and the rows with no organization do not answer;
  • (b) no organization object, no source: on the real engine, AuthzStoreUnavailableError naming sys_organization (pinned as measured; isMissingTableError not widened); on a double whose probe answers nothing, no tenant rung and the tenant write refused (SETTINGS_VALIDATION);
  • (c) a stated lean source: rows with no organization read and written, unchanged;
  • (d) no engine: the in-memory store unchanged;
  • (e) the existing plugin pins keep their assertions.

Ablation (scripts/ablation-replace.mjs on the committed route): the engine branch of organizationDecision replaced by the old lean answer. (a) and both (b) pins went red; (c), (d) and the plugin pins (e) stayed green, the expected direction. Restored: blob equals HEAD, git diff HEAD empty.

Runs on e12143e6 (after merging origin/main):

  • vitest run in packages/services/service-settings: 58 files / 987 tests passed (982 before the new pins).
  • pnpm --filter @objectstack/service-settings typecheck: exit 0.
  • Derived gates, the full dogfood battery (! title) and the contract-review-tier review: reported on the card in the os-dev-report and on this PR.

Out of scope

Hosts outside this repo that bind SettingsService themselves (cloud control-plane mocks, embedded runtimes) are downstream of the published package. The changeset's FROM / TO and its one-line fix are the hand-off; no cloud card is filed from here.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_014FFTcf6msoLbXiD6BR7wHW

…s as the organization D9 derives through the bound engine

Claude-Session: https://claude.ai/code/session_014FFTcf6msoLbXiD6BR7wHW
Co-authored-by: Claude <noreply@anthropic.com>
… organization facts; pin the derivation

The suites that bind their own engine now say what the composition holds:
a double states the lean source at the bind site, and a suite running the
real ObjectQL forwards that engine's registry onto the adapter. Pins: one
organization through a registry-less adapter reads and writes as it; no
organization object is loud on the real engine and zero organizations on a
double; a stated lean source and no engine keep their readings.

Claude-Session: https://claude.ai/code/session_014FFTcf6msoLbXiD6BR7wHW
Co-authored-by: Claude <noreply@anthropic.com>
…rived organization; name the omission's symptom

Claude-Session: https://claude.ai/code/session_014FFTcf6msoLbXiD6BR7wHW
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot requested a review from os-sales October 23, 2026 01:50
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 23, 2026
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

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

Coarse fallback — 9 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 4d7e2b1f384a4cb7226c8f629abdf33fede45cf1 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 4d7e2b1f384a4cb7226c8f629abdf33fede45cf1

⚠️ 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 4d7e2b1f384a4cb7226c8f629abdf33fede45cf1 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1f6cbda31c1abf3b38ee84fe0314800105668bb5
Local-runs: none
Scope: .changeset/25249-settings-self-bound-organization.md, packages/services/service-settings/src/settings-service.ts, packages/services/service-settings/src/index.ts, packages/services/service-settings/src/tests/settings-organization.testkit.ts, packages/services/service-settings/src/settings-organization-reach.pin.test.ts, packages/services/service-settings/src/settings-getmany.test.ts, packages/services/service-settings/src/settings-global-rung.test.ts, packages/services/service-settings/src/settings-secret-rotation.test.ts, packages/services/service-settings/src/settings-system-context.pin.test.ts, packages/services/service-settings/src/settings-user-reference.pin.test.ts, packages/services/service-settings/src/settings-user-rung-owner.pin.test.ts, packages/services/service-settings/src/sys-secret-orphan-report.test.ts, scripts/engine-double-contract.pinned.json + packages/core/src/security/resolve-authz-context.ts (tryFind :351-442, registryHoldsObject :1235-1243, resolveGuestOrganization :1276-1296), packages/core/src/security/system-write-organization.ts (resolveSystemWriteOrganization, the decision type), packages/core/src/security/authz-store-unavailable.ts (AuthzStoreUnavailableError.object :153, isAuthzStoreUnavailableError :175), packages/core/src/security/index.ts (:189, :212 the two exports), packages/services/service-settings/src/settings-service-plugin.ts (init :212-224, the kernel:ready bind :318-346, wrapEngineAsSettingsEngine :887-920), packages/services/service-settings/src/settings-service.types.ts (SettingsEngine :122-150, SettingsServiceOptions :369-424), packages/services/service-settings/src/settings-organization-isolation.pin.test.ts (unchanged, pin (e)), packages/services/service-settings/src/settings-routes.test.ts (:751-752, :844), packages/objectql/src/registry.ts (SchemaRegistry.getObject :4575-4578), packages/objectql/src/engine.ts (get registry() :5261), docs/adr/0131-total-organization-ownership-no-null-organization-id.md (D9 note :706-735, on the head), scripts/check-engine-double-contract.mjs (header), scripts/check-changeset-no-major.mjs (judgeLevel, the no (narrowing) lanes), scripts/check-settings-bind-window.mjs (:1046-1054, a self-test fixture string), content/docs/data-modeling/drivers.mdx, content/docs/protocol/kernel/config-resolution.mdx, content/docs/protocol/kernel/index.mdx (the three pages the drift check names), AGENTS.md Post-Task Checklist 3 (:1085-1098), packages/plugins/plugin-auth/src/auth-plugin.test.ts :580/:587, packages/plugins/plugin-email/src/mail-test-recipient.pin.test.ts :84, packages/services/service-sms/src/capability-arg.config-parity.contract.test.ts :103, packages/services/service-sms/src/sms-test-recipient.pin.test.ts :59 (the four outside construction sites).

Inputs read: card #25249 body, its claim 6354964172, the ruling 6355249103 with execution parameters 1-9, and the Ruling-ref comment 6355476531; the PR body, its 13-file list and the one bot comment (docs drift); git diff 54df640b388c66153ba1e52c9ccc0082293358fa 1f6cbda31c1abf3b38ee84fe0314800105668bb5 (13 files, +252/-29, six commits 3c27c0a7..1f6cbda3); the 40 check-runs on the head, read twice (02:15Z and 02:17Z); PR #25226's contract review 6343868210 and seat check 6344522520 for the R1-R4 reading. Every cited file was read by ref (the head or 54df640b), never from a working tree.

① Derived judgments

1. The route (ruling item 1). organizationDecision() at the head (settings-service.ts :1897-1901) is three lines: the bound organizationSource when set; { kind: 'no-organization-object' } when this.engine is unset; else resolveGuestOrganization(this.engine, await this.tenancyPosture?.()). organizationReachOf (:1872-1880) is unchanged in shape: the caller's own organization first, then the wall (organizationWallStands), then the decision mapped derived → own, no-organization-object → itself, refuse → unowned. There is no second predicate, no registry sniff in the service (the only registry read is core's registryHoldsObject, inside the one call the plugin also makes, :344-345), and no catch anywhere on the path: a thrown probe propagates, as the plugin path leaves it (6343868210 ③ 3a). The wall is asked before the decision, so under group / isolated the probe is never issued; with no posture source the predicate reads single (core :1284 tenancyPosture ?? 'single'), the same reading the plugin's omitted posture gets. Right, and exactly item 1.

2. The public surface (item 2). SettingsServiceOptions at the head (:369-424) has engine, crypto, cryptoProvider, secretStore, audit, auditWriter, env, engineBindPending, objectName, logger and no organization; bindEngine's extras (:907-926) keep the same five keys as on 54df640b (only the organization docblock changes). index.ts gains a comment and no export; SettingsOrganizationSource stays module-level, imported by the testkit from ../settings-service.js. The one new import, resolveGuestOrganization from @objectstack/core, is a dependency the package already carries (the plugin imports it at :23). No new public input; Clause-②: no is earned. The consequence the ruling foresaw holds: a host that hands its engine to the constructor has no place to state a source and must bind through bindEngine instead; the changeset says so in its fix line.

3. The test migration (item 4). Seven files, two ways, matching the ruling's split:

  • Doubles state LEAN_ORGANIZATION_SOURCE at the bind: settings-getmany (:95), settings-user-rung-owner.pin (:103), settings-system-context.pin (:94 and :197-202). The system-context constructor-engine case moves to bindEngine with the same cryptoProvider and secretStore, which extras already accepts (:910-912) — behaviour-equivalent, and the test's assertions (the call sequence and the isSystem context on every call) are untouched.
  • Real-ObjectQL suites forward the engine's registry through withEngineRegistry over the production adapter: settings-global-rung (:155), settings-secret-rotation (:310-317, over all three adapter variants), sys-secret-orphan-report (:278-283), settings-user-reference.pin (:101). The testkit's helper is Object.assign(wrapped, { registry: engine.registry }) — the real SchemaRegistry, whose getObject answers undefined for an unregistered name (registry.ts :4575-4578), and none of the four suites registers SysOrganization (read at the head), so D9 answers no-organization-object from the composition's own fact. No stub getObject with a canned answer, no default inside the service, no new constructor input. The four outside-package construction sites pass { env } or { env: {} } and bind no engine (read at the head) — unchanged, no edit, as item 4 says.

4. The pins (item 5). All in settings-organization-reach.pin.test.ts, a new section after the plugin pins:

  • (a) :310-330 — real ObjectQL, SysOrganization registered with one row, bound through wrapEngineAsSettingsEngine (no registry), no source, no posture. Core reads the adapter as registering the object (P1), probes sys_organization through the adapter's verbatim find, derives ORG; the pin asserts the tenant and user rungs read and write own(ORG), the two NULL rows answer default, and the store's rows are ['brand', ORG, ..], ['legacy', null, ..], ['theme', null, ..], ['theme', ORG, ..]. The derived organization is the one the engine's own insert path would stamp. Right.
  • (b) real :332-340 — same host, no organization object: AuthzStoreUnavailableError with .object === 'sys_organization' (the field exists, authz-store-unavailable.ts :153). Pinned as measured; the diff touches neither isMissingTableError nor tryFind, and the service converts nothing. Right, and the 2026-08-30 boundary stands.
  • (b) double :342-347 — an engine with no registry whose find answers [] for everything but sys_setting: registered-by-default, zero ids, refuse no-organization → unowned; the read answers default, the tenant write is refused SETTINGS_VALIDATION through organizationlessTenantKeyError (:2160-2175). Right.
  • (c) :350-360 — the stated lean source on both the double and the real engine without the object: the NULL row reads, two writes land one NULL row. (d) :362-368 — no engine: the in-memory store's one row has organization_id null (private readonly memory :859). (e) — the four existing describe blocks keep their assertions byte for byte; the diff adds a theme user-scope specifier to the shared MANIFEST (:48) that the plugin pins do not read, and settings-organization-isolation.pin.test.ts is untouched. Right.
  • (f) the ablation is the dev's report (PR body: (a) and both (b) red, (c), (d), (e) green, tree restored); not re-run here by rule.
  • The double's update routes through assertEngineUpdateDispatch, which puts one update row for this file into scripts/engine-double-contract.pinned.json (a grow-only coverage ledger, hand-edit forbidden by its $comment; Lint & Repo Gates success on the head). The pins never reach update (every write is an insert or is refused first), so the row records the double's shape, not a path — as the gate defines it.

5. Docs (item 6). index.ts :81-85 gains the declaration (the bindEngine organization extra answering no-organization-object) and names both symptoms of omitting it (a tenant-rung read or write refused as organization-less; AuthzStoreUnavailableError naming sys_organization). The retired sentence is rewritten on the source type (:203-208), the field (:874-877) and the extra (:921-924). No path under docs/adr/** is in the diff; the D9 note on the head (:706-735) still draws the exception's boundary and nothing here contradicts it. The drift check names three content/docs pages via the SettingsService symbol alone; read at the head, none mentions an organization source, bindEngine, or the lean reading (the drivers.mdx :503 row is about NULL-safe row identity, unrelated), so no page restates what this PR retires and no page edit is owed.

6. Legitimate callers. Plugin path: init constructs the service with no engine and engineBindPending: true (:212-224); the kernel:ready hook binds with tenancyPosture and organization (:337-345), so organizationDecision returns at its first line, exactly 54df640b's source call. The pre-bind window reads as no engine → lean, as before, and writes are refused by the pending flag regardless. The no-objectql branch settles without an engine → unchanged. os serve, os dev and every plugin-bound composition are unchanged. No-engine store: branch two, unchanged. Global keys: scope === 'global' ? UNOWNED : … at :1630 and :1736-1737, so a self-binding host that reads only global keys never triggers the probe — the place ADR-0138 D4 item 3 puts the refusal. Walled hosts: the wall precedes the decision, no probe. The only reading that moves is the ruling's: an unstated self-binding host on a store with no sys_organization behind a registry-less engine, which now meets unowned or the loud read error instead of NULL rows; cured by the one-line declaration or a registry-bearing engine. Repo-wide, outside tests, the only constructor and the only bindEngine caller is the plugin (scripts/check-settings-bind-window.mjs :1052 is a self-test fixture string, not a caller).

7. Premises P1-P4 (item 9), re-read on the head by this review: P1 registryHoldsObject answers true when registry.getObject is not a function (core :1235-1243); P2 SettingsEngine declares find / insert / update only (:122) and wrapEngineAsSettingsEngine returns those three with find forwarded verbatim (:887-920); P3 the plugin's one bindEngine call states a source (:344-345); P4 the plugin is the only non-test constructor (grep over the head's *.ts/*.mts/*.js/*.mjs outside test paths). All four hold.

8. Gate verdicts on the head (40 check-runs, read at 02:17Z): Build Core, TypeScript Type Check, the four Type Check · lanes, Lint & Repo Gates and its eight Lint · lanes, Check Changeset, Governed Surface Queue Guard, Check PR Size, Check Documentation Links, Flag docs affected by code changes, the three claim/branch guards, Auto Label, filter, the Dogfood Regression Gate aggregate: all success. Test Core shards 1/6, 5/6, 6/6 success; shards 2/6, 3/6, 4/6 in_progress at both reads. skipped: 18.x upgrade harness (opt-in), Packed-tarball smoke (opt-in), Dogfood Regression Gate (shard/3), Dogfood Verify CLI, Console Pin Gate, Build Docs, Temporal Conformance. Zero failures.

② Semver level

Changeset .changeset/25249-settings-self-bound-organization.md: '@objectstack/service-settings': patch; summary fix(service-settings)!: …; Clause-②: no (narrowing) and Metadata-protocol: none, bare and line-initial; the adr-0087 comment marker not-required (no-migration-prescription) with its why; the **BREAKING** banner; FROM / TO; who is affected; the one-line fix. The PR body carries the same two declaration lines and the title carries the !.

  • Level. Under ruling A (AGENTS.md Post-Task Checklist 3, :1092-1093: a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner) the level follows the metadata-protocol kind alone; no authorable key, value, type, default or meaning moves here, so none is the right kind and patch the right level. Check Changeset is success on the head.
  • Clause-②: no (narrowing). no is earned (① 2: nothing is added to any accept set, unlike fix(service-settings)!: no tenant- or user-rung settings read admits an organization-less row (ADR-0131 D8) #25226's bindEngine input). (narrowing) is the face: which organization an unstated self-binding host reads and writes as shrinks from "NULL rows on every composition" to D9's answer, and the FROM / TO say so in the ruling's own terms (item 3: the registry-less engine over a store with no organization object is unowned or meets the loud read error; no engine, a stated source, a registry-bearing engine and the plugin are unchanged). No staged window, no dual reading.
  • ADR-0087 marker. Honest on the facts I read: no export, type, option, error code or route is added, removed or renamed; SettingsServiceOptions and the extras are unchanged; the remedy is a one-line bind-site declaration, not a consumer rewrite the migrator could prescribe.

③ Boundary flags

  1. R1-R4 on this head. R1-R3's lane, 18.x upgrade harness (opt-in), is skipped on 1f6cbda3 (no label applied, unlike fix(service-settings)!: no tenant- or user-rung settings read admits an organization-less row (ADR-0131 D8) #25226 where the seat labelled it); R4's dogfood shards and Dogfood Verify CLI are skipped on pull_request events as they were on fix(service-settings)!: no tenant- or user-rung settings read admits an organization-less row (ADR-0131 D8) #25226; no walled boot is reported for this head (the PR body defers the dogfood battery to the os-dev-report). What compensates, judged by code: the only runtime change is organizationDecision's second and third branches, reachable only when no organization source is bound; the plugin binds one on every engine bind (:344-345), so every composition those lanes boot (os serve, os dev, the 18.0 / 18.0.0 fixtures, a walled boot) runs 54df640b's source call unchanged. The compensation holds. Escalated to the seat, not a block: per ruling item 8 the seat either applies the harness label on this head or names the compensating main battery in the ACCEPT, and the ! title's dogfood battery is the dev's to report.
  2. Test Core shards 2/6, 3/6, 4/6 were in_progress at both reads (shards 1, 5, 6 success). Their conclusions are the gate verdicts; the seat lands on the finished set. Nothing in the diff lies outside packages/services/service-settings and scripts/engine-double-contract.pinned.json, and the package's own suite is the dev's reported 58 files / 987 tests green on e12143e6 (unverified here, by rule).
  3. Dev flags, answered. The branch-name note (claude/project-thread-c4vx2u instead of the issue-slug convention): the three claim/branch guards are success, and the claim 6354964172 names the branch. The ablation (item 5 f) is dev-reported; its direction matches what the code implies ((c), (d), (e) do not reach the engine branch). External hosts (item 7): the changeset's FROM / TO and fix line are the hand-off; no cloud card, as ruled. open_questions: none listed on the PR; none found open here.
  4. Docs drift names three pages by the class symbol and reports index.ts as unanchored; read by hand (① 5), none restates the retired sentence. No edit owed; the ruling's "only if the drift check names one" is satisfied by that reading.
  5. Observations outside the surface, no action owed on this PR. (i) A self-binding host now pays one sys_organization probe per no-organization read of a non-global key and per setMany batch, as the plugin path already does (6343868210 ③ 4); a cost, not a correctness question. (ii) The testkit's withEngineRegistry mutates the adapter object in place; test-only, and the adapter is built fresh per boot in every suite. (iii) 6343868210 ③ 3a (resolveLocalizationContextUncached's direct fallback) is unchanged by this diff and remains its own card's.

Implemented-by: claude/project-thread-c4vx2u
Reviewed-by: session_014FFTcf6msoLbXiD6BR7wHW

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-23T03:25Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25249 (claim 6354964172, Ruling-ref: 6355249103 letter A; os-dev-report 6356026070; card session branch claude/project-thread-c4vx2u) · reviewed head 1f6cbda31c1abf3b38ee84fe0314800105668bb5

  • Shape: when no organization source is bound, SettingsService now asks ADR-0131 D9 itself, through the engine it holds, with the same call the plugin makes (resolveGuestOrganization), and fails closed. That means a self-binding host that wants the lean composition states it (bindEngine(…, { organization })). A stated source and plugin-bound compositions (os serve, os dev) are unchanged. SettingsServiceOptions and the bindEngine extras are unchanged, so nothing is added to any accept set. 13 files, +252 / −29.
  • Contract review: CONTRACT_REVIEW_TIER, isolated, read-only. Record 6355870258 is on this head: PASS, Local-runs: none. It confirms the route is exactly ruling item 1 (no second predicate, no registry sniff in the service, no catch on the path), Clause-②: no (narrowing) with FROM / TO, patch with the BREAKING banner, and the ADR-0087 marker. Every change narrows what an unstated host reads as, and no boundary is relaxed (lane floor checked).
  • Boundary flag ③ 1, resolved by the seat as ruling item 8 allows: the seat applied needs:upgrade-harness to this head through the relay. 18.x upgrade harness (opt-in) then ran on 1f6cbda3 and concluded success (02:41Z), which covers R1–R3. For R4, the full dogfood battery is in the report (289 files / 2491 tests, VERDICT 0). The review's code-path reading also holds: the plugin binds a source on every engine bind, so a walled boot runs the unchanged source call.
  • Pins and ablation (report): pins (a) and both (b) in settings-organization-reach.pin.test.ts, red under ablation. 45 tests in 7 files now state their composition through one testkit. No service default and no stub getObject, and the four construction sites outside the package are untouched. service-settings passes 987 / 987.
  • Gates: dispatch-gates --ran 76 / 76. CI on this head: 36 success and 10 expected skips, none failed or pending, mergeable clean. It merges cleanly onto today's main.
  • Deviation accepted: one tool-written row in scripts/engine-double-contract.pinned.json (check-engine-double-contract --write), the gate's ledger for the new pin's double.
  • Out-of-scope finding (hosts outside this repo that bind SettingsService themselves): Acceptance notes. The published changeset's FROM / TO and one-line fix are the hand-off, per ruling item 7, and no card is owed here.
  • Landing: not governed (check-governed-merges --pr 25350: 281 lines). Ready + auto-merge through the relay.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 23, 2026 03:26
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 23, 2026 03:26
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 23, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 23, 2026
…-c4vx2u

# Conflicts:
#	packages/services/service-settings/src/settings-service.ts
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4b63a5dd714e75ce46791a0cbd256675081000da
Local-runs: none
Scope: .changeset/25249-settings-self-bound-organization.md, packages/services/service-settings/src/settings-service.ts, packages/services/service-settings/src/index.ts, packages/services/service-settings/src/tests/settings-organization.testkit.ts, packages/services/service-settings/src/settings-organization-reach.pin.test.ts, packages/services/service-settings/src/settings-getmany.test.ts, packages/services/service-settings/src/settings-global-rung.test.ts, packages/services/service-settings/src/settings-secret-rotation.test.ts, packages/services/service-settings/src/settings-system-context.pin.test.ts, packages/services/service-settings/src/settings-user-reference.pin.test.ts, packages/services/service-settings/src/settings-user-rung-owner.pin.test.ts, packages/services/service-settings/src/sys-secret-orphan-report.test.ts, scripts/engine-double-contract.pinned.json + packages/core/src/security/organization-reach.ts (whole file at origin/main: resolveOrganizationReach, organizationIdOf, organizationReachRowOwner, organizationReachAdmits, UNOWNED_ORGANIZATION_REACH), packages/core/src/security/resolve-authz-context.ts at origin/main (tryFind :359-451, registryHoldsObject :1236-1244, resolveGuestOrganization :1277-1296, localizationFallbackReach as #25352 rewrote it), packages/services/service-settings/src/settings-service-plugin.ts at the head (kernel:ready bind :318-346, wrapEngineAsSettingsEngine :887-920), packages/services/service-settings/src/settings-service.types.ts at the head (SettingsEngine :122, SettingsServiceOptions keys), packages/services/service-settings/src/settings-organization-reach-memory.pin.test.ts (new on main from #25352, whole file), the #25352 commit b3034eeffa (stat and its settings-service.ts / resolve-authz-context.ts hunks), the merge commit 4b63a5dd itself (git show --cc).

Inputs read: card #25249 body, claim 6354964172, ruling 6355249103 (items 1-9), Ruling-ref 6355476531, os-dev-report 6356026070, the epic PM lane note 6356760330; PR #25350 body, 13-file list, the drift-check comment, the earlier record 6355870258 on 1f6cbda3 and the seat's ACCEPT 6356644152; git diff b964ea17 4b63a5dd (the net diff: merge-base with origin/main is b964ea17, 13 files, +252/-29, the same list and counts the PR reports); git show --cc 4b63a5dd (one conflicted file, two hunks); git diff 1f6cbda3 4b63a5dd -- packages/services/service-settings (two files: settings-service.ts and the new memory pin, both from main); the 40 check-runs on 4b63a5dd, read twice. Every file read by ref, never from a working tree. origin/main has moved two commits past the merge base (4d7e2b1f, fb63d396, both refactors inside plugin-security / plugin-auth, no path in service-settings or core security), so the net diff against today's main is the same 13 files.

① Derived judgments

1. The merge delta is mechanical and the route is ruling item 1, unchanged. The merge touched one file with conflicts, settings-service.ts, in two hunks, and the combined diff (--cc) shows exactly two lines that are neither parent's: resolveGuestOrganization, added inside main's multi-name @objectstack/core import, and a deriveOrganization callback that calls this.organizationDecision(), in place of main's inline callback (the bound source when set, else { kind: 'no-organization-object' }). Everything else in the file is one parent's text verbatim: the branch's organizationDecision() (:1899-1903: stated source first; no engine is { kind: 'no-organization-object' }; else resolveGuestOrganization(this.engine, await this.tenancyPosture?.())) and its three rewritten docblocks, and main's resolveOrganizationReach call, organizationReachAdmits / organizationReachRowOwner / organizationIdOf uses and removed local type. The net diff against main for this file is therefore +33/-9 and contains only what the branch meant: the one import name, three docblocks, the callback, the method. No catch, no second predicate, no registry sniff was introduced; the only registry read on the path is still core's registryHoldsObject inside the one call the plugin makes.

2. main's resolveOrganizationReach maps decisions exactly as the branch's inline code did. Read at origin/main (organization-reach.ts :86-93): organizationIdOf(facts.callerOrganizationId) first (typeof value === 'string' && value !== '', the same normalization the branch's removed callerOrganizationIdOf(ctx) applied to ctx.tenantId, and the service passes ctx.tenantId); then await facts.wallStands() → UNOWNED_ORGANIZATION_REACH; then await facts.deriveOrganization() with derived → own, no-organization-object → itself, anything else → unowned. Same four steps, same order, lazy: the wall is asked only for a caller naming none, and the derivation only where no wall stands, so under group / isolated the probe is never issued, as before. A throw from either fact propagates (OrganizationReachFacts docblock says so; the function has no try). wallStands is a callback to this.organizationWallStands(), whose behaviour is untouched (no source → false; a throw is not caught). Net: organizationReachOf on the new head answers byte-for-byte what it answered on 1f6cbda3 for every (caller, wall, decision) triple.

3. The D9 predicate main hands the engine to is the one the earlier record judged. tryFind, registryHoldsObject and resolveGuestOrganization at origin/main are identical to their text at the old base 54df640b (compared function-by-function; #25352's hunks on resolve-authz-context.ts touch grantAnchorsPlatformStanding, localizationFallbackReach and the fallback's row filter only). So P1 holds on main (:1237-1238: no registry.getObject function → true), the probe is still tryFind(ql, 'sys_organization', {}, 2) with its isMissingTableError-only quiet branch, and pin (b) real's measured AuthzStoreUnavailableError is still what main produces. main's localization fallback now routes through the same resolveOrganizationReach with its own catch → UNOWNED_ORGANIZATION_REACH (R-2, "kept by design"); that catch is in core's fallback, not in the service, and the service's path stays uncaught, as item 1 requires.

4. Docblocks: no contradiction survives. Grep of the head's settings-service.ts for "Unset": the only hits are the branch's rewritten sentences (:202-207 on SettingsOrganizationSource, :874-875 on the field, :922-923 on the extra) and the untouched posture-field sentence (:867-869, about the wall, not the organization). main's new block comment above UNOWNED (:156-176) and its organizationReachOf docblock addendum (:1872-1876, the "kept by design" R-1/R-2 note) describe the rule and the fact sources; neither says an unset source reads as no organization object. One sentence is now under-specified rather than wrong: :175 "D9's decision from its organization source" (the decision now comes from the source or, unset, from the engine); organizationReachOf's own docblock links SettingsOrganizationSource, whose text carries the unset reading. Noted in ③ 4, not a block.

5. Nothing else in the branch's surface moved through the merge. git diff 1f6cbda3 4b63a5dd -- packages/services/service-settings lists two files only: settings-service.ts (above) and main's new settings-organization-reach-memory.pin.test.ts. The testkit, the nine test files, index.ts's host paragraph, settings-service.types.ts, settings-service-plugin.ts and the changeset are byte-identical to 1f6cbda3 (the net diff's hunks for them equal the PR's file-list patches, read side by side). scripts/engine-double-contract.pinned.json carries the branch's one row for the reach pin's double plus main's rows, no collision (Lint & Repo Gates success on the head). SettingsServiceOptions still has no organization; the bindEngine extras are unchanged; the plugin's one bind still states tenancyPosture and an organization source that calls resolveGuestOrganization(engine, …) with the admission posture (:337-345), so every plugin-bound composition returns at organizationDecision's first line. The only non-test constructor and bindEngine caller at the head is the plugin (:212, :318). Premises P1-P4 re-read on the new head: all hold.

6. #25352's new pin is consistent with this route. settings-organization-reach-memory.pin.test.ts constructs new SettingsService({ env: {} }), registers a manifest and binds no engine; its two cases exercise the in-memory store's row reach. On this head that is organizationDecision's second branch (no engine → no-organization-object), which the PR keeps and pin (d) also asserts. Its docblock phrase "a caller with no organization, on a service with no organization source, writes as the lean arm" is true for the store it tests because no engine is bound; the sentence names the source where the engine is what now decides. Advisory, ③ 4.

7. Pins, tests, docs: as the earlier record verified. Record 6355870258 ① 3-7 (migration of 45 tests two ways, pins (a)-(e), index.ts declaration line, the three drift-named pages, the outside construction sites) stands unchanged: none of those files changed between the heads, and the only runtime input they depend on, resolveGuestOrganization and its two helpers, is identical on main. The ablation (f) remains dev-reported, not re-run here.

8. Gate verdicts on 4b63a5dd (40 check-runs, read twice; latest completion seen 04:21:52Z): 31 success, among them Build Core, TypeScript Type Check, the four Type Check · lanes, Lint & Repo Gates and its eight Lint · lanes, Check Changeset, Governed Surface Queue Guard, Check PR Size, Check Documentation Links, Flag docs affected by code changes, the three claim/branch guards, Auto Label, filter, the Dogfood Regression Gate aggregate, Test Core shards 1/6, 5/6, 6/6, and 18.x upgrade harness (opt-in) (success, completed 04:10:26Z, the needs:upgrade-harness label persisting onto this head). Test Core shards 2/6, 3/6, 4/6 in_progress at both reads. 6 skipped: Packed-tarball smoke (opt-in), Dogfood Regression Gate (shard/3), Dogfood Verify CLI, Console Pin Gate, Build Docs, Temporal Conformance. Zero failures.

② Semver level

The changeset .changeset/25249-settings-self-bound-organization.md is byte-identical to the one reviewed at 1f6cbda3: '@objectstack/service-settings': patch; fix(service-settings)!: summary; Clause-②: no (narrowing) and Metadata-protocol: none bare and line-initial, matching the PR body; the adr-0087 marker not-required (no-migration-prescription) with its why; the **BREAKING** banner; FROM / TO; who is affected; the one-line fix. Check Changeset is success on this head. The judgement of 6355870258 ② is re-affirmed on the new head with one added fact: #25352 declared Clause-②: yes (widening) for the core exports it added, and this PR adds no use of them beyond what the merge carried from main, so nothing in this PR's net diff widens any accept set. no is earned (no new public input), (narrowing) is the face (which organization an unstated self-binding host reads and writes as shrinks to D9's answer), patch with the banner follows AGENTS.md Post-Task Checklist 3 for a non-protocol breaking change, Metadata-protocol: none is true (no authorable key moves).

③ Boundary flags

  1. R1-R4 on this head. R1-R3: 18.x upgrade harness (opt-in) ran on 4b63a5dd and is success (the seat's label carried over; both 18.x fixture legs boot through the plugin). R4: the dogfood shards and Dogfood Verify CLI are skipped on pull_request events, as on the old head, and no walled boot is reported for this head; the lane note 6356760330 asks for R1 and R4 "on the final head". What is measured: the dev's full dogfood battery on e12143e6 (289 files / 2491 tests, VERDICT 0; the later branch commits touched only service-settings tests and the ledger) and refactor(core, plugin-auth, plugin-security, service-settings): one definition each for the admin_full_access anchor rule and the organization-reach rule, exported from core security #25352's own R1 and R4 walled os serve boots (isolated and group) on its merged head 264c7ef59f, i.e. on the main side of this merge. What is not measured: the two sides together. Judged by code, the compensation holds for the same reason as before: the only runtime change this PR brings is organizationDecision's second and third branches, reachable only when no organization source is bound, and the plugin binds one on every engine bind (:344-345), so a walled boot runs main's source call and main's resolveOrganizationReach exactly as refactor(core, plugin-auth, plugin-security, service-settings): one definition each for the admin_full_access anchor rule and the organization-reach rule, exported from core security #25352 measured them. Escalated to the seat, not a block: per ruling item 8 the seat either names the compensating battery (the two above) in its ACCEPT of this head or measures R4 on 4b63a5dd.
  2. Test Core shards 2/6, 3/6, 4/6 in_progress at both reads. Their conclusions are the gate verdicts; the seat lands on the finished set. No path in the net diff lies outside packages/services/service-settings and the engine-double ledger, and shards 1, 5, 6 are green.
  3. Merge, not rebase. The head is a merge commit; the net diff against main is the review object and is the branch's 13 files with no stowaway hunk (① 1, 5). The two resolved hunks are the smallest resolution possible (one import name, one callback), and no code from either parent was dropped: main's organizationReachRowOwner / organizationReachAdmits uses in the write owner, withinReach and loadScopedRows are present verbatim.
  4. Observations, no action owed on this PR. (i) settings-service.ts :175 ("D9's decision from its organization source") and the memory pin's docblock ("on a service with no organization source") are main's sentences that now under-describe the unset case; both are true for the shapes they describe (the plugin path; the no-engine store) and neither restates the retired "unset reads as no organization object" reading. A later touch of either file could say "from its source, or through its engine". (ii) 6355870258 ③ 5's three observations (one probe per no-organization read of a non-global key; the testkit's in-place Object.assign; the fallback's own catch being core's, not the service's) stand; the third is now explicit in organization-reach.ts's header and main's R-2 note.
  5. Dev flags, answered. Branch name: the three claim/branch guards are success on this head. External hosts: the changeset's FROM / TO and fix line are the hand-off, no cloud card, as ruled (item 7). Deviation (the tool-written ledger row): accepted by the seat on the old head and unchanged. open_questions: none on the card or PR; none found here.

Implemented-by: claude/project-thread-c4vx2u
Reviewed-by: session_014FFTcf6msoLbXiD6BR7wHW

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT(新 head)· 2026-10-23T04:49Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25249 (claim 6354964172, Ruling-ref: 6355249103 letter A; os-dev-report 6357630080, superseding 6356026070) · reviewed head 4b63a5dd714e75ce46791a0cbd256675081000da. This supersedes the seat's ACCEPT 6356644152 on 1f6cbda3.


Generated by Claude Code

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

Labels

Projects

None yet

2 participants