Skip to content

fix(rest): one anonymous-intake rule honours every declared public-form withdrawal - #21566

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-21475-public-form-withdrawal-second-means
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-21475-public-form-withdrawal-second-means

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21475
Clause-②: yes (widening) — five new exports on @objectstack/metadata-core's index (the shared anonymous-intake rule); graded minor.

What

A second declared means of withdrawing a public form from anonymous intake is now honoured by every anonymous door; pinned both sides.

Which forms a view opens to anonymous intake is now one rule, defined once in @objectstack/metadata-core (anonymousFormIntakeCandidates, with anonymousFormIntakeSlugs / anonymousFormIntakeSlug / publicFormSlug), and read by:

  • both anonymous form doors in @objectstack/rest (registerFormEndpoints → findPublicFormView), and
  • the organization-scoped view write check in @objectstack/metadata-protocol (anonymousFormIntakeOrgScopeRefusal), whose local projection (src/anonymous-form-intake.ts) is deleted in favour of the shared one.

So no door reads one declared means and not the other, there is no second per-door check, and the write-time judgement cannot drift from what the doors serve. The rule follows SharingConfigSchema as declared, including its defaults, so a raw stored body and its parse get the same answer.

Pins (both sides)

  • packages/metadata-core/src/anonymous-form-intake.test.ts — the rule itself, every closed shape, raw-vs-parse parity, the three candidate shapes.
  • packages/rest/src/public-form-withdrawal.test.ts — new block: withdrawn by either declared means ⇒ both doors 404 FORM_NOT_FOUND, createData never called; published ⇒ 200 / 201 (control); with tenancy resolving an organization and with tenancy unregistered. Existing fixtures that never declared public sharing per the schema now declare it.
  • packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts — an org-scoped walled write that withdraws through the second means now counts as a change and is refused 403 NOT_OVERRIDABLE, nothing saved.
  • Dogfood, single posture (showcase-public-form-withdrawal.dogfood.test.ts, real showcase boot): withdrawn env-wide by the second means ⇒ both doors 404 FORM_NOT_FOUND and no row lands; republished ⇒ 200/201 and the row lands. The existing allowAnonymous pins stay green.
  • Dogfood, walled posture (public-form-withdrawal-walled.dogfood.test.ts): the same withdrawal org-scoped is refused 403; env-wide closes both doors; republish restores.

Ablation (one-shot, not kept)

Deleted the second-means check from the shared rule via node scripts/ablation-replace.mjs (anchor 1 → 0, blob bf5a099a → ad331202), rebuilt @objectstack/metadata-core, ablation-dist-preflight --absent confirmed the guard gone from all 12 built files. Result: rest 4 failed / 12 passed (exactly the four second-means cases), metadata-protocol 1 failed / 22 passed, dogfood 2 failed / 9 passed, metadata-core 5 failed / 8 passed; every allowAnonymous pin and every control stayed green. Restore: blob == HEAD, git diff HEAD empty; rebuilt and the preflight found the guard back in dist; git status --porcelain empty.

Tests (at 0164be245)

  • @objectstack/metadata-core vitest: 17 files / 311 passed.
  • @objectstack/rest vitest: 262 files / 5044 passed, 327 skipped.
  • @objectstack/metadata-protocol vitest: 206 passed, 3 skipped files / 3177 passed, 19 skipped.
  • @objectstack/dogfood (the four public-form files): 4 files / 17 passed.
  • typecheck for metadata-core, metadata-protocol, rest, dogfood: all Done.
  • node scripts/pm/dispatch-gates.mjs --commands: 114 derived commands run; 113 exit 0; check:dual-build-cjs-loads answered PREREQUISITE NOT MET (needs a full workspace build): NOT MEASURED, left to CI. --ran reconciliation: 114 of 114 accounted for.
  • @objectstack/spec check:generated: all 15 artifacts current after gen:docs (the regenerated content/docs/references/ui/sharing.mdx carries the corrected module header).
  • eslint, narrowed to the 12 changed .ts files (--no-inline-config --format json): 12 files, 0 errors, 0 warnings. Narrowing is sound: eslint.config.mjs enables no type-aware linting (no parserOptions.project), so this diff cannot move any untouched file's verdict. The full pnpm lint is CI's.

Acceptance notes

  • content/docs/ui/forms.mdx and the sharing.zod.ts module header are corrected to state the rule as enforced.
  • A published skill's description of the public-form opt-in now under-states it. skills/** is a Tier H surface, so it is left out of this PR to keep this one ungoverned; it needs its own follow-up (carrier: none yet).
  • Upgrade note: a form whose sharing never declared public sharing as the schema defines it stops being served anonymously after this lands. The changeset says so in class-level terms and points at the public forms guide.

Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/metadata-core, @objectstack/metadata-protocol, @objectstack/rest, @objectstack/spec, touching 8 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/metadata-core/src/index.ts, packages/metadata-protocol/src/protocol.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ui/forms.mdx (via /forms/:slug (route, a path literal in a comment on a changed line), /forms/:slug/submit (route, a path literal in a comment on a changed line))
  • content/docs/ui/public-data-collection.mdx (via /forms/:slug (route, a path literal in a comment on a changed line), /forms/:slug/submit (route, a path literal in a comment on a changed line))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-6.mdx (via /forms/:slug (route, a path literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/metadata-core/src/index.ts, packages/metadata-protocol/src/protocol.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 141 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1ca1eb09727d7769bc605428ad1e6fb62d743f49 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f7f6721ffa96467cc63a36e498e9317ba359f313 — the merge of head 769594d9c68e109be0535ac9ee3b2e5fbd168ef2 into base 1ca1eb09727d7769bc605428ad1e6fb62d743f49, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f7f6721ffa96467cc63a36e498e9317ba359f313 && git checkout f7f6721ffa96467cc63a36e498e9317ba359f313
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1ca1eb09727d7769bc605428ad1e6fb62d743f49 769594d9c68e109be0535ac9ee3b2e5fbd168ef2 && git checkout -B drift-repro 1ca1eb09727d7769bc605428ad1e6fb62d743f49 && git merge --no-ff 769594d9c68e109be0535ac9ee3b2e5fbd168ef2

node scripts/docs-audit/affected-docs.mjs --json 1ca1eb09727d7769bc605428ad1e6fb62d743f49

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 1ca1eb09727d7769bc605428ad1e6fb62d743f49 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f38427a9ce2920a435c3c3d5ed68234e85ee56de
Local-runs: none

Inputs: card #21475 (body and its three comments — triage 5962746069, claim 5966742063, os-dev-report 5967097888), PR #21566 (body, 16-path file list, net diff origin/main 5dbcee8a6...f38427a9c, +320/−95), and the check-runs on this head. Two existence reads beyond that set, both to verify claims judged in ③: #21567 and objectui#11545. Nothing built, run or re-run. Class-level record: it names what the diff itself shows and narrates no pre-fix behaviour beyond that.

① Derived judgments

  1. packages/spec/src/ui/sharing.zod.ts — comment-only, RIGHT. Both hunks sit inside the module-header TSDoc and a // comment inside the aliases map. No key, default, .describe() text, alias target or strictObject option moves; SharingConfigSchema's accept set is byte-identical to main. The regenerated content/docs/references/ui/sharing.mdx is the header's projection (gen:docs) and matches the source hunk. Contract-tier review was owed on the path hit alone; there is no widening tell and no narrowing on this file.

  2. @objectstack/metadata-core public entry (src/index.ts, new export * from './anonymous-form-intake.js') — FIVE new exported symbols on a published package: publicFormSlug, anonymousFormIntakeSlug, anonymousFormIntakeCandidates, anonymousFormIntakeSlugs and the type AnonymousFormIntakeCandidate. The package is published (exports["."] → dist/index, 17.6.0, in the fixed group, not private) and the symbols are reachable from its entry type graph, so under contract-review.md's exports-map rule this IS the published surface growing. The PLACEMENT is right (both consumers already depend on metadata-core; it is the file's own established sink pattern). The DECLARATION of it is wrong — ②.

  3. @objectstack/metadata-protocol — src/anonymous-form-intake.ts deleted; on main it was never exported from src/index.ts (its sole importer was protocol.ts), so no public surface moves. protocol.ts now judges an organization-scoped view write by the shared rule: an org-scoped write that withdraws through enabled now counts as a change to the anonymous slug set and is refused 403 NOT_OVERRIDABLE in the walled posture (pinned). RIGHT — the write-time projection and the doors can no longer disagree, which is the ruling on the card.

  4. @objectstack/rest rest-server.ts findPublicFormView — the per-door candidate scan and slug normalisation are replaced by anonymousFormIntakeCandidates; the three candidate shapes (nested form, each formViews entry, the flattened viewKind: 'form' config), the key and the object-name resolution are preserved; no export moves. RIGHT.

  5. Runtime behaviour narrowing — a form whose sharing sets enabled: false or omits it (schema default false) is no longer served by either anonymous door. Judged against the DECLARED contract: SharingConfigSchema.enabled read "Enable public sharing", default false, before this PR, and the schema accepts exactly the same bodies after it. The runtime is pulled back to the declared contract (ADR-0049 enforce-or-remove, the enforce arm the triage ruling names). Under the directional clause-② ruling that is neither a widening nor a declared-contract narrowing: no BREAKING banner and no ADR-0087 disposition is demanded, and none of the five categories could honestly carry one (no spec change to register; the body carries a migration prescription, so no-migration-prescription is refused by construction). RIGHT. It IS an upgrade-visible change for a stored form authored on the pre-fix guide text (main's forms.mdx prose named two keys), which the changeset's migration line addresses — ③(A). Both example apps already declare enabled: true (app-crm lead.view.ts, app-showcase inquiry.view.ts), so the showcase dogfood precondition expect(published.config.sharing.enabled).toBe(true) holds on main.

  6. "No door reads one means and not the other" — on main the only non-test readers of allowAnonymous are the two sites this diff replaces; the rule now has one home. One residual describe string still names a single key: packages/spec/src/api/rest-server.zod.ts, the api.requireAuth tombstone prescription ("a public form view (sharing.allowAnonymous)"). Prose, not a door — ③(D), not a verdict driver.

  7. Pins — both sides on every posture the card names: unit (every closed shape including absent enabled and a truthy non-boolean; raw-vs-parse parity against SharingConfigSchema), rest (4 withdrawn shapes × 2 tenancy postures → both doors 404 FORM_NOT_FOUND, createData never called; published control 200/201), metadata-protocol (org-scoped enabled withdrawal refused, nothing saved), dogfood single and walled. Existing fixtures that never declared enabled: true now do — the fixture catching up with the schema, not a pin weakened. RIGHT. The ablation is the dev's report, not re-run here.

  8. Governance and size — the 16-path file list touches no governed surface; head repo = base repo; +320/−95; draft, auto-merge not armed. Governed Surface Queue Guard succeeded.

② Semver level

Changeset .changeset/public-form-withdrawal-one-rule.md: metadata-core, metadata-protocol, rest all patch; PR body Clause-②: no; the card's claim also Clause-②: no.

  • @objectstack/rest patch — RIGHT (a fix; no public surface moves).
  • @objectstack/metadata-protocol patch — RIGHT (a fix; the deleted module was internal).
  • @objectstack/metadata-core patch — WRONG. The diff adds five exported symbols to a published package's index. The Check Changeset step's own rule text ("WHICH LEVEL", maintainer ruling 2026-09-04, decision batch [WIP] Add query enhancements and advanced validation features #35): "A purely additive widening of a published package's public surface (a new exported symbol on an index …) takes at least minor. … a fix( that widens an index is therefore minor." AGENTS.md Post-Task Checklist step 3 states the same consequence: Clause-②: yes takes at least minor. The declaration criterion (accept set relaxed OR public surface enlarged) is met on its second arm by these exports, so the no is a false no — the kind landing-operations calls an auditable false declaration. The mechanical half (check-widening-tells T3) reads only packages/spec/api-surface/*.json and cannot see a metadata-core index, which is exactly why this reading is the review's to make; Check Changeset green here is the level axis standing down on a no, not a verdict on the level.
  • No (narrowing) arm, no BREAKING banner, no ADR-0087 marker — RIGHT as is (①.5): the behaviour change enforces a declared default, the schema's accept set does not move, and nothing authorable is removed or renamed. The in-family precedent .changeset/21331-public-form-withdrawal.md (rest-only, no new export, patch, Clause-②: no) is consistent with this reading: that diff widened no index; this one does.

What must change, exactly — and nothing else:
(i) .changeset/public-form-withdrawal-one-rule.md frontmatter: '@objectstack/metadata-core': minor. The other two rows stay patch; the fixed group publishes in lockstep regardless, the declaration is what is judged.
(ii) PR body line: Clause-②: yes (the (widening) arm is accurate and optional). The level axis then requires at least one moved package at minor, which (i) satisfies. The card's claim comment carries the same no as the dispatch's prediction; the enqueue gate reads the claim, so the dispatching seat owns bringing that carrier to yes in the form its skill prescribes — a corrected declaration on the card, never a silent edit.
(iii) No BREAKING banner, no ADR-0087 marker, no change to the migration text, no code change.
A fresh ## Contract review record is owed on the head that carries (i) and (ii); this one is the contract-tier review of the act, so a PASS on that head clears the clause-② enqueue gate.

③ Boundary flags

(A) Dev Q1 — CHANGELOG key-level migration. Decided A by the seat; the changeset at this head carries it ("enabled defaults to false … Migration: add enabled: true to the form's sharing block (and to any stored overlay of it)"). ANSWERED, RIGHT. It states the post-fix rule and the one-line fix; the keys it names are already public in this PR's tests and docs; it narrates no pre-fix behaviour beyond what the diff shows. Gate-consistent: a migration prescription interacts with check:adr-0087-registration only on a changeset that declares breaking, which this one correctly does not. RUNNER rule 2 holds — the reproduction stays off the card and the PR.

(B) Dev Q2 — the published skill's under-statement. Decided A: a separate Tier H card, #21567 (open, created 2026-10-03T08:20Z, Blocked-by: #21566, names skills/objectstack-api/SKILL.md); the console mirror is objectui#11545 (open, 08:20Z, names the developer PublicFormsPage.tsx). Both verified to exist and to say what was claimed. ANSWERED, RIGHT: keeping skills/** out of this PR keeps the p1 fix ungoverned (Prime Directive #14); the Tier H edit cannot land on green anyway.

(C) Raised here, not in the dev's questions — the claim's file surface named packages/rest/src, packages/metadata-protocol/src, packages/qa/dogfood/test, .changeset/, and packages/spec "only if the key is retired rather than enforced (stop on breach; explain in the report)". The diff adds packages/metadata-core/src/** (the rule's new home), content/docs/**, and a comment-only packages/spec touch under the enforce arm. The report explains the placement; the spec touch is prose only. Acknowledged deviation, architecturally right, not verdict-driving — but it is the deviation that produced the ② mismatch: a rule sunk into a published package widens that package's index, and Clause-②: no was predicted on a file surface that contained no published index.

(D) Residual prose naming one key for the public-form opt-in, outside this PR: the api.requireAuth tombstone text in packages/spec/src/api/rest-server.zod.ts (a live describe string), plus the ADR-0087 migration-registry prose the dev already listed (shipped release text, not for rewriting here). Candidates for #21567's "re-check any other page" line or a sibling docs card; no carrier yet. Escalated as a note, not a block.

(E) Check-runs on this head, read from the commit's check-runs API at 2026-10-03T08:38Z (32 runs). Completed success: Check Changeset, Governed Surface Queue Guard, Spec property liveness, Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Build Core, Build Docs, Dogfood Verify CLI, Check PR Size, Check Documentation Links, Auto Label, filter, the four claim/single-writer guards and Flag docs affected by code changes. Completed skipped: Console Pin Gate, Packed-tarball smoke (opt-in). STILL in_progress when this record was rendered, so NOT judged green here: Lint & Repo Gates, Type Check · workspace, Test Core (6/6 shards), Dogfood Regression Gate (3/3 shards), Temporal Conformance (live PG + MySQL). No run concluded failure. The verdict below does not rest on them: ② fails on the diff and the changeset alone, and the corrected head will carry its own full check set for the fresh record.

Implemented-by: claude/issue-21475-public-form-withdrawal-second-means
Reviewed-by: session_018zT8d8NpiQ1ExhuNd5TxY6

VERDICT: FAIL

The shared rule widens @objectstack/metadata-core's public index by five
exported symbols, which takes at least minor (Clause-② yes, widening).

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 769594d9c68e109be0535ac9ee3b2e5fbd168ef2
Local-runs: none

Inputs: card #21475 (body and its four comments — triage 5962746069, claim 5966742063, os-dev-report 5967097888, claim correction 5967269801), PR #21566 (body as read at this head, 16-path file list, net diff origin/main...769594d9c, +320/−95), the prior ## Contract review on this PR (comment 5967245381, on head f38427a9c, FAIL on ② only), and the check-runs on this head. Two existence reads beyond that set, both re-verifying claims judged in ③: #21567 and objectui#11545. Nothing built, run or re-run. Class-level record: it names what the diff itself shows and narrates no pre-fix behaviour beyond that.

This is the fresh record the prior one said was owed on the head carrying its two fixes. The head-to-head delta f38427a9c..769594d9c is ONE hunk in ONE file — .changeset/public-form-withdrawal-one-rule.md, frontmatter '@objectstack/metadata-core': patch → minor; the other two rows, the body text and the migration line are byte-identical. No source, test, docs or spec path moved, so the prior record's ① and ③ judgments were re-read against the full diff rather than re-derived from scratch; each is restated below with what was re-checked.

① Derived judgments

  1. packages/spec/src/ui/sharing.zod.ts — comment-only, RIGHT (unchanged from the prior record). Both hunks sit inside the module-header TSDoc and a // comment inside the aliases map; the strictObject body, every key, every .default(...) and every alias target are byte-identical to main. SharingConfigSchema's accept set does not move, so no api-surface baseline is touched and the packages/spec touch is under the enforce arm, not a retirement. The regenerated content/docs/references/ui/sharing.mdx is that header's gen:docs projection and matches the source hunk line for line.

  2. @objectstack/metadata-core public entry (src/index.ts, new export * from './anonymous-form-intake.js') — FIVE new exported symbols on a published package: publicFormSlug, anonymousFormIntakeSlug, anonymousFormIntakeCandidates, anonymousFormIntakeSlugs and the type AnonymousFormIntakeCandidate. Re-verified at this head: package.json is not private, exports["."] resolves to dist/index.{js,cjs} with .d.ts/.d.cts types, version 17.6.0, member of the fixed group; export * re-exports the interface as well as the four functions, so all five are reachable from the entry type graph. Under contract-review.md's exports-map rule this IS the published surface growing, and under the Check Changeset step's WHICH LEVEL rule (pr-automation.yml, maintainer ruling 2026-09-04, batch [WIP] Add query enhancements and advanced validation features #35) "a new exported symbol on an index" takes at least minor. Placement RIGHT (both consumers already depend on metadata-core; the file's own established sink pattern). Declaration — now RIGHT, see ②.

  3. @objectstack/metadata-protocol — src/anonymous-form-intake.ts deleted. Re-verified on origin/main: the module's only importer was protocol.ts (git grep anonymous-form-intake over packages/metadata-protocol/src returns that one line); it was never exported from the package's index.ts, so no public surface of this package moves. protocol.ts now imports anonymousFormIntakeSlugs from @objectstack/metadata-core and anonymousFormIntakeOrgScopeRefusal judges proposed vs served slug sets by the shared rule. Consequence, stated at the level the diff shows: an organization-scoped view write that withdraws through enabled now counts as a change to the anonymous slug set and is refused 403 NOT_OVERRIDABLE in the walled posture (pinned). RIGHT — the write-time projection and the doors can no longer disagree, which is the ruling on the card.

  4. @objectstack/rest rest-server.ts findPublicFormView — the per-door candidate scan and slugMatchesPublicLink are replaced by anonymousFormIntakeCandidates(view) plus c.slug !== slug. Re-read with wider context: the three candidate shapes (nested form, each formViews entry keyed, the flattened viewKind: 'form' config keyed by view.name), the key, the slug normalisation (/forms/x, forms/x, x) and the object-name resolution chain (c.form?.data?.object ?? view?.list?.data?.object ?? …) are preserved; the old guard view.form && view.form.sharing collapses into the rule's null return for a missing sharing, which is the same set of open candidates. No export moves. RIGHT.

  5. Runtime narrowing judged against the DECLARED contract — a form whose sharing sets enabled: false or omits it (schema default false) is no longer served by either anonymous door. SharingConfigSchema.enabled read "Enable public sharing", default false, before this PR and the schema accepts exactly the same bodies after it; the runtime is pulled back to the declared contract (ADR-0049 enforce arm, as the triage ruling names). Under the directional clause-② ruling that is neither a widening nor a declared-contract narrowing: no BREAKING banner and no ADR-0087 disposition is owed, and none of the five categories could honestly carry one. RIGHT. It IS upgrade-visible for a stored form authored on main's forms.mdx prose (which named two keys); the changeset's migration line addresses it — ③(A). Both example apps already declare enabled: true (re-verified: app-crm lead.view.ts:124, app-showcase inquiry.view.ts:71), so the showcase dogfood precondition expect(published.config.sharing.enabled).toBe(true) holds on main.

  6. "No door reads one means and not the other" — re-verified by git grep allowAnonymous over packages/*/src excluding tests and packages/spec: ZERO non-test readers on main outside the two sites this diff replaces, and ZERO on this head. The rule has one home. Residual prose naming a single key stays as the prior record found it: the api.requireAuth tombstone prescription in packages/spec/src/api/rest-server.zod.ts:197 (a live describe string), the ADR-0087 migration-registry text and two conversion-registry fixtures. Prose and fixtures, not doors — ③(D), not a verdict driver.

  7. Pins — both sides on every posture the card names: unit (anonymous-form-intake.test.ts: every closed shape including absent enabled and a truthy non-boolean switch; raw-vs-parse parity against SharingConfigSchema; the three shapes; de-dup and sort), rest (public-form-withdrawal.test.ts: 4 withdrawn shapes × 2 tenancy postures → both doors 404 FORM_NOT_FOUND, createData never called; published control 200/201), metadata-protocol (org-scoped enabled withdrawal refused, no row saved), dogfood single posture (withdrawn by enabled: false, then by absent enabled, both doors 404 and nothing lands; republish restores 200/201) and walled (org-scoped refused 403, env-wide closes both doors, republish restores). Two existing rest fixtures that never declared enabled: true now do — fixtures catching up with the schema, not pins weakened. RIGHT. The ablation is the dev's report, not re-run here.

  8. Governance and size — the 16-path file list hits none of the six GOVERNED_SURFACES rows (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, docs/NORTH-STAR.md); head repo = base repo; +320/−95 (size/m); still draft, auto-merge not armed, no review submitted. Governed Surface Queue Guard succeeded on this head. No needs:contract-review marker is on the PR, so there is no label act for this record to perform.

② Semver level

Changeset .changeset/public-form-withdrawal-one-rule.md at this head: '@objectstack/metadata-core': minor, '@objectstack/metadata-protocol': patch, '@objectstack/rest': patch. PR body, line 2: Clause-②: yes (widening) — five new exports on …. Card: claim 5966742063 said Clause-②: no; correction 5967269801 (08:40Z) declares Clause-②: yes (widening) and names the five exports and this PR's head — a corrected declaration on the card, not a silent edit, in the form the prior record asked for.

  • @objectstack/metadata-core minor — RIGHT. Five new exported symbols on a published index (①.2) is the additive widening the WHICH LEVEL rule grades at least minor; the commit type fix( does not lower it. This is the prior record's (i), landed in 769594d9c as the sole change.
  • @objectstack/metadata-protocol patch — RIGHT (a fix; the deleted module was internal, no exported surface moves).
  • @objectstack/rest patch — RIGHT (a fix; no public surface moves).
  • Declaration/level consistency — RIGHT. Clause-②: yes with the (widening) arm: the value token yes is first after the colon, the arm is the fixed spelling in the parenthetical opened as the next non-blank thing, and the trailing — … reasoning is what clause2-line.mjs leaves unread by design. yes requires at least one moved package at minor, which metadata-core now satisfies; the (widening) arm is accurate (public surface enlarged, accept set unchanged). This is the prior record's (ii).
  • No (narrowing) arm, no BREAKING banner, no ADR-0087 marker, migration text unchanged — RIGHT as is (①.5), and that is the prior record's (iii): nothing else changed, as required.
  • The in-family precedent .changeset/21331-public-form-withdrawal.md (rest-only, no new export, patch, Clause-②: no) remains consistent with this reading: that diff widened no index; this one does and is now graded for it.

The ② finding of the prior record is FIXED on this head, with exactly the three prescribed consequences and nothing else.

③ Boundary flags

(A) Dev Q1 — CHANGELOG key-level migration. Decided A by the seat; the changeset at this head carries it unchanged from f38427a9c ("enabled defaults to false … Migration: add enabled: true to the form's sharing block (and to any stored overlay of it)"). ANSWERED, RIGHT. It states the post-fix rule and the one-line fix; the keys it names are already public in this PR's tests and docs; it narrates no pre-fix behaviour beyond what the diff shows. Gate-consistent: a migration prescription interacts with check:adr-0087-registration only on a changeset that declares breaking, which this one correctly does not. RUNNER rule 2 holds — the reproduction stays off the card and the PR.

(B) Dev Q2 — the published skill's under-statement. Decided A: a separate Tier H card, #21567 (re-read: open, created 2026-10-03T08:20Z, names skills/objectstack-api/SKILL.md); the console mirror is objectui#11545 (re-read: open, 08:20Z, names the developer Public Forms page). ANSWERED, RIGHT: keeping skills/** out of this PR keeps the p1 fix ungoverned (Prime Directive #14).

(C) Claim file-surface deviation — the claim named packages/rest/src, packages/metadata-protocol/src, packages/qa/dogfood/test, .changeset/, and packages/spec only under the retire arm; the diff adds packages/metadata-core/src/**, content/docs/** and a comment-only packages/spec touch under the enforce arm. Acknowledged in the dev's report; architecturally right (one rule, one home, both consumers already downstream); it was the deviation that produced the prior ② mismatch, and the card's correction comment now carries the consequence. Closed as a note.

(D) Residual prose naming one key for the public-form opt-in, outside this PR: packages/spec/src/api/rest-server.zod.ts:197 (api.requireAuth tombstone, a live describe string), plus the ADR-0087 migration-registry and conversion-registry text the dev already listed (shipped release text, not for rewriting here). Candidates for #21567's "re-check any other page" line or a sibling docs card; no carrier yet. Escalated as a note, not a block.

(E) Check-runs on this head, read from the commit's check-runs API at 2026-10-03T08:45Z (39 runs, including two Check Changeset runs). Completed success (21): Check Changeset (×2), Governed Surface Queue Guard, Spec property liveness, Type Check · source gates, Type Check · debt ledger, Build Docs, Dogfood Verify CLI, Check PR Size, Check Documentation Links, Auto Label, filter, the four claim/single-writer guards (×2 each) and Flag docs affected by code changes. Completed skipped (4): Console Pin Gate, Packed-tarball smoke (opt-in), and one duplicate each of Auto Label / Check PR Size. STILL in_progress (14) when this record was rendered, so NOT judged green here: Lint & Repo Gates, Type Check · workspace, Type Check · consumer gates, Build Core, Test Core (6/6 shards), Dogfood Regression Gate (3/3 shards), Temporal Conformance (live PG + MySQL). No run concluded failure. The verdict below is the contract-tier reading of the diff and its declarations; the queue's own precondition — every check green on this head — is a separate leg this record does not certify, and in_progress is not green. The owning seat enqueues only once the remaining runs conclude success.

Implemented-by: claude/issue-21475-public-form-withdrawal-second-means
Reviewed-by: session_018zT8d8NpiQ1ExhuNd5TxY6

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21475-public-form-withdrawal-second-means branch October 3, 2026 09:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

Projects

None yet

2 participants