test: гард check-typeset-coverage — вывод meta-info замыкается на вход meta-compile

Связь «прочитал вывод → подал на вход» держалась на честном слове и рвалась
молча в обе стороны: meta-info печатал метки, которых нет в словаре
meta-compile (33 из 41), а в карте meta-info не было видов, которые
meta-compile эмитит, — и они печатались по-английски посреди русского вывода.
Снэпшоты этого не видят: кейсы читают и компилируют по отдельности, круг не
замыкая.

Инвариант строгий в одну сторону: каждая метка читалки обязана быть ключом в
typeSynonyms у meta-compile и вести ровно в тот же канон. Обратное неверно
намеренно — компилятор знает формы, которых читалка не печатает. Заодно гард
сверяет objectTypeMap и refTypeMap между портами навыка: переводы — часть
вывода, и расхождение портов дало бы разный текст на один и тот же файл.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-09-20 17:35:19 +03:00
co-authored by Claude Opus 5
parent 74e88e75c2
commit 1d8a6f2f58
3 changed files with 132 additions and 0 deletions
+19
View File
@@ -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]): фикстура
+1
View File
@@ -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 навыки: позиционным остаётся только путь ко входу'],
+112
View File
@@ -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);