Skip to content

feat(spec,service-automation,lint)!: an http node's url and headers are text slots, its body a value slot, the rest literals — the {…} token refused (#19939 pass 4, S5) - #23062

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-19939-p4-s5-http
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-19939-p4-s5-http

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Part of #19939
Clause-②: yes (narrowing: a {…} token is refused at http url, each headers value, any string of a literal body, method, durable, timeoutMs and signingSecret, and as a whole headers map, and a body value that names a dialect must be a valid CEL value envelope; widening: the exported FlowNodeTextSlot type gains the optional eachValue field, LEDGER_DECLARED_NODE_CONFIG_SCHEMAS gains http, and the published HttpConfig JSON Schema marks body as a value slot)

Pass 4, stage S5 of #19939 (stage plan 6096263424, claim 6122880753), under ruling D on #11182 and ADR-0032 Decisions 2 and 3. An http node reads each config key by what it is: url and every headers value are text slots with {{ }} holes (the plan's carry line), body is a value slot (a CEL value envelope or a literal), and method, durable, timeoutMs and signingSecret are literals, read as written (S4's seat order 6117306001, answer B, applied per key on the census below). A single-brace {…} token at any of them is refused. The executor parses its config as authored, renders the request once before either mode, and no longer imports the single-brace interpolator. #19939 stays open for S6 and S7, so this PR says Part of.

The census, per key (base 9432bac07)

Every token-bearing string at an http node position, in examples/**, content/docs/** and docs/** (references and releases excluded), packages/qa/dogfood, packages/platform-objects and every tracked TS / JS file. The test column is the fix round's structural sweep at 1105b4a06 (each of the seven keys on an object typed http, ternary branches and helper arguments followed); the first census matched http fixtures by text and missed the ten strings marked †, rewritten in af1317fd1. hotcrm is not measured (another repository; the dispatch carries it).

key S5 shipped (examples, docs, dogfood, platform-objects) tests why
url text slot, {{ }} 0 6: lint-flow-credential-literals.test.ts:269 (a template draws no credential finding), validate-flow-template-paths.test.ts:181, flow-builtin-node-config-values.test.ts:240, flow-text-slot-template.test.ts:259, † runtime-authoring-gate.flow-credential-literal.test.ts:39 and † flow-credential-literal-doors.e2e.test.ts:71 (the DARK credential controls) the plan's carry line (binding): a url is text
headers.* text slot per value, {{ }}; a token written as the whole map refused 0 5: flow.test.ts:786 (Bearer {$Credential.token}), flow-builtin-node-config-values.test.ts:224 (headers: '{headers}', the build-door carve-out control), † runtime-authoring-gate.flow-credential-literal.test.ts:42 and † flow-credential-literal-doors.e2e.test.ts:74 (the DARK credential controls), † lint-flow-credential-literals.test.ts:84 (the scanner's template control) the plan's carry line (binding)
body value slot, one value (the whole payload) 5 tokens in 4 bodies: showcase flows/index.ts:1059 (text with a hole), :1239 (two whole tokens), :1444 (one); content/docs/automation/flows.mdx:464 (one) 5: † flow.test.ts:492-:493 (the approval flow, two tokens), flow.test.ts:788, flow-walk.test.ts:165, validate-flow-template-paths.test.ts:560, † validate-flow-template-paths.test.ts:465 (the hand-off helper, fed :441, :490, :500) measured pull
method literal (B) 0 0 no flow computes it
durable literal (B) 0 1: flow-builtin-node-config-values.test.ts:223, the build-door carve-out control the one test pins the interpolation itself, not a use
timeoutMs literal (B) 0 2: config-parse.test.ts:193 (the post-interpolation parse), flow-builtin-node-config-values.test.ts:222 (the carve-out control) both pin the interpolation itself
signingSecret literal (B), run-resolved, ⛔ not a template or value slot 0 5: http-node-signing.test.ts:208, :241, :255, :309 (a secret through a template, and the "resolved to nothing" refusal), guard-refusal-inventory.test.ts:160 the tests pin the interpolation itself; the credential channel is unchanged

The test, applied. A test that exists to pin the interpolation is not pull: it is the mechanism this card retires, the same reading S4's B gave the designer's picker ("a surface, not a use"). So the four zero-shipped keys are literals, and each such test is re-judged in this PR (below).

FROM → TO, per position

position you wrote write instead what changes
url 'https://api.example.com/v1/orders/{record.id}' 'https://api.example.com/v1/orders/{{ record.id }}' a Date renders as its ISO text, not JSON-quoted
headers.X 'r-{record.id}' 'r-{{ record.id }}' a header that renders to no text is not sent (the template sent it empty)
headers (whole) '{headers}' the map: { 'X-Ref': '{{ record.id }}' } a map is not computed into it
body { id: '{record.id}', status: 'done' } { dialect: 'cel', source: "{'id': record.id, 'status': dyn('done')}" } an absent id fails the run instead of sending null; a literal beside a field read is wrapped in dyn(…) (CEL holds a map's values to one type)
body { text: 'Done: {record.title}' } { dialect: 'cel', source: "{'text': 'Done: ' + record.title}" } an absent title fails the run instead of sending 'Done: '
method / durable / timeoutMs '{verb}' / '{flag}' / '{timeout}' the value itself: 'POST' / true / 5000 a literal now; nothing computes it
signingSecret '{signing_key}' the secret itself (a metadata-API save keeps it in the write-only credential store), or '' refused at the run

The one-line fix: double the braces in url and the headers values, write body as one CEL value envelope that builds the payload, and write method, durable, timeoutMs and signingSecret as the values themselves.

The mechanism assumptions, measured

  • A1 holds. HttpConfigSchema (io-node-config.zod.ts:745 at the base) carries url, method, headers, body, durable, timeoutMs, signingSecret and the surface / history keys of its strictObject, nothing else.
  • A2 holds, and what replaces the order. PARSED_AFTER_INTERPOLATION = new Set(['http']) existed because http-nodes.ts:182 parsed interpolate(raw, …): the build doors judged an http rule only on a config carrying no token at all. The executor no longer interpolates, so the set had one member and loses it; the constant and its one line in builtinValueJudged are deleted (a set that is always empty is a check that can never fire). What replaces the order: credential channel → parse the config as authored → render url and the headers values → evaluate body → either mode. That is the order every other contract-carrying builtin already had (notify since S4).
  • A3 holds, with one note. http-nodes.ts imports renderTextSlot and resolveValueSlot, not interpolate. A literal body passes through resolveValueSlot's residual interpolate() (the identity on a token-free literal; the contract parse refused any token first), the same residual call S1–S4's slots make, which S7 deletes. The three ?? left in the executor are the declared absent-key defaults, not tolerance of an off-spec shape: cfg.method ?? 'POST' / ?? 'GET' (the contract documents both, and why there is no .default()), and payload: body ?? {} on the outbox arm (unchanged).
  • A4 holds, and the request is rendered once. At the base the whole config was interpolated in the executor before the durable branch, and service-messaging's outbox and dispatcher render nothing (http-outbox.ts stores the url, headers and payload as handed; read at the base). After S5 the executor renders url and the headers and evaluates body once, before the durable / inline branch, so the outbox enqueues exactly what the inline call would send — pinned (http-slots.test.ts 3: the enqueued url, headers and payload are the rendered ones, and nothing in the enqueue carries a {{).

signingSecret — the one judgement this PR settles itself

The binding text: "signingSecret is a run-resolved key (RUN_RESOLVED_KEYS): its secret-reference resolution stays exactly as it is. ⛔ Do not turn it into a template or value slot." Read with A2 ("once no {var} is read at any http key") and A3 ("no longer calls interpolate"), the resolution that stays is the write-only credential channel, and it does: holdsFlowCredential / resolveFlowCredential and RUN_RESOLVED_KEYS are byte-identical. What goes is the {token} interpolation of the secret, which the census shows only the interpolation's own tests ever wrote. The key is therefore a literal (S4's B), a token in it is refused with a remedy that never echoes the value, and because the build doors leave a run-resolved key to the run, that refusal is the executor's parse — pinned both ways (registerFlow and objectstack validate accept signingSecret: '{k}'; the run refuses it, nothing is sent). The dead "set but resolved to no value" refusal goes with the interpolation: with nothing rendered, the parsed secret is the authored one. Four axes: no measured business use (tests only); one dialect and S7's resolver deletion stay possible (keeping interpolate on this one key would block it); a token is refused loudly at the run instead of signing with its braces or leaving unsigned; immediate retirement, no window. The boundary this leaves is in Acceptance notes.

What lands

The contract (@objectstack/spec).

  • url stays z.string() and each headers value z.string(); the contract's refinement refuses a single-brace token in either through the one text-slot judge (textSlotTemplateRefusal), with its hole spelling, and a hole over a $ root the engine does not bind.
  • body takes FlowValueSlotSchema: an object naming a dialect must be a valid CEL value envelope, and a {…} token in any string of a literal is refused by the one value-slot judge, which names the envelope that builds the whole body.
  • method, durable, timeoutMs and signingSecret keep their 17.x types; a token or an envelope there is refused with the literal's remedy. The refusal of a token at method, durable, timeoutMs and of a token written as the whole headers map carries READ_AS_WRITTEN_PARAM (S4's mechanism, unchanged), so the build doors report it. The refinement runs with when: () => true, so a token at a typed literal (timeoutMs: '{t}') gets the literal's sentence beside the type's refusal; the build doors report the marked one, once.
  • Every describe() of the http keys is restated, and the module docblock.

The ledger and the doors.

  • FLOW_NODE_EXPRESSION_PATHS appends the http body row (one value, the whole payload); every older row is byte-identical (pinned).
  • FLOW_NODE_TEXT_SLOTS gains http.url and http.headers. The exported FlowNodeTextSlot gains the optional eachValue: flowNodeTextSlotSources yields each value of the map at its own path (headers.X-Ref), so registerFlow and objectstack validate judge and compile each header like every other text slot.
  • HttpConfigSchema joins LEDGER_DECLARED_NODE_CONFIG_SCHEMAS, which carries the body's xExpression: 'value' to the reconciliation ratchet (the descriptor offers body with no marker: one slot, one channel).
  • flow-node-config-refusals.ts: PARSED_AFTER_INTERPOLATION and its line are deleted (A2); RUN_RESOLVED_KEYS is unchanged.

The executor (@objectstack/service-automation). http-nodes.ts parses the config as authored (after the credential channel), renders url and each header through renderTextSlot (a url that renders to no text refuses the node, a header that renders to no text is not sent), evaluates body through the shared resolveValueSlot, reads the literals as written, and no longer imports interpolate. The descriptor's help strings are restated; its types are unchanged.

The lint (@objectstack/lint, patch). The notify-literal hint machinery becomes per node type (LITERAL_KEYS / LITERAL_HINTS): at an http literal both wrong-syntax hints say that neither brace dialect resolves there and name the value itself. The flow-double-brace-interpolation hints name the http url and headers among the text slots. The credential-literal docblock stops calling http's single brace its template. objectstack validate refuses each position through the shared judges and the contract, with no rule change.

ADR-0087, D3 only. 18.flow-value-slot-template-dialect-refused (surface: the http body and its four literals; replacement, reason, acceptance), 18.flow-text-slot-single-brace-refused and 18.flow-text-slot-unbound-dollar-root-refused (surface: the http url and headers values), and 18.flow-builtin-node-config-values-refused with its STEP18_RATIONALE fragment (it said http parses after interpolating and that only an http typed slot may take a sole-token template; both stop being true here). No D2: an absent value in the body wrote null or nothing under the template and fails the run under CEL; a whole token at timeoutMs / durable resolved to its type and is text now; a Date in a url or header rendered JSON-quoted under the interpolator and as ISO text under the engine. No key is removed, so no tombstone. The changeset marker is not-required (already-registered …) naming the four ids.

Sites. The showcase's three bodies (flows/index.ts) and the flows guide's http example are migrated; a literal beside a field read inside a CEL map literal is wrapped in dyn(…) (measured: {'id': record.id, 'status': 'done'} is refused by the CEL checker at registerFlow, "Map value uses wrong type"). The flows guide gains an http paragraph and the http keys in its text-slot and value-slot lists and its dialect table; the service-automation README's dialect table likewise. Test fixtures that wrote the retired spellings at these keys are re-judged: in the first pass flow.test.ts's api flow, the lint credential-literal fixtures, config-parse.test.ts, the signing tests, the guard-refusal inventory and the build-door carve-out control; in the fix round (af1317fd1) every remaining fixture the structural sweep found outside the refusal pins: the two DARK credential controls (metadata-protocol save door, cli doors; {{ api_token }} at the url and header, still no advisory), the lint scanner's template header control ({{ run_token }}), flow-walk.test.ts's body (a CEL envelope), validate-flow-template-paths.test.ts's url pin (a {{ }} hole) and hand-off payload (moved from the body, which no template reaches, to an X-Billing-Amount header text slot), and flow.test.ts's approval-flow body (a CEL envelope, now parsed against HttpConfigSchema).

Pins, and the ablations that turn them red

  • http-slots.test.ts (service-automation, 21 engine pins): each position refused at registerFlow, located and in its own judge's words; the executor's contract parse refuses url, header and body tokens and each literal's token as a guard, nothing sent; signingSecret accepted at registerFlow and refused at the run; holes render in the url and headers, the body envelope evaluates (dyn), a literal body and a nested envelope are sent as written, a whole hole holding a map renders JSON; the durable enqueue carries the rendered request; a url rendering nothing refuses the node, a header rendering nothing is not sent, an absent variable in the body fails the run with EXECUTION_ERROR naming body.
  • Spec: io-node-config.test.ts (24 contract pins: text slots, body, literals with the marker, signingSecret without one and never echoing the value, the build doors, the JSON projection, the describes); flow-node-expression-paths.test.ts (the body row, read whole; the census row list); flow-text-slot-template.test.ts (the eight slots, eachValue on headers only, the header sources); flows-translation-face.test.ts (http.url / http.headers classified); builtin-node-config.test.ts (the ledger-declared map lists http).
  • service-automation: config-expression-ledger.test.ts (http declares its one value slot on the spec Zod channel alone); config-parse.test.ts, http-node-signing.test.ts, guard-refusal-inventory.test.ts re-judged.
  • lint: validate-expressions.test.ts (objectstack validate refuses each position; signingSecret left to the run); lint-flow-patterns.test.ts (the http literal hints, the contract's literal keys asked of the contract; the lint: flow-double-brace-interp / flow-bare-dollar-ref never reach an http node's request payload — the same stripRegions union that blinded the flow-template rules #16405 controls move to a CRUD filter, the one position S6 has not reached).

Ablations, each through scripts/ablation-replace.mjs (WRAP, trap on EXIT/INT/TERM; anchor x1 → x0; restored blob == HEAD blob, git diff HEAD empty):

# ablated red green after restore
1 the contract's url text-slot refusal the 2 url pins ✓
2 the contract's header text-slot refusal the 2 header pins ✓
3 the literal token refusal the 3 literal pins + the build-door pin ✓
4 the read-as-written marker on it the same 4 (the build doors stop reporting the token) ✓
5 the signingSecret token refusal its pin + the run-resolved pin ✓
6 the whole-headers token refusal its pin + the build-door pin ✓
7 the ledger's body row the 2 body-row pins + the census row list ✓
8 the two FLOW_NODE_TEXT_SLOTS http rows, through dist/: rebuild, ablation-dist-preflight.mjs @objectstack/spec 'http-ablated-s5' (marker present in 2 built files) exactly the 4 door pins: registerFlow url / header (http-slots.test.ts), objectstack validate url / header ✓ after rebuild; preflight --absent (absent from 232 built files, tree clean); http-slots.test.ts 21/21

Ablations 1–7 ran on spec src (those pins import src); 8 is the door, which service-automation and lint read through the spec's dist.

Verification record

Each suite through scripts/pm/os-verify-lock.sh (slot issue-19939-s5, a shared box). origin/main was merged once, through os-regen-merge.sh (ca951bcb5, from 9f4b652e3; no path overlapped, and the generated reference kept this branch's bytes); the last commit after it, 1105b4a06, is one spec test file.

  • At af1317fd1 (the head; test files only on 1105b4a06): metadata-protocol FULL "227 passed | 3 skipped (230) / 28190 passed"; lint FULL "137 / 6579"; spec local "653 / 19526 passed | 1 todo", repo "56 / 973"; cli unit 281 + the 2 build-dependent pins after the cli build; the cli e2e in its nightly tier "1 / 2"; typecheck exit 0 for lint, metadata-protocol, cli, spec; the turbo-affected closure against 9f4b652e3 (76 packages, 71 test tasks plus dogfood) all exit 0; the 120 derived gates accounted for (118 run, 2 NOT MEASURED); ESLint narrowed 44 files, 0 / 0. Size: 50 files, +1,566 / −366 against 9f4b652e3.
  • At 1105b4a06: spec --project local FULL: "653 passed (653) / 19526 passed | 1 todo"; the gate union (below); eslint narrowed (below).
  • At ca951bcb5 (the merge): the sa + lint + showcase dependency closure built ("Tasks: 60 successful, 60 total"); check:generated exit 0 (14 artifacts, none stale; check:api-surface and check:authorable-surface ✓); spec --project repo FULL "56 passed (56) / 973 passed"; spec --project local FULL "1 failed | 652 passed (653) / 19525 passed" — the one red was builtin-node-config.test.ts's ledger-declared key list, which this stage widens; fixed in 1105b4a06, and that file with flow.test.ts reads "250 passed" there; spec typecheck exit 0 (test layer 52 / 246 / 135 held); the http / notify / ledger set of service-automation "10 passed / 175 passed"; the lint set "7 passed / 796 passed"; @objectstack/example-showcase FULL "33 passed (33) / 409 passed".
  • At bef14eda3 (before the merge; the implementation unchanged since): service-automation FULL "189 passed (189) / 2481 passed"; lint FULL "137 passed (137) / 6579 passed"; spec --project repo FULL "56 / 973"; typecheck exit 0 for spec, service-automation (test layer 0 errors) and lint (test layer 2 / 6 / 2 held).
  • Ablations: the table above (src at 1105b4a06; the dist ablation at 1105b4a06).
  • The gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 1105b4a06 (no paths; "derived from the tree of 'objectstack-ai/objectstack' at commit 1105b4a", the --repo assertion holds, no stale-tree warning): 118 derived; 116 run, each exit 0; 2 NOT MEASURED: pnpm check:type-check-debt (the dispatch rules it out) and pnpm check:dual-build-cjs-loads (it needs the whole workspace's dist). --ran over the annotated record (COMMAND :: exit N): "118 derived, 116 run, 2 NOT-MEASURED, 0 UNRUN", exit 0. At the merge ca951bcb5 the same battery had one refusal, check:skill-examples exit 3 (PREREQUISITE NOT MET: the client SDK unbuilt); after that closure was built it exits 0. The five artifact rosters kept in a directory this diff touches each exit 0 at the head: check-changeset-fixed, check:meta-url-spelling, check:authz-resolver, check:error-code-casing, check:filter-alias-parity. The three PR-context guards (check-closing-target-claim, check-partof-closing-keyword, check-single-claim-paths) need this PR's number and body, so CI's runs on this PR are their record.
  • ESLint, narrowed: eslint --no-inline-config --format json over the 40 changed .ts / .mjs files of the net diff against 9f4b652e3, at 1105b4a06: exit 0; the JSON report holds 40 files, 0 errors, 0 warnings. The config enables no type-aware linting (eslint.config.mjs: every parserOptions is { ecmaVersion, sourceType }, no project / projectService), so no untouched file's verdict can move. The whole-tree pnpm lint is CI's.
  • Size: 46 files, +1,528 / −346 (1,874 changed lines, generated included), against 9f4b652e3.

Out of the claim's file surface

Each a mechanical consequence of the slots, or a stale sentence about them:

  • flow-node-expression-paths.ts (the body row) and schemaless-node-config.zod.ts (http joins the ledger-declared map); dropped-refinements.baseline.json (the spec build refused until automation/HttpConfig was declared: sites "" and body, 711 → 713).
  • 18.flow-builtin-node-config-values-refused.ts and its STEP18_RATIONALE fragment (they said http parses after interpolating).
  • Docblocks that named the old order or the http single brace: flow-template-token.ts, builtin-node-config.zod.ts, flow-value-slot-template.ts; parse-config.ts, template.ts, value-slot-map.ts, the service-automation README; lint credential-literal.ts, flow-walk.ts, validate-flow-template-paths.ts, validate-expressions.ts; service-messaging's http-outbox.ts and plugin-webhooks's webhook-headers.ts (comment-only, nothing published moves).
  • Test fixtures and pins outside the http tests: flow.test.ts, builtin-node-config.test.ts, flow-builtin-node-config-values.test.ts, flows-translation-face.test.ts, the lint credential-literal tests.
  • Fixtures the fix-round sweep found outside the kit, rewritten to their intent in af1317fd1: metadata-protocol's runtime-authoring-gate.flow-credential-literal.test.ts and cli's flow-credential-literal-doors.e2e.test.ts (the DARK controls, {{ api_token }} at the url and header), and lint's flow-walk.test.ts (the body a CEL envelope) and validate-flow-template-paths.test.ts (the url pin a {{ }} hole; the hand-off payload moved to an X-Billing-Amount header).
  • In-place fixes in the same sentences (the exemption's four conditions hold: the same stale single-brace teaching, mechanical, no other claim on the file, the same gate family): template.ts and the flows guide still named a notify recipient as single-brace, the README's sentence named recipients, and a ledger test comment counted S4's notify rows as six (three).

Acceptance notes

  • signingSecret is refused at the run, not at the build doors — the binding run-resolved order. So a Studio save of signingSecret: '{k}' is accepted, moves into the credential store, and every run of that node is refused (as a guard, nothing sent, the remedy naming the secret itself). Making it a build-door refusal would mean reading the marked token refusal ahead of RUN_RESOLVED_KEYS, which would also refuse a packaged flow whose credential-store row supplies the secret the run actually signs with; that is a contract decision for the seat, not taken here.
  • A body envelope that evaluates to null sends no body inline and {} to the outbox (payload: body ?? {}) — the same asymmetry an absent body had before; unchanged.
  • A CEL map literal holds one value type. {'id': record.id, 'status': 'done'} is refused at registerFlow by the CEL checker ("Map value uses wrong type, expected type 'dyn' but found 'string'"), loud, and the value-slot judge's own remedy names dyn(…); the docs, the describe, the changeset and the examples write the dyn form.
  • validate-flow-template-paths still path-checks a single-brace {record.x} in an http body, beside the value-slot judge's refusal of the same token; its single-brace reading goes in S7.
  • The lint's http literal keys are a local set, as S4's notify set is; a pin asks the contract which keys refuse a token as read-as-written and holds the set to it (the contract also lists headers, whose whole-map token it refuses; its values are text slots, where {{ }} is the spelling).
  • NOT MEASURED: pnpm check:type-check-debt (the dispatch rules it out: --re-measure plus an unlocked whole-workspace build) and pnpm check:dual-build-cjs-loads (it loads every published require entry, so it needs the whole workspace's dist; CI's); hotcrm at these keys (another repository; the dispatch carries it).
  • Carried by the seat, not here: the skill text (skills/objectstack-automation/SKILL.md:236–:241 lists http among the slots that keep the single brace) to skills(objectstack-automation): SKILL.md still teaches {$User.<path>} as a kept value-slot spelling and the text-slot run-user remedy through it, which #19939 pass 2 (PR #22563) refuses #22585; the objectui designer's http pickers to an objectui companion card.

Generated by Claude Code

…value slot, the rest literals

Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/lint, @objectstack/plugin-webhooks, @objectstack/service-automation, @objectstack/service-messaging, @objectstack/spec, touching 41 documentable anchor(s). ⚠️ 12 changed file(s) yielded no anchor (packages/lint/src/credential-literal.ts, packages/lint/src/validate-flow-template-paths.ts, packages/plugins/plugin-webhooks/src/webhook-headers.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/client-sdk.mdx (via automation.trigger (sdk, the route ledger binds it to POST /automation/trigger/:name, selected by route anchor /trigger/:name))
  • content/docs/api/plugin-endpoints.mdx (via /trigger/:name (route, bridged from symbol timeoutMs — its route source's handler names it))
  • content/docs/automation/approvals.mdx (via actorId (literal, a string literal in LITERAL_KEYS; a string literal in NOTIFY_LITERAL_KEYS))
  • content/docs/automation/flows.mdx (via timeoutMs (symbol, a field of const object HTTP_LITERALS; a field of const object HTTP_LITERAL_AS_WRITTEN), actorId (literal, a string literal in LITERAL_KEYS; a string literal in NOTIFY_LITERAL_KEYS), signingSecret (literal, a string literal in LITERAL_KEYS; a string literal in httpConfigRefusals), sourceObject (literal, a string literal in LITERAL_KEYS; a string literal in NOTIFY_LITERAL_KEYS), timeoutMs (literal, a string literal in HttpConfigSchema; a string literal in LITERAL_KEYS))
  • content/docs/automation/hook-bodies.mdx (via timeoutMs (symbol, a field of const object HTTP_LITERALS; a field of const object HTTP_LITERAL_AS_WRITTEN), timeoutMs (literal, a string literal in HttpConfigSchema; a string literal in LITERAL_KEYS))
  • content/docs/automation/jobs.mdx (via timeoutMs (symbol, a field of const object HTTP_LITERALS; a field of const object HTTP_LITERAL_AS_WRITTEN), timeoutMs (literal, a string literal in HttpConfigSchema; a string literal in LITERAL_KEYS))
  • content/docs/automation/webhooks.mdx (via timeoutMs (symbol, a field of const object HTTP_LITERALS; a field of const object HTTP_LITERAL_AS_WRITTEN), timeoutMs (literal, a string literal in HttpConfigSchema; a string literal in LITERAL_KEYS))
  • content/docs/concepts/metadata-lifecycle.mdx (via signingSecret (literal, a string literal in LITERAL_KEYS; a string literal in httpConfigRefusals))
  • content/docs/deployment/environment-variables.mdx (via timeoutMs (symbol, a field of const object HTTP_LITERALS; a field of const object HTTP_LITERAL_AS_WRITTEN), timeoutMs (literal, a string literal in HttpConfigSchema; a string literal in LITERAL_KEYS))
  • content/docs/protocol/kernel/lifecycle.mdx (via timeoutMs (symbol, a field of const object HTTP_LITERALS; a field of const object HTTP_LITERAL_AS_WRITTEN), timeoutMs (literal, a string literal in HttpConfigSchema; a string literal in LITERAL_KEYS))

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

  • content/docs/releases/v16.mdx (via timeoutMs (symbol, a field of const object HTTP_LITERALS; a field of const object HTTP_LITERAL_AS_WRITTEN), timeoutMs (literal, a string literal in HttpConfigSchema; a string literal in LITERAL_KEYS))
  • content/docs/releases/v17/17-0.mdx (via timeoutMs (symbol, a field of const object HTTP_LITERALS; a field of const object HTTP_LITERAL_AS_WRITTEN), timeoutMs (literal, a string literal in HttpConfigSchema; a string literal in LITERAL_KEYS))
  • content/docs/releases/v17/17-3.mdx (via automation.trigger (sdk, the route ledger binds it to POST /automation/trigger/:name, selected by route anchor /trigger/:name), /trigger/:name (route, bridged from symbol timeoutMs — its route source's handler names it))
  • content/docs/releases/v17/17-4.mdx (via timeoutMs (symbol, a field of const object HTTP_LITERALS; a field of const object HTTP_LITERAL_AS_WRITTEN), timeoutMs (literal, a string literal in HttpConfigSchema; a string literal in LITERAL_KEYS))
  • content/docs/releases/v17/17-5.mdx (via timeoutMs (symbol, a field of const object HTTP_LITERALS; a field of const object HTTP_LITERAL_AS_WRITTEN), timeoutMs (literal, a string literal in HttpConfigSchema; a string literal in LITERAL_KEYS))
  • content/docs/releases/v17/17-6.mdx (via signingSecret (literal, a string literal in LITERAL_KEYS; a string literal in httpConfigRefusals))
  • content/docs/releases/v17/17-7.mdx (via timeoutMs (symbol, a field of const object HTTP_LITERALS; a field of const object HTTP_LITERAL_AS_WRITTEN), actorId (literal, a string literal in LITERAL_KEYS; a string literal in NOTIFY_LITERAL_KEYS), signingSecret (literal, a string literal in LITERAL_KEYS; a string literal in httpConfigRefusals), timeoutMs (literal, a string literal in HttpConfigSchema; a string literal in LITERAL_KEYS))

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
  • 12 changed file(s) yielded no anchor (packages/lint/src/credential-literal.ts, packages/lint/src/validate-flow-template-paths.ts, packages/plugins/plugin-webhooks/src/webhook-headers.ts, …) — pages documenting those are invisible to this run
  • 8 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 — 139 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 c5b43b606807e26d873e2407cbd38a844e624895 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json c5b43b606807e26d873e2407cbd38a844e624895

⚠️ 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 c5b43b606807e26d873e2407cbd38a844e624895 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…e kit

The S5 contract refuses a single-brace token at an http url, a header value
and the body. A repository sweep found seven fixtures outside the kit that
still wrote one there:

- the two DARK credential controls (metadata-protocol save door, cli doors)
  now write `{{ api_token }}` at the url and the header;
- the lint scanner's template control header is `{{ run_token }}`;
- the flow-walk region pin's http body is a CEL value envelope;
- the template-path http url pin is a `{{ }}` hole, and the hand-off
  payload moves from the body (a value slot no template reaches) to an
  `X-Billing-Amount` header text slot;
- the spec approval flow's http body is a CEL envelope, now pinned against
  HttpConfigSchema beside the CRUD contracts.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1105b4a069d6071f3947345639a583245d0332a3
Local-runs: none

Read at 2026-10-12T15:03Z by the isolated at-tier reviewer of domain:spec seat 1 (#6017), on PR #23062 (Part of #19939, pass 4 stage S5). Head confirmed: git rev-parse of 1105b4a069 on the fetched branch equals the PR's head.sha; the net diff is against the merge base 9f4b652e3 (46 files, +1,528 / -346); the PR read draft, mergeable_state blocked at the first read and unknown at the second (a recompute after the red named below). Inputs: card #19939's body and all 63 comments (the stage plan 6096263424, the S4 seat order 6117306001, the claim 6122880753, the dev report 6126394968, the ACCEPT 6126438722 included), the PR body, file list and diff, and the head's check-runs read twice. Two spellings are written in words below because a double-brace opener in a comment is read as a stamp token: "double-brace hole" is the text-slot dialect of ADR-0032 Decision 3, "single-brace token" the retired 17.x dialect.

Verdict in one line: the contract judgments ①②③ stand, and the head is refused on one count — a dependent pin in packages/metadata-protocol writes the retired single-brace spelling at an http url and header and expects the save door not to refuse it; this diff retires exactly that and did not re-judge the fixture, the census row that claims every test file missed it, and Test Core (5/6) is red on that package while the base is green. One fixture rewrite and a run over the dependents is the patch round.

① Derived judgments

Each accept-set or public-surface change the diff implies, judged against the tree at the head and at the base (read with git show / git grep; nothing run):

  1. url and every headers value become text slots (double-brace holes); a single-brace token there is refused by the contract, at registerFlow, at objectstack validate, at the metadata save door and by the executor's parse. Right. The stage plan's carry line binds it (6096263424: "that makes notify.actionUrl, http.url and http.headers.* text slots"). HttpConfigSchema's superRefine composes the one text-slot judge (textSlotTemplateRefusal) at the url and per header value; FLOW_NODE_TEXT_SLOTS gains the two rows, the headers row flagged eachValue. Every non-test reader of that roster at the head goes through flowNodeTextSlotSources (lint validate-expressions.ts:2492, lint-flow-patterns.ts:2047, engine.ts:10850) or strips by key (withoutTextSlots), so the new map-valued row is judged at every door and skipped at none. The $runId the examples and the whole-map remedy write is in the engine's bound $ roots (flow-text-slot-template.ts:137). Census of shipped surfaces: git grep at 9f4b652e3 finds no single-brace token at url or at any header in examples/**, content/docs/** (references and releases excluded), docs/**, packages/qa or packages/platform-objects.
  2. body becomes a value slot (FlowValueSlotSchema): a CEL envelope evaluated once before either mode, a literal sent as written, a single-brace token in any string of a literal refused, a malformed envelope refused. Right. FLOW_NODE_EXPRESSION_PATHS gains the http.body (value) row with every older row pinned byte-identical; HttpConfigSchema joins LEDGER_DECLARED_NODE_CONFIG_SCHEMAS, so the marker reaches the reconciliation ratchet on one channel (the descriptor offers body with no marker, pinned in config-expression-ledger.test.ts). celValueSlotSchema is z.unknown().superRefine(…), so the HttpConfig TS type of body stays unknown and the published JSON Schema stays {} plus the xExpression: 'value' marker and description — an annotation, not a wider accept set — while the runtime refuses more, which is exactly why automation/HttpConfig enters the dropped-refinements ledger at "" and body (711 → 713). The executor's call resolveValueSlot(engine, cfg.body, variables, 'body', context) matches the signature at the head. Census: five tokens in four shipped bodies (showcase flows/index.ts at 1059, 1239, 1444; flows.mdx at 464), all migrated in this diff; git grep at the base finds the same bodies. A CEL map literal that mixes a literal with a field read needs dyn(…); the examples, the describe, the docs and the changeset write it.
  3. method, durable, timeoutMs and signingSecret are literals read as written; a single-brace token or a CEL envelope there is refused with the literal's remedy. Right under S4's answer B (6117306001) applied per key: zero shipped tokens at all four at the base (corroborated by git grep), the only sites being tests that pinned the interpolation itself, each re-judged in this diff. The refusals at method, durable, timeoutMs and at a token written as the whole headers map carry READ_AS_WRITTEN_PARAM, so the build doors' value arm reports them as the run would (S4's mechanism, unchanged). The refinement runs with a when that is always true, so a typed literal's token draws the type's refusal beside the remedy at the executor and the doors report the marked one — pinned (['custom', 'invalid_type']).
  4. signingSecret: the credential channel and RUN_RESOLVED_KEYS are byte-identical; the token refusal is the executor's parse alone and never echoes the value. Right — the order is judged in ③. Confirmed at the head: FLOW_CREDENTIAL_CLEARED is '', so a cleared secret never reads as a token; a held store row replaces the authored value before the parse (http-nodes.ts, the credential channel); parseNodeConfig renders path: message and no input (parse-config.ts:103), and HTTP_SIGNING_SECRET_TOKEN_REFUSAL names no value. The 17.x "set but resolved to no value" refusal goes with the interpolation it guarded.
  5. The executor parses as authored after the credential channel, renders the url and headers and evaluates the body once before either mode, and no longer imports interpolate; PARSED_AFTER_INTERPOLATION and its line in builtinValueJudged are deleted. Right: the set had one member and would be a check that can never fire; the durable enqueue is pinned to carry the rendered url, headers and payload with nothing left to render at delivery. The three ?? that remain are the documented absent-key defaults (method per mode, payload), not tolerance of an off-spec shape, so Prime Directive Add comprehensive test suite for Zod schema validation #12 holds.
  6. A header that renders to no text is not sent (17.x sent it empty); a url that renders to no text refuses the node. Right and declared: pinned in http-slots.test.ts, stated in the changeset's FROM → TO table and in the text-slot D3 entry's acceptance criteria.
  7. Public surface. The diff adds or removes no export in any package (checked over the whole diff), and the api-surface/ snapshot is rightly untouched. FlowNodeTextSlot (interface), HttpConfigSchema (const) and LEDGER_DECLARED_NODE_CONFIG_SCHEMAS (const) are published (api-surface/automation.json), so the two widenings the Clause-② line names — the optional eachValue member and the http key — are real published-surface changes. The pinned objectui sibling (9646e2ed2b) reads LEDGER_DECLARED_NODE_CONFIG_SCHEMAS only through a union and a lookup (flow-node-config.contractKeys-11968.test.tsx), never a pinned key set, and reads neither FLOW_NODE_TEXT_SLOTS nor FlowNodeTextSlot: nothing is removed and nothing the sibling imports moves; the Console Pin Gate's skipped on this head is its paths filter (the pin did not move), not a missing reading.
  8. ADR-0087: D3 only, no D2, no tombstone. Right: no key is removed; the four step-18 entries are amended in the diff; the no-D2 reasons (a Date JSON-quoted vs ISO text; an absent value writing null or nothing vs a CEL fault; a whole token at a typed key resolving to its type vs text) are the reasons the earlier text-slot and value-slot stages recorded.
  9. Generated artifacts. The .describe() restatements regenerate content/docs/references/automation/io-node-config.mdx (in the diff); json-schema.manifest/ lists schema names, not contents, so it is rightly unmoved; the authorable key set of HttpConfig is unchanged; Type Check · consumer gates (check:docs, check:api-surface, check:exported-any) and the aggregate TypeScript Type Check are success on the head.
  10. Lint. NOTIFY_LITERAL_* become the per-node-type LITERAL_KEYS / LITERAL_HINTS (non-exported); the http literal set is pinned against the contract ("asked of the contract, never listed"); objectstack validate refuses each position through the shared judges and the contract with no rule change. Right.
  11. The retired spelling's sites in the repository's tests — WRONG in one row, and the one count this head is refused on. The PR's census claims every test file and lists, for url, four sites and, for headers.*, two plus the lint's credential-literal fixtures; the PR body says "test fixtures that wrote the retired spellings at these keys are re-judged". packages/metadata-protocol/src/runtime-authoring-gate.flow-credential-literal.test.ts is missing from that row: its DARK case (line 39, url: 'https://example.invalid/hook?api_key={api_token}'; line 42, Authorization: 'Bearer {api_token}') saves an active flow through evaluateRuntimeAuthoringGate and asserts verdict.error is null — "the publish is NOT refused". On this head that pin cannot pass, by construction, not by flakiness: the gate runs runRuntimeAuthoringRules (@objectstack/lint/runtime), whose EXPRESSION_INVALID rule runs for a flow write (authoring-rules.ts:684–686, runStackExpressionPasses), and that pass now refuses a single-brace token at an http url and at each header value at error through the text-slot judge — the diff's own validate-expressions.test.ts pins exactly that — and an error finding is the gate's refusal (runtime-authoring-gate.ts, "errors refuse, warnings advise"). The diff rewrote the same two controls in the lint's own lint-flow-credential-literals.test.ts to double-brace holes and left this sibling, in a package the diff does not touch but which depends on @objectstack/spec and @objectstack/lint; the dev report's tests field lists spec, service-automation, lint and the showcase, no dependent, so the closure turbo ls --affected names was not run. The red is at the SAVE door, a door this stage claims. Remedy, mechanical: rewrite the DARK fixture's url and Authorization to the double-brace spelling (the connector input's clientSecret: '{api_token}' is not an http position and stays), run the affected closure (or pnpm test, Post-Task Checklist 1), and correct the census row and the "re-judged" sentence in the PR body. No contract text changes.

Wrong: item 11. Items 1–10 stand and carry to the next head's record unchanged unless the patch round moves them.

② Semver level

  • .changeset/19939-flow-http-slots.md: @objectstack/spec major, @objectstack/service-automation major, @objectstack/lint patch — matches what the diff publishes: an accept-set narrowing on a published authoring contract (HttpConfigSchema) and on the executor (a template secret, a token at any key and a malformed envelope now refused; a header that renders empty omitted), on the v18 line (.changeset/pre.json is mode: pre, tag: next at the base); the lint ships hint text and a per-node literal map only, its refusals coming from the spec. The levels equal stages S3 (19939-flow-collection-value-slots.md) and S4 (19939-flow-notify-value-slots.md) at the base. No skip-changeset label, rightly.
  • Clause-②: yes (narrowing: …; widening: …): the arm word is the first token in the parenthetical, so clause2-line.mjs reads yes with the narrowing arm — breaking, major — the shape S2 ruled and S3/S4 wrote. The yes is true on the published FlowNodeTextSlot member and the LEDGER_DECLARED_NODE_CONFIG_SCHEMAS key; the third item it names (the JSON Schema body marker) is an annotation on an unchanged {} schema rather than a wider accept set — a wording nit that moves neither the arm nor the level. The same line sits in the PR body and in the changeset body, where check-adr-0087-registration reads it.
  • ADR-0087 marker: not-required (already-registered …) naming the four step-18 ids, each amended in this diff, with the no-D2 reasons stated; no key removed, so no tombstone. The breaking changeset carries its FROM → TO table and the one-line fix (Post-Task Checklist 3). Check Changeset and Lint & Repo Gates are success on the head.
  • Comment-only touches in @objectstack/service-messaging (a TSDoc on HttpDelivery.headers) and @objectstack/plugin-webhooks (webhook-headers.ts) publish no behaviour or surface and owe no changeset entry; examples/app-showcase is private. The patch round's fixture edit publishes nothing and changes none of this.

③ Boundary flags

Dev report 6126394968: open_questions is empty. Every out_of_scope_findings entry, every deviations entry and the PR's Acceptance notes, answered:

  • The signingSecret order — the token refused only at the executor's parse. Judged RIGHT and not escalated; the maintainer's veto window stands as it did for S4's answer B. The binding text keeps the key's run-resolved resolution exactly as it is and forbids a template or value slot there: the resolution that stays is the write-only credential channel (holdsFlowCredential / resolveFlowCredential, RUN_RESOLVED_KEYS), all byte-identical, and the only other reading — keeping the single-brace interpolator alive on this one key — contradicts the dispatch's own A3 (the executor no longer calls interpolate) and S7's resolver deletion. Leaving the key to the run is the pre-existing service-automation: a built-in node's config value its own contract refuses still registers, then fails every run — the built-in half of #21848's class #21898 carve-out, not a new choice: the build doors did not judge this key at 17.x either, so no door regresses, and the one accept-set change is at the run and fail-closed (a guard, nothing sent, config.signingSecret named, the remedy naming the secret itself or '', no value echoed) — louder than 17.x, where a token resolving to nothing was also caught only at the run. The residual the dev names is real and bounded: a Studio save of a single-brace token at this key is accepted into the credential store and every run of that node refuses, loudly; refusing at the save door would mean reading the marked refusal ahead of RUN_RESOLVED_KEYS and would also refuse a packaged flow whose store row supplies the real secret — a contract decision outside this stage, correctly left in the Acceptance notes for the seat. A follow-up the seat may take without touching the run-resolved order: a warn-level lint hint at that key.
  • Skill text skills/objectstack-automation/SKILL.md:236–241 lists http among the single-brace slots. Right not to touch: skills/** is Tier H and one path would make this PR Tier H; the stage plan assigns the pointer to skills(objectstack-automation): SKILL.md still teaches {$User.<path>} as a kept value-slot spelling and the text-slot run-user remedy through it, which #19939 pass 2 (PR #22563) refuses #22585 at each landing. Carried by the seat.
  • objectui designer: the url picker writes single-brace tokens; body is stored as a JSON string. Carried to the objectui companion per the stage plan (objectui#12101 is the route the ACCEPT names). For the carry: a JSON-string body was already broken silently at the 17.x run (the interpolator read it as a token); it is loud at save now, the right direction. Not a Console Pin Gate matter (nothing the sibling imports moves; ① 7).
  • A CEL map literal needs dyn(…). A property of the CEL checker, refused loudly at registerFlow with the value-slot remedy naming dyn(…); the examples, describe, docs and changeset write it. No action.
  • hotcrm NOT MEASURED at the http keys. Another repository; it joins the existing hotcrm carry on this card (stage plan: recorded on seat post [PM seat] domain:spec · seat 2 — 🟢 zhuangjianguo · session_019oFY6Hi35J7atvgVFnmoaK #18549's ledger). No action here.
  • Files beyond the claim's surface. Each is named in the PR body with its reason, and each is a mechanical consequence of the slots or a stale sentence about them (the ledger body row, the ledger-declared map, the dropped-refinements ledger, the builtin-values D3 entry and its STEP18_RATIONALE fragment, docblocks, two comment-only sibling-package edits, test fixtures). Accepted. The patch round adds one more file of the same class, in packages/metadata-protocol, and the PR body names it the same way.
  • In-place fixes under the exemption (template.ts and flows.mdx still called a notify recipient single-brace; the README named recipients; a ledger test comment counted S4's notify rows as six). Read in the diff: the same stale single-brace teaching S4 retired, mechanical, same gate family. Accepted.
  • Two sentences at the executor for a token at a typed literal (a when that is always true). The doors report the marked one, once; the executor's refusal naming both the type and the remedy is loud, not wrong. Accepted; pinned.
  • A header that renders to no text is omitted. Declared in the changeset and the D3 entry; pinned. Accepted (① 6).
  • PR-context guards run locally while the body says CI's runs are the record. CI's three guard check-runs are success on the head. Resolved.
  • No base probe through the engine. The 17.x behaviour is cited from the base's own pins, which the diff removes in its minus lines (config-parse.test.ts "parses the INTERPOLATED config", http-node-signing.test.ts "signs with a secret that arrives through a template" and the two "resolved to nothing" cases). Accepted.
  • Acceptance notes: validate-flow-template-paths still path-checks a single-brace token in an http body beside the judge's refusal (S7 takes its single-brace reading; a second located error, never a silent pass); a body envelope evaluating to null sends no body inline and {} to the outbox (pre-existing, unchanged); the lint's literal set is pinned to the contract; check:type-check-debt and check:dual-build-cjs-loads, NOT MEASURED locally, are CI's — Type Check · debt ledger and Type Check · consumer gates are success on the head.

Check-runs on the head (41 at the final read, the CI run complete, nothing in progress): 34 success — the aggregate TypeScript Type Check and its four Type Check · jobs, Lint & Repo Gates, Build Core, Dogfood Regression Gate, Governed Surface Queue Guard, Check Changeset, the three PR-context guards, Test Core (1/6), (2/6), (3/6), (4/6), (6/6) and the rest included — 5 skipped (Console Pin Gate, Dogfood Verify CLI, the dogfood shard matrix entry, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL) — paths filters), and 2 failure: Test Core (5/6) and, through it, the required aggregate Test Core. The failure's signature, read from the check-runs API's annotations: failed step "Run this shard's tests", command (packages/metadata-protocol) pnpm run test exited (1), then "Process completed with exit code 1". The step's own log is not readable from this container (the Actions log host is outside its egress policy, refused on the fetch), so the first error line is not quoted here; ① 11 names the pin that is red on this head by construction, which the seat's patch round can confirm against the log in one line. The merge base 9f4b652e3 and current main e49aaec733 both carry Test Core success, so this red shares no signature with the base and counts. Earlier reads of the same head: 25 success, 5 skipped, 9 in progress, 0 failure at the first; 31 success, 5 skipped, 3 in progress, 1 failure at the second.

No governed path in the file list; 1,874 changed lines; Part of #19939 is the body's first line and the part-of guard is success.

Implemented-by: claude/issue-19939-p4-s5-http
Reviewed-by: session_019G5ZnRrTMiwcHG6iZM19zf

VERDICT: FAIL


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: af1317fd10f1e1cb17d78931cf8a632e60724658
Local-runs: none

Read at 2026-10-12T19:27Z by the isolated at-tier reviewer of domain:spec seat 1 (#6017), on PR #23062 (Part of #19939, pass 4 stage S5), the head after the FAIL record 6126921434 on 1105b4a069. Head confirmed: git rev-parse of af1317fd1 on the fetched branch equals the PR's head.sha; the net diff is against the merge base 9f4b652e3 (50 files, +1,566 / -366, 1,932 changed lines; no governed path; head repo is the base repo); the PR read draft, mergeable: true, mergeable_state unstable at the first read and clean at the second, no auto-merge. Inputs: card #19939's body and all 65 comments (the stage plan 6096263424, the S4 seat order 6117306001, the claim 6122880753, both dev reports 6126394968 and 6130859286, the ACCEPT 6126438722 and the fix-round acceptance 6130920034 included), the PR body as corrected by the seat, its file list, the net diff, the delta 1105b4a069..af1317fd1, the previous record (whose judgments are re-checked below, not adopted), and the head's check-runs read twice. Two spellings are written in words below because a double-brace opener in a comment is read as a stamp token: "double-brace hole" is the text-slot dialect of ADR-0032 Decision 3, "single-brace token" the retired 17.x dialect.

Verdict in one line: the delta is test files only and every rewritten fixture keeps its intent (the two DARK credential controls included, which now also pin that the v18 spelling passes the save door); the previous record's items 1 to 10 hold on this head's net diff, its one FAIL count is discharged by the fixture rewrite and by Test Core (5/6) green; the semver level and the Clause-② line are unchanged and right; every flag in the fix-round report is answered. PASS.

① Derived judgments

The delta 1105b4a069..af1317fd1, verified test-only. Read on the fetched branch and through the REST compare: one commit, 6 files, +38 / -20, every path a *.test.ts or *.e2e.test.ts. The non-test net diff against 9f4b652e3 is byte-identical between the two heads (each head's diff against the base with test files excluded, compared line by line, differs nowhere), so every accept-set and public-surface change on this head is the one the previous record judged, re-read below. Four of the six files are new to the PR (46 → 50 files): packages/cli/test/flow-credential-literal-doors.e2e.test.ts, packages/lint/src/flow-walk.test.ts, packages/lint/src/validate-flow-template-paths.test.ts, packages/metadata-protocol/src/runtime-authoring-gate.flow-credential-literal.test.ts; each is named in the PR body's out-of-surface list with its reason.

Each rewritten fixture, judged for its intent:

  • The two DARK credential controls (metadata-protocol save door, :43 / :46; cli doors e2e, :73 / :76): the url's api_key query value and the Authorization header become double-brace holes over api_token; the connector input's clientSecret keeps its single-brace token (not an http key; connector-nodes.ts hands the input on raw, and nothing judges it). Intent kept, and stronger on one half. The scanner's literal predicate (credential-literal.ts, PLATFORM_TEMPLATE is a single-brace pattern over a brace-free inside) matches the inside of a double-brace hole exactly as it matched the single-brace token, so "a templated value draws no flow-credential-literal advisory" is unchanged in substance, and credential-literal.test.ts still pins the single-brace token as not a literal at the unit level, so the 17.x reading is not lost. The verdict.error null assertion now pins what the previous head could not pass: the v18 spelling clears the save door's expression pass (api_token is a plain root, so the text-slot judge admits the hole; the unbound-$-root refusal does not apply). The LIT twin is untouched and still draws its three advisories. The metadata-protocol test runs in Test Core (5/6), success on this head; the cli e2e is nightly-tier by filename (packages/cli/vitest-tiers.ts: *.e2e.test.*) and not in the PR run — its fixture is the same shape as the one CI ran, and its green is the dev's local reading, recorded in ③.
  • lint-flow-credential-literals.test.ts:84 — the scan control header X-Auth-Token becomes a double-brace hole: same predicate, same verdict; the test's connector token control keeps its single-brace token. Not weakened.
  • flow-walk.test.ts:165 — the http body becomes a CEL envelope; the pin is that walkFlowNodes keeps ['body', 'url'] on an http node out of the loop region strip, copy-on-write identity included; the body's content is not read. Intent kept.
  • validate-flow-template-paths.test.ts — :181: the freeform http: block url becomes a double-brace hole and the pin still asserts one FLOW_TEMPLATE_UNKNOWN_FIELD finding (the rule's templateRefsIn reads both spellings, :220 to :224 at the head). The billing hand-off (:445 to :510) and the nested-loop pin (:567) move the templated payload from body to an X-Billing-Amount header text slot, the two injections and CLEAN_PAYLOAD written as holes; the four pins (two filter negative controls, unknown field, lookup traversal) keep their assertions on the findings array. What this relocates: the test no longer exercises the rule reading an http body through the region-stripped view. That half is flow-walk.test.ts's pin above, and the rule reads walked.localConfig through the same walkFlowNodes (validate-flow-template-paths.ts:606, :638), so the composition is covered: the walk keeps body, the rule scans every string leaf of the kept config. A template in an http body is refused by the value-slot judge at every door now — a double-brace hole in a literal body too, since the judge reads its inner braces as a token (lint-flow-patterns.test.ts:2416) — so a header is the position where this pin stays meaningful past S7. Judged right; the Acceptance note that the rule, called directly, still path-checks a single-brace token in an http body stays true and goes to S7.
  • flow.test.ts:492 — the approval flow's http body becomes a CEL envelope with dyn(…) on both values, and a new pin parses that node's config against HttpConfigSchema beside the CRUD contracts: additive. The old single-brace body was never refused by FlowSchema.parse (a body token is the value-slot judge's at registerFlow and objectstack validate, pinned in io-node-config.test.ts), so this is the sweep writing the v18 spelling into a teaching fixture, not a pin relaxed.

The previous record's items 1 to 10, re-checked on this head (each against the net diff and the head tree with git show and git grep; nothing run):

  1. url and every headers value are text slots; a single-brace token there is refused by the contract, at registerFlow, at objectstack validate, at the metadata save door and by the executor's parse. Holds. HttpConfigSchema's superRefine(httpConfigRefusals), run with a when that is always true, composes textSlotTemplateRefusal at url and at each string value of headers; FLOW_NODE_TEXT_SLOTS gains http.url and http.headers with eachValue: true, and flowNodeTextSlotSources yields each header at headers.X-Ref and nothing when the key holds no map (pinned). Every non-test reader of the roster at the head goes through flowNodeTextSlotSources (validate-expressions.ts:2492, lint-flow-patterns.ts:2047, engine.ts:10850) or strips by key (withoutTextSlots, lint-flow-patterns.ts:875), so no reader treats the map row as one string. $runId, the remedy's and the docs' example root, is in FLOW_ENGINE_VARIABLES. The save door's coverage of a header VALUE is the path that reddened the previous head (a 422 at config.headers.Authorization), confirmed from the other side now by Test Core (5/6) green.
  2. body is a value slot. Holds. body: FlowValueSlotSchema.optional(), and celValueSlotSchema is z.unknown().superRefine(…), so HttpConfig['body'] stays unknown and the published JSON {} gains only the xExpression: 'value' marker and the description; the ledger row http.body (value) is appended with every older row pinned byte-identical; LEDGER_DECLARED_NODE_CONFIG_SCHEMAS gains http; the executor's resolveValueSlot(engine, cfg.body, variables, 'body', context) matches the head's signature. The three showcase bodies and the flows guide's example are migrated with dyn(…).
  3. method, durable, timeoutMs and signingSecret are literals read as written. Holds under S4's answer B per key: the token refusals at method, durable, timeoutMs and at a token written as the whole headers map carry READ_AS_WRITTEN_PARAM, an envelope at a literal is refused through the type's error map, and the refinement runs with when always true, so a typed literal's token draws the type's refusal beside the remedy and the doors report the marked one (pinned ['custom', 'invalid_type']).
  4. signingSecret: the credential channel and RUN_RESOLVED_KEYS are byte-identical; its token refusal is the executor's parse alone and never echoes the value. Holds. RUN_RESOLVED_KEYS is { http: ['signingSecret'] } unchanged, builtinValueJudged returns false for an issue at that path, HTTP_SIGNING_SECRET_TOKEN_REFUSAL is a constant with no value interpolated, and the executor's channel lines are comment-only in the diff; pinned both ways (registerFlow accepts, the run refuses, nothing sent).
  5. The executor parses as authored after the channel, renders the url and headers and evaluates the body once before either mode, and no longer imports interpolate; PARSED_AFTER_INTERPOLATION and its line in builtinValueJudged are deleted. Holds: the import line is swapped for renderTextSlot and resolveValueSlot; the order is parse → renderTextSlot(cfg.url) → renderHeaders → resolveValueSlot(body) → durable or inline; the enqueue is pinned to carry the rendered request with no opener left. The remaining ?? are the documented per-mode method default and payload: body ?? {} (Prime Directive Add comprehensive test suite for Zod schema validation #12 holds).
  6. A url that renders to no text refuses the node; a header that renders to no text is not sent. Holds (if (!url) return refuseNode(…); renderHeaders skips an undefined render), pinned, declared in the changeset and the text-slot D3 entry.
  7. Public surface. Holds: no export line is added or removed anywhere in the net diff; FlowNodeTextSlot, HttpConfigSchema, LEDGER_DECLARED_NODE_CONFIG_SCHEMAS, FLOW_NODE_TEXT_SLOTS, flowNodeTextSlotSources and FlowValueSlotSchema are listed in api-surface/automation.json at the head, which is rightly unmoved (it records existence, not shape); the two widenings are additive (an optional member, a new key). The pinned objectui (9646e2ed2b, .objectui-sha unchanged in the diff): at that sha the readers of these names are tests that read LEDGER_DECLARED_NODE_CONFIG_SCHEMAS as a plain record and parse token-free http configs through HttpConfigSchema (flow-node-config.contractKeys-11968.test.tsx, flow-canvas-seeds.spec-parse.test.tsx at key level), so nothing the sibling imports moves; Console Pin Gate is skipped by its paths filter.
  8. ADR-0087: D3 only, no D2, no tombstone. Holds: the four step-18 entries and the STEP18_RATIONALE fragment are amended in the diff; no key is removed; the no-D2 reasons are stated in the changeset marker.
  9. Generated artifacts. Holds: the reference page is regenerated in the diff and Type Check · consumer gates is success. The dropped-refinements.baseline.json row automation/HttpConfig at "" and body (226 → 227 schemas, 711 → 713 sites) is the ledger's declared, hand-edited growth — its description says it refuses undeclared growth and that adding a site is a decision — for the superRefine on the whole object and the body's value-slot refinement, the same shape as automation/NotifyConfig's row at the base from S4. A visibility ledger, not an accept set.
  10. Lint. Holds: LITERAL_KEYS / LITERAL_HINTS per node type, non-exported; the http literal set pinned against the contract ("asked of the contract, never listed"); the lint: flow-double-brace-interp / flow-bare-dollar-ref never reach an http node's request payload — the same stripRegions union that blinded the flow-template rules #16405 controls moved to a CRUD filter; objectstack validate refuses each position through the shared judges and the contract with no rule change.
  11. The retired spelling's sites in the repository's tests — the previous record's one WRONG row — is discharged on this head: the fix round's structural sweep (every object typed http, ternary branches and helper arguments followed) found 13 strings in 6 files outside the refusal pins and rewrote all of them (the delta above); the PR body's census rows and its "re-judged" sentence now list each with a dagger.

Wrong: none on this head.

② Semver level

  • .changeset/19939-flow-http-slots.md: @objectstack/spec major, @objectstack/service-automation major, @objectstack/lint patch — unchanged since the previous head and matching what the diff publishes: an accept-set narrowing on a published authoring contract (HttpConfigSchema) and on the executor, on the v18 line (.changeset/pre.json is mode: pre, tag: next at the head); the lint ships hint text and a per-node-type literal map, its refusals coming from the spec's rosters and contract that it reads — the same levels S3 (19939-flow-collection-value-slots.md) and S4 (19939-flow-notify-value-slots.md) shipped on this line. No skip-changeset label, rightly.
  • Clause-②: yes (narrowing: …; widening: …) — byte-identical in the PR body and the changeset body. The arm word is the first token in the parenthetical, so the one reader (scripts/pm/clause2-line.mjs) reads yes with the narrowing arm: breaking, major — the shape ruled at S2 (6109208049) and S3 (6112829060). The yes is true on the published FlowNodeTextSlot member and the LEDGER_DECLARED_NODE_CONFIG_SCHEMAS key; the third item it names (the JSON Schema body marker) is an annotation on an unchanged {} schema, a wording nit that moves neither the arm nor the level.
  • ADR-0087 marker: not-required (already-registered …) naming the four step-18 entry ids as the entries spell them, each amended in this diff, with the no-D2 reasons; the FROM → TO table and the one-line fix are present (Post-Task Checklist 3). Check Changeset and Lint & Repo Gates are success on the head.
  • Comment-only touches in @objectstack/service-messaging and @objectstack/plugin-webhooks publish nothing and owe no entry; examples/app-showcase is private; the delta publishes nothing and changes none of this.

③ Boundary flags

  • The previous record's FAIL count — discharged. The metadata-protocol DARK fixture is rewritten in af1317fd1 (① above); Test Core (5/6) and the aggregate Test Core are success on this head, so the previous signature (the shard's "Run this shard's tests" step, packages/metadata-protocol pnpm run test exited 1) does not recur; the census row for url and headers.* and the "re-judged" sentence are corrected in the PR body; the dependent closure the previous record asked for was run by the dev (76 affected packages, 71 test tasks plus dogfood, the dev's reading at this head), and CI's six shards are the record for the in-repo closure.
  • Fix-round report 6130859286. open_questions is empty. deviations: the seven PR-body text corrections — the coverage sentence, the three dagger cells, the re-judged sentence, the out-of-surface line, the verification bullet for this head, the standing Acceptance note on validate-flow-template-paths — each read present in the body as it stands; the process deviation (the push went out before the closure finished, so CI and the closure measured the same head) has no consequence. out_of_scope_findings, each answered: (1) validate-flow-template-paths.test.ts still feeds the rule single-brace tokens at non-http positions earlier stages made value slots (:411, :535, the notify bodies) — it calls the rule directly, never a door; the rule's single-brace reading is S7's by the stage plan, and the positions are outside this stage's keys: accepted, not escalated. (2) The cli *.e2e.test.* files run only in the nightly tier (ci: e2e and live tiers move to a nightly run on main; PR and queue runs keep unit, integration and conformance (maintainer-directed, part B tiering) #16455; vitest-tiers.ts at the head): by design, accepted — with the consequence stated: the cli DARK door pin is not in the PR run, its green on this head is the dev's local reading, and the first nightly after landing is its CI record.
  • The first report's flags and the Acceptance notes, re-affirmed on this head (read again, not adopted): the signingSecret order is right and not escalated (RUN_RESOLVED_KEYS byte-identical; the token refused at the run fail-closed, nothing sent, no value echoed; the residual — a Studio save of a token accepted into the credential store, every run refused loudly — stays in the Acceptance notes for the seat, since refusing at the save door would also refuse a packaged flow whose store row supplies the real secret, a contract decision outside this stage; the maintainer's veto window stands as it did for S4's answer B). The skill text (skills/objectstack-automation/SKILL.md:236 to :241) is Tier H and carried to skills(objectstack-automation): SKILL.md still teaches {$User.<path>} as a kept value-slot spelling and the text-slot run-user remedy through it, which #19939 pass 2 (PR #22563) refuses #22585 by the stage plan. The objectui designer's http pickers and JSON-string body go to v18: Studio's subflow input and script inputs fields author a CEL value envelope, not a {var} token (the objectui half of objectstack-ai/objectstack#19939 pass 4, S1) objectui#12101 (a JSON-string body is a single-brace-token-shaped literal and is refused at save, loud — the right direction; nothing the pinned sibling imports moves, ① 7). A CEL map literal needs dyn(…) — a property of the CEL checker, refused loudly at registerFlow, written in the docs, the describe, the changeset and the examples. hotcrm NOT MEASURED at the http keys — another repository, on the seat's ledger. The files beyond the claim's surface — each named in the PR body with its reason, the delta's four of the same class. The exemption in-place fixes (notify-recipient single-brace sentences, the README's recipients, a ledger test comment's count) — the same stale teaching S4 retired, mechanical. The two sentences at a typed literal — the doors report the marked one, once; pinned. The omitted empty header — declared and pinned. The three PR-context guards — success on the head, in both trigger sets. No base probe through the engine — the 17.x behaviour is cited from the base's own pins, removed in this diff's minus lines. A body envelope evaluating to null (no body inline, {} to the outbox) — pre-existing, unchanged. check:type-check-debt and check:dual-build-cjs-loads NOT MEASURED locally — Type Check · debt ledger and Type Check · consumer gates are success on the head.

Check-runs on the head (48 at the final read, nothing in progress): 41 success, 7 skipped, 0 failure. Two trigger sets. The push set (started 2026-10-12T16:05Z, its Test Core aggregate complete 2026-10-12T16:35Z): the aggregate TypeScript Type Check and its four Type Check · jobs, Lint & Repo Gates and its nine Lint · jobs, Build Core, Build Docs, Dogfood Regression Gate, Governed Surface Queue Guard, Check Changeset, Check PR Size, Auto Label, Spec property liveness, Check Documentation Links, Flag docs affected by code changes, filter, the four PR-context guards, Test Core (1/6) through (6/6) and the aggregate Test Core — every one success; 5 skipped by paths filters (Console Pin Gate, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), the dogfood shard matrix entry, Packed-tarball smoke (opt-in)). A second pull_request set at 2026-10-12T19:13Z (the seat's body edit): Check Changeset and the four PR-context guards success, Check PR Size and Auto Label skipped. Of the seven required contexts, six are success and Temporal Conformance is skipped by its paths filter. No red on the head, so no signature comparison against the base is owed.

No governed path in the file list; 1,932 changed lines; Part of #19939 is the body's first line and the part-of guard is success; the PR carries no needs:contract-review label.

Implemented-by: claude/issue-19939-p4-s5-http
Reviewed-by: session_019G5ZnRrTMiwcHG6iZM19zf

VERDICT: PASS


Generated by Claude Code

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 protocol:system size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants