Repository navigation
Commit b793010
Fixes #21409
Clause-②: no (narrowing)
Dispatched by the PM claim `5953981594` (PM loop round 1, `domain:spec`
seat 1), on the triage direction in the card body (the B answer
`5952826307` to `5952467081`). Session
`session_01UtnxvdiN376GF3sgXwAw4d`.
The row wildcard `'*'` is what a `count` aggregates (`COUNT(*)`). It is
now admitted in exactly one place, a measure that counts. Everywhere
else it is refused at the authoring parse, naming the slot and
prescribing a `count` or a column. The measured `500 DATABASE_ERROR` at
`POST /api/v1/analytics/dataset/query` is now a `400 VALIDATION_FAILED`.
## What changes (`@objectstack/spec`)
| position | slot | refusal | spelled as |
|---|---|---|---|
| 1 | cube measure `MetricSchema.sql`, under any `type` but `count` |
`custom` at `sql` | refinement asking the ONE predicate |
| 2 | cube dimension `DimensionSchema.sql` | `invalid_format` at `sql` |
pattern `ANALYTICS_COLUMN_PATH` (the dataset dimension's own) |
| 3 | dataset measure `DatasetMeasureSchema.field`, under any
`aggregate` but `count` (or none: a `derived` measure) | `custom` at
`field` | refinement asking the ONE predicate |
| control | dataset dimension `DatasetDimensionSchema.field` |
`invalid_format` (since PR #21240) | unchanged |
- **One predicate.** `rowWildcardOutsideCount(reference, aggregate)` and
its refusal `rowWildcardOutsideCountRefusal(slot, aggregateKey,
aggregate)` live in
`packages/spec/src/data/analytics-column-reference.ts`, beside the
column-reference grammar, outside the `data` barrel (not published API).
Both measure refinements call them. Neither restates the rule. The pin
asserts each issue IS the builder's output for its slot.
- **Dropped refinements.** The two measure refinements are cross-field,
so they cannot be a JSON-Schema `pattern`. They are declared in
`dropped-refinements.baseline.json`: the roots `data/Metric` and
`ui/DatasetMeasure`, plus the embedded sites the build printed
(`data/Cube`, `ui/Dataset`, and the four installed-package API schemas).
The measured counts move to 217 schemas / 652 sites. Position 2 is a
`pattern`, so the published JSON Schema states it.
- **ADR-0087.** One D3 entry:
`migrations/entries/semantic/18.analytics-row-wildcard-outside-count-refused.ts`.
`registry.ts` was regenerated by `gen:migration-registry`, never edited
between markers. There is no D2 conversion: rewriting to `count` changes
the figure the author asked for, and only the author can name a column.
There is no `RETIRED_KEYS_BY_MAJOR` row, and no `STEP18_RATIONALE`
fragment. That fragment is optional, and adding it would be a hand edit
to `registry.ts` outside the claimed generated region.
`spec-changes.json` and the upgrade guide stay at protocol 17, as for
every major-18 entry, and both checks are green.
- **Why an entry.** I judged this against
`cube-member-sql-expression-retired` and
`dataset-member-field-expression-refused`, the two accept-set narrowings
of the same slots. Both register a D3 entry because a stored document
needs a prescription and has no mechanical rewrite. The same holds here.
- **Liveness.** `analytics_cube` `measures.sql` and `dimensions.sql`
were the notes that pointed the count-only boundary at #21000. They are
re-pointed here. `dataset` `measures.field` states the narrowing. All
three stay `live`, re-verified 2026-10-02.
- **Docs.** `content/docs/references/ui/dataset.mdx` is regenerated: the
measure `field` describe now says `"*"` is for a count.
- **Changeset.** `@objectstack/spec` `minor`, with the BREAKING banner,
the `(narrowing)` arm, FROM → TO and one ADR-0087 marker (`registered
analytics-row-wildcard-outside-count-refused`).
- **Runtime.** Unchanged. No strategy code in `service-analytics` was
touched.
## Zone 1, read as written — one point flagged, not silently chosen
The direction says "one cross-field rule per position, sharing one
predicate". Position 2, a cube dimension, has no aggregate, so no rule
there can be cross-field. Zone 2 item 2 says to find how the control
(the dataset dimension, PR #21240) spells its `'*'` refusal and follow
it. The control spells it as a `pattern`, `ANALYTICS_COLUMN_PATH`, not
as a refinement. So position 2 takes that same pattern, and the two
dimension slots now publish one identical pattern. The cross-field
predicate covers the two measure slots, where an aggregate exists.
This is strictly stronger than a third refinement would be. The
published JSON Schema carries this half, and no dropped-refinement row
is needed for it. There is still one rule source (`COLUMN_PATH`, read
twice) and one cross-field predicate (read twice). Nothing has a second
spelling.
## The PM's mechanism assumptions, measured
1. **Confirmed.** On `ceb4a939b4`, `analytics-column-reference.ts`
declared the shared grammar, and `ANALYTICS_COLUMN_REFERENCE` admitted
`'*'` for every member. `cube-member-sql-column-reference.test.ts`
pinned `'*'` on a cube dimension. That pin is now replaced by the
refusal, because it pinned exactly the branch removed.
2. **Partly falsified.** The control is a pattern, not a refinement (see
above). The measure positions are refinements (`superRefine` chained on
the strict objects, the `DatasetSchema` precedent), because only they
are cross-field.
3. **Measured. What a stored document meets now:**
- **At `/meta` reads.** It is served as stored, with the refusal on
`_diagnostics`. Probe through `computeMetadataDiagnostics` on the built
spec: a stored dataset with `{ aggregate: 'sum', field: '*' }` reads
back as `valid: false` with `measures.1.field` / `custom`. A stored cube
reads back as `measures.total.sql` / `custom` and
`dimensions.everything.sql` / `invalid_format`. A re-save through the
write door is refused at the slot.
- **At the dataset query door.** The route parses every dataset it is
handed, inline or saved. A stored dataset carrying such a measure is
refused `400 VALIDATION_FAILED` on every query. That includes a query
that selects only its healthy `count`, which answered `200` before. It
fails closed, never a stand-down, and the blast radius is the dataset.
The door test pins both selections.
- **Stored `analytics_cube` rows.** Read from code: these never reach
the analytics registry. `serve.ts` feeds it from the stack definition's
`analyticsCubes` only, and that parse (`defineStack`) refuses such a
cube.
## Census: no producer (triage's "no producer is known", measured)
- **This repo at `ceb4a939b4`.** `git grep` of every `field` / `sql`
value spelled `'*'` over `examples`, `packages` (fixtures included),
`content`, `skills`, `apps`, `scripts` and `docs` found 173 hits. Each
was read in its enclosing object literal: 154 under a `count`. The other
19 are QueryAST aggregations (`function: 'sum', field: '*'` in objectql
conformance tests, which is not one of the three positions), comments,
and strategy-level `method: 'count'` literals. Zero sit at a non-count
cube measure, a cube dimension or a non-count dataset measure.
- One more author was found through a loop variable: the cube-dimension
accept pin above.
- **objectui at the `.objectui-sha` pin `89cad75d55`.** Read-only `git
grep` at the pin found zero `field` / `sql` values spelled `'*'`. Lit
controls: 51 `aggregate: 'sum'`, 438 `field: 'amount'`. A `'*'` scan of
the 302 files mentioning `aggregate` found only `objectName: '*'` bus
events, query-builder `'*'` and i18n required marks. None is a dataset
or cube slot.
- **Deployed metadata.** NOT MEASURED.
## The door cell: 500 → 400
`packages/rest/src/analytics-dataset-row-wildcard-door.test.ts` drives
the real route over a real `ObjectQL` engine with a better-sqlite3
`SqlDriver`. It uses `AnalyticsServicePlugin`'s own composition, once
per strategy, with read counters proving which strategy answered.
- **Before.** I ran this test against the BASE spec build (`ceb4a939b4`,
dist verified free of the new predicate): `Tests 20 failed | 4 passed
(24)`.
- Every inline cell answered `{"error":"Internal server
error","code":"DATABASE_ERROR"}: expected 500 to be 400`. That held for
`sum`, `avg`, `min`, `max` and `count_distinct` over `'*'`, on both
strategies.
- The saved dataset, querying its healthy count, answered `200`.
- The four count controls were green.
- **After.** On the fixed spec build: `Tests 42 passed (42)` (this
file's 34 plus the neighbouring
`analytics-16019-driver-declared-fault.test.ts`'s 8).
- Every refused cell answers 400 `VALIDATION_FAILED` with the issue at
`measures.2.field` (`custom`).
- Raw-SQL and engine-aggregate counters stay at 0.
- The `count`-over-`'*'` controls answer the row counts,
`[{a,2,2},{b,1,1}]`, on native SQL (raw-SQL counter ≥ 1) and on ObjectQL
(aggregate counter ≥ 1).
- The cells for a saved dataset selecting the wildcard measure itself
were added after the base run, so their base answer is NOT MEASURED.
They compile the same measure the inline cells do.
## Tests (final union at `60644d73d9`, after the `main` merge)
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2` gave `Test Files 601 passed (601)`, `Tests 17647 passed
| 1 todo`.
- It includes the new
`src/data/analytics-row-wildcard-count-only.test.ts` (31 cases: every
non-count `AggregationMetricType` and `AggregationFunction` option,
every `DimensionType`, the `derived` case, the controls, `CubeSchema`
and the `analytics_cube` door, `defineCube`, `DatasetSchema` and the
`dataset` door, `defineStack` with STACK_SCHEMA_INVALID / 422, the
JSON-Schema halves with the ledger rows, and the D3 entry).
- `pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2
src/analytics-dataset-row-wildcard-door.test.ts
src/analytics-16019-driver-declared-fault.test.ts` gave `Tests 42 passed
(42)`.
- Also run before the merge (`934b70a2db`; the incoming `main` commits
touch neither package):
- `rest --project local`: `256 passed (256)` files, 4848 tests.
- `service-analytics`, as the main consumer of both shapes: `168 passed
(168)` files, 3794 tests.
- spec repo-project subset (`cube-member-inner-name-retirement`,
`cube-refresh-key-retirement`, `step18-rationale-merge`,
`liveness/evidence`, `liveness/proof-registry`): `131 passed`.
- `pnpm --filter @objectstack/spec typecheck` and `pnpm --filter
@objectstack/rest typecheck`: exit 0.
- The whole spec repo project (48 files) is NOT MEASURED locally. One
run exceeded the foreground cap, so it is declared to CI.
## Ablation (one-shot, not kept)
From the committed fix, through `scripts/ablation-replace.mjs`,
`rowWildcardOutsideCount` was made to answer `false` (anchor 1 → 0,
marker 0 → 1, blob `2bb692602dc8` → `4bdfba658269`). The new spec file
went `19 failed | 12 passed (31)`. Red: the predicate table, every
position-1 and position-3 cell, and the four door cases. Green: position
2 (a pattern, untouched by the predicate), every control, the
JSON-Schema halves and the D3 pin. That is the predicted direction.
Restored with `git checkout HEAD --`: blob equals the HEAD blob and `git
diff HEAD` is empty, under a `trap` on EXIT/INT/TERM. The spec suite
imports the source by relative path, so no `dist/` sits on its
resolution path.
## Gates
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` (no paths, merge base `39a912ea7`) derived 115 families. I
ran all 115, and `--ran` reconciles **113 run green, 2 NOT MEASURED
(exit 3, prerequisite), 0 UNRUN**:
- `pnpm check:dual-build-cjs-loads`: needs every package's `dist`, which
means a whole-repo build.
- `pnpm check:type-check-debt`: its `--re-measure` builds the ledgered
packages' closure itself, and that build passed the 300 s per-gate cap.
- Both are whole-tree, and CI's `Lint & Repo Gates` runs them.
- `check:skill-examples` first exited 3 (client-react unbuilt). After
building `client` and `client-react` it exited 0 (`259 prose examples
type-check`).
- `pnpm --filter @objectstack/spec check:generated` passes all 15
artifacts after the merge. The only stale artifact before was
`content/docs/references/**`, regenerated with `gen:docs`.
- **Clause-② measured.** `node scripts/pm/check-widening-tells.mjs
--declaration no --diff` (merge-base diff) exited 0 with no widening
tell. It stated two silences: the `rowWildcardOutsideCountRefusal(`
lines name an imported factory it does not resolve. The predicate is not
exported from any published entry (`check:api-surface` green, artifacts
byte-identical), so the arm is `no (narrowing)`, as triage wrote.
- **ESLint (narrowed, a measurement).**
- Population: `eslint.config.mjs` lints
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`, and all 8
changed `.ts` files fall inside it.
- Count: `--format json` read back 8 files, 0 errors, 0 warnings.
- Invariance: the config never enables type-aware linting (no
`parserOptions.project`, no typed rules), so this diff cannot move a
verdict on an untouched file.
- The repo-wide `pnpm lint` is CI's.
## Serial
- PR #21413 landed while this branch was in flight and touched
`dropped-refinements.baseline.json` and `registry.ts`. I merged `main`
through `scripts/pm/os-regen-merge.sh`.
- The baseline conflicted on its `measured` counts only. It was not
text-merged: I took `main`'s file and re-declared this branch's 12
sites, and the counts were recomputed from the entries. `gen:schema`
then validated the ledger against the tree.
- `gen:migration-registry` reproduced the auto-merged region
byte-identically. #21413's
`agent-memory-store-retired-and-limits-required` entry is present at
HEAD.
- #21365 had not landed at merge time. Whichever lands later merges
`main`.
## Acceptance notes (not filed)
- **Runtime inference mints the same shape, unreached by this parse.**
`service-analytics` `inferMeasure` turns a caller-named measure with an
empty prefix (`_sum`, `_avg`, `_min`, `_max`, `_count_distinct`) into `{
type: 'sum' …, sql: '*' }` (`key.slice(0, -suffix.length) || '*'`). The
caller-measure gate admits `inferredSql === '*'`. Read from code only,
NOT MEASURED at a door, and outside this card's no-strategy-edit
surface. Carrier: the `domain:services` lane; no carrier named.
- **A `derived` dataset measure's `field` is read by nothing.** The
compiler skips it. This card now refuses `'*'` there, but any column
value still parses inert. Observed while reading the compiler; no
producer found. Carrier: none named.
- **#21000's enum retirement is untouched.** `AggregationMetricType`
`number` / `string` / `boolean` are still covered by the predicate's
"anything but count" for as long as they exist.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 535d1d2 commit b793010
13 files changed
Lines changed: 986 additions & 59 deletions
File tree
- .changeset
- content/docs/references/ui
- packages
- rest/src
- spec
- liveness
- src
- data
- migrations
- entries/semantic
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
86 | | - | |
| 86 | + | |
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
| |||
124 | 124 | | |
125 | 125 | | |
126 | 126 | | |
127 | | - | |
| 127 | + | |
128 | 128 | | |
129 | 129 | | |
130 | 130 | | |
| |||
0 commit comments