Skip to content

Commit cec7d05

Browse files
authored
Merge branch 'main' into claude/issue-10268-release-section-coverage
2 parents 2690cd2 + a7ea328 commit cec7d05

21 files changed

Lines changed: 2155 additions & 184 deletions
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os doctor` no longer prints `✓ Test coverage` / `✓ Deprecations` about a tree it
6+
never examined, and no longer warns `@objectstack/spec Not built` about a
7+
workspace that is not part of the tree (#10679).
8+
9+
`findMissingTests()` and `findDeprecatedUsages()` both walk
10+
`<cwd>/packages/spec/src` — a path that exists in this monorepo and in no
11+
application built with the framework. Both answered "that directory is not here"
12+
with the same value they return for "I walked it and found nothing wrong" (an
13+
empty array), so in a stock `create-objectstack -t blank` scaffold every run
14+
printed, verbatim:
15+
16+
```
17+
✓ Test coverage All *.zod.ts files have matching tests
18+
✓ Deprecations No @deprecated tags found
19+
```
20+
21+
about files doctor never opened. The command exits 0 either way, so "no problems
22+
found" and "I never looked" were byte-identical to every downstream reader.
23+
24+
Doctor already refuses to do this one screen down: the ADR-0120 D5e advisory's
25+
`✓ Unique scope` is withheld unless `ledgerReadingIsComplete()` says the ledger
26+
half was read in full. These two checks escaped that discipline; this restores
27+
it, in the same shape #5413 used for the ledger — whether the tree was examined
28+
is now a fact in the return type rather than an absence, so the print site
29+
cannot reach the `` from the unexamined arm. Where the tree is absent doctor
30+
prints an informational, named-reason skip instead:
31+
32+
```
33+
ℹ Test coverage Skipped — no packages/spec/src in this directory (monorepo-only check)
34+
ℹ Deprecations Skipped — no packages/spec/src in this directory (monorepo-only check)
35+
```
36+
37+
`--verbose` adds the resolved directory it looked for. The skip is deliberately
38+
not a warning: nothing is wrong in an application that has no
39+
`packages/spec/src`, and withholding a false `` must not manufacture a false
40+
``.
41+
42+
The adjacent `⚠ @objectstack/spec Not built` probe read `<cwd>/packages/spec/dist`
43+
with no check that the workspace it names exists, so in an application it warned
44+
about an absent package and prescribed `pnpm --filter @objectstack/spec build`, a
45+
command that cannot succeed there. It is now gated on `packages/spec/package.json`
46+
being present. Inside the monorepo the row is unchanged; outside it there is no
47+
row, and an application's spec dependency stays covered by the `Dependencies`
48+
check and by the spec-version-gap advisory.
49+
50+
Exit codes are untouched — 1 exactly when an error row exists, warnings never
51+
flip it. One visible consequence: a stock scaffold with no other findings now
52+
ends on `✅ Environment is healthy and ready for development!` instead of
53+
`⚠️ Environment is functional but has some warnings`, because the warning it
54+
used to carry was about a workspace that was never there.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@objectstack/service-analytics": patch
3+
---
4+
5+
Apply a dataset's definition-level `filter` on the ObjectQL analytics path
6+
(#10413, phase 1). `/api/v1/analytics/query` served by a driver that reports
7+
`objectqlAggregate` but not `nativeSql` (MongoDB, the memory driver) reached
8+
`engine.aggregate` with no `filter` key at all: the dataset's own scope — a
9+
`filter: { is_deleted: false }` on the dataset definition — was dropped, so
10+
every measure aggregated the whole table while the dashboard door, on the same
11+
cube and the same measure names, answered the scoped numbers. The scope is now
12+
ANDed into the strategy's whole-call filter (never merged key-by-key, so a
13+
caller's own `where` and the time windows cannot be overwritten by it), and the
14+
representative SQL echo renders it too.
15+
16+
Per-MEASURE `filter`s on this path are still not applied: an
17+
`engine.aggregate` aggregation is `{ field, method, alias }` and cannot carry a
18+
predicate of its own. Widening that contract is #10576; lowering the measure
19+
filters into it is phase 2 of #10413. The native-SQL path already applies both
20+
(#10298).

apps/docs/next.config.mjs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,28 @@ const withMDX = createMDX();
66
/** @type {import('next').NextConfig} */
77
const config = {
88
reactStrictMode: true,
9+
// `next dev` otherwise writes `apps/docs/AGENTS.md` and `apps/docs/CLAUDE.md`
10+
// into the working tree whenever it detects an AI coding agent (Next 16
11+
// `ensureAgentRulesForDev`). Neither path is tracked or ignored, so both land
12+
// as `??` and any `git add -A` after booting the docs server sweeps them into
13+
// an unrelated PR. Those two filenames are load-bearing in this repo: the
14+
// root `AGENTS.md` is the binding playbook and agents are told to read the
15+
// `AGENTS.md` governing the files they touch, so a Next-authored stub
16+
// pointing at `node_modules/next/dist/docs/` can be read as this repo's
17+
// discipline -- and the generated text argues for committing itself, which
18+
// nudges an agent the wrong way. The generated `CLAUDE.md` is the single
19+
// line `@AGENTS.md`, an import directive that pulls the stub in.
20+
//
21+
// Disabled at the source rather than gitignored on purpose: an ignore rule
22+
// for `apps/docs/AGENTS.md` would silently swallow a future repo-authored
23+
// `AGENTS.md` for this app, failing invisibly at exactly the moment someone
24+
// was trying to add real guidance there. A visible untracked file is a better
25+
// failure than an invisibly ignored one for this filename specifically.
26+
//
27+
// This only stops future generation: a checkout that already ran `next dev`
28+
// keeps its copies until they are removed once by hand
29+
// (`rm -f apps/docs/AGENTS.md apps/docs/CLAUDE.md`).
30+
agentRules: false,
931
// No `output: 'standalone'` here on purpose. The standalone packer
1032
// (`writeStandaloneDirectory` -> `copyTracedFiles`) opens
1133
// `.next/next-server.js.nft.json`, and that open is the ONLY thing in a

examples/app-showcase/test/approval-resume-relation-expand.test.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,20 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
6868
const openKernels: Array<{ shutdown?: () => Promise<void> }> = [];
6969
const openDrivers: Array<{ disconnect?: () => Promise<void> }> = [];
7070
afterEach(async () => {
71-
while (openDrivers.length) {
72-
try { await openDrivers.pop()?.disconnect?.(); } catch { /* noop */ }
73-
}
71+
// Kernels first, drivers second: the kernel's own teardown still wants a
72+
// live driver to drain against -- that is the rule, regardless of what it
73+
// costs on any given day.
74+
//
75+
// Measured here (#10373): this file's own DATABASE_ERROR lines are all
76+
// "no such table" probes against sys_* tables bootShowcaseApprovals()
77+
// never provisions, not post-disconnect reads -- swapping the order left
78+
// the count unchanged (36 suite-wide / 31 in this file, before and after).
7479
while (openKernels.length) {
7580
try { await openKernels.pop()?.shutdown?.(); } catch { /* noop */ }
7681
}
82+
while (openDrivers.length) {
83+
try { await openDrivers.pop()?.disconnect?.(); } catch { /* noop */ }
84+
}
7785
});
7886

7987
interface Booted {

examples/embed-objectql/src/index.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
// shape you would ship in a `*.object.ts` to a full ObjectStack backend. One
1414
// object model, two hosts; only the installed capability set differs.
1515

16+
import { realpathSync } from 'node:fs';
17+
import { join, resolve } from 'node:path';
18+
import { fileURLToPath } from 'node:url';
1619
import { ObjectQL } from '@objectstack/objectql/core';
1720
import { InMemoryDriver } from '@objectstack/driver-memory';
1821
import { ObjectSchema, Field, type ServiceObject } from '@objectstack/spec/data';
@@ -56,8 +59,41 @@ export async function runEmbeddedEngine(): Promise<AccountRow[]> {
5659
}) as Promise<AccountRow[]>;
5760
}
5861

59-
// Allow `node`/`tsx`-style direct execution to print the result.
60-
if (import.meta.url === `file://${process.argv[1]}`) {
62+
// ─── entry guard ───────────────────────────────────────────────────────
63+
// ⛔ NOT ``import.meta.url === `file://${process.argv[1]}` ``. Node symlink-resolves
64+
// `import.meta.url` but leaves `process.argv[1]` exactly as the caller typed it, and
65+
// the template also skips the percent-encoding `pathToFileURL` applies — so that
66+
// spelling goes INERT (exit 0, no output) through a symlink AND on any checkout path
67+
// containing a character that needs encoding (a `#` in a parent directory name is
68+
// enough, with no symlink involved). Compare RESOLVED PATHS, never URL strings.
69+
//
70+
// Same predicate as `packages/cli/src/utils/invocation.ts` (`isProcessEntry`) and
71+
// `scripts/invoked-as.mjs` (`invokedAs`). Spelled out rather than imported because
72+
// neither home is legally reachable from this file — the PR for #10269 carries the
73+
// boundary measurement. ⚠️ Two predicates answering this question differently IS the
74+
// defect this closes; change one, change all of them.
75+
function isProcessEntry(): boolean {
76+
const entryArg = process.argv[1];
77+
if (!entryArg) return false; // `node --eval` / the REPL
78+
const self = resolve(fileURLToPath(import.meta.url));
79+
const entry = resolve(entryArg);
80+
// `node <dir>` gives the ENTRY ARGUMENT, and only it, directory resolution.
81+
const candidates = [entry, join(entry, 'index.js'), join(entry, 'index.mjs'), join(entry, 'index.ts')];
82+
if (candidates.includes(self)) return true;
83+
const realSelf = realOrSelf(self);
84+
return candidates.some((candidate) => realOrSelf(candidate) === realSelf);
85+
}
86+
87+
/** `realpathSync`, degrading to the input for a path that cannot be read. */
88+
function realOrSelf(p: string): string {
89+
try {
90+
return realpathSync(p);
91+
} catch {
92+
return p;
93+
}
94+
}
95+
96+
if (isProcessEntry()) {
6197
runEmbeddedEngine()
6298
.then((rows) => {
6399
// eslint-disable-next-line no-console

packages/cli/src/commands/doctor-env-provenance.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,18 @@ describe('os doctor, end to end, against a posture that only exists in .env', ()
447447
const healthy = await runDoctor();
448448

449449
expect(healthy.exitCode).toBeUndefined();
450-
expect(healthy.out).toContain('Environment is functional');
450+
// #10679 — this used to read `toContain('Environment is functional')`, and
451+
// it passed for a reason that had nothing to do with #5387: the temp cwd
452+
// has no `packages/spec`, and doctor warned `@objectstack/spec Not built`
453+
// about that absent workspace on every run. Removing that phantom warning
454+
// leaves this cwd with no findings at all, so the summary is now the
455+
// healthy one. What the control actually claims — doctor reached its
456+
// summary and did NOT refuse to call this environment usable — is what the
457+
// matcher says instead, and it still cannot pass for the broken leg below
458+
// (that one prints `Some critical issues found`).
459+
expect(healthy.out).toMatch(
460+
/Environment is (healthy and ready for development|functional but has some warnings)/,
461+
);
451462
expect(healthy.out).not.toContain('Tenancy posture');
452463
// The report says what it read even when everything is fine — that is the
453464
// "not a silent merge" half, and it is only observable on a healthy run.

packages/cli/src/commands/doctor-tenancy-posture-report.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,19 @@ describe('os doctor reports an unrecognized posture and exits non-zero', () => {
295295

296296
// Doctor completes normally. This is the sentence #5382 quoted, and here it
297297
// is CORRECT: this environment really can start.
298+
//
299+
// #10679 — the matcher accepts either non-error summary. The control used
300+
// to pin `Environment is functional but has some warnings` literally, and
301+
// it held only because the temp cwd has no `packages/spec` and doctor
302+
// warned `@objectstack/spec Not built` about that absent workspace every
303+
// time. With that phantom warning gone this cwd has no findings, so the
304+
// summary is the healthy one. Either sentence proves the control's actual
305+
// claim; neither can be produced by the broken leg below, which prints
306+
// `Some critical issues found` and exits 1.
298307
expect(healthy.exitCode).toBeUndefined();
299-
expect(healthy.out).toContain('Environment is functional');
308+
expect(healthy.out).toMatch(
309+
/Environment is (healthy and ready for development|functional but has some warnings)/,
310+
);
300311
expect(healthy.out).not.toContain('Tenancy posture');
301312

302313
// ── The case: one character changed ──────────────────────────────────

0 commit comments

Comments
 (0)