diff --git a/plugins/ultraplan-local/examples/02-real-cli/REGENERATED.md b/plugins/ultraplan-local/examples/02-real-cli/REGENERATED.md new file mode 100644 index 0000000..a5cf2f3 --- /dev/null +++ b/plugins/ultraplan-local/examples/02-real-cli/REGENERATED.md @@ -0,0 +1,85 @@ +# REGENERATED.md — examples/02-real-cli + +| Field | Value | +|-------|-------| +| Calibrated against | ultraplan-local v3.4.1 | +| Last regenerated | TBD — filled in after the B3 pipeline run | +| Source brief author | Hand-authored by operator (B1 session, 2026-05-04) | +| Baseline author | B2 session, 2026-05-04 | +| Pipeline run | TBD — B3 will run `/ultraresearch-local`, `/ultraplan-local`, `/ultraexecute-local` | + +## What this example demonstrates + +(Placeholder — fylles ut når B3-pipeline-runet er ferdig. Skal forklare +hvorfor 02-real-cli er en *runnable* fixture, i motsetning til 01-add-verbose-flag +som kun er artifacts.) + +## Baseline (delivered by B2, 2026-05-04) + +`tally` — a ~80-line zero-dep Node.js CLI that counts literal-substring +occurrences of a pattern in a text file. Three flags (`--json`, +`-i`/`--ignore-case`, `--lines`), `--help`, exit codes 0/1/2. + +Layout: + +``` +examples/02-real-cli/ +├── tally.mjs # CLI (≈ 80 lines, hand-rolled argv parser) +├── tests/tally.test.mjs # 10 node:test cases (all pass < 3s) +├── fixtures/ +│ ├── sample.txt # 9-line input with known counts (foo×7, Foo×1, fo+ regex × 9) +│ └── poem.txt # 5-line input for --lines vs total distinction +└── REGENERATED.md # this file +``` + +(Placeholder — utvides med en mini-walk-through av baseline når B3 har +sluttført pipeline-runet.) + +## Pipeline run (delivered by B3, TBD) + +### `/ultraresearch-local` + +(Placeholder — research-swarmen forventes å rapportere 0 topics og produsere +en kort placeholder-brief. Fyll inn faktisk output her.) + +### `/ultraplan-local` + +(Placeholder — plan.md forventes med 3–5 steg som målretter `tally.mjs` ++ `tests/tally.test.mjs`. Lim inn step-overskrifter + manifest-summary.) + +### `/ultraexecute-local` + +(Placeholder — `progress.json` forventes med `verify_passed: true` på alle +steg. Lim inn commit-SHA-ene + steg-tellingen.) + +## How to re-run this example + +```bash +cd /path/to/ultraplan-local +# 1. Re-run the pipeline against the existing brief +/ultraresearch-local --project .claude/projects/2026-05-04-examples-02-real-cli +/ultraplan-local --project .claude/projects/2026-05-04-examples-02-real-cli +/ultraexecute-local --project .claude/projects/2026-05-04-examples-02-real-cli + +# 2. Verify all 10 Success Criteria from brief.md hold: +node --test examples/02-real-cli/tests/tally.test.mjs +node examples/02-real-cli/tally.mjs --regex 'fo+' examples/02-real-cli/fixtures/sample.txt +node examples/02-real-cli/tally.mjs --json --regex 'fo+' examples/02-real-cli/fixtures/sample.txt +``` + +(Placeholder — utvides med eksakte forventede verdier etter B3.) + +## Regeneration triggers + +When to re-run this example: + +- ultraplan-local minor version bump (e.g. v3.4 → v3.5) +- `plan_version` schema bump +- Manifest YAML required-key additions +- `progress.json` schema bump +- Pipeline-output format change (brief / research / plan / progress) + +When regenerating: re-run the pipeline against the existing `brief.md` and +update this file plus `examples/02-real-cli/` artifacts. The fixture itself +(`tally.mjs`, fixtures, baseline tests) stays stable across regenerations — +only the pipeline outputs change. diff --git a/plugins/ultraplan-local/examples/02-real-cli/fixtures/poem.txt b/plugins/ultraplan-local/examples/02-real-cli/fixtures/poem.txt new file mode 100644 index 0000000..ed5bf09 --- /dev/null +++ b/plugins/ultraplan-local/examples/02-real-cli/fixtures/poem.txt @@ -0,0 +1,5 @@ +foo on this line +nothing here +foo and foo here +silence +foo diff --git a/plugins/ultraplan-local/examples/02-real-cli/fixtures/sample.txt b/plugins/ultraplan-local/examples/02-real-cli/fixtures/sample.txt new file mode 100644 index 0000000..57b9326 --- /dev/null +++ b/plugins/ultraplan-local/examples/02-real-cli/fixtures/sample.txt @@ -0,0 +1,9 @@ +Foo bar baz +The quick brown fox jumps over the foo +foo foo bar foo +food for thought. +fooo, fooooo, very loud +This line has no match here. +A line without the magic word +And another one without it +The end. Final period. diff --git a/plugins/ultraplan-local/examples/02-real-cli/tally.mjs b/plugins/ultraplan-local/examples/02-real-cli/tally.mjs new file mode 100755 index 0000000..0f9d8da --- /dev/null +++ b/plugins/ultraplan-local/examples/02-real-cli/tally.mjs @@ -0,0 +1,80 @@ +#!/usr/bin/env node +import { readFileSync } from 'node:fs'; + +const HELP = `Usage: tally [options] + +Count literal-substring occurrences of in . + +Options: + -i, --ignore-case Case-insensitive matching + --lines Count lines containing pattern (not total occurrences) + --json Emit a JSON object on stdout + -h, --help Show this help and exit + +Exit codes: 0=success 1=file error 2=invalid argv +`; + +function fail(msg, code = 2) { + process.stderr.write(`tally: ${msg}\n`); + process.exit(code); +} + +function parseArgs(argv) { + const positional = []; + const flags = { json: false, ignoreCase: false, lines: false }; + for (const a of argv) { + if (a === '--json') flags.json = true; + else if (a === '-i' || a === '--ignore-case') flags.ignoreCase = true; + else if (a === '--lines') flags.lines = true; + else if (a === '-h' || a === '--help') { process.stdout.write(HELP); process.exit(0); } + else if (a.startsWith('-')) fail(`unknown flag: ${a}`); + else positional.push(a); + } + if (positional.length !== 2) fail('expected '); + return { pattern: positional[0], file: positional[1], flags }; +} + +function countOccurrences(text, pattern, ignoreCase) { + if (pattern.length === 0) return 0; + const haystack = ignoreCase ? text.toLowerCase() : text; + const needle = ignoreCase ? pattern.toLowerCase() : pattern; + let count = 0, idx = 0; + while ((idx = haystack.indexOf(needle, idx)) !== -1) { count++; idx += needle.length; } + return count; +} + +function countLines(text, pattern, ignoreCase) { + if (pattern.length === 0) return 0; + const needle = ignoreCase ? pattern.toLowerCase() : pattern; + let count = 0; + for (const line of text.split('\n')) { + const haystack = ignoreCase ? line.toLowerCase() : line; + if (haystack.includes(needle)) count++; + } + return count; +} + +function main() { + const { pattern, file, flags } = parseArgs(process.argv.slice(2)); + let text; + try { + text = readFileSync(file, 'utf8'); + } catch (err) { + const what = err.code === 'ENOENT' ? 'file not found' : 'read error'; + process.stderr.write(`tally: ${what}: ${file}\n`); + process.exit(1); + } + const count = flags.lines + ? countLines(text, pattern, flags.ignoreCase) + : countOccurrences(text, pattern, flags.ignoreCase); + if (flags.json) { + process.stdout.write(JSON.stringify({ + pattern, file, count, + flags: { json: flags.json, ignoreCase: flags.ignoreCase, lines: flags.lines }, + }) + '\n'); + } else { + process.stdout.write(count + '\n'); + } +} + +main(); diff --git a/plugins/ultraplan-local/examples/02-real-cli/tests/tally.test.mjs b/plugins/ultraplan-local/examples/02-real-cli/tests/tally.test.mjs new file mode 100644 index 0000000..0b132bb --- /dev/null +++ b/plugins/ultraplan-local/examples/02-real-cli/tests/tally.test.mjs @@ -0,0 +1,93 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; + +const here = dirname(fileURLToPath(import.meta.url)); +const TALLY = resolve(here, '..', 'tally.mjs'); +const SAMPLE = resolve(here, '..', 'fixtures', 'sample.txt'); +const POEM = resolve(here, '..', 'fixtures', 'poem.txt'); + +function run(...args) { + return spawnSync('node', [TALLY, ...args], { encoding: 'utf8' }); +} + +test('plain count: tally foo sample.txt prints 7', () => { + const r = run('foo', SAMPLE); + assert.equal(r.status, 0); + assert.equal(r.stdout.trim(), '7'); + assert.equal(r.stderr, ''); +}); + +test('JSON output: tally --json foo sample.txt parses with count 7', () => { + const r = run('--json', 'foo', SAMPLE); + assert.equal(r.status, 0); + const parsed = JSON.parse(r.stdout); + assert.equal(parsed.count, 7); + assert.equal(parsed.pattern, 'foo'); + assert.equal(parsed.flags.json, true); + assert.equal(parsed.flags.ignoreCase, false); + assert.equal(parsed.flags.lines, false); +}); + +test('case-sensitive default: tally Foo sample.txt prints 1', () => { + const r = run('Foo', SAMPLE); + assert.equal(r.status, 0); + assert.equal(r.stdout.trim(), '1'); +}); + +test('case-insensitive: tally -i Foo == tally -i foo (and exceeds case-sensitive)', () => { + const ri1 = run('-i', 'Foo', SAMPLE); + const ri2 = run('-i', 'foo', SAMPLE); + const rcs = run('foo', SAMPLE); + assert.equal(ri1.status, 0); + assert.equal(ri2.status, 0); + assert.equal(ri1.stdout, ri2.stdout); + assert.ok(Number(ri1.stdout) > Number(rcs.stdout)); +}); + +test('--lines mode: tally --lines foo poem.txt prints 3 (not total occurrences 4)', () => { + const lines = run('--lines', 'foo', POEM); + const total = run('foo', POEM); + assert.equal(lines.status, 0); + assert.equal(total.status, 0); + assert.equal(lines.stdout.trim(), '3'); + assert.equal(total.stdout.trim(), '4'); +}); + +test('flag in last position: tally foo sample.txt --json equals tally --json foo sample.txt', () => { + const last = run('foo', SAMPLE, '--json'); + const first = run('--json', 'foo', SAMPLE); + assert.equal(last.status, 0); + assert.equal(first.status, 0); + assert.equal(last.stdout, first.stdout); +}); + +test('missing argument: tally foo exits 2 with stderr', () => { + const r = run('foo'); + assert.equal(r.status, 2); + assert.match(r.stderr, /^tally: /); + assert.equal(r.stdout, ''); +}); + +test('unknown flag: tally --unknown foo sample.txt exits 2 with stderr', () => { + const r = run('--unknown', 'foo', SAMPLE); + assert.equal(r.status, 2); + assert.match(r.stderr, /^tally: /); + assert.equal(r.stdout, ''); +}); + +test('file not found: tally foo /does/not/exist exits 1 with stderr', () => { + const r = run('foo', '/does/not/exist'); + assert.equal(r.status, 1); + assert.match(r.stderr, /^tally: /); + assert.equal(r.stdout, ''); +}); + +test('--help: stdout contains "Usage:", exit 0', () => { + const r = run('--help'); + assert.equal(r.status, 0); + assert.match(r.stdout, /Usage:/); + assert.match(r.stdout, /--ignore-case/); +});