Skip to content

fix(service-settings): a host that binds SettingsService without an organization source reads as the lean composition (NULL rows) on a store that registers sys_organization — derive from the bound engine, or refuse #25249

Closed

Description

@objectstack-fleet

Filing gate ③: a finding of the contract review of PR #25226 (#25202; record 6343868210, ③ 3b). It does not block that PR.

  • Who acts: the shared queue.
  • Dedupe: none.

Part of #15212.

What is on main after #25226

  • SettingsService.organizationReachOf answers no-organization-object (NULL rows read and written) when no organization source is bound (settings-service.ts, about :1872 at 576b8283).
  • The in-repo production binder, SettingsServicePlugin, always binds one.
  • But index.ts documents hosts that bind their own engine ("control-plane mocks, embedded runtimes"). Such a host on a composition that registers sys_organization keeps a NULL arm, and keeps the duplicate-insert shape fix(service-settings)!: no tenant- or user-rung settings read admits an organization-less row (ADR-0131 D8) #25226 fixed.

Wanted

  • When the source is unset, the service derives the organization from the bound engine (the same resolveSystemWriteOrganization probe), or refuses to bind without one on a composition that registers sys_organization.

Acceptance

  • A self-binding host on a sys_organization composition reads and writes the derived organization, or is refused at bind. This is pinned.
  • The lean composition is unchanged.

Activity

  1. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: card thread of the Sales project's fleet director (2026-10-23T01:10Z)
    Session: session_014FFTcf6msoLbXiD6BR7wHW
    Account: os-sales
    Branch: claude/project-thread-c4vx2u
    Worktree: objectstack-issue-25249
    Domain: domain:services
    Seat: domain:services#1
    File surface: packages/services/service-settings/src/settings-service.ts (organization reach, bind), the service-settings tests whose mock engines model the lean composition, and one .changeset/*.md (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default — dispatch-gates --tier: no path-derived mandate for packages/services/service-settings/src/settings-service.ts, effort: high
    Clause-②: no (narrowing)
    Metadata-protocol: none
    Responsibility: service-settings SettingsService.organizationReachOf: an unset organization source reads as the no-organization-object arm (NULL rows) | the in-repo binder SettingsServicePlugin always supplies a source, so os serve is covered; a host that constructs or binds the service itself is not | a host that binds its own engine (index.ts names control-plane mocks and embedded runtimes); no in-repo caller outside tests
    Thread-read: none
    Serial constraints cleared: none — no open PR names service-settings, and no pm:dispatched card's declared surface includes it (the last change to the file, PR 25226, is merged)

    Planned route (the card offers "derive from the bound engine, or refuse"; this takes derive). With no organization source stated, the service asks ADR-0131 D9's predicate itself, through the engine it holds, by the same resolveGuestOrganization call the plugin makes. An engine that exposes no registry is read the way that predicate reads it: the organization object is taken as registered (fail closed), so a host that means the lean composition states it with an explicit no-organization-object source. A service with no engine (the in-memory store) keeps the lean reading. That narrows what an unstated host reads and writes, hence (narrowing): a ! title, the full dogfood battery, and a contract-review-tier review before the queue.

  2. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: needs_decision fork review (project review thread 「#25249 组织推导裁决」) · letter A · maintainer 「对于这种类似的问题,能否你自己开一个单独的 fable thread 做评审,对于自己有把握决定的问题,自己决定,不能决定的开决策卡?」 2026-10-23T01:33Z

    Proxy ruling under the maintainer's delegation quoted above (carried into this project by its owner's standing rule 「卡线程报 needs_decision 时,另开 fable 裁决线程,有把握就裁并把裁决落在 GitHub 上,没把握才给 Bill 发决策卡」), by the review thread's session session_0157eFnRDZbbD82agNfmyW9r (project thread), on the domain:services seat 1 card thread's needs_decision fork (claim 6354964172, session session_014FFTcf6msoLbXiD6BR7wHW, branch claude/project-thread-c4vx2u at 3c27c0a, one file changed, no PR yet). Marker: auto-adjudicated. ⚠️ Reversible on one word from the maintainer; nothing here forecloses that.

    The question

    The card (filed from PR #25226's contract review, record 6343868210 ③ 3b) wants a host that binds SettingsService with no organization source to 「derive from the bound engine, or refuse」, with the lean composition unchanged. The card thread measured the obstacle: the service holds the narrow SettingsEngine (find / insert / update, settings-service.types.ts:122), the plugin's own wrapEngineAsSettingsEngine forwards those three and nothing else (settings-service-plugin.ts:893-920), so the service cannot ask a registry whether sys_organization is registered — and ADR-0131 D9's predicate, asked through such an engine, reads a registry that cannot answer as 「registered」. The forks:

    • A (the card thread's recommendation, on the branch) — with no source stated, the service asks D9 itself through the engine it holds, by the one call the plugin makes (resolveGuestOrganization), fail closed: an engine that exposes no registry is read as registering the organization object, so a self-binding host that means the lean composition states it (bindEngine(…, { organization }) answering no-organization-object). No engine keeps the lean reading. Plugin-bound compositions (os serve, os dev) are unchanged.
    • B — derive only when the engine exposes a registry; a registry-less engine keeps today's NULL reading. Not breaking; the repo's own wrapper shape, the one the card names, stays unfixed.
    • C — with no source and an engine, refuse at bind. Loudest; every unstated self-binding host fails at startup, and the constructor path needs a new input.

    Governing text, read at origin/main 97967a9

    • ADR-0131 D9 and its note (ruling 6328987873 on [Decision] A production os serve that composes no organization object (no auth secret, or no auth) still writes ownerless rows under --no-server: refuse that boot too, or refuse such writes in the engine everywhere #25133, letter A): 「The rule above applies where an organization can exist. When the composition registers no sys_organization, there is none to derive … the exception is deliberate」; 「the exception holds only for programmatic embedding and --dev」; not taken there: 「letter B, the engine refusing every organization-less write, which would withdraw this exception」. ADR-0131 D8: no layer has a NULL arm.
    • packages/core/src/security/system-write-organization.ts, resolveSystemWriteOrganization, on its organizationObjectRegistered input: 「It is a required input, not a defaulted one: a caller that does not know the answer must not be handed the permissive reading by omission.」
    • packages/core/src/security/resolve-authz-context.ts:1235-1243, registryHoldsObject: 「A registry that cannot answer is not an answer — true, so the read is issued and decides」; :1276-1296, resolveGuestOrganization: organizationObjectRegistered = !!ql && typeof ql.find === 'function' && registryHoldsObject(ql, 'sys_organization'), probe tryFind(ql, 'sys_organization', {}, 2); tryFind :351-440: no engine answers [], a thrown read answers [] only under isMissingTableError(err, object) and otherwise raises AuthzStoreUnavailableError (the 2026-08-30 loudness ruling quoted there).
    • ADR-0138 D4 item 2, as guest-organization.ts and the resolveGuestOrganization docblock carry it: the guest step is bound to the engine's predicate, 「never a second spelling of it」.
    • PR fix(service-settings)!: no tenant- or user-rung settings read admits an organization-less row (ADR-0131 D8) #25226 (279fdcf469, contract review PASS 6343868210): the reach table — 「names none, composition registers no organization object (lean embedding; ruled kept) → no-organization-object」; its ③ 3b: 「SettingsService.bindEngine without an organization source reads as the lean composition (NULL rows) on any composition … Suggest the service derive from the bound engine when the source is unset, or refuse.」 Its ② records why that PR declared Clause-②: yes (narrowing): bindEngine gained the optional organization input.
    • settings-service.ts:200-206 (the sentence this card retires): 「Unset — a service constructed directly — reads as a composition that registers no organization object: nothing names an organization to it.」 index.ts:78: a host that binds its own engine (「control-plane mocks, embedded runtimes」). In-repo constructors outside tests: one, the plugin (settings-service-plugin.ts:212), which always binds a source (:344-345). SettingsServiceOptions (settings-service.types.ts:370-424) takes engine and no organization.
    • .claude/skills/pm-dispatch/SKILL.md 〈升级与决策〉: 「具名不升级类(立即行动):恢复不变量」; 「既有门禁内部参数与盲区修复(加强,非削弱,非新增)」; 「契约收紧(严格 schema、publish 时响亮拒绝)优于消费端宽容(?? 回退、静默容错)」; 「过渡也从紧 —— 创业阶段不渐进」; 「协议为基准:spec 与代码不一致默认改代码对齐」.

    Ruling: A

    Why not B. B writes, as a third arm of the service, exactly the reading D9's predicate refuses to take by omission: 「registry invisible ⇒ lean」. For the one shape the card names (the repo's own wrapper, and every hand-written SettingsEngine a host must write because IDataEngine.update has another signature) B changes nothing, so neither half of the acceptance line — 「reads and writes the derived organization, or is refused」 — is met. It is the consumer-side tolerance the four axes rank last: a host on a sys_organization store keeps writing NULL rows until the driver's NOT NULL (7b-0) or the identity index refuses them, which is the #25226 shape again.

    Why not C. The service cannot see the registry, so 「refuse at bind on a composition that registers sys_organization」 can only be spelled 「refuse every unstated bind that carries an engine」 — wider than the acceptance, since it refuses the host D9 could have served, and it discards the route the card names first. It also needs a new constructor input (SettingsServiceOptions has none), a widening beside a narrowing. The loudness C buys, A already has, and at the place ADR-0138 D4 item 3 puts the refusal: where the organization matters (the first tenant-rung read or write by a caller naming none), not at boot for a host that reads only global keys.

    Why A is inside the text, and this lane's to carry. A restores the invariant (恢复不变量): D8's 「no NULL arm」 and D9's 「required input, not defaulted」 now hold on the self-binding path as #25226 made them hold on the plugin path. It aligns the consumer with the protocol's own predicate, one spelling (resolveGuestOrganization, the call the plugin already makes), no probe of the service's own. It is a narrowing with one revert: who may read shrinks (an unstated host on a sys_organization store stops reading NULL rows it can no longer write there), nothing widens, no gate changes, no capability moves, no registered contract entry is rewritten — the sentence retired is a settings-service.ts docblock from #25226 that the same PR's contract review flagged as the residual. The lean exception of the D9 note is kept, not withdrawn (A is not #25133's letter B): the plugin path, a service with no engine, a stated source, and an engine whose registry.getObject answers (the real ObjectQL exposes get registry(), engine.ts:5261) all keep NULL rows; what moves is one shape — a registry-less wrapper over a store with no sys_organization object, with nothing stated — and it moves to a loud answer, cured by one line. Precedent for a narrowing ! PR under this delegation: 6345869077 on #25095.

    Fixed here, so the dev opens no second decision:

    1. Route. A as on 3c27c0a: organizationDecision() answers the stated source first; no engine is the lean reading; otherwise resolveGuestOrganization(this.engine, await this.tenancyPosture?.()). ⛔ No second predicate, no registry sniffing arm (that is B), and no catch in the service that turns a thrown probe into the lean reading — the plugin path leaves that throw uncaught by design (6343868210 ③ 3a).
    2. Surface. No new public input: SettingsServiceOptions gains no organization, bindEngine's extras are unchanged. The declaration a lean self-binding host makes is bindEngine(engine, audit, { organization: async () => ({ kind: 'no-organization-object' }) }), or an engine whose registry.getObject answers. Hence Clause-②: no (narrowing) (unlike fix(service-settings)!: no tenant- or user-rung settings read admits an organization-less row (ADR-0131 D8) #25226, nothing is added to the accept set), Metadata-protocol: none, changeset patch for @objectstack/service-settings with the BREAKING banner (AGENTS.md Post-Task Checklist 3: a non-protocol breaking change ships patch), ADR-0087 marker expected not-required (no-migration-prescription) — unverified, the gate decides.
    3. Changeset migration text. FROM: a host that binds or constructs the service with an engine and states no organization source read and wrote sys_setting rows with no organization on every composition. TO: it reads and writes as the organization ADR-0131 D9 derives through that engine; over a store with no sys_organization object and an engine that exposes no registry, a caller naming none is unowned (tenant writes refused, reads from the global rung or default) or meets the predicate's loud read error. Who: only a host that binds the service itself — in this repo none in production; os serve, os dev and every plugin-bound composition are unchanged. The one-line fix for a lean host is the declaration in item 2. ⛔ No staged window, no dual reading (「过渡也从紧」).
    4. Tests. Every service-settings test that modelled the lean composition through a registry-less engine with no source says so truthfully, one of two ways: the stated source at its bind (a shared constant is fine), or — where the test runs a real ObjectQL behind the wrapper — the real engine's registry forwarded on the wrapped engine, which is the composition's own fact and lets D9 answer no-organization-object itself. ⛔ Not a default inside the service, not a stub getObject with a canned answer, not a new constructor input. A test that models a sys_organization composition gives its mock a sys_organization row and asserts the derived organization. Measured by this ruling on origin/main 97967a9 with the branch's one file applied: 7 files / 45 tests red of 58 / 982 (baseline on 97967a9: 58 / 982 green) — settings-secret-rotation.test.ts 15, settings-user-rung-owner.pin.test.ts 15, settings-getmany.test.ts 4, sys-secret-orphan-report.test.ts 4, settings-global-rung.test.ts 3, settings-system-context.pin.test.ts 2, settings-user-reference.pin.test.ts 2; the card thread's reading is confirmed. 22 of the 45 run a real ObjectQL behind the repo's wrapper and raise AuthzStoreUnavailableError from tryFind (the probe meets OBJECT_NOT_FOUND for sys_organization, which isMissingTableError does not recognise); the rest run mocks whose probe answers [] and read unowned (defaults answer, tenant writes refused with the per-organization sentence). The four files outside the package that construct the service (auth-plugin.test.ts:580,587, mail-test-recipient.pin.test.ts:84, capability-arg.config-parity.contract.test.ts:103, sms-test-recipient.pin.test.ts:59) pass { env } and bind no engine: unchanged, no edit.
    5. Pins (the card's 「This is pinned」; service-settings, mock engine unless said): (a) a self-binding host, registry-less wrapper over a store holding one sys_organization row, no source, caller names none, no posture or single: tenant and user rung read and write own(that organization); (b) the same host over a store with no sys_organization object, no source: loud — with a mock whose probe answers [], unowned (the tenant write refused with the existing per-organization sentence, the read answering the global rung or default); behind a real ObjectQL with no such object, AuthzStoreUnavailableError from tryFind (measured here: the engine's OBJECT_NOT_FOUND is not a missing-table code) — pinned as measured, ⛔ not converted in the service and ⛔ isMissingTableError not widened (the 2026-08-30 ruling quoted beside it); (c) a stated lean source: NULL rows, unchanged; (d) no engine: unchanged; (e) the existing plugin pins (settings-organization-reach.pin.test.ts, settings-organization-isolation.pin.test.ts) keep their assertions; (f) ablation — the engine branch of organizationDecision removed — (a) is red.
    6. Docs. The docblocks on the branch stand; index.ts:78's host paragraph gains the declaration line and names the symptom of omitting it (a tenant-rung read or write by a caller naming none that is refused as organization-less, or that raises AuthzStoreUnavailableError naming sys_organization); the changeset carries the same two sentences. ⛔ No docs/adr/** edit: the D9 note already draws the exception's boundary and nothing here contradicts it. A content/docs page only if the docs-drift check names one (unverified).
    7. External hosts. Cloud control-plane mocks and embedded runtimes are downstream of the published package; the changeset's FROM / TO and one-line fix are the hand-off. This project files no cloud card (references/cross-repo-coordination.md: a single-lane repo this project cannot reach; splitting by repo is the director's call) — the card thread's report lists the hand-off under out_of_scope_findings so the director can route one pointer if wanted.
    8. Review. CONTRACT_REVIEW_TIER from an isolated reviewer before the queue (narrowing face, as fix(service-settings)!: no tenant- or user-rung settings read admits an organization-less row (ADR-0131 D8) #25226), the full dogfood battery on the final head (! title), R1–R4 as fix(service-settings)!: no tenant- or user-rung settings read admits an organization-less row (ADR-0131 D8) #25226's seat check measured them (a walled boot measured, or the compensating main battery named). Hand-back to domain:services seat 1 ([PM seat] domain:services — 🟢 os-sales #6021) for ACCEPT.
    9. Premises, falsifiable, re-verified on the branch's base before the test migration is written (all read true by this ruling at 97967a9): P1 registryHoldsObject answers true when registry.getObject is not a function (core :1235-1243); P2 SettingsEngine and wrapEngineAsSettingsEngine expose no registry and forward find verbatim; P3 the plugin binds a source on every engine bind (:344-345); P4 the plugin is the only non-test constructor in the repo. If one fails, stop and report the fork; ⛔ do not fall back to B, and do not add the constructor input (C).

    Why A, on the four axes (business standpoint)

    • ① Long-term soundness (weight ≥50%). Two years out the platform has one answer to 「which organization does a caller naming none act in?」: D9's predicate, asked once per layer through the engine that layer holds, with the lean composition a thing a host says rather than a thing a layer assumes. A puts the settings service on that answer with zero private defaults; B keeps a private default and names it 「the registry was not visible」; C answers the question by refusing to ask it.
    • ② Measured business pull. In-repo production hosts: none (the plugin binds a source). The affected class is documented (index.ts:78: control-plane mocks, embedded runtimes) and today, on a sys_organization store, its NULL write fails at the driver's NOT NULL or duplicates on a nullable table — the fix(service-settings)!: no tenant- or user-rung settings read admits an organization-less row (ADR-0131 D8) #25226 defect with one more door. Pull is the correctness of a named host class, not a user report; p3 stays.
    • ③ AI-error prevention. An agent that wires SettingsService itself gets the derived organization without knowing D9 exists. An agent that wires a lean composition and forgets the declaration meets, on its first tenant write, the sentence that names the organization it did not supply — not a NULL row a ceremony reports months later (B), and not a boot refusal on a host that only ever reads global keys (C).
    • ④ Startup no-sprawl. No new input, no new gate, no staged window; the migration is one line per host and lives inside the package's own tests here.

    Only-① pick: A. Do ②③④ flip it: no. Effort for the chosen letter: M, one PR, effort: high (p3, domain:services).

    os-decision-facets
    ① 长远:A 让 settings 服务用 D9 自己的谓词、经它持有的 engine 问一次「无组织的调用者属于哪个组织」,服务不再私藏默认值;lean 组合是宿主声明的事,不是某一层假定的事。B 把「看不见 registry」写成第三个默认分支;C 用拒绝绑定来回避提问。
    ② 拉动:仓内生产宿主零受影响(插件恒绑来源);受影响的是 index.ts 点名的自绑定宿主类(控制面 mock、嵌入运行时),它们今天在 sys_organization 存储上写 NULL 行会撞 NOT NULL 或重复插入,即 #25226 同一缺陷的另一扇门;p3 不变。
    ③ 防错:自己接线的 agent 不知道 D9 也能拿到推导出的组织;忘记声明 lean 的 agent 在第一次 tenant 写入就被点名拒绝,而不是留下一行几个月后才被 ceremony 报出的 NULL 行(B),也不是在只读 global 键的宿主上启动即拒(C)。
    ④ 不扩散:不加输入、不加门禁、不设过渡窗;迁移每个宿主一行,仓内只在本包测试里。
    Prior rulings read: sys_organization,organization-less,derivable,embedding,no-organization-object → 5 hits; ADR-0069 D3, ADR-0069 D5, ADR-0131 D9 (ADR-0069's two are MFA and network gating — read, not on point); thread: none; other cards: 6343868210 / 6344522520 (PR #25226), 6328987873 (#25133), 6345869077 (#25095); repo: objectstack-ai/objectstack

    Execution after the ruling

    • The card thread continues under its claim 6354964172, same branch claude/project-thread-c4vx2u, same session; its next comment on this card carries Ruling-ref: plus this comment's id at line start (no re-claim: the claim's route, surface and Clause-②: no (narrowing) stand). ⛔ Ruling is not dispatching; the thread already holds its slot.
    • One PR, domain:services owns it, file surface as the claim names it: settings-service.ts, the service-settings tests of item 4, index.ts's host paragraph, one .changeset/*.md. Title fix(service-settings)!: …. Review and landing per item 8.
    • Serial constraints as the claim records them (none open on the file).

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling-ref: 6355249103

    Executing letter A under claim 6354964172 (2026-10-23T01:51Z), no re-claim. PR #25350 (draft) on claude/project-thread-c4vx2u at e12143e6.

    • Premises P1–P4 re-read on the branch before the test migration: all four hold.
    • Route as on 3c27c0a, unchanged. No new input, no catch, no second predicate.
    • 45 tests in 7 files migrated: doubles state the lean source at the bind, real-ObjectQL suites forward the engine's registry. No service default, no stub getObject.
    • Pins (a)–(e) added in settings-organization-reach.pin.test.ts; ablation (f) turns (a) and both (b) red.
    • index.ts host paragraph and the changeset carry the declaration and the symptom of omitting it. No docs/adr edit.

    Derived gates, the full dogfood battery and the contract-review-tier review follow in the os-dev-report.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25249,
      "status": "done",
      "branch": "claude/project-thread-c4vx2u",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25350",
      "session": "session_014FFTcf6msoLbXiD6BR7wHW, card thread (mode:thread) of the Sales project's fleet director, seat domain:services#1, under claim 6354964172 and ruling 6355249103 (letter A)",
      "premise_still_valid": true,
      "summary": "With no organization source stated, SettingsService.organizationDecision now asks ADR-0131 D9 itself through the engine it holds, by the same resolveGuestOrganization call the plugin makes: the stated source first, no engine is the lean reading, otherwise resolveGuestOrganization(this.engine, posture). No second predicate, no registry sniff, no catch, no new input: SettingsServiceOptions and the bindEngine extras are unchanged. A registry-less engine is read fail closed, so a lean self-binding host declares no-organization-object in the bindEngine organization extra. The plugin path is unchanged (it always binds a source). Premises P1-P4 re-read on the branch before the migration: all hold.",
      "tests": "Migration: 45 tests in 7 files, red on the route alone, now state their composition through src/__tests__/settings-organization.testkit.ts. Doubles state LEAN_ORGANIZATION_SOURCE at the bind (settings-getmany, settings-user-rung-owner.pin, settings-system-context.pin, whose constructor-engine case moves to bindEngine with the same crypto provider and secret store). Real-ObjectQL suites forward the real engine's registry onto the production adapter with withEngineRegistry (settings-secret-rotation, sys-secret-orphan-report, settings-global-rung, settings-user-reference.pin). No service default, no stub getObject. The four outside construction sites are untouched. Pins in settings-organization-reach.pin.test.ts: (a) registry-less adapter over a real engine with one sys_organization row, no source: tenant and user rungs read and write as that organization; (b) no organization object, no source: real engine raises AuthzStoreUnavailableError naming sys_organization (pinned as measured, isMissingTableError not widened), a double whose probe answers nothing reads no tenant rung and refuses the tenant write SETTINGS_VALIDATION; (c) stated lean source: NULL rows unchanged; (d) no engine: unchanged; (e) the plugin pins keep their assertions. Ablation (f), scripts/ablation-replace.mjs on the committed route, engine branch replaced by the old lean answer: (a) and both (b) red, (c), (d), (e) green; restored, blob equals HEAD, git diff HEAD empty. Suite after merging origin/main: 58 files / 987 tests passed; typecheck exit 0; the reach pin file 12/12 on the final head 1f6cbda3.",
      "gates": "dispatch-gates --commands on the final head 1f6cbda3, after a full pnpm build under the lock: 76 derived families, every one exit 0 (check:type-check-debt run under the lock). --ran reconciliation: 76 derived, 76 run, 0 NOT-MEASURED (a derived zero). Two CI reds on the first head were this PR's and are fixed: Engine test-double contract gate (the new pin's double now opens update with assertEngineUpdateDispatch, and the pinned ledger learned it via check-engine-double-contract --write) and ObjectQL double limit gate (the double now applies the caller's limit). PR check-runs on 1f6cbda3: 41, 34 success, 7 skipped (opt-in upgrade harness, packed-tarball smoke, dogfood shards, Dogfood Verify CLI, Console Pin Gate, Build Docs, Temporal Conformance), 0 failures.",
      "dogfood_battery": "pnpm --filter @objectstack/dogfood test under os-verify-lock, no shard, no path, after building the dogfood closure: 289 files passed, 3 skipped (292); 2491 tests passed, 18 skipped (2509); VERDICT command-exit 0. Run on e12143e6; the later commits touch only service-settings tests and the engine-double ledger, so the dist it consumed is the final head's.",
      "contract_review": "CONTRACT_REVIEW_TIER (narrowing face), rendered by an isolated subagent at that tier, read-only (Local-runs: none), adopted by this session: PASS on 1f6cbda3, record 6355870258 on PR 25350. Its escalation to the seat, not a block: R1-R3's opt-in 18.x upgrade harness lane and R4's walled boot are not measured on this head; it judges the compensation holds because the plugin binds a source on every engine bind, so every composition those lanes boot runs main's source call unchanged. The seat applies the harness label or names the compensating main battery in its ACCEPT.",
      "mcp_calls": "Reads only on this card: issue_read get_comments on 25249, pull_request_read get_comments on 25226, search_pull_requests x1, pull_request_read get_check_runs and get_status on 25350, get_job_logs x3, plus the claim-phase issue reads; the review subagent used issue_read and pull_request_read only. Zero MCP write tools. subscribe_pr_activity on PR 25350 is the session's event subscription, not a GitHub write.",
      "api_writes": "All through the scripts/pm fleet-write relay as objectstack-fleet[bot]: 25249 Claim comment 6354964172 and its label/assign pair (post-stamped, label-write), pr_create 25350 with assignee os-sales (dispatch.mjs), pr_request_reviewers 25350 os-sales (dispatch.mjs), the Ruling-ref comment on 25249 (post-stamped), the contract-review record 6355870258 on 25350 (post-stamped), this report (post-stamped). git push is not a REST write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "External hosts that bind SettingsService themselves (cloud control-plane mocks, embedded runtimes) are downstream of the published package: the changeset's FROM / TO and one-line fix are the hand-off. No cloud card filed, per ruling item 7; the director may route one pointer."
      ],
      "deviations": [
        "File surface: scripts/engine-double-contract.pinned.json gained one row, tool-written by check-engine-double-contract --write, because the new pin's double routes update through assertEngineUpdateDispatch. Outside the claim's declared surface; the gate requires it.",
        "Docs drift check named three content/docs pages via the SettingsService symbol only; read by hand, none states the retired reading, so no page edit. No docs/adr edit, per ruling item 6.",
        "Branch: claude/project-thread-c4vx2u (harness-assigned), declared in the PR body for the Duplicate Fix Guard."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Lane note · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-23T03:35Z. This card stays in the shared queue (pm:queue), for any seat. The epic seat is not taking it.

  6. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25249,
      "status": "done",
      "branch": "claude/project-thread-c4vx2u",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25350",
      "head": "4b63a5dd714e75ce46791a0cbd256675081000da",
      "session": "session_014FFTcf6msoLbXiD6BR7wHW, card thread (mode:thread), seat domain:services#1, claim 6354964172, ruling 6355249103 (letter A). Supersedes report 6356026070 for the new head after the merge-queue ejection.",
      "premise_still_valid": true,
      "summary": "The merge queue ejected head 1f6cbda3 on a conflict with PR 25352 (card 25315, organization-reach rule moved into core security). Merged origin/main into the branch (merge commit 4b63a5dd, no rebase, no force-push). Two hunks in settings-service.ts, resolved mechanically: main's multi-name @objectstack/core import keeps every name and gains resolveGuestOrganization; organizationReachOf keeps main's resolveOrganizationReach call and its deriveOrganization callback now calls this.organizationDecision(). organizationDecision is unchanged: stated source first, no engine is the lean reading, else resolveGuestOrganization(this.engine, posture), the one D9 call of ruling item 1. resolveOrganizationReach maps decisions as the branch's inline code did (caller organization, wall, derived to own, no-organization-object to itself, else unowned), with no catch. engine-double-contract.pinned.json auto-merged. P1-P4 hold on the new head.",
      "tests": "On 4b63a5dd after a full pnpm build: service-settings vitest 59 files / 989 tests passed, including PR 25352's new settings-organization-reach-memory.pin.test.ts (binds no engine, so it reads the kept lean branch) beside this PR's settings-organization-reach.pin.test.ts; tsc --noEmit exit 0. Ablation rerun (scripts/ablation-replace.mjs, same anchor: the engine branch of organizationDecision replaced by the old lean answer) over both pin files: (a) and both (b) red, the other 11 green; restored, blob equals HEAD, git diff HEAD empty.",
      "gates": "dispatch-gates --commands on 4b63a5dd: 76 derived families, every one exit 0 (check:type-check-debt under the lock); --ran: 76 run, 0 NOT-MEASURED. check:engine-double-contract and check:objectql-double-limit exit 0. PR check-runs on 4b63a5dd: 41, 35 success (18.x upgrade harness opt-in included, needs:upgrade-harness carried onto this head), 6 skipped, 0 failures. origin/main has since moved 3 commits; git merge-tree against it is clean, so no further push.",
      "dogfood_battery": "pnpm --filter @objectstack/dogfood test on 4b63a5dd under os-verify-lock, no shard, no path, after the full build: 289 files passed, 3 skipped (292); 2491 tests passed, 18 skipped (2509); VERDICT command-exit 0. This measures the two sides of the merge together.",
      "contract_review": "CONTRACT_REVIEW_TIER (narrowing face), fresh record on the new head by an isolated read-only subagent at that tier, adopted by this session: PASS, record 6357419302 on PR 25350. It finds the merge delta mechanical (exactly two merge-only lines: the import name and the callback), the narrowing semantics unchanged, and the D9 helpers identical on main. Advisory only: two main-side sentences (settings-service.ts near the UNOWNED block, and the memory pin's docblock) under-describe the unset-source case; not restated as the retired reading, left for a later touch.",
      "mcp_calls": "Reads only since the last report: pull_request_read get_check_runs on 25350; the review subagent used issue_read and pull_request_read only. Zero MCP write tools.",
      "api_writes": "Through the scripts/pm fleet-write relay as objectstack-fleet[bot]: the contract-review record 6357419302 on 25350 (post-stamped) and this report (post-stamped). git push of the merge commit is not a REST write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "Unchanged from 6356026070: external self-binding hosts take the changeset's FROM / TO and one-line fix as the hand-off; no cloud card (coordinator confirmed)."
      ],
      "deviations": [
        "Unchanged: one tool-written row in scripts/engine-double-contract.pinned.json, accepted by the seat; branch claude/project-thread-c4vx2u declared in the PR body."
      ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · 2026-10-23T05:14Z

    reading value
    PR #25350 MERGED through the queue on head 4b63a5dd (re-ACCEPT 6357648319, after the MERGE_CONFLICT dequeue against #25352)
    origin/main carries 9d60b42874

    State: the card closed completed through the PR. This stroke clears pm:dispatched and the assignee. Hosts outside this repo that bind SettingsService themselves get their hand-off from the published changeset's FROM / TO and one-line fix (ruling 6355249103 item 7).


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions