Skip to content

feat(plugin-email,plugin-auth)!: close the sys_email_template organization door; retire the provenance stamp and the auth SMS seed (ADR-0131 C4, stage 1) - #22087

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-15205-templates-resolve-registry
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-15205-templates-resolve-registry

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #15205
Clause-②: no (narrowing)

Stage 1 of ADR-0131 card C4 (#15205), under the maintainer's ruling C on ADR-0131 §6 Q1 (decision card #22005): email templates are not overridden per organization. What stops being accepted: an organization's create and update of a sys_email_template row are refused with 403 PERMISSION_DENIED, and the message names the closed door. System-context writes still pass: the built-in seed, the declared-template boot sweep, the live projection of a Studio email_template save into its sending row, and later the v18 migration ceremony's promotion (#15211). The template provenance stamp retires, and with it three published exports of @objectstack/plugin-email: bindEmailTemplateProvenanceStamp, unbindEmailTemplateProvenanceStamp and EMAIL_TEMPLATE_PROVENANCE_PACKAGE. A boot with phone sign-in on no longer seeds the built-in auth SMS texts into sys_notification_template. What renders unchanged: every email template (the loader still reads sys_email_template, and a Studio edit still reaches the mail at once and survives a restart); every built-in auth SMS text, byte for byte, at every recipient locale; and any sys_notification_template row an operator already has, which still wins.

This PR carries a Tier H ADR edit (a dated note under ADR-0131 §6 Q1), so it needs the maintainer's approval to merge.

The loader half of the card (stage 2: the registry loader, seedTemplates, the boot sweep bootstrapEffectiveEmailTemplates / EffectiveEmailTemplateSources, the live projector) waits on #15206 (C5). Under single, a Studio save of an email_template lands organization-scoped today, and no registry read that omits the organization sees it, live or after a cold boot (measured at e67ba80049; the stop report is on #15205). References: #15194 (the ADR-0131 execution tree), #21785 and #22062 (the current shape of the boot sweep, unchanged here).

What changes

  • plugin-email: the door. New module src/email-template-door.ts (not exported) registers beforeInsert / beforeUpdate hooks on sys_email_template, at priority 10. A write is refused when its hook session names a caller and is not system-elevated. A write with no execution context at all (no caller) passes, the same scope ADR-0123 D2 draws. Delete is not part of this door. EmailServicePlugin binds the door where it bound the stamp, and unbinds it in destroy().
  • plugin-email: the stamp retires. src/email-template-provenance.ts, its per-row test and its three exports are deleted. With the door closed, no non-system update reaches the engine's write, so nothing marks a row customized any more. Rows already marked keep their mark, and the boot seeders still skip them.
  • plugin-auth: the SMS seed retires. seedPhoneSmsTemplates and its kernel:ready call are gone. resolvePhoneSmsTemplateBody walks the locale chain one rung at a time. At each rung it uses an active, non-blank row at that locale; otherwise the built-in text at that locale, when no row exists there at all; otherwise it moves to the next rung. The built-in walk is the floor, as it is on a failed read. That is exactly what the seeded store rendered: the seed inserted the built-in text wherever no row existed, and a deactivated or blank row passed its rung on. Nothing in plugin-auth's package entry changes.
  • ADR-0131: the dated ruling-C note under §6 Q1, spelled as ruled. It names bootstrapEffectiveEmailTemplates / EffectiveEmailTemplateSources. The ADR text is otherwise unchanged.
  • Docs: content/docs/permissions/system-context.mdx row 58 now anchors the door (email-template-door.ts#isOrganizationWrite) in place of the retired stamp. content/docs/automation/email-templates.mdx says a template is changed in Studio and the rows are closed to organization writes.
  • scripts/audits/14744-before-update-per-row-value-probe.mjs: this audit script imported the retired module. Its plugin-email subject and that import are dropped, which is all it needed. The probe still runs (probe: 7 subjects). The dated audit record docs/audits/2026-09-multi-update-per-row-value-census.md stays as measured.
  • platform-objects (cross-lane, its own commit bffc546129): the sys_email_template field help for is_system ("tenants may edit") and customized ("set when an admin edits") became false with this change. Both are corrected in en, zh-CN, ja-JP and es-ES. node scripts/check-i18n-bundles.mjs --write regenerated the en bundle. See Acceptance notes.
  • Changeset: @objectstack/plugin-email and @objectstack/plugin-auth take minor with the BREAKING banner, and @objectstack/platform-objects takes patch. The ADR-0087 disposition is not-required (no-migration-prescription), and each retired export is named.

Pins, each negative one ablated and restored by blob

The SMS legs were re-run at head a4ba9d5070 and the door legs at 1c2a3ad64b (unit) and a93579f453 (dogfood, through dist); neither the door's source nor its tests have changed since. Every leg used scripts/ablation-replace.mjs, which proved that the mutation landed on disk and that the restore matched the HEAD blob with an empty git diff HEAD. Every driver carries an EXIT/INT/TERM trap that restores from HEAD.

Pin Where Ablation Observed
An organization's create, update and predicate update are refused (PERMISSION_DENIED, 403, door named); nothing reaches the driver plugin-email/src/email-template-door.test.ts Refusal line removed 4 failed, 3 passed: the three refusals and the no-customized case went red; the system, no-caller and unbind cases stayed green
The same door over HTTP (PATCH and POST /api/v1/data/sys_email_template as the org admin) dogfood email-template-overlay-survives-boot.dogfood.test.ts, case 3 Refusal line removed, plugin-email rebuilt, ablation-dist-preflight found the marker in 2 dist files Case 3 red (PATCH answered 200), cases 1 and 2 green; after restore and rebuild, --absent passed, the tree was clean, and all 3 were green
No update marks a row customized email-template-door.test.ts §3 (a) The retired stamp restored beside the closed door GREEN: the stamp is unreachable once the door is closed (7/7)
(b) Stamp restored and door refusal removed RED (1 failed): the pin can fail
A fresh boot with phone sign-in on writes no sys_notification_template row plugin-auth/src/phone-sms-seed-retired.test.ts The retired seed restored at kernel:ready RED: 4 rows inserted. The first attempt stayed green because the harness did not answer the data service AuthManager writes through; fixed in a93579f453, then re-ablated
Built-in SMS texts render byte for byte as the seeded store did: every built-in text and locale, plus 4^6 operator stores x 2 topics x 7 locales same file Per-rung built-in clause removed RED (1 failed)
An existing row still wins same file Rows ignored RED (1 failed)

Not ablated, positive pins: a single Studio save still reaches the mail and survives a cold boot. That is dogfood cases 1 and 2, unchanged and green.

Tests

Every result below is at head a4ba9d5070. Each exit code was captured before any pipe.

  • Gates: 123 of 123 exited 0. That is the 62 the dispatch derived plus the 61 more node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives on this change. Reconciled with --ran (exit codes recorded): 123 derived, 123 run, 0 NOT-MEASURED, 0 UNRUN.
  • Fixed along the way (the earlier reds were real):
    • check-tenant-audit-census (and its self-test): the retired seed's write site leaves the census at 232. node scripts/tenant-audit-census.mjs --write regenerated the census page and the counts file, and the page's hand-written figures follow it.
    • check:engine-double-contract and check:where-matcher: the new SMS test doubles now conform. The ledger scripts/engine-double-contract.pinned.json was updated by node scripts/check-engine-double-contract.mjs --write.
  • Re-run after building the packages they read, now green: check:dual-build-cjs-loads and spec check:skill-examples first answered PREREQUISITE NOT MET (exit 3).
  • Packages:
    • @objectstack/plugin-email test: 535 passed. typecheck: OK.
    • @objectstack/plugin-auth test: 2616 passed, 10 skipped. typecheck: OK.
    • @objectstack/platform-objects test: 1006 passed. typecheck: OK.
    • The dependency closure was built first (turbo build --filter=@objectstack/dogfood^..., 63/63).
  • Dogfood: email-template-overlay-survives-boot.dogfood.test.ts, email-template-materialization.dogfood.test.ts and email-template-boot-sweep.test.ts passed 8 of 8. These are the dogfood files that boot plugin-email; none sends SMS. examples/app-showcase/test/email-template-locale.test.ts passed 6 of 6.
  • Narrowed lint, three parts:
    • Population: the 20 changed source files, linted with npx eslint --no-inline-config --format json. None was reported as ignored by the config.
    • Result: 20 files, 0 errors, 0 warnings.
    • Why the narrowing excludes nothing: eslint.config.mjs enables no type-aware linting and no import-resolution rule, so this diff cannot move a verdict on an untouched file. The repo-wide pnpm lint is CI's.

维护者速读(草稿)

  • 改了什么:组织不能再直接新建或修改邮件模板记录(sys_email_template),数据接口会返回 403 并说明这扇门已关;平台自己的写入(内置模板、声明模板、Studio 保存后的同步)照常。随之退役"已自定义"标记钩子及其三个导出。手机短信验证码/邀请的内置文案不再在启动时写成 sys_notification_template 行,而是按语言逐级取:有运营自建的行就用行,没有就用内置文案,发出去的短信逐字节不变。ADR-0131 §6 Q1 下补一条日期注记,记录你 10 月 6 日的裁决 C。
  • 为什么改:落实裁决 C 与「我宁可先不让他编辑」——邮件模板不按组织覆盖。先关门,能让 v18 升级仪式要迁移的"已自定义"模板不再继续增加;模板改为从注册表读取的那一半要等 C5(feat(metadata-core,metadata-protocol,objectql,plugin-security): the sys_metadata family goes tenant-less; the per-organization overlay axis retires; managed content is sealed (ADR-0131 D6/D7/D13) #15206),否则单租户下 Studio 改模板会失效(已实测)。
  • 风险与代价(含回滚):若有脚本或集成通过数据接口写邮件模板记录,会开始收到 403;改模板请走 Studio。代码里直接调用那三个已退役导出的地方会编译失败,删掉调用即可。回滚:revert 本 PR 即恢复旧行为,不涉及任何数据迁移或删除。
  • 席位意见:
  • 你要做的:审阅 ADR-0131 的日期注记(Tier H),同意则批准合并;另请确认 platform-objects 那一处字段说明的跨车道修正可以随本 PR 一起落地。

Acceptance notes

  • The dogfood overlay file is not unchanged. The order asked that email-template-overlay-survives-boot.dogfood.test.ts stay green and unchanged. Its first two cases (the single Studio edit path) are byte-unchanged and green. Its third case, "a data-door edit is stamped customized and survives the next cold boot", pinned exactly the behaviour this PR retires. Measured unchanged on this branch, it went red with the new refusal (expected 403 to be 200). Per the original order's rule ("a dogfood file that pins the retired behaviour is updated in this PR"), case 3 now pins the closed door: an edit and a create are refused with 403 PERMISSION_DENIED and the door named, nothing is marked customized, and the metadata-door wording survives the next cold boot.
  • Cross-lane edit, declared: the platform-objects help-text fix sits in domain:engine's package, outside the claim's file surface. It is made because this change made the two texts false. It lives in one separate commit (bffc546129), so the seat can declare it or drop it.
  • Left for stage 2, by the order's fence: the module docblock of bootstrap-declared-email-templates.ts still names the retired stamp in a @link. That is a comment only, but that file is outside stage 1. packages/spec/liveness/email_template.json's _note narrates the stamp; it is narrative, not an evidence anchor, check:liveness does not read it, and packages/spec is untouched here.
  • Door scope: a write that names no caller (an engine call with no execution context) is not refused. It is not an organization's write, and every known in-repo writer of the table passes the system context anyway.

Generated by Claude Code

claude added 9 commits October 7, 2026 13:15
…retire the provenance stamp

An organization's create and update of a sys_email_template row are refused
with PERMISSION_DENIED / 403 naming the closed door (ADR-0131 D6, ruling C on
section 6 Q1). System-context writes pass: the seeds, the boot sweep, the live
projector of a Studio save. The provenance stamp, which marked such an edit
customized, retires with its exports.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…render per locale rung

A fresh boot with phone sign-in on writes no sys_notification_template row.
Each locale rung renders an operator's active row, or the built-in text where
no row exists at that locale: byte for byte what the seeded store rendered.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…rganization door

Cases 1 and 2 (a Studio save reaches the mail and survives a cold boot) are
unchanged. Case 3 pinned the retired behaviour, a data-door edit stamped
customized; it now pins the refusal of an edit and a create, and that the
metadata-door wording survives the next cold boot.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…door on the docs pages

The dated note names ruling C (decision card 22005, record 6020178017) and the
two symbols the C4 retirement list gains; the ADR text is otherwise unchanged.
The isSystem census row 58 anchors the closed sys_email_template door, the
email-templates page says where a template is edited now, and the 14744
audit probe drops its retired plugin-email subject.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…rganizations may edit a row

The organization door on sys_email_template is closed (ADR-0131 D6, ruling C),
so the is_system help's 'tenants may edit' and the customized help's 'set when
an admin edits' became false. Both say what holds now, in the four bundled
locales; the en bundle is regenerated by check-i18n-bundles --write.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…form-objects patch

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

The ablation that put the seed back stayed green: AuthManager reads its engine
from the 'data' service, which the harness did not answer, so the restored
seed had no engine to write through. The harness now answers both.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…write site retired

node scripts/tenant-audit-census.mjs --write regenerated the census tables
(232 write call sites, one fewer: the retired seedPhoneSmsTemplates insert),
and the page's hand-written figures follow the census.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…where-matcher contracts

The boot harness routes findOne and update through ObjectQL's own dispatch
predicates (ledger recorded by check-engine-double-contract --write), and both
row stores refuse a where combinator instead of reading it as a field name.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/plugin-email, touching 28 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/plugins/plugin-email/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

42 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-email/src/index.ts) — pages documenting those are invisible to this run
  • 4 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 — 20 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 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770

⚠️ 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 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770 → 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

维护者速读(终稿)· PR #22087(C4 第一段,#15205)

domain:services 2 号席(#21118)· session_01WMQprn46CND82KmY8sZWBu · 2026-10-07T15:28Z。本 PR 含 ADR 改动(Tier H),只能由你批准合并。


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 8c5aa50 Oct 8, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-15205-templates-resolve-registry branch October 8, 2026 04:58
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
…e names, ruling dates and foreign example ids (objectstack-ai#22125)

Part of objectstack-ai#22093
Clause-②: no

Wording only: no schema, key, type, export or error-code change. Patch
changeset for `@objectstack/spec`, `@objectstack/service-automation` and
`@objectstack/platform-objects`. objectui#11785 is the Studio half
(splitting an author message from the detail); this PR changes the
server-side strings only. The card keeps two named remainders, listed
under "Remainder of the card and its route" below.

## What changed

Form help and refusals that an author reads no longer carry
service-interface names (`IXxxService.method()`), ruling dates
("maintainer ruling DATE", "ruled DATE") or another product's example
ids. The rationale they carried moves into the code comment beside each
string. Each describe and refusal still states the rule, why it exists
and the repair.

## How "author-facing" was measured (instrument and radius)

Studio reaches these strings through five routes. I measured each one at
`a543e244f`:

| Route | What Studio renders | Instrument |
| :--- | :--- | :--- |
| R1 | `/meta/types` `schema`: every `DEFAULT_METADATA_TYPE_REGISTRY`
type, derived as `metadata-protocol` derives it (`z.toJSONSchema`,
`unrepresentable: 'any'`, with the `io: 'input'` retry) | Every
`description` in all 27 type schemas |
| R2 | `/meta/types` `form`: all 17 `METADATA_FORM_REGISTRY` layouts |
Every `description` / `helpText` / `label` / `placeholder` / `hint` |
| R3 | The package dialog, which derives its form from `ManifestSchema`
client-side (objectui `package-schema.ts`) | Every `description` |
| R4 | The flow inspector, which reads node `configSchema` from `GET
/api/v1/automation/actions` | An AST string scan of every file that
declares a `configSchema` (13 files) |
| R5 | Refusals read at a door | An AST scan of every string literal in
`packages/spec/src` (1284 files, 80190 string parts, comments excluded)
|

The class patterns were: interface
`\bI[A-Z]\w*(Service\|Driver\|Engine\|…)\b`; ruling
`\bruling\b\|\bruled\b\|\b20\d\d-\d\d-\d\d\b`; foreign
`steedos\|superset\|salesforce\|apache\|airtable\|…`.

**Positive control: both strings the filer measured are found.**

- The notify Template help is found by R4 at
`service-automation/src/builtin/notify-node.ts:203`, as both an
interface hit and a ruling hit. The spec copy is found by R5 at
`io-node-config.zod.ts:250`.
- `com.steedos.crm` is found by R5 at `manifest.zod.ts:283`.

R1 to R3 found neither string (needle count 0). So the Studio-rendered
Template help is produced by the service-automation descriptor, not by
the spec describe. That is why the producer side is changed too.

**After the change**, R1 to R3 hold no interface, ruling or foreign-id
hit. The one exception is the "Airtable parity" wording (see Acceptance
notes). The needles `II18nService`, `IEmailService`, `com.steedos.crm`
and `maintainer ruling` count 0. The R5 hit count drops by 22.

## Every string changed

Lines are at `a543e244f`. Before is the internal reference that was
removed; After is what replaced it. No changed string carried a tracker
number, so no tracker number moved.

| # | Where | Reach | Before | After | Rationale now lives in |
| :-- | :--- | :--- | :--- | :--- | :--- |
| 1 | `services/service-automation/src/builtin/notify-node.ts:203`,
notify `configSchema.properties.template.description` | R4: Studio
notify Template help (filer-measured) | "else the deployment default
(II18nService.getDefaultLocale()) … (maintainer ruling 2026-09-01). A
producer-set payload.locale is not consulted." | "else the deployment
default locale … The node's payload.locale is not consulted." | Comment
above `template:` in the descriptor |
| 2 | `spec/src/automation/io-node-config.zod.ts:250`,
`NotifyConfigSchema.template` describe | Reference docs and published
JSON Schema; the contract row 1 mirrors | Same as row 1, with backticks
| Same as row 1 | TSDoc above `template` (it already named both; one
sentence added) |
| 3 | `spec/src/kernel/manifest.zod.ts:283`, `MANIFEST_ID_EXAMPLES` |
R5: every package-id refusal (filer-measured on `POST /api/v1/packages`)
| `'com.steedos.crm', 'org.apache.superset'` | `'com.acme.crm',
'org.example.help-desk'` | Doc comment on the constant |
| 4 | `manifest.zod.ts:305`, `manifestIdRefusal` | R5: same doors |
"Invalid package id 'VALUE' on `manifest.id`. Expected reverse-domain
notation ('com.steedos.crm', 'org.apache.superset') — …" | "Invalid
package id 'VALUE'. A package id (`manifest.id`) is written in
reverse-domain notation, like 'com.acme.crm' or 'org.example.help-desk'
— …", with the rule and suggestion arm unchanged | TSDoc on
`manifestIdRefusal` |
| 5 | `manifest.zod.ts:360-361`, `@example` on `id` | TSDoc; held equal
to row 3 | `com.steedos.crm`, `org.apache.superset` | `com.acme.crm`,
`org.example.help-desk` | (none) |
| 6 | `spec/src/data/field.zod.ts:1142`, `required` describe | R1:
object, field | "(maintainer ruling 2026-08-18)" | Removed | TSDoc above
`required` |
| 7 | `field.zod.ts:1170`, `multiple` describe | R1: object, field |
"(maintainer ruling 2026-09-13), and on `radio` by the narrower
2026-08-22 ruling … (maintainer ruling 2026-08-18)" | "on any other
type, `radio` included, is REFUSED at parse …" | TSDoc above `multiple`
(new Declarability paragraph) |
| 8 | `spec/src/ui/view.zod.ts:1175` / `:1221`, `GROUPING_FIELD_RULING`
in the padded grouping-field refusal | R5: view save | "… (ruled
2026-09-10.)" | Removed, constant deleted | Comment where the constant
was |
| 9 | `view.zod.ts:3444`, form-view field `visibleWhen` describe | R1:
view | "refused at parse (ruled 2026-08-27):" | "refused at parse:" |
TSDoc above |
| 10 | `view.zod.ts:3655`, section `collapsible` describe | R1: view |
"(ruled 2026-09-18)" | Removed | The collapse-pair TSDoc (already
records the ruling) |
| 11 | `view.zod.ts:3663`, section `collapsed` describe | R1: view |
"(ruled 2026-09-18; refusing the combination at the declaration, and
warning on it, were both rejected — nobody can depend on a section that
cannot be opened)" | ", so writing `collapsed: true` alone is a correct
way to say \"collapsed by default\"" | The collapse-pair TSDoc (already
records the rejected letters) |
| 12 | `view.zod.ts:3715`, form-view section `visibleWhen` describe |
R1: view | "(ruled 2026-08-27)" | Removed | TSDoc above |
| 13 | `view.zod.ts:3959`, `SUBMIT_REDIRECT_RULING` in seven
`submitBehavior.url` refusals (`:3988`, `:3993`, `:4003`, `:4010`,
`:4018`, `:4029`, `:4039`) | R5: form-view save | "(ruled 2026-08-11)" |
Removed, constant deleted | Comment where the constant was |
| 14 | `view.zod.ts:4097` / `:4123`, `FORM_VIEW_FEATURES_RULING` in the
`features.*` predicate refusal | R5: form-view save | "scope root (ruled
2026-08-27)." | "scope root." | Comment where the constant was |
| 15 | `view.zod.ts:4444`, redirect-arm `url` describe | R1: view |
"…successful submit. Ruled 2026-08-11: (1) …" | "…successful submit. (1)
…" | The `checkSubmitRedirectUrl` comment |
| 16 | `spec/src/ui/page.zod.ts:941` (`kind`) and `:990` (`source`)
describes | R1: page | "(ADR-0065; ADR-0080 amendment 2026-06-30)" |
"(ADR-0065; ADR-0080)" | TSDoc above `kind` (new) and `source` (already)
|
| 17 | `spec/src/system/email-template.form.ts:19`, Identity section
help. Its three translated values in
`platform-objects/src/apps/translations/{zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts:2183`
are hand-written, and `en` is regenerated by `pnpm i18n:extract` | R2:
email_template form | "Template identifier resolved by
IEmailService.sendTemplate({ template: name, locale, ... })." | "Senders
address this template by its name; the locale selects which language
version of it is sent." | Comment above `description:` |
| 18 | `spec/src/data/filter.zod.ts:475`, null ordering comparand
refusal | R5: filter validation | "Ruled 2026-09-01: a null ordering
comparand is refused at the validation entrance." | Sentence removed |
Builder TSDoc |
| 19 | `filter.zod.ts:564`, `{ $field }` in a list position refusal | R5
| "Ruled 2026-08-11: declared = enforced (ADR-0049)." | Sentence removed
| Comment inside the builder |
| 20 | `filter.zod.ts:589`, null list member refusal | R5 | "Ruled
2026-08-31: a null list member is refused at the validation entrance." |
Sentence removed | Builder TSDoc |
| 21 | `filter.zod.ts:801`, blank `$between` bound refusal | R5 | "Ruled
2026-09-17: a blank $between bound is refused at the validation
entrance." | Sentence removed | Builder TSDoc |
| 22 | `spec/src/ui/action.zod.ts:985`, action `description` describe
(added in patch round 1) | R1: action, in the authoring (`io: 'input'`)
derivation; the action Description help objectui#11785 measured | "(one
dialog, not two —)": the dash an earlier strip of the cited ruling left
dangling | "(one dialog, not two)" | Docblock above `description`
(already cites the ruling; one sentence added) |

The regenerated `content/docs/references/**` files follow from rows 2,
6, 7, 9 to 12, 15, 16 and 22.

## Remainder of the card and its route

This PR delivers the card minus two named rows. It is therefore `Part of
objectstack-ai#22093`, and the card stays open for both.

1. **The governed `submitBehavior` row.**
- What: `view.zod.ts:4478`, the top-level `submitBehavior` describe,
keeps "(ruled 2026-08-11)".
- Why it is not here: `gen:react-blocks` projects this describe into
`skills/objectstack-ui/references/react-blocks.md`, which is a governed
Tier H surface, so changing it here would make this whole sweep a Tier H
landing. Measured: the regenerated skill file differs in exactly that
one row.
- Route: a separate Tier H draft PR carrying the one describe edit, the
regenerated `react-blocks.md` row and its reference-docs row, landed on
the maintainer's approval.
   - A comment beside the describe records the deferral.
2. **The `sys_email` field help naming `IEmailService.send`.**
- What: `packages/platform-objects`, `en.objects.generated.ts:2500` and
`:2508`. The source is in the `sys_email` object definitions.
- Why it is not here: open PR objectstack-ai#22087 regenerates the same four
`*.objects.generated.ts` bundles, so folding it here would put two PRs
on those bundles at once.
   - Route: the same class of edit, made after that PR lands.

## Pins

- **Refusal `code` is asserted with the sentence:**
  - `manifest.test.ts` asserts `invalid_format`.
  - `view-form-features-root.test.ts` asserts `custom`.
- **Negative pins on what the author reads.** No interface name, ruling
date or foreign id appears in:
  - `io-node-config.test.ts`
  - `notify-node.test.ts`, on the Studio-served descriptor text
- `manifest.test.ts`. Its headline sentence carries no `manifest.id`,
the key is still named as a locator, and there is no
steedos/superset/apache.
  - `filter.test.ts`
  - `view-form-features-root.test.ts`
  - `view-submit-redirect-url.test.ts`
- **Patch round 1: `action-description.test.ts`.** It derives
`ActionSchema` the way `/meta/types` serves it (`io: 'input'`) and
asserts three things about the `description` help:
  - it is present;
  - no dash is left dangling before a close paren;
  - it has no ruling date, service interface or tracker id.
- **Pins that changed direction.** These earlier pins asserted that the
date was present:
- `view-submit-redirect-url.test.ts`, 7 assertions: "cites the ruling so
the refusal is traceable"
  - `view-form-features-root.test.ts:68`
  - `filter.test.ts:644`
  - `io-node-config.test.ts:347` and its `2026-09-01` pin

They are now negative pins. `filter.test.ts:661` used the date as its
discriminator between two messages. It now discriminates on the
blank-bound headline.
- No whole-prose pin was added.

