Skip to content

docs(spec): a notify node's recipients names a written-out email address as an author constant, not an audience selector - #25739

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-25696-notify-recipients-describe
Oct 25, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-25696-notify-recipients-describe

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #25696

Clause-②: no
Metadata-protocol: semantic
Ruling-ref: 6386629237

What this changes

The domain:spec half of ruling B on #25647 (6386629237, batch #12 item 2, maintainer 「25635 25647 25675 同意」):

「B = 只关「表达式算出来的地址」:notify 收件人若由表达式(CEL 信封)求值得到一个不属于任何用户的邮箱地址,节点失败并给出定位提示指向 { emailField };求值得到某个用户的地址仍解析为该用户。元数据里写死的地址是作者常量,照常发出。」

Its spec workload: 「domain:spec 改描述文字,把「写死地址」写成作者常量,并从受众选择器列表里去掉笼统的「an email address」。」

Text only, all in packages/spec/src/automation/io-node-config.zod.ts. No key, shape, default, enum or accepted value moves; everything that parsed still parses.

  • The recipients docblock (the NotifyRecipientsSlotSchema JSDoc):
  • The field .describe() (what the reference docs and the JSON Schema read) and the union's .meta() description say the same.
    • The .describe() sentence "Delivery to an email-field recipient is not live yet: until the messaging service resolves the form, a run that reaches the node fails there" is replaced with what the runtime does.
  • NOTIFY_RECIPIENTS_RULE, the sentence a refused recipients value leads with, now lists "an email address written out, an author constant sent as written" beside a user id and an audience selector. A refused author is told every string spelling. Its opening, "recipients is who the notification goes to", is unchanged (pinned by io-node-config.test.ts).
  • The email-field section note no longer calls { emailField } "the one recipient form that is an email ADDRESS". It is the form for an address the record supplies, and it is delivered since d1e329e96d. An address written out by the author is the other way to one.
  • Regenerated content/docs/references/automation/io-node-config.mdx with gen:docs, and added the changeset .changeset/25696-notify-recipients-describe.md (@objectstack/spec: minor, the level Metadata-protocol: semantic takes).

The refusal is made: the runtime half landed

When this PR opened, the computed-address refusal was worded as planned, because the runtime half was still open (triage 6387591836). PR #25721 (d1e329e96d) has since landed the runtime half for both #25571 and #25647, and this branch merged it (f6c07d3953). The text now states what is made, read from the merged packages/services/service-automation/src/builtin/notify-node.ts:

  • resolveRecipients marks each recipient that an envelope computed and that reads as an address (mailedAddressOf: bare, or the remainder after user:).
  • screenComputedAddresses runs only when toStringList(cfg.channels).includes('email').
  • resolveEmailFieldAddress reads the stored row and requires isOneAddress.
  • admitEmailFieldSend caps sends at EMAIL_FIELD_SENDS_PER_WINDOW (5) per EMAIL_FIELD_SEND_WINDOW_SECONDS (3600), and admits on a counter fault.
  • A written-out address never reaches the screen.

What each claim in the new text rests on

  • A written-out address reaches the user who holds it, or else, on the email channel, the address itself: RecipientResolver.resolveOne → resolveEmail (a sys_user lookup by email, verbatim on a miss), then email-channel.ts's EMAIL_SHAPE(recipient) branch. screenComputedAddresses never sees a written-out value.
  • Computed address, email node: screenComputedAddresses, quoted above.
  • Computed address, non-email node: the mails gate in registerNotifyNode passes audience.value.recipients through unscreened.
  • Email-field delivery: resolveEmailFieldAddress, isOneAddress and admitEmailFieldSend.

Verification (head 83ce3e8bad, origin/main d1e329e96d merged in)

83ce3e8bad differs from 2e4fad24c5, where the build and the consumer-suite readings below were taken, only in a docblock comment and the changeset. A round-2 edit to .changeset/25610-notify-email-field-recipient.md (aed84ec582) was restored byte-identical to the merge base (29733bf86f). Round 3 (83ce3e8bad) states the confirmed email-channel scope.

Readings at 83ce3e8bad:

  • Spec rebuilt: VERDICT 0.

  • dispatch-gates --commands for the two changed paths derives 83 commands. 82 exit 0. check:liveness exits 1 on the main-caused translation/flows.screens row.

  • --ran prints: ✓ dispatch-gates --ran: 83 derived famil(ies) accounted for — 83 run, 0 NOT-MEASURED.

  • check:generated: its only red lane is check:liveness.

  • Spec typecheck: VERDICT 0.

  • Spec test: 1 failed | 720 passed (721), 23 failed | 21810 passed. All 23 failures are in check-liveness.test.ts, the main-caused file.

  • Spec test:repo: 79 passed (79), 1269 passed (1269).

  • The changeset family and check:nul-bytes exit 0.

  • pnpm install --frozen-lockfile exit 0. Full turbo run build --filter=!@objectstack/docs --concurrency=2: VERDICT command-exit 0.

  • check:generated after the text change named content/docs/references/** stale. After gen:docs the reference is regenerated, and check:docs exits 0.

  • pnpm --filter @objectstack/spec test: Test Files 1 failed | 720 passed (721), Tests 23 failed | 21810 passed | 1 todo (21834).

  • pnpm --filter @objectstack/spec test:repo: 79 passed (79) files, 1269 passed (1269) tests, VERDICT 0.

  • pnpm --filter @objectstack/spec typecheck: VERDICT 0.

  • Full suites of the packages that read the contract:

    • @objectstack/lint: 161 passed (161) files, 7285 passed (7285) tests.
    • @objectstack/service-automation: 202 passed (202) files, 2659 passed (2659) tests.
    • @objectstack/metadata-protocol: 263 passed | 3 skipped (266) files, 21485 passed | 19 skipped (21504) tests.
    • Each VERDICT 0.
  • Derived gates at 2e4fad24c5: 107 commands, 105 exit 0, and 2 exit 1 (check:liveness, and check:generated because it runs check:liveness).

    • Both name only translation/flows.screens → packages/services/service-automation/src/engine.ts, the same main-caused row.
    • --ran prints: ✓ dispatch-gates --ran: 107 derived famil(ies) accounted for — 107 run, 0 NOT-MEASURED (a DERIVED zero — all 107 recorded an exit code and none of them is 3).
  • affected-docs: 0 docs name something this change touched.

  • Control-byte self-scan: no hits. check:nul-bytes exits 0.

  • No ablation: no tested behaviour changes here.

Acceptance notes


Generated by Claude Code

… constant

The `recipients` docblock no longer lists "an email address" among the
audience selectors. A string address the author writes out is named an
author constant, sent as written; an address a CEL envelope evaluates to
resolves to the user who holds it, and failing the node on one that no
user holds, pointing to `{ emailField }`, is stated as planned: the
runtime half has not landed. Text only: no key, shape or default moves.

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

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 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 — 144 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 6679644a1a56b552d6864b125900221823ec78b3 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 6679644a1a56b552d6864b125900221823ec78b3

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI red, not this PR's · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-25T04:32Z

Two checks are red on 6cfe103516, and both are one red on main:

  • Spec property liveness: one key-mention citation, translation/flows.screens → packages/services/service-automation/src/engine.ts.
  • Test Core (1/6): every failure is in packages/spec/scripts/liveness/check-liveness.test.ts, which runs the real ledgers.

Cause: #25734 (0189524a41, merged 2026-10-25T03:42Z) moved the only screens mention out of engine.ts. main itself is red on Spec property liveness since then, and every PR whose change reaches packages/spec inherits it. This PR's diff (describe text in io-node-config.zod.ts) is not involved.

Fix: card #25742, claimed by this seat, with the ledger repoint dispatched now (claim 6393460981). Once it merges, this PR takes a base merge and its CI re-runs. No change to this PR is needed for the red.


Generated by Claude Code

…mail-field delivery the runtime now makes

The runtime half landed in d1e329e. A computed recipient that reads as
an address, in a node whose channels include `email`, now resolves to the
user who holds it or fails the node pointing to `{ emailField }`. An
email-field recipient is mailed, read from the stored trigger row, one
plain ASCII mailbox, at most 5 sends per address per hour, best-effort.
The describes, the union description, the docblock and the changeset now
say so, in place of the "planned" and "not live yet" sentences.

Claude-Session: https://claude.ai/code/session_01PC4NPXPgPmpDRG9DdDnyXT
Co-authored-by: Claude <noreply@anthropic.com>
…e made recipients text

Claude-Session: https://claude.ai/code/session_01PC4NPXPgPmpDRG9DdDnyXT
Co-authored-by: Claude <noreply@anthropic.com>
…ips in the same release

Its last paragraph said delivery lands separately and that a run reaching
such a node fails there. The runtime half landed in d1e329e and its own
changeset is pending beside this one, so the paragraph now says delivery
ships in this same release in service-automation's notify node.

Claude-Session: https://claude.ai/code/session_01PC4NPXPgPmpDRG9DdDnyXT
Co-authored-by: Claude <noreply@anthropic.com>
…erge-base text

A correction to another lane's pending release note is refused by the
release-note gate until a person confirms it, which would hold this PR's
landing on a release decision that is not this PR's. The stale paragraph
is routed to the maintainer as a separate decision instead.

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

The maintainer confirmed that ruling B's screen of a computed address
applies only to a notify node whose channels include email (ruling record
6393736930). The recipients docblock and the changeset no longer call the
scope unconfirmed.

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

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #25696 · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-25T06:13Z · head 83ce3e8bad · reports: round 0 6393558914, round 1 6393965971, round 2 6394072990, round 3 addendum on #25696 (subagent of this seat's session, no cloud session) · seat review at TIER_DEFAULT. The path limb hits (packages/spec/src/automation/**), so a same-shape ## Contract review PASS at CONTRACT_REVIEW_TIER on the landing head is owed. needs:contract-review is added in this stroke.

  1. Shape. Draft, base main, first line Fixes #25696, title docs(spec):. Clause-②: no, Metadata-protocol: semantic and Ruling-ref: 6386629237 are line-initial in the body and in the changeset (@objectstack/spec minor, the level semantic takes). 3 files: io-node-config.zod.ts text, the regenerated content/docs/references/automation/io-node-config.mdx, and .changeset/25696-notify-recipients-describe.md. Not governed. The body was rewritten through the relay for rounds 1–3 and read back identical.
  2. The ruling, measured on the diff. Ruling B on [Decision] A notify node already mails a written-out or computed email address: does ruling 6377842908's 「⛔ 不接受自由文本或表达式拼出的地址」 reach it — refuse both (A), refuse computed addresses only (B), or leave both (C)? #25647 (「domain:spec 改描述文字,把「写死地址」写成作者常量,并从受众选择器列表里去掉笼统的「an email address」」):
  3. No key, shape, default or accepted value moves. Text only, so Clause-②: no holds. semantic is right, because what an email address in recipients means is now stated, and so is the refusal sentence.
  4. Gates.
    • At 83ce3e8bad:
    • At 2e4fad24c5 (round 1, same source apart from one docblock comment and the changeset): full build exit 0, and consumer suites lint 161/7285, service-automation 202/2659, metadata-protocol 263/21485.
    • CI on 83ce3e8bad: the only reds so far are main's (Spec property liveness, Test Core (1/6)). The TypeScript Type Check reds on aed84ec582 and 29733bf86f were lanes cancelled by the next push, not type errors.
  5. Producers.
  6. Seat corrections during the rounds, on record.
  7. Deviations accepted. mcp_calls 0. Relay writes 4 (one pr_create and three report comments). The worktree is kept for a next round. The stray /full-build.pid at the filesystem root, from round 0, is left as is: a safety check refused its removal, and the seat did not try what was refused.
  8. Landing. Not governed. Once fix(spec/liveness): drop the comment-only engine.ts pointer from the translation/flows.screens row #25751 is in main, the seat brings main in through the relay (pr_update_branch). The CONTRACT_REVIEW_TIER review runs on that landing head. On PASS with every check green, the seat sends pr_ready + automerge_enable.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 83ce3e8bad176db06fa92b71e34f6cbca50ab5af
Local-runs: none
Scope: packages/spec/src/automation/io-node-config.zod.ts + packages/services/service-automation/src/builtin/notify-node.ts, packages/services/service-messaging/src/recipient-resolver.ts, packages/services/service-messaging/src/email-channel.ts, packages/services/service-messaging/src/inbox-channel.ts, packages/services/service-messaging/src/messaging-service.ts

Reviewer: domain:spec contract review on PR #25739 for card #25696 · session session_01PC4NPXPgPmpDRG9DdDnyXT (a subagent of the seat session, reviewing adversarially against the seat's ACCEPT 6394280653) · 2026-10-25T09:10Z

Inputs: card #25696 (body and all 10 comments), ruling 6386629237 and ruling record 6393736930 on #25647, PR #25739 (body, 3 files, git diff origin/main...83ce3e8bad176db06fa92b71e34f6cbca50ab5af in a detached read-only worktree; merge base d1e329e96d), the 58 check-runs on the head, and the cited runtime read at origin/main 5874d25302 — notify-node.ts is byte-identical there and at the head. The rest of the diff (.changeset/25696-notify-recipients-describe.md, the regenerated content/docs/references/automation/io-node-config.mdx) is the lane seat's ACCEPT at TIER_DEFAULT (6394280653).

① Derived judgments

Accept set: unchanged — right. Every changed line in io-node-config.zod.ts is a comment or a string literal: the recipients docblock, the NotifyRecipientsSlotSchema .meta() description, the field .describe(), the email-field section comment, and NOTIFY_RECIPIENTS_RULE. No key, union arm, default, regex or superRefine moved; everything that parsed still parses. Clause-②: no is true.

Public surface: no export moves — right. NOTIFY_RECIPIENTS_RULE is a module-private const (read only by notifyRecipientsRefusal and the list-entry refusal); its one pin, io-node-config.test.ts:636, holds the unchanged opening clause. What does move is documentation surface: the JSON-Schema description and the reference page. check:docs, check:api-surface and check:authorable-surface ride TypeScript Type Check, which is success on this head, so the regenerated artifacts match the source. The new runtime strings (.describe(), .meta(), the rule) carry no tracker number; the docblock comments cite the ruling and the PR, which is where citations belong.

Every sentence of the new text, read against notify-node.ts and the messaging service — true.

  • A written-out address is never screened: resolveRecipients pushes to computed only when isExpressionEnvelopeShaped(raw) (notify-node.ts:245). Right.
  • "sent as written: to the user who holds it, or else, on the email channel, to the address itself": RecipientResolver.resolveOne routes an address-shaped string through looksLikeEmail to resolveEmail, a sys_user lookup by email that keeps the address verbatim on a miss or a lookup error (recipient-resolver.ts:144, :227-246); the email channel's resolveRecipient sends to any EMAIL_SHAPE recipient as the address (email-channel.ts:192). Right. One precision point, not a falsity: a written-out user:a@b.c skips the lookup (resolveOne hands the remainder on as the id, :137) and mails the address directly; the text does not describe that spelling for a written-out value, only for a computed one.
  • "bare or as user:ADDRESS": mailedAddressOf (:292-299) covers both, and leaves role:, team: and owner_of: alone. Right.
  • The screen runs only when channels include email: const mails = toStringList(cfg.channels).includes('email') (:754-757). Right.
  • A held address resolves to that user's id: screened[entry.index] = id (:376). Right.
  • One no user holds fails the node pointing to { emailField }: the refusal names recipients: { emailField: 'FIELD' } and channels: 'email' (:366-374). Right.
  • One that cannot be looked up fails the node: no data engine (:348-354), a failing lookup (:358-363). Right.
  • "inbox alone, the messaging default" and "mails nobody": messaging-service.ts:1114 defaults the fan-out to ['inbox']; the channels in the tree are inbox, email and sms, and only email sends to an address. A non-email node's computed recipients pass unscreened (:757). Right.
  • Email-field delivery: the address is read from the stored row by id, never off context.record (resolveEmailFieldAddress, :444); isOneAddress demands one plain ASCII mailbox (:395-405, :463); the counter key is the lowercased address under one prefix, 5 per 3600 s across every flow and node (:486-492, :585-590); a faulting counter admits and warns once (:591-599); every no-address path fails the node and sends nothing (:433-471). Right. "best-effort" also covers the one case the text does not spell out: with no cache service the lazy store falls back per process, so the limit holds per node of a deployment (:572-573).
  • "The runtime delivers it since feat(service-automation)!: notify mails an email-field recipient, limited per address, and refuses a computed address no user holds #25721 (d1e329e96d)": that commit is the merge base, and notify-node.ts is identical at the head and at origin/main. Right.
  • Ruling citations: the docblock's reading of 6386629237 B (「元数据里写死的地址是作者常量,照常发出」; the computed address must be a user's) and of 6393736930 A (the screen covers only nodes whose channels include email) match the records. The hedge the triage order required while the runtime half was open is gone, correctly, because the runtime half has landed. Right.
  • The four text sites (docblock, .describe(), .meta(), the rule) agree with each other and with the executor's own descriptor text at notify-node.ts:638. Right.

Gate verdicts on this head — the check-runs. 43 check-runs success or skipped; 3 failure: Spec property liveness (121090188716), Test Core (1/6) (121090568006: packages/spec pnpm run test exited 1) and the roster Test Core. The required set is therefore red, and no landing follows this record until it is green (Multi-agent discipline §7). Cause, verified read-only and not this diff's: the liveness ledger row translation.json flows.screens cites packages/services/service-automation/src/engine.ts#renderFlowTextSlot in its evidence, and git grep screens origin/main -- …/engine.ts finds nothing since #25734 (0189524a41, 15 commits down origin/main; the merge base d1e329e96d is 6 down, so this head inherits it). The spec test run on this head is the same ledger read in scripts/liveness/check-liveness.test.ts. The fix, PR #25751 (card #25742), is open and edits only packages/spec/liveness/translation.json and its changeset; this PR touches neither file. On main's own push runs Test Core was green at 0189524a41 and d1e329e96d because packages/spec was not in their affected set; Spec property liveness runs on pull_request and merge_group only, so the red surfaces on every spec-touching PR and queue build. Reading: a main-red inherited by this head, named as such; the seat's plan (merge main once #25751 lands, then re-run) is the right one.

② Semver level

@objectstack/spec: minor, with Clause-②: no and Metadata-protocol: semantic line-initial in both the PR body and the changeset body (Check Changeset success). Right. The protocol's stated meaning of a bare address string in recipients moves from "an audience selector the messaging service resolves" to "an author constant, sent as written", and the text now states the computed-address rule and the email-field delivery — a change in meaning of what an author writes against, read by the JSON Schema and the reference page, so the kind is semantic and the level follows it to minor. none/patch would under-declare it. No ADR-0087 marker is owed (nothing breaks), and the changeset's migration section is correctly absent: "authored metadata needs no change". The changeset carries no model identifier and no closing keyword; the PR body's one closing keyword is Fixes #25696.

③ Boundary flags

open_questions is [] in all four dev reports (6393558914, 6393965971, 6394072990, 6394264201). The flags the reports carry, each answered or escalated:

Implemented-by: claude/issue-25696-notify-recipients-describe
Reviewed-by: session_01PC4NPXPgPmpDRG9DdDnyXT

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0433b8133fd84f7ffe6cb2ea532dc52fc6faade0
Local-runs: none
Scope: packages/spec/src/automation/io-node-config.zod.ts + packages/services/service-automation/src/builtin/notify-node.ts, packages/services/service-messaging/src/recipient-resolver.ts, packages/services/service-messaging/src/email-channel.ts, packages/services/service-messaging/src/inbox-channel.ts, packages/services/service-messaging/src/sms-channel.ts, packages/services/service-messaging/src/messaging-service.ts

Reviewer: domain:spec contract review on PR #25739 for card #25696, the confirming record on the merge head · session session_01PC4NPXPgPmpDRG9DdDnyXT (a subagent of the seat session, reviewing adversarially; the prior record 6395784809 and the seat's ACCEPT 6394280653 were read as inputs, not adopted) · 2026-10-25T14:22Z

Inputs: card #25696 (no comment newer than the prior record), PR #25739 (body unchanged: Fixes #25696, then Clause-②: no and Metadata-protocol: semantic line-initial; 3 files; no PR comment newer than 6395784809; labels documentation, size/s, tooling), the prior record 6395784809 on 83ce3e8bad, the seat's ACCEPT 6394280653, git diff origin/main...0433b8133f in a detached read-only worktree at origin/main = 6679644a1a, the check-runs on the head (REST, twice), and the cited runtime read at origin/main. The head is a merge commit by objectstack-fleet[bot] (the relay's pr_update_branch): first parent 83ce3e8bad (the reviewed head), second parent 6679644a1a (origin/main); its tree differs from origin/main in exactly the three files of the PR. The rest of the diff (.changeset/25696-notify-recipients-describe.md, the regenerated content/docs/references/automation/io-node-config.mdx) stays the lane seat's ACCEPT at TIER_DEFAULT (6394280653).

① Derived judgments

1. The PR's own change is unchanged on this head — confirmed. git diff origin/main...0433b8133f (the merge base is origin/main itself) and git diff d1e329e96d..83ce3e8bad (the reviewed head against its merge base) are identical line for line apart from the index lines: the same 3 files, 74 insertions, 18 deletions, the same hunks. The three files are byte-identical at 83ce3e8bad and at 0433b8133f. Every judgment of 6395784809 ① about the diff therefore holds on this head and was re-read rather than carried: a text-only change (docblock, .meta() description, .describe(), section comment, NOTIFY_RECIPIENTS_RULE); no key, union arm, default, regex or superRefine moved, so everything that parsed still parses and Clause-②: no is true; NOTIFY_RECIPIENTS_RULE stays module-private and its pinned opening clause (io-node-config.test.ts:636 expects 'recipients is who the notification goes to'; the test file is unchanged on main) is intact; the four text sites agree with each other and with the executor's descriptor at notify-node.ts:638.

2. What main brought in: 19 commits (d1e329e96d..6679644a1a), none touching the diff's files or the cited code. Read off git diff --stat of origin/main against the merge base: nothing under packages/spec/src/**, packages/services/service-automation/**, packages/services/service-messaging/**, content/docs/references/**, examples/app-showcase/src/automation/**, nor .objectui-sha; under packages/spec/ only the three liveness ledgers (connector.json, flow.json, translation.json, 7 lines, #25796). notify-node.ts is one blob (23d59800) at 5874d25302 (the prior record's origin/main), at origin/main now and at this head; the messaging files are unchanged between 5874d25302 and origin/main. The docs generator (scripts/build-schemas.ts, packages/spec/scripts/**) is unchanged, so the regenerated io-node-config.mdx the head carries is the one check:docs passed at 83ce3e8bad against the same generator; the only workflow change on main is #25746 (Test Core 6 → 8 shards).

3. Every sentence of the new text, re-read against the runtime at origin/main — true. Read again, not adopted:

  • Only an envelope-shaped entry is screened: isExpressionEnvelopeShaped(raw) gates mailedAddressOf (notify-node.ts:245); mailedAddressOf covers a bare address and user:ADDRESS, and leaves role:, team: and owner_of: alone (:292-299). Right.
  • The screen runs only when channels include email (:754-757); a held address becomes that user's id (:376); no data engine (:348-354), a failing lookup (:358-363) and an address no user holds (:366-374) each fail the node, the last naming recipients: { emailField: 'FIELD' } and channels: 'email'. Right.
  • A written-out address is sent as written: the resolver's looksLikeEmail hands it to resolveEmail (recipient-resolver.ts:144), which keeps the address verbatim on a miss or an error (:227-246); the email channel sends to any EMAIL_SHAPE recipient as the address (email-channel.ts:192); user: hands the remainder on as an id (:132). Right.
  • "inbox alone, the messaging default": messaging-service.ts:1114 defaults the fan-out to ['inbox']. "Mails nobody": the registered channels are inbox, email and sms (inbox-channel.ts:136, email-channel.ts:240, sms-channel.ts:186); only email-channel.ts and the resolver read an address shape, and sms resolves a recipient as a literal phone number or a sys_user id (sms-channel.ts:141-156), so an address-shaped recipient on a non-email node reaches no mailbox. Right.
  • Email-field delivery: the address is read from the stored row by id (:444), must be one plain ASCII mailbox (:395-405, :463), every no-address path fails the node and sends nothing (:433-471); 5 sends per 3600 s per lowercased address under one prefix (:486-492, :585-590); a faulting counter admits and warns once (:591-599); the per-node degradation without a cache service (:570-573) is what "best-effort" covers. Right.
  • "The runtime delivers it since feat(service-automation)!: notify mails an email-field recipient, limited per address, and refuses a computed address no user holds #25721 (d1e329e96d)": that commit is the first parent's merge base and the file is identical since. Right.
    The precision point the prior record made stands and is not a falsity: inbox-channel.ts:149 / :205 writes a row keyed by the recipient string with no user check, and the text claims nothing for an address on an inbox-only node.

4. The prior record's one forward-looking line is superseded, not falsified. 6395784809 ① named the inherited red (Spec property liveness and Test Core (1/6), from the translation.json flows → screens row citing engine.ts#renderFlowTextSlot) and PR #25751 as the fix. #25751 was closed unmerged; the fix that landed is #25796 (e2a4b6a063), which repoints that row to screen-nodes.ts#registerScreenNodes and i18n-resolver.ts#flowScreenCopyKey and four more rows across connector.json and flow.json to the seam modules, with a patch changeset (Clause-②: no, Metadata-protocol: none; no status or key moves). On this head Spec property liveness (121190681689) is success. The diagnosis — a main-red inherited by the head, not this diff's — was right; the vehicle differs.

5. Gate verdicts on this head — the check-runs. CI on the merge head started 2026-10-25T14:05Z. First read, 2026-10-25T14:12Z: 45 check-runs, 0 red, 11 still running. Completed success by then: Check Changeset, Governed Surface Queue Guard (not governed), Spec property liveness, Check PR Size, The card this PR closes must claim this branch, Part-of PR must not also close its card, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Check Documentation Links, Flag docs affected by code changes, Lint · PM dispatch-gates, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Dogfood Regression Gate, Auto Label, filter; the UI and opt-in jobs skipped. Re-read, 2026-10-25T14:22Z: 46 check-runs, 0 red, 31 success, 10 skipped, 5 still running — Test Core (3/8), (4/8), (5/8), (7/8) and Type Check · workspace. Newly success by then: Lint & Repo Gates (the required aggregate, 121191839390) with its five remaining lanes, Type Check · source gates, Type Check · debt ledger, Type Check · consumer gates, Build Core, Build Docs, and Test Core (1/8), (2/8), (6/8), (8/8); the required aggregates TypeScript Type Check and Test Core were not yet created at the re-read (one lane of the first and four shards of the second still running). The judgments the prior record read off TypeScript Type Check success at 83ce3e8bad — check:docs and check:authorable-surface (Type Check · source gates, lint.yml) and check:api-surface (Type Check · consumer gates) — hold on this head too: both lanes are success at the re-read, and ① 2 says why the regenerated artifacts could not have drifted from main's side. ci-failure.mjs --pr 25739 answers UNDETERMINED while any check runs ("not-red-yet is not green"). No check-run is red at either read. This record states the reading; landing precondition ② (every check green on the head) is the seat's to read before pr_ready, and a red that appears later re-opens this item and nothing else here.

② Semver level

Unchanged from 83ce3e8bad and re-confirmed on the merge head: @objectstack/spec minor, with Clause-②: no and Metadata-protocol: semantic line-initial in the PR body (lines 3–4, after Fixes #25696) and in .changeset/25696-notify-recipients-describe.md (byte-identical to the reviewed head; Check Changeset success on this head). Right: the protocol's stated meaning of a bare address string in recipients moves from "an audience selector the messaging service resolves" to "an author constant, sent as written", and the computed-address rule and the email-field delivery are now stated, in text the JSON Schema and the reference page carry — a change in meaning of what an author writes against, so semantic, and the level follows it to minor; none or patch would under-declare it. No ADR-0087 marker is owed (nothing breaks) and the migration section is correctly absent. Of the pending changesets main brought in, one names @objectstack/spec (25790-spec-liveness-seam-citations.md, patch, ledger evidence pointers only) and none states anything about the notify surface; the pending 25610-notify-email-field-recipient.md is unchanged (③ F7).

③ Boundary flags

open_questions is [] in all four dev reports (6393558914, 6393965971, 6394072990, 6394264201), and no new report or comment has landed since the prior record. The nine flags of 6395784809 ③, re-read on this head, and one new one:

Implemented-by: claude/issue-25696-notify-recipients-describe
Reviewed-by: session_01PC4NPXPgPmpDRG9DdDnyXT

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 25, 2026 14:39
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 25, 2026 14:39
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 25, 2026
Merged via the queue into main with commit 52f3f51 Oct 25, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25696-notify-recipients-describe branch October 25, 2026 15:08
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/s tooling

Projects

None yet

2 participants