Files
evomap__evolver/test/skill2gepParser.test.js
evolver-publish 4854a3b9b9 Release v1.87.3
2026-06-01 15:20:47 +00:00

269 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { parseSkillMd, inferCategory } = require('../src/gep/skill2gep');
// inferCategory takes (signals, description); the description carries intent.
const categoryFor = (description) => inferCategory([], description);
// Regression coverage for the parser defects that dropped a Skill's
// "governance tail" (candidate-gating / Human Gate / Output Contract) and
// mislabeled an optimize Skill as `repair`. See fix/skill2gep-parser-governance-loss.
// A SKILL.md shaped like the real paranoia-ai-system-evolver: a multi-section
// workflow with nested sub-bullets, a trailing Human Gate section, and an
// Output Contract section. Its description mentions "rollback" (a safe-change
// mechanism) but its intent is to UPGRADE a system -> optimize, not repair.
const SKILL_MD = [
'---',
'name: sample-evolver',
'description: Use when upgrading an AI system with VOI, OODA, evals, human gates, versioning, and rollback.',
'---',
'',
'# Sample Evolver',
'',
'## Quick Workflow',
'1. Define the current task and the system layer being changed.',
'2. Pass a VOI gate before gathering more information.',
'3. Make the operating model explicit:',
' - compression: shortest model that explains most cases?',
' - causality: which mediator variables connect input to outcome?',
' - control points: which mediator can you intervene on?',
'4. Maintain a compact OODA state:',
' - Observe: goal, context, evidence.',
' - Orient: current frame, uncertainty map.',
' - Decide / Act / Evaluate.',
'5. Keep every change as `candidate` until evidence, evals, approval, and rollback are present.',
'',
'## Human Gate Defaults',
'Ask for human confirmation before:',
'- writing long-term memory',
'- changing production strategy or user-visible systems',
'- promoting a `candidate` to current rule',
'',
'## Output Contract',
'End by stating:',
'- what changed',
'- which evals ran',
'- how to rollback',
].join('\n');
describe('skill2gep parseSkillMd governance-tail preservation', () => {
const parsed = parseSkillMd(SKILL_MD);
const blob = JSON.stringify(parsed.strategy).toLowerCase();
it('keeps the candidate-gating step (not truncated away)', () => {
assert.ok(blob.includes('candidate'), 'strategy should retain the `candidate` gating step');
});
it('keeps the Human Gate section items', () => {
assert.ok(
blob.includes('human confirmation') || blob.includes('long-term memory') || blob.includes('production strategy'),
'strategy should include Human Gate items from the trailing section'
);
});
it('keeps the Output Contract section items', () => {
assert.ok(
blob.includes('what changed') || blob.includes('how to rollback'),
'strategy should include Output Contract items from the trailing section'
);
});
// Flat extraction: every list item is its own step, including a governance
// section's items, regardless of indentation. (Folding was removed in PR
// #156 after it grew a long tail of indentation edge cases; flat extraction
// preserves the same tail with no indentation reasoning.)
it('emits a deeper-indented governance section as its own independent steps', () => {
const md = [
'---', 'name: t', 'description: optimize a system', '---', '# T', '',
'## Quick Workflow',
'1. define the layer being changed',
'2. validate and record',
'## Human Gate Defaults',
'Ask before:',
' - writing long-term memory',
' - changing production strategy',
].join('\n');
const p = parseSkillMd(md);
assert.ok(p.strategy.some((s) => /writing long-term memory/.test(s)),
'governance item should be its own step');
assert.ok(p.strategy.some((s) => /changing production strategy/.test(s)),
'second governance item should also be its own step');
// No merging of distinct items into one combined step.
assert.ok(!p.strategy.some((s) => /validate and record.*writing long-term/s.test(s)),
'distinct items must not be merged');
});
it('emits every sub-bullet as its own step (no folding)', () => {
const opModelIdx = parsed.strategy.findIndex((s) => /operating model/i.test(s));
assert.ok(opModelIdx >= 0, 'should have an "operating model" step');
// Each sub-point stands alone as its own step.
assert.ok(parsed.strategy.some((s) => /^compression:/i.test(s.trim())),
'compression sub-point should be its own step');
assert.ok(parsed.strategy.some((s) => /^control points:/i.test(s.trim())),
'control-points sub-point should be its own step');
});
it('extracts the full governance tail without truncating (cap clears a rich Skill)', () => {
assert.ok(parsed.strategy.length > 10,
'multi-section Skill should yield a rich strategy list, not a truncated one');
assert.ok(parsed.strategy.length <= 28, 'but should stay within the compact cap');
});
it('keeps a uniformly-indented list as separate steps', () => {
const md = [
'---', 'name: t', 'description: optimize the build', '---', '# T', '',
'## Workflow',
' 1. first step do the thing',
' 2. second step validate it',
' 3. third step record outcome',
].join('\n');
const p = parseSkillMd(md);
assert.equal(p.strategy.length, 3, 'uniform 3-item list must stay 3 separate steps');
});
});
describe('skill2gep inferCategory (Bugbot #156 follow-ups)', () => {
it('does NOT let cross-cutting safety words (rollback) force repair on an upgrade skill', () => {
assert.equal(categoryFor('Use when upgrading an AI system with versioning and rollback and guard rails'), 'optimize');
});
it('keeps repair-first priority: a genuine fix intent still classifies repair', () => {
assert.equal(categoryFor('review and fix critical production bugs and crashes'), 'repair');
});
it('does not misclassify "tunnel" as optimize (no greedy tun\\w* match)', () => {
assert.equal(categoryFor('implement a tunnel for secure remote access'), 'innovate');
});
// Bugbot #156 (post-refactor, Medium): "add" must still classify innovate...
it('classifies "add ..." descriptions as innovate', () => {
assert.equal(categoryFor('add a new monitoring dashboard'), 'innovate');
assert.equal(categoryFor('add retry logic to the client'), 'innovate');
});
// ...but the \b-bounded "add" must not false-positive on address/additional/padding.
it('does not treat address/additional/padding as innovate via "add"', () => {
assert.equal(categoryFor('optimize additional logging throughput'), 'optimize');
assert.equal(categoryFor('reduce padding in the buffer layout'), 'optimize');
});
// Bugbot #156 round 2 (High): substring matching must survive inflected
// forms and the project's underscore signal format.
it('matches inflected repair forms (errors / fixed / crashes)', () => {
assert.equal(categoryFor('handle errors and crashes after a fix was reverted'), 'repair');
});
it('matches underscore-separated signals like log_error / test_failure', () => {
assert.equal(inferCategory(['log_error', 'test_failure'], ''), 'repair');
});
it('keeps short preconditions like "Git"/"npm" (no 5-char gate, no folding)', () => {
const md = [
'---', 'name: t', 'description: optimize the build', '---',
'# T', '', '## Prerequisites', '- Git', '- npm', '- a configured CI token', '',
'## Workflow', '1. do the thing carefully', '2. validate it',
].join('\n');
const p = parseSkillMd(md);
assert.ok(p.preconditions.includes('Git'), 'short "Git" precondition must survive');
assert.ok(p.preconditions.includes('npm'), 'short "npm" precondition must survive');
assert.equal(p.preconditions.length, 3, 'all three preconditions kept, none folded/dropped');
});
it('paranoia-style upgrade description (with rollback) stays optimize', () => {
assert.equal(
inferCategory(
['use_when_upgrading_an_ai_system', 'tool_routing', 'schema'],
'Use when upgrading an AI system with VOI, OODA, evals, human gates, versioning, and rollback'
),
'optimize'
);
});
});
// A Chinese-authored SKILL.md: headings carry no English token, so the section
// keyword tables must recognize CJK synonyms or the parser silently falls back
// to a thin gene (strategy=fallback, avoid=[]). Mirrors the real game-* skills
// in ParanoiaSkills that distilled thin before this fix.
const CJK_SKILL_MD = [
'---',
'name: cjk-curator',
// Real game-* skills carry a comma-separated English description; the signal
// tokenizer splits on commas, so this is what produces ASCII signal tokens.
'description: Use when curating game design sources, research, screening, review, and ingestion into a durable local knowledge base.',
'---',
'',
'# 资料策展',
'',
'## 触发条件',
'- 来源研究、首轮建档、候选审核、标准入库时使用',
'',
'## 快速工作流',
'1. Observe确认任务模式从研究、建档、审核中选一个主模式。',
'2. VOI 门:只收集会改变决策的信息,先做去重和短读。',
'3. Decide状态推进必须有证据未深读不得进入 accepted。',
'',
'## 输出门',
'- 检查 catalog、registry、update-history 是否同步完成',
'- 未知字段保留 unknown不能悄悄猜满',
'',
'## 不要做',
'- 不要见文就收、短读即入库',
'- 不要忽略去重、证据门和置信度',
'',
'## 前置条件',
'- Git',
'- 已配置的本地知识库目录',
].join('\n');
describe('skill2gep parseSkillMd CJK section headings', () => {
const parsed = parseSkillMd(CJK_SKILL_MD);
it('extracts strategy steps from Chinese workflow + output-gate headings', () => {
assert.ok(parsed.strategy.length >= 5,
`expected rich strategy from CJK headings, got ${parsed.strategy.length}`);
assert.ok(parsed.strategy.some((s) => /确认任务模式/.test(s)),
'workflow step from "## 快速工作流" must be captured');
assert.ok(parsed.strategy.some((s) => /catalogregistry/.test(s)),
'governance-tail step from "## 输出门" must be captured');
});
it('extracts avoid items from a Chinese "不要做" heading', () => {
assert.ok(parsed.avoid.length >= 2,
`expected avoid items from "## 不要做", got ${parsed.avoid.length}`);
assert.ok(parsed.avoid.some((s) => /见文就收/.test(s)),
'anti-pattern from "## 不要做" must be captured');
});
it('still derives signals from the (English) frontmatter description', () => {
// The CJK heading is now matched, but the signal tokenizer keeps ASCII
// [a-z0-9_] only, so signals come from the English description. Pure-CJK
// body words do not tokenize — a documented out-of-scope limitation.
assert.ok(parsed.signals_match.some((s) => /curat|knowledge|base|source/.test(s)),
'signals from English frontmatter description must survive');
});
it('documents the CJK signal-tokenizer gap: a CJK-only trigger body adds no signals', () => {
const cjkOnly = [
'---', 'name: t', 'description: 用于资料策展', '---',
'# T', '', '## 触发条件', '- 来源研究、首轮建档、候选审核',
].join('\n');
// No ASCII description, CJK-only body -> tokenizer yields nothing. This is
// the known limitation; if a future PR adds a CJK segmenter, update this.
assert.equal(parseSkillMd(cjkOnly).signals_match.length, 0);
});
it('keeps short preconditions from a Chinese "前置条件" heading', () => {
assert.ok(parsed.preconditions.includes('Git'),
'short "Git" precondition under CJK heading must survive');
});
it('does not regress: an English SKILL.md still parses unchanged', () => {
const p = parseSkillMd(SKILL_MD);
assert.ok(p.strategy.some((s) => /what changed/i.test(s)),
'English governance tail still captured after adding CJK keywords');
});
});