From be52c12b216b9777de65407c9a94e19f33d2782b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 15:27:58 +0000 Subject: [PATCH 1/4] fix(cli): `objectui check` judges only files that declare themselves ObjectUI schemas (#5127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A root `type` is not evidence that a file is a UI schema — `type` heads at least seven unrelated JSON vocabularies, the commonest being `package.json`'s `"type": "module"`. Measured at this repo's root: 46 warnings, 45 of them package manifests, and the first line a user saw in their own project. A file now enters type judgement only on a positive marker: `$schema` pointing at an objectui.org host, or a structural key declared on `BaseSchema`. An exclusion list of filenames was rejected — that is a second hand-maintained list of the shape #5115 had just finished deleting. Option D lands with it: the command reports how many eligible files carried no marker, so the narrowed judgement surface is never silent. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE --- ...k-schema-positive-marker-and-skip-count.md | 35 +++ .../src/__tests__/check-jsonc-parse.test.ts | 28 +- .../src/__tests__/check-known-types.test.ts | 15 +- .../src/__tests__/check-schema-marker.test.ts | 271 ++++++++++++++++++ packages/cli/src/commands/check.ts | 149 +++++++++- 5 files changed, 484 insertions(+), 14 deletions(-) create mode 100644 .changeset/check-schema-positive-marker-and-skip-count.md create mode 100644 packages/cli/src/__tests__/check-schema-marker.test.ts diff --git a/.changeset/check-schema-positive-marker-and-skip-count.md b/.changeset/check-schema-positive-marker-and-skip-count.md new file mode 100644 index 000000000..3ae7d8a04 --- /dev/null +++ b/.changeset/check-schema-positive-marker-and-skip-count.md @@ -0,0 +1,35 @@ +--- +'@object-ui/cli': minor +--- + +`objectui check` judges a file's `type` only when the file says it is an ObjectUI schema, and reports how many it declined to judge. + +A root `type` was treated as a component key wherever it appeared. `type` heads at +least seven unrelated JSON vocabularies, and the most common of them is +`package.json`'s `"type": "module"` — so the first line a user saw running +`objectui check` in their own project was a warning about their own package +manifest. Measured at this repository's root: 46 warnings, 45 of them +`package.json` (objectui#5127). + +A file now enters type judgement only when it positively reads as an ObjectUI +schema — it declares `"$schema"` pointing at an `objectui.org` URL, or it carries +a structural key from `BaseSchema` (`children`, `body`, `className`, +`placeholder`, `style`, the `visible`/`hidden`/`disabled` predicate family, +`testId`, `ariaLabel`). Every other root-`type` vocabulary — JSON Schema's +`"array"`, an `.eslintrc.json`'s `"commonjs"`, a package manifest's `"module"` — +is simply never judged. The `$schema` match is on the URL's host rather than a +literal string, so a lookalike origin is not accepted and the canonical spelling +can be confirmed without invalidating files that already declare it. + +A list of filenames to exclude was the alternative and was rejected: it is a +second hand-maintained list of the shape objectui#5115 had just finished +deleting, and it can only ever enumerate the foreign vocabularies someone already +thought of. This is producer-side declaration instead. + +Because the marker narrows what is checked, the command now also reports the +count of files that had a root `type` and no marker, with the `$schema` line that +opts one back in. That number is the coverage this gate gives up until schema +files declare themselves, and printing it is what keeps the loss visible rather +than silent. The `.yaml`/`.yml` half of the scan is unchanged — it was never +type-judged, before this change or after it. Exit codes are untouched: a JSON +parse failure remains the only thing that fails the run. diff --git a/packages/cli/src/__tests__/check-jsonc-parse.test.ts b/packages/cli/src/__tests__/check-jsonc-parse.test.ts index 444ce4757..80a427ffe 100644 --- a/packages/cli/src/__tests__/check-jsonc-parse.test.ts +++ b/packages/cli/src/__tests__/check-jsonc-parse.test.ts @@ -31,7 +31,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; -import { check } from '../commands/check.js'; +import { check, OBJECTUI_SCHEMA_URL } from '../commands/check.js'; let cwd: string; let lines: string[]; @@ -194,9 +194,18 @@ describe('objectui check — genuinely malformed JSON still fails the run', () = }); }); +/** + * These fixtures declare `$schema` because objectui#5127 gated the warning arm + * behind a positive ObjectUI marker: a bare `{"type": ...}` file is no longer + * judged at all, so without the declaration every assertion below would pass + * for the wrong reason — including the two that assert SILENCE, which would + * then be measuring nothing. The gate itself is pinned in + * `check-schema-marker.test.ts`; this section is about the warning arm the gate + * admits files to. + */ describe('objectui check — the unknown-type warning arm is untouched (objectui#5127)', () => { it('still warns for an unrecognised root type, and still does not fail the run', async () => { - writeFile('bogus.json', '{"type":"totally-made-up-xyz"}'); + writeFile('bogus.json', `{"$schema":"${OBJECTUI_SCHEMA_URL}","type":"totally-made-up-xyz"}`); await check(cwd); @@ -210,7 +219,10 @@ describe('objectui check — the unknown-type warning arm is untouched (objectui // Files that previously died at the parse step now reach the type check — // the warning arm's reach grows, but its verdict and its exit-code // neutrality are unchanged. - writeFile('commented.json', '{\n // a comment\n "type": "totally-made-up-xyz",\n}\n'); + writeFile( + 'commented.json', + `{\n // a comment\n "$schema": "${OBJECTUI_SCHEMA_URL}",\n "type": "totally-made-up-xyz",\n}\n` + ); await check(cwd); @@ -219,7 +231,10 @@ describe('objectui check — the unknown-type warning arm is untouched (objectui }); it('stays silent for a registered type', async () => { - writeFile('grid.json', '{"type":"object-grid","objectApiName":"account"}'); + writeFile( + 'grid.json', + `{"$schema":"${OBJECTUI_SCHEMA_URL}","type":"object-grid","objectApiName":"account"}` + ); await check(cwd); @@ -230,7 +245,10 @@ describe('objectui check — the unknown-type warning arm is untouched (objectui it('does not run the type check on a file that failed to parse', async () => { // A parse failure short-circuits before the schema arm, exactly as the // thrown `JSON.parse` used to. - writeFile('broken-typed.json', '{ "type": "totally-made-up-xyz", }}'); + writeFile( + 'broken-typed.json', + `{ "$schema": "${OBJECTUI_SCHEMA_URL}", "type": "totally-made-up-xyz", }}` + ); await check(cwd); diff --git a/packages/cli/src/__tests__/check-known-types.test.ts b/packages/cli/src/__tests__/check-known-types.test.ts index e9e1b49bb..455ab96c3 100644 --- a/packages/cli/src/__tests__/check-known-types.test.ts +++ b/packages/cli/src/__tests__/check-known-types.test.ts @@ -25,14 +25,23 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; -import { check } from '../commands/check.js'; +import { check, OBJECTUI_SCHEMA_URL } from '../commands/check.js'; let cwd: string; let lines: string[]; let restoreLog: () => void; -function writeSchema(name: string, body: unknown): void { - writeFileSync(join(cwd, name), JSON.stringify(body)); +/** + * Every fixture declares the ObjectUI `$schema` URL, because objectui#5127 + * gated type judgement behind a positive marker: a bare `{"type": ...}` file is + * not judged at all now. Without the declaration the three warning assertions + * below would fail and — worse — the two SILENCE assertions would keep passing + * while measuring nothing, which is the shape of a test that survives the + * deletion of the feature it covers. The gate is pinned separately, in + * `check-schema-marker.test.ts`; this file is about the derived key set. + */ +function writeSchema(name: string, body: Record): void { + writeFileSync(join(cwd, name), JSON.stringify({ $schema: OBJECTUI_SCHEMA_URL, ...body })); } /** Warnings only, with the ANSI colouring chalk may add stripped off. */ diff --git a/packages/cli/src/__tests__/check-schema-marker.test.ts b/packages/cli/src/__tests__/check-schema-marker.test.ts new file mode 100644 index 000000000..a743406fb --- /dev/null +++ b/packages/cli/src/__tests__/check-schema-marker.test.ts @@ -0,0 +1,271 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * `objectui check`'s positive schema marker (objectui#5127). + * + * A root `type` is not evidence that a file is a UI schema — `type` heads at + * least seven unrelated JSON vocabularies, and the most common of them is + * `package.json`'s `"type": "module"`. The command judged all of them, so the + * FIRST line a user saw running it in their own project was a warning about + * their own package manifest: 45 of the 46 warnings this repository produced + * were exactly that. + * + * Half of this file pins that those files stopped being judged. The OTHER half + * pins that real schemas are still judged and still warn — a suite that only + * proved things went quiet would pass a change that deleted the feature, which + * is the failure mode this gate is one step away from by construction. + * + * Fixtures live under `os.tmpdir()`, never in the repo tree: `check()` globs + * every JSON file under the directory it is handed, so a fixture committed + * inside this workspace would be scanned by every other run of the command as + * well — including the repo's own `pnpm check`. + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; + +import { check, OBJECTUI_SCHEMA_URL } from '../commands/check.js'; + +let cwd: string; +let lines: string[]; +let exitCodes: number[]; +let restoreLog: () => void; + +function writeSchema(name: string, body: unknown): void { + writeFileSync(join(cwd, name), JSON.stringify(body)); +} + +/** Write a fixture verbatim — for JSONC sources, not `JSON.stringify` output. */ +function writeRaw(name: string, body: string): void { + writeFileSync(join(cwd, name), body); +} + +/** + * The CSI sequences chalk may add. The escape byte is built with + * `String.fromCharCode` rather than spelled into the source, so this file holds + * no raw control character and no escape a tooling pass could materialise into + * one (objectui AGENTS.md byte discipline). + */ +const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'); + +function plainLines(): string[] { + return lines.map((l) => l.replace(ANSI, '')); +} + +function unknownTypeWarnings(): string[] { + return plainLines().filter((l) => l.includes('Unknown schema type')); +} + +/** The count `check` reports for files it declined to judge, or 0 if silent. */ +function skippedCount(): number { + const line = plainLines().find((l) => l.startsWith('Skipped ')); + if (!line) return 0; + const match = /^Skipped (\d+) file/.exec(line); + if (!match) throw new Error(`skip line present but unparseable: ${line}`); + return Number(match[1]); +} + +beforeEach(() => { + cwd = mkdtempSync(join(tmpdir(), 'objectui-check-marker-')); + lines = []; + exitCodes = []; + const original = console.log; + console.log = (...args: unknown[]) => { + lines.push(args.map(String).join(' ')); + }; + // `check()` calls `process.exit(1)` itself; record the call instead of + // letting it tear the Vitest worker down. + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { + exitCodes.push(code ?? 0); + return undefined as never; + }) as never); + restoreLog = () => { + console.log = original; + exitSpy.mockRestore(); + }; +}); + +afterEach(() => { + restoreLog(); + rmSync(cwd, { recursive: true, force: true }); +}); + +describe('objectui check — foreign root-`type` vocabularies are never judged', () => { + it('says nothing about `package.json`\'s "type": "module" — the card\'s headline symptom', async () => { + writeSchema('package.json', { name: 'x', version: '1.0.0', type: 'module' }); + await check(cwd); + expect(unknownTypeWarnings()).toEqual([]); + }); + + it('says nothing about a JSON Schema document', async () => { + // Two documents, and the SECOND one is the load-bearing half. `object` is + // itself a registered component key, so a JSON Schema whose root type is + // `object` was silent before this change too — it would pass here for a + // reason that has nothing to do with the marker. `array` is not registered, + // so the array document is one that genuinely warned before. + // + // Note the array document carries `items`, which is why `items` is absent + // from the structural-key set even though ObjectUI nodes use it: a marker + // key shared with JSON Schema re-admits exactly the files this gate exists + // to keep out. + writeSchema('thing.schema.json', { + $schema: 'http://json-schema.org/draft-07/schema#', + title: 'Thing', + type: 'object', + required: ['id'], + properties: { id: { type: 'string' } }, + }); + writeSchema('tags.schema.json', { + $schema: 'http://json-schema.org/draft-07/schema#', + title: 'Tags', + type: 'array', + items: { type: 'string' }, + }); + await check(cwd); + expect(unknownTypeWarnings()).toEqual([]); + expect(skippedCount()).toBe(2); + }); + + it('says nothing about an `.eslintrc.json`', async () => { + writeSchema('.eslintrc.json', { root: true, type: 'commonjs', rules: {} }); + await check(cwd); + expect(unknownTypeWarnings()).toEqual([]); + }); + + it('says nothing about a tsconfig-derived file, and does not count it as skipped', async () => { + // A `tsconfig` carries no root `type` at all, so it was never ELIGIBLE for + // judgement and must not inflate the skipped count either — that count + // reports lost coverage, and a file that was never judged is not lost + // coverage. It is written as real JSONC to keep the objectui#5237 parse + // arm exercised alongside this one. + writeRaw( + 'tsconfig.build.json', + '{\n // extends the base config\n "extends": "./tsconfig.json",\n "compilerOptions": { "outDir": "dist" },\n}\n' + ); + await check(cwd); + expect(unknownTypeWarnings()).toEqual([]); + expect(skippedCount()).toBe(0); + expect(exitCodes).toEqual([]); + }); + + it('does not judge a foreign file even when its `type` happens to name a real component', async () => { + // `form` IS a registered component key, so this file was silent before the + // marker too — but for the wrong reason. Pinning it keeps the gate honest: + // the file is out of scope, not accidentally acceptable. + writeSchema('package.json', { name: 'x', type: 'form' }); + await check(cwd); + expect(unknownTypeWarnings()).toEqual([]); + expect(skippedCount()).toBe(1); + }); +}); + +describe('objectui check — a real schema is still judged, by either marker arm', () => { + it('judges a file that declares the ObjectUI `$schema` URL', async () => { + writeSchema('leaf.json', { + $schema: OBJECTUI_SCHEMA_URL, + type: 'totally-made-up-xyz', + label: 'Send Email', + }); + await check(cwd); + expect(unknownTypeWarnings()).toEqual([ + expect.stringContaining('Unknown schema type "totally-made-up-xyz" in leaf.json'), + ]); + expect(skippedCount()).toBe(0); + }); + + it('judges a file carrying a structural key, with no `$schema` at all', async () => { + writeSchema('page.json', { + type: 'totally-made-up-xyz', + children: [{ type: 'text', body: 'hi' }], + }); + await check(cwd); + expect(unknownTypeWarnings()).toHaveLength(1); + expect(unknownTypeWarnings()[0]).toContain('"totally-made-up-xyz"'); + }); + + it('stays silent for a REGISTERED type on a judged file — the marker admits, it does not warn', async () => { + writeSchema('grid.json', { type: 'object-grid', className: 'h-full' }); + writeSchema('ns-grid.json', { $schema: OBJECTUI_SCHEMA_URL, type: 'view:grid' }); + await check(cwd); + expect(unknownTypeWarnings()).toEqual([]); + expect(skippedCount()).toBe(0); + }); + + it('accepts the `$schema` URL on any objectui.org host, and rejects a lookalike origin', async () => { + // The matcher compares the URL's HOST, not a string prefix: the canonical + // spelling is still awaiting the maintainer's confirmation, and + // `https://objectui.org.example.com/...` is a different origin that a + // `startsWith` test would have answered YES for. + writeSchema('www.json', { + $schema: 'https://www.objectui.org/schema/v1/objectui.schema.json', + type: 'totally-made-up-xyz', + }); + writeSchema('lookalike.json', { + $schema: 'https://objectui.org.example.com/schema.json', + type: 'totally-made-up-xyz', + }); + await check(cwd); + expect(unknownTypeWarnings()).toEqual([ + expect.stringContaining('Unknown schema type "totally-made-up-xyz" in www.json'), + ]); + expect(skippedCount()).toBe(1); + }); + + it('does not treat a non-URL `$schema` value as an ObjectUI declaration', async () => { + writeSchema('relative.json', { $schema: './objectui-schema.json', type: 'totally-made-up-xyz' }); + await check(cwd); + expect(unknownTypeWarnings()).toEqual([]); + expect(skippedCount()).toBe(1); + }); +}); + +describe('objectui check — the narrowed judgement surface is never silent (option D)', () => { + it('reports how many eligible files went unjudged, and how to opt one in', async () => { + writeSchema('package.json', { name: 'x', type: 'module' }); + // The two leaves are the majority shape of the real in-repo corpus: a + // single node with no structural key, which is precisely the coverage this + // interim gives up and which this count keeps visible. + writeSchema('leaf-1.json', { type: 'button', label: 'Send Email', icon: 'mail' }); + writeSchema('leaf-2.json', { type: 'badge', variant: 'default' }); + await check(cwd); + // Two leaves plus the manifest: three files had a root `type` string and + // no marker. + expect(skippedCount()).toBe(3); + const hint = plainLines().find((l) => l.includes('A file is checked when it declares')); + expect(hint).toBeDefined(); + expect(hint).toContain(OBJECTUI_SCHEMA_URL); + }); + + it('says nothing when every eligible file carried a marker', async () => { + writeSchema('page.json', { type: 'card', children: [] }); + await check(cwd); + expect(skippedCount()).toBe(0); + expect(plainLines().some((l) => l.startsWith('Skipped '))).toBe(false); + }); + + it('is a report, not a failure — the run still passes', async () => { + writeSchema('package.json', { name: 'x', type: 'module' }); + await check(cwd); + expect(skippedCount()).toBe(1); + expect(plainLines().some((l) => l.includes('All checks passed'))).toBe(true); + expect(exitCodes).toEqual([]); + }); +}); + +describe('objectui check — the marker gate did not disturb the parse arm', () => { + it('still counts malformed JSON as an error and still exits 1', async () => { + writeRaw('broken.json', '{ "type": "card", "children": [ }'); + await check(cwd); + expect(plainLines().some((l) => l.includes('Invalid JSON in broken.json'))).toBe(true); + expect(plainLines().some((l) => l.includes('Found 1 errors'))).toBe(true); + expect(exitCodes).toEqual([1]); + }); +}); diff --git a/packages/cli/src/commands/check.ts b/packages/cli/src/commands/check.ts index ba2f12fc6..72a997e21 100644 --- a/packages/cli/src/commands/check.ts +++ b/packages/cli/src/commands/check.ts @@ -14,6 +14,112 @@ import { parse as parseJsonc, printParseErrorCode, type ParseError } from 'jsonc import { isKnownSchemaType } from '../utils/known-schema-types.js'; +/** + * The canonical, permanent public identifier for the ObjectUI schema protocol. + * + * ⚠️ PROPOSED — confirmed by the maintainer at merge (objectui#5127). A + * `$schema` URL is a permanent public identifier: files in user projects will + * carry it forever, so it is a maintainer decision, not a dev one. The + * reasoning behind this spelling is in the PR body. + * + * The matcher below deliberately does NOT compare against this string. It + * recognises the ObjectUI ORIGIN, so every file that already declares an + * `objectui.org` schema URL keeps working whichever exact path the maintainer + * confirms. + */ +export const OBJECTUI_SCHEMA_URL = 'https://objectui.org/schema/v1/objectui.schema.json'; + +/** + * Root keys that positively identify a file as an ObjectUI schema node. + * + * Every entry is a property DECLARED on `BaseSchema` + * (`packages/types/src/base.ts`) — this set is read out of the protocol's own + * node contract, not invented here, and it is closed: it grows only when + * `BaseSchema` grows. + * + * `BaseSchema`'s remaining properties are deliberately absent because their + * NAMES are generic enough to head foreign root vocabularies, and a marker + * that matches a foreign file re-creates the exact defect this gate exists to + * remove. Measured over this repository (objectui#5127): `name` appears at the + * root of 45 of the 46 foreign judged files and `description` at 44 — both are + * `package.json` keys — so admitting `BaseSchema` wholesale takes the foreign + * retention from 0 files to all 46. `id`, `label`, `data` and `type` itself are + * held out on the same reasoning. + */ +const OBJECTUI_STRUCTURAL_KEYS: readonly string[] = [ + // Composition — the `children`/`body`-class keys the ruling names. + 'body', + 'children', + // Presentation. + 'className', + 'style', + 'placeholder', + // Conditional rendering and interaction state. + 'visible', + 'visibleWhen', + 'visibleOn', + 'hidden', + 'hiddenOn', + 'disabled', + 'disabledOn', + // Test and accessibility handles. + 'testId', + 'ariaLabel', +]; + +/** + * Does this `$schema` value point at ObjectUI? + * + * Matches on the URL's HOST, not on a literal string, for two reasons. The + * canonical URL above is still awaiting the maintainer's confirmation, and a + * literal comparison would silently stop recognising every migrated file the + * moment the confirmed path differs by one character. And a `startsWith` test + * against a prefix answers YES for `https://objectui.org.example.com/…`, which + * is a different origin entirely — so the value is parsed as a URL and the + * hostname compared, rather than the string matched. + */ +function pointsAtObjectUi(value: unknown): boolean { + if (typeof value !== 'string') return false; + let hostname: string; + try { + ({ hostname } = new URL(value)); + } catch { + // Not a URL — `$schema` is present but says nothing about ObjectUI. + return false; + } + const host = hostname.toLowerCase(); + return host === 'objectui.org' || host.endsWith('.objectui.org'); +} + +/** + * Does this parsed file positively read as an ObjectUI schema (objectui#5127)? + * + * `type` carries at least seven unrelated vocabularies in JSON — the most + * common of them being `package.json`'s `"type": "module"` — so the presence + * of a root `type` is not evidence that a file is a UI schema. Before this + * gate, `objectui check` judged every root `type` against the component-key + * universe and the FIRST line a user saw in their own project was a warning + * about their own `package.json`: 45 of the 46 warnings this repository + * produced were exactly that. + * + * The fix is producer-side declaration, not a consumer-side skip list: a file + * is judged when it SAYS it is an ObjectUI schema (`$schema`) or when it is + * structurally recognisable as one. A list of filenames to exclude was + * considered and rejected — it is a second hand-maintained list of the shape + * objectui#5115 had just finished deleting, and it can only ever enumerate the + * foreign vocabularies someone already thought of. + * + * ⚠️ The structural arm is a TRANSITIONAL fallback, not the contract. It is + * drawn for precision over recall on purpose: a foreign file wrongly judged is + * this card's defect reappearing in a user's project, while a real schema + * wrongly skipped is a coverage debt that the corpus migration repays and that + * the skipped-file count below keeps visible in the meantime. + */ +function isObjectUiSchemaFile(content: Record): boolean { + if (pointsAtObjectUi(content.$schema)) return true; + return OBJECTUI_STRUCTURAL_KEYS.some((key) => key in content); +} + /** * Render a `jsonc-parser` error the way `JSON.parse` renders its own: a reason, * then where it happened. The parser reports a byte offset; the line/column is @@ -44,6 +150,12 @@ export async function check(cwd: string = process.cwd()) { console.log(`Analyzing ${files.length} files...`); let errors = 0; + // Files that a root `type` string made ELIGIBLE for type judgement and that + // no ObjectUI marker admitted. Reported below so the narrowed judgement + // surface is never silent (objectui#5127) — a check that quietly stops + // checking is worse than one that warns too loudly, because nothing prompts + // a re-read. + let skipped = 0; for (const file of files) { try { @@ -82,12 +194,24 @@ export async function check(cwd: string = process.cwd()) { // Schema validation: check for ObjectUI schema patterns if (content && typeof content === 'object' && content.type) { - // The known-type universe is DERIVED from the repository's - // registration calls (see `packages/cli/src/utils/known-schema-types.ts` - // and the script that writes it), not typed by hand. The array that - // used to sit here had drifted both ways at once — objectui#5115. - if (typeof content.type === 'string' && !isKnownSchemaType(content.type)) { - console.log(chalk.yellow(`⚠️ Unknown schema type "${content.type}" in ${file}`)); + if (typeof content.type === 'string') { + // The marker gate (objectui#5127). Only a file that positively + // reads as an ObjectUI schema enters type judgement; every other + // root-`type` vocabulary — `package.json`'s `"module"`, + // JSON Schema's `"object"`, and the rest — is simply not judged. + // + // Note this arm is reached for `.json` only, as the parse above + // is. The glob also matches `.yaml`/`.yml`, and those files are + // read by neither arm, before this change or after it. + if (!isObjectUiSchemaFile(content as Record)) { + skipped++; + } else if (!isKnownSchemaType(content.type)) { + // The known-type universe is DERIVED from the repository's + // registration calls (see `packages/cli/src/utils/known-schema-types.ts` + // and the script that writes it), not typed by hand. The array that + // used to sit here had drifted both ways at once — objectui#5115. + console.log(chalk.yellow(`⚠️ Unknown schema type "${content.type}" in ${file}`)); + } } } } @@ -96,6 +220,19 @@ export async function check(cwd: string = process.cwd()) { errors++; } } + + if (skipped > 0) { + console.log( + chalk.dim( + `Skipped ${skipped} file${skipped === 1 ? '' : 's'} with a root "type" and no ObjectUI schema marker.` + ) + ); + console.log( + chalk.dim( + ` A file is checked when it declares "$schema": "${OBJECTUI_SCHEMA_URL}" or carries a structural key (children/body/className/...).` + ) + ); + } if (errors === 0) { console.log(chalk.green('✓ All checks passed')); From 015e2eee9add1fceeb8ebe36be8bb92bc22b0c5e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 15:33:35 +0000 Subject: [PATCH 2/4] test(cli): replace a phantom `.eslintrc.json` fixture the ablation exposed (#5127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverse-verification predicted 8 red and observed 7. The miss was the `.eslintrc.json` case: `globSync` does not match dot-prefixed names without `dot: true`, so that file was never scanned and its assertion of silence held whatever the marker did — green in both directions, measuring nothing. It is replaced by a deployment-resource descriptor, which is scanned and whose root `type` is unregistered, plus an explicit test that pins the dotfile scope itself with a scanned sibling as counter-probe. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE --- .../src/__tests__/check-schema-marker.test.ts | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/__tests__/check-schema-marker.test.ts b/packages/cli/src/__tests__/check-schema-marker.test.ts index a743406fb..3a12dc5f2 100644 --- a/packages/cli/src/__tests__/check-schema-marker.test.ts +++ b/packages/cli/src/__tests__/check-schema-marker.test.ts @@ -134,10 +134,37 @@ describe('objectui check — foreign root-`type` vocabularies are never judged', expect(skippedCount()).toBe(2); }); - it('says nothing about an `.eslintrc.json`', async () => { - writeSchema('.eslintrc.json', { root: true, type: 'commonjs', rules: {} }); + it('says nothing about a deployment resource descriptor', async () => { + // The third big root-`type` vocabulary after package manifests and JSON + // Schema: infrastructure resources. `properties` is deliberately not a + // marker key, so this file is not admitted by the structural arm. + writeSchema('bucket.json', { + type: 'aws:s3/bucket:Bucket', + properties: { acl: 'private' }, + }); await check(cwd); expect(unknownTypeWarnings()).toEqual([]); + expect(skippedCount()).toBe(1); + }); + + it('never even sees a dot-prefixed config — those are outside the scan', async () => { + // `.eslintrc.json` is the obvious fixture for this suite and it would be a + // PHANTOM: `globSync` does not match dot-prefixed names without `dot: true`, + // so the file is not scanned at all and an assertion of silence over it + // holds whatever the marker does. Measured, and it is why this test asserts + // the scope rather than the marker. + // + // The scanned sibling is the counter-probe: without it, "no warnings" here + // would be indistinguishable from a scan that read nothing. + writeSchema('.eslintrc.json', { root: true, type: 'totally-made-up-xyz', rules: {} }); + writeSchema('page.json', { type: 'totally-made-up-xyz', children: [] }); + await check(cwd); + // Exactly one warning, from the scanned file — the dotfile contributed + // neither a warning nor a skip. + expect(unknownTypeWarnings()).toEqual([ + expect.stringContaining('Unknown schema type "totally-made-up-xyz" in page.json'), + ]); + expect(skippedCount()).toBe(0); }); it('says nothing about a tsconfig-derived file, and does not count it as skipped', async () => { From ddea409cdaade10898677f087c72e10d9c7bad88 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 09:22:27 +0000 Subject: [PATCH 3/4] =?UTF-8?q?fix(cli):=20drop=20the=20`$schema`=20arm=20?= =?UTF-8?q?=E2=80=94=20no=20public=20ObjectUI=20schema=20URL=20is=20minted?= =?UTF-8?q?=20(#5127)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer ruling 2026-08-20 (verbatim 「C」) supersedes the `$schema` half of the 2026-08-19 「全部接受」 ruling. The structural arm and the option-D skip count are unaffected and stand. `pointsAtObjectUi()` and `OBJECTUI_SCHEMA_URL` are removed, and the skip hint now names the structural keys by rendering the gate's own array — so it cannot advertise a way in that the build does not honour. The fixtures in `check-known-types.test.ts` and `check-jsonc-parse.test.ts` declared that URL to stay judged; they now carry a structural key instead. Each silence assertion among them gains a counter-probe, because a fixture that is no longer admitted leaves those assertions green while measuring nothing. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE --- .../src/__tests__/check-jsonc-parse.test.ts | 47 ++++--- .../src/__tests__/check-known-types.test.ts | 42 +++++-- .../src/__tests__/check-schema-marker.test.ts | 116 ++++++++++++------ packages/cli/src/commands/check.ts | 77 ++++++------ 4 files changed, 179 insertions(+), 103 deletions(-) diff --git a/packages/cli/src/__tests__/check-jsonc-parse.test.ts b/packages/cli/src/__tests__/check-jsonc-parse.test.ts index 80a427ffe..c53169954 100644 --- a/packages/cli/src/__tests__/check-jsonc-parse.test.ts +++ b/packages/cli/src/__tests__/check-jsonc-parse.test.ts @@ -31,7 +31,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; -import { check, OBJECTUI_SCHEMA_URL } from '../commands/check.js'; +import { check } from '../commands/check.js'; let cwd: string; let lines: string[]; @@ -195,17 +195,23 @@ describe('objectui check — genuinely malformed JSON still fails the run', () = }); /** - * These fixtures declare `$schema` because objectui#5127 gated the warning arm - * behind a positive ObjectUI marker: a bare `{"type": ...}` file is no longer - * judged at all, so without the declaration every assertion below would pass - * for the wrong reason — including the two that assert SILENCE, which would - * then be measuring nothing. The gate itself is pinned in + * These fixtures carry `className` — a STRUCTURAL marker key — because + * objectui#5127 gated the warning arm behind a positive ObjectUI marker: a + * bare `{"type": ...}` file is no longer judged at all, so without a marker + * every assertion below would pass for the wrong reason — including the two + * that assert SILENCE, which would then be measuring nothing. + * + * An earlier revision declared a `$schema` URL here instead. The maintainer's + * 2026-08-20 ruling removed that arm, so those fixtures would have been + * admitted by nothing at all while every assertion in this section stayed + * green. Each silence assertion below is now paired with a counter-probe that + * proves judgement actually ran. The gate itself is pinned in * `check-schema-marker.test.ts`; this section is about the warning arm the gate * admits files to. */ describe('objectui check — the unknown-type warning arm is untouched (objectui#5127)', () => { it('still warns for an unrecognised root type, and still does not fail the run', async () => { - writeFile('bogus.json', `{"$schema":"${OBJECTUI_SCHEMA_URL}","type":"totally-made-up-xyz"}`); + writeFile('bogus.json', '{"className":"p-0","type":"totally-made-up-xyz"}'); await check(cwd); @@ -221,7 +227,7 @@ describe('objectui check — the unknown-type warning arm is untouched (objectui // neutrality are unchanged. writeFile( 'commented.json', - `{\n // a comment\n "$schema": "${OBJECTUI_SCHEMA_URL}",\n "type": "totally-made-up-xyz",\n}\n` + '{\n // a comment\n "className": "p-0",\n "type": "totally-made-up-xyz",\n}\n' ); await check(cwd); @@ -233,27 +239,38 @@ describe('objectui check — the unknown-type warning arm is untouched (objectui it('stays silent for a registered type', async () => { writeFile( 'grid.json', - `{"$schema":"${OBJECTUI_SCHEMA_URL}","type":"object-grid","objectApiName":"account"}` + '{"className":"p-0","type":"object-grid","objectApiName":"account"}' ); + // Counter-probe: same marker, same admission path, a type nothing + // registers. Its warning is what makes `grid.json`'s silence a verdict + // instead of a file the gate never let through. + writeFile('probe.json', '{"className":"p-0","type":"totally-made-up-xyz"}'); await check(cwd); - expect(unknownTypeWarnings()).toEqual([]); + expect(unknownTypeWarnings()).toEqual([ + expect.stringContaining('Unknown schema type "totally-made-up-xyz" in probe.json'), + ]); expect(exitCodes).toEqual([]); }); it('does not run the type check on a file that failed to parse', async () => { // A parse failure short-circuits before the schema arm, exactly as the // thrown `JSON.parse` used to. - writeFile( - 'broken-typed.json', - `{ "$schema": "${OBJECTUI_SCHEMA_URL}", "type": "totally-made-up-xyz", }}` - ); + writeFile('broken-typed.json', '{ "className": "p-0", "type": "totally-made-up-xyz", }}'); + // Counter-probe: byte-for-byte the same document minus the stray brace. + // It warns, so the silence about `broken-typed.json` is attributable to + // the parse short-circuit — the only difference between the two files — + // and not to a marker the gate declined. + writeFile('parsed-typed.json', '{ "className": "p-0", "type": "totally-made-up-xyz" }'); await check(cwd); expect(parseErrorLines()).toHaveLength(1); - expect(unknownTypeWarnings()).toEqual([]); + expect(parseErrorLines()[0]).toContain('broken-typed.json'); + expect(unknownTypeWarnings()).toEqual([ + expect.stringContaining('Unknown schema type "totally-made-up-xyz" in parsed-typed.json'), + ]); expect(exitCodes).toEqual([1]); }); }); diff --git a/packages/cli/src/__tests__/check-known-types.test.ts b/packages/cli/src/__tests__/check-known-types.test.ts index 455ab96c3..3d54f90cc 100644 --- a/packages/cli/src/__tests__/check-known-types.test.ts +++ b/packages/cli/src/__tests__/check-known-types.test.ts @@ -25,23 +25,35 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; -import { check, OBJECTUI_SCHEMA_URL } from '../commands/check.js'; +import { check } from '../commands/check.js'; let cwd: string; let lines: string[]; let restoreLog: () => void; /** - * Every fixture declares the ObjectUI `$schema` URL, because objectui#5127 - * gated type judgement behind a positive marker: a bare `{"type": ...}` file is - * not judged at all now. Without the declaration the three warning assertions - * below would fail and — worse — the two SILENCE assertions would keep passing - * while measuring nothing, which is the shape of a test that survives the - * deletion of the feature it covers. The gate is pinned separately, in + * Every fixture carries `className` — a STRUCTURAL marker key — because + * objectui#5127 gated type judgement behind a positive ObjectUI marker: a bare + * `{"type": ...}` file is not judged at all now. Without a marker the warning + * assertions below would fail and — worse — the SILENCE assertion would keep + * passing while measuring nothing, which is the shape of a test that survives + * the deletion of the feature it covers. + * + * That is not assumed here, it is measured: dropping the injection from this + * helper turns the warning tests red and leaves the silence test GREEN, which + * is precisely why that test carries its own counter-probe below rather than + * trusting this comment. + * + * `className` is the least semantically loaded key in the marker set — it says + * nothing about a node's children, visibility or interaction state — so it + * perturbs no fixture's meaning. An earlier revision declared a `$schema` URL + * instead; the maintainer's 2026-08-20 ruling removed that arm, and a marker + * that names a way in the build no longer honours is a fixture that admits + * nothing. The gate itself is pinned separately, in * `check-schema-marker.test.ts`; this file is about the derived key set. */ function writeSchema(name: string, body: Record): void { - writeFileSync(join(cwd, name), JSON.stringify({ $schema: OBJECTUI_SCHEMA_URL, ...body })); + writeFileSync(join(cwd, name), JSON.stringify({ className: 'p-0', ...body })); } /** Warnings only, with the ANSI colouring chalk may add stripped off. */ @@ -92,8 +104,16 @@ describe('objectui check — unknown schema types', () => { writeSchema('grid.json', { type: 'object-grid', objectApiName: 'account' }); writeSchema('ns-grid.json', { type: 'view:grid', objectApiName: 'account' }); writeSchema('gallery-ok.json', { type: 'object-gallery' }); + // Counter-probe. Written by the same helper, so it carries the same marker + // and travels the same admission path; its warning is what makes the three + // silences above VERDICTS rather than a judgement that never ran. Without + // it this assertion holds equally well when nothing is judged at all — + // measured, and the reason it is here (objectui#5127). + writeSchema('probe.json', { type: 'totally-made-up-xyz' }); await check(cwd); - expect(unknownTypeWarnings()).toEqual([]); + expect(unknownTypeWarnings()).toEqual([ + expect.stringContaining('Unknown schema type "totally-made-up-xyz" in probe.json'), + ]); }); it('still warns for a type nothing registers', async () => { @@ -108,6 +128,10 @@ describe('objectui check — unknown schema types', () => { // list would otherwise be free to become a breaking change by accident. writeSchema('bogus.json', { type: 'totally-made-up-xyz' }); await check(cwd); + // Both halves of this test's own sentence. Asserting only the exit + // neutrality would keep passing if the type were never REPORTED either, + // which is the state a lost marker puts this fixture in. + expect(unknownTypeWarnings()).toHaveLength(1); expect(lines.some((l) => l.includes('All checks passed'))).toBe(true); }); }); diff --git a/packages/cli/src/__tests__/check-schema-marker.test.ts b/packages/cli/src/__tests__/check-schema-marker.test.ts index 3a12dc5f2..ab4ed07fb 100644 --- a/packages/cli/src/__tests__/check-schema-marker.test.ts +++ b/packages/cli/src/__tests__/check-schema-marker.test.ts @@ -32,7 +32,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; -import { check, OBJECTUI_SCHEMA_URL } from '../commands/check.js'; +import { check } from '../commands/check.js'; let cwd: string; let lines: string[]; @@ -64,6 +64,24 @@ function unknownTypeWarnings(): string[] { return plainLines().filter((l) => l.includes('Unknown schema type')); } +/** The advisory line `check` prints under the skipped-file count. */ +function hintLine(): string { + const line = plainLines().find((l) => l.includes('A file is checked when')); + if (line === undefined) throw new Error('no hint line was printed'); + return line; +} + +/** + * The marker keys the hint advertises to the reader, read back out of the + * printed sentence rather than restated here — so a test can check that what + * the command SAYS and what it DOES are the same set. + */ +function advertisedKeys(): string[] { + const match = /structural key: (.+)\.$/.exec(hintLine()); + if (!match) throw new Error(`hint present but unparseable: ${hintLine()}`); + return match[1].split(', '); +} + /** The count `check` reports for files it declined to judge, or 0 if silent. */ function skippedCount(): number { const line = plainLines().find((l) => l.startsWith('Skipped ')); @@ -194,20 +212,7 @@ describe('objectui check — foreign root-`type` vocabularies are never judged', }); }); -describe('objectui check — a real schema is still judged, by either marker arm', () => { - it('judges a file that declares the ObjectUI `$schema` URL', async () => { - writeSchema('leaf.json', { - $schema: OBJECTUI_SCHEMA_URL, - type: 'totally-made-up-xyz', - label: 'Send Email', - }); - await check(cwd); - expect(unknownTypeWarnings()).toEqual([ - expect.stringContaining('Unknown schema type "totally-made-up-xyz" in leaf.json'), - ]); - expect(skippedCount()).toBe(0); - }); - +describe('objectui check — a real schema is still judged by the structural arm', () => { it('judges a file carrying a structural key, with no `$schema` at all', async () => { writeSchema('page.json', { type: 'totally-made-up-xyz', @@ -220,37 +225,36 @@ describe('objectui check — a real schema is still judged, by either marker arm it('stays silent for a REGISTERED type on a judged file — the marker admits, it does not warn', async () => { writeSchema('grid.json', { type: 'object-grid', className: 'h-full' }); - writeSchema('ns-grid.json', { $schema: OBJECTUI_SCHEMA_URL, type: 'view:grid' }); + writeSchema('ns-grid.json', { type: 'view:grid', body: [] }); await check(cwd); expect(unknownTypeWarnings()).toEqual([]); expect(skippedCount()).toBe(0); }); - it('accepts the `$schema` URL on any objectui.org host, and rejects a lookalike origin', async () => { - // The matcher compares the URL's HOST, not a string prefix: the canonical - // spelling is still awaiting the maintainer's confirmation, and - // `https://objectui.org.example.com/...` is a different origin that a - // `startsWith` test would have answered YES for. - writeSchema('www.json', { - $schema: 'https://www.objectui.org/schema/v1/objectui.schema.json', - type: 'totally-made-up-xyz', - }); - writeSchema('lookalike.json', { - $schema: 'https://objectui.org.example.com/schema.json', + it('does not admit a file on `$schema` alone — no URL is a marker', async () => { + // ⛔ NEGATIVE fixtures. ObjectUI publishes NO `$schema` URL, and none is + // being minted: the maintainer ruled against it (2026-08-20, verbatim + // 「C」), superseding the `$schema` half of the 2026-08-19 ruling. An + // earlier revision of this command admitted any file whose `$schema` host + // was `objectui.org`; these two files pin that that arm is GONE, so + // re-adding it turns this test red rather than passing unnoticed. + // + // The paths below are arbitrary on purpose — no spelling of an ObjectUI + // schema URL means anything to this command, which is the whole point. + writeSchema('declared.json', { + $schema: 'https://objectui.org/some/path.json', type: 'totally-made-up-xyz', }); + writeSchema('relative.json', { $schema: './objectui-schema.json', type: 'totally-made-up-xyz' }); + // Counter-probe: the same unregistered type, admitted structurally. Its + // warning proves the two silences above are the gate declining these + // files, not a run that judged nothing. + writeSchema('probe.json', { type: 'totally-made-up-xyz', children: [] }); await check(cwd); expect(unknownTypeWarnings()).toEqual([ - expect.stringContaining('Unknown schema type "totally-made-up-xyz" in www.json'), + expect.stringContaining('Unknown schema type "totally-made-up-xyz" in probe.json'), ]); - expect(skippedCount()).toBe(1); - }); - - it('does not treat a non-URL `$schema` value as an ObjectUI declaration', async () => { - writeSchema('relative.json', { $schema: './objectui-schema.json', type: 'totally-made-up-xyz' }); - await check(cwd); - expect(unknownTypeWarnings()).toEqual([]); - expect(skippedCount()).toBe(1); + expect(skippedCount()).toBe(2); }); }); @@ -266,9 +270,43 @@ describe('objectui check — the narrowed judgement surface is never silent (opt // Two leaves plus the manifest: three files had a root `type` string and // no marker. expect(skippedCount()).toBe(3); - const hint = plainLines().find((l) => l.includes('A file is checked when it declares')); - expect(hint).toBeDefined(); - expect(hint).toContain(OBJECTUI_SCHEMA_URL); + const hint = hintLine(); + expect(hint).toContain('children'); + expect(hint).toContain('className'); + // ⛔ The hint must not advise declaring a `$schema` URL: no such URL exists + // and no arm reads one (maintainer ruling 2026-08-20, verbatim 「C」). A + // hint naming a way in the build does not honour is worse than no hint — + // the reader follows it, nothing changes, and the command reads as broken + // rather than narrow. This is the "comment claims != code enforces" shape, + // pinned so the sentence cannot outlive the code it describes. + expect(hint).not.toContain('$schema'); + expect(hint).not.toContain('http'); + }); + + it('names only markers the gate actually honours — every key in the hint admits a file', async () => { + // The mechanical version of the assertion above. The hint is READ back and + // each key it advertises is fed to the command on its own file: if the + // sentence ever names a key the gate does not honour, that file is skipped + // and this test goes red. Nothing here restates the key set by hand, so it + // cannot drift from the gate the way a duplicated list would. + writeSchema('package.json', { name: 'x', type: 'module' }); + await check(cwd); + const keys = advertisedKeys(); + // Sanity: the hint advertises a real set, not an empty one. + expect(keys.length).toBeGreaterThan(0); + + // The manifest has served its purpose (it made the hint print); leave it in + // place and it would keep counting as a skipped file in the second run. + rmSync(join(cwd, 'package.json')); + lines = []; + for (const [i, key] of keys.entries()) { + writeSchema(`node-${i}.json`, { type: 'totally-made-up-xyz', [key]: 'x' }); + } + await check(cwd); + // Every advertised key admitted its file, so every file was judged and + // warned; none was skipped. + expect(unknownTypeWarnings()).toHaveLength(keys.length); + expect(skippedCount()).toBe(0); }); it('says nothing when every eligible file carried a marker', async () => { diff --git a/packages/cli/src/commands/check.ts b/packages/cli/src/commands/check.ts index 72a997e21..c5fe4176e 100644 --- a/packages/cli/src/commands/check.ts +++ b/packages/cli/src/commands/check.ts @@ -14,21 +14,6 @@ import { parse as parseJsonc, printParseErrorCode, type ParseError } from 'jsonc import { isKnownSchemaType } from '../utils/known-schema-types.js'; -/** - * The canonical, permanent public identifier for the ObjectUI schema protocol. - * - * ⚠️ PROPOSED — confirmed by the maintainer at merge (objectui#5127). A - * `$schema` URL is a permanent public identifier: files in user projects will - * carry it forever, so it is a maintainer decision, not a dev one. The - * reasoning behind this spelling is in the PR body. - * - * The matcher below deliberately does NOT compare against this string. It - * recognises the ObjectUI ORIGIN, so every file that already declares an - * `objectui.org` schema URL keeps working whichever exact path the maintainer - * confirms. - */ -export const OBJECTUI_SCHEMA_URL = 'https://objectui.org/schema/v1/objectui.schema.json'; - /** * Root keys that positively identify a file as an ObjectUI schema node. * @@ -68,28 +53,34 @@ const OBJECTUI_STRUCTURAL_KEYS: readonly string[] = [ ]; /** - * Does this `$schema` value point at ObjectUI? + * ⛔ There is deliberately NO `$schema` arm, and no ObjectUI `$schema` URL + * exists to declare. * - * Matches on the URL's HOST, not on a literal string, for two reasons. The - * canonical URL above is still awaiting the maintainer's confirmation, and a - * literal comparison would silently stop recognising every migrated file the - * moment the confirmed path differs by one character. And a `startsWith` test - * against a prefix answers YES for `https://objectui.org.example.com/…`, which - * is a different origin entirely — so the value is parsed as a URL and the - * hostname compared, rather than the string matched. + * An earlier revision of this file admitted a file whose root `$schema` had an + * `objectui.org` host, completed by a canonical URL proposed for the + * maintainer to confirm. The maintainer ruled against minting that identifier + * (2026-08-20, objectui#5127, verbatim 「C」), superseding the `$schema` half of + * the 2026-08-19 ruling. The structural arm below is unaffected and stands. + * + * - A `$schema` URL is a permanent public identifier that lives in USERS' + * files, which no sweep can ever reach. Minting the address before the + * document it names exists is backwards. + * - The only consumer that needs a URL rather than any agreed marker is an + * editor resolving `$schema` over HTTP. An agent authoring schemas instead + * needs a contract it can read BEFORE writing — a file, and `node_modules` + * is right there — and a good error AFTER writing, which is this command, + * already version-correct from the installed packages. + * - The value would be untyped, unchecked and host-matched, so an agent that + * mis-remembered the path would be silently accepted: a version-fossil + * generator, in files we cannot fix. + * - Zero files declared it, in this repository or anywhere else, so the arm + * matched nothing. Dropping it gives up no coverage that was being + * delivered. + * + * Because that matching was host-based rather than literal, the arm can be + * added later without invalidating a single file — most soundly once + * `@object-ui/types` ships a generated JSON Schema, which is filed separately. */ -function pointsAtObjectUi(value: unknown): boolean { - if (typeof value !== 'string') return false; - let hostname: string; - try { - ({ hostname } = new URL(value)); - } catch { - // Not a URL — `$schema` is present but says nothing about ObjectUI. - return false; - } - const host = hostname.toLowerCase(); - return host === 'objectui.org' || host.endsWith('.objectui.org'); -} /** * Does this parsed file positively read as an ObjectUI schema (objectui#5127)? @@ -102,9 +93,9 @@ function pointsAtObjectUi(value: unknown): boolean { * about their own `package.json`: 45 of the 46 warnings this repository * produced were exactly that. * - * The fix is producer-side declaration, not a consumer-side skip list: a file - * is judged when it SAYS it is an ObjectUI schema (`$schema`) or when it is - * structurally recognisable as one. A list of filenames to exclude was + * The fix is a positive marker, not a consumer-side skip list: a file is + * judged when it is structurally recognisable as an ObjectUI node. A list of + * filenames to exclude was * considered and rejected — it is a second hand-maintained list of the shape * objectui#5115 had just finished deleting, and it can only ever enumerate the * foreign vocabularies someone already thought of. @@ -116,7 +107,6 @@ function pointsAtObjectUi(value: unknown): boolean { * the skipped-file count below keeps visible in the meantime. */ function isObjectUiSchemaFile(content: Record): boolean { - if (pointsAtObjectUi(content.$schema)) return true; return OBJECTUI_STRUCTURAL_KEYS.some((key) => key in content); } @@ -227,9 +217,16 @@ export async function check(cwd: string = process.cwd()) { `Skipped ${skipped} file${skipped === 1 ? '' : 's'} with a root "type" and no ObjectUI schema marker.` ) ); + // The hint names the marker keys by rendering the gate's OWN array, never + // a sentence written alongside it. A hint that describes a way in that the + // build does not honour is worse than no hint: the reader follows it, + // nothing changes, and the command looks broken rather than narrow. The + // previous revision of this line advised declaring a `$schema` URL — an + // arm the 2026-08-20 ruling removed — which is exactly that failure had it + // outlived the code it described. console.log( chalk.dim( - ` A file is checked when it declares "$schema": "${OBJECTUI_SCHEMA_URL}" or carries a structural key (children/body/className/...).` + ` A file is checked when its root carries an ObjectUI structural key: ${OBJECTUI_STRUCTURAL_KEYS.join(', ')}.` ) ); } From 9f118b934628d62c709cebb777e9efe5ca438349 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 09:28:22 +0000 Subject: [PATCH 4/4] docs(changeset): drop the `$schema` arm from the release note (#5127) The note advertised a `$schema` URL as one of two ways a file opts into type judgement. The maintainer's 2026-08-20 ruling removed that arm, so the structural key is the only marker, and a release note describing a way in that does not exist is a defect in its own right. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE --- ...k-schema-positive-marker-and-skip-count.md | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/.changeset/check-schema-positive-marker-and-skip-count.md b/.changeset/check-schema-positive-marker-and-skip-count.md index 3ae7d8a04..84f3ac9f6 100644 --- a/.changeset/check-schema-positive-marker-and-skip-count.md +++ b/.changeset/check-schema-positive-marker-and-skip-count.md @@ -2,7 +2,7 @@ '@object-ui/cli': minor --- -`objectui check` judges a file's `type` only when the file says it is an ObjectUI schema, and reports how many it declined to judge. +`objectui check` judges a file's `type` only when the file is recognisable as an ObjectUI schema, and reports how many it declined to judge. A root `type` was treated as a component key wherever it appeared. `type` heads at least seven unrelated JSON vocabularies, and the most common of them is @@ -11,25 +11,29 @@ least seven unrelated JSON vocabularies, and the most common of them is manifest. Measured at this repository's root: 46 warnings, 45 of them `package.json` (objectui#5127). -A file now enters type judgement only when it positively reads as an ObjectUI -schema — it declares `"$schema"` pointing at an `objectui.org` URL, or it carries -a structural key from `BaseSchema` (`children`, `body`, `className`, -`placeholder`, `style`, the `visible`/`hidden`/`disabled` predicate family, -`testId`, `ariaLabel`). Every other root-`type` vocabulary — JSON Schema's -`"array"`, an `.eslintrc.json`'s `"commonjs"`, a package manifest's `"module"` — -is simply never judged. The `$schema` match is on the URL's host rather than a -literal string, so a lookalike origin is not accepted and the canonical spelling -can be confirmed without invalidating files that already declare it. +A file now enters type judgement only when its root carries a structural key +declared on `BaseSchema` — `children`, `body`, `className`, `placeholder`, +`style`, the `visible`/`hidden`/`disabled` predicate family, `testId`, +`ariaLabel`. Every other root-`type` vocabulary — JSON Schema's `"array"`, an +`.eslintrc.json`'s `"commonjs"`, a package manifest's `"module"` — is simply +never judged. The key set is read out of the node contract rather than invented, +and it is closed: it grows only when `BaseSchema` grows. A list of filenames to exclude was the alternative and was rejected: it is a second hand-maintained list of the shape objectui#5115 had just finished deleting, and it can only ever enumerate the foreign vocabularies someone already -thought of. This is producer-side declaration instead. +thought of. This is a positive marker instead. Because the marker narrows what is checked, the command now also reports the -count of files that had a root `type` and no marker, with the `$schema` line that -opts one back in. That number is the coverage this gate gives up until schema -files declare themselves, and printing it is what keeps the loss visible rather -than silent. The `.yaml`/`.yml` half of the scan is unchanged — it was never -type-judged, before this change or after it. Exit codes are untouched: a JSON -parse failure remains the only thing that fails the run. +count of files that had a root `type` and no marker, together with the marker +keys that opt one back in. That number is the coverage this gate gives up until +schema files are recognisable, and printing it is what keeps the loss visible +rather than silent. The `.yaml`/`.yml` half of the scan is unchanged — it was +never type-judged, before this change or after it. Exit codes are untouched: a +JSON parse failure remains the only thing that fails the run. + +No public `$schema` URL is introduced. An earlier revision also admitted a file +whose root `$schema` had an `objectui.org` host; the maintainer ruled against +minting that identifier (2026-08-20, objectui#5127), so the structural key is the +only marker. Because the matching was host-based rather than literal, that arm +can be added later without invalidating a single file.