## Where the ruling dates in refusals come from (lineage)

This PR reverses no ruling.

- The 2026-08-29 adjudication carried out objectstack-ai#12522's charter (comment
5425845726): strip tracker ids, and keep ADR ids and migration commands.
That charter does not mention ruling dates.
- "ADR ids, protocol versions, error codes and ruling dates stay" is PR
objectstack-ai#13298's own commit note (fd289be), not ruled text. The code comments
that grew from it, saying the date is "what a refused author can act
on", are rewritten here.
- Triage 6041940817 on this card directs that "rationale (interface
names, dates, tracker ids) moves into code comments". This PR follows
that direction for describes and refusals alike, and the seat confirmed
it in patch round 1. ADR ids stay.

## Tests and gates

**Patch round 1, on the merged head `026204631`.**

- **Merge:** origin/main was merged at `54ace18c6` with `bash
scripts/pm/os-regen-merge.sh`.
- Merge commit `75cec8cd1`, parents `34bf72933` and `54ace18c6`. There
were no conflicts.
- Step 2 kept the branch's bytes of the 7 reference-docs files main had
not moved.
- The regeneration ran only after the merge commit (never in MERGE
state). It reproduced main's side byte for byte; the only regenerated
delta is the action row, in 3 docs files.
- Main's landed migration entries are still present on the merged head.
Three were checked by name: `flow-edge-unresolved-or-repeated-refused`,
`sys-presence-organization-column-retired` and
`sys-job-organization-column-retired`.
  - The delta against main is exactly this branch's 30 paths.
- **Builds and regeneration, all under the verify lock:**
  - `pnpm --filter @objectstack/spec build` passed.
  - `gen:schema` and `gen:docs` passed.
  - The `service-automation^...` closure build (spec excluded) passed.
- **spec tests:** `pnpm --filter @objectstack/spec exec vitest run
--maxWorkers=2` over 26 targeted files (round 0's 25 plus
`action-description.test.ts`): **26 files, 2475 tests passed.** This is
a declared narrowing; CI `Test Core` runs the full suite.
- **service-automation:** `notify-node.test.ts`, **16 passed.**
- **platform-objects:** `src/apps/translations/*.test.ts`, **24 files,
430 passed.**
- **Typecheck:** `@objectstack/spec` `typecheck` passed.
- **Generated artifacts:** `pnpm --filter @objectstack/spec
check:generated` reports **all 15 generated artifacts up to date**
(react-blocks and api-surface included).
- **Reverse verification, patch round 1 (one-off):**
- `action.zod.ts` went back to the `54ace18c6` bytes; landing confirmed
by `grep -c` = 1.
- **1 of 16 tests in `action-description.test.ts` went red:** "carries
no residue of a stripped reference".
- The file was restored from HEAD, hash-identical to its HEAD blob, and
the suite was green again (16 of 16).
- **Reverse verification, round 0 (at `34bf72933`):**
  - The 5 touched sources went back to the `a543e244f` bytes.
- **39 of 347 spec tests went red across all 5 changed spec test files,
and 1 of 16 in `notify-node.test.ts`.** Every red is a pin this PR added
or flipped.
  - Restored byte-identical to HEAD.
- **Lint:** `pnpm exec eslint --no-inline-config --format json` over the
20 changed lintable files at `026204631`: **20 files, 0 errors, 0
warnings.**
- Population: `eslint.config.mjs` matches
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`.
- Invariance: the config never enables type-aware linting (no
`parserOptions.project`, see its note near line 328), so this diff
cannot move the verdict on any file it does not touch.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `026204631` derived 113 families (30
paths against merge base `54ace18c6`).
  - **109 run, all exit 0**, each exit code captured before any pipe.
- **4 NOT MEASURED, recorded as reasoned claims and read from CI on the
landing head:** `check:skill-examples`, `check:dual-build-cjs-loads`,
`check:i18n` and `check:type-check-debt`. In round 0 each exited 3
(PREREQUISITE NOT MET). `check:type-check-debt`'s re-measure is a
45-task workspace build and is not run outside the verify lock.
- `--ran` reconciliation: **113 accounted, 109 run, 4 NOT MEASURED
(claimed), 0 unrun.**

## Acceptance notes

- **Measured but not changed, as outside the three classes or outside
reach:**
- `stack.zod.ts:458-459` (`IJobService`, `IEmailService.sendTemplate`):
`defineStack` collection describes. R1 to R3 do not serve them, and they
are not refusals.
- Strings whose reader is a plugin, driver or API developer, for whom
the interface is the contract they implement or call. None is in R1 to
R3:
    - `data-engine.zod.ts:165,190` (`IDataEngine.find()`)
    - `hook.zod.ts:1269` (`IScopedContext`)
    - `query.zod.ts:429`
    - the `driver.zod.ts:287-388` refusals (`IDataDriver`)
    - `automation-api.zod.ts:452`
- `execution-context.zod.ts:441` ("the 2026-09-03 ruling"):
`preserveAudit` is server-constructed and never authored.
- `plugin-rest-api.zod.ts:138` ("ruling record, 2026-09-01"): a
tombstone on `RestApiEndpointSchema`. Its own comment records that
nothing parses that schema outside its unit tests, so no door reaches
it.
- `component.zod.ts:365` ("Since the console release of 2026-08-21
(`c86185eb5`)"): a release date and a commit sha, not a ruling.
- Undated ruling words. These carry no date and no "maintainer ruling",
so they fall outside the class as the seat scoped it:
    - `action.zod.ts:1139` and `:2157`
    - `page.zod.ts:93`
    - `view-grouping-query.ts:433`
    - `context.zod.ts:50`
    - `protocol.zod.ts:1681`
    - `endpoint-publish-gate.ts:368,416`
    - the `error-code-ledger.zod.ts` notes
- "Airtable parity" wording is the only R1 to R3 hit left:
`page.zod.ts:911`, `page.form.ts:113`, `view.form.ts:164`, plus
`view.zod.ts:1706`, `page.zod.ts:647` and `component.zod.ts:1766`. It is
a design note naming a competitor, not an example id.
- Salesforce mentions in `export.zod.ts:292` and `object.zod.ts:1528`
are comparisons, not ids.
- `src/migrations/**`, about 1,240 dated or "ruling" hits: upgrade-guide
records, not a form or a door.
- `manifest.zod.ts:546`, TSDoc `@example` on `dependencies`
(`@steedos/plugin-auth`): TSDoc only.
`packages/core/src/artifact-packages.ts:114` cites that exact example,
so changing it moves a comment in core.
- The `sys_email` field help naming `IEmailService.send` is the second
named remainder; see "Remainder of the card and its route".
- **objectui:** it has no copy of any changed string at `9990f9e` (grep
for `II18nService`, `IEmailService`, `steedos.crm`, `Invalid package
id`).

---
_Generated by [Claude
Code](https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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 size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants