Skip to content

Commit c8e9c33

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-16589-memory-driver-scope-refusal
2 parents 1c7ea04 + fe2b755 commit c8e9c33

9 files changed

Lines changed: 961 additions & 61 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/metadata": patch
3+
---
4+
5+
docs(metadata): the `@objectstack/metadata/errors` header records what the leaf entry actually loads — 4 packages, 83 modules, 2.2 MB — instead of leaving the reader to infer it is light (#15346)
6+
7+
`packages/metadata/src/errors.ts` is the leaf subpath `@objectstack/metadata/errors`, and its header explains why the subpath exists: the root entry pulls the manager, every loader and the YAML/filesystem machinery behind them, and "a consumer that wants a 40-line predicate should not have to load any of that". Measured, that promise half holds — and the header did not say which half.
8+
9+
**The half that holds, and stays:** MEASURED 2026-09-08 on `origin/main` `8c1515e847`, with `scripts/check-lean-entry-closure.mjs`'s exported `measure()`/`byPackage()` (one fresh child per published condition; module set collected through `module.registerHooks` unioned with `require.cache`). Both published conditions load exactly four packages, and the manager, every loader, `chokidar`, `glob`, `js-yaml` and `readdirp` are absent from all of them. That is the claim the header actually wrote, and it is accurate.
10+
11+
**The half that had drifted:** the general conclusion a reader takes away. `import` loads 83 modules / 2,201,709 bytes; `require` loads 83 / 2,355,405. Since the maintainer's 2026-08-30 ruling the file re-exports from `@objectstack/types`, which builds to one bundled module importing **both** `@objectstack/spec/api` (1,401,253 B) and `@objectstack/spec/security` (195,367 B) at module top, as values — `api` is the dominant edge at 7x `security`, and the two spec entries are independent of each other.
12+
13+
The header now separates the two costs the way the finding did, because conflating them is the available mistake: **in-repo the marginal contribution is 143 bytes** — the only current consumer of the subpath, `@objectstack/metadata-protocol`, declares `@objectstack/spec` itself and pays for that closure anyway — while the megabytes are what an **out-of-repo** consumer pays, which nothing here can measure. The figures are labelled PROVENANCE, dated and tree-pinned, in the manner the neighbouring gate keeps its own: nothing derives from them, nothing compares against them, and the header says to re-take them rather than quote them. They rot fast — the same measurement four days earlier at `6e67b86c0` found the same 83 modules but 2,487,842 bytes, 286,133 more, without this file changing at all.
14+
15+
**Why `patch` and not `skip-changeset`, measured rather than assumed.** `@objectstack/metadata` is released (17.3.0) and `errors.ts` is a shipped source file, so "a comment publishes nothing" needed checking rather than asserting. Building the package before and after the edit: `dist/errors.js`, `dist/errors.cjs`, `dist/errors.d.ts` and `dist/errors.d.cts` are **byte-identical** (esbuild strips the comment, and there is no `sourcesContent`), but `dist/errors.js.map` and `dist/errors.cjs.map` **change** — the export statement moved from source line 65 to line 113, so the `mappings` VLQ moves with it (`;AAgEA,…``;AAgHA,…`), at identical file size. `npm pack --dry-run` lists both maps in the tarball. So this diff does publish bytes from a released package, which is exactly what `skip-changeset` is not for. Nothing executable and no type changes: a consumer's stack traces resolve to the right source line, and that is the whole of it.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): the `field.valueDomain` liveness note stops claiming the settings door is "unchanged until then"
6+
7+
The `valueDomain` row of the published `liveness/field.json` ledger ended on a sentence written
8+
while the re-point was still in the future:
9+
10+
> The settings door (`service-settings/value-domains.ts`) re-points onto the shared predicate in
11+
> its own follow-up card and is unchanged until then.
12+
13+
Both halves of the 2026-09-02 ruling have since landed — the settings half (#15434) and the engine
14+
half (#15316) — and the engine half rewrote this note wholesale while carrying that sentence
15+
forward verbatim. "Unchanged until then" therefore described a state that no longer existed: the
16+
door it names had already re-pointed, one commit earlier.
17+
18+
The sentence now says what is true of that door, read off its source rather than off a PR title:
19+
its second copy of all three definitions is deleted, `firstRejectedDomainMember` asks
20+
`isValueDomainMember` — the same call `record-validator.ts` makes — and what remains on that side
21+
is the door's own business (which declarations it agrees to enforce, how a multi-value carrier is
22+
walked, the fragments the env-override log line needs). A re-added local table reddens
23+
`value-domains.shared-predicate.pin.test.ts`.
24+
25+
Ledger-note text only. The row's `status` is untouched — it tracks the engine write path, and
26+
`liveness/state-counts.md` is derived by `gen:liveness-counts` from the row states, none of which
27+
move here (`check:liveness` reports the counts file current).

packages/cli/src/commands/generate-string-family-width.pin.test.ts

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,44 @@
173173
* ⚠️ Scope, as `generateMigrationSql`'s docblock and the `--format` help text
174174
* already say (#15521): this is a POSTGRESQL claim and nothing else. Neither
175175
* generator reproduces the driver's dialect branching.
176+
*
177+
* ## ⭐ ...and the oracle runs on SQLite, so the PostgreSQL claim rests on a
178+
* PREMISE — now measured rather than assumed (#16394)
179+
*
180+
* `PRAGMA table_info` is the tell: the real chain below runs against an
181+
* in-memory better-sqlite3 database while every width it asserts is a
182+
* PostgreSQL claim. That is sound exactly while ONE premise holds — *the width
183+
* the chain produces does not depend on the dialect* — and nothing was holding
184+
* it. Measured as a mutation battery against this very oracle:
185+
*
186+
* ```
187+
* leg driver-side mutation this file
188+
* L7 a Postgres gate on `createColumn`'s quoted line 2 failed / 79
189+
* L7b the same gate as the FIRST LINE of all green
190+
* `keyableTextLength`
191+
* ```
192+
*
193+
* L7b changes the column a real PostgreSQL deployment gets and this file
194+
* reported everything fine; L7 reddened only because a source-text assertion
195+
* here happens to quote the line the mutation landed on — luck about placement,
196+
* not coverage. The premise is now carried in two halves:
197+
*
198+
* - HERE, in §F2's dialect-parity block: the two width bodies and the
199+
* dispatch mirror are asked on a driver configured for EACH dialect the
200+
* emitter branches on, and must answer identically. No server is needed —
201+
* `isPostgres` reads the CONFIG — so it runs wherever this file runs, and
202+
* it reddens on L7b.
203+
* - In `driver-sql`, by
204+
* `sql-driver-16394-character-width-dialect-parity.test.ts`: the same
205+
* object through the same real chain on a LIVE Postgres and MySQL, columns
206+
* read back from the server's own catalog and compared against the SQLite
207+
* ones. That half also covers `createColumn`'s DISPATCH, which asking the
208+
* bodies cannot see move. ⚠️ It lives there and not here because the
209+
* `Temporal Conformance (live PG + MySQL)` job — the only job in this
210+
* repository that provisions a live server — runs `pnpm --filter
211+
* @objectstack/driver-sql test` and nothing else, so a live cell written
212+
* into this file would be provisioned by no job and would report itself
213+
* un-run forever.
176214
*/
177215

178216
import fs from 'node:fs';
@@ -966,6 +1004,20 @@ class DriverOracle extends SqlDriver {
9661004
return this.declaredVarcharLength(field);
9671005
}
9681006

1007+
/**
1008+
* `SqlDriver.varcharColumnChars`, unmodified — the emitter's own read-only
1009+
* mirror of which arm `createColumn` sends a field to, and at what width.
1010+
*
1011+
* ⚠️ A MIRROR, not the dispatch. It is the driver's own second copy of that
1012+
* switch, so it reaches one layer past the two width bodies and still stops
1013+
* short of `createColumn` itself; the real chain is {@link createdColumns},
1014+
* and its live-dialect half is `driver-sql`'s
1015+
* `sql-driver-16394-character-width-dialect-parity.test.ts`.
1016+
*/
1017+
public varcharCharsFor(field: unknown, keyed?: { unique: boolean }): number | null {
1018+
return this.varcharColumnChars(field, keyed);
1019+
}
1020+
9691021
/**
9701022
* ⭐ THE REAL CHAIN. The columns `initObjects` actually creates for one
9711023
* object, read back out of the database it created them in.
@@ -1006,8 +1058,34 @@ const ORACLE = new DriverOracle({
10061058
useNullAsDefault: true,
10071059
});
10081060

1061+
/**
1062+
* [#16394] The SAME oracle, configured for each dialect the emitter branches on.
1063+
*
1064+
* ⭐ These are never dialled. `isPostgres` / `isMysql` are derived from
1065+
* `SqlDriver.clientSpelling(this.config)` — a read of the CONFIG, with no
1066+
* connection anywhere in it — so a dialect-configured driver answers every
1067+
* declaration-only question this file asks without a server existing. That is
1068+
* what makes the dialect axis affordable HERE, in Test Core, rather than only
1069+
* in the one job that provisions live servers.
1070+
*
1071+
* The connection strings are deliberately unreachable (port 1) and deliberately
1072+
* present: knex wants one, and a value nothing can dial is the loudest possible
1073+
* statement that nothing is meant to.
1074+
*/
1075+
const DIALECT_ORACLES: ReadonlyArray<{ dialect: string; oracle: DriverOracle }> = [
1076+
{ dialect: 'sqlite', oracle: ORACLE },
1077+
{
1078+
dialect: 'postgres',
1079+
oracle: new DriverOracle({ client: 'pg', connection: 'postgres://never:dialled@127.0.0.1:1/none' }),
1080+
},
1081+
{
1082+
dialect: 'mysql',
1083+
oracle: new DriverOracle({ client: 'mysql2', connection: 'mysql://never:dialled@127.0.0.1:1/none' }),
1084+
},
1085+
];
1086+
10091087
afterAll(async () => {
1010-
await ORACLE.disconnect();
1088+
for (const { oracle } of DIALECT_ORACLES) await oracle.disconnect();
10111089
});
10121090

10131091
/**
@@ -1626,6 +1704,97 @@ describe('#16091 — the driver is the ORACLE, not just the source text', () =>
16261704
expect(sqlWidth(columnsFor({ type: 'email', maxLength: 1000 }).sql)).toBe(1000);
16271705
});
16281706

1707+
// ── F2b: the DIALECT axis (#16394) ────────────────────────────────────────
1708+
//
1709+
// Everything above — source readers, leaf differentials and the real chain
1710+
// alike — asks ONE driver, configured for SQLite, and then asserts the answer
1711+
// as a POSTGRESQL claim. The step between the two is a premise nothing was
1712+
// holding: that the width does not depend on the dialect. It is false as soon
1713+
// as anyone writes a dialect gate into one of these bodies, and a gate inside
1714+
// `keyableTextLength` left every assertion in this file green while changing
1715+
// the column a real PostgreSQL deployment gets.
1716+
//
1717+
// ⛔ NOT a source-text guard over the width arm. "No Postgres token appears
1718+
// in these lines" reddens because a TOKEN appeared, which a rename evades and
1719+
// which is the layer this file spent four rounds leaving. What follows CALLS
1720+
// the driver's own bodies on a driver configured for each dialect and
1721+
// compares the VALUES they return, so it reddens because the width moved.
1722+
//
1723+
// ⚠️ What it does not reach: `createColumn`'s dispatch, and everything
1724+
// `initObjects` does around it, on a real Postgres. That needs a server, and
1725+
// it is measured — over this same corpus of arms — by `driver-sql`'s
1726+
// `sql-driver-16394-character-width-dialect-parity.test.ts`, which runs in
1727+
// the one job that provisions one.
1728+
1729+
it('control — the dialect oracles really are DIFFERENT dialects, unconnected', () => {
1730+
// ⛔ Non-vacuity, and the whole load-bearing half of it: if all three
1731+
// instances reported the same dialect, the parity sweep below would compare
1732+
// one driver against itself 38 times and pass while measuring nothing.
1733+
expect(DIALECT_ORACLES.map((d) => d.oracle.dialectName)).toEqual(['sqlite', 'postgres', 'mysql']);
1734+
expect(DIALECT_ORACLES.map((d) => d.dialect)).toEqual(['sqlite', 'postgres', 'mysql']);
1735+
// `dialectName` is derived from the config, so it answers with no server —
1736+
// which is what makes this block affordable outside the live job.
1737+
expect(new Set(DIALECT_ORACLES.map((d) => d.oracle.dialectName)).size).toBe(3);
1738+
// ...and every one of them really is a driver that still answers.
1739+
for (const { dialect, oracle } of DIALECT_ORACLES) {
1740+
expect(oracle, dialect).toBeInstanceOf(SqlDriver);
1741+
expect(oracle.keyableCharsFor({ maxLength: PROBE_CHARS }), dialect).toBe(PROBE_CHARS);
1742+
}
1743+
});
1744+
1745+
it('both width bodies and the emitter mirror answer the SAME on every dialect', () => {
1746+
const disagreements: string[] = [];
1747+
const answers = new Set<string>();
1748+
let compared = 0;
1749+
1750+
const record = (what: string, shown: string, per: (number | null)[]): void => {
1751+
const distinct = new Set(per.map((v) => String(v)));
1752+
answers.add([...distinct].sort().join('|'));
1753+
compared += 1;
1754+
if (distinct.size !== 1) {
1755+
disagreements.push(
1756+
`${what} @ ${shown}: ` +
1757+
DIALECT_ORACLES.map((d, i) => `${d.dialect}=${String(per[i])}`).join(' '),
1758+
);
1759+
}
1760+
};
1761+
1762+
for (const maxLength of WIDTH_DECLARATIONS) {
1763+
const shown = JSON.stringify(maxLength) ?? String(maxLength);
1764+
record('keyableTextLength', shown, DIALECT_ORACLES.map((d) => d.oracle.keyableCharsFor({ maxLength })));
1765+
record('declaredVarcharLength', shown, DIALECT_ORACLES.map((d) => d.oracle.declaredCharsFor({ maxLength })));
1766+
// The mirror, over every character TYPE and both keyednesses — so a gate
1767+
// written into the SWITCH rather than into a width body is seen too, at
1768+
// the one layer that can be reached without a server. MEMBERSHIP is the
1769+
// driver's own, read off its case labels, exactly as the sweeps above.
1770+
for (const type of [...armMembers('text'), ...armMembers('email'), ...characterCatchAllMembers()]) {
1771+
const field: Record<string, unknown> = { type };
1772+
if (maxLength !== undefined) field.maxLength = maxLength;
1773+
for (const keyed of [undefined, { unique: true }]) {
1774+
record(
1775+
`varcharColumnChars(${type}${keyed ? ' keyed' : ''})`,
1776+
shown,
1777+
DIALECT_ORACLES.map((d) => d.oracle.varcharCharsFor(field, keyed)),
1778+
);
1779+
}
1780+
}
1781+
}
1782+
1783+
// Non-vacuity: the sweep really ran over every declaration, and the driver
1784+
// really gave more than one answer across it — a body that returned one
1785+
// constant everywhere would agree with itself on every dialect.
1786+
expect(compared).toBeGreaterThan(WIDTH_DECLARATIONS.length * 2);
1787+
expect(answers.size).toBeGreaterThan(3);
1788+
1789+
expect(
1790+
disagreements.slice(0, 20),
1791+
`${disagreements.length} of ${compared} width questions are answered differently by ` +
1792+
'dialect. The generators emit ONE dialect\'s DDL, and this file reads its oracle off ' +
1793+
'another — that is only sound while the driver\'s character width is dialect-invariant. ' +
1794+
'Fix driver-sql, never this expectation.',
1795+
).toEqual([]);
1796+
});
1797+
16291798
it('every character TYPE, at every declaration, takes the width initObjects CREATES', async () => {
16301799
// ⭐ The width half of F1b, and it reaches one layer the two cases above
16311800
// cannot: `createColumn`'s DISPATCH. `ORACLE.keyableCharsFor(...)` answers

0 commit comments

Comments
 (0)