Skip to content

feat(spec): export the anonymous-form-intake candidates from @objectstack/spec/ui; metadata-core re-exports the same bindings - #22056

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22047-spec-ui-anonymous-form-intake
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22047-spec-ui-anonymous-form-intake

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #22047

Clause-②: yes (widening: new exports on the published @objectstack/spec/ui entry; @objectstack/spec changeset at least minor)

What changes

  • New on @objectstack/spec/ui: publicFormSlug, anonymousFormIntakeSlug, anonymousFormIntakeCandidates, anonymousFormIntakeSlugs and the AnonymousFormIntakeCandidate type. This is the candidates half of the anonymous-form-intake rule, which the triage ruling on console(public forms page): the developer Public Forms page lists a form as published by its own reading of sharing, which ignores sharing.enabled — a form the server no longer serves still shows as published objectui#11545 (5967405932) asks a console to read instead of re-deriving "published" from the sharing keys.
  • Moved, not copied. The bodies are the ones that were in packages/metadata-core/src/anonymous-form-intake.ts, unchanged except for indentation (2 spaces, as in the rest of packages/spec). diff -w between the BASE lines 52-105 and the new module is empty. The scan still covers the three shapes in the same order: nested form, then formViews entries, then a viewKind: 'form' item's config.
  • @objectstack/metadata-core re-exports the same bindings (export { … } from '@objectstack/spec/ui', plus export type for the interface), so one copy remains. Its remaining code imports publicFormSlug and the type from the spec. Its posture, withdrawal-layer and object-name parts stay where they were. @objectstack/rest and @objectstack/metadata-protocol are not edited and keep importing from metadata-core. packages/metadata-core/src/index.ts is unchanged.
  • Changeset .changeset/22047-spec-ui-anonymous-form-intake.md: @objectstack/spec minor, @objectstack/metadata-core patch (its built dist/index.{js,cjs,d.ts,d.cts} now import these functions from @objectstack/spec/ui instead of defining them).

Where it lives, and why (H3)

It goes in a new module, packages/spec/src/ui/anonymous-form-intake.ts, next to sharing.zod.ts. It is not added to sharing.zod.ts, for three reasons:

  • The module imports nothing. It has no zod import and no schema import, and does no work at load time. It stays as cheap as a browser can import, whichever entry reaches it. sharing.zod.ts imports zod and two schema helpers.
  • This is the existing pattern for runtime helpers in spec/ui that do not live inside a schema module. chart-aggregate.ts, i18n-label-resolver.ts and view-grouping-query.ts are sibling non-.zod.ts modules. expandViewContainer sits in view.zod.ts only because it reads that module's member constants. These four functions read no schema.
  • The file name matches the one in metadata-core, so the move is easy to follow in history.

Trade-off: files[] ships src/**/*.zod.ts as source, so this module's source is not in the tarball. Its JS and declarations are, in dist/ui/index.{mjs,js,d.mts,d.ts}.

Prime Directive 2 (no business logic in packages/spec: schemas, types and constants only) holds here the way ADR-0053 D-D2 reads it. A pure helper that states what the contract's own vocabulary denotes is protocol, not business logic. It lives beside that vocabulary, and a server package re-exports it: D-D2 moved nextUtcCalendarDay into @objectstack/spec/data and has @objectstack/core re-export it. These four functions only say which sharing declarations open a form. The two checks that read server state (another layer's withdrawal, the tenancy posture) stay in metadata-core. So does anonymousFormObjectName, a pure read of the form and the view, because that is where this export's surface was drawn. expandViewContainer (view.zod.ts) is the placement precedent: a pure helper beside the schema it serves. The reason two codebases must agree on this rule byte for byte is the triage ruling on objectui#11545 (5967405932), as the module's header now says (patch round 1, 6c5741c6).

Pins and measurements

Identity pin (3). This is in packages/metadata-core/src/anonymous-form-intake.test.ts; the existing cases are unchanged, and the pin adds 3 import lines and 1 describe. For each of the four names it asserts that ./anonymous-form-intake.js[name] and ./index.js[name] are toBe (Object.is) @objectstack/spec/ui[name].

  • Ablation: run once and not kept, through scripts/ablation-replace.mjs with the fix committed first. The re-export of anonymousFormIntakeCandidates was replaced by a wrapper that returns the spec function's answer.
    • Result: Tests 1 failed | 45 passed (46). Only the identity case for anonymousFormIntakeCandidates failed (expected [Function] to be [Function] // Object.is equality). Every behaviour test passed against the wrapper, so only the identity pin can catch a copy.
    • Restore was verified by the tool: the blob equals HEAD (c08671875) and git diff HEAD is empty. metadata-core's test reads its own src directly, so the mutation needed no rebuild to take effect.

H5: identity in the built dual output. A one-time node probe, run from packages/rest, compared the four functions in metadata-core's built output with @objectstack/spec/ui's:

condition metadata-core export === spec/ui export
ESM (dist/index.js vs dist/ui/index.mjs) true for all four
CJS (dist/index.cjs vs dist/ui/index.js) true for all four
ESM vs CJS (cross-condition) false: the dual-package split every spec export already has

Parity pin (1). packages/spec/src/ui/anonymous-form-intake.test.ts (26 cases) pins each of the three shapes on its own (open; withdrawn by either switch; no link), all three in one body in scan order, a config without viewKind: 'form', slug normalisation, and raw input against SharingConfigSchema.parse input. Its expectations are the same as metadata-core's existing ones. A one-time parity probe compared the BASE metadata-core functions (git show 8caa131e5, lines 52-105) with the built spec/ui and metadata-core functions now. It compared candidate keys, key presence, slugs, whether each candidate is a form object from the input, and the slug set:

shape bodies with an open slug BASE = spec/ui = metadata-core
nested form 13 4 13
formViews entry (plus an open sibling) 13 13 13
viewKind: 'form' + config 13 4 13
all three in one body 13 4 13
config without viewKind: 'form' 13 0 13
non-view input 4 0 4
real producers: showcase inquiry.view.ts, crm lead.view.ts, as containers and as expandViewContainer items 9 4 9

That is 78 bodies with 0 mismatches, and 21 leaf inputs (anonymousFormIntakeSlug, publicFormSlug) with 0 mismatches.

H1. Lines 52-105 call nothing from @objectstack/spec/security, applyInjectedSystemColumns or resolveRecordWallOrganizationField. The new module has no imports, and the four bodies compile unchanged in packages/spec.

H2. ./ui is in browser-reachable-entries.json's unjudged list, so check:browser-reachable-entries asserts nothing about it (it passed). The module is plain function declarations with no top-level statements, and the package declares "sideEffects": false.

Declaration surface downstream. metadata-core's dist/index.d.ts and .d.cts now reference @objectstack/spec/ui. Measured with tsc --noEmit --extendedDiagnostics --listFiles, building metadata-core from BASE source and then from HEAD source. These are absolute numbers from a shared box:

program files BASE → HEAD memory BASE → HEAD
packages/rest 579 → 580 1,246,583K → 1,268,472K
packages/objectql 574 → 574 983,063K → 994,921K
packages/plugins/plugin-security 495 → 495 1,079,234K → 1,084,561K
packages/metadata-protocol 808 → 808 1,319,331K → 1,318,969K
packages/qa/http-conformance 345 → 345 357,199K → 357,044K

The one new file in rest is spec/dist/ui/index.d.ts, the CJS barrel, reached through metadata-core/dist/index.d.cts. The chunks it re-exports were already in that program in both flavours. tsc exited 0 in every program on both builds.

Prose that named the old home (H4)

  • packages/spec/src/ui/sharing.zod.ts:19-23 said the rule lives in anonymousFormIntakeCandidates "in @objectstack/metadata-core". It now names anonymous-form-intake.ts beside that module, which metadata-core re-exports to the server's doors.
  • content/docs/references/ui/sharing.mdx:22-26 was regenerated from that docblock by gen:docs, not edited by hand.
  • packages/metadata-core/src/anonymous-form-intake.ts:13-20: the module docblock says the candidates half is declared in @objectstack/spec/ui, whose docblock is now the authority on it. The scan-shape paragraph moved with the code.
  • Judged still true and left alone:
    • packages/rest/src/rest-server.ts:10698 reads "(anonymousFormIntakeCandidates, @objectstack/metadata-core)". That is where rest imports the function from, and metadata-core still exports it. The file is also outside this card's surface.
    • packages/metadata-core/src/index.ts:141-145 ("both read this one rule").
    • The docs/qa/platform-checklist mechanism references name anonymousFormIntakeWithdrawnIn and anonymousFormExplicitWithdrawals, which stay in metadata-core.

Verification (HEAD cfdc8804f0; the source tree is the same as 3e9a9e48b1 plus the changeset)

  • pnpm --filter @objectstack/spec build (JS + DTS): exit 0. check-dts-emitted reported 38/38.
  • pnpm --filter @objectstack/spec check:generated: all 15 artifacts up to date. Before regeneration, 3 were stale: api-surface/ (+5 rows in ui.json), export-origins/ (+5) and content/docs/references/** (the H4 sentence). They were regenerated with gen:api-surface, gen:export-origins and gen:docs.
  • check:api-surface, check:export-origins, check:docs, check:exported-any, check:dual-source-exports (0 accepted dual-source), check:entry-nameability, check:browser-reachable-entries, check:liveness, check:llms-txt, check:skill-examples: exit 0.
  • pnpm --filter @objectstack/spec test: 620 files, 18511 passed, 1 todo. The new file alone: 26 passed.
  • pnpm --filter @objectstack/metadata-core test: 18 files, 415 passed. src/anonymous-form-intake.test.ts alone: 46 passed.
  • pnpm --filter @objectstack/metadata-protocol test: 219 files passed, 3 skipped; 28135 tests passed. The focused run of runtime-authoring-gate.public-form-intake, protocol.runtime-authoring-gate and protocol.org-scoped-write-refused passed 257.
  • pnpm --filter @objectstack/rest test: 260 files, 4914 passed, 326 skipped. The focused run of public-form-routes, public-form-routes.stored-row, public-form-withdrawal and public-form-intake-availability passed 76.
  • pnpm --filter @objectstack/spec typecheck and pnpm --filter @objectstack/metadata-core typecheck: exit 0. --listFiles shows both new test files are in their packages' test programs.
  • Gates derived by node scripts/pm/dispatch-gates.mjs --commands at cfdc8804f0, and checked with --ran: 110 derived, 109 run, 0 unrun, 1 NOT MEASURED. The NOT MEASURED one is pnpm check:dual-build-cjs-loads, exit 3, PREREQUISITE NOT MET: it needs every package built. The CJS half of the H5 probe above loaded metadata-core/dist/index.cjs with require. The 5 roster gates the lead list flagged under a touched directory also pass (check:meta-url-spelling and check:spec-changes through check:generated; check:authz-resolver, check:error-code-casing, check:filter-alias-parity).
  • Lint is a narrowed run, and it measures something: eslint --no-inline-config --format json over the 6 changed TS files reported 6 files, 0 errors and 0 warnings. All 6 are in the population of eslint.config.mjs (files: ['**/*.{ts,…}'] minus NEVER_LINTED, and --print-config resolves each one). That config never turns on type-aware linting (no parserOptions.project, no typed rules; see its own note near line 327), so this diff cannot change the result for any file it does not touch. The repo-wide pnpm lint is left to CI.

Acceptance notes

  • Not covered by this card (the card's "Not this card" section): whether another layer withdraws a form, whether the posture makes a form unavailable, any server-side "published" answer, and the objectui page change. objectui#11545 restarts once objectui uses a release that carries these exports.
  • main gained feat(spec,analytics): a dataset answer's measure column states its aggregate, labelled or not (fields[].aggregate) #22021 (a spec analytics change) after this branch was cut. It touches none of these files and none of the generated artifacts, so main is not merged here, and the merge queue rebuilds on the current main.

Generated by Claude Code

claude added 3 commits October 7, 2026 03:49
…ule from @objectstack/spec/ui

publicFormSlug, anonymousFormIntakeSlug, anonymousFormIntakeCandidates,
anonymousFormIntakeSlugs and the AnonymousFormIntakeCandidate type move,
unchanged in what they decide, from @objectstack/metadata-core into
@objectstack/spec/ui beside SharingConfigSchema. metadata-core re-exports
the same bindings, so the server callers keep importing them from there
and one copy remains; the identity is pinned in its test.

Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7
Co-authored-by: Claude <noreply@anthropic.com>
…eference for the new /ui exports

Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7
Co-authored-by: Claude <noreply@anthropic.com>
… patch for the /ui anonymous-form-intake export

Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 4 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json, packages/spec/src/ui/index.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json, packages/spec/src/ui/index.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 — 139 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 5bd8cb100b9ab7043d1c3bb6db31c99b5ce630b9 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 292e65b61043ccd97f891df881f11f68204dec2d — the merge of head 6c5741c6b71966e88773503fd0bfb41fe5b0f38d into base 5bd8cb100b9ab7043d1c3bb6db31c99b5ce630b9, 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 292e65b61043ccd97f891df881f11f68204dec2d && git checkout 292e65b61043ccd97f891df881f11f68204dec2d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5bd8cb100b9ab7043d1c3bb6db31c99b5ce630b9 6c5741c6b71966e88773503fd0bfb41fe5b0f38d && git checkout -B drift-repro 5bd8cb100b9ab7043d1c3bb6db31c99b5ce630b9 && git merge --no-ff 6c5741c6b71966e88773503fd0bfb41fe5b0f38d

node scripts/docs-audit/affected-docs.mjs --json 5bd8cb100b9ab7043d1c3bb6db31c99b5ce630b9

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

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

Inputs read: card #22047 (body, claim 6030439569, dev report 6031101423); objectui#11545 comments 5967405932, 6029776692, 6029809270; PR #22056 (body, 10-file list, net diff against main); the head's check-runs; main and the head through git show on the read-only checkout. Nothing built, run or re-run.

① Derived judgments

Public surface, @objectstack/spec/ui (widened — right). src/ui/index.ts gains export * from './anonymous-form-intake', so the built entry (dist/ui/index.{mjs,js,d.mts,d.ts} under the ./ui exports map) gains exactly five names: publicFormSlug(publicLink: string): string, anonymousFormIntakeSlug(sharing: unknown): string | null, anonymousFormIntakeCandidates(view: unknown): AnonymousFormIntakeCandidate[], anonymousFormIntakeSlugs(view: unknown): string[], and interface AnonymousFormIntakeCandidate { form: Record keyed by string with any values; key?: string; slug: string }. api-surface/ui.json (+5 rows, generated from the built .d.ts) and export-origins/ui.json (+5 rows, all at src/ui/anonymous-form-intake.ts) carry exactly those and nothing else; the root entry . re-exports named ui symbols only and gains none, so no other baseline moves. Nothing is removed, renamed or narrowed. The module imports nothing and has no top-level statement (sideEffects: false holds), and its source does not ship (files[] carries src/**/*.zod.ts only) — stated truthfully in the PR body. AnonymousFormIntakeCandidate.form carries any inside a Record, which check:exported-any does not flag by its own stated scope (the type itself is not any); it is the shape main already published from metadata-core.

Public surface, @objectstack/metadata-core (unchanged — right). src/index.ts is untouched (export * from './anonymous-form-intake.js'); that module now forwards export { anonymousFormIntakeCandidates, anonymousFormIntakeSlug, anonymousFormIntakeSlugs, publicFormSlug } from '@objectstack/spec/ui' plus export type { AnonymousFormIntakeCandidate }, so every name the package exported on main is still exported with the same signatures, and the interface is structurally the one main declared (next point). Its remaining code binds publicFormSlug and the type from the spec (anonymousFormExplicitWithdrawals, anonymousFormSlot, anonymousFormSharingPath, anonymousFormIntakeWithdrawnIn); anonymousFormObjectName, anonymousFormIntakePosture, anonymousFormIntakeUnavailability, anonymousFormIntakeUnavailableRemedy and anonymousFormIntakeUnavailableMessage stay as they were. @objectstack/rest and @objectstack/metadata-protocol are not in the diff and keep importing from metadata-core. The built output now imports @objectstack/spec/ui; metadata-core depends on @objectstack/spec as workspace:* and both sit in the one fixed changeset group, so a published metadata-core pins the exact spec version that carries the names — no floor hazard.

The rule's accept set (unchanged — right; the card's binding guard). main's packages/metadata-core/src/anonymous-form-intake.ts lines 52–105 (interface and four functions) against the new module's lines 50–103: diff -w is empty (a read of the two refs, nothing run). So anonymousFormIntakeSlug still opens a form only on enabled === true and allowAnonymous === true and a non-empty string publicLink; publicFormSlug still strips leading slashes then forms/; anonymousFormIntakeCandidates still scans the nested form, then every formViews entry in Object.entries order, then config only when viewKind === 'form', keeping the open ones in that order with key only where a key exists; anonymousFormIntakeSlugs is still the sorted de-duplicated set. No conversion, no alias, no new response surface.

Spec pins (right). packages/spec/src/ui/anonymous-form-intake.test.ts pins each of the three shapes alone (open, with the expected key and its presence; withdrawn through enabled: false, through allowAnonymous: false, and with no link), the three in one body with the order asserted as [undefined, 'nested'], ['a', 'a'], ['inquiry.contact', 'flat'] while a withdrawn formViews.b sibling is dropped, config without viewKind: 'form' as not a form, the four slug spellings, raw input against SharingConfigSchema.parse input (the schema's enabled default of false is what makes that pin meaningful), non-object input, de-duplication and sort, and that the /ui entry's bindings are the module's own functions. All three candidate shapes and their scan order are pinned.

Identity pin (right, and able to fail). packages/metadata-core/src/anonymous-form-intake.test.ts adds one describe; the existing cases are byte-unchanged in the diff. For each of the four names it asserts Object.is between ./anonymous-form-intake.js[name], ./index.js[name] and @objectstack/spec/ui[name]. metadata-core's vitest config declares no alias, so the test's import and the re-export resolve the same specifier through the exports map to one module instance; the assertion holds only for a re-export and fails for a wrapper or a copy. The dev's ablation (the anonymousFormIntakeCandidates re-export swapped for a wrapper returning the spec function's answer; fix committed first; restore verified) turned exactly that one case red with every behaviour case green — the pin is the only test that can catch a copy, and it does. The CJS half of the identity is the dev's one-time H5 probe, not a kept test (③).

Prime Directive 2 (allowed). PD2 reads "No business logic in packages/spec. Spec = schemas/types/constants only." ADR-0053 D-D2 records how the repository reads it for pure helpers: a function that states what the contract's own vocabulary denotes is "protocol, not business logic — the same species as the pure helpers spec/data already owns", chosen because it adds no dependency edge and the server package re-exports it so that package's surface is unchanged. These four functions are pure (no I/O, no state, no imports), interpret SharingConfigSchema's own keys and ViewSchema's own form slots, read no server state, and spec/ui already carries the same species (chartAggregateCategoryKey, resolveI18nLabel, columnSummaryAlias, expandViewContainer). The halves that read server state stay in metadata-core. Allowed under that text and that record. The PR body's paragraph says PD2 "holds here the way ADR-0053 D-D2 reads it" — true, and the right framing (satisfied, not outweighed); the dev report's deviation (6) paraphrases it as "outweighed by the ruling", which is not what the body says.

Published prose, sentence by sentence. sharing.zod.ts (ships as source) and the regenerated content/docs/references/ui/sharing.mdx (the same words, from gen:docs): the rule now named as anonymous-form-intake.ts beside the module, re-exported by metadata-core to the server's doors — true (rest-server.ts imports it from metadata-core). New module JSDoc reaching dist/ui/*.d.ts: the three criteria and the enabled default — true against SharingConfigSchema (z.boolean().default(false); allowAnonymous is .optional().default(false)); the scan order — true; "re-exports these bindings (the same functions, not a copy)" — true and pinned; "no imports and no module-load work" — true. metadata-core docblock: "declared in @objectstack/spec/ui … whose docblock is the authority" — true. Changeset: the title sentence, "what they decide is unchanged", the three shapes in order, "the spec's own bindings, not wrappers or copies", "exports, names and types are unchanged", "built output now loads @objectstack/spec/ui" — all true. Two imprecisions, neither a contract fact, are flagged in ③.

Fixes #22047 (met). The export (five names on /ui); the three pins the card owes (the four functions answer the same on the three shapes — 26 spec cases plus the identity that makes the server's answer the same function; metadata-core's callers and tests unchanged — index.ts untouched, existing cases byte-unchanged, rest and metadata-protocol not edited; the parity pin by identity); the regenerated baselines (api-surface/ui.json, export-origins/ui.json, sharing.mdx). The claim's file surface is respected: no consumer edit, no server response surface, no change in what the rule decides.

② Semver level

  • .changeset/22047-spec-ui-anonymous-form-intake.md: @objectstack/spec: minor — right for a widening (five new exports on a published entry; nothing removed, renamed or narrowed; Clause-②: yes takes at least minor). @objectstack/metadata-core: patch — right: names and types unchanged, the implementation moved to a re-export, the built output now imports @objectstack/spec/ui; the fixed group carries both to one version. Not skip-changeset: both publish.
  • Clause-②: lines. PR body: Clause-②: yes (widening: new exports on the published …) — the reader takes the value yes and the first token inside the parenthetical, widening, and leaves the rest as the seat's argument; declared and well-formed. Changeset: Clause-②: yes (widening) — the fixed spelling. Both match the diff: a widening, no narrowing, nothing an author can write removed; no ADR-0087 disposition is owed. Check Changeset on the head: success, both runs.

③ Boundary flags

Dev report (6031101423) deviations, each answered:

  1. 2-space indentation in the spec module, diff -w empty — confirmed here against main lines 52–105; it is packages/spec's style. Accepted.
  2. The identity pin added inside the existing metadata-core test file — within the claim's declared cross-lane surface ("also its test file"); existing cases byte-unchanged in the diff. Accepted.
  3. main not merged; feat(spec,analytics): a dataset answer's measure column states its aggregate, labelled or not (fields[].aggregate) #22021 named as disjoint — not re-measured here; the net diff against main is the ten files above, and the merge queue's rebuild on current main is the control. Accepted.
  4. PR body Clause-② in the yes (widening: …) form, changeset in the closed-pair form — both read yes plus widening (②). Accepted.
  5. Two local gates re-run after first runs that measured nothing — a local-run matter; the check-runs on the head are the gate verdicts (below). Accepted.
  6. PD2 — answered in ①; the body's framing is the true one.
  • open_questions: none.
  • Out-of-scope finding (check:skill-examples and check:query-options-erasure race on the gitignored packages/spec/.examples-build/*.ts when run in parallel locally; CI runs them one after another): not a defect in this diff and not a landing condition; no Acceptance-notes bullet is needed. Escalated to the dispatching seat as a tooling note, for a card at its discretion.

Flags raised by this review:

  • Wording, published (fix at the seat's discretion; it does not move the verdict). The changeset's last bullet and the spec module's "What stays in @objectstack/metadata-core" paragraph group anonymousFormObjectName under the halves that "read server state". It does not: it reads only the form's data.object and the view's list.data.object, form.data.object and object. It stays in metadata-core because the card drew the surface there, not because it reads server state. The PR body's "(…, the target object)" carries the same slip, as does the card's own sentence the dev followed. One-line fix: name the object-name helper as kept with the server halves rather than as one that reads server state.
  • Precedent wording. The new module's header cites expandViewContainer (view.zod.ts) as the precedent for "a rule that two independent codebases must agree on byte for byte"; the precedent's own docblock states no such reason — the reason is the ruling's and D-D2's. The citation is accurate as to where the precedent lives; the PR body's "gives the same reason as the precedent" overstates by that much. Wording only.
  • CJS identity is probe-measured, not pinned. The identity pin runs under the ESM resolution; dist/index.cjs against dist/ui/index.js is the dev's one-time H5 probe (true for all four). check:dual-build-cjs-loads was NOT MEASURED locally (prerequisite) and is hosted by Build Core, which is success on the head. Acceptable for this card; a kept CJS pin would be its own card if ever wanted.
  • Check-runs on the head, as read 2026-10-07T04:55Z: 28 completed success — among them Check Changeset (both runs), Spec property liveness, Build Docs, Build Core (hosts check:dual-build-cjs-loads), Type Check · source gates (hosts check:generated --reconcile-only and check:export-origins), Type Check · consumer gates (hosts check:api-surface, check:exported-any, check:dual-source-exports, check:browser-reachable-entries), Type Check · debt ledger, Temporal Conformance, the three Dogfood shards and their gate, Governed Surface Queue Guard, and the four PR-automation guards on both runs; 4 skipped (Auto Label and Check PR Size on the second run, Console Pin Gate, Packed-tarball smoke); 0 failed; 8 in progress (Lint & Repo Gates, Test Core shards 1 to 6, Type Check · workspace). Not waited on or polled. This verdict is on the contract; the landing still waits on every check green.

Implemented-by: claude/issue-22047-spec-ui-anonymous-form-intake
Reviewed-by: session_01GV6oYwgc1kWiUCb1YaprQ7

VERDICT: PASS


Generated by Claude Code

…s no server state; precedent and ruling attributed correctly

The changeset's last bullet and the module's 'What stays' paragraph no
longer group anonymousFormObjectName with the halves that read server
state, and name only the two checks that can withhold an open form. The
'Why this half lives in packages/spec' paragraph cites expandViewContainer
as the placement precedent only, states Prime Directive 2 as ADR-0053
D-D2 reads it, and attributes the byte-for-byte reason to the triage
ruling. Comment and changeset text only.

Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 7, 2026 05:40
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 7, 2026 05:40
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 56c8844 Oct 7, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22047-spec-ui-anonymous-form-intake branch October 7, 2026 06:06
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