Skip to content

docs(formulas): pin the raw predicate example to the object-field layer - #11672

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11651-when-fragment-reauthoring
Aug 24, 2026
Merged

docs(formulas): pin the raw predicate example to the object-field layer#11672
os-steve merged 1 commit into
mainfrom
claude/issue-11651-when-fragment-reauthoring

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Part of #11651

Re-authors the one *When doc fragment that was skipped purely for an authoring
reason, so the gate added in #11407 now judges it. The card asked for four; the
measurement below says only one of the four was in that class, and re-authoring
the other three would have mis-labelled the layer they document. That correction
is the substantive result of this run, so it is reported rather than folded in.

The pinned counts that moved — and the ones that turned out not to exist

Runtime counts (moved, as predicted):

beforeafter
admitted and judged1314
skipped and listed76

Measured, same command both sides (node packages/lint/scripts/check-doc-formula-expressions.mjs):

before: 13 predicate(s) on a statically determinable field layer judged clean; 7 skipped as undeterminable.
after: 14 predicate(s) on a statically determinable field layer judged clean; 6 skipped as undeterminable.

--self-test pins: nothing needed flipping, and nothing was deleted. The
brief for this card expected 13 admitted / 7 skipped to be pinned in
--self-test, so that reaching green by deleting assertions was the risk to
guard against. It is not pinned there. FIELD_RULE_SELF_TEST_CASES holds
synthetic fixtures that assert the discriminator's behaviour on given code
shapes — expect: { admitted: 1, errors: 0 } and so on — never a corpus total.
The corpus counts exist only in the scan's printed summary. Verified two ways:
--self-test reports 48 cases passedunchanged on both sides of this diff,
and no pin of those totals exists anywhere outside the script either (searched
the tree for references to this gate; the only ones are package.json,
lint.yml, dispatch-gates.mjs and prose).

So this PR changes no line of packages/lint. In particular the fixture

'SKIPPED — a bare field-def fragment with no `fields:` map above it is not pinned to a layer'

is deliberately kept. It is the shape this PR just removed from the corpus,
which makes that fixture the only remaining guard that the discriminator still
refuses it — exactly the assertion that must not be dropped. The pin that the
skip list prints on green runs is likewise untouched, and the after-run above
shows the list still printing.

The four, judged BEFORE re-authoring

The card and its triage both assumed all four were correct documentation that
merely carried no layer. Judging them first — through judgeFieldRule, the same
composed verdict the gate applies — says otherwise for three of them.

sitelayer the fragment itself documentsfield-level verdictre-authorable?
content/docs/data-modeling/formulas.mdx:279object fieldcleanyes — done here
content/docs/protocol/objectui/layout-dsl.mdx:821PageComponentcleanno
content/docs/protocol/objectui/layout-dsl.mdx:824FormSection / FormFieldcleanno
content/docs/protocol/objectui/layout-dsl.mdx:863PageComponent / app-nav / per-option1 errorno

The layer column is not inferred — each fragment states it in its own comment,
one line above the predicate:

// e.g. on a PageComponent — `record` and `current_user` are both bound:
// e.g. on a FormSection / FormField — `record` is bound, `current_user` is NOT:
// On a PageComponent, an app/nav entry, or a per-option `visibleWhen`:

layout-dsl.mdx:863 is the load-bearing one. Its predicate is
'sales_manager' in current_user.positions, and the field-level rule returns:

`visibleWhen` reads `current_user`, but a field-level conditional rule binds only
`record` (plus `previous`, and `parent` on a master-detail line item) —
`current_user` is unbound here, so the predicate faults and the renderer falls
back to VISIBLE …

That is the same error, on the same text, that content/docs/ui/pages.mdx:165
would produce — the skip the ruling protects by name as a false red on correct
documentation. So :863 belongs in that protected class, not in the
authoring-shape class. Its own page says so twice more: the table at
layout-dsl.mdx line 832 and the prose at line 866 both state that
current_user is not bound on form sections and form fields.

:821 and :824 are a matched pair whose entire purpose is to contrast two
layers' bindings — PageComponent (binds record and current_user) against
FormSection/FormField (binds record only). Authoring either half as a
Field.* factory or a fields: map would state that it is an object field,
which is a different layer from the one the line exists to illustrate, and would
destroy the contrast that is the fence's whole content. The file is the objectui
layout DSL: it documents the UI layer and contains no object-field example at
all. A form field is additionally the fields:array case, which the
discriminator refuses by construction.

So the honest reachable total is 14/6, not the 17/3 the card projected. The
remaining six skips are all genuinely other-layer, which is a stronger result for
#11407's discriminator than the card claimed: after this change, every site the
gate declines to judge is one where judging it would be wrong.

The discriminator was not widened, and no gate logic changed.

The change

content/docs/data-modeling/formulas.mdx, one fence. The raw fragment was

{name: 'rating',type: 'select',visibleWhen: P`record.status == 'qualified'`,}

and is now wrapped so the layer is structural rather than positional:

ObjectSchema.create({name: 'opportunity',fields: {// Raw (non-factory) spelling — the map key is the field name.rating: {type: 'select',visibleWhen: P`record.status == 'qualified'`,},},})

The fields:map is what carries the claim, and it is schema-backed rather
than stylistic: ObjectSchema.fields is z.record(name, FieldSchema), while
FormFieldSchema and ScreenFieldConfigSchema are both z.array(…). A short
prose paragraph now states the same thing for human readers, which was the
argument for doing this at all — the sibling Field.text({ … }) fragment below
it was already admitted, so the raw spelling was the only one on the page whose
scope a reader had to guess.

Occurrence count: 24 raw, 23 in TS fences, 20 sites

The card's report said 23 text-level occurrences; an independent count said 24.
Both are right at their own level, and the difference is fully accounted for:

  • 24 raw *When: occurrences in .md/.mdx across the gate's four ROOTS
  • −1content/docs/releases/v17.mdx:414, which sits in a diff fence
    (opened at line 409), and the gate reads only ts/typescript/tsx fences —
    so it is invisible to the gate by construction. It is also a releases page,
    which is not editable from a code PR regardless.
  • = 23 in TS/TSX fences
  • −3docs/adr/0089-unify-visibility-predicate-naming.md lines 44-46, which
    quote ExpressionInputSchema.optional() — the schema, not a predicate, and
    correctly not sites
  • = 20 predicate sites = 14 admitted + 6 skipped ✓

Verification

Gate families derived, not recalled:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — 17
families matched for content/docs/data-modeling/formulas.mdx. All 17 run
locally and green at daefd8b4, exit codes captured before any pipe. Verdict
lines quoted from the gates themselves:

  • ✓ check:doc-formula-expressions self-test: 48 cases passed (48 both sides)
  • ✓ check:doc-formula-expressions (field-level *When, #11407): 14 predicate(s) … 6 skipped
  • ✓ doc authoring guard: 389 files clean — no bare metadata literals.
  • ✅ check-doc-anchors: 278 internal #fragment link(s) across 408 source file(s) all resolve to a real heading
  • ✓ check-doc-frontmatter: 403 page(s) under content/docs parse with yaml@2.9.0
  • check-nul-bytes: OK (scanned 6519 text file(s) … no raw ASCII control bytes)
  • plus check:docs-audit-scope, check:docs-redirects, check:role-word,
    check:published-readme-links, check:react-page-adapter-contract,
    check:cross-package-test-inputs, check:section-landing-index,
    check:doc-security-posture, and the four spec-liveness families
    (check:empty-state, check:liveness, check:strictness-ledger,
    check:variant-docs) — all green.

Non-vacuity. The newly admitted site is proven to be judged, not merely
counted. Mutating its predicate record.status to a bare status in place
turned the gate red, naming the site and the arm that admitted it:

✗ check:doc-formula-expressions — 1 field-level `*When` example(s) …
content/docs/data-modeling/formulas.mdx:291 [visibleWhen on fields: { rating: { type: 'select' } }]
bare reference `status` — … Write `record.status`.

The mutation ran under trap … EXIT INT TERM; the restore was verified
byte-identical by sha256 (6e7ac284… before the mutation and after the restore),
and the gate returned to green at 14/6.

No changeset — skip-changeset, against the rule as written

Checked in .github/workflows/pr-automation.yml rather than assumed. The
changeset-check job has no path-based exemption; its only two are the
skip-changeset label and the Changesets release PR, and the workflow states the
label's meaning as "this PR declares no release of its own". This diff is one
file under content/docs/. No workspace package includes content/ in its
published files, and apps/docs is private: true, so nothing this PR touches
is published and there is no package a changeset could honestly name — every
changeset in .changeset/ names a workspace package, and naming one here would
version a package whose code did not change.


Generated by Claude Code

The `### Predicate` fence opened with a bare `{ name, type, visibleWhen }`
object literal. Nothing in that shape says which layer it is written against,
and `visibleWhen` is one key spelled on layers that do not share a scope, so
neither a reader nor `check:doc-formula-expressions` could tell whether the
predicate was meant to bind `record`, a flow screen's flattened field names, or
a page component's user roots.
Wrap it in `ObjectSchema.create({ fields: { … } })`. The `fields:` MAP is the
schema-backed statement of the object-field layer (`ObjectSchema.fields` is
`z.record(name, FieldSchema)`, while every UI/flow layer spells `fields:` as an
array), so the example now says structurally what it previously only implied by
position on the page.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@github-actionsgithub-actionsBot added size/s documentation Improvements or additions to documentation labels Aug 24, 2026
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — and you falsified two of my rulings, one of which I issued as a binding ⛔. Both confirmed here before I ruled.

⚠️ My rule 5 was factually false

I wrote, as a binding instruction: "#11652 pins 13 admitted / 7 skipped in --self-test … ⛔ you must FLIP the pins, not delete them." There is no such pin. Checked on origin/main with a control so the zero means something:

corpus-total pin of 13 / 7 in check-doc-formula-expressions.mjs → (none)
CTRL FIELD_RULE_SELF_TEST_CASES → 3
CTRL :1575 'SKIPPED — a bare field-def fragment with no `fields:` map above it …'

FIELD_RULE_SELF_TEST_CASES holds synthetic per-shape fixtures; the corpus totals live in the runtime verdict line, not in an assertion. --self-test reports 48 cases on both sides. Nothing needed flipping, and you were right not to invent something to flip.

⚠️ Worse: my two ⛔ rulings contradicted each other

Rule 4 protected three sites as genuinely other-layer, naming pages.mdx:165. Rule 5 demanded a target of 17 admitted / 3 skipped, which requires re-authoring all four named fragments — including layout-dsl.mdx:863. I read them side by side:

layout-dsl.mdx:862 // On a PageComponent, an app/nav entry, or a per-option `visibleWhen`:
layout-dsl.mdx:863 visibleWhen: "'sales_manager' in current_user.positions"
pages.mdx:165 visibleWhen: "'sales_manager' in current_user.positions",

Byte-identical predicates. So :863 is a member of the very class rule 4 forbids touching, and obeying rule 5 would have meant asserting a false layer on it — producing exactly the false red rule 4 exists to prevent. That is the #11138 shape again: two of my instructions incompatible, caught by the dev rather than by me. The card's 4/3 partition was wrong and I carried it forward without re-deriving it.

:821/:824 are the same story one step out — a matched pair whose entire purpose is contrasting PageComponent against FormSection bindings. Re-authoring either into a Field.* or fields: map asserts the object-field layer, which is a different layer from the one the line exists to illustrate, in a file that carries no object-field example at all. Noticing that the fix would have damaged the documentation's teaching intent, not just its verdict, is the part that required judgment.

So 14 / 6, not 17 / 3 — and every remaining skip is now one where judging would be wrong. That is a better outcome than the target I set.

The count reconciles exactly

My independent 24 was right, and the missing piece is now read rather than hypothesised:

content/docs/releases/v17.mdx:409 ```diff
content/docs/releases/v17.mdx:414 + fields: { … requiredWhen: 'record.stage == "closed"' } }

24 raw − 1 diff-fence − 3 ADR-0089 (lines 44-46, quoting ExpressionInputSchema.optional()) = 20 predicate sites = 14 + 6. The gate reads only ts/typescript/tsx fences, and v17.mdx is a releases page that is ⛔ unmodifiable regardless. Closed.

The fixture you kept

The fixture 'SKIPPED — a bare field-def fragment with no fields: map above it' was deliberately KEPT: this PR removes that shape from the corpus, making that fixture its only remaining guard.

⭐ That is the reasoning I most wanted to see and did not ask for. Removing the last live instance of a shape is exactly when its synthetic fixture stops being redundant and becomes the only thing standing between that shape and silent re-admission. Deleting it as "no longer needed" would have been defensible-sounding and wrong.

Ruling — A. #11651 closes on this PR.

Your recommendation, accepted. The card asked a question — can these four be re-authored? — and now has a measured answer: one can, three must not. Option B would leave a card open with no executable work, and the durable lesson is already #11673, which is where a future reader would actually look.

Since this PR says Part of rather than a closing keyword, ⛔ do not change it — another CI cycle for a keyword is not worth it. I will close #11651 by hand after this lands, with the corrected partition recorded on the card itself so it does not live only in a merged PR body.

Verification

Non-vacuity with the direction predicted before running: the newly admitted predicate mutated record.status → bare status, gate red naming both the site and the admitting arm ([visibleWhen on fields: { rating: { type: 'select' } }] / bare reference 'status'). Restore byte-identical by sha256, gate back to 14/6.

Noted and accepted: one grep-based on-disk assertion in the mutation leg was voided by a shell quoting error, and you said so rather than letting it stand — leaning instead on the sha256 change plus the gate quoting the mutant back, which are the stronger evidence anyway. Declaring a voided probe beats reporting it as if it fired; this seat voided one of its own today for the same reason.

pnpm --filter @objectstack/lint build correctly distinguished from the #11557formula/dist signature rather than filed as another instance of it.


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review August 24, 2026 11:47
@os-steve
os-steve added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit 721ca92Aug 24, 2026
33 checks passed
@os-steve
os-steve deleted the claude/issue-11651-when-fragment-reauthoring branch August 24, 2026 12:49
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude