Skip to content

Commit 4e4111c

Browse files
fix(platform-objects): sys_email field help names no service interface; QA clause requotes the package-id refusal (#22309)
Part of #22093 Clause-②: no This PR delivers remainders (b) and (c) of #22093, as the seat's partial-landing record (`6049519522`) names them. Remainder (a), the governed `submitBehavior` describe, is not in this PR and stays on the card. Wording only: no field, option, key, type or export changes. It ships a patch changeset for `@objectstack/platform-objects`. The `docs/qa` change publishes nothing. ## (b) The `sys_email` field help no longer names a service interface Studio shows a field's `description` as its help in the object forms: the `help` leaf in each locale's `objects` bundle. Two `sys_email` fields named the `IEmailService.send` service interface there. Each one now says what the field holds and when it is set, in product words, and keeps every fact it stated before. The implementation reference moves into the code comment beside each field (`sys-email.object.ts`), in the same form PR #22125 used for the other 22 rows. | Field | Locale | Before | After | | :--- | :--- | :--- | :--- | | `headers_json` | en (source) | "Custom headers supplied to IEmailService.send, as a JSON object of name → value." | "Custom headers supplied with the message when it was sent, as a JSON object of name → value." | | `headers_json` | zh-CN | "传给 IEmailService.send 的自定义邮件头,…" | "发送邮件时随附的自定义邮件头,…" | | `headers_json` | ja-JP | "IEmailService.send に渡されたカスタムヘッダーを、…" | "メッセージの送信時に指定されたカスタムヘッダーを、…" | | `headers_json` | es-ES | "Cabeceras personalizadas facilitadas a IEmailService.send, como un objeto JSON …" | "Cabeceras personalizadas facilitadas con el mensaje al enviarlo, como un objeto JSON …" | | `status` | en (source) | "Lifecycle state — queued by IEmailService.send before transport call" | "Lifecycle state — queued when the message is submitted for sending, before the first delivery attempt" | | `status` | zh-CN | "生命周期状态——在调用传输层之前由 IEmailService.send 排队" | "生命周期状态——邮件提交发送时即进入排队,早于首次投递尝试" | | `status` | ja-JP | "ライフサイクル状態 — トランスポート呼び出し前に IEmailService.send がキューに入れます" | "ライフサイクル状態 — 送信が要求された時点で、最初の配信試行の前にキューに入ります" | | `status` | es-ES | "Estado del ciclo de vida; se pone en cola mediante IEmailService.send antes de la llamada al transporte." | "Estado del ciclo de vida; queda en cola al solicitarse el envío del mensaje, antes del primer intento de entrega." | For `headers_json`, only the first sentence changes. The rest of each leaf is unchanged. The `status` wording uses the vocabulary of the neighbouring `attempt_count` help in each locale ("delivery attempts", 投递尝试, 配信試行, intentos de entrega). ### How the bundles were produced - **en:** regenerated by the package's documented generator, `pnpm i18n:extract`, after the source edit. It rewrote exactly the two `en` leaves. - **zh-CN, ja-JP, es-ES:** the translated leaf is itself where the translated text lives. The generator's merge mode never overwrites an existing translated leaf. The bundle header ("Edit translations in place … Do not hand-edit the structure — only the leaf string values") and AGENTS.md's Documentation Guardrails row both name re-translating the leaf in place as the maintenance path. So I re-translated the two leaves in place and then ran the generator. It wrote all 11 files and left the three translated bundles and their `*.source-hashes.generated.ts` tables byte-identical. That shows the edit changed leaf values only, never structure. Neither leaf had a recorded source hash, so no provenance entry moves. A second `pnpm i18n:extract` at the head leaves `git status` empty. - The dispatch named es-ES and ja-JP. zh-CN carried the same phrase, so it is rewritten too. ### Pin `packages/platform-objects/src/apps/translations/objects-help-no-service-interface.test.ts` walks every string leaf of the four `objects` bundles. It asserts that none matches the service-interface pattern the notify Template help pin already uses (`\bI[A-Z]\w*Service\b`). It also checks the two `sys_email` source descriptions. A positive control asserts that the walk reaches both rewritten leaves in every locale, so a walk that reaches nothing cannot pass. Today no other leaf in the four bundles matches, so the pin covers the whole class for this package's object help, not just these two rows. Ablation (one-off, no file kept): `scripts/ablation-replace.mjs` put the old zh-CN `status` leaf back, landing confirmed on disk (anchor 1 → 0, blob `5cf8f2e7e81d` → `64f05c216782`). The pin went red as 1 failed and 5 passed, naming `sys_email.fields.status.help` in zh-CN only. The file was restored with the blob equal to HEAD and `git diff HEAD` empty. ## (c) The QA clause quotes the current package-id refusal `docs/qa/platform-checklist/areas/cli.json`, item `cli.plugin-manifest-build-contract`, acceptance clause A2: | Before | After | | :--- | :--- | | "a located Zod error ('Invalid package id '' on `manifest.id`. Expected reverse-domain notation …')" | "a located Zod error ('Invalid package id ''. A package id (`manifest.id`) is written in reverse-domain notation, like …')" | The new quote is taken from `manifestIdRefusal` (`packages/spec/src/kernel/manifest.zod.ts`) at `6729e107e`. I checked it against the live refusal: `ManifestSchema.safeParse` with `id: ''` answers on path `id` with "Invalid package id ''. A package id (`manifest.id`) is written in reverse-domain notation, like 'com.acme.crm' or 'org.example.help-desk' — …". The item's `revision` moves from 2 to 3, with one `history` entry in the file's convention (`revision`, `date`, `change`, `ref`). The scored property is unchanged, and so is A2's `verify` text: probe (d) still reads a located reverse-domain refusal that names `manifest.id` and echoes the received `''`. ## Verification (at `acc26751b`) - **Build:** the extract closure under the verify lock, `pnpm turbo run build --filter='@objectstack/lint...' --filter='@objectstack/platform-objects...' --concurrency=2`: 8 of 8 tasks, `VERDICT command-exit 0`. - **Tests:** `pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2 src/apps/translations src/audit/sys-email`: 27 files, 443 tests passed. - **Typecheck:** `pnpm --filter @objectstack/platform-objects typecheck`, all three legs including `check:test-typecheck`: exit 0. - **Lint:** `eslint --no-inline-config --format json` on the 6 changed TS files: 6 files, 0 errors, 0 warnings. The population comes from `eslint.config.mjs`: `**/*.{ts,…}` minus `NEVER_LINTED`. Type-aware linting is not enabled, so this diff cannot move the verdict on any untouched file. - **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 65 commands. I ran them together with the 52-row artifact-roster block, 115 unique commands. 107 exited 0. - Three are PR-context gates that print NOT WIRED (exit 2) without `PR_BODY` or `PR_NUMBER`. `check:partof-closing-keyword` was re-run against this body. - Four exited 3 with PREREQUISITE NOT MET, because they read a whole-workspace build this run did not make: `check:dual-build-cjs-loads`, `check:i18n` (the built CLI), `check:lean-entry-closure` and `check:published-readme-exports`. For `check:i18n`, the documented command it runs for this package (`pnpm i18n:extract`) was run directly, and it is idempotent at the head. - `check:type-check-debt` was not run locally: its re-measure is a workspace build outside the verify lock. CI reads it. ## Acceptance notes - The zh-CN, ja-JP and es-ES `headers_json` help leaves omit the last English sentence ("Never returned on the generic data path — …"). This translation lag predates this PR, and the source-hash mechanism treats a leaf with no recorded hash as legacy-trusted, so nothing flags it. This PR re-translates only the sentence that named the interface. Noted, not filed. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent dc4a5c6 commit 4e4111c

8 files changed

Lines changed: 89 additions & 12 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
The `sys_email` field help for Headers (JSON) and Status no longer names the `IEmailService.send` service interface (#22093)
6+
7+
Clause-②: no
8+
9+
Studio shows these descriptions as field help in the object forms. They now say what the field holds and when it is set, in product words: Headers (JSON) holds the custom headers supplied with the message when it was sent, and Status is `queued` when the message is submitted for sending, before the first delivery attempt. Every other fact the help states is unchanged. The `en` bundle is regenerated from the source, and the `zh-CN`, `ja-JP` and `es-ES` translations of the two leaves are re-translated in place. Wording only: no field, option, key or type changes.

‎docs/qa/platform-checklist/areas/cli.json‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,7 +1499,7 @@
14991499
"title": "os plugin build enforces the manifest at the packaging boundary: an invalid manifest is refused with located errors, `packaging` forks the artifact, and the compiled manifest the artifact carries is readable back",
15001500
"since": "v15",
15011501
"status": "active",
1502-
"revision": 2,
1502+
"revision": 3,
15031503
"priority": "P2",
15041504
"surface": "cli",
15051505
"personas": [
@@ -1536,7 +1536,7 @@
15361536
"evidence": "each refusal's full output with its exit code, plus the directory listing taken after it"
15371537
},
15381538
{
1539-
"clause": "identity is guarded at the SCHEMA, with located errors for every shape: `id` and `version` are required by ManifestSchema (an absent one fails safeParse with a located error), a non-semver version fails its SemVer regex, and `id: \"\"` fails manifest.id's reverse-domain rule — a located Zod error ('Invalid package id '' on `manifest.id`. Expected reverse-domain notation …'), not the build's post-parse 'must declare both \"id\" and \"version\"' guard. That guard (build.ts) is now unreachable from a manifest that passed safeParse; an empty id never reaches it, and it is not scored",
1539+
"clause": "identity is guarded at the SCHEMA, with located errors for every shape: `id` and `version` are required by ManifestSchema (an absent one fails safeParse with a located error), a non-semver version fails its SemVer regex, and `id: \"\"` fails manifest.id's reverse-domain rule — a located Zod error ('Invalid package id ''. A package id (`manifest.id`) is written in reverse-domain notation, like …'), not the build's post-parse 'must declare both \"id\" and \"version\"' guard. That guard (build.ts) is now unreachable from a manifest that passed safeParse; an empty id never reaches it, and it is not scored",
15401540
"oracle": "log",
15411541
"verify": "probes 6b, 6c and 6d, each read as its OWN located refusal under 'objectstack.plugin.json is invalid:' — 6b the missing key, 6c the version regex, 6d the reverse-domain refusal naming manifest.id with the received '' — each exiting nonzero with no artifact. 6d printing 'must declare both \"id\" and \"version\"' instead would mean the schema rule regressed. Collapsing the three into one 'it refuses' observation cannot tell which rule answered",
15421542
"evidence": "the three refusals side by side, each with its exit code"
@@ -1593,6 +1593,12 @@
15931593
"date": "2026-10-04",
15941594
"change": "A2 and refusal probe (d) re-pointed: `id: \"\"` is now a located Zod error at safeParse — manifest.id enforces the reverse-domain rule (MANIFEST_ID_PATTERN / manifestIdRefusal, manifest.zod.ts) — so the build.ts post-parse 'must declare both \"id\" and \"version\"' guard is unreachable from it and is no longer scored. Stale clause. Source 097d26859 (#18319)",
15951595
"ref": "#21735"
1596+
},
1597+
{
1598+
"revision": 3,
1599+
"date": "2026-10-08",
1600+
"change": "text refresh only, no property change: A2 quoted the package-id refusal headline as 'Invalid package id '' on `manifest.id`. Expected reverse-domain notation …', which manifestIdRefusal (manifest.zod.ts) no longer prints — the headline now names the package id in product words and the key rides in the second sentence as a parenthetical locator. Requoted from the source at 6729e107e. The scored property is unchanged: probe (d) is still a located reverse-domain refusal at safeParse that names manifest.id and echoes the received ''",
1601+
"ref": "#22093"
15961602
}
15971603
]
15981604
},

‎packages/platform-objects/src/apps/translations/en.objects.generated.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2500,15 +2500,15 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
25002500
},
25012501
headers_json: {
25022502
label: "Headers (JSON)",
2503-
help: "Custom headers supplied to IEmailService.send, as a JSON object of name → value. Written in both delivery modes (it is audit evidence as much as delivery input). Absent on rows written before this column existed, which read back as \"no custom headers\". Never returned on the generic data path — headers are the ordinary place a credential goes; the delivery paths recover it through the engine's privileged accessor."
2503+
help: "Custom headers supplied with the message when it was sent, as a JSON object of name → value. Written in both delivery modes (it is audit evidence as much as delivery input). Absent on rows written before this column existed, which read back as \"no custom headers\". Never returned on the generic data path — headers are the ordinary place a credential goes; the delivery paths recover it through the engine's privileged accessor."
25042504
},
25052505
attachments_json: {
25062506
label: "Attachments (JSON)",
25072507
help: "Attachments as a JSON array of { filename, contentType?, size, hash, cid?, contentForm, inline?, storageKey?, contentReclaimedAt? }. Content up to the plugin-email budget (SYS_EMAIL_ATTACHMENT_LIMIT_BYTES, 256 KiB combined raw — ~350 KB of base64 at worst) is base64 in `inline`; larger content goes to the file-storage capability and the element carries `storageKey` instead, so the row stays bounded either way. filename/contentType/size/hash are PERMANENT audit evidence; out-of-row content is a delivery artifact and is deleted a grace window after the row reaches a terminal state, at which point `storageKey` is replaced by `contentReclaimedAt`."
25082508
},
25092509
status: {
25102510
label: "Status",
2511-
help: "Lifecycle state — queued by IEmailService.send before transport call",
2511+
help: "Lifecycle state — queued when the message is submitted for sending, before the first delivery attempt",
25122512
options: {
25132513
queued: "queued",
25142514
sent: "sent",

‎packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2500,15 +2500,15 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
25002500
},
25012501
headers_json: {
25022502
label: "Cabeceras (JSON)",
2503-
help: "Cabeceras personalizadas facilitadas a IEmailService.send, como un objeto JSON de nombre → valor. Se escriben en ambos modos de entrega (son tanto evidencia de auditoría como entrada de la entrega). Ausentes en las filas escritas antes de que existiera esta columna, que se leen como «sin cabeceras personalizadas»."
2503+
help: "Cabeceras personalizadas facilitadas con el mensaje al enviarlo, como un objeto JSON de nombre → valor. Se escriben en ambos modos de entrega (son tanto evidencia de auditoría como entrada de la entrega). Ausentes en las filas escritas antes de que existiera esta columna, que se leen como «sin cabeceras personalizadas»."
25042504
},
25052505
attachments_json: {
25062506
label: "Adjuntos (JSON)",
25072507
help: "Adjuntos como un array JSON de { filename, contentType?, size, hash, cid?, contentForm, inline?, storageKey?, contentReclaimedAt? }. El contenido que cabe en el presupuesto de plugin-email (SYS_EMAIL_ATTACHMENT_LIMIT_BYTES, 256 KiB brutos combinados — ~350 KB de base64 en el peor caso) va en base64 en `inline`; el contenido mayor se guarda en la capacidad file-storage y el elemento lleva `storageKey` en su lugar, de modo que la fila permanece acotada en ambos casos. filename/contentType/size/hash son evidencia de auditoría PERMANENTE; el contenido fuera de la fila es un artefacto de entrega y se elimina tras un periodo de gracia una vez que la fila alcanza un estado terminal, momento en el que `storageKey` se sustituye por `contentReclaimedAt`."
25082508
},
25092509
status: {
25102510
label: "Estado",
2511-
help: "Estado del ciclo de vida; se pone en cola mediante IEmailService.send antes de la llamada al transporte.",
2511+
help: "Estado del ciclo de vida; queda en cola al solicitarse el envío del mensaje, antes del primer intento de entrega.",
25122512
options: {
25132513
queued: "En cola",
25142514
sent: "Enviado",

‎packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2500,15 +2500,15 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
25002500
},
25012501
headers_json: {
25022502
label: "ヘッダー(JSON)",
2503-
help: "IEmailService.send に渡されたカスタムヘッダーを、name → value の JSON オブジェクトとして保持します。両方の配信モードで書き込まれます(配信の入力であると同時に監査証跡でもあるため)。この列が存在する前に書き込まれた行には値がなく、「カスタムヘッダーなし」として読み戻されます。"
2503+
help: "メッセージの送信時に指定されたカスタムヘッダーを、name → value の JSON オブジェクトとして保持します。両方の配信モードで書き込まれます(配信の入力であると同時に監査証跡でもあるため)。この列が存在する前に書き込まれた行には値がなく、「カスタムヘッダーなし」として読み戻されます。"
25042504
},
25052505
attachments_json: {
25062506
label: "添付ファイル(JSON)",
25072507
help: "添付ファイルの JSON 配列で、要素は { filename, contentType?, size, hash, cid?, contentForm, inline?, storageKey?, contentReclaimedAt? } の形をとります。plugin-email の予算(SYS_EMAIL_ATTACHMENT_LIMIT_BYTES、合計生サイズ 256 KiB —— 最悪でも base64 で約 350 KB)以内の内容は base64 で `inline` に格納され、それを超える内容は file-storage ケイパビリティに置かれて要素は代わりに `storageKey` を持つため、いずれの場合も行のサイズは有界に保たれます。filename/contentType/size/hash は**恒久的な**監査証跡です。行外の内容は配信用の成果物であり、行が終端状態に達してから猶予期間を過ぎると削除され、その時点で `storageKey` は `contentReclaimedAt` に置き換わります。"
25082508
},
25092509
status: {
25102510
label: "ステータス",
2511-
help: "ライフサイクル状態 — トランスポート呼び出し前に IEmailService.send がキューに入れます",
2511+
help: "ライフサイクル状態 — 送信が要求された時点で、最初の配信試行の前にキューに入ります",
25122512
options: {
25132513
queued: "キュー済み",
25142514
sent: "送信済み",
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// #22093 — object field help is product guidance, in every locale.
4+
//
5+
// Studio renders a field's `help` (its source `description`) in the object
6+
// forms. A service-interface name there (`IEmailService.send`) tells an author
7+
// nothing they can act on; where the field is written from belongs in the code
8+
// comment beside the field. The four `objects` bundles are walked whole, so a
9+
// leaf added later with the same shape is caught too, in the locale it lands in.
10+
11+
import { describe, expect, it } from 'vitest';
12+
13+
import { SysEmail } from '../../audit/sys-email.object.js';
14+
import { enObjects } from './en.objects.generated.js';
15+
import { esESObjects } from './es-ES.objects.generated.js';
16+
import { jaJPObjects } from './ja-JP.objects.generated.js';
17+
import { zhCNObjects } from './zh-CN.objects.generated.js';
18+
19+
/** The same shape the notify Template help pin uses (service-automation). */
20+
const SERVICE_INTERFACE = /\bI[A-Z]\w*Service\b/;
21+
22+
const BUNDLES = { en: enObjects, 'es-ES': esESObjects, 'ja-JP': jaJPObjects, 'zh-CN': zhCNObjects };
23+
24+
function stringLeaves(node: unknown, path: string, out: Map<string, string>): Map<string, string> {
25+
if (typeof node === 'string') out.set(path, node);
26+
else if (node && typeof node === 'object') {
27+
for (const [key, value] of Object.entries(node)) stringLeaves(value, path ? `${path}.${key}` : key, out);
28+
}
29+
return out;
30+
}
31+
32+
describe('objects bundles: field help names no service interface', () => {
33+
it('the pattern recognises the interface spelling it exists to keep out', () => {
34+
expect('Custom headers supplied to IEmailService.send').toMatch(SERVICE_INTERFACE);
35+
});
36+
37+
for (const [locale, bundle] of Object.entries(BUNDLES)) {
38+
it(`${locale}: no string leaf carries a service-interface name`, () => {
39+
const leaves = stringLeaves(bundle, '', new Map());
40+
// Positive control: the walk reaches the two leaves this card rewrote.
41+
expect(leaves.get('sys_email.fields.headers_json.help')).toEqual(expect.any(String));
42+
expect(leaves.get('sys_email.fields.status.help')).toEqual(expect.any(String));
43+
const offenders = [...leaves].filter(([, text]) => SERVICE_INTERFACE.test(text)).map(([p]) => p);
44+
expect(offenders).toEqual([]);
45+
});
46+
}
47+
48+
it('the sys_email source descriptions the en bundle is generated from name none either', () => {
49+
for (const name of ['headers_json', 'status'] as const) {
50+
const description = SysEmail.fields[name]?.description;
51+
expect(description).toEqual(expect.any(String));
52+
expect(description).not.toMatch(SERVICE_INTERFACE);
53+
}
54+
});
55+
});

‎packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2500,15 +2500,15 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
25002500
},
25012501
headers_json: {
25022502
label: "邮件头(JSON)",
2503-
help: "传给 IEmailService.send 的自定义邮件头,以 name → value 的 JSON 对象存储。两种投递模式下都会写入(它既是投递输入,也是审计证据)。在该列出现之前写入的行没有此值,读回时按「无自定义邮件头」处理。"
2503+
help: "发送邮件时随附的自定义邮件头,以 name → value 的 JSON 对象存储。两种投递模式下都会写入(它既是投递输入,也是审计证据)。在该列出现之前写入的行没有此值,读回时按「无自定义邮件头」处理。"
25042504
},
25052505
attachments_json: {
25062506
label: "附件(JSON)",
25072507
help: "附件的 JSON 数组,元素形状为 { filename, contentType?, size, hash, cid?, contentForm, inline?, storageKey?, contentReclaimedAt? }。在 plugin-email 预算(SYS_EMAIL_ATTACHMENT_LIMIT_BYTES,合计原始大小 256 KiB —— 最坏约 350 KB 的 base64)以内的内容以 base64 存放在 `inline`;超出的内容存入 file-storage 能力,元素改为携带 `storageKey`,因此两种情况下行体积都有界。filename/contentType/size/hash 是**永久**审计证据;行外内容属于投递工件,在行到达终态并经过宽限窗后被删除,届时 `storageKey` 由 `contentReclaimedAt` 取代。"
25082508
},
25092509
status: {
25102510
label: "状态",
2511-
help: "生命周期状态——在调用传输层之前由 IEmailService.send 排队",
2511+
help: "生命周期状态——邮件提交发送时即进入排队,早于首次投递尝试",
25122512
options: {
25132513
queued: "排队中",
25142514
sent: "已发送",

‎packages/platform-objects/src/audit/sys-email.object.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,17 @@ export const SysEmail = ObjectSchema.create({
150150
// `sys_secret` row per delivery with no cascade or retention, a
151151
// boot-window fail-open, a per-row decrypt on every tick); this card
152152
// adopts that decision rather than re-deciding it.
153+
//
154+
// The description below is this field's help in Studio's object forms, so
155+
// it reads as product guidance (#22093): the headers are the ones the
156+
// caller of `IEmailService.send` passed (its `headers` option, stored
157+
// through `encodeHeadersForRow`) — recorded here, not in the help text.
153158
headers_json: Field.textarea({
154159
label: 'Headers (JSON)',
155160
required: false,
156161
internal: true,
157162
description:
158-
'Custom headers supplied to IEmailService.send, as a JSON object of name → value. '
163+
'Custom headers supplied with the message when it was sent, as a JSON object of name → value. '
159164
+ 'Written in both delivery modes (it is audit evidence as much as delivery input). '
160165
+ 'Absent on rows written before this column existed, which read back as "no custom headers". '
161166
+ 'Never returned on the generic data path — headers are the ordinary place a '
@@ -184,7 +189,9 @@ export const SysEmail = ObjectSchema.create({
184189
label: 'Status',
185190
required: true,
186191
defaultValue: 'queued',
187-
description: 'Lifecycle state — queued by IEmailService.send before transport call',
192+
// Studio form help, so product words (#22093): `IEmailService.send`
193+
// writes the row as `queued` before its first transport call.
194+
description: 'Lifecycle state — queued when the message is submitted for sending, before the first delivery attempt',
188195
group: 'State',
189196
},
190197
),

0 commit comments

Comments
 (0)