Skip to content

feat(formula, plugin-email, lint)!: a mail template hole whose formatter the whitelist does not name is refused by name, never rendered raw - #27217

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-j1jtvb
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-j1jtvb

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #26948

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — skills/objectstack-formula/SKILL.md:444 already teaches the formatter whitelist as TEMPLATE_FORMATTERS, which is exactly the rule this PR enforces, and no skills/** file teaches or shows a mail hole whose formatter is outside it (grep over skills/** for formatter, email template and emailTemplates: none of the hits teaches a mail formatter name); the docs page content/docs/automation/email-templates.mdx gains the refusal here.

维护者速读

  • 改了什么:邮件模板里写错或不存在的格式化器(如 {{ amount | curency:EUR }})现在在 os validate / os build / os lint 和元数据发布门(422 INVALID_METADATA)被按名拒绝,并列出全部可用格式化器;已存的旧模板渲染时报 TEMPLATE_INVALID,不再发出原始值。
  • 为什么改:原来邮件照常发出,金额处显示 1234.5、日期处显示原始时间戳,作者毫无提示;应用内模板早就拒绝这种写法,两种方言不一致。
  • 风险与回滚:收窄(BREAKING),只影响拼错格式化器的模板;仓内出厂模板、示例、种子、技能零命中。回滚即 revert 本 PR。
  • 先看:packages/formula/src/template-engine.ts(judgeHoleFormatter 新增的 known-formatter 规则)、packages/plugins/plugin-email/src/template-engine.ts(去掉原始值回退)、content/docs/automation/email-templates.mdx。
  • 两分钟验证:pnpm --filter @objectstack/lint exec vitest run src/validate-email-template-hole-grammar.unknown-formatter.test.ts,预期 8/8 通过(三个面、三条命令、发布门各被拒)。
  • 需要你做的:无,席位落地(须在 feat(spec, formula, plugin-email, lint): a mail template formats an amount in the record's own currency with currency:@path #26937 之后落地,届时合入 main)。

What changes

  • @objectstack/formula, judgeHoleFormatter (template-engine.ts): a third rule, known-formatter, judged after one-formatter and before the currency argument. A hole whose formatter is a single word that TEMPLATE_FORMATTERS does not hold is refused, and the sentence names the hole and lists every known formatter. TemplateHoleGrammarRule gains 'known-formatter'. The check reads the exported list, so an inherited object key (toString, constructor) is not a formatter either.
  • The mail grammar (mail-hole-grammar.ts): no code change. parseMailHole already calls judgeHoleFormatter, so it now carries the refusal and judgeMailHoleGrammar reports the hole. Only its header names the new rule.
  • @objectstack/plugin-email renderer (template-engine.ts): the // unknown formatter → raw fallback is gone. A refused hole throws TEMPLATE_INVALID from the grammar as the other refusals do, and EmailService prefixes the template and locale. If formatValue ever answered undefined for a name the grammar passed, the render throws TEMPLATE_INVALID too, so a raw value is never printed in its place.
  • @objectstack/lint email-template-hole-grammar: no code change (it reads judgeMailHoleGrammar). It now refuses the hole at os validate, os build, os lint and the runtime publish gate. Only its header comment names the new case.
  • The in-app dialect: its own parse already refused an unknown formatter and keeps its own sentence. One side effect: an inherited object key ({{ x | toString }}) used to pass !FORMATTERS[name] and run as a formatter, printing [object Object]. Now the shared rule refuses it.
  • Docs: content/docs/automation/email-templates.mdx gains a paragraph on the rule. The TEMPLATE_INVALID TSDoc on IEmailService.sendTemplate / renderTemplate (packages/spec/src/contracts/email-service.ts) names the third hole it refuses.
  • Changesets: formula, plugin-email and lint minor (narrowed, (narrowing) arm, ADR-0087 not-required (no-migration-prescription): a conversion cannot know which formatter was meant), spec patch (TSDoc only, none). @objectstack/metadata-protocol gets only a new test file, which is not in its published files, so it has no entry.

Census (on origin/main 72629f3)

I grepped every {{ path | name hole in the repo, excluding CHANGELOGs, release notes and generated translations. Then I checked each mail template carrier by hand: the showcase and app emails, plugin-email auth-templates.ts, the spec seeds, dogfood and downstream fixtures, the upgrade-harness SQL fixtures and skills/**. Examples 0, seeds 0, skills 0, apps 0. The only mail holes with a formatter are the datetime:long holes in auth-templates.ts and the docs examples, and all of them use known formatters. The unknown names the grep found (bogus, nope, shout, no_such_formatter) are in-app dialect tests. They are refused there already and their results do not change.

Merge order with #26937 and #26939

Serial with #26937 (merged at 22058635). This branch kept its hunks out of #26937's, and merged main once #26937 landed (9b72c4e1, no rebase). The one conflict was TemplateHoleGrammarRule. It now has five members: one-formatter, known-formatter, currency-code-literal, currency-code-path-mail-only and currency-code-path-undeclared. judgeHoleFormatter keeps #26937's dialect parameter, and known-formatter sits after one-formatter and before the currency block. 395c096e adds the unknown-formatter clause to MAIL_TEMPLATE_HOLES and its comment (the CURRENCY_ARGUMENT_RULE sentence stays verbatim), and to the validateEmailTemplateHoleGrammar roster comment in authoring-rules.ts. It also widens the spec changeset and corrects the IEmailService TSDoc. 97613711 regenerates content/docs/references/system/email-template.mdx.

Residual (follow-up #27225). This PR refuses an unknown one-word formatter. Formatter text that is not one word ({{ x | date-long }}, {{ x | currency :EUR }}, {{ x | two words }}) fails FILTER_RE / WORD_RE, so parseMailHole answers null. Every mail door then accepts it and the renderer prints the hole verbatim (pinned at text-field-html-outlets.pin.test.ts:284), while the in-app dialect refuses it. #27225 carries that.

Tests

  • New packages/formula/src/template-unknown-formatter.test.ts (9 cases). It covers the refusal and its sentence, every whitelisted name passing, inherited keys refused, the one-formatter rule winning on a chained hole, non-word text left verbatim, parseMailHole / judgeMailHoleGrammar locating triple-braced holes, the in-app sentence unchanged, and in-app toString now refused.
  • New packages/plugins/plugin-email/src/template-unknown-formatter.render.test.ts (5 cases). It covers renderTemplate on all three faces refused with the template, locale, hole and known list, sendTemplate sending nothing, and the corrected name rendering.
  • Re-pinned packages/plugins/plugin-email/src/template-engine.test.ts: "falls back to the raw value for an unknown formatter (no throw)" is now "refuses an unknown formatter with TEMPLATE_INVALID, never the raw value", for both entry points.
  • New packages/lint/src/validate-email-template-hole-grammar.unknown-formatter.test.ts (8 cases). It covers all three faces, os validate / build / lint, and the runtime publish gate.
  • New packages/metadata-protocol/src/runtime-authoring-gate.email-template-unknown-formatter.test.ts. It checks the 422 INVALID_METADATA envelope (code and status, plus issues), and that the corrected name publishes.

Acceptance notes

  • Text after the | that is not a single word ({{ x | two words }}, {{ x | }}) still names no formatter in the mail grammar and is left as written, as text-field-html-outlets.pin.test.ts pins. The in-app dialect refuses the same text as an invalid hole. This PR leaves that difference alone, because the card is about formatter names.
  • In the ablation, the sendTemplate case and the re-pinned template-engine.test.ts case stayed green. That is the renderer's own formatted === undefined guard: it still throws TEMPLATE_INVALID when the grammar rule is removed. That is the intended second line, and the face cases, which assert the grammar's sentence, went red.

Verification

Run on head 857e696 (origin/main 72629f3 + 3 commits).

Package tests and typecheck (under os-verify-lock)

pnpm --filter @objectstack/formula --filter @objectstack/plugin-email --filter @objectstack/lint --filter @objectstack/metadata-protocol --filter @objectstack/service-messaging test, then the same filters plus @objectstack/spec typecheck, then pnpm --filter @objectstack/spec check:generated: VERDICT command-exit 0.

  • formula: Test Files 57 passed (57), Tests 1493 passed.
  • plugin-email: Test Files 41 passed (41), Tests 717 passed.
  • lint: Test Files 172 passed (172), Tests 7530 passed.
  • metadata-protocol: Test Files 278 passed, 4 skipped (282), Tests 21750 passed, 26 skipped.
  • service-messaging: Test Files 61 passed (61), Tests 734 passed.
  • typecheck: formula, plugin-email, lint, metadata-protocol and spec all Done, with check:test-typecheck OK.
  • spec check:generated: "All 14 generated artifacts are up to date".
  • eslint --no-inline-config over the changed .ts files: exit 0.
Ablation of the rule

node scripts/ablation-replace.mjs made the known-formatter branch unreachable (anchor hit once, --hold), followed by pnpm --filter @objectstack/formula build. ablation-dist-preflight confirmed the marker in dist/index.js and dist/index.mjs. Results, all red as expected:

  • formula new file: 5 of 9 failed.
  • plugin-email new file plus re-pin: 3 of 36 failed (the three face cases).
  • lint new file: 7 of 8 failed.
  • metadata-protocol new file: 1 of 1 failed.

The restore was --restore (blob equal to HEAD, git diff HEAD empty), followed by a rebuild. ablation-dist-preflight --absent confirmed the marker was gone from all 6 built files and the tree was clean.

Derived gates

node scripts/pm/dispatch-gates.mjs --commands: 105 commands, run serially. 103 exit 0, including check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check:doc-authoring and check:docs. 2 exit 3, PREREQUISITE NOT MET (they need a full repo build, so nothing was measured): spec check:skill-examples and check:dual-build-cjs-loads. CI runs both. --ran reconciliation: 105 derived, 103 run, 2 NOT-MEASURED, 0 UNRUN.

Dogfood (owed by the narrowing arm)

pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' build && pnpm --filter @objectstack/dogfood test on head 857e696 (no OS_TEST_SHARD, no path): VERDICT command-exit 0. Test Files 348 passed, 3 skipped (351); Tests 2804 passed, 20 skipped (2824). No failures.


Generated by Claude Code

…r the whitelist does not name is refused by name, never rendered raw

Claude-Session: https://claude.ai/code/session_01WGufzgBzVZJqh8CQJLrZu8
Co-authored-by: Claude <noreply@anthropic.com>
… mail formatter refusal on every door

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 857e696253024f3894f7a6fc4f1543b090948ce6
Local-runs: probe — git merge-tree --write-tree origin/main 857e696 (object store only, no checkout, no build, no test) to read what the head becomes once it carries #26937, which merged before this PR was opened
Scope: packages/formula/src/template-engine.ts, packages/formula/src/mail-hole-grammar.ts, packages/formula/src/template-unknown-formatter.test.ts, packages/lint/src/validate-email-template-hole-grammar.ts, packages/lint/src/validate-email-template-hole-grammar.unknown-formatter.test.ts, packages/metadata-protocol/src/runtime-authoring-gate.email-template-unknown-formatter.test.ts, packages/plugins/plugin-email/src/template-engine.ts, packages/plugins/plugin-email/src/template-engine.test.ts, packages/plugins/plugin-email/src/template-unknown-formatter.render.test.ts, packages/spec/src/contracts/email-service.ts + packages/spec/src/system/email-template.zod.ts, content/docs/references/system/email-template.mdx, packages/lint/src/authoring-rules.ts, packages/plugins/plugin-email/src/email-service.ts, packages/plugins/plugin-email/src/templates/auth-templates.ts, packages/plugins/plugin-email/src/text-field-html-outlets.pin.test.ts, packages/formula/src/mail-html-hole-placement.ts, packages/services/service-messaging/src/email-channel.ts, packages/services/service-messaging/src/inbox-channel.ts, packages/services/service-automation/src/builtin/notify-node.ts, packages/plugins/plugin-auth/src/auth-manager-email-sms-delivery.ts, skills/objectstack-formula/SKILL.md, docs/adr/0087-metadata-protocol-upgrade-contract.md, scripts/check-adr-0087-registration.mjs

Inputs read: card #26948 (body, triage 6455660116, claim 6465542682, dev report 6467173175), PR #27217 body and file list, the net diff 72629f38..857e696, the check-runs on the head (none exist: total_count 0, combined status pending, 0 workflow runs), PR #26937 and its file list, origin/main at 9dacccf9.

① Derived judgments

  1. judgeHoleFormatter gains known-formatter (packages/formula/src/template-engine.ts): a formatter name that is a single \w+ word and not in TEMPLATE_FORMATTERS is refused, after one-formatter, before the currency argument. Right. The whitelist is Object.keys(FORMATTERS) (own enumerable keys) and the check is Array.prototype.includes, so constructor, __proto__, hasOwnProperty, toString, valueOf are refused and nothing is looked up on the object. There is no second whitelist check anywhere: the lint rule, the publish door and the placement judge all read parseMailHole.
  2. The mail grammar carries it with no code change (mail-hole-grammar.ts): parseMailHole already calls judgeHoleFormatter, so {{ due | dat:long }}, {{ due | Date }} and {{{ link | lowr }}} are now holes with a refusal, located by judgeMailHoleGrammar. Right. The accept set of email-template-hole-grammar, os validate / build / lint and the email_template runtime gate narrows by exactly that set; the door answers 422 INVALID_METADATA with the rule, the path and the known list (pinned in the metadata-protocol test).
  3. The mail renderer (plugin-email/src/template-engine.ts): the refusal throws TEMPLATE_INVALID before formatValue runs; the raw fallback is gone; a second guard throws if formatValue ever answers undefined. Right. EmailService prefixes email template NAME (locale=L) (unchanged wrap at email-service.ts:1465). The inherited keys never reach formatValue's FORMATTERS[name] lookup because the grammar refuses them first.
  4. Send paths, read on origin/main. sendTemplate throws; service-messaging email-channel.ts:425 and inbox-channel.ts:254 both grade TEMPLATE_INVALID permanent, so a delivery row dead-letters on attempt one with the code on the row — one row per recipient, so a bulk or scheduled send loses only the rows of the broken template, and the refusal is on each row, not swallowed. The notify node (notify-node.ts:1147-1166) enqueues through messaging.emit and reports enqueued; the refusal surfaces on the delivery rows, not on the flow run, exactly as the TEMPLATE_INVALID refusals of feat(formula,plugin-email,lint)!: a mail template hole takes one formatter and a literal currency code, refused at authoring and at render #26799 and feat(spec, formula, plugin-email, lint): a mail template formats an amount in the record's own currency with currency:@path #26937 do today. plugin-auth's notices are best-effort (auth-manager-email-sms-delivery.ts:217, logged, never rethrown) and the built-in auth templates use only datetime:long. Right: no send path that degrades gracefully today is turned into a dropped batch; the one thing that changes is that a raw value is no longer delivered.
  5. In-app side effect (templateEngine.compile): {{ x | toString }} used to pass !FORMATTERS[name] and print [object Object]; {{ x | __proto__ }} used to throw a TypeError out of evaluate rather than return an EvalResult. Both are now a compile refusal with the shared sentence, so flow text slots, action copy, confirmText, titleFormat and promptTemplate narrow by the inherited keys too. Right, and a hardening. An unknown word name keeps the dialect's own sentence (pinned).
  6. TemplateHoleGrammarRule gains one member. On the head's base that is the only public type change; on main the union already has four members (feat(spec, formula, plugin-email, lint): a mail template formats an amount in the record's own currency with currency:@path #26937), so the merged union is five. Right once merged (see ④ M1).
  7. Not narrowed, by design: text after | that is not one word ({{ x | two words }}, {{ x | date-long }}, {{ x | currency :EUR }}) is still no hole in the mail grammar and is printed verbatim, pinned by text-field-html-outlets.pin.test.ts:284; the in-app dialect refuses the same text. The PR leaves this as it was. Right as a boundary of this card, see ④ S1.
  8. Census, re-run on origin/main 9dacccf9 (git grep over examples, apps, platform-objects, qa fixtures, skills, plugin-email templates, excluding tests and pins): the only mail holes with a formatter are eight datetime:long in plugin-email/src/templates/auth-templates.ts and one datetime in an email-service.ts comment; in-app holes in examples/app-showcase use default:'…'; skills teach date:long and currency:@currency_code. The upgrade-harness SQL fixtures carry no piped hole. Examples 0, seeds 0, skills 0, apps 0, platform-objects 0, plugin defaults 0 — the dev's census holds.

② Semver level

  • @objectstack/formula, @objectstack/lint, @objectstack/plugin-email: minor, BREAKING banner, Clause-②: no (narrowing), Metadata-protocol: narrowed, one adr-0087: not-required (no-migration-prescription) marker each. AGENTS.md §3: a kind takes at least minor on a package the diff moves; (narrowing) is BREAKING. Matches the diff. Plugin-email's narrowed is read as the render half of the one accept-set narrowing (a stored template is authored metadata judged at its next publish; until then the renderer refuses it), which is how authoring-rules.ts already describes the door.
  • @objectstack/spec: patch, Clause-②: no, Metadata-protocol: none — TSDoc on IEmailService.sendTemplate / renderTemplate only. Matches on the head's base. After the merge it must also carry the MAIL_TEMPLATE_HOLES describe change (④ M1 e); that is still describe-only, so the level and both lines stay.
  • @objectstack/metadata-protocol: a test file only; its published files are dist, README.md, CHANGELOG.md. No changeset is right.
  • The PR body's Clause-②: no (narrowing) and Metadata-protocol: narrowed match the changesets and the diff.
  • ADR-0087: the category's one mechanical check is that the body carries no FROM→TO prescription; none of the three bodies does (the "remedy" names a correction a human makes, it prescribes no rewrite). A conversion cannot know which formatter was meant, so no D2 entry; nothing stored changes shape, so no D3 migration; the D1 half — a machine-readable refusal at the door naming the rule and the remedy — is delivered and pinned. Disposition right. ⚠ The gate itself (check-adr-0087-registration) has not run on this head: see ④ M1.

③ Boundary flags

④ Design judgment

The narrowing, read against the published spec on main: the spec's email_template faces describe a hole as {{ path | formatter[:arg] }} over a whitelist the formula skill names, IEmailService already declares TEMPLATE_INVALID for a hole the authoring rules refuse, and the in-app dialect refuses an unknown name. The PR takes no declared capability away at the consumer: no document ever promised that an unknown name renders the raw value; it makes the spec's own refusal real on the one dialect where it was missing. The design holds; the head does not.

M1 — must-fix: the head is not landable and no gate has judged it. #26937 merged into main as 22058635 at 09:58Z; this PR was opened at 10:56Z from 72629f38, the pre-#26937 base, and never merged main. GitHub reports mergeable_state: dirty; git merge-tree confirms a content conflict in packages/formula/src/template-engine.ts over the TemplateHoleGrammarRule union. Because the merge ref does not exist, zero check-runs and zero workflow runs exist on 857e696 — there is no gate verdict for this record to adopt, and the PR body's local results are the dev's own account, which this review does not take as the gates'. The head must merge main, and the merge must do all of this (verified by reading the merge-tree result 9f721ee8):

  • (a) TemplateHoleGrammarRule becomes the five-member union: one-formatter, known-formatter, currency-code-literal, currency-code-path-mail-only, currency-code-path-undeclared; CURRENCY_ARGUMENT_RULE stays beside it.
  • (b) judgeHoleFormatter keeps the fifth dialect parameter from feat(spec, formula, plugin-email, lint): a mail template formats an amount in the record's own currency with currency:@path #26937; the known-formatter branch sits after the one-formatter branch and before the formatter === 'currency' block (git places it there, but the file is unusable until the union hunk is resolved); WORD_RE stays.
  • (c) plugin-email/src/template-engine.ts auto-merges correctly — codePath resolution, then formatValue(parsed.formatter, raw, arg, …), then the formatted === undefined throw — keep it as merged; mail-hole-grammar.ts and the lint rule header auto-merge clean.
  • (d) The formula test calls judgeHoleFormatter with four arguments; it stays valid since dialect defaults to in-app. feat(spec, formula, plugin-email, lint): a mail template formats an amount in the record's own currency with currency:@path #26937's template-currency-code-path.test.ts pins the spec description against CURRENCY_ARGUMENT_RULE, so the clause added in (e) must leave that sentence intact.
  • (e) MAIL_TEMPLATE_HOLES and its comment in packages/spec/src/system/email-template.zod.ts gain the unknown-formatter refusal (③ A), gen:schema && gen:docs, and the spec changeset says so.
  • (f) The validateEmailTemplateHoleGrammar roster comment in packages/lint/src/authoring-rules.ts (around line 2470-2482, the text feat(spec, formula, plugin-email, lint): a mail template formats an amount in the record's own currency with currency:@path #26937's S1 reworded) enumerates the refusals; add the unknown-formatter clause.
  • (g) Then CI runs on the merged head: every derived family, check-adr-0087-registration, check:generated, check:skill-examples, check:dual-build-cjs-loads, and the dogfood battery the (narrowing) arm owes. This record adopts no conclusion until they exist.

S1 — should-fix: file the non-word-formatter card. {{ x | date-long }} or {{ x | currency :EUR }} fails FILTER_RE, fails WORD_RE, and parseMailHole answers null, so every door accepts it and the renderer prints the hole's text to the recipient, while the in-app dialect refuses it as an invalid hole. The card's sentence ("a formatter not in TEMPLATE_FORMATTERS") reaches it; the PR's Acceptance notes draw the boundary at word names, and the verbatim behaviour is pinned (text-field-html-outlets.pin.test.ts:284), so it is its own narrowing. The dev wrote "noted, not filed": file it, and name it in the PR body as the follow-up.

Nothing else found. Probes that held: prototype keys at every door (①1); the rule order against currency:@path (a misspelled curency:@code is refused as known-formatter, a correct currency:@code passes to #26937's rules); a chained unknown name is refused as one-formatter first; renderTemplate / renderPlainTextTemplate handed a body directly throw the same TEMPLATE_INVALID; the placement judge reads refused holes as holes, unchanged.

Implemented-by: claude/project-thread-j1jtvb
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

VERDICT: FAIL

… roster name the unknown-formatter refusal

Claude-Session: https://claude.ai/code/session_01WGufzgBzVZJqh8CQJLrZu8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:system tests tooling labels Oct 28, 2026
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/formula, @objectstack/lint, @objectstack/plugin-email, @objectstack/spec, touching 6 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/formula/src/mail-hole-grammar.ts, packages/lint/src/validate-email-template-hole-grammar.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/client-sdk.mdx (via IEmailService (symbol, a top-level interface))
  • content/docs/deployment/self-hosting.mdx (via IEmailService (symbol, a top-level interface))
  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object))
  • content/docs/kernel/contracts/index.mdx (via IEmailService (symbol, a top-level interface))

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

  • content/docs/releases/v17/17-1.mdx (via IEmailService (symbol, a top-level interface))
  • content/docs/releases/v18/18-1.mdx (via IEmailService (symbol, a top-level interface))

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/formula/src/mail-hole-grammar.ts, packages/lint/src/validate-email-template-hole-grammar.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 56 of 208 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 — 150 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 ae16687ad430f4f9870ea5ba2bc9b63d6f41fc31 → packageMentionDocs.

Which tree this was computed on

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

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

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

…nown-formatter clause

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

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-28T12:25Z

Head 97613711a5d206bb091499645224c9c1aced6d14. Card #26948.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 12:26
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 12:26
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 40320f1 Oct 28, 2026
76 of 78 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-j1jtvb branch October 28, 2026 12:48
os-support-ai pushed a commit that referenced this pull request Oct 28, 2026
…-formatter refusal (#27217) beside the tenant default

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzizxYz7JyC5JLW3Gmv
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:system size/m tests tooling

Projects

None yet

2 participants