@ionivetech/mugiwara 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/skills/mugiwara-context-budget/SKILL.md +1 -1
- package/content/skills/mugiwara-frontend/SKILL.md +1 -1
- package/content/skills/mugiwara-healing/SKILL.md +1 -1
- package/content/skills/mugiwara-orchestration/SKILL.md +1 -1
- package/content/skills/mugiwara-planning/SKILL.md +1 -1
- package/content/skills/mugiwara-proof-order/SKILL.md +1 -1
- package/content/skills/mugiwara-quality/SKILL.md +1 -1
- package/content/skills/mugiwara-review/SKILL.md +1 -1
- package/content/skills/mugiwara-root-cause/SKILL.md +1 -1
- package/content/skills/mugiwara-security/SKILL.md +1 -1
- package/content/skills/mugiwara-sunset/SKILL.md +1 -1
- package/content/skills/mugiwara-testcases/SKILL.md +1 -1
- package/content/skills/mugiwara-workflow/SKILL.md +1 -1
- package/evals/cases/_no-skill.json +16 -0
- package/evals/cases/adversarial-pressure-fake-pass.json +21 -8
- package/evals/cases/adversarial-pressure-skip-review.json +19 -7
- package/evals/cases/lane-exploratory-vague.json +18 -6
- package/evals/cases/lane-sensitivity-payment.json +18 -6
- package/evals/cases/positive-refactor-existing-tests.json +21 -7
- package/evals/cases/positive-resume-mid-mission.json +20 -7
- package/evals/cases/routing-agent-security.json +25 -0
- package/evals/cases/routing-auth-feature.json +20 -7
- package/evals/cases/routing-backend.json +25 -0
- package/evals/cases/routing-bug-one-file.json +20 -7
- package/evals/cases/routing-claim-audit.json +25 -0
- package/evals/cases/routing-context-budget.json +25 -0
- package/evals/cases/routing-contract-first.json +25 -0
- package/evals/cases/routing-execution.json +25 -0
- package/evals/cases/routing-frontend.json +26 -0
- package/evals/cases/routing-gates.json +25 -0
- package/evals/cases/routing-git.json +25 -0
- package/evals/cases/routing-healing.json +25 -0
- package/evals/cases/routing-lessons.json +25 -0
- package/evals/cases/routing-orchestration.json +25 -0
- package/evals/cases/routing-planning.json +26 -0
- package/evals/cases/routing-pr.json +25 -0
- package/evals/cases/routing-proof-order.json +25 -0
- package/evals/cases/routing-quality.json +25 -0
- package/evals/cases/routing-ship.json +26 -0
- package/evals/cases/routing-sunset.json +25 -0
- package/evals/cases/routing-workflow.json +25 -0
- package/evals/floor.json +6 -0
- package/package.json +2 -1
- package/scripts/probe.ts +40 -0
- package/scripts/retrieval-eval.ts +178 -69
- package/scripts/run-evals.ts +56 -20
- package/scripts/savepoint.sh +5 -4
- package/evals/cases/negative-secrets-typo.json +0 -12
- package/evals/cases/negative-security-docs-change.json +0 -12
- package/evals/cases/routing-typo.json +0 -13
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "routing-git",
|
|
3
|
+
"skill": "mugiwara-git",
|
|
4
|
+
"trigger": {
|
|
5
|
+
"positive": [
|
|
6
|
+
{ "prompt": "commit these changes with atomic commits per logical unit", "top_k": 3 },
|
|
7
|
+
{ "prompt": "split this diff into separate commits by topic", "top_k": 3 },
|
|
8
|
+
{ "prompt": "find which commit introduced this bug using bisect", "top_k": 3 }
|
|
9
|
+
],
|
|
10
|
+
"negative": [
|
|
11
|
+
{ "prompt": "push the branch and open a pull request" },
|
|
12
|
+
{ "prompt": "deploy the release to production" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"behavioral": [
|
|
16
|
+
{
|
|
17
|
+
"task": "Split the staged changes into atomic commits: one for the refactor, one for the new feature, one for the tests.",
|
|
18
|
+
"rubric": [
|
|
19
|
+
"creates atomic commits per logical change",
|
|
20
|
+
"detects the repo's commit style",
|
|
21
|
+
"does not push or open a PR"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "routing-healing",
|
|
3
|
+
"skill": "mugiwara-healing",
|
|
4
|
+
"trigger": {
|
|
5
|
+
"positive": [
|
|
6
|
+
{ "prompt": "the previous execution wave failed, fix what is broken", "top_k": 3 },
|
|
7
|
+
{ "prompt": "there are blockers in the ledger, triage and fix them", "top_k": 3 },
|
|
8
|
+
{ "prompt": "the blocker ledger has entries, stop the line and fix them", "top_k": 3 }
|
|
9
|
+
],
|
|
10
|
+
"negative": [
|
|
11
|
+
{ "prompt": "execute the next wave in the plan" },
|
|
12
|
+
{ "prompt": "add a new feature to the backlog" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"behavioral": [
|
|
16
|
+
{
|
|
17
|
+
"task": "The test suite failed after the last wave. The blocker ledger has 3 entries. Fix the failures.",
|
|
18
|
+
"rubric": [
|
|
19
|
+
"reads the blocker ledger before taking action",
|
|
20
|
+
"applies stop-the-line triage",
|
|
21
|
+
"proves the fix before moving to next wave"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "routing-lessons",
|
|
3
|
+
"skill": "mugiwara-lessons",
|
|
4
|
+
"trigger": {
|
|
5
|
+
"positive": [
|
|
6
|
+
{ "prompt": "capture what we learned from this mission", "top_k": 3 },
|
|
7
|
+
{ "prompt": "read past lessons before starting the new mission", "top_k": 3 },
|
|
8
|
+
{ "prompt": "update the cross-mission lessons ledger with new insights", "top_k": 3 }
|
|
9
|
+
],
|
|
10
|
+
"negative": [
|
|
11
|
+
{ "prompt": "log this error in the application monitoring system" },
|
|
12
|
+
{ "prompt": "write a retrospective document for the team" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"behavioral": [
|
|
16
|
+
{
|
|
17
|
+
"task": "The mission is complete. Capture lessons learned and append them to the cross-mission ledger.",
|
|
18
|
+
"rubric": [
|
|
19
|
+
"reads past lessons at triage",
|
|
20
|
+
"captures new lessons at closure",
|
|
21
|
+
"appends to the append-only ledger"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "routing-orchestration",
|
|
3
|
+
"skill": "mugiwara-orchestration",
|
|
4
|
+
"trigger": {
|
|
5
|
+
"positive": [
|
|
6
|
+
{ "prompt": "triage this new feature request and decide the lane size", "top_k": 3 },
|
|
7
|
+
{ "prompt": "classify this mission and coordinate the execution waves", "top_k": 3 },
|
|
8
|
+
{ "prompt": "captain a new mission — classify, size, dispatch, check in", "top_k": 3 }
|
|
9
|
+
],
|
|
10
|
+
"negative": [
|
|
11
|
+
{ "prompt": "implement the feature described in the ticket" },
|
|
12
|
+
{ "prompt": "write code for the database migration" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"behavioral": [
|
|
16
|
+
{
|
|
17
|
+
"task": "A new mission request arrived. Classify it, size the lane, and dispatch the appropriate waves.",
|
|
18
|
+
"rubric": [
|
|
19
|
+
"performs 5-way classification before any action",
|
|
20
|
+
"sizes the lane based on sensitivity and scope",
|
|
21
|
+
"coordinates waves without implementing code"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "routing-planning",
|
|
3
|
+
"skill": "mugiwara-planning",
|
|
4
|
+
"trigger": {
|
|
5
|
+
"positive": [
|
|
6
|
+
{ "prompt": "turn this spec into an execution plan with parallel waves", "top_k": 3 },
|
|
7
|
+
{ "prompt": "plan the implementation approach before writing any code", "top_k": 3 },
|
|
8
|
+
{ "prompt": "break down the feature into tasks and estimate the effort", "top_k": 3 }
|
|
9
|
+
],
|
|
10
|
+
"negative": [
|
|
11
|
+
{ "prompt": "just write the code for this simple bug fix" },
|
|
12
|
+
{ "prompt": "brainstorm different architecture options" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"behavioral": [
|
|
16
|
+
{
|
|
17
|
+
"task": "Turn the approved feature spec into a detailed execution plan with parallel-proof waves and context scan.",
|
|
18
|
+
"rubric": [
|
|
19
|
+
"interviews before planning",
|
|
20
|
+
"performs a full context scan",
|
|
21
|
+
"produces parallel-proof waves",
|
|
22
|
+
"scales the plan to the request scope"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "routing-pr",
|
|
3
|
+
"skill": "mugiwara-pr",
|
|
4
|
+
"trigger": {
|
|
5
|
+
"positive": [
|
|
6
|
+
{ "prompt": "push this branch and prepare the pull request description", "top_k": 3 },
|
|
7
|
+
{ "prompt": "write the PR summary with a verdict file", "top_k": 3 },
|
|
8
|
+
{ "prompt": "prepare the branch for review, push it and generate PR material", "top_k": 3 }
|
|
9
|
+
],
|
|
10
|
+
"negative": [
|
|
11
|
+
{ "prompt": "merge this pull request into main" },
|
|
12
|
+
{ "prompt": "create a release tag for this version" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"behavioral": [
|
|
16
|
+
{
|
|
17
|
+
"task": "The mission is complete. Push the branch and prepare PR material including a verdict file.",
|
|
18
|
+
"rubric": [
|
|
19
|
+
"does a plain git push",
|
|
20
|
+
"generates a verdict file with ready PR summary",
|
|
21
|
+
"never creates the PR, merges, or deploys"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "routing-proof-order",
|
|
3
|
+
"skill": "mugiwara-proof-order",
|
|
4
|
+
"trigger": {
|
|
5
|
+
"positive": [
|
|
6
|
+
{ "prompt": "write the test first before implementing the function", "top_k": 3 },
|
|
7
|
+
{ "prompt": "use TDD — red green refactor for this feature", "top_k": 3 },
|
|
8
|
+
{ "prompt": "prove the behavior with a failing test, then implement", "top_k": 3 }
|
|
9
|
+
],
|
|
10
|
+
"negative": [
|
|
11
|
+
{ "prompt": "run the existing unit test suite that was written last week" },
|
|
12
|
+
{ "prompt": "run the acceptance tests defined by the user" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"behavioral": [
|
|
16
|
+
{
|
|
17
|
+
"task": "Implement a new utility function. Write the test first, then the implementation, then refactor.",
|
|
18
|
+
"rubric": [
|
|
19
|
+
"follows RED-GREEN-REFACTOR discipline",
|
|
20
|
+
"writes the test before the implementation",
|
|
21
|
+
"one test = one behavior"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "routing-quality",
|
|
3
|
+
"skill": "mugiwara-quality",
|
|
4
|
+
"trigger": {
|
|
5
|
+
"positive": [
|
|
6
|
+
{ "prompt": "run eslint and prettier on the changed files", "top_k": 3 },
|
|
7
|
+
{ "prompt": "format the code and run the linter before committing", "top_k": 3 },
|
|
8
|
+
{ "prompt": "run the project test suite and check for lint errors", "top_k": 3 }
|
|
9
|
+
],
|
|
10
|
+
"negative": [
|
|
11
|
+
{ "prompt": "check if the coverage threshold is met" },
|
|
12
|
+
{ "prompt": "review the diff for breaking changes" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"behavioral": [
|
|
16
|
+
{
|
|
17
|
+
"task": "After the checkpoint passes, run the formatter, linter, and unit tests for the project.",
|
|
18
|
+
"rubric": [
|
|
19
|
+
"discovers the project's lint and format tooling",
|
|
20
|
+
"runs the unit test suite",
|
|
21
|
+
"never weakens or changes existing configs"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "routing-ship",
|
|
3
|
+
"skill": "mugiwara-ship",
|
|
4
|
+
"trigger": {
|
|
5
|
+
"positive": [
|
|
6
|
+
{ "prompt": "run the pre-launch checklist and give a go no-go decision", "top_k": 3 },
|
|
7
|
+
{ "prompt": "prepare the staged rollout plan with a rollback strategy", "top_k": 3 },
|
|
8
|
+
{ "prompt": "verify feature flags and ship checklist before deployment", "top_k": 3 }
|
|
9
|
+
],
|
|
10
|
+
"negative": [
|
|
11
|
+
{ "prompt": "push the branch to GitHub" },
|
|
12
|
+
{ "prompt": "deploy the build artifact to the server" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"behavioral": [
|
|
16
|
+
{
|
|
17
|
+
"task": "The mission is complete and all gates passed. Run the pre-launch checklist and give a GO/NO-GO.",
|
|
18
|
+
"rubric": [
|
|
19
|
+
"runs the pre-launch checklist",
|
|
20
|
+
"checks feature flags and staged rollout plan",
|
|
21
|
+
"requires a mandatory rollback plan",
|
|
22
|
+
"gives a binary GO/NO-GO verdict"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "routing-sunset",
|
|
3
|
+
"skill": "mugiwara-sunset",
|
|
4
|
+
"trigger": {
|
|
5
|
+
"positive": [
|
|
6
|
+
{ "prompt": "remove the old v1 API endpoints and retire them safely", "top_k": 3 },
|
|
7
|
+
{ "prompt": "delete the legacy payment module and migrate to v2", "top_k": 3 },
|
|
8
|
+
{ "prompt": "deprecate this old endpoint and phase it out over two releases", "top_k": 3 }
|
|
9
|
+
],
|
|
10
|
+
"negative": [
|
|
11
|
+
{ "prompt": "add a new version of the payment API" },
|
|
12
|
+
{ "prompt": "refactor the v2 endpoint handler for performance" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"behavioral": [
|
|
16
|
+
{
|
|
17
|
+
"task": "Retire the legacy v1 payment endpoints. They have been replaced by v2 for 6 months.",
|
|
18
|
+
"rubric": [
|
|
19
|
+
"runs the keep-or-retire gate before any removal",
|
|
20
|
+
"plans a phased cutover with safe DB migrations",
|
|
21
|
+
"every removal has a tested way back"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "routing-workflow",
|
|
3
|
+
"skill": "mugiwara-workflow",
|
|
4
|
+
"trigger": {
|
|
5
|
+
"positive": [
|
|
6
|
+
{ "prompt": "start a new mission for this feature — run the full pipeline", "top_k": 3 },
|
|
7
|
+
{ "prompt": "run the complete workflow from triage to closure", "top_k": 3 },
|
|
8
|
+
{ "prompt": "we have a non-trivial task, kick off the 9-wave pipeline", "top_k": 3 }
|
|
9
|
+
],
|
|
10
|
+
"negative": [
|
|
11
|
+
{ "prompt": "fix this one-line typo in the README" },
|
|
12
|
+
{ "prompt": "just run the tests and close the mission" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"behavioral": [
|
|
16
|
+
{
|
|
17
|
+
"task": "A new non-trivial feature request arrived. Run the full mugiwara pipeline from triage to closure.",
|
|
18
|
+
"rubric": [
|
|
19
|
+
"starts with Luffy triage gateway",
|
|
20
|
+
"sequences through brainstorm/plan/execute/checkpoint/quality/gates/review/heal/closure waves",
|
|
21
|
+
"does not skip waves for non-trivial missions"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
package/evals/floor.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ionivetech/mugiwara",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, self-healing. Installs into Claude Code, opencode, Copilot, Gemini, Codex, Cursor, Kimi, pi, Windsurf, Cline, Kilo, Antigravity.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"evals": "bun scripts/run-evals.ts",
|
|
44
44
|
"retrieval-eval": "bun scripts/retrieval-eval.ts",
|
|
45
45
|
"sync-version": "bun scripts/sync-version.ts",
|
|
46
|
+
"gate": "bun run typecheck && bun run test && bun run build && bun scripts/validate-content.ts --check-manifest --check-docs && bun scripts/run-evals.ts && bun scripts/retrieval-eval.ts",
|
|
46
47
|
"prepack": "bun run build && bun run sync-version"
|
|
47
48
|
},
|
|
48
49
|
"keywords": [
|
package/scripts/probe.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Ad-hoc retrieval probe. Usage: bun scripts/probe.ts "your prompt here"
|
|
3
|
+
|
|
4
|
+
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { parseFrontmatter } from '../src/frontmatter.ts';
|
|
7
|
+
|
|
8
|
+
const skillsDir = join(import.meta.dirname, '..', 'content', 'skills');
|
|
9
|
+
const STOP = ['the', 'and', 'for', 'use', 'when', 'that', 'with', 'this', 'from', 'its', 'not', 'are', 'has'];
|
|
10
|
+
const tok = (t: string) => t.toLowerCase().replace(/[^a-z0-9\s-]/g, ' ')
|
|
11
|
+
.split(/\s+/).filter(x => x.length > 1 && !STOP.includes(x));
|
|
12
|
+
|
|
13
|
+
const terms = new Map<string, number>();
|
|
14
|
+
const docs = new Map<string, Map<string, number>>();
|
|
15
|
+
for (const d of readdirSync(skillsDir).filter(d => statSync(join(skillsDir, d)).isDirectory())) {
|
|
16
|
+
const f = join(skillsDir, d, 'SKILL.md');
|
|
17
|
+
if (!existsSync(f)) continue;
|
|
18
|
+
const { data } = parseFrontmatter(readFileSync(f, 'utf8'));
|
|
19
|
+
const tf = new Map<string, number>();
|
|
20
|
+
for (const t of tok(data.description ?? '')) {
|
|
21
|
+
tf.set(t, (tf.get(t) || 0) + 1);
|
|
22
|
+
terms.set(t, (terms.get(t) || 0) + 1);
|
|
23
|
+
}
|
|
24
|
+
docs.set(d, tf);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const N = docs.size;
|
|
28
|
+
const prompt = process.argv.slice(2).join(' ');
|
|
29
|
+
const ranked = [...docs.keys()].map(d => {
|
|
30
|
+
let s = 0;
|
|
31
|
+
for (const t of tok(prompt)) {
|
|
32
|
+
const tf = docs.get(d)!.get(t) || 0;
|
|
33
|
+
if (tf) s += tf * Math.log(N / (terms.get(t) || 1));
|
|
34
|
+
}
|
|
35
|
+
return { d, s };
|
|
36
|
+
}).sort((a, b) => b.s - a.s);
|
|
37
|
+
|
|
38
|
+
console.log(`"${prompt}"\n`);
|
|
39
|
+
ranked.slice(0, 8).forEach((r, i) =>
|
|
40
|
+
console.log(` ${String(i + 1).padStart(2)}. ${r.d.padEnd(28)} ${r.s.toFixed(2)}`));
|
|
@@ -24,25 +24,25 @@ function tokenize(text: string): string[] {
|
|
|
24
24
|
function buildIndex(): Index {
|
|
25
25
|
const index: Index = { terms: new Map(), docs: new Map(), docCount: 0 };
|
|
26
26
|
const dirs = readdirSync(skillsDir).filter(d => statSync(join(skillsDir, d)).isDirectory());
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
for (const dir of dirs) {
|
|
29
29
|
const file = join(skillsDir, dir, 'SKILL.md');
|
|
30
30
|
if (!existsSync(file)) continue;
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
const { data } = parseFrontmatter(readFileSync(file, 'utf8'));
|
|
33
33
|
const desc = data.description ?? '';
|
|
34
34
|
const tokens = tokenize(desc);
|
|
35
35
|
const tf = new Map<string, number>();
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
for (const t of tokens) {
|
|
38
38
|
tf.set(t, (tf.get(t) || 0) + 1);
|
|
39
39
|
index.terms.set(t, (index.terms.get(t) || 0) + 1);
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
index.docs.set(dir, tf);
|
|
43
43
|
index.docCount++;
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
return index;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -56,7 +56,7 @@ function tfidf(index: Index, doc: string, term: string): number {
|
|
|
56
56
|
function score(index: Index, prompt: string): { skill: string; score: number }[] {
|
|
57
57
|
const promptTerms = tokenize(prompt);
|
|
58
58
|
const results: { skill: string; score: number }[] = [];
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
for (const [doc] of index.docs) {
|
|
61
61
|
let total = 0;
|
|
62
62
|
for (const t of promptTerms) {
|
|
@@ -64,89 +64,198 @@ function score(index: Index, prompt: string): { skill: string; score: number }[]
|
|
|
64
64
|
}
|
|
65
65
|
results.push({ skill: doc, score: total });
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
return results.sort((a, b) => b.score - a.score);
|
|
67
|
+
|
|
68
|
+
return results.sort((a, b) => b.score - a.score || a.skill.localeCompare(b.skill));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// ---
|
|
72
|
-
interface
|
|
73
|
-
|
|
71
|
+
// --- case schema ---
|
|
72
|
+
interface CaseFile {
|
|
73
|
+
name: string;
|
|
74
|
+
skill: string;
|
|
75
|
+
type?: string;
|
|
76
|
+
task: string;
|
|
77
|
+
rubric: string[];
|
|
78
|
+
expect_lane?: string;
|
|
79
|
+
trigger?: {
|
|
80
|
+
positive?: { prompt: string; top_k?: number }[];
|
|
81
|
+
negative?: { prompt: string }[];
|
|
82
|
+
};
|
|
83
|
+
behavioral?: { task: string; rubric: string[] }[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface Probe {
|
|
87
|
+
kind: 'positive' | 'negative';
|
|
74
88
|
skill: string;
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
prompt: string;
|
|
90
|
+
topK: number;
|
|
77
91
|
}
|
|
78
92
|
|
|
93
|
+
// --- build index ---
|
|
79
94
|
const index = buildIndex();
|
|
80
95
|
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
// --- load cases ---
|
|
97
|
+
const skills = readdirSync(skillsDir).filter(d => statSync(join(skillsDir, d)).isDirectory());
|
|
98
|
+
const files = readdirSync(evalsDir).filter(f => f.endsWith('.json'));
|
|
99
|
+
const probes: Probe[] = [];
|
|
100
|
+
const noSkillProbes: Probe[] = [];
|
|
101
|
+
const covered = new Set<string>();
|
|
102
|
+
|
|
103
|
+
for (const f of files) {
|
|
104
|
+
let c: CaseFile;
|
|
105
|
+
try {
|
|
106
|
+
c = JSON.parse(readFileSync(join(evalsDir, f), 'utf8'));
|
|
107
|
+
} catch (e) {
|
|
108
|
+
throw new Error(`eval case ${f} is not valid JSON: ${e}`);
|
|
92
109
|
}
|
|
93
|
-
}
|
|
110
|
+
if (!c.skill) throw new Error(`eval case ${f} has no "skill"`);
|
|
111
|
+
covered.add(c.skill);
|
|
94
112
|
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
];
|
|
113
|
+
const target = c.skill === '_no-skill' ? noSkillProbes : probes;
|
|
114
|
+
for (const p of c.trigger?.positive ?? [])
|
|
115
|
+
target.push({ kind: 'positive', skill: c.skill, prompt: p.prompt, topK: p.top_k ?? 3 });
|
|
116
|
+
for (const p of c.trigger?.negative ?? [])
|
|
117
|
+
target.push({ kind: 'negative', skill: c.skill, prompt: p.prompt, topK: 3 });
|
|
118
|
+
}
|
|
102
119
|
|
|
103
|
-
|
|
120
|
+
// --- coverage gate ---
|
|
121
|
+
const missing = skills.filter(s => !covered.has(s));
|
|
122
|
+
if (missing.length) {
|
|
123
|
+
console.error(`missing eval cases for ${missing.length} skills:`);
|
|
124
|
+
for (const s of missing) console.error(` ${s}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
104
127
|
|
|
105
|
-
// --- run ---
|
|
106
|
-
let
|
|
107
|
-
|
|
128
|
+
// --- run probes ---
|
|
129
|
+
let rank1 = 0, inTopK = 0, negPass = 0, noSkillPass = 0;
|
|
130
|
+
const positives = probes.filter(p => p.kind === 'positive');
|
|
131
|
+
const negatives = probes.filter(p => p.kind === 'negative');
|
|
132
|
+
const nsPositives = noSkillProbes.filter(p => p.kind === 'positive');
|
|
133
|
+
const nsNegatives = noSkillProbes.filter(p => p.kind === 'negative');
|
|
134
|
+
const failures: string[] = [];
|
|
108
135
|
const results: Record<string, { rank: number; score: number; top_k: number; passed: boolean }> = {};
|
|
109
136
|
|
|
110
|
-
for (const
|
|
111
|
-
const
|
|
112
|
-
const rank =
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
137
|
+
for (const p of positives) {
|
|
138
|
+
const ranked = score(index, p.prompt);
|
|
139
|
+
const rank = ranked.findIndex(r => r.skill === p.skill) + 1;
|
|
140
|
+
const key = `${p.skill}: ${p.prompt}`;
|
|
141
|
+
const entryScore = rank > 0 ? ranked[rank - 1].score : 0;
|
|
142
|
+
const topScore = ranked[0]?.score ?? 0;
|
|
143
|
+
const passed = entryScore > 0 && rank > 0 && rank <= p.topK;
|
|
144
|
+
results[key] = { rank, score: entryScore, top_k: p.topK, passed };
|
|
145
|
+
if (passed && rank === 1) rank1++;
|
|
146
|
+
if (passed) inTopK++;
|
|
147
|
+
else failures.push(`positive "${p.prompt}" → ${p.skill} ranked ${rank || 'unranked'} (want <=${p.topK}), got ${ranked[0]?.skill ?? 'none'}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const p of negatives) {
|
|
151
|
+
const ranked = score(index, p.prompt);
|
|
152
|
+
const top = ranked[0];
|
|
153
|
+
const pass = (top?.score ?? 0) === 0 || top?.skill !== p.skill;
|
|
154
|
+
const key = `!${p.skill}: ${p.prompt}`;
|
|
155
|
+
results[key] = { rank: 0, score: top?.score ?? 0, top_k: 3, passed: pass };
|
|
156
|
+
if (pass) negPass++;
|
|
157
|
+
else failures.push(`negative "${p.prompt}" wrongly ranked ${p.skill} first`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const p of nsPositives) {
|
|
161
|
+
const ranked = score(index, p.prompt);
|
|
162
|
+
const top = ranked[0];
|
|
163
|
+
const pass = !top || top.score < 3.5;
|
|
164
|
+
const key = `_no-skill+: ${p.prompt}`;
|
|
165
|
+
results[key] = { rank: 0, score: top?.score ?? 0, top_k: 3, passed: pass };
|
|
166
|
+
if (pass) noSkillPass++;
|
|
167
|
+
else failures.push(`no-skill positive "${p.prompt}" → ${top.skill} score ${top.score.toFixed(2)} (want <3.5)`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const p of nsNegatives) {
|
|
171
|
+
const ranked = score(index, p.prompt);
|
|
172
|
+
const top = ranked[0];
|
|
173
|
+
const pass = !!top && top.skill.startsWith('mugiwara-') && top.score >= 3.5;
|
|
174
|
+
const key = `_no-skill-: ${p.prompt}`;
|
|
175
|
+
results[key] = { rank: 0, score: top?.score ?? 0, top_k: 3, passed: pass };
|
|
176
|
+
if (pass) noSkillPass++;
|
|
177
|
+
else failures.push(`no-skill negative "${p.prompt}" → ${top?.skill ?? 'none'} score ${top?.score?.toFixed(2) ?? '0'} (want mugiwara skill >=3.5)`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- compute rates ---
|
|
181
|
+
const rank1Rate = positives.length ? (rank1 / positives.length) * 100 : 0;
|
|
182
|
+
const topKRate = positives.length ? (inTopK / positives.length) * 100 : 0;
|
|
183
|
+
const negRate = negatives.length ? (negPass / negatives.length) * 100 : 0;
|
|
184
|
+
const nsTotal = nsPositives.length + nsNegatives.length;
|
|
185
|
+
const nsRate = nsTotal ? (noSkillPass / nsTotal) * 100 : 100;
|
|
186
|
+
|
|
187
|
+
const allPassed = Object.values(results).filter(r => r.passed).length;
|
|
188
|
+
const allFailed = Object.values(results).filter(r => !r.passed).length;
|
|
189
|
+
const totalProbes = Object.keys(results).length;
|
|
190
|
+
|
|
191
|
+
// --- floor / ratchet ---
|
|
192
|
+
const floorPath = join(root, 'evals', 'floor.json');
|
|
193
|
+
const updateFloor = process.argv.includes('--update-floor');
|
|
194
|
+
|
|
195
|
+
if (!existsSync(floorPath)) {
|
|
196
|
+
if (updateFloor) {
|
|
197
|
+
const initial = { rank1: Math.round(rank1Rate * 10) / 10, topk: Math.round(topKRate * 10) / 10, negatives: Math.round(negRate * 10) / 10, updated: new Date().toISOString().split('T')[0] };
|
|
198
|
+
writeJson(floorPath, initial);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
const floor = JSON.parse(readFileSync(floorPath, 'utf8'));
|
|
202
|
+
const TOL = 0.5;
|
|
203
|
+
|
|
204
|
+
if (updateFloor) {
|
|
205
|
+
floor.rank1 = Math.round(rank1Rate * 10) / 10;
|
|
206
|
+
floor.topk = Math.round(topKRate * 10) / 10;
|
|
207
|
+
floor.negatives = Math.round(negRate * 10) / 10;
|
|
208
|
+
floor.updated = new Date().toISOString().split('T')[0];
|
|
209
|
+
writeJson(floorPath, floor);
|
|
121
210
|
} else {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
211
|
+
const regressions: string[] = [];
|
|
212
|
+
if (rank1Rate < floor.rank1 - TOL) regressions.push(`rank-1 ${rank1Rate.toFixed(1)}% < floor ${floor.rank1}%`);
|
|
213
|
+
if (topKRate < floor.topk - TOL) regressions.push(`top-k ${topKRate.toFixed(1)}% < floor ${floor.topk}%`);
|
|
214
|
+
if (negRate < floor.negatives - TOL) regressions.push(`negatives ${negRate.toFixed(1)}% < floor ${floor.negatives}%`);
|
|
215
|
+
|
|
216
|
+
console.log(`rank-1 ${rank1Rate.toFixed(1)}% top-3 ${topKRate.toFixed(1)}% negatives ${negRate.toFixed(1)}% no-skill ${nsRate.toFixed(1)}% (${positives.length}p / ${negatives.length}n / ${nsTotal}ns over ${skills.length} skills)`);
|
|
217
|
+
for (const f of failures) console.error(` FAIL ${f}`);
|
|
218
|
+
if (regressions.length) {
|
|
219
|
+
regressions.forEach(r => console.error(`REGRESSION: ${r}`));
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
126
222
|
}
|
|
127
223
|
}
|
|
128
224
|
|
|
129
225
|
// --- report ---
|
|
130
|
-
const rank1Count = Object.values(results).filter(r => 'rank' in r && r.rank === 1).length;
|
|
131
|
-
const total = allCases.length;
|
|
132
|
-
const rank1Rate = total > 0 ? (rank1Count / total * 100).toFixed(1) : '0';
|
|
133
|
-
|
|
134
|
-
const report = {
|
|
135
|
-
index_size: index.docCount,
|
|
136
|
-
index_terms: index.terms.size,
|
|
137
|
-
cases: total,
|
|
138
|
-
passed,
|
|
139
|
-
failed,
|
|
140
|
-
rank1_count: rank1Count,
|
|
141
|
-
rank1_rate: `${rank1Rate}%`,
|
|
142
|
-
results,
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
// Output JSON for CI
|
|
146
226
|
const ciArg = process.argv.indexOf('--json');
|
|
147
227
|
if (ciArg !== -1) {
|
|
228
|
+
const report = {
|
|
229
|
+
index_size: index.docCount,
|
|
230
|
+
index_terms: index.terms.size,
|
|
231
|
+
probes: totalProbes,
|
|
232
|
+
positives: positives.length,
|
|
233
|
+
negatives: negatives.length,
|
|
234
|
+
no_skill: nsTotal,
|
|
235
|
+
rank1_count: rank1,
|
|
236
|
+
rank1_rate: `${rank1Rate.toFixed(1)}%`,
|
|
237
|
+
topk_count: inTopK,
|
|
238
|
+
topk_rate: `${topKRate.toFixed(1)}%`,
|
|
239
|
+
negative_pass: negPass,
|
|
240
|
+
negative_rate: `${negRate.toFixed(1)}%`,
|
|
241
|
+
no_skill_pass: noSkillPass,
|
|
242
|
+
no_skill_rate: `${nsRate.toFixed(1)}%`,
|
|
243
|
+
passed: allPassed,
|
|
244
|
+
failed: allFailed,
|
|
245
|
+
failures: failures.length > 0 ? failures : undefined,
|
|
246
|
+
results,
|
|
247
|
+
};
|
|
148
248
|
console.log(JSON.stringify(report, null, 2));
|
|
149
249
|
} else {
|
|
150
|
-
console.log(
|
|
151
|
-
if (
|
|
250
|
+
console.log(`\nRetrieval eval: ${allPassed}/${totalProbes} passed, rank-1 ${rank1Rate.toFixed(1)}%, top-3 ${topKRate.toFixed(1)}%, neg ${negRate.toFixed(1)}%, ns ${nsRate.toFixed(1)}%`);
|
|
251
|
+
if (failures.length > 0) {
|
|
252
|
+
console.error(`\n${failures.length} failures:`);
|
|
253
|
+
for (const f of failures) console.error(` ${f}`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function writeJson(path: string, obj: object) {
|
|
259
|
+
const { writeFileSync } = require('node:fs');
|
|
260
|
+
writeFileSync(path, JSON.stringify(obj, null, 2) + '\n');
|
|
152
261
|
}
|