mirror of
https://github.com/EvoMap/evolver.git
synced 2026-09-18 21:47:53 +08:00
269 lines
12 KiB
JavaScript
269 lines
12 KiB
JavaScript
'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) => /catalog、registry/.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');
|
||
});
|
||
});
|