diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9010334e7..939a3d8c40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -427,7 +427,7 @@ The repository root also has a `docs/` directory, and it is **not** part of the **A code block inside one of those records is a SPECIMEN, not an example to copy.** An ADR states what was decided on a date and a dated audit states what was measured on one, so a snippet inside either is part of the record — edited until it compiles, it makes the record say something it never said. Fence such a block `plaintext` (an unhighlighted spelling `scripts/check-doc-fence-languages.mjs` already lists, and one `scripts/check-doc-snippet-types.mjs` does not compile), never `ts` / `tsx` / `typescript`. Code a reader may copy belongs in `content/docs/**` or `skills/objectui/**`, where a gate compiles it and a wrong line can be fixed without falsifying a record. Maintainer ruling 2026-09-08 (objectui#8363); the four records that predate it are not edited — they are named, with their measured counts, in that gate's `UNGATED_DOCS` ledger. -**A `json` fence is what a reader copies into a metadata file, so its body must pass a strict `JSON.parse`.** A block that annotates JSON with `//` or `/* … */` comments or trailing commas is fenced `jsonc` instead, and it must still be one JSON document once those are removed. The tag is how a reader tells a deliberate annotation from a mistake, and a `json` fence that does not parse is always the mistake. The other non-parsing shapes are admitted by no JSON dialect, so rewrite them rather than retag them. A `${…}` expression goes on one line inside its string, because a raw newline in a string is invalid under both tags. A bad/good comparison becomes one fence per document, with its label on the line above. An elision (`...`, `[...]`, a `"..."` member) is removed, which leaves an empty list or the bare node. A counter-example still parses: what is wrong with it belongs in its content, not its syntax. This is the contract `parseJsonFence` in `scripts/check-skill-examples.mjs` enforces on the skills tree's marked fences. ⚠️ Nothing enforces it for `content/docs` today. `scripts/check-doc-expression-carriage.mjs` reads these fences but normalises comments, raw newlines and elisions away by design, and whether a gate should block a `json` fence that does not parse is the open decision objectui#10943. +**A `json` fence is what a reader copies into a metadata file, so its body must pass a strict `JSON.parse`.** A block that annotates JSON with `//` or `/* … */` comments or trailing commas is fenced `jsonc` instead, and it must still be one JSON document once those are removed. The tag is how a reader tells a deliberate annotation from a mistake, and a `json` fence that does not parse is always the mistake. The other non-parsing shapes are admitted by no JSON dialect, so rewrite them rather than retag them. A `${…}` expression goes on one line inside its string, because a raw newline in a string is invalid under both tags. A bad/good comparison becomes one fence per document, with its label on the line above. An elision (`...`, `[...]`, a `"..."` member) is removed, which leaves an empty list or the bare node. A counter-example still parses: what is wrong with it belongs in its content, not its syntax. This is the contract `parseJsonFence` in `scripts/check-skill-examples.mjs` enforces on the skills tree's marked fences. `scripts/check-doc-expression-carriage.mjs` imports that same function for every `json` fence on the surface `check:doc-types` walks (objectui#10943). A `json` fence that does not parse lands on that census's unparsed list, and the pin 'has no blind spot on the corpus it ships against' in `scripts/__tests__/check-doc-expression-carriage.test.ts`, which keeps that list empty, fails the pull request that adds it. The census's comment, raw-newline, trailing-comma and elision tolerances apply to `jsonc` only. ```bash # Start the documentation site dev server diff --git a/packages/vscode-extension/README.md b/packages/vscode-extension/README.md index 0731d1ef77..600ab76e70 100644 --- a/packages/vscode-extension/README.md +++ b/packages/vscode-extension/README.md @@ -122,7 +122,7 @@ Access these commands via the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`): Customize the extension behavior in VSCode settings: -```json +```jsonc { // Preview settings "objectui.preview.port": 3000, diff --git a/scripts/__tests__/check-doc-expression-carriage.test.ts b/scripts/__tests__/check-doc-expression-carriage.test.ts index 1585e115ab..c674c21953 100644 --- a/scripts/__tests__/check-doc-expression-carriage.test.ts +++ b/scripts/__tests__/check-doc-expression-carriage.test.ts @@ -21,12 +21,15 @@ import { parseFence, parseFenceDialect, RENDERER_SOURCE, + requireJsonContract, ROOT_PAGES, runControls, sanitizeFence, splitTopLevel, SURFACE_LABEL, toJsonDialect, + UNPARSED_PRESCRIPTIONS, + unparsedPrescription, } from '../check-doc-expression-carriage.mjs'; import { APP_DOCS as TYPES_APP_DOCS, @@ -35,6 +38,7 @@ import { packageReadmePages as typesPackageReadmePages, ROOT_PAGES as TYPES_ROOT_PAGES, } from '../check-doc-component-types.mjs'; +import { parseJsonFence } from '../check-skill-examples.mjs'; const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..'); const GATE = 'scripts/check-doc-expression-carriage.mjs'; @@ -194,7 +198,9 @@ describe('check-doc-expression-carriage: the controls can see, and can fail', () }); describe('check-doc-expression-carriage: the parse surface', () => { - const parse = (body: string) => parseFence(body); + // These are the `jsonc` tolerances. Since objectui#10943 a `json` fence gets + // none of them; the next describe block pins that half. + const parse = (body: string) => parseFence(body, 'jsonc'); it('drops comments outside strings and keeps a // that is data', () => { const out = parse('{ "type": "text", // a note\n "content": "https://example.com" }'); @@ -290,6 +296,117 @@ describe('check-doc-expression-carriage: the parse surface', () => { }); }); +/** + * objectui#10943, ruled A on 2026-09-28. A `json` fence is parsed STRICTLY with + * the skills tree's own `parseJsonFence`, and `jsonc` keeps the tolerances. The + * maintainer granted this as a strength increase of ONE existing pin, 'has no + * blind spot on the corpus it ships against', and not as a new gate. + * + * Every tolerance body below is pinned on BOTH tags: `json` must reject it and + * `jsonc` must still parse it. If a later edit quietly sent `json` back through + * `sanitizeFence`, the `jsonc` half would stay green and the `json` half would + * go red. A pin needs both halves to see that regression. + */ +describe('check-doc-expression-carriage: `json` is strict, `jsonc` keeps the tolerances (objectui#10943)', () => { + it('judges `json` with the skills tree’s own parseJsonFence, the same function and not a copy', () => { + // Identity, not behavioural equality, for the reason the scan-surface pin + // below gives for its constants: an import has nothing to drift. + expect(requireJsonContract()).toBe(parseJsonFence); + }); + + it('parses a good `json` fence and hands its value on', () => { + const out = parseFence('{\n "type": "text",\n "content": "${user.name}"\n}', 'json'); + expect(out).toEqual({ + ok: true, + reason: null, + values: [{ type: 'text', content: '${user.name}' }], + wrapped: false, + }); + }); + + // One body per tolerance the gate's header lists, plus the object-body retry + // and the multi-document split. This census reads each of them under `jsonc`, + // and none of them is JSON. + const notJson: Array<[string, string]> = [ + ['a line comment', '{\n // a note\n "type": "text"\n}'], + ['a block comment', '{ /* a note */ "type": "text" }'], + ['a raw newline inside a string', '{ "type": "text", "content": "${ a\n ? 1 : 2 }" }'], + ['a trailing comma', '{ "type": "text", "content": "x", }'], + ['an elision', '{ "type": "form", "fields": [...] }'], + ['an object BODY', '"dependencies": {\n "@object-ui/plugin-x": "workspace:*"\n}'], + ['two top-level documents', '{ "type": "a" }\n\n{ "type": "b" }'], + ]; + + it.each(notJson)('rejects %s under `json` with the contract’s own error, and reads it under `jsonc`', (_label, body) => { + const error = parseJsonFence(body, 'json'); + expect(error, 'the contract itself must reject this body, or this row is not about the contract').not.toBeNull(); + + const strict = parseFence(body, 'json'); + expect(strict).toEqual({ ok: false, reason: `invalid-json: ${error}`, values: [], wrapped: false }); + + expect(parseFence(body, 'jsonc').ok).toBe(true); + }); + + it('parses a good `jsonc` fence carrying a comment', () => { + const out = parseFence('{\n // Preview settings\n "objectui.preview.port": 3000,\n}', 'jsonc'); + expect(out.ok).toBe(true); + expect(out.values).toEqual([{ 'objectui.preview.port': 3000 }]); + }); + + it('prescribes the ruled remedy for `json`, and keeps the blind-spot remedy for every other language', () => { + expect(UNPARSED_PRESCRIPTIONS.json).toContain('retag as `jsonc` if the example needs comments or trailing commas'); + expect(unparsedPrescription('json')).toBe(UNPARSED_PRESCRIPTIONS.json); + expect(unparsedPrescription('jsonc')).toBe(UNPARSED_PRESCRIPTIONS.jsonc); + expect(UNPARSED_PRESCRIPTIONS.jsonc).toContain('sanitizeFence'); + }); + + /** + * The census on a throwaway tree: a good `json` fence, the SAME annotated body + * once as `json` and once as `jsonc`. Only the `json` copy may land on the + * unparsed list. The entry must name the file, the fence's opening line, the + * language and the contract's own parse error. The CLI stays report-only (exit + * 0) and prints the ruled remedy. + */ + it('names file, fence line, language and parse error, and the CLI prints the `jsonc` retag remedy', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'carriage-strict-')); + const page = `${DOCS_ROOT}/guide/fences.md`; + const annotated = ['{', ' // VS Code settings are JSONC', ' "objectui.preview.port": 3000', '}']; + fs.mkdirSync(path.join(root, DOCS_ROOT, 'guide'), { recursive: true }); + fs.writeFileSync( + path.join(root, page), + [ + '# Fences', // 1 + '', // 2 + '```json', // 3 + '{ "type": "text", "content": "ok" }', + '```', + '', + '```json', // 7: the one that must land on the list + ...annotated, + '```', + '', + '```jsonc', + ...annotated, + '```', + '', + ].join('\n'), + ); + + const census = analyze(root, { channels: deriveChannels(ROOT), carriage: await loadCarriage() }); + expect(census.counters.fences).toBe(3); + expect(census.counters.parsed).toBe(2); + expect(census.unparsed).toEqual([ + { file: page, line: 7, lang: 'json', reason: `invalid-json: ${parseJsonFence(annotated.join('\n'), 'json')}` }, + ]); + + const run = spawnSync(process.execPath, [GATE, '--root', root], { cwd: ROOT, encoding: 'utf8' }); + expect(run.status, run.stderr).toBe(0); + expect(run.stdout).toContain(`${page}:7 (json) invalid-json: `); + expect(run.stdout).toContain(UNPARSED_PRESCRIPTIONS.json); + fs.rmSync(root, { recursive: true, force: true }); + }); +}); + /** * objectui#7878 — the scan surface. * @@ -520,14 +637,23 @@ describe('check-doc-expression-carriage: the real tree, and the posture', () => // blocking through the back door — the exact thing the ruling forbade. }); + /** + * objectui#10943 strengthened this pin and added no gate. A `json` fence that + * is not JSON now lands on `census.unparsed`, so this assertion is what reds + * the pull request that adds one. The list holds two different things, so the + * message gives the remedy for each language actually on it. The text comes + * from the same constant the CLI prints. + */ it('has no blind spot on the corpus it ships against', async () => { const census = analyze(ROOT, { channels: deriveChannels(ROOT), carriage: await loadCarriage() }); - expect( - census.unparsed, - 'a json fence under content/docs that this gate cannot parse is a fence it says NOTHING ' + - 'about — the size of its blind spot, not a docs rule. Teach `sanitizeFence` the spelling ' + - `(see the tolerances in ${GATE}'s header), or fix the fence if it is simply malformed.`, - ).toEqual([]); + const langs: string[] = [...new Set(census.unparsed.map((fence: { lang: string }) => fence.lang))]; + const remedies = langs.map((lang) => + lang === 'json' + ? `A \`json\` fence here is NOT JSON — a docs defect, not a blind spot: ${unparsedPrescription(lang)}.` + : `A \`${lang}\` fence here is one this gate says NOTHING about — the size of its blind spot, ` + + `not a docs rule: ${unparsedPrescription(lang)}.`, + ); + expect(census.unparsed, remedies.join(' ')).toEqual([]); }); /** @@ -693,7 +819,9 @@ describe('check-doc-expression-carriage: the real tree, and the posture', () => // those modules for the imports to resolve at all. If this list ever falls // behind the gate's imports the failure is a module-resolution stack trace // rather than the message below, which is why the message is asserted and not - // merely the exit code. + // merely the exit code. `check-skill-examples.mjs` is deliberately NOT copied: + // objectui#10943's import of it is guarded, because it loads `typescript`, and + // an orphan missing it must still reach the message below. for (const file of [ 'check-doc-expression-carriage.mjs', 'check-doc-component-types.mjs', diff --git a/scripts/check-doc-expression-carriage.mjs b/scripts/check-doc-expression-carriage.mjs index 4614974d8c..fff4883aec 100644 --- a/scripts/check-doc-expression-carriage.mjs +++ b/scripts/check-doc-expression-carriage.mjs @@ -14,8 +14,9 @@ * node scripts/check-doc-expression-carriage.mjs --list every fence, parsed or not * node scripts/check-doc-expression-carriage.mjs --self-test the controls alone * Exit: 0 = the census ran, whatever it found. 1 = the INSTRUMENT is broken — - * a derivation returned nothing, the spec artifact is missing, or a - * built-in control failed. Never 1 for a finding; see "Report-only" below. + * a derivation returned nothing, the spec artifact is missing, the + * strict `json` contract could not be imported, or a built-in control + * failed. Never 1 for a finding; see "Report-only" below. * * ## The hole this measures (objectui#7851) * @@ -98,11 +99,16 @@ * A fence this file cannot parse is a fence it says nothing about, and a census * that reports only its hits hides how much it never read. So every run prints * `parsed` and `unparsed` PER FENCE LANGUAGE (`json` and `jsonc` today) and - * names every unparsed fence with its reason; `--list` prints the whole - * inventory, parsed and unparsed alike. - * objectui#7418's prototype reached 0 unparsed on `guide/expressions.md` - * (39 of 39); this file reaches 0 unparsed over the whole tree, which takes - * four tolerances beyond `JSON.parse`, all of them REMOVALS of non-data or an + * names every unparsed fence with its file, its opening line, its language and + * the parse error; `--list` prints the whole inventory, parsed and unparsed + * alike. Whether that list is empty on the real tree is not claimed here: the + * pin 'has no blind spot on the corpus it ships against' in this file's test + * re-derives it on every run. + * + * The two languages are held to DIFFERENT contracts (objectui#10943, see the + * next section). A `json` fence is parsed STRICTLY, with none of what follows. + * A `jsonc` fence — and only a `jsonc` fence among the judged — gets four + * tolerances beyond `JSON.parse`, all of them REMOVALS of non-data or an * envelope around it. None of them can invent a key: * * 1. Line comments and block comments outside strings. The pages annotate @@ -117,7 +123,45 @@ * * Plus one retry, not a tolerance: a fence that is an object BODY rather than * an object (`"dependencies": { … }`, a package.json excerpt) is retried - * wrapped in braces. + * wrapped in braces. Like the four tolerances, it runs for `jsonc` and never + * for `json`. The fences the blind-spot measurement reads (every language this + * gate does not judge, below) go through the same tolerant path, because that + * leg only asks whether a body holds a typed node; ⛔ it judges nothing. + * + * ## `json` is strict and `jsonc` is annotated: one contract with the skills tree + * + * objectui#10943, ruled A on that card on 2026-09-28. The maintainer named it + * as an exception under 「新增门禁默认否」: a strength increase of this census's + * one existing pin, not a new gate. A `json` fence is what a reader copies into a + * metadata file, so it is parsed with `parseJsonFence` from + * `check-skill-examples.mjs`: `JSON.parse` and nothing else. That is the contract + * the skills tree already holds its examples to. The function is IMPORTED + * rather than re-spelled, so the two doc trees hold one `json` / `jsonc` + * contract rather than two that agree today. No tolerance, no object-body retry + * and no multi-document split applies to `json`. A `json` fence that needs one + * of them is not JSON, and the remedy is 「retag as `jsonc` if the example + * needs comments or trailing commas」, or else fixing the fence. + * `UNPARSED_PRESCRIPTIONS` below holds that sentence, and the CLI and the pin + * both print it from there. + * + * What changed is WHO gets red, not the posture below. The CLI still exits 0 + * on anything it reads in a page. The blocking half is the test pin that was + * already there, 'has no blind spot on the corpus it ships against', which + * asserts the unparsed list is empty. Before objectui#10943, a `json` fence with + * a comment or a trailing comma parsed through the tolerances and never reached + * that list. Now it does, so the pull request that adds one goes red. + * + * ⛔ Not generalised here, by the same ruling: the `jsonc` tolerance list is + * not widened, and no other fence language gains a parse check. + * + * ⚠️ The import is a guarded dynamic `import()`, not a static one, and for a + * measured reason. `check-skill-examples.mjs` imports `typescript` and + * `check-doc-snippet-types.mjs` at load. In a checkout with no install, a + * static import fails at module LINK, before the CLI can print its "A failure, + * not a skip" line, and the orphan pin in this file's test then reads a + * module-resolution stack trace instead. So the failure is held until + * `requireJsonContract` raises it, inside the same instrument check that + * reports a missing `@objectstack/spec`. * * And one normalization that is NOT on the judged path at all — `toJsonDialect`, * which de-dialects a JS object literal (unquoted keys, single-quoted strings) @@ -238,6 +282,65 @@ import { closesFence, openFence } from './markdown-fence-scan.mjs'; const scriptDir = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(scriptDir, '..'); +// ── The strict `json` contract, imported (objectui#10943) ───────────────────── + +/** + * `parseJsonFence` from `check-skill-examples.mjs`, the one reading of "a `json` + * fence parses" in this repository. It is held here rather than imported + * statically for the reason the header gives: that module loads `typescript` at + * import time, and a static import would turn an uninstalled checkout's loud + * instrument failure into a module-resolution stack trace. + * + * One `const` DECLARATION, and not a top-level `try`: `check:entry-guard` + * refuses a statement that runs on import in a file that exports, and a + * rejected `import()` settles into `error` here instead of throwing. + */ +const jsonContractLoad = await import('./check-skill-examples.mjs').then( + (module) => ({ parseJsonFence: module.parseJsonFence, error: null }), + (error) => ({ parseJsonFence: null, error }), +); + +/** + * The imported contract, or a loud failure. ⛔ Never a fallback: judging a + * `json` fence with anything but the skills tree's parser is how the two doc + * trees would drift back into two contracts. + */ +export function requireJsonContract() { + if (typeof jsonContractLoad.parseJsonFence === 'function') return jsonContractLoad.parseJsonFence; + const cause = jsonContractLoad.error instanceof Error ? ` (${jsonContractLoad.error.message})` : ''; + throw new Error( + `the strict \`json\` contract, \`parseJsonFence\` in scripts/check-skill-examples.mjs, could not be ` + + `imported${cause}. Run \`pnpm install\` first. This gate does not judge a \`json\` fence with a ` + + 'parser of its own.', + ); +} + +/** + * The remedy for an unparsed fence, by language. The census CLI and the pin in + * this file's test both print it from here, so the two cannot prescribe + * different fixes. + * + * The two entries are two different things. A `json` entry is a DOCS DEFECT: + * the fence is not JSON, and its remedy is the sentence objectui#10943 ruled. A + * `jsonc` entry is this instrument's BLIND SPOT: a spelling the tolerances do + * not cover. Its remedy is the one this pin carried before that card. + */ +export const UNPARSED_PRESCRIPTIONS = { + json: + 'retag as `jsonc` if the example needs comments or trailing commas; otherwise fix the fence. A `json` ' + + 'fence is parsed STRICTLY (`JSON.parse` and nothing else, the contract `parseJsonFence` in ' + + 'scripts/check-skill-examples.mjs holds the skills tree to), because it is what a reader copies into ' + + 'a metadata file', + jsonc: + 'teach `sanitizeFence` the spelling (see the tolerances in scripts/check-doc-expression-carriage.mjs’s ' + + 'header), or fix the fence if it is simply malformed', +}; + +/** The prescription for one unparsed fence. Only `json` is strict; every other language is `jsonc`'s case. */ +export function unparsedPrescription(lang) { + return lang === 'json' ? UNPARSED_PRESCRIPTIONS.json : UNPARSED_PRESCRIPTIONS.jsonc; +} + /** * The guide tree, the first leg of the walk. Spelled here because * `check-doc-component-types.mjs` declares its own `DOCS_ROOT` as a plain `const` @@ -565,13 +668,33 @@ export function splitTopLevel(text) { return { values, reason: junk.trim() === '' ? null : 'text-outside-any-value' }; } -/** Parse one fence body, with the object-BODY retry the header describes. */ -export function parseFence(body) { +/** + * Parse one fence body. `lang` is the fence's info string, and it picks the + * contract (objectui#10943): + * + * `json` STRICT: the imported `parseJsonFence`, which is `JSON.parse` + * and nothing else. No tolerance, no object-body retry and no + * multi-document split. + * anything else `jsonc`, and the unscanned languages the blind-spot leg + * measures: the four tolerances and the object-BODY retry the + * header describes, exactly as before that card. + */ +export function parseFence(body, lang) { // Every return carries the same four fields. A result whose SHAPE depends on // the outcome makes every caller — this file's own census and the pins in // `scripts/__tests__` alike — narrow a union before it can read `values`, and // `tsconfig.scripts.json` type-checks those pins. const failed = (reason) => ({ ok: false, reason, values: [], wrapped: false }); + + if (lang === 'json') { + const error = requireJsonContract()(body, 'json'); + if (error !== null) return failed(`invalid-json: ${error}`); + // The contract has just accepted this exact body, and its `json` branch IS + // `JSON.parse(body)`. Parsing again only reads the value out; it cannot + // disagree with the verdict. + return { ok: true, reason: null, values: [JSON.parse(body)], wrapped: false }; + } + const attempt = (text) => { const { values, reason } = splitTopLevel(text); if (reason) return failed(reason); @@ -698,6 +821,9 @@ export function toJsonDialect(source) { * tolerances, same "a fence that is an object BODY" retry — the ONE difference is * that the body is de-dialected first, so the blind-spot leg asks its question of * both spellings instead of only the one that happens to be JSON. + * + * No language is passed, so this is the tolerant path: the leg measures + * unscanned fences and judges none of them (objectui#10943 left it untouched). */ export function parseFenceDialect(body) { return parseFence(toJsonDialect(body)); @@ -728,8 +854,10 @@ export function scanFences(root) { body, scanned, // A fence outside the scanned languages is parsed too, but only to - // MEASURE the dialect blind spot below — it is never judged. - ...parseFence(body.join('\n')), + // MEASURE the dialect blind spot below — it is never judged. The + // language picks the contract: strict for `json`, the tolerances for + // everything else (objectui#10943). + ...parseFence(body.join('\n'), open.lang), }); open = null; body = []; @@ -859,7 +987,9 @@ export function analyze(root, { channels, carriage }) { bump(fence.lang, fence.ok ? 'parsed' : 'unparsed'); if (!fence.ok) { counters.unparsed++; - unparsed.push({ file: fence.file, line: fence.line, reason: fence.reason }); + // File, opening line, language and the parse error: the language is what + // decides which remedy applies (`unparsedPrescription`, objectui#10943). + unparsed.push({ file: fence.file, line: fence.line, lang: fence.lang, reason: fence.reason }); continue; } counters.parsed++; @@ -949,7 +1079,9 @@ export const CONTROL_FIXTURES = { export function runControls({ channels, carriage }) { const judge = (source) => { - const parsed = parseFence(source); + // Both fixtures are strict JSON, judged the way the census judges a `json` + // fence, so every run also exercises the imported contract (objectui#10943). + const parsed = parseFence(source, 'json'); if (!parsed.ok) return { failed: `the fixture did not parse: ${parsed.reason}` }; const found = []; for (const value of parsed.values) { @@ -1044,6 +1176,9 @@ if (isEntrypoint(import.meta.url)) { try { channels = deriveChannels(); carriage = await loadCarriage(); + // objectui#10943: the strict `json` contract is an input like the two above, + // and a missing one is the same loud instrument failure, never a skip. + requireJsonContract(); } catch (error) { console.error( `❌ ${error.message}\n\n` + @@ -1137,11 +1272,27 @@ if (isEntrypoint(import.meta.url)) { if (counters.unparsed === 0) { console.log('✅ Blind spot: none — every fence above was parsed and judged.'); } else { - console.log( - `\n⚠️ ${counters.unparsed} fence(s) this gate could NOT read — its blind spot, printed because a\n` + - ' census that reports only its hits hides how much it never looked at:', - ); - for (const fence of unparsed) console.log(` ${fence.file}:${fence.line} ${fence.reason}`); + // objectui#10943: two different things share this list, so they are printed + // apart, each with its own remedy. A `json` entry is a fence that is not JSON. + // Any other entry is a spelling the tolerances cannot read. + const notJson = unparsed.filter((fence) => fence.lang === 'json'); + const blind = unparsed.filter((fence) => fence.lang !== 'json'); + if (notJson.length > 0) { + console.log( + `\n⚠️ ${notJson.length} \`json\` fence(s) are not JSON. \`json\` is parsed STRICTLY, with no tolerance ` + + '(objectui#10943):', + ); + for (const fence of notJson) console.log(` ${fence.file}:${fence.line} (json) ${fence.reason}`); + console.log(` Remedy: ${unparsedPrescription('json')}.`); + } + if (blind.length > 0) { + console.log( + `\n⚠️ ${blind.length} fence(s) this gate could NOT read — its blind spot, printed because a\n` + + ' census that reports only its hits hides how much it never looked at:', + ); + for (const fence of blind) console.log(` ${fence.file}:${fence.line} (${fence.lang}) ${fence.reason}`); + console.log(` Remedy: ${unparsedPrescription('jsonc')}.`); + } } if (list) { console.log('\nEvery fence, in document order:');