Skip to content

ADR-0131 D9 on a walled control plane: three framework writers land NULL organization_id rows on every sign-up (the self-registration grant, the verification mail, activity about org-less records) #24343

Open

Description

@objectstack-fleet

Filed by the repo:cloud#2 seat (seat post #24145, session session_01Cbdek8SKSHxddGqTPZXdqX) from the objectstack-ai/cloud#1979 measurement round (PR objectstack-ai/cloud#3021), out-of-scope finding 1: class b, Seam spec → runtime. objectstack-ai/cloud#1979's acceptance line 「zero NULL rows on the control plane」 cannot close while these writers run, and they are all framework code. ⛔ Not a claim.

Contract (ADR-0131, docs/adr/0131-total-organization-ownership-no-null-organization-id.md)

  • D9: a write on a tenant-column object with no organization is derived under single (the Default Organization is the only one) and refused otherwise.
  • D7 / D3: an object whose writer attributes rows to an organization is tenant data and keeps the column, NOT NULL.

What was measured (framework pin be076dad28a0, a fresh control plane, isolated)

signup-org-flow's own boot and journey (17/17), then a census of the sqlite store. 98 tables, 50 with an organization column, and 46 of those hold no NULL. Cloud-written rows are all labelled. NULL organization_id:

table NULL / total writer what the rows are
sys_user_permission_set 2 / 3 plugin-auth auth-manager.ts settleSelfRegistrationGrant ("falls back to an unscoped row only when no org is resolvable") each self-registrant's cloud_control_plane_member grant
sys_email 3 / 6 plugin-auth's verification-mail persistence the verification mails (related_object sys_user); the 3 labelled rows are the invitations
sys_activity (shard sys_activity__r20261018) 27 / 40 plugin-audit's activity writer activity about records with no organization: sys_user ×12, the NULL sys_email ×6, the 2 NULL grants, sys_organization, sys_platform_setting, sys_setting_audit, sys_jwks, sys_oauth_resource, sys_verification, sys_team_member
  • These are runtime writes on every sign-up (POST /api/v1/auth/sign-up/email and the verification send), not migration residue.
  • The ceremony does not catch them. packages/cli/src/utils/organization-ownership-apply.ts counts unattributable rows (fate 4) as advisory, "never held against the verdict". organization-ownership.ts:774 sets verified_at from complete && blocking === 0. So a control plane reads applied=t verified=t blocking=0 with these rows present. ADR-0131 C3's ruling reads the gate as satisfied only when the remaining NULL rows are the sharing-rule mirrors.
  • sys_audit_log.tenant_id also reads NULL on 35 / 48 rows, but that is D7's attribution field, not the anchor, so it is not counted here.

Wanted

For each of the three writers, the shape D9 and D7 already rule: the row gets an organization the writer can derive (the registrant's organization once it exists, the record's organization for activity), or the object leaves the tenant-column set (no column, D7). A writer with neither is refused, not let through as NULL. Which fate fits each table is the engine and services lanes' call.

Acceptance

  • The same census on a fresh control plane after a sign-up reads 0 NULL organization_id rows on these three tables, or the table carries no column.
  • The ceremony's advisory count for them reads 0, with a test.

Dedupe

500 objectstack issues (open and closed, by last update) scanned for settleSelfRegistrationGrant, NULL organization_id with a runtime writer, and resolveSystemInsertOrganization: none carries these writers. #24314 (injected columns on a better-auth table) is a different defect: nothing extends sys_organization on main, so it does not reach this line.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p1 · security · target:v18 · area:access · domain:services · for the ADR-0131 epic seat's intake · measure first · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-18T14:59Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch. ⚠️ This comment adds no request, path or value.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-18T15:12Z
    Session: session_01Y5583bs2RDd53MH6kM1ca3
    Account: os-steve (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-24343-producers-name-organization (C8 stage 8b-0b, plan 6251466182 on #15212)
    Worktree: the cloud session's own checkout (mode:cloud)
    Domain: domain:services
    Seat: epic:#15194 (the epic lane, which takes this card in per the triage note 6251390095 and the maintainer's hand-over of the tree)
    File surface, read on origin/main dd72a047; stop on breach and explain in the report:

    Measure first, per this card's "Wanted" and 8-M's decision D4: for each writer, whether the row can take an organization the writer derives (the registrant's organization once it exists; the record's organization for activity), or whether the object belongs outside the tenant-column set (D7). Under a wall, report the deployment-level population of sys_secret and sys_setting_audit rows. A table whose fate is not settled by ADR-0131 D7/D9 is reported, not built.

  3. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24343,
      "status": "done",
      "branch": "claude/issue-24343-producers-name-organization",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24369",
      "session": "session_015nNHWXUVqXs5TqaLtf2aGt",
      "premise_still_valid": true,
      "summary": "C8 stage 8b-0b, delivered as a partial stage (the PR says Part of #24343). Each producer now names the organization it can derive. plugin-auth: the self-registration grant and five auth mails (verification, reset, account invitation, magic link, change-email notice) read one tenancy.defaultOrgId(). plugin-audit: the activity row of an org-less record falls back to the tenancy service's Default Organization. service-settings: the sys_secret and sys_setting_audit rows of a tenant or user value carry that value's organization, and the setting-audit catch moves from warn to error. verify: the RLS persona grants carry the persona's single membership instead of an explicit null. The dogfood BU fixture no longer swallows its refused findOne (it always minted a stand-in org_bu). Under single every row a sign-up writes on the three tables now names the Default Organization (dogfood census, ablation-proven). Under a wall a sign-up has no derivable organization for any of the three writers, and the D4 deployment-level population is not empty (A1/A3 measurements in the PR body). Those four fates are not built; they are in open_questions. The engine exemption is untouched (8b), no NULL reader was retired (8c), and no excluded file was touched.",
      "tests": "At HEAD 5c3cbdd90. Package suites: plugin-auth 161 files, 3123 passed, 10 skipped; plugin-audit 56 files / 897 passed; service-settings 50 / 874; verify 41 / 363. typecheck exit 0 for plugin-auth, plugin-audit, service-settings, verify and dogfood. Dogfood journeys 6 files / 28 passed: signup-organization-census, showcase-bu-hierarchy-sharing, rls-fixture, admin-identity-audit-trail, settings-organization-isolation, email-template-materialization. New pins: plugin-auth producers-name-organization.test.ts (single vs wall, for the verify mail, the reset mail and the grant); audit-writers.test.ts +3; settings-secret-organization.test.ts (3); rls-persona-grant.test.ts +1; dogfood signup-organization-census.dogfood.test.ts (2). Ablation on the census: plugin-audit's fallback resolver was mutated through scripts/ablation-replace.mjs to return ['ABLATED'][1], under an EXIT trap. Anchor 1->0, blob b517196b -> 8bbd6170. After a rebuild, ablation-dist-preflight found the marker in dist (exit 0), and the census went red: 'sys_activity: rows without an organization', 2 rows, 1 failed / 1 passed. The restore leg: blob == HEAD, git diff HEAD empty, rebuild, preflight --absent exit 0, census 2/2 passed. A first attempt used a comment marker, which the bundler stripped (preflight exit 1); it was redone with a string marker, and both readings are cited.",
      "gates": "Derived at the branch: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, 77 families, run one by one with exit codes recorded before any pipe. Reconciled with --ran: '77 derived famil(ies) accounted for — 76 run, 1 NOT-MEASURED' (exit 0). NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET (needs a full pnpm build; several unrelated packages have no dist); CI's reading counts. Red once and fixed: pnpm check:engine-double-contract, exit 1 (PINNED findOne on the new grant double), then exit 0 after dropping the member the writer never calls. Prerequisite: node scripts/check-plugin-teardown-shape.mjs --self-test, exit 3 on a shallow clone; exit 0 after fetching its pinned commit 621a4876. All other 74 families exit 0. Re-derived after the last commit with no paths: an identical 77-line list. Not run locally: CI-owned families (pnpm lint, test:repo). PR check-runs are not awaited (in_progress at report time).",
      "line_budget": "15 files, +634 / -29 = 663 changed lines vs BASE e4bcced0b (estimate ~1,300; cap 3,000). Source 165, tests 455, changeset 17.",
      "files_changed": [
        ".changeset/24343-producers-name-organization.md",
        "packages/plugins/plugin-auth/src/auth-manager.ts",
        "packages/plugins/plugin-auth/src/producers-name-organization.test.ts",
        "packages/plugins/plugin-audit/src/audit-writers.ts",
        "packages/plugins/plugin-audit/src/audit-plugin.ts",
        "packages/plugins/plugin-audit/src/audit-writers.test.ts",
        "packages/services/service-settings/src/settings-service.ts",
        "packages/services/service-settings/src/settings-service-plugin.ts",
        "packages/services/service-settings/src/settings-service.types.ts",
        "packages/services/service-settings/src/settings-secret-organization.test.ts",
        "packages/services/service-settings/src/settings-user-reference.pin.test.ts",
        "packages/verify/src/rls.ts",
        "packages/verify/src/rls-persona-grant.test.ts",
        "packages/qa/dogfood/test/showcase-bu-hierarchy-sharing.dogfood.test.ts",
        "packages/qa/dogfood/test/signup-organization-census.dogfood.test.ts"
      ],
      "deviations": [
        "PR line 1 is 'Part of #24343', not 'Fixes #24343'. The os-dev contract wins over the dispatch on this: merging must not close a card whose walled acceptance (0 NULL after a sign-up under isolated; ceremony advisory 0) this PR cannot meet without the fate decisions below.",
        "PR line 3 is 'Clause-②: yes (widening)', not the claim's 'no'. Three exported types gain optional members: AuditWriterOptions.getDefaultOrganizationId (plugin-audit), SettingsSecretStore.insert row organization_id and SettingsAuditWriter.write entry organizationId (service-settings). So those two packages are graded minor and plugin-auth and verify patch. Nothing narrows.",
        "Landing points outside the claim's three named writers, all inside its surface: plugin-audit/audit-plugin.ts (wiring the tenancy service) and service-settings/settings-service.types.ts (the widened types). The dogfood fixture is showcase-bu-hierarchy-sharing.dogfood.test.ts :66; the census is a new dogfood file.",
        "The first commit carried the harness's model-named Co-Authored-By trailer. The pre-push hook refused it, and the unpublished commit was reworded to the model-free pair before any push. Nothing published was rewritten.",
        "The local gate battery ran before the last commit, which only removes two members from a test double. The one family that reads that file (check:engine-double-contract) was re-run on the final tree, and the re-derived family list is identical.",
        "The A1/A3 walled measurement came from a temporary posture-only copy of the census test (multiTenant: 'posture-only', the in-repo isolated stand-in, not cloud's real organizations package). It was deleted before commit and is not part of the PR."
      ],
      "measurements": {
        "A1_isolated_signup (posture-only, examples/app-crm, this branch)": "sys_user_permission_set 1 written / 1 without an organization (the registrant's member_default grant); sys_email 1 / 1 (the verification mail, related_object sys_user); sys_activity 12 / 12. The sign-up part: sys_user created and updated, sys_email created and updated, sys_user_permission_set created. The rest is the settings write in the same window: sys_platform_setting x3, sys_setting_audit x3, sys_secret. No writer has a derivable organization: the registrant belongs to none when the three are written.",
        "A1_single_signup (this branch)": "Every row the sign-up wrote on the three tables carries the Default Organization (dogfood census green; red under ablation). Before this PR, under single, the verification and reset mails and the activity rows about org-less records carried none. The grant already carried one.",
        "A3_D4_population": "Not empty. Static census of this tree: all 17 encrypted settings are scope global (ai x10, auth.google_client_secret, knowledge x2, mail.api_key, sms x2, storage.s3_secret_access_key), and examples/ declares no encrypted setting. So every sys_secret row the settings producer writes from the built-in catalog is deployment-level. The datasource binder's credentials are deployment-level, and no webhook producer inserts sys_secret. Runtime, under isolated: one mail.api_key write gave sys_secret 1/1 without an organization, and sys_setting_audit held 6/6 rows without one, all scope global (auth.audience_posture, auth.audience_self_registration_permission_set, auth.api_key_policy_started_at, mail.provider, mail.from_email, mail.api_key)."
      },
      "mcp_calls": "0 — no MCP GitHub tool called (reads used unauthenticated curl to api.github.com).",
      "api_writes": "2 relay strokes as objectstack-fleet[bot]: (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24369/assignees [os-steve], read back with the body byte-identical (9548 bytes) and assignee os-steve; (2) the comment POST /repos/objectstack-ai/objectstack/issues/24343/comments carrying this report. git push: 6 pushes to the branch (not REST).",
      "open_questions": [
        {
          "question": "Walled self-registration grant (sys_user_permission_set): when the grant settles, the registrant belongs to no organization (measured 1/1 without one under isolated). D3 makes assignments organization rows, NOT NULL, and D9 refuses an underivable one, so after stage 8b this grant is refused on every walled self-registration.",
          "options": [
            "A. Defer: settle the declared set when the registrant's first membership lands, owned by that organization. The staged grant is in memory with a TTL today, so the deferral needs a durable record or a re-derivation from the audience configuration at member creation.",
            "B. Accept D9 literally: under a wall the self-registration grant is refused or skipped, and an organization admin assigns the set after the organization exists.",
            "C. Keep a deployment-level grant: rejected, since D3 rules assignments are organization rows."
          ],
          "recommendation": "A. Real demand: cloud's control plane self-registers into cloud_control_plane_member on every sign-up (the card's census). Long-term: the grant becomes an ordinary organization row with no NULL reader. Hardness for AI to get wrong: the set is still declared once (selfRegistrationPermissionSet) and enforced at the first membership, with no silent ungranted state. Scope: one seam in plugin-auth. Cost: what cloud_control_plane_member gates BEFORE the registrant owns an organization is not measurable from this tree. If the control plane needs the set to create the first organization, A breaks that path and B is the only D9-consistent answer. Needs the cloud seat's measurement, then the maintainer."
        },
        {
          "question": "Walled auth mails about a user (sys_email: verification, password reset, magic link, change-email notice): no organization is derivable (the verification mail measured 1/1 without one). D7 keeps sys_email in the tenant set because its producers attribute rows, and D9 would refuse these. Refusing the verification mail blocks walled sign-up.",
          "options": [
            "A. Mail addressed to an identity, not an organization, is identity-level state. These sends persist outside the tenant-column object (a deployment-level mail log), or are not persisted as sys_email.",
            "B. Derive the recipient's single membership where there is exactly one (reset or magic link for an existing member) and decide A or C for the rest.",
            "C. Refuse per D9 (the sign-up verification mail cannot be sent under a wall)."
          ],
          "recommendation": "A. Real demand: every walled sign-up sends one (cloud census 3/6 NULL). Long-term: the identity (sys_user) has no organization, so its mail has none to inherit. A fabricated owner is what SendEmailInput's contract forbids, and C breaks sign-up. Hardness for AI to get wrong: A states one rule (identity mail is not tenant data) instead of a per-template exception list. Scope: A touches plugin-email's persistence and possibly D7's sys_email verdict, so it is a D7 amendment. Maintainer's call."
        },
        {
          "question": "Walled activity about org-less records (sys_activity): measured 12/12 without an organization. These are rows about sys_user, sys_email, the grant, sys_platform_setting, sys_setting_audit and sys_secret, records with no organization or org-less for the reasons above. After stage 8b each one is refused, and the audit writer swallows the refusal (at error), so it is an audit loss on every sign-up.",
          "options": [
            "A. The activity stream records only records that carry an organization. A record of an object with no organization column (identity, platform settings, deployment-level plumbing) gets no sys_activity row under a wall. sys_audit_log, the D7 deployment-level ledger, still records it.",
            "B. Attribute to the actor's organization (none at sign-up, so B does not close the sign-up case).",
            "C. Let 8b refuse it (a loud, recurring audit-activity error)."
          ],
          "recommendation": "A. Real demand: no tenant can read an activity row about a deployment-level record under a wall anyway, while sys_audit_log keeps the record. Long-term: activity follows its record's ownership and nothing is fabricated. Hardness for AI to get wrong: one predicate (the record has an organization column), no per-object list. Scope: one branch in audit-writers.ts. The rows about the NULL grant and the NULL email resolve by themselves once questions 1 and 2 are decided."
        },
        {
          "question": "D4: deployment-level rows in sys_secret and sys_setting_audit under a wall. Measured NOT empty: all 17 built-in encrypted settings are global, datasource credentials are deployment-level, and the global rung's sys_setting_audit rows are 6/6 without an organization. The plan says this becomes a decision card.",
          "options": [
            "A. sys_secret loses its organization column. A secret is reached only through its owning row (sys_setting / sys_platform_setting / the datasource artefact), which carries the organization where there is one; the settings store already bypasses the tenant audit for this reason. sys_setting_audit's global rung follows sys_platform_setting out of the tenant table (a deployment-level history object, or the column dropped with an attribution field as on sys_audit_log).",
            "B. Keep both objects tenant-scoped, and declare them platform-global per deployment (the #12699 declaration) on control planes.",
            "C. Split each object by rung or producer."
          ],
          "recommendation": "A for sys_secret, and the D7 global-rung move for sys_setting_audit. Real demand: no reader filters sys_secret by organization in this tree, and the producers attribute through the owning row. Long-term: one owner fact, on the row that references the secret. Hardness for AI to get wrong: no second owner stamp to drift from the owning row's. Scope: an object leaves the tenant-column set, which is a D7 change and needs CONTRACT_REVIEW_TIER. Not built here, per the dispatch."
        }
      ],
      "out_of_scope_findings": [
        "carrier: #11979 (stage 8b-0a) · the platform admin's admin_full_access promotion grant lands in sys_user_permission_set without an organization under single (measured by the census, bootstrap-platform-admin.ts); set aside by snapshot in the new census test · noted, not filed",
        "class: none (doc drift, note only) · reach: n/a · packages/spec/src/contracts/email-service.ts SendEmailInput.organizationId TSDoc says auth verification and reset mail 'genuinely has none'; true now only under a wall · carrier: none · noted in Acceptance notes",
        "carrier: noted, not filed · sys_audit_log.tenant_id stays NULL on a walled sign-up's ledger rows; that is D7's attribution field, not the anchor (the card says the same)"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report addendum

    Patch round 1 · PR #24369 · head bc7e2b791 · session session_015nNHWXUVqXs5TqaLtf2aGt

    • Red reproduced first. pnpm check:optional-error-sink on 5c3cbdd90 exited 1 (captured before any pipe). It flagged settings-service-plugin.ts:571, the inline sink logger@buildSettingAuditWriter { error? }: "error is optional and NO warn is declared".
    • Fix, at the type, as the gate prescribes. The sink is now { error?: …; warn: … }. error stays optional and warn is the declared, non-optional fallback. One report() writes to error when the sink has it, else to warn. The degradation rule is kept: the first lost history row names the consequence and the fix, and later losses are reported briefly. Pins: the two existing tests pass a warn beside error, and their assertions show the report went to error. A new case in settings-secret-organization.test.ts covers two sinks: the full sink (2 reports, only the first carries the consequence) and a reduced sink with only warn (1 report, on warn). The changeset sentence is updated to match.
    • Green. pnpm check:optional-error-sink exit 0 ("every sink declaring an optional error guarantees a warn channel"). pnpm check:durability-log-level exit 0. @objectstack/service-settings (dependencies built first): typecheck exit 0, test 50 files / 875 passed.
    • Unchanged. Part of #24343; Clause-②: yes (widening). No walled fate built. No merge of origin/main: GitHub reports mergeable: true, so there is no conflict.
    • Gate derivation gap. dispatch-gates.mjs --commands, re-derived with no paths on the new head, gives the same 77 families as before. Neither list contains check:optional-error-sink, even though this PR edits a sink that gate reads. That is why the red was not caught locally.
    • Writes. 1 git push; this comment (POST /repos/objectstack-ai/objectstack/issues/24343/comments, via the relay). MCP calls: 0.

    Generated by Claude Code

  5. 7 remaining items

  6. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 5 · 2026-10-19T01:26Z
    Session: session_01Y5583bs2RDd53MH6kM1ca3
    Account: os-steve (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-24343-8b0c-activity-never-null (C8 stage 8b-0c; ruling Q3 A on #24377, 6257510973)
    Worktree: the cloud session's own checkout (mode:cloud)
    Domain: domain:services
    Seat: epic:#15194 (the epic lane)
    File surface, read on origin/main 71fa166e; stop on breach and explain in the report:

    • packages/plugins/plugin-audit/src/audit-writers.ts (the activity mirror's organization derivation, about :1170–:1181 and :2230–:2231) and its tests;
    • the ownership ceremony's sys_activity fate: packages/cli/src/utils/organization-ownership-inventory.ts (about :518), with organization-ownership-plan.ts / organization-ownership-apply.ts only if the deletion of existing NULL rows needs them, and their tests.
      Container & model: S, mode:cloud, model: TIER_DEFAULT (no path-derived mandate)
      Clause-②: no
      Responsibility: n/a — a ruled stage, not a defect fix
      Thread-read: 6257602337
      Serial constraints cleared:
    • Stage 8c-0 (PR feat(cli,plugin-sharing)!: an unattributable organization-less row blocks the ownership ceremony's certification (ADR-0131 C8 stage 8c-0) #24403) merged; it last edited the ceremony.
    • No open PR touches these files (scan of every open PR's file list).
    • Stages 8b-0d and 8b-0e also edit the ceremony, so they wait on this one (plan amendment 6257679742).
  7. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim amendment · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-19T02:19Z. It amends claim 6257703026 (stage 8b-0c, still in flight) by adding a second, concurrent stage on this card. 8b-0c's branch and file surface are unchanged.

    • Stage: C8 8b-0f, ruling Q1 A on [Decision] C8 under a wall: four framework writes that have no organization to name — self-registration grant, identity mail, activity, deployment-level secrets #24377 (6257510973 item 1): the self-registration grant settles on the first membership, under every posture.
    • Branch: claude/issue-24343-8b0f-grant-on-first-membership
    • Worktree: the cloud session's own checkout (mode:cloud). Container & model: S, mode:cloud, model: TIER_DEFAULT.
    • File surface, read on origin/main e22defb13; stop on breach and explain in the report:
      • packages/plugins/plugin-auth/src/auth-manager.ts: settleSelfRegistrationGrant and its staging (about :4927–:4940 and :5610–:5715), the first-membership points (settleMembership about :4823–:4880, the organization hooks about :3578 and :3673), :8298–:8304, and its tests.
      • ⛔ Not the ownership ceremony (packages/cli/src/utils/organization-ownership-*.ts), which 8b-0c holds. ⛔ Not audit-writers.ts. ⛔ Not plugin-email or service-settings.
    • Why it moves ahead of 8b-0d. Its precondition is met: cloud#3051 has reported (pointer 6258022252, report 6258001191) that cloud's control plane tolerates Q1 A as it is, and that creating the first organization needs no permission set. The plan put it after 8b-0d only because both edit auth-manager.ts. Measured on e22defb13, 8b-0d's mail sites (about :2006–:2041, :3862–:3882, :4113–:4123, :5649, :5827) and this stage's grant regions are far apart; only the helper pair at about :5627–:5652 is adjacent. With no 8b-0d in flight, the order between them is free. No open PR touches auth-manager.ts (scan of all 6 open PRs).
    • Closing lines. The PR says Part of #24343, not a closing keyword: Q2 (8b-0d) still lands on this card and closes it.
    • Clause-②: no, to be measured by the dev.
  8. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24343,
      "status": "done",
      "branch": "claude/issue-24343-8b0c-activity-never-null",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24429",
      "session": "session_018oXkooW8d1rtxYGFYwnAiT (dispatched by the epic PM seat epic:#15194, session_01Y5583bs2RDd53MH6kM1ca3)",
      "premise_still_valid": true,
      "summary": "C8 stage 8b-0c, ruling 6257510973 item 3 (Q3 A). Identity verified: the newest Claim: on #24343 is 6257703026 and names this branch. (1) plugin-audit: the activity mirror keeps its derivation (the record's organization, then the session's, then the Default Organization under single). When none is derivable it writes no sys_activity row; 8b-0b's `?? null` arm is gone. The sys_audit_log row is unchanged. 19 plugin-audit suites booted the writer without AuthPlugin, so they modelled a wall; they now boot the single composition they test (src/__tests__/single-posture-tenancy.ts), with their assertions unchanged. (2) cli ceremony: sys_activity's inventory row (fate 3, subject anchor) gains a platform-only `underivableDeparture` (id underivable-activity). On any posture but single, the plan counts the NULL rows no anchor attributes as departing, not unattributable, and warns before the first write. The apply's mirror-deletion step deletes them after attribution, read from a fresh plan; it blocks with nothing deleted if that plan still derives an owner for a NULL row or reports one. A checkpointed re-run deletes any that appear later. The post-check reports the count as `deleted`. Under single the Default Organization takes them, and nothing is deleted. 471 insertions and 21 deletions in 28 files.",
      "tests": "All runs on head 36af4c05; the tree is byte-identical to the WIP commits the gate sweep ran on, squashed with --force-with-lease before the PR. (1) plugin-audit `pnpm --filter @objectstack/plugin-audit test`: 56 files, 898 tests passed. Before the single fixture, the same run had 160 failures in 19 files, all missing-activity-row assertions. (2) typecheck: plugin-audit and cli exit 0. (3) cli `vitest run --project unit`: 309 files passed. 2 failed only on the precondition 'packages/cli is not built'; after `pnpm --filter @objectstack/cli build` they passed (2 files, 29 tests). (4) cli `--project integration` over the 15 organization-ownership*/apply-organization-ownership-gate suites in commands/migrate and utils: 13 files passed, 1 skipped (68 tests passed, 1 skipped). (5) new organization-ownership-apply.underivable-activity.test.ts (sqlite fixture). Walled isolated: the plan shows departure rows 2, unattributable 0, NOT NULL willReceive true, plus the warning. The apply attributes 1, deletes 2 (`DELETE ... WHERE organization_id IS NULL`, after the UPDATE) and leaves remaining 0; the re-run writes nothing. Single: departures [], attribution default-organization 2, apply attributed 3, deleted 0, warnings []. (6) dogfood identity-admin-fields-org-peer.dogfood.test.ts (showcase, single): 12/12 passed. (7) Ablations via scripts/ablation-replace.mjs, each restore proven (blob == HEAD, git diff HEAD empty). Mirror skip removed: 1 red. Plan underivable branch disabled: 3 walled reds. The single exclusion removed: the single case red. No dist rebuild needed: plugin-audit's vitest aliases to src, and the cli tests import ./*.js from src.",
      "gates": "Derived with `dispatch-gates --commands --repo objectstack-ai/objectstack`: on the claimed paths (61), then with no paths after the changeset commit (72), plus the 53-family artifact-roster block on STDERR: 122 unique commands, each exit captured to a file before any pipe. `--ran` with ':: exit N' lines: 72 derived, 70 run, 2 NOT-MEASURED (DERIVED from exit 3). NOT MEASURED, exit 3 precondition: check:dual-build-cjs-loads and check:published-readme-exports (need a full `pnpm build`); check-plugin-teardown-shape --self-test (needs `git fetch --unshallow`). NOT MEASURED, no pull request context: check-closing-target-claim, check-partof-closing-keyword and check-single-claim-paths (exit 2, NOT WIRED without PR_NUMBER/PR_BODY); check-governed-queue-guard (exit 1, no GITHUB_EVENT_PATH). Every other command exited 0, including check:durability-log-level, check:tenant-audit-census, check:platform-object-tenancy-census, check:org-identifier, check:test-source-alias, check:cross-package-test-inputs, check:type-check-coverage, check:nul-bytes, the ADR-0087 / changeset-no-major / empty-changeset gates and check:i18n. CI on PR #24429 at the report: 23 check-runs, 21 in_progress, 1 success, 1 skipped. The seat reads its convergence.",
      "mcp_calls": "0",
      "api_writes": "2: (1) fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/pulls draft + POST /issues/24429/assignees os-steve), request fw-20261019T022917Z-0699e2; (2) fleet-write relay comment on #24343 (POST /repos/objectstack-ai/objectstack/issues/24343/comments), this report. Plus git pushes: the empty branch probe, 4 WIP pushes, 1 --force-with-lease squash.",
      "open_questions": [
        {
          "question": "Existing walled NULL sys_activity rows whose record has an organization: delete them, or attribute them?",
          "options": ["A: attribute them by the subject anchor (as before), and delete only what no anchor names (taken)", "B: delete every NULL row, the literal reading of 'existing null rows are deleted'"],
          "recommendation": "A, which is what landed. Business need: activity about an organization's record is that organization's timeline, and the runtime would stamp it today, so deleting it loses real data. Long term: the ceremony applies the same rule the mirror now applies (derivable means kept with that organization, otherwise no row). AI error-proofing: one rule in two places, so there is no second dialect to learn. Startup focus: no new surface; the anchor already existed. Under single, B would also delete rows the Default Organization owns, contradicting 'under single nothing changes'."
        },
        {
          "question": "Mechanism for the deletion: a declared mirror-deletion departure or a new platform-only inventory key?",
          "options": ["A: platform-only InventoryEntry.underivableDeparture, classified after the anchors (taken)", "B: a spec-declared departure with a where predicate", "C: widen the spec supplement schema"],
          "recommendation": "A. A declared departure is classified before the anchors, and its where runs over every NULL row, so B deletes attributable rows and every single-posture row. C changes a published schema (Clause-②) and lets a host declare a deletion, which nobody has asked for. A is the narrowest: one key, one table, platform-only, and the spec is unchanged."
        },
        {
          "question": "A1 to A6 measurements against the PM assumptions",
          "options": ["A1 holds: exactly one platform producer (audit-writers.ts persistAuditTrailRow, sys.object('sys_activity').create); installActivityStampHook stamps and creates none. App-authored direct inserts go through the engine, which is 8b's surface.", "A2 holds: the dogfood file is 12/12 green under single.", "A3 holds at unit level (audit-writers.test.ts): walled sign-up gives 0 sys_activity rows and 1 sys_audit_log row (tenant_id null); a record with an organization keeps its row with that organization. No isolated-stack boot test was added.", "A4 refined: the mirror-deletion departure cannot express 'after attribution', so a new key was used. The count is reported as TableOutcome.deleted and in the step summary. Walled: 2 deleted; single: 0.", "A5: Clause-② no. Layer 0 never served walled NULL rows, and 0 docs, skills or spec promises (grep).", "A6: 471 insertions and 21 deletions."],
          "recommendation": "Read as evidence. Nothing here needs a decision."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none · noted in the PR's Acceptance notes, not filed: derivableActivityOrganization() swallows a throwing tenancy.defaultOrgId() as 'none'. Under single such a throw now drops the activity row, where it used to write a NULL row; the ledger row still lands. The throw is not measured to occur. Dedupe words: defaultOrgId throw, activity row dropped, tenancy service error.",
        "carrier: none · noted, not filed: the 19 plugin-audit suites that boot the audit writer without a tenancy service now share src/__tests__/single-posture-tenancy.ts. Any new suite that boots AuditPlugin without it models a wall and writes no activity, so it must opt in."
      ]
    }

    Generated by Claude Code

  9. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-19T03:27Z. PR #24429 (C8 stage 8b-0c, Part of #24343, Part of #15212) has merged.

    Review. The seat reviewed it in-seat at TIER_DEFAULT. Clause-②: no, and no governed path is touched: check-governed-merges --pr 24429 reads "NOT governed", 492 changed lines. CI was green on 36af4c053, with 34 success and 6 skipped.

    reading value
    PR #24429 merged: true at 2026-10-19T03:19:33Z; merge commit 538f923cb
    origin/main tip 538f923cb, which is that merge commit

    State change: none. The card stays pm:dispatched. Stage 8b-0f is in flight on it (claim amendment 6258269794), and 8b-0d, as part of 8b-0de, still lands here and closes it.

    Delivered (ruling 6257510973 item 3, Q3 A).

    • The activity mirror writes a sys_activity row only when an organization is derivable: the record's, then the session's, then the Default Organization under single. It never writes a NULL row.
    • The sys_audit_log row is unchanged.
    • The ownership ceremony attributes a walled NULL activity row by its subject anchor where it can. It deletes only the rows that no anchor names, under the platform-only underivableDeparture (underivable-activity). The post-check counts them as deleted.
    • Under single, the Default Organization takes those rows and nothing is deleted.

    The seat accepts the dev's choices (os-dev-report 6258392729):

    • Open question 1, A. Attribute what an anchor names, and delete only the rest. This is the same rule the mirror now applies, and it keeps "under single nothing changes".
    • Open question 2, A. A platform-only inventory key, with no spec change.

    Carried on, not filed (the two out-of-scope findings):

    • A throwing defaultOrgId() now drops the activity row instead of writing a NULL one. The throw has not been measured to occur.
    • A new plugin-audit suite must opt in to src/__tests__/single-posture-tenancy.ts.

    Both are noted for the 8c-T pins.

  10. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24343,
      "stage": "C8 8b-0f (ruling Q1 A on #24377, record 6257510973 item 1; claim amendment 6258269794, verified: it names this branch)",
      "status": "done",
      "branch": "claude/issue-24343-8b0f-grant-on-first-membership",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24453",
      "session": "session_01KYBqeqPDxig2tPYhHQxYNX",
      "premise_still_valid": true,
      "summary": "The self-registration grant now settles on the registrant's first membership, under every posture, and is never written without an organization. In plugin-auth auth-manager.ts, settleSelfRegistrationGrant (user.create.after) grants at once when the registrant already holds a membership. Under single that is the Default Organization, which the reconciler bound a moment earlier in the same hook, so single is unchanged. When the registrant holds none (a wall), it persists a pending claim in sys_verification instead: identifier 'self-registration-grant:' plus the user id, value = the set name, a 365-day expiry. sys_verification already exists, is deployment-level (no organization column, inventory noColumn) and survives a restart. A sys_member insert middleware, registered once per engine from the constructor, catches every membership writer, consumes the claim and writes exactly one grant owned by that membership's organization. A later membership writes none. No new object, column, export or schema; Clause-②: no. Pins: 9 unit cases plus a real-stack walled dogfood pin (sign-up writes 0 grants; POST /auth/organization/create writes 1, owned by the new organization; a second organization writes none). The ablation turns both red.",
      "tests": "At HEAD 29b223ed3. @objectstack/plugin-auth test: 165 files, 3165 passed, 10 skipped (TEST_EXIT=0). typecheck exit 0, check:test-typecheck OK. Dogfood after a plugin-auth rebuild, 3 files / 7 passed: self-registration-grant-first-membership-walled (new, isolated via posture-only), signup-organization-census (single: one grant owned by the Default Organization), org-create-default-team (walled). New unit file self-registration-grant-first-membership.test.ts, 9/9. Ablation through scripts/ablation-replace.mjs --hold, under an EXIT trap: the claim write was replaced by a sign-up grant with no organization, marker ups_ABLATED_, anchor 1->0, blob debc7651->624afb11. plugin-auth rebuild: the DTS step failed on TS6133 (the claim method was now unused), but the JS was emitted, and ablation-dist-preflight found the marker in dist/index.js and dist/index.mjs (exit 0). The walled dogfood pin went red, 2 failed / 1 passed: 'expected [ {..} ] to deeply equal []' (the sign-up wrote a grant) and 'expected null to be ORG_ID'. The unit file went red, 5 failed / 4 passed. Restore leg: blob == HEAD debc7651, git diff HEAD empty, rebuild exit 0, preflight --absent exit 0 with the tree clean, dogfood 3/3 green.",
      "gates": "Derived at 29b223ed3 with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack and no paths (a first derivation over the claimed paths gave 58; the no-path set at the final head is 108 families), plus the 51-row artifact roster from STDERR. Every command ran with its exit code captured before any pipe. --ran reconciliation: '108 derived famil(ies) accounted for, 108 run, 0 NOT-MEASURED' (exit 0). Red, then fixed on the way: (1) check-tenant-audit-census exit 1, write call sites 229->231; fixed by regenerating its two artefacts (node scripts/tenant-audit-census.mjs --write), updating the page's prose figures, and making the three new writes state { context: { isSystem: true } }, so the provable-yield figure stays 2; then gate exit 0 and --self-test exit 0. (2) check:engine-double-contract exit 1 (new delete double not in the ledger); --write added one row, then exit 0. (3) check:where-matcher exit 1 (the new double read combinators as field names); the double now refuses them, then exit 0. Prerequisite, not findings: check:skill-examples, check:published-readme-exports and check:dual-build-cjs-loads exited 3 (unbuilt packages) and each exited 0 after building those packages; check-plugin-teardown-shape --self-test exited 3 on the shallow clone and exited 0 after fetching its pinned commit 621a4876. Roster rows that need a PR context, NOT WIRED locally: check-closing-target-claim exit 2, check-single-claim-paths exit 2, check-governed-queue-guard exit 1 (no GITHUB_EVENT_PATH); CI's reading counts. check-partof-closing-keyword run with the PR body as PR_BODY: exit 0. After merging origin/main (db1f1fd2, spec only): spec build and check:generated exit 0. Re-derived after the last commit: identical family list and roster. Not run locally: CI-owned families (pnpm lint, test:repo). PR check-runs not awaited (in_progress at report time).",
      "line_budget": "10 files, +632 / -131 = 763 changed lines against the merge-base (estimate 0.3-0.6k; cap 3,000). auth-manager.ts 255; tests 413 (new unit 224, new dogfood 102, three edited 87); tenant-audit census artefacts 76; changeset 14; engine-double ledger 5.",
      "files_changed": [
        ".changeset/24343-8b0f-grant-on-first-membership.md",
        "packages/plugins/plugin-auth/src/auth-manager.ts",
        "packages/plugins/plugin-auth/src/self-registration-grant-first-membership.test.ts",
        "packages/plugins/plugin-auth/src/audience-posture.test.ts",
        "packages/plugins/plugin-auth/src/find-envelope-limb-removal.test.ts",
        "packages/plugins/plugin-auth/src/producers-name-organization.test.ts",
        "packages/qa/dogfood/test/self-registration-grant-first-membership-walled.dogfood.test.ts",
        "content/docs/permissions/tenant-audit-census.mdx",
        "docs/audits/2026-08-tenant-audit-write-call-sites.counts.md",
        "scripts/engine-double-contract.pinned.json"
      ],
      "measurements": {
        "A1_paths": "Writers of a user's sys_member row on this tree: better-auth organization/create (creator as owner) and accept-invitation, better-auth server-side add-member, reconcileMembership (user.create.after, session.create.before, the one-time backfill), ensureDefaultOrganization's owner bind (ensure-default-organization.ts :608), and host user.create.after hooks. No SCIM or import path writes sys_member directly (they create users; membership then comes from the reconciler). All of them write through the data engine, so ql.registerMiddleware sees them; the comment at auth-manager.ts :852-:877 refutes the old adapter-bypass claim, and plugin-security's organization_admin auto-grant uses the same seam (security-plugin.ts :4617). The walled dogfood pin drives better-auth's real organization create through it: 1 grant, owned by the new organization.",
        "A1_single": "Sign-up under single ends with exactly the grant it had: one row naming member_default, owned by the Default Organization (signup-organization-census green at the final head; unit case 'writes exactly the grant it always had').",
        "A2_persistence": "The in-memory stage (10-minute TTL, keyed by email) cannot cross a restart. Nothing persisted marks a self-registrant: admission decides from the request (creation class, bootstrap probe, pending-invitation lookup; audience-posture.ts :441-:500), sys_user.source distinguishes only idp_provisioned / env_native, and sys_account.provider_id is 'credential' for admin-created users too. The narrowest existing persisted store is sys_verification: better-auth's one-shot claim table keyed by identifier with an expiry, deployment-level (platform census reach 'out', tenantField null; ownership inventory noColumn :272-:279), and swept on expiry by better-auth (internal-adapter findVerificationValue deletes expired rows). Its value column is internal: true, so the claim is read back through recoverInternalFieldsForSystemRead. No new sys_* object and no inventory row.",
        "A3_existing_null_grants": "Not touched (ceremony files are 8b-0c's). See out_of_scope_findings[0].",
        "A4_pins": "Walled: sign-up 0 sys_user_permission_set rows, the first organization 1 row with that organization, a second organization 0 more (dogfood plus unit). Single: unchanged. Ablation red, as cited in tests. The existing plugin-auth suites are green; three existing cases were updated (named in the PR body).",
        "A5_clause2": "no. No packages/spec byte, export, route or object definition changed. AudienceConfigSchema.selfRegistrationPermissionSet (auth-config.zod.ts :437-:452), the generated reference page content/docs/references/system/auth-config.mdx :57/:174, the settings manifest (auth.manifest.ts :118-:124) and its translations all say 'the permission set granted to each self-registrant'. None promises the grant at sign-up time. Releases v17 17-3 describes the declaration only."
      },
      "deviations": [
        "Worktree: the primary checkout held this branch, so it was detached (git checkout --detach) and the branch checked out in ../objectstack-issue-24343-8b0f. The empty branch was pushed before any edit.",
        "File surface: beyond auth-manager.ts and its tests, the diff carries three gate-forced riders. (a) content/docs/permissions/tenant-audit-census.mdx and docs/audits/2026-08-tenant-audit-write-call-sites.counts.md, regenerated plus prose figures, because check-tenant-audit-census holds them to the write-call-site population this stage moves. (b) scripts/engine-double-contract.pinned.json, one row. (c) A new dogfood test file (the walled real-stack pin). All are named in the PR body. No ceremony, plugin-audit, plugin-sharing, ADR-0131, plugin-email or service-settings file was touched, and no identity-mail send site was touched.",
        "Helper pair at about :5627-:5652: the doc comment of derivableWriteOrganizationId only (the grant is no longer one of its readers). No code change there.",
        "producers-name-organization.test.ts lost its two grant cases. The single one is re-pinned in the new file. The walled one asserted the retired behaviour (a grant with no organization), and the new walled pins assert the opposite. Named in the PR body.",
        "Size 763 changed lines, above the 0.3-0.6k estimate: 413 are tests and 76 are the regenerated census artefacts.",
        "origin/main (db1f1fd2, one spec-only commit) was merged once before the PR opened, per AGENTS.md section 10. The spec was rebuilt and check:generated is green."
      ],
      "mcp_calls": "0 — no MCP GitHub tool called (reads were unauthenticated curl to api.github.com).",
      "api_writes": "2 relay strokes as objectstack-fleet[bot] through scripts/pm/fleet-write/dispatch.mjs. (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24453/assignees [os-steve]; the body read back byte-identical (9333 bytes) and the assignee is os-steve. (2) This report: POST /repos/objectstack-ai/objectstack/issues/24343/comments. git push: 9 pushes to the branch (not REST). No labels written: none named, and the PR carries a changeset.",
      "open_questions": [
        {
          "question": "Where the pending grant lives between sign-up and the first membership, and for how long. Built: a sys_verification claim with a 365-day expiry.",
          "options": [
            "A. sys_verification claim, 365 days (built). Existing deployment-level one-shot store; better-auth sweeps it on expiry; no new object; a registrant who joins after a year holds only the baseline.",
            "B. The same claim with no practical expiry (a far-future expires_at). No lost grant, but an orphan row per never-joining registrant, kept forever.",
            "C. A dedicated pending-grant object. Most explicit, but it is a new sys_* object, needs an ownership-inventory row in 8b-0c's file, and is a new capability surface."
          ],
          "recommendation": "A. Real demand: the one walled deployment measured (cloud#3051) holds the same set as its isDefault baseline, so a lapsed claim costs nothing there, and nothing else in the tree reads the grant in that window. Long term: the grant stays one organization row written with a membership, and the claim is short-lived plumbing in an existing store. Hard for AI to get wrong: one writer and one seam, with a loud error when no seam exists. Startup scope: no new object and no new gate. The 365 is a judgement call; the maintainer may prefer a different horizon."
        },
        {
          "question": "single with membershipPolicy 'invite-only': a self-registrant gets no membership at sign-up, so the grant now also waits for the first membership there. Before, it was written at once, owned by the Default Organization.",
          "options": [
            "A. Keep (built): the ruling says 'under every posture', and a grant with no membership is exactly what D3 retires.",
            "B. Special-case single to grant the Default Organization at sign-up even without a membership."
          ],
          "recommendation": "A. It follows the ruling literally and keeps one rule. B reintroduces a grant with no membership behind it. Under the default 'auto' policy, single is unchanged."
        }
      ],
      "out_of_scope_findings": [
        "carrier: the epic seat's next ceremony stage (8b-0c holds the ceremony) · A3, the fate of existing walled NULL-organization sys_user_permission_set rows from earlier self-registrations. Evidence: inventory organization-ownership-inventory.ts :572 gives sys_user_permission_set fate 'attribution' with anchors [], so under a wall these rows fall to plan.unattributable (organization-ownership-plan.ts :1215-:1225), and since 8c-0 they block certification. The runtime rule this stage lands: the grant belongs to the user's first membership. Recommendation: (1) a NULL row whose user holds one or more memberships is attributed to the user's earliest sys_member row (created_at ascending), the same 'first membership' the runtime uses; (2) a NULL row whose user holds none is deleted, not re-parked. The cloud#3051 report (6258022252) shows the set duplicates the isDefault baseline on the one walled deployment, and re-parking would couple the ceremony to plugin-auth's private claim shape (identifier prefix 'self-registration-grant:'). Alternative for (2): convert the row to that claim, so the runtime grants it on a later first membership. Under a wall the card's census names the self-registration grant as the only NULL writer on this table (2/3). The single-posture admin promotion grant is #11979's.",
        "class: none (observation, note only) · reach: n/a · scripts/tenant-audit-census.mjs classifies sys_verification as 'enabled' (tenancy-enabled), while scripts/platform-object-tenancy-census.json :690-:696 has it reach 'out', tenantField null, and the ownership inventory lists it noColumn. The two instruments disagree on one object. carrier: none · noted in the PR's riders · dedupe words: tenant-audit census sys_verification enabled classification managedBy better-auth",
        "class: none (doc polish, note only) · reach: n/a · AudienceConfigSchema.selfRegistrationPermissionSet's describe ('granted to each self-registrant') could say 'with the self-registrant's first membership'. Not changed here, because no published schema byte should move. carrier: none"
      ]
    }

    Generated by Claude Code

  11. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-19T05:24Z. PR #24453 (C8 stage 8b-0f, Part of #24343, Part of #15212) has merged.

    Review. The seat reviewed it in-seat at TIER_DEFAULT. It declares Clause-②: no, and no governed path is touched: check-governed-merges --pr 24453 reports "NOT governed" over 763 changed lines.

    reading value
    PR #24453 merged: true at 2026-10-19T05:22:56Z; merge commit 95246668d

    State change: none. The card stays pm:dispatched. Its remaining stage, Q2 (8b-0d), moved to 18.x with the 18.0 scope (ruling records 6259608309 and 6259698357 on #15194), so the card stays open until then.

    Delivered (ruling 6257510973 item 1, Q1 A).

    • The self-registration grant settles on the registrant's first membership, under every posture.
    • Under single it is granted at sign-up, owned by the Default Organization, as before.
    • Under a wall, sign-up writes a pending claim in sys_verification. That object is deployment-level and survives a restart; the claim expires after 365 days. A sys_member insert middleware turns the claim into exactly one grant, owned by the first membership's organization.
    • The grant is never written without an organization.

    The seat accepts the dev's choices (os-dev-report 6259665833):

    • Open question 1, A: the sys_verification claim, 365 days, with no new object.
    • Open question 2, A: under single with invite-only, the grant also waits for the first membership. The ruling says 「under every posture」.

    Carried to 18.x: the out-of-scope A3 finding, the fate of existing walled NULL self-registration grants, goes to the ceremony. The dev recommends attributing such a grant to the user's earliest membership, or deleting it when the user has none. It rides with the 18.x upgrade path.

  12. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Maintainer decision: off the 18.0 label, to 18.x · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-19T07:57Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.

    Provenance. The seat put this to the maintainer in chat, at the end of its p1 sweep: 「三张只跟多组织隔离部署(walled 模式)有关的卡挂着 v18 标签……我倾向把这三张的 v18 标签去掉,改排 18.x」. The maintainer answered 「两件事 同意」.


    Generated by Claude Code

  13. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Epic seat (epic:#15194, session_01Y5583bs2RDd53MH6kM1ca3) · 2026-10-19T12:06Z. State change: pm:dispatched → pm:blocked. Nothing is in flight on this card. Its 18.0 part has landed, and its remaining stages are on the 18.x line.

    Blocked-by: #24522

    Under ruling record 6266750393 on #15194, the 18.x line is serial and keeps main releasable after every merge. Step 0 (#24522) measures and builds the 18.0→HEAD upgrade harness. Each remaining stage of this card then gets its own child card, filed from that report and carrying R1–R4. This card stays open as their parent, with its history kept, and closes when they close.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions