@bluewombat/work-ledger 0.8.0 → 0.10.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # WorkLedger
2
2
 
3
- Kernel package: the durable store of FeatureStandards, Subtasks, states,
3
+ Kernel package: the durable store of TicketStandards, states,
4
4
  Attempts, and Traces. Glue writes here before it acts.
5
5
 
6
6
  Behavioural contract: [SPECS.md](./SPECS.md). Persistence adapters live under
package/SPECS.md CHANGED
@@ -2,17 +2,17 @@
2
2
 
3
3
  Kernel package, not a Transformer. It has many operations, not one IN and one
4
4
  OUT. Its code, identifiers, comments, and messages name only what it owns:
5
- Feature records, Plans, Attempts, Traces, states, bails, and the Persistence
5
+ Ticket records, Attempts, Traces, states, bails, and the Persistence
6
6
  Port. No Transformer is named inside that perimeter, because none of them exist
7
7
  there.
8
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
9
+ WorkLedger is the durable store of TicketStandards, states, Attempts, and
10
+ Traces. It is the system's source of truth. Glue writes here **before** it
11
11
  acts. Isolated workspaces are a consequence of that state, never the source of
12
12
  it.
13
13
 
14
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
15
+ to a TicketTracker. It accepts an already-normalized TicketStandard (or an
16
16
  `invalid` verdict). It does not convert raw intentions.
17
17
 
18
18
  Callers inject a persistence adapter. Adapters are their own packages under
@@ -54,7 +54,7 @@ Transformers do not import this package. Conductor does. Host chooses the adapte
54
54
  ## 3. Two layers
55
55
 
