Skip to content

docs(spec): name the node slot in the structural-condition ruling and its ADR-0087 entry - #17761

Merged
os-bill merged 3 commits into
mainfrom
claude/issue-17493-node-door-refusal-residues
Sep 12, 2026
Merged

docs(spec): name the node slot in the structural-condition ruling and its ADR-0087 entry#17761
os-bill merged 3 commits into
mainfrom
claude/issue-17493-node-door-refusal-residues

Conversation

@os-bill

@os-bill os-bill commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Part of #17493

Items ① and ② of the card. ⛔ Item ③ is a ruling, not a refactor — it is measured and reported here, and deliberately not implemented; the card stays open on it, and this PR therefore carries no closing keyword.

Nothing in this diff changes behaviour. The refusal already shipped (#17322 at registerFlow, #17495 at objectstack validate); what shipped with it was the notification, and this is that half.

  • Clause-②: no — this PR puts no new key on any published payload.

① The on-site ruling that had become false

packages/spec/src/automation/flow-node-expression-paths.ts — the docblock of structuralConditionRefusal.

It admitted a whitespace-only string on this ground, verbatim as it stood:

every string, including a whitespace-only one. What a non-empty string says stays validateExpression('predicate', …)'s verdict, and a whitespace-only condition meaning false is consistent on both sides and is ruled correct, not a defect.

#15807 removed that ground (FlowEdgeSchema.condition composes EvaluatedExpressionInputSchema), #17322 ruled on the disagreement that left, and #17495 finished it at the third door. ⛔ Not deleted — it records a real decision, so the edit says what changed it:

  • The admission itself is unchanged and still correct. This function answers the SHAPE question; a string is a well-shaped structural condition. The blank is refused beside it, by the imported evaluated-slot rule, answering EVALUATED_EXPRESSION_SOURCE_REQUIRED and not STRUCTURAL_CONDITION_SHAPE_REFUSAL.
  • A new section records why item ③'s question is open, without answering it.
  • The same stale ground sat three lines from the symbol, in flow-node-expression-paths.test.ts's own case comment ("not authored on both sides and stays so"). Same defect class, same file face, mechanical: the comment is corrected, both assertions untouched. ⭐ Declared as a bounded on-site fix beyond the dispatched file face.

② The ADR-0087 entry named only the edge slot

packages/spec/src/migrations/entries/semantic/18.flow-edge-condition-evaluated-slot-source-required.tssurface and acceptanceCriteria.

Both named only edges[].condition, so a consumer replaying the chain was told to sweep the edge key alone and a deployment carrying a blank config.condition on a node was never told to look. Both now name both structural slots, plus the node key's own locator phrase (the one the structural pass builds, e.g. node 'gate' (start) condition) beside the edge's flows.N.edges.N.condition, and the sweep carries the warning that deleting a condition from a start node opens the trigger gate rather than preserving it.

  • id, replacement and reason are untouched, per the card.
  • No backticks in surface — measured, 0 (build-upgrade-guide.ts renders it inside a code span).
  • No new entry. This is one decision reaching its second slot, which is what both follow-up changesets (blank-node-condition-refused-at-registration.md, validate-refuses-blank-structural-condition.md) already declared by taking the not-required (already-registered …) disposition against this very entry.
  • packages/spec/src/migrations/registry.ts is regenerated by the repo's own generator, pnpm --filter @objectstack/spec gen:migration-registry — ⛔ never hand-edited. check:migration-registry is green.

⛔ ③ Measured, reported, NOT implemented

registerFlow still ACCEPTS a whitespace-only string at both sibling ledger predicate slots, on this head, against controls that prove the probe reaches them:

slot valid CEL (positive control) envelope with a blank source (refusal control) ' ' ''
config.conditions[].expression ACCEPTED REFUSED — PREDICATE_SLOT_STRING_REFUSAL at config.conditions[0].expression ACCEPTED ACCEPTED
screen.fields[].visibleWhen ACCEPTED REFUSED — PREDICATE_SLOT_STRING_REFUSAL at config.fields[0].visibleWhen ACCEPTED ACCEPTED
config.condition (lit control — the slot #17322 DID narrow) ACCEPTED REFUSED REFUSED REFUSED

evaluateCondition(' ') and evaluateCondition('') both answer false — the same silent dead branch. The current behaviour is pinned as correct by #15572 at packages/services/service-automation/src/decision-predicate-envelope.test.ts (the it block at :110:114 on this head; the card cited :113:117 from aefbb07b2, a 3-line drift, the pin itself intact).

⛔ Narrowing those slots re-judges that pin and moves a published accept-set: a ruling, not a refactor. packages/services/** is untouched by this diff — 0 files.

验收备注

Verification

Every run below is on the final head 5a77c75b29 (origin/main merged at that commit), with the gate's own verdict line, never a bare exit status read through a pipe.

  • pnpm --filter @objectstack/spec build && check:generated && typecheck && test — VERDICT command-exit 0; 473 test files / 13436 tests passed.
  • The one test-layer file this diff touches, re-run alone: pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/automation/flow-node-expression-paths.test.ts — 1 file / 26 tests passed.
  • node scripts/pm/dispatch-gates.mjs --commands derived 84 gate families for this change set; 84 run, 82 green, 0 unrun — reconciled with --ran, exit codes captured before any pipe. The two non-green exited 3 = PREREQUISITE NOT MET, i.e. NOT MEASURED and not a finding: check:dual-build-cjs-loads and check:type-check-debt both refuse without a repo-wide build closure, and both are declared to CI, which builds one.
  • Item ②'s own gate, check:adr-0087-registration — exit 0, verdict: "this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)". check:migration-registry exit 0, verdict: "src/migrations/registry.ts is current (202 semantic, 167 retired-key, 178 retired-def)". check:spec-changes and check:upgrade-guide both report their artifact up to date; check:docs reports 222 generated files in sync.
  • eslint . --no-inline-config --format json over the whole repo, eslint's own population read from its JSON output: 6638 files, 0 errors, 0 warnings. No narrowing claimed and none needed.
  • check:nul-bytes exit 0 (8441 text files), plus a control-character self-scan over every changed file: 0 hits.

Changeset: .changeset/17493-node-door-refusal-residues.md, @objectstack/spec: patch, card-scoped filename in the tree's prevailing spelling.


Generated by Claude Code

… its ADR-0087 entry

The blank structural condition is refused at all three doors since #17322 and
#17495; two places in `packages/spec` still described the world before that.

`structuralConditionRefusal`'s docblock admitted a whitespace-only string on
#15662's ground that it "is consistent on both sides and is ruled correct, not
a defect". #15807 removed that ground at the edge door and #17322 ruled on the
disagreement. The admission itself is unchanged and still correct — this
function answers the SHAPE question and the blank is refused beside it by the
imported evaluated-slot rule — so what changes is the recorded reason, plus a
section recording, without answering, the open question at the sibling ledger
`predicate` slots.

The ADR-0087 D3 entry `flow-edge-condition-evaluated-slot-source-required` named
only `edges[].condition` in its `surface` and `acceptanceCriteria`, so a consumer
replaying the chain was told to sweep the edge key alone and a deployment
carrying a blank `config.condition` was never told to look. Both fields now name
both structural slots, with the node key's own locator phrase. `registry.ts` is
regenerated by `pnpm --filter @objectstack/spec gen:migration-registry`.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
… its assertions

The same stale ground as the docblock, three lines from the symbol: the case
admitted a blank structural condition as "not authored on both sides", which
#15807 and #17322 ended. The two assertions are unchanged and still correct —
they pin that this function does NOT carry the blank rule — and the comment now
says so instead of restating a ruling that no longer holds.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/automation/flow-node-expression-paths.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/error-catalog.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/api/index.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/concepts/metadata-lifecycle.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/data-modeling/drivers.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/data-modeling/objects.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/deployment/cli.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/deployment/environment-variables.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/deployment/validating-metadata.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/kernel/cluster.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/kernel/contracts/metadata-service.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/kernel/services-checklist.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/permissions/authorization.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/permissions/permission-sets.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/plugins/packages.mdx (via sys_metadata (literal, a string literal in surface))

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

  • content/docs/releases/implementation-status.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/releases/v16.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/releases/v17/17-0.mdx (via sys_metadata (literal, a string literal in surface))
  • content/docs/releases/v17/17-3.mdx (via sys_metadata (literal, a string literal in surface))

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
  • 1 changed file(s) yielded no anchor (packages/spec/src/automation/flow-node-expression-paths.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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.

Coarse fallback — 136 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 ec739e67192ce686f956cd490b7bc2b9d0d64cacpackageMentionDocs.

Which tree this was computed on

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants