Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .changeset/service-analytics-typecheck-gate.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
---
"@objectstack/service-analytics": patch
---

fix(service-analytics): wire the `typecheck` script so turbo stops silently no-opping the gate, and clear the 10 type errors it was hiding (#12939)

`packages/services/service-analytics/package.json` declared only `build` and
`test`. Root `typecheck` is `turbo run typecheck`, which **no-ops a package
that has no such script and reports success** — so no tsc read this package's
`src/` from the typecheck lane at all. `build` is tsup (esbuild; the DTS pass
processes declarations only) and `test` is vitest (esbuild transform), and
neither type-checks. The package was reached only by the `check:type-check-debt`
ratchet, which asserts the error count does not *grow* — never that it is zero.

Adding the one-line script (mirroring its sibling `service-settings`, repaired
the same way in #7925) makes the task real. The tests are already inside the
program — the package `tsconfig.json` includes `src` and the tests live in
`src/__tests__/**` — so `tsc --noEmit --listFiles` lists **83 of the 83**
`*.test.ts` files on disk. The new gate reads the tests, not just the source.

All 10 errors were stale tests, not source defects; no non-test source file
changed. Nothing was silenced: no `any` added, no `@ts-expect-error`, no
`@ts-nocheck`, `strict` untouched, and the tsconfig `include`/`exclude` are
byte-identical — excluding the tests would have converted a missing gate into
a lying one.

- `__tests__/measure-source-field-gate.test.ts` (7 x TS2339). `promise.catch(fn)`
does not drop the resolved branch from the type, so
`service.query(...).catch((e) => e as Error)` was `AnalyticsResult | Error`
and every `err.message` / `err.field` / `err.member` / `err.param` read was a
property access on `AnalyticsResult`. A local `refusalOf()` helper narrows it
once via `then<never, Refusal>`; as a bonus the resolved branch now fails by
name instead of surfacing later as `expect(undefined).toMatch(...)`.
- `__tests__/objectql-timedimension-projection.test.ts` (2 x TS7053). The
`TABLE` fixture was inferred as `{ id: number; due_date: string; priority:
string }[]` and the aggregate stand-in indexes it by a computed `string` key.
Annotated as the `Row` (`Record<string, unknown>`) the file already declares.
- `__tests__/analytics-service.test.ts` (1 x TS6133). An unused
`AnalyticsDriverCapabilities` type import. The capability literals in this
file are inline `ctx` objects checked contextually at each `canHandle` call
site, so the import added no coverage and is removed.

`service-analytics` graduates out of the `check:type-check-coverage` DEBT
ledger: 65/78 -> 66/78 workspace packages type-checked, 382 -> 372 frozen raw
errors, 13 -> 12 ledger entries.
1 change: 1 addition & 0 deletions packages/services/service-analytics/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@
},
"scripts": {
"build": "tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,6 @@ import { AnalyticsService } from '../analytics-service.js';
import { CubeRegistry } from '../cube-registry.js';
import { NativeSQLStrategy } from '../strategies/native-sql-strategy.js';
import { ObjectQLStrategy } from '../strategies/objectql-strategy.js';
import type { AnalyticsDriverCapabilities } from '../strategies/types.js';

// ─────────────────────────────────────────────────────────────────
// Test fixtures
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,27 @@ import { describe, it, expect, vi } from 'vitest';
import type { Cube } from '@objectstack/spec/data';
import { AnalyticsService } from '../analytics-service.js';

/**
* The refusal a query produced, typed as the Error it actually is.
*
* `promise.catch(fn)` does NOT drop the resolved branch from the type, so
* `service.query(...).catch((e) => e as Error)` is `AnalyticsResult | Error`
* and every `err.message` / `err.field` read below was a TS2339 against
* `AnalyticsResult` -- 7 of the 10 errors this package's unwired `typecheck`
* script hid. Narrowing once here rather than casting at each read also gives
* the resolved branch an honest failure: a query that is NOT refused now says
* so by name, instead of surfacing later as `expect(undefined).toMatch(...)`.
*/
type Refusal = Error & { code?: string; field?: string; member?: string; param?: string };

const refusalOf = (query: Promise<unknown>): Promise<Refusal> =>
query.then<never, Refusal>(
() => {
throw new Error('expected the query to be refused, but it resolved');
},
(e) => e as Refusal,
);

const silentLogger = {
info: vi.fn(),
debug: vi.fn(),
Expand DownExpand Up@@ -107,9 +128,9 @@ describe('#4437 — measure source-field gate', () => {
// alternative guaranteed not to work.
const { service } = makeService();

const err = await service
.query({ cube: 'showcase_invoice', measures: ['ghost_sum'] } as any)
.catch((e) => e as Error);
const err = await refusalOf(
service.query({ cube: 'showcase_invoice', measures: ['ghost_sum'] } as any),
);

expect(err.message).toMatch(/Valid measures: count\./);
expect(err.message).not.toMatch(/Valid measures:[^.]*ghost_sum/);
Expand DownExpand Up@@ -268,9 +289,9 @@ describe('#4437 — measure source-field gate', () => {
};
const { service } = makeService({ cubes: [joined] });

const err = await service
.query({ cube: 'joined_cube', measures: ['remote_sum'] } as any)
.catch((e) => e as Error & { code?: string; field?: string; member?: string; param?: string });
const err = await refusalOf(
service.query({ cube: 'joined_cube', measures: ['remote_sum'] } as any),
);

expect(err).toBeInstanceOf(Error);
// It got as far as the strategy — i.e. past this gate — and was declined
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,7 +41,7 @@ const dataset = DatasetSchema.parse({
measures: [{ name: 'count', aggregate: 'count', field: 'id' }],
});

const TABLE = [
const TABLE: Row[] = [
{ id: 1, due_date: '2026-01-10', priority: 'high' },
{ id: 2, due_date: '2026-01-20', priority: 'low' },
{ id: 3, due_date: '2026-02-05', priority: 'high' },
Expand Down
10 changes: 0 additions & 10 deletions scripts/check-type-check-coverage.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -588,16 +588,6 @@ const DEBT = {
errors: 11,
note: 'all code-tier (TS2554 wrong arity x10, TS2552).',
},
'@objectstack/service-analytics': {
errors: 10,
note: 'code-tier 9 (TS2339 x7, TS7053 x2) + 1 noise (TS6133). Re-measured 10 at e8db1a230, up from 7 '
+ 'at 5ab08428 and 3 before that. All 7 TS2339 sit in __tests__/measure-source-field-gate.test.ts, '
+ 'the same file that carried 4 of them when this entry was last written; the +3 arrived with #5716 '
+ '/ PR #5963 rewriting that gate\'s refusals -- no new file, no new error class. This entry is the '
+ 'standing specimen for why the ERROR-COUNT layer needed a ratchet of its own: the PACKAGE layer '
+ 'of this gate has been closed to new debt the whole time, and the count still walked 3 -> 7 -> 10 '
+ 'unremarked (#5278).',
},
'@objectstack/service-automation': {
errors: 3,
note: 'code-tier 3 (TS2341 x3), all in src/nested-region-parity.test.ts at 95/151/180, where the '
Expand Down
Loading