56
56
  ```
57
- commands / queries src/ledger/ FeatureStandard states, Plan freeze,
57
+ commands / queries src/ledger/ TicketStandard states,
58
58
  bail, Attempt, Trace, refusals
59
59
  │
60
60
  │ Persistence Port (plain records)
@@ -65,7 +65,7 @@ Transformers do not import this package. Conductor does. Host chooses the adapte
65
65
  | Layer | Owns | Does not own |
66
66
  | ------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------- |
67
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 |
68
+ | Port | The record shapes that cross the boundary, and the verbs `load` / `save` / `listSummaries` | TicketStandard state names as a state machine |
69
69
  | Adapter | How those records are stored and listed | Whether `merging` may be cancelled |
70
70
 
71
71
  A caller never talks to an adapter. It talks to the ledger. The ledger never
@@ -92,7 +92,7 @@ Hard rules. Breaking one is a spec bug, not a refactor.
92
92
  honest. Its package does not exist until someone implements it.
93
93
  7. **Domain tests inject a Port.** They use a test double, or an adapter package
94
94
  on a temp path. They do not reach into adapter internals. Adapter tests do
95
- not run FeatureStandard transitions.
95
+ not run TicketStandard transitions.
96
96
 
97
97
  ## 5. Persistence Port
98
98
 
@@ -101,15 +101,15 @@ does not care.
101
101
 
102
102
  | Operation | In | Out |
103
103
  | --------------- | ------------------------- | ------------------------------------ |
104
- | `load` | Feature `key` | Aggregate, or missing |
105
- | `save` | Feature `key` + aggregate | Durable replace of that key. Atomic. |
104
+ | `load` | Ticket `key` | Aggregate, or missing |
105
+ | `save` | Ticket `key` + aggregate | Durable replace of that key. Atomic. |
106
106
  | `listSummaries` | none | One summary per known key |
107
107
 
108
108
  `save` replaces the whole aggregate for that key. The ledger read-modify-writes.
109
109
  The adapter does not merge. If `save` cannot complete, it fails; it does not
110
110
  leave a half-written key that `load` could return.
111
111
 
112
- `listSummaries` exists so `claim` can pick the next `received` FeatureStandard
112
+ `listSummaries` exists so `claim` can pick the next `received` TicketStandard
113
113
  without loading every Trace. A summary carries at least: `key`, `project`,
114
114
  `state`, `priority`, `received_at`, bail expiry if any.
115
115
 
@@ -135,7 +135,7 @@ segment (`fake%3A42.json`). The adapter never uses the raw key as a filename.
135
135
  file or the new file, never a truncated JSON document that parses as a
136
136
  different aggregate.
137
137
 
138
- This adapter knows directories and JSON. It does not know `merging`, `ready`,
138
+ This adapter knows directories and JSON. It does not know `merging`, `resume`,
139
139
  or Isolator.
140
140
 
141
141
  ## 7. Adapter packages
@@ -154,53 +154,46 @@ through the Port.
154
154
 
155
155
  ## 8. Aggregate
156
156
 
157
- One aggregate per FeatureStandard `key` (`<manager>:<external_id>`). The Adapter
157
+ One aggregate per TicketStandard `key` (`<manager>:<external_id>`). The Adapter
158
158
  produced `key` and `fingerprint`; WorkLedger owns everything else.
159
159
 
160
160
  | Part | Content |
161
161
  | ------------------- | --------------------------------------------------------------------------------------------------------------------- |
162
162
  | Identity | `key`, `project`, `fingerprint` |
163
- | Intention | FeatureStandard fields this package stores (intention, title, priority, source, …) |
164
- | Feature state | Exactly one of §9 |
163
+ | Intention | TicketStandard fields this package stores (intention, title, priority, source, …) and its place in a Feature: `feature`, `blocked_by` (Tickets that must be `done` first), `line` (the line it lands on) |
164
+ | Ticket state | Exactly one of §9 |
165
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 |
166
+ | Pending fingerprint | Set when an update arrives during `running` / `integrating`; applied only after the in-flight work stops |
167
+ | Attempts | Numbered from 1, each with a Trace when an Attempt ran, and stamped with the round (`resumes` at the time) it belongs to |
170
168
  | 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 |
169
+ | Resumes | How many `resumeTicket` were taken. A round tells two escalations of one kind apart |
172
170
  | Parked refusal | A report `assembly.validate` sent back, held on the aggregate because there may be no Submission yet. Present only while `integrating`; cleared once validate accepts |
173
171
  | Parked refusals | How many times `assembly.validate` sent the work back with no Submission's own counter to hold it. Shares the `maxRefusals` budget with `submission.refusals` |
174
- | Bail | Optional. `expires_at` when a feature or Subtask is claimed |
175
- | Declared workspaces | Optional absolute paths glue wrote **before** creating those directories |
172
+ | Bail | Optional. `expires_at` while a unit is held (from `startUnit`) and while the work is assembled and judged |
173
+ | Declared workspace | Optional absolute path glue wrote **before** creating that directory |
176
174
 
177
- Unknown fields from a FeatureStandard that FeatureAdapter would drop are not
175
+ Unknown fields from a TicketStandard that Adapter would drop are not
178
176
  invented here either. WorkLedger does not reopen the Adapter's closed shape.
179
177
 
180
- `integrated` Subtasks are never removed from the Plan. An Attempt is never
181
- rewritten; a new Attempt is appended.
178
+ An Attempt is never rewritten; a new Attempt is appended.
182
179
 
183
180
  ## 9. States
184
181
 
185
- FeatureStandard states, copied from the product glossary. WorkLedger is the
182
+ TicketStandard states, copied from the product glossary. WorkLedger is the
186
183
  component that actually holds them.
187
184
 
188
185
  | State | Meaning |
189
186
  | ------------- | -------------------------------------------------- |
190
187
  | `received` | Admitted, not yet claimed |
191
188
  | `invalid` | Stored as unusable, with a code; not silent |
192
- | `planning` | Breakdown in progress (bail held) |
193
- | `running` | Subtask loop (bail held) |
189
+ | `running` | The unit is made (bail held once it starts) |
194
190
  | `escalated` | Frozen for a human |
195
- | `integrating` | All Subtasks `integrated`; assembly Gates |
191
+ | `integrating` | The unit passed its Gates; assembly Gates |
196
192
  | `submitted` | Offered to an Authority; waiting on its judgement |
197
193
  | `merging` | Final fold into WorkLineStable. Point of no return |
198
194
  | `done` | Terminal. In WorkLineStable |
199
195
  | `cancelled` | Terminal. Abandoned before final integration |
200
196
 
201
- Subtask states: `pending`, `runnable`, `running`, `escalated`, `integrated`,
202
- `cancelled`.
203
-
204
197
  A command that would require a transition not in this section is `refused`.
205
198
  The aggregate is unchanged.
206
199
 
@@ -214,43 +207,40 @@ Idempotence is on `key` unless a row says otherwise.
214
207
 
215
208
  | Command | Meaning | Refused when |
216
209
  | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
217
- | `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` |
210
+ | `admit` | Store a TicketStandard 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 (`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` |
218
211
  | `recordInvalid` | Store `invalid` with code, reason and the fingerprint of what was received | `key` already in `merging` or `done` |
219
- | `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` |
220
- | `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, …) |
221
- | `refusePlan` | Breakdown refused. State → `escalated` with the Breakdown code | Not `planning` |
222
- | `startSubtask` | Named Subtask → `running`. Feature stays `running`. Bail renewed or held | Feature not `running`; Subtask not `runnable`; another Subtask already `running` |
223
- | `recordAttempt` | Append an Attempt + Trace (`ended`, optional `report` and `refusedBy`) | That Subtask is not `running`; Attempt number not the next integer |
224
- | `markSubtaskIntegrated` | Subtask → `integrated`. If no Subtask remains non-terminal → Feature → `integrating` | Subtask not `running`; Feature not `running` |
225
- | `markSubmitted` | Feature → `submitted`. Records the Authority's opaque `reference`. A second call keeps the first reference and any `last_report` | Not `integrating` or `submitted` |
226
- | `recordRefusal` | Authority / assembly Gates sent the work back. Increment `refusals`, store `last_report`, Feature → `integrating`. Clears a stale parked refusal, if one is held | Not `submitted` |
227
- | `recordParkedRefusal` | `assembly.validate` sent the work back before any Submission exists. Store the parked report, increment `parked_refusals` | Not `integrating` |
212
+ | `claim` | Take the next `received` TicketStandard for a Project (priority then `received_at`). State → `running`, with no bail yet. No-op with `empty` when none | That Project already has `running`, `escalated`, `integrating`, `submitted`, or `merging` |
213
+ | `startUnit` | The unit starts: take the bail | Not `running`; a bail is already held (`another-running`) |
214
+ | `recordAttempt` | Append an Attempt + Trace (`ended`, optional `report` and `refusedBy`) | Not `running`; Attempt number not the next integer |
215
+ | `markIntegrating` | The unit passed its Gates. State → `integrating`, bail renewed | Not `running` |
216
+ | `markSubmitted` | Ticket → `submitted`. Records the Authority's opaque `reference`. A second call keeps the first reference and any `last_report` | Not `integrating` or `submitted` |
217
+ | `recordRefusal` | Authority / assembly Gates sent the work back. Increment `refusals`, store `last_report`, Ticket → `integrating`. Clears a stale parked refusal, if one is held | Not `submitted` |
218
+ | `recordRealign` | The line moved on while the Submission waited. Ticket → `integrating`, `realigns` + 1, the report of an earlier refusal dropped (it was repaired), no refusal counted. Only from `submitted`. `markSubmitted` keeps `realigns` |
219
+ | `recordParkedRefusal` | `assembly.validate` sent the work back before any Submission exists. Store the parked report, increment `parked_refusals` | Not `integrating` |
228
220
  | `clearParkedRefusal` | `assembly.validate` accepted. Drop the parked report; the counter stays — it is the budget already spent | — |
