mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-29 13:05:18 +02:00
feat(profiles): add opt-in Lean/Full and hybrid Auto selection
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
'use strict';
|
||||
// Hidden grader for forge-cli: drives run(argv, state) through the twelve
|
||||
// contractual behaviors plus never-throw fuzzing and static hygiene. Prints
|
||||
// ECC_EVAL_SCORE and always exits 0.
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const checks = [];
|
||||
const record = (name, ok) => checks.push({ name, ok: Boolean(ok) });
|
||||
|
||||
const root = process.cwd();
|
||||
let run;
|
||||
try { ({ run } = require(path.join(root, 'src', 'cli.js'))); } catch { /* scored below */ }
|
||||
|
||||
const USAGE = 'usage: snippet <add|get|list|remove|search|export|import>\n';
|
||||
const ADD_USAGE = 'usage: add <name> [--tags t1,t2] <text...>\n';
|
||||
|
||||
if (typeof run !== 'function') {
|
||||
for (let i = 0; i < 26; i++) record(`check-${i + 1}`, false);
|
||||
} else {
|
||||
const call = (argv, state) => {
|
||||
try {
|
||||
const result = run(argv, state);
|
||||
if (!result || typeof result.code !== 'number'
|
||||
|| typeof result.stdout !== 'string' || typeof result.stderr !== 'string') return null;
|
||||
return result;
|
||||
} catch { return null; }
|
||||
};
|
||||
|
||||
// Basic lifecycle.
|
||||
let s = {};
|
||||
let r = call(['add', 'hello', 'hello', 'world'], s);
|
||||
record('add-happy', r && r.code === 0 && r.stdout === 'created hello\n' && r.stderr === '');
|
||||
r = call(['add', 'hello', 'different', 'text'], s);
|
||||
const afterDup = call(['get', 'hello'], s);
|
||||
record('add-duplicate-rejected', r && r.code === 1 && r.stderr === "error: snippet 'hello' already exists\n"
|
||||
&& afterDup && afterDup.stdout === 'hello world\n');
|
||||
const m1 = call(['add'], s);
|
||||
const m2 = call(['add', 'justname'], s);
|
||||
record('add-missing-args-usage', m1 && m1.code === 2 && m1.stderr === ADD_USAGE
|
||||
&& m2 && m2.code === 2 && m2.stderr === ADD_USAGE);
|
||||
r = call(['add', 'Bad_Name', 'text'], s);
|
||||
record('invalid-name-rejected', r && r.code === 2 && r.stderr === "error: invalid snippet name 'Bad_Name'\n");
|
||||
r = call(['get', 'hello'], s);
|
||||
record('get-happy', r && r.code === 0 && r.stdout === 'hello world\n');
|
||||
r = call(['get', 'ghost'], s);
|
||||
record('get-unknown', r && r.code === 2 && r.stderr === "error: no snippet named 'ghost'\n");
|
||||
|
||||
// Listing and tags.
|
||||
s = {};
|
||||
call(['add', 'bravo', 'second'], s);
|
||||
call(['add', 'alpha', '--tags', 'x,y', 'first'], s);
|
||||
call(['add', 'charlie', '--tags', 'y', 'third'], s);
|
||||
r = call(['list'], s);
|
||||
record('list-sorted', r && r.code === 0 && r.stdout === 'alpha\nbravo\ncharlie\n');
|
||||
r = call(['list'], {});
|
||||
record('list-empty', r && r.code === 0 && r.stdout === 'no snippets\n');
|
||||
r = call(['list', '--tag', 'y'], s);
|
||||
record('list-tag-filter', r && r.code === 0 && r.stdout === 'alpha\ncharlie\n');
|
||||
|
||||
// Removal.
|
||||
r = call(['remove', 'bravo'], s);
|
||||
const gone = call(['get', 'bravo'], s);
|
||||
record('remove-happy', r && r.code === 0 && r.stdout === 'removed bravo\n' && gone && gone.code === 2);
|
||||
r = call(['remove', 'bravo'], s);
|
||||
record('remove-unknown', r && r.code === 2 && r.stderr === "error: no snippet named 'bravo'\n");
|
||||
|
||||
// Search over name and text, case-insensitive, sorted.
|
||||
r = call(['search', 'FIRST'], s);
|
||||
record('search-text-case-insensitive', r && r.code === 0 && r.stdout === 'alpha\n');
|
||||
r = call(['search', 'char'], s);
|
||||
record('search-name-match', r && r.code === 0 && r.stdout === 'charlie\n');
|
||||
r = call(['search', 'zzz'], s);
|
||||
record('search-no-matches', r && r.code === 0 && r.stdout === 'no matches\n');
|
||||
|
||||
// Export/import round-trip with stable ordering.
|
||||
r = call(['export'], s);
|
||||
let doc = null;
|
||||
try { doc = r && JSON.parse(r.stdout); } catch { /* wrong */ }
|
||||
record('export-json-sorted', doc && r.code === 0 && sameDoc(doc, {
|
||||
snippets: { alpha: { text: 'first', tags: ['x', 'y'] }, charlie: { text: 'third', tags: ['y'] } } })
|
||||
&& r.stdout.indexOf('alpha') < r.stdout.indexOf('charlie'));
|
||||
const importedState = { snippets: { alpha: { text: 'preexisting', tags: [] } } };
|
||||
r = call(['import', JSON.stringify({ snippets: {
|
||||
alpha: { text: 'first', tags: ['x', 'y'] }, delta: { text: 'fourth', tags: ['z'] } } })], importedState);
|
||||
const delta = call(['get', 'delta'], importedState);
|
||||
const alpha = call(['get', 'alpha'], importedState);
|
||||
record('import-merge-skip-existing', r && r.code === 0 && r.stdout === 'imported 1, skipped 1\n'
|
||||
&& delta && delta.stdout === 'fourth\n' && alpha && alpha.stdout === 'preexisting\n');
|
||||
const beforeExport = call(['export'], s);
|
||||
r = call(['import', '{not json'], s);
|
||||
const afterExport = call(['export'], s);
|
||||
record('import-malformed-atomic', r && r.code === 1 && r.stderr === 'error: invalid JSON\n'
|
||||
&& beforeExport && afterExport && beforeExport.stdout === afterExport.stdout);
|
||||
|
||||
// Usage fallbacks.
|
||||
r = call(['bogus'], {});
|
||||
record('unknown-command-usage', r && r.code === 2 && r.stderr === USAGE);
|
||||
r = call([], {});
|
||||
record('no-command-usage', r && r.code === 2 && r.stderr === USAGE);
|
||||
|
||||
// Never-throw fuzzing on junk input.
|
||||
const fuzz = [['--help', 'x'], ['get'], ['add', 'x', 'y', '--tags'], ['import']];
|
||||
fuzz.forEach((argv, index) => {
|
||||
record(`fuzz-never-throws-${index + 1}`, call(argv, {}) !== null);
|
||||
});
|
||||
}
|
||||
|
||||
function sameDoc(a, b) { return JSON.stringify(a) === JSON.stringify(b); }
|
||||
|
||||
// Static hygiene.
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
||||
record('no-external-dependencies', !pkg.dependencies && !pkg.devDependencies);
|
||||
} catch { record('no-external-dependencies', false); }
|
||||
try {
|
||||
const sources = [];
|
||||
const walk = directory => {
|
||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||
const item = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) walk(item);
|
||||
else if (entry.name.endsWith('.js')) sources.push(fs.readFileSync(item, 'utf8'));
|
||||
}
|
||||
};
|
||||
walk(path.join(root, 'src'));
|
||||
record('no-leftover-todos', sources.every(source => !/TODO|FIXME/.test(source)));
|
||||
} catch { record('no-leftover-todos', false); }
|
||||
|
||||
const okCount = checks.filter(c => c.ok).length;
|
||||
for (const c of checks) console.log(`${c.ok ? 'ok' : 'not ok'} - ${c.name}`);
|
||||
console.log(`ECC_EVAL_SCORE ${JSON.stringify({ score: okCount / checks.length, passed: okCount, total: checks.length })}`);
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,46 @@
|
||||
# snippet-cli
|
||||
|
||||
A small in-process snippet manager. No external dependencies; Node.js standard
|
||||
library only.
|
||||
|
||||
## Contract
|
||||
|
||||
`src/cli.js` is CommonJS and exports `run(argv, state)`:
|
||||
|
||||
- `argv`: array of command-line words (already split, no program name).
|
||||
- `state`: any plain object, created by the caller as `{}`. The CLI keeps its
|
||||
data in it and mutates it in place; it survives across calls.
|
||||
- Returns synchronously: `{ code, stdout, stderr }` — a number and two strings
|
||||
(empty string when there is nothing to print). `run` must **never throw**,
|
||||
on any input.
|
||||
- All printed lines end with `\n`.
|
||||
|
||||
## Commands (all behavior below is contractual)
|
||||
|
||||
1. `add <name> [--tags a,b] <text...>` — creates a snippet from the remaining
|
||||
words joined by single spaces. Prints `created <name>`, code 0.
|
||||
2. Adding an existing name: code 1, stderr `error: snippet '<name>' already exists`,
|
||||
state unchanged.
|
||||
3. `add` with a missing name or missing text: code 2, stderr
|
||||
`usage: add <name> [--tags t1,t2] <text...>`.
|
||||
4. Names must match `^[a-z0-9][a-z0-9-]*$`; otherwise code 2, stderr
|
||||
`error: invalid snippet name '<name>'`.
|
||||
5. `get <name>` — prints the exact text, code 0. Unknown name: code 2, stderr
|
||||
`error: no snippet named '<name>'`.
|
||||
6. `remove <name>` — prints `removed <name>`, code 0. Unknown name: same as `get`.
|
||||
7. `list` — every snippet name, sorted ascending, one per line. With no
|
||||
snippets: prints `no snippets`. Always code 0.
|
||||
8. `list --tag <t>` — only snippets whose tags include `t`.
|
||||
9. `search <term>` — case-insensitive substring match over name **and** text;
|
||||
prints matching names sorted, one per line; prints `no matches` when empty.
|
||||
Code 0.
|
||||
10. `export` — prints `JSON.stringify` of `{ snippets: { <name>: { text, tags } } }`
|
||||
with names sorted and each `tags` array sorted. Code 0.
|
||||
11. `import <json>` — merges an exported document: names not already present
|
||||
are added, existing names are skipped. Prints `imported <N>, skipped <M>`,
|
||||
code 0. Malformed JSON: code 1, stderr `error: invalid JSON`, state
|
||||
unchanged.
|
||||
12. No command or an unknown command: code 2, stderr
|
||||
`usage: snippet <add|get|list|remove|search|export|import>`.
|
||||
|
||||
Run the tests with `npm test`.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "snippet-cli",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": { "test": "node --test test/" }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
// TODO: implement per README. The contract is run(argv, state) -> { code, stdout, stderr }.
|
||||
function run(_argv, _state) {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { run } = require('../src/cli');
|
||||
|
||||
test('add then get round-trips a snippet', () => {
|
||||
const state = {};
|
||||
const added = run(['add', 'hello', 'hello', 'world'], state);
|
||||
assert.equal(added.code, 0);
|
||||
assert.equal(added.stdout, 'created hello\n');
|
||||
const got = run(['get', 'hello'], state);
|
||||
assert.equal(got.code, 0);
|
||||
assert.equal(got.stdout, 'hello world\n');
|
||||
});
|
||||
|
||||
test('list on empty state', () => {
|
||||
const result = run(['list'], {});
|
||||
assert.equal(result.code, 0);
|
||||
assert.equal(result.stdout, 'no snippets\n');
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "forge-cli",
|
||||
"category": "spec-thoroughness",
|
||||
"manualIds": ["skill:tdd-workflow"],
|
||||
"checkTimeoutMs": 30000,
|
||||
"selection": {
|
||||
"id": "complex-forge-cli",
|
||||
"category": "complex-spec-thoroughness",
|
||||
"expectedIds": ["skill:tdd-workflow"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Build the snippet manager CLI per the README — all twelve numbered behaviors are contractual, including exact messages, exit codes, sorting, and the never-throw guarantee. `npm test` must pass, and add tests for the tricky edges (duplicates, invalid names, bad imports) so we don't regress them.
|
||||
Reference in New Issue
Block a user