feat(brain-config): cross-ref имена config-driven (greenfield #3 cross-ref)
docStem снимает версию; buildCrossRefPatterns строит linkRe/crossRe/normalizeName/pathToName из normative_files плюс DEFAULT_ALIASES; CLI спредит в detectMismatches. Дефолт 5 доков = детект как хардкод; greenfield распознаёт свои доки. shell/observer — отдельные планы. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
# greenfield cross-ref config-driven имена — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (инлайн под стеной — субагенты запрещены VA-4; production-правки под TDD-gate override + floor-escape). Steps — checkbox (`- [ ]`).
|
||||
|
||||
**Goal:** Сделать regex-имена нормативных доков в `cross-ref-checker` производными от `normative_files` (config), сохранив поведение claude-brain байт-в-байт.
|
||||
|
||||
**Architecture:** Спека `2026-06-15... → 2026-06-16-greenfield-regex-names-config-design.md` (Подход A). Новый pure `docStem` (снятие версии) + `buildCrossRefPatterns(map, aliases)` строит `{linkRe,crossRe,normalizeName,pathToName}` из config-карты; `DEFAULT_ALIASES` — маленький лидерровский дефолт. `extractCrossRefs`/`detectMismatches` уже принимают эти regex (шов Task 4) — CLI строит и спредит в opts. Дефолт (claude-brain 5 доков) → детект идентичен хардкоду. **Scope: только cross-ref** (shell/observer — отдельные планы).
|
||||
|
||||
**Tech Stack:** Node ESM (.mjs), vitest (`tools/`).
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `tools/cross-ref-checker.mjs` — добавить `docStem` (export), `DEFAULT_ALIASES`, `buildCrossRefPatterns` (export); `buildNormativeMap` fallback → `docStem`; CLI wiring.
|
||||
- Modify: `tools/cross-ref-checker.test.mjs` — тесты `docStem`, `buildCrossRefPatterns`, `buildNormativeMap` docStem-fallback.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `docStem` — снятие версии (pure)
|
||||
|
||||
**Files:** `tools/cross-ref-checker.mjs`, `tools/cross-ref-checker.test.mjs`
|
||||
|
||||
- [ ] **Step 1 (RED test):** в `cross-ref-checker.test.mjs` добавить (импорт `docStem` к существующему импорту из `./cross-ref-checker.mjs`):
|
||||
|
||||
```javascript
|
||||
import { docStem } from './cross-ref-checker.mjs';
|
||||
describe('docStem (#3 cross-ref)', () => {
|
||||
it('срезает директорию, .md и хвост версии', () => {
|
||||
expect(docStem('docs/Pravila_raboty_Claude_v1_1.md')).toBe('Pravila_raboty_Claude');
|
||||
expect(docStem('docs/Tooling_v8_3.md')).toBe('Tooling');
|
||||
expect(docStem('docs/Plugin_stack_rules_v1.md')).toBe('Plugin_stack_rules');
|
||||
expect(docStem('MyRules_v2.md')).toBe('MyRules');
|
||||
expect(docStem('CLAUDE.md')).toBe('CLAUDE');
|
||||
expect(docStem('docs/Report_2024.md')).toBe('Report_2024'); // не версия (нет _v)
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2:** Run RED — `npx vitest run --config vitest.config.tools.mjs tools/cross-ref-checker.test.mjs` → FAIL «docStem is not a function».
|
||||
|
||||
- [ ] **Step 3 (impl):** в `cross-ref-checker.mjs` после `import`-ов (до `NORMATIVE_FILES`) добавить:
|
||||
|
||||
```javascript
|
||||
// Снять директорию, расширение .md и хвост версии (_v1_1 / _v8_3) → стем дока.
|
||||
// Общий для greenfield short-name (cross-ref) и file-stem (shell/observer — отдельные планы).
|
||||
export function docStem(path) {
|
||||
const base = String(path || '').replace(/\\/g, '/').split('/').pop() || '';
|
||||
return base.replace(/\.md$/i, '').replace(/_v\d+(?:[._]\d+)*$/i, '');
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4:** Run GREEN — тот же прогон → PASS docStem.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: `DEFAULT_ALIASES` + `buildCrossRefPatterns` (pure)
|
||||
|
||||
**Files:** `tools/cross-ref-checker.mjs`, `tools/cross-ref-checker.test.mjs`
|
||||
|
||||
- [ ] **Step 1 (RED test):** добавить:
|
||||
|
||||
```javascript
|
||||
import { buildCrossRefPatterns } from './cross-ref-checker.mjs';
|
||||
describe('buildCrossRefPatterns (#3 cross-ref)', () => {
|
||||
const map = buildNormativeMap(['docs/Pravila_raboty_Claude_v1_1.md','docs/Plugin_stack_rules_v1.md','docs/Tooling_v8_3.md']);
|
||||
const grab = (re, nm, t) => [...t.matchAll(re)].map((m) => nm(m[1]));
|
||||
it('детектит имена и алиасы; PSR_v1 раньше PSR; алиас → канон', () => {
|
||||
const { crossRe, normalizeName } = buildCrossRefPatterns(map);
|
||||
expect(grab(crossRe, normalizeName, 'PSR_v1 v3.24')).toEqual(['PSR_v1']);
|
||||
expect(grab(crossRe, normalizeName, 'PSR v3.24')).toEqual(['PSR_v1']);
|
||||
expect(grab(crossRe, normalizeName, 'Pravila v1.44')).toEqual(['Pravila']);
|
||||
});
|
||||
it('pathToName — инверсия карты', () => {
|
||||
expect(buildCrossRefPatterns(map).pathToName['docs/Tooling_v8_3.md']).toBe('Tooling');
|
||||
});
|
||||
it('greenfield: имена из карты, без алиасов', () => {
|
||||
const { crossRe, normalizeName } = buildCrossRefPatterns({ MyRules: 'docs/MyRules_v2.md' }, {});
|
||||
expect(grab(crossRe, normalizeName, 'MyRules v1.0')).toEqual(['MyRules']);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2:** Run RED → FAIL «buildCrossRefPatterns is not a function».
|
||||
|
||||
- [ ] **Step 3 (impl):** в `cross-ref-checker.mjs` после `PATH_TO_NAME`/`UNIVERSAL_VERSION_TRACKED` добавить:
|
||||
|
||||
```javascript
|
||||
// Лидерра-специфичные алиасы (короткие/файловые формы → канон). Дефолт; greenfield без алиасов.
|
||||
const DEFAULT_ALIASES = Object.freeze({ PSR: 'PSR_v1', Plugin_stack_rules: 'PSR_v1', Pravila_raboty_Claude: 'Pravila' });
|
||||
|
||||
function escapeRe(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
||||
|
||||
// Построить regex/normalizeName из карты {имя:путь} ∪ алиасы. Альтернативы — длинные раньше
|
||||
// коротких (prefix-safety: PSR_v1 до PSR). Дефолт-карта (Лидерра 5) → детект как хардкод-regex.
|
||||
export function buildCrossRefPatterns(normativeMap = {}, aliases = DEFAULT_ALIASES) {
|
||||
const names = Object.keys(normativeMap);
|
||||
const paths = Object.values(normativeMap);
|
||||
const pathToName = {};
|
||||
for (const [name, path] of Object.entries(normativeMap)) pathToName[path] = name;
|
||||
const byLenDesc = (a, b) => b.length - a.length;
|
||||
const linkAlt = paths.map(escapeRe).sort(byLenDesc).join('|');
|
||||
const linkRe = new RegExp(
|
||||
`\\[[^\\]]+\\]\\((${linkAlt})\\)(?:[^\\n*]{0,200}?)\\*\\*[^*\\n]*?v(\\d+\\.\\d+(?:\\.\\d+)?)`, 'g');
|
||||
const nameAlt = [...new Set([...names, ...Object.keys(aliases)])].map(escapeRe).sort(byLenDesc).join('|');
|
||||
const crossRe = new RegExp(`\\b(${nameAlt})\\s+v(\\d+\\.\\d+(?:\\.\\d+)?)\\b(?!\\s*→)`, 'g');
|
||||
const normalizeName = (raw) => aliases[raw] || raw;
|
||||
return { linkRe, crossRe, normalizeName, pathToName };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4:** Run GREEN → PASS.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: `buildNormativeMap` fallback → `docStem`
|
||||
|
||||
**Files:** `tools/cross-ref-checker.mjs`, `tools/cross-ref-checker.test.mjs`
|
||||
|
||||
- [ ] **Step 1 (RED test):** добавить:
|
||||
|
||||
```javascript
|
||||
describe('buildNormativeMap greenfield fallback (#3)', () => {
|
||||
it('имя без версии (docStem), не basename с версией', () => {
|
||||
const m = buildNormativeMap(['docs/MyRules_v2.md']);
|
||||
expect(m.MyRules).toBe('docs/MyRules_v2.md');
|
||||
expect(m.MyRules_v2).toBeUndefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2:** Run RED → FAIL (`m.MyRules` undefined, имя было `MyRules_v2`).
|
||||
|
||||
- [ ] **Step 3 (impl):** в `buildNormativeMap` заменить строку fallback:
|
||||
|
||||
```javascript
|
||||
const name = PATH_TO_NAME[p] || docStem(p);
|
||||
```
|
||||
|
||||
(была `PATH_TO_NAME[p] || String(p).split('/').pop().replace(/\.md$/, '')`.)
|
||||
|
||||
- [ ] **Step 4:** Run GREEN → PASS (новые + прежние buildNormativeMap-тесты: claude-brain имена из PATH_TO_NAME, не затронуты).
|
||||
|
||||
---
|
||||
|
||||
## Task 4: CLI wiring — строить patterns из config
|
||||
|
||||
**Files:** `tools/cross-ref-checker.mjs`
|
||||
|
||||
- [ ] **Step 1 (impl):** в CLI-блоке (после try/catch с `normativeMap`, перед `loadFiles`) вставить построение patterns и спред в opts:
|
||||
|
||||
```javascript
|
||||
const patterns = buildCrossRefPatterns(normativeMap);
|
||||
const files = loadFiles(process.cwd(), normativeMap);
|
||||
const m = detectMismatches(files, { normativeFiles: normativeMap, ...patterns });
|
||||
```
|
||||
|
||||
(заменяет текущие строки 147-148 `const files = ...` + `const m = detectMismatches(files, { normativeFiles: normativeMap });`. `detectMismatches` форвардит opts в `extractCrossRefs` — построенные linkRe/crossRe/normalizeName/pathToName применяются. claude-brain map=5 → детект идентичен.)
|
||||
|
||||
- [ ] **Step 2 (verify):** владелец в терминале: `npx vitest run --config vitest.config.tools.mjs tools/cross-ref-checker.test.mjs` → PASS (новые + все прежние ~20). Затем полный свод → без регрессий.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Полный свод + коммит (терминал владельца)
|
||||
|
||||
- [ ] **Step 1:** `npx vitest run --config vitest.config.tools.mjs` → зелёный, новых падений нет.
|
||||
- [ ] **Step 2:** commit:
|
||||
|
||||
```bash
|
||||
git add tools/cross-ref-checker.mjs tools/cross-ref-checker.test.mjs docs/superpowers/specs/2026-06-16-greenfield-regex-names-config-design.md docs/superpowers/plans/2026-06-16-greenfield-cross-ref-config-plan.md docs/superpowers/plans/2026-06-16-classifier-context-wiring-plan.md
|
||||
git commit -m "feat(brain-config): cross-ref имена config-driven (greenfield #3 cross-ref)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** §3 (cross-ref) — Task 1 (docStem §2), Task 2 (buildCrossRefPatterns + DEFAULT_ALIASES + prefix-safety), Task 3 (buildNormativeMap docStem-fallback §2), Task 4 (CLI). §4/§5 (shell/observer) — вне scope (отдельные планы). §6 fail-safe: пустой config → `buildNormativeMap([])` = universal-only → patterns строятся из CLAUDE/MEMORY (тест «greenfield»/прежние покрывают форму).
|
||||
- **Placeholders:** нет; весь код реальный.
|
||||
- **Type consistency:** `docStem`/`buildCrossRefPatterns`/`DEFAULT_ALIASES`/`escapeRe` — единые имена; `{linkRe,crossRe,normalizeName,pathToName}` совпадает с opts `extractCrossRefs`.
|
||||
- **Стена:** production-правки `cross-ref-checker.mjs` под TDD-gate override + floor-escape (как #2); тест-правки `.test.mjs` — gate молчит (не production). Полный свод + коммит — терминал владельца (harness-collapse).
|
||||
@@ -0,0 +1,98 @@
|
||||
# Дизайн: greenfield-hardening regex-имён нормативки из config (Task 7 follow-up #3)
|
||||
|
||||
**Дата:** 2026-06-16
|
||||
**Статус:** Approved (brainstorming, владелец одобрил «ок»)
|
||||
**Контекст:** третий пункт остатка Фазы 1 (handoff №5 §6). Дизайн §7 normative-files-config-model
|
||||
ссылался на «Подход #2 из brainstorming» без деталей — подход (пере)установлен здесь.
|
||||
**Масштаб (решение владельца):** minimal-safe prep перед Фазой 2 (конкретного гринфилд-наката нет),
|
||||
YAGNI. **Подход A** (config-имена + алиасы-дефолт), НЕ богатая настройка (B).
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель и контракт
|
||||
|
||||
Захардкоженные имена/стемы нормативных доков (`Pravila`/`PSR_v1`/`Tooling` + сокращения) в 3 движковых
|
||||
файлах выводятся из настройки `normative_files`, чтобы движок распознавал нормативку **чужого** проекта.
|
||||
|
||||
**Контракт:** дефолты = текущие лидерровские значения → `claude-brain` (Лидерра) ведёт себя **байт-в-байт**
|
||||
по поведению (детект cross-ref, protected-set, norm-классификация неизменны). Greenfield с другими именами
|
||||
доков распознаётся по своей настройке. Пустой config → universal-only / база (fail-safe, не падение).
|
||||
|
||||
**Граница (YAGNI):** короткое имя (`Pravila`) generically из пути НЕ выводится — для greenfield short-name =
|
||||
basename. Алиасы (`PSR`↔`PSR_v1`, `Pravila_raboty_Claude`↔`Pravila`) — только лидерровские, как маленький
|
||||
хардкод-дефолт. Это «minimal», не богатая per-doc config-модель (отклонённый B).
|
||||
|
||||
## 2. Производные имён из пути (общий алгоритм версии)
|
||||
|
||||
`docStem(path)` — общий алгоритм снятия версии: basename → срезать `.md` → срезать хвост версии
|
||||
`_v<цифры>(_<цифры>)*` (формы `_v1_1`, `_v8_3`): `docs/Pravila_raboty_Claude_v1_1.md` → `Pravila_raboty_Claude`.
|
||||
|
||||
Из пути выводятся ДВЕ формы (нужны разным потребителям):
|
||||
- **file-stem** = `docStem(path)` — `Pravila_raboty_Claude`. Потребители: shell-content-rules (protect),
|
||||
observer (norm-классификация).
|
||||
- **short-name** = `PATH_TO_NAME[path]` (Лидерра: `Pravila`) **иначе** `docStem(path)` (greenfield).
|
||||
Версионный хвост СРЕЗАН → `MyRules`, не `MyRules_v2` — это снимает неоднозначность «basename с версией»
|
||||
(замечание наставника): greenfield short-name всегда без версии. Потребитель: cross-ref (version-tracking).
|
||||
|
||||
`buildNormativeMap` (§3) обновляет fallback с `.replace(/\.md$/, '')` (оставлял версию) на `docStem`.
|
||||
claude-brain не затронут — его имена из `PATH_TO_NAME`; тестов на version-keeping fallback нет.
|
||||
|
||||
## 3. Компонент 1 — cross-ref-checker.mjs
|
||||
|
||||
- Новая чистая `buildCrossRefPatterns(normativeMap, aliases = DEFAULT_ALIASES)` → `{ linkRe, crossRe,
|
||||
normalizeName, pathToName }`:
|
||||
- `pathToName` — инверсия `normativeMap` ({path: name}).
|
||||
- `linkRe` — alternation путей (`Object.values(normativeMap)`, экранированы).
|
||||
- `crossRe` — alternation имён ∪ ключей `aliases`, отсортированных **длинные-раньше** (prefix-safety:
|
||||
`PSR_v1` до `PSR`) → детект как сейчас.
|
||||
- `normalizeName` — `aliases[raw] || raw` (config-имена — identity; лидерровские алиасы → канон).
|
||||
- `DEFAULT_ALIASES = { PSR: 'PSR_v1', Plugin_stack_rules: 'PSR_v1', Pravila_raboty_Claude: 'Pravila' }`
|
||||
(хардкод-дефолт, только лидерровские).
|
||||
- `extractCrossRefs`/`detectMismatches` уже принимают `linkRe`/`crossRe`/`normalizeName`/`pathToName`
|
||||
параметрами (шов Task 4) — CLI строит через `buildCrossRefPatterns(buildNormativeMap(loadConfig().normative_files))`
|
||||
и передаёт. Дефолт (claude-brain map = 5) → behavior-идентичен текущим хардкод-regex.
|
||||
|
||||
## 4. Компонент 2 — shell-content-rules.mjs
|
||||
|
||||
- `buildProtectedPatterns(configPaths)` уже UNION база + config (§D2). Добавить: нормативные защитные
|
||||
паттерны выводятся из `normative_files` (file-stem) и попадают в augment; **убрать** хардкод 32-34
|
||||
(`/Pravila_raboty_Claude/`, `/Plugin_stack_rules/`, `/Tooling_v8_3/`) из `DEFAULT_PROTECTED_PATTERNS`.
|
||||
- Реализация: новая чистая `normativeStemPatterns(normativeFiles)` → массив `/<stem>/i`; CLI/потребитель
|
||||
передаёт их (вместе с `protected_paths`) в `buildProtectedPatterns`. Общая база (CLAUDE/memory/.env/
|
||||
settings/…) — без изменений.
|
||||
- claude-brain: `normative_files` (3) → стемы = текущие 32-34 → защищены как сейчас. Greenfield: его доки
|
||||
защищены. **Fail-safe:** пустой `normative_files` → нормативных стемов нет, база защищает universal.
|
||||
|
||||
## 5. Компонент 3 — observer-transcript-parser.mjs
|
||||
|
||||
- `classifyFilePath(path, normativeStems = DEFAULT_NORMATIVE_STEMS)` — 371-373 заменяются циклом по
|
||||
`normativeStems` (стем → `(?:^|/)<stem>[^/]*\.md$` → `'norm'`). Дефолт = текущие 3 стема.
|
||||
- Универсальные (`CLAUDE.md`/`MEMORY.md`/`memory/`, 370/375/376) + `Открытые_вопросы` (374) — хардкод
|
||||
(универсальные / Лидерра-спец, YAGNI).
|
||||
- §/R citation-паттерны (529-531/578-579, `Pravila §N` / `PSR_v1 RN`) — **не трогаем**: только добавляют
|
||||
сигнал, greenfield без них безопасен (Лидерра-нотация).
|
||||
- Wiring: потребитель `classifyFilePath` (CLI/caller) выводит стемы из `loadConfig().normative_files`.
|
||||
|
||||
## 6. Fail-safe (стиль §5.1)
|
||||
|
||||
| Ключ | Пусто/нет | Направление |
|
||||
|---|---|---|
|
||||
| `normative_files` (cross-ref) | crossRe/linkRe = только universal (CLAUDE/MEMORY) | safe degrade |
|
||||
| `normative_files` (shell) | нормативных стемов нет → база защищает universal | safe (no false-block) |
|
||||
| `normative_files` (observer) | стемов нет → norm-классификация только universal | safe degrade |
|
||||
|
||||
## 7. Testing (TDD)
|
||||
|
||||
Чистые builder-функции — RED→GREEN:
|
||||
- `buildCrossRefPatterns` — дефолт-карта даёт детект как текущий хардкод; greenfield-карта даёт свои имена;
|
||||
prefix-safety (PSR_v1 до PSR); пустой → universal-only.
|
||||
- `normativeStemPatterns` — стемы из путей (версия/расширение срезаны); пустой → [].
|
||||
- `classifyFilePath` с кастомными стемами — greenfield-док → 'norm'; дефолт = текущие 3.
|
||||
Wiring CLI/caller — config-seam (как #2): `await import` + try/catch fallback на дефолт. Полный свод +
|
||||
коммит — терминал владельца (harness-collapse под Claude-Bash).
|
||||
|
||||
## 8. Исполнение под стеной
|
||||
|
||||
Production-файлы → `enforce-tdd-gate` (floor-escape не снимает) + harness-collapse (RED не достать) →
|
||||
правки кода под TDD-gate override + floor-escape per file (как #2). Pure builder-функции с TDD-тестами;
|
||||
тесты — `.test.mjs` (не production, gate молчит). Канон: design v6 + план Фазы 1 + handoff №5 + этот дизайн.
|
||||
@@ -21,12 +21,41 @@ const PATH_TO_NAME = {
|
||||
// Универсальные version-tracked доки (есть у любого проекта; не в настройке).
|
||||
const UNIVERSAL_VERSION_TRACKED = Object.freeze({ CLAUDE: 'CLAUDE.md', MEMORY: 'MEMORY.md' });
|
||||
|
||||
// Снять директорию, расширение .md и хвост версии (_v1_1 / _v8_3) → стем дока.
|
||||
// Общий для greenfield short-name (cross-ref) и file-stem (shell/observer — отдельные планы).
|
||||
export function docStem(path) {
|
||||
const base = String(path || '').replace(/\\/g, '/').split('/').pop() || '';
|
||||
return base.replace(/\.md$/i, '').replace(/_v\d+(?:[._]\d+)*$/i, '');
|
||||
}
|
||||
|
||||
// Лидерра-специфичные алиасы (короткие/файловые формы → канон). Дефолт; greenfield без алиасов.
|
||||
const DEFAULT_ALIASES = Object.freeze({ PSR: 'PSR_v1', Plugin_stack_rules: 'PSR_v1', Pravila_raboty_Claude: 'Pravila' });
|
||||
|
||||
function escapeRe(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
||||
|
||||
// Построить regex/normalizeName из карты {имя:путь} ∪ алиасы. Альтернативы — длинные раньше
|
||||
// коротких (prefix-safety: PSR_v1 до PSR). Дефолт-карта (Лидерра 5) → детект как хардкод-regex.
|
||||
export function buildCrossRefPatterns(normativeMap = {}, aliases = DEFAULT_ALIASES) {
|
||||
const names = Object.keys(normativeMap);
|
||||
const paths = Object.values(normativeMap);
|
||||
const pathToName = {};
|
||||
for (const [name, path] of Object.entries(normativeMap)) pathToName[path] = name;
|
||||
const byLenDesc = (a, b) => b.length - a.length;
|
||||
const linkAlt = paths.map(escapeRe).sort(byLenDesc).join('|');
|
||||
const linkRe = new RegExp(
|
||||
`\\[[^\\]]+\\]\\((${linkAlt})\\)(?:[^\\n*]{0,200}?)\\*\\*[^*\\n]*?v(\\d+\\.\\d+(?:\\.\\d+)?)`, 'g');
|
||||
const nameAlt = [...new Set([...names, ...Object.keys(aliases)])].map(escapeRe).sort(byLenDesc).join('|');
|
||||
const crossRe = new RegExp(`\\b(${nameAlt})\\s+v(\\d+\\.\\d+(?:\\.\\d+)?)\\b(?!\\s*→)`, 'g');
|
||||
const normalizeName = (raw) => aliases[raw] || raw;
|
||||
return { linkRe, crossRe, normalizeName, pathToName };
|
||||
}
|
||||
|
||||
// Собрать {name:path} из проектного списка (config) ∪ universal. Имя — из PATH_TO_NAME,
|
||||
// иначе basename без .md (greenfield; regex-имена — отдельный заход, дизайн §4.1/§7).
|
||||
// иначе docStem (greenfield: без версии; #3 cross-ref).
|
||||
export function buildNormativeMap(normativeFilesList = []) {
|
||||
const map = {};
|
||||
for (const p of (Array.isArray(normativeFilesList) ? normativeFilesList : [])) {
|
||||
const name = PATH_TO_NAME[p] || String(p).split('/').pop().replace(/\.md$/, '');
|
||||
const name = PATH_TO_NAME[p] || docStem(p);
|
||||
map[name] = p;
|
||||
}
|
||||
return { ...map, ...UNIVERSAL_VERSION_TRACKED };
|
||||
@@ -144,8 +173,9 @@ if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/cross-ref-
|
||||
const { loadConfig } = await import('./brain-config.mjs');
|
||||
normativeMap = buildNormativeMap(loadConfig().normative_files);
|
||||
} catch { /* brain-config недоступен → дефолт NORMATIVE_FILES */ }
|
||||
const patterns = buildCrossRefPatterns(normativeMap);
|
||||
const files = loadFiles(process.cwd(), normativeMap);
|
||||
const m = detectMismatches(files, { normativeFiles: normativeMap });
|
||||
const m = detectMismatches(files, { normativeFiles: normativeMap, ...patterns });
|
||||
if (m.length === 0) {
|
||||
console.log(`[cross-ref-checker] OK — 0 drift in ${Object.keys(files).length} files`);
|
||||
process.exit(0);
|
||||
|
||||
@@ -200,3 +200,41 @@ describe('buildNormativeMap (Task 7 follow-up)', () => {
|
||||
expect(buildNormativeMap([])).toEqual({ CLAUDE: 'CLAUDE.md', MEMORY: 'MEMORY.md' });
|
||||
});
|
||||
});
|
||||
|
||||
import { docStem, buildCrossRefPatterns } from './cross-ref-checker.mjs';
|
||||
describe('docStem (#3 cross-ref)', () => {
|
||||
it('срезает директорию, .md и хвост версии', () => {
|
||||
expect(docStem('docs/Pravila_raboty_Claude_v1_1.md')).toBe('Pravila_raboty_Claude');
|
||||
expect(docStem('docs/Tooling_v8_3.md')).toBe('Tooling');
|
||||
expect(docStem('docs/Plugin_stack_rules_v1.md')).toBe('Plugin_stack_rules');
|
||||
expect(docStem('MyRules_v2.md')).toBe('MyRules');
|
||||
expect(docStem('CLAUDE.md')).toBe('CLAUDE');
|
||||
expect(docStem('docs/Report_2024.md')).toBe('Report_2024');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildCrossRefPatterns (#3 cross-ref)', () => {
|
||||
const map = buildNormativeMap(['docs/Pravila_raboty_Claude_v1_1.md', 'docs/Plugin_stack_rules_v1.md', 'docs/Tooling_v8_3.md']);
|
||||
const grab = (re, nm, t) => [...t.matchAll(re)].map((m) => nm(m[1]));
|
||||
it('детектит имена и алиасы; PSR_v1 раньше PSR; алиас → канон', () => {
|
||||
const { crossRe, normalizeName } = buildCrossRefPatterns(map);
|
||||
expect(grab(crossRe, normalizeName, 'PSR_v1 v3.24')).toEqual(['PSR_v1']);
|
||||
expect(grab(crossRe, normalizeName, 'PSR v3.24')).toEqual(['PSR_v1']);
|
||||
expect(grab(crossRe, normalizeName, 'Pravila v1.44')).toEqual(['Pravila']);
|
||||
});
|
||||
it('pathToName — инверсия карты', () => {
|
||||
expect(buildCrossRefPatterns(map).pathToName['docs/Tooling_v8_3.md']).toBe('Tooling');
|
||||
});
|
||||
it('greenfield: имена из карты, без алиасов', () => {
|
||||
const { crossRe, normalizeName } = buildCrossRefPatterns({ MyRules: 'docs/MyRules_v2.md' }, {});
|
||||
expect(grab(crossRe, normalizeName, 'MyRules v1.0')).toEqual(['MyRules']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildNormativeMap greenfield fallback (#3)', () => {
|
||||
it('имя без версии (docStem), не basename с версией', () => {
|
||||
const m = buildNormativeMap(['docs/MyRules_v2.md']);
|
||||
expect(m.MyRules).toBe('docs/MyRules_v2.md');
|
||||
expect(m.MyRules_v2).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user