Repository navigation
fix(spec,service-automation): the wait executor reads its declared contract only (#4045) - #4161
Merged
Merged
Conversation
…ntract only (#4045) `wait` keeps its contract in `waitEventConfig` — a declared, `.describe()`-annotated block on `FlowNodeSchema` that sits in the authorable-field list, reaches the generated reference, and is what the showcase actually authors. Its descriptor publishes no `configSchema`, which is by design rather than the gap it looks like. The executor nevertheless also read six loose `config` keys behind `wec.X ?? loose.X`, two of them (`duration`, `signal`) spellings the spec never declared anywhere. That is the `notify.source` shape #4050 retired: a second de-facto contract announced only by a code comment, so an author who wrote it got a flow that worked forever and was never steered to the declared spelling (PD #12). Nothing in-repo authors it. - New ADR-0087 D2 conversion `flow-node-wait-event-config-lift` lifts config.{eventType,timerDuration,duration,timeoutMs,signalName,signal} onto the declared block, in the executor's own `??` precedence — a declared value wins and its loose counterpart is left shadowed, as `renameConfigKey` treats a shadowed alias. - `eventType` is stamped `'timer'` when the lift would otherwise leave the block without one. Load-bearing, not tidiness: the loader parses the CONVERTED flow (`applyConversionsToFlow` → `FlowSchema.parse`) and `waitEventConfig.eventType` is required once the block exists, so a stored flow carrying only `config: { duration: 'PT1M' }` would have gone from working to failing to load. `'timer'` is the exact default the executor applied to that shape. - The six `?? loose.*` fallbacks are deleted. The surviving `?? 'timer'` is not one: `waitEventConfig` is itself optional, and a node without it is a valid timer wait. Registered in both lists the layer keeps: `CONVERSIONS_BY_MAJOR[17]` and `step17.conversionIds`. The first draft missed the second and the repo's own ratchet caught it — `migrations.test.ts` replays every conversion fixture through the chain, so an unwired conversion fails there rather than shipping inert. Verified at the real seam: the new executor tests author the legacy shape and go through `registerFlow`, which is what applies the conversion. Negative control run by unregistering the conversion — two of the three fail without it; the third passes either way because it pins the executor's half of the precedence, and its name says so. A second negative control pins the `eventType` default: deleting it from the converted output makes `FlowSchema.parse` throw. Regenerated `spec-changes.json` and `docs/protocol-upgrade-guide.md` (the step rationale is a generated-artifact input). All 12 spec gates and 8 root gates pass; spec suite 7097 tests green. Deliberately unchanged and filed as #4158: `waitEventConfig.timeoutMs` is declared a timeout guard but read as a timer duration, and `onTimeout` has zero readers — `wait` has no timeout implementation at all, while the showcase authors `onTimeout: 'continue'`. Implementing or retracting that is a behaviour change, not a contract cleanup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 108 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…-flow-submit-jfpjy4
…#4045) Corrects a factual claim in the previous commit. It said "Nothing in-repo authors it" of the loose `config` back door — that is wrong. The showcase's own `wait_revision` node authored exactly that shape: config: { eventType: 'signal', signalName: 'budget_revision' } so the back door was not hypothetical, and the example that demonstrates `wait` was itself on the spelling this PR retires. It moves to `waitEventConfig`. The conversion already handled this shape correctly — the executor's behaviour is identical either way, and a new test pins the exact combination the showcase hit: the DECLARED key names sitting in the UNDECLARED location, which is what the candidate ordering has to get right (`signalName` before `signal`). It also asserts the converted flow still parses, since the lift creates the required block. Found while checking whether a stuck `objectstack verify` CI step could be caused by this change rather than by the runner — it could not, but the audit that ruled it out is what surfaced the false claim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq
os-zhuang
marked this pull request as ready for review
July 30, 2026 14:09
os-zhuang
added a commit
that referenced
this pull request
Jul 30, 2026
#4183) `packages/spec` has eight checked-in generated artifacts, each with its own gate, split across two CI jobs that run their gates SEQUENTIALLY. The first stale artifact therefore masks every one behind it: you fix it, push, and learn about the next on the following run. Two pushes on #4040 (`check:docs`, then `check:api-surface`), two more on #4161 (`check:spec-changes`, then `check:upgrade-guide`) — four round trips spent discovering something one local run could have said at once. Every gate runs; a failure does not stop the rest. The summary lists all stale artifacts with the exact `gen:` command for each. `--fix` regenerates ONLY what this run proved stale. Deliberately not a regenerate-everything button: blanket regeneration rewrites artifacts whose staleness you never saw, which is how a real semantic change lands silently inside a mechanical diff. The gate -> generator ledger reconciles against `package.json` on EVERY run, in both directions, rather than behind a `--self-test` flag. An unclassified `check:`/`gen:` script fails the run instead of quietly dropping out of coverage — otherwise the summary would still say "all artifacts up to date" while checking fewer, the exact class of lie this exists to remove. It proved itself by rejecting its own `package.json` entry on the first run. Two things it refuses to be silent about: the `check:api-surface` stale-`dist` trap is printed inline when that gate is the one failing (it reads the built `.d.ts`, so an unbuilt tree reports newly-added exports as breaking removals), and the four source audits it does NOT run are named, so "all up to date" never reads as "everything passed". It also reports that `gen:openapi` and `gen:sbom` have no gate at all. Verified both directions: clean built tree -> all 8 pass; a change injected into a migration step's `rationale` -> `check:upgrade-guide` fails and `--fix` regenerates that artifact and no other. That run also corrected an earlier reading of mine: the two ADR-0087 gates have different inputs — the conversion registry drives `spec-changes.json`, the rationale prose drives `protocol-upgrade-guide.md` — they had only appeared to fail together because #4161 changed both. AGENTS.md's hand-rolled loop over eight hardcoded gate names is replaced by the command; that list could not survive a ninth artifact, and the ledger can. Claude-Session: https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Jul 30, 2026
Merged
os-zhuang
added a commit
that referenced
this pull request
Jul 31, 2026
…never got (#4219) The drift comment on #4161 was computed by the mapper bug #4206 fixed: the service-automation change collapsed to `packages/services`, so these four docs were attributed to the wrong package — and a service-automation-only diff would have reported none of them. Re-derived with the fixed mapper and audited against the implementation. flows.mdx carries the #4161 axis itself: a `runAs:'user'` run that resolved no trigger user has its data operations refused. Also documents `node.type` as an open string checked against the live registry (ADR-0018), the script executor as naming a callable with logger-backed `email`/`slack` markers, the `.strict()` shells, and six previously undocumented node keys. implementation-status.mdx gains the real `/api/v1` route prefixes, the memory driver's actual `InMemoryDriver.supports` matrix, and the ADR-0090 D3 `Role` -> `Position` rename — which ratchets the role-word baseline down by one, so that gate-mandated update ships here too. Gates: docs build, check:doc-authoring, check:role-word, check:nul-bytes, check:release-notes, check:skill-examples (198 prose examples). Claude-Session: https://claude.ai/code/session_01J2x8Tie9WT1VgWFifqMKrR Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…ptions state each decision in words instead of a tracker number (stage 4) (objectstack-ai#21555) Part of objectstack-ai#20749 Clause-②: no Stage 4 of the `domain:spec` lane's share of the runtime-string burn-down (ruling `5902360492`, form D): class (b) of the stage-3 census, the text `packages/spec/src` shows authors and administrators. Every rewritten string now states in words what the cited decision was, or drops a citation its sentence already explained. Text only. ## What changed - **56 ADR-0087 conversion summaries** in `packages/spec/src/conversions/registry.ts` (68 tracker ids): every protocol 16 → 17 summary that carried an id. A summary is the "Change" column of `docs/protocol-upgrade-guide.md`, the `to` text of `spec-changes.json`'s `converted[]` records and what `os migrate meta --json` reports under `specChanges`, so it is read by an author upgrading metadata. - **Three descriptions** (3 ids): `FieldSchema.autonumberFormat`'s `.meta()` description (`data/field.zod.ts`), and the route descriptions of `GET /:type/:name/layers` and `POST /:type/:name/publish` (`api/plugin-rest-api.zod.ts`). - **Generated, by `check:generated --fix`** (exactly the three artifacts it proved stale): `docs/protocol-upgrade-guide.md` (56 rows), `packages/spec/spec-changes.json` (56 summaries, twice each: the per-major record and the aggregate), and the `autonumberFormat` rows of `content/docs/references/data/field.mdx`, `data/object.mdx` and `system/migration.mdx`. - One `@objectstack/spec` **patch** changeset, `Clause-②: no`. ## Size: the split (A2) At this base the class (b) population is unchanged from the stage-3 census: 91 conversion summaries with 108 ids (81 distinct cards) plus 3 descriptions with 3 ids. That is over the ~60-card bar, so this PR delivers up to a protocol-step boundary, lowest step first: | protocol step | summaries with ids | ids | distinct cards | |---|--:|--:|--:| | toMajor 11 / 13 / 14 / 15 | 0 | 0 | 0 | | **toMajor 17 (delivered)** | **56** | **68** | **48** | | toMajor 18 (next stage) | 35 | 40 | 34 (33 not cited in 17) | The three descriptions ride this part (3 more cards, no overlap): 59 messages, 71 ids, 51 distinct cards delivered. The next stage's exact list is the 35 toMajor-18 summaries at the end of this body. ## Delivered: each site, the decision read, the new words Every cited card was read through REST with all comments; the record column names the comment (or commit) the decision was read from. Where a summary already said why, the citation is dropped and the sentence kept. Placeholders `OBJECT_NAME` / `RULE_NAME` below stand for the angle-bracket spelling in the source. | conversion (head line) | cited | decision as read (record) | summary now reads | |---|---|---|---| | `action-execute-to-target` (:727) | objectstack-ai#3713 | `execute` is the deprecated alias of `target`; spec and objectui resolved the pair in opposite directions; align on the spec rule and drop the alias so the divergence is unrepresentable (body (closed completed, no comments)) | action key 'execute' → 'target' (the deprecated handler alias; the spec and the renderer had resolved the pair in opposite directions, so one key now names the handler) | | `field-conditionalRequired-to-requiredWhen` (:767) | objectstack-ai#3754 | same fold-and-drop as objectstack-ai#3713: `requiredWhen` canonical, alias folded and dropped from parsed output (body (closed completed, no comments)) | field key 'conditionalRequired' → 'requiredWhen' (the deprecated predicate alias, folded into the canonical key so no reader picks its own precedence) | | `agent-tools-to-skills` (:822) | objectstack-ai#3894 | ADR-0109 accepted; `agent.tools[]` removed because it resolved names against the full registry with no surface check, breaking ADR-0064 (tool set = union of skills' tools) (PR body (merged)) | agent key 'tools' removed — declare capability in a skill (ADR-0064: an agent's tools are exactly its skills' tools, and this inline slot resolved names against the whole registry with no surface check) | | `sharing-rule-access-level-full-to-edit` (:881) | objectstack-ai#3865 | route B is the end state: sharing grants read/edit only; delete, transfer and re-share come from object permissions, ownership and admin scope; `full` → `edit` is lossless (5105498900) | sharing-rule accessLevel 'full' → 'edit' (`full` never granted more than `edit`; a sharing rule grants read or edit, while delete and transfer come from object permissions and ownership) | | `flow-node-crud-object-alias` (:954) | objectstack-ai#3796 | the seven aliases (six open-coded `??` + the shim's last `object`) graduate straight into the D2 layer; the `readAliasedConfig` shim is deleted (5125152179) | CRUD flow-node config key 'object' → 'objectName' (the last alias in the executors' `readAliasedConfig` shim graduates into this layer, and the shim is deleted) | | `flow-node-notify-config-aliases` (:1077) | objectstack-ai#3796, objectstack-ai#4045 | objectstack-ai#3796: `??` fallbacks graduate, `actionUrl` canonical (downstream chain uses it). objectstack-ai#4045: `notify.source` was a read-but-undeclared shape → conversion layer, not configSchema (5125152179; 5127636357, 5138487536) | notify flow-node config keys 'to' → 'recipients', 'subject' → 'title', 'body' → 'message', 'url' → 'actionUrl' (executor `??` fallbacks graduated into this layer; `actionUrl` is canonical because the notification chain downstream already uses it), and nested 'source: {object, id}' → 'sourceObject' / 'sourceId' (a shape the executor read that no config schema declared) | | `flow-node-wait-event-config-lift` (:1288) | objectstack-ai#4045 | `node.config.eventType` etc. were an undeclared second contract beside the declared `waitEventConfig`; graduate them (objectstack-ai#4161) (5130277099, 5138487536) | wait flow-node loose config keys → the declared `waitEventConfig` block: 'eventType', 'timerDuration'/'duration' → 'timerDuration', 'signalName'/'signal' → 'signalName', 'timeoutMs' (the executor also read these keys from the loose config, a second contract beside the declared block) | | `flow-node-map-flow-alias` (:1364) | objectstack-ai#4045 | reconciliation found the `map.flow` alias (undeclared executor fallback); graduated (objectstack-ai#4228) (5138487536) | map flow-node config key 'flow' → 'flowName' (an undeclared spelling the executor accepted through a bare fallback; it graduates into this layer) | | `flow-node-subflow-flow-alias` (:1421) | objectstack-ai#4278 | reconcile the schemaless nodes' forms with their executors and check `subflow`; its bare `flowName ?? flow` fallback graduates (body (closed completed, no comments) + conversion docblock) | subflow flow-node config key 'flow' → 'flowName' (an undeclared spelling the executor accepted through a bare fallback, found when the schemaless nodes were reconciled with their executors; it graduates into this layer) | | `flow-node-connector-config-lift` (:1529) | objectstack-ai#4045 | executor reads only `connectorConfig`; the descriptor schema rooted the triple at `config`, so the Studio form wrote unread keys; descriptor stops publishing, stored loose keys lift (5132926517, 5138487536) | connector_action flow-node loose config keys 'connectorId' / 'actionId' / 'input' → the declared `connectorConfig` block (the executor reads only that block; the published designer form had been writing these keys where nothing read them) | | `flow-node-script-config-aliases` (:1641) | objectstack-ai#3796 | as above: open-coded `??` fallbacks graduate into the D2 layer (5125152179) | script flow-node config keys 'functionName' → 'function', 'input' → 'inputs' (executor `??` fallbacks, graduated into this layer) | | `app-dead-authoring-keys-removed` (:1742) | objectstack-ai#4001, objectstack-ai#4509, objectstack-ai#4667, objectstack-ai#4709 | liveness audits: keys unread or wrongly encoded are removed; objectstack-ai#4709: the "no shell read homePageId" premise was false, ruling B keeps the retirement (an ID cross-reference that dangles is the wrong encoding) (5158421901 (objectstack-ai#4509), 5159774792 (objectstack-ai#4667), 5164227920 (objectstack-ai#4709 ruling B)) | app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits found each one unread or wrongly encoded; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, homePageId WAS read by objectui's console before v17 but encoded the landing page as an ID cross-reference that silently fell back when it dangled — the landing page is the first nav item (the first retirement record said nothing read it, a premise since corrected; the retirement stands), and no renderer ever sorted areas) | | `app-area-fail-open-gates-removed` (:1853) | objectstack-ai#4651 | ruling B: remove both keys; removing a fail-open gate is strictly safer than keeping it; prescription names the two enforced layers (5160072589) | navigation-area keys 'visible'/'requiredPermissions' removed (ADR-0049 — FAIL-OPEN access gates: no layer ever read them, so a 'hidden' or permission-gated area was served and rendered to every user, while the identically named keys on a navigation ITEM and on the APP are enforced; gate the items inside the area, or gate the app) | | `permission-rls-priority-removed` (:1955) | objectstack-ai#3896 | the objectstack-ai#3896 security-audit line: rls.priority promised conflict resolution that cannot exist (policies OR-combine) and had no reader; removed (card body + commits d6bfb3d (objectstack-ai#3990), eb95d97 (objectstack-ai#3998)) | RLS-policy key 'priority' removed (a security audit found no reader: policies OR-combine, so the promised conflict-resolution semantics cannot exist; dropping it changes no outcome) | | `tool-inert-authoring-keys-removed` (:2024) | objectstack-ai#3896 | the audit close-out removes the four inert tool keys (permissions gated nothing, active:false withdrew nothing) (commit eb95d97 (objectstack-ai#3998)) | tool keys 'category'/'permissions'/'active'/'builtIn' removed (authorable and inert, so removed under ADR-0049 enforce-or-remove; permissions gated nothing, active:false withdrew nothing) | | `action-inert-keys-removed` (:2137) | objectstack-ai#3896 | close-out sweep: enforce-or-remove worklist, fourteen inert authoring keys leave the surface (commit 12a19a8 (objectstack-ai#4054)) | action keys 'shortcut'/'bulkEnabled' removed (inert, removed under ADR-0049 enforce-or-remove: no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions) | | `flow-inert-keys-removed` (:2162) | objectstack-ai#3896 | as above (commit 12a19a8 (objectstack-ai#4054)) | flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (inert, removed under ADR-0049 enforce-or-remove: active:false never stopped a flow; status is the enforced lifecycle) | | `view-inert-keys-removed` (:2221) | objectstack-ai#3896 | as above (commit 12a19a8 (objectstack-ai#4054)) | view keys removed as inert (ADR-0049 enforce-or-remove): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live) | | `view-list-passthrough-keys-removed` (:2267) | objectstack-ai#7176 | maintainer ruling: retire under ADR-0049; pass-through-only reads are dead in effect (5236139723) | view list keys removed: 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (a key that is only passed through is dead in effect; ADR-0049 enforce-or-remove) | | `view-export-options-pdf-removed` (:2335) | objectstack-ai#8010, objectstack-ai#1301 | option A; `pdf` leaves the enum (honest narrowing, not a runtime console.warn); PDF export declined (5270998514; objectstack-ai#1301 is not planned) | list-view export format 'pdf' removed (PDF export was declined as not planned, and ObjectGrid dropped the declared format from the menu with only a runtime console.warn; an honest enum replaces that warning) | | `dashboard-inert-keys-removed` (:2404) | objectstack-ai#3896 | close-out sweep (as above) (commit 12a19a8 (objectstack-ai#4054)) | dashboard keys 'aria'/'performance' and widget 'performance' removed (inert, removed under ADR-0049 enforce-or-remove: no renderer applied any of them) | | `dashboard-widget-responsive-removed` (:2474) | objectstack-ai#4876, objectstack-ai#11027 | objectstack-ai#4876 ruling A: retire widget `responsive` (no reader); objectstack-ai#11027 ruling B: retire `page.components[].responsive`, equally unread (5169512655; 5380752244) | dashboard widget key 'responsive' removed (no renderer ever applied per-widget breakpoint overrides; the page.components[].responsive key this entry once deferred to was measured equally unread and retired at protocol 18) | | `dashboard-widget-action-aria-removed` (:2555) | objectstack-ai#5010 | retire the four dead widget keys; colorVariant kept (body ruling + 5179556002) | dashboard widget keys 'actionUrl'/'actionType'/'actionIcon' and 'aria' removed (no renderer ever drew a per-widget action button, and widget ARIA attributes never reached the DOM; use header.actions[] and the widget title/description) | | `dashboard-widget-compareto-converged` (:2652) | objectstack-ai#5011 | converge `compareTo` on the implemented executor contract `{ kind, dimension? }`; `1y` rewrites, other offsets delegated (5173559485) | dashboard widget 'compareTo' converged on the executor's { kind, dimension? } contract (the shape the dataset executor implements; the bare strings and { offset: '1y' } rewrite mechanically; other { offset } durations have no faithful target and are reported, not guessed) | | `agent-knowledge-removed` (:2726) | objectstack-ai#3896 | close-out sweep (as above) (commit 12a19a8 (objectstack-ai#4054)) | agent key 'knowledge' removed (inert, removed under ADR-0049 enforce-or-remove: declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level) | | `skill-trigger-phrases-removed` (:2744) | objectstack-ai#3896 | close-out sweep (as above) (commit 12a19a8 (objectstack-ai#4054)) | skill key 'triggerPhrases' removed (inert, removed under ADR-0049 enforce-or-remove: activation is triggerConditions + the agent's skills[] allowlist; phrases were a dead-end projection) | | `stack-api-require-auth-removed` (:2782) | objectstack-ai#3963 | delete the `api.requireAuth` opt-out; anonymous always denied; public surfaces derive authorization from a declaration (form, share link, book audience) (body (decision recorded in body)) | stack key 'api.requireAuth' removed — anonymous access is always denied; publish public surfaces by declaration (a public form, a share link or `book.audience: 'public'`), which replaced the deployment-wide opt-out | | `flow-node-wait-timeout-keys-removed` (:2864) | objectstack-ai#4158 | wait never had a timeout: withdraw the contract (route B), `timeoutMs` moves to `timerDuration` (body (closed completed) + conversion docblock) | waitEventConfig keys 'timeoutMs' (→ 'timerDuration', stringified — its only reader used it as the duration) and 'onTimeout' (removed — zero readers, so no timeout ever fired): wait never had a timeout, so its timeout contract is withdrawn rather than built | | `datasource-inert-blocks-removed` (:2944) | objectstack-ai#4583 | all 20 dead datasource keys removed; each job already has a different live mechanism (5157934690) | datasource keys 'retryPolicy'/'healthCheck' and external 'label'/'requirePermission' removed (nothing retried, nothing probed on a schedule, and the federation label/permission were read by nobody; each of those jobs already has a live mechanism) | | `mapping-inert-keys-removed` (:3032) | objectstack-ai#4509 | the three mapping keys retire (schema defaults made authorWarn impossible; removal is the only signal) (5158421901, 5158744185) | mapping keys 'extractQuery'/'errorPolicy'/'batchSize' removed (no exporter reads a mapping, error handling belongs to the import request, and the write path sizes its own batches) | | `book-translations-removed` (:3087) | objectstack-ai#4667 | six dead authorWarn keys retired (5159774792) | book keys 'translations' (book-level and group-level) removed (no resolver read them; the tree endpoint and portal render labels verbatim, so a localized book served its authoring locale to everyone). Localize the docs instead: `doc.translations` is live | | `job-id-removed` (:3149) | objectstack-ai#4667 | as above (5159774792) | job key 'id' removed (nothing read it; `name` is the job's identity everywhere, so two jobs differing only in `id` were the same job, and the key's own description advertised an override that did not exist) | | `translation-validation-messages-removed` (:3206) | objectstack-ai#4667, objectstack-ai#3778, objectstack-ai#14381 | objectstack-ai#4667 retire; objectstack-ai#3778 legacy-key table had pointed `errors` at it; objectstack-ai#14381 an object-scoped key ships with its reader (ADR-0049 enforced) (5159774792; objectstack-ai#3778 body; 5503980929) | translation key 'validationMessages' removed (no resolver read it, so a translated rule message was stored and never shown; the legacy-key table of the translation-bundle migration had been steering retired `errors:` authors into it). Author the message on the rule itself (`object.validations[].message`), and translate it under the object-scoped group `objects.OBJECT_NAME._validations.RULE_NAME.message`, which the write path resolves (17.3.0, a translation key shipped together with its reader) | | `datasource-capabilities-removed` (:3264) | objectstack-ai#4583 | as above (5157934690) | datasource key 'capabilities' removed (eleven flags no code read; pushdown comes from the driver's own supports.*, and `readOnly` never made anything read-only) | | `datasource-read-replicas-removed` (:3319) | objectstack-ai#4468 | remove: read-replica routing is an unbuilt feature (5150771330) | datasource key 'readReplicas' removed (no driver opened a replica connection and no query path splits reads from writes; front replicas behind one endpoint and point `config` at it) | | `datasource-config-driver-key-aliases` (:3419) | objectstack-ai#4456 | the factory's undeclared `??` fallbacks graduate to a D2 entry and are deleted from the reader (5157922838) | datasource config keys → canonical per driver: sqlite 'file'/'database' → 'filename', postgres/mysql 'connectionString' → 'url' and 'user' → 'username', mongo 'uri' → 'url' and 'user' → 'username' (undeclared driver-factory `??` fallbacks, graduated into this layer and deleted from the reader) | | `flow-node-script-branch-keys-removed` (:3665) | objectstack-ai#4343 | operator ruling: `script` converges to a pure function-call node; the five branch keys retire (5151704360) | script flow-node config keys 'actionType' (→ 'function' when it was shorthand for one; otherwise removed — 'email'/'slack' were logger-backed stubs that delivered nothing), plus 'template' / 'recipients' / 'variables' (fed those stubs) and 'script' (inline JS the runtime never executed); script is now a pure function-call node, the only path that ran real logic | | `object-managed-by-system-to-system-data` (:3762) | objectstack-ai#3355 | retire `system`, new value `system-data` (not `platform-data`) (5157022965) | object managedBy 'system' → 'system-data' (ADR-0103's residual bucket named the engine-owned half v16 had already moved out to `engine-owned`; the rename leaves the name describing what the bucket actually holds: admin/user-writable platform data) | | `object-enable-trash-mru-removed` (:3829) | objectstack-ai#3207, objectstack-ai#2377 | remove `enable.trash` / `enable.mru`; soft delete stays parked; last slice of the dead-property removals (5156966571, 5161298967; 5051634768) | object capability flags 'enable.trash'/'enable.mru' removed (the last slice of the dead author-facing property removals: no recycle bin and no MRU tracking ever ran; both default-true flags gated nothing) | | `object-index-type-partial-removed` (:3912) | objectstack-ai#5248, objectstack-ai#4943 | remove both index keys; no DDL consumer; return enforce-first on real demand (5199336983; 5194762679 (duplicate)) | object index keys 'indexes[].type'/'indexes[].partial' removed (no driver ever read either: the index method is the dialect's choice and a partial index is built by a database-layer migration, not declared) | | `retry-policy-converged` (:4070) | objectstack-ai#4661, objectstack-ai#4964 | one RetryPolicy declaration, `backoffMs`, merged default 0 / 1 with pre-17 job defaults written out; flow.errorHandling joins, default 0 (silent retry can double-write) (5158710540 (analysis); 5173148383) | retry policy unified across job.retryPolicy, try_catch retry and flow.errorHandling: base delay 'retryDelayMs' → 'backoffMs', and the pre-17 job defaults (maxRetries 3, backoffMultiplier 2) written out explicitly now that the merged default is 0 / 1: two declarations that differed only by accident became one, and retry is opt-in because a retry replays whatever the attempt already did | | `hook-body-crypto-hash-removed` (:4249) | objectstack-ai#4391 | remove the capability token and its build-time inference (5156969500) | script-body capability token 'crypto.hash' removed (the sandbox never installed ctx.crypto.hash, so the token granted a call that always threw; the CLI inferred it too) | | `dataset-measure-array-string-agg-removed` (:4419) | objectstack-ai#6188 | retire `array_agg` / `string_agg`; keep and enforce `count_distinct` (5219849918) | dataset measure aggregates 'array_agg' / 'string_agg' removed (no SQL backend compiled them and the v1 dataset runtime refused them by name, so a measure declaring one never produced a value; the measure is dropped, and with it any derived measure left referencing it) | | `connector-rate-limit-config-removed` (:4544) | objectstack-ai#4911 | outbound rate-limit vocabulary removed: no engine exists (implementation-first) (body ruling + 5169405647) | connector key 'rateLimitConfig' removed (no outbound rate-limiting engine exists; the runtime's only token bucket limits INBOUND requests, so every knob here was inert while reading like a configured cap. The whole ConnectorRateLimitConfig shape went with it) | | `field-mapping-transform-removed` (:4669) | objectstack-ai#5552, objectstack-ai#3278 | enforce-or-remove: all five members dead, the union retires; `js` dialect was retired as redundant with the L2 script body (5199338349; objectstack-ai#3278 body) | field-mapping key 'transform' removed (the whole five-member FieldMappingTransform union went with it: no runtime ever executed constant/cast/lookup/javascript/map, and the javascript member advertised dialect="js", a dialect already retired because JavaScript belongs in a script body. The enforced transform pipeline is the import mapping's string-enum `mapping.fieldMapping[].transform`, which is unaffected) | | `theme-inert-token-scales-removed` (:4786) | objectstack-ai#5021 | retire all nine token groups; re-declare under `customVars` (5175091297) | theme keys 'typography.fontSize'/'fontWeight'/'lineHeight'/'letterSpacing', 'typography.fontFamily.heading'/'mono', 'animation' and 'zIndex' removed (ADR-0049 — the engine emitted --font-size-*, --font-weight-*, --line-height-*, --letter-spacing-*, --duration-*, --timing-*, --z-*, --font-heading and --font-mono faithfully, and no first-party component or stylesheet has ever read one. Re-declare any variable you actually consume under customVars, which emits it verbatim) | | `page-header-subtitle-alias` (:4940) | objectstack-ai#3226 | route B: a D2 conversion rewrites `description` → `subtitle`; the consumer's bare `??` retires (5160118898, 5194297145) | page-header component prop 'description' → 'subtitle' (the off-spec spelling a renderer tolerated through a bare `subtitle ?? description` fallback; `subtitle` is the declared key, and the fallback retires) | | `record-picker-display-field-to-label-field` (:5165) | objectstack-ai#5775 | direction A: `labelField` (the delivered spelling) becomes canonical; `displayField` retires via conversion (5202137085) | record-picker component prop 'displayField' → 'labelField' (the required key no renderer read; `labelField ?? 'name'` is what renders the row, so the delivered spelling became the declared one) | | `record-picker-inert-keys-removed` (:5287) | objectstack-ai#5775 | `searchFields` / `multiple` retire (zero readers) (5202137085) | record-picker component props 'searchFields'/'multiple' removed (the control is a plain single-select with no search box; neither key had a reader) | | `page-card-body-to-children` (:5414) | objectstack-ai#5775 | `children` is the one composition key (5202137085) | page:card component prop 'body' → 'children' (one composition key across every container; the card renderer already reads both) | | `inline-action-api-params-to-body-extra` (:5582) | objectstack-ai#5777 | direction A: the static payload gets its own key (`bodyExtra`); `params` keeps one meaning (5202138112, 5228796853) | inline type:'api' action prop 'params' (object form) → 'bodyExtra' (a static payload and a parameter definition are two things, so the payload gets its own key; `params` stays the ActionParam[] definition array) | | `page-tabs-type-to-tab-style` (:5847) | objectstack-ai#6776 | Route A: rename to the spelling the renderer reads (5229120747, 5229693342) | page:tabs component prop 'type' → 'tabStyle' (a props key named `type` collides with the node's dispatch key and is unauthorable in flat/JSX carriers; `tabStyle` is the spelling the renderer reads in all of them) | | `page-structure-inert-keys-removed` (:6035) | objectstack-ai#6946 | retire three zero-reader UI keys (body (maintainer ruling) + 5232767409) | page:header prop 'icon' and page:card prop 'actions' removed (neither has a renderer read point in objectui; the header resolves icons per action and the card renders title/children/footer only) | | `record-details-layout-removed` (:6201) | objectstack-ai#6946 | as above (as above) | record:details component prop 'layout' removed (the declared auto\|custom modes were never implemented; the renderer branches only on inline\|compact, values the schema never permitted, so both legal values selected nothing) | | `app-hidden-to-unpublished` (:6343) | objectstack-ai#4829 | A1: a machine-managed key carries the publish gate; `hidden` back to navigation presentation only; ADR-0045 amended (5173161521) | stored app publish gate 'hidden' → '_unpublished' (ADR-0045 amended — `hidden` carried BOTH the publish gate and 'keep out of the App Switcher', so the built-in Account app was withheld from every non-builder; the gate is now the machine-managed `_unpublished`, and `hidden` is navigation presentation only, never an access gate. Stored rows only — an authored `hidden: true` is left untouched) | | `action-global-nav-location-removed` (:6456) | objectstack-ai#6888 | direction 2: retire `global_nav` (no demand; the designer previewed a surface the product lacks) (5229990375) | action location 'global_nav' removed (no running-app surface rendered it; the ⌘K palette reads no action metadata, while the Studio designer previewed a command-palette frame for it. The value is stripped and the key kept, so an action left with no location becomes the documented headless shape `locations: []`) | Descriptions: | site | cited | decision as read (record) | change | |---|---|---|---| | `data/field.zod.ts` `autonumberFormat` | objectstack-ai#6555 | route 3: `{0000}` is a declared contract default, and both hand-written fallbacks read it (5225535766, family done 5240072006) | "⇒ the contract default `{0000}`, which every driver and the engine fallback read, so one field numbers alike on every backend." | | `api/plugin-rest-api.zod.ts` `GET /:type/:name/layers` | objectstack-ai#5882 | ruling B: its own `/layers` path and schema, one route one shape (recorded 5216370790) | "…hence its own path and its own response schema, since one route answers one shape." | | `api/plugin-rest-api.zod.ts` `POST /:type/:name/publish` | objectstack-ai#7294 | declare the served publish route's response, the save door's discipline (5237410722) | "The route was served for a long time with no declaration behind it — this entry is what makes its response contract nameable, the same declared-equals-returned rule the save door follows." | ## Text-only proof (A4) Stage 3's AST-skeleton + string-text tool (`skeleton.cjs`, one line changed: the TypeScript 6.0.3 load path), BASE `1ac7308d7a` against this branch: **3 of 3 SAME** on both legs, exit 0 each — `registry.ts` 62346 tokens, 4920 string groups, 56 changed; `field.zod.ts` 8682 / 581 / 1; `plugin-rest-api.zod.ts` 4912 / 475 / 2 (one literal re-split into three `+` pieces, which the skeleton reads as one string); parse diagnostics 0 / 0. Every changed group carried an id before and carries none after; every other string is byte-identical. Controls on scratch copies of the head `registry.ts`, each mutation counted on disk first: an identifier rename → DIFF exit 1; `!==` flipped to `===` → DIFF exit 1; one summary re-split into two `+` operands → SAME exit 0; a `surface` string (never carried an id) changed → text leg VIOLATION exit 1. No repo file was mutated for the controls. Census after the edit (stage 3's instrument, unchanged): non-test 219 → 160 messages, 429 → 358 ids; class (b) conversion summaries 91 / 108 → 35 / 40 (all toMajor 18); descriptions 3 / 3 → 0. ## Pins and quotes (A6) - **Tests:** no test asserts a changed summary or description phrase. The id-bearing fragments and the distinctive phrases of all 59 messages were searched across every `*.test.*` / `*.spec.*`; the hits are other files' own prose with their own citations (test titles and comments such as `(objectstack-ai#3896 close-out)` in `view.test.ts`), not quotes of a summary. - **`content/docs/**`:** the only quotes are the three generated `references/**` pages, regenerated. `content/docs/releases/v17/17-0.mdx:1052` repeats one action-key phrase with its own `(objectstack-ai#3896 close-out)`; it is release-owned and untouched. - **`skills/**`:** no quote of any changed text. ## Gates `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at `db81973cf6` (9 paths, 493 changed lines): 105 commands, each run from the worktree with its exit code written before any pipe; `--ran` → "✓ dispatch-gates --ran: 105 derived famil(ies) accounted for — 105 run, 0 NOT-MEASURED". Three first exited 3 (prerequisite: unbuilt lint / client packages) and were re-run green after the full package build (71 tasks). Plus `@objectstack/spec` build, `check:generated` ("All 15 generated artifacts are up to date"), the package `test` project (603 files, 17845 passed), 48 of the 51 `test:repo` files, and `typecheck`. Details are in the report on the card. ## Acceptance notes - **The `objectstack-ai#3896` citations.** Eight summaries cited `objectstack-ai#3896` as an "audit" or a "close-out". The card's own body is the sharing-rule REST finding that opened that security-audit line; the decisions the summaries cited (remove `rls.priority`, the four inert tool keys, then the fourteen-key enforce-or-remove sweep) are recorded in the landed commits `d6bfb3d0ab`, `eb95d97c02` and `12a19a88a5`. Each summary already said why its key went, so the new words name the rule (ADR-0049 enforce-or-remove) rather than the card. - Comments in `conversions/registry.ts` still carry tracker ids; they belong to objectstack-ai#20234's comment stages and are untouched. So is `migrations/registry.ts`. - `docs/protocol-upgrade-guide.md` is not a governed surface (`check-governed-merges`' register). - **Hot file:** open PR objectstack-ai#21547 (objectstack-ai#21459) also edits `conversions/registry.ts`: it adds one toMajor-18 entry and its ordering row, in a region this PR does not touch. A local `git merge-tree` of the two heads is clean (the file is hand-written, so no merge driver is involved). Neither PR's spec-changes / upgrade-guide output includes the other's entries, so whichever lands second merges `main` and regenerates them. ## Next stage: the 35 toMajor-18 summaries (file:line at this head · ids · conversion) - `registry.ts:6550` objectstack-ai#8321 `field-malformed-scale-precision-removed` - `registry.ts:6658` objectstack-ai#8762 `record-chatter-position-vocabulary` - `registry.ts:6777` objectstack-ai#9198 `element-input-target-variable-removed` - `registry.ts:7024` objectstack-ai#9220 `element-filter-removed` - `registry.ts:7177` objectstack-ai#9249 `element-form-removed` - `registry.ts:7355` objectstack-ai#15178,objectstack-ai#19620 `translation-per-app-settings-removed` - `registry.ts:7609` objectstack-ai#9249 `translation-component-submit-label-removed` - `registry.ts:7782` objectstack-ai#3951,objectstack-ai#9227 `field-column-lists-canonicalized` - `registry.ts:7909` objectstack-ai#10414 `metric-filters-removed` - `registry.ts:8104` objectstack-ai#17296 `cube-sub-day-granularities-removed` - `registry.ts:8237` objectstack-ai#18612 `cube-join-sql-and-relationship-removed` - `registry.ts:8502` objectstack-ai#10054 `record-highlights-field-icon-removed` - `registry.ts:8759` objectstack-ai#11027 `page-component-responsive-removed` - `registry.ts:8860` objectstack-ai#11805 `object-grid-default-sort-removed` - `registry.ts:9047` objectstack-ai#21445 `object-grid-resizable-columns-removed` - `registry.ts:9250` objectstack-ai#17260 `object-kanban-quick-add-removed` - `registry.ts:9563` objectstack-ai#12497,objectstack-ai#1883 `permission-allow-restore-purge-removed` - `registry.ts:9881` objectstack-ai#6837 `field-reference-to-alias` - `registry.ts:10301` objectstack-ai#14478 `hook-timeout-to-timeout-ms` - `registry.ts:10342` objectstack-ai#14478 `job-timeout-to-timeout-ms` - `registry.ts:10946` objectstack-ai#14478 `api-endpoint-cache-ttl-to-cache-ttl-seconds` - `registry.ts:11015` objectstack-ai#14478 `dashboard-refresh-interval-to-refresh-interval-seconds` - `registry.ts:11376` objectstack-ai#14478 `memory-persistence-auto-save-interval-to-ms` - `registry.ts:11600` objectstack-ai#14478 `turso-config-timeout-to-timeout-ms` - `registry.ts:11690` objectstack-ai#17063 `view-page-mount-removed` - `registry.ts:11795` objectstack-ai#17053,objectstack-ai#8221 `list-view-sort-string-clause-to-array` - `registry.ts:12295` objectstack-ai#19054 `object-tenancy-organization-field-removed` - `registry.ts:12405` objectstack-ai#20085 `view-item-owner-hidden-removed` - `registry.ts:12549` objectstack-ai#20230 `view-overlay-owner-hidden-removed` - `registry.ts:13137` objectstack-ai#6206,objectstack-ai#17321 `page-component-filter-record-to-rule-array` - `registry.ts:13392` objectstack-ai#20161 `report-joined-chart-removed` - `registry.ts:13657` objectstack-ai#20221 `form-layout-inline-grid-to-vertical` - `registry.ts:13813` objectstack-ai#19992 `currency-config-precision-removed` - `registry.ts:13917` objectstack-ai#20321 `permission-rls-tags-removed` - `registry.ts:14056` objectstack-ai#15429 `flow-decision-mode-inclusive-explicit` --- _Generated by [Claude Code](https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#4045 的
wait一半。另一半(超时契约压根没实现)单开为 #4158,刻意没有塞进本 PR。先纠正我自己在 #4045 上说错的判断
我一度把
wait报成「没有configSchema,所以设计器配不了它」。那是错的,已在 issue 上更正。wait的契约不在config里,而在FlowNodeSchema.waitEventConfig(flow.zod.ts:199)—— 每个属性都有.describe()、在可授权字段清单里、进了生成参考文档、showcase 也确实这么写。描述符不带configSchema是设计如此。站得住的那一半
执行器另外读了 6 个 loose
config键,其中duration和signal是 spec 从未声明过的拼法。这就是 #4050 退役掉的notify.source形状:一份只由代码注释宣告的第二契约,写了它的作者会得到一个永远能跑、也永远不会被引导到声明拼法的流程(PD #12)。而且它不是假想的 —— 见下方第二处更正。
做法
新增 ADR-0087 D2 转换
flow-node-wait-event-config-lift,把 6 个 loose 键提升到声明块,优先级严格镜像被删掉的??链 —— 已声明的值胜出,其 loose 对应物留在原地(和renameConfigKey对被遮蔽别名的处理一致)。一个会把存量流程写坏的陷阱
engine.ts:1465是applyConversionsToFlow(...)紧接FlowSchema.parse(converted)—— 加载器解析的是转换后的流程。而waitEventConfig.eventType没有.optional():所以一个只写了
config: { duration: 'PT1M' }的存量流程,若提升后不带eventType,会从能跑变成加载失败。转换因此补上'timer'—— 执行器对这个形状原本的默认值。这不是收尾修饰,是这个改动能不能安全上线的关键。?? 'timer'在执行器里保留了,并在注释里说明它不是同类兜底:waitEventConfig本身可选,没有它的 wait 节点是合法的定时等待。第二处更正:
4450fba第一版 commit 里我写了 "Nothing in-repo authors it"。错的。 showcase 自己的
wait_revision节点写的正是这个形状:所以这道后门不是假想的,而且演示
wait的那个示例本身就在用被退役的拼法。它已改为waitEventConfig。转换本来就正确处理了这个形状(执行器行为两边一致),新增的测试钉住了 showcase 撞上的那个精确组合:声明的键名出现在未声明的位置 —— 这正是候选顺序必须处理对的地方(
signalName先于signal),并断言转换后的流程仍能 parse。这个错误是在排查一个卡住的
objectstack verifyCI 步骤时发现的 —— 排查结论是那不是本改动引起的,但排查过程本身翻出了这句假陈述。两份清单,第二份我第一版漏了
这个层维护两份注册:
CONVERSIONS_BY_MAJOR[17]和step17.conversionIds(链式回放走后者)。我第一版只加了前者,被仓库自己的棘轮抓住 ——migrations.test.ts把每个转换的 fixture 都过一遍链,未接线的转换会在那里失败,而不是静默地失效上线。顺带一提:这本身又是一个「两份手写清单」实例(#3786 那个模式),但这一个有闸门,所以它的表现正如设计。
验证
wait-node.test.tscheck:spec-changes/check:upgrade-guide)pnpm --filter @objectstack/spec exec tsc --noEmit(CI 的 typecheck 门禁)两处负控都做实了:
eventType——FlowSchema.parse抛错,证明那个默认值是承重的。新的执行器测试走
registerFlow(也就是真正施加转换的那个 seam),所以它们证明的是存量源端到端可用,不只是「执行器不再去看 config 了」。生成物
spec-changes.json和docs/protocol-upgrade-guide.md已重生成 —— 迁移步骤的rationale是生成物输入(该字段被声明为「the one place prose is load-bearing」,所以我也扩写了它)。两道闸门都是先报 FAIL 再修好的,不是猜的。一条存量情况,非本 PR 造成
packages/services/service-automation里src/engine.test.ts有 2 个类型错误(缺resumeAuthority,#3951 加该字段时带.default留下的)。我的 diff 里resumeAuthority出现 0 次,也不含engine.test.ts;而且 CI 的 typecheck 只覆盖@objectstack/spec,所以这两个错误既不是我引入的、也不在门禁内。记录在此,未在本 PR 处理。🤖 Generated with Claude Code
https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq