Skip to content

fix(spec): the sharing.publicLink describe says the value is a slug and names where it is served - #22159

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22079-publiclink-describe
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22079-publiclink-describe

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22079
Clause-②: no

The describe half of the card. The redirect half landed as PR #22098 (dd39171835), so this PR closes the card.

What changes

FormView.sharing.publicLink (packages/spec/src/ui/sharing.zod.ts) described itself as a Generated public share URL. It is neither generated nor a URL. The describe now reads:

Public slug of the form, chosen by the author: not a URL, and never generated. /forms/x, forms/x and x name one slug, x. It is served to anonymous visitors only while enabled and allowAnonymous are both true: the REST form door answers GET /api/v1/forms/:slug (default API base), and a host that serves the console shows the form at /_console/f/:slug and redirects /forms/:slug there, query string carried.

The two generated reference pages that print it (content/docs/references/ui/sharing.mdx, content/docs/references/ui/view.mdx) are regenerated with gen:docs after the spec build (which runs gen:schema). Nothing is hand-edited. The changeset is @objectstack/spec patch.

Describe text only. The accepted values, keys, types and exports do not change.

Each clause, and what makes it true on every host (read at base ef1fcb26a2)

  • A slug, not a URL. publicFormSlug() (packages/spec/src/ui/anonymous-form-intake.ts:69-71) strips leading slashes and one forms/ prefix, so /forms/x, forms/x and x are one slug. Every door compares that slug to the request's :slug (anonymousFormIntakeCandidates, c.slug !== slug in findPublicFormView, packages/rest/src/rest-server.ts:10749).
  • Never generated. No server path writes it. The only first-party writer outside authored metadata is objectui's Public Forms page, which saves /forms/ plus the slug the user typed (objectui apps/console/src/pages/developer/PublicFormsPage.tsx:243, :314, at 9990f9e). objectui's validateSharingConfig refuses an enabled config without one; it does not make one.
  • Only while enabled and allowAnonymous are both true. anonymousFormIntakeSlug() returns null unless enabled === true, allowAnonymous === true and publicLink is a non-empty string (anonymous-form-intake.ts:74-81). Those two switches are necessary, not sufficient: a withdrawal in another layer or the tenancy posture can still withhold the form. "Only while" claims no more than that.
  • The REST form door. registerFormEndpoints registers GET {basePath}/forms/:slug and POST {basePath}/forms/:slug/submit (rest-server.ts:10842, :11012) for every base (registerForBase, :4519). The default base is /api/v1 (getApiBasePath, :4472-4475); api.apiPath can move it, hence "(default API base)".
  • The console page, only where the console is served. The console mounts at the constant CONSOLE_PATH = '/_console' (packages/cli/src/utils/console.ts:54). The pinned console (.objectui-sha a58626c88) routes /f/:slug to the public FormPage (objectui apps/console/src/App.tsx:242). The CLI mounts the console only when the UI tier is on, --no-console / OS_DISABLE_CONSOLE=1 are absent and a console dist resolves (packages/cli/src/commands/serve.ts:5035, :5064). So the describe says "a host that serves the console".
  • The redirect, with its query. GET /forms/:slug answers 302 to ${CONSOLE_PATH}/f/${encodeURIComponent(slug)} plus the request's query string, only when the anonymous door serves that slug (console.ts:578-585, anonymousFormDoorServes :865). It is registered inside createConsoleStaticPlugin, so it exists exactly where the console does.

Pins and readers

git grep "Generated public share URL" at base: three hits, the describe and the two generated pages. No test pins the text. At head: one hit, the changeset quoting the old text.

Out of scope

Verification

