feat(lint): os validate refuses an api flow with no per-flow secret - #20593
Conversation
…no per-flow secret An `api`-bound flow (the engine's deriveTriggerBinding: array-form record pre-check, then resolveFlowTriggerKind === 'api') whose start node carries no string config.secret non-empty after trim is now an `error`, flow-api-trigger-secret-missing, in the file's never-fire family. The automation engine refuses the same flow at registration (ADR-0041), so `os validate` no longer passes a flow no runtime will register. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 39aca93a6b12167e5b14402a0d4c806cc22efe5d && git checkout 39aca93a6b12167e5b14402a0d4c806cc22efe5d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0f6dcac5e99d0c6211f0d8a0e150a112d78a776f afa9e266fd9729c6a12cd3bea1a348da10379ef2 && git checkout -B drift-repro 0f6dcac5e99d0c6211f0d8a0e150a112d78a776f && git merge --no-ff afa9e266fd9729c6a12cd3bea1a348da10379ef2
node scripts/docs-audit/affected-docs.mjs --json 0f6dcac5e99d0c6211f0d8a0e150a112d78a776f
|
…ecret rule The changeset's Clause-② line now matches the PR body byte for byte: yes (narrowing), because os validate / os build / os lint and the runtime metadata publish gate newly refuse a secretless api-bound flow while the new exported rule id widens @objectstack/lint. It gains a BREAKING banner in the launch-window shape (minor, one-line fix) and the ADR-0087 disposition not-required (no-migration-prescription): the missing value is a shared secret no migration can supply. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Read-only, from git objects at ① Derived judgmentsAccept-set change — the rule refuses exactly the engine's registration refusal, no more and no less: RIGHT.
Triage's "no second rule: read the engine's predicate, or state in the rule why it cannot" — the permitted branch, stated truly: RIGHT. Where the refusal surfaces: RIGHT, and no stored row is re-judged. The Message and hint: RIGHT.
Public-surface change: one new export, Pins: RIGHT, and they hold the substance. The new block asserts the rule id, Docs — the drift bot's The dev's out-of-scope finding: confirmed, and correctly outside. engine.ts Gate coverage on this head, read ONCE (not polled, not awaited). Concluded ② Semver level
③ Boundary flagsEvery deviation and open question in os-dev-report
Implemented-by: VERDICT: PASS Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 36532203829 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
…lidate-api-flow-secret
…nly, as its own rule The api-trigger secret judgement moves out of validateFlowTriggerReadiness into its own exported rule, validateFlowApiTriggerSecret, on its own registry entry: gating, all three commands, surfaces CLI_ONLY with a surfaceReason. The runtime publish gate judges a /meta save before saveMetaItem restores the inbound-hook secret the flow read path withholds, so a signed flow's GET, edit, PUT round trip would reach the rule secretless and be refused. Until the gate judges the carried-forward body, the id stays off that surface; os validate / os build / os lint keep the refusal. Pins cover both sides of the wall, with a positive control at the gate. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…mmands The Clause-② line matches the PR body again (os validate / os build / os lint only). The publish-gate sentences go, replaced by one saying the gate is deliberately not covered yet. The Why paragraph and the ADR-0087 reason name 17.5.0, the release whose published changelog carries the engine's registration refusal, instead of "the same release". Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
The CLI-only validateFlowApiTriggerSecret entry takes the registry from 46 to 47 rules for every command, and check:docs-transcript-drift holds each pasted `Running author-time rules (N)...` line to authoringRulesFor(cmd). Exactly the four quoted lines change. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Round B, judged on this head alone. Read-only, from git objects at ① Derived judgmentsSurface: the rule is CLI-only, structurally, and the CLI reach is real on all three declared commands: RIGHT. Surface bookkeeping: nothing stale. Registry at base: 46 entries, 43 on The split is the registry's own precedent, not the #7214 silent split: RIGHT.
Refusal truth: the CLI refuses exactly the flow registration refuses, no more and no less: RIGHT. Engine ( Message and hint: RIGHT. The message names the same two declarations the engine's message names, renders only a bad value's type, and cites ADR-0041 only; the hint names Public surface: two new exports on Scope: exactly the nine files, and nothing generated owes a move. Changeset, four transcripts, Gate coverage on this head, read ONCE (not polled, not awaited). 42 check-runs, every one Acceptance notes (not FAIL reasons).
② Semver level
③ Boundary flagsEvery deviation and open question in os-dev-report
Implemented-by: VERDICT: PASS Generated by Claude Code |
… to the commits that decided them (objectstack-ai#20612) Part of objectstack-ai#20597 Clause-②: no The `packages/lint` stage of the dead-citation sweep: the `domain:spec` lane's only package (census `5884031174` on objectstack-ai#20556, claim `5885046469`). Every comment or docblock line in 22 of the 23 claimed `packages/lint/src/` files that cited a tracker number answering 404 now cites, in ruling C+D's form C, the commit in this repository's history that decided what the line describes, and says in its own words what was decided. Comments only: 81 lines out, 81 in, across 22 files. No code token, string literal, rule message, hint or rule id moves. `authoring-rules.ts` (5 sites) is excluded and left at its base blob: PR objectstack-ai#20593 (objectstack-ai#20553) edits it and was still open at the last read (2026-09-29T07:34Z). So this PR says `Part of`: those 5 sites stay for a follow-up once that PR lands, with their anchors already verified (see Acceptance notes). The census is the gate's own `node scripts/check-issue-citations.mjs --census --json`, filtered to `packages/lint/`. Before: base `7a1faf1a5d`, 2026-09-29T06:42:03Z to 06:45:24Z, board enumerated (185 pages, frontier objectstack-ai#20606). After: head `0c7b847f18`, 07:28:22Z to 07:31:52Z (185 pages, frontier objectstack-ai#20611). ## Measurement | file (under `packages/lint/src/`) | dead before | after | numbers, then anchor | |---|---:|---:|---| | `lint-flow-patterns.ts` | 9 | 0 | objectstack-ai#13681 ×9 to `8ed9c54b4` (objectstack-ai#14394 stays, 200) | | `validate-hook-body-writes.ts` | 9 | 0 | objectstack-ai#8663 ×6 to `192213f66`; objectstack-ai#13657 ×3 to `b003cf2e8` | | `runtime-gate.ts` | 8 | 0 | objectstack-ai#10064 ×5 to `def0d3e63`; objectstack-ai#19370 ×2 to `a227afa41` (objectstack-ai#19143 stays); objectstack-ai#9798 to `c7655d472` (objectstack-ai#9261 stays) | | `validate-searchable-fields.ts` | 7 | 0 | objectstack-ai#8404 ×4 to `b849e6911`, the `pre-objectstack-ai#8404` control at `:312` included; objectstack-ai#10001 ×3 to `f1b5ad39a` | | `authoring-rules.ts` | 5 | **5** | excluded: PR objectstack-ai#20593 holds the file | | `validate-expressions.ts` | 5 | 0 | objectstack-ai#6290 ×5 to `e9b526597` (objectstack-ai#6584, that commit's own PR, stays) | | `validate-react-page-props.ts` | 5 | 0 | objectstack-ai#11284 ×4 to `5383fa670`; objectstack-ai#8404 to `b849e6911` | | `validate-sortable-fields.ts` | 5 | 0 | objectstack-ai#10001 ×4 to `f1b5ad39a`; objectstack-ai#8404 to `b849e6911` | | `validate-flow-node-writes.ts` | 4 | 0 | objectstack-ai#8663 ×4 to `192213f66` | | `validate-page-field-bindings.ts` | 4 | 0 | objectstack-ai#6629 ×2 to `cd584d559` (plus the slash-joined `:208`, see Deviations); objectstack-ai#8664 ×2 to `8798cd2a6` | | `validate-translation-references.ts` | 4 | 0 | objectstack-ai#14700 ×3 to `de3c52beb` (objectstack-ai#14253 stays); objectstack-ai#6124 to `b3c1f3cd5` | | `lint-liveness-properties.ts` | 3 | 0 | objectstack-ai#10262 ×3 to `2aca1bc4c` | | `validate-action-body-writes.ts` | 3 | 0 | objectstack-ai#8663 ×3 to `192213f66` | | `validate-security-posture.ts` | 3 | 0 | objectstack-ai#19370 ×3 to `a227afa41` (objectstack-ai#8310 stays) | | `flow-template-grammar.ts` | 2 | 0 | objectstack-ai#11060 ×2 to `815585513` | | `data-model-rules.ts` | 1 | 0 | objectstack-ai#10064 to `def0d3e63` | | `reference-integrity-suite.ts` | 1 | 0 | objectstack-ai#13653 to `36d287803` | | `validate-component-types.ts` | 1 | 0 | objectstack-ai#12950 to `225e7690f` (objectstack-ai#12183 stays) | | `validate-empty-combinators.ts` | 1 | 0 | objectstack-ai#6528 to `3510e4a25` (objectstack-ai#5659 stays) | | `validate-list-view-field-refs.ts` | 1 | 0 | objectstack-ai#10001 to `f1b5ad39a` | | `validate-readonly-action-writes.ts` | 1 | 0 | objectstack-ai#13653 to `36d287803` | | `validate-readonly-flow-writes.ts` | 1 | 0 | objectstack-ai#13653 to `36d287803` | | `validate-readonly-hook-writes.ts` | 1 | 0 | objectstack-ai#13653 to `36d287803` | | **23 files** | **84** | **5** | 21 numbers; 19 removed from the 22 edited files, to 19 distinct shas | Per-file counts at base equal the claim's (census at `f11b5f20a2`) in all 23 files. A second instrument agrees site for site: every `#N` in the 23 files, classified by the TypeScript parser as comment, string or code, and each of 360 distinct numbers probed by REST `issues/N` without following redirects. At base it found 1,323 sites (1,259 comment, 64 string, 0 code); 339 numbers answer 200 and 21 answer 404, the census's 21. Its dead comment sites are the census's 84 plus one slash-joined `objectstack-ai#5775/objectstack-ai#6629` the grammar does not read, and it found one dead **string**: `validate-react-page-props.ts:1198` (see Acceptance notes). At head: 1,243 sites and 344 numbers, the same 339 answer 200, and 5 answer 404, all in `authoring-rules.ts` comments or that one string. Lit controls objectstack-ai#16862, objectstack-ai#16847 and objectstack-ai#17698 answered 200, and dead controls objectstack-ai#16714, objectstack-ai#16715 and objectstack-ai#16697 answered 404, at every checkpoint (5 at base, 5 at head). ## Why each anchor decides its line Each sha resolves uniquely, is an ancestor of `origin/main` and of the base, has one parent, and names the number it replaces in its own message (15 of 19) or its own diff (17 of 19); every one does at least one. Each was read for the rule its line states. - **objectstack-ai#13681 to `8ed9c54b4`**: lands the per-iteration containment rule PAIR (`flow-loop-body-uncontained`, `flow-try-catch-without-catch`) and its measured minimal `catch`; its diff wrote all nine lines, and its changeset records the measurements the lines cite. objectstack-ai#14394 (the rule card, 200) stays beside it. - **objectstack-ai#8663 to `192213f66`**: "three write rules ask anchor provenance before exempting a system column"; its body names objectstack-ai#8663 and its diff wrote the `[objectstack-ai#8663]` lines in all three rule files. - **objectstack-ai#13657 to `b003cf2e8`**: the post-hook half of the declared-field door, one envelope on every driver; its diff wrote the three lines. - **objectstack-ai#10064 to `def0d3e63`**: name-keys collection-resident publish-gate finding paths; its body reads "maintainer ruling 2026-08-20: Option A" for objectstack-ai#10064. - **objectstack-ai#19370 to `a227afa41`**: `security-role-word` crosses to the runtime publish gate, whole, per ruling batch objectstack-ai#203 item 3 letter B; it maps `position` / `app` and writes the past-tense crossing lines. - **objectstack-ai#9798 to `c7655d472`**: the change that carried objectstack-ai#9798 to done (its body names it), restoring the sys_comment unscoped multi-delete refusal that could not fire through the wired engine, the declared-but-unenforced fail-open the line lists beside objectstack-ai#9261 and ADR-0110 D3. - **objectstack-ai#8404 to `b849e6911`**: warns when `searchableFields` declares an unprovisioned injected anchor, adding the optional provenance index the lines describe; the SORT twin line names it as the SEARCH wiring. - **objectstack-ai#10001 to `f1b5ad39a`**: a standalone ViewItem record's nested `config.sort` / `config.searchableFields` reach the runtime publish gate, the RECORD rung. - **objectstack-ai#6290 to `e9b526597`**: `current_user` joins `SCOPE_ROOTS`, the field-level rejection becomes its own rule, and option-level `visibleWhen` is walked for the first time. `:770` quotes `SCOPE_ROOTS`' docblock in `packages/formula`; the quote now stops at "the last one this list was missing", verbatim, with the commit outside the quotation. - **objectstack-ai#11284 to `5383fa670`**: the ListView react-tier vocabulary converges on the metadata-tier spelling, deprecate-first; its changeset reads "(objectstack-ai#11284, maintainer ruling 2026-08-23)". - **objectstack-ai#6629 to `cd584d559`**: drops the retired `displayField` / `searchFields` from the record_picker entry and adds `component-field-specs-liveness.test.ts`. - **objectstack-ai#8664 to `8798cd2a6`**: names what actually guards the `unprovisionedAnchors` wiring; its diff wrote both lines. - **objectstack-ai#14700 to `de3c52beb`**: descends into `conditional` `then` / `otherwise` when building the `_validations` universe; its diff wrote all three lines. - **objectstack-ai#6124 to `b3c1f3cd5`**: the squash commit of objectstack-ai#6124 itself, leg 1 of the `_views` key ruling (the CLI i18n extractor keyed by the runtime view identity). - **objectstack-ai#10262 to `2aca1bc4c`**: adds the package-internal test seam for `getNested`'s array fan-out; its diff wrote all three lines. - **objectstack-ai#11060 to `815585513`**: its body records "Maintainer ruling on objectstack-ai#11060 (2026-08-23): option A", the CEL-mirrored six with no second semantics, which the lines quote. - **objectstack-ai#13653 to `36d287803`**: gates a hook body's `ctx.api` write to a readonly field, and shares `buildReadonlyIndex` from the flow rule, the export `:118` describes. - **objectstack-ai#12950 to `225e7690f`**: created `validate-component-types.ts`, the author-time rejection for unknown component types in spec-reserved namespaces (stage 5's anchor for the same number). - **objectstack-ai#6528 to `3510e4a25`**: the squash commit of objectstack-ai#6528 itself, one implementation of the filter identity reduction (maintainer ruling 2026-08-06, option 1). The line read `PR objectstack-ai#6528`; it now names the commit. Rung: no ADR, `docs/NORTH-STAR.md` or `scripts/adr-anchors/` file records any of these 19 decisions (the one lint anchor file, `data-model-rules.ts`, pins ADR-0120, which none of these lines cites), so the commit rung is the right one, as in objectstack-ai#20234's stages. ## Mechanical proof - **Token guard** (scratch `tokcmp.mjs`: TypeScript 6.0.3 leaf tokens, JSDoc kinds excluded, controls mutate the head text in memory only). The merge base `c96beb2707` against the head, 22 files, 54,508 base tokens (the 22 files are byte-identical at `7a1faf1a5d` and at the merge base): - Real run: 0 files with a token change (exit 0). - Comment-insertion control (`runtime-gate.ts`): 0 (exit 0). - Code-insertion positive control (`validate-hook-body-writes.ts`): DIFFER at token 216 (exit 1). - String positive control (a parser-located `StringLiteral` in `validate-react-page-props.ts`): DIFFER at token 5 (exit 1). - **Line balance**: every file is +N/−N (81/81 across 22 files), every changed line is comment-shaped, and every line count is equal at base and head. - **Tracker numbers**: added-not-removed is empty in every file, and no `PR #N` stands on an added line. Net-removed: 80 sites (the census's 79 in these files plus the slash-joined one), 19 numbers. The numbers kept on added lines all answer 200: objectstack-ai#5659, objectstack-ai#5775, objectstack-ai#8310, objectstack-ai#8340, objectstack-ai#9261, objectstack-ai#9313, objectstack-ai#12183, objectstack-ai#13390, objectstack-ai#14253, objectstack-ai#14394, objectstack-ai#19143, and objectstack-ai#6584 (a pull request, the anchor commit's own PR). - **Shas**: 19 distinct on added lines, 0 on removed lines. `rev-parse --disambiguate` answers 1 object for each; `merge-base --is-ancestor` exits 0 against `origin/main` and against the base; each is single-parent; the repository is not shallow; the control leg `e9584681a4` exits 0. - **Literal readers**: every string or regex literal in the repository that carries one of the 21 numbers (85 literals) was matched against the 23 files' text: no reader of any rewritten line. The lint tests that read these sources as text stay green below. For example, `validate-expressions.test.ts` strips comments before it matches, and `validate-security-posture.runtime-surface.test.ts` collects the `stack.X` reads inside `validateSecurityRoleWord`, which no added line carries. ## Tests and gates (at head `0c7b847f18`) - `pnpm exec turbo run build --concurrency=2 --filter=./packages/* --filter=./packages/*/*` under `os-verify-lock`: Tasks 71 successful, 71 total, VERDICT command-exit 0. - `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2` under the lock: Test Files 115 passed (115), Tests 5363 passed (5363); then `pnpm --filter @objectstack/lint typecheck`: exit 0, `check:test-typecheck` OK (2 files / 6 errors / 2 pinned signatures held). VERDICT command-exit 0. The same two runs passed with the same counts on the pre-merge head `2ce32f6b48`. - Lint, a proven narrowing: `eslint --no-inline-config --format json` over the 22 touched `.ts` files gives 22 files, 0 errors, 0 warnings. `isPathIgnored` is false for all 22, read through eslint's API. `eslint.config.mjs:327-328` says type-aware linting is never enabled, so a comment edit cannot move an untouched file's verdict. The repo-wide `pnpm lint` is CI's. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`: 54 families derived, all run, every one exit 0. `--ran` reads "54 derived, 54 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero (every line carries its exit code). Among them: - `node scripts/check-issue-citations.mjs` (the live diff-scoped run) judged 18 citations across 22 files: 17 answer as issues and 1 answers as a pull request, the kept objectstack-ai#6584; `pnpm check:issue-citations` (self-test, 114 cases in 8 batteries) passes. - `pnpm check:doc-authoring`: the sibling prose-id baseline holds, 810 pinned sites across 230 files, no growth. - `pnpm check:nul-bytes`: OK over 9,239 tracked text files; a control-byte scan of the 23 changed files finds none. - No generated page carries a lint docblock: no page under `content/docs/references/` names any of the 19 numbers, and no generator reads `packages/lint/src`, so nothing was regenerated. - Changeset: `patch` for `@objectstack/lint`. `files[]` ships `dist`, and the rewritten comments reach it: 12 of the 19 shas appear in the built `dist` (for example `8ed9c54b4` and `def0d3e63` in `index.d.ts`, `b849e6911` in `index.js` and `index.d.ts`); the positive control, the unchanged sentence "the near-miss shape: a `try_catch` that declares no `catch`" of an exported docblock, is in `index.d.ts`. Hence patch, not `skip-changeset`. - Merge probe: a no-driver `merge-tree` of the head onto `origin/main` `0f6dcac5e9`, from a bare shared clone with no `merge.*` config, exits 0. The two commits `main` gained after the merge touch none of the 23 files. - No ablation or reverse verification: the change is comment-only, so there is no behaviour to invert. ## Hypotheses (measured first) 1. **Holds.** 84 dead sites, 21 numbers, 23 files at the tip `7a1faf1a5d`, equal per file to the claim. 2. **Holds.** Only comment and docblock lines moved. The one dead number inside a string (`validate-react-page-props.ts:1198`, a finding `message`) stays byte-identical; no test or script reads a rewritten line by literal. 3. **Holds, and conditions the card.** PR objectstack-ai#20593 was still open at 07:34Z, so `authoring-rules.ts` stays at its base blob. At that read, the 9 open PRs' full file lists and the newest `Claim:` on all 11 `pm:dispatched` cards name none of the other 23 paths. 4. **Holds.** `validate-searchable-fields.ts:312` `pre-objectstack-ai#8404` is listed dead before and is gone after. 5. **Holds, with nothing to regenerate.** No lint docblock projects into a generated page; no release page is touched. ## Deviations - Two changed lines beyond the census's sites. `validate-page-field-bindings.ts:208` carried `objectstack-ai#5775/objectstack-ai#6629`, a slash-joined dead number the citation grammar does not read; it now reads "the same objectstack-ai#5775 residue class (commit cd584d5)", stage 5's precedent for the slash-joined `objectstack-ai#9972`. `runtime-gate.ts:780` is the other half of the rewritten `:779` sentence and held no number. - `origin/main` was merged once (`0c7b847f18`, merging `c96beb2707`): the first derivation read STALE TREE because `scripts/sdui-manifest.record.json` changed on `main`. The merge was clean, no driver-routed path and no lockfile change, and it touches none of the 23 files; the build, tests and gates above ran after it. - Commit trailers follow AGENTS.md's model-free pair (`Claude-Session` plus `Co-authored-by: Claude`); the pre-push trailer check passed on every push. ## Acceptance notes **What stays for this card** (why it says `Part of`): `authoring-rules.ts`, 5 sites, excluded while PR objectstack-ai#20593 holds it. Anchors, verified the same way, for whoever takes it after that PR lands: `:198` objectstack-ai#10064 to `def0d3e63`; `:1117` objectstack-ai#16659 to `ecdfc9411` (it added `flow-schedule-organization-missing` to the registry); `:1687` "(PR objectstack-ai#8546)" to `ba5e957ef`, that PR's own squash commit; `:1713` and `:1733` objectstack-ai#19370 to `a227afa41`. **Form D, not touched:** `validate-react-page-props.ts:1198` is the `react-prop-deprecated` finding `message`, which ends "...is removed after the deprecation window (objectstack-ai#11284)." An author sees it, so it takes ruling D (no number), which is a string change and outside this comment-only scope. `scripts/doc-authoring-prose-id.baseline.json` pins it (`objectstack-ai#11284: 1` for this file). It needs a form-D carrier. **Outside the census's surface**, which blanks strings and defers test files (noted, not swept here): - `packages/lint/src/*.test.ts` titles and comments still cite several of these dead numbers (objectstack-ai#6290, objectstack-ai#8404, objectstack-ai#8663, objectstack-ai#10001, objectstack-ai#10064, objectstack-ai#10262, objectstack-ai#13681, objectstack-ai#19370 and others). - Hand-written docs pages cite them too: `content/docs/automation/hook-bodies.mdx` (objectstack-ai#8663, objectstack-ai#13657), `content/docs/automation/flows.mdx` (objectstack-ai#11060) and `content/docs/deployment/validating-metadata.mdx` (objectstack-ai#19370). - `packages/formula/src/cel-engine.ts` cites objectstack-ai#6290 four times, including the docblock `validate-expressions.ts:770` quotes. It is in the census, in another lane's package. **Wording, each true of its commit.** - `validate-expressions.ts:571` keeps objectstack-ai#6584 beside `e9b526597`: objectstack-ai#6584 is that commit's own PR, so "arrived in commit e9b5265, and needed that same change (objectstack-ai#6584) to be noticed" states the one act both old numbers named. - `runtime-gate.ts:362` names the objectstack-ai#9798 shape in words, as the fail-open that commit c7655d4 ended, next to objectstack-ai#9261 and ADR-0110 D3. - `lint-flow-patterns.ts:343` reads "The measured case commit 8ed9c54 records, exactly: one row with a null owner killed the sweep"; that commit wrote the sentence, and `c02f70e13` later fixed the same shape in the showcase flow. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…les.ts to the commits that decided them (objectstack-ai#20631) Part of objectstack-ai#20597 Clause-②: no Stage 2 of the `packages/lint` dead-citation sweep (claim `5888191846`). Stage 1 (PR objectstack-ai#20612) left `packages/lint/src/authoring-rules.ts` at its base blob while PR objectstack-ai#20593 held the file. That PR has landed (`e651556e2d`). Each of the file's five comment and docblock lines that cited a tracker number answering 404 now cites, in ruling C+D's form C, the commit in this repository's history that decided what the line states. Each line still says what was decided. Comments only: 5 lines out, 5 in, in one file, plus one `@objectstack/lint` `patch` changeset. This PR says `Part of`: the form-D finding-message string at `validate-react-page-props.ts:1198` (`(objectstack-ai#11284)`, shown to authors) stays on the card as a separate decision. It is byte-identical here (see Acceptance notes). ## Measurement The instrument is the gate's own `node scripts/check-issue-citations.mjs --census --json`, filtered to `packages/lint/`. - **Before:** base `1322cc72c9`, 2026-09-29T10:18:50Z to 10:22:33Z, board enumerated (185 pages, frontier objectstack-ai#20627). Repo-wide `allocated-but-absent` 2,209. - **After:** head `2e1955b492`, 11:13:21Z to 11:16:47Z (185 pages, frontier objectstack-ai#20630). Repo-wide `allocated-but-absent` 2,204, exactly 5 fewer. No finding at head is absent at base. | site in `authoring-rules.ts` | before | after | anchor | |---|---|---|---| | `:201` (the `AuthoringFinding.path` docblock) | objectstack-ai#10064 | commit | `def0d3e63` | | `:1123` | objectstack-ai#16659 | commit | `ecdfc9411` | | `:1722` | `(PR objectstack-ai#8546)` | commit | `ba5e957ef` | | `:1748` | `[objectstack-ai#19370]` | commit | `a227afa41` | | `:1768` | `[ADR-0090 D3 / objectstack-ai#8310 → objectstack-ai#19370]` | commit | `a227afa41` (ADR-0090 D3 and objectstack-ai#8310 stay) | | **`packages/lint` total** | **5** | **0** | 4 numbers, to 4 distinct shas | The five lines on `origin/main` `e651556e2d` are the same lines at the base: `packages/lint` is byte-identical between `e651556e2d` and `1322cc72c9`. ## Why each anchor decides its line Each sha resolves uniquely (`rev-parse --disambiguate` gives 1 object). Each is single-parent. `merge-base --is-ancestor` exits 0 against `origin/main` and against the base, and the repository is not shallow. Each commit's own diff was read for the rule its line states. - **objectstack-ai#10064 to `def0d3e63`**: "key collection-resident publish-gate finding paths by name, not the private snapshot index". Its body names objectstack-ai#10064 as the card it lands, "(maintainer ruling 2026-08-20: Option A)". Its own diff wrote this very docblock: the positional-as-rules-emit-it sentence, the `objects.acme_invoice.sharingModel` example and the pointer to `nameKeyFindingPath`, which the same commit introduced in `runtime-gate.ts`. - **objectstack-ai#16659 to `ecdfc9411`**: the squash commit that declares a time-triggered flow's acting organization. Its diff adds `FLOW_SCHEDULE_ORGANIZATION_MISSING` (`flow-schedule-organization-missing`, at `warning`) to `validate-flow-trigger-readiness.ts`. It also wrote the `authoring-rules.ts` sentence "... added a sixth id, `flow-schedule-organization-missing`, at `warning`" that this line opens, and its sub-commits name objectstack-ai#16659. The live objectstack-ai#17396 retirement beside it stays. - **`(PR objectstack-ai#8546)` to `ba5e957ef`**: PR objectstack-ai#8546's own squash commit, "permission/book cross the runtime publish gate; object measured dirty stays behind". Its `authoring-rules.ts` diff changes `runtimeTypes: ['seed']` to `['seed', 'permission', 'book']`, which is objectstack-ai#8310 slice 1 as the line states. The live objectstack-ai#8310 stays. - **objectstack-ai#19370 to `a227afa41`** (two sites): "`security-role-word` crosses to the runtime publish gate, whole". Its body names objectstack-ai#19370 as the card it lands. Its own `authoring-rules.ts` diff wrote both lines: "[objectstack-ai#19370] It has since crossed, also whole, on its own entry" and the `[ADR-0090 D3 / objectstack-ai#8310 → objectstack-ai#19370]` marker. Stage 1 cited the same commit for the same number in `runtime-gate.ts` and `validate-security-posture.ts`. Rung: no file under `docs/adr/**`, `docs/NORTH-STAR.md` or `scripts/adr-anchors/` names any of the four numbers. So the commit rung is right, as in stage 1. ADR-0090 D3 already stands on `:1768` and is kept. ## Mechanical proof - **Token and residue guard.** A scratch instrument on the TypeScript 6.0.3 parser compares the base blob with the head blob at two levels. The first is leaf AST tokens, with JSDoc nodes excluded. The second is the non-comment residue: every comment range dropped, everything else compared byte for byte. The controls mutate the head text in memory only. - Real run: 3,543 tokens at base and at head, tokens EQUAL, residue EQUAL (exit 0). - Dark control, a whole comment line inserted: tokens EQUAL, residue EQUAL, comment ranges 957 to 958 (exit 0). - Lit control, a code statement inserted: DIFFER at token 0, residue DIFFER (exit 1). - Lit control, one character inserted into a parser-located string literal: DIFFER at token 5, residue DIFFER (exit 1). The first string control was a no-op and is void: it searched by text and landed in a comment, reading EQUAL. It was re-anchored on a parser-located literal and re-run. The mutation was confirmed landed. - **Line balance**: +5/−5, and every changed line is comment-shaped. The file has 2,011 lines at base and at head. - **Tracker numbers**: removed objectstack-ai#10064, objectstack-ai#16659, objectstack-ai#8546 and objectstack-ai#19370 ×2. The added lines carry only the live objectstack-ai#8310 ×2, which stands on both the removed and the added side of `:1722` and `:1768`. So added-not-removed is empty, and no `PR #N` stands on an added line. There are 215 `#N` tokens at base and 210 at head. - **Shas**: 4 distinct on added lines (`a227afa41` ×2), none on removed lines. - **Literal readers**: `scripts/doc-authoring-prose-id.baseline.json` pins this file's string sites as objectstack-ai#4463, objectstack-ai#4716, objectstack-ai#4717, objectstack-ai#7220, objectstack-ai#8309 and objectstack-ai#9698, none of them these four numbers. `check-docs-transcript-drift` loads the registry module, not its comments. ## Tests and gates (at head `2e1955b492`) - Build under `os-verify-lock`: `pnpm exec turbo run build --concurrency=2 --filter=./packages/* --filter=./packages/*/*`. The last run printed Tasks 71 successful, 71 total, and VERDICT command-exit 0. It took three attempts inside a 270 s timeout on a shared box. The first two were cut off at 39 of 46 and 66 of 68 tasks, and turbo's cache carried their finished tasks forward. - `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2` under the lock: Test Files 115 passed (115), Tests 5379 passed (5379), VERDICT command-exit 0. - `pnpm --filter @objectstack/lint typecheck` under the lock: exit 0. `check:test-typecheck` OK (2 files, 6 errors, 2 pinned signatures held). VERDICT command-exit 0. - Lint, as a proven narrowing: `eslint --no-inline-config --format json packages/lint/src/authoring-rules.ts` reports 1 file, 0 errors, 0 warnings. `isPathIgnored` is false, read through eslint's API. `eslint.config.mjs:327-328` says type-aware linting is never enabled, so a comment edit cannot move an untouched file's verdict. The repo-wide `pnpm lint` is CI's. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 54 families, and all 54 ran with exit 0. `--ran` reads "54 derived, 54 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero. Among them: - `node scripts/check-issue-citations.mjs`, the live diff-scoped run, judged 2 citations in 1 file, the kept objectstack-ai#8310 ×2, and both answer as issues. `pnpm check:issue-citations` passes its self-test (114 cases in 8 batteries). - `pnpm check:doc-authoring`: the sibling prose-id baseline holds, 810 pinned sites across 230 files, no growth. - `pnpm check:nul-bytes`: OK over 9,253 tracked text files. A control-byte scan of both changed files finds none. - Generated pages: none to regenerate. No page under `content/docs/references/` names the four numbers, `AuthoringFinding` or `nameKeyFindingPath`, and no generator reads `packages/lint/src`. - Changeset: `patch` for `@objectstack/lint`, a new file (stage 1's `lint-provenance-anchors.md` is untouched). `files[]` ships `dist`, and the rewritten comments reach it: - `commit def0d3e` is in the `AuthoringFinding` docblock of `dist/runtime-*.d.ts`, beside the unchanged "Positional as RULES emit it", the positive control. - `commit ba5e957` (cited only here) and `commit a227afa` are in `dist/index.js` and `dist/index.cjs`. - None of the four numbers remains in `dist`. - Merge probe: a no-driver `merge-tree` of the head onto `origin/main` `542670da6d`, from a bare shared clone with no `merge.*` config, exits 0. None of the three commits `main` gained since the base touches `packages/lint`. - No ablation or reverse verification: the change is comment-only, so there is no behaviour to invert. ## Hypotheses (measured first) 1. **Holds.** At the base the census reads exactly 5 dead sites in `packages/lint`, all in `authoring-rules.ts`, and after the change it reads 0. The card's sixth site, the `(objectstack-ai#11284)` string at `validate-react-page-props.ts:1198`, is outside the census because the census blanks string literals. It was read directly: still present, and the file is byte-identical from base to head. 2. **Holds.** The five sites read `:201` objectstack-ai#10064, `:1123` objectstack-ai#16659, `:1722` `(PR objectstack-ai#8546)`, `:1748` and `:1768` objectstack-ai#19370, on `e651556e2d` and at the base alike. All four anchors were re-verified above from their own diffs, not copied. 3. **Holds.** PR objectstack-ai#20593's new lines cite objectstack-ai#20553, objectstack-ai#20611 and objectstack-ai#20552 (and ADR-0041). All three answer 200, and the census finds no dead site on them. None of the five lines' sentences changed in meaning. The one adjacency is described under Acceptance notes. ## Deviations - Commit trailers follow AGENTS.md's model-free pair (`Claude-Session` plus `Co-authored-by: Claude`), not the model-named trailer the harness reminder suggested. The pre-push trailer check passed on both pushes. - The first lit string control was a no-op: it searched by text and landed in a comment. It is reported void above and was re-run on a parser-located literal. ## Acceptance notes **Form D, not touched (why this PR says `Part of`):** `validate-react-page-props.ts:1198` is the `react-prop-deprecated` finding `message`. It ends "...is removed after the deprecation window (objectstack-ai#11284)." An author sees it, so it takes ruling D (no number). That is a string change, outside this comment-only claim. `scripts/doc-authoring-prose-id.baseline.json` pins it (`objectstack-ai#11284: 1` for that file), and that baseline is shrink-only. **An ordinal beside PR objectstack-ai#20593's insertion, kept verbatim:** the paragraph above `:1123` now ends "objectstack-ai#20553 made it five", counting the rules that emit `error`. `:1123` reads "Commit ecdfc94 added a sixth id", an ordinal that commit wrote itself. The two count different things: rules that emit `error`, and ids in the rule file. The ordinal is also imprecise on its own terms, because `validate-flow-trigger-readiness.ts` exported six ids before `ecdfc9411`, so the new one was its seventh. This PR moves only the tracker number, so the word stays as written. **Outside the census's surface (noted, not swept):** stage 1 notes that lint test titles and hand-written docs still cite these numbers. `content/docs/deployment/validating-metadata.mdx` cites objectstack-ai#19370 at `:472`, `:483` and `:515`. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20553
Clause-②: yes (narrowing —
os validate/os build/os lintnewly refuse a secretlessapi-bound flow; the new exported rule idFLOW_API_TRIGGER_SECRET_MISSINGwidens@objectstack/lint)This PR is the
os validatehalf of the card. Triage split the skill half out to #20569, which stays open and is not addressed here.What this changes
packages/lint/src/validate-flow-trigger-readiness.tsgains one rule id,flow-api-trigger-secret-missing, at severityerror. It names a flow bound to the inboundapitrigger when the flow's start node carries no usableconfig.secret.trim(). The rule fires for a missing, blank or non-string secret. It also fires for anapiflow with no start node, because the engine reads that flow'sconfigas{}and refuses it too. That finding is located atflows[i].nodes.statusis not read. The engine refuses anobsoleteflow as well.type: 'api'and/or a start-nodetriggerType: 'api') and what is wrong with the secret. It gives the type of a bad value only, never the value, because findings travel into CI logs (and, once Seam: the /meta save path runs the runtime authoring gate on the redacted body, before #20552's stored-secret carry-forward, so flow-api-trigger-secret-missing cannot run at the runtime surface #20611 moves the rule onto the runtime publish gate, into that gate's responses).config.secretplus signing withx-objectstack-signature. For a flow that is only ever started explicitly, it prescribestype: 'autolaunched'with notriggerType: 'api'.error, in the file's never-fire family. The question the family's Severity section asks is whether this stack alone is enough to know the flow is dead. Here the verdict isregisterFlow's own hardcoded refusal, which runs before any trigger is consulted. So I measured the "installing something fixes it" hypothesis, and it is false. An engine with a registeredapitrigger that would arm anything still refused every secretless shape below.flow-DESCRIPTOR-VERDICTconvention: the descriptor is theapitrigger's secret, and the verdict is "missing".index.tsre-exportsvalidateFlowApiTriggerSecretandFLOW_API_TRIGGER_SECRET_MISSING.rule-id-barrel-exports.test.tsrequires every rule id to be reachable from a published barrel, and the wiring guard requires every exported rule to be registered.authoring-rules.tsgains the registry entryvalidateFlowApiTriggerSecret(tier: 'gating', all three commands,surfaces: CLI_ONLYwith asurfaceReason) and updates its family comment, which said "Four rules answer yes and emiterror".content/docsCLI transcripts quoteRunning author-time rules (N).... The new entry takes the registry from 46 to 47, andcheck:docs-transcript-driftholds each quote toauthoringRulesFor(cmd), so exactly those four lines move to 47..changeset/20553-validate-api-flow-secret.mdbumps@objectstack/lintminor.Which flows are
api-bound: the engine's binding, not a reading oftypebindsApiTriggeris the engine'sderiveTriggerBinding, in its own order:triggerTypepre-check. This is the same predicate as this file'sisArrayRecordTriggered.resolveFlowTriggerKind(flow) === 'api'. This is the spec export the file already reads.I measured it on the built
AutomationEngine.registerFlowatf11b5f20a2, with a scratch script (deleted afterwards) and a recording trigger registered for each kind. The script compared the engine against two candidate derivations over 15 shapes:type === 'api' OR triggerType === 'api'type: 'api', no / blank / non-string secrettype: 'api', secretapistartedautolaunched/screen/record_change+triggerType: 'api', no secrettype: 'api'+ scalartimeRelative: 'daily'type: 'api',obsolete, no secretautolaunched, neithertype: 'api'+config.scheduletype: 'schedule'+triggerType: 'api'type: 'api'+record-after-createrecord_changestartedtype: 'api'+ array record tokenrecord_changestartedtype: 'api'+timeRelativeobjectThe composed derivation agrees with the engine on all 15 shapes. The disjunction disagrees on the 5 bold precedence shapes, and would refuse flows the engine registers. A start-less
type: 'api'flow was measured separately: the engine refused it with the secret error.Why the rule carries the check instead of reading the runtime's
AutomationEngine.validateApiTriggerSecret, a private method inpackages/services/service-automation/src/engine.tscalled fromregisterFlow. It also lives inline inApiTrigger.start()inpackages/triggers/trigger-api/src/api-trigger.ts.@objectstack/specexports none for the secret. The only spec hits are outbound-webhook signing keys. The spec does export the kind half,resolveFlowTriggerKind, and the rule reads it.@objectstack/speconly, never on a runtime.Verification record (HEAD
afa9e266fd; the premise, corpus and first two ablations were measured at29caa84eb3)Round 3, at
afa9e266fd— the rule is CLI-only until #20611.flow-api-trigger-secret-missingmoved into its own exported rule,validateFlowApiTriggerSecret, on its ownCLI_ONLYregistry entry.@objectstack/lint: 115 files, 5379 passed; typecheck exit 0.@objectstack/metadata-protocol: 189 files passed, 3 skipped (2768 tests passed, 19 skipped), including #20552's two round-trip pins inprotocol.metadata-redaction.test.ts, which failed while the id sat on the runtime gate.service-automation(7 files, 45),metadata-service(72) and runtimeautomation-flow-credential-projection(7) pass. CLI consumers (36 files): unit 12/257, integration 6/82 + 6/42, nightly.e2e6/70 + 6/41. The built CLI prints "Running author-time rules (47)": a secretless probe exits 1 with the finding, a signed one exits 0.dispatch-gatesderived 89 commands, all exit 0 (two answered PREREQUISITE NOT MET first and passed after building what they named);--ran: 89 derived, 89 run, 0 NOT-MEASURED.Premise, measured first, at
origin/mainf11b5f20a2(unmodified tree).git grep -c secret -- packages/lint/src/validate-flow-trigger-readiness.tsgave no output with exit 1, i.e. 0 hits. The lit controlgit grep -c triggerTypeon the same file answered 39.node packages/cli/bin/run.js validate objectstack.config.ts. I ran it on a throwaway stack, deleted afterwards, underexamples/app-showcase/.probe-20553/. The stack hadrequires: ['automation', 'triggers', 'queue']and one flow:type: 'api',status: 'active',runAs: 'system', startconfig: { hookId: 'intake' }.✓ Validation passed, exit 0. A start-less variant also passed, exit 0.29caa84eb3.✗ Author-time rules failed (1 issue),rule: flow-api-trigger-secret-missing at flows[0].nodes[0].config.secret, exit 1.flows[0].nodes.secret: 'whsec_probe'gave✓ Validation passed, exit 0.afa9e266fd. The rule's own registry entry issurfaces: CLI_ONLY, so the gate does not reach it.runRuntimeAuthoringRules({ type: 'flow', item })from the built@objectstack/lint/runtimegaveerrors: []for a secretless flow;rulesRunheldvalidateFlowTriggerReadinessbut notvalidateFlowApiTriggerSecret. At29caa84eb3, before the split, the same call gaveerrors: [["flow-api-trigger-secret-missing","flows[0].nodes[0].config.secret"]]— the behaviour that broke [security] a flow's inbound-hook secret (config.secreton the start node) is served in cleartext by the flow-definition read; after #20529 every armed hook carries one #20552's round-trip pins once [security] a flow's inbound-hook secret (config.secreton the start node) is served in cleartext by the flow-definition read; after #20529 every armed hook carries one #20552 landed.Build.
turbo run build --filter='@objectstack/cli...' --concurrency=2, 59/59 tasks.pnpm --filter @objectstack/lint build: exit 0, andcheck-dts-emittedreported 4/4.--filter='@objectstack/example-showcase^...', 60/60 tasks.os-verify-lock.shand printedVERDICT command-exit 0.Tests. Counts at
afa9e266fdunless marked.@objectstack/lint, whole package: 115 files, 5379 passed (5373 at29caa84eb3; the 6 added are the wall pins below).rule-id-barrel-exports.test.tsandauthoring-rule-wiring.test.ts: 117 passed.type: 'api'flow fails, checked exhaustively on rule id, severity,whereandpath;autolaunchedflow passes;triggerType: 'api'onautolaunched,screenandrecord_changeflows is judged liketype: 'api', and passes with a secret;' ',''and a tab-newline secret fail, as do a number, boolean, null, array and object, and the value is never echoed;obsoleteanddraftflows are judged;validateFlowTriggerReadinessalone no longer emits it; its registry entry is gating, on all three commands,surfaces: ['cli'], with a reason;os validate/os build/os linteach still refuse a secretless flow through the table; and the runtime gate emits noflow-api-trigger-secret-missingfor it, with a positive control (the same gate still refuses a deadrecord_changeflow withflow-trigger-unroutable).provoketable gains this id aserror. The clean-stack floor gains a signedapiflow.@objectstack/clitest that reaches the validate, build or lint rule table: 36 files atafa9e266fd(the merge ofmainadded one). None was edited.unitproject: 12 files, 257 passed..e2efiles, run withOS_TEST_TIERS=nightly: 6 files / 70 passed, then 6 files / 41 passed.integrationproject: 6 files / 82 passed, then 6 files / 42 passed.Typecheck.
pnpm --filter @objectstack/lint typecheck: exit 0.tsc --noEmitcovers the rule file.check:test-typecheckreported "OK, test layer compiles under tsconfig.test.json", and its debt is unchanged.tsc --listFiles -p tsconfig.test.jsonlists the test file.Ablations. The first two ran at
29caa84eb3on the pre-split code, withscripts/ablation-replace.mjsin WRAP mode and an outertraprestoring the absolute path; the subject is imported from relative source, so nodist/is involved. The third ran at825c33ff9fthrough lint's builtdist/(metadata-protocol→@objectstack/lintis a known unaliased pair): with the id dropped at the runtime surface only (marker proven in 4dist/files),protocol.metadata-redaction.test.tspassed 26/26; restored (blob == HEAD789b320b,git diff HEADempty, marker absent from all 14dist/files), exactly its two round-trip pins failed again (2 failed / 24 passed).if (secretProblem) {anchor went from 1 hit to 0, and the blob moved from4b53700dto46f09b8d.provokerow. The pass-controls stayed green.4b53700d, andgit diff HEADis empty.type OR triggerTypedisjunction. The anchor went from 1 hit to 0, and the blob moved from4b53700dto341d0408.api + config.schedule.git diff HEADis empty.Gates.
afa9e266fd.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 89 commands (the fourcontent/docstranscripts add 29 docs families). Each was run with its exit code captured before any pipe.check:dual-build-cjs-loadsand@objectstack/spec'scheck:skill-examplesfirst answeredPREREQUISITE NOT MET, exit 3. That is not a measurement. After building the packages they named, both exited 0.--rangave "89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero, all 89 recorded an exit code)".yes (narrowing), and no package whosepackages/**/src/**it moves is gradedpatch".check-adr-0087-registration. "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition":[BREAKING+clause-②-narrowing] not-required (no-migration-prescription). Its--self-test: 441 assertions.Lint, narrowed and proven. eslint
--no-inline-config --format jsonover the 4 changed.tsfiles reported 4 files, 0 errors and 0 warnings. Three facts make that narrowing a measurement:eslint.config.mjslines 327-328 state that the config never enables type-aware linting, so this diff cannot move an untouched file's verdict.The repo-wide
pnpm lintis declared to CI.Corpus sweep, at
29caa84eb3.os validateover all 4 example stacks:app-crm,app-multi-package,app-showcase(after building its closure) andapp-todo. All answered✓ Validation passed, exit 0, with 0 hits of the new id.api-bound example flow,showcase_inbound_task_webhook, carriessecret: 'showcase-webhook-secret'. That secret predates PR fix(trigger-api,service-automation): refuse an api flow with no per-flow secret, at arm time and at registration (#20529) #20551, which gave no example or fixture a secret.api-bound flow. A grep fortype/triggerType'api'overpackages/cli/testhit only theos explaintype-enum doc, which is arecord_changeexample.Pin sweep.
os validatepasses a secretlessapiflow.packages/lintlists this file's rule ids exhaustively. The one non-lint hit,flow-trigger-kind.ts, is a docblock mention.content/docshas no "secret optional" line for the inbound trigger. The only hit iswebhooks.mdxP3, which is about outbound webhooks.Other checks.
grep -naPfor raw control bytes over the 9 changed files found nothing.origin/mainatc96beb2707([security] a flow's inbound-hook secret (config.secreton the start node) is served in cleartext by the flow-definition read; after #20529 every armed hook carries one #20552's landing, which surfaced the round-trip conflict) in merge commit825c33ff9f.origin/mainhas since moved to7510663c87;dispatch-gatesreports none of those commits touched what its derivation reads.Acceptance notes
authoring-rules.tsgains the CLI-onlyvalidateFlowApiTriggerSecretentry (amended into the claim by the seat's fork ruling), and fourcontent/docstranscripts move their quoted rule count from 46 to 47 (admitted as the registry's own quotation; nothing undercontent/docs/releases/).index.tscarries the barrel lines the rule-id barrel test and the wiring guard require.:507and:618"the binding this rule already derives".:618(routesToSomeTrigger) is a routes-anywhere disjunction. As anapiderivation it disagrees with the engine on 5 shapes, per the table above.:507is precedence-ordered, but it is reached only inside 1e.yes (narrowing): the new exported rule id widens@objectstack/lint, andos validate/os build/os lintnewly refuse a stack they used to pass. The changeset carries the line byte-for-byte, a**BREAKING**banner (shippedminorunder the launch-window convention) and the ADR-0087 dispositionnot-required (no-migration-prescription). The engine's own refusal already shipped in 17.5.0 (PR fix(trigger-api,service-automation): refuse an api flow with no per-flow secret, at arm time and at registration (#20529) #20551's published changelog entry).saveMetaItemruns the runtime authoring gate (protocol.ts:16352) before it restores the stored secret the flow read path withholds (:16588, [security] a flow's inbound-hook secret (config.secreton the start node) is served in cleartext by the flow-definition read; after #20529 every armed hook carries one #20552), so on that gate a signed flow's GET → edit → PUT arrives secretless. With this id on the gate,protocol.metadata-redaction.test.ts's two round-trip pins failed (measured at825c33ff9f; 26/26 with the id dropped there). The id therefore sits on its ownCLI_ONLYregistry entry until Seam: the /meta save path runs the runtime authoring gate on the redacted body, before #20552's stored-secret carry-forward, so flow-api-trigger-secret-missing cannot run at the runtime surface #20611 makes the gate judge the carried-forward body. Meanwhile the/metadoor behaves as it did before this PR: it stores a secretless flow, and the engine refuses it at registration. The/automationwrite doors callregisterFlowdirectly and never reach this gate, so their400 VALIDATION_FAILEDanswer is unchanged.engine.tsvalidateApiTriggerSecretanswers "declares noconfig.secret" even when a non-string secret is present. The measured case wassecret: 12345. Carrier: none now that [security] a flow's inbound-hook secret (config.secreton the start node) is served in cleartext by the flow-definition read; after #20529 every armed hook carries one #20552, which heldservice-automation/src/**, has landed.scheduleflow. Carrier: none.config.secreton anapiflow's start node. The sibling repo is not in this change.Generated by Claude Code