# Sub-plan A — context-verity (проверенный контекст) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Чистый модуль `tools/context-verity.mjs`, который проверяет verified-context артефакт наставника (§5.5 спеки): резолвит EXTRACTED-цитаты по символу/контенту, понижает неразрешённые до INFERRED (secure-default), и даёт freeze-side guard для ✅O2.

**Architecture:** Чистое ядро по образцу `tools/plan-lock.mjs::refResolves` `[plan-lock.mjs:226]` — никакого прямого I/O, доступ к файлам инъектируется (`readFileImpl`) для детерминированных тестов. Резолв по СИМВОЛУ (anchor-подстрока должна найтись в файле; номер строки в ref — только подсказка, SE-1). Модуль standalone и тестируем изолированно; wiring в `plan-lock.freezeArtifact` ОТЛОЖЕН в sub-plan C (там verified-context артефакт реально производится наставником и встраивается в sealed-артефакт).

**Tech Stack:** Node ESM (`.mjs`), vitest (tools-config). Источник истины спеки: `docs/superpowers/specs/2026-06-10-router-mentor-live-rebuild-design.md` §5.5/§5.6/§5.7(secure-default)/§5.10 + ✅O2.

**Scope (граница):**
- **В A:** `tools/context-verity.mjs` (чистый) + `tools/context-verity.test.mjs`. Функции: `parseRef`, `resolveCitation`, `verifyArtifact`, `artifactHasUnresolvedExtracted`, константа `CITATION_KINDS`.
- **НЕ в A (→ sub-plan C):** вызов `artifactHasUnresolvedExtracted` из `plan-lock.freezeArtifact` (требует, чтобы verified-context артефакт уже встраивался в sealed-артефакт — это шов наставника, C). A экспортирует guard готовым к подключению.

**Квирк среды (ОБЯЗАТЕЛЬНО для шагов «Run»):** vitest запускается ТОЛЬКО из `app/`:
```
cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs <фильтр> --reporter dot
```
cwd между Bash-вызовами ненадёжен — каждый Run-шаг префиксить `cd app && ...` в одной команде.

**NB git:** в этой среде git зажат стеной у Claude — `git commit`-шаги выполняет ВЛАДЕЛЕЦ/исполнитель вне Claude-стены (или субагент с git-доступом per Pravila §15.1). Команды коммитов даны как есть.

---

### Task 1: Схема артефакта + `parseRef` + `CITATION_KINDS`

**Files:**
- Create: `tools/context-verity.mjs`
- Test: `tools/context-verity.test.mjs`

**Схема verified-context артефакта (§5.5):** массив записей
```
{ id: string, claim: string, ref: "file:line", kind: "EXTRACTED"|"INFERRED", anchor?: string, derivation_ref?: string }
```
`anchor` — литеральная подстрока исходника, которую EXTRACTED-цитата утверждает (для символ-резолва SE-1). `derivation_ref` — для INFERRED (§5.5/SE-2).

- [ ] **Step 1: Write the failing test**

```js
// tools/context-verity.test.mjs
import { describe, it, expect } from 'vitest';
import { parseRef, CITATION_KINDS } from './context-verity.mjs';

describe('parseRef', () => {
  it('парсит file:line', () => {
    expect(parseRef('tools/router-engine.mjs:106')).toEqual({ file: 'tools/router-engine.mjs', line: 106 });
  });
  it('голый файл без строки → line:null', () => {
    expect(parseRef('tools/x.mjs')).toEqual({ file: 'tools/x.mjs', line: null });
  });
  it('не-строка → null', () => {
    expect(parseRef(42)).toBe(null);
  });
  it('CITATION_KINDS заморожен и содержит оба вида', () => {
    expect(CITATION_KINDS).toEqual(['EXTRACTED', 'INFERRED']);
    expect(Object.isFrozen(CITATION_KINDS)).toBe(true);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/context-verity.test.mjs --reporter dot`
Expected: FAIL — `Cannot find module './context-verity.mjs'` / `parseRef is not a function`.

- [ ] **Step 3: Write minimal implementation**

```js
// tools/context-verity.mjs
#!/usr/bin/env node
/**
 * context-verity (§5.6) — машинная сверка verified-context артефакта наставника.
 * Резолв EXTRACTED-цитат по СИМВОЛУ/контенту (anchor-подстрока в файле; номер строки в
 * ref — подсказка, SE-1). Чистое ядро (I/O инъектируется), образец — plan-lock.refResolves.
 * verity = РЕЗОЛВ цитат, НЕ истина (SE-6) — ревью владельца обязателен (§5.10).
 */

// Допустимые виды цитат (§5.5).
export const CITATION_KINDS = Object.freeze(['EXTRACTED', 'INFERRED']);

/** Парс "file:line" → {file, line:number|null} | null (не-строка). Голый файл → line:null. */
export function parseRef(ref) {
  if (typeof ref !== 'string') return null;
  const m = ref.match(/^(.*):(\d+)$/);
  if (!m) return { file: ref, line: null };
  return { file: m[1], line: Number(m[2]) };
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/context-verity.test.mjs --reporter dot`
Expected: PASS — 4 passed.

- [ ] **Step 5: Commit**

```bash
git add tools/context-verity.mjs tools/context-verity.test.mjs
git commit -m "feat(context-verity): схема артефакта + parseRef + CITATION_KINDS (sub-plan A Task 1)"
```

---

### Task 2: `resolveCitation` — резолв по anchor (SE-1)

**Files:**
- Modify: `tools/context-verity.mjs`
- Test: `tools/context-verity.test.mjs`

- [ ] **Step 1: Write the failing test**

```js
// добавить в context-verity.test.mjs
import { resolveCitation } from './context-verity.mjs';

describe('resolveCitation', () => {
  const fakeRead = (file) => file === 'tools/x.mjs' ? 'export function buildRouterPrompt() {}' : null;

  it('anchor найден в файле → resolved', () => {
    const r = resolveCitation({ ref: 'tools/x.mjs:1', anchor: 'buildRouterPrompt' }, fakeRead);
    expect(r.resolved).toBe(true);
  });
  it('anchor НЕ найден → не resolved', () => {
    const r = resolveCitation({ ref: 'tools/x.mjs:1', anchor: 'runJudge' }, fakeRead);
    expect(r.resolved).toBe(false);
  });
  it('файл не прочитан → не resolved', () => {
    const r = resolveCitation({ ref: 'tools/missing.mjs:1', anchor: 'x' }, fakeRead);
    expect(r.resolved).toBe(false);
  });
  it('нет anchor (SE-1: резолв по символу обязателен) → не resolved', () => {
    const r = resolveCitation({ ref: 'tools/x.mjs:1', anchor: '' }, fakeRead);
    expect(r.resolved).toBe(false);
  });
  it('readFileImpl бросил → не resolved (не крашит)', () => {
    const r = resolveCitation({ ref: 'tools/x.mjs:1', anchor: 'x' }, () => { throw new Error('io'); });
    expect(r.resolved).toBe(false);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/context-verity.test.mjs --reporter dot`
Expected: FAIL — `resolveCitation is not a function`.

- [ ] **Step 3: Write minimal implementation**

```js
// добавить в context-verity.mjs
/**
 * Резолв одной EXTRACTED-цитаты: anchor-подстрока ОБЯЗАНА встретиться в файле
 * (SE-1: по символу/контенту, не по номеру строки — он лишь подсказка). readFileImpl
 * инъектируется (в проде — fs.readFileSync; в тестах — мок). Возвращает {resolved, reason}.
 */
export function resolveCitation(cite, readFileImpl) {
  const parsed = parseRef(cite && cite.ref);
  if (!parsed || !parsed.file) return { resolved: false, reason: 'ref не парсится' };
  const anchor = String((cite && cite.anchor) || '').trim();
  if (!anchor) return { resolved: false, reason: 'нет anchor — резолв по символу невозможен (SE-1)' };
  let src;
  try { src = readFileImpl(parsed.file); } catch { src = null; }
  if (src == null) return { resolved: false, reason: `файл не прочитан: ${parsed.file}` };
  return String(src).includes(anchor)
    ? { resolved: true, reason: 'anchor найден (символ-резолв)' }
    : { resolved: false, reason: 'anchor не найден в файле' };
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/context-verity.test.mjs --reporter dot`
Expected: PASS — 9 passed (4 + 5).

- [ ] **Step 5: Commit**

```bash
git add tools/context-verity.mjs tools/context-verity.test.mjs
git commit -m "feat(context-verity): resolveCitation по символу (SE-1, sub-plan A Task 2)"
```

---

### Task 3: `verifyArtifact` — резолв EXTRACTED + flagged

**Files:**
- Modify: `tools/context-verity.mjs`
- Test: `tools/context-verity.test.mjs`

- [ ] **Step 1: Write the failing test**

```js
// добавить в context-verity.test.mjs
import { verifyArtifact } from './context-verity.mjs';

describe('verifyArtifact — резолв EXTRACTED', () => {
  const read = (file) => file === 'a.mjs' ? 'function good(){}' : null;

  it('все EXTRACTED резолвятся → ok, нет flagged', () => {
    const art = [{ id: '1', claim: 'есть good', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'good' }];
    const r = verifyArtifact(art, read);
    expect(r.ok).toBe(true);
    expect(r.flagged).toEqual([]);
    expect(r.extractedCount).toBe(1);
  });
  it('неразрешённая EXTRACTED → flagged + не ok', () => {
    const art = [{ id: '2', claim: 'нет bad', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'bad' }];
    const r = verifyArtifact(art, read);
    expect(r.ok).toBe(false);
    expect(r.flagged.map((f) => f.id)).toContain('2');
  });
  it('INFERRED не требует резолва', () => {
    const art = [
      { id: '1', claim: 'есть good', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'good' },
      { id: '3', claim: 'предположение', ref: 'a.mjs:1', kind: 'INFERRED', derivation_ref: 'd1' },
    ];
    const r = verifyArtifact(art, read);
    expect(r.ok).toBe(true);
  });
  it('битая запись → flagged, не крашит', () => {
    const r = verifyArtifact([null, 'bad', { kind: 'EXTRACTED', anchor: 'good', ref: 'a.mjs:1', id: '1' }], read);
    expect(r.flagged.length).toBeGreaterThanOrEqual(2);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/context-verity.test.mjs --reporter dot`
Expected: FAIL — `verifyArtifact is not a function`.

- [ ] **Step 3: Write minimal implementation**

```js
// добавить в context-verity.mjs
/**
 * Проверить весь артефакт. EXTRACTED-записи обязаны резолвиться (по anchor). Неразрешённая
 * EXTRACTED попадает в flagged И downgraded (понижение — Task 4). INFERRED не резолвится.
 * Битая запись (null/не-объект) → flagged, не крашит (зеркало N2 action-journal).
 * ok = есть ≥1 EXTRACTED И нет неразрешённых (VA-9: 0 EXTRACTED не «проверен» — Task 4 ужесточит).
 * @returns {{ok, flagged:[{id,reason}], downgraded:[{id,reason}], extractedCount, entries}}
 */
export function verifyArtifact(artifact, readFileImpl) {
  const list = Array.isArray(artifact) ? artifact : [];
  const flagged = [];
  const downgraded = [];
  let extractedCount = 0;
  const entries = [];
  for (const e of list) {
    if (!e || typeof e !== 'object' || Array.isArray(e)) { flagged.push({ id: null, reason: 'битая запись' }); continue; }
    if (e.kind === 'EXTRACTED') {
      extractedCount++;
      const r = resolveCitation({ ref: e.ref, anchor: e.anchor }, readFileImpl);
      if (!r.resolved) {
        flagged.push({ id: e.id ?? null, reason: r.reason });
        downgraded.push({ id: e.id ?? null, reason: r.reason });
        entries.push({ ...e, kind: 'INFERRED', downgraded_from: 'EXTRACTED' }); // secure-default (Task 4 verify)
        continue;
      }
    }
    entries.push(e);
  }
  const ok = extractedCount > 0 && downgraded.length === 0;
  return { ok, flagged, downgraded, extractedCount, entries };
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/context-verity.test.mjs --reporter dot`
Expected: PASS — 13 passed.

- [ ] **Step 5: Commit**

```bash
git add tools/context-verity.mjs tools/context-verity.test.mjs
git commit -m "feat(context-verity): verifyArtifact резолв EXTRACTED + flagged (sub-plan A Task 3)"
```

---

### Task 4: Secure-default downgrade (S2/§5.5) + пустой артефакт (VA-9)

**Files:**
- Modify: `tools/context-verity.test.mjs` (поведение уже в коде Task 3 — фиксируем тестами + проверяем downgrade-форму)

- [ ] **Step 1: Write the failing test**

```js
// добавить в context-verity.test.mjs
describe('secure-default downgrade + VA-9', () => {
  const read = (file) => file === 'a.mjs' ? 'function good(){}' : null;

  it('неразрешённая EXTRACTED понижается до INFERRED в entries (S2)', () => {
    const art = [{ id: '2', claim: 'нет bad', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'bad' }];
    const r = verifyArtifact(art, read);
    expect(r.entries[0].kind).toBe('INFERRED');
    expect(r.entries[0].downgraded_from).toBe('EXTRACTED');
    expect(r.downgraded.map((d) => d.id)).toContain('2');
  });
  it('пустой артефакт (0 EXTRACTED) НЕ проверен (VA-9)', () => {
    expect(verifyArtifact([], read).ok).toBe(false);
    expect(verifyArtifact([{ id: '3', kind: 'INFERRED', ref: 'a.mjs:1', claim: 'x', derivation_ref: 'd' }], read).ok).toBe(false);
  });
  it('не-массив → не проверен, не крашит', () => {
    expect(verifyArtifact(null, read).ok).toBe(false);
    expect(verifyArtifact('bad', read).ok).toBe(false);
  });
});
```

- [ ] **Step 2: Run test to verify it fails OR passes (поведение в коде Task 3)**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/context-verity.test.mjs --reporter dot`
Expected: PASS — поведение downgrade/VA-9 уже реализовано в Task 3; этот шаг ФИКСИРУЕТ его тестами (red-green не нужен — это закрепление инварианта). Если FAIL — вернуться к Task 3 коду.

- [ ] **Step 3: (нет нового кода — инвариант закреплён тестами)**

Если все 16 проходят — реализация Task 3 корректно покрывает S2 + VA-9. Иначе исправить `verifyArtifact` по падению.

- [ ] **Step 4: Run — подтвердить 16 passed**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/context-verity.test.mjs --reporter dot`
Expected: PASS — 16 passed.

- [ ] **Step 5: Commit**

```bash
git add tools/context-verity.test.mjs
git commit -m "test(context-verity): закрепить secure-default downgrade + VA-9 (sub-plan A Task 4)"
```

---

### Task 5: Freeze-side guard `artifactHasUnresolvedExtracted` (✅O2, экспорт для sub-plan C)

**Files:**
- Modify: `tools/context-verity.mjs`
- Test: `tools/context-verity.test.mjs`

- [ ] **Step 1: Write the failing test**

```js
// добавить в context-verity.test.mjs
import { artifactHasUnresolvedExtracted } from './context-verity.mjs';

describe('artifactHasUnresolvedExtracted (✅O2 freeze-side guard)', () => {
  const read = (file) => file === 'a.mjs' ? 'function good(){}' : null;

  it('все EXTRACTED резолвятся → false (печать разрешена)', () => {
    const art = [{ id: '1', claim: 'good', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'good' }];
    expect(artifactHasUnresolvedExtracted(art, read)).toBe(false);
  });
  it('есть неразрешённая EXTRACTED → true (печать ОТКАЗана)', () => {
    const art = [{ id: '2', claim: 'bad', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'bad' }];
    expect(artifactHasUnresolvedExtracted(art, read)).toBe(true);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/context-verity.test.mjs --reporter dot`
Expected: FAIL — `artifactHasUnresolvedExtracted is not a function`.

- [ ] **Step 3: Write minimal implementation**

```js
// добавить в context-verity.mjs
/**
 * ✅O2 freeze-side guard: артефакт с ХОТЬ ОДНОЙ неразрешённой EXTRACTED не печатается.
 * Чистый предикат для подключения в plan-lock.freezeArtifact (wiring — sub-plan C, где
 * verified-context артефакт встраивается в sealed-артефакт). Здесь — только готовый guard.
 */
export function artifactHasUnresolvedExtracted(artifact, readFileImpl) {
  return verifyArtifact(artifact, readFileImpl).downgraded.length > 0;
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/context-verity.test.mjs --reporter dot`
Expected: PASS — 18 passed.

- [ ] **Step 5: Commit**

```bash
git add tools/context-verity.mjs tools/context-verity.test.mjs
git commit -m "feat(context-verity): freeze-side guard artifactHasUnresolvedExtracted (O2, sub-plan A Task 5)"
```

---

### Task 6: Регрессия tools-vitest GREEN

**Files:** (нет правок — проверка)

- [ ] **Step 1: Прогнать ВЕСЬ tools-набор**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs --reporter dot`
Expected: PASS — весь tools-набор зелёный, новый `context-verity.test.mjs` (18) включён, 0 регрессий в соседних машинах М1-М7. Зафиксировать число passed (baseline для следующих sub-plan).

- [ ] **Step 2: Если RED — systematic-debugging**

При падении — НЕ править наугад: `superpowers:systematic-debugging`, root cause, точечный фикс, повтор Step 1.

- [ ] **Step 3: Commit (если были фиксы Step 2; иначе пропустить)**

```bash
git add tools/context-verity.mjs tools/context-verity.test.mjs
git commit -m "fix(context-verity): регрессия tools-vitest GREEN (sub-plan A Task 6)"
```

---

## R6.3 amendments (ревью 5-скил цепочки 2026-06-10)

Поправки к задачам выше (исполнять ВМЕСТЕ; OLD→NEW):

**SE6 (Task 1 схема + Task 2/3 тест).** `anchor` УСЛОВНО-ОБЯЗАТЕЛЕН при `kind==='EXTRACTED'` (без него `resolveCitation` не резолвит → secure-default понижает до INFERRED). В типе `anchor?`, но для EXTRACTED — обязателен. Добавить явный тест «EXTRACTED без anchor → downgraded» (поведение уже в `resolveCitation` Task 2 — закрепить против footgun'а SE6).

**VF-1 (Task 3/4 verifyArtifact — INFERRED-guard).** Изменить `verifyArtifact`: INFERRED-запись с пустым/отсутствующим `derivation_ref` → `flagged` (НЕ молчаливо проходит). NEW-тест: `{kind:'INFERRED', derivation_ref:''}` → flagged; с непустым → ok. Нота: «закрывает полосу обхода verity через INFERRED-метку (§5.5 SE-2); лимит доли O9 — открыт». ⚠️ Согласовать с `ok`-критерием Task 3 (`extractedCount>0 && downgraded.length===0`): добавить, что наличие INFERRED-без-`derivation_ref` → `ok:false`.

**SE9 (Task 4 reframe — НЕ red-green).** Заголовок «Step 1: Write the failing test» → «Step 1: Закрепляющий тест инварианта Task 3 (характеризация, БЕЗ RED)». Step 2 «Run … fails OR passes» → «Run — Expected PASS (инвариант уже в коде Task 3)». Нота про TDD-gate: «характеризационный тест проходит сразу — норма, не нарушение TDD».

**V-6 / SE13 (cross-cutting).** Шаги enumeration/характеризация/smoke/регрессия — пометить «[НЕ-TDD: …]» (enforce-tdd-gate проходит сразу — норма). «N passed» в задачах — ИЛЛЮСТРАТИВНО: сверяй ЗЕЛЁНОСТЬ, не точное число (SE13).

**Self-review A — обновить:** +SE6 (EXTRACTED⇒anchor) / +VF-1 (INFERRED⇒derivation_ref, `flagged`/`ok:false`) / +SE9 (характеризация).

---

## Self-Review (writing-plans)

**1. Spec coverage:**
- §5.5 артефакт `{id,claim,ref,kind}` + `anchor`/`derivation_ref` → Task 1/3. ✅
- §5.6 context-verity резолв по символу (SE-1) → Task 2. ✅
- §5.5 secure-default downgrade (S2) → Task 3/4. ✅
- §5.6 пустой артефакт ≠ проверен (VA-9) → Task 4. ✅
- ✅O2 freeze-side guard → Task 5 (wiring в plan-lock → sub-plan C, явно отложено). ✅
- Регрессия → Task 6. ✅
- **Вне A (→ C):** §5.4 staleness-поле, §5.3 слоистый граф, §6 живой шов, §6.2 вход наставника. НЕ покрыты намеренно (другие sub-plan).

**2. Placeholder scan:** реальный код в каждом code-шаге; команды Run с ожидаемым выводом; нет «TBD». ✅

**3. Type consistency:** `parseRef`→`{file,line}`; `resolveCitation`→`{resolved,reason}`; `verifyArtifact`→`{ok,flagged,downgraded,extractedCount,entries}`; `artifactHasUnresolvedExtracted`→boolean. Имена согласованы между задачами. ✅

**Известное ограничение (честно):** `anchor`-резолв проверяет НАЛИЧИЕ подстроки в файле, НЕ её позицию у hinted-строки (SE-1: строка — подсказка, не якорь). Это осознанно — номера строк хрупки. Точность «у нужной строки» — design-choice O-вопрос для sub-plan C при необходимости (сейчас наличие в файле достаточно для anti-fabrication).
