From 99f0e9d2961d7eaefed016cc83da2aad59c48b33 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 21:29:41 +0000 Subject: [PATCH 01/15] test(service-analytics): pin analytics_cube.public visibility at getMeta and every query door Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude --- .../__tests__/cube-public-visibility.test.ts | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 packages/services/service-analytics/src/__tests__/cube-public-visibility.test.ts diff --git a/packages/services/service-analytics/src/__tests__/cube-public-visibility.test.ts b/packages/services/service-analytics/src/__tests__/cube-public-visibility.test.ts new file mode 100644 index 00000000000..a6da4f6d9c2 --- /dev/null +++ b/packages/services/service-analytics/src/__tests__/cube-public-visibility.test.ts @@ -0,0 +1,200 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * `analytics_cube.public` — the analytics API honours an authored cube's + * visibility (the Cube.dev semantics this schema follows: a cube declared + * `public: false` is neither discovered nor queryable through the API). + * + * Until this change the key was parsed, stored and read by NOTHING: `getMeta` + * listed a `public: false` cube and every query door answered it, so an author + * who wrote the flag got a cube exactly as exposed as one that did not — an + * access-shaped key that gated nothing. The default was `false` too, which is + * why it could never simply be switched on: enforcing it as declared would have + * hidden every authored cube. The default is now the mainstream "visible", and + * an explicit `false` hides. + * + * What this file pins, one door at a time: + * + * - discovery: `getMeta()` omits a hidden cube, and `getMeta(name)` answers a + * hidden name with nothing — the same answer as a name no cube has; + * - the query doors: `query()` (`POST /analytics/query`) and `generateSql()` + * (`POST /analytics/sql`) refuse a hidden cube with the declared + * `CUBE_NOT_FOUND` / 404 envelope — never an empty result — before a strategy + * runs and before the registry is touched; + * - the controls: a cube that declares `public: true`, and one that omits the + * key (input shape, and parsed through `CubeSchema` the way `defineCube` does), + * are listed and answered; + * - the three INTERNAL producers (`inferCubeFromQuery`, `compileDataset`, + * `CubeRegistry.inferFromObject`) mint visible cubes, so the ad-hoc KPI path + * and the dataset door — which reaches `query()` through `DatasetExecutor` — + * keep answering after the flag became enforced. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { CubeSchema, type Cube } from '@objectstack/spec/data'; +import { DatasetSchema } from '@objectstack/spec/ui'; +import { AnalyticsService } from '../analytics-service.js'; +import { CubeRegistry } from '../cube-registry.js'; +import { compileDataset } from '../dataset-compiler.js'; + +const silentLogger = { + info: vi.fn(), + debug: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + child: vi.fn().mockReturnThis(), +} as any; + +const measures = { count: { name: 'count', label: 'Count', type: 'count' as const, sql: '*' } }; +const dimensions = { status: { name: 'status', label: 'Status', type: 'string' as const, sql: 'status' } }; + +const hiddenCube: Cube = { name: 'hidden_cube', sql: 'hidden_table', measures, dimensions, public: false }; +const visibleCube: Cube = { name: 'visible_cube', sql: 'visible_table', measures, dimensions, public: true }; +/** Input shape with the key OMITTED — `register` never parses, so this is what an unparsed caller hands in. */ +const omittedCube: Cube = { name: 'omitted_cube', sql: 'omitted_table', measures, dimensions }; +/** Parsed the way `defineCube()` and `defineStack({ analyticsCubes })` parse an authored cube. */ +const parsedCube = CubeSchema.parse({ name: 'parsed_cube', sql: 'parsed_table', measures, dimensions }); + +/** Records every object an aggregate ran against, so a refusal can prove no strategy ran. */ +function makeService(cubes: Cube[] = [hiddenCube, visibleCube, omittedCube, parsedCube]) { + const aggregated: string[] = []; + const service = new AnalyticsService({ + logger: silentLogger, + cubes, + queryCapabilities: () => ({ nativeSql: false, objectqlAggregate: true, inMemory: false }), + executeAggregate: async (objectName: string) => { + aggregated.push(objectName); + return [{ count: 1 }]; + }, + isRegisteredObject: (n: string) => ['crm_account', 'opportunity'].includes(n), + }); + return { service, aggregated }; +} + +const HIDDEN_REFUSAL = { code: 'CUBE_NOT_FOUND', status: 404, cube: 'hidden_cube' }; + +describe('analytics_cube.public — discovery (`getMeta`)', () => { + it('omits a cube declared `public: false` and lists every visible one', async () => { + const { service } = makeService(); + + const names = (await service.getMeta()).map((c) => c.name); + + expect(names).not.toContain('hidden_cube'); + expect(names).toEqual(['visible_cube', 'omitted_cube', 'parsed_cube']); + }); + + it('answers a hidden name with nothing — the same answer as a name no cube has', async () => { + const { service } = makeService(); + + expect(await service.getMeta('hidden_cube')).toEqual([]); + expect(await service.getMeta('no_such_cube')).toEqual([]); + expect((await service.getMeta('visible_cube')).map((c) => c.name)).toEqual(['visible_cube']); + }); +}); + +describe('analytics_cube.public — every query door refuses a hidden cube', () => { + it('`query()` refuses with the declared CUBE_NOT_FOUND/404 envelope, and no strategy runs', async () => { + const { service, aggregated } = makeService(); + + await expect( + service.query({ cube: 'hidden_cube', measures: ['hidden_cube.count'] }), + ).rejects.toMatchObject(HIDDEN_REFUSAL); + + expect(aggregated).toEqual([]); + }); + + it('`generateSql()` refuses the same way — the dry-run door shows no statement for a hidden cube', async () => { + const { service, aggregated } = makeService(); + + await expect( + service.generateSql({ cube: 'hidden_cube', measures: ['hidden_cube.count'] }), + ).rejects.toMatchObject(HIDDEN_REFUSAL); + + expect(aggregated).toEqual([]); + }); + + it('refuses before the registry is touched — a suffix-inferred measure does not augment the hidden cube', async () => { + const { service } = makeService(); + const before = service.cubeRegistry.get('hidden_cube'); + + await expect( + service.query({ cube: 'hidden_cube', measures: ['amount_sum'] }), + ).rejects.toMatchObject(HIDDEN_REFUSAL); + + expect(service.cubeRegistry.get('hidden_cube')).toBe(before); + }); + + it('a refusal is not an empty result: the promise rejects rather than resolving with no rows', async () => { + const { service } = makeService(); + + const outcome = await service.query({ cube: 'hidden_cube', measures: ['hidden_cube.count'] }).then( + (result) => ({ resolved: result }), + (error: unknown) => ({ rejected: error }), + ); + + expect(outcome).not.toHaveProperty('resolved'); + expect(outcome).toHaveProperty('rejected'); + }); +}); + +describe('analytics_cube.public — the controls stay open', () => { + it.each(['visible_cube', 'omitted_cube', 'parsed_cube'])('`%s` is answered by `query()`', async (name) => { + const { service, aggregated } = makeService(); + + await service.query({ cube: name, measures: [`${name}.count`] }); + + expect(aggregated).toEqual([name.replace('_cube', '_table')]); + }); + + it('an authored cube that omits `public` parses to visible (the spec default feeds the runtime)', () => { + expect(parsedCube.public).toBe(true); + }); +}); + +describe('analytics_cube.public — the internal producers mint visible cubes', () => { + it('the ad-hoc KPI path: an inferred cube is answered again on the next request, and listed', async () => { + const { service, aggregated } = makeService([]); + + await service.query({ cube: 'crm_account', measures: ['count'] }); + // The first request REGISTERED the inferred cube; the second resolves it + // from the registry, which is where a hidden verdict would now refuse it. + await service.query({ cube: 'crm_account', measures: ['count'] }); + + expect(aggregated).toEqual(['crm_account', 'crm_account']); + expect(service.cubeRegistry.get('crm_account')?.public).toBe(true); + expect((await service.getMeta()).map((c) => c.name)).toEqual(['crm_account']); + }); + + it('the dataset door: `queryDataset` reaches `query()` through DatasetExecutor with its compiled cube', async () => { + const service = new AnalyticsService({ + logger: silentLogger, + queryCapabilities: () => ({ nativeSql: true, objectqlAggregate: false, inMemory: false }), + executeRawSql: async () => [{ stage: 'won', total: 3 }], + }); + const dataset = DatasetSchema.parse({ + name: 'pipeline', + label: 'Pipeline', + object: 'opportunity', + dimensions: [{ name: 'stage', field: 'stage', type: 'string' }], + measures: [{ name: 'total', aggregate: 'count' }], + }); + + const result = await service.queryDataset(dataset, { dimensions: ['stage'], measures: ['total'] }); + + expect(result.rows).toEqual([{ stage: 'won', total: 3 }]); + expect(service.cubeRegistry.get('pipeline')?.public).toBe(true); + }); + + it('`compileDataset` and `CubeRegistry.inferFromObject` write the visible default', () => { + const compiled = compileDataset(DatasetSchema.parse({ + name: 'pipeline', + label: 'Pipeline', + object: 'opportunity', + measures: [{ name: 'total', aggregate: 'count' }], + })); + const inferred = new CubeRegistry().inferFromObject('tasks', [{ name: 'title', type: 'text', label: 'Title' }]); + + expect(compiled.cube.public).toBe(true); + expect(inferred.public).toBe(true); + }); +}); From bdfdee23136a5c5e27a0748f2be3cb2dfb3bb305 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 21:49:02 +0000 Subject: [PATCH 02/15] feat(analytics): enforce analytics_cube.public and default it to visible CubeSchema.public defaults to true (it was false while nothing read it). service-analytics reads it: getMeta omits a cube declared public: false, and query() / generateSql() refuse it with CUBE_NOT_FOUND / 404 before any strategy runs. The three internal mints (inferCubeFromQuery, compileDataset, CubeRegistry.inferFromObject) write the visible default, so the ad-hoc KPI path and the dataset door keep answering. Test fixtures that restated the old default are re-spelled public: true; the showcase cube, which is the app's /analytics/* demonstration, drops its public: false. The liveness row for public flips to live. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude --- .../20282-analytics-cube-public-enforced.md | 24 ++++++++ .../src/data/analytics/showcase.cube.ts | 7 ++- examples/app-showcase/test/gap-fill.test.ts | 9 +++ ...oss-field-refusal-operand-withhold.test.ts | 2 +- ...gregate-bridge-function-vocabulary.test.ts | 2 +- .../src/__tests__/analytics-service.test.ts | 2 +- .../cross-field-engine-fallback.test.ts | 2 +- .../cross-field-offset-dataset.test.ts | 2 +- .../cross-field-reference-refusal.test.ts | 2 +- .../src/__tests__/cube-inference-gate.test.ts | 2 +- .../dataset-refusal-envelope.test.ts | 2 +- .../dimension-source-field-gate.test.ts | 14 ++--- .../__tests__/dotted-measure-refusal.test.ts | 4 +- .../__tests__/filter-array-lowering.test.ts | 2 +- .../filter-normalizer-not-null-safe.test.ts | 2 +- .../filter-operator-coverage.test.ts | 2 +- .../filter-value-type-fidelity.test.ts | 2 +- .../__tests__/icontains-dialect-sql.test.ts | 2 +- .../icontains-text-comparand-refusal.test.ts | 2 +- .../like-metacharacter-escape.test.ts | 2 +- ...asure-field-and-filter-compilation.test.ts | 2 +- .../measure-source-field-gate.test.ts | 8 +-- .../native-sql-datetime-filter-column.test.ts | 2 +- .../native-sql-datetime-filter.test.ts | 2 +- ...ative-sql-filter-logic-conformance.test.ts | 2 +- .../src/__tests__/native-sql-rls.test.ts | 8 +-- .../native-sql-temporal-conformance.test.ts | 2 +- ...jectql-contains-canonical-operator.test.ts | 2 +- .../__tests__/objectql-dataset-filter.test.ts | 2 +- .../objectql-echo-operator-coverage.test.ts | 2 +- .../__tests__/objectql-icontains-arm.test.ts | 2 +- ...bjectql-read-scope-vacancy-refusal.test.ts | 2 +- .../read-scope-comparand-three-faces.test.ts | 4 +- .../read-scope-empty-nin-refusal.test.ts | 2 +- .../read-scope-eq-array-refusal.test.ts | 2 +- .../read-scope-not-null-safe.test.ts | 2 +- .../read-scope-vacancy-three-faces.test.ts | 4 +- .../__tests__/sql-dialect-vocabulary.test.ts | 2 +- .../src/__tests__/sql-echo-debug-gate.test.ts | 2 +- .../__tests__/text-match-sqlite-nul.test.ts | 2 +- .../text-operator-case-exactness.test.ts | 2 +- .../text-operator-non-text-column.test.ts | 2 +- .../unlisted-refusal-envelope.test.ts | 4 +- .../where-boolean-flag-refusal.test.ts | 2 +- .../where-equality-slot-list-refusal.test.ts | 2 +- .../__tests__/where-face-arms-refusal.test.ts | 2 +- .../__tests__/where-source-field-gate.test.ts | 16 ++--- .../__tests__/where-type-face-refusal.test.ts | 2 +- .../src/analytics-service.ts | 36 +++++++++-- .../service-analytics/src/cube-registry.ts | 3 +- .../service-analytics/src/cube-visibility.ts | 61 +++++++++++++++++++ .../service-analytics/src/dataset-compiler.ts | 5 +- packages/spec/liveness/README.md | 2 +- packages/spec/liveness/analytics_cube.json | 10 +-- packages/spec/src/data/analytics.test.ts | 11 +++- packages/spec/src/data/analytics.zod.ts | 21 ++++++- 56 files changed, 241 insertions(+), 84 deletions(-) create mode 100644 .changeset/20282-analytics-cube-public-enforced.md create mode 100644 packages/services/service-analytics/src/cube-visibility.ts diff --git a/.changeset/20282-analytics-cube-public-enforced.md b/.changeset/20282-analytics-cube-public-enforced.md new file mode 100644 index 00000000000..038b706adbb --- /dev/null +++ b/.changeset/20282-analytics-cube-public-enforced.md @@ -0,0 +1,24 @@ +--- +'@objectstack/spec': minor +'@objectstack/service-analytics': minor +--- + +An analytics cube's `public` now takes effect, and it defaults to visible: `CubeSchema.public` defaults to `true` (it was `false`), and the analytics service hides a cube that declares `public: false` from discovery and refuses every query against it (#20282). + +Clause-②: yes + +Until this change nothing read `public`. `GET /api/v1/analytics/meta` listed a `public: false` cube and every query door answered it, so the flag withheld nothing. Its declared default, `false`, could not simply be switched on: enforcing it as declared would have hidden every cube that omits the key. The default is now the Cube.dev default (visible), and an explicit `false` is enforced: + +- `GET /api/v1/analytics/meta` omits a cube declared `public: false`, and `?cube=` naming one answers `[]`, the same as a name no cube has. +- `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` refuse it with `404 CUBE_NOT_FOUND`, and the message says how to expose it. The refusal comes before any SQL is built, and it is never an empty result. + +`public` is visibility, not row security. An object's records stay governed by its permissions and row-level security on every door, whether or not a cube over it is hidden. What `public: false` withholds is the cube's own definition: its name, its measures (raw SQL included) and its dimensions. + +What to expect after upgrading: + +- **A cube that omits `public`** stays visible and queryable. It was visible before too, because nothing read the key. A client that parses cube metadata through the published JSON Schema now materializes `public: true` where it materialized `false`. +- **A cube that writes `public: false`** is now hidden and refused. If you wrote it only because it was the old default, delete the line (cubes are visible by default). A dashboard or report that queries such a cube starts answering `404 CUBE_NOT_FOUND` until you do. +- **A compiled artifact built before this release** carries a materialized `public: false` on every cube, because `os compile` writes the parsed stack with its defaults applied. Recompile it with this release before serving cubes from it. +- **Cubes the platform mints itself** stay visible: the cube inferred for an ad-hoc query on an object (the KPI path), a compiled dataset's cube (`POST /api/v1/analytics/dataset/query`), and `CubeRegistry.inferFromObject`. Each wrote a literal `false`, the old default, and now writes `true`. + +The showcase example's `showcase_delivery` cube, which is the app's demonstration of `/api/v1/analytics/*`, drops its `public: false`. diff --git a/examples/app-showcase/src/data/analytics/showcase.cube.ts b/examples/app-showcase/src/data/analytics/showcase.cube.ts index dfab7d71b4f..fe8bb4cca28 100644 --- a/examples/app-showcase/src/data/analytics/showcase.cube.ts +++ b/examples/app-showcase/src/data/analytics/showcase.cube.ts @@ -95,7 +95,12 @@ export const DeliveryCube = defineCube({ refreshKey: { every: '1 hour', }, - public: false, + // No `public` key: the cube is VISIBLE, the default. It is this app's + // demonstration of the `/api/v1/analytics/*` surface (src/coverage.ts marks + // `analyticsCubes` demonstrated, and the platform checklist's dashboards item + // discovers and queries it there). `public: false` would hide it from + // `/analytics/meta` and refuse every query against it — this file authored + // exactly that, inertly, until the analytics service began reading the key. }); export const allCubes = [DeliveryCube]; diff --git a/examples/app-showcase/test/gap-fill.test.ts b/examples/app-showcase/test/gap-fill.test.ts index 947758d7a6b..749a0b0dce2 100644 --- a/examples/app-showcase/test/gap-fill.test.ts +++ b/examples/app-showcase/test/gap-fill.test.ts @@ -36,6 +36,15 @@ describe('showcase gap fill — analytics cube', () => { expect(DeliveryCube.joins?.project?.name).toBe('showcase_project'); }); + it('is VISIBLE on the analytics API — it demonstrates /api/v1/analytics/*, so it cannot be hidden', () => { + // `public: false` hides a cube from `/analytics/meta` and refuses every + // query against it (service-analytics `cube-visibility.ts`). This cube is + // the showcase's `analyticsCubes` demonstration (src/coverage.ts), so a + // hidden one would demonstrate a 404. `defineCube` parses, so the omitted + // key reads back as the schema default. + expect(DeliveryCube.public).toBe(true); + }); + it('keys every join by a FOREIGN-KEY FIELD of its own base object, not by the target', () => { // #18612: the `joins` record KEY is what both strategies join ON — native // emits `ON ""."" = ""."id"`, ObjectQL lowers `fkField: key` diff --git a/packages/runtime/src/cross-field-refusal-operand-withhold.test.ts b/packages/runtime/src/cross-field-refusal-operand-withhold.test.ts index 8d058add690..9d8bbc07580 100644 --- a/packages/runtime/src/cross-field-refusal-operand-withhold.test.ts +++ b/packages/runtime/src/cross-field-refusal-operand-withhold.test.ts @@ -128,7 +128,7 @@ const CUBE: Cube = { (n) => [n, { name: n, label: n, type: 'string', sql: n }], ), ), - public: false, + public: true, } as unknown as Cube; interface WireBearingError extends Error { diff --git a/packages/services/service-analytics/src/__tests__/aggregate-bridge-function-vocabulary.test.ts b/packages/services/service-analytics/src/__tests__/aggregate-bridge-function-vocabulary.test.ts index 48d3e4bb0a9..8e1cb40dd24 100644 --- a/packages/services/service-analytics/src/__tests__/aggregate-bridge-function-vocabulary.test.ts +++ b/packages/services/service-analytics/src/__tests__/aggregate-bridge-function-vocabulary.test.ts @@ -84,7 +84,7 @@ const cubeWithMeasureType = (type: string): Cube => ({ sql: 'opportunity', measures: { revenue: { name: 'revenue', label: 'Revenue', type, sql: 'amount' } as Cube['measures'][string] }, dimensions: { region: { name: 'region', label: 'Region', type: 'string', sql: 'region' } }, - public: false, + public: true, }); async function analyticsVia(engine: unknown, cube: Cube): Promise { diff --git a/packages/services/service-analytics/src/__tests__/analytics-service.test.ts b/packages/services/service-analytics/src/__tests__/analytics-service.test.ts index b1e1af241b0..1ee6c8df071 100644 --- a/packages/services/service-analytics/src/__tests__/analytics-service.test.ts +++ b/packages/services/service-analytics/src/__tests__/analytics-service.test.ts @@ -31,7 +31,7 @@ const ordersCube: Cube = { granularities: ['day', 'week', 'month'], }, }, - public: false, + public: true, }; const baseQuery: AnalyticsQuery = { diff --git a/packages/services/service-analytics/src/__tests__/cross-field-engine-fallback.test.ts b/packages/services/service-analytics/src/__tests__/cross-field-engine-fallback.test.ts index 567917bdd41..5b6efb53e95 100644 --- a/packages/services/service-analytics/src/__tests__/cross-field-engine-fallback.test.ts +++ b/packages/services/service-analytics/src/__tests__/cross-field-engine-fallback.test.ts @@ -85,7 +85,7 @@ const CUBE: Cube = { (n) => [n, { name: n, label: n, type: 'string', sql: n }], ), ), - public: false, + public: true, } as unknown as Cube; interface WireBearingError extends Error { diff --git a/packages/services/service-analytics/src/__tests__/cross-field-offset-dataset.test.ts b/packages/services/service-analytics/src/__tests__/cross-field-offset-dataset.test.ts index b3d779a15aa..912ddb5e74f 100644 --- a/packages/services/service-analytics/src/__tests__/cross-field-offset-dataset.test.ts +++ b/packages/services/service-analytics/src/__tests__/cross-field-offset-dataset.test.ts @@ -126,7 +126,7 @@ const CUBE: Cube = { dimensions: Object.fromEntries( Object.keys(CROSS_FIELD_OFFSET_OBJECT_FIELDS).map((n) => [n, { name: n, label: n, type: 'string', sql: n }]), ), - public: false, + public: true, } as unknown as Cube; /** The ruling's dataset: the on-time count, the late count, and the rate. */ diff --git a/packages/services/service-analytics/src/__tests__/cross-field-reference-refusal.test.ts b/packages/services/service-analytics/src/__tests__/cross-field-reference-refusal.test.ts index 2276d3f2de3..24d019ff678 100644 --- a/packages/services/service-analytics/src/__tests__/cross-field-reference-refusal.test.ts +++ b/packages/services/service-analytics/src/__tests__/cross-field-reference-refusal.test.ts @@ -156,7 +156,7 @@ const CUBE: Cube = { amount: { name: 'amount', label: 'Amount', type: 'number', sql: 'amount' }, budget: { name: 'budget', label: 'Budget', type: 'number', sql: 'budget' }, }, - public: false, + public: true, } as unknown as Cube; // ── The supported arm: routed, not refused ─────────────────────────────────── diff --git a/packages/services/service-analytics/src/__tests__/cube-inference-gate.test.ts b/packages/services/service-analytics/src/__tests__/cube-inference-gate.test.ts index 354a34b3bb4..0cab0adbe4e 100644 --- a/packages/services/service-analytics/src/__tests__/cube-inference-gate.test.ts +++ b/packages/services/service-analytics/src/__tests__/cube-inference-gate.test.ts @@ -40,7 +40,7 @@ const authoredCube: Cube = { sql: 'some_physical_table', measures: { count: { name: 'count', label: 'Count', type: 'count', sql: '*' } }, dimensions: {}, - public: false, + public: true, }; /** Records which object each aggregate ran against, so we can assert none ran. */ diff --git a/packages/services/service-analytics/src/__tests__/dataset-refusal-envelope.test.ts b/packages/services/service-analytics/src/__tests__/dataset-refusal-envelope.test.ts index 13e0786177e..6a0f8d0d6fb 100644 --- a/packages/services/service-analytics/src/__tests__/dataset-refusal-envelope.test.ts +++ b/packages/services/service-analytics/src/__tests__/dataset-refusal-envelope.test.ts @@ -113,7 +113,7 @@ const bareCube: Cube = { sql: 'crm_opportunity', measures: { revenue: { name: 'revenue', label: 'Revenue', type: 'sum', sql: 'amount' } }, dimensions: { stage: { name: 'stage', label: 'Stage', type: 'string', sql: 'stage' } }, - public: false, + public: true, }; function nativeCtx(allowed: Set): StrategyContext { diff --git a/packages/services/service-analytics/src/__tests__/dimension-source-field-gate.test.ts b/packages/services/service-analytics/src/__tests__/dimension-source-field-gate.test.ts index 8e08c91d780..fd0516c99d4 100644 --- a/packages/services/service-analytics/src/__tests__/dimension-source-field-gate.test.ts +++ b/packages/services/service-analytics/src/__tests__/dimension-source-field-gate.test.ts @@ -281,7 +281,7 @@ describe('#5520 — the gate: a dimension over a missing field is a 400, not a d industry: { name: 'industry', label: 'Industry', type: 'string', sql: 'industry' }, legacy: { name: 'legacy', label: 'Legacy', type: 'string', sql: 'dropped_column' }, }, - public: false, + public: true, }; const { service, aggregated } = makeService({ cubes: [authored] }); @@ -350,7 +350,7 @@ describe('#5520 — the dataset face: refused before SQL exists, so nothing can dimensions: { bogus_dim: { name: 'bogus_dim', label: 'x', type: 'string', sql: 'bogus_dim' }, }, - public: false, + public: true, }; const { service } = makeService({ cubes: [derived], native: true }); @@ -425,7 +425,7 @@ describe('#5520 — what the gate must NOT do', () => { dimensions: { assessed: { name: 'assessed', label: 'Assessed', type: 'time', sql: 'assessed_at' }, }, - public: false, + public: true, }; const { service, aggregated } = makeService({ cubes: [authored] }); @@ -457,7 +457,7 @@ describe('#5520 — what the gate must NOT do', () => { dimensions: { anything: { name: 'anything', label: 'x', type: 'string', sql: 'anything' }, }, - public: false, + public: true, }; const { service } = makeService({ cubes: [derived] }); @@ -478,7 +478,7 @@ describe('#5520 — what the gate must NOT do', () => { sql: 'crm_account', measures: { count: { name: 'count', label: 'Count', type: 'count', sql: '*' } }, dimensions: {}, - public: false, + public: true, }; const { service } = makeService({ cubes: [joined] }); @@ -504,7 +504,7 @@ describe('#5520 — what the gate must NOT do', () => { sql: "CASE WHEN annual_revenue > 0 THEN 'yes' ELSE 'no' END", }, }, - public: false, + public: true, }; const { service } = makeService({ cubes: [computed] }); @@ -536,7 +536,7 @@ describe('#5520 — what the gate must NOT do', () => { dimensions: { ghost: { name: 'ghost', label: 'x', type: 'string', sql: 'ghost' }, }, - public: false, + public: true, }; const { service } = makeService({ cubes: [external] }); diff --git a/packages/services/service-analytics/src/__tests__/dotted-measure-refusal.test.ts b/packages/services/service-analytics/src/__tests__/dotted-measure-refusal.test.ts index 0dce4b45f35..107b890f493 100644 --- a/packages/services/service-analytics/src/__tests__/dotted-measure-refusal.test.ts +++ b/packages/services/service-analytics/src/__tests__/dotted-measure-refusal.test.ts @@ -320,7 +320,7 @@ describe('[#5918] the warm registry gets the same answer as the cold one', () => sql: 'crm_account', measures: { count: { name: 'count', label: 'Count', type: 'count', sql: '*' } }, dimensions: {}, - public: false, + public: true, }; const { error, sqls } = await run( { cube: 'crm_account', measures: ['owner.region_count_distinct'] }, @@ -382,7 +382,7 @@ describe('[#5918] the surfaces the ruling leaves alone', () => { }, }, dimensions: {}, - public: false, + public: true, }; const { error, sqls } = await run( { cube: 'crm_account', measures: ['owner.amount_sum'] }, diff --git a/packages/services/service-analytics/src/__tests__/filter-array-lowering.test.ts b/packages/services/service-analytics/src/__tests__/filter-array-lowering.test.ts index acaefc7cd3f..02aed489cf2 100644 --- a/packages/services/service-analytics/src/__tests__/filter-array-lowering.test.ts +++ b/packages/services/service-analytics/src/__tests__/filter-array-lowering.test.ts @@ -72,7 +72,7 @@ const CUBE: Cube = { { name: n, label: n, type: n === 'amount' ? 'number' : 'string', sql: n }, ]), ), - public: false, + public: true, } as unknown as Cube; /** Point sql.js at the `.wasm` shipped inside its own package (Node-safe). */ diff --git a/packages/services/service-analytics/src/__tests__/filter-normalizer-not-null-safe.test.ts b/packages/services/service-analytics/src/__tests__/filter-normalizer-not-null-safe.test.ts index 5aad0a15108..d47237422b2 100644 --- a/packages/services/service-analytics/src/__tests__/filter-normalizer-not-null-safe.test.ts +++ b/packages/services/service-analytics/src/__tests__/filter-normalizer-not-null-safe.test.ts @@ -101,7 +101,7 @@ const CUBE: Cube = { { name: n, label: n, type: n === 'amount' ? 'number' : 'string', sql: n }, ]), ), - public: false, + public: true, } as unknown as Cube; /** Point sql.js at the `.wasm` shipped inside its own package (Node-safe). */ diff --git a/packages/services/service-analytics/src/__tests__/filter-operator-coverage.test.ts b/packages/services/service-analytics/src/__tests__/filter-operator-coverage.test.ts index 5dbf0744b00..2e1d4f24909 100644 --- a/packages/services/service-analytics/src/__tests__/filter-operator-coverage.test.ts +++ b/packages/services/service-analytics/src/__tests__/filter-operator-coverage.test.ts @@ -54,7 +54,7 @@ const CUBE: Cube = { name: { name: 'name', label: 'Name', type: 'string', sql: 'name' }, score: { name: 'score', label: 'Score', type: 'number', sql: 'score' }, }, - public: false, + public: true, } as unknown as Cube; /** diff --git a/packages/services/service-analytics/src/__tests__/filter-value-type-fidelity.test.ts b/packages/services/service-analytics/src/__tests__/filter-value-type-fidelity.test.ts index 1d27a9db1c7..99c81736282 100644 --- a/packages/services/service-analytics/src/__tests__/filter-value-type-fidelity.test.ts +++ b/packages/services/service-analytics/src/__tests__/filter-value-type-fidelity.test.ts @@ -300,7 +300,7 @@ const CUBE: Cube = { code: { name: 'code', label: 'Code', type: 'string', sql: 'code' }, score: { name: 'score', label: 'Score', type: 'number', sql: 'score' }, }, - public: false, + public: true, } as unknown as Cube; const ROW_CASES: Array<{ name: string; filter: FilterCondition; expected: string[]; note: string }> = [ diff --git a/packages/services/service-analytics/src/__tests__/icontains-dialect-sql.test.ts b/packages/services/service-analytics/src/__tests__/icontains-dialect-sql.test.ts index 38911e42e3a..da4c34ab668 100644 --- a/packages/services/service-analytics/src/__tests__/icontains-dialect-sql.test.ts +++ b/packages/services/service-analytics/src/__tests__/icontains-dialect-sql.test.ts @@ -104,7 +104,7 @@ const CUBE: Cube = { id: { name: 'id', label: 'Id', type: 'string', sql: 'id' }, name: { name: 'name', label: 'Name', type: 'string', sql: 'name' }, }, - public: false, + public: true, } as unknown as Cube; const query = (where: unknown): AnalyticsQuery => diff --git a/packages/services/service-analytics/src/__tests__/icontains-text-comparand-refusal.test.ts b/packages/services/service-analytics/src/__tests__/icontains-text-comparand-refusal.test.ts index 9213983459d..1e1111e8627 100644 --- a/packages/services/service-analytics/src/__tests__/icontains-text-comparand-refusal.test.ts +++ b/packages/services/service-analytics/src/__tests__/icontains-text-comparand-refusal.test.ts @@ -75,7 +75,7 @@ const CUBE: Cube = { dimensions: Object.fromEntries( [['id', 'string'], ['name', 'string'], ['amt', 'number']].map(([n, t]) => [n, { name: n, label: n, type: t, sql: n }]), ), - public: false, + public: true, } as unknown as Cube; const quiet = { debug() {}, info() {}, warn() {}, error() {}, child() { return quiet; } } as never; diff --git a/packages/services/service-analytics/src/__tests__/like-metacharacter-escape.test.ts b/packages/services/service-analytics/src/__tests__/like-metacharacter-escape.test.ts index a41003eadf7..efe37af05c9 100644 --- a/packages/services/service-analytics/src/__tests__/like-metacharacter-escape.test.ts +++ b/packages/services/service-analytics/src/__tests__/like-metacharacter-escape.test.ts @@ -117,7 +117,7 @@ const CUBE: Cube = { id: { name: 'id', label: 'Id', type: 'string', sql: 'id' }, name: { name: 'name', label: 'Name', type: 'string', sql: 'name' }, }, - public: false, + public: true, } as unknown as Cube; const query = (where: unknown): AnalyticsQuery => diff --git a/packages/services/service-analytics/src/__tests__/measure-field-and-filter-compilation.test.ts b/packages/services/service-analytics/src/__tests__/measure-field-and-filter-compilation.test.ts index e001d5190ce..18615fd21b6 100644 --- a/packages/services/service-analytics/src/__tests__/measure-field-and-filter-compilation.test.ts +++ b/packages/services/service-analytics/src/__tests__/measure-field-and-filter-compilation.test.ts @@ -230,7 +230,7 @@ describe('[#10298] `/api/v1/analytics/query` compiles every per-measure `filter` // A MANIFEST cube — no dataset registry entry, so there is nothing to // scope by and the emitted statement must be what it always was. cubes: [{ - name: 'crm_case', title: 'Cases', sql: 'crm_case', public: false, + name: 'crm_case', title: 'Cases', sql: 'crm_case', public: true, measures: { count: { name: 'count', label: 'Count', type: 'count', sql: '*' }, amount_sum: { name: 'amount_sum', label: 'Amount', type: 'sum', sql: 'amount' }, diff --git a/packages/services/service-analytics/src/__tests__/measure-source-field-gate.test.ts b/packages/services/service-analytics/src/__tests__/measure-source-field-gate.test.ts index 6cbc7586243..97a33ea7794 100644 --- a/packages/services/service-analytics/src/__tests__/measure-source-field-gate.test.ts +++ b/packages/services/service-analytics/src/__tests__/measure-source-field-gate.test.ts @@ -231,7 +231,7 @@ describe('#4437 — measure source-field gate', () => { legacy: { name: 'legacy', label: 'Legacy', type: 'sum', sql: 'dropped_column' }, }, dimensions: {}, - public: false, + public: true, }; const { service, aggregated } = makeService({ cubes: [authored] }); @@ -254,7 +254,7 @@ describe('#4437 — measure source-field gate', () => { sql: 'SELECT * FROM showcase_invoice WHERE status = 1', measures: { anything_sum: { name: 'anything_sum', label: 'x', type: 'sum', sql: 'anything' } }, dimensions: {}, - public: false, + public: true, }; const { service } = makeService({ cubes: [derived] }); @@ -285,7 +285,7 @@ describe('#4437 — measure source-field gate', () => { remote_sum: { name: 'remote_sum', label: 'Remote', type: 'sum', sql: 'account.balance' }, }, dimensions: {}, - public: false, + public: true, }; const { service } = makeService({ cubes: [joined] }); @@ -327,7 +327,7 @@ describe('#4437 — measure source-field gate', () => { sql: 'remote_table', measures: { ghost_sum: { name: 'ghost_sum', label: 'x', type: 'sum', sql: 'ghost' } }, dimensions: {}, - public: false, + public: true, }; const { service } = makeService({ cubes: [external] }); diff --git a/packages/services/service-analytics/src/__tests__/native-sql-datetime-filter-column.test.ts b/packages/services/service-analytics/src/__tests__/native-sql-datetime-filter-column.test.ts index d3917e2bff3..28147b10c08 100644 --- a/packages/services/service-analytics/src/__tests__/native-sql-datetime-filter-column.test.ts +++ b/packages/services/service-analytics/src/__tests__/native-sql-datetime-filter-column.test.ts @@ -41,7 +41,7 @@ const cube: Cube = { assessed: { name: 'assessed', label: 'Assessed', type: 'time', sql: 'assessed_at' }, title: { name: 'title', label: 'Title', type: 'string', sql: 'title' }, }, - public: false, + public: true, }; /** diff --git a/packages/services/service-analytics/src/__tests__/native-sql-datetime-filter.test.ts b/packages/services/service-analytics/src/__tests__/native-sql-datetime-filter.test.ts index a3c8d68618d..2df254b6bef 100644 --- a/packages/services/service-analytics/src/__tests__/native-sql-datetime-filter.test.ts +++ b/packages/services/service-analytics/src/__tests__/native-sql-datetime-filter.test.ts @@ -55,7 +55,7 @@ const cube: Cube = { assessed: { name: 'assessed', label: 'Assessed', type: 'time', sql: 'assessed_at' }, score: { name: 'score', label: 'Score', type: 'number', sql: 'score' }, }, - public: false, + public: true, }; const EPOCH_2025_06_18 = Date.parse('2025-06-18T00:00:00.000Z'); diff --git a/packages/services/service-analytics/src/__tests__/native-sql-filter-logic-conformance.test.ts b/packages/services/service-analytics/src/__tests__/native-sql-filter-logic-conformance.test.ts index 22563cc3a83..4cb26852ce5 100644 --- a/packages/services/service-analytics/src/__tests__/native-sql-filter-logic-conformance.test.ts +++ b/packages/services/service-analytics/src/__tests__/native-sql-filter-logic-conformance.test.ts @@ -57,7 +57,7 @@ const CUBE: Cube = { { name: n, label: n, type: 'string', sql: n }, ]), ), - public: false, + public: true, } as unknown as Cube; /** Point sql.js at the `.wasm` shipped inside its own package (Node-safe). */ diff --git a/packages/services/service-analytics/src/__tests__/native-sql-rls.test.ts b/packages/services/service-analytics/src/__tests__/native-sql-rls.test.ts index 0a93c605a65..379d042f59e 100644 --- a/packages/services/service-analytics/src/__tests__/native-sql-rls.test.ts +++ b/packages/services/service-analytics/src/__tests__/native-sql-rls.test.ts @@ -12,7 +12,7 @@ const cube: Cube = { sql: 'opportunity', measures: { revenue: { name: 'revenue', label: 'Revenue', type: 'sum', sql: 'amount' } }, dimensions: { region: { name: 'region', label: 'Region', type: 'string', sql: 'account.region' } }, - public: false, + public: true, }; const query: AnalyticsQuery = { @@ -124,7 +124,7 @@ describe('NativeSQLStrategy — base-column qualification under joins', () => { region: { name: 'region', label: 'Region', type: 'string', sql: 'account.region' }, }, joins: { account: { name: 'account' } }, - public: false, + public: true, }; it('qualifies a base-table dimension with the base table when the cube has joins', async () => { @@ -146,7 +146,7 @@ describe('NativeSQLStrategy — base-column qualification under joins', () => { name: 'tasks', title: 'Tasks', sql: 'task', measures: { c: { name: 'c', label: 'Count', type: 'count', sql: '*' } }, dimensions: { status: { name: 'status', label: 'Status', type: 'string', sql: 'status' } }, - public: false, + public: true, }; const strategy = new NativeSQLStrategy(); const ctx = ctxWith({ getCube: (n) => (n === 'tasks' ? soloCube : undefined) }); @@ -171,7 +171,7 @@ describe('NativeSQLStrategy — multi-hop joins (ADR-0071)', () => { account: { name: 'crm_account' }, 'account__owner': { name: 'core_user' }, }, - public: false, + public: true, }; const mhQuery: AnalyticsQuery = { cube: 'sales', diff --git a/packages/services/service-analytics/src/__tests__/native-sql-temporal-conformance.test.ts b/packages/services/service-analytics/src/__tests__/native-sql-temporal-conformance.test.ts index e741855c51f..6424bed85e8 100644 --- a/packages/services/service-analytics/src/__tests__/native-sql-temporal-conformance.test.ts +++ b/packages/services/service-analytics/src/__tests__/native-sql-temporal-conformance.test.ts @@ -70,7 +70,7 @@ const CUBE: Cube = { at: { name: 'at', label: 'At', type: 'time', sql: 'happened_at' }, on: { name: 'on', label: 'On', type: 'time', sql: 'happened_on' }, }, - public: false, + public: true, } as unknown as Cube; const resolveTokens = (filter: T): T => diff --git a/packages/services/service-analytics/src/__tests__/objectql-contains-canonical-operator.test.ts b/packages/services/service-analytics/src/__tests__/objectql-contains-canonical-operator.test.ts index 8cd5cfb95a8..1bcfe5279cb 100644 --- a/packages/services/service-analytics/src/__tests__/objectql-contains-canonical-operator.test.ts +++ b/packages/services/service-analytics/src/__tests__/objectql-contains-canonical-operator.test.ts @@ -86,7 +86,7 @@ const CUBE: Cube = { id: { name: 'id', label: 'Id', type: 'string', sql: 'id' }, stage: { name: 'stage', label: 'Stage', type: 'string', sql: 'stage' }, }, - public: false, + public: true, } as unknown as Cube; const query = (where: unknown): AnalyticsQuery => diff --git a/packages/services/service-analytics/src/__tests__/objectql-dataset-filter.test.ts b/packages/services/service-analytics/src/__tests__/objectql-dataset-filter.test.ts index 51bed5097e3..8a265835f24 100644 --- a/packages/services/service-analytics/src/__tests__/objectql-dataset-filter.test.ts +++ b/packages/services/service-analytics/src/__tests__/objectql-dataset-filter.test.ts @@ -429,7 +429,7 @@ describe('[#10413] what the dataset filter must and must not do', () => { return runAggregate(ALL_ROWS, options); }, cubes: [{ - name: 'crm_opportunity', title: 'Opportunities', sql: 'crm_opportunity', public: false, + name: 'crm_opportunity', title: 'Opportunities', sql: 'crm_opportunity', public: true, measures: { count: { name: 'count', label: 'Count', type: 'count', sql: '*' } }, dimensions: {}, }], diff --git a/packages/services/service-analytics/src/__tests__/objectql-echo-operator-coverage.test.ts b/packages/services/service-analytics/src/__tests__/objectql-echo-operator-coverage.test.ts index 44ba5a52f61..05e760c9161 100644 --- a/packages/services/service-analytics/src/__tests__/objectql-echo-operator-coverage.test.ts +++ b/packages/services/service-analytics/src/__tests__/objectql-echo-operator-coverage.test.ts @@ -79,7 +79,7 @@ const CUBE: Cube = { { name: n, label: n, type: n === 'amount' ? 'number' : 'string', sql: n }, ]), ), - public: false, + public: true, } as unknown as Cube; /** Point sql.js at the `.wasm` shipped inside its own package (Node-safe). */ diff --git a/packages/services/service-analytics/src/__tests__/objectql-icontains-arm.test.ts b/packages/services/service-analytics/src/__tests__/objectql-icontains-arm.test.ts index 6abc2c83684..047ac5bb0f6 100644 --- a/packages/services/service-analytics/src/__tests__/objectql-icontains-arm.test.ts +++ b/packages/services/service-analytics/src/__tests__/objectql-icontains-arm.test.ts @@ -73,7 +73,7 @@ const CUBE: Cube = { dimensions: Object.fromEntries( [['id', 'string'], ['name', 'string'], ['amt', 'number']].map(([n, t]) => [n, { name: n, label: n, type: t, sql: n }]), ), - public: false, + public: true, } as unknown as Cube; const quiet = { debug() {}, info() {}, warn() {}, error() {}, child() { return quiet; } } as never; diff --git a/packages/services/service-analytics/src/__tests__/objectql-read-scope-vacancy-refusal.test.ts b/packages/services/service-analytics/src/__tests__/objectql-read-scope-vacancy-refusal.test.ts index 1f0aeecef64..541a2615dfa 100644 --- a/packages/services/service-analytics/src/__tests__/objectql-read-scope-vacancy-refusal.test.ts +++ b/packages/services/service-analytics/src/__tests__/objectql-read-scope-vacancy-refusal.test.ts @@ -101,7 +101,7 @@ const CUBE: Cube = { dimensions: Object.fromEntries( ['id', 'owner'].map((n) => [n, { name: n, label: n, type: 'string', sql: n }]), ), - public: false, + public: true, } as unknown as Cube; interface WireBearingError extends Error { diff --git a/packages/services/service-analytics/src/__tests__/read-scope-comparand-three-faces.test.ts b/packages/services/service-analytics/src/__tests__/read-scope-comparand-three-faces.test.ts index 50ca0c931b6..e8b0c857eae 100644 --- a/packages/services/service-analytics/src/__tests__/read-scope-comparand-three-faces.test.ts +++ b/packages/services/service-analytics/src/__tests__/read-scope-comparand-three-faces.test.ts @@ -97,7 +97,7 @@ const CUBE: Cube = { dimensions: Object.fromEntries( ['id', 'region'].map((n) => [n, { name: n, label: n, type: 'string', sql: n }]), ), - public: false, + public: true, } as unknown as Cube; const QUERY = { cube: 'deals', dimensions: ['id'], measures: ['n'] } as AnalyticsQuery; @@ -399,7 +399,7 @@ describe('[#20018] `applyReadScope` judges the JOINED object\'s scope too', () = sql: 'opportunity', measures: { revenue: { name: 'revenue', label: 'Revenue', type: 'sum', sql: 'amount' } }, dimensions: { region: { name: 'region', label: 'Region', type: 'string', sql: 'account.region' } }, - public: false, + public: true, }; const ctxWith = (accountScope: unknown): StrategyContext => diff --git a/packages/services/service-analytics/src/__tests__/read-scope-empty-nin-refusal.test.ts b/packages/services/service-analytics/src/__tests__/read-scope-empty-nin-refusal.test.ts index 5bdfe160f8e..db635b7e3d7 100644 --- a/packages/services/service-analytics/src/__tests__/read-scope-empty-nin-refusal.test.ts +++ b/packages/services/service-analytics/src/__tests__/read-scope-empty-nin-refusal.test.ts @@ -61,7 +61,7 @@ const CUBE: Cube = { dimensions: Object.fromEntries( ['id', 'owner'].map((n) => [n, { name: n, label: n, type: 'string', sql: n }]), ), - public: false, + public: true, } as unknown as Cube; /** Point sql.js at the `.wasm` shipped inside its own package (Node-safe). */ diff --git a/packages/services/service-analytics/src/__tests__/read-scope-eq-array-refusal.test.ts b/packages/services/service-analytics/src/__tests__/read-scope-eq-array-refusal.test.ts index 5bbc337d919..11bc7b919fb 100644 --- a/packages/services/service-analytics/src/__tests__/read-scope-eq-array-refusal.test.ts +++ b/packages/services/service-analytics/src/__tests__/read-scope-eq-array-refusal.test.ts @@ -154,7 +154,7 @@ const CUBE: Cube = { dimensions: Object.fromEntries( ['id', 'status'].map((n) => [n, { name: n, label: n, type: 'string', sql: n }]), ), - public: false, + public: true, } as unknown as Cube; const QUERY = { cube: 'tickets', dimensions: ['id'], measures: ['n'] } as AnalyticsQuery; diff --git a/packages/services/service-analytics/src/__tests__/read-scope-not-null-safe.test.ts b/packages/services/service-analytics/src/__tests__/read-scope-not-null-safe.test.ts index cbcd23640b7..5b045baa869 100644 --- a/packages/services/service-analytics/src/__tests__/read-scope-not-null-safe.test.ts +++ b/packages/services/service-analytics/src/__tests__/read-scope-not-null-safe.test.ts @@ -93,7 +93,7 @@ const cube: Cube = { sql: 'deal', measures: { revenue: { name: 'revenue', label: 'Revenue', type: 'sum', sql: 'amount' } }, dimensions: { stage: { name: 'stage', label: 'Stage', type: 'string', sql: 'stage' } }, - public: false, + public: true, }; const query: AnalyticsQuery = { diff --git a/packages/services/service-analytics/src/__tests__/read-scope-vacancy-three-faces.test.ts b/packages/services/service-analytics/src/__tests__/read-scope-vacancy-three-faces.test.ts index bb38816f580..5e77d843e7e 100644 --- a/packages/services/service-analytics/src/__tests__/read-scope-vacancy-three-faces.test.ts +++ b/packages/services/service-analytics/src/__tests__/read-scope-vacancy-three-faces.test.ts @@ -98,7 +98,7 @@ const CUBE: Cube = { dimensions: Object.fromEntries( ['id', 'owner'].map((n) => [n, { name: n, label: n, type: 'string', sql: n }]), ), - public: false, + public: true, } as unknown as Cube; const QUERY = { cube: 'deals', dimensions: ['id'], measures: ['n'] } as AnalyticsQuery; @@ -345,7 +345,7 @@ describe('[#13926] `applyReadScope` guards the JOINED object\'s scope too', () = sql: 'opportunity', measures: { revenue: { name: 'revenue', label: 'Revenue', type: 'sum', sql: 'amount' } }, dimensions: { region: { name: 'region', label: 'Region', type: 'string', sql: 'account.region' } }, - public: false, + public: true, }; const ctxWith = (accountScope: unknown): StrategyContext => diff --git a/packages/services/service-analytics/src/__tests__/sql-dialect-vocabulary.test.ts b/packages/services/service-analytics/src/__tests__/sql-dialect-vocabulary.test.ts index 938d60a7ad7..9d31e7d327a 100644 --- a/packages/services/service-analytics/src/__tests__/sql-dialect-vocabulary.test.ts +++ b/packages/services/service-analytics/src/__tests__/sql-dialect-vocabulary.test.ts @@ -95,7 +95,7 @@ const CUBE: Cube = { id: { name: 'id', label: 'Id', type: 'string', sql: 'id' }, name: { name: 'name', label: 'Name', type: 'string', sql: 'name' }, }, - public: false, + public: true, } as unknown as Cube; /** diff --git a/packages/services/service-analytics/src/__tests__/sql-echo-debug-gate.test.ts b/packages/services/service-analytics/src/__tests__/sql-echo-debug-gate.test.ts index 5568b902cf5..df59f81cfc4 100644 --- a/packages/services/service-analytics/src/__tests__/sql-echo-debug-gate.test.ts +++ b/packages/services/service-analytics/src/__tests__/sql-echo-debug-gate.test.ts @@ -62,7 +62,7 @@ const usersCube: Cube = { dimensions: { id: { name: 'id', label: 'Id', type: 'string', sql: 'id' }, }, - public: false, + public: true, }; /** diff --git a/packages/services/service-analytics/src/__tests__/text-match-sqlite-nul.test.ts b/packages/services/service-analytics/src/__tests__/text-match-sqlite-nul.test.ts index 8b6afe29e67..b6590e5b1ab 100644 --- a/packages/services/service-analytics/src/__tests__/text-match-sqlite-nul.test.ts +++ b/packages/services/service-analytics/src/__tests__/text-match-sqlite-nul.test.ts @@ -167,7 +167,7 @@ const CUBE: Cube = { label: { name: 'label', label: 'Label', type: 'string', sql: 'label' }, v: { name: 'v', label: 'V', type: 'string', sql: 'v' }, }, - public: false, + public: true, } as unknown as Cube; const query = (where: unknown): AnalyticsQuery => diff --git a/packages/services/service-analytics/src/__tests__/text-operator-case-exactness.test.ts b/packages/services/service-analytics/src/__tests__/text-operator-case-exactness.test.ts index 7e3fa8750fc..8e915e8b126 100644 --- a/packages/services/service-analytics/src/__tests__/text-operator-case-exactness.test.ts +++ b/packages/services/service-analytics/src/__tests__/text-operator-case-exactness.test.ts @@ -178,7 +178,7 @@ const CUBE: Cube = { id: { name: 'id', label: 'Id', type: 'string', sql: 'id' }, name: { name: 'name', label: 'Name', type: 'string', sql: 'name' }, }, - public: false, + public: true, } as unknown as Cube; /** A second cube over the GLOB-metacharacter fixture — see `GLOB_ROWS`. */ diff --git a/packages/services/service-analytics/src/__tests__/text-operator-non-text-column.test.ts b/packages/services/service-analytics/src/__tests__/text-operator-non-text-column.test.ts index 3460829a0d1..5647ac47396 100644 --- a/packages/services/service-analytics/src/__tests__/text-operator-non-text-column.test.ts +++ b/packages/services/service-analytics/src/__tests__/text-operator-non-text-column.test.ts @@ -74,7 +74,7 @@ const CUBE: Cube = { name: { name: 'name', label: 'Name', type: 'string', sql: 'name' }, score: { name: 'score', label: 'Score', type: 'number', sql: 'score' }, }, - public: false, + public: true, } as unknown as Cube; const query = (where: unknown): AnalyticsQuery => diff --git a/packages/services/service-analytics/src/__tests__/unlisted-refusal-envelope.test.ts b/packages/services/service-analytics/src/__tests__/unlisted-refusal-envelope.test.ts index d9bd2da11bb..878765de469 100644 --- a/packages/services/service-analytics/src/__tests__/unlisted-refusal-envelope.test.ts +++ b/packages/services/service-analytics/src/__tests__/unlisted-refusal-envelope.test.ts @@ -171,7 +171,7 @@ const joinedCube: Cube = { joins: { account: { name: 'account' }, }, - public: false, + public: true, }; /** A cube declaring exactly ONE measure — so `revenue` is undeclared on it. */ @@ -181,7 +181,7 @@ const countOnlyCube: Cube = { sql: 'crm_opportunity', measures: { count: { name: 'count', label: 'Count', type: 'count', sql: '*' } }, dimensions: { stage: { name: 'stage', label: 'Stage', type: 'string', sql: 'stage' } }, - public: false, + public: true, }; function ctxFor(cube: Cube): StrategyContext { diff --git a/packages/services/service-analytics/src/__tests__/where-boolean-flag-refusal.test.ts b/packages/services/service-analytics/src/__tests__/where-boolean-flag-refusal.test.ts index f36a77aa67a..2d86018753e 100644 --- a/packages/services/service-analytics/src/__tests__/where-boolean-flag-refusal.test.ts +++ b/packages/services/service-analytics/src/__tests__/where-boolean-flag-refusal.test.ts @@ -74,7 +74,7 @@ const CUBE: Cube = { dimensions: Object.fromEntries( [['id', 'string'], ['stage', 'string'], ['amt', 'number']].map(([n, t]) => [n, { name: n, label: n, type: t, sql: n }]), ), - public: false, + public: true, } as unknown as Cube; const quiet = { debug() {}, info() {}, warn() {}, error() {}, child() { return quiet; } } as never; diff --git a/packages/services/service-analytics/src/__tests__/where-equality-slot-list-refusal.test.ts b/packages/services/service-analytics/src/__tests__/where-equality-slot-list-refusal.test.ts index 3d705930101..9f7068302e4 100644 --- a/packages/services/service-analytics/src/__tests__/where-equality-slot-list-refusal.test.ts +++ b/packages/services/service-analytics/src/__tests__/where-equality-slot-list-refusal.test.ts @@ -239,7 +239,7 @@ const CUBE: Cube = { dimensions: Object.fromEntries( ['id', 'stage'].map((n) => [n, { name: n, label: n, type: 'string', sql: n }]), ), - public: false, + public: true, } as unknown as Cube; describe('[#19888] every analytics face refuses before anything runs (real engine)', () => { diff --git a/packages/services/service-analytics/src/__tests__/where-face-arms-refusal.test.ts b/packages/services/service-analytics/src/__tests__/where-face-arms-refusal.test.ts index 09852e15e92..380f86bb575 100644 --- a/packages/services/service-analytics/src/__tests__/where-face-arms-refusal.test.ts +++ b/packages/services/service-analytics/src/__tests__/where-face-arms-refusal.test.ts @@ -307,7 +307,7 @@ const CUBE: Cube = { dimensions: Object.fromEntries( [['id', 'string'], ['amt', 'number'], ['stage', 'string']].map(([n, t]) => [n, { name: n, label: n, type: t, sql: n }]), ), - public: false, + public: true, } as unknown as Cube; describe('[#20010] every analytics face refuses before anything runs (real engine)', () => { diff --git a/packages/services/service-analytics/src/__tests__/where-source-field-gate.test.ts b/packages/services/service-analytics/src/__tests__/where-source-field-gate.test.ts index d3aac4e0f56..6cf54fbfe99 100644 --- a/packages/services/service-analytics/src/__tests__/where-source-field-gate.test.ts +++ b/packages/services/service-analytics/src/__tests__/where-source-field-gate.test.ts @@ -341,7 +341,7 @@ describe('#5669 — the gate: a `where` over a missing field is a 400, not a dri industry: { name: 'industry', label: 'Industry', type: 'string', sql: 'industry' }, legacy: { name: 'legacy', label: 'Legacy', type: 'string', sql: 'dropped_column' }, }, - public: false, + public: true, }; const { service, aggregated } = makeService({ cubes: [authored] }); @@ -442,7 +442,7 @@ describe('#5669 — the dataset face: refused before SQL exists, so nothing can sql: 'SELECT * FROM crm_account', measures: { count: { name: 'count', label: 'Count', type: 'count', sql: '*' } }, dimensions: {}, - public: false, + public: true, }; const { service } = makeService({ cubes: [derived], native: true }); @@ -514,7 +514,7 @@ describe('#5669 — what the gate must NOT do', () => { dimensions: { assessed: { name: 'assessed', label: 'Assessed', type: 'time', sql: 'assessed_at' }, }, - public: false, + public: true, }; const { service, aggregated, filters } = makeService({ cubes: [authored] }); @@ -542,7 +542,7 @@ describe('#5669 — what the gate must NOT do', () => { revenue: { name: 'revenue', label: 'Revenue', type: 'sum', sql: 'annual_revenue' }, }, dimensions: {}, - public: false, + public: true, }; const { service, aggregated, filters } = makeService({ cubes: [authored] }); @@ -571,7 +571,7 @@ describe('#5669 — what the gate must NOT do', () => { sql: 'SELECT * FROM crm_account WHERE active = 1', measures: { count: { name: 'count', label: 'Count', type: 'count', sql: '*' } }, dimensions: {}, - public: false, + public: true, }; const { service } = makeService({ cubes: [derived] }); @@ -604,7 +604,7 @@ describe('#5669 — what the gate must NOT do', () => { sql: 'crm_account', measures: { count: { name: 'count', label: 'Count', type: 'count', sql: '*' } }, dimensions: {}, - public: false, + public: true, }; const { service } = makeService({ cubes: [joined] }); @@ -636,7 +636,7 @@ describe('#5669 — what the gate must NOT do', () => { sql: 'crm_account', measures: { count: { name: 'count', label: 'Count', type: 'count', sql: '*' } }, dimensions: {}, - public: false, + public: true, }; const { service } = makeService({ cubes: [joined] }); @@ -707,7 +707,7 @@ describe('#5669 — what the gate must NOT do', () => { sql: "CASE WHEN annual_revenue > 0 THEN 'yes' ELSE 'no' END", }, }, - public: false, + public: true, }; const { service } = makeService({ cubes: [computed] }); diff --git a/packages/services/service-analytics/src/__tests__/where-type-face-refusal.test.ts b/packages/services/service-analytics/src/__tests__/where-type-face-refusal.test.ts index 9b8535030a7..54d83013f69 100644 --- a/packages/services/service-analytics/src/__tests__/where-type-face-refusal.test.ts +++ b/packages/services/service-analytics/src/__tests__/where-type-face-refusal.test.ts @@ -313,7 +313,7 @@ const CUBE: Cube = { dimensions: Object.fromEntries( [['id', 'string'], ['amt', 'number'], ['stage', 'string']].map(([n, t]) => [n, { name: n, label: n, type: t, sql: n }]), ), - public: false, + public: true, } as unknown as Cube; const quiet = { debug() {}, info() {}, warn() {}, error() {}, child() { return quiet; } } as never; diff --git a/packages/services/service-analytics/src/analytics-service.ts b/packages/services/service-analytics/src/analytics-service.ts index 178fc717956..9c2ef53a1ed 100644 --- a/packages/services/service-analytics/src/analytics-service.ts +++ b/packages/services/service-analytics/src/analytics-service.ts @@ -33,6 +33,7 @@ import { // docblock for why the edge is acyclic and why it was worth adding. import { matchMissingColumnOfRelation } from '@objectstack/types'; import { CubeRegistry } from './cube-registry.js'; +import { cubeNotPublicError, isCubePublic } from './cube-visibility.js'; // The object-level read admission asked at this door, ahead of every strategy // — the layer the raw-SQL path could not inherit from the engine. See that // module's header for the request that reached the database without it. @@ -1347,6 +1348,12 @@ export class AnalyticsService implements IAnalyticsService { if (!queryInput.cube) { throw new Error('Cube name is required in analytics query'); } + // `analytics_cube.public` — first, ahead of token resolution, cube + // inference, admission and every strategy: a hidden cube is refused + // whatever else the request carries, and the refusal leaves the registry + // exactly as it found it. This is also the dataset door's gate — + // `DatasetExecutor` reaches its compiled cube through this method. + this.assertCubePublic(queryInput.cube); // [#12230] Expand `{current_user_id}` / date-macro placeholders at THIS // seam — before strategy selection — so every strategy compiles the same @@ -1969,12 +1976,16 @@ export class AnalyticsService implements IAnalyticsService { /** * Get cube metadata for discovery. + * + * Only cubes the analytics API exposes are listed: a cube declared + * `public: false` is omitted, and asking for it by name answers `[]` — the + * same answer as a name no cube has (`cube-visibility.ts`). */ async getMeta(cubeName?: string): Promise { - // If a fallback service is configured, merge its metadata with the registry - const cubes = cubeName + const cubes = (cubeName ? [this.cubeRegistry.get(cubeName)].filter(Boolean) as Cube[] - : this.cubeRegistry.getAll(); + : this.cubeRegistry.getAll() + ).filter(isCubePublic); return cubes.map(cube => ({ name: cube.name, @@ -1999,6 +2010,9 @@ export class AnalyticsService implements IAnalyticsService { if (!queryInput.cube) { throw new Error('Cube name is required for SQL generation'); } + // Same gate as `query()`: the dry-run door must not hand out the + // statement — the cube's measures, raw SQL included — of a hidden cube. + this.assertCubePublic(queryInput.cube); // [#12230] Same token seam as `query()` — the dry-run door must show the // statement that would actually run (a resolved user id in the params, or @@ -2017,6 +2031,17 @@ export class AnalyticsService implements IAnalyticsService { // ── Internal ───────────────────────────────────────────────────── + /** + * Refuse a query against a cube declared `public: false` (the + * `CUBE_NOT_FOUND` / 404 envelope — `cube-visibility.ts` says why that code). + * A name with no registered cube passes: it is the ad-hoc path's to infer or + * refuse (`assertInferableCube`), and every cube that path mints is visible. + */ + private assertCubePublic(name: string): void { + const cube = this.cubeRegistry.get(name); + if (cube && !isCubePublic(cube)) throw cubeNotPublicError(name); + } + /** * Ensure a cube exists for the given query and that it knows about every * measure referenced by the query. @@ -2660,7 +2685,10 @@ export class AnalyticsService implements IAnalyticsService { sql: cubeName, measures, dimensions, - public: false, + // Visible: this cube is minted FOR the request that names it and is + // registered, so the next request resolves it from the registry — where + // a hidden verdict would refuse the very KPI path that minted it. + public: true, }; } diff --git a/packages/services/service-analytics/src/cube-registry.ts b/packages/services/service-analytics/src/cube-registry.ts index d7ab4efe229..f8855904a0f 100644 --- a/packages/services/service-analytics/src/cube-registry.ts +++ b/packages/services/service-analytics/src/cube-registry.ts @@ -158,7 +158,8 @@ export class CubeRegistry { sql: objectName, measures, dimensions, - public: false, + // The schema default (visible) — a hidden cube is refused by every query door. + public: true, }; this.register(cube); diff --git a/packages/services/service-analytics/src/cube-visibility.ts b/packages/services/service-analytics/src/cube-visibility.ts new file mode 100644 index 00000000000..44d9fcf712d --- /dev/null +++ b/packages/services/service-analytics/src/cube-visibility.ts @@ -0,0 +1,61 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { Cube } from '@objectstack/spec/data'; + +/** + * The reader of `analytics_cube.public` — the one place the analytics API asks + * whether a registered cube is exposed. + * + * ## What the key means (the Cube.dev semantics the schema follows) + * + * A cube declared `public: false` is hidden from the analytics API: discovery + * (`getMeta`, `GET /analytics/meta`) omits it, and every door that runs a query + * against a named cube refuses it — `query()` (`POST /analytics/query`, and the + * dataset door, which reaches `query()` through `DatasetExecutor`) and + * `generateSql()` (`POST /analytics/sql`). Anything else is visible; the schema + * default is `true`. + * + * It is VISIBILITY, not row security. An object's records stay governed by the + * object's permissions and row-level security on every door (`assertReadAdmitted` + * and the read scope in `analytics-service.ts`), whether or not some cube over + * that object is hidden — a caller who may read the object may still aggregate it + * through an ad-hoc cube named after the object. What `false` withholds is the + * cube's own semantic definition: its name, its measures (raw SQL included) and + * its dimensions. + * + * ## Why `=== false` and not a truthiness test + * + * The registry holds the INPUT shape (`Cube` is `z.input`): + * `CubeRegistry.register` never parses, and the internal producers + * (`inferCubeFromQuery`, `compileDataset`, `CubeRegistry.inferFromObject`) build + * typed literals. So an absent key reaches this function as `undefined`, and the + * reading of an absent key is the schema's default — visible. Only the explicit + * value the author wrote hides; `analytics.test.ts` in `@objectstack/spec` pins + * that default, so the two cannot drift apart silently. + * + * ## Why the refusal is `CUBE_NOT_FOUND` / 404 + * + * To an API consumer a hidden cube is exactly as absent as one nobody declared: + * discovery omits both, so the query doors answer both with the same declared + * envelope instead of a second code that would only tell a caller which names + * exist. The message differs, because the person reading it is usually the + * author who wrote the flag, and it says how to expose the cube. It is never an + * empty result: a query that silently returned no rows would read as "no data" + * on a dashboard. + */ +export function isCubePublic(cube: Cube): boolean { + return cube.public !== false; +} + +/** The `CUBE_NOT_FOUND` / 404 refusal every query door answers a hidden cube with. */ +export function cubeNotPublicError(name: string): Error & { code: string; status: number; cube: string } { + const err = new Error( + `Cube '${name}' is not available through the analytics API: it declares \`public: false\`, which hides it ` + + `from /analytics/meta and refuses every query against it. Remove \`public: false\` from the cube ` + + `definition (cubes are visible by default) to query it, or query a cube that is not hidden.`, + ) as Error & { code: string; status: number; cube: string }; + err.code = 'CUBE_NOT_FOUND'; + err.status = 404; + err.cube = name; + return err; +} diff --git a/packages/services/service-analytics/src/dataset-compiler.ts b/packages/services/service-analytics/src/dataset-compiler.ts index d9a2386ccac..497d62ca63d 100644 --- a/packages/services/service-analytics/src/dataset-compiler.ts +++ b/packages/services/service-analytics/src/dataset-compiler.ts @@ -685,7 +685,10 @@ export function compileDataset( sql: dataset.object, measures, dimensions, - public: false, + // Visible: `queryDataset` runs the selection through `DatasetExecutor`, + // which reaches this cube by name through `AnalyticsService.query()` — the + // door that refuses a hidden cube (`cube-visibility.ts`). + public: true, }; if (Object.keys(joins).length > 0) cube.joins = joins; diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index ddcee1dcc7a..0a5c19fd9fa 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -940,7 +940,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | realtime_subscription | seeded 2026-09-04 (#14446) — a TRANSPORT-PROTOCOL surface, the fifth category the `SPEC_ONLY_SCHEMAS` override has had to reach. `SubscriptionSchema` (`packages/spec/src/api/realtime.zod.ts`) is what a client declares to open a realtime subscription: the item type of `RealtimeConfigSchema.subscriptions` and the `Subscription` the generated API reference publishes. Like `query` it is a request surface rather than stored metadata, and like `query` that is exactly why it went unasked — no registry holds it, `RealtimeConfigSchema` is `.passthrough()` so nothing downstream even refuses an unknown key, and the whole vocabulary sat outside the denominator while the reference kept publishing it. Rooted on `SubscriptionSchema` rather than on `RealtimeConfigSchema` for the reason the four `RestServerConfig` sub-objects document one row up: the walk drilled exactly ONE level when this was rooted (it recurses as of #17424; the rooting stands), so with the config as the root `events[].type` and `events[].filters` would inherit a container verdict instead of carrying rows of their own — #4956's shape. **Dead 6 = every key it has, and the CONTAINER is the finding**: nothing outside `packages/spec` imports `SubscriptionSchema`, `SubscriptionEventSchema` or `RealtimeConfigSchema` at all, so no key beneath them can be read (the `manifest.contributes` reasoning). The two keys the card measured are the sharp ones. `events[].type` accepts `RealtimeEventType`, whose four members (`record.created` / `record.updated` / `record.deleted` / `field.changed`) are DISJOINT from what the engine publishes (`DataEventType`'s `data.record.*`, live emitter in `service-knowledge`), so an author who writes the enum's own `record.created` gets a subscription that silently never fires — and the enum is what the API reference shows them. Its direction is settled by the 2026-09-02 triage and quoted verbatim in the row: enforce means REPOINTING THE ENUM, never changing what the runtime publishes. `field.changed` is the same spelling the sibling `DataEventType` REMOVED in 17.0.0 (#4673, PR #4685) for having no producer; it survives here only because this enum was never in a ratchet's denominator. `events[].filters` is `z.unknown().optional()` — the textbook ADR-0049 fourth state, no shape and no reader, failing in the permissive direction (a subscriber who filters receives every event). ⚠️ Three spellings of a realtime subscription exist and only the third is executed: this one, `websocket.zod.ts#EventSubscriptionSchema`, and the plain interface `contracts/realtime-service.ts#RealtimeSubscriptionOptions` that `in-memory-realtime-adapter.ts#matchesSubscription` actually reads. The file note names the same-name-different-shape traps so the next census does not mistake one for a consumer. Zero live | | sharing_rule | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, and the first one PAID (`connector` and `analytics_cube` are still owed on that card). Not a registered kind: it is bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reaches the walk through `getMetadataTypeSchema`'s unregistered-kind fallback, so this ledger governs a type `listMetadataTypeSchemaTypes()` still does not enumerate. One shape fact decides every row: the AUTHORING shape is not the ENFORCED shape. ADR-0057 D6 makes the `sys_sharing_rule` row canonical (`object_name` + `criteria_json` + `recipient_type`/`recipient_id` + `access_level`) and `bootstrapDeclaredSharingRules` translates each authored key into it at boot — nothing re-parses `SharingRuleSchema` at enforcement time — so every consumer cited reads a COLUMN and every row carries the `producer` (#4837) that populates it, which is the `seed.env` lesson applied to a whole type rather than to one key. Preview read points ENUMERATED per the #7131 rule and the answer recorded rather than skipped: `registerBuiltinPreviews()` (objectui @dda8f381) registers twenty types and `sharing_rule` is not one of them; what objectui does consume is the whole shape, on the CREATE door only (`AUTHOR_SHAPE_ONLY_TYPES` — the EDIT door is deliberately ungated because a served body carries the `_diagnostics` decoration this `.strict()` schema rejects). The single non-`live` row is `type`, the `SharingRuleType` discriminator: one member, `criteria`, whose only reader is a defensive `=== 'owner'` comparison that is unreachable for every value the schema admits. `planned` on the `action.operation` precedent (a one-member discriminator held `planned` until a runtime half dispatched on it, #15080), and deliberately NOT an enforce-or-remove candidate: the key is required, so removing it would break every authored rule to delete nothing. | | connector | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, paid in the same diff as `analytics_cube`, which empties that map. Not a registered kind: bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. **What the walk actually resolves, measured:** the binding names `DeclarativeConnectorEntrySchema`. ⚠️ The MECHANISM changed with the `connectionTimeoutMs` retirement and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def, and the key-set conclusion used to rest on that attachment. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child, and what preserves the walked shape is the pipe's read-through `shape`, NOT a `superRefine` attachment. The CONCLUSION is unchanged and re-measured on the built entry rather than inherited: both carriers expose 30 keys and the key sets are byte-identical, with no entry-only and no base-only key. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the three rows where they are the whole verdict. **ONE SCHEMA, TWO DOORS** is the shape fact behind the 29/1/44 split (live/planned/dead; counts read from the generated `state-counts.md` row, never hand-kept here): the ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code — so a key can have a real consumer and still do nothing when a metadata author writes it. The keys an authored entry can reach are exactly the author-supplied `ConnectorProviderContext` fields plus `provider` and `enabled` — `name` is itself one of those fields (the former "plus `name`" tail double-counted it), `loadPackageFile` is host-injected rather than authored, and `provider` selects the factory without ever reaching the context; `type` and `icon` reach that context and are dropped by all three shipped factories, and each says so on its own row. `authentication` is the ledger's `planned`, and ⛔ NOT "refused outright" — the former tail here said exactly that and all three instruments contradict it, including the one it cites: the KEY is ACCEPTED (`connector.zod.ts` declares `authentication: ConnectorAuthConfigSchema.optional().default({ type: 'none' })`, and the accepted value does nothing); what #7990 refuses is a non-`none` VALUE (`if (entry.authentication && entry.authentication.type !== 'none')`, whose own message prescribes "drop `authentication` (or set `{ type: 'none' }`)"); and ADR-0097 §3, titled "Credentials are references", rejects **inline secrets** in stack metadata, not the key. Accepted-and-ignored, plus a loud refusal of every value but `{ type: 'none' }`, is exactly the basis of the `planned` verdict — which the row itself already stated ("the accepted value does nothing"), so the summary, not the row, was the wrong half. The 44 `dead`, re-measured at this head and partitioned so every row is counted exactly once: three declared subsystems with no engine — `syncConfig` (8), `fieldMappings` (7), `health` (15, both sub-blocks) — plus `triggers` (6, and the schema's own docblock says so: #3197), the connector's nested `webhooks` (one blanket verdict, recorded in the undrilled baseline), `status`, `metadata`, `actions.description`/`.outputSchema`, and the three top-level `retiredKey` tombstones `rateLimitConfig`, `errorMapping` and `connectionTimeoutMs`. That sums to 44, the dead count the generated `state-counts.md` row carries. ⚠️ `retryConfig` IS NO LONGER IN THIS LIST: all eight of its sub-keys went `live` when #18975 made the declared policy execute at the one platform fetch site, which is the same measurement the falsification note at the end of this row records — so a reader who still finds "`retryConfig` (8)" among the dead is reading a stale copy. ⚠️ Nor is it "the two timeouts" any more: `requestTimeoutMs` is `live` (it becomes `resilientFetch`'s per-attempt deadline) and `connectionTimeoutMs` is the retired tombstone named above. ⭐ SIX rows in this ledger are `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent) — `rateLimitConfig`, `errorMapping`, `connectionTimeoutMs`, `fieldMappings.transform`, `triggers.interval` and `health.circuitBreaker.monitoringWindow` — but ⛔ that six is NOT a separate addend: the last three are already inside the `fieldMappings`, `triggers` and `health` counts above, which is exactly the double-count that made the previous "and four `retiredKey` tombstones" tail drift. Count them by name, never by adding the tail. **A prior in-repo claim is recorded here with its DIRECTION measured rather than remembered, because this row's job is the history of how the type got here**: the conversion registry's note inside `connector-rate-limit-config-removed`'s fixture reads "`retryConfig` and the timeouts beside it are untouched by THIS conversion — a statement about its scope, not a liveness verdict. They are not live: declared, defaulted and documented, and read by nothing." ⚠️ It asserts they are NOT live, and it scopes "untouched" to that one conversion. The former tail here quoted it as asserting the OPPOSITE ("they are live") and called it false when seeded — an inversion that turned this whole passage upside down, and it is corrected rather than carried. Measured direction: the note was TRUE when this ledger was seeded (2026-09-17) and is STALE now, #18975 having made the declared policy execute at the one platform fetch site (`connectorFetchOptions` → `resilientFetch`), so `retryConfig`'s eight sub-keys are `live` on their own rows and `requestTimeoutMs` is `live` beside them; only `connectionTimeoutMs` still answers to it, as the retired tombstone. ⛔ The stale comment is not rewritten from here — it is #19729's, as a dated note beside it — and it is not a line this PR's diff touches. ⚠️ The seeding note's supporting census — "the word does not occur outside `packages/spec` at all" — is FALSE at this head and is corrected rather than carried: `git grep -n retryConfig 14fdebd766 -- . ':!packages/spec'` returns 67 **matching lines** over 15 files — `git grep -o` on the same tree and pathspec returns 77 **occurrences**, and a line is not an occurrence, which is the trap a re-measurer falls into next (26 matching lines in the materializer `packages/services/service-automation/src/plugin.ts` and its materialization test, 22 across `connector-rest` and `connector-openapi` — providers, connectors and their tests — 13 in five `.changeset` fragments, and 6 on two `content/docs` pages). ⛔ Re-read that as the standing lesson of this row: a census is a count plus the tree it was taken against, and a bare "does not occur" with no commit behind it is the shape that rots first. The timeouts half is settled on its own rows: `requestTimeoutMs` is `live`, `connectionTimeoutMs` is retired | -| analytics_cube | seeded 2026-09-17 (#18582) — the third debt, paid in the same diff as `connector`. Not a registered kind either: bound in `UNREGISTERED_KIND_SCHEMAS` by #10194 and reached through the same unregistered-kind fallback. **ONE Cube shape, THREE producers, one registry** is what decides every row: `cube-registry.ts` names them itself — authored cubes (`analyticsCubes[]` / `defineCube()`, threaded by the CLI into `AnalyticsServiceConfig.cubes`), COMPILED DATASETS (ADR-0021, where `dataset-compiler` mints a Cube), and ad-hoc query inference. Only the first is the authoring door governed here, so a key whose only reader sits on the compiled-dataset path is not live for an authored cube however busy that reader is — the #4837 producer rule on a shape with three producers. That is `dimensions.granularities` (read by `dataset-executor#granularityOf`, whose argument is a `CompiledDataset` an authored cube never becomes) and `measures.format` (written by the compiler, threaded to the wire from the DATASET measure instead). The query path is genuinely live: `sql` is the FROM table AND the object whose RLS read scope is injected, `measures.type` picks the aggregate, `measures.sql`/`dimensions.sql` the column, `joins[].name` the joined table. The 10 `dead` are the caching block (`refreshKey.every`/`.sql` — no refresh scheduler exists anywhere), the access-control flag (`public` — three sites write `false`, nothing reads it: a knob that was never wired, not a hole that was opened), the three `description`s, and the inner `name` on each of `measures`/`dimensions`, where the record KEY is the identity. It was 12 until #18612 RETIRED `joins[].relationship` and the REQUIRED `joins[].sql` (ADR-0049 enforce-or-remove, maintainer-ruled batch #154): the ON clause is SYNTHESISED as an FK equality and the authored one was never consulted, so a declared join condition came back REPLACED under a 200. `CubeJoinSchema` is a `strictObject`, so the route was strict deletion plus a `guidance` prescription and the two rows left this ledger with the keys — not the `retiredKey()` route, which keeps the row. **#10238 is not prejudged**: whether cube authoring is live end to end is still its own measurement — this ledger answers the per-key question only | +| analytics_cube | seeded 2026-09-17 (#18582) — the third debt, paid in the same diff as `connector`. Not a registered kind either: bound in `UNREGISTERED_KIND_SCHEMAS` by #10194 and reached through the same unregistered-kind fallback. **ONE Cube shape, THREE producers, one registry** is what decides every row: `cube-registry.ts` names them itself — authored cubes (`analyticsCubes[]` / `defineCube()`, threaded by the CLI into `AnalyticsServiceConfig.cubes`), COMPILED DATASETS (ADR-0021, where `dataset-compiler` mints a Cube), and ad-hoc query inference. Only the first is the authoring door governed here, so a key whose only reader sits on the compiled-dataset path is not live for an authored cube however busy that reader is — the #4837 producer rule on a shape with three producers. That is `dimensions.granularities` (read by `dataset-executor#granularityOf`, whose argument is a `CompiledDataset` an authored cube never becomes) and `measures.format` (written by the compiler, threaded to the wire from the DATASET measure instead). The query path is genuinely live: `sql` is the FROM table AND the object whose RLS read scope is injected, `measures.type` picks the aggregate, `measures.sql`/`dimensions.sql` the column, `joins[].name` the joined table. The 9 `dead` are the caching block (`refreshKey.every`/`.sql` — no refresh scheduler exists anywhere), the three `description`s, `measures.format`, `dimensions.granularities`, and the inner `name` on each of `measures`/`dimensions`, where the record KEY is the identity. **#20282** flips the tenth, the visibility flag `public`, `dead` → `live` 2026-09-27: seeded as a knob that was never wired (three internal mints wrote `false`, nothing read it), it is now read by `service-analytics`' `cube-visibility.ts#isCubePublic` — `getMeta` omits a hidden cube and `query()` / `generateSql()` refuse it — in the same change that moved its default from `false` to the Cube.dev `true`, since enforcing the old default would have hidden every authored cube. It was 12 until #18612 RETIRED `joins[].relationship` and the REQUIRED `joins[].sql` (ADR-0049 enforce-or-remove, maintainer-ruled batch #154): the ON clause is SYNTHESISED as an FK equality and the authored one was never consulted, so a declared join condition came back REPLACED under a 200. `CubeJoinSchema` is a `strictObject`, so the route was strict deletion plus a `guidance` prescription and the two rows left this ledger with the keys — not the `retiredKey()` route, which keeps the row. **#10238 is not prejudged**: whether cube authoring is live end to end is still its own measurement — this ledger answers the per-key question only | The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every misleading entry carries `authorWarn` so authors hear about it at compile time diff --git a/packages/spec/liveness/analytics_cube.json b/packages/spec/liveness/analytics_cube.json index a69eb35c18c..96b71e86359 100644 --- a/packages/spec/liveness/analytics_cube.json +++ b/packages/spec/liveness/analytics_cube.json @@ -1,6 +1,6 @@ { "type": "analytics_cube", - "_note": "CubeSchema (packages/spec/src/data/analytics.zod.ts). Seeded 2026-09-17 (#18582): the LAST of the three PENDING_GOVERNANCE debts #18133 declared when PR #18581 widened the governance denominator from the registered kinds to `authorableTypes()`; `sharing_rule` was paid first (PR #18587) and `connector` is paid in the same diff as this file, which empties the map. NOT a registered metadata KIND — it is bound in `UNREGISTERED_KIND_SCHEMAS` (#10194) and reaches this walk through `getMetadataTypeSchema`'s unregistered-kind fallback, so the ledger governs it while `listMetadataTypeSchemaTypes()` still does not enumerate it. THE SHAPE FACT THAT DECIDES EVERY ROW BELOW: one Cube shape, THREE producers, one registry. `packages/services/service-analytics/src/cube-registry.ts` names them itself — (1) authored cubes (`defineStack({ analyticsCubes })` / `defineCube()`), threaded by the CLI into `AnalyticsServiceConfig.cubes` and registered by `registerAll`; (2) COMPILED DATASETS (ADR-0021), where `dataset-compiler.ts` MINTS a Cube from a `dataset` document; (3) ad-hoc query inference (`inferCubeFromQuery`). Only (1) is the authoring door this ledger governs, so a key whose only reader sits on path (2) is NOT live here however busy that reader is — that is the #4837 producer rule applied to a shape with three producers, and it is what decides `dimensions.granularities` and `measures.format` below. Every `live` row therefore carries a `producer` naming the CLI threading site: a consumer citation alone would be the `seed.env` shape, where the mechanism was right and nobody supplied the input. #10238 IS NOT PREJUDGED: the PENDING_GOVERNANCE row this file discharges said whether cube authoring is live end-to-end is its own measurement and 'this row does not prejudge it'. This ledger does not answer that question either — it answers the per-key one (who reads this key?), and the answers below are mixed: the query path (`sql`, `measures.sql`/`.type`, `dimensions.sql`/`.type`, `joins.name`) is genuinely consumed, while the caching, access-control and display-annotation keys are not. PREVIEW READ POINTS ENUMERATED (the #7131 mechanical rule, objectui @dda8f3815): `registerBuiltinPreviews()` in packages/app-shell/src/views/metadata-admin/previews/index.ts registers nineteen types and `analytics_cube` is NOT one of them — this type has no registered metadata-admin preview. Recorded rather than skipped, because 'the type has no registered preview' is the sentence a later sweep needs. What objectui DOES consume is the whole SHAPE: `clientValidation.ts` maps `analytics_cube` to `CubeSchema` itself, and unlike `sharing_rule` it is absent from `AUTHOR_SHAPE_ONLY_TYPES`, so both the CREATE and the EDIT door in metadata-admin refuse a cube this schema rejects. ADR-0054: no row here carries a `proof`, and none is owed — the `analytics` high-risk class binds `dataset/dimensions.dateGranularity` (the dataset door), not this type.", + "_note": "CubeSchema (packages/spec/src/data/analytics.zod.ts). Seeded 2026-09-17 (#18582): the LAST of the three PENDING_GOVERNANCE debts #18133 declared when PR #18581 widened the governance denominator from the registered kinds to `authorableTypes()`; `sharing_rule` was paid first (PR #18587) and `connector` is paid in the same diff as this file, which empties the map. NOT a registered metadata KIND — it is bound in `UNREGISTERED_KIND_SCHEMAS` (#10194) and reaches this walk through `getMetadataTypeSchema`'s unregistered-kind fallback, so the ledger governs it while `listMetadataTypeSchemaTypes()` still does not enumerate it. THE SHAPE FACT THAT DECIDES EVERY ROW BELOW: one Cube shape, THREE producers, one registry. `packages/services/service-analytics/src/cube-registry.ts` names them itself — (1) authored cubes (`defineStack({ analyticsCubes })` / `defineCube()`), threaded by the CLI into `AnalyticsServiceConfig.cubes` and registered by `registerAll`; (2) COMPILED DATASETS (ADR-0021), where `dataset-compiler.ts` MINTS a Cube from a `dataset` document; (3) ad-hoc query inference (`inferCubeFromQuery`). Only (1) is the authoring door this ledger governs, so a key whose only reader sits on path (2) is NOT live here however busy that reader is — that is the #4837 producer rule applied to a shape with three producers, and it is what decides `dimensions.granularities` and `measures.format` below. Every `live` row therefore carries a `producer` naming the CLI threading site: a consumer citation alone would be the `seed.env` shape, where the mechanism was right and nobody supplied the input. #10238 IS NOT PREJUDGED: the PENDING_GOVERNANCE row this file discharges said whether cube authoring is live end-to-end is its own measurement and 'this row does not prejudge it'. This ledger does not answer that question either — it answers the per-key one (who reads this key?), and the answers below are mixed: the query path (`sql`, `measures.sql`/`.type`, `dimensions.sql`/`.type`, `joins.name`) is genuinely consumed, and so is the visibility key `public` (enforced at discovery and at every query door since 2026-09-27 — its row), while the caching and display-annotation keys are not. PREVIEW READ POINTS ENUMERATED (the #7131 mechanical rule, objectui @dda8f3815): `registerBuiltinPreviews()` in packages/app-shell/src/views/metadata-admin/previews/index.ts registers nineteen types and `analytics_cube` is NOT one of them — this type has no registered metadata-admin preview. Recorded rather than skipped, because 'the type has no registered preview' is the sentence a later sweep needs. What objectui DOES consume is the whole SHAPE: `clientValidation.ts` maps `analytics_cube` to `CubeSchema` itself, and unlike `sharing_rule` it is absent from `AUTHOR_SHAPE_ONLY_TYPES`, so both the CREATE and the EDIT door in metadata-admin refuse a cube this schema rejects. ADR-0054: no row here carries a `proof`, and none is owed — the `analytics` high-risk class binds `dataset/dimensions.dateGranularity` (the dataset door), not this type.", "props": { "name": { "status": "live", @@ -134,9 +134,11 @@ } }, "public": { - "status": "dead", - "verifiedAt": "2026-09-17", - "note": "An ACCESS-CONTROL key that gates nothing, which is the worst class this ledger records (a security no-op / false compliance, the reason the gate exists). Declared `z.boolean().default(false)` under the comment 'Access Control'. Census: three sites WRITE it — `analytics-service.ts#inferCubeFromQuery`, `cube-registry.ts#inferFromObject` and `dataset-compiler.ts`, each a literal `public: false` — and the showcase example authors `public: false`; nothing anywhere READS `cube.public`. Access to `/api/v1/analytics/query` is decided by the REST layer's own auth plus the per-object RLS/tenant scope `applyReadScope` injects from `cube.sql` and `cube.joins[].name`, never by this flag, so `public: true` grants nothing and `public: false` withholds nothing. ⛔ Do NOT read the dead verdict as 'the cube is exposed': it is not a hole that was opened, it is a knob that was never wired. ADR-0049 wants a DECISION here rather than a sweep — either enforce it at the query door or remove it — and the removal half is not free, because a `.default(false)` key is present on every parsed cube." + "status": "live", + "verifiedAt": "2026-09-27", + "evidence": "packages/services/service-analytics/src/cube-visibility.ts#isCubePublic — the one reader: a cube is exposed unless it declares `public: false` (the registry holds the input shape, so an omitted key reads as the schema default, visible); packages/services/service-analytics/src/analytics-service.ts#getMeta filters every cube it serves on `GET /api/v1/analytics/meta` through it, so a hidden cube is omitted and a `?cube=` naming one answers `[]`; packages/services/service-analytics/src/analytics-service.ts#assertCubePublic runs first in `query()` (`POST /api/v1/analytics/query`, and the dataset door, whose `DatasetExecutor` reaches its compiled cube through `query()`) and in `generateSql()` (`POST /api/v1/analytics/sql`), refusing a hidden cube with packages/services/service-analytics/src/cube-visibility.ts#cubeNotPublicError (`CUBE_NOT_FOUND` / 404) before token resolution, cube inference, admission or any strategy runs. Pinned door by door in packages/services/service-analytics/src/__tests__/cube-public-visibility.test.ts.", + "producer": "packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS — the `analytics` entry declares `configKey: 'analyticsCubes'` and the capability resolver threads it into the plugin (`const cubes = (config as any).analyticsCubes ?? (config as any).cubes ?? []; arg = { cubes }`); packages/services/service-analytics/src/analytics-service.ts#registerAll (`if (config.cubes) this.cubeRegistry.registerAll(config.cubes)`) is where the authored array becomes the registry every consumer below resolves through. Without this thread an authored cube reaches no reader at all — the `seed.env` shape (#4837).", + "note": "VISIBILITY on the analytics API — the Cube.dev semantics this schema follows — not row security: an object's records stay governed by its permissions and RLS on every door (`assertReadAdmitted` and the injected read scope), whether or not a cube over it is hidden; what `false` withholds is the cube's own definition (name, measures with their raw SQL, dimensions). Dead until 2026-09-27 — declared `z.boolean().default(false)` under an 'Access Control' comment and read by nothing, so `public: false` withheld nothing (false compliance). Enforced in the same change that moved the default to `true`: enforcing the old default as declared would have hidden every authored cube. The three INTERNAL mints (`inferCubeFromQuery`, `compileDataset`, `CubeRegistry.inferFromObject`) wrote the old default `false` as a literal; they now write `true`, because the ad-hoc KPI path and the dataset door both reach their own minted cube through the refusing `query()`. The one in-repo author of `public: false` was `examples/app-showcase`, whose cube IS the app's /analytics/* demonstration — it now omits the key. Not a door: the metadata API (`/api/v1/meta/analytics_cube`) is the model-authoring surface and keeps listing hidden cubes to those who may read metadata; and `@objectstack/driver-memory`'s standalone `MemoryAnalyticsService` does not read the key — no in-repo composition registers it as the `analytics` service, and behind `AnalyticsService` it is reached only through the gated `query()`/`generateSql()`." } } } diff --git a/packages/spec/src/data/analytics.test.ts b/packages/spec/src/data/analytics.test.ts index 2f239db88ef..0c469cc58a9 100644 --- a/packages/spec/src/data/analytics.test.ts +++ b/packages/spec/src/data/analytics.test.ts @@ -411,7 +411,7 @@ describe('CubeSchema', () => { const cube = CubeSchema.parse(validCube); expect(cube.name).toBe('orders'); - expect(cube.public).toBe(false); + expect(cube.public).toBe(true); }); it('should accept cube with all fields', () => { @@ -437,10 +437,17 @@ describe('CubeSchema', () => { expect(cube.public).toBe(true); }); + it('keeps an explicit `public: false` (the hidden cube the analytics API refuses)', () => { + expect(CubeSchema.parse({ ...validCube, public: false }).public).toBe(false); + }); + it('should apply defaults', () => { const cube = CubeSchema.parse(validCube); - expect(cube.public).toBe(false); + // Visible by default — the Cube.dev default. `service-analytics` hides a + // cube only on an explicit `public: false` (cube-visibility.ts), so a + // `false` default would hide every cube that omits the key. + expect(cube.public).toBe(true); expect(cube.title).toBeUndefined(); expect(cube.joins).toBeUndefined(); expect(cube.refreshKey).toBeUndefined(); diff --git a/packages/spec/src/data/analytics.zod.ts b/packages/spec/src/data/analytics.zod.ts index 4bab38c7e78..96666bdb5f4 100644 --- a/packages/spec/src/data/analytics.zod.ts +++ b/packages/spec/src/data/analytics.zod.ts @@ -394,8 +394,25 @@ export const CubeSchema = lazySchema(() => strictObject( }, ).optional(), - /** Access Control */ - public: z.boolean().default(false), + /** + * Visibility on the analytics API (the Cube.dev `public` semantics). + * + * Defaults to VISIBLE. `false` hides the cube from discovery + * (`GET /analytics/meta`) and refuses every query door that names it + * (`POST /analytics/query`, `POST /analytics/sql`) with `CUBE_NOT_FOUND`. + * It is visibility, not row security: an object's records stay governed by + * its permissions and row-level security on every door, whether or not a + * cube over it is hidden. Enforced by `@objectstack/service-analytics` + * (`cube-visibility.ts`). The default was `false` while nothing read the + * key; it moved to the mainstream default in the change that enforced it, + * because enforcing `false` as declared would have hidden every cube. + */ + public: z.boolean().default(true).describe( + 'Whether the analytics API exposes this cube. Default true (visible). false hides it from ' + + 'GET /analytics/meta and refuses POST /analytics/query and /analytics/sql for it ' + + '(CUBE_NOT_FOUND). Visibility only: the underlying object\'s permissions and row-level ' + + 'security still govern its records on every door.' + ), // ADR-0010 — runtime protection envelope (internal — set by loader). // [#10194] See the docblock above for why this spread became load-bearing From 02087be462e6abab0ade4449305802bfa915b3e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 22:19:58 +0000 Subject: [PATCH 03/15] chore(spec): declare the Cube.public default change and regenerate its projections data/Cube:public false -> true is declared in DEFAULT_CHANGES_BY_MAJOR (the authorable-defaults ratchet refuses an undeclared default move); the reference page and the liveness state counts are regenerated by their generators. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude --- content/docs/references/data/analytics.mdx | 2 +- packages/spec/authorable-defaults/data.json | 2 +- packages/spec/liveness/state-counts.md | 4 ++-- packages/spec/scripts/lib/default-changes.ts | 20 ++++++++++++++++++++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/content/docs/references/data/analytics.mdx b/content/docs/references/data/analytics.mdx index 870bb30ecbc..844fd3f80af 100644 --- a/content/docs/references/data/analytics.mdx +++ b/content/docs/references/data/analytics.mdx @@ -129,7 +129,7 @@ Type: `[string, string]` | **dimensions** | `Record; … }>` | ✅ | Qualitative attributes | | **joins** | `Record` | optional | | | **refreshKey** | `{ every?: string; sql?: string }` | optional | | -| **public** | `boolean` | optional (default: `false`) | | +| **public** | `boolean` | optional (default: `true`) | Whether the analytics API exposes this cube. Default true (visible). false hides it from GET /analytics/meta and refuses POST /analytics/query and /analytics/sql for it (CUBE_NOT_FOUND). Visibility only: the underlying object's permissions and row-level security still govern its records on every door. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | | **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | diff --git a/packages/spec/authorable-defaults/data.json b/packages/spec/authorable-defaults/data.json index 3088b10c4c8..38ddf81d7c9 100644 --- a/packages/spec/authorable-defaults/data.json +++ b/packages/spec/authorable-defaults/data.json @@ -10,7 +10,7 @@ "data/CrossFieldValidation:events = [\"insert\",\"update\"]", "data/CrossFieldValidation:priority = 100", "data/CrossFieldValidation:severity = \"error\"", - "data/Cube:public = false", + "data/Cube:public = true", "data/CurrencyConfig:currencyMode = \"dynamic\"", "data/CurrencyConfig:defaultCurrency = \"CNY\"", "data/CurrencyConfig:precision = 2", diff --git a/packages/spec/liveness/state-counts.md b/packages/spec/liveness/state-counts.md index 585f3d78237..d206f5c1e5c 100644 --- a/packages/spec/liveness/state-counts.md +++ b/packages/spec/liveness/state-counts.md @@ -66,5 +66,5 @@ for both corollaries. | `realtime_subscription` | 0 | 0 | 0 | 6 | 0 | 6 | | `sharing_rule` | 16 | 0 | 0 | 0 | 1 | 17 | | `connector` | 29 | 0 | 0 | 44 | 1 | 74 | -| `analytics_cube` | 17 | 0 | 0 | 10 | 0 | 27 | -| **total** | **933** | **5** | **1** | **168** | **12** | **1119** | +| `analytics_cube` | 18 | 0 | 0 | 9 | 0 | 27 | +| **total** | **934** | **5** | **1** | **167** | **12** | **1119** | diff --git a/packages/spec/scripts/lib/default-changes.ts b/packages/spec/scripts/lib/default-changes.ts index bf12e690054..58b82a043a7 100644 --- a/packages/spec/scripts/lib/default-changes.ts +++ b/packages/spec/scripts/lib/default-changes.ts @@ -554,5 +554,25 @@ export const DEFAULT_CHANGES_BY_MAJOR: Readonly