Skip to content

feat(spec)!: the analytics row wildcard '*' is admitted only where a count consumes it (#21409) - #21431

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-21409-star-count-only
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-21409-star-count-only

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 spec+service-analytics: retire the cube metric types number / string / boolean — they existed to carry a SQL expression, which #20943 retired from a member's sql #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

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.
  • spec+service-analytics: retire the cube metric types number / string / boolean — they existed to carry a SQL expression, which #20943 retired from a member's sql #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

claude added 5 commits October 2, 2026 14:06
… '*'

The door cell the narrowing moves: a dataset measure aggregating the row
wildcard under any aggregate other than count, inline and saved, on both
strategies, beside the count-over-'*' controls.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
…t consumes it

A cube measure's sql and a dataset measure's field admit '*' only under
count, by one shared predicate (rowWildcardOutsideCount) both measure
refinements call; a cube dimension's sql takes the column path without
the wildcard arm, the dataset dimension's own pattern. One ADR-0087 D3
entry, the regenerated registry region, and the liveness notes
re-pointed here. Generated artifacts and the dropped-refinement ledger
follow in the next commit.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
…sites and regenerate the dataset reference page

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
…minor, narrowing)

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
…ar-count-only

# Conflicts:
#	packages/spec/dropped-refinements.baseline.json
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 12 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/dropped-refinements.baseline.json, packages/spec/liveness/analytics_cube.json, packages/spec/liveness/dataset.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/data-api.mdx (via DimensionSchema (symbol, a top-level const), MetricSchema (symbol, a top-level const))
  • content/docs/data-modeling/analytics.mdx (via DatasetMeasureSchema (symbol, a top-level const))

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

  • content/docs/releases/v16.mdx (via queryDataset (sdk, the bare tail of client method analytics.queryDataset, bound to POST /api/v1/analytics/dataset/query))
  • content/docs/releases/v17/17-2.mdx (via MetricSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-3.mdx (via analytics.queryDataset (sdk, the route ledger binds it to POST /api/v1/analytics/dataset/query), queryDataset (sdk, the bare tail of client method analytics.queryDataset, bound to POST /api/v1/analytics/dataset/query))
  • content/docs/releases/v17/17-5.mdx (via queryDataset (sdk, the bare tail of client method analytics.queryDataset, bound to POST /api/v1/analytics/dataset/query))
  • content/docs/releases/v17/17-6.mdx (via /api/v1/analytics/dataset/query (route, a path literal in reason; a path literal in semantic))
  • content/docs/releases/v9.mdx (via queryDataset (sdk, the bare tail of client method analytics.queryDataset, bound to POST /api/v1/analytics/dataset/query))

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
  • 3 changed file(s) yielded no anchor (packages/spec/dropped-refinements.baseline.json, packages/spec/liveness/analytics_cube.json, packages/spec/liveness/dataset.json) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 138 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 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2fee7e60b19967ac3b9a8b2e1f68562aa1b7c783 — the merge of head 30437505b8b1cbcff045b29fcc3d6eaccdc9e64c into base 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2, 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 2fee7e60b19967ac3b9a8b2e1f68562aa1b7c783 && git checkout 2fee7e60b19967ac3b9a8b2e1f68562aa1b7c783
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2 30437505b8b1cbcff045b29fcc3d6eaccdc9e64c && git checkout -B drift-repro 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2 && git merge --no-ff 30437505b8b1cbcff045b29fcc3d6eaccdc9e64c

node scripts/docs-audit/affected-docs.mjs --json 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 60644d73d981410172f2ccc5ab6a81d5947faad7
Local-runs: none

Reviewed at 2026-10-02T16:07Z. Inputs: card #21409 (body, claim 5953981594, dev report 5955982656), PR #21431 (body, 13-file list, net diff origin/main... the head), the head's check-runs, origin/main for the three schema files and the control commit 434c6c7cab (#21240), service-analytics read only for the dev's out-of-scope finding, objectui at pin 89cad75d55. Read-only: nothing built, run or re-run. The diff is five commits on top of merge-base 39a912ea73; no packages/metadata-protocol path is in it (the throwaway probe is absent, as reported).

① Derived judgments

  1. Position 1, cube measure MetricSchema.sql — RIGHT. Base: .regex(ANALYTICS_COLUMN_REFERENCE) admitted '*' under every type. Head: the same regex plus a root superRefine that adds custom at ['sql'] when sql === '*' and type !== 'count'. type is required (AggregationMetricType), so the predicate's "no aggregate" arm is unreachable here. Accept-set delta: exactly '*' under a non-count type; a column, a dotted path and count over '*' parse byte-identically (pinned), and an expression was already refused at base ([Decision] analytics field gate (#20917): an authored cube member whose sql is an expression — keep the stand-down, judge its identifiers, refuse it, or retire expressions #20943).
  2. Position 2, cube dimension DimensionSchema.sql — RIGHT, and the pattern is the right form. Base regex CUBE_MEMBER_SQL is ANALYTICS_COLUMN_REFERENCE, i.e. ^(?:\*|COLUMN_PATH)$; head regex ANALYTICS_COLUMN_PATH is ^COLUMN_PATH$, both built from the one COLUMN_PATH source. The delta is exactly '*': an expression, a quoted or $-prefixed spelling, a function call, an empty string and a broken path were refused at base already (the EXPRESSIONS pin in cube-member-sql-column-reference.test.ts still holds on both members). Census of authored dimension sql values: examples/**, packages/platform-objects, skills/**, content/** author no dimension over '*' (the 180 in-repo field/sql hits spelled '*' at origin/main are count measures, QueryAST aggregations, comments or runtime literals); the one in-repo author was the spec pin the diff moves to the refusal; objectui at the pin has zero field/sql values spelled '*' in any quoting. On the card's "one cross-field rule per position, sharing one predicate; no second spelling": a dimension has no aggregate, so no cross-field rule exists for it; the card's own control (dataset dimension field, feat(spec)!: an analytics dataset dimension's and measure's field is a column reference (#21220) #21240 at 434c6c7cab) refuses '*' by this very pattern; the pattern reaches the published JSON Schema where a refinement would not and would have cost a ledger row. Position 2 has one spelling (the pattern) and the two measure positions share one predicate, so nothing is spelled twice. The refusal text names the slot, says '*' is no dimension and prescribes a count measure or the column; code invalid_format is the pattern's own.
  3. Position 3, dataset measure DatasetMeasureSchema.field — RIGHT. Base: .regex(ANALYTICS_COLUMN_REFERENCE).optional() admitted '*' under any aggregate and with none. Head: root superRefine, custom at ['field'] when field === '*' and aggregate !== 'count', the absent aggregate included (a derived measure; a non-derived measure without an aggregate was already refused by DatasetSchema's own refinement). Delta: exactly '*' outside count. The derived arm is inside the card's direction ("admitted only where a count consumes it; elsewhere refused") and the PR table states it. Controls pinned: count over '*', a count with no field, every non-count aggregate over a column, and the dataset dimension still refusing '*' with invalid_format.
  4. Refusal, code and path — RIGHT. rowWildcardOutsideCountRefusal(slot, aggregateKey, aggregate) names the slot (the measure's sql, or measures[].field) and the aggregate written (under type: 'sum', or with no aggregate), and prescribes the two ways out, count or a column (amount, account.amount). The pins assert code custom, path ['sql'] / ['field'], and the message byte-equal to the builder for every non-count enum member (derived from the enums, not restated); every door is pinned (CubeSchema, the analytics_cube write door, defineCube, DatasetSchema, the dataset write door, defineStack with 422 STACK_SCHEMA_INVALID and exactly three issues).
  5. Door cell packages/rest/src/analytics-dataset-row-wildcard-door.test.ts — RIGHT. The real route over better-sqlite3 through AnalyticsServicePlugin's own composition, once per strategy, with read counters. Refused cells (inline and saved, five non-count aggregates) assert 400 VALIDATION_FAILED, custom at measures.2.field (index 2 is the wildcard measure in the fixture), and zero reads on both counters; the count controls assert 200 with the row counts and the answering strategy's counter. The 500 → 400 base reading is the dev's measurement on the base dist (the card's own record covers sum); the saved-wildcard-selected cells' base answer is NOT MEASURED, as the dev declared. The head's Test Core shards are what say it is green (see ③.8).
  6. Published surface — RIGHT, Clause-②: no (narrowing). analytics-column-reference.ts is imported only by analytics.zod.ts and dataset.zod.ts, never re-exported by src/data/index.ts; rowWildcardOutsideCount and its refusal builder are absent from packages/spec/api-surface/*.json on the head; api-surface/, api-surface-signatures.json and json-schema.manifest/ are untouched by the diff; check:api-surface ran green on the head (Type Check · consumer gates). .superRefine() on zod 4.6.5 returns the same object schema, so the MetricSchema.shape readers (analytics-strictness-batchd.test.ts, build-schemas-check-mode.test.ts) are unaffected. No new export, key or enum member.
  7. JSON Schema leg and the ledger — RIGHT. The two measure refinements are custom checks z.toJSONSchema drops, so each site is ledgered: roots data/Metric and ui/DatasetMeasure at ""; containers data/Cube at measures.valueType (the record-value spelling the ledger already uses) and ui/Dataset at measures.element; the four installed-package API schemas at two embedded paths each. 2 + 2 + 8 = 12 sites; counts 215 → 217 (two new schema rows) and 640 → 652. build-schemas.ts refuses a ledger that does not match the tree and runs inside the spec build, which Build Core ran green on this head. The dimension half is a pattern and needs no row; the pin holds the cube dimension's published pattern equal to the dataset dimension's.
  8. ADR-0087 entry 18.analytics-row-wildcard-outside-count-refused.ts — RIGHT. surface names the three positions; replacement prescribes a count, a column, or deleting field on a derived measure; acceptanceCriteria states the three positions with their codes, the count exemption (count over '*', a dataset count with no field), every door, and the measured 500 becoming a figure. No D2: there is no lossless rewrite. No RETIRED_KEYS_BY_MAJOR row: no key leaves any shape, and a value narrowing is not a key retirement. The registry.ts region is regenerated (check:migration-registry, green in Lint & Repo Gates).
  9. Liveness rows — RIGHT. analytics_cube measures.sql and dimensions.sql no longer point the count-only boundary at spec+service-analytics: retire the cube metric types number / string / boolean — they existed to carry a SQL expression, which #20943 retired from a member's sql #21000; each states the analytics: '*' runs only under count, but a cube measure's or dimension's sql and a dataset measure's field admit it under any aggregate — a summed '*' answers 500 at the dataset door (split from #21000) #21409 narrowing, its form (predicate / pattern) and the D3 id; dataset measures.field states the narrowing and its ledger row. All three stay live, correctly: the keys are still read at the cited symbols (dataset-compiler.ts sql: m.field ?? '*', both strategies). Spec property liveness is green on the head.

② Semver level

  • @objectstack/spec minor, BREAKING banner, Clause-②: no (narrowing) on the PR body and in the changeset, exactly one marker (registered analytics-row-wildcard-outside-count-refused): the right level for a published accept-set narrowing with no new export under the launch-window convention (AGENTS.md Post-Task step 3). Check Changeset (ADR-0087 registration, no-major) is green on the head.
  • Sentence by sentence the changeset states what ships: the three bullets match ①.1–3; "each refusal names the slot and the aggregate ... prescribes a count or a column" matches the builder; the byte-identical controls are pinned; the stored-document paragraph discloses the fail-closed answer on a saved dataset's other measures, which the door test pins; the kit bullets (non-public predicate, D3 without D2 or retired-key row, 12 ledger sites, liveness, regenerated ui/dataset page, runtime untouched) match the diff; the reach paragraph matches my census (in-repo and objectui at the pin). The five-aggregate 500 reading is the dev's base measurement, stated as such.
  • One sentence is not literally true, for the seat's ACCEPT (prose face, outside this record's verdict): the FROM → TO block attributes the outcomes parsed (FROM) and ZodError at measures.0.field (custom) (TO) to a defineDataset({ ... }) call. defineDataset is an identity function (return dataset;) on base and head alike; that call parses nothing and throws nothing. The ZodError surfaces where the dataset is parsed: DatasetSchema.parse, defineStack({ datasets }) (422 STACK_SCHEMA_INVALID), the dataset write door, and the query route (400). The cube half (defineCube) does parse, so that line is right. The shape mapping and the one-line fix are correct. Since the changeset ships as CHANGELOG and cannot be amended after release, I ask the seat to have the dataset FROM → TO spelled against DatasetSchema.parse or defineStack before landing.
  • content/docs/references/ui/dataset.mdx: the two regenerated rows mirror the new .describe() text ("*" for a count); check:docs is green on the head (Type Check · source gates).

③ Boundary flags

open_questions is empty. Each deviation in report 5955982656:

  1. Dimension spelled as a pattern — answered in ①.2: the right form, the control's own, flagged not silently chosen; no accept-set change beyond '*'.
  2. "Zone 2 item 2 partly falsified: the control is a pattern" — confirmed at 434c6c7cab: DatasetDimensionSchema.field is .regex(ANALYTICS_COLUMN_PATH). The dev read the control correctly.
  3. No STEP18_RATIONALE fragment — REPORTING, and a follow-up for the seat. The fragment list is hand-written text outside the generated markers, no gate requires one, and adding it would have exceeded the claimed generated-region surface, so stopping and flagging was right. But both sibling narrowings of these slots (cube-member-sql-expression-retired, dataset-member-field-expression-refused) carry a fragment, and build-upgrade-guide.ts renders the joined step-18 rationale into the upgrade guide, so that page's narrative currently omits this narrowing while its table row appears. A one-fragment docs follow-up, not a contract gap.
  4. verifiedAt bumped to 2026-10-02 on the three rows — accepted. The evidence anchors are unchanged symbol anchors; the dev declares a re-read; the README allows the stamp only with a reading; the gate accepts the date. REPORTING.
  5. Merge commit 60644d73d9 without the trailer pair — REPORTING, no contract effect: the four authored commits carry the pair, the repository is squash-only (allow_squash_merge alone, message from the PR body), so the merge message never reaches main.
  6. dropped-refinements.baseline.json conflict resolved from main's blob plus re-declared sites — RIGHT: the net diff against main is exactly the 12 declared sites and the two recomputed counts, main's rows (feat(spec)!: an agent's memory contract states exactly what the runtime honours — maxEntries and reflectionInterval are required once long-term memory is enabled, longTerm.store is retired, and the block is live #21413's included) retained; the build validates the ledger (①.7).
  7. Throwaway probe in packages/metadata-protocol — absent from the 13-file list and from git diff --stat origin/main...head. Confirmed.
  8. NOT MEASURED items, named to the check-run that measures each on this head: pnpm check:dual-build-cjs-loads → ci.yml job build-core, step "Every published require entry point actually loads" → Build Core, success. pnpm check:type-check-debt → lint.yml job typecheck-debt → Type Check · debt ledger, success. The whole spec repo vitest project → turbo run test test:repo inside the Test Core (1/6) … (6/6) shards → all six success, and the Test Core rollup success (concluded 2026-10-02T16:06Z). At my final read every one of the head's 35 check-runs had concluded: 33 success, 2 skipped by design (Console Pin Gate, path-filtered; Packed-tarball smoke (opt-in)), none failed, none still running. Also measured on the head: check:api-surface → Type Check · consumer gates, success; check:docs, check:spec-changes, check:upgrade-guide → Type Check · source gates, success; check:migration-registry, pnpm lint → Lint & Repo Gates, success; the ledger → Build Core, success.
  9. Out-of-scope finding A — service-analytics inferMeasure (escalated to the seat, nothing filed). Read at origin/main: inferMeasure('_sum') takes key.slice(0, -4) || '*' and mints { type: 'sum', sql: '*' }; assertMeasureFields returns no source for sql === '*'; assertCallerMembersResolvable admits inferredSql === '*' explicitly; NativeSQLStrategy#resolveMeasureSql emits the operand verbatim (measure.sql === '*' ? '*' : qualify...), so a caller measures: ['_sum'] (likewise _avg, _min, _max, _count_distinct) reaches the database as SUM(*) at POST /api/v1/analytics/query. Neither mint site parses the minted cube through CubeSchema, so this PR's spec refusal does not reach it. Same consequence (a server fault at a public door for a wildcard outside count), different class: a runtime-minted shape from a caller string, not an authored metadata shape the contract admits; the fix sits in the mint (the || '*' default belongs to the count key alone, or an empty prefix is refused as the dotted case is). NOT MEASURED at a door here. Carrier: domain:services.
  10. Out-of-scope finding B — a derived dataset measure's field is read by nothing. dataset-compiler.ts pushes a derived measure and continues before m.field is read; DatasetSchema's refinement refuses neither field nor aggregate beside derived, and the schema's own doc says aggregate is "ignored at compile time". That is the ADR-0049 declared-but-ignored shape on a key combination, an authoring trap per Prime Directive chore: version packages #10, so a finding rather than an observation: a candidate spec card to refuse field and aggregate beside derived (a cross-field narrowing with its own kit), low priority, no producer found. Escalated to the seat for the filing decision; nothing filed.

Implemented-by: claude/issue-21409-star-count-only
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

…that parse

defineDataset is an identity function and parses nothing; the dataset
half of the FROM/TO block now names DatasetSchema.parse, defineStack
(422) and the dataset query route (400), as measured.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI red on b79d1be80d, not this PR's · domain:spec seat 1 (session_01UtnxvdiN376GF3sgXwAw4d) · 2026-10-02T16:31Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b79d1be80d2a37297f27f7c84716ca4b7001fcfc
Local-runs: none

Reviewed at 2026-10-02T16:39Z. A narrow re-review of the one-commit delta over the PASS record 5956317829 (head 60644d73d9). Inputs: card #21409 (body, claim 5953981594, dev reports 5955982656 and 5956509330), PR #21431 (body, 13-file list, commit list, the net diff origin/main... the head and the delta 60644d73d9..b79d1be80d), the head's check-runs, and the code the corrected changeset block attributes its answers to, read at the head and at the merge-base 39a912ea73 (packages/spec/src/ui/dataset.zod.ts, packages/spec/src/data/analytics-column-reference.ts, packages/spec/src/stack.zod.ts, packages/spec/src/api/error-code-ledger.zod.ts, packages/rest/src/rest-server.ts, packages/cli/src/commands/migrate/meta.ts, packages/spec/src/migrations/chain.ts and registry.ts), plus the two tests on the head. Read-only: nothing built, run or re-run.

① Derived judgments

  1. The delta is the changeset alone — RIGHT, and every ① judgment of record 5956317829 stands. git diff --stat 60644d73d9..b79d1be80d is one file, .changeset/21409-analytics-row-wildcard-count-only.md, +8 / -4, all inside the FROM → TO fence; the commit's own stat says the same. The net diff origin/main...b79d1be80d (merge-base 39a912ea73, the second parent of the branch's main merge) is the same 13 files the PR lists and the previous record judged: no schema, test, ledger, liveness, registry, docs or rest path moved, and no packages/runtime or packages/metadata-protocol path is in it. No other path moved, so nothing here is named FAIL. The accept-set delta is therefore unchanged: positions 1 and 3 refuse '*' outside count through the one predicate (custom at sql / field), position 2 takes ANALYTICS_COLUMN_PATH (invalid_format), the published surface gains no export, key or enum member, and Clause-②: no (narrowing) is still the arm.

② Semver level

  • Level and declaration — carried, unchanged by the delta. @objectstack/spec minor, the BREAKING banner, Clause-②: no (narrowing) (changeset and PR body alike), exactly one ADR-0087 marker (registered analytics-row-wildcard-outside-count-refused): the delta edits nothing outside the FROM → TO fence, so the previous record's reading of the level holds. Check Changeset is green on this head.
  • The corrected dataset FROM → TO block, sentence by sentence against the code:
    1. "DatasetSchema.parse accepted it" (FROM, at base) — TRUE. At 39a912ea73 DatasetMeasureSchema.field is .regex(ANALYTICS_COLUMN_REFERENCE), whose pattern is ^(?:\*|COLUMN_PATH)$, so '*' passes under aggregate: 'sum'; DatasetSchema's own refinement refuses only a non-derived measure with no aggregate or a non-count measure with no field, neither of which the literal is; DatasetDimensionSchema.type is optional, so { name: 'stage', field: 'stage' } parses; name, label, object, dimensions, measures are the literal's only keys and all are declared on the strict object. The literal parses at base with no issue.
    2. "a dataset query selecting deals answered 500 DATABASE_ERROR" (FROM, at base) — TRUE as a measurement the record carries, not re-run here. The first dev report's base run of the door test (spec dist at ceb4a939b4) read {"error":"Internal server error","code":"DATABASE_ERROR"}: expected 500 to be 400 for the inline sum cell on both strategies, and the card body records the same cell from spec(dataset): a dataset dimension/measure field admits a SQL expression at author time; narrow it to a column reference, as #20943 did for a cube member's sql #21220's probe (5940326148 on spec+service-analytics: retire the cube metric types number / string / boolean — they existed to carry a SQL expression, which #20943 retired from a member's sql #21000). The literal is that cell's shape: an inline dataset, sum over '*', the wildcard measure selected; the base compiler lowers the measure's field verbatim (packages/services/service-analytics/src/dataset-compiler.ts, sql: m.field ?? '*'), so SUM(*) reached the driver.
    3. "DatasetSchema.parse throws a ZodError at measures.0.field (custom)" (TO) — TRUE. On the head DatasetMeasureSchema ends in a .superRefine that asks rowWildcardOutsideCount(measure.field, measure.aggregate) and adds { code: 'custom', path: ['field'], message: rowWildcardOutsideCountRefusal('measures[].field', 'aggregate', measure.aggregate) }; nested in DatasetSchema.measures, the path is ['measures', 0, 'field'], and .parse throws the ZodError. The quoted opening of the message (measures[].field is the row wildcard '*' under aggregate: 'sum') is the builder's first sentence verbatim. The spec pin asserts exactly one issue, ['custom', ['measures', 1, 'field']], for its two-measure fixture through DatasetSchema and the dataset write door.
    4. "defineStack({ datasets }) refuses it at datasets.N.measures.0.field (422 STACK_SCHEMA_INVALID)" (TO) — TRUE. ObjectStackDefinitionSchema.datasets is z.array(DatasetSchema).optional(); buildDefinedStack runs ObjectStackDefinitionSchema.safeParse(normalized, ...) and on failure throws StackSchemaInvalidError(formatZodError(...), result.error.issues), whose code is 'STACK_SCHEMA_INVALID' and whose status is the base class's 422; the ADR-0112 ledger row says the same. The spec pin asserts code, status and the path ['datasets', 0, 'measures', 1, 'field'] among exactly three issues, so datasets.N.measures.0.field is the right spelling for a dataset at index N whose wildcard measure sits at index 0.
    5. "POST /api/v1/analytics/dataset/query answers 400 VALIDATION_FAILED for an inline or a saved copy" (TO) — TRUE. In rest-server.ts the route takes body.dataset or, failing that, loads the saved body.datasetName item through getMetaItems and translateMetaItem; both branches converge on stripReadDecorations and then DatasetSchema.parse(dataset) inside a try whose catch answers res.status(400).json({ code: 'VALIDATION_FAILED', message: 'Invalid dataset definition.', detail }) before svc.queryDataset is reached. The door test pins both branches on both strategies (five aggregates inline, five saved selecting the wildcard, five saved selecting only the count), with custom at measures.2.field in detail and zero reads on both counters. Its base answer for the saved-wildcard-selected cells stays NOT MEASURED, as declared before.
  • The rest of the changeset, re-read for attributions to a call that does not parse: defineCube({...}) in the cube half parses (return CubeSchema.parse(config)), so its FROM → TO line is right; "a re-save through the metadata write door is refused at the slot" names the dataset binding, which is DatasetSchema (pinned); "the stack definition, whose parse refuses it" names defineStack, which parses analyticsCubes through CubeSchema; no defineDataset attribution remains anywhere in the changeset.
  • One sentence is not literally true, for the seat's ACCEPT (prose face, outside this record's verdict): the one-line-fix paragraph ends "There is no mechanical rewrite, so os migrate meta lists nothing for it." The first half is right (no D2 conversion; the entry's conversionIds is empty). The second is not: os migrate meta's default chain runs to CHAIN_TERMINUS_MAJOR = Math.max(PROTOCOL_MAJOR, ...MIGRATION_MAJORS), which is 18 on this head (PROTOCOL_VERSION is 17.0.0; step 18 is registered); applyMetaMigrations maps every step.semantic entry of every hop crossed into a MigrationTodo; and printMigrationReport prints each one under "manual change(s) require your judgment" with its surface → replacement, why and verify, by design never dropped or filtered ("ADR-0087 D3 is never silence"). So on --from 17 the tool lists this entry; what it does not do is rewrite anything for it. Same class as the previous record's defineDataset finding: the mapping and the one-line fix are right, the tool attribution beside them is wrong, and the text ships as CHANGELOG. I ask the seat to have it spelled "rewrites nothing for it and lists the entry as a manual notice", or dropped, before landing.

③ Boundary flags

open_questions is empty in both reports. The round-2 deviations and findings in 5956509330:

  1. The probe sat in packages/runtime (src/zz-probe-21409-inferred-wildcard.test.ts), beside the runtime door test — absent, confirmed. It is in none of the PR's 13 files; git log --name-only 39a912ea73..b79d1be80d over every commit reachable from the head (the six branch commits and the main side of the merge) names no packages/runtime path and no zz-probe or metadata-protocol path; git ls-tree -r b79d1be80d carries no such file. The placement itself was right for what it measured: POST /api/v1/analytics/query is the runtime dispatcher's route, which @objectstack/rest does not mount. REPORTING, no contract effect.
  2. The changeset commit (b79d1be80d, authored 2026-10-02T16:14:21Z) preceded the probe, which was never committed. The order cannot be read from the remote — an uncommitted file leaves no trace — and the dev states it plainly with a clean-status proof after the deletion. Its consequence is what matters here: the probe measured finding A (a caller-named _sum / _avg minted by inferMeasure at the cube query door), which is outside this card's surface and outside the changeset's subject; the changeset claims the authoring parse and the dataset door and says "Runtime. Unchanged.", and nothing in it claims the runtime refuses a minted SUM(*). No sentence was owed to the probe's reading, so the order moved no contract face. REPORTING.
  3. Out-of-scope finding A (class a, now measured: 500 DATABASE_ERROR at POST /api/v1/analytics/query for _sum / _avg on both strategies, on an ad-hoc and on an authored cube, with the named-column controls at 200) and finding B (a derived dataset measure's field and aggregate are read by nothing) — the seat files them; no judgment is owed in this record.
  4. The round-1 deviations (5955982656) were answered in record 5956317829 ③.1–10; the delta touches none of their surfaces, so those answers stand.
  5. Check-runs on this head — not all green: one shard is red, and that bars landing until it is green, whatever this record's verdict says. At my final read every one of the head's 35 check-runs had concluded, none still running: 31 success, 2 skipped by design (Console Pin Gate, path-filtered; Packed-tarball smoke (opt-in)), and 2 failure — Test Core (4/6) and, on it, the Test Core rollup (concluded 2026-10-02T16:38:07Z). The failure is Test Core (4/6) (job 110922939161, step "Run this shard's tests", concluded 2026-10-02T16:30:29Z): turbo reports Failed: @objectstack/rest#test alone; the @objectstack/rest local vitest project read Test Files 1 failed | 255 passed (256), Tests 1 failed | 4847 passed | 322 skipped (5170), and the one red is src/import-template-route.test.ts, suite "the * agrees with the engine: starred exactly when the import door refuses a blank", case "for every shape of default the engine reads", with Error: Test timed out in 5000ms. Against the rule's exceptions: it is not a merge-base-same-signature red — main's own Test Core (4/6) is green at 3a6d92f78b (concluded 2026-10-02T16:31:35Z) and on the earlier main run, and the previous head 60644d73d9 had all six shards green — and it is not a red by design. What the evidence does say: the file is byte-identical between origin/main and the head, the PR's only packages/rest change is the new door test, and the delta under review is changeset-only, so the code tree this shard ran is the tree that passed the same shard on 60644d73d9; the red is one load-sensitive case (it boots a stack, registers one object per default shape, syncs schemas, then builds a workbook and imports a row for each shape, under vitest's default 5 s timeout) timing out, and the * in its title is the required-field mark on an import template, not the row wildcard this PR narrows. The dev's local rest --project local run at 934b70a2db reported the same population green (4848 passed, 322 skipped). A re-run is the seat's act, not this record's (read-only here); until that shard and the Test Core rollup are green on this head, the second landing condition is unmet. The other five shards (1/6, 2/6, 3/6, 5/6, 6/6) and the Dogfood Regression Gate rollup are success. The NOT MEASURED items the previous record mapped to check-runs read the same on this head: Build Core success (check:dual-build-cjs-loads), Type Check · debt ledger success (check:type-check-debt), Type Check · consumer gates success (check:api-surface), Type Check · source gates success (check:docs, check:spec-changes, check:upgrade-guide), Lint & Repo Gates success (check:migration-registry, pnpm lint), Spec property liveness success, Check Changeset success, Governed Surface Queue Guard success; the whole spec repo vitest project runs inside the six Test Core shards, whose only red is the rest timeout above.

Implemented-by: claude/issue-21409-star-count-only
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 30437505b8b1cbcff045b29fcc3d6eaccdc9e64c
Local-runs: none

Reviewed at 2026-10-02T17:18Z. A narrow re-review of the two-commit delta over the PASS record 5956873511 (head b79d1be80d), itself over 5956317829 (head 60644d73d9). Inputs: card #21409 (body, claim 5953981594, dev reports 5955982656, 5956509330 and 5957123353), PR #21431 (body, 13-file list, commit list, the net diff origin/main... the head, the delta b79d1be80d..fce0c9a042, and the merge 30437505b8 read against both of its parents and against main's own movement 39a912ea73..3a6d92f78b), the head's check-runs, and for ② the same code as last round (packages/spec/src/migrations/chain.ts and registry.ts, packages/cli/src/commands/migrate/meta.ts, the D3 entry). Read-only: nothing built, run or re-run.

① Derived judgments

  1. b79d1be80d..fce0c9a042 is the one changeset hunk and nothing else — RIGHT. git diff --stat is one file, .changeset/21409-analytics-row-wildcard-count-only.md, +3 / -1: the last sentence of the one-line-fix paragraph, respelled. No other path.
  2. The merge 30437505b8 is pure — RIGHT. Parents fce0c9a042 and 3a6d92f78b (origin/main at the time; the new merge-base). (a) The net diff origin/main...30437505b8 is the same 13 files both earlier records judged, 986 insertions / 59 deletions, and diffing it against the previous head's net diff leaves exactly the respelled hunk (the changeset's line count 119 → 121); no path was added or lost. (b) The first-parent diff fce0c9a042..30437505b8 names the same 45 files as main's movement 39a912ea73..3a6d92f78b, 2803 insertions / 245 deletions on both sides, byte-identical per file for 44 of them; the 45th, packages/spec/src/migrations/registry.ts, differs only in hunk context, and its added and removed lines are exactly main's 90 added and removed lines, with nothing of this branch's moves among them. (c) The second-parent diff 3a6d92f78b..30437505b8 equals the branch's own net diff, and on registry.ts its added lines are exactly the branch's own entry region, the same lines 39a912ea73..fce0c9a042 adds. (d) The generated region holds both entries once each: the generator inlines each entry object, and analytics-row-wildcard-outside-count-refused appears once at the head (its inlined entry's id), 0 times on main and once on fce0c9a042; main's dashboard-widget-single-series-multi-measure-refused appears at the head exactly as it does on main (3 textual hits for one entry: its own inlined id, plus two mentions of it inside a sibling entry's prose) and 0 times on the branch before the merge; feat(spec)!: an agent's memory contract states exactly what the runtime honours — maxEntries and reflectionInterval are required once long-term memory is enabled, longTerm.store is retired, and the block is live #21413's agent-memory-store-retired-and-limits-required is unchanged (3 / 3 / 3). entries/semantic holds 348 files at the head, 347 on each parent — main's set plus this branch's one. The head's registry.ts blob is a49da25ca28fe75334d02ee7243cd82f6e8b31b4, the blob the dev reports gen:migration-registry reproduced byte-identically; check:migration-registry on this head is measured by Lint & Repo Gates (③.3). No other path moved, so nothing here is named FAIL, and every ① judgment of the two earlier records stands.

② Semver level

  • Level and declaration — carried. @objectstack/spec minor, the BREAKING banner, Clause-②: no (narrowing) (changeset and PR body alike), exactly one ADR-0087 marker (registered analytics-row-wildcard-outside-count-refused): the delta changes one prose sentence and merges main, and neither moves the level. Check Changeset is green on this head.
  • The respelled sentence — TRUE against the code. "There is no mechanical rewrite: os migrate meta rewrites nothing for it, and lists the entry analytics-row-wildcard-outside-count-refused as a manual change that requires your judgment." The entry's conversionIds is empty, so applyMetaMigrations applies no edit for it: nothing is rewritten. meta.ts runs the chain to CHAIN_TERMINUS_MAJOR = Math.max(PROTOCOL_MAJOR, ...MIGRATION_MAJORS), 18 on this head; composeMigrationChain(17, 18) keeps every registered major above 17 and at most 18, so step 18 is crossed; chain.ts maps every step.semantic entry of each crossed step into a MigrationTodo; and printMigrationReport prints the header N manual change(s) require your judgment: followed by one [protocol 18] surface → replacement line per entry with its why and verify, by design never dropped, filtered or summarised. The sentence's words track that header. One nuance, not an untruth: the printed line carries the entry's surface → replacement headline (the three slots — a cube measure's sql, a cube dimension's sql and a dataset measure's field — authored as the row wildcard where no count consumes it, → what the member meant: a count, a column, or no field on a derived measure), not the id string; the changeset names the entry by the id the ADR-0087 marker and the registry use, which is how a reader of the CHANGELOG finds it.
  • The whole changeset, re-read one last time for a tool or door attribution that is not literally true: none remains. Each attribution names a call that does what the sentence says: DatasetSchema.parse (parses; refuses at measures.0.field), defineStack (ObjectStackDefinitionSchema.safeParse; 422 STACK_SCHEMA_INVALID), POST /api/v1/analytics/dataset/query (parses inline and saved copies; 400 VALIDATION_FAILED), defineCube (CubeSchema.parse), the dataset metadata write door (DatasetSchema), the stack definition's parse for authored cubes, the read-path _diagnostics (the dev's measured computeMetadataDiagnostics reading, carried), and now os migrate meta. The measurement sentences (the five-aggregate 500 at the dataset door; the two censuses) are stated as measurements and carry from the earlier records.

③ Boundary flags

open_questions and out_of_scope_findings are both empty in 5957123353.

  1. The merge commit 30437505b8 carries git's default message without the trailer pair — REPORTING, no contract effect, as for 60644d73d9 before it. The repository allows squash merges only (allow_squash_merge true; merge commits and rebase merges off) with the PR body as the squash message, so no merge-commit message reaches main; fce0c9a042 and the four authored commits carry the pair. The dev's reason for not amending (not re-entering the pre-commit regen deferral on a merge commit) is a process note for the seat, not a contract matter.
  2. Earlier rounds' flags (5955982656 deviations 1–8; 5956509330 deviations 1–2 and findings A / B) were answered in records 5956317829 and 5956873511; this delta touches none of their surfaces, so those answers stand. Finding A (inferMeasure minting SUM(*) at POST /api/v1/analytics/query) and finding B (a derived dataset measure's inert field / aggregate) remain the seat's to file.
  3. Check-runs on this head — all green. At my final read every one of the head's 35 check-runs had concluded, none still running: 33 success, 2 skipped by design (Console Pin Gate, path-filtered; Packed-tarball smoke (opt-in)), 0 failure. Each conclusion: success — Auto Label, Build Core, Build Docs, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate and its shards 1/3, 2/3, 3/3, Dogfood Verify CLI, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, Temporal Conformance (live PG + MySQL), Test Core and its shards 1/6 through 6/6, The card this PR closes must claim this branch, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter; skipped — the two named above. In particular Test Core (4/6) is success (concluded 2026-10-02T17:07:25Z) and the Test Core rollup is success (2026-10-02T17:16:35Z): the Flaky on Test Core: packages/rest import-template-route.test.ts › "the * agrees with the engine … for every shape of default the engine reads" times out at 5000ms on PRs that touch no packages/rest file #21428 timeout in import-template-route.test.ts that reddened both on b79d1be80d did not recur, and the @objectstack/rest population — the new door test included, now over the merged tree that carries main's native-SQL strategy changes the dev declared not re-run locally — ran green in that shard. The NOT MEASURED mappings of the earlier records hold on this head: Build Core (check:dual-build-cjs-loads), Type Check · debt ledger (check:type-check-debt), Type Check · consumer gates (check:api-surface), Type Check · source gates (check:docs, check:spec-changes, check:upgrade-guide), Lint & Repo Gates (check:migration-registry on the merged registry, pnpm lint), Spec property liveness, Check Changeset, Governed Surface Queue Guard, all success; the whole spec repo vitest project ran inside the six green Test Core shards. Both landing conditions are met on this head: this PASS record, and every check green.

Implemented-by: claude/issue-21409-star-count-only
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 17:20
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 17:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit b793010 Oct 2, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21409-star-count-only branch October 2, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants