Skip to content

Commit 8c254ff

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-nsypdd
2 parents 56ccd0c + c61f75d commit 8c254ff

8 files changed

Lines changed: 362 additions & 28 deletions

‎.changeset/console-b41ed7f68956.md‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/console": patch
3+
---
4+
5+
Console (objectui) refreshed to `b41ed7f68956`. Frontend changes in this range:
6+
7+
Derived from the changesets objectui declared over the range — 9 releasing of 10 changesets added across 15 non-merge commits; omitted: 1 release-nothing changeset, 6 commits carrying no changeset (they ship no package code).
8+
9+
- **minor** — The Console's first page load no longer downloads the chat UI (objectui#13410). The `plugin-chatbot` chunk (169.6 KB gzipped, the third-largest eager chunk) and most of `vendor-ma… (objectui `47138b718`)
10+
- **patch** — Clause-②: yes Metadata-protocol: none (objectui `b41ed7f68`)
11+
- **patch** — Clause-②: no Metadata-protocol: none (objectui `cd70b7818`)
12+
- **patch** — Clause-②: no Metadata-protocol: none (objectui `cd70b7818`)
13+
- **patch** — The `@objectstack/*` pin moves from `915e940499` to objectstack `e649c2d91e`, the 18.1.0 release commit (objectstack#25606), past objectstack#25483 and objectstack#25564, which sh… (objectui `0984c77e1`)
14+
- **patch** — A `type: 'form'` action fired inside an app opens its form inside that app, headed by a label, with a Cancel (objectui#13433). (objectui `576fd257d`)
15+
- **patch** — A create form no longer locks fields through `readonlyWhen`. The server does not apply `readonlyWhen` on insert, but the form evaluated it against the seeded defaults, so a predic… (objectui `a78608f38`)
16+
- **patch** — `html` and `richtext` values are now sanitized with `rehype-sanitize`, the sanitizer the `markdown` pipeline already used, instead of a hand-written pattern filter. (objectui `f3044f708`)
17+
- **patch** — Settings: a setting's `visible` predicate is evaluated with the renderer's expression interpreter, and a predicate that does not evaluate hides the setting. (objectui `06ce712fc`)
18+
19+
**In this console build, declared nowhere** — objectui merged 6 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387.
20+
21+
- _(no changeset)_ chore(test): refresh the per-file test timings (objectui#12208) (#13028) (objectui `723776b5b`)
22+
- _(no changeset)_ chore: release packages (#13289) (objectui `a3c0da922`)
23+
- _(no changeset)_ ci: pin credential-holding third-party actions by sha and default ci.yml and check-links.yml to a read-only token (#13430) (objectui `798138418`)
24+
- _(no changeset)_ fix(ci): the affected-tests fast path runs: the shard step compares the output value, not the selector's key=value line (objectui#13350) (#13358) (objectui `861fdff76`)
25+
- _(no changeset)_ ci(test-shard-timings-refresh): push and open the refresh pull request as the fleet App so it runs CI (#13362) (objectui `cf2419005`)
26+
- _(no changeset)_ ci(repo-gates): restore the build's turbo cache, save and prune on push only (#13360) (objectui `3688e5cd4`)
27+
28+
Metadata-protocol: none (the console refresh changes no key, value or type a metadata author writes)
29+
30+
objectui range: `2958f17e9c6a...b41ed7f68956`

‎.objectui-sha‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2958f17e9c6add35636bb07aa9da4b1dcb1daa5f
1+
b41ed7f68956c8cfda21bc790548361af81b09ac

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"description": "ObjectStack Protocol & Specification - Monorepo for TypeScript Interfaces, JSON Schemas, and Convention Configurations",
66
"scripts": {
7-
"build": "turbo run build --filter=!@objectstack/docs",
7+
"build": "turbo run build --filter=!@objectstack/docs --filter=!./ui*/apps/site",
88
"dev": "node scripts/check-dev-prereqs.mjs && pnpm check:console-sha && pnpm --filter @objectstack/example-showcase dev",
99
"dev:showcase": "node scripts/check-dev-prereqs.mjs && pnpm check:console-sha && pnpm --filter @objectstack/example-showcase dev",
1010
"dev:crm": "node scripts/check-dev-prereqs.mjs && pnpm check:console-sha && pnpm --filter @objectstack/example-crm dev",

‎packages/sdui-parser/objectui-lockstep.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
],
77
"objectui": {
88
"repo": "https://github.com/objectstack-ai/objectui.git",
9-
"rev": "2958f17e9c6add35636bb07aa9da4b1dcb1daa5f",
10-
"revDate": "2026-10-23T22:00:30+00:00",
9+
"rev": "b41ed7f68956c8cfda21bc790548361af81b09ac",
10+
"revDate": "2026-10-25T01:50:02+00:00",
1111
"source": "packages/sdui-parser/src",
1212
"files": [
1313
"packages/sdui-parser/src/body-dialect.ts",
@@ -21,7 +21,7 @@
2121
"packages/sdui-parser/src/validate.ts"
2222
]
2323
},
24-
"recordedAgainstPin": "2958f17e9c6add35636bb07aa9da4b1dcb1daa5f",
24+
"recordedAgainstPin": "b41ed7f68956c8cfda21bc790548361af81b09ac",
2525
"grammarRegion": {
2626
"file": "packages/sdui-parser/src/parse.ts",
2727
"delimiter": "/* ---------------------- the JS literal subset (#6614) ---------------------- */",

‎scripts/check-published-readme-exports.mjs‎

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2217,8 +2217,44 @@ function prerequisiteNotMetText(message) {
22172217
* result depends on whether it was passed. `root` is the self-test's
22182218
* scratch workspace and defaults to the repo — see `publishedDocs`.
22192219
*/
2220+
/**
2221+
* The directory objectui's tree is imported under (objectstack#25677), as a
2222+
* bare segment: a literal with no path separator is no watch hint, so this
2223+
* declares no population (see DECLARED_WORKSPACE_POPULATION).
2224+
*/
2225+
const UI_TREE = 'ui';
2226+
2227+
/** Is a repo-relative POSIX path the `ui/` tree or inside it? */
2228+
export function inUiTree(rel) {
2229+
return rel === UI_TREE || rel.startsWith(`${UI_TREE}/`);
2230+
}
2231+
2232+
/**
2233+
* This gate's population with the `ui/` tree taken out, by PATH: every member
2234+
* whose directory is `ui` or under it, and every document it publishes.
2235+
*
2236+
* Those packages' README claims are objectui's own gate's to judge
2237+
* (`ui/scripts/check-readme-exports.mjs`, which objectui runs in its
2238+
* `repo-gates.yml` after a full build), and their built type entries are not
2239+
* in the closure this gate's lane builds -- so left
2240+
* in, they turn every run on the merged tree into PREREQUISITE NOT MET over
2241+
* packages it was never asked about, and the objectstack READMEs go unjudged
2242+
* with them. By path and not by `@object-ui/` scope: the `ui/` tree is the unit
2243+
* the import defined, and a scope test would miss a member of it with another
2244+
* name. A tree with no `ui/` directory comes back unchanged.
2245+
*
2246+
* An objectstack README that imports a `ui/` member now reads it as a
2247+
* non-member, the same as `react`: not this repo's own scope, so skipped.
2248+
*/
2249+
export function outsideUiTree({ byName, docs }) {
2250+
return {
2251+
byName: new Map([...byName].filter(([, member]) => !inUiTree(member.dir))),
2252+
docs: docs.filter((doc) => !inUiTree(doc.file)),
2253+
};
2254+
}
2255+
22202256
function run({ unreadReport: wantsUnreadReport = false, root = ROOT } = {}) {
2221-
const { byName, docs } = publishedDocs(SELF, { root });
2257+
const { byName, docs } = outsideUiTree(publishedDocs(SELF, { root }));
22222258

22232259
// Pass 1: which workspace type entries do the READMEs actually reach?
22242260
const { importStatements, ownScope, unresolvable, targets: reached } = reachedTargets(
@@ -2497,11 +2533,12 @@ const SELF_TEST_BATTERIES = Object.freeze({
24972533
'THE FOURTH STATE ON THAT AXIS (#10417)': 4,
24982534
'THE UNBUILT-CLOSURE PREDICATE, and the two states it excludes (#15600)': 5,
24992535
'THE EXIT-CODE SPLIT, END TO END on a scratch workspace (#15600)': 8,
2536+
'THE `ui/` TREE IS OUT OF THIS POPULATION, BY PATH (#25697)': 5,
25002537
});
25012538

25022539
// DELETING an entry silences that battery's floor exactly as effectively as
25032540
// zeroing it, so the roster's own size is pinned too.
2504-
const SELF_TEST_BATTERY_FLOOR = 26;
2541+
const SELF_TEST_BATTERY_FLOOR = 27;
25052542

25062543
// The key an assertion is filed under when no battery is open. It is not a
25072544
// declared battery, so it reds by the same set difference rather than silently
@@ -4298,6 +4335,91 @@ function selfTest() {
42984335
rmSync(fixtureRoot, { recursive: true, force: true });
42994336
}
43004337

4338+
// -- THE `ui/` TREE IS OUT OF THIS POPULATION, BY PATH ---------------------
4339+
//
4340+
// Driven through the production `run` over a scratch workspace, like the
4341+
// split above. The `ui/` member is UNBUILT on purpose: that is the merged
4342+
// tree's state in this gate's lane, and left in the population it refuses the
4343+
// whole run. The control puts the same unbuilt member outside `ui/` and must
4344+
// still refuse, so the green run is the exclusion and not a fixture that
4345+
// never reached the refusal.
4346+
battery('THE `ui/` TREE IS OUT OF THIS POPULATION, BY PATH (#25697)');
4347+
eq(
4348+
'inUiTree — the tree and its descendants, by path segment',
4349+
['ui', 'ui/packages/types', 'ui/README.md', 'uikit/x', 'packages/ui', 'packages/ui/README.md'].map(inUiTree),
4350+
[true, true, true, false, false, false],
4351+
);
4352+
eq(
4353+
'outsideUiTree — drops the members under `ui/` and the documents they publish, keeps the rest',
4354+
(() => {
4355+
const kept = outsideUiTree({
4356+
byName: new Map([
4357+
['@objectstack/a', { dir: 'packages/a' }],
4358+
['@object-ui/b', { dir: 'ui/packages/b' }],
4359+
['object-ui-root', { dir: 'ui' }],
4360+
['@objectstack/ui-kit', { dir: 'packages/ui' }],
4361+
]),
4362+
docs: [{ file: 'packages/a/README.md' }, { file: 'ui/packages/b/README.md' }, { file: 'ui/README.md' }],
4363+
});
4364+
return [[...kept.byName.keys()], kept.docs.map((d) => d.file)];
4365+
})(),
4366+
[['@objectstack/a', '@objectstack/ui-kit'], ['packages/a/README.md']],
4367+
);
4368+
const uiRoot = mkdtempSync(join(tmpdir(), 'readme-exports-ui-tree-'));
4369+
try {
4370+
const member = (rel, name, built) => {
4371+
const dir = join(uiRoot, rel);
4372+
mkdirSync(join(dir, 'dist'), { recursive: true });
4373+
writeFileSync(
4374+
join(dir, 'package.json'),
4375+
`${JSON.stringify(
4376+
{
4377+
name,
4378+
version: '0.0.0',
4379+
files: ['dist', 'README.md'],
4380+
exports: { '.': { types: './dist/index.d.ts', import: './dist/index.js' } },
4381+
},
4382+
null,
4383+
2,
4384+
)}\n`,
4385+
);
4386+
writeFileSync(join(dir, 'README.md'), ['```typescript', `import { Documented } from '${name}';`, '```', ''].join('\n'));
4387+
if (built) writeFileSync(join(dir, 'dist', 'index.d.ts'), 'export declare const Documented: string;\n');
4388+
};
4389+
mkdirSync(join(uiRoot, 'scripts'), { recursive: true });
4390+
writeFileSync(join(uiRoot, BASELINE_REL), `${JSON.stringify({ entries: [] }, null, 2)}\n`);
4391+
member('packages/fixture', '@objectstack/ui-tree-fixture', true);
4392+
const drive = (globs) => {
4393+
writeFileSync(join(uiRoot, WORKSPACE_FILE), `packages:\n${globs.map((g) => ` - ${g}\n`).join('')}`);
4394+
const printed = [];
4395+
const { log, error } = console;
4396+
console.log = (...args) => printed.push(args.join(' '));
4397+
console.error = (...args) => printed.push(args.join(' '));
4398+
try {
4399+
return { exit: run({ root: uiRoot }), text: printed.join('\n') };
4400+
} finally {
4401+
console.log = log;
4402+
console.error = error;
4403+
}
4404+
};
4405+
4406+
member('ui/packages/unbuilt', '@object-ui/ui-tree-unbuilt', false);
4407+
const merged = drive(['packages/*', 'ui/packages/*']);
4408+
eq('END TO END — an unbuilt member under `ui/` does not refuse the run', merged.exit, EXIT_OK);
4409+
eq('END TO END — and the run does not name it', /ui-tree-unbuilt/.test(merged.text), false);
4410+
4411+
member('packages/unbuilt', '@object-ui/ui-tree-unbuilt', false);
4412+
rmSync(join(uiRoot, 'ui'), { recursive: true, force: true });
4413+
const control = drive(['packages/*']);
4414+
eq(
4415+
'END TO END — CONTROL: the same unbuilt member outside `ui/` still refuses, naming it',
4416+
[control.exit, /@object-ui\/ui-tree-unbuilt -> packages\/unbuilt\/dist\/index\.d\.ts/.test(control.text)],
4417+
[EXIT_PREREQUISITE_NOT_MET, true],
4418+
);
4419+
} finally {
4420+
rmSync(uiRoot, { recursive: true, force: true });
4421+
}
4422+
43014423
// The authority convention (#8435): the baseline path must never be offered
43024424
// as a co-equal author remedy.
43034425
const remedy = freshRemedy();

‎scripts/check-type-check-coverage.mjs‎

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,9 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync,
528528
import { tmpdir, totalmem } from 'node:os';
529529
import { join, posix, resolve } from 'node:path';
530530
import { getHeapStatistics } from 'node:v8';
531+
import ts from 'typescript';
531532
import { gitFreeEnv } from './git-env.mjs';
533+
import { maskComments } from './js-comment-mask.mjs';
532534
import {
533535
selfTest as workspaceEnumeratorSelfTest,
534536
workspaceEnumeratorFloorFailures,
@@ -1623,14 +1625,38 @@ function includeRoot(glob) {
16231625
return glob.split('*')[0].replace(/\/$/, '');
16241626
}
16251627

1628+
/**
1629+
* A tsconfig's text read the way tsc reads it: JSON with comments, line AND
1630+
* block, and trailing commas. `JSON.parse` over the text with whole-line `//`
1631+
* comments stripped was the reader here, and it refused every config tsc
1632+
* accepts with a block comment or a trailing comma -- objectui's root
1633+
* `tsconfig.json` and three of its package configs are written that way, so
1634+
* the gate stopped on the first of them the moment `ui/` became a workspace
1635+
* member (objectstack#25697).
1636+
*
1637+
* TypeScript's own parser is the authority, not a second JSONC dialect typed
1638+
* here: what tsc compiles is what this gate claims to describe. Its refusals
1639+
* stay refusals -- a syntax error, a merge-conflict marker, a root that is not
1640+
* an object -- and so does a document holding no value at all, which the
1641+
* parser reads as `{}` and the old reader refused: an empty config read as
1642+
* "include everything, exclude nothing" is the silent pass this gate exists to
1643+
* refuse.
1644+
*
1645+
* @returns {{ config: object } | { error: string }}
1646+
*/
1647+
function parseTsconfigText(text, fileName) {
1648+
if (maskComments(text).trim() === '') return { error: 'the file holds no JSON value' };
1649+
const { config, error } = ts.parseConfigFileTextToJson(fileName, text);
1650+
if (error) return { error: ts.flattenDiagnosticMessageText(error.messageText, '\n') };
1651+
return { config };
1652+
}
1653+
16261654
function readTsconfig(dir, file) {
1627-
const raw = readFileSync(join(ROOT, dir, file), 'utf8').replace(/^\s*\/\/.*$/gm, '');
1628-
let parsed;
1629-
try {
1630-
parsed = JSON.parse(raw);
1631-
} catch (cause) {
1632-
throw new Error(`${dir}/${file} is not parseable, so its test coverage cannot be judged`, { cause });
1655+
const read = parseTsconfigText(readFileSync(join(ROOT, dir, file), 'utf8'), `${dir}/${file}`);
1656+
if (read.error !== undefined) {
1657+
throw new Error(`${dir}/${file} is not parseable, so its test coverage cannot be judged: ${read.error}`);
16331658
}
1659+
const parsed = read.config;
16341660
const include = parsed.include ?? null;
16351661
const declared = Array.isArray(include) ? include.filter((g) => typeof g === 'string') : [];
16361662
const roots = declared.length > 0
@@ -4169,12 +4195,15 @@ function tsconfigChainFacts(dir) {
41694195
let configAbs = join(ROOT, dir, 'tsconfig.json');
41704196
const facts = { selectsFiles: false, declaresTypeRoots: false };
41714197
for (let depth = 0; depth < 8; depth++) {
4172-
let parsed;
4198+
let text;
41734199
try {
4174-
parsed = JSON.parse(readFileSync(configAbs, 'utf8').replace(/^\s*\/\/.*$/gm, ''));
4200+
text = readFileSync(configAbs, 'utf8');
41754201
} catch {
41764202
return facts;
41774203
}
4204+
const read = parseTsconfigText(text, configAbs);
4205+
if (read.error !== undefined) return facts;
4206+
const parsed = read.config;
41784207
if (Array.isArray(parsed.include) && parsed.include.length > 0) facts.selectsFiles = true;
41794208
if (Array.isArray(parsed.files)) facts.selectsFiles = true;
41804209
if (parsed.compilerOptions?.typeRoots !== undefined) facts.declaresTypeRoots = true;
@@ -4348,10 +4377,12 @@ function remeasureProject({ pkgAbs, rootAbs, parsed, unreachable, chain }) {
43484377
function measureTestDebt(dir, hiddenTests = []) {
43494378
const rootAbs = ROOT.replaceAll('\\', '/').replace(/\/$/, '');
43504379
const pkgAbs = join(ROOT, dir).replaceAll('\\', '/').replace(/\/$/, '');
4351-
const parsed = JSON.parse(
4352-
readFileSync(join(ROOT, dir, 'tsconfig.json'), 'utf8').replace(/^\s*\/\/.*$/gm, ''),
4353-
);
4380+
// readTsconfig first: it is the reader that refuses an unparseable config.
43544381
const { roots } = readTsconfig(dir, 'tsconfig.json');
4382+
const { config: parsed } = parseTsconfigText(
4383+
readFileSync(join(ROOT, dir, 'tsconfig.json'), 'utf8'),
4384+
`${dir}/tsconfig.json`,
4385+
);
43554386
const unreachable = hiddenTests.filter(
43564387
(rel) => !roots.some((root) => root === '' || rel === root || rel.startsWith(`${root}/`)),
43574388
);
@@ -5051,6 +5082,8 @@ const SELF_TEST_BATTERIES = Object.freeze({
50515082
// The closure build's shared-verify-lock contract: 11 verdict-reader rows, 2
50525083
// wiring pins, 6 real-lock cases on a private lock file.
50535084
'verify-lock cases (#23470)': 19,
5085+
// parseTsconfigText: what tsc accepts reads, what tsc refuses still refuses.
5086+
'JSONC tsconfig reader (#25697)': 8,
50545087
});
50555088

50565089
// DELETING an entry silences that battery's floor exactly as effectively as
@@ -5059,7 +5092,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
50595092
// key in the literal above, so the roster falls below this number; the table
50605093
// cross-check in `batteryFloorFailures()` is the other half, and names WHICH
50615094
// label collided.
5062-
const SELF_TEST_BATTERY_FLOOR = 65;
5095+
const SELF_TEST_BATTERY_FLOOR = 66;
50635096

50645097
// The ledger `batteryFloorFailures()` reads.
50655098
//
@@ -7695,6 +7728,57 @@ function selfTest() {
76957728
if (!c.ok) failures.push(`prerequisiteNotMetText — ${c.label}`);
76967729
}
76977730

7731+
// ── THE JSONC TSCONFIG READER (#25697) ─────────────────────────────────────
7732+
//
7733+
// tsc reads tsconfig files as JSON with comments and trailing commas, and so
7734+
// must this gate: the accepted rows below are all shapes tsc compiles, and
7735+
// the first is objectui's root config's own (a block comment between keys).
7736+
// The refused rows are what keeps "tolerant" from meaning "anything parses":
7737+
// each one used to throw under JSON.parse and must still come back as an
7738+
// error, never as a config that excludes nothing.
7739+
const jsoncCases = [
7740+
{
7741+
label: 'a block comment between keys reads',
7742+
text: '{\n "compilerOptions": {\n "skipLibCheck": true,\n\n /* Bundler mode */\n'
7743+
+ ' "moduleResolution": "bundler"\n },\n "include": ["src"]\n}\n',
7744+
include: ['src'],
7745+
},
7746+
{
7747+
label: 'trailing commas in an object and an array read',
7748+
text: '{ "include": ["src", "test",], "exclude": ["**/*.test.ts",], }',
7749+
include: ['src', 'test'],
7750+
exclude: ['**/*.test.ts'],
7751+
},
7752+
{
7753+
label: 'a whole-line `//` comment still reads, as it did before',
7754+
text: '// the base config\n{ "include": ["src/**/*"] }',
7755+
include: ['src/**/*'],
7756+
},
7757+
{
7758+
label: 'a `//` inside a string value is the value, not a comment',
7759+
text: '{ "include": ["src"], "compilerOptions": { "baseUrl": "http://x//y" } }',
7760+
include: ['src'],
7761+
},
7762+
{ label: 'a truncated document is refused', text: '{ "include": [', refused: true },
7763+
{ label: 'a merge-conflict marker is refused', text: '<<<<<<< HEAD\n{ "include": ["src"] }', refused: true },
7764+
{ label: 'a root that is not an object is refused', text: '["src"]', refused: true },
7765+
{ label: 'a document holding only a comment is refused, not read as {}', text: '// nothing here\n', refused: true },
7766+
];
7767+
for (const c of jsoncCases) {
7768+
registerCase('JSONC tsconfig reader (#25697)');
7769+
const got = parseTsconfigText(c.text, 'fixture/tsconfig.json');
7770+
if (c.refused) {
7771+
if (got.error === undefined) {
7772+
failures.push(`parseTsconfigText — ${c.label}: read as ${JSON.stringify(got.config)} instead of refused`);
7773+
}
7774+
continue;
7775+
}
7776+
const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
7777+
if (got.error !== undefined || !same(got.config.include, c.include) || !same(got.config.exclude, c.exclude)) {
7778+
failures.push(`parseTsconfigText — ${c.label}: got ${JSON.stringify(got)}`);
7779+
}
7780+
}
7781+
76987782
// The shared workspace enumerator is a plain module with no CI invocation of
76997783
// its own (#11510); every gate that consolidated onto it folds in its checks.
77007784
// ⛔ NOT a battery of this file either, for the same reason as the fold-in

0 commit comments

Comments
 (0)