Skip to content

fix(automation,lint,spec): validate the expression slots a node's configSchema declares (#4027) - #4040

Merged
os-zhuang merged 3 commits into
mainfrom
claude/console-screen-flow-submit-jfpjy4
Jul 30, 2026
Merged

fix(automation,lint,spec): validate the expression slots a node's configSchema declares (#4027)#4040
os-zhuang merged 3 commits into
mainfrom
claude/console-screen-flow-submit-jfpjy4

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #4027. Follow-up to the #3528 line (#3771 / objectui#2899 / #3788 / hotcrm#505).

The gap

A node type's designer configSchema and the config keys its validators traverse are two hand-written lists that nothing reconciled. Both validators hardcoded the same two keys and assumed the rest:

Validator Traversed
AutomationEngine.validateFlowExpressions (registration) config.condition, edge.condition
validateStackExpressions (objectstack validate) config.condition, edge.condition

Its own TSDoc stated the assumption outright — "node string fields are templates (a different dialect) and are validated by the template engine, not as CEL". A declared expression property outside those two keys was therefore validated by nobody.

That is how #3528 shipped. screen.fields[].visibleWhen has been on the screen descriptor since #3304 — typed xExpression: 'expression', documented as bare CEL one comment above, offered to authors in Studio — and neither validator traversed it. HotCRM authored the predicate in the other dialect, '{createOpportunity} == true', the {var} form its sibling config keys use correctly. It passed tsc, passed objectstack validate, passed registerFlow, and failed silently at run time.

Not cosmetic, because required is enforced: a field the author had made conditional rendered unconditionally and blocked Submit on an input the user was never shown. The run paused forever, no resume was ever issued, and no layer produced a diagnostic.

