From 1d8a6f2f58b3719d0a65d054fc546a621767be9d Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 20 Sep 2026 17:35:19 +0300 Subject: [PATCH] =?UTF-8?q?test:=20=D0=B3=D0=B0=D1=80=D0=B4=20check-typese?= =?UTF-8?q?t-coverage=20=E2=80=94=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=20meta-?= =?UTF-8?q?info=20=D0=B7=D0=B0=D0=BC=D1=8B=D0=BA=D0=B0=D0=B5=D1=82=D1=81?= =?UTF-8?q?=D1=8F=20=D0=BD=D0=B0=20=D0=B2=D1=85=D0=BE=D0=B4=20meta-compile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Связь «прочитал вывод → подал на вход» держалась на честном слове и рвалась молча в обе стороны: meta-info печатал метки, которых нет в словаре meta-compile (33 из 41), а в карте meta-info не было видов, которые meta-compile эмитит, — и они печатались по-английски посреди русского вывода. Снэпшоты этого не видят: кейсы читают и компилируют по отдельности, круг не замыкая. Инвариант строгий в одну сторону: каждая метка читалки обязана быть ключом в typeSynonyms у meta-compile и вести ровно в тот же канон. Обратное неверно намеренно — компилятор знает формы, которых читалка не печатает. Заодно гард сверяет objectTypeMap и refTypeMap между портами навыка: переводы — часть вывода, и расхождение портов дало бы разный текст на один и тот же файл. Co-Authored-By: Claude Opus 5 (1M context) --- tests/skills/README.md | 19 ++++ tests/skills/check-all.mjs | 1 + tests/skills/check-typeset-coverage.mjs | 112 ++++++++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 tests/skills/check-typeset-coverage.mjs diff --git a/tests/skills/README.md b/tests/skills/README.md index 271e9e891..c558949d4 100644 --- a/tests/skills/README.md +++ b/tests/skills/README.md @@ -108,6 +108,7 @@ node tests/skills/check-inline-drift.mjs --list # реестр: семья → | `check-type-synonyms.mjs` | словари типов: по общему ключу meta-edit понимает то же, что meta-compile (авторитет) | | `check-inline-drift.mjs` | копии общих inline-утилит совпадают с эталонами своих вариантов | | `check-type-maps.mjs` | карты типов метаданных согласованы с таблицей `ChildObjects` из `docs/1c-configuration-spec.md` | +| `check-typeset-coverage.mjs` | имена типов: всё, что печатает `meta-info`, принимает `meta-compile` (авторитет); карты не разошлись между портами | | `check-format-versions.mjs` | проверенный диапазон версий формата: границы одинаковы во всех навыках и сходятся с лестницей из `docs/1c-configuration-spec.md` | | `check-form-purposes.mjs` | таблица назначений форм в `form-add`: состав видов, свойство «основная форма» и оба порта сходятся с `docs/1c-form-spec.md` | | `check-positional-binding.mjs` | read-only навыки (`*-info` / `*-validate` / `cfe-diff`): позиционным остаётся только путь ко входу, лишний позиционный аргумент не перезаписывает указанный файл | @@ -122,6 +123,24 @@ node tests/skills/check-inline-drift.mjs --list # реестр: семья → Правишь общую утилиту — правишь эталон, копируешь во всех потребителей варианта, прогоняешь гард. +`check-typeset-coverage.mjs` замыкает круг «прочитал вывод → подал на вход». `meta-info` — +читающий навык, и модель несёт его строки обратно в `meta-compile`; до гарда эта связь держалась на +честном слове и рвалась молча в обе стороны. `meta-info` печатал `ДокументОбъект.Заказ` в источниках +подписки, а компилятор отвечал «Неизвестный тип»: русских объектных имён в словаре не было — 33 +метки из 41. И наоборот, в карте читалки не было `ConstantValueManager` и +`ChartOfCalculationTypesObject`, хотя компилятор их эмитит, — типы печатались по-английски посреди +русского вывода. Снэпшоты этого не видят: кейсы читают и компилируют по отдельности, круг не +замыкая. + +Инвариант строгий в одну сторону: каждая метка, которую умеет печатать `meta-info`, обязана быть +ключом в `typeSynonyms` у `meta-compile` и вести ровно в тот же канон. Обратное неверно намеренно — +компилятор знает формы, которых читалка не печатает (английские, полные варианты рядом с +аббревиатурами). Заодно гард сверяет `objectTypeMap`/`refTypeMap` между портами навыка: значения +переводов — часть вывода, и расхождение портов дало бы разный текст на один и тот же файл. + +Добавляешь вид в карту `meta-info` — заводи русскую метку в словарь `meta-compile` (и в копию +`meta-edit`, её держит `check-type-synonyms.mjs`). + `check-nonascii-fs.mjs` держит `tests/common/fsutil.mjs` — единственное место, где тесты, хуки и движок `web-test` удаляют и копируют файлы. На Windows `fs.rmSync` и `fs.cpSync` **молча** ничего не делают, если не-ASCII символы есть в самом аргументе пути ([nodejs/node#61067][nafs]): фикстура diff --git a/tests/skills/check-all.mjs b/tests/skills/check-all.mjs index 29d6b1a26..7daeb6957 100644 --- a/tests/skills/check-all.mjs +++ b/tests/skills/check-all.mjs @@ -16,6 +16,7 @@ const GUARDS = [ ['check-inline-drift.mjs', 'общие inline-реализации: копии совпадают с эталонами'], ['check-ps-define-before-call.mjs', 'порядок в .ps1: функция объявлена выше точки вызова'], ['check-type-maps.mjs', 'карты типов метаданных: согласованы со спецификацией'], + ['check-typeset-coverage.mjs', 'имена типов: вывод meta-info замыкается на вход meta-compile'], ['check-format-versions.mjs', 'проверенный диапазон версий формата: согласован со спецификацией'], ['check-form-purposes.mjs', 'назначения форм в form-add: согласованы со спецификацией и между портами'], ['check-positional-binding.mjs', 'read-only навыки: позиционным остаётся только путь ко входу'], diff --git a/tests/skills/check-typeset-coverage.mjs b/tests/skills/check-typeset-coverage.mjs new file mode 100644 index 000000000..80ba60262 --- /dev/null +++ b/tests/skills/check-typeset-coverage.mjs @@ -0,0 +1,112 @@ +#!/usr/bin/env node +// Раундтрип имён типов: всё, что ПЕЧАТАЕТ meta-info, обязан ПРИНИМАТЬ meta-compile (АВТОРИТЕТ). +// +// Зачем гард. meta-info — читающий навык, и его вывод модель подаёт обратно на вход компилятора. +// До этой проверки связь держалась на честном слове и рвалась молча в обе стороны: +// * meta-info печатал «ДокументОбъект.Заказ» в источниках подписки, а meta-compile отвечал +// «Неизвестный тип» — не были заведены синонимы русских объектных имён (33 метки из 41); +// * в карте meta-info не было ConstantValueManager и ChartOfCalculationTypesObject, хотя +// meta-compile их эмитит, — и типы печатались по-английски посреди русского вывода. +// Ни один снэпшот этого не видел: кейсы читают и компилируют по отдельности, круг не замыкая. +// +// Инвариант СТРОГИЙ в одну сторону: для каждой метки, которую умеет печатать meta-info, в словаре +// meta-compile должен быть ключ (метка в нижнем регистре), ведущий РОВНО в тот же канон. Обратное +// неверно намеренно: компилятор знает формы, которых читалка не печатает (английские, полные +// варианты рядом с аббревиатурами). +// +// Плюс проверяем, что карты не разошлись между портами навыка: значения переводов — часть вывода, +// а расхождение портов дало бы разный текст на один и тот же файл. +// +// Парсит .ps1 как канон, .py как копию. Выход 1 при расхождении. +// Запуск: node tests/skills/check-typeset-coverage.mjs +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const read = (f) => readFileSync(join(ROOT, f), 'utf8'); + +// Метки, зашитые прямо в Format-SingleTypeSet / Format-SingleType, а не в карту. +const INLINE_LABELS = { + Characteristic: 'Характеристика', + DefinedType: 'ОпределяемыйТип', + AnyRef: 'ЛюбаяСсылка', + AnyIBRef: 'ЛюбаяСсылкаИБ', +}; + +// $objectTypeMap = @{ "CatalogObject"="СправочникОбъект"; … } — записи в одну или несколько строк. +function parsePs1Map(text, name) { + const m = new RegExp(`\\$${name} = @\\{([\\s\\S]*?)\\n\\}`).exec(text); + if (!m) throw new Error(`карта $${name} не найдена в .ps1 — реестр протух`); + const map = new Map(); + const re = /"([A-Za-z]+)"\s*=\s*"([^"]+)"/g; + let e; + while ((e = re.exec(m[1])) !== null) map.set(e[1], e[2]); + if (map.size === 0) throw new Error(`карта $${name} пуста — реестр протух`); + return map; +} + +// object_type_map = { "CatalogObject": "СправочникОбъект", … } +function parsePyMap(text, name) { + const m = new RegExp(`^${name} = \\{([\\s\\S]*?)^\\}`, 'm').exec(text); + if (!m) throw new Error(`карта ${name} не найдена в .py — реестр протух`); + const map = new Map(); + const re = /"([A-Za-z]+)"\s*:\s*"([^"]+)"/g; + let e; + while ((e = re.exec(m[1])) !== null) map.set(e[1], e[2]); + if (map.size === 0) throw new Error(`карта ${name} пуста — реестр протух`); + return map; +} + +function parseSynonyms(text) { + const map = new Map(); + const re = /\$script:typeSynonyms\["([^"]+)"\]\s*=\s*"([^"]*)"/g; + let m; + while ((m = re.exec(text)) !== null) map.set(m[1], m[2]); + if (map.size === 0) throw new Error('словарь typeSynonyms не найден в meta-compile — реестр протух'); + return map; +} + +const infoPs1 = read('.claude/skills/meta-info/scripts/meta-info.ps1'); +const infoPy = read('.claude/skills/meta-info/scripts/meta-info.py'); +const synonyms = parseSynonyms(read('.claude/skills/meta-compile/scripts/meta-compile.ps1')); + +let drift = 0; + +// ── 1. Порты навыка-читалки не разошлись ──────────────────────────────────── +for (const [ps1Name, pyName] of [['objectTypeMap', 'object_type_map'], ['refTypeMap', 'ref_type_map']]) { + const a = parsePs1Map(infoPs1, ps1Name); + const b = parsePyMap(infoPy, pyName); + for (const [k, v] of a) { + if (!b.has(k)) { console.log(`DRIFT ${pyName}: нет ключа "${k}" (в .ps1 = "${v}")`); drift++; } + else if (b.get(k) !== v) { console.log(`DRIFT ${pyName}."${k}" = "${b.get(k)}" != .ps1 "${v}"`); drift++; } + } + for (const k of b.keys()) { + if (!a.has(k)) { console.log(`DRIFT ${ps1Name}: нет ключа "${k}", который есть в .py`); drift++; } + } + console.log(` ${ps1Name}: ${a.size} меток, порты совпадают`); +} + +// ── 2. Каждая метка читалки принимается компилятором ──────────────────────── +const labels = new Map([ + ...parsePs1Map(infoPs1, 'objectTypeMap'), + ...parsePs1Map(infoPs1, 'refTypeMap'), + ...Object.entries(INLINE_LABELS), +]); + +for (const [canon, label] of [...labels].sort((x, y) => x[1].localeCompare(y[1]))) { + const key = label.toLowerCase(); + if (!synonyms.has(key)) { + console.log(`DRIFT meta-info печатает "${label}", а meta-compile его не знает (ждётся synonyms["${key}"] = "${canon}")`); + drift++; + } else if (synonyms.get(key) !== canon) { + console.log(`DRIFT synonyms["${key}"] = "${synonyms.get(key)}", а meta-info печатает это как "${canon}"`); + drift++; + } +} +if (drift === 0) console.log(` меток читалки: ${labels.size}, все приняты компилятором`); + +console.log(drift === 0 + ? `OK — вывод meta-info замыкается на вход meta-compile (${labels.size} меток)` + : `\n${drift} DRIFT(s) — завести метку в словарь meta-compile или убрать из карты meta-info.`); +process.exit(drift ? 1 : 0);