Skip to content

ADR-0138 D5 ask: on a multi-organisation hosted environment a public form lands in the deployment default organisation — an app serving several legal entities needs the form to name its organisation #25574

Closed as not planned

Description

@objectstack-fleet

Ruled: 6386464761 · re-ruling of batch #9 item 1 (#25496) · letter A · 2026-10-24T16:06:19Z (多组织托管环境不提供公共表单,维持 ADR-0138 D4;pm:on-hold,Restart-when: a named hosted deployment asks for per-organization public forms;PR #25641 / #25644 不合并)
Ruled: 6377889413 · batch #9 item 1 (#25496, 6377842908) · letter B · 2026-10-24T00:43:45Z (implement ADR-0138 D5: a public form binds to an organisation at publish time)

Filed from the objectstack-ai/hotsrm design review, 2026-10-23, spec 18.0.0. Related: #23224 (E1 grants channel resolves D4's organisation), #15194 (ADR-0131 epic).

What the ADR says today

ADR-0138 D4: a public-form submission lands in the deployment's default organisation; a multi-organisation environment is refused until D5 lands. #23224 implemented the D4 resolution.

The app case

A supplier self-registration form is the front door of an SRM. A group with several legal entities runs one hosted environment with one organisation per entity (ADR-0131 line). Each entity publishes its own registration link; the application must land in that entity's organisation, because admission, approval routes and the supplier number sequence are all organisation-scoped. Under D4 every link lands in the default organisation, and the app cannot move the row afterwards (ADR-0131 D9/D10 forbid re-owning).

The app's current workaround is invited registration only (an organisation invite with the external role once #25233 lands), which drops the public link entirely. That is acceptable for v0.2 but not for a public procurement notice (SOE pack) where anyone may apply.

Wanted (D5 shape, from the app side)

