fix(metadata-protocol,metadata-core,rest): saving or publishing a public form a walled posture cannot take intake for says why - #21608
Conversation
…e predicate; gate-local advisory Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…e save/publish intake advisory Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 25 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fb4aa46c31e92827e27e77a1c62a2c540af94819 && git checkout fb4aa46c31e92827e27e77a1c62a2c540af94819
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9a4182a752fd53b24a14bbcd2e1b4270e1174e7a 0687a7f17ee5d2cd528b5efc51900da548cdad87 && git checkout -B drift-repro 9a4182a752fd53b24a14bbcd2e1b4270e1174e7a && git merge --no-ff 0687a7f17ee5d2cd528b5efc51900da548cdad87
node scripts/docs-audit/affected-docs.mjs --json 9a4182a752fd53b24a14bbcd2e1b4270e1174e7a
|
Fixes #21476
Clause-②: yes (widening)
This is part 2 of 2 of #21476, the publish half. Part 1 (PR #21580,
a7ab047cf) delivered the anonymous doors and the administrator's read. This PR delivers the seat's answer A (ACCEPT5968878000) to part 1's open question:runtime-authoring-gate.ts;@objectstack/metadata-core;protocol.ts.With this, every surface triage's ruling
5962758813names is delivered: both doors, the administrator's read, and the administrator's publish.What
On a walled posture in force (
grouporisolated), saving a view (PUT /meta/view/:name) or publishing its draft (POST /meta/view/:name/publish, and the package batch publish) can carry an open public form whose object is walled by an organization column. That write now answers success with onewarningadvisory per such form underadvisories:public-form-intake-unavailable;sharing, under the write's root (views[0].formViews.contact.sharing,views[0].config.sharingorviews[0].form.sharing);messageis the administrator's read's reason, byte for byte;hintis the remedy: declaretenancy: { enabled: false }if the rows belong to no organization.It never blocks and never 422s. It is omitted-when-empty as before, and a draft save is not judged (#4463 D1).
One predicate, moved.
anonymousFormIntakeUnavailability(object, posture, readObjectSchema)now lives in@objectstack/metadata-core(anonymous-form-intake.ts). Next to it are:anonymousFormIntakePosture(tenancy);anonymousFormIntakeUnavailableMessage, built fromanonymousFormIntakeUnavailableRemedy;anonymousFormSharingPath;anonymousFormObjectName;AnonymousFormIntakeUnavailable.Three readers call those exports: both anonymous doors, the admin read (
rest-server.ts), and the gate rule. No copy is left inrest. The reason text is the same bytes as part 1's, proven by evaluating part 1's function from$BASEagainst the export over 8 inputs: byte-identical.The gate rule is
findPublicFormIntakeGaps, besidefindPlatformScheduleOrgGaps. It is pure, and it reads only what the gate already holds:assertRuntimeAuthoringRulesalready gathers (registry plus stored rows), folded with this batch's pending drafts, now computed once and shared with the shared rules;tenancyPostureInForce.It adds no network or engine call.
The posture input is
tenancyPostureInForce()inprotocol.ts. It readsanonymousFormIntakePosture(this.getServicesRegistry().get('tenancy')), the same service and the same reader the doors use, and the same channelanonymousFormIntakeOrgScopeRefusalalready readstenancythrough.Two deviations from the dispatch's mechanism hypotheses, each measured
The predicate judges the object's EFFECTIVE schema. It now applies metadata-core's
applyInjectedSystemColumnsbefore resolving the wall column. The doors read served object documents, which already carry the injectedorganization_id, so for them this is the same reference and their answers are unchanged. The rest suite is 4883 / 4883 before and after, the same count as part 1.The gate's universe is different. Its stored-row winners and a batch's pending drafts are raw bodies, because
foldStoredCollectiondoes not apply the read exits'governServedItem. Judged raw, a Studio-authored object reads as unwalled, and the advisory would disagree with the doors.The predicate is synchronous for a synchronous reader. The gate is pure and synchronous. The doors need the object read to stay lazy: part 1's pin asserts that the single posture reads no object. So the export has two overloads:
nullwithout reading when no wall is in force.The doors' call sites are unchanged.
orgWallEnforced()is NOT aligned (Zone 2 #3: measured, then left as is)The advisory reads the posture IN FORCE. The #6285 schedule refusal keeps reading the REQUESTED posture through
orgWallEnforced().I measured the alternative with a one-off mutation:
orgWallEnforced()reading the in-force posture, its throw arm kept. My prediction was that every #6285 refusal row driven throughsaveMetaItemwith no tenancy service would turn red. Observed: 6 red / 28 passed, acrossprotocol.platform-schedule-org-gate.test.tsandprotocol.bracketed-refusal-opener-absence.test.ts:refuses the publish …;… under the group posture;… refuses the publish that promotes it;OS_ALLOW_UNLINTED … loud log;[#6710] DOES gate an unscoped kernel;survives a deployment whose OS_TENANCY_POSTURE is unparseable ….So aligning would narrow a refusal that the docblock and ADR-0105 defend (the unparseable-posture row). It would also contradict the #6155 Q3=A ruling, which names
postureEnforcesWall(resolveTenancyPosture())as that input verbatim. Per the dispatch, it stays. The split is documented on both inputs, and it is reported as a finding below. The mutation was restored, proven by blob == HEAD and an emptygit diff HEAD.Pins
packages/metadata-core/src/anonymous-form-intake.test.ts, +15 cases (13 → 28):tenancy.tenantField;singleand no tenancy service, with zero object reads;single, legacymulti);packages/metadata-protocol/src/runtime-authoring-gate.public-form-intake.test.ts(new, pure), 12 cases:toEqualagainst the metadata-core reason and remedy;single, no posture, a withdrawn form, a draft, a non-view write;orgWallEnforced: truewithsinglein force raises nothing;422 INVALID_METADATA) discloses the rule inrulesRun.packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, +8 end-to-end rows throughsaveMetaItemandpublishMetaItem, with a realtenancyservice in the services table:isolatedandgroup× PUT and publish: success, exactly one advisory, the reason equal toanonymousFormIntakeUnavailableMessage, and the row landed active;single, no tenancy service, and a degraded deployment (OS_TENANCY_POSTURE=isolated, service in forcesingle), where the doors serve the form and nothing is raised.packages/rest/src/public-form-intake-availability.test.tsis unchanged and green (16/16). It now exercises the moved export throughdist/.bootStack(showcaseStack, { multiTenant: 'posture-only' }), postureisolated.config.sharingwith the reason.PUT /meta/view/showcase_inquiry.contactanswered 200 with exactly one advisory:path: "views[0].config.sharing"andmessageidentical (toBe) to the admin read's warning.PUT ?mode=draftanswered 200 with no advisories.POST …/publishanswered 200 with the same advisory.Ablations, direction predicted before each run
runtime-authoring-gate.ts, src)Both mutations went through
scripts/ablation-replace.mjsin WRAP mode: the anchor hit 1 → 0, and the restore was proven by blob == HEAD and an emptygit diff HEAD.B is dist-mediated, so it used a type-valid mutation carrying a string-literal marker (part 1 measured that a DTS refusal leaves the mutated JS in
dist/):ablation-dist-preflightfound the marker in 2 built files before the run.--absentreported the marker absent from all 12 built files and the tree clean against HEAD.dist/index.jsanddist/index.cjs.Tests and gates
The code is final at
dc0a93d4c4.0687a7f17eadds only docs and the changeset (git diff dc0a93d4c4 0687a7f17etouches nopackages/path).test: 17 files, 326 passed.dc0a93d4c4).test(--project local): 258 files, 4883 passed, 326 skipped.test:repo: 5 files, 177 passed.check:test-typecheck.dispatch-gates --repo objectstack-ai/objectstack --commandsat0687a7f17ederived 95 commands. All 95 exit 0.PREREQUISITE NOT MET:check:skill-examples(client-react unbuilt) andcheck:dual-build-cjs-loads(8 packages unbuilt). Both were re-run green after building those packages, so they are measured, not skipped.--ran: 95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN.eslint --no-inline-config --format jsonon the 7 changed.tsfiles gave 7 files, 0 errors, 0 warnings, none ignored.eslint.config.mjsnever enables type-aware linting (noparserOptions.project), so the narrowing cannot move an untouched file's verdict. Fullpnpm lintis CI's.mainmoved 4 commits past$BASE(6c5697dffb). The only overlap with these packages is one new metadata-protocol test file (the spec-validation 422 face inventory), which does not touch the authoring gate. The branch is not merged; CI runs the merge ref.Docs
content/docs/deployment/validating-metadata.mdx:✓ᵛ);content/docs/ui/forms.mdx: the "wires the anonymous REST endpoints automatically" rule list gains the walled-posture rule. The form is not offered, the admin read and the save/publish response say why, and the remedy is given.skills/**is governed and not edited. Two published skill sentences are already false, made so by thesharing.enabledrule and by part 1, not by this PR:skills/objectstack-api/SKILL.md"AnyFormViewdeclared withsharing.allowAnonymous: trueand apublicLinkslug is auto-mounted";skills/objectstack-ui/SKILL.md's "Public / anonymous form" row.Acceptance notes
GET /meta/view),/layersand the runtime dispatcher's/metaread carry none (part 1's note, unchanged).advisoriesand_diagnostics.warningsfor this rule is NOT MEASURED: no objectui checkout.Generated by Claude Code