Skip to content

test(scripts): census the $-dialect lowercase aliases before objectui#8568 is ruled - #8977

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-8568-alias-census
Sep 10, 2026
Merged

test(scripts): census the $-dialect lowercase aliases before objectui#8568 is ruled#8977
os-steve merged 3 commits into
mainfrom
claude/issue-8568-alias-census

Conversation

@os-steve

@os-steve os-steve commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Refs #8568

⛔ This PR rules NOTHING. objectui#8568 names three directions — retire the converter's aliases, teach ValueDataSource the aliases, keep both and document the split — and this change takes no position on any of them. The card stays OPEN.

It delivers only the prerequisite the card names in its own words:

option 1's cost is entirely a function of how many authors are relying on the tolerance, and nobody has measured that.

Zero behaviour change. No acceptance set moves. operatorMap, ValueDataSource and every $-dialect arm are byte-identical to main. Against the merge-base, the change set is exactly 3 files, 958 insertions, 0 deletions: two new files under scripts/ plus one scripts entry in the root package.json.


The number

pnpm census:dollar-dialect-aliases, HEAD 738cc8bdc:

ALIASES = accepted but not canonical (4) : $notin $notcontains $startswith $endswith

Alias occurrences, whole tree              : 24
  of which in operator-key position        : 7
Alias occurrences in the AUTHORED CORPUS   : 0
  of which in operator-key position        : 0

role                  total  payload  mention
converter-ledger         10        3        7
refusal-ledger            6        0        6
implementation            4        4        0
release-notes             4        0        4

Every one of the 24 occurrences is either the map that defines the aliases, or something pointing at that map. Nothing in examples/, apps/, content/, docs/, e2e/ or skills/ authors one. The four "payloads" outside the map are the converter's own unit tests exercising the alias arms.

So: in the corpus this repository can reach, option 1's blast radius is zero authored filters and the work is the ledger — the map's four rows, the converter's alias assertions, and the README rows PR #8565's pin will name.

Why examples/ is zero for a structural reason, not a lucky one

examples/ authors filters exclusively in the object-form dialect ({ "operator": "eq" }), not the $-dialect. Measured: 7 rows across 3 files under components-complex-filter-builder/, and no $-operator in key position anywhere in that tree. That dialect has its own table (FILTER_OPERATOR_ALIASES), which is tolerant by design and case-folding by construction (normalizeFilterOperator lowercases before the lookup) and carries bare startswith / endswith / notcontains / notin as first-class members. Option 1 does not touch it.

Conflating the two dialects is the single largest way to misread this number, and it is easy: a grep for startswith cannot tell them apart. The census requires the $ and requires operator-key position, and ships a control that fails if a sibling row is ever counted.


⚠️ What this census CANNOT see — the part that bounds every option

