Skip to content

Commit 4a10eae

Browse files
author
StackMemory Bot (CLI)
committed
refactor(docs,tests): rename POETIC spec format to PROSE
Replace the POETIC acronym (Purpose, Observables, Edges, Triggers, Invariants, Contracts) with PROSE: - P — Purpose - R — Rules & Constraints - O — Observables - S — Scenarios - E — Expectations Rename the spec file and update all test comments, section headers, and IDs to match. No behavioral changes to the tests themselves.
1 parent 7a6bf53 commit 4a10eae

2 files changed

Lines changed: 108 additions & 120 deletions

File tree

Lines changed: 44 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# POETIC Platform Overview
1+
# PROSE Platform Overview
22

3-
> POETIC = **P**urpose, **O**bservables, **E**dges, **T**riggers, **I**nvariants, **C**ontracts
3+
> PROSE = **P**urpose, **R**ules & Constraints, **O**bservables, **S**cenarios, **E**xpectations
44
55
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`.
66

@@ -18,6 +18,21 @@ Decisions, frames, and anchors written in one session must be retrievable in a s
1818

1919
---
2020

21+
## R — Rules & Constraints
22+
23+
Boundaries the platform must respect.
24+
25+
### R.1 Uninitialized projects
26+
Commands that require an initialized project must fail gracefully with a clear message when run outside a `.stackmemory` project.
27+
28+
### R.2 Empty result sets
29+
Search queries that match nothing must return an empty result set, not an error.
30+
31+
### R.3 Idempotent initialization
32+
Running `stackmemory init` in an already-initialized project must not corrupt existing data.
33+
34+
---
35+
2136
## O — Observables
2237

2338
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
3651

3752
---
3853

39-
## EEdges & Constraints
54+
## SScenarios
4055

41-
Boundaries the platform must respect.
42-
43-
### E.1 Uninitialized projects
44-
Commands that require an initialized project must fail gracefully with a clear message when run outside a `.stackmemory` project.
56+
Events and actions that change platform state.
4557

46-
### E.2 Empty result sets
47-
Search queries that match nothing must return an empty result set, not an error.
48-
49-
### E.3 Idempotent initialization
50-
Running `stackmemory init` in an already-initialized project must not corrupt existing data.
51-
52-
---
53-
54-
## T — Triggers
55-
56-
Events that change platform state.
57-
58-
### T.1 Frame push
58+
### S.1 Frame push
5959
Pushing a frame creates a new scoped context entry and updates the active frame stack.
6060

61-
### T.2 Frame pop
61+
### S.2 Frame pop
6262
Popping a frame removes the active frame and restores the previous frame as active.
6363

64-
### T.3 Decision record
64+
### S.3 Decision record
6565
Recording a decision persists it with a timestamp and rationale.
6666

67-
### T.4 Snapshot capture
67+
### S.4 Snapshot capture
6868
Capturing a snapshot persists the current context state for later handoff.
6969

7070
---
7171

72-
## IInvariants
72+
## EExpectations
7373

74-
Properties that must always hold true.
74+
Properties that must always hold true and guarantees the platform makes to users and integrations.
7575

76-
### I.1 Frame stack integrity
76+
### E.1 Frame stack integrity
7777
At any time, there is at most one active frame, and the frame stack is non-circular.
7878

79-
### I.2 Decision immutability
79+
### E.2 Decision immutability
8080
Once recorded, a decision's identifier, title, and rationale must not change.
8181

82-
### I.3 Project isolation
82+
### E.3 Project isolation
8383
Two projects initialized in different directories must not share context unless explicitly synced.
8484

85-
---
86-
87-
## C — Contracts
88-
89-
Guarantees the platform makes to users and integrations.
90-
91-
### C.1 CLI contract
85+
### E.4 CLI contract
9286
All CLI commands return a zero exit code on success and a non-zero exit code on failure, with human-readable output.
9387

94-
### C.2 SQLite contract
88+
### E.5 SQLite contract
9589
The local SQLite database is self-contained within `.stackmemory/` and portable across machines with the same CLI version.
9690

97-
### C.3 MCP contract
98-
When exposed via MCP, tools advertise stable names and JSON schemas that do not change without a version bump.
99-
10091
---
10192

10293
## Test mapping
10394

104-
| POETIC ID | Test case |
105-
|-----------|-----------|
95+
| PROSE ID | Test case |
96+
|----------|-----------|
10697
| P.1 | `initializes a project with stackmemory init` |
10798
| P.2 | `retrieves decisions across sessions` |
99+
| R.1 | `fails gracefully outside an initialized project` |
100+
| R.2 | `returns empty results for non-matching search` |
101+
| R.3 | `init is idempotent` |
108102
| O.1 | `reports status for initialized and uninitialized projects` |
109103
| O.2 | `lists pushed frames` |
110104
| O.3 | `lists recorded decisions` |
111105
| O.4 | `searches stored context` |
112-
| E.1 | `fails gracefully outside an initialized project` |
113-
| E.2 | `returns empty results for non-matching search` |
114-
| E.3 | `init is idempotent` |
115-
| T.1 | `pushing a frame creates a scoped entry` |
116-
| T.2 | `popping a frame restores the previous frame` |
117-
| T.3 | `recording a decision persists rationale` |
118-
| T.4 | `capturing a snapshot persists handoff state` |
119-
| I.1 | `active frame stack remains consistent` |
120-
| I.2 | `recorded decisions are immutable` |
121-
| I.3 | `projects in different directories are isolated` |
122-
| C.1 | `CLI commands return correct exit codes` |
123-
| C.2 | `SQLite database is self-contained in .stackmemory` |
124-
| C.3 | `MCP tools expose stable schemas` |
106+
| S.1 | `pushing a frame creates a scoped entry` |
107+
| S.2 | `popping a frame restores the previous frame` |
108+
| S.3 | `recording a decision persists rationale` |
109+
| S.4 | `capturing a snapshot persists handoff state` |
110+
| E.1 | `active frame stack remains consistent` |
111+
| E.2 | `recorded decisions are immutable` |
112+
| E.3 | `projects in different directories are isolated` |
113+
| E.4 | `CLI commands return correct exit codes` |
114+
| E.5 | `SQLite database is self-contained in .stackmemory` |

src/__tests__/integration/platform-overview.test.ts

Lines changed: 64 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/**
2-
* POETIC Platform Overview Integration Tests
2+
* PROSE Platform Overview Integration Tests
33
*
4-
* POETIC = Purpose, Observables, Edges, Triggers, Invariants, Contracts
5-
* Spec source: docs/specs/POETIC-platform-overview.md
4+
* PROSE = Purpose, Rules & Constraints, Observables, Scenarios, Expectations
5+
* Spec source: docs/specs/PROSE-platform-overview.md
66
*
7-
* Each test maps to a POETIC ID in the spec so prose and code stay coupled.
7+
* Each test maps to a PROSE ID in the spec so prose and code stay coupled.
88
*/
99

1010
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
@@ -49,13 +49,13 @@ function run(args: string, cwd: string, expectError = false): string {
4949
}
5050
}
5151

52-
describe('POETIC Platform Overview', { timeout: 60_000 }, () => {
52+
describe('PROSE Platform Overview', { timeout: 60_000 }, () => {
5353
let testDir: string;
5454

5555
beforeEach(() => {
5656
const rawDir = path.join(
5757
os.tmpdir(),
58-
`stackmemory-poetic-test-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
58+
`stackmemory-prose-test-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
5959
);
6060
fs.mkdirSync(rawDir, { recursive: true });
6161
// Resolve symlinks (macOS /var -> /private/var) so the test's path matches
@@ -96,6 +96,52 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => {
9696
});
9797
});
9898

99+
// ---------------------------------------------------------------------------
100+
// R — Rules & Constraints
101+
// ---------------------------------------------------------------------------
102+
103+
describe('R.1 Uninitialized projects', () => {
104+
it('fails gracefully outside an initialized project', () => {
105+
run('init', testDir);
106+
fs.rmSync(path.join(testDir, '.stackmemory', 'context.db'), {
107+
force: true,
108+
});
109+
110+
const output = execSync(`node ${cliPath} context show`, {
111+
cwd: testDir,
112+
encoding: 'utf8',
113+
timeout: 30_000,
114+
env: { ...process.env, STACKMEMORY_LOG_LEVEL: 'ERROR' },
115+
});
116+
expect(output).toMatch(/not initialized|not set up|no project/i);
117+
});
118+
});
119+
120+
describe('R.2 Empty result sets', () => {
121+
it('returns empty results for non-matching search', () => {
122+
run('init', testDir);
123+
run('decision add "Some decision" --why "Some rationale"', testDir);
124+
125+
const searchOutput = run('search xyznonexistentquery123', testDir);
126+
// Empty result should not throw; output should indicate no matches or be empty-ish.
127+
expect(searchOutput).not.toContain('Error');
128+
});
129+
});
130+
131+
describe('R.3 Idempotent initialization', () => {
132+
it('init is idempotent', () => {
133+
run('init', testDir);
134+
const firstInitFiles = fs.readdirSync(path.join(testDir, '.stackmemory'));
135+
136+
run('init', testDir);
137+
const secondInitFiles = fs.readdirSync(
138+
path.join(testDir, '.stackmemory')
139+
);
140+
141+
expect(secondInitFiles.sort()).toEqual(firstInitFiles.sort());
142+
});
143+
});
144+
99145
// ---------------------------------------------------------------------------
100146
// O — Observables
101147
// ---------------------------------------------------------------------------
@@ -154,57 +200,13 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => {
154200
const searchOutput = run('search FTS5', testDir);
155201
expect(searchOutput).toMatch(/FTS5|search|result/i);
156202
});
157-
158-
it('returns empty results for non-matching search', () => {
159-
run('init', testDir);
160-
run('decision add "Some decision" --why "Some rationale"', testDir);
161-
162-
const searchOutput = run('search xyznonexistentquery123', testDir);
163-
// Empty result should not throw; output should indicate no matches or be empty-ish.
164-
expect(searchOutput).not.toContain('Error');
165-
});
166203
});
167204

168205
// ---------------------------------------------------------------------------
169-
// EEdges & Constraints
206+
// SScenarios
170207
// ---------------------------------------------------------------------------
171208

172-
describe('E.1 Uninitialized projects', () => {
173-
it('fails gracefully outside an initialized project', () => {
174-
run('init', testDir);
175-
fs.rmSync(path.join(testDir, '.stackmemory', 'context.db'), {
176-
force: true,
177-
});
178-
179-
const output = execSync(`node ${cliPath} context show`, {
180-
cwd: testDir,
181-
encoding: 'utf8',
182-
timeout: 30_000,
183-
env: { ...process.env, STACKMEMORY_LOG_LEVEL: 'ERROR' },
184-
});
185-
expect(output).toMatch(/not initialized|not set up|no project/i);
186-
});
187-
});
188-
189-
describe('E.3 Idempotent initialization', () => {
190-
it('init is idempotent', () => {
191-
run('init', testDir);
192-
const firstInitFiles = fs.readdirSync(path.join(testDir, '.stackmemory'));
193-
194-
run('init', testDir);
195-
const secondInitFiles = fs.readdirSync(
196-
path.join(testDir, '.stackmemory')
197-
);
198-
199-
expect(secondInitFiles.sort()).toEqual(firstInitFiles.sort());
200-
});
201-
});
202-
203-
// ---------------------------------------------------------------------------
204-
// T — Triggers
205-
// ---------------------------------------------------------------------------
206-
207-
describe('T.1 Frame push', () => {
209+
describe('S.1 Frame push', () => {
208210
it('pushing a frame creates a scoped entry', () => {
209211
run('init', testDir);
210212
run('context push "Feature: payment flow"', testDir);
@@ -214,7 +216,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => {
214216
});
215217
});
216218

217-
describe('T.2 Frame pop', () => {
219+
describe('S.2 Frame pop', () => {
218220
it('popping a frame restores the previous frame', () => {
219221
run('init', testDir);
220222
run('context push "Outer frame"', testDir);
@@ -232,7 +234,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => {
232234
});
233235
});
234236

235-
describe('T.3 Decision record', () => {
237+
describe('S.3 Decision record', () => {
236238
it('recording a decision persists rationale', () => {
237239
run('init', testDir);
238240
run('decision add "Use pnpm" --why "Fast, disk efficient"', testDir);
@@ -243,7 +245,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => {
243245
});
244246
});
245247

246-
describe('T.4 Snapshot capture', () => {
248+
describe('S.4 Snapshot capture', () => {
247249
it('capturing a snapshot persists handoff state', () => {
248250
run('init', testDir);
249251
run(
@@ -252,18 +254,18 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => {
252254
);
253255

254256
const captureOutput = run(
255-
'capture --no-commit -m "POETIC snapshot"',
257+
'capture --no-commit -m "PROSE snapshot"',
256258
testDir
257259
);
258260
expect(captureOutput).toMatch(/handoff|snapshot|capture/i);
259261
});
260262
});
261263

262264
// ---------------------------------------------------------------------------
263-
// IInvariants
265+
// EExpectations
264266
// ---------------------------------------------------------------------------
265267

266-
describe('I.1 Frame stack integrity', () => {
268+
describe('E.1 Frame stack integrity', () => {
267269
it('active frame stack remains consistent', () => {
268270
run('init', testDir);
269271
run('context push "Alpha frame"', testDir);
@@ -280,7 +282,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => {
280282
});
281283
});
282284

283-
describe('I.2 Decision immutability', () => {
285+
describe('E.2 Decision immutability', () => {
284286
it('recorded decisions are immutable', () => {
285287
run('init', testDir);
286288
run(
@@ -302,7 +304,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => {
302304
});
303305
});
304306

305-
describe('I.3 Project isolation', () => {
307+
describe('E.3 Project isolation', () => {
306308
it('projects in different directories are isolated', () => {
307309
const projectA = path.join(testDir, 'project-a');
308310
const projectB = path.join(testDir, 'project-b');
@@ -320,11 +322,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => {
320322
});
321323
});
322324

323-
// ---------------------------------------------------------------------------
324-
// C — Contracts
325-
// ---------------------------------------------------------------------------
326-
327-
describe('C.1 CLI contract', () => {
325+
describe('E.4 CLI contract', () => {
328326
it('CLI commands return correct exit codes', () => {
329327
expect(() => run('init', testDir)).not.toThrow();
330328
expect(() => run('decision list', testDir)).not.toThrow();
@@ -334,7 +332,7 @@ describe('POETIC Platform Overview', { timeout: 60_000 }, () => {
334332
});
335333
});
336334

337-
describe('C.2 SQLite contract', () => {
335+
describe('E.5 SQLite contract', () => {
338336
it('SQLite database is self-contained in .stackmemory', () => {
339337
run('init', testDir);
340338
run('decision add "DB test" --why "Check local DB"', testDir);

0 commit comments

Comments
 (0)