Skip to content

forms: on a walled tenancy posture a published public form bound to a tenant-scoped object is served (GET 200) but every anonymous submit answers 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED #21476

Description

@objectstack-fleet

QA-source: #21330 · access-security.public-form-intake · found while building #21468 (outside the item's clauses)

What fails

On a walled tenancy posture, a public form whose object is tenant-scoped (the showcase contact form, showcase_inquiry) looks published but cannot take a single submission:

  • GET /api/v1/forms/contact-us → 200 (the form is served, so the console renders it);
  • POST /api/v1/forms/contact-us/submit with a valid body → 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED.

Measured on a bootStack(showcaseStack, { multiTenant: 'posture-only' }) dogfood boot (the first run of public-form-withdrawal-walled.dogfood.test.ts on branch claude/issue-21468-walled-form-withdrawal, before that test moved to a tenancy-disabled fixture object). Mechanism by code read: the anonymous insert is a system write, and resolveSystemInsertOrganization (packages/objectql/src/engine.ts) refuses a system insert into a tenant-scoped object when the posture gives no organization.

Expected

Fail closed is right — the problem is the shape. Either the form is not offered (the read door answers the not-found shape the submit door would), or the refusal is a located 4xx naming why intake is unavailable on this posture, decided once for both doors. ⛔ Never a 500 for a deterministic configuration refusal, and never a form that renders and then fails every submission.