The authored-corpus zero is consumer-local, not seam-wide (objectui#6839). This repo is a renderer; the filters it lowers are authored as metadata, and the metadata store is not in this tree. Invisible from here, and therefore not in any count above:

  • view / list-view / sharing-rule criteria already stored in a deployment database. FilterConditionField writes criteria_json into exactly such a store, and nothing here can enumerate what is in one;
  • seed, demo and app metadata shipped from objectstack-ai/objectstack or any other producer repo;
  • filters assembled at runtime by published @object-ui/* consumers;
  • any filter whose operator key is COMPUTED ({ [op]: value }), which no static scan resolves.

A zero here means "this repo does not rely on the tolerance". It does not mean "no author relies on the tolerance", and it must not be quoted as if it did.

One measured fact that narrows the unseen population

kvToCondition — the reader that loads stored $-criteria back into the filter builder — has arms for $contains, $icontains, $notContains, $ncontains, $gt, $lt, $gte, $lte, $startsWith, $endsWith, $null, $exists, $in, $nin, $ne. It has an arm for none of the four aliases (verified: 0 matches for case '$notin'|'$startswith'|'$endswith'|'$notcontains'), so they fall to default: return null — "criteria can't be represented", which drops the admin into the raw-JSON editor.

⇒ On the one authoring surface this repo ships, a stored lowercase alias already fails to round-trip today. It executes on the ObjectStack adapter and cannot be reopened in the builder. That is a real, pre-existing degradation for exactly the population option 1 would break, and it lowers option 1's marginal cost.


Verification of the card's own claims

PR #8565's reconciliation pin does redden when the aliases are removed — CONFIRMED. readme-filter-operator-table.test.ts reads operatorMap out of filter-converter.ts's source (operatorMapFromSource, anchored on const operatorMap) and asserts, among others, that every spelling in the README's first column lowers — so a README row whose spelling no longer lowers is reported as is refused by convertFiltersToAST, so it is not a supported operator, and that row's worked example throws. Removing an alias reddens on two independent assertions.

Anchors re-derived, not trusted. The four spellings are exactly the four named. Their positions are not: measured today they are at filter-converter.ts L75 / L79 / L81 / L83 with the comment at L65 — a uniform +6 from the line numbers in the dispatch. The set held; the addresses did not.


What is delivered, and why a committed script rather than a one-off

A one-off number would have been enough for a stable count. This one is not stable and is not simple:

  1. It must be re-measured at the moment a direction lands, and by whoever lands it — a count from today does not price a change made next week.
  2. The naive measurement is wrong in a way that looks right. Two dialects, one of them deliberately case-folding; a case-insensitive grep reports the canonical corpus as alias reliance, and a case-insensitive exclusion erases the aliases entirely. Both hazards were live while this was being written.
  3. After option 1 the alias set is empty, and a script that hunted four hardcoded spellings would keep reporting on a dialect the code no longer speaks. So the set is DERIVED — operatorMap read from source (the technique the docs(data-objectstack): re-derive the filter-operator table from convertFiltersToAST and pin it #8565 pin already uses, because the map is not exported) minus @objectstack/spec's FILTER_OPERATORS. Hardcoding the four would have rebuilt this card's own defect one level up.

Report-only, under the tree's existing census:* convention (census:body-dialect, census:tsconfig-test-parity, census:cross-file-line-citations) — the spelling that already means "runnable, reported, not blocking". Not wired into CI: objectui#8568 reserves that decision.

The four controls, because a zero with no lit positive control is not a reading

control kind reading on this tree
canonical-twin positive, must be LIT 3/3 twins visible to the same scanner: $notContains=42, $startsWith=36, $endsWith=29
authored-reach positive, must be LIT 9 canonical $-operator payloads in NON-TEST authored files (AuditLogPage.tsx:122 $gte, recordReadability.ts:164 $in, …)
impossible negative, must be DEAD $startswithzzz: 0
sibling-dialect negative, must be DEAD 4 $-free bait rows present in FILTER_OPERATOR_ALIASES; 0 counted as $-dialect

authored-reach excludes tests deliberately: reaching only the suites is not reaching the metadata. sibling-dialect measures its own bait, because "counted zero sibling rows" and "there are no sibling rows left to count" are otherwise the same reading.

Ablation — the controls can actually fail

Run on the committed tree, mutation proven on disk by anchor counts and blob hash before the result was read, restored by git checkout HEAD -- path and proven restored by blob equality plus an empty git diff HEAD:

leg sibling-dialect census exit
baseline PASS — 4 bait rows, 0 counted 0
$-guard loosened at BOTH layers (prefilter + regexes) FAIL — 4 bait rows, 10 counted 1, "this run is NOT a reading"
restored PASS — 4 bait rows, 0 counted 0

⚠️ Reported because it is what happened, not what was predicted. The first ablation loosened the REGEX alone and the control stayed GREEN — the literal prefilter in scanText had already excluded the file before the regex ran. So the runtime control witnesses the composed scanner; a one-layer regression is caught by the suite, which pins $-strictness and case-sensitivity directly on scanText. That asymmetry is now written where the control is defined instead of being left for the next reader.

The suite also caught two real defects in the instrument, both of the "PASS while witnessing nothing" family: the census scanning itself (its fixture text includes the impossible spelling, so the negative control fired on its own declaration the moment the files became tracked), and the bait gap above. Both are fixed in the second commit.


Gates

Re-run on the merged tree at HEAD 738cc8bdc, after origin/main advanced to efead6c60 mid-flight. Every exit code captured to a file before it was read, never through a pipe.

gate exit note
pnpm run type-check:scripts 0 scripts/**/*.ts is a glob include, so the new test is covered automatically
pnpm exec turbo run lint --concurrency=2 0 CI's own pnpm lint; 47/47 tasks, 0 errors repo-wide
pnpm exec vitest run scripts/ 0 136 files passed, 2 skipped; 3990 tests passed
pnpm run check:new-line-citations 0 the gate that landed in #8974 while this was in flight — 0 new cross-file citations added by this branch, its own controls firing
pnpm run check:control-bytes 0 plus a direct grep -naP over the diff, no hits
node scripts/check-changeset-presence.mjs 0 none owed — nothing published moved
node scripts/check-governed-queue-guard.mjs --test (3 paths) 0 NOT GOVERNED — ordinary review route
pnpm run check:doc-types 0
pnpm run check:doc-snippets 0 first run exit 2 = PREREQUISITE NOT MET; scoped build, then 0
pnpm run check:doc-examples 0 same: exit 2 first, then 0. 124 blocks, 89 ledger rows all matching
pnpm run check:entry-guard 0 shrink-only counters unmoved
node scripts/check-unreferenced-sources.mjs 0
node scripts/check-pre-install-import-graph.mjs 0
node scripts/check-lint-coverage.mjs 0 46/46
node scripts/check-type-check-coverage.mjs 0

Lint population, stated so the reading is checkable. An auxiliary whole-tree pnpm exec eslint . --no-inline-config --format json (not CI's command — CI runs turbo run lint per package) judged 4720 files by eslint's own config. The two new files carry 0 errors and 0 warnings. The files it reports with errors are all under apps/, packages/ and e2e/; this diff touches none of them. Invariance: eslint.config.js declares no type-aware linting (0 matches for projectService, parserOptions, project:, tsconfigRootDir, and 0 for tsconfig), so nothing in this diff can move the verdict on any untouched file.

Heavy runs went through the container's shared verify lock; each printed VERDICT command-exit 0. Those wall-clock figures are shared-box seconds, not idle-box seconds.

Merge with main. origin/main advanced from 3ff3fc63f to efead6c60 during this task. Merged in and resolved: the only conflict was the root package.json, where #8974 and this branch each added a scripts entry immediately after census:cross-file-line-citations. Both entries kept. origin/main is an ancestor of this head, and the change set against the new merge-base is unchanged at 3 files / 958 insertions.

Serial adjacency, re-derived here rather than cited. All open PRs enumerated and their file lists read directly. At the first reading none touched the root package.json; by the time #8974 merged, one did — which is the conflict resolved above, and a demonstration of why the dispatch said not to trust any adjacency snapshot, including this one. Both new files are new, so no open PR can conflict on them.


Out of scope, filed separately

objectui#8976 — the same seam has a third acceptance-set disagreement, in the direction opposite to this card's: $icontains is a canonical member of the spec's FILTER_OPERATORS, ValueDataSource executes it, this repo's own FilterConditionField emits it for the containsCaseInsensitive builder operator, and convertFiltersToAST refuses it with INVALID_FILTER / 400. Measured both legs in one process with a $contains control that passes on both sides. PR #8565's pin is structurally blind to it: it holds the README tables against operatorMap's keys and the unknown-operator error's list, and $icontains is in neither.

Noted, not filed: kvToCondition's missing alias arms (written up above as census evidence — it reads as consistent with the contract-first direction rather than as a defect, and whoever rules objectui#8568 is the reader who needs it).


For whoever rules the three options

  • In-repo cost of option 1 is zero authored filters and a bounded ledger edit. CI will name the ledger rows: PR docs(data-objectstack): re-derive the filter-operator table from convertFiltersToAST and pin it #8565's pin reddens on removal, by design.
  • The out-of-repo cost is unmeasured and unmeasurable from here. Stored criteria_json is the population that matters and it is not in this tree. The census says so in its own output rather than leaving the zero to be quoted flat.
  • Those authors are already half-broken. A stored lowercase alias does not round-trip through the filter builder today.
  • Re-run pnpm census:dollar-dialect-aliases at the moment a direction lands. It derives its own alias set, so it stays correct on a tree where the aliases are gone.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB

…sks about

objectui#8568 records two acceptance sets for one `$`-dialect and names three
directions without ruling any of them. It says the choice needs one number
first: "option 1's cost is entirely a function of how many authors are relying
on the tolerance, and nobody has measured that."

This adds that measurement and nothing else. No acceptance set moves, no
`operatorMap` row changes, and the census is report-only under the tree's
existing `census:*` convention rather than a `check:*` gate, so nothing here
makes an undecided question blocking.

The alias set is DERIVED — `operatorMap` read from the converter's source (the
technique `readme-filter-operator-table.test.ts` already uses, because the map
is not exported) minus `@objectstack/spec`'s `FILTER_OPERATORS`. Hardcoding the
four spellings would have rebuilt the card's own defect one level up, and would
have left the census hunting a dialect the code no longer speaks on a tree where
the aliases had been retired.

Four controls, because a zero with no lit positive control is not a reading:
two positive (the camelCase twins must be visible to the same scanner; a
canonical `$`-operator payload must be found in a NON-TEST authored file) and
two negative (an impossible spelling reads zero; the `$`-free sibling dialect in
`FILTER_OPERATOR_ALIASES` is never counted). The last one is the conflation the
census exists to avoid: that table is tolerant by design and case-folding by
construction, and reading its rows as `$`-dialect reliance would price the
breaking option against filters it does not touch.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
…bait its negative control declines

Two defects in the instrument, both found by running it rather than by reading
it, and both of the same family: a control that reports PASS while witnessing
nothing.

SELF-REFERENCE. The census and its suite carry every alias spelling as fixture
text, including the impossible spelling the negative control is built on. The
moment those two files became TRACKED, the census started scanning them and the
`impossible` control fired on its own declaration — caught by the suite on the
first run after the first commit, which is the only moment the defect is
visible. They are carved out now, and the number of occurrences carved out is
printed rather than hidden.

BAIT. `sibling-dialect` asserted only that zero `$`-free rows had been counted.
"The census counted zero sibling rows" and "there are no sibling rows left to
count" are the same reading unless the bait is measured, so the control now
counts the `$`-free rows in `FILTER_OPERATOR_ALIASES` with a deliberately
`$`-less probe and fails when it finds NONE.

Ablation, on the committed tree: loosening the `$`-guard at both layers
(the literal prefilter and the regexes it guards) turns `sibling-dialect` red
with 10 conflated hits and the census exits 1, "this run is NOT a reading";
restoring returns it to PASS with 4 bait rows and 0 counted. Ablating the REGEX
ALONE leaves the control GREEN, because the prefilter has already excluded the
file — so the runtime control witnesses the COMPOSED scanner, and the
`$`-strictness of the regexes is pinned directly on `scanText` in the suite
instead. That asymmetry is now written down where the control is defined rather
than left for the next reader to rediscover.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
Both sides added a scripts entry immediately after `census:cross-file-line-citations`;
kept both. No other file conflicted, and this branch touches nothing under packages/.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
@os-steve
os-steve marked this pull request as ready for review September 10, 2026 14:08
@os-steve
os-steve added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit ca67d42 Sep 10, 2026
33 checks passed
@os-steve
os-steve deleted the claude/issue-8568-alias-census branch September 10, 2026 14:26
os-steve pushed a commit that referenced this pull request Sep 10, 2026
…empty (objectui#8568)

PR #8977's live-tree case asserted `aliases.length > 0`, `rows.length > 0` and
`selfCarved > 0` unconditionally. Retiring the four aliases makes all three
false at once, by construction: the census derives its subject set as
operatorMap-minus-spec, and that set is now empty. The census script already
models the empty tree and a synthetic unit case already pins it; only the
live-tree case had the pre-ruling shape baked in.

The blindness guard those three carried is real, so it is re-expressed rather
than deleted. On an empty tree the instrument's reach is proved by the
`authored-reach` control, which counts canonical `$`-operator payloads in
non-test authored files and cannot be satisfied by a scanner that never opened
the authored corpus; the empty set is then held to being DERIVED rather than the
residue of a failed read on either side. The pre-retirement branch is kept
intact, so the case still holds on any tree that carries an alias again.

The subset check is recomputed in the test rather than by calling
`deriveAliases` again: measured under ablation, a stubbed `deriveAliases`
returning `[]` satisfies its own output, so reusing it would have made this
branch pass while witnessing nothing.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
github-merge-queue Bot pushed a commit that referenced this pull request Sep 10, 2026
…rtFiltersToAST` (objectui#8568) (#8988)

* fix(core)!: retire the four lowercase $-dialect aliases (objectui#8568)

`convertOperatorToAST`'s `operatorMap` carried `$notin`, `$notcontains`,
`$startswith` and `$endswith` "for tolerance" while `ValueDataSource` refused
the same four by design (objectui#8447: alias arms there "would fossilise a
second dialect"). One authored filter therefore had two fates depending on
which data source was behind the view. The maintainer ruled the tolerant side
out: the `$` dialect follows `@objectstack/spec`'s spellings, with no
deprecation window, and the documentation follows the implementation.

The four are refused BY NAME and the refusal names the canonical spelling, so
the repair is a key rename rather than a diff of two operator lists. A generic
unknown operator still gets the generic message; that asymmetry is pinned.

`packages/data-objectstack/README.md`'s tables and PR #8565's reconciliation
pin follow the code: the pin reddened on exactly the four rows that had to
change, which is the signal it was built to give.

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

* test(core): make the named-refusal assertion discriminate (objectui#8568)

Ablating the named arm left `names the canonical spelling` GREEN: the generic
unknown-operator message's own "Supported operators:" list already spells
`$nin`, `$notContains`, `$startsWith` and `$endsWith`, so `toContain(canonical)`
was a case that passed while witnessing nothing. The prescription phrase is what
only the named arm can produce, so that is what is asserted now, with the
measurement recorded beside it.

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

* test(scripts): keep the alias census a reading once the alias set is empty (objectui#8568)

PR #8977's live-tree case asserted `aliases.length > 0`, `rows.length > 0` and
`selfCarved > 0` unconditionally. Retiring the four aliases makes all three
false at once, by construction: the census derives its subject set as
operatorMap-minus-spec, and that set is now empty. The census script already
models the empty tree and a synthetic unit case already pins it; only the
live-tree case had the pre-ruling shape baked in.

The blindness guard those three carried is real, so it is re-expressed rather
than deleted. On an empty tree the instrument's reach is proved by the
`authored-reach` control, which counts canonical `$`-operator payloads in
non-test authored files and cannot be satisfied by a scanner that never opened
the authored corpus; the empty set is then held to being DERIVED rather than the
residue of a failed read on either side. The pre-retirement branch is kept
intact, so the case still holds on any tree that carries an alias again.

The subset check is recomputed in the test rather than by calling
`deriveAliases` again: measured under ablation, a stubbed `deriveAliases`
returning `[]` satisfies its own output, so reusing it would have made this
branch pass while witnessing nothing.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants