diff --git a/tools/observer-chain-detector.mjs b/tools/observer-chain-detector.mjs deleted file mode 100644 index 606c1de..0000000 --- a/tools/observer-chain-detector.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname, join } from 'node:path'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const DEFAULT_MAP_PATH = join(__dirname, 'observer-chain-map.json'); - -/** Load the node->chains map. Throws on missing/invalid JSON (caller handles). */ -export function loadChainMap(path = DEFAULT_MAP_PATH) { - const raw = JSON.parse(readFileSync(path, 'utf8')); - const map = new Map(); - for (const [node, chains] of Object.entries(raw)) { - if (node === '_note') continue; - if (Array.isArray(chains) && chains.length > 0) map.set(node, chains); - } - return map; -} - -/** node_chosen -> array of L-chains, or null if not in any chain. */ -export function chainsFor(node, map) { - if (!node || typeof node !== 'string') return null; - const chains = map.get(node); - return chains && chains.length > 0 ? chains : null; -} diff --git a/tools/observer-chain-detector.test.mjs b/tools/observer-chain-detector.test.mjs deleted file mode 100644 index 8ad6708..0000000 --- a/tools/observer-chain-detector.test.mjs +++ /dev/null @@ -1,32 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { loadChainMap, chainsFor } from './observer-chain-detector.mjs'; - -const map = loadChainMap(); - -describe('chainsFor', () => { - it('returns chain array for a single-chain node', () => { - expect(chainsFor('billing-audit', map)).toEqual(['L13']); - }); - - it('returns all chains for a multi-chain node', () => { - expect(chainsFor('discovery-interview', map)).toEqual(['L1', 'L2']); - }); - - it('returns null for direct', () => { - expect(chainsFor('direct', map)).toBeNull(); - }); - - it('returns null for an unknown node', () => { - expect(chainsFor('totally-unknown-xyz', map)).toBeNull(); - }); - - it('returns null for empty/null/undefined', () => { - expect(chainsFor('', map)).toBeNull(); - expect(chainsFor(null, map)).toBeNull(); - expect(chainsFor(undefined, map)).toBeNull(); - }); - - it('ignores the _note metadata key', () => { - expect(chainsFor('_note', map)).toBeNull(); - }); -}); diff --git a/tools/observer-chain-map.json b/tools/observer-chain-map.json deleted file mode 100644 index 8fcfeb6..0000000 --- a/tools/observer-chain-map.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "_note": "node_chosen -> L-цепочки. Только узлы, входящие хотя бы в одну L1-L13. Узлы вне цепочек (direct, прочее) НЕ включаются -> chainsFor вернёт null. Имена ключей = реальные значения primary_rationale.node_chosen (skill-id из skill_invoked). MCP/agent-узлы (laravel-boost, openapi-mcp-server, api-docs, sentry-mcp, redis-mcp, pest, github-mcp) в node_chosen не появляются, но включены для полноты покрытия цепочек L1-L13 (контролёр C6 требует, чтобы каждая L из routing-off-phase.md была покрыта). Синхронизируется с docs/routing-off-phase.md через tools/observer-chain-map-checker.mjs.", - "discovery-interview": ["L1", "L2"], - "superpowers:brainstorming": ["L1", "L16", "L17"], - "superpowers:writing-plans": ["L1"], - "superpowers:subagent-driven-development": ["L1"], - "audit-portal": ["L2"], - "process-analysis": ["L3"], - "process-modeling": ["L3", "L4"], - "mermaid": ["L4"], - "adr-kit:adr": ["L4", "L5"], - "adr-kit:judge": ["L5"], - "operations": ["L4"], - "architecture-patterns:architecture-patterns": ["L5"], - "deptrac": ["L5", "L14"], - "rector": ["L14"], - "php-insights": ["L14"], - "larastan": ["L14"], - "laravel-backend-patterns": ["L14"], - "security-review": ["L6"], - "openapi-mcp-server": ["L7"], - "api-docs": ["L7"], - "laravel-boost": ["L7", "L13"], - "superpowers:systematic-debugging": ["L8"], - "sentry-mcp": ["L8", "L13"], - "redis-mcp": ["L8", "L13"], - "ccpm": ["L9"], - "product-management:brainstorm": ["L9"], - "github-mcp": ["L9"], - "promptfoo": ["L10"], - "data-scientist": ["L10"], - "claude-api": ["L10"], - "skill-creator:skill-creator": ["L11"], - "hookify:hookify": ["L11"], - "plugin-dev:create-plugin": ["L11"], - "claude-md-management:claude-md-improver": ["L12"], - "claude-md-management:revise-claude-md": ["L12"], - "billing-audit": ["L13"], - "pest": ["L13"], - "ru-tax-accounting": ["L13"], - "security-go-live": ["L15"], - "pdn-152fz-audit": ["L15"], - "threat-model": ["L15"], - "nuclei": ["L15"], - "ward": ["L15"], - "owasp-zap": ["L15"], - "gitleaks": ["L15"], - "semgrep": ["L15"], - "trailofbits": ["L15"], - "marketing": ["L16"], - "marketing-ru": ["L16"], - "yandex-metrika-mcp": ["L16"], - "yandex-wordstat-mcp": ["L16"], - "telegram-mcp": ["L16"], - "postiz": ["L16"], - "perplexity-mcp": ["L17"], - "exa-mcp": ["L17"], - "firecrawl-mcp": ["L17"] -} diff --git a/tools/observer-retrofill-chain-ref.mjs b/tools/observer-retrofill-chain-ref.mjs deleted file mode 100644 index 3d6df8d..0000000 --- a/tools/observer-retrofill-chain-ref.mjs +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env node -/** - * One-shot retrofill: add primary_rationale.chain_ref to existing v2 episodes - * in docs/observer/episodes-*.jsonl. Idempotent (skips lines that already have - * chain_ref), atomic per file (tmp + rename). Pure fs, no LLM. - * - * Usage: node tools/observer-retrofill-chain-ref.mjs [--dry-run] - */ -import { readFileSync, writeFileSync, renameSync, readdirSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname, join } from 'node:path'; -import { loadChainMap, chainsFor } from './observer-chain-detector.mjs'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const OBS_DIR = join(__dirname, '..', 'docs', 'observer'); - -/** Add chain_ref to a single parsed episode object (pure). Idempotent. */ -export function retrofillLine(ep, map) { - if (!ep || ep.schema_version !== 2 || !ep.primary_rationale) return ep; - if ('chain_ref' in ep.primary_rationale) return ep; // idempotent - ep.primary_rationale.chain_ref = chainsFor(ep.primary_rationale.node_chosen, map); - return ep; -} - -/** Process one JSONL file atomically (tmp + rename). Returns {changed, total}. */ -export function retrofillFile(path, map, { dryRun = false } = {}) { - const lines = readFileSync(path, 'utf8').split(/\r?\n/); - let changed = 0; - let total = 0; - const out = lines.map((line) => { - if (!line.trim()) return line; - total++; - const ep = JSON.parse(line); - const before = ep.primary_rationale && 'chain_ref' in ep.primary_rationale; - const next = retrofillLine(ep, map); - const after = next.primary_rationale && 'chain_ref' in next.primary_rationale; - if (!before && after) changed++; - return JSON.stringify(next); - }); - if (!dryRun && changed > 0) { - const tmp = `${path}.tmp`; - writeFileSync(tmp, out.join('\n'), 'utf8'); - renameSync(tmp, path); - } - return { changed, total }; -} - -function main() { - const dryRun = process.argv.includes('--dry-run'); - const map = loadChainMap(); - const files = readdirSync(OBS_DIR).filter((f) => /^episodes-\d{4}-\d{2}\.jsonl$/.test(f)); - for (const f of files) { - const { changed, total } = retrofillFile(join(OBS_DIR, f), map, { dryRun }); - console.log(`${dryRun ? '[dry-run] ' : ''}${f}: ${changed}/${total} lines get chain_ref`); - } -} - -if (process.argv[1]?.endsWith('observer-retrofill-chain-ref.mjs')) main(); diff --git a/tools/observer-retrofill-chain-ref.test.mjs b/tools/observer-retrofill-chain-ref.test.mjs deleted file mode 100644 index 9ba7410..0000000 --- a/tools/observer-retrofill-chain-ref.test.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { retrofillLine } from './observer-retrofill-chain-ref.mjs'; -import { loadChainMap } from './observer-chain-detector.mjs'; - -const map = loadChainMap(); - -describe('retrofillLine', () => { - it('adds chain_ref to a v2 episode with a known node', () => { - const ep = { schema_version: 2, primary_rationale: { node_chosen: 'billing-audit' } }; - const out = retrofillLine(ep, map); - expect(out.primary_rationale.chain_ref).toEqual(['L13']); - }); - - it('sets chain_ref null for a direct v2 episode', () => { - const ep = { schema_version: 2, primary_rationale: { node_chosen: 'direct' } }; - expect(retrofillLine(ep, map).primary_rationale.chain_ref).toBeNull(); - }); - - it('is idempotent — does not overwrite existing chain_ref', () => { - const ep = { schema_version: 2, primary_rationale: { node_chosen: 'direct', chain_ref: ['L1'] } }; - expect(retrofillLine(ep, map).primary_rationale.chain_ref).toEqual(['L1']); - }); - - it('skips v1 episodes (no schema_version 2)', () => { - const ep = { foo: 'bar' }; - expect(retrofillLine(ep, map)).toEqual({ foo: 'bar' }); - }); -}); diff --git a/tools/observer-transcript-parser.mjs b/tools/observer-transcript-parser.mjs index a64cf16..3baed96 100644 --- a/tools/observer-transcript-parser.mjs +++ b/tools/observer-transcript-parser.mjs @@ -22,7 +22,6 @@ import { readRouterState, extractRouterFields, extractClassifierOutput } from '. import { CLASSIFIER_MODEL } from './router-config.mjs'; import { homedir } from 'node:os'; import { detectChoiceProvenance, detectAskUserQuestionChoice } from './observer-choice-detector.mjs'; -import { loadChainMap, chainsFor } from './observer-chain-detector.mjs'; import { buildHookMap, resolveScriptCounts } from './observer-hook-resolver.mjs'; // recommendNode / buildClassificationMap / buildDormancyMap были использованы // для слепого fallback на heuristic recommended_node — убрано 2026-05-26 @@ -33,13 +32,6 @@ import { JUDGE_PER_CALL_USD } from './cost-pricing.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); -let CHAIN_MAP = null; -try { - CHAIN_MAP = loadChainMap(); -} catch { - CHAIN_MAP = new Map(); // битый/отсутствующий JSON -> chainsFor вернёт null, observer не падает -} - let HOOK_MAP = null; function getHookMap() { if (HOOK_MAP) return HOOK_MAP; @@ -57,7 +49,6 @@ function getHookMap() { * the regex on its own would happily slurp into candidates_considered. * Sources, in order: * - tools/observer-known-nodes.txt — bare names (brainstorming, ccpm, …) - * - tools/observer-chain-map.json keys — incl. plugin:skill form * - sentinel "direct" (no-skill marker used by node_chosen) * Tooling IDs (#NN) and arbitrary plugin:skill forms pass via regex below. */ @@ -70,9 +61,8 @@ const KNOWN_NODES = (() => { if (t) set.add(t); } } catch { - // file missing in some test sandboxes — fall back to chain-map keys only + // file missing in some test sandboxes — whitelist falls back to "direct" + regex shapes } - if (CHAIN_MAP) for (const node of CHAIN_MAP.keys()) set.add(node); return set; })(); @@ -960,7 +950,6 @@ export function parseTranscript(transcriptText, fallbackSessionId = null, option return { step: 1, node_chosen: skills.length > 0 ? skills[0] : 'direct', - chain_ref: chainsFor(skills.length > 0 ? skills[0] : 'direct', CHAIN_MAP), triggers_matched: merge(extractTriggers(turn), tag ? tag.triggers : []), candidates_considered: merge(extractCandidates(turn), tag ? tag.candidates : []), boundaries_applied: merge(extractBoundaries(turn), tag ? tag.boundaries : []), diff --git a/tools/observer-transcript-parser.test.mjs b/tools/observer-transcript-parser.test.mjs index f6bde1e..c45d540 100644 --- a/tools/observer-transcript-parser.test.mjs +++ b/tools/observer-transcript-parser.test.mjs @@ -127,25 +127,6 @@ describe('parseTranscript', () => { expect(parseTranscript(t).primary_rationale.node_chosen).toBe('direct'); }); - it('attaches chain_ref for a node that belongs to a chain', () => { - const t = jsonl([ - userPrompt('go', '2026-05-19T10:00:00Z'), - assistantTurn( - [{ type: 'tool_use', id: 't1', name: 'Skill', input: { skill: 'billing-audit' } }], - '2026-05-19T10:01:00Z' - ), - ]); - expect(parseTranscript(t).primary_rationale.chain_ref).toEqual(['L13']); - }); - - it('sets chain_ref null for a direct episode', () => { - const t = jsonl([ - userPrompt('go', '2026-05-19T10:00:00Z'), - assistantTurn([{ type: 'tool_use', id: 't1', name: 'Read', input: {} }], '2026-05-19T10:01:00Z'), - ]); - expect(parseTranscript(t).primary_rationale.chain_ref).toBeNull(); - }); - it('hard_floor invoked when a superpowers skill is used', () => { const t = jsonl([ userPrompt('go', '2026-05-19T10:00:00Z'),