diff --git a/docs/specs/POETIC-platform-overview.md b/docs/specs/PROSE-platform-overview.md similarity index 65% rename from docs/specs/POETIC-platform-overview.md rename to docs/specs/PROSE-platform-overview.md index b21e35b3..a394c249 100644 --- a/docs/specs/POETIC-platform-overview.md +++ b/docs/specs/PROSE-platform-overview.md @@ -1,6 +1,6 @@ -# POETIC Platform Overview +# PROSE Platform Overview -> POETIC = **P**urpose, **O**bservables, **E**dges, **T**riggers, **I**nvariants, **C**ontracts +> PROSE = **P**urpose, **R**ules & Constraints, **O**bservables, **S**cenarios, **E**xpectations This document describes StackMemory's platform behavior in plain English. Each section is intentionally testable and maps directly to integration tests in `src/__tests__/integration/platform-overview.test.ts`. @@ -18,6 +18,21 @@ Decisions, frames, and anchors written in one session must be retrievable in a s --- +## R — Rules & Constraints + +Boundaries the platform must respect. + +### R.1 Uninitialized projects +Commands that require an initialized project must fail gracefully with a clear message when run outside a `.stackmemory` project. + +### R.2 Empty result sets +Search queries that match nothing must return an empty result set, not an error. + +### R.3 Idempotent initialization +Running `stackmemory init` in an already-initialized project must not corrupt existing data. + +--- + ## O — Observables These are the externally visible states and queries the platform must support. @@ -36,89 +51,64 @@ After recording a decision, `stackmemory decision list` must include the decisio --- -## E — Edges & Constraints +## S — Scenarios -Boundaries the platform must respect. - -### E.1 Uninitialized projects -Commands that require an initialized project must fail gracefully with a clear message when run outside a `.stackmemory` project. +Events and actions that change platform state. -### E.2 Empty result sets -Search queries that match nothing must return an empty result set, not an error. - -### E.3 Idempotent initialization -Running `stackmemory init` in an already-initialized project must not corrupt existing data. - ---- - -## T — Triggers - -Events that change platform state. - -### T.1 Frame push +### S.1 Frame push Pushing a frame creates a new scoped context entry and updates the active frame stack. -### T.2 Frame pop +### S.2 Frame pop Popping a frame removes the active frame and restores the previous frame as active. -### T.3 Decision record +### S.3 Decision record Recording a decision persists it with a timestamp and rationale. -### T.4 Snapshot capture +### S.4 Snapshot capture Capturing a snapshot persists the current context state for later handoff. --- -## I — Invariants +## E — Expectations -Properties that must always hold true. +Properties that must always hold true and guarantees the platform makes to users and integrations. -### I.1 Frame stack integrity +### E.1 Frame stack integrity At any time, there is at most one active frame, and the frame stack is non-circular. -### I.2 Decision immutability +### E.2 Decision immutability Once recorded, a decision's identifier, title, and rationale must not change. -### I.3 Project isolation +### E.3 Project isolation Two projects initialized in different directories must not share context unless explicitly synced. ---- - -## C — Contracts - -Guarantees the platform makes to users and integrations. - -### C.1 CLI contract +### E.4 CLI contract All CLI commands return a zero exit code on success and a non-zero exit code on failure, with human-readable output. -### C.2 SQLite contract +### E.5 SQLite contract The local SQLite database is self-contained within `.stackmemory/` and portable across machines with the same CLI version. -### C.3 MCP contract -When exposed via MCP, tools advertise stable names and JSON schemas that do not change without a version bump. - --- ## Test mapping -| POETIC ID | Test case | -|-----------|-----------| +| PROSE ID | Test case | +|----------|-----------| | P.1 | `initializes a project with stackmemory init` | | P.2 | `retrieves decisions across sessions` | +| R.1 | `fails gracefully outside an initialized project` | +| R.2 | `returns empty results for non-matching search` | +| R.3 | `init is idempotent` | | O.1 | `reports status for initialized and uninitialized projects` | | O.2 | `lists pushed frames` | | O.3 | `lists recorded decisions` | | O.4 | `searches stored context` | -| E.1 | `fails gracefully outside an initialized project` | -| E.2 | `returns empty results for non-matching search` | -| E.3 | `init is idempotent` | -| T.1 | `pushing a frame creates a scoped entry` | -| T.2 | `popping a frame restores the previous frame` | -| T.3 | `recording a decision persists rationale` | -| T.4 | `capturing a snapshot persists handoff state` | -| I.1 | `active frame stack remains consistent` | -| I.2 | `recorded decisions are immutable` | -| I.3 | `projects in different directories are isolated` | -| C.1 | `CLI commands return correct exit codes` | -| C.2 | `SQLite database is self-contained in .stackmemory` | -| C.3 | `MCP tools expose stable schemas` | +| S.1 | `pushing a frame creates a scoped entry` | +| S.2 | `popping a frame restores the previous frame` | +| S.3 | `recording a decision persists rationale` | +| S.4 | `capturing a snapshot persists handoff state` | +| E.1 | `active frame stack remains consistent` | +| E.2 | `recorded decisions are immutable` | +| E.3 | `projects in different directories are isolated` | +| E.4 | `CLI commands return correct exit codes` | +| E.5 | `SQLite database is self-contained in .stackmemory` | diff --git a/src/__tests__/integration/platform-overview.test.ts b/src/__tests__/integration/platform-overview.test.ts index 02d8fcfb..7a8fee4e 100644 --- a/src/__tests__/integration/platform-overview.test.ts +++ b/src/__tests__/integration/platform-overview.test.ts @@ -1,10 +1,10 @@ /** - * POETIC Platform Overview Integration Tests + * PROSE Platform Overview Integration Tests * - * POETIC = Purpose, Observables, Edges, Triggers, Invariants, Contracts - * Spec source: docs/specs/POETIC-platform-overview.md + * PROSE = Purpose, Rules & Constraints, Observables, Scenarios, Expectations + * Spec source: docs/specs/PROSE-platform-overview.md * - * Each test maps to a POETIC ID in the spec so prose and code stay coupled. + * Each test maps to a PROSE ID in the spec so prose and code stay coupled. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; @@ -49,13 +49,13 @@ function run(args: string, cwd: string, expectError = false): string { } } -describe('POETIC Platform Overview', { timeout: 60_000 }, () => { +describe('PROSE Platform Overview', { timeout: 60_000 }, () => { let testDir: string; beforeEach(() => { const rawDir = path.join( os.tmpdir(), - `stackmemory-poetic-test-${Date.now()}-${Math.random().toString(36).slice(2, 7)}` + `stackmemory-prose-test-${Date.now()}-${Math.random().toString(36).slice(2, 7)}` ); fs.mkdirSync(rawDir, { recursive: true }); // Resolve symlinks (macOS /var -> /private/var) so the test's path matches @@ -96,6 +96,52 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => { }); }); + // --------------------------------------------------------------------------- + // R — Rules & Constraints + // --------------------------------------------------------------------------- + + describe('R.1 Uninitialized projects', () => { + it('fails gracefully outside an initialized project', () => { + run('init', testDir); + fs.rmSync(path.join(testDir, '.stackmemory', 'context.db'), { + force: true, + }); + + const output = execSync(`node ${cliPath} context show`, { + cwd: testDir, + encoding: 'utf8', + timeout: 30_000, + env: { ...process.env, STACKMEMORY_LOG_LEVEL: 'ERROR' }, + }); + expect(output).toMatch(/not initialized|not set up|no project/i); + }); + }); + + describe('R.2 Empty result sets', () => { + it('returns empty results for non-matching search', () => { + run('init', testDir); + run('decision add "Some decision" --why "Some rationale"', testDir); + + const searchOutput = run('search xyznonexistentquery123', testDir); + // Empty result should not throw; output should indicate no matches or be empty-ish. + expect(searchOutput).not.toContain('Error'); + }); + }); + + describe('R.3 Idempotent initialization', () => { + it('init is idempotent', () => { + run('init', testDir); + const firstInitFiles = fs.readdirSync(path.join(testDir, '.stackmemory')); + + run('init', testDir); + const secondInitFiles = fs.readdirSync( + path.join(testDir, '.stackmemory') + ); + + expect(secondInitFiles.sort()).toEqual(firstInitFiles.sort()); + }); + }); + // --------------------------------------------------------------------------- // O — Observables // --------------------------------------------------------------------------- @@ -154,57 +200,13 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => { const searchOutput = run('search FTS5', testDir); expect(searchOutput).toMatch(/FTS5|search|result/i); }); - - it('returns empty results for non-matching search', () => { - run('init', testDir); - run('decision add "Some decision" --why "Some rationale"', testDir); - - const searchOutput = run('search xyznonexistentquery123', testDir); - // Empty result should not throw; output should indicate no matches or be empty-ish. - expect(searchOutput).not.toContain('Error'); - }); }); // --------------------------------------------------------------------------- - // E — Edges & Constraints + // S — Scenarios // --------------------------------------------------------------------------- - describe('E.1 Uninitialized projects', () => { - it('fails gracefully outside an initialized project', () => { - run('init', testDir); - fs.rmSync(path.join(testDir, '.stackmemory', 'context.db'), { - force: true, - }); - - const output = execSync(`node ${cliPath} context show`, { - cwd: testDir, - encoding: 'utf8', - timeout: 30_000, - env: { ...process.env, STACKMEMORY_LOG_LEVEL: 'ERROR' }, - }); - expect(output).toMatch(/not initialized|not set up|no project/i); - }); - }); - - describe('E.3 Idempotent initialization', () => { - it('init is idempotent', () => { - run('init', testDir); - const firstInitFiles = fs.readdirSync(path.join(testDir, '.stackmemory')); - - run('init', testDir); - const secondInitFiles = fs.readdirSync( - path.join(testDir, '.stackmemory') - ); - - expect(secondInitFiles.sort()).toEqual(firstInitFiles.sort()); - }); - }); - - // --------------------------------------------------------------------------- - // T — Triggers - // --------------------------------------------------------------------------- - - describe('T.1 Frame push', () => { + describe('S.1 Frame push', () => { it('pushing a frame creates a scoped entry', () => { run('init', testDir); run('context push "Feature: payment flow"', testDir); @@ -214,7 +216,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => { }); }); - describe('T.2 Frame pop', () => { + describe('S.2 Frame pop', () => { it('popping a frame restores the previous frame', () => { run('init', testDir); run('context push "Outer frame"', testDir); @@ -232,7 +234,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => { }); }); - describe('T.3 Decision record', () => { + describe('S.3 Decision record', () => { it('recording a decision persists rationale', () => { run('init', testDir); run('decision add "Use pnpm" --why "Fast, disk efficient"', testDir); @@ -243,7 +245,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => { }); }); - describe('T.4 Snapshot capture', () => { + describe('S.4 Snapshot capture', () => { it('capturing a snapshot persists handoff state', () => { run('init', testDir); run( @@ -252,7 +254,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => { ); const captureOutput = run( - 'capture --no-commit -m "POETIC snapshot"', + 'capture --no-commit -m "PROSE snapshot"', testDir ); expect(captureOutput).toMatch(/handoff|snapshot|capture/i); @@ -260,10 +262,10 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => { }); // --------------------------------------------------------------------------- - // I — Invariants + // E — Expectations // --------------------------------------------------------------------------- - describe('I.1 Frame stack integrity', () => { + describe('E.1 Frame stack integrity', () => { it('active frame stack remains consistent', () => { run('init', testDir); run('context push "Alpha frame"', testDir); @@ -280,7 +282,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => { }); }); - describe('I.2 Decision immutability', () => { + describe('E.2 Decision immutability', () => { it('recorded decisions are immutable', () => { run('init', testDir); run( @@ -302,7 +304,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => { }); }); - describe('I.3 Project isolation', () => { + describe('E.3 Project isolation', () => { it('projects in different directories are isolated', () => { const projectA = path.join(testDir, 'project-a'); const projectB = path.join(testDir, 'project-b'); @@ -320,11 +322,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => { }); }); - // --------------------------------------------------------------------------- - // C — Contracts - // --------------------------------------------------------------------------- - - describe('C.1 CLI contract', () => { + describe('E.4 CLI contract', () => { it('CLI commands return correct exit codes', () => { expect(() => run('init', testDir)).not.toThrow(); expect(() => run('decision list', testDir)).not.toThrow(); @@ -334,7 +332,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => { }); }); - describe('C.2 SQLite contract', () => { + describe('E.5 SQLite contract', () => { it('SQLite database is self-contained in .stackmemory', () => { run('init', testDir); run('decision add "DB test" --why "Check local DB"', testDir);