229
- | `markMerging` | Feature → `merging` | Not `integrating` or `submitted` |
230
- | `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` |
231
- | `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 |
232
- | `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 |
233
- | `cancel` | Feature → `cancelled`. Remaining Subtasks `cancelled`. `integrated` Subtasks stay `integrated` | `merging` or `done`; an escalation that was born in `merging` |
234
- | `declareWorkspace` | Record a WorkSpaceFeature and/or WorkSpaceSubtask path **before** glue creates it | Feature `done` or `cancelled` |
235
- | `clearWorkspace` | Drop a declared path after glue deleted that directory | — |
236
- | `expireBail` | Bail past expiry: Subtask `running` → `runnable` (from zero, consume an Attempt); Feature `planning` → `received` | No bail, or bail not expired |
237
- | `releaseBail` | Same ledger outcome as an expired bail in `planning` / `running`, without waiting for the clock. A `running` Feature with no Subtask held is success with no rewrite | Feature not `planning` or `running` |
221
+ | `markMerging` | Ticket → `merging` | Not `integrating` or `submitted` |
222
+ | `markDone` | Ticket → `done`. Records the fold's `reference` when given (the work line's own name for it, opaque). Clear bail | Not `merging` |
223
+ | `escalate` | Ticket → `escalated`. For kind `unit`, a Trace is required if an Attempt exists. Consumes `pending_fingerprint` | Ticket already `done` or `cancelled`; a `unit` escalation when the last Attempt has no Trace |
224
+ | `resumeTicket` | Human `resume`. Escalation `unit` → `running` from zero, no bail. Escalation from `merging`, `submitted`, or `assembly` → `integrating`. Counts a round | Not `escalated` or `invalid`; `invalid` without a new admissible TicketStandard |
225
+ | `cancel` | Ticket → `cancelled` | `merging` or `done`; an escalation that was born in `merging` |
226
+ | `declareWorkspace` | Record the workspace path **before** glue creates it | Ticket `done` or `cancelled` |
227
+ | `clearWorkspace` | Drop the declared path after glue deleted that directory | — |
228
+ | `expireBail` | Bail past expiry: a `running` Ticket's unit stops (consume an Attempt; the workspace stays declared); any other state only loses its bail | No bail, or bail not expired |
229
+ | `releaseBail` | A `running` Ticket's unit stops, without waiting for the clock and without consuming an Attempt (the stopped pass recorded its own); the workspace stays declared. A `running` Ticket with no bail is success with no rewrite | Ticket not `running` |
238
230
  | `renewBail` | Push bail expiry forward by the configured duration. Allowed while a bail is held | No bail |
239
231
 
240
- `resumeReady` from `invalid` requires `admit` of a corrected FeatureStandard
232
+ `resumeTicket` from `invalid` requires `admit` of a corrected TicketStandard
241
233
  first (`invalid` → `received` is `admit` after correction, then claim).
242
234
 
243
- In-flight update (`admit` while `planning` / `running` / `integrating` /
244
- `submitted`): same fingerprint as the live intention, or as the fingerprint
245
- already pending → success, no rewrite (an echo of a report). A different
246
- fingerprint is stored as pending; do not change the frozen Plan; do not
247
- rewrite the live intention. `escalate` consumes `pending_fingerprint`.
248
- Conductor calls `escalate({ kind: "plan" })` after a Subtask is marked
249
- integrated, and when entering `integrating`, if the field is set. WorkLedger
235
+ In-flight update (`admit` while `running` / `integrating` / `submitted`): same
236
+ fingerprint as the live intention, or as the fingerprint already pending →
237
+ success, no rewrite (an echo of a report). A different fingerprint is stored as
238
+ pending; do not rewrite the live intention. `escalate` consumes
239
+ `pending_fingerprint`. Conductor calls `escalate({ kind: "unit" })` after the
240
+ unit ends, and when entering `integrating`, if the field is set. WorkLedger
250
241
  does not watch a Transformer's clock.
251
242
 
252
- Escalation `kind` is one of `plan`, `subtask`, `assembly`, `submitted`,
253
- `merging`.
243
+ Escalation `kind` is one of `unit`, `assembly`, `submitted`, `merging`.
254
244
 
255
245
  ## 11. Queries
256
246
 
@@ -258,34 +248,74 @@ Escalation `kind` is one of `plan`, `subtask`, `assembly`, `submitted`,
258
248
  | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
259
249
  | `get` | Aggregate for a `key`, or missing |
260
250
  | `list` | One summary per known key, every state. No persist, no bail, no transition |
261
- | `listInFlight` | The `list` subset that is mid-flight (`planning` / `running` / `integrating` / `submitted`) |
262
- | `activeOn` | The active FeatureStandard on a Project (`planning` / `running` / `escalated` / `integrating` / `submitted` / `merging`), or none |
251
+ | `listInFlight` | The `list` subset that is mid-flight (`running` / `integrating` / `submitted`) |
252
+ | `activeOn` | The active TicketStandard on a Project (`running` / `escalated` / `integrating` / `submitted` / `merging`), or none |
263
253
  | `nextReceived` | What `claim` would take, without taking it |
264
- | `nextRunnable` | A `runnable` Subtask of a Feature (ties: the one that unblocks the most others). Glue still serializes |
265
254
  | `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 |
266
255
 
267
256
  Queries do not persist. They do not take a bail.
268
257
 
258
+ ### Features
259
+
260
+ A Feature is stored through the same Port, beside the Tickets, with
261
+ `kind: "feature"`. Ticket queries (`list`, `listInFlight`, `activeOn`,
262
+ `nextReceived`, `listDeclaredWorkspaces`) never return it; its own queries do.
263
+
264
+ | Feature state | Meaning |
265
+ | ------------- | ------------------------------------------------------- |
266
+ | `waiting` | Admitted, not yet opened |
267
+ | `open` | Its line exists and its Tickets are made, one at a time |
268
+ | `submitted` | Its line is with the Authority |
269
+ | `escalated` | Frozen for a human |
270
+ | `done` | Terminal. Its line is folded |
271
+ | `cancelled` | Terminal |
272
+
273
+ | Command | Meaning | Refused when |
274
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
275
+ | `setFeatureSkipE2e` | Put on or take off the tracker's e2e skip mark of a Feature (`intention.skip_e2e`), while it is `waiting`, `open` or `escalated`. A Ticket's `intention.e2e` is set by its admission | `submitted`, `done`, `cancelled` (`illegal-transition`); unknown key (`not-found`) |
276
+ | `admitFeature` | Store as `waiting`. While waiting, an edit replaces the intention; once held (`open` / `submitted` / `escalated`), it is recorded as pending and the echo of an edit is a success | `done` (`too-late`); `cancelled` (`illegal-transition`) |
277
+ | `openFeature` | `waiting` → `open`, recording the line and the keys of its Tickets | Not `waiting`; the Project already holds a Feature (`project-busy`) |
278
+ | `markFeatureSubmitted` | `open` → `submitted`, recording the Authority's opaque `reference` | Not `open` |
279
+ | `markFeatureDone` | `submitted` → `done`, recording the fold's reference when given | Not `submitted` |
280
+ | `escalateFeature` | `open` or `submitted` → `escalated` with the report; consumes a pending edit | Any other state |
281
+ | `resumeFeature` | `escalated` → `open`, counting a round. The Submission, if there is one, stays | Not `escalated` |
282
+ | `cancelFeature` | Any state but `done` → `cancelled` | `done` (`point-of-no-return`) |
283
+
284
+ | Query | Out |
285
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
286
+ | `getFeature` | The aggregate, or missing |
287
+ | `listFeatures` | One summary per Feature |
288
+ | `activeFeatureOn` | The Feature that holds a Project: `open`, `submitted` or `escalated` |
289
+ | `nextFeature` | What would open next: `waiting`, every Feature it waits on `done`, highest priority then oldest. Nothing while a Feature holds the Project |
290
+
291
+ A Ticket that waits on others is not claimed until each is `done`; a blocker the
292
+ ledger does not know blocks. `claim` and `nextReceived` skip a received Ticket
293
+ that may not start and take the next one that may.
294
+
269
295
  ## 12. Bail and reconcile
270
296
 
271
- While a FeatureStandard is `planning`, `running`, `integrating`, `submitted`,
272
- or `merging`, it holds a bail (`expires_at`). There is no holder id in this
273
- implementation. Glue renews by calling `startSubtask` / `claim`-family
274
- commands; `renewBail` exists if expiry would otherwise pass during a long
275
- Attempt.
297
+ While a unit is held (`startUnit`) and while a TicketStandard is `integrating`,
298
+ `submitted`, or `merging`, it holds a bail (`expires_at`). There is no holder id
299
+ in this implementation. Glue renews with `startUnit` / `markIntegrating` /
300
+ `markSubmitted`; `renewBail` exists if expiry would otherwise pass during a
301
+ long Attempt.
276
302
 
277
- Expired bail: `expireBail`. The Subtask becomes `runnable` from zero and
278
- consumes an Attempt. Declared Subtask workspace is cleared (glue deletes the
279
- directory). The ledger decides; glue acts.
303
+ Expired bail: `expireBail`. A `running` Ticket's unit stops and consumes an
304
+ Attempt. The declared workspace stays declared, with `first_stage_done` if the
305
+ stage before the producer was complete in it: the next pass picks it up, or
306
+ starts over if the directory is gone (`clearWorkspace`, `releaseBail` + glue).
307
+ A human's `resumeTicket` of a unit forgets both: that unit starts from zero.
308
+ The ledger decides; glue acts.
280
309
 
281
310
  A clean stop (or an operator) that knows the holder is gone calls
282
- `releaseBail`: the same `planning` / `running` outcome, without the clock.
283
- It refuses `integrating`, `submitted`, `merging`, and every terminal state —
284
- those are not a held Subtask. A `running` Feature that already has no Subtask
285
- `running` is success with no rewrite.
311
+ `releaseBail`: the same `running` outcome, without the clock and without
312
+ consuming an Attempt (`markFirstStageDone` records that the stage before the
313
+ producer is complete in the declared workspace; `declareWorkspace` resets it). It refuses
314
+ `integrating`, `submitted`, `merging`, and every terminal state — those do not
315
+ hold a unit. A `running` Ticket that holds no bail is success with no rewrite.
286
316
 
287
317
  After a process start, glue calls `listDeclaredWorkspaces` and destroys
288
- directories that are not on that list. It does not invent Feature state from
318
+ directories that are not on that list. It does not invent Ticket state from
289
319
  disk.
290
320
 
291
321
  ## 13. Refusals
@@ -297,11 +327,8 @@ A refused command leaves the store as it was. `code` is one token.
297
327
  | `not-found` | `key` is unknown |
298
328
  | `duplicate` | Reserved; `admit` of an identical fingerprint is success, not this |
299
329
  | `illegal-transition` | State does not allow this command |
300
- | `project-busy` | `claim` while that Project already has an active FeatureStandard |
301
- | `plan-frozen` | `recordPlan` when a Plan already exists |
302
- | `plan-not-frozen` | A Subtask command before `recordPlan` |
303
- | `subtask-not-runnable` | `startSubtask` on a Subtask whose dependencies are not `integrated` |
304
- | `another-running` | `startSubtask` while one Subtask is already `running` |
330
+ | `project-busy` | `claim` while that Project already has an active TicketStandard; `openFeature` while it holds a Feature |
331
+ | `another-running` | `startUnit` while a unit is already held |
305
332
  | `missing-trace` | `escalate` after an Attempt with no Trace |
306
333
  | `point-of-no-return` | `cancel` in `merging` or `done`, or abandon after merging-born escalation |
307
334
  | `too-late` | `admit` / `recordInvalid` / intention edit in `done` |
@@ -345,22 +372,21 @@ directories here.
345
372
 
346
373
  ## 16. Acceptance
347
374
 
348
- 1. `admit` of a new FeatureStandard → `get` returns it in `received` with that
375
+ 1. `admit` of a new TicketStandard → `get` returns it in `received` with that
349
376
  `key` and `fingerprint`.
350
377
  2. `admit` of the same `key` and fingerprint again → success, aggregate
351
378
  unchanged (byte-for-byte on the stored fingerprint and intention).
352
379
  3. `admit` of the same `key` with a new fingerprint while `received` → stored
353
380
  intention updates.
354
381
  4. `admit` while `running` with a new fingerprint → pending fingerprint set;
355
- frozen Plan unchanged; Feature state still `running`. Same fingerprint
382
+ Ticket state still `running`. Same fingerprint
356
383
  while `running` or `submitted` → success, no pending field.
357
- 5. `claim` on a Project with one `received` → that Feature is `planning` and
358
- holds a bail. A second `claim` on the same Project → `project-busy`.
359
- 6. `recordPlan` with two Subtasks, B depending on A → A `runnable`, B
360
- `pending`. `startSubtask(B)` → `subtask-not-runnable`.
361
- 7. `startSubtask(A)` then `startSubtask` on any other → `another-running`.
362
- 8. `markSubtaskIntegrated` on the last non-terminal Subtask → Feature
363
- `integrating`.
384
+ 5. `claim` on a Project with one `received` → that Ticket is `running`, with
385
+ no bail. A second `claim` on the same Project → `project-busy`.
386
+ 6. `startUnit` takes the bail once; a second call → `another-running`.
387
+ 7. `recordAttempt` numbers the Attempts from 1; a gap → `illegal-transition`.
388
+ 8. `markIntegrating` on a `running` Ticket → `integrating`; on any other
389
+ state → `illegal-transition`.
364
390
  9. `cancel` in `merging` → `point-of-no-return`; state still `merging`.
365
391
  10. `escalate` after `recordAttempt` without a Trace → `missing-trace`; state
366
392
  unchanged.
@@ -382,8 +408,8 @@ consequence: another answer was possible.
382
408
 
383
409
  1. **Not a Transformer.** A store with many verbs is not IN/OUT-shaped. One IN
384
410
  and one OUT would force a toolbox CLI and hide the transition table.
385
- 2. **Commands, not `set(state)`.** The product rules (plan freeze, one active
386
- FeatureStandard per Project, no abandon in `merging`) live here or they live
411
+ 2. **Commands, not `set(state)`.** The product rules (one active
412
+ TicketStandard per Project, no abandon in `merging`) live here or they live
387
413
  nowhere.
388
414
  3. **Injected Persistence Port.** Sniffing env or a path to pick SQLite later
389
415
  would couple the ledger to ops. Glue chooses; the ledger stays portable.
@@ -394,12 +420,12 @@ consequence: another answer was possible.
394
420
  verbs would leak SQL into the ledger. Adapters may split storage internally.
395
421
  6. **Filesystem and SQLite both ship.** Enough to be truth on disk or in one
396
422
  file. Postgres is an adapter, not a prerequisite.
397
- 7. **Keys encoded for filenames.** FeatureAdapter keys contain `:`. Using them
423
+ 7. **Keys encoded for filenames.** Adapter keys contain `:`. Using them
398
424
  raw would create extra directories or illegal paths.
399
425
  8. **Refused commands do not persist.** A failed transition must not look like
400
426
  history.
401
427
  9. **`persist-failed` is not `invalid`.** Disk or database blips must not brand
402
- a FeatureStandard unusable.
428
+ a TicketStandard unusable.
403
429
  10. **Declared workspaces are ledger facts, not discoveries.** Glue writes the
404
430
  path before creating the directory, so crash reconcile has a list that
405
431
  disk cannot lie about.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
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";
1
+ export type { FeatureAdmission, FeatureCommands, FeatureGetResult, } from "./ledger/feature-commands.js";
2
+ export type { ClaimResult, CommandOk, CommandRefused, CommandResult, DeclaredWorkspace, DeclareWorkspaceInput, EscalateInput, GetResult, OpenWorkLedgerOptions, RecordAttemptInput, TicketAdmission, WorkLedger, } from "./ledger/open-work-ledger.js";
3
3
  export { openWorkLedger } from "./ledger/open-work-ledger.js";
4
4
  export type { PersistencePort, PersistSaveResult } from "./persist/port.js";
5
5
  export type * from "./records.js";
6
+ export { isFeatureAggregate, isFeatureSummary } from "./records.js";
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,gBAAgB,GACjB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EACV,WAAW,EACX,SAAS,EACT,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,aAAa,EACb,SAAS,EACT,qBAAqB,EACrB,kBAAkB,EAClB,eAAe,EACf,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;AAClC,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { openWorkLedger } from "./ledger/open-work-ledger.js";
2
+ export { isFeatureAggregate, isFeatureSummary } from "./records.js";
2
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAG9D,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,71 @@
1
+ import type { PersistencePort } from "../persist/port.js";
2
+ import type { FeatureAggregate, FeatureIntention, FeatureSummary } from "../records.js";
3
+ import { type CommandResult } from "./results.js";
4
+ export type FeatureAdmission = FeatureIntention;
5
+ export type FeatureGetResult = {
6
+ ok: true;
7
+ aggregate: FeatureAggregate;
8
+ } | {
9
+ ok: false;
10
+ code: "not-found";
11
+ };
12
+ export type FeatureCommands = {
13
+ /** Store a Feature as `waiting`. An edit while it waits replaces the intention; while it is held, it is recorded as pending. */
14
+ admitFeature(feature: FeatureAdmission): Promise<CommandResult>;
15
+ /** What would open next on the Project: waiting, every blocker `done`, highest priority then oldest. Nothing while the Project holds a Feature. */
16
+ nextFeature(project: string): Promise<FeatureSummary | undefined>;
17
+ /** `waiting` → `open`, with the line its Tickets land on (none without an Authority) and the keys of those Tickets. */
18
+ openFeature(input: {
19
+ key: string;
20
+ line?: string;
21
+ tickets: string[];
22
+ }): Promise<CommandResult>;
23
+ /** `open` → `submitted`: the line is with the Authority. */
24
+ markFeatureSubmitted(input: {
25
+ key: string;
26
+ reference: string;
27
+ }): Promise<CommandResult>;
28
+ /** The Authority's judgement sent the line back: `submitted` → `open`, with why, for the next fix. */
29
+ recordFeatureRefusal(input: {
30
+ key: string;
31
+ report: string;
32
+ refusedBy?: string;
33
+ }): Promise<CommandResult>;
34
+ /** The line it is offered to moved on: `submitted` → `open`, to be aligned again. No fix is owed. */
35
+ recordFeatureRealign(key: string): Promise<CommandResult>;
36
+ /** A check refused the line before it was offered: kept, with the round it costs. */
37
+ recordFeatureParkedRefusal(input: {
38
+ key: string;
39
+ report: string;
40
+ refusedBy?: string;
41
+ }): Promise<CommandResult>;
42
+ /** Whatever sent the line back is repaired and checked. */
43
+ clearFeatureParkedRefusal(key: string): Promise<CommandResult>;
44
+ /** `submitted` → `done`. */
45
+ markFeatureDone(key: string, input?: {
46
+ reference?: string | undefined;
47
+ }): Promise<CommandResult>;
48
+ /** The tracker's e2e skip label moved: honoured while the Feature waits, is open or is escalated. */
49
+ setFeatureSkipE2e(input: {
50
+ key: string;
51
+ skip: boolean;
52
+ }): Promise<CommandResult>;
53
+ /** `open` or `submitted` → `escalated`, for a human. Consumes a pending edit. */
54
+ escalateFeature(input: {
55
+ key: string;
56
+ report?: string;
57
+ }): Promise<CommandResult>;
58
+ /** `escalated` → `open`, a round later. */
59
+ resumeFeature(key: string): Promise<CommandResult>;
60
+ /** Anything but `done` → `cancelled`. */
61
+ cancelFeature(key: string): Promise<CommandResult>;
62
+ getFeature(key: string): Promise<FeatureGetResult>;
63
+ listFeatures(): Promise<FeatureSummary[]>;
64
+ /** The Feature that holds the Project: `open`, `submitted` or `escalated`. */
65
+ activeFeatureOn(project: string): Promise<FeatureSummary | undefined>;
66
+ };
67
+ export declare function openFeatureCommands(options: {
68
+ persist: PersistencePort;
69
+ now: () => number;
70
+ }): FeatureCommands;
71
+ //# sourceMappingURL=feature-commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-commands.d.ts","sourceRoot":"","sources":["../../src/ledger/feature-commands.ts"],"names":[],"mappings":"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"}