Commit 83b3d32
fix(metadata-protocol,metadata-core,rest): saving or publishing a public form a walled posture cannot take intake for says why (#21608)
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 (ACCEPT `5968878000`) to part 1's
open question:
- a gate-local warning advisory in `runtime-authoring-gate.ts`;
- the predicate moved into `@objectstack/metadata-core`;
- the gate fed the posture in force from `protocol.ts`.
With this, every surface triage's ruling `5962758813` names is
delivered: both doors, the administrator's read, and the administrator's
publish.
## What
On a walled posture in force (`group` or `isolated`), 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 **one `warning` advisory per such form**
under `advisories`:
- rule `public-form-intake-unavailable`;
- located at the form's `sharing`, under the write's root
(`views[0].formViews.contact.sharing`, `views[0].config.sharing` or
`views[0].form.sharing`);
- `message` is the administrator's read's reason, byte for byte;
- `hint` is the remedy: declare `tenancy: { 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:
- its posture reader `anonymousFormIntakePosture(tenancy)`;
- the reason, `anonymousFormIntakeUnavailableMessage`, built from
`anonymousFormIntakeUnavailableRemedy`;
- the location, `anonymousFormSharingPath`;
- the target object, `anonymousFormObjectName`;
- the type `AnonymousFormIntakeUnavailable`.
Three readers call those exports: both anonymous doors, the admin read
(`rest-server.ts`), and the gate rule. No copy is left in `rest`. The
reason text is the same bytes as part 1's, proven by evaluating part 1's
function from `$BASE` against the export over 8 inputs: byte-identical.
- **The gate rule** is `findPublicFormIntakeGaps`, beside
`findPlatformScheduleOrgGaps`. It is pure, and it reads only what the
gate already holds:
- the object universe `assertRuntimeAuthoringRules` already gathers
(registry plus stored rows), folded with this batch's pending drafts,
now computed once and shared with the shared rules;
- one new pure input, `tenancyPostureInForce`.
It adds no network or engine call.
- **The posture input** is `tenancyPostureInForce()` in `protocol.ts`.
It reads
`anonymousFormIntakePosture(this.getServicesRegistry().get('tenancy'))`,
the same service and the same reader the doors use, and the same channel
`anonymousFormIntakeOrgScopeRefusal` already reads `tenancy` through.
### Two deviations from the dispatch's mechanism hypotheses, each
measured
1. **The predicate judges the object's EFFECTIVE schema.** It now
applies metadata-core's `applyInjectedSystemColumns` before resolving
the wall column. The doors read served object documents, which already
carry the injected `organization_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 `foldStoredCollection` does not
apply the read exits' `governServedItem`. Judged raw, a Studio-authored
object reads as unwalled, and the advisory would disagree with the
doors.
2. **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:
- a synchronous reader gets a synchronous answer;
- a reader returning a promise gets a promise, or `null` without 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 through `saveMetaItem` with no tenancy
service would turn red. Observed: **6 red / 28 passed**, across
`protocol.platform-schedule-org-gate.test.ts` and
`protocol.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 empty `git diff HEAD`.
## Pins
- `packages/metadata-core/src/anonymous-form-intake.test.ts`, +15 cases
(13 → 28):
- both walled postures;
- the effective schema;
- a declared `tenancy.tenantField`;
- controls: tenancy-disabled, absent object, no wall column;
- `single` and no tenancy service, with zero object reads;
- the asynchronous reader;
- posture-in-force reading (degraded reads `single`, legacy `multi`);
- sharing path across all three form shapes;
- object name;
- message ending with the remedy.
-
`packages/metadata-protocol/src/runtime-authoring-gate.public-form-intake.test.ts`
(new, pure), 12 cases:
- per walled posture, exactly one advisory, compared with `toEqual`
against the metadata-core reason and remedy;
- each form shape's path;
- a pending raw object in the batch;
- controls: tenancy-disabled, `single`, no posture, a withdrawn form, a
draft, a non-view write;
- `orgWallEnforced: true` with `single` in force raises nothing;
- a refused view write (`422 INVALID_METADATA`) discloses the rule in
`rulesRun`.
-
`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`,
+8 end-to-end rows through `saveMetaItem` and `publishMetaItem`, with a
real `tenancy` service in the services table:
- `isolated` and `group` × PUT and publish: success, exactly one
advisory, the reason equal to `anonymousFormIntakeUnavailableMessage`,
and the row landed active;
- controls on PUT and publish: tenancy-disabled object, `single`, no
tenancy service, and a **degraded** deployment
(`OS_TENANCY_POSTURE=isolated`, service in force `single`), where the
doors serve the form and nothing is raised.
- Part 1's `packages/rest/src/public-form-intake-availability.test.ts`
is **unchanged** and green (16/16). It now exercises the moved export
through `dist/`.
- **Real-boot measurement** (a one-off file, not committed):
`bootStack(showcaseStack, { multiTenant: 'posture-only' })`, posture
`isolated`.
- The admin read warning is `config.sharing` with the reason.
- `PUT /meta/view/showcase_inquiry.contact` answered 200 with exactly
one advisory: `path: "views[0].config.sharing"` and `message` identical
(`toBe`) to the admin read's warning.
- `PUT ?mode=draft` answered 200 with no advisories.
- `POST …/publish` answered 200 with the same advisory.
## Ablations, direction predicted before each run
| Ablation | Predicted | Observed |
|---|---|---|
| A: the gate rule's findings removed from the verdict
(`runtime-authoring-gate.ts`, src) | only the advisory rows: 8 red (4
pure, 4 end-to-end) | full metadata-protocol suite **8 failed / 3204
passed**, exactly those 8 |
| B: the predicate answers "available" everywhere (metadata-core,
rebuilt) | 20 red: metadata-core 5, the rest door and admin-read rows 7,
advisory rows 8; every control green | **5 + 7 + 8 = 20 red**, every
control green |
Both mutations went through `scripts/ablation-replace.mjs` in WRAP mode:
the anchor hit 1 → 0, and the restore was proven by blob == HEAD and an
empty `git 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-preflight` found the marker in 2 built files before the
run.
- After restore and rebuild, `--absent` reported the marker absent from
all 12 built files and the tree clean against HEAD.
- Positive control: the restored guard is present in `dist/index.js` and
`dist/index.cjs`.
## Tests and gates
The code is final at `dc0a93d4c4`. `0687a7f17e` adds only docs and the
changeset (`git diff dc0a93d 0687a7f` touches no `packages/`
path).
- metadata-core `test`: 17 files, **326 passed**.
- metadata-protocol full suite: 208 files, **3212 passed**, 19 skipped
(at `dc0a93d4c4`).
- rest `test` (`--project local`): 258 files, **4883 passed**, 326
skipped. `test:repo`: 5 files, 177 passed.
- typecheck: metadata-core, metadata-protocol and rest all clean. rest's
includes `check:test-typecheck`.
- Five public-form dogfood files (walled intake, walled withdrawal,
showcase withdrawal, showcase public form, read-back masking): **5 files
/ 20 passed**.
- `dispatch-gates --repo objectstack-ai/objectstack --commands` at
`0687a7f17e` derived 95 commands. All 95 exit 0.
- Two first answered exit 3 `PREREQUISITE NOT MET`:
`check:skill-examples` (client-react unbuilt) and
`check: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, narrowed: `eslint --no-inline-config --format json` on the 7
changed `.ts` files gave 7 files, 0 errors, 0 warnings, none ignored.
`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.
- `main` moved 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`:
- adds the runtime-only row "Public-form anonymous intake on this
deployment's tenancy posture — advisory only" (`✓ᵛ`);
- rewrites the sentence that called the platform-schedule row "the one
deliberate exception". There are now two deployment-fact rows.
- `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 the `sharing.enabled` rule and by part 1,
not by this PR:
- `skills/objectstack-api/SKILL.md` "Any `FormView` declared with
`sharing.allowAnonymous: true` and a `publicLink` slug is auto-mounted";
- `skills/objectstack-ui/SKILL.md`'s "Public / anonymous form" row.
## Acceptance notes
- **Finding, the posture split, kept deliberately.** The #6285 refusal
reads the REQUESTED posture, while the doors, the engine and this
advisory read the posture IN FORCE. On a degraded deployment the refusal
turns away a schedule-flow publish the engine would stamp. Aligning it
is a ruling's call (#6155 Q3=A names the input), measured above at 6
pinned refusals. This is a code read with no public-door reach measured,
so it is not filed; it is noted for the seat.
- The advisory's object read is the gate's universe. A form bound to an
object that is in neither the live universe nor this batch gets no
advisory, and the doors offer such a form too, so the two agree.
- The intake reason still rides only RestServer's single-item view read
and the write responses. The list read (`GET /meta/view`), `/layers` and
the runtime dispatcher's `/meta` read carry none (part 1's note,
unchanged).
- The console's rendering of `advisories` and `_diagnostics.warnings`
for this rule is NOT MEASURED: no objectui checkout.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 54521f0 commit 83b3d32
10 files changed
Lines changed: 761 additions & 98 deletions
File tree
- .changeset
- content/docs
- deployment
- ui
- packages
- metadata-core/src
- metadata-protocol/src
- rest/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
478 | 478 | | |
479 | 479 | | |
480 | 480 | | |
| 481 | + | |
481 | 482 | | |
482 | 483 | | |
483 | 484 | | |
| |||
594 | 595 | | |
595 | 596 | | |
596 | 597 | | |
597 | | - | |
598 | | - | |
599 | | - | |
600 | | - | |
601 | | - | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
602 | 609 | | |
603 | 610 | | |
604 | 611 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| 103 | + | |
103 | 104 | | |
104 | 105 | | |
105 | 106 | | |
| |||
Lines changed: 105 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
9 | 15 | | |
10 | 16 | | |
11 | 17 | | |
| |||
79 | 85 | | |
80 | 86 | | |
81 | 87 | | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
0 commit comments