All at head 8d2e686f3 unless named. Each exit code captured before any pipe.

  • Build: pnpm --filter @objectstack/spec build exit 0 (at 8479c9a3a; the later commit touches only the two generated pages, not packages/spec).
  • Generated artifacts: pnpm --filter @objectstack/spec check:generated at 8479c9a3a: exit 1, 1 of 15 artifact(s) stale: content/docs/references/**. Then gen:docs (exit 0) changed exactly ui/sharing.mdx and ui/view.mdx, one line each. At 8d2e686f3: All 15 generated artifacts are up to date, exit 0.
  • Tests: pnpm --filter @objectstack/spec test: Test Files 623 passed (623), Tests 18619 passed | 1 todo (18620), exit 0.
  • Typecheck: pnpm --filter @objectstack/spec typecheck exit 0 (check:test-typecheck: OK).
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 103 commands for the 4 paths. All 103 ran. Six first exited 3 (PREREQUISITE NOT MET: @objectstack/formula, @objectstack/lint, @objectstack/client, @objectstack/client-react, @objectstack/objectql and the workspace were not built). They exited 0 after turbo run build of those closures and then the whole workspace (--filter=!@objectstack/docs). dispatch-gates --ran: 103 derived famil(ies) accounted for, 103 run, 0 NOT-MEASURED. Also run, for the six roster families the derivation flags as rostered under one of these paths: check-changeset-fixed, check:meta-url-spelling, check:spec-changes, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, all exit 0.
  • Lint (narrowed, declared): pnpm exec eslint --no-inline-config --format json packages/spec/src/ui/sharing.zod.ts: 1 file, 0 errors, 0 warnings. The population comes from eslint.config.mjs:971 (**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}), so the .md/.mdx paths are outside it. The config never enables type-aware linting (eslint.config.mjs:326-328), so this diff cannot change the verdict on any file it does not touch. The full pnpm lint is CI's.
  • Control bytes: check:nul-bytes exit 0, and a self-scan of the 4 files for C0/DEL bytes found none.
  • No reverse verification or ablation: no type and no behaviour changes, and no test was added. Describe prose is not pinned, because nothing parses it.

Generated by Claude Code

claude added 2 commits October 8, 2026 02:47
…nd names where it is served

The describe called the value a "Generated public share URL". The author
chooses it, nothing generates it, and the server reads it as a slug
(publicFormSlug: /forms/x, forms/x and x are one slug). The describe now
says so and names the REST form door and, on a console host, the console
page /_console/f/:slug with the /forms/:slug redirect.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…publicLink describe

pnpm --filter @objectstack/spec gen:docs, after check:generated named
content/docs/references/** as the one stale artifact.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/s label Oct 8, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:ui tooling labels Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 4 documentable anchor(s).

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

  • content/docs/protocol/objectui/actions.mdx _(via /console/f/:slug (route, a path literal in SharingConfigSchema))
  • content/docs/ui/forms.mdx _(via /console/f/:slug (route, a path literal in SharingConfigSchema), /api/v1/forms/:slug (route, a path literal in SharingConfigSchema), /forms/:slug (route, a path literal in SharingConfigSchema))
  • content/docs/ui/public-data-collection.mdx (via /api/v1/forms/:slug (route, a path literal in SharingConfigSchema), /forms/:slug (route, a path literal in SharingConfigSchema))

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

  • content/docs/releases/v17/17-6.mdx (via /api/v1/forms/:slug (route, a path literal in SharingConfigSchema), /forms/:slug (route, a path literal in SharingConfigSchema))
  • content/docs/releases/v17/17-7.mdx (via /forms/:slug (route, a path literal in SharingConfigSchema))

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
  • 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 fec87e7e0753f66a6bb92607f436b6b1429f4112 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json fec87e7e0753f66a6bb92607f436b6b1429f4112

⚠️ 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 fec87e7e0753f66a6bb92607f436b6b1429f4112 → 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: 8d2e686f38b1bfe69d4e211623b2ea40837f1476
Local-runs: none

PR #22159 on card #22079: the describe half of the card. The redirect half landed as PR #22098 (dd39171835), an ancestor of this PR's merge base ef1fcb26a2. Inputs read on GitHub: the card body and all 11 comments (triage 6038401972 / 6044725872, claim 6051101486, dev report 6051575021, seat ACCEPT 6051595691, the redirect half's records), the PR body, its 4-file list and net diff against main at the head (+26/-3, 2 commits), the one PR comment (docs-drift check) and the head's check-runs. Source at the head was read through the contents API; nothing was checked out, built, run or re-run. Reviewed 2026-10-08T03:51Z by an isolated subagent of the seat session.

① Derived judgments

The diff changes ONE .describe() string, SharingConfigSchema.publicLink (packages/spec/src/ui/sharing.zod.ts:98-104), and the two generated pages that print it. What that implies, each judged:

  • Accept set: unchanged. The six keys (enabled, publicLink, password, allowedDomains, expiresAt, allowAnonymous), their types, optionality and defaults are byte-identical around the edit; the strictObject wrapper and its alias table are untouched. No authorable-surface movement. RIGHT.
  • Public exports: unchanged. SharingConfigSchema, SharingConfig, SharingConfigParsed are as before; no api-surface regeneration is owed and none is in the diff. RIGHT.
  • Emitted text: changed. The JSON Schema description for publicLink and the two reference rows (content/docs/references/ui/sharing.mdx:58, content/docs/references/ui/view.mdx:489) now carry the new sentence, one line each, produced by the repo generator (gen:docs after the spec build) and identical to the describe. Nothing under references/ is hand-edited (Documentation Guardrails). The describe carries no pipe character, so the MDX table cell is intact; Build Docs on the head is success. RIGHT.
  • One carrier, so "of the form" is the right noun. FormViewSchema.sharing (view.zod.ts:4386) is the only carrier of SharingConfigSchema (the module header, sharing.zod.ts:15-23, records the measurement; the two regenerated pages are exactly the schema's own page and the view page). RIGHT.

The describe's six clauses, each read against source at the head:

  1. "chosen by the author: not a URL, and never generated" — no server-side writer; anonymousFormIntakeSlug only reads it. The objectui-side reading (the Public Forms page saves /forms/ plus the typed slug) is the dev's and the seat's, taken as reported and consistent with the card's 17.7.0 measurement. RIGHT.
  2. "/forms/x, forms/x and x name one slug, x" — publicFormSlug (packages/spec/src/ui/anonymous-form-intake.ts:69-71): strip leading slashes, then one forms/ prefix. RIGHT.
  3. "served to anonymous visitors only while enabled and allowAnonymous are both true" — anonymousFormIntakeSlug (:74-81) returns null unless both are === true and publicLink is a non-empty string; findPublicFormView (packages/rest/src/rest-server.ts:10741-10756) iterates only those candidates and then applies the withdrawal layers and the tenancy posture. "Only while" states a necessary condition and promises nothing more. RIGHT; the hand-written content/docs/ui/forms.mdx:23 says the same.
  4. "the REST form door answers GET /api/v1/forms/:slug (default API base)" — registerFormEndpoints registers ${basePath}/forms/:slug (rest-server.ts:10842) for every base registerForBase is called with (:4519; unscoped and environment-scoped), and getApiBasePath() is api.apiPath ?? basePath/version (:4472-4475), /api/v1 by default. The parenthesis covers apiPath and the scoped mount. RIGHT.
  5. "a host that serves the console shows the form at /_console/f/:slug" — CONSOLE_PATH = '/_console' is a constant (packages/cli/src/utils/console.ts:54); the console's /f/:slug public route is what the card measured on 17.7.0 and what view.zod.ts:3415, :4392 and forms.mdx:13, :278 already name. "A host that serves the console" is the right guard: the mount sits behind --no-console / OS_DISABLE_CONSOLE=1. RIGHT.
  6. "redirects /forms/:slug there, query string carried" — console.ts:578-585: app.get('/forms/:slug', ...) answers 302 to ${CONSOLE_PATH}/f/${encodeURIComponent(slug)} plus new URL(c.req.url).search, only when anonymousFormDoorServes (:865) gets a 200 from the door in-process; it is registered inside the console plugin, so it exists exactly where clause 5 does. RIGHT, and it is in the base (PR feat(cli): the authored public form path /forms/:slug redirects to the console form page when the anonymous door serves it #22098).

Spelling: route parameters are spelled :slug, the spelling view.zod.ts already uses, so no angle-bracket fragment reaches the MDX or a GitHub body. Backticks inside a describe are an exercised generator path (view.zod.ts:3445). The REST-door clause goes beyond triage's wording in 6044725872 (slug plus the console URL); it is what keeps the describe true on a host without the console. RIGHT.

Nothing in ① is judged wrong.

② Semver level

.changeset/22079-publiclink-describe.md: '@objectstack/spec': patch; body carries Clause-②: no, no arm, no ADR-0087 marker (none owed: nothing breaking). The diff publishes a changed describe string, which ships in the package's runtime schema and JSON Schema description, and nothing else: no key, type, default or export moves, so no widening and no narrowing. patch is the floor for a fix in a released package and the ceiling for prose; skip-changeset would be wrong, since the package publishes. PR body line 2, the changeset, claim 6051101486 and triage 6044725872 all say Clause-②: no and agree. Check Changeset on the head: success (both runs). The PR title fix(spec): ... matches the level. RIGHT.

③ Boundary flags

Dev report 6051575021: open_questions: [], nothing to answer. Its deviations, each judged:

  • Commit trailers are the model-free pair and the PR footer is the session-URL form: both head commits carry Claude-Session: and Co-authored-by: Claude and no model identifier. RIGHT per AGENTS.md.
  • :slug spelling and the REST-door clause beyond triage's wording: RIGHT (①).
  • Six extra roster families run; a full workspace build spent to measure check:dual-build-cjs-loads instead of declaring it NOT MEASURED; the worktree created fresh off ef1fcb26a2 and removed: dev-side cost, no contract effect. Fine.

Out-of-scope findings, both kept as Acceptance notes by the seat in 6051595691, each judged:

  • (a) Hand-written docs omit the visitor link. content/docs/ui/forms.mdx and content/docs/ui/public-data-collection.mdx name /_console/f/:slug and the two REST endpoints, not the /forms/:slug redirect. Read at the head: nothing on either page, nor on content/docs/protocol/objectui/actions.mdx:180-185, is now FALSE (forms.mdx:13, :23, :91, :99, :278; public-data-collection.mdx:30, :38-42, :57), so this is an omission, not drift; the docs-drift check's three rows are re-verification prompts and none contradicts. An Acceptance note is the right carrier for THIS PR. Escalated to the seat: this is the second PR after feat(cli): the authored public form path /forms/:slug redirects to the console form page when the anonymous door serves it #22098 to park the same note with no carrier; one docs-only card (the forms.mdx mode table names the visitor link /forms/:slug and its redirect) would stop it being lost. Not a condition on this verdict.
  • (b) A full URL stored in publicLink is a slug that nothing can match. z.string() accepts https://host/forms/x; publicFormSlug leaves it whole (no leading slash, no forms/ prefix), so it is a slug containing / that neither GET .../forms/:slug (one path segment) nor the redirect can ever match: a silent 404 for every visitor. Read-only inference, unmeasured; no first-party producer writes one. The new describe makes the trap visible at authoring ("not a URL") and claims no refusal, so declared-not-enforced does not bite here; a parse-time refusal would be Clause-②: yes (narrowing) in the spec lane, a separate card. Escalated to the seat as a metadata-authoring trap under Prime Directive chore: version packages #10 (file an issue): the alias table in the same schema maps url, shareUrl and shareLink onto publicLink (sharing.zod.ts:76-79), which invites exactly that value shape. Recommend filing; not a condition on this verdict.

Noted, no action: the alias-block comment The slug/URL. (sharing.zod.ts:74) is now looser than the describe beneath it; a code comment, not an authored-facing surface.

Scope and shape: 4 files, all inside the surface claim 6051101486 names; no governed path. Fixes #22079 is right: the card's other defect (the authored path answering 404) was closed by PR #22098, and triage 6044725872 scoped the remainder to exactly this describe. The card this PR closes must claim this branch, Part-of PR must not also close its card and No other open PR may claim the same issue: success.

Check-runs on the head, as read (conclusions are the gate verdicts; in_progress is not a pass):

  • 2026-10-08T03:33Z: 16 success, 5 skipped, 15 in_progress, 0 failure.
  • 2026-10-08T03:36Z: 20 success, 5 skipped, 11 in_progress, 0 failure (Type Check · source gates, Build Docs, Check Changeset, Governed Surface Queue Guard, Spec property liveness, Dogfood Verify CLI, Build Core among the successes).
  • 2026-10-08T03:51Z (final read): 32 success, 5 skipped, 1 in_progress, 0 failure. Green by name: Lint & Repo Gates, TypeScript Type Check (with Type Check · source gates / workspace / consumer gates / debt ledger), Build Core, Dogfood Regression Gate (aggregator and all three shards), Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard, Check Changeset, Spec property liveness, Build Docs, Test Core (2/6) through (6/6). Still running: Test Core (1/6); its Test Core aggregator context had not been created at this read. No gate family the diff derives has answered anything but success; the one open shard is the seat's to read green before landing, and this verdict does not stand in for it.

Implemented-by: claude/issue-22079-publiclink-describe
Reviewed-by: session_01RPo7FUd6bSnAfkWMAKi848

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 04:04
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 04:04
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit d7c5c33 Oct 8, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22079-publiclink-describe branch October 8, 2026 04:41
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/s tooling

Projects

None yet

2 participants