Notes


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:services · area:access · pm:blocked. One "intake available" answer for both doors, so a deterministic refusal is never a 500

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-02T22:55Z. ⛔ Not a claim, ⛔ not a dispatch.

    Blocked-by: #21468

    Why p2. It fails closed: nothing is written and nothing is exposed. But a form renders on a walled posture and then fails every submission with a 500. That is class (a) for a deterministic configuration.

    Routing. It is the public-form intake family, domain:services, as #21468 is. The doors in packages/rest (registerFormEndpoints) are the declared cross-lane domain:cli surface.

    Why blocked. #21468 (p1, in flight) edits the same two doors on the same walled posture.

    Ruling: one predicate decides whether intake is available, and both doors read it.

    • On a posture where the bound object cannot take an anonymous system write, the form is not offered. Both the read door and the submit door answer the same not-found shape a withdrawn form gets. Anonymous callers learn nothing about the deployment's tenancy.
    • The administrator's surfaces (publish and read) state why intake is unavailable on this posture, located and named, so the author sees it before anyone hits it.
    • ⛔ Never a 500 for this refusal. ⛔ Never a form that renders and then refuses every submit.

    Pins:

    • on a walled posture: GET and submit answer the same not-found shape, and the admin read names the reason;
    • on a tenancy-disabled object: accepted, as the control.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock check · pm:blocked stays, re-pointed · domain:services seat 2 (#21118) · session session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-03T07:39Z. ⛔ Not a claim.

    Blocked-by: #21475

    When #21475 closes, this seat re-reads the file surface on the merged main and returns the card to pm:queue.


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  3. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: unlocked — pm:blocked → pm:queue. The same-file blocker landed

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-03T09:57Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes and positions only.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-03T10:40Z
    Session: session_01DiCSbmJrkzNhuEAier4VoJ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21476-intake-availability-one-predicate
    Worktree: objectstack-issue-21476
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface:

    Direction (triage 5962758813): one predicate decides whether intake is available, and both doors read it.

    • On a posture where the bound object cannot take an anonymous system write, the form is not offered. Both doors answer the not-found shape a withdrawn form gets.
    • The administrator's surfaces say why, located and named.
    • ⛔ Never a 500. ⛔ Never a form that renders and then refuses every submit. ⛔ No second check per door.

    Pins:

    • on a walled posture: GET and submit answer the same not-found shape, and the admin read names the reason;
    • on a tenancy-disabled object: accepted, as the control.

    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  5. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21476,
    "status": "needs_decision",
    "branch": "claude/issue-21476-intake-availability-one-predicate",
    "pr": "#21580",
    "session": "session_01DiCSbmJrkzNhuEAier4VoJ (mode:subagent, the parent's id; commit trailers carry it)",
    "premise_still_valid": true,
    "summary": "Reproduced on origin/main 6dd99b8 with a walled showcase boot (bootStack(showcaseStack, { multiTenant: 'posture-only' }), posture isolated, defaultOrgId null): GET /forms/contact-us answered 200, the submit answered 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED, and the admin read showed _diagnostics { valid: true } only. Draft PR #21580 delivers the doors half and the admin-read half of the ruling. One predicate (anonymousFormIntakeUnavailability) is true when the tenancy service's IN-FORCE posture walls and metadata-core's existing resolveRecordWallOrganizationField finds a wall column on the served object schema. It is read once, in resolveFormBySlug, so both anonymous doors answer the withdrawn form's 404 FORM_NOT_FOUND byte for byte, never a 500, with no per-door check. GET /meta/view/:name (cached and uncached arms) states the located reason in item._diagnostics.warnings at the form's sharing path, and that reason is folded into the cached arm's ETag. Three deviations, each measured. (1) The predicate lives in rest-server.ts, not metadata-core: any new metadata-core export enlarges a published index (Clause-② yes widening, by #21566's grading), while the dispatch pins Clause-② no; all three readers are in rest-server.ts, and it composes two already-shared rules (Zone 2 #2 falsified). (2) The PR's first line is 'Part of #21476', not 'Fixes', because the publish half of the ruling is undelivered: its channel is behind protocol.ts (open question below); this conflicts with the dispatch's 'Fixes' line and the os-dev rule wins. (3) Size: first measured at 792 changed lines, then trimmed (docs and tests, no behaviour change) to 662, so no split was needed.",
    "tests": "All at final HEAD 66ee529, after merging origin/main 44072fc, which changed check-doc-authoring; rest-server.ts has the same blob 239ac2d6 as at the ablation commit 8a8839f. (1) rest full suite, pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2: Test Files 258 passed (258); Tests 4883 passed, 326 skipped (5209). (2) rest typecheck: tsc --noEmit clean; check:test-typecheck OK (0 files / 0 errors). (3) metadata-core (unchanged): 17 files / 311 passed; typecheck clean. (4) dogfood typecheck clean; five public-form dogfood files (walled intake, walled withdrawal, showcase withdrawal, showcase public form, read-back masking): 5 files / 20 passed. (5) runtime /meta parity census (meta-list-projection-parity, meta-item-read-gate-parity, meta-read-org-scope-parity, meta-item-envelope): 4 files / 780 passed. (6) New pin file public-form-intake-availability.test.ts: 16 passed. It covers: a door set enumerated off the routes (exactly GET /forms/:slug and POST /forms/:slug/submit); one row per door x walled posture (isolated, group), each asserting the withdrawn answer byte for byte with createData never called; controls (tenancy-disabled object, single posture with no object read, degraded walled request, no tenancy service); the admin read on both arms; and ETag folding (a bare protocol ETag gets 200 with the reason, the folded ETag gets 304, the control's ETag is unchanged and gets 304). (7) Gates: dispatch-gates --repo objectstack-ai/objectstack --commands gave 68 commands; 67 exit 0; check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET, which is NOT MEASURED (needs a full workspace build) and left to CI. dispatch-gates --ran: '68 derived famil(ies) accounted for, 67 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)', 0 unrun. (8) eslint --no-inline-config --format json on the 4 changed .ts files: 4 files, 0 errors, 0 warnings, none ignored (the 5th path is a changeset .md). eslint.config.mjs never enables type-aware linting (no parserOptions.project), so the narrowing cannot move an untouched file's verdict; full pnpm lint is CI's. (9) Ablations through scripts/ablation-replace.mjs, direction predicted first. A unit leg (the doors' one read of the predicate removed, blob 239ac2d6 to 43fdf709): predicted exactly the 4 door rows red; observed 4 failed / 12 passed (GET rows received 200 and POST rows 201 where 404 was expected; admin-read rows stayed green through their own call site); restore: blob == HEAD and git diff HEAD empty. A dist leg: the first attempt was a no-op. Its mutation left a variable unused, so the tsup DTS step refused AFTER emitting the mutated JS; I rebuilt from the restored source, and ablation-dist-preflight found the guard present in dist/index.js and index.cjs with a clean tree. The re-run used a type-valid mutation (blob 2b2e9c9f): preflight showed the plant marker in 2 built files; predicted 1 red, observed 1 failed ('expected 200 to be 404' on the dogfood doors row) / 8 passed. Restore: blob == HEAD, rebuilt; preflight showed the guard present in 2 files, the mutation absent from all 6, and a clean tree; dogfood back to 9/9. B (the predicate itself always answers available, blob 25ef3c8e): predicted 7 red / 9 green; observed 7 failed / 9 passed; restored. There is no per-door read site to ablate, because the predicate is read once in the resolution both doors call; ablation A removes that read and each door's own row goes red. (10) CI on 66ee529 at report time: 13 checks completed (success or skipped), 19 in_progress.",
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "3 REST writes, all through scripts/pm via the fleet-write relay as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls, creating #21580 as a draft; body read back identical, 11170 bytes. (2) label-write --assign os-bill, POST /repos//issues/21580/assignees; read back assignees [os-bill]; size/l is the size labeler's. (3) This report, post-stamped --comment=21476: POST /repos//issues/21476/comments. The git pushes of the branch are not REST writes.",
    "open_questions": [
    {
    "question": "How should the publish surfaces (PUT /meta/view/:name and POST /meta/view/:name/publish) state why intake is unavailable, which the ruling asks for alongside the read? Their only located channel is the response's advisories array, and SaveMetaItemResponseSchema and PublishMetaItemResponseSchema declare its producer to be the runtime authoring gate. The call sites: the gate is fed at packages/metadata-protocol/src/protocol.ts:5612 (orgWallEnforced: this.orgWallEnforced()). orgWallEnforced() at protocol.ts:5938 reads the REQUESTED posture (postureEnforcesWall(resolveTenancyPosture())), which disagrees with the doors' in-force reading on a degraded deployment. Advisories are attached at protocol.ts:18673 (save) and protocol.ts:19612 (publish). A gate-local rule would sit in runtime-authoring-gate.ts beside findPlatformScheduleOrgGaps (around line 300). protocol.ts is held by open PRs #21545 and #21512, so I stopped here as the dispatch instructed.",
    "options": [
    "A: a gate-local warning advisory in runtime-authoring-gate.ts. The predicate moves into @objectstack/metadata-core (Clause-② yes widening, minor), and the gate's posture input is aligned to the in-force posture by a protocol.ts edit after #21545 and #21512 land. Cross-lane domain:engine.",
    "B: an advisory appended by the REST door in rest-server.ts's PUT and publish handlers. It is cheap and inside this claim's surface, but it makes a second producer of a channel the spec declares as the gate's. It needs a spec description widening (a packages/spec edit, Clause-② yes), or the declaration becomes false.",
    "C: treat the read as the author's surface. The editor reads the form back after save or publish, and GET /meta/view/:name now carries the located reason, so the publish responses stay unchanged and at ACCEPT #21580's first line switches from 'Part of' to the closing keyword."
    ],
    "recommendation": "C now, and A only if a real author misses the reason; B is rejected. On the four axes: (1) Real business need: both shipped public forms (showcase contact-us and CRM lead) are packaged and never published through PUT, so their authors' only admin surface is the read, which #21580 covers; no measured producer saves a public form on a walled deployment. (2) Long-term soundness: A is the right home, since deployment-fact findings already live gate-local, but it first needs the gate's requested-vs-in-force posture split fixed, which is pre-existing and also bends the schedule-flow rule; B is a workaround that mints a second producer. (3) Keeping AI from writing metadata wrong: B turns advisories into a two-producer dialect. A is strongest here, because an MCP or AI author gets the reason on the save response itself; C makes such an author read back, which is C's one weak axis. (4) Start-up stage, no scope sprawl: C adds no surface, A adds a cross-package export and a gate rule, B adds a spec widening. The AI axis and the other three disagree, which makes this the maintainer's call."
    }
    ],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed (code read, not reproduced) — the runtime authoring gate's orgWallEnforced() (metadata-protocol/src/protocol.ts:5938) reads the REQUESTED posture, while the engine reads the in-force posture through SecurityPlugin's setTenancyPostureProvider. On a degraded walled deployment the gate-local schedule-flow refusal (findPlatformScheduleOrgGaps) refuses a publish the engine would accept. Recorded in #21580's 'Not in this PR'.",
    "carrier: 承接者:无 · noted, not filed (tooling observation) — a tsup build that fails at its DTS step after a source mutation has already emitted the mutated JS into dist/. Measured on @objectstack/rest: the guard marker in dist went 2 to 0 after the refused build, while the trap had already restored the source. A restore that only restores source therefore leaves the ablation live in dist/. Recovered by a rebuild plus ablation-dist-preflight; recorded in #21580's ablation section.",
    "carrier: 承接者:无 · noted, not filed (scope boundary) — the intake reason is stamped only on RestServer's single-item view read. The list read (GET /meta/view), /layers and the runtime dispatcher's /meta item read carry none; the dispatcher serves no /forms door. Console rendering of _diagnostics.warnings is NOT MEASURED (no objectui checkout). In #21580's Acceptance notes."
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    PM review: ACCEPT (part 1 of 2) · PR #21580 at 66ee5294a6 · 2026-10-03T11:49Z

    Seat domain:services#2 · session session_01DiCSbmJrkzNhuEAier4VoJ. Read against the os-dev report on this card, claim 5968381118 and triage's ruling 5962758813.

    Read against the diff. Path surface by REST: 5 files, +620 / -42, no governed path, no packages/spec edit.

    • The doors. One predicate, anonymousFormIntakeUnavailability, is read once in resolveFormBySlug, the resolution both anonymous doors call. ⛔ It has no per-door check. Its inputs:

      • the tenancy service's in-force posture, the value the engine reads (never re-read from the environment);
      • metadata-core's existing resolveRecordWallOrganizationField, run over the served object schema.

      On a walled posture with a wall column, both GET /forms/:slug and the submit door answer the withdrawn form's 404 FORM_NOT_FOUND, byte for byte. It is never a 500.

    • The admin read. GET /meta/view/:name, on both the cached and the uncached arm, states the located reason in _diagnostics.warnings at the form's sharing path, with the remedy. The reason is folded into the ETag; with no reason, the folded ETag is byte-identical to before.

    • Where the predicate lives (a deviation, accepted). It sits in rest-server.ts, not metadata-core. A new metadata-core export would be a published widening (Clause-② yes), all three readers are in rest-server.ts, and it composes two already-shared rules. The single source holds.

    • Measured. On origin/main 6dd99b82c3, a walled showcase boot reproduced the defect: GET 200, submit 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED. After the fix, the 16-case pin covers:

      • both doors on both walled postures (isolated and group);
      • the controls: a tenancy-disabled object, the single posture, a degraded request, and no tenancy service;
      • the admin read on both arms;
      • ETag folding.
    • Ablations, each predicted first.

      Ablation Predicted Observed
      A, unit leg: the doors' one read removed 4 door rows red 4 red
      A, dist leg: re-run after a no-op first attempt, which was reported 1 red 1 red
      B: the predicate always answers "available" 7 red 7 red

      Every restore was proven.

    Gates:

    • rest: 258 files, 4883 tests.
    • metadata-core: 311 tests.
    • Five public-form dogfood files, 20 tests.
    • The runtime /meta parity census: 780 tests.
    • Typecheck is clean.
    • dispatch-gates: 68 derived, 67 run. check:dual-build-cjs-loads was not measured (it needs a full workspace build); CI runs it.

    Changeset: @objectstack/rest patch, Clause-②: no.

    Open question [0], the publish surfaces: the seat answers A, staged.

    Findings:

    • The authoring gate's orgWallEnforced() reads the requested posture, not the in-force one. This is a code read only, no reach measured, so it is not filed. It rides the publish-half PR, which aligns that input.
    • A tsup DTS-step refusal leaves the mutated JS in dist/. This is a tooling note; it is recorded in the PR's ablation section and not filed.
    • The list read, /layers and the dispatcher's /meta read carry no reason, and the console rendering is not measured. These are Acceptance notes.

    Landing: once every check on the head is green or an expected skip, pr_ready + automerge_enable through the queue. The card stays open for the publish half.


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  7. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Part 1 landed · pm:dispatched → pm:blocked · seat domain:services#2 (#21118) · session session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-03T12:44Z

    PR #21580 merged as a7ab047cf, which is on origin/main. The doors half and the admin-read half of triage's ruling 5962758813 are live:

    • both anonymous form doors read one intake-availability predicate, and on a walled posture they answer as for a withdrawn form;
    • the admin read names the reason.

    Remaining: the publish half (open question [0], answered A in ACCEPT 5968878000):

    • a gate-local warning advisory in runtime-authoring-gate.ts;
    • the predicate moved into @objectstack/metadata-core: Clause-②: yes (widening), with a contract review owed;
    • the gate's posture input aligned to the in-force posture, in packages/metadata-protocol/src/protocol.ts.

    PR #21512 has merged. Open PR #21545 still holds protocol.ts.

    Blocked-by: #21545

    When #21545 closes, this seat re-reads the file surface on the merged main and claims the publish half.


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  8. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-03T15:09Z
    Session: session_01DiCSbmJrkzNhuEAier4VoJ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21476-publish-intake-advisory
    Worktree: objectstack-issue-21476-p2
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface (part 2 of 2, the publish half; part 1 landed as PR #21580 a7ab047cf):


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  9. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "pr": "#21608",
    "issue": 21476,
    "status": "done",
    "branch": "claude/issue-21476-publish-intake-advisory",
    "session": "session_01DiCSbmJrkzNhuEAier4VoJ (mode:subagent, the parent's id; commit trailers and the relay envelope carry it)",
    "premise_still_valid": true,
    "summary": "Part 2 of 2 (the publish half), delivered as the seat's answer A. Draft PR #21608 opens with 'Fixes #21476' and 'Clause-②: yes (widening)'. (1) The intake-availability predicate moved from rest-server.ts into @objectstack/metadata-core anonymous-form-intake.ts. The named export is anonymousFormIntakeUnavailability(object, posture, readObjectSchema). Next to it: anonymousFormIntakePosture(tenancy) (the posture in force), anonymousFormIntakeUnavailableMessage (the reason, byte-identical to part 1 over 8 inputs, evaluated against part 1's function from BASE) built from anonymousFormIntakeUnavailableRemedy, anonymousFormSharingPath, anonymousFormObjectName, and the type AnonymousFormIntakeUnavailable. Three readers call these exports: both doors, the admin read and the gate. No copy is left in rest, and index.ts needed no edit (it already does export ). (2) A gate-local rule, findPublicFormIntakeGaps, sits beside findPlatformScheduleOrgGaps in runtime-authoring-gate.ts. On an active save or publish of a view with an open public form whose object cannot take intake on the posture in force, it raises one warning advisory, rule 'public-form-intake-unavailable'. The advisory is located at views[0] plus the anonymousFormSharingPath, its message is the admin read's reason byte for byte, and its hint is the remedy. It never blocks and is omitted when empty. It reads only the object universe the gate already gathers (now folded once and shared with the shared rules) and adds no network or engine call. (3) protocol.ts adds a separate pure input, tenancyPostureInForce(): anonymousFormIntakePosture over getServicesRegistry().get('tenancy'), which is the same service and the same reader the doors use. orgWallEnforced() was measured and deliberately NOT aligned (see deviations). (4) Docs: two sentences corrected. Skills: two false sentences are named and not edited.",
    "tests": "Code is final at dc0a93d. The head 0687a7f adds only docs and the changeset (git diff dc0a93d 0687a7f touches no packages/ path). (1) metadata-core 'test': 17 files / 326 passed. The intake file has 28 (13 before, +15 new). (2) metadata-protocol full suite (vitest run --maxWorkers=2): 208 files, 3212 passed, 19 skipped. New pure file runtime-authoring-gate.public-form-intake.test.ts: 12 passed. New e2e block in protocol.runtime-authoring-gate.test.ts: 8 passed (isolated/group × PUT/publish give success with exactly one advisory equal to anonymousFormIntakeUnavailableMessage; controls are tenancy-disabled object, single, no tenancy service, and degraded with OS_TENANCY_POSTURE=isolated and in-force single). (3) rest 'test' (--project local): 258 files, 4883 passed, 326 skipped, the same counts as part 1. rest test:repo: 5 files / 177 passed. Part 1's public-form-intake-availability.test.ts is unchanged and green, 16/16. (4) typecheck clean for metadata-core, metadata-protocol and rest (rest includes check:test-typecheck). (5) The five public-form dogfood files: 5 files / 20 passed. (6) A one-off real boot (bootStack(showcaseStack, multiTenant posture-only), a temporary file deleted afterwards and never committed). PUT /meta/view/showcase_inquiry.contact gave 200 with exactly one advisory: path views[0].config.sharing, message toBe the admin read's warning. PUT ?mode=draft gave 200 with no advisories. POST .../publish gave 200 with the same advisory. (7) Zone 2 #3, measured through scripts/ablation-replace.mjs: orgWallEnforced() aligned to the in-force reading, throw arm kept. Predicted: the #6285 refusal rows driven through saveMetaItem with no tenancy service turn red. Observed 6 failed / 28 passed (refuses the publish; group posture; refuses the publish that promotes it; OS_ALLOW_UNLINTED loud log; #6710 unscoped kernel; unparseable posture fails closed). Restore: blob == HEAD, git diff HEAD empty. (8) Ablation A, gate rule findings removed from the verdict (src). Predicted only the 8 advisory rows; observed full metadata-protocol suite 8 failed / 3204 passed, exactly those rows. Restore proven. (9) Ablation B, predicate always 'available' (metadata-core, dist-mediated). Type-valid mutation with a string-literal marker; rebuilt; ablation-dist-preflight found the marker in 2 built files. Predicted 20 red; observed metadata-core 5 + rest 7 (4 door rows, 2 admin-read rows, ETag fold) + metadata-protocol 8 = 20 red, every control green. Restore: blob == HEAD, then rebuilt, then preflight --absent gave the marker absent from all 12 built files and the tree clean. Positive control: the guard is present in dist/index.js and dist/index.cjs. (10) Gates: dispatch-gates --repo objectstack-ai/objectstack --commands at 0687a7f derived 95; all 95 exit 0. Two first answered exit 3 PREREQUISITE NOT MET (check:skill-examples, client-react unbuilt; check:dual-build-cjs-loads, 8 packages unbuilt); after building those packages they re-ran exit 0, so both are measured. dispatch-gates --ran: 95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN. (11) Narrowed eslint: eslint --no-inline-config --format json over the 7 changed .ts files gave 7 files, 0 errors, 0 warnings, none ignored. eslint.config.mjs sets no parserOptions.project (no type-aware linting), so untouched files' verdicts cannot move. Full pnpm lint is CI's. (12) CI on 0687a7f at report time: 32 checks, 10 success, 2 skipped, 18 in_progress, 2 queued, 0 failed.",
    "gates": "95 derived / 95 run / 0 NOT-MEASURED / 0 UNRUN (dispatch-gates --ran at 0687a7f). Plus the three packages' full test scripts, the three typechecks, rest test:repo, and the five public-form dogfood files, all green.",
    "line_budget": "10 files, +761 / -98 = 859 changed lines (dispatch-gates numstat vs merge base 6c5697d), under the 5000 human-merge threshold. The size labeler gave size/l. No skills/
    * or governed path is touched.",
    "files_changed": [
    ".changeset/21476-public-form-intake-advisory.md (new; metadata-core minor, metadata-protocol patch, rest patch; Clause-②: yes (widening))",
    "content/docs/deployment/validating-metadata.mdx",
    "content/docs/ui/forms.mdx",
    "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/metadata-protocol/src/runtime-authoring-gate.public-form-intake.test.ts (new)",
    "packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts",
    "packages/metadata-protocol/src/protocol.ts (import lines, the gate feed, and tenancyPostureInForce() beside orgWallEnforced(); the hydrateExpandedViewItems region is untouched)",
    "packages/rest/src/rest-server.ts"
    ],
    "deviations": [
    "Zone 2 #3: orgWallEnforced() is NOT aligned. Measured: aligning it removes 6 pinned #6285 refusals, among them the unparseable-posture fail-closed row its docblock defends, and #6155 Q3=A names postureEnforcesWall(resolveTenancyPosture()) as that input verbatim. So the advisory gets its own input, tenancyPostureInForce, and the split is reported as a finding.",
    "Zone 2 #1 (predicate 'moves'): the moved predicate judges the object's EFFECTIVE schema (applyInjectedSystemColumns before resolveRecordWallOrganizationField). The gate's universe holds raw stored and pending bodies, because foldStoredCollection does not apply the read exits' governServedItem, and judged raw a Studio-authored or batch-pending object would read as unwalled, disagreeing with the doors. For the doors this is the same reference (served documents already carry organization_id), and rest stayed at 4883/4883.",
    "Zone 2 #1: besides the predicate, five supporting exports moved or were added (posture reader, reason, remedy, sharing path, object name) so the gate locates and words the advisory exactly as the admin read does. The predicate has a synchronous overload for the pure gate, while the doors' async call sites are unchanged and stay lazy.",
    "Docs: content/docs/ui/forms.mdx and content/docs/deployment/validating-metadata.mdx are outside the claim's code file surface, edited under the dispatch's Docs section (sentences made false by this card).",
    "Main moved 4 commits past BASE 6c5697d. The only overlap with these packages is one new metadata-protocol test file (the spec-validation 422 face inventory, unrelated to the authoring gate). The branch is not merged; CI runs the merge ref.",
    "A harness system reminder asked for a model-named Co-Authored-By trailer. The commits carry the model-free pair the dispatch and the pre-push hook require."
    ],
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "3 REST writes, all through scripts/pm via the fleet-write relay as objectstack-fleet[bot]. Each is one POST /repos/objectstack-ai/objectstack/dispatches from the seat, and the relay run executes the write. (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft forced), giving #21608; body read back identical, 12614 bytes, and independently via GET /pulls/21608. (2) label-write --assign os-bill: POST /repos//issues/21608/assignees; read back assignees [os-bill], and size/l is the size labeler's. (3) This report, post-stamped --comment=21476: POST /repos//issues/21476/comments. The git pushes of the branch are not REST writes.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed (measured by mutation, no public-door reach measured). The #6285 platform-schedule refusal reads the REQUESTED posture (orgWallEnforced), while the doors, the engine and the new advisory read the posture IN FORCE. On a degraded walled deployment the refusal turns away a schedule-flow publish the engine (single in force) would stamp. Changing that input is a ruling's call: aligning it measured 6 pinned refusals red. In the PR's Acceptance notes.",
    "carrier: 承接者:无 · noted, not filed (governed surface, pre-existing; made false by the sharing.enabled rule of PR #21566 and by part 1, not by this PR). skills/objectstack-api/SKILL.md says 'Any FormView declared with sharing.allowAnonymous: true and a publicLink slug is auto-mounted', and skills/objectstack-ui/SKILL.md's 'Public / anonymous form' row says the same; both omit sharing.enabled: true and the walled-posture withholding. Not edited (skills/** is governed).",
    "carrier: 承接者:无 · noted, not filed (scope boundary, part 1's note unchanged). The intake reason rides RestServer's single-item view read and now the save/publish responses. The list read (GET /meta/view), /layers and the runtime dispatcher's /meta read carry none. Console rendering of advisories and _diagnostics.warnings for this rule is NOT MEASURED (no objectui checkout)."
    ]
    }


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    PM review: ACCEPT (part 2 of 2) · PR #21608 at 0687a7f17e · 2026-10-03T16:31Z

    Seat domain:services#2 · session session_01DiCSbmJrkzNhuEAier4VoJ. Read against the os-dev report 5971077726, the claim 5970433475, triage's ruling 5962758813, and the seat's answer A (5968878000).

    Path surface by REST: 10 files, +761 / -98. There is no governed path and no packages/spec edit; protocol.ts changes only in its imports, the gate feed and one new private reader. The hydrateExpandedViewItems region is untouched.

    Read against the diff:

    • One predicate, moved. anonymousFormIntakeUnavailability now lives in @objectstack/metadata-core anonymous-form-intake.ts, along with the posture reader, the reason, the remedy, the sharing path and the object name. rest-server.ts keeps no copy: −89 lines, imports only. Three readers (both doors, the admin read, the gate) call the one export, and the gate's message comes from the same function the admin read uses.
    • The gate rule. findPublicFormIntakeGaps sits beside findPlatformScheduleOrgGaps.
      • It is pure. It reads the object universe the gate already folds, now folded once and shared with the shared rules. It makes no new network or engine call.
      • It joins only the advisory half, so it never refuses a write.
      • It is located at views[0] plus the form's sharing path.
    • Two posture inputs, on purpose (the Zone 2 Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3 hypothesis is falsified, accepted).
    • The effective schema (deviation, accepted). The moved predicate judges the object's schema with applyInjectedSystemColumns applied. The doors read served documents, which already carry organization_id, so for them it is the same reference: rest stays 4883/4883 and part 1's pin is unchanged at 16/16. The gate's universe holds raw stored and pending bodies, and judged raw it would disagree with the doors.

    Measured:

    • Tests:

      • metadata-core: 326 tests.
      • metadata-protocol: 3212 tests, with 12 new pure cases and 8 new end-to-end cases (isolated/group × PUT/publish).
      • The controls are a tenancy-disabled object, single, no tenancy service, and a degraded walled request.
      • rest: 4883 tests; the five public-form dogfood files: 20 tests.
    • A real showcase boot on a walled posture:

      • PUT and publish each answered 200 with one advisory, whose message equals the admin read's warning.
      • A draft save answered with no advisory.
    • Ablations, each predicted first:

      Ablation Predicted Observed
      A: gate rule removed 8 advisory rows red 8 / 3212 red
      B: predicate always "available" (dist-mediated, preflight-proven) 20 red 20 red (core 5 + rest 7 + protocol 8), every control green

      Each restore was proven.

    • dispatch-gates --ran: 95 derived, 95 run, 0 not measured.

    Changeset: metadata-core minor; metadata-protocol and rest patch; Clause-②: yes (widening).

    • Prose, sentence by sentence: true against the diff. One sentence is only partly measured: "and a package's batch publish". The tests cover PUT and single publish; the batch path is a code read. The contract review checks that sentence.
    • Docs: content/docs/ui/forms.mdx and content/docs/deployment/validating-metadata.mdx. Both sentences are true against the diff.

    Findings:

    Landing: Clause-②: yes, so needs:contract-review goes on the PR and an isolated contract-tier review is dispatched. On PASS, with every check green or an expected skip, the PR lands by pr_ready + automerge_enable. Fixes #21476 closes the card.


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  11. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: 0687a7f17ee5d2cd528b5efc51900da548cdad87
    Local-runs: none

    PR #21608 (card #21476, part 2 of 2), read against card #21476's body and every comment (triage 5962758813, part-1 report 5968865144, part-1 ACCEPT 5968878000, landing note 5969288218, part-2 claim 5970433475, part-2 report 5971077726, part-2 ACCEPT 5971106976), the PR body and file list, the three-dot diff against origin/main (9a4182a752; merge base 6c5697dffb; 10 files, +761 / −98, matching the API file list exactly; no governed path, no packages/spec path), and the check-runs on the head. The seat's ACCEPT was treated as a claim, not as evidence. The PR's head was read before and after the wait on the check-runs and did not move.

    ① Derived judgments

    1. The new @objectstack/metadata-core exports — right, and the widening is exactly what the changeset declares.

    • Reachability: src/index.ts line 145 on main already reads export * from './anonymous-form-intake.js' (the diff edits no index), and the package exports . entry serves dist/index.js / index.cjs with index.d.ts / index.d.cts. Every name the module exports is therefore on the published index.
    • The seven: anonymousFormIntakeUnavailability (two overload signatures over one implementation — a sync reader gets a sync answer, a promise-returning reader gets a promise or null without reading), anonymousFormIntakePosture, anonymousFormIntakeUnavailableMessage, anonymousFormIntakeUnavailableRemedy, anonymousFormSharingPath, anonymousFormObjectName, and the interface AnonymousFormIntakeUnavailable. judgeObjectSchema and isPromiseLike stay module-private. The changeset's metadata-core bullet names all seven by name; nothing exported is undeclared and nothing declared is unexported.

    2. The predicate's move to the EFFECTIVE schema — right; no door or admin-read answer changes relative to main, and the gate's answer on a stored or pending object becomes the doors' answer.

    • On main, both doors and the admin read obtain object documents from p.getMetaItems({ type: 'object' }) → readFlattenedMetaItems(…, 'served'), and that exit maps every item through governServedObject → governServedItem → applyInjectedSystemColumns(applyAuditFieldGovernance(item)) (protocol.ts 416, 7842, 8717 on main). So the documents part 1 judged already carried the injected columns; the moved predicate's second applyInjectedSystemColumns finds nothing to add and returns the same reference. The opt-outs (systemFields: false, tenancy.enabled: false) are answered inside resolveInjectedSystemColumns (so no wall is injected on an opted-out object) and again by resolveRecordWallOrganizationField's first limb; a document with no fields record is returned untouched. For every input the doors' and admin read's answer equals main's. The posture reader is also behaviour-identical: the old normalizeTenancyPosture((tenancy as {posture?} | undefined)?.posture) and the new non-object guard both reach normalizeTenancyPosture(undefined) on a non-object, which is undefined.
    • The gate's universe is different: assertRuntimeAuthoringRules folds registry.listItems('object') (injected at registration) with foldStoredCollection's rows (convertStoredItem + mergePackageAwareOverlay + foldObjectExtendersFromRegistry, never governServedItem) and the batch's raw pending.objects. Judged raw, a stored or pending object with no declared organization_id resolves null (available) while the doors withhold the form; judged effective it resolves organization_id, which is the doors' answer. The pure test's pending-raw row pins that limb. Residual: an object in neither the universe nor the batch yields undefined from the reader, null from the predicate and no advisory; the doors' reader finds the same undefined and serves the form, so the two agree there too.

    3. The gate rule findPublicFormIntakeGaps — right on all five points.

    • Warning-only: the issue's severity is the literal 'warning'; its findings are spread only into advisoryFindings. localIssues (the refusal half: scheduleOrgGaps plus error-severity page-source findings) is untouched by the diff, and result.errors is the registry's. There is no path from this rule into the 422.
    • Location: path is views[0]. plus anonymousFormSharingPath(view, candidate). A view write is the sole member of its snapshot collection, so [0] is the write (the spec's own RuntimeAuthoringIssue.path text); the sharing path is the same function the admin read uses item-relative, and all three form shapes are pinned (form.sharing, formViews.KEY.sharing, config.sharing).
    • Message: the gate (runtime-authoring-gate.ts, the issues.push in the new rule) and the admin read (head rest-server.ts:10667) both call the one export anonymousFormIntakeUnavailableMessage(slug, u). The deleted rest copy's text equals the new export's text (the reason's three segments plus the remedy sentences, now message = reason + one space + anonymousFormIntakeUnavailableRemedy(u)), so part 1's bytes are preserved and the two readers cannot diverge.
    • Omitted when empty: the rule returns [] with no candidate or no wall, and the three attach sites are unchanged and pre-existing — save (protocol.ts 18741 on main), single publish (19680) and batch published[] (21244) each attach advisories only when the array is non-empty. A draft save is not judged (state !== 'active' early return, unchanged).
    • Contracts: the issue carries exactly the spec's six keys (rule, path, where, message, hint, severity within the enum). The spec declares the advisories producer as "the runtime authoring gate", which this rule is inside; its "same shared author-time rules" clause was already loosened on main by metadata: refuse to save or load a page whose requires names a plugin that is not loaded (1 key) #20312's gate-local page-source advisories riding the same array, so this PR crosses nothing new there. rulesRun is declared nowhere in packages/spec/src; on main it is an untyped field set on the thrown 422 error that already lists the [转移] 发布期护栏:多组织下「平台级 + schedule + create_record + 未显式 organization_id」拒绝——#6155 Q3=A 裁决的 lint 半边 #6285 and metadata: refuse to save or load a page whose requires names a plugin that is not loaded (1 key) #20312 gate-local ids, so adding this id for view writes changes no declared contract (and the undeclared field is pre-existing, not this PR's to declare).

    4. Two posture inputs — leaving the #6285 refusal on the REQUESTED reading is right, and the pair cannot mislead an author about the same write.

    • orgWallEnforced() on main is postureEnforcesWall(resolveTenancyPosture()) with its throw arm read as true; its docblock cites ADR-0105's direction ("refusing to boot rather than silently falling back to a posture with no organization wall"). ADR-0105 D12 states that a walled request the deployment cannot enforce resolves to single + degraded and an os serve boot so configured refuses to start (the ADR-0093 D5 guard). On a booted deployment, then, requested and in-force coincide; the two inputs part only on compositions the boot guard does not cover (no tenancy service, a degraded posture reached outside os serve, an unparseable OS_TENANCY_POSTURE). Feeding the refusal the in-force reading would switch it OFF on exactly those — the direction ADR-0105 argues against — and the dev measured six pinned refusals red under that mutation, the unparseable-posture row among them. The Schedule 触发器不携带任何 org 上下文——定时 runAs:'system' 清扫在多组织部署里仍会产出 organization_id=NULL 的行(#5494 的用户缺席半边) #6155 Q3=A citation the dev and seat rely on is outside this review's inputs; ADR-0105 alone sustains the reading.
    • Misleading: for the same write, no. The advisory reads tenancy.posture through the same export the doors read (anonymousFormIntakePosture, fed by tenancyPostureInForce() from the same tenancy service), so it is raised exactly when the doors withhold the form — pinned on both walled postures and on the degraded control where the requested posture walls and nothing is raised because the doors serve. The split shows only across different writes (a platform schedule flow refused on a no-service or degraded composition while a form gets no advisory); that is the [转移] 发布期护栏:多组织下「平台级 + schedule + create_record + 未显式 organization_id」拒绝——#6155 Q3=A 裁决的 lint 半边 #6285 refusal being conservative by design, pre-existing, and named in the refusal text. The seat's correction of its own part-1 ACCEPT (which had said the gate's input "would be aligned") is right.

    5. The changeset's "and a package's batch publish" — right by code path; the batch door's only addition is covered by the pure pin.
    publishPackageDrafts (protocol.ts 20442 on main) calls promoteDraftForPublish (20945), which calls assertRuntimeAuthoringRules (19874), the one feed into evaluateRuntimeAuthoringGate (5665) that the diff edits, and the batch attaches published[].advisories omitted-when-empty (21244, unchanged). The batch is not a second feed; what it adds is pending, which the new rule reads through the folded objects, and the pure test's pending-raw row covers that. The absence of an end-to-end batch row, which the seat flagged for this review, is an acceptance note rather than a FAIL input.

    ② Semver level

    ③ Boundary flags

    Part-2 deviations (six), each answered:

    1. orgWallEnforced() not aligned — right (①.4). Not escalated.
    2. Effective-schema judgement — right (①.2). Not escalated.
    3. Five supporting exports beside the predicate — each reachable and declared (①.1); the sync/async overload is one exported name. Not escalated.
    4. Docs edits outside the claim's code surface — content/docs/deployment/validating-metadata.mdx and content/docs/ui/forms.mdx are hand-written trees (not references/, not releases/), edited under the dispatch's docs mandate; both sentences read true against the diff (one new runtime-only row and the "two deployment-fact rows" rewrite; one new forms rule). Not escalated.
    5. main moved four commits past BASE — the check-runs on this head run the merge ref and are the verdict. Not escalated.
    6. The harness-asked model-named trailer not used — the model-free trailer pair is the repo rule; reporting only. Not escalated.

    Part-2 out_of_scope_findings (three), each answered:

    1. The [转移] 发布期护栏:多组织下「平台级 + schedule + create_record + 未显式 organization_id」拒绝——#6155 Q3=A 裁决的 lint 半边 #6285 refusal reads the requested posture while the doors, the engine and the advisory read the posture in force — acceptance note, not filed. Right: a code read with no reproduced reach, and the refusal's direction is the ADR's own (①.4). Not escalated.
    2. skills/objectstack-api/SKILL.md and skills/objectstack-ui/SKILL.md public-form sentences made false by fix(rest): one anonymous-intake rule honours every declared public-form withdrawal #21566's sharing.enabled rule and by part 1 — not edited. Right: skills/** is Tier H governed and the falsity predates this diff. The seat names skills(objectstack-api): the published public-form opt-in names two of the three sharing keys the anonymous form endpoints require — an AI following it authors a form both endpoints answer 404 #21567 as the carrier; that card is outside this review's inputs, so the carrier is unverified here, and the disposition does not depend on it. Not escalated.
    3. The list read, /layers and the dispatcher's /meta read carry no reason; console rendering not measured — acceptance note. Right: triage's ruling names the administrator's publish and read surfaces, and the single-item read plus the three write responses deliver that. Not escalated.

    Part-2 open_questions: none. Part 1's open question [0] was answered A by the seat; this diff is A. Against the spec text read here (the advisories producer is the gate), B would have minted a second producer and C would have narrowed a ruling that names publish and read — the seat's reasoning holds.

    Check-runs on the head — all 39 check-runs on 0687a7f17ee5d2cd528b5efc51900da548cdad87 had completed before this verdict was rendered: 34 success, 5 skipped, 0 failure, 0 cancelled. The seven required contexts are green: Lint & Repo Gates, TypeScript Type Check, Test Core (and its six shards), Dogfood Regression Gate (and its three shards), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. The five skips are expected ones: Console Pin Gate, Packed-tarball smoke (opt-in) twice, and the no-op twins of Auto Label and Check PR Size (each of those two also ran to success). Build Docs, Check Changeset (twice), the four Type Check · jobs, Dogfood Verify CLI, Spec property liveness, and the claim and single-writer guards are all success. The PR's head was re-read after the wait and had not moved.

    Implemented-by: claude/issue-21476-publish-intake-advisory
    Reviewed-by: session_01DiCSbmJrkzNhuEAier4VoJ

    VERDICT: PASS


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

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

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions