@bluewombat/work-ledger 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/SPECS.md +401 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger/check-plan.d.ts +17 -0
- package/dist/ledger/check-plan.d.ts.map +1 -0
- package/dist/ledger/check-plan.js +73 -0
- package/dist/ledger/check-plan.js.map +1 -0
- package/dist/ledger/open-work-ledger.d.ts +122 -0
- package/dist/ledger/open-work-ledger.d.ts.map +1 -0
- package/dist/ledger/open-work-ledger.js +641 -0
- package/dist/ledger/open-work-ledger.js.map +1 -0
- package/dist/persist/port.d.ts +13 -0
- package/dist/persist/port.d.ts.map +1 -0
- package/dist/persist/port.js +2 -0
- package/dist/persist/port.js.map +1 -0
- package/dist/records.d.ts +125 -0
- package/dist/records.d.ts.map +1 -0
- package/dist/records.js +3 -0
- package/dist/records.js.map +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# WorkLedger
|
|
2
|
+
|
|
3
|
+
Kernel package: the durable store of FeatureStandards, Subtasks, states,
|
|
4
|
+
Attempts, and Traces. Glue writes here before it acts.
|
|
5
|
+
|
|
6
|
+
Behavioural contract: [SPECS.md](./SPECS.md). Persistence adapters live under
|
|
7
|
+
`packages/` (`@bluewombat/persist-fs`, `@bluewombat/persist-sqlite`). This
|
|
8
|
+
package owns the Port, not the bytes.
|
|
9
|
+
|
|
10
|
+
## Stack
|
|
11
|
+
|
|
12
|
+
Repository stack — TypeScript, Node 24.20, Biome, `node:test`. See [docs/DEVELOPMENT.md](../../../docs/DEVELOPMENT.md).
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
From the repository root:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
nvm use
|
|
20
|
+
npm install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
| Task | Command |
|
|
26
|
+
| --------- | ---------------- |
|
|
27
|
+
| Lint | `npm run lint` |
|
|
28
|
+
| Format | `npm run format` |
|
|
29
|
+
| Typecheck | `npm run tsc` |
|
|
30
|
+
| Test | `npm test` |
|
|
31
|
+
| Build | `npm run build` |
|
|
32
|
+
|
|
33
|
+
There is no CLI in this implementation. Glue injects a persistence adapter and
|
|
34
|
+
calls commands on the returned ledger.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { openWorkLedger } from "@bluewombat/work-ledger";
|
|
38
|
+
import { openFilesystemPersist } from "@bluewombat/persist-fs";
|
|
39
|
+
|
|
40
|
+
const persist = await openFilesystemPersist({ root: "/absolute/path/to/ledger" });
|
|
41
|
+
const ledger = openWorkLedger({ persist });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
SQLite is the same Port:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { openSqlitePersist } from "@bluewombat/persist-sqlite";
|
|
48
|
+
|
|
49
|
+
const persist = await openSqlitePersist({ path: "/absolute/path/to/ledger.sqlite" });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Layout
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
SPECS.md behavioural contract
|
|
56
|
+
README.md how to build and run
|
|
57
|
+
src/
|
|
58
|
+
index.ts openWorkLedger; records; Persistence Port
|
|
59
|
+
records.ts persisted shapes (no behaviour)
|
|
60
|
+
persist/port.ts Persistence Port only
|
|
61
|
+
ledger/ commands and queries (imports Port, not an adapter)
|
|
62
|
+
```
|
package/SPECS.md
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# WorkLedger
|
|
2
|
+
|
|
3
|
+
Kernel package, not a Transformer. It has many operations, not one IN and one
|
|
4
|
+
OUT. Its code, identifiers, comments, and messages name only what it owns:
|
|
5
|
+
Feature records, Plans, Attempts, Traces, states, bails, and the Persistence
|
|
6
|
+
Port. No Transformer is named inside that perimeter, because none of them exist
|
|
7
|
+
there.
|
|
8
|
+
|
|
9
|
+
WorkLedger is the durable store of FeatureStandards, Subtasks, states, Attempts,
|
|
10
|
+
and Traces. It is the system's source of truth. Glue writes here **before** it
|
|
11
|
+
acts. Isolated workspaces are a consequence of that state, never the source of
|
|
12
|
+
it.
|
|
13
|
+
|
|
14
|
+
WorkLedger does not isolate a directory, run a Builder, fold a Child, or report
|
|
15
|
+
to a FeatureManager. It accepts an already-normalized FeatureStandard (or an
|
|
16
|
+
`invalid` verdict). It does not convert raw intentions.
|
|
17
|
+
|
|
18
|
+
Callers inject a persistence adapter. Adapters are their own packages under
|
|
19
|
+
`packages/` (`persist-fs`, `persist-sqlite`, later `persist-postgres`).
|
|
20
|
+
The domain never imports an adapter; an adapter never imports the ledger.
|
|
21
|
+
|
|
22
|
+
## 1. Job
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
caller WorkLedger adapter
|
|
26
|
+
------ ---------- -------
|
|
27
|
+
command or query check, transition, persist load / save / list
|
|
28
|
+
Persistence Port (injected)
|
|
29
|
+
← record, or refused + code
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**In:** a command or a query, plus an injected Persistence Port. **Out:** a
|
|
33
|
+
typed result. No JSON lines, no exit code, no Status protocol. Those belong to
|
|
34
|
+
Transformers. This package is a function set imported in process.
|
|
35
|
+
|
|
36
|
+
There is no required CLI. If a CLI appears later, it is a thin adapter over the
|
|
37
|
+
same functions, the way a Transformer's `cli.ts` is.
|
|
38
|
+
|
|
39
|
+
## 2. Faces and wiring
|
|
40
|
+
|
|
41
|
+
| Face | Entry | Must not |
|
|
42
|
+
| ------- | -------------------------------------------------- | ------------------------------------- |
|
|
43
|
+
| Import | `openWorkLedger({ persist })` then command methods | Read `process.env` to pick an adapter |
|
|
44
|
+
| Adapter | A value that implements the Persistence Port | Live under `src/ledger/` or in this package |
|
|
45
|
+
|
|
46
|
+
Glue builds the adapter (`openFilesystemPersist({ root })` from
|
|
47
|
+
`@bluewombat/persist-fs`, `openSqlitePersist({ path })` from
|
|
48
|
+
`@bluewombat/persist-sqlite`) and passes it in. The ledger does not sniff the
|
|
49
|
+
environment, the working directory, or a file extension to choose a backend.
|
|
50
|
+
It never names a concrete adapter.
|
|
51
|
+
|
|
52
|
+
Transformers do not import this package. Conductor does. Host chooses the adapter.
|
|
53
|
+
|
|
54
|
+
## 3. Two layers
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
commands / queries src/ledger/ FeatureStandard states, Plan freeze,
|
|
58
|
+
bail, Attempt, Trace, refusals
|
|
59
|
+
│
|
|
60
|
+
│ Persistence Port (plain records)
|
|
61
|
+
▼
|
|
62
|
+
adapters packages/plugins/persist-<name>/ bytes on disk, rows in a database
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
| Layer | Owns | Does not own |
|
|
66
|
+
| ------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------- |
|
|
67
|
+
| Ledger | Legal transitions, idempotence, bound counters, bail | `node:fs`, `sqlite`, `pg`, paths |
|
|
68
|
+
| Port | The record shapes that cross the boundary, and the verbs `load` / `save` / `listSummaries` | FeatureStandard state names as a state machine |
|
|
69
|
+
| Adapter | How those records are stored and listed | Whether `merging` may be cancelled |
|
|
70
|
+
|
|
71
|
+
A caller never talks to an adapter. It talks to the ledger. The ledger never
|
|
72
|
+
talks to `node:fs`. It talks to the Port.
|
|
73
|
+
|
|
74
|
+
## 4. Adapter isolation
|
|
75
|
+
|
|
76
|
+
Hard rules. Breaking one is a spec bug, not a refactor.
|
|
77
|
+
|
|
78
|
+
1. **One package per adapter**, under `packages/plugins/persist-<name>/` (name is
|
|
79
|
+
`fs`, `sqlite`, later `postgres`, …). Nothing of an adapter leaks into
|
|
80
|
+
`src/ledger/` or into a sibling adapter.
|
|
81
|
+
2. **`src/ledger/` does not import an adapter package**, `node:fs`, or
|
|
82
|
+
`node:sqlite`. It imports only the Port type (and record types). A test that
|
|
83
|
+
fails if `ledger` grows such an import is part of this contract.
|
|
84
|
+
3. **An adapter package does not import `src/ledger/`.** It imports the Port and
|
|
85
|
+
the persisted record types from `@bluewombat/work-ledger`. An adapter that
|
|
86
|
+
needs a transition table has crossed the line.
|
|
87
|
+
4. **Shared persisted shapes live in this package** (`src/records.ts`). They are
|
|
88
|
+
data, not behaviour.
|
|
89
|
+
5. **No `if (adapter === "sqlite")` in the ledger.** New backends add a package
|
|
90
|
+
and a constructor. They do not edit command code.
|
|
91
|
+
6. **Filesystem and SQLite both ship.** Postgres is named so the Port stays
|
|
92
|
+
honest. Its package does not exist until someone implements it.
|
|
93
|
+
7. **Domain tests inject a Port.** They use a test double, or an adapter package
|
|
94
|
+
on a temp path. They do not reach into adapter internals. Adapter tests do
|
|
95
|
+
not run FeatureStandard transitions.
|
|
96
|
+
|
|
97
|
+
## 5. Persistence Port
|
|
98
|
+
|
|
99
|
+
Three operations. Adapters may store one JSON file or ten SQL tables; the Port
|
|
100
|
+
does not care.
|
|
101
|
+
|
|
102
|
+
| Operation | In | Out |
|
|
103
|
+
| --------------- | ------------------------- | ------------------------------------ |
|
|
104
|
+
| `load` | Feature `key` | Aggregate, or missing |
|
|
105
|
+
| `save` | Feature `key` + aggregate | Durable replace of that key. Atomic. |
|
|
106
|
+
| `listSummaries` | none | One summary per known key |
|
|
107
|
+
|
|
108
|
+
`save` replaces the whole aggregate for that key. The ledger read-modify-writes.
|
|
109
|
+
The adapter does not merge. If `save` cannot complete, it fails; it does not
|
|
110
|
+
leave a half-written key that `load` could return.
|
|
111
|
+
|
|
112
|
+
`listSummaries` exists so `claim` can pick the next `received` FeatureStandard
|
|
113
|
+
without loading every Trace. A summary carries at least: `key`, `project`,
|
|
114
|
+
`state`, `priority`, `received_at`, bail expiry if any.
|
|
115
|
+
|
|
116
|
+
An adapter that cannot satisfy atomic `save` is not a WorkLedger adapter.
|
|
117
|
+
|
|
118
|
+
## 6. Filesystem adapter (`@bluewombat/persist-fs`)
|
|
119
|
+
|
|
120
|
+
Constructor: `openFilesystemPersist({ root })`. `root` is an absolute directory.
|
|
121
|
+
The adapter creates it if missing. This package does not live here.
|
|
122
|
+
|
|
123
|
+
Physical layout is this adapter's private choice. `persist-fs` uses
|
|
124
|
+
one file per key:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
<root>/<encoded-key>.json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`<encoded-key>` is `encodeURIComponent(key)` so a key `fake:42` is one path
|
|
131
|
+
segment (`fake%3A42.json`). The adapter never uses the raw key as a filename.
|
|
132
|
+
|
|
133
|
+
`save` writes a sibling temporary file in `root`, then renames onto the target
|
|
134
|
+
(atomic on the same filesystem). A crash during `save` leaves either the old
|
|
135
|
+
file or the new file, never a truncated JSON document that parses as a
|
|
136
|
+
different aggregate.
|
|
137
|
+
|
|
138
|
+
This adapter knows directories and JSON. It does not know `merging`, `ready`,
|
|
139
|
+
or Isolator.
|
|
140
|
+
|
|
141
|
+
## 7. Adapter packages
|
|
142
|
+
|
|
143
|
+
Same Port, own package, own constructor.
|
|
144
|
+
|
|
145
|
+
| Adapter | Package | Constructor sketch |
|
|
146
|
+
| ---------- | ------------------------------- | ------------------------------------ |
|
|
147
|
+
| Filesystem | `packages/plugins/persist-fs` | `openFilesystemPersist({ root })` |
|
|
148
|
+
| SQLite | `packages/plugins/persist-sqlite` | `openSqlitePersist({ path })` |
|
|
149
|
+
| Postgres | `packages/work-ledger-postgres` | `openPostgresPersist({ url })` |
|
|
150
|
+
|
|
151
|
+
They may split an aggregate into tables. `load` / `save` still look like one
|
|
152
|
+
aggregate to the ledger. A Postgres adapter that exposes SQL to glue has leaked
|
|
153
|
+
through the Port.
|
|
154
|
+
|
|
155
|
+
## 8. Aggregate
|
|
156
|
+
|
|
157
|
+
One aggregate per FeatureStandard `key` (`<manager>:<external_id>`). The Adapter
|
|
158
|
+
produced `key` and `fingerprint`; WorkLedger owns everything else.
|
|
159
|
+
|
|
160
|
+
| Part | Content |
|
|
161
|
+
| ------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
162
|
+
| Identity | `key`, `project`, `fingerprint` |
|
|
163
|
+
| Intention | FeatureStandard fields this package stores (intention, title, priority, source, …) |
|
|
164
|
+
| Feature state | Exactly one of §9 |
|
|
165
|
+
| Invalid | `code` + `reason` when state is `invalid` |
|
|
166
|
+
| Pending fingerprint | Set when an update arrives during `planning` / `running` / `integrating`; applied only after the in-flight work stops |
|
|
167
|
+
| Plan | Absent until freeze. Then frozen: Subtasks, dependencies, `planned_at`. No automatic rewrite |
|
|
168
|
+
| Subtask states | Per Subtask, exactly one of §9 |
|
|
169
|
+
| Attempts | Numbered, each with a Trace when an Attempt ran, and stamped with the round (`resumes` at the time) it belongs to |
|
|
170
|
+
| Bound counters | Attempts used, as this package tracks them |
|
|
171
|
+
| Resumes | How many `resumeReady` were taken. A round tells two escalations of one kind apart; absent reads as 0 |
|
|
172
|
+
| Bail | Optional. `expires_at` when a feature or Subtask is claimed |
|
|
173
|
+
| Declared workspaces | Optional absolute paths glue wrote **before** creating those directories |
|
|
174
|
+
|
|
175
|
+
Unknown fields from a FeatureStandard that FeatureAdapter would drop are not
|
|
176
|
+
invented here either. WorkLedger does not reopen the Adapter's closed shape.
|
|
177
|
+
|
|
178
|
+
`integrated` Subtasks are never removed from the Plan. An Attempt is never
|
|
179
|
+
rewritten; a new Attempt is appended.
|
|
180
|
+
|
|
181
|
+
## 9. States
|
|
182
|
+
|
|
183
|
+
FeatureStandard states, copied from the product glossary. WorkLedger is the
|
|
184
|
+
component that actually holds them.
|
|
185
|
+
|
|
186
|
+
| State | Meaning |
|
|
187
|
+
| ------------- | -------------------------------------------------- |
|
|
188
|
+
| `received` | Admitted, not yet claimed |
|
|
189
|
+
| `invalid` | Stored as unusable, with a code; not silent |
|
|
190
|
+
| `planning` | Breakdown in progress (bail held) |
|
|
191
|
+
| `running` | Subtask loop (bail held) |
|
|
192
|
+
| `escalated` | Frozen for a human |
|
|
193
|
+
| `integrating` | All Subtasks `integrated`; assembly Gates |
|
|
194
|
+
| `submitted` | Offered to an Authority; waiting on its judgement |
|
|
195
|
+
| `merging` | Final fold into WorkLineStable. Point of no return |
|
|
196
|
+
| `done` | Terminal. In WorkLineStable |
|
|
197
|
+
| `cancelled` | Terminal. Abandoned before final integration |
|
|
198
|
+
|
|
199
|
+
Subtask states: `pending`, `runnable`, `running`, `escalated`, `integrated`,
|
|
200
|
+
`cancelled`.
|
|
201
|
+
|
|
202
|
+
A command that would require a transition not in this section is `refused`.
|
|
203
|
+
The aggregate is unchanged.
|
|
204
|
+
|
|
205
|
+
## 10. Commands
|
|
206
|
+
|
|
207
|
+
Each command is a function on the opened ledger. Illegal input or an illegal
|
|
208
|
+
transition returns `refused` with a `code`. It does not throw for domain
|
|
209
|
+
reasons. It does not persist a refused command.
|
|
210
|
+
|
|
211
|
+
Idempotence is on `key` unless a row says otherwise.
|
|
212
|
+
|
|
213
|
+
| Command | Meaning | Refused when |
|
|
214
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
215
|
+
| `admit` | Store a FeatureStandard in `received`. Same `key` already `received` with the same fingerprint → success, no rewrite. Same `key`, different fingerprint, not in-flight → update the stored intention. Same `key` in-flight (`planning` / `running` / `integrating` / `submitted`) with the same fingerprint (or the fingerprint already pending) → success, no rewrite. A different fingerprint while in-flight is recorded as pending; the live work is not rewritten. | `key` in `merging` or `done`; `key` in `cancelled` |
|
|
216
|
+
| `recordInvalid` | Store `invalid` with code, reason and the fingerprint of what was received | `key` already in `merging` or `done` |
|
|
217
|
+
| `claim` | Take the next `received` FeatureStandard for a Project (priority then `received_at`). Hold bail. State → `planning`. No-op with `empty` when none, or when that Project already has an active FeatureStandard | That Project already has `planning`, `running`, `escalated`, `integrating`, `submitted`, or `merging` |
|
|
218
|
+
| `recordPlan` | Freeze the Plan. Subtasks start `pending` / `runnable`. State → `running` | Not `planning`; Plan already frozen; Plan fails the same structural checks FeatureBreakdown already applied (empty, cycle, missing definition of done, …) |
|
|
219
|
+
| `refusePlan` | Breakdown refused. State → `escalated` with the Breakdown code | Not `planning` |
|
|
220
|
+
| `startSubtask` | Named Subtask → `running`. Feature stays `running`. Bail renewed or held | Feature not `running`; Subtask not `runnable`; another Subtask already `running` |
|
|
221
|
+
| `recordAttempt` | Append an Attempt + Trace (`ended`, optional `report` and `refusedBy`) | That Subtask is not `running`; Attempt number not the next integer |
|
|
222
|
+
| `markSubtaskIntegrated` | Subtask → `integrated`. If no Subtask remains non-terminal → Feature → `integrating` | Subtask not `running`; Feature not `running` |
|
|
223
|
+
| `markSubmitted` | Feature → `submitted`. Records the Authority's opaque `reference`. A second call keeps the first reference and any `last_report` | Not `integrating` or `submitted` |
|
|
224
|
+
| `recordRefusal` | Authority / assembly Gates sent the work back. Increment `refusals`, store `last_report`, Feature → `integrating` | Not `submitted` |
|
|
225
|
+
| `markMerging` | Feature → `merging` | Not `integrating` or `submitted` |
|
|
226
|
+
| `markDone` | Feature → `done`. Records the fold's `reference` when given (the work line's own name for it, opaque). Clear bail. Clear declared Subtask workspace | Not `merging` |
|
|
227
|
+
| `escalate` | Feature → `escalated` (whole feature frozen). Trace required if an Attempt exists for the named Subtask. Consumes `pending_fingerprint`. | Feature already `done` or `cancelled`; escalation without Trace when an Attempt existed |
|
|
228
|
+
| `resumeReady` | Human `ready`. Plan escalation → `planning` (Plan unfrozen, Breakdown replayed). Subtask escalation → that Subtask `runnable` from zero, Feature `running`. Escalation from `merging`, `submitted`, or `assembly` → `integrating` | Not `escalated` or `invalid`; `invalid` without a new admissible FeatureStandard |
|
|
229
|
+
| `cancel` | Feature → `cancelled`. Remaining Subtasks `cancelled`. `integrated` Subtasks stay `integrated` | `merging` or `done`; an escalation that was born in `merging` |
|
|
230
|
+
| `declareWorkspace` | Record a WorkSpaceFeature and/or WorkSpaceSubtask path **before** glue creates it | Feature `done` or `cancelled` |
|
|
231
|
+
| `clearWorkspace` | Drop a declared path after glue deleted that directory | — |
|
|
232
|
+
| `expireBail` | Bail past expiry: Subtask `running` → `runnable` (from zero, consume an Attempt); Feature `planning` → `received` | No bail, or bail not expired |
|
|
233
|
+
| `renewBail` | Push bail expiry forward by the configured duration. Allowed while a bail is held | No bail |
|
|
234
|
+
|
|
235
|
+
`resumeReady` from `invalid` requires `admit` of a corrected FeatureStandard
|
|
236
|
+
first (`invalid` → `received` is `admit` after correction, then claim).
|
|
237
|
+
|
|
238
|
+
In-flight update (`admit` while `planning` / `running` / `integrating` /
|
|
239
|
+
`submitted`): same fingerprint as the live intention, or as the fingerprint
|
|
240
|
+
already pending → success, no rewrite (an echo of a report). A different
|
|
241
|
+
fingerprint is stored as pending; do not change the frozen Plan; do not
|
|
242
|
+
rewrite the live intention. `escalate` consumes `pending_fingerprint`.
|
|
243
|
+
Conductor calls `escalate({ kind: "plan" })` after a Subtask is marked
|
|
244
|
+
integrated, and when entering `integrating`, if the field is set. WorkLedger
|
|
245
|
+
does not watch a Transformer's clock.
|
|
246
|
+
|
|
247
|
+
Escalation `kind` is one of `plan`, `subtask`, `assembly`, `submitted`,
|
|
248
|
+
`merging`.
|
|
249
|
+
|
|
250
|
+
## 11. Queries
|
|
251
|
+
|
|
252
|
+
| Query | Out |
|
|
253
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
254
|
+
| `get` | Aggregate for a `key`, or missing |
|
|
255
|
+
| `list` | One summary per known key, every state. No persist, no bail, no transition |
|
|
256
|
+
| `listInFlight` | The `list` subset that is mid-flight (`planning` / `running` / `integrating` / `submitted`) |
|
|
257
|
+
| `activeOn` | The active FeatureStandard on a Project (`planning` / `running` / `escalated` / `integrating` / `submitted` / `merging`), or none |
|
|
258
|
+
| `nextReceived` | What `claim` would take, without taking it |
|
|
259
|
+
| `nextRunnable` | A `runnable` Subtask of a Feature (ties: the one that unblocks the most others). Glue still serializes |
|
|
260
|
+
| `listDeclaredWorkspaces` | Keys and paths the ledger believes exist. Glue reconciles disk to this list. Orphans on disk that are not listed are glue's to destroy |
|
|
261
|
+
|
|
262
|
+
Queries do not persist. They do not take a bail.
|
|
263
|
+
|
|
264
|
+
## 12. Bail and reconcile
|
|
265
|
+
|
|
266
|
+
While a FeatureStandard is `planning`, `running`, `integrating`, `submitted`,
|
|
267
|
+
or `merging`, it holds a bail (`expires_at`). There is no holder id in this
|
|
268
|
+
implementation. Glue renews by calling `startSubtask` / `claim`-family
|
|
269
|
+
commands; `renewBail` exists if expiry would otherwise pass during a long
|
|
270
|
+
Attempt.
|
|
271
|
+
|
|
272
|
+
Expired bail: `expireBail`. The Subtask becomes `runnable` from zero and
|
|
273
|
+
consumes an Attempt. Declared Subtask workspace is cleared (glue deletes the
|
|
274
|
+
directory). The ledger decides; glue acts.
|
|
275
|
+
|
|
276
|
+
After a process start, glue calls `listDeclaredWorkspaces` and destroys
|
|
277
|
+
directories that are not on that list. It does not invent Feature state from
|
|
278
|
+
disk.
|
|
279
|
+
|
|
280
|
+
## 13. Refusals
|
|
281
|
+
|
|
282
|
+
A refused command leaves the store as it was. `code` is one token.
|
|
283
|
+
|
|
284
|
+
| Code | When |
|
|
285
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------- |
|
|
286
|
+
| `not-found` | `key` is unknown |
|
|
287
|
+
| `duplicate` | Reserved; `admit` of an identical fingerprint is success, not this |
|
|
288
|
+
| `illegal-transition` | State does not allow this command |
|
|
289
|
+
| `project-busy` | `claim` while that Project already has an active FeatureStandard |
|
|
290
|
+
| `plan-frozen` | `recordPlan` when a Plan already exists |
|
|
291
|
+
| `plan-not-frozen` | A Subtask command before `recordPlan` |
|
|
292
|
+
| `subtask-not-runnable` | `startSubtask` on a Subtask whose dependencies are not `integrated` |
|
|
293
|
+
| `another-running` | `startSubtask` while one Subtask is already `running` |
|
|
294
|
+
| `missing-trace` | `escalate` after an Attempt with no Trace |
|
|
295
|
+
| `point-of-no-return` | `cancel` in `merging` or `done`, or abandon after merging-born escalation |
|
|
296
|
+
| `too-late` | `admit` / `recordInvalid` / intention edit in `done` |
|
|
297
|
+
| `persist-failed` | Adapter `save` failed. Aggregate in memory is not the source of truth; caller retries or escalates |
|
|
298
|
+
|
|
299
|
+
`persist-failed` is the only refusal that may mean "we do not know if bytes
|
|
300
|
+
landed". Glue treats it as unavailable, not as `invalid`.
|
|
301
|
+
|
|
302
|
+
## 14. What is implemented
|
|
303
|
+
|
|
304
|
+
Shipped:
|
|
305
|
+
|
|
306
|
+
- Import face, `openWorkLedger({ persist })`
|
|
307
|
+
- Filesystem adapter (`packages/plugins/persist-fs`)
|
|
308
|
+
- SQLite adapter (`packages/plugins/persist-sqlite`)
|
|
309
|
+
- Commands and queries in §10–§11
|
|
310
|
+
- Isolation tests in §4
|
|
311
|
+
|
|
312
|
+
Not shipped:
|
|
313
|
+
|
|
314
|
+
- Postgres adapter
|
|
315
|
+
- CLI
|
|
316
|
+
- A human UI
|
|
317
|
+
- Pause / emergency-stop flags (Conductor admission, not this store)
|
|
318
|
+
|
|
319
|
+
## 15. Layout
|
|
320
|
+
|
|
321
|
+
```
|
|
322
|
+
SPECS.md this contract
|
|
323
|
+
README.md how to build and run
|
|
324
|
+
src/
|
|
325
|
+
index.ts openWorkLedger; re-export commands, records, Port
|
|
326
|
+
records.ts persisted shapes (no behaviour)
|
|
327
|
+
persist/
|
|
328
|
+
port.ts Persistence Port only
|
|
329
|
+
ledger/ commands, queries, transitions; imports Port, not an adapter
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Adapters are sibling packages under `packages/plugins/persist-<name>/`, not
|
|
333
|
+
directories here.
|
|
334
|
+
|
|
335
|
+
## 16. Acceptance
|
|
336
|
+
|
|
337
|
+
1. `admit` of a new FeatureStandard → `get` returns it in `received` with that
|
|
338
|
+
`key` and `fingerprint`.
|
|
339
|
+
2. `admit` of the same `key` and fingerprint again → success, aggregate
|
|
340
|
+
unchanged (byte-for-byte on the stored fingerprint and intention).
|
|
341
|
+
3. `admit` of the same `key` with a new fingerprint while `received` → stored
|
|
342
|
+
intention updates.
|
|
343
|
+
4. `admit` while `running` with a new fingerprint → pending fingerprint set;
|
|
344
|
+
frozen Plan unchanged; Feature state still `running`. Same fingerprint
|
|
345
|
+
while `running` or `submitted` → success, no pending field.
|
|
346
|
+
5. `claim` on a Project with one `received` → that Feature is `planning` and
|
|
347
|
+
holds a bail. A second `claim` on the same Project → `project-busy`.
|
|
348
|
+
6. `recordPlan` with two Subtasks, B depending on A → A `runnable`, B
|
|
349
|
+
`pending`. `startSubtask(B)` → `subtask-not-runnable`.
|
|
350
|
+
7. `startSubtask(A)` then `startSubtask` on any other → `another-running`.
|
|
351
|
+
8. `markSubtaskIntegrated` on the last non-terminal Subtask → Feature
|
|
352
|
+
`integrating`.
|
|
353
|
+
9. `cancel` in `merging` → `point-of-no-return`; state still `merging`.
|
|
354
|
+
10. `escalate` after `recordAttempt` without a Trace → `missing-trace`; state
|
|
355
|
+
unchanged.
|
|
356
|
+
11. `declareWorkspace` then `listDeclaredWorkspaces` includes that path.
|
|
357
|
+
`clearWorkspace` removes it.
|
|
358
|
+
12. `save` via the filesystem adapter: after a successful `admit`, the file
|
|
359
|
+
`<root>/<encoded-key>.json` exists and `load` round-trips the aggregate.
|
|
360
|
+
13. Filesystem `save` is atomic: a reader never observes truncated JSON.
|
|
361
|
+
14. `src/ledger/` has no import of an adapter package, `node:fs`, or `node:sqlite`.
|
|
362
|
+
15. `src/persist/` holds only `port.ts`.
|
|
363
|
+
16. Opening the ledger with a different Persistence Port (a test double) still
|
|
364
|
+
passes 1–11. No filesystem is required for those.
|
|
365
|
+
17. SQLite `save` then `load` round-trips the same aggregate.
|
|
366
|
+
|
|
367
|
+
## 17. Choices
|
|
368
|
+
|
|
369
|
+
Decisions taken while writing this spec. Each one is a choice, not a
|
|
370
|
+
consequence: another answer was possible.
|
|
371
|
+
|
|
372
|
+
1. **Not a Transformer.** A store with many verbs is not IN/OUT-shaped. One IN
|
|
373
|
+
and one OUT would force a toolbox CLI and hide the transition table.
|
|
374
|
+
2. **Commands, not `set(state)`.** The product rules (plan freeze, one active
|
|
375
|
+
FeatureStandard per Project, no abandon in `merging`) live here or they live
|
|
376
|
+
nowhere.
|
|
377
|
+
3. **Injected Persistence Port.** Sniffing env or a path to pick SQLite later
|
|
378
|
+
would couple the ledger to ops. Glue chooses; the ledger stays portable.
|
|
379
|
+
4. **Adapters are sibling packages.** A directory inside this package would mix
|
|
380
|
+
kernel with a file format. Separate packages make a SQLite backend a new
|
|
381
|
+
`packages/` directory, not an edit of command code.
|
|
382
|
+
5. **Port is load / save / listSummaries of one aggregate.** Fine-grained row
|
|
383
|
+
verbs would leak SQL into the ledger. Adapters may split storage internally.
|
|
384
|
+
6. **Filesystem and SQLite both ship.** Enough to be truth on disk or in one
|
|
385
|
+
file. Postgres is an adapter, not a prerequisite.
|
|
386
|
+
7. **Keys encoded for filenames.** FeatureAdapter keys contain `:`. Using them
|
|
387
|
+
raw would create extra directories or illegal paths.
|
|
388
|
+
8. **Refused commands do not persist.** A failed transition must not look like
|
|
389
|
+
history.
|
|
390
|
+
9. **`persist-failed` is not `invalid`.** Disk or database blips must not brand
|
|
391
|
+
a FeatureStandard unusable.
|
|
392
|
+
10. **Declared workspaces are ledger facts, not discoveries.** Glue writes the
|
|
393
|
+
path before creating the directory, so crash reconcile has a list that
|
|
394
|
+
disk cannot lie about.
|
|
395
|
+
11. **No CLI.** The caller is TypeScript glue. A
|
|
396
|
+
CLI can wait; two faces would be invented before a second caller exists.
|
|
397
|
+
12. **Stack: TypeScript on Node.js 24, Biome, `node:test` run through `tsx`.**
|
|
398
|
+
Recorded here as a decision; the commands live in this package's README
|
|
399
|
+
when it exists.
|
|
400
|
+
|
|
401
|
+
How this package is built and run: [README.md](./README.md).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { PlanCheckResult, PlanDraft } from "./ledger/check-plan.js";
|
|
2
|
+
export type { ClaimResult, ClearWorkspaceInput, CommandOk, CommandRefused, CommandResult, DeclaredWorkspace, DeclareWorkspaceInput, EscalateInput, FeatureAdmission, GetResult, OpenWorkLedgerOptions, RecordAttemptInput, WorkLedger, } from "./ledger/open-work-ledger.js";
|
|
3
|
+
export { openWorkLedger } from "./ledger/open-work-ledger.js";
|
|
4
|
+
export type { PersistencePort, PersistSaveResult } from "./persist/port.js";
|
|
5
|
+
export type * from "./records.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACzE,YAAY,EACV,WAAW,EACX,mBAAmB,EACnB,SAAS,EACT,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,qBAAqB,EACrB,kBAAkB,EAClB,UAAU,GACX,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC5E,mBAAmB,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PlannedSubtask } from "../records.js";
|
|
2
|
+
export type PlanDraft = {
|
|
3
|
+
id: string;
|
|
4
|
+
intention: string;
|
|
5
|
+
definition_of_done: string;
|
|
6
|
+
depends_on: string[];
|
|
7
|
+
};
|
|
8
|
+
export type PlanCheckResult = {
|
|
9
|
+
ok: true;
|
|
10
|
+
subtasks: PlanDraft[];
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
};
|
|
14
|
+
export declare function checkPlan(drafts: PlanDraft[]): PlanCheckResult;
|
|
15
|
+
export declare function initialSubtaskState(draft: PlanDraft): PlannedSubtask["state"];
|
|
16
|
+
export declare function refreshRunnable(subtasks: PlannedSubtask[]): PlannedSubtask[];
|
|
17
|
+
//# sourceMappingURL=check-plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-plan.d.ts","sourceRoot":"","sources":["../../src/ledger/check-plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,SAAS,EAAE,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC;AAmClF,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,eAAe,CA0B9D;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,GAAG,cAAc,CAAC,OAAO,CAAC,CAE7E;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CAW5E"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
function findCycle(subtasks) {
|
|
2
|
+
const byId = new Map(subtasks.map((st) => [st.id, st]));
|
|
3
|
+
const color = new Map();
|
|
4
|
+
for (const st of subtasks) {
|
|
5
|
+
color.set(st.id, "white");
|
|
6
|
+
}
|
|
7
|
+
const visit = (id) => {
|
|
8
|
+
color.set(id, "gray");
|
|
9
|
+
const node = byId.get(id);
|
|
10
|
+
if (node !== undefined) {
|
|
11
|
+
for (const dep of node.depends_on) {
|
|
12
|
+
const depColor = color.get(dep);
|
|
13
|
+
if (depColor === "gray") {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (depColor === "white" && visit(dep)) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
color.set(id, "black");
|
|
22
|
+
return false;
|
|
23
|
+
};
|
|
24
|
+
for (const st of subtasks) {
|
|
25
|
+
if (color.get(st.id) === "white" && visit(st.id)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
export function checkPlan(drafts) {
|
|
32
|
+
if (drafts.length === 0) {
|
|
33
|
+
return { ok: false };
|
|
34
|
+
}
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
for (const draft of drafts) {
|
|
37
|
+
if (draft.id.length === 0 || seen.has(draft.id)) {
|
|
38
|
+
return { ok: false };
|
|
39
|
+
}
|
|
40
|
+
seen.add(draft.id);
|
|
41
|
+
if (draft.intention.trim().length === 0 || draft.definition_of_done.trim().length === 0) {
|
|
42
|
+
return { ok: false };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const ids = new Set(drafts.map((d) => d.id));
|
|
46
|
+
for (const draft of drafts) {
|
|
47
|
+
for (const dep of draft.depends_on) {
|
|
48
|
+
if (!ids.has(dep)) {
|
|
49
|
+
return { ok: false };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (findCycle(drafts) || drafts.every((d) => d.depends_on.length > 0)) {
|
|
54
|
+
return { ok: false };
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, subtasks: drafts };
|
|
57
|
+
}
|
|
58
|
+
export function initialSubtaskState(draft) {
|
|
59
|
+
return draft.depends_on.length === 0 ? "runnable" : "pending";
|
|
60
|
+
}
|
|
61
|
+
export function refreshRunnable(subtasks) {
|
|
62
|
+
const integrated = new Set(subtasks.filter((st) => st.state === "integrated").map((st) => st.id));
|
|
63
|
+
return subtasks.map((st) => {
|
|
64
|
+
if (st.state !== "pending") {
|
|
65
|
+
return st;
|
|
66
|
+
}
|
|
67
|
+
if (st.depends_on.every((dep) => integrated.has(dep))) {
|
|
68
|
+
return { ...st, state: "runnable" };
|
|
69
|
+
}
|
|
70
|
+
return st;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=check-plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-plan.js","sourceRoot":"","sources":["../../src/ledger/check-plan.ts"],"names":[],"mappings":"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"}
|