Skip to content

docs(spec): state the template dialect's real placeholder contract - #16416

Open
huangyiirene wants to merge 2 commits into
mainfrom
claude/issue-15947-template-dialect-prose-example
Open

docs(spec): state the template dialect's real placeholder contract#16416
huangyiirene wants to merge 2 commits into
mainfrom
claude/issue-15947-template-dialect-prose-example

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #15947

Docs-only. Rewords one imprecise prose example in packages/spec, on the reading in triage comment 5556793638 rather than on the card body.

What the card claimed, and what is actually there

The card reported a declared-vs-enforced contradiction: spec declares a {{var}} template grammar, the only implementation reads single-brace {field}. Re-measured on main at fd75728bc6, that premise does not hold.

TemplateExpressionInputSchema is typedExpressionStringArm('template') unioned with a dialect: 'template' envelope. It judges the dialect tag and non-emptiness and nothing else — and the same docblock already said so one clause later: "No template syntax is judged at parse time." There is no declared accept-set for an implementation to contradict. packages/spec/src/shared/typed-expression-envelope-dialect.test.ts:64 pins that directly, passing the string not a template {{{ through as unjudged.

So the entire defect is that the docblock introduced the dialect as "anything with {{var}} interpolation", which reads as a grammar. It is not one.

Why the fix is not "the two spellings are equivalent"

The prescription this started from was to say {{var}} and {var} are equivalent and normalised by the implementation. Measured across this tree that is true of titleFormat and false of every other template-typed slot — so writing it into a docblock that names notification bodies and prompt templates in the same sentence would have replaced one imprecise claim with a more expensive one.

render path accepts
the registered template dialect engine — @objectstack/formula's templateEngine, registered at packages/formula/src/registry.ts:44, HOLE_RE at template-engine.ts:31 {{var}} only
messaging service-messaging/src/template-renderer.ts:39, email plugin-email/src/template-engine.ts:34, i18n service-i18n/src/file-i18n-adapter.ts:77 {{var}} only
titleFormat — objectui record-title.ts:173, objectstack metadata-protocol/src/protocol.ts:11402 both; {{var}} normalised down to {var} first

That is also the real reason the in-repo single-brace titleFormat authors are fine: they sit on the one render path that takes both. The new prose says exactly that, and no more.

The diff

  • packages/spec/src/shared/expression.zod.ts — the TemplateExpressionInputSchema docblock (the card's anchor), plus the template row of the ## Dialects table in the module header, which carried the identical claim 260 lines above and publishes into the same reference page. Leaving it would have left the file disagreeing with itself.
  • packages/spec/src/data/object.zod.ts — the titleFormat describe(), the one slot where both spellings really do work.
  • content/docs/references/** — regenerated, never hand-edited (check:generated --fix; exactly one artifact was proved stale).
  • .changeset/ — patch, see below.

Not touched: TemplateExpressionInputSchema's judging logic, any accept-set, any export, any of the in-repo authors, content/docs/releases/.

Changeset: patch, not skip-changeset

skip-changeset is for a diff that publishes nothing from a released package. Both edited strings publish:

  • the TSDoc reaches packages/spec/dist/expression.zod-CJCppEJk.d.ts:247
  • the describe() reaches packages/spec/dist/index.js and dist/index.mjs

@objectstack/spec therefore ships changed bytes, and takes a patch. Nothing breaking, so no ADR-0087 marker is owed — check:adr-0087-registration and check:changeset-no-major are green.

Verification

  • pnpm --filter @objectstack/spec test — 482 files, 13102 tests, exit 0.
  • packages/spec/src/shared/expression-dialect-docs.pin.test.ts pins the ## Dialects table against the ExpressionDialect enum; its own scope note says rewording a cell is free and adding or dropping a row is not, which is exactly what this diff respects. Run by name alongside typed-expression-envelope-dialect.test.ts: 2 files, 39 tests, exit 0 — including the pin's anti-vacuity assertion, which proves the reworded row still parses.
  • pnpm --filter @objectstack/spec typecheck — exit 0.
  • pnpm --filter @objectstack/spec check:generated — all 15 generated artifacts up to date.
  • The 91 gate families derived by node scripts/pm/dispatch-gates.mjs --commands for this change set: 90 measured green. pnpm check:dual-build-cjs-loads reports PREREQUISITE NOT MET — it reads built output for packages this worktree has no dist/ for (hono, account, setup, studio and others), so it needs the repo-wide build and is left to CI. Four others (check:doc-formula-expressions, check:doc-security-posture, check:skill-examples, check:docs-transcript-drift) reported the same prerequisite on first run and are green after building the lint and client-react closures.
  • pnpm lint (eslint . --no-inline-config, whole repo, not narrowed) — exit 0 in 58s at 760ca754d3.

Generated by Claude Code

`TemplateExpressionInputSchema`'s docblock illustrated the `template`
dialect with `{{var}}` interpolation, which reads as a declared grammar.
It is not one: the schema judges the dialect tag and non-emptiness and
nothing else, and the accepted placeholder spelling belongs to whichever
renderer consumes the slot.

Measured on this tree, the two spellings are not interchangeable
everywhere, so "they are equivalent" would be imprecise in the other
direction:

- `@objectstack/formula`'s registered `templateEngine`, the messaging
  renderer, the email plugin and the i18n adapters match `{{var}}` only.
- The `titleFormat` renderers accept both and normalize `{{var}}` down
  to `{var}` first, which is why single-brace `titleFormat` values are
  legal by construction.

Docblock and `.describe()` prose only. No schema, export or accept-set
movement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:data tooling labels Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 1 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/shared/expression.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/data-modeling/formulas.mdx (via titleFormat (symbol, a field of const object ObjectSchemaBase, an authorable key of data/Object))
  • content/docs/data-modeling/objects.mdx (via titleFormat (symbol, a field of const object ObjectSchemaBase, an authorable key of data/Object))
  • content/docs/data-modeling/schema-design.mdx (via titleFormat (symbol, a field of const object ObjectSchemaBase, an authorable key of data/Object))

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

  • content/docs/releases/v17.mdx (via titleFormat (symbol, a field of const object ObjectSchemaBase, an authorable key of data/Object))

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
  • 1 changed file(s) yielded no anchor (packages/spec/src/shared/expression.zod.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 130 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 dff0bdd3ac51df2653e1bd7beb7f2b3ca0fa58c3packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 dff0bdd3ac51df2653e1bd7beb7f2b3ca0fa58c3 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

PM — the deviation from the dispatch wording is endorsed, and the dispatch was wrong. Stating that plainly, because the audit trail should not read as a dev freelancing.

My claim comment on #15947 (5562036736) instructed: reword the example so it says {{var}} and {var} are "equivalent and normalised by the implementation." The dev did not write that sentence, and was right not to.

Why my instruction was wrong. It is true of titleFormat — which is the single renderer the triage seat measured (objectui record-title.ts:173, and metadata-protocol/src/protocol.ts:11402). It is false of every other template-typed consumer, each of which matches double-brace only:

consumer site
@objectstack/formula templateEngine registry.ts:44, HOLE_RE at template-engine.ts:31
service-messaging template-renderer.ts:39
plugin-email template-engine.ts:34
service-i18n file-i18n-adapter.ts:77

That docblock introduces the dialect for notification bodies and prompt templates, not just object titles. My wording would have replaced one imprecise claim with a broader and more expensive one — an author reading it would have been told single-brace works in an email template, where it does not.

The error is mine and it has a name: triage measured one renderer's behaviour, and I restated it as a property of the dialect. The dev measured the rest of the population and wrote the per-renderer truth instead. That is the correct handling of a PM instruction that does not survive contact with a measurement — and it is why the dispatch said to report rather than absorb a disagreement.

Also endorsed: the one-line scope widening. The identical claim sits in the same file's ## Dialects table row at :23 and publishes into the same generated reference page. Fixing the example while leaving the table would have left the file contradicting itself. That is inside the card's intent, not beyond it.

Landing: flipping to ready and arming auto-merge. Clause ② is no and measured — check:api-surface and check:authorable-surface both green, so no export and no authorable key moved, and accept/reject behaviour is byte-identical. The patch changeset is right and the reasoning for rejecting skip-changeset is correct: both edited strings publish, measured into dist/expression.zod-CJCppEJk.d.ts:247 and into dist/index.js / dist/index.mjs.

Two pieces of method worth naming, since they are what made this report trustworthy: check:generated was baselined on pristine main (15/15 green) before concluding the post-edit staleness was the diff's own; and the one out-of-scope finding was not filed because the dedupe search returned #16321 as its first hit — the same search doubling as the positive control proving the channel returns hits.

If Test Core comes back red here, read Test Core (5/6)'s duration first: it is being killed at its 30-minute wall on a large share of runs today (#16173), which is not this diff's doing — this one moves no executable byte.


Generated by Claude Code

@huangyiirene
huangyiirene marked this pull request as ready for review September 6, 2026 21:28
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:data size/s tooling

Projects

None yet

2 participants