- A public form binds to an organisation at publish time (the form's URL carries the organisation; the grants channel resolves it the way #23224 resolves D4).
- The guest anchor's bindings are evaluated under that organisation; everything else in ADR-0138 unchanged.
(Re-ruled A on 2026-10-24T16:06:19Z: not built. If ever built, the binding is opened by an organisation administrator inside their organisation, never declared by the package.)

Acceptance

Two organisations in one environment each publish the same package's registration form; a submission on each link lands in its own organisation with its own number sequence; a link for an organisation the form is not published to is refused.

Dedupe words: ADR-0138 D5 public form organisation multi-organisation hosted guest entry per-organisation link.


Re-filed as objectstack-fleet[bot] from #25447, same body and labels. #25447 was written off the fleet relay, under the maintainer's personal account, by a project-thread session through GitHub MCP; pm-dispatch allows content writes only through the relay. #25447's thread (triage, ruling, comments) stays there for reference; this card replaces it in the queue.
Session: session_018fnREjMeK9XQeHDwkJXKMW

Activity

  1. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Retriage request (lane for one cross-domain PR) · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T09:30Z

    ⛔ Not a claim; the card stays pm:queue and undispatched.

    What blocks dispatch. Ruling B (6377889413; reconciliation 6377958424 on the predecessor #25447) says to implement ADR-0138 D5: "a public form binds to an organisation at publish time … the guest anchor's bindings evaluate under that organisation". The first-touch row for #25447 (round record 6369194370 on #6015) reads "server / spec". Read on origin/main e649c2d91e, the D4 resolution this extends lives in packages/core/src/security/guest-organization.ts and its neighbours (domain:server). Only the publish-time binding on the form is packages/spec. Also, ADR-0138 D5 reads "shape only, built when demand arrives", so the ADR's own text is a Tier H edit that must ride a separate docs PR.

    The answer asked for (one of):

    • A: designate one lane to claim a single cross-domain PR (the spec binding plus the guest-organization resolution and the form door), with the claim declaring both surfaces. By weight of code this is more server than spec.
    • B: split it into a spec card (the binding key, landing with its consumer) and a server card, and name the lanes. Either way, the ADR-0138 dated note is its own Tier H PR.

    Generated by Claude Code

  2. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    priority:p1High: required for production / M2
    and removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 24, 2026
  3. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Retriage answer: A, one cross-domain PR claimed by domain:spec · p2 → p1 · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-24T10:03Z

    The answer to 6382575332. pm:retriage comes off; pm:queue stays. ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01PC4NPXPgPmpDRG9DdDnyXT
    Account: sunhaolin (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-25574-public-form-organization
    Worktree: objectstack-issue-25574
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface: under triage's cross-domain answer A (6382869387), one PR. (1) packages/spec: the publish-time organization binding on a public form, in the schema that declares a form's public publication (the dev locates it on origin/main and names it), its tests, and regenerated shards. (2) domain:server surfaces: packages/core/src/security/guest-organization.ts and its neighbours in packages/core/src/security/ (resolve the guest's organization from the form's binding in place of today's multi-organization refusal), the form door packages/rest/src/form-routes.ts (the link names the organization; a link for an organization the form is not published to is refused), and their tests. (3) One changeset .changeset/25574-public-form-organization.md. The ADR-0138 dated note ("D5 is now built") is a separate Tier H docs PR, Refs #25574, on its own branch claude/issue-25574-adr-0138-d5-note; it does not hold the code PR. The cross-lane note goes to seat post #6021. (stop on breach; explain in the report)
    Container & model: L, mode:cloud (spec plus packages/core and packages/rest ⇒ build-heavy, one card per container), model: default (TIER_DEFAULT; dispatch-gates --tier for this surface: no path-derived mandate), effort: xhigh (p1; an anonymous-principal boundary). Clause-②: yes ⇒ a CONTRACT_REVIEW_TIER contract review is owed before the queue.
    Clause-②: yes (widening)
    Metadata-protocol: added
    Responsibility: n/a — not a defect card
    Ruling-ref: 6377889413 (on #25447, batch #9 item 1, letter B); the governing record is the later ruling per reconciliation 6377958424; retriage answer 6382869387 (A, one PR claimed by domain:spec, p1)
    Thread-read: 6382869387
    Serial constraints cleared: PR #25620 (#25459, this seat) adds one door pin in packages/rest/src/public-form-section-i18n.test.ts. It is a different file from form-routes.ts, and both PRs test the same GET /forms/:slug door, so whichever lands second merges origin/main and re-runs the door's tests. No other open PR touches packages/core/src/security/, packages/rest/src/form* or the form schema (all open PRs' file lists read 2026-10-24T10:26Z); no open pm:dispatched claim declares them. This seat's #25580 (automation/flow.zod.ts, runtime/action-execution.ts, mcp) is disjoint.
    Premise: holds on origin/main 7265b33e77 — packages/core/src/security/guest-organization.ts still refuses a public-form submission on a multi-organization deployment pending ADR-0138 D5 (its header at about :13–:38 and the refusal message at about :60: "own yet (ADR-0138 D5). Nothing was read or written.").


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatched · round 1 · domain:spec seat 2 · 2026-10-24T10:29Z

    • Back end: mode:cloud, cloud session session_01KzNShz7MFV7SFobNr9FWFf (created by this seat's session session_01PC4NPXPgPmpDRG9DdDnyXT through create_session, outcome branch as claim 6383098358 names). Liveness probes and any takeover read that session first; the claim stays the identity.
    • Order given: the ruling verbatim, triage's cross-domain answer A, the PM's mechanism assumptions marked as leads to re-measure, the consumer-package test list, the Writes: line. The cross-lane note for the domain:server surfaces is on seat post [PM seat] domain:services — ⏳ vacant · archive of domain:server seat 1 (#6367) #6021.

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25574,
      "status": "done",
      "branch": "claude/issue-25574-public-form-organization",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25641",
      "session": "session_01KzNShz7MFV7SFobNr9FWFf",
      "premise_still_valid": true,
      "summary": "ADR-0138 D5 built for the public form doors. The binding sits on the one declaration of a form's public publication, `SharingConfigSchema` in packages/spec/src/ui/sharing.zod.ts (carried by FormViewSchema.sharing): a new `organizations` key, a non-empty list of organization slugs, read by `anonymousFormIntakeOrganizations` (spec/ui, re-exported by metadata-core). `resolvePublicFormOrganization` (packages/core/src/security/public-form-organization.ts, beside guest-organization.ts) decides unbound / bound / refuse; both form doors (packages/rest/src/form-routes.ts resolveFormBySlug) read `?organization=SLUG`, answer every refusal as the unpublished form (404 FORM_NOT_FOUND, byte-identical), and the submit door threads the bound organization id as the write's tenantId. The unbound form keeps D4 exactly; a link naming an organization on an unbound form is refused, never ignored. Booted acceptance measured on app-showcase at OS_TENANCY_POSTURE=isolated with three organizations: acme and globex links each landed in their own organization with their own INQ sequence, initech (not listed), no organization, a case variant, a repeated parameter and an unknown slug all 404 with nothing written. ADR note PR: https://github.com/objectstack-ai/objectstack/pull/25644 (branch claude/issue-25574-adr-0138-d5-note, `Refs #25574`, Tier H, carries the 维护者速读 section; only docs/adr/0138-…md). Both PRs draft, assignee sunhaolin; worktrees removed.",
      "tests": "At head c990aed37 (after merging origin/main, which brought #25620): packages/spec src/ui/sharing.test.ts + anonymous-form-intake.test.ts 2 files/46 tests pass; spec src/ui 153 files/5623 tests pass. packages/core new src/security/public-form-organization.test.ts 15/15; core src/security 55 files/1118 tests pass. packages/metadata-core 19 files/800 tests pass (incl. bound-form availability on isolated/group and binding-omitted fails closed). packages/metadata-protocol 263 files/21473 tests pass (3 skipped). packages/rest full suite 321 files/6412 tests pass (pre-merge); post-merge the nine public-form* files incl. public-form-section-i18n.test.ts and new public-form-organization.test.ts 9 files/127 tests pass; rest typecheck + check:test-typecheck OK. Typecheck clean for spec, core, metadata-core, metadata-protocol, rest. packages/qa/dogfood: no public-form test reads the new key; not run locally, declared to CI. Ablation via scripts/ablation-replace.mjs on packages/rest/src/form-routes.ts (src-resolved, no dist leg): (1) delete the tenantId threading -> anchor 1->0, 4 of 15 tests red, blob restored identical; (2) delete `if (organization.kind === 'refuse') return null;` -> anchor 1->0, 7 of 15 red, restored identical. Booted measurement: commands and output in the PR body section 'Booted measurement'.",
      "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at c990aed37 derived 119; all 119 run, exits captured before any pipe, all exit 0 (check-plugin-teardown-shape.mjs --self-test first exit 3 on the shallow clone's missing pinned control commit 621a4876; after fetching that one commit exit 0); --ran: '119 derived famil(ies) accounted for — 119 run, 0 NOT-MEASURED (a DERIVED zero)'. spec check:generated: all 14 up to date post-merge (api-surface, export-origins, docs regenerated and committed pre-merge). check:dev-prereqs exit 0. ADR PR at 4512b4bb8: 20 derived, 20 run, all exit 0, --ran DERIVED zero (check-adr-symbol-anchors first exit 1 on an anchor to the not-yet-landed file; reworded to the export name). CI on #25641 at c990aed37 when reported: Lint & Repo Gates success, Build Core success, Governed Surface Queue Guard success, Check Changeset success, Spec property liveness success, Type Check source/consumer/debt success, Dogfood Regression Gate success; Type Check · workspace and Test Core (1-6/6) in_progress.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0 — no MCP GitHub tool called",
      "api_writes": "3 relay strokes — fleet-write pr_create #25641 (POST /repos/objectstack-ai/objectstack/pulls + POST /issues/25641/assignees), fleet-write pr_create #25644 (POST /pulls + POST /issues/25644/assignees, plus the relay's governed-review follow-up for a Tier H diff), post-stamped os-report comment on #25574 (POST /issues/25574/comments). git push is not a REST write.",
      "deviations": [
        "File surface beyond the claim: packages/metadata-core/src/anonymous-form-intake.ts (+ its test) and packages/metadata-protocol/src/runtime-authoring-gate.ts (one call). D4 item 5 names anonymousFormIntakeUnavailability as the form doors' walled-posture enforcer 'until D5 exists'; a bound form must pass it on a walled posture, and leaving its two non-door readers (the admin view read in rest-server.ts and the save/publish advisory) unchanged would have them tell the author a bound form is not offered while the doors serve it.",
        "PM mechanism assumption 1 disproved: guest-organization.ts is the class 4/5 endpoint doors' refusal and does not touch the form doors; the form doors met the multi-organization case as a 404 from anonymousFormIntakeUnavailability on a walled posture and as the engine's SystemWriteOrganizationRequiredError on single with several organizations. guest-organization.ts is unchanged; the new decision lives beside it.",
        "Primary checkout was detached (git checkout --detach) so the claim branch, which the harness had checked out there with no commits, could be checked out in the dedicated worktree."
      ],
      "files_changed": [
        ".changeset/25574-public-form-organization.md",
        "content/docs/references/ui/sharing.mdx",
        "content/docs/references/ui/view.mdx",
        "packages/core/src/security/index.ts",
        "packages/core/src/security/public-form-organization.ts",
        "packages/core/src/security/public-form-organization.test.ts",
        "packages/metadata-core/src/anonymous-form-intake.ts",
        "packages/metadata-core/src/anonymous-form-intake.test.ts",
        "packages/metadata-protocol/src/runtime-authoring-gate.ts",
        "packages/rest/src/form-routes.ts",
        "packages/rest/src/rest-server.ts",
        "packages/rest/src/public-form-organization.test.ts",
        "packages/spec/api-surface/ui.json",
        "packages/spec/authorable-surface/ui.json",
        "packages/spec/export-origins/ui.json",
        "packages/spec/liveness/view.json",
        "packages/spec/src/ui/anonymous-form-intake.ts",
        "packages/spec/src/ui/anonymous-form-intake.test.ts",
        "packages/spec/src/ui/sharing.test.ts",
        "packages/spec/src/ui/sharing.zod.ts",
        "(ADR PR #25644) docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md"
      ],
      "open_questions": [
        {
          "question": "Identify the bound organizations by slug or by id? (decided: slug)",
          "options": [
            "A slug: what a public notice link carries and an admin sees; globally unique; a slug change breaks the link (fails closed, 404)",
            "B id: stable and always present, but opaque in a public URL and unknown to a package author"
          ],
          "recommendation": "A, implemented; recorded as a trade-off, not a fork."
        },
        {
          "question": "URL spelling of the organization (decided: query parameter `?organization=SLUG` on the existing two routes)",
          "options": [
            "A query parameter on /forms/:slug, one route, single-org URLs unchanged",
            "B a path segment, which adds a second route shape per door"
          ],
          "recommendation": "A, implemented. The routes do not adopt refuseUnknownQueryParams here (a narrowing of existing routes, out of this card)."
        },
        {
          "question": "anonymousFormIntakeUnavailability's new binding argument: required or optional? (decided: optional, omitted = unbound)",
          "options": [
            "A optional: omission fails closed (a bound form is withheld on a walled posture), no breaking signature change on a published export",
            "B required: compile-time safety, but narrows a published metadata-core export (a BREAKING changeset)"
          ],
          "recommendation": "A, implemented, because the omission fails closed."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · objectui's public form page /_console/f/:slug must forward ?organization= to the API for a bound form to render in the console; the REST door is the contract here and the console side lives in objectui · noted in PR Acceptance notes, not filed",
        "carrier: none (承接者:无) · the two form door routes read req.query (now `organization`) without a closed query-parameter set; converting existing routes is per-lane policy (AGENTS.md route rule 5) · noted in PR Acceptance notes, not filed"
      ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim addendum (amends the file surface of 6383098358, this round) · domain:spec seat 2 · 2026-10-24T12:51Z

    The dev measured where the form doors actually meet the multi-organization case (report 6384293192, deviation 2), and the claim's lead was wrong. guest-organization.ts is the refusal for the class 4/5 endpoint doors, and it stays unchanged. The form doors are refused by anonymousFormIntakeUnavailability on a walled posture, and by the engine's SystemWriteOrganizationRequiredError on single. PR #25641 declares the surface that followed. Added:

    • packages/metadata-core/src/anonymous-form-intake.ts and its test: the posture predicate takes the form's binding as an optional fourth argument; leaving it out fails closed.
    • packages/metadata-protocol/src/runtime-authoring-gate.ts (one call) and packages/rest/src/rest-server.ts (the administrator's view read): the predicate's two non-door readers. Each passes the binding, so neither tells an author that a bound form is not offered while the doors serve it.
    • packages/core/src/security/public-form-organization.ts with its test and the security/index.ts export: the new decision, placed beside guest-organization.ts in place of an edit to it. It is in the claimed packages/core/src/security/ neighbourhood and is named here because the claim named a different file.
    • packages/spec/src/ui/anonymous-form-intake.ts with its test: the reader anonymousFormIntakeOrganizations beside the binding.
    • Generated files from the spec change (api-surface, authorable-surface, export-origins, liveness/view.json, content/docs/references/ui/{sharing,view}.mdx).
    • Patch round 1, now on the dev: packages/rest/src/rest-server-canonical-query-ast.test.ts. The new organization probe is a server-built query: slot in form-routes.ts, which the census pins.

    metadata-core, metadata-protocol and rest-server.ts are domain:server surfaces; the cross-lane note on #6021 covers them. Nothing here is a governed path (check-governed-merges --pr 25641: not governed, 634 lines).


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Tier H record → PR #25644 comment 6384528458 · domain:spec seat 2 · 2026-10-24T13:03Z · awaiting a human merge, after PR #25641. PR #25641 is in patch round 1 (the rest query-slot census, Test Core (6/6)); its ACCEPT follows the round-2 report.

  9. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25574,
      "status": "done",
      "branch": "claude/issue-25574-public-form-organization",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25641",
      "session": "session_01KzNShz7MFV7SFobNr9FWFf",
      "premise_still_valid": true,
      "summary": "Patch round 1: the red `Test Core (6/6)` on c990aed37 was `@objectstack/rest#test:repo`, reproduced locally as `expected 5 to be 4` in src/rest-server-canonical-query-ast.test.ts (the line-initial `query:` census). Cause as the seat read it: the bound form's organization probe in form-routes.ts adds one canonical slot. Fixed the census's way, probe unchanged: `REGISTRAR_QUERY_SLOT_FLOORS['form-routes.ts']` 0 -> 1, the sum control 4 -> 5, a dated [#25574] entry in the floors docblock and the sum control's comment, and the slice-11 sentence restated in the past tense and marked superseded for that file. New head 39d31d67b. ADR-0138 D5 built for the public form doors. The binding sits on the one declaration of a form's public publication, `SharingConfigSchema` in packages/spec/src/ui/sharing.zod.ts (carried by FormViewSchema.sharing): a new `organizations` key, a non-empty list of organization slugs, read by `anonymousFormIntakeOrganizations` (spec/ui, re-exported by metadata-core). `resolvePublicFormOrganization` (packages/core/src/security/public-form-organization.ts, beside guest-organization.ts) decides unbound / bound / refuse; both form doors (packages/rest/src/form-routes.ts resolveFormBySlug) read `?organization=SLUG`, answer every refusal as the unpublished form (404 FORM_NOT_FOUND, byte-identical), and the submit door threads the bound organization id as the write's tenantId. The unbound form keeps D4 exactly; a link naming an organization on an unbound form is refused, never ignored. Booted acceptance measured on app-showcase at OS_TENANCY_POSTURE=isolated with three organizations: acme and globex links each landed in their own organization with their own INQ sequence, initech (not listed), no organization, a case variant, a repeated parameter and an unknown slug all 404 with nothing written. ADR note PR: https://github.com/objectstack-ai/objectstack/pull/25644 (branch claude/issue-25574-adr-0138-d5-note, `Refs #25574`, Tier H, carries the 维护者速读 section; only docs/adr/0138-…md). Both PRs draft, assignee sunhaolin; worktrees removed.",
      "tests": "Round 2 at head 39d31d67b: `pnpm --filter @objectstack/rest run test:repo` before the fix: 'Test Files 1 failed | 4 passed (5)', 'AssertionError: expected 5 to be 4'; after: EXIT 0, 'Test Files 5 passed (5)', 'Tests 280 passed | 18 skipped (298)'. `pnpm --filter @objectstack/core run test:repo`: EXIT 0, 'Test Files 6 passed (6)', 'Tests 82 passed (82)'. `pnpm --filter @objectstack/spec run test:repo`: EXIT 0, 'Test Files 79 passed (79)', 'Tests 1269 passed (1269)'. metadata-core and metadata-protocol declare no test:repo script (checked in each package.json). rest typecheck + check:test-typecheck OK at 39d31d67b. Round 1 readings, unchanged: At head c990aed37 (after merging origin/main, which brought #25620): packages/spec src/ui/sharing.test.ts + anonymous-form-intake.test.ts 2 files/46 tests pass; spec src/ui 153 files/5623 tests pass. packages/core new src/security/public-form-organization.test.ts 15/15; core src/security 55 files/1118 tests pass. packages/metadata-core 19 files/800 tests pass (incl. bound-form availability on isolated/group and binding-omitted fails closed). packages/metadata-protocol 263 files/21473 tests pass (3 skipped). packages/rest full suite 321 files/6412 tests pass (pre-merge); post-merge the nine public-form* files incl. public-form-section-i18n.test.ts and new public-form-organization.test.ts 9 files/127 tests pass; rest typecheck + check:test-typecheck OK. Typecheck clean for spec, core, metadata-core, metadata-protocol, rest. packages/qa/dogfood: no public-form test reads the new key; not run locally, declared to CI. Ablation via scripts/ablation-replace.mjs on packages/rest/src/form-routes.ts (src-resolved, no dist leg): (1) delete the tenantId threading -> anchor 1->0, 4 of 15 tests red, blob restored identical; (2) delete `if (organization.kind === 'refuse') return null;` -> anchor 1->0, 7 of 15 red, restored identical. Booted measurement: commands and output in the PR body section 'Booted measurement'.",
      "gates": "Round 2 at 39d31d67b: the derived list is identical to round 1's (119 commands); all 119 re-run, exits captured before any pipe; 117 exit 0 first time, `spec check:skill-examples` and `check:dual-build-cjs-loads` first exit 3 (PREREQUISITE NOT MET: the recreated worktree had only the rest closure built), after `pnpm build` both exit 0; --ran: '119 derived famil(ies) accounted for — 119 run, 0 NOT-MEASURED (a DERIVED zero)'. CI on 39d31d67b when reported: 33 check-runs success, Test Core (3/6) and (4/6) in_progress, none failed. Round 1: Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at c990aed37 derived 119; all 119 run, exits captured before any pipe, all exit 0 (check-plugin-teardown-shape.mjs --self-test first exit 3 on the shallow clone's missing pinned control commit 621a4876; after fetching that one commit exit 0); --ran: '119 derived famil(ies) accounted for — 119 run, 0 NOT-MEASURED (a DERIVED zero)'. spec check:generated: all 14 up to date post-merge (api-surface, export-origins, docs regenerated and committed pre-merge). check:dev-prereqs exit 0. ADR PR at 4512b4bb8: 20 derived, 20 run, all exit 0, --ran DERIVED zero (check-adr-symbol-anchors first exit 1 on an anchor to the not-yet-landed file; reworded to the export name). CI on #25641 at c990aed37 when reported: Lint & Repo Gates success, Build Core success, Governed Surface Queue Guard success, Check Changeset success, Spec property liveness success, Type Check source/consumer/debt success, Dogfood Regression Gate success; Type Check · workspace and Test Core (1-6/6) in_progress.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0 — no MCP GitHub tool called (round 1 or 2)",
      "api_writes": "4 relay strokes in all — round 1: fleet-write pr_create #25641 (POST /pulls + POST /issues/25641/assignees), fleet-write pr_create #25644 (POST /pulls + POST /issues/25644/assignees, plus the relay's governed-review follow-up), post-stamped os-dev-report on #25574 (POST /issues/25574/comments, comment 6384293192); round 2: post-stamped os-dev-report round 2 on #25574 (POST /issues/25574/comments). git push is not a REST write.",
      "deviations": [
        "File surface beyond the claim: packages/metadata-core/src/anonymous-form-intake.ts (+ its test) and packages/metadata-protocol/src/runtime-authoring-gate.ts (one call). D4 item 5 names anonymousFormIntakeUnavailability as the form doors' walled-posture enforcer 'until D5 exists'; a bound form must pass it on a walled posture, and leaving its two non-door readers (the admin view read in rest-server.ts and the save/publish advisory) unchanged would have them tell the author a bound form is not offered while the doors serve it.",
        "PM mechanism assumption 1 disproved: guest-organization.ts is the class 4/5 endpoint doors' refusal and does not touch the form doors; the form doors met the multi-organization case as a 404 from anonymousFormIntakeUnavailability on a walled posture and as the engine's SystemWriteOrganizationRequiredError on single with several organizations. guest-organization.ts is unchanged; the new decision lives beside it.",
        "Primary checkout was detached (git checkout --detach) so the claim branch, which the harness had checked out there with no commits, could be checked out in the dedicated worktree.",
        "Round 1's reported packages/rest count (321 files / 6412 tests) was the `local` vitest project only (`pnpm test`); the `repo` project (`test:repo`, 5 files) was never run, which is how the census red reached CI. Round 2 runs test:repo for rest, core and spec.",
        "The PR body's Tests and Gates sections still cite head c990aed37 and the round-1 counts: the dev writes the body once and does not PATCH it. Suggested seat edit: under ## Tests add 'Round 2 at 39d31d67b: `pnpm --filter @objectstack/rest run test:repo` 5 files / 280 passed, 18 skipped (the query-slot census now counts the form doors' organization read: form-routes 0 -> 1, sum 4 -> 5); core test:repo 6/82, spec test:repo 79/1269'; under ## Gates replace the head with 39d31d67b (119/119, DERIVED zero)."
      ],
      "files_changed": [
        ".changeset/25574-public-form-organization.md",
        "content/docs/references/ui/sharing.mdx",
        "content/docs/references/ui/view.mdx",
        "packages/core/src/security/index.ts",
        "packages/core/src/security/public-form-organization.ts",
        "packages/core/src/security/public-form-organization.test.ts",
        "packages/metadata-core/src/anonymous-form-intake.ts",
        "packages/metadata-core/src/anonymous-form-intake.test.ts",
        "packages/metadata-protocol/src/runtime-authoring-gate.ts",
        "packages/rest/src/form-routes.ts",
        "packages/rest/src/rest-server.ts",
        "packages/rest/src/public-form-organization.test.ts",
        "packages/spec/api-surface/ui.json",
        "packages/spec/authorable-surface/ui.json",
        "packages/spec/export-origins/ui.json",
        "packages/spec/liveness/view.json",
        "packages/spec/src/ui/anonymous-form-intake.ts",
        "packages/spec/src/ui/anonymous-form-intake.test.ts",
        "packages/spec/src/ui/sharing.test.ts",
        "packages/spec/src/ui/sharing.zod.ts",
        "packages/rest/src/rest-server-canonical-query-ast.test.ts",
        "(ADR PR #25644) docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md"
      ],
      "open_questions": [
        {
          "question": "Identify the bound organizations by slug or by id? (decided: slug)",
          "options": [
            "A slug: what a public notice link carries and an admin sees; globally unique; a slug change breaks the link (fails closed, 404)",
            "B id: stable and always present, but opaque in a public URL and unknown to a package author"
          ],
          "recommendation": "A, implemented; recorded as a trade-off, not a fork."
        },
        {
          "question": "URL spelling of the organization (decided: query parameter `?organization=SLUG` on the existing two routes)",
          "options": [
            "A query parameter on /forms/:slug, one route, single-org URLs unchanged",
            "B a path segment, which adds a second route shape per door"
          ],
          "recommendation": "A, implemented. The routes do not adopt refuseUnknownQueryParams here (a narrowing of existing routes, out of this card)."
        },
        {
          "question": "anonymousFormIntakeUnavailability's new binding argument: required or optional? (decided: optional, omitted = unbound)",
          "options": [
            "A optional: omission fails closed (a bound form is withheld on a walled posture), no breaking signature change on a published export",
            "B required: compile-time safety, but narrows a published metadata-core export (a BREAKING changeset)"
          ],
          "recommendation": "A, implemented, because the omission fails closed."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · objectui's public form page /_console/f/:slug must forward ?organization= to the API for a bound form to render in the console; the REST door is the contract here and the console side lives in objectui · noted in PR Acceptance notes, not filed",
        "carrier: none (承接者:无) · the two form door routes read req.query (now `organization`) without a closed query-parameter set; converting existing routes is per-lane policy (AGENTS.md route rule 5) · noted in PR Acceptance notes, not filed"
      ],
      "round": 2
    }

    Generated by Claude Code

  10. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25641 comment 6384989912 · domain:spec seat 2 · 2026-10-24T13:49Z · after round 2 (the rest census and forms.mdx). Waits for the CONTRACT_REVIEW_TIER review (quota wall; it reads PR #25644 in the same pass) and green checks.

  11. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25574,
      "status": "done",
      "branch": "claude/issue-25574-public-form-organization",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25641",
      "session": "session_01KzNShz7MFV7SFobNr9FWFf",
      "premise_still_valid": true,
      "summary": "Patch round 1 addendum (same round 2): content/docs/ui/forms.mdx corrected — the walled-posture bullet now states the exception (a form listing `sharing.organizations` is offered on any posture at `/forms/:slug?organization=ORG_SLUG`, its submission carries that organization, and neither the view read nor a save warns for it); a new subsection 'Publishing one form to several organizations' documents the key, the link parameter, that a missing, unlisted or unknown organization answers 404 FORM_NOT_FOUND like an unpublished form with nothing written, the refused near-miss spellings, and the unbound form's refusal of a named organization; the REST contract's 404 FORM_NOT_FOUND row gains the organization case. public-data-collection.mdx :51 read and left unchanged (the link only selects among the slugs the author published). New head 08b020c9d. Patch round 1: the red `Test Core (6/6)` on c990aed37 was `@objectstack/rest#test:repo`, reproduced locally as `expected 5 to be 4` in src/rest-server-canonical-query-ast.test.ts (the line-initial `query:` census). Cause as the seat read it: the bound form's organization probe in form-routes.ts adds one canonical slot. Fixed the census's way, probe unchanged: `REGISTRAR_QUERY_SLOT_FLOORS['form-routes.ts']` 0 -> 1, the sum control 4 -> 5, a dated [#25574] entry in the floors docblock and the sum control's comment, and the slice-11 sentence restated in the past tense and marked superseded for that file. New head 39d31d67b. ADR-0138 D5 built for the public form doors. The binding sits on the one declaration of a form's public publication, `SharingConfigSchema` in packages/spec/src/ui/sharing.zod.ts (carried by FormViewSchema.sharing): a new `organizations` key, a non-empty list of organization slugs, read by `anonymousFormIntakeOrganizations` (spec/ui, re-exported by metadata-core). `resolvePublicFormOrganization` (packages/core/src/security/public-form-organization.ts, beside guest-organization.ts) decides unbound / bound / refuse; both form doors (packages/rest/src/form-routes.ts resolveFormBySlug) read `?organization=SLUG`, answer every refusal as the unpublished form (404 FORM_NOT_FOUND, byte-identical), and the submit door threads the bound organization id as the write's tenantId. The unbound form keeps D4 exactly; a link naming an organization on an unbound form is refused, never ignored. Booted acceptance measured on app-showcase at OS_TENANCY_POSTURE=isolated with three organizations: acme and globex links each landed in their own organization with their own INQ sequence, initech (not listed), no organization, a case variant, a repeated parameter and an unknown slug all 404 with nothing written. ADR note PR: https://github.com/objectstack-ai/objectstack/pull/25644 (branch claude/issue-25574-adr-0138-d5-note, `Refs #25574`, Tier H, carries the 维护者速读 section; only docs/adr/0138-…md). Both PRs draft, assignee sunhaolin; worktrees removed.",
      "tests": "Round 2 at head 39d31d67b: `pnpm --filter @objectstack/rest run test:repo` before the fix: 'Test Files 1 failed | 4 passed (5)', 'AssertionError: expected 5 to be 4'; after: EXIT 0, 'Test Files 5 passed (5)', 'Tests 280 passed | 18 skipped (298)'. `pnpm --filter @objectstack/core run test:repo`: EXIT 0, 'Test Files 6 passed (6)', 'Tests 82 passed (82)'. `pnpm --filter @objectstack/spec run test:repo`: EXIT 0, 'Test Files 79 passed (79)', 'Tests 1269 passed (1269)'. metadata-core and metadata-protocol declare no test:repo script (checked in each package.json). rest typecheck + check:test-typecheck OK at 39d31d67b. Round 1 readings, unchanged: At head c990aed37 (after merging origin/main, which brought #25620): packages/spec src/ui/sharing.test.ts + anonymous-form-intake.test.ts 2 files/46 tests pass; spec src/ui 153 files/5623 tests pass. packages/core new src/security/public-form-organization.test.ts 15/15; core src/security 55 files/1118 tests pass. packages/metadata-core 19 files/800 tests pass (incl. bound-form availability on isolated/group and binding-omitted fails closed). packages/metadata-protocol 263 files/21473 tests pass (3 skipped). packages/rest full suite 321 files/6412 tests pass (pre-merge); post-merge the nine public-form* files incl. public-form-section-i18n.test.ts and new public-form-organization.test.ts 9 files/127 tests pass; rest typecheck + check:test-typecheck OK. Typecheck clean for spec, core, metadata-core, metadata-protocol, rest. packages/qa/dogfood: no public-form test reads the new key; not run locally, declared to CI. Ablation via scripts/ablation-replace.mjs on packages/rest/src/form-routes.ts (src-resolved, no dist leg): (1) delete the tenantId threading -> anchor 1->0, 4 of 15 tests red, blob restored identical; (2) delete `if (organization.kind === 'refuse') return null;` -> anchor 1->0, 7 of 15 red, restored identical. Booted measurement: commands and output in the PR body section 'Booted measurement'.",
      "gates": "Round 2 final at 08b020c9d (after the doc fix): derived list unchanged (119); all 119 re-run, exits captured before any pipe, all exit 0 first time; --ran: '119 derived famil(ies) accounted for — 119 run, 0 NOT-MEASURED (a DERIVED zero)'. Earlier in round 2 at 39d31d67b: the derived list is identical to round 1's (119 commands); all 119 re-run, exits captured before any pipe; 117 exit 0 first time, `spec check:skill-examples` and `check:dual-build-cjs-loads` first exit 3 (PREREQUISITE NOT MET: the recreated worktree had only the rest closure built), after `pnpm build` both exit 0; --ran: '119 derived famil(ies) accounted for — 119 run, 0 NOT-MEASURED (a DERIVED zero)'. CI on 39d31d67b when reported: 33 check-runs success, Test Core (3/6) and (4/6) in_progress, none failed. Round 1: Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at c990aed37 derived 119; all 119 run, exits captured before any pipe, all exit 0 (check-plugin-teardown-shape.mjs --self-test first exit 3 on the shallow clone's missing pinned control commit 621a4876; after fetching that one commit exit 0); --ran: '119 derived famil(ies) accounted for — 119 run, 0 NOT-MEASURED (a DERIVED zero)'. spec check:generated: all 14 up to date post-merge (api-surface, export-origins, docs regenerated and committed pre-merge). check:dev-prereqs exit 0. ADR PR at 4512b4bb8: 20 derived, 20 run, all exit 0, --ran DERIVED zero (check-adr-symbol-anchors first exit 1 on an anchor to the not-yet-landed file; reworded to the export name). CI on #25641 at c990aed37 when reported: Lint & Repo Gates success, Build Core success, Governed Surface Queue Guard success, Check Changeset success, Spec property liveness success, Type Check source/consumer/debt success, Dogfood Regression Gate success; Type Check · workspace and Test Core (1-6/6) in_progress.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0 — no MCP GitHub tool called (round 1 or 2)",
      "api_writes": "5 relay strokes in all — round 1: fleet-write pr_create #25641 (POST /pulls + POST /issues/25641/assignees), fleet-write pr_create #25644 (POST /pulls + POST /issues/25644/assignees, plus the relay's governed-review follow-up), post-stamped os-dev-report on #25574 (POST /issues/25574/comments, comment 6384293192); round 2: post-stamped os-dev-report round 2 (this comment, superseding round-2 comment 6384796585 posted before the doc addendum arrived) and the earlier round-2 comment 6384796585 on #25574 (POST /issues/25574/comments). git push is not a REST write.",
      "deviations": [
        "File surface beyond the claim: packages/metadata-core/src/anonymous-form-intake.ts (+ its test) and packages/metadata-protocol/src/runtime-authoring-gate.ts (one call). D4 item 5 names anonymousFormIntakeUnavailability as the form doors' walled-posture enforcer 'until D5 exists'; a bound form must pass it on a walled posture, and leaving its two non-door readers (the admin view read in rest-server.ts and the save/publish advisory) unchanged would have them tell the author a bound form is not offered while the doors serve it.",
        "PM mechanism assumption 1 disproved: guest-organization.ts is the class 4/5 endpoint doors' refusal and does not touch the form doors; the form doors met the multi-organization case as a 404 from anonymousFormIntakeUnavailability on a walled posture and as the engine's SystemWriteOrganizationRequiredError on single with several organizations. guest-organization.ts is unchanged; the new decision lives beside it.",
        "Primary checkout was detached (git checkout --detach) so the claim branch, which the harness had checked out there with no commits, could be checked out in the dedicated worktree.",
        "Round 1's reported packages/rest count (321 files / 6412 tests) was the `local` vitest project only (`pnpm test`); the `repo` project (`test:repo`, 5 files) was never run, which is how the census red reached CI. Round 2 runs test:repo for rest, core and spec.",
        "The PR body's Tests and Gates sections still cite head c990aed37 and the round-1 counts, and it does not name the doc change: the dev writes the body once and does not PATCH it. Suggested seat edit: under ## Tests add 'Round 2 at 08b020c9d: `pnpm --filter @objectstack/rest run test:repo` 5 files / 280 passed, 18 skipped (the query-slot census counts the form doors' organization read: form-routes 0 -> 1, sum 4 -> 5); core test:repo 6/82; spec test:repo 79/1269'; under ## Gates replace the head with 08b020c9d (119/119, DERIVED zero); under ## What changes add 'Docs: content/docs/ui/forms.mdx states the bound-form exception to the walled-posture rule and documents sharing.organizations, the ?organization= link and its 404 FORM_NOT_FOUND refusals'."
      ],
      "files_changed": [
        ".changeset/25574-public-form-organization.md",
        "content/docs/references/ui/sharing.mdx",
        "content/docs/references/ui/view.mdx",
        "packages/core/src/security/index.ts",
        "packages/core/src/security/public-form-organization.ts",
        "packages/core/src/security/public-form-organization.test.ts",
        "packages/metadata-core/src/anonymous-form-intake.ts",
        "packages/metadata-core/src/anonymous-form-intake.test.ts",
        "packages/metadata-protocol/src/runtime-authoring-gate.ts",
        "packages/rest/src/form-routes.ts",
        "packages/rest/src/rest-server.ts",
        "packages/rest/src/public-form-organization.test.ts",
        "packages/spec/api-surface/ui.json",
        "packages/spec/authorable-surface/ui.json",
        "packages/spec/export-origins/ui.json",
        "packages/spec/liveness/view.json",
        "packages/spec/src/ui/anonymous-form-intake.ts",
        "packages/spec/src/ui/anonymous-form-intake.test.ts",
        "packages/spec/src/ui/sharing.test.ts",
        "packages/spec/src/ui/sharing.zod.ts",
        "packages/rest/src/rest-server-canonical-query-ast.test.ts",
        "content/docs/ui/forms.mdx",
        "(ADR PR #25644) docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md"
      ],
      "open_questions": [
        {
          "question": "Identify the bound organizations by slug or by id? (decided: slug)",
          "options": [
            "A slug: what a public notice link carries and an admin sees; globally unique; a slug change breaks the link (fails closed, 404)",
            "B id: stable and always present, but opaque in a public URL and unknown to a package author"
          ],
          "recommendation": "A, implemented; recorded as a trade-off, not a fork."
        },
        {
          "question": "URL spelling of the organization (decided: query parameter `?organization=SLUG` on the existing two routes)",
          "options": [
            "A query parameter on /forms/:slug, one route, single-org URLs unchanged",
            "B a path segment, which adds a second route shape per door"
          ],
          "recommendation": "A, implemented. The routes do not adopt refuseUnknownQueryParams here (a narrowing of existing routes, out of this card)."
        },
        {
          "question": "anonymousFormIntakeUnavailability's new binding argument: required or optional? (decided: optional, omitted = unbound)",
          "options": [
            "A optional: omission fails closed (a bound form is withheld on a walled posture), no breaking signature change on a published export",
            "B required: compile-time safety, but narrows a published metadata-core export (a BREAKING changeset)"
          ],
          "recommendation": "A, implemented, because the omission fails closed."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · objectui's public form page /_console/f/:slug must forward ?organization= to the API for a bound form to render in the console; the REST door is the contract here and the console side lives in objectui · noted in PR Acceptance notes, not filed",
        "carrier: none (承接者:无) · the two form door routes read req.query (now `organization`) without a closed query-parameter set; converting existing routes is per-lane policy (AGENTS.md route rule 5) · noted in PR Acceptance notes, not filed",
        "carrier: none (承接者:无) · content/docs/ui/forms.mdx §11 checklist line 'submissions land on the project resolved from hostname/path' predates this PR and was already inaccurate (no hostname/path resolution exists); not made more false here, left unchanged · noted, not filed"
      ],
      "round": 2
    }

    Generated by Claude Code

  12. os-elon-musk commented on Oct 24, 2026

    @os-elon-musk
    Collaborator

    Ruling: re-ruling of batch #9 item 1 sub-item #25447 (re-filed as this card) · letter A · maintainer 「25447 A」 2026-10-24T16:06:19Z · supersedes B (6377889413, 「#9 同意」 00:43:45Z)

    A = 多组织托管环境不提供公共表单。维持 ADR-0138 D4 今天的做法(多组织托管环境上公共表单走廊态 404 + 管理员告警),⛔ 不建 D5 的「包作者在表单上点名组织」式绑定。

    维护者原话(本线程,2026-10-24 16:03–16:06Z):

    • 「ADR-0138 不现实啊,我开发元数据应用的时候,根本不知道哪个组织用啊」
    • 「为什么需要这个需求,如果 多组织托管环境里 不支持公共表单是不是最简单?」
    • 「25447 A」

    裁后执行(A 的工作量)

    • 本卡 pm:dispatched → pm:on-hold(本笔换标)。Restart-when: a named hosted deployment (a hotsrm public-notice release or a cloud customer) asks for per-organization public forms. 重启时的形态是「组织管理员在自己组织里开通并绑定公共表单」,⛔ 不是包声明 sharing.organizations。
    • PR feat(forms): a public form binds to the organizations it is published to (ADR-0138 D5) #25641(代码)与 PR docs(adr): ADR-0138 dated note — D5 is built for the public form doors #25644(ADR 说明)不合并:spec 席经 relay 关闭两个 PR,分支保留;席位同笔撤回认领 6383098358。
    • ADR-0138 D5 加一句「若将来建,绑定由组织管理员开,不由包声明」。Tier H,随 spec 车道下一个 docs PR 走,维护者手合;不单开 PR。
    • hotsrm 维持邀请制注册(卡面自述 v0.2 可接受)。

    裁不派。总裁决记录:本评论;#25447 上留指针。


    Generated by Claude Code

  13. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim withdrawn, card closed as not planned · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T16:11Z

    This executes ruling A, 6386464761 (the maintainer's 「25447 A」; it supersedes B 6377889413). It also carries the maintainer's later word in the seat's session, verbatim: 「25447 决定不支持,可以关卡 25644 作废」. That word closes the card rather than holding it, so pm:on-hold comes off in this stroke. A closed card carries no pm-state label.

    • Claim 6383098358 is withdrawn. The assignee is cleared in this stroke.
    • PR feat(forms): a public form binds to the organizations it is published to (ADR-0138 D5) #25641 (code) and PR docs(adr): ADR-0138 dated note — D5 is built for the public form doors #25644 (the ADR-0138 note) are closed unmerged through the relay in this stroke. Their branches claude/issue-25574-public-form-organization and claude/issue-25574-adr-0138-d5-note are kept. The dev's cloud session is archived.
    • Restart, as 6386464761 sets it: a named hosted deployment (a hotsrm public-notice release or a cloud customer) asks for per-organization public forms. The shape then is an organization administrator enabling and binding the public form in their own organization, ⛔ not a package declaring sharing.organizations. Reopening this card is the restart.
    • Owed to the domain:spec lane: one sentence under ADR-0138 D5 ("if built, the binding is opened by the organization administrator, not declared by the package"). It is Tier H and rides the lane's next docs PR, merged by the maintainer, not a PR of its own. It is recorded on the seat post [PM seat] domain:spec · seat 2 — 🟢 sunhaolin #18549.
    • The objectui follow-up the seat would have filed at merge (/_console/f/:slug forwarding ?organization=) is not filed.

    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

    area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specenhancementNew feature or requestpriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions