frontier-mcp 0.1.0 → 0.2.1
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 +10 -5
- package/dist/driver-registry.d.ts +15 -0
- package/dist/driver-registry.d.ts.map +1 -0
- package/dist/driver-registry.js +19 -0
- package/dist/driver-registry.js.map +1 -0
- package/dist/server.d.ts +23 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +81 -39
- package/dist/server.js.map +1 -1
- package/dist/storage/driver.d.ts +41 -8
- package/dist/storage/driver.d.ts.map +1 -1
- package/dist/storage/driver.js.map +1 -1
- package/dist/storage/markdown/create.d.ts +5 -5
- package/dist/storage/markdown/create.d.ts.map +1 -1
- package/dist/storage/markdown/create.js +17 -17
- package/dist/storage/markdown/create.js.map +1 -1
- package/dist/storage/markdown/driver.d.ts +35 -1
- package/dist/storage/markdown/driver.d.ts.map +1 -1
- package/dist/storage/markdown/driver.js +155 -84
- package/dist/storage/markdown/driver.js.map +1 -1
- package/dist/storage/markdown/migrate.d.ts +4 -4
- package/dist/storage/markdown/migrate.d.ts.map +1 -1
- package/dist/storage/markdown/migrate.js +1 -1
- package/dist/storage/markdown/migrate.js.map +1 -1
- package/dist/storage/markdown/ticket.d.ts +16 -5
- package/dist/storage/markdown/ticket.d.ts.map +1 -1
- package/dist/storage/markdown/ticket.js +19 -7
- package/dist/storage/markdown/ticket.js.map +1 -1
- package/dist/storage/markdown/watcher.d.ts +33 -0
- package/dist/storage/markdown/watcher.d.ts.map +1 -0
- package/dist/storage/markdown/watcher.js +142 -0
- package/dist/storage/markdown/watcher.js.map +1 -0
- package/dist/tools/create-tickets.d.ts +4 -4
- package/dist/tools/create-tickets.d.ts.map +1 -1
- package/dist/tools/create-tickets.js.map +1 -1
- package/dist/tools/list-efforts.d.ts.map +1 -1
- package/dist/tools/list-efforts.js +2 -1
- package/dist/tools/list-efforts.js.map +1 -1
- package/dist/tools/update-ticket.d.ts +3 -3
- package/dist/tools/update-ticket.d.ts.map +1 -1
- package/dist/tools/update-ticket.js.map +1 -1
- package/dist/tools/workspace-line.d.ts +18 -0
- package/dist/tools/workspace-line.d.ts.map +1 -0
- package/dist/tools/workspace-line.js +22 -0
- package/dist/tools/workspace-line.js.map +1 -0
- package/dist/workspace.d.ts +5 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +42 -5
- package/dist/workspace.js.map +1 -1
- package/docs/agents/issue-tracker.md +14 -2
- package/package.json +1 -1
- package/dist/workspace-index.d.ts +0 -39
- package/dist/workspace-index.d.ts.map +0 -1
- package/dist/workspace-index.js +0 -83
- package/dist/workspace-index.js.map +0 -1
- package/dist/workspace-watcher.d.ts +0 -24
- package/dist/workspace-watcher.d.ts.map +0 -1
- package/dist/workspace-watcher.js +0 -87
- package/dist/workspace-watcher.js.map +0 -1
|
@@ -34,8 +34,8 @@ const CANDIDATE_HEADROOM = 1000;
|
|
|
34
34
|
* write X. See ADR 0005.
|
|
35
35
|
*/
|
|
36
36
|
export async function createTicketFiles(request) {
|
|
37
|
-
const {
|
|
38
|
-
const { reservations, tickets } = await reserve(
|
|
37
|
+
const { storage, effort, dir, issues, drafts, rescan, validate, createEffort } = request;
|
|
38
|
+
const { reservations, tickets } = await reserve(storage, drafts.length, rescan);
|
|
39
39
|
try {
|
|
40
40
|
const ids = reservations.map(entry => entry.id);
|
|
41
41
|
// The last rule that can only be checked now: the ids are real, and nothing
|
|
@@ -65,10 +65,10 @@ export async function createTicketFiles(request) {
|
|
|
65
65
|
* Hold exclusive id guards for `count` fresh ids, run `use`, then release.
|
|
66
66
|
* Migration mints through this so a parallel create_tickets cannot collide.
|
|
67
67
|
*/
|
|
68
|
-
export async function withIdReservations(
|
|
68
|
+
export async function withIdReservations(storage, count, rescan, use) {
|
|
69
69
|
if (count === 0)
|
|
70
70
|
return use([]);
|
|
71
|
-
const { reservations } = await reserve(
|
|
71
|
+
const { reservations } = await reserve(storage, count, rescan);
|
|
72
72
|
try {
|
|
73
73
|
return await use(reservations.map(entry => entry.id));
|
|
74
74
|
}
|
|
@@ -172,9 +172,9 @@ async function writeAll(issues, files) {
|
|
|
172
172
|
throw lost.reason;
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
|
-
async function reserve(
|
|
175
|
+
async function reserve(storage, count, rescan, attempt = 1) {
|
|
176
176
|
const used = idsOf(await rescan());
|
|
177
|
-
const reservations = await claim(
|
|
177
|
+
const reservations = await claim(storage, used, count, highestMinted(used) + 1, []);
|
|
178
178
|
let settled;
|
|
179
179
|
try {
|
|
180
180
|
settled = await rescan();
|
|
@@ -192,7 +192,7 @@ async function reserve(scratch, count, rescan, attempt = 1) {
|
|
|
192
192
|
throw new Error(`Could not reserve ${String(count)} Ticket ids after ${String(MAX_ATTEMPTS)} attempts — ` +
|
|
193
193
|
'another session is creating Tickets continuously. Retry in a moment.');
|
|
194
194
|
}
|
|
195
|
-
return reserve(
|
|
195
|
+
return reserve(storage, count, rescan, attempt + 1);
|
|
196
196
|
}
|
|
197
197
|
function release(reservations) {
|
|
198
198
|
return Promise.allSettled(reservations.map(entry => unlink(entry.guard).catch(() => { })));
|
|
@@ -202,23 +202,23 @@ function release(reservations) {
|
|
|
202
202
|
* each attempt depends on the one before it, and a bumped candidate is the
|
|
203
203
|
* normal case rather than an error.
|
|
204
204
|
*/
|
|
205
|
-
async function claim(
|
|
205
|
+
async function claim(storage, used, count, candidate, taken) {
|
|
206
206
|
if (taken.length === count)
|
|
207
207
|
return taken;
|
|
208
208
|
if (candidate > highestMinted(used) + CANDIDATE_HEADROOM + count) {
|
|
209
209
|
await release(taken);
|
|
210
210
|
throw new Error(`No free Ticket id within ${String(CANDIDATE_HEADROOM)} of the highest in use. ` +
|
|
211
|
-
|
|
211
|
+
`Sweep the abandoned .frontier-id-*.guard files under ${storage}.`);
|
|
212
212
|
}
|
|
213
213
|
const id = `T${String(candidate)}`;
|
|
214
214
|
if (used.has(id))
|
|
215
|
-
return claim(
|
|
216
|
-
const guard = guardFor(
|
|
215
|
+
return claim(storage, used, count, candidate + 1, taken);
|
|
216
|
+
const guard = guardFor(storage, id);
|
|
217
217
|
// A guard we cannot take belongs to a session that is still allocating. Bump
|
|
218
218
|
// rather than wait: an id may be skipped, never reused.
|
|
219
219
|
if (!(await hold(guard)))
|
|
220
|
-
return claim(
|
|
221
|
-
return claim(
|
|
220
|
+
return claim(storage, used, count, candidate + 1, taken);
|
|
221
|
+
return claim(storage, used, count, candidate + 1, [...taken, { id, guard }]);
|
|
222
222
|
}
|
|
223
223
|
/**
|
|
224
224
|
* Take a guard, or report that somebody else has it.
|
|
@@ -251,11 +251,11 @@ async function hold(guard) {
|
|
|
251
251
|
/**
|
|
252
252
|
* Beside the Efforts rather than inside one, because the counter is repo-global:
|
|
253
253
|
* two sessions creating into different Efforts must still collide here. Hidden
|
|
254
|
-
* and not `.md`, so no scan mistakes it for a Ticket, and
|
|
255
|
-
* no Effort because it is not a directory.
|
|
254
|
+
* and not `.md`, so no scan mistakes it for a Ticket, and the storage scan
|
|
255
|
+
* yields it no Effort because it is not a directory.
|
|
256
256
|
*/
|
|
257
|
-
function guardFor(
|
|
258
|
-
return join(
|
|
257
|
+
function guardFor(storage, id) {
|
|
258
|
+
return join(storage, `.frontier-id-${id}.guard`);
|
|
259
259
|
}
|
|
260
260
|
function idsOf(tickets) {
|
|
261
261
|
return new Set(tickets.map(entry => entry.id).filter(id => id !== undefined));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.js","sourceRoot":"","sources":["../../../src/storage/markdown/create.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"create.js","sourceRoot":"","sources":["../../../src/storage/markdown/create.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,41 @@
|
|
|
1
1
|
import type { StorageDriver } from '../driver.ts';
|
|
2
|
+
export interface MarkdownDriverOptions {
|
|
3
|
+
/**
|
|
4
|
+
* The directory under `root` this driver keeps its Efforts in, relative to
|
|
5
|
+
* the workspace root and one segment deep. Defaults to `.scratch`.
|
|
6
|
+
*
|
|
7
|
+
* It is a parameter rather than a constant because it is this driver's answer
|
|
8
|
+
* to "what do you call your storage", which the seam exists to keep from
|
|
9
|
+
* being everybody's.
|
|
10
|
+
*/
|
|
11
|
+
readonly storageDir?: string;
|
|
12
|
+
/** Debounce filesystem watcher events before dropping the scan. */
|
|
13
|
+
readonly watcherDebounceMs?: number;
|
|
14
|
+
/**
|
|
15
|
+
* When the watcher drops the scan unconditionally after attaching, to cover
|
|
16
|
+
* the events an `fs.watch` loses while it is still warming up — see
|
|
17
|
+
* {@link watchStorage}. It exists so a test can pin those moments and assert
|
|
18
|
+
* on them; the default schedule is the right one for real use.
|
|
19
|
+
*/
|
|
20
|
+
readonly watcherSettleMs?: readonly number[];
|
|
21
|
+
}
|
|
2
22
|
/**
|
|
3
23
|
* The markdown driver — the only driver in v1. Bound to `root`, the resolved
|
|
4
24
|
* workspace, so that no path crosses the {@link StorageDriver} interface.
|
|
25
|
+
*
|
|
26
|
+
* It is the workspace *cached and live*: it holds the scan a Board is built
|
|
27
|
+
* from, drops it when it writes, and watches the storage directory so a change
|
|
28
|
+
* another process made drops it too. Both are markdown policy — a full scan is
|
|
29
|
+
* cheap enough to cache wholesale, and `node:fs.watch` is how this physical
|
|
30
|
+
* model learns it moved. A driver over a database would cache and notice
|
|
31
|
+
* neither the same way, which is why nothing above the seam does it for them.
|
|
32
|
+
* "Live" arrives slightly after construction, though: a watch is not delivering
|
|
33
|
+
* events the instant it is created, and what it misses in that window is gone
|
|
34
|
+
* rather than delayed, so the driver drops its scan a few times while the watch
|
|
35
|
+
* warms.
|
|
36
|
+
*
|
|
37
|
+
* Close it to stop the watcher; a process that keeps a workspace open forever
|
|
38
|
+
* never needs to.
|
|
5
39
|
*/
|
|
6
|
-
export declare function createMarkdownDriver(root: string): StorageDriver;
|
|
40
|
+
export declare function createMarkdownDriver(root: string, { storageDir, watcherDebounceMs, watcherSettleMs, }?: MarkdownDriverOptions): StorageDriver;
|
|
7
41
|
//# sourceMappingURL=driver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../../src/storage/markdown/driver.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../../src/storage/markdown/driver.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAGV,aAAa,EAEd,MAAM,cAAc,CAAC;AAwCtB,MAAM,WAAW,qBAAqB;IACpC;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,EACE,UAAgC,EAChC,iBAAiB,EACjB,eAAe,GAChB,GAAE,qBAA0B,GAC5B,aAAa,CA+Hf"}
|
|
@@ -6,11 +6,13 @@ import { migrateEffortFiles } from './migrate.js';
|
|
|
6
6
|
import { applyEdit } from './serialize.js';
|
|
7
7
|
import { currentRevision, GuardHeld, withGuard, writeAtomically } from './write.js';
|
|
8
8
|
import { applyMapEdit, readMapDocument, readSection, readSpecOpening, scaffoldMap, SECTION_HEADINGS, } from './header-doc.js';
|
|
9
|
-
import {
|
|
9
|
+
import { parseTicketSummary, ticketBody } from './ticket.js';
|
|
10
|
+
import { watchStorage } from './watcher.js';
|
|
10
11
|
/**
|
|
11
|
-
* The layout this driver serves, per AGENTS.md
|
|
12
|
+
* The layout this driver serves, per AGENTS.md, under the storage directory it
|
|
13
|
+
* was constructed with:
|
|
12
14
|
*
|
|
13
|
-
*
|
|
15
|
+
* <storage>/<effort>/
|
|
14
16
|
* ├── map.md # Header doc — wayfinder
|
|
15
17
|
* ├── spec.md # Header doc — to-spec
|
|
16
18
|
* └── issues/
|
|
@@ -18,7 +20,7 @@ import { parseTicket } from './ticket.js';
|
|
|
18
20
|
*
|
|
19
21
|
* Anything else in an Effort directory is ignored, never an error.
|
|
20
22
|
*/
|
|
21
|
-
const
|
|
23
|
+
const DEFAULT_STORAGE_DIR = '.scratch';
|
|
22
24
|
const ISSUES_DIR = 'issues';
|
|
23
25
|
const MAP_FILE = 'map.md';
|
|
24
26
|
const SPEC_FILE = 'spec.md';
|
|
@@ -29,65 +31,112 @@ const HEADER_DOCS = [
|
|
|
29
31
|
/**
|
|
30
32
|
* The markdown driver — the only driver in v1. Bound to `root`, the resolved
|
|
31
33
|
* workspace, so that no path crosses the {@link StorageDriver} interface.
|
|
34
|
+
*
|
|
35
|
+
* It is the workspace *cached and live*: it holds the scan a Board is built
|
|
36
|
+
* from, drops it when it writes, and watches the storage directory so a change
|
|
37
|
+
* another process made drops it too. Both are markdown policy — a full scan is
|
|
38
|
+
* cheap enough to cache wholesale, and `node:fs.watch` is how this physical
|
|
39
|
+
* model learns it moved. A driver over a database would cache and notice
|
|
40
|
+
* neither the same way, which is why nothing above the seam does it for them.
|
|
41
|
+
* "Live" arrives slightly after construction, though: a watch is not delivering
|
|
42
|
+
* events the instant it is created, and what it misses in that window is gone
|
|
43
|
+
* rather than delayed, so the driver drops its scan a few times while the watch
|
|
44
|
+
* warms.
|
|
45
|
+
*
|
|
46
|
+
* Close it to stop the watcher; a process that keeps a workspace open forever
|
|
47
|
+
* never needs to.
|
|
32
48
|
*/
|
|
33
|
-
export function createMarkdownDriver(root) {
|
|
34
|
-
const
|
|
49
|
+
export function createMarkdownDriver(root, { storageDir = DEFAULT_STORAGE_DIR, watcherDebounceMs, watcherSettleMs, } = {}) {
|
|
50
|
+
const storage = join(root, storageDir);
|
|
35
51
|
let writes = Promise.resolve();
|
|
36
52
|
const walk = async () => {
|
|
37
53
|
// A repo that has never used the tracker is a supported starting state,
|
|
38
54
|
// not an error: it simply holds no Efforts.
|
|
39
|
-
const slugs = await readDirectories(
|
|
40
|
-
const scanned = await Promise.all(slugs.map(slug => readEffort(
|
|
55
|
+
const slugs = await readDirectories(storage);
|
|
56
|
+
const scanned = await Promise.all(slugs.map(slug => readEffort(storage, slug)));
|
|
41
57
|
return scanned
|
|
42
58
|
.filter((entry) => entry !== undefined)
|
|
43
59
|
.toSorted((a, b) => a.effort.slug.localeCompare(b.effort.slug));
|
|
44
60
|
};
|
|
45
61
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
62
|
+
* Hold the in-flight walk, not just its result, so concurrent callers share
|
|
63
|
+
* one scan instead of racing several. A failed scan is never cached as the
|
|
64
|
+
* answer.
|
|
49
65
|
*
|
|
50
|
-
* The
|
|
51
|
-
*
|
|
52
|
-
*
|
|
66
|
+
* The whole scan is one cache rather than one per method: `listEfforts` and
|
|
67
|
+
* `listTickets` are two readings of a single walk, and every caller that
|
|
68
|
+
* wants a Board wants both.
|
|
53
69
|
*/
|
|
54
|
-
let
|
|
70
|
+
let cachedScan;
|
|
55
71
|
const scan = () => {
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
cachedScan ??= walk().catch((error) => {
|
|
73
|
+
cachedScan = undefined;
|
|
74
|
+
throw error;
|
|
58
75
|
});
|
|
59
|
-
return
|
|
76
|
+
return cachedScan;
|
|
60
77
|
};
|
|
78
|
+
/** Drop the scan, so the next read rebuilds from disk. */
|
|
79
|
+
const invalidate = () => {
|
|
80
|
+
cachedScan = undefined;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* A write moves the workspace on whether or not it succeeded partway, so the
|
|
84
|
+
* next read rebuilds rather than trusting a scan taken before it.
|
|
85
|
+
*/
|
|
86
|
+
const moved = async (operation) => {
|
|
87
|
+
try {
|
|
88
|
+
return await operation();
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
invalidate();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const watcher = watchStorage(root, storageDir, invalidate, {
|
|
95
|
+
debounceMs: watcherDebounceMs,
|
|
96
|
+
settleMs: watcherSettleMs,
|
|
97
|
+
});
|
|
61
98
|
return {
|
|
62
99
|
async listEfforts() {
|
|
63
100
|
return (await scan()).map(entry => entry.effort);
|
|
64
101
|
},
|
|
65
102
|
async listTickets() {
|
|
66
|
-
return (await scan()).flatMap(entry => entry
|
|
103
|
+
return (await scan()).flatMap(entry => summariesOf(entry));
|
|
104
|
+
},
|
|
105
|
+
async readTickets(handles) {
|
|
106
|
+
// Deliberately the fresh walk rather than the cached one. A body is read
|
|
107
|
+
// for the few Tickets being worked, and reading it against a scan taken
|
|
108
|
+
// before another process wrote is exactly the staleness `get_tickets`
|
|
109
|
+
// exists not to serve. It costs a walk a concurrent Board would have
|
|
110
|
+
// shared — the price of an answer nothing can have staled underneath it.
|
|
111
|
+
return readBodies(storage, await walk(), handles);
|
|
67
112
|
},
|
|
68
113
|
updateTicket(handle, edit, expectedRevision) {
|
|
69
|
-
return serialized(() => write(
|
|
114
|
+
return moved(() => serialized(() => write(storage, handle, edit, expectedRevision)));
|
|
70
115
|
},
|
|
71
116
|
createTickets(effort, drafts, options) {
|
|
72
|
-
// The scan is deliberately not the
|
|
73
|
-
// its own guards, and
|
|
74
|
-
//
|
|
75
|
-
return serialized(() => create(
|
|
117
|
+
// The scan is deliberately not the cached one: allocation re-reads under
|
|
118
|
+
// its own guards, and a walk started before them would defeat the check
|
|
119
|
+
// it exists to make.
|
|
120
|
+
return moved(() => serialized(() => create(storage, effort, drafts, options, walk)));
|
|
76
121
|
},
|
|
77
122
|
readMap(effort) {
|
|
78
|
-
return serialized(() => loadMap(
|
|
123
|
+
return serialized(() => loadMap(storage, effort));
|
|
79
124
|
},
|
|
80
125
|
editMap(effort, edit, expectedRevision, options) {
|
|
81
|
-
return serialized(() => saveMap(
|
|
126
|
+
return moved(() => serialized(() => saveMap(storage, effort, edit, expectedRevision, options)));
|
|
82
127
|
},
|
|
83
128
|
readSpec(effort) {
|
|
84
|
-
return serialized(() => loadSpec(
|
|
129
|
+
return serialized(() => loadSpec(storage, effort));
|
|
85
130
|
},
|
|
86
131
|
putSpec(effort, body, expectedRevision, options) {
|
|
87
|
-
return serialized(() => saveSpec(
|
|
132
|
+
return moved(() => serialized(() => saveSpec(storage, effort, body, expectedRevision, options)));
|
|
88
133
|
},
|
|
89
134
|
migrateEffort(effort, options) {
|
|
90
|
-
return serialized(() => migrate(
|
|
135
|
+
return moved(() => serialized(() => migrate(storage, effort, options, walk)));
|
|
136
|
+
},
|
|
137
|
+
close() {
|
|
138
|
+
watcher.close();
|
|
139
|
+
invalidate();
|
|
91
140
|
},
|
|
92
141
|
};
|
|
93
142
|
/**
|
|
@@ -104,15 +153,15 @@ export function createMarkdownDriver(root) {
|
|
|
104
153
|
return next;
|
|
105
154
|
}
|
|
106
155
|
}
|
|
107
|
-
async function migrate(
|
|
108
|
-
const dir = join(
|
|
109
|
-
if ((await readEffort(
|
|
156
|
+
async function migrate(storage, effort, options, walk) {
|
|
157
|
+
const dir = join(storage, effort);
|
|
158
|
+
if ((await readEffort(storage, effort)) === undefined)
|
|
110
159
|
throw new NoSuchEffort(effort);
|
|
111
160
|
const entries = await readTicketFiles(dir, effort);
|
|
112
|
-
const allTickets = (await walk()).flatMap(
|
|
161
|
+
const allTickets = (await walk()).flatMap(summariesOf);
|
|
113
162
|
const issues = join(dir, ISSUES_DIR);
|
|
114
163
|
return migrateEffortFiles({
|
|
115
|
-
|
|
164
|
+
storage,
|
|
116
165
|
effort,
|
|
117
166
|
issues,
|
|
118
167
|
entries,
|
|
@@ -121,24 +170,24 @@ async function migrate(scratch, effort, options, walk) {
|
|
|
121
170
|
rename: options.rename,
|
|
122
171
|
// Allocation must not share a walk started before the guards — same reason
|
|
123
172
|
// createTickets builds its own rescan.
|
|
124
|
-
rescan: async () => (await walk()).flatMap(
|
|
173
|
+
rescan: async () => (await walk()).flatMap(summariesOf),
|
|
125
174
|
readContents: filename => readFile(join(issues, filename), 'utf8'),
|
|
126
175
|
});
|
|
127
176
|
}
|
|
128
|
-
async function create(
|
|
129
|
-
const dir = join(
|
|
130
|
-
const missing = (await readEffort(
|
|
177
|
+
async function create(storage, effort, drafts, options, walk) {
|
|
178
|
+
const dir = join(storage, effort);
|
|
179
|
+
const missing = (await readEffort(storage, effort)) === undefined;
|
|
131
180
|
if (missing && !options.createEffort)
|
|
132
181
|
throw new NoSuchEffort(effort);
|
|
133
182
|
// The directory is made inside, after validation — an Effort created here and
|
|
134
183
|
// then refused would show up in list_efforts as an empty one nobody asked for.
|
|
135
184
|
const filenames = await createTicketFiles({
|
|
136
|
-
|
|
185
|
+
storage,
|
|
137
186
|
effort,
|
|
138
187
|
dir,
|
|
139
188
|
issues: join(dir, ISSUES_DIR),
|
|
140
189
|
drafts,
|
|
141
|
-
rescan: async () => (await walk()).flatMap(
|
|
190
|
+
rescan: async () => (await walk()).flatMap(summariesOf),
|
|
142
191
|
validate: options.validate ?? (() => { }),
|
|
143
192
|
createEffort: missing,
|
|
144
193
|
});
|
|
@@ -151,8 +200,8 @@ async function create(scratch, effort, drafts, options, walk) {
|
|
|
151
200
|
return ticket;
|
|
152
201
|
});
|
|
153
202
|
}
|
|
154
|
-
async function write(
|
|
155
|
-
const located = await locate(
|
|
203
|
+
async function write(storage, handle, edit, expectedRevision) {
|
|
204
|
+
const located = await locate(storage, handle);
|
|
156
205
|
if (located === undefined)
|
|
157
206
|
throw new NoSuchTicket(handle);
|
|
158
207
|
const { dir, effort, filename, ticket } = located;
|
|
@@ -190,7 +239,7 @@ async function write(scratch, handle, edit, expectedRevision) {
|
|
|
190
239
|
const warnings = [];
|
|
191
240
|
if (edit.status === 'resolved' || edit.status === 'dropped') {
|
|
192
241
|
try {
|
|
193
|
-
await refreshMapDerived(
|
|
242
|
+
await refreshMapDerived(storage, effort);
|
|
194
243
|
}
|
|
195
244
|
catch {
|
|
196
245
|
warnings.push('Decisions-so-far / dropped Out-of-scope may be stale — Map derived refresh failed after the Ticket write.');
|
|
@@ -198,15 +247,15 @@ async function write(scratch, handle, edit, expectedRevision) {
|
|
|
198
247
|
}
|
|
199
248
|
return { ticket: written, warnings };
|
|
200
249
|
}
|
|
201
|
-
async function loadMap(
|
|
202
|
-
const path = join(
|
|
250
|
+
async function loadMap(storage, effort) {
|
|
251
|
+
const path = join(storage, effort, MAP_FILE);
|
|
203
252
|
const revision = await currentRevision(path);
|
|
204
253
|
if (revision === undefined)
|
|
205
254
|
throw new NoSuchMap(effort);
|
|
206
255
|
return readMapDocument(await readFile(path, 'utf8'), revision);
|
|
207
256
|
}
|
|
208
|
-
async function saveMap(
|
|
209
|
-
const dir = join(
|
|
257
|
+
async function saveMap(storage, effort, edit, expectedRevision, options) {
|
|
258
|
+
const dir = join(storage, effort);
|
|
210
259
|
const path = join(dir, MAP_FILE);
|
|
211
260
|
const current = await currentRevision(path);
|
|
212
261
|
const handle = `map:${effort}`;
|
|
@@ -214,7 +263,7 @@ async function saveMap(scratch, effort, edit, expectedRevision, options) {
|
|
|
214
263
|
// createEffort only guards starting a new Effort. An Effort that already
|
|
215
264
|
// holds a Spec or issues/ must be able to gain a Map without that flag —
|
|
216
265
|
// that is wayfinder's handoff, not a mistyped slug.
|
|
217
|
-
if ((await readEffort(
|
|
266
|
+
if ((await readEffort(storage, effort)) === undefined && !options.createEffort) {
|
|
218
267
|
throw new NoSuchEffort(effort);
|
|
219
268
|
}
|
|
220
269
|
await mkdir(join(dir, ISSUES_DIR), { recursive: true });
|
|
@@ -247,16 +296,16 @@ const DERIVED_REFRESH_ATTEMPTS = 16;
|
|
|
247
296
|
* Invents the GENERATED-bearing headings when absent so a slim Map still gets
|
|
248
297
|
* a truthful Decisions / Out-of-scope cache after resolve or drop.
|
|
249
298
|
*/
|
|
250
|
-
async function refreshMapDerived(
|
|
251
|
-
return refreshMapDerivedOnce(
|
|
299
|
+
async function refreshMapDerived(storage, effort) {
|
|
300
|
+
return refreshMapDerivedOnce(storage, effort, DERIVED_REFRESH_ATTEMPTS);
|
|
252
301
|
}
|
|
253
|
-
async function refreshMapDerivedOnce(
|
|
254
|
-
const path = join(
|
|
302
|
+
async function refreshMapDerivedOnce(storage, effort, remaining) {
|
|
303
|
+
const path = join(storage, effort, MAP_FILE);
|
|
255
304
|
const revision = await currentRevision(path);
|
|
256
305
|
if (revision === undefined)
|
|
257
306
|
return;
|
|
258
307
|
const contents = await readFile(path, 'utf8');
|
|
259
|
-
const entries = await readTicketFiles(join(
|
|
308
|
+
const entries = await readTicketFiles(join(storage, effort), effort);
|
|
260
309
|
const updated = applyMapEdit(contents, {}, ticketPointers(entries, 'resolved'), ticketPointers(entries, 'dropped'), { ensureDerivedSections: true });
|
|
261
310
|
if (updated === contents)
|
|
262
311
|
return;
|
|
@@ -276,7 +325,7 @@ async function refreshMapDerivedOnce(scratch, effort, remaining) {
|
|
|
276
325
|
throw new Error(`Could not refresh Map derived blocks for ${effort} after concurrent edits.`, { cause: error });
|
|
277
326
|
}
|
|
278
327
|
await new Promise(wait => setTimeout(wait, 10));
|
|
279
|
-
return refreshMapDerivedOnce(
|
|
328
|
+
return refreshMapDerivedOnce(storage, effort, remaining - 1);
|
|
280
329
|
}
|
|
281
330
|
}
|
|
282
331
|
/** Internal signal: another writer changed the Map while we held its guard. */
|
|
@@ -286,15 +335,15 @@ class DerivedRefreshLost extends Error {
|
|
|
286
335
|
this.name = 'DerivedRefreshLost';
|
|
287
336
|
}
|
|
288
337
|
}
|
|
289
|
-
async function loadSpec(
|
|
290
|
-
const path = join(
|
|
338
|
+
async function loadSpec(storage, effort) {
|
|
339
|
+
const path = join(storage, effort, SPEC_FILE);
|
|
291
340
|
const revision = await currentRevision(path);
|
|
292
341
|
if (revision === undefined)
|
|
293
342
|
throw new NoSuchSpec(effort);
|
|
294
343
|
return { body: await readFile(path, 'utf8'), revision };
|
|
295
344
|
}
|
|
296
|
-
async function saveSpec(
|
|
297
|
-
const dir = join(
|
|
345
|
+
async function saveSpec(storage, effort, body, expectedRevision, options) {
|
|
346
|
+
const dir = join(storage, effort);
|
|
298
347
|
const path = join(dir, SPEC_FILE);
|
|
299
348
|
const current = await currentRevision(path);
|
|
300
349
|
const handle = `spec:${effort}`;
|
|
@@ -303,7 +352,7 @@ async function saveSpec(scratch, effort, body, expectedRevision, options) {
|
|
|
303
352
|
if (current === undefined) {
|
|
304
353
|
// Same rule as Maps: createEffort starts an Effort; putting a Spec onto an
|
|
305
354
|
// Effort that already has a Map needs no flag.
|
|
306
|
-
if ((await readEffort(
|
|
355
|
+
if ((await readEffort(storage, effort)) === undefined && !options.createEffort) {
|
|
307
356
|
throw new NoSuchEffort(effort);
|
|
308
357
|
}
|
|
309
358
|
await mkdir(join(dir, ISSUES_DIR), { recursive: true });
|
|
@@ -354,14 +403,14 @@ function ticketPointers(entries, status) {
|
|
|
354
403
|
});
|
|
355
404
|
}
|
|
356
405
|
/** Find the file backing a Ticket, by id or by its `<effort>#<order>` handle. */
|
|
357
|
-
async function locate(
|
|
358
|
-
const slugs = await readDirectories(
|
|
359
|
-
const perEffort = await Promise.all(slugs.map(async (slug) => ({ slug, entries: await readTicketFiles(join(
|
|
406
|
+
async function locate(storage, handle) {
|
|
407
|
+
const slugs = await readDirectories(storage);
|
|
408
|
+
const perEffort = await Promise.all(slugs.map(async (slug) => ({ slug, entries: await readTicketFiles(join(storage, slug), slug) })));
|
|
360
409
|
for (const { slug, entries } of perEffort) {
|
|
361
410
|
const found = entries.find(entry => entry.ticket.id === handle || entry.ticket.handle === handle);
|
|
362
411
|
if (found !== undefined) {
|
|
363
412
|
return {
|
|
364
|
-
dir: join(
|
|
413
|
+
dir: join(storage, slug),
|
|
365
414
|
effort: slug,
|
|
366
415
|
filename: found.filename,
|
|
367
416
|
ticket: found.ticket,
|
|
@@ -381,10 +430,36 @@ function defaultsFor(ticket) {
|
|
|
381
430
|
blockedBy: ticket.blockedBy,
|
|
382
431
|
};
|
|
383
432
|
}
|
|
433
|
+
/** The Tickets of one scanned Effort, without the files behind them. */
|
|
434
|
+
function summariesOf(scanned) {
|
|
435
|
+
return scanned.entries.map(entry => entry.ticket);
|
|
436
|
+
}
|
|
384
437
|
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
438
|
+
* The named Tickets with their prose, read from disk now. The scan located the
|
|
439
|
+
* files; their bodies are read here rather than carried by every scan, so the
|
|
440
|
+
* cost is paid per Ticket worked instead of per Ticket in the workspace.
|
|
441
|
+
*
|
|
442
|
+
* A name matching nothing yields nothing — naming what was missing belongs to
|
|
443
|
+
* the caller, which is the only side that knows what it asked for.
|
|
444
|
+
*/
|
|
445
|
+
async function readBodies(storage, scanned, handles) {
|
|
446
|
+
const wanted = new Set(handles);
|
|
447
|
+
const found = scanned.flatMap(entry => entry.entries
|
|
448
|
+
.filter(({ ticket }) => wanted.has(ticket.handle) || (ticket.id !== undefined && wanted.has(ticket.id)))
|
|
449
|
+
.map(({ filename, ticket }) => ({
|
|
450
|
+
ticket,
|
|
451
|
+
path: join(storage, entry.effort.slug, ISSUES_DIR, filename),
|
|
452
|
+
})));
|
|
453
|
+
return Promise.all(found.map(async ({ ticket, path }) => withBody(ticket, ticketBody(await readText(path)))));
|
|
454
|
+
}
|
|
455
|
+
function withBody(ticket, body) {
|
|
456
|
+
return { ...ticket, body };
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Every Ticket in one Effort with the file behind it, in sort order, each read
|
|
460
|
+
* for its fields and not for its prose. Paths never cross the driver interface,
|
|
461
|
+
* so this pairing stays inside the driver — it is how a write finds the file a
|
|
462
|
+
* Ticket came from, and how a body fetch finds the one it must open.
|
|
388
463
|
*/
|
|
389
464
|
async function readTicketFiles(dir, effort) {
|
|
390
465
|
const issues = join(dir, ISSUES_DIR);
|
|
@@ -403,15 +478,11 @@ async function readTicketFiles(dir, effort) {
|
|
|
403
478
|
// sorted listing is the only order it has.
|
|
404
479
|
return {
|
|
405
480
|
filename,
|
|
406
|
-
ticket:
|
|
481
|
+
ticket: parseTicketSummary(effort, { filename, contents }, position + 1, revision ?? ''),
|
|
407
482
|
};
|
|
408
483
|
}));
|
|
409
484
|
return withUniqueHandles(parsed.toSorted((a, b) => a.ticket.order - b.ticket.order));
|
|
410
485
|
}
|
|
411
|
-
/** Every Ticket in one Effort, in sort order. A missing `issues/` yields none. */
|
|
412
|
-
async function readTickets(dir, effort) {
|
|
413
|
-
return (await readTicketFiles(dir, effort)).map(entry => entry.ticket);
|
|
414
|
-
}
|
|
415
486
|
/**
|
|
416
487
|
* Two files can share an `NN-` prefix, which would give two id-less Tickets the
|
|
417
488
|
* same `<effort>#<order>` handle and make one of them unfetchable. Later
|
|
@@ -434,29 +505,29 @@ function withUniqueHandles(entries) {
|
|
|
434
505
|
});
|
|
435
506
|
}
|
|
436
507
|
/**
|
|
437
|
-
* A directory under
|
|
438
|
-
* schema recognizes — a Header doc or an `issues/` directory.
|
|
439
|
-
* also where these repos keep loose scratch output, and a
|
|
440
|
-
* notes is not an empty Effort.
|
|
508
|
+
* A directory under the storage directory is an Effort only once it holds
|
|
509
|
+
* something the schema recognizes — a Header doc or an `issues/` directory.
|
|
510
|
+
* That directory is also where these repos keep loose scratch output, and a
|
|
511
|
+
* directory of research notes is not an empty Effort.
|
|
441
512
|
*/
|
|
442
|
-
async function readEffort(
|
|
443
|
-
const dir = join(
|
|
444
|
-
const
|
|
445
|
-
const files = new Set(
|
|
513
|
+
async function readEffort(storage, slug) {
|
|
514
|
+
const dir = join(storage, slug);
|
|
515
|
+
const listing = await readDir(dir);
|
|
516
|
+
const files = new Set(listing.filter(entry => entry.isFile()).map(entry => entry.name));
|
|
446
517
|
const headerDocs = HEADER_DOCS.filter(([, filename]) => files.has(filename)).map(([doc]) => doc);
|
|
447
|
-
const hasIssues =
|
|
518
|
+
const hasIssues = listing.some(entry => entry.isDirectory() && entry.name === ISSUES_DIR);
|
|
448
519
|
if (headerDocs.length === 0 && !hasIssues)
|
|
449
520
|
return undefined;
|
|
450
|
-
const [
|
|
451
|
-
|
|
521
|
+
const [entries, destination, specOpening] = await Promise.all([
|
|
522
|
+
readTicketFiles(dir, slug),
|
|
452
523
|
headerDocs.includes('map')
|
|
453
524
|
? readText(join(dir, 'map.md')).then(contents => readSection(contents, SECTION_HEADINGS.destination))
|
|
454
525
|
: undefined,
|
|
455
526
|
headerDocs.includes('spec') ? readText(join(dir, 'spec.md')).then(readSpecOpening) : undefined,
|
|
456
527
|
]);
|
|
457
528
|
return {
|
|
458
|
-
effort: { slug, headerDocs, ticketCount:
|
|
459
|
-
|
|
529
|
+
effort: { slug, headerDocs, ticketCount: entries.length, destination, specOpening },
|
|
530
|
+
entries,
|
|
460
531
|
};
|
|
461
532
|
}
|
|
462
533
|
/** A file that vanished between the scan and the read is read as empty, not as a failure. */
|