This PR

  • FLOW_NODE_EXPRESSION_PATHS (@objectstack/spec) — the declared ledger of expression-bearing node config paths, each recording the dialect it takes, plus a pure resolver that expands fields[].visibleWhen over a concrete config (array indices filled in).

  • Both validators read the ledger. A malformed predicate is now located and quoted at registration and at objectstack validate:

    Flow 'lead_conversion' has 1 invalid expression (ADR-0032 §1a). …
      • node 'screen_1' (screen) screen field visibleWhen at config.fields[1].visibleWhen:
        invalid CEL predicate: … — do not wrap references in `{…}` template braces
          source: `{createOpportunity} == true`
    
  • A reconciliation ratchet derives the expression properties from the live descriptors and fails in both directions — a new xExpression property with no ledger entry (the Console: screen-flow Submit never calls the resume endpoint — every screen flow is un-completable from the UI #3528 shape), or a stale entry no descriptor declares. It walks every registered builtin, which is the audit the original triage never did, and asserts the derivation is non-empty so it can't pass vacuously.

Three dialects, not two — the part that nearly broke this

xExpression takes two values that disagree about braces, and a third dialect exists in validateExpression:

Slot Dialect {x}
screen.fields[].visibleWhen bare CEL error (#1491 brace-trap)
loop.collection, map.collection single-brace {var} flow interpolation correct
notify body, text templates ADR-0032 §3 double-brace {{ }} legacy-form error

My first cut routed xExpression: 'template' slots through validateExpression's 'template' role — which enforces the double-brace dialect and rejects a single {x}. A test I'd written for exactly this caught it: it would have failed every valid loop.collection in every existing flow.

So the ledger records the dialect and only bare-CEL slots are checked. loop/map collections are recorded as flow-template and deliberately skipped — no validator implements their dialect. That is a real remaining gap, stated in the type's TSDoc rather than papered over, and the ledger gives a future validator one place to hook in. A test pins that a correct {tasks} still registers cleanly.

Correcting a false contract claim

ActionDescriptor.configSchema's TSDoc claimed registerFlow() "checks … that config satisfies configSchema". It never did. FlowNodeSchema.config is z.record(z.unknown()), so types, required, enum and unknown keys are unenforced. Per Prime Directive #10 — never advertise a capability the runtime doesn't deliver — the doc now states exactly what is checked (declared bare-CEL slots) and what is designer-facing only.

Test plan

Check Result
@objectstack/spec ✅ 6895 passed (265 files)
@objectstack/service-automation ✅ 440 passed (39 files), 13 new
@objectstack/lint ✅ 545 passed (37 files), 5 new
tsc --noEmit — spec, lint ✅ clean
ESLint on all changed files ✅ 0 errors

Ratchet verified to fail without the fix. Deleting the screen ledger entry turns 3 tests red, reporting the missing path by name:

× every xExpression property a builtin declares is in the ledger
    + "screen.fields[].visibleWhen (predicate)"
× screen.fields[].visibleWhen is covered — the #3528 regression
× resolves each element of a field repeater, with its index

New behavioural coverage asserts the exact HotCRM predicate is rejected, that the message names the node / label / indexed path / source, that the corrected bare CEL registers cleanly, that all malformed fields are reported rather than just the first, and that a screen's field names are not checked against the trigger object's schema (no hint is passed — otherwise every correct predicate would carry a spurious "unknown field" finding).

Findings not fixed here

🤖 Generated with Claude Code

https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq


Generated by Claude Code

…figSchema declares (#4027)

A node type's designer `configSchema` and the config keys its validators traverse
were two unreconciled lists. The engine's `registerFlow` pass and the author-time
`objectstack validate` pass each hardcoded `config.condition` / `edge.condition`
and assumed every other node string was a `{var}` template, so a declared
expression property outside that hardcoded set was validated by nobody.

That is how #3528 shipped. `screen.fields[].visibleWhen` has been on the `screen`
descriptor since #3304 — typed `xExpression: 'expression'` (bare CEL), documented
as such, offered to authors in Studio — and neither validator traversed it. An app
authored the predicate in the other dialect (`'{createOpportunity} == true'`, the
`{var}` form its sibling config keys use correctly) and it passed tsc, passed
`objectstack validate`, passed registration, and failed silently at run time.
Because `required` IS enforced, a field the author had made conditional rendered
unconditionally and blocked Submit on an input the user was never shown: the run
paused forever and no resume was ever issued.

- `FLOW_NODE_EXPRESSION_PATHS` (spec) is the declared ledger of expression-bearing
  node config paths, each recording the dialect it takes, with a pure resolver
  that expands `fields[].visibleWhen` over a real config.
- Both validators read it. A malformed predicate is now located and quoted at
  registration and at `objectstack validate`:
  `node 'screen_1' (screen) screen field visibleWhen at config.fields[1].visibleWhen`.
- A reconciliation ratchet derives the expression properties from the LIVE
  descriptors and fails in both directions — a new `xExpression` property with no
  ledger entry, or a stale entry no descriptor declares. It walks every registered
  builtin, which is the audit the original triage never did.

Dialects are recorded rather than assumed because there are three and two of them
disagree about braces: bare CEL (`{…}` is the #1491 brace-trap), single-brace
`{var}` flow interpolation (`{…}` is correct), and ADR-0032 §3's double-brace text
template. Only bare-CEL slots are checked; `loop.collection` / `map.collection`
are recorded as `flow-template` and skipped, since no validator implements their
dialect and checking them under either of the others would reject every valid
flow. A test pins that.

`ActionDescriptor.configSchema` no longer claims `registerFlow()` validates
`config` against it. It never did — `FlowNodeSchema.config` is
`z.record(z.unknown())`, so types, `required`, `enum` and unknown keys remain
unenforced. The TSDoc now states what is checked and what is designer-facing only,
per Prime Directive #10: don't advertise a capability the runtime doesn't deliver.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 5:51am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/lint, packages/services, @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 2 commits July 30, 2026 05:38
…ma describe change

`content/docs/references/` is generated from the Zod source by
`packages/spec/scripts/build-docs.ts` and gated by `check:docs`. Correcting
`ActionDescriptor.configSchema`'s `.describe()` in the previous commit changed its
generated row, so the checked-in reference went stale.

Regenerated with `gen:schema && gen:docs` — a one-line diff, no other file touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq
…apshot

`check:api-surface` gates `@objectstack/spec`'s public exports against a checked-in
snapshot. The expression-slot ledger adds five — `FLOW_NODE_EXPRESSION_PATHS`,
`FlowNodeExpressionPath`, `FlowNodeExpressionRole`, `ResolvedFlowNodeExpression`
and `resolveFlowNodeExpressions` — so the snapshot needed regenerating.

Additive only: 0 breaking (nothing removed or narrowed), 5 added. `+5` lines and
nothing else changed.

Every step of the `TypeScript Type Check` job now passes locally: spec `tsc
--noEmit`, `check:docs`, `check:skill-refs`, `check:react-blocks`, the workspace
build, examples typecheck, downstream-contract typecheck, `check:api-surface` and
`check:skill-examples` — plus the other six spec `check:*` gates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq
@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 06:21
@os-zhuang
os-zhuang merged commit e4c61a7 into main Jul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/console-screen-flow-submit-jfpjy4 branch July 30, 2026 06:21
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…o regenerate it (#4046)

`packages/spec` has eight checked-in generated artifacts, each with its own CI gate,
split across two jobs that run their gates sequentially — so the first stale artifact
masks every one behind it and you get one red build per artifact. AGENTS.md documented
the auto-gen boundary but never mapped a change to the generator it invalidates.

#4040 paid that twice, neither time a logic error: a `.describe()` string landed in
content/docs/references/ (check:docs), and one new export landed in api-surface.json
(check:api-surface) — the second only visible once the first was fixed.

Adds a change → gate → regenerate table plus a loop that runs all eight, and records
that check:liveness / check:empty-state / check:skill-examples / check:react-conformance
have no generator (a failure there is a real finding).

Also documents a trap found while verifying it: check:api-surface reads the built
dist/*.d.ts, so a stale dist reports exports as REMOVED when nothing was removed.
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…emas (#4001 Tier-A) (#4071)

Zod's default .strip silently discarded undeclared keys on authorable schemas —
the instance kept parsing, so a mis-spelled or wrong-layer key shipped as
metadata that quietly ignored the author's config (#3405, #1535). This lands
the #4001 Tier-A slice inside the v17 breaking window.

- docs/audits/2026-07-unknown-key-strictness-ledger.md: the authorable / wire /
  open triage (#4001 step 1), classified by "who writes this schema's input",
  with the ratchet's verified next targets.
- shared/suggestions.zod.ts: new strictUnknownKeyError factory generalizing the
  #3746 hand-rolled map (alias table + length-relative edit distance +
  tombstone/wrong-layer guidance); ui/action.zod.ts re-homes onto it with
  byte-identical messages.
- security/permission.zod.ts: PermissionSetSchema, ObjectPermissionSchema,
  FieldPermissionSchema, AdminScopeSchema are .strict().
  EffectiveObjectPermissionSchema explicitly .strip()s back — response shapes
  stay wire-tolerant.
- automation/flow.zod.ts: FlowSchema, FlowNodeSchema, FlowEdgeSchema,
  FlowVariableSchema are .strict(); a node's config record stays open (the
  executor's configSchema, #4027/#4040, and the ADR-0087 conversion layer own it).

Three real defects the gate caught in its first two runs, all previously
invisible — each key written by real code, silently dropped at parse:
PermissionSetSchema could represent neither `description` (authored by the
built-in default sets, read by the Setup projection) nor the ADR-0010 runtime
protection envelope (stamped by applyProtection on every metadata type and
round-tripped through getMetaItemLayered -> saveMetaItem; every sibling
registered type already spread MetadataProtectionFields, permission was the
outlier); and a dogfood fixture declared the object-level `sharingModel` on a
flow. Two of the three are inverse drift — the direction the per-property
liveness ledger cannot see.

Migration is behavior-preserving by construction: any key now rejected was
previously stripped and therefore never had a runtime effect.
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…4001 step 3) (#4119)

Third click of the unknown-key strictness ratchet (flow + permission in
#4071, RLS / sharing / position in #4099). Approval is a v17-new authoring
surface — tightened while young, before stored volume exists:

- ApprovalNodeConfigSchema, ApprovalNodeApproverSchema,
  ApprovalEscalationSchema and DecisionOutputDefSchema are .strict() with
  fixable errors via the shared strictUnknownKeyError factory.
- ApprovalNodeConfig's guidance is the ADR-0019 re-home map: steps ->
  successive approval nodes on the canvas, entryCriteria -> the entering
  edge's condition, onApprove/onReject -> the approve/reject out-edges,
  rejectionBehavior -> a declared back-edge (ADR-0044) with maxRevisions.
- The published JSON schema (getApprovalNodeConfigJsonSchema) carries
  additionalProperties:false into the Studio property form AND registerFlow's
  per-node config validation (#4027/#4040). Verified the #3746 hazard:
  z.toJSONSchema on the strict lazySchema does not throw.

Verified: spec 6995 tests + tsc clean; all 12 check gates; plugin-approvals
326 / service-automation 457; dogfood 72 files / 418 tests; showcase / crm /
todo validate clean.
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>
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.

A designer configSchema and the executor's wire payload are two unchecked lists — nothing validates flow node config keys at author time

2 participants