Skip to content

feat(spec)!: refuse the wait / connector_action loose config keys naming their sibling block; their D2 lifts leave the load path (F3 of #23543) - #23742

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23543-retire-wait-connector-config-lift
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23543-retire-wait-connector-config-lift

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Part of #23543
Clause-②: yes (narrowing)

This PR lands family F3 of ruling 6182001882 (item 3) only. F2, F4 and F5-http stay on #23543, one PR each, in the ruled order. #23543 remains open.

What

Ruling F3 A: the named loose keys under a wait or connector_action node's config are refused with the prescription naming the sibling block; the lift deletes a loose key the block shadows at rehydration, so stored flows keep loading; both entries set retiredFromLoadPath.

  • One key table, two readers. A new leaf module, packages/spec/src/automation/flow-sibling-block-loose-keys.ts (not on the automation barrel, so no public export moves), declares WAIT_EVENT_CONFIG_LOOSE_KEYS and CONNECTOR_CONFIG_LOOSE_KEYS. These are the tables the two lifts in conversions/registry.ts used to declare themselves, moved byte for byte. The registry now reads them from the leaf, so the keys refused at authoring and the keys lifted at rehydration are one set by construction.
  • Authoring refusal. siblingBlockLooseConfigRefusals runs in the FlowSchema superRefine's collectFlowGraphs walk, beside connectorActionConfigRefusals, so a node inside a region body is reached too. Each named loose key is one custom issue at nodes.N.config.KEY: "A wait node's config.duration was removed in @objectstack/spec 18 (ADR-0087 D2) … Move it to waitEventConfig.timerDuration …", followed by the house os migrate meta --from 16 sentence. config.timeoutMs gets its own move: it goes to waitEventConfig.timerDuration as a string, because waitEventConfig.timeoutMs is itself a tombstone. The refusal applies only to these keys: eventType, timerDuration, duration, signalName, signal and timeoutMs on wait; connectorId, actionId and input on connector_action. Any other config key is judged as before. At defineStack this surfaces as STACK_SCHEMA_INVALID / 422.
  • The lifts at rehydration. Precedence is unchanged: the block wins, and the first loose candidate present decides. What is new is that a loose key that does not lift is DELETED, with its own notice, instead of being left in place. That covers a key the block shadows, a later candidate shadowed by the first one present, and a null. None of these was ever read. Kept, the flow parse would now refuse them, and the stored row would stop loading. The connector completeness guard is unchanged: a node whose pair cannot complete is left untouched and refused, as before.
  • The D2 entries. flow-node-wait-event-config-lift and flow-node-connector-config-lift get retiredFromLoadPath: true and retiredAfter: '17.7.0', the stamp PR feat(spec)!: retire seven overdue ADR-0087 D2 conversions from the load path at 18; record why eight stay #23523, PR feat(spec)!: retire the datasource driver spelling mongo — refused naming mongodb, its D2 conversion leaves the load path (F1 of #23543) #23600 and PR feat(spec)!: retire the page kind jsx — refused naming html, its D2 conversion leaves the load path (F5-jsx of #23543) #23638 used. Each "stays on the load path — a recorded verdict" paragraph is rewritten as the retirement record. Both fixtures now show the deleted shadowed key, and expectedNotices goes from 4 to 5 on each.
  • The D3 entry. 18.flow-sibling-block-loose-config-keys-refused (semantic, step 18) carries conversionIds: ['flow-node-wait-event-config-lift', 'flow-node-connector-config-lift'], both already members of step 17.
  • The verdict-list pin in conversions.test.ts moves both entries to the retired list. The kept-without-ruling list now holds four entries: flow-node-http-callout-rename, page-component-visibility-to-visibleWhen, retry-policy-converged and view-visibleOn-to-visibleWhen. The removal-version pin's JOIN gains one row per entry.

Mechanism assumptions, measured

  1. The refused key set is the set the lifts move. It holds. The two tables were WAIT_EVENT_CONFIG_LIFTS (eventType; timerDuration/duration; signalName/signal; timeoutMs) and CONNECTOR_CONFIG_LIFTS (connectorId/actionId/input) in registry.ts. They now live once, in the leaf, and both readers import it. The pin refuses every named loose key and no other asserts the six wait keys exactly. A non-lifted key (note) is not refused, and the same keys under another node type are not this rule's.
  2. Where the delete goes. It holds as assumed. The seams that replay retired entries are the automation engine's registerFlow canonicalizer (applyConversionsToFlow(…, { includeRetired: true }), engine.ts), stored-row rehydration, and the metadata save door's flow verdict (protocol.ts, full chain). So the delete lives in the replayed lift. Pinned in conversions.test.ts under the sibling-block lifts are retired (#23543 F3), for each node type:
    • A loose-only stored row and a block-plus-shadowed-key stored row are each refused by FlowSchema as written.
    • After the seam's replay, each parses and carries the block alone.
    • End to end through registerFlow, connector-nodes.test.ts has a new shadowed-input case: the node's config comes back {} and the action receives the block's input. The case in wait-node.test.ts that reads the declared value asserts the deleted loose key the same way.
  3. Readers. The two executors already read only their blocks: wait-node.ts reads waitEventConfig and connector-nodes.ts reads connectorConfig. Neither has a loose-key read to delete. Each had a comment saying the shape is "rewritten at load", which now says the authoring parse refuses it and the rehydration seam rewrites a stored row. connector-action-config-required.test.ts had one case naming the lift. It now asserts three issues: the missing block, plus one at each loose key, each naming its connectorConfig key.
  4. The os migrate meta load-path suites. None has F3 as its subject. The cli migrate-meta-load-conversions, migrate-meta-composed-load-conversions and migrate-meta-composed (load-path case), and the spec's stack-authored-input, all drive retry-policy-converged (a flow-level errorHandling.retryDelayMs). PR feat(spec)!: retire the datasource driver spelling mongo — refused naming mongodb, its D2 conversion leaves the load path (F1 of #23543) #23600 re-pointed them to that entry. They stay unchanged, and they pass on this tree (cli unit tier, 3 files / 18 tests). F2 retires that entry, so F2 is the family that inherits the "no live subject left" question PR feat(spec)!: retire the datasource driver spelling mongo — refused naming mongodb, its D2 conversion leaves the load path (F1 of #23543) #23600 recorded.
  5. Generated outputs. None changed. check:generated read "All 14 generated artifacts are up to date" before and after the main merge. No export, .describe() or authorable key moved: the refusal is a superRefine issue.

Fixture triage (pin sweep: type: 'wait' / 'connector_action' with a config carrying a named key, whole repo, single- and multi-line)

  • Re-spelled to the block. These used the retired location without testing the lift: lint validate-expressions.flow-cel-root.test.ts (two rows), lint lint-flow-patterns.test.ts (legacyWaitFlow, and the loop-body connector row, whose action key became actionId), and service-automation run-summary.test.ts (three connector rows).
  • Rewritten. The lift cases in conversions.test.ts now replay includeRetired (the stored-row seam). The two cases that pinned "the shadowed key is left in place" now pin its deletion. New: a connector describe and the retirement describe.
  • Unchanged on purpose:
    • connector-nodes.test.ts and wait-node.test.ts still register the loose shape through registerFlow, which is the stored-row seam. Their comments now say so.
    • lint-flow-patterns.test.ts's config: { durationMs } wait row uses no lifted key.
  • Docs that went incomplete. content/docs/automation/flows.mdx: the connectorConfig and waitEventConfig rows now say the same keys under config are refused.

Changeset

.changeset/23543-f3-retire-wait-connector-config-lift.md:

  • Bumps: @objectstack/spec minor; @objectstack/service-automation patch (comment-only executor changes).
  • It is BREAKING and carries Clause-②: yes (narrowing). It names each refused spelling and the block key it moves to, the timeoutMs special case, and how stored rows keep loading (the shadowed key deleted at rehydration).
  • ADR-0087 disposition: registered flow-sibling-block-loose-config-keys-refused.

Verification

Head 23bebd66 merges origin/main 3b46855f (it carries #23418's registry.ts.template change) through scripts/pm/os-regen-merge.sh. There were no conflicts and the template equals main's. After pnpm install --frozen-lockfile and a spec rebuild, check:generated read "All 14 generated artifacts are up to date". The branch's delta against origin/main is exactly this PR's 16 files.

  • After the merge, on 23bebd66: spec src/conversions, src/automation, src/migrations, src/shared and src/stack*: 113 files / 3883 passed.
  • Before the merge, on cb5a4f9f:
    • @objectstack/spec: full test, 679 files / 20770 passed (1 todo). typecheck exit 0.
    • @objectstack/service-automation: full test, 186 files / 2487 passed. typecheck exit 0.
    • @objectstack/lint: the two edited files, 2 / 305 passed.
    • @objectstack/cli: unit tier, the three migrate-meta load-path suites, 3 / 18 passed. The full unit and integration tiers are declared to CI, since this diff touches no cli file.
  • Gates. dispatch-gates.mjs --commands derived 111 commands. Each was run one by one, with its exit code captured before any pipe. --ran reports "111 derived famil(ies) accounted for — 107 run, 4 NOT-MEASURED (3 DERIVED from a recorded exit 3, 1 claimed)".
    • The three exit-3 prerequisites: check-plugin-teardown-shape --self-test (shallow clone), check:skill-examples (client dist absent) and check:dual-build-cjs-loads (full build absent).
    • The claimed one: check:dts-closure exited 1 because my local dependency closure was built with OS_SKIP_DTS=1, so no package except spec carries a .d.ts here. That is an environment state, not a finding, and CI builds the full set.
    • Notable greens: check-adr-0087-registration --base origin/main, check-changeset-no-major, check-empty-changeset, check:nul-bytes.
    • The union is being re-run on 23bebd66; the report comment on spec(conversions): eight ADR-0087 D2 conversions stay on the 18 load path after #23487 — five families each need a ruling before they can retire #23543 carries that reading.
  • Ablation 1, the refusal (scripts/ablation-replace.mjs). The walk's call for (const refusal of siblingBlockLooseConfigRefusals(node)) was replaced by an empty array.
    • On-disk proof: anchor 1 → 0, blob f69316ecce7b → 95cf82982a02.
    • Result: 9 tests went red across the three files (conversions.test.ts, retired-key-removal-version.pin.test.ts with both JOIN rows, and connector-action-config-required.test.ts). Direction: red.
    • Restore proven: blob == HEAD and git diff HEAD empty.
  • Ablation 2, the shadow delete. The connector lift's drop branch was made to re-insert the loose key.
    • On-disk proof: anchor 1 → 0, blob 940997a39a4e → 9450deebd49e.
    • Result: 3 tests went red (the fixture harness's notice count, the shadowed-key case, and the stored-row-loads case). Direction: red.
    • Restore proven: blob == HEAD and git diff HEAD empty.
    • Spec tests import src, so neither ablation has a dist leg.

Acceptance notes

  • objectui (not touched here). The ruling names no objectui half for F3. The premise readings on the card found Studio authoring wait and connector_action through the declared blocks, and the connector descriptor has not published a config schema since the lift landed. No objectui reader was re-measured in this PR.
  • The missing-block and loose-key refusals stack. A connector_action node with only config.connectorId / config.actionId now gets three issues: the missing connectorConfig block, plus one at each loose key. Each issue is true and located.

Generated by Claude Code

…eir D2 lifts leave the load path (F3)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZyGVR3TdcTDLhuzyEbkBN
…ts and the shadowed-key delete

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZyGVR3TdcTDLhuzyEbkBN
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZyGVR3TdcTDLhuzyEbkBN
…d loose config location; docs: name the refusal

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/spec, touching 27 documentable anchor(s).

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/connectors.mdx (via actionId (literal, a string literal in CONNECTOR_CONFIG_LIFTS; a string literal in CONNECTOR_CONFIG_LOOSE_KEYS), connectorConfig (literal, a string literal in siblingBlockLooseConfigRefusals), connectorId (literal, a string literal in CONNECTOR_CONFIG_LIFTS; a string literal in CONNECTOR_CONFIG_LOOSE_KEYS), connector_action (literal, a string literal in looseKeyTargets))
  • content/docs/automation/flows.mdx (via FlowSchema (symbol, a top-level const), actionId (literal, a string literal in CONNECTOR_CONFIG_LIFTS; a string literal in CONNECTOR_CONFIG_LOOSE_KEYS), connectorConfig (literal, a string literal in siblingBlockLooseConfigRefusals), connectorId (literal, a string literal in CONNECTOR_CONFIG_LIFTS; a string literal in CONNECTOR_CONFIG_LOOSE_KEYS), connector_action (literal, a string literal in looseKeyTargets), eventType (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS), signalName (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS), timeoutMs (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS; a string literal in looseKeyMove), timerDuration (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS), waitEventConfig (literal, a string literal in looseKeyMove; a string literal in siblingBlockLooseConfigRefusals))
  • content/docs/automation/hook-bodies.mdx (via timeoutMs (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS; a string literal in looseKeyMove))
  • content/docs/automation/jobs.mdx (via timeoutMs (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS; a string literal in looseKeyMove))
  • content/docs/automation/webhooks.mdx (via timeoutMs (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS; a string literal in looseKeyMove))
  • content/docs/deployment/environment-variables.mdx (via timeoutMs (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS; a string literal in looseKeyMove))
  • content/docs/kernel/runtime-services/settings-service.mdx (via actionId (literal, a string literal in CONNECTOR_CONFIG_LIFTS; a string literal in CONNECTOR_CONFIG_LOOSE_KEYS))
  • content/docs/protocol/kernel/config-resolution.mdx (via actionId (literal, a string literal in CONNECTOR_CONFIG_LIFTS; a string literal in CONNECTOR_CONFIG_LOOSE_KEYS))
  • content/docs/protocol/kernel/lifecycle.mdx (via timeoutMs (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS; a string literal in looseKeyMove))

⛔ 7 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via connector_action (literal, a string literal in looseKeyTargets))
  • content/docs/releases/v16.mdx (via timeoutMs (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS; a string literal in looseKeyMove))
  • content/docs/releases/v17/17-0.mdx (via FlowSchema (symbol, a top-level const), connector_action (literal, a string literal in looseKeyTargets), timeoutMs (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS; a string literal in looseKeyMove), timerDuration (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS), waitEventConfig (literal, a string literal in looseKeyMove; a string literal in siblingBlockLooseConfigRefusals))
  • content/docs/releases/v17/17-4.mdx (via FlowSchema (symbol, a top-level const), timeoutMs (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS; a string literal in looseKeyMove))
  • content/docs/releases/v17/17-5.mdx (via retiredAfter (symbol, a field of const object flowNodeConnectorConfigLift; a field of const object flowNodeWaitEventConfigLift), retiredFromLoadPath (symbol, a field of const object flowNodeConnectorConfigLift; a field of const object flowNodeWaitEventConfigLift), actionId (literal, a string literal in CONNECTOR_CONFIG_LIFTS; a string literal in CONNECTOR_CONFIG_LOOSE_KEYS), connectorConfig (literal, a string literal in siblingBlockLooseConfigRefusals), connectorId (literal, a string literal in CONNECTOR_CONFIG_LIFTS; a string literal in CONNECTOR_CONFIG_LOOSE_KEYS), connector_action (literal, a string literal in looseKeyTargets), eventType (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS), timeoutMs (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS; a string literal in looseKeyMove), timerDuration (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS), waitEventConfig (literal, a string literal in looseKeyMove; a string literal in siblingBlockLooseConfigRefusals))
  • content/docs/releases/v17/17-6.mdx (via connector_action (literal, a string literal in looseKeyTargets))
  • content/docs/releases/v17/17-7.mdx (via FlowSchema (symbol, a top-level const), timeoutMs (literal, a string literal in WAIT_EVENT_CONFIG_LIFTS; a string literal in WAIT_EVENT_CONFIG_LOOSE_KEYS; a string literal in looseKeyMove))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 142 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 3b46855fb97365902e1e81a6484cd41bd363e575 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 3b46855fb97365902e1e81a6484cd41bd363e575

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 3b46855fb97365902e1e81a6484cd41bd363e575 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 23bebd666bc6cc6617321a5a3090847f0760bdb9
Local-runs: none

Family F3 of ruling 6182001882 (items 3 and 7) on card #23543, PR #23742, read at 2026-10-16T00:24Z. Inputs: the card body and its seventeen comments, the PR body and its 16-file list, the net diff against main at merge-base 3b46855fb9 (+621/−129, equal to the three-dot diff against origin/main 140238a47b), the precedent diffs of PR #23600 (F1) and PR #23638 (F5-jsx) with their contract reviews, and the 41 check-runs on this head. Nothing was built, run or re-run; every tree reading below is git show / git grep against the fetched head. objectui was not read: the ruling names no objectui half for F3.

Gate verdicts on this head: 41 check-runs, 36 success, 5 skipped, 0 failing or pending. The five required contexts — Lint & Repo Gates, TypeScript Type Check, Test Core (and 1–6/6), Build Core, Governed Surface Queue Guard — are all success; so are Check Changeset, Spec property liveness, Dogfood Regression Gate, Build Docs, the four Type Check · lanes, the nine Lint · lanes, and the card, claim, single-writer and Part-of guards. The 5 skipped are the PR fast-set skips (Console Pin Gate, Temporal Conformance (live PG + MySQL), the dogfood shards and Dogfood Verify CLI, the opt-in packed-tarball smoke), measured by main's full battery, not on the PR.

Governance: none of the 16 paths is in GOVERNED_SURFACES (register read on the head); head repo equals base repo; draft; Part of #23543 is the first line and no closing keyword follows; the assignee mirrors the card's; 750 changed lines; no content/docs/releases/ edit.

① Derived judgments

Accept-set changes (each narrows what an author may write):

  1. The flow parse refuses every named loose key under a wait or connector_action node's config (flow.zod.ts superRefine, in the collectFlowGraphs walk beside connectorActionConfigRefusals, so a region-body node is reached; siblingBlockLooseConfigRefusals in the new leaf automation/flow-sibling-block-loose-keys.ts). One custom issue per key at nodes.N.config.KEY: "A wait node's config.duration was removed in @objectstack/spec 18 (ADR-0087 D2) … Move it to waitEventConfig.timerDuration …", then the house os migrate meta --from 16 sentence (both lifts are step-17 members, migrations/registry.ts.template:1099-1100). Refused: eventType, timerDuration, duration, signalName, signal, timeoutMs on wait; connectorId, actionId, input on connector_action; a key present with null too (only === undefined is skipped, and JSON cannot carry it). Not refused: any other config key on these types (note), and the same keys on another node type — both pinned as controls. At defineStack it is STACK_SCHEMA_INVALID / 422 with the one issue at flows.0.nodes.1.config.KEY when the block is complete. RIGHT — ruling item 3's "refused with the prescription naming the sibling block", at every door of the authoring funnel: normalizeStackInput replays live entries only (apply.ts:156; no includeRetired anywhere under stack*.ts or the CLI's validate and compile paths), so defineStack, os validate, os build, and os lint through a config that calls defineStack. tsc cannot carry it (FlowNodeSchema.config is an open record), so the parse-time prescription is the whole channel, as item 7 says; the runtime strings cite the ADR and no tracker number.

  2. One table, two readers. WAIT_EVENT_CONFIG_LOOSE_KEYS / CONNECTOR_CONFIG_LOOSE_KEYS moved byte for byte from registry.ts into the leaf; registry.ts imports them (:49) and the refusal reads them, so the refused set and the lifted set cannot drift (pinned: refuses every named loose key and no other). The leaf is not on the automation barrel, the spec exports map carries fixed subpaths only (no ./automation/* wildcard) and files ships src/**/*.zod.ts only, so no public module appears; check:generated reads 14 artifacts up to date on the head (api-surface and export-origins unchanged). RIGHT.

  3. Both lifts leave the load path (retiredFromLoadPath: true, retiredAfter: '17.7.0', the value conversions/types.ts gives for an 18.0.0-next.N label; the package reads 18.0.0-next.1). The funnel no longer rewrites an authored loose key (pinned: stack unchanged, zero notices); the data-at-rest seams replay them under includeRetired: the engine's registerFlow canonicalizer (engine.ts:4576), applyConversionsToStoredItem (stored.ts:107, pinned true), the artifact-ingestion door inside its window, and the metadata save door's flow path (judgment 6). Neither CONVERSIONS_NOT_REPLAYED_AT_REHYDRATION (engine.ts:2251) nor FLOW_FALLBACK_VERDICT_NOT_REPLAYED (protocol.ts:3240) names either entry — each list holds only the decision-mode default flip. RIGHT — item 3's "both entries set retiredFromLoadPath".

  4. The lifts now DELETE a loose key that does not lift, with a notice (liftWaitEventConfig, liftConnectorConfigShape). Three cases, each read against the ?? chain the lifts mirror: (a) a key the block shadows — never read, the declared value won; (b) a later candidate shadowed by the first present one (duration beside timerDuration) — never read, the chain stopped at the first present value; (c) a null — the chain skipped it and the old lift left it in place; it is still skipped for precedence (the next candidate lifts: pinned) and now deleted. Precedence is unchanged in every case; what changes is that the stored row comes back with the block alone, which it must, because judgment 1 now refuses the kept key at the parse the seam runs next (FlowSchema.parse(converted)). The connector completeness guard is unchanged: a node whose pair cannot complete returns untouched and is refused at the block and at each loose key (pinned: nodes.1.connectorConfig, nodes.1.config.connectorId) — it never loaded. RIGHT — item 3's "the lift deletes a loose key the block shadows at rehydration, so stored flows keep loading"; (b) and (c) are the same deletion for the same reason (a value no reading ever reached) and sit inside the letter, not beyond it: without them a stored { timerDuration, duration } or { timerDuration: null, … } row would stop loading at 18, the exact hazard the ruling closes. Pinned end to end: registerFlow on a block-plus-shadowed-input row registers, config comes back {}, and the action receives the block's input (connector-nodes.test.ts); the wait case asserts the deleted loose key the same way (wait-node.test.ts).

  5. config.timeoutMs is prescribed as waitEventConfig.timerDuration, as a string. The block spelling waitEventConfig.timeoutMs is itself a 17 tombstone, and the chain's end state for a stored config.timeoutMs: 60000 is timerDuration: '60000': the wait lift moves it onto waitEventConfig.timeoutMs, then flow-node-wait-timeout-keys-removed, ordered after it in the chain (registry.ts:16730, :16751), stringifies it onto timerDuration when that is unset and drops it otherwise (removeWaitTimeoutKeys, whose docblock records "timeoutMs moves to timerDuration rather than being dropped, because that IS what it did"). The prescription states that end state and its "already set — delete" branch. RIGHT — a prescription naming waitEventConfig.timeoutMs would send the author into a second tombstone.

  6. The metadata save door, read off the code path, not the changeset's words. A NEW authored save of a flow carrying a named loose key through the metadata API (POST /meta, Studio draft or publish) is not refused; it is lifted and accepted. With an automation service reachable, resolveFlowCanonicalizer() is the engine's canonicalizeFlow (protocol.ts:22685-22775, engine.ts:4576): applyConversionsToFlow(…, { includeRetired: true, excludeConversionIds: CONVERSIONS_NOT_REPLAYED_AT_REHYDRATION }), then FlowSchema.parse(converted); the body persisted is result.storable, the lifted shape with the shadowed key deleted, one notice per key on the engine logger and nothing to the caller. Without one, the fallback judges flowGateVerdictBody = applyConversionsToFlow(request.item, { includeRetired: true, … }) (:22771) and persists the RAW body, loose key kept, for registerFlow to lift on the next boot — the [#21982] posture ("the FULL chain, retired entries included — spec(conversions): 15 ADR-0087 D2 conversions are still on the load path at 18 though their window closed (10 toMajor: 17, 5 older): retire each or record why it stays, one verdict per entry (family closing card after #23421) #23487 retired those spellings from the authoring funnel, not from this seam"). So the changeset's "still replays both conversions" is exact, and the D3 entry says it in plain words ("a script that still sends the loose spelling keeps saving").

  7. Readers and fixtures. Both executors already read only their blocks (wait-node.ts:225, connector-nodes.ts:55); the connector descriptor publishes no configSchema (connector-nodes.ts:39, deliberate since A designer configSchema and the keys its executor actually reads are still unreconciled — notify honours cfg.source, which no schema declares #4045), so a schema-driven form cannot be re-taught the loose shape; only comments moved, and they now say what judgment 1 and 3 say. Head-tree sweep for a named loose key under config outside tests, across packages/**, examples/**, skills/**, content/docs/** and the service-automation README: zero writers (the showcase, the skills example examples-flows.md and connectors.mdx author the blocks; the only hits are release-owned CHANGELOG quotations, which stay). The five re-spelled fixtures (two lint suites, run-summary.test.ts) used the retired location without testing the lift; connector-action-config-required.test.ts now pins the three-issue shape. RIGHT.

  8. Verdict-list pin and D3 entry. conversions.test.ts: both entries move from the kept list — now exactly the four entries of F2, F4 and F5-http (flow-node-http-callout-rename, page-component-visibility-to-visibleWhen, retry-policy-converged, view-visibleOn-to-visibleWhen) — to the retired list with retiredAfter: '17.7.0'; the new describe pins the three claims that must hold together (a stored loose-only row and a block-plus-shadowed row each refused as written and loading after the seam's replay with the block alone; the funnel no longer rewrites; defineStack refuses naming the block key). retired-key-removal-version.pin.test.ts gains one refusal row per entry in its JOIN, so every retired entry still has a renderer. 18.flow-sibling-block-loose-config-keys-refused.ts is a SemanticMigration in the sibling shape, conversionIds naming both entries; both registry fixtures show the deleted shadowed key and expectedNotices goes 4 → 5. RIGHT — item 7's "verdict-list pin updated", shrinking the kept-without-ruling list by exactly two.

  9. Docs. content/docs/automation/flows.mdx: the connectorConfig and waitEventConfig rows name the refusal. No .describe() moved, so content/docs/references/ is untouched, rightly. RIGHT.

② Semver level

  • .changeset/23543-f3-retire-wait-connector-config-lift.md: @objectstack/spec: minor, @objectstack/service-automation: patch; summary feat(spec)!:; **BREAKING (authoring):** banner; FROM → TO for every refused key, the timeoutMs special case, the "block already set — delete" rule, the os migrate meta --from 16 sentence, how stored rows keep loading, and the ADR-0087 disposition marker registered flow-sibling-block-loose-config-keys-refused, which names the D3 entry's id exactly. RIGHT. The repo is in Changesets pre mode (.changeset/pre.json, tag next), where a BREAKING spec change grades minor; the F1 and F5-jsx changesets carry the identical frontmatter, banner and arm shape. Check Changeset and Lint & Repo Gates (which carries check-adr-0087-registration) are success.
  • Rows. spec is the one package whose published behaviour changes. service-automation's two source edits are comment-only; F1's record took no row for comment-only moves (cli, service-datasource), and the dev's patch row here (flagged in its report) is the other defensible reading — the shipped source text changed — at the cost of one no-op patch in pre mode. Accepted, not owed. lint moved only test files and metadata-protocol is untouched: no row is owed and none is missing.
  • The Clause-②: line. The changeset's yes (narrowing) is the ruling's spelling (item 7) and the right arm: judgments 1–4 narrow; the widening yes names is the published upgrade catalogue (the D3 entry ships in the spec tarball's protocol-upgrade-guide.md / spec-changes.json), as the F5-jsx record counted it — with no new export this time. The PR body's line 2 reads bare Clause-②: yes: the gap F1's record ③ item 1 named, from the same source — the claim 6203354520 wrote the bare line and the dev copied it. No gate reads the arm from the body (check-changeset-no-major reads yes and finds spec at minor; the registration gate takes the arm from the changeset), so it is not a condition of this record; the body is the PR-scoped carrier, and the owning seat amends it to Clause-②: yes (narrowing) before the PR is armed, the body read back after the write. See ③ item 1.

③ Boundary flags

Dev open_questions (6204710307):

  1. A connector_action node with only config.connectorId / config.actionId gets three issues (the missing block plus one per loose key): keep, or suppress the per-key issues when the block is absent? The seat answers A, and A is right. The accept set is identical either way — the node is refused in both — so this is presentation inside the ruling, not a decision for the card: each issue is true and located where the author wrote the key, the block issue already names the move, and B would add a special case to a rule that is otherwise one table, the drift the leaf exists to prevent. Pinned on the head (connector-action-config-required.test.ts; the completeness-guard case in conversions.test.ts). Riding this item, the seat's one-line edit: the body's Clause-② arm (②).

Dev deviations:

  1. File surface widened by the pin sweep. Accepted — each is a fixture or a doc row named in the report and re-read in ① judgment 7; registry.ts's hunks are the import and the two entries only (13 hunks, between lines 46 and 1788), MAJOR_18_CONVERSIONS and every other entry untouched, as the claim required.
  2. Executors comment-only; service-automation still at patch. Accepted (②).
  3. The os migrate meta load-path suites unchanged; F2 inherits the no-live-subject question. Verified on the head: packages/cli/test/migrate-meta-load-conversions.test.ts, migrate-meta-composed-load-conversions.test.ts, migrate-meta-composed.test.ts and spec/src/stack-authored-input.test.ts all drive retry-policy-converged (8 / 9 / 5 / 1 references) and none names an F3 key. Escalated to the F2 dispatch order on spec(conversions): eight ADR-0087 D2 conversions stay on the 18 load path after #23487 — five families each need a ruling before they can retire #23543, as the F1 record's item 9 already carried it: F2 is the family that retires their last live subject, so its PR gives the authored-input shim a synthetic probe or retires the suites.
  4. Design additions beyond the letter: the null delete, the shadowed later candidate, the timeoutMs prescription. Accepted — ① judgments 4 and 5: each is the chain's own precedence stated once, and each is needed for a stored row to keep loading.
  5. Round-1 check:dts-closure recorded as a claimed NOT-MEASURED under an OS_SKIP_DTS closure. Accepted — an environment state; Type Check · consumer gates and Build Core are success on this head.
  6. Attribution trailers per AGENTS.md; main merged before the PR opened through os-regen-merge.sh. Accepted — process notes; check:generated reads up to date on the head and the three-dot diff equals the merge-base diff.

Dev out_of_scope_findings: none.

Carried forward from the F1 and F5-jsx records:

  1. F3's dispatch order was to name the load-path suites. It did (claim 6203354520); item 4 closes it for F3.
  2. New, escalated to the seat before the F2 and F5-http dispatches (not a condition of this record): the flow save door's posture under item 7 — ① judgment 6. The question for the card, in one line: does the metadata save door judge a NEW flow save (no stored baseline row) as authored input and refuse the retired spellings, replaying retired entries only when re-saving an existing row — or is the door a rehydration seam by design (build: a script node's undeclared config key passes objectstack validate, compile and registerFlow, then fails every run — the key half of #21898's class (subflow by reading) #21982), as it is today for every retired flow entry? F2's retryDelayMs rename and F5-http's node-type renames converge at that same door, so one answer serves three families; a "refuse" answer is a protocol.ts change with its own card and review, never a rider.

Report corrections, not flags: the dev report reads Test Core 3/6 in_progress; on this head it is success, and the tally is 41 (36 success, 5 skipped).

Premise re-read on the head: no examples/**, packages/** non-test source, skills/** or content/docs/** authors a named loose key under config; the showcase writes both blocks (examples/app-showcase/src/automation/flows/index.ts:345, :643). The ruling names no objectui half for F3, this PR owes none, and the pinned .objectui-sha (1f23d5b070) is unchanged; Console Pin Gate is skipped on the PR and measured by main's battery.

Implemented-by: claude/issue-23543-retire-wait-connector-config-lift
Reviewed-by: session_01LFqdRsuM5nkqoJdHd5ztVk

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · objectstack domain:spec seat 2 (#18549) · session session_01LFqdRsuM5nkqoJdHd5ztVk · 2026-10-16T00:27Z

This is the seat review of record for family F3 of #23543 (ruling 6182001882 items 3 and 7, claim 6203354520), on head 23bebd666bc6cc6617321a5a3090847f0760bdb9.

  1. Form.
    • Draft to main. The first line is Part of #23543, and there is no closing keyword: F2, F4 and F5-http remain on the card.
    • Clause-②: the seat amended the body's line 2 from yes to yes (narrowing), the ruling's spelling, which the changeset already carried (contract review ②). Only that line changed, and the body was read back after the write.
    • 16 files, +621/−129. One BREAKING changeset: @objectstack/spec minor (pre mode) and @objectstack/service-automation patch. ADR-0087 disposition registered flow-sibling-block-loose-config-keys-refused, which matches the D3 entry's id.
  2. What it does.
    • The FlowSchema walk refuses the named loose keys under config:
      • on a wait node: eventType, timerDuration, duration, signalName, signal, timeoutMs;
      • on a connector_action node: connectorId, actionId, input.
    • Each refusal is located at nodes.N.config.KEY and names the waitEventConfig / connectorConfig key to move to.
    • The refused set and the lifted set come from one leaf table, so they are the same by construction.
    • At rehydration, the replayed lifts delete a loose key the block shadows, so stored rows keep loading with the block alone.
    • Both entries set retiredFromLoadPath and retiredAfter: '17.7.0'.
    • The verdict-list pin now keeps F2's, F4's and F5-http's four entries.
  3. Gates.
    • CI on this head: 41 check-runs, 36 success, 5 skipped (the PR fast set), none red.
    • Dev local: 111 derived families. The one NOT-MEASURED is check-plugin-teardown-shape --self-test (exit 3, shallow clone), which CI's Lint & Repo Gates covers.
    • Two ablations: the refusal went red in 9 tests and the shadow delete in 3, each restored.
  4. Contract review: PASS at CONTRACT_REVIEW_TIER, comment 6205149412, on this head. No judgment was marked WRONG.
  5. Open question (report 6204710307). A connector_action node with only loose keys gets three issues: the missing block, plus one at each loose key. The seat's answer is A, keep all three. The accept set is identical either way; each issue is true and located where the author wrote the key. Suppressing them would add a special case to a one-table rule. The contract review reads it the same way.
  6. Accepted deviations.
    • The file surface widened through the pin sweep: two lint fixtures, run-summary.test.ts and two flows.mdx rows. Each is re-spelled to the declared blocks, and none tested the lift.
    • The service-automation patch row covers comment-only edits.
    • The design additions are accepted: the null delete, the shadowed later candidate, and timeoutMs → waitEventConfig.timerDuration as a string. Each is the ?? chain's own precedence, stated once.
  7. Carried to the card, not conditions of this landing.
  8. Landing terms. Ready, then auto-merge. No generated artifact moved (check:generated was up to date before and after the main merge), so this PR is outside the relay order of feat(spec)!: a page declares nothing the console does not honour — isDefault only on a bound record page, a closed template set, regions its template places #23708 and spec liveness: the console pin hop to objectui 1f23d5b070fa makes page.print and translation.flows.successMessage / errorMessage read planned while the shipped console applies them (family card; the parentDataSource leg is #23585) #23710.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 16, 2026 00:28
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 16, 2026 00:28
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit d176b0a Oct 16, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23543-retire-wait-connector-config-lift branch October 16, 2026 01:00
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants