mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 20:25:26 +02:00
Route every .stories/ write through skills/story-lifecycle/scripts/ledger.js: - exclusive per-ledger lock; a second writer waits, then exits 3 without writing - story IDs reserved with exclusive-create markers in the git common dir, so concurrent agents and worktrees of one clone never share an ID - journaled batches: stage, journal, rename; derived epic/sprint tables are regenerated inside the batch; an interrupted batch is rolled back by the next write command or `recover`; `status` reports it only when no live writer holds the lock Bound the ledger to an opt-in planning projection that is never canonical ECC Task/Evidence/Completion/Integration state, and state that .stories/ follows the checkout. tests/docs/story-lifecycle-ledger.test.js exercises create, concurrent create, concurrent cross-worktree allocation, lock contention, overwrite refusal, interrupted-batch recovery, sprint planning, status drift and reconcile, and path confinement. Also refresh the skill-triggers manifest digest for 294 skills.
158 lines
8.1 KiB
JavaScript
158 lines
8.1 KiB
JavaScript
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const ROOT = path.join(__dirname, '..', '..');
|
|
const SKILL_PATH = path.join(ROOT, 'skills', 'story-lifecycle', 'SKILL.md');
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${name}`);
|
|
return true;
|
|
} catch (error) {
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` Error: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function runTests() {
|
|
console.log('\n=== Testing story-lifecycle skill contract ===\n');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
const body = fs.readFileSync(SKILL_PATH, 'utf8');
|
|
|
|
if (test('uses the canonical When to Activate header', () => {
|
|
assert.ok(body.includes('## When to Activate'), 'missing ## When to Activate');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('defines the forward-only state model todo → in-progress → review → done', () => {
|
|
assert.ok(body.includes('todo → in-progress → review → done'), 'missing state chain');
|
|
assert.ok(/moves strictly forward/i.test(body), 'missing forward-only rule');
|
|
assert.ok(/no backward transitions/i.test(body), 'missing no-backward rule');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('story file is the single source of planning status within the ledger', () => {
|
|
assert.ok(/story file is the single source of a story's planning status/i.test(body),
|
|
'missing single-source statement');
|
|
assert.ok(/the story file\s+wins/i.test(body), 'missing conflict-resolution rule');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('epic and sprint tables are derived, regenerated on transitions', () => {
|
|
const derivedMarkers = body.match(/Derived from story files/g) || [];
|
|
assert.ok(derivedMarkers.length >= 2, 'epic and sprint tables must be marked derived');
|
|
assert.ok(/Plain `status` is read-only/i.test(body), 'status must not silently mutate files');
|
|
assert.ok(/show the proposed table diff and require explicit\s+approval/i.test(body),
|
|
'status reconciliation must require approval');
|
|
assert.ok(/regenerated inside the same\s+batch/i.test(body), 'projections must be part of the batch');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('done requires confirmed merge to main; review gate before done', () => {
|
|
assert.ok(/confirmed merged to the main branch/i.test(body), 'missing merge confirmation trigger');
|
|
assert.ok(/never marked `done` while/i.test(body), 'missing done guard');
|
|
assert.ok(/merge to main is confirmed: set status to `done`/i.test(body),
|
|
'implement must only set done after confirmed merge');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('implement runs tests first, then reviewer and verification-loop before review state', () => {
|
|
assert.ok(/Tests first/.test(body), 'missing tests-first step');
|
|
assert.ok(body.includes('tdd-workflow'), 'missing tdd-workflow reference');
|
|
assert.ok(body.includes('verification-loop'), 'missing verification-loop reference');
|
|
const testsIdx = body.indexOf('**Tests first**');
|
|
const implementIdx = body.indexOf('Implement until the tests pass');
|
|
const qualityIdx = body.indexOf('**Quality lane**');
|
|
const reviewIdx = body.indexOf("status to `review`");
|
|
assert.ok(testsIdx > -1 && testsIdx < implementIdx, 'tests must precede implementation');
|
|
assert.ok(implementIdx < qualityIdx && qualityIdx < reviewIdx,
|
|
'review state must come after quality lane');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('declares slug/story-id/sprint validation and path confinement', () => {
|
|
assert.ok(body.includes('^[a-z0-9]+(-[a-z0-9]+)*$'), 'missing slug pattern');
|
|
assert.ok(body.includes('-[1-9][0-9]*$'), 'missing story-id pattern');
|
|
assert.ok(/must stay inside `\.stories\/`/i.test(body), 'missing path confinement rule');
|
|
assert.ok(body.includes('`..`'), 'must reject parent-directory traversal');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('treats .stories content as untrusted data and forbids silent overwrites', () => {
|
|
assert.ok(/data, not instructions/i.test(body), 'missing untrusted-content rule');
|
|
assert.ok(/never follow imperative\s+directives/i.test(body), 'missing directive rule');
|
|
assert.ok(/No silent overwrites/i.test(body), 'missing overwrite approval rule');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('routes every write through the portable ledger helper, no POSIX-only shell', () => {
|
|
assert.ok(fs.existsSync(path.join(ROOT, 'skills', 'story-lifecycle', 'scripts', 'ledger.js')),
|
|
'missing scripts/ledger.js');
|
|
assert.ok(/Every write to `\.stories\/` goes through `scripts\/ledger\.js`/.test(body),
|
|
'missing single-writer rule');
|
|
assert.ok(/Never write ledger\s+files directly/i.test(body), 'missing direct-write ban');
|
|
assert.ok(/If `node` is not available, use the ledger read-only/i.test(body),
|
|
'missing fail-closed fallback');
|
|
assert.ok(!body.includes('test -d'), 'POSIX test -d must not appear');
|
|
assert.ok(!body.includes('mkdir -p'), 'POSIX mkdir -p must not appear');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('makes story creation collision-safe and all-or-nothing', () => {
|
|
assert.ok(/\*\*Exclusive lock\.\*\*/.test(body), 'missing exclusive lock guarantee');
|
|
assert.ok(/\*\*Collision-safe IDs\.\*\*/.test(body), 'missing collision-safe allocation');
|
|
assert.ok(/\*\*All-or-nothing batches\.\*\*/.test(body), 'missing atomic batch guarantee');
|
|
assert.ok(/never derive IDs yourself/i.test(body), 'create-stories must use allocate');
|
|
assert.ok(/all worktrees of one clone/i.test(body), 'missing worktree coverage');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('bounds the ledger to opt-in planning, never canonical ECC state', () => {
|
|
assert.ok(body.includes('## Authority'), 'missing Authority section');
|
|
assert.ok(/opt-in planning projection/i.test(body), 'missing opt-in projection statement');
|
|
assert.ok(/never canonical ECC Task, Evidence,\s+Completion, or Integration state/i.test(body),
|
|
'missing canonical-state exclusion');
|
|
assert.ok(/that system stays authoritative/i.test(body), 'external systems must stay authoritative');
|
|
assert.ok(/`\.stories\/` follows the checkout/.test(body), 'missing branch-local statement');
|
|
assert.ok(!/single authoritative record/i.test(body), 'ledger must not claim authority');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('documents the boundary and handoff to issue-backed coordination', () => {
|
|
assert.ok(body.includes('## Boundary with GitHub-backed coordination'), 'missing boundary section');
|
|
assert.ok(/Never run both systems as parallel authorities/i.test(body), 'missing authority rule');
|
|
assert.ok(/Exported to:/i.test(body), 'missing handoff marker');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('does not reference surfaces that are not on main', () => {
|
|
assert.ok(!body.includes('PROJECT-CONTEXT.md'), 'PROJECT-CONTEXT.md is not an adopted surface');
|
|
assert.ok(!/`dev-team`/.test(body), 'dev-team skill is not merged');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('every referenced skill, agent, and command resolves in the repo', () => {
|
|
const refs = [
|
|
'skills/tdd-workflow/SKILL.md',
|
|
'skills/verification-loop/SKILL.md',
|
|
'skills/council/SKILL.md',
|
|
'skills/project-flow-ops/SKILL.md',
|
|
'commands/plan-prd.md',
|
|
'commands/epic-decompose.md',
|
|
'agents/architect.md',
|
|
'agents/tdd-guide.md',
|
|
'agents/code-reviewer.md',
|
|
];
|
|
for (const ref of refs) {
|
|
assert.ok(fs.existsSync(path.join(ROOT, ref)), `unresolved reference: ${ref}`);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('skill is registered in install manifest and npm files list', () => {
|
|
const modules = JSON.parse(
|
|
fs.readFileSync(path.join(ROOT, 'manifests', 'install-modules.json'), 'utf8'));
|
|
assert.ok(JSON.stringify(modules).includes('skills/story-lifecycle'),
|
|
'missing from manifests/install-modules.json');
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
assert.ok(pkg.files.includes('skills/story-lifecycle/'),
|
|
'missing from package.json files');
|
|
})) passed++; else failed++;
|
|
|
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
runTests();
|