Skip to content

Commit c87ef70

Browse files
feat(spec)!: 收紧 automation/ 主体剩余九个可授权形状的未知键(#4001 批 11) (#4974)
* feat(spec)!: close nine authorable automation shapes against unknown keys (#4001 batch 11) zod's default `.strip` discards an undeclared key and parses on. Batch 11 closes the nine remaining shapes in `automation/`'s main body, each rejection naming the surface, the key, and — where the word is recognisable — the canonical spelling. `flow.zod.ts`: the four OUTER shapes were closed earlier; their six nested authoring blocks were not, so the gate rejected `nodee:` at node level while `connectorConfig: { connectorID }` parsed clean and dispatched against an undefined connector id. Now strict: `FlowNode.connectorConfig` / `.position` / `.inputSchema` / `.waitEventConfig` / `.boundaryConfig`, `Flow.errorHandling`. Deliberately still open, now pinned in code AND tests so a later sweep stops rather than "finishing" the file: the node `config` slot (ADR-0018) and `FlowVersionHistorySchema` (emitted, not authored — its `definition` is `FlowSchema`, so the authored half is gated anyway). Every alias is a real in-repo spelling of the same knob on a neighbouring surface, taken from an AST census over the repo's own payloads, not guessed: `backoffMs` from `shared/retry-policy.zod.ts` (#4661), `initialDelayMs` / `maxDelayMs` from connector `RetryConfig`, `attachedToRef` / `cancelActivity` from BPMN 2.0. Two cases get a prescription instead of a rename because a rename would be wrong: `inputSchema.optional` is the opposite polarity of `required`, and `errorHandling.maxAttempts` counts the first attempt while `maxRetries` does not — renaming it alone would quietly run one attempt fewer. `time-relative-trigger.zod.ts`: the schema #4852 made visible (its only site was line-wrapped, the old counter read zero, and a zero-site file is skipped by the coverage walk). It sits under the open node `config` slot, so it is the only key gate there is — and it is `safeParse`d at BIND time, so `offsetDay` beside a valid mode used to bind a sweep with the author's narrowing discarded and report itself configured. `flow-function.zod.ts`: binds at authoring only (boot reads `normalizeFlowFunctionEntry`), stated in the code rather than implied. That is why it matters: the reader takes two keys and ignores the rest by construction, so a misspelled `effect` was dropped and then not looked for — the function ran and its writes were counted as none, keeping #4354's broken-sweep query silent. `webhook.zod.ts`: the ledger's `(p)` resolved to the opposite of "spec-only" — three parse doors, one of them `bootstrapDeclaredWebhooks` at BOOT, which warns and SKIPS on failure. Hence the ADR-0010 envelope in the same commit: `applyProtection` stamps `_packageId`/`_provenance` on every type, so closing the shape without declaring them would have turned every package-shipped webhook into a skipped subscription after a redeploy. Strictness also rides `.extend()` onto the connector `WebhookConfigSchema` — verified against real zod and pinned in `connector.test.ts`. Ledger: `automation/` 67 → 58 strip; three rows reached zero and were deleted by the #4852 reverse pin. Gate re-proved red in both directions before trusting it, and every new assertion was proved to depend on the tightening by neutering `strictObject` (19 red, restored byte-identical). Part of #4001 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 * docs(spec): regenerate reference pages for the webhook ADR-0010 envelope `gen:docs` only — the property tables for `automation/Webhook` and the `integration/WebhookConfig` extension gain the `protection` block and the seven `_lock*`/`_provenance`/`_package*` rows. Checked for the #3746 trap: no page description or frontmatter moved, so no internal comment leaked onto a public reference page. Part of #4001 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 * fix(spec): correct three history strings that named the wrong failure Self-review against the actual behaviour, before the claim shipped. The `connectorConfig` / `boundaryConfig` / `position` / `inputSchema` prose named a slip on a REQUIRED key (`connectorID`, `attachedToRef`) as the silent case. It never was: a required key spelled wrong then reads as MISSING, which zod has always rejected loudly. Nothing was silent about it. What `.strip` actually swallowed is the OPTIONAL half — the mapped `input` map (dispatched as `input ?? {}`: a successful connector call carrying nothing), BPMN's `cancelActivity: false` (so `interrupting` fell to its `true` default and a NON-interrupting event cancelled the host anyway), `optional: false` (so `required` fell to false and `validateNodeInputSchemas` had nothing to require), a canvas hint beside x/y. I.e. exactly the keys an author adds to CONSTRAIN behaviour, replaced by a permissive default. The corrected version is the stronger argument as well as the true one, and getting it wrong is the failure mode this campaign has already paid for four times (the withdrawn guidance prescriptions): rejection prose is behaviour, not documentation. Fixed in the schemas, the module comment, the tests and the ledger row; a test now pins the required-key case as loud so the distinction cannot quietly re-blur. Part of #4001 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent fe83042 commit c87ef70

15 files changed

Lines changed: 958 additions & 56 deletions
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
'@objectstack/spec': major
3+
---
4+
5+
Close nine authorable automation shapes against unknown keys (#4001 batch 11, ADR-0078)
6+
7+
zod's default is `.strip`: a key a schema does not declare is silently discarded
8+
and the parse still succeeds. On an authoring surface that is the worst failure
9+
mode — the author (increasingly, an AI) gets a success envelope and ships
10+
metadata that quietly ignores what they wrote. This batch closes the nine
11+
remaining shapes in `automation/`'s main body.
12+
13+
**BREAKING.** Each of these now raises a named, fixable error instead of dropping
14+
the key. The rejection carries the surface, the offending key, and — where the
15+
word is recognisable — the canonical spelling.
16+
17+
**`flow.zod.ts` — the six NESTED blocks.** The four outer shapes (flow / node /
18+
edge / variable) were closed earlier; their inner blocks were not, so the gate
19+
rejected `nodee:` at node level while `connectorConfig: { connectorID }` — one
20+
capital letter — parsed clean and dispatched the action against an undefined
21+
connector id. Now strict: `FlowNode.connectorConfig`, `.position`,
22+
`.inputSchema` (each parameter declaration), `.waitEventConfig`,
23+
`.boundaryConfig`, and `Flow.errorHandling`.
24+
25+
Renames the rejections offer, each one a real spelling of the same knob on a
26+
neighbouring surface in this repo:
27+
28+
| you wrote | write instead | where the other word comes from |
29+
|---|---|---|
30+
| `connectorConfig.params` / `parameters` / `arguments` / `payload` | `input` | script-node `config.inputs`, integration products |
31+
| `waitEventConfig.event` / `signal` / `duration` / `delay` | `eventType` / `signalName` / `timerDuration` ||
32+
| `boundaryConfig.attachedToRef` / `cancelActivity` | `attachedToNodeId` / `interrupting` | BPMN 2.0's own attribute names |
33+
| `errorHandling.backoffMs` | `retryDelayMs` | `shared/retry-policy.zod.ts` (#4661) |
34+
| `errorHandling.initialDelayMs` / `maxDelayMs` | `retryDelayMs` / `maxRetryDelayMs` | connector `RetryConfig` |
35+
| `errorHandling.retries` / `attempts` / `onError` | `maxRetries` / `strategy` ||
36+
37+
Two are prescriptions rather than renames, because a rename would be wrong:
38+
`inputSchema`'s `optional` is the opposite polarity of `required` (write
39+
`required: false`), and `errorHandling.maxAttempts` counts the first attempt
40+
while `maxRetries` counts the ones after it (write `maxRetries: maxAttempts - 1`).
41+
42+
**Deliberately still open**, both now pinned in code and in tests so a later
43+
sweep stops rather than "finishing" the file: the flow node `config` slot
44+
(ADR-0018 — the plugin node-type namespace, owned by each executor's
45+
`configSchema`) and `FlowVersionHistorySchema` (emitted on publish, never
46+
authored; the flow *inside* a history record is still gated by `FlowSchema`).
47+
48+
**`time-relative-trigger.zod.ts`.** `config.timeRelative` sits under the open
49+
node `config` slot, so this schema is the only key gate it has — and it is
50+
`safeParse`d at BIND time, not only at authoring. `{ …valid, offsetDay: 7 }`
51+
used to bind a sweep that ran daily with the author's narrowing discarded, and
52+
reported itself configured; it now refuses to bind and says why. `field`
53+
`dateField`, `filters``filter`, `objectName``object`, `limit`
54+
`maxRecords`; `schedule` and `runAs` get pointed at the layer that owns them.
55+
56+
**`flow-function.zod.ts`.** `{ handler, effect }` in `defineStack({ functions })`.
57+
This binds at authoring only — the boot path reads entries with
58+
`normalizeFlowFunctionEntry`, not a parse — which is exactly why it matters:
59+
that reader takes two keys and ignores the rest by construction, so a misspelled
60+
`effect` was dropped at the schema and then not looked for. The function still
61+
registered, still ran, and its writes were still counted as none, keeping
62+
#4354's broken-sweep query silent on the one run that needed it.
63+
64+
**`webhook.zod.ts`.** `object_name``object` and `active``isActive` (the
65+
`sys_webhook` column names, for anyone re-authoring from a row), `events`
66+
`triggers`, `endpoint``url`; the five props #3494 removed now reject with
67+
their reason instead of vanishing. Strictness also rides `.extend()` onto the
68+
connector `WebhookConfigSchema`.
69+
70+
**`webhook` also gains the ADR-0010 protection envelope** (`protection`, plus
71+
the loader-set `_lock` / `_lockReason` / `_lockSource` / `_provenance` /
72+
`_packageId` / `_packageVersion` / `_lockDocsUrl`). This is not a separate
73+
feature: both metadata load paths call `applyProtection` on every type, so a
74+
package-loaded webhook already carried those keys when `plugin-webhooks`
75+
re-parsed it at boot. Closing the shape without declaring them would have turned
76+
every package-shipped webhook into a skipped subscription after a redeploy.

content/docs/automation/flows.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ Each node performs a specific action in the flow.
144144
| `position` | `{ x, y }` | optional | Visual position on canvas |
145145
| `timeoutMs` | `number` | optional | Per-node execution timeout |
146146
| `inputSchema` | `object` | optional | Declared input parameter types, for Studio form generation and runtime validation |
147-
| `waitEventConfig` | `object` | optional | `wait`-node event descriptor (`eventType`, `timerDuration`, `signalName`, `timeoutMs`, `onTimeout`) |
147+
| `waitEventConfig` | `object` | optional | `wait`-node event descriptor (`eventType`, `timerDuration`, `signalName`). `timeoutMs` / `onTimeout` were removed in 17 (#4158) — `wait` has no timeout; `timerDuration` accepts a bare number as milliseconds |
148148
| `boundaryConfig` | `object` | optional | BPMN boundary-event descriptor (interop) |
149149

150150
<Callout type="info">

content/docs/references/automation/webhook.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ const result = WebhookSchema.parse(data);
9292
| **secret** | `string` | optional | Signing secret for HMAC signature verification |
9393
| **isActive** | `boolean` || Whether webhook is active |
9494
| **description** | `string` | optional | Webhook description |
95+
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this webhook. |
96+
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
97+
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
98+
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
99+
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
100+
| **_packageId** | `string` | optional | Owning package machine id. |
101+
| **_packageVersion** | `string` | optional | Owning package version. |
102+
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
95103

96104

97105
---

content/docs/references/integration/connector.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,14 @@ Synchronization strategy
487487
| **secret** | `string` | optional | Signing secret for HMAC signature verification |
488488
| **isActive** | `boolean` || Whether webhook is active |
489489
| **description** | `string` | optional | Webhook description |
490+
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this webhook. |
491+
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
492+
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
493+
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
494+
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
495+
| **_packageId** | `string` | optional | Owning package machine id. |
496+
| **_packageVersion** | `string` | optional | Owning package version. |
497+
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
490498
| **events** | `Enum<'record.created' \| 'record.updated' \| 'record.deleted' \| 'sync.started' \| 'sync.completed' \| 'sync.failed' \| 'auth.expired' \| 'rate_limit.exceeded'>[]` | optional | Connector events to subscribe to (not yet enforced — no runtime dispatches these; see #3197) |
491499
| **signatureAlgorithm** | `Enum<'hmac_sha256' \| 'hmac_sha512' \| 'none'>` || Webhook signature algorithm |
492500

0 commit comments

Comments
 (0)