@nickmeriano/task 0.10.0 → 0.12.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 +232 -1
- package/dist/asks.test.js +6 -0
- package/dist/asks.test.js.map +1 -1
- package/dist/board/github.d.ts +68 -0
- package/dist/board/github.d.ts.map +1 -0
- package/dist/board/github.js +112 -0
- package/dist/board/github.js.map +1 -0
- package/dist/board/handler.d.ts +50 -0
- package/dist/board/handler.d.ts.map +1 -0
- package/dist/board/handler.js +183 -0
- package/dist/board/handler.js.map +1 -0
- package/dist/board/handler.test.d.ts +11 -0
- package/dist/board/handler.test.d.ts.map +1 -0
- package/dist/board/handler.test.js +229 -0
- package/dist/board/handler.test.js.map +1 -0
- package/dist/board/pages-function.d.ts +23 -0
- package/dist/board/pages-function.d.ts.map +1 -0
- package/dist/board/pages-function.js +34 -0
- package/dist/board/pages-function.js.map +1 -0
- package/dist/board/source.d.ts +118 -0
- package/dist/board/source.d.ts.map +1 -0
- package/dist/board/source.js +333 -0
- package/dist/board/source.js.map +1 -0
- package/dist/board/source.test.d.ts +12 -0
- package/dist/board/source.test.d.ts.map +1 -0
- package/dist/board/source.test.js +165 -0
- package/dist/board/source.test.js.map +1 -0
- package/dist/board/tar.d.ts +28 -0
- package/dist/board/tar.d.ts.map +1 -0
- package/dist/board/tar.js +188 -0
- package/dist/board/tar.js.map +1 -0
- package/dist/board/tar.test.d.ts +9 -0
- package/dist/board/tar.test.d.ts.map +1 -0
- package/dist/board/tar.test.js +110 -0
- package/dist/board/tar.test.js.map +1 -0
- package/dist/claim-io.d.ts.map +1 -1
- package/dist/claim-io.js +4 -3
- package/dist/claim-io.js.map +1 -1
- package/dist/claim.d.ts +14 -2
- package/dist/claim.d.ts.map +1 -1
- package/dist/claim.js +9 -3
- package/dist/claim.js.map +1 -1
- package/dist/claim.test.js +41 -1
- package/dist/claim.test.js.map +1 -1
- package/dist/cli.js +127 -7
- package/dist/cli.js.map +1 -1
- package/dist/export.d.ts +163 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +259 -0
- package/dist/export.js.map +1 -0
- package/dist/export.test.d.ts +12 -0
- package/dist/export.test.d.ts.map +1 -0
- package/dist/export.test.js +264 -0
- package/dist/export.test.js.map +1 -0
- package/dist/functions/board.js +709 -0
- package/dist/git-serve.d.ts +14 -1
- package/dist/git-serve.d.ts.map +1 -1
- package/dist/git-serve.js +30 -2
- package/dist/git-serve.js.map +1 -1
- package/dist/git-serve.test.d.ts +1 -0
- package/dist/git-serve.test.d.ts.map +1 -1
- package/dist/git-serve.test.js +36 -1
- package/dist/git-serve.test.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +12 -3
- package/dist/server.js.map +1 -1
- package/dist/trailers.d.ts +51 -0
- package/dist/trailers.d.ts.map +1 -0
- package/dist/trailers.js +32 -0
- package/dist/trailers.js.map +1 -0
- package/package.json +3 -3
- package/skill/SKILL.md +16 -1
- package/src/asks.test.ts +15 -0
- package/src/board/github.ts +151 -0
- package/src/board/handler.test.ts +276 -0
- package/src/board/handler.ts +228 -0
- package/src/board/pages-function.ts +42 -0
- package/src/board/source.test.ts +203 -0
- package/src/board/source.ts +422 -0
- package/src/board/tar.test.ts +128 -0
- package/src/board/tar.ts +199 -0
- package/src/claim-io.ts +12 -3
- package/src/claim.test.ts +56 -1
- package/src/claim.ts +27 -4
- package/src/cli.ts +127 -7
- package/src/export.test.ts +361 -0
- package/src/export.ts +354 -0
- package/src/git-serve.test.ts +41 -1
- package/src/git-serve.ts +30 -2
- package/src/index.ts +10 -0
- package/src/server.ts +12 -3
- package/src/trailers.ts +65 -0
- package/ui/dist/assets/index-B8M_DaOt.js +229 -0
- package/ui/dist/assets/{index-CoKCUYic.css → index-eHsqltgs.css} +1 -1
- package/ui/dist/index.html +3 -3
- package/ui/dist/assets/index-BjsorZOU.js +0 -229
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A repository, read as boards.
|
|
3
|
+
*
|
|
4
|
+
* This is `findBoards` + `FileStore`, re-expressed against a git tree instead
|
|
5
|
+
* of a filesystem, and producing byte-identical API payloads. That
|
|
6
|
+
* equivalence is the whole point: the hosted board and an exported board's
|
|
7
|
+
* live proxy run the *same* SPA as `task serve`, so anything that differs
|
|
8
|
+
* here shows up as a broken screen rather than a type error. The shapes below
|
|
9
|
+
* are pinned to `server.ts` — change one and change both — and the ticket
|
|
10
|
+
* format itself is not restated: parsing comes from `ticket-doc.ts`, the one
|
|
11
|
+
* definition every server shares, in its lenient mode (a hand-edited file
|
|
12
|
+
* that no longer parses becomes a degraded ticket or a skipped comment, never
|
|
13
|
+
* a broken board).
|
|
14
|
+
*
|
|
15
|
+
* Read-only by construction. There is no code here that could write, which is
|
|
16
|
+
* how "the board can't edit your repo" stays true no matter what the routing
|
|
17
|
+
* layer does.
|
|
18
|
+
*
|
|
19
|
+
* Cache-free on purpose. A snapshot is immutable per commit, so *where* to
|
|
20
|
+
* keep one is a deployment question — the hosted worker layers isolate memory
|
|
21
|
+
* and KV over `snapshotAt`, the live proxy keeps a bounded `SnapshotMemory` —
|
|
22
|
+
* and this module answers only "what is at this commit".
|
|
23
|
+
*/
|
|
24
|
+
import type { Ask, Comment, Goal, Task as StoredTask } from "../types.ts";
|
|
25
|
+
import { type CommitInfo } from "./github.ts";
|
|
26
|
+
export { STATUSES, type Status } from "../types.ts";
|
|
27
|
+
export type { Ask, Comment, Goal };
|
|
28
|
+
export interface BoardInfo {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
prefix: string;
|
|
32
|
+
}
|
|
33
|
+
/** A stored task plus the count `GET /tasks` sends — exactly what `task serve` lists. */
|
|
34
|
+
export interface Task extends StoredTask {
|
|
35
|
+
commentCount: number;
|
|
36
|
+
}
|
|
37
|
+
export interface BoardData {
|
|
38
|
+
info: BoardInfo;
|
|
39
|
+
tasks: Task[];
|
|
40
|
+
/** Keyed by task key. */
|
|
41
|
+
comments: Map<string, Comment[]>;
|
|
42
|
+
/** Live goals first, archived flagged — the shape `GET /goals` serves. */
|
|
43
|
+
goals: Goal[];
|
|
44
|
+
}
|
|
45
|
+
export interface Snapshot {
|
|
46
|
+
commit: CommitInfo;
|
|
47
|
+
boards: BoardInfo[];
|
|
48
|
+
data: Map<string, BoardData>;
|
|
49
|
+
/**
|
|
50
|
+
* Historically "the tree listing hit GitHub's cap". The tarball read is
|
|
51
|
+
* always complete, so this is now always false — kept because the SPA's
|
|
52
|
+
* payloads carry it.
|
|
53
|
+
*/
|
|
54
|
+
truncated: boolean;
|
|
55
|
+
}
|
|
56
|
+
export declare class BoardSourceError extends Error {
|
|
57
|
+
readonly status: number;
|
|
58
|
+
/**
|
|
59
|
+
* Machine-readable cause, sent beside the message so the SPA can choose a
|
|
60
|
+
* screen rather than infer one from a status code. Optional: most of these
|
|
61
|
+
* are one of a kind and the message is the whole story.
|
|
62
|
+
*/
|
|
63
|
+
readonly reason: string | null;
|
|
64
|
+
constructor(status: number, message: string, reason?: string | null);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Board directories, from a flat list of repo paths.
|
|
68
|
+
*
|
|
69
|
+
* The CLI finds a board by the presence of `.task/config.json`; so does this.
|
|
70
|
+
* Depth is capped the same way, and the ordering — "." first, then
|
|
71
|
+
* lexicographic — is copied so the default board is the same one you'd get
|
|
72
|
+
* locally.
|
|
73
|
+
*/
|
|
74
|
+
export declare function findBoardDirs(paths: string[]): string[];
|
|
75
|
+
/**
|
|
76
|
+
* One board from its ticket files. `files` is keyed by path relative to the
|
|
77
|
+
* board's `.task/` — `tickets/12/ticket.md`, `tickets/12/comments/<stem>.md` —
|
|
78
|
+
* which is how `snapshotAt` fetches them and how the test feeds them from
|
|
79
|
+
* disk.
|
|
80
|
+
*/
|
|
81
|
+
export declare function readTicketsBoard(id: string, configBytes: Uint8Array, files: Map<string, Uint8Array>): BoardData;
|
|
82
|
+
/**
|
|
83
|
+
* Every board in a repository at a known commit, ready to serve.
|
|
84
|
+
*
|
|
85
|
+
* One request for the whole repository: every path, plus bytes for anything
|
|
86
|
+
* under a .task/ directory. (Earlier shapes — a tree listing plus per-file or
|
|
87
|
+
* batched blob reads — scaled subrequests with board size and tripped
|
|
88
|
+
* Workers' 50-subrequest ceiling; a tarball is one request regardless.)
|
|
89
|
+
*
|
|
90
|
+
* `ref` is only for the error message: told a branch, "this repository has
|
|
91
|
+
* no .task directory" is both wrong and confusing — the repository plainly
|
|
92
|
+
* has one, you were just looking at it — and `task init` is not the fix for
|
|
93
|
+
* previewing a branch that forked before the boards existed.
|
|
94
|
+
*/
|
|
95
|
+
export declare function snapshotAt(token: string, owner: string, repo: string, commit: CommitInfo, ref?: string): Promise<Snapshot>;
|
|
96
|
+
/**
|
|
97
|
+
* Every board at `ref`, cache-free: "what is HEAD" plus one tarball. Callers
|
|
98
|
+
* that serve more than one view put a cache between the two — see
|
|
99
|
+
* `SnapshotMemory` — because the second request is the expensive one and a
|
|
100
|
+
* commit never changes.
|
|
101
|
+
*/
|
|
102
|
+
export declare function loadSnapshot(token: string, owner: string, repo: string, ref: string): Promise<Snapshot>;
|
|
103
|
+
/**
|
|
104
|
+
* Parsed snapshots, keyed by `owner/repo@sha`, bounded.
|
|
105
|
+
*
|
|
106
|
+
* A commit is immutable, so this never serves anything stale: the only lookup
|
|
107
|
+
* that goes to GitHub on a warm instance is "what is HEAD right now", and a
|
|
108
|
+
* new commit is a new key. Bounded because an isolate's memory is not ours to
|
|
109
|
+
* fill; `Map` iterates in insertion order, so the first key is the oldest.
|
|
110
|
+
*/
|
|
111
|
+
export declare class SnapshotMemory {
|
|
112
|
+
private readonly snapshots;
|
|
113
|
+
private readonly max;
|
|
114
|
+
constructor(max?: number);
|
|
115
|
+
get(key: string): Snapshot | undefined;
|
|
116
|
+
remember(key: string, snapshot: Snapshot): Snapshot;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../src/board/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAUH,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,UAAU,EAAE,MAAM,aAAa,CAAA;AACzE,OAAO,EAAgC,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAE3E,OAAO,EAAE,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAA;AACnD,YAAY,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAalC,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf;AAED,yFAAyF;AACzF,MAAM,WAAW,IAAK,SAAQ,UAAU;IACtC,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,SAAS,CAAA;IACf,KAAK,EAAE,IAAI,EAAE,CAAA;IACb,yBAAyB;IACzB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAA;IAChC,0EAA0E;IAC1E,KAAK,EAAE,IAAI,EAAE,CAAA;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,UAAU,CAAA;IAClB,MAAM,EAAE,SAAS,EAAE,CAAA;IACnB,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAC5B;;;;OAIG;IACH,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;gBAClB,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,GAAG,IAAW;CAM1E;AAkCD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAcvD;AAQD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,EACV,WAAW,EAAE,UAAU,EACvB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,GAC7B,SAAS,CAkIX;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,UAAU,CAC9B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,UAAU,EAClB,GAAG,SAAS,GACX,OAAO,CAAC,QAAQ,CAAC,CAsDnB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,QAAQ,CAAC,CAGnB;AAED;;;;;;;GAOG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8B;IACxD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;gBAEhB,GAAG,SAAK;IAIpB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAItC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,QAAQ;CASpD"}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A repository, read as boards.
|
|
3
|
+
*
|
|
4
|
+
* This is `findBoards` + `FileStore`, re-expressed against a git tree instead
|
|
5
|
+
* of a filesystem, and producing byte-identical API payloads. That
|
|
6
|
+
* equivalence is the whole point: the hosted board and an exported board's
|
|
7
|
+
* live proxy run the *same* SPA as `task serve`, so anything that differs
|
|
8
|
+
* here shows up as a broken screen rather than a type error. The shapes below
|
|
9
|
+
* are pinned to `server.ts` — change one and change both — and the ticket
|
|
10
|
+
* format itself is not restated: parsing comes from `ticket-doc.ts`, the one
|
|
11
|
+
* definition every server shares, in its lenient mode (a hand-edited file
|
|
12
|
+
* that no longer parses becomes a degraded ticket or a skipped comment, never
|
|
13
|
+
* a broken board).
|
|
14
|
+
*
|
|
15
|
+
* Read-only by construction. There is no code here that could write, which is
|
|
16
|
+
* how "the board can't edit your repo" stays true no matter what the routing
|
|
17
|
+
* layer does.
|
|
18
|
+
*
|
|
19
|
+
* Cache-free on purpose. A snapshot is immutable per commit, so *where* to
|
|
20
|
+
* keep one is a deployment question — the hosted worker layers isolate memory
|
|
21
|
+
* and KV over `snapshotAt`, the live proxy keeps a bounded `SnapshotMemory` —
|
|
22
|
+
* and this module answers only "what is at this commit".
|
|
23
|
+
*/
|
|
24
|
+
import { parseAskLenient, parseCommentLenient, parseGoalLenient, parseTicketLenient, } from "../ticket-doc.js";
|
|
25
|
+
import { isTicketKey } from "../id.js";
|
|
26
|
+
import { fetchRepoArchive, headCommit } from "./github.js";
|
|
27
|
+
export { STATUSES } from "../types.js";
|
|
28
|
+
const TASK_DIR = ".task";
|
|
29
|
+
const CONFIG_FILE = "config.json";
|
|
30
|
+
const TICKETS_DIR = "tickets";
|
|
31
|
+
const TICKET_FILE = "ticket.md";
|
|
32
|
+
const COMMENTS_DIR = "comments";
|
|
33
|
+
const ASKS_DIR = "asks";
|
|
34
|
+
const GOALS_DIR = "goals";
|
|
35
|
+
/** Same ceiling as the CLI's `findBoards`, for the same reason. */
|
|
36
|
+
const MAX_DEPTH = 6;
|
|
37
|
+
export class BoardSourceError extends Error {
|
|
38
|
+
status;
|
|
39
|
+
/**
|
|
40
|
+
* Machine-readable cause, sent beside the message so the SPA can choose a
|
|
41
|
+
* screen rather than infer one from a status code. Optional: most of these
|
|
42
|
+
* are one of a kind and the message is the whole story.
|
|
43
|
+
*/
|
|
44
|
+
reason;
|
|
45
|
+
constructor(status, message, reason = null) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "BoardSourceError";
|
|
48
|
+
this.status = status;
|
|
49
|
+
this.reason = reason;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function str(value, fallback = "") {
|
|
53
|
+
return typeof value === "string" ? value : fallback;
|
|
54
|
+
}
|
|
55
|
+
function num(value, fallback = 0) {
|
|
56
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
57
|
+
}
|
|
58
|
+
/** `.task/config.json`, defensively — it's a file anyone can hand-edit. */
|
|
59
|
+
function parseConfig(bytes, fallbackName) {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
62
|
+
if (parsed && typeof parsed === "object") {
|
|
63
|
+
const config = parsed;
|
|
64
|
+
return {
|
|
65
|
+
name: str(config.name, fallbackName),
|
|
66
|
+
prefix: str(config.prefix, "TASK").toUpperCase(),
|
|
67
|
+
version: num(config.version, 1),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// fall through
|
|
73
|
+
}
|
|
74
|
+
return { name: fallbackName, prefix: "TASK", version: 1 };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Board directories, from a flat list of repo paths.
|
|
78
|
+
*
|
|
79
|
+
* The CLI finds a board by the presence of `.task/config.json`; so does this.
|
|
80
|
+
* Depth is capped the same way, and the ordering — "." first, then
|
|
81
|
+
* lexicographic — is copied so the default board is the same one you'd get
|
|
82
|
+
* locally.
|
|
83
|
+
*/
|
|
84
|
+
export function findBoardDirs(paths) {
|
|
85
|
+
const marker = `${TASK_DIR}/${CONFIG_FILE}`;
|
|
86
|
+
const dirs = [];
|
|
87
|
+
for (const path of paths) {
|
|
88
|
+
// The directory has to be `.task` itself, not merely end in it — a bare
|
|
89
|
+
// suffix match would read `x.task/config.json` as a board at `x`.
|
|
90
|
+
if (path !== marker && !path.endsWith(`/${marker}`))
|
|
91
|
+
continue;
|
|
92
|
+
const dir = path.slice(0, -(TASK_DIR.length + CONFIG_FILE.length + 2));
|
|
93
|
+
const id = dir === "" ? "." : dir.replace(/\/$/, "");
|
|
94
|
+
if (id !== "." && id.split("/").length > MAX_DEPTH)
|
|
95
|
+
continue;
|
|
96
|
+
dirs.push(id);
|
|
97
|
+
}
|
|
98
|
+
dirs.sort((a, b) => (a === "." ? -1 : b === "." ? 1 : a < b ? -1 : 1));
|
|
99
|
+
return dirs;
|
|
100
|
+
}
|
|
101
|
+
/** `projects/phone/web` → "phone web"-ish fallback name, matching `task init`. */
|
|
102
|
+
function nameFromDir(id) {
|
|
103
|
+
if (id === ".")
|
|
104
|
+
return "tasks";
|
|
105
|
+
return id.split("/").pop() ?? "tasks";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* One board from its ticket files. `files` is keyed by path relative to the
|
|
109
|
+
* board's `.task/` — `tickets/12/ticket.md`, `tickets/12/comments/<stem>.md` —
|
|
110
|
+
* which is how `snapshotAt` fetches them and how the test feeds them from
|
|
111
|
+
* disk.
|
|
112
|
+
*/
|
|
113
|
+
export function readTicketsBoard(id, configBytes, files) {
|
|
114
|
+
const config = parseConfig(configBytes, nameFromDir(id));
|
|
115
|
+
const decoder = new TextDecoder();
|
|
116
|
+
const docs = new Map();
|
|
117
|
+
const comments = new Map();
|
|
118
|
+
const asks = new Map();
|
|
119
|
+
const goals = [];
|
|
120
|
+
for (const [path, bytes] of files) {
|
|
121
|
+
// Goals: one file per goal, archived ones one level down. Old branches
|
|
122
|
+
// simply have no goals/ — the list stays empty and every reader tolerates
|
|
123
|
+
// a ticket's goal ref not resolving.
|
|
124
|
+
const goalMatch = new RegExp(`^${GOALS_DIR}/(archive/)?([^/]+)\\.md$`).exec(path);
|
|
125
|
+
if (goalMatch) {
|
|
126
|
+
if (goalMatch[2].startsWith("."))
|
|
127
|
+
continue;
|
|
128
|
+
const doc = parseGoalLenient(decoder.decode(bytes));
|
|
129
|
+
if (doc) {
|
|
130
|
+
goals.push({
|
|
131
|
+
slug: goalMatch[2],
|
|
132
|
+
title: doc.title,
|
|
133
|
+
description: doc.description,
|
|
134
|
+
createdAt: doc.createdAt,
|
|
135
|
+
updatedAt: doc.updatedAt,
|
|
136
|
+
...(goalMatch[1] ? { archived: true } : {}),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const ticketMatch = new RegExp(`^${TICKETS_DIR}/([^/]+)/${TICKET_FILE}$`).exec(path);
|
|
142
|
+
if (ticketMatch && isTicketKey(ticketMatch[1])) {
|
|
143
|
+
const doc = parseTicketLenient(decoder.decode(bytes));
|
|
144
|
+
if (doc)
|
|
145
|
+
docs.set(ticketMatch[1], doc);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const commentMatch = new RegExp(`^${TICKETS_DIR}/([^/]+)/${COMMENTS_DIR}/([^/]+)\\.md$`).exec(path);
|
|
149
|
+
if (commentMatch && isTicketKey(commentMatch[1]) && !commentMatch[2].startsWith(".")) {
|
|
150
|
+
const doc = parseCommentLenient(decoder.decode(bytes));
|
|
151
|
+
if (!doc)
|
|
152
|
+
continue;
|
|
153
|
+
const taskKey = commentMatch[1];
|
|
154
|
+
const list = comments.get(taskKey) ?? [];
|
|
155
|
+
list.push({
|
|
156
|
+
id: commentMatch[2],
|
|
157
|
+
taskId: `${config.prefix}-${taskKey}`,
|
|
158
|
+
author: doc.author,
|
|
159
|
+
body: doc.body,
|
|
160
|
+
createdAt: doc.createdAt,
|
|
161
|
+
});
|
|
162
|
+
comments.set(taskKey, list);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const askMatch = new RegExp(`^${TICKETS_DIR}/([^/]+)/${ASKS_DIR}/([^/]+)\\.md$`).exec(path);
|
|
166
|
+
if (askMatch && isTicketKey(askMatch[1]) && !askMatch[2].startsWith(".")) {
|
|
167
|
+
const doc = parseAskLenient(decoder.decode(bytes));
|
|
168
|
+
if (!doc)
|
|
169
|
+
continue;
|
|
170
|
+
const taskKey = askMatch[1];
|
|
171
|
+
const list = asks.get(taskKey) ?? [];
|
|
172
|
+
// Ordinal assigned after the sort below — 0 is a placeholder.
|
|
173
|
+
list.push({
|
|
174
|
+
id: askMatch[2],
|
|
175
|
+
taskId: `${config.prefix}-${taskKey}`,
|
|
176
|
+
ordinal: 0,
|
|
177
|
+
text: doc.body,
|
|
178
|
+
author: doc.author,
|
|
179
|
+
createdAt: doc.createdAt,
|
|
180
|
+
resolvedAt: doc.resolvedAt,
|
|
181
|
+
resolvedBy: doc.resolvedBy,
|
|
182
|
+
});
|
|
183
|
+
asks.set(taskKey, list);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Comments whose ticket file is missing or unreadable would count against a
|
|
187
|
+
// ticket that isn't drawn — drop them, and order the rest the CLI's way.
|
|
188
|
+
for (const taskKey of comments.keys()) {
|
|
189
|
+
if (!docs.has(taskKey))
|
|
190
|
+
comments.delete(taskKey);
|
|
191
|
+
}
|
|
192
|
+
for (const list of comments.values()) {
|
|
193
|
+
list.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
194
|
+
}
|
|
195
|
+
// Asks: same orphan rule, and ordinals are creation-order positions — the
|
|
196
|
+
// exact numbering the CLI's `asks()` derives.
|
|
197
|
+
for (const taskKey of asks.keys()) {
|
|
198
|
+
if (!docs.has(taskKey))
|
|
199
|
+
asks.delete(taskKey);
|
|
200
|
+
}
|
|
201
|
+
for (const list of asks.values()) {
|
|
202
|
+
list.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
203
|
+
list.forEach((ask, index) => (ask.ordinal = index + 1));
|
|
204
|
+
}
|
|
205
|
+
// `blocks` is derived from the other tickets' stored `blocked_by` — the
|
|
206
|
+
// relation lives on one side only, so the two views can't disagree.
|
|
207
|
+
const blocks = new Map();
|
|
208
|
+
for (const [key, doc] of docs) {
|
|
209
|
+
for (const blocker of doc.blockedBy) {
|
|
210
|
+
blocks.set(blocker, [...(blocks.get(blocker) ?? []), key]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const list of blocks.values())
|
|
214
|
+
list.sort((a, b) => a.localeCompare(b));
|
|
215
|
+
const tasks = [...docs.entries()]
|
|
216
|
+
.map(([key, doc]) => ({
|
|
217
|
+
id: `${config.prefix}-${key}`,
|
|
218
|
+
key,
|
|
219
|
+
title: doc.title,
|
|
220
|
+
description: doc.description,
|
|
221
|
+
status: doc.status,
|
|
222
|
+
tags: doc.tags,
|
|
223
|
+
goal: doc.goal,
|
|
224
|
+
needsHuman: (asks.get(key) ?? []).some((a) => !a.resolvedAt),
|
|
225
|
+
asks: asks.get(key) ?? [],
|
|
226
|
+
blocks: blocks.get(key) ?? [],
|
|
227
|
+
blockedBy: doc.blockedBy,
|
|
228
|
+
prs: doc.prs,
|
|
229
|
+
position: doc.position,
|
|
230
|
+
createdAt: doc.createdAt,
|
|
231
|
+
updatedAt: doc.updatedAt,
|
|
232
|
+
commentCount: comments.get(key)?.length ?? 0,
|
|
233
|
+
}))
|
|
234
|
+
.sort((a, b) => a.position - b.position ||
|
|
235
|
+
a.createdAt.localeCompare(b.createdAt) ||
|
|
236
|
+
a.key.localeCompare(b.key));
|
|
237
|
+
goals.sort((a, b) => Number(a.archived === true) - Number(b.archived === true) || a.slug.localeCompare(b.slug));
|
|
238
|
+
return { info: { id, name: config.name, prefix: config.prefix }, tasks, comments, goals };
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Every board in a repository at a known commit, ready to serve.
|
|
242
|
+
*
|
|
243
|
+
* One request for the whole repository: every path, plus bytes for anything
|
|
244
|
+
* under a .task/ directory. (Earlier shapes — a tree listing plus per-file or
|
|
245
|
+
* batched blob reads — scaled subrequests with board size and tripped
|
|
246
|
+
* Workers' 50-subrequest ceiling; a tarball is one request regardless.)
|
|
247
|
+
*
|
|
248
|
+
* `ref` is only for the error message: told a branch, "this repository has
|
|
249
|
+
* no .task directory" is both wrong and confusing — the repository plainly
|
|
250
|
+
* has one, you were just looking at it — and `task init` is not the fix for
|
|
251
|
+
* previewing a branch that forked before the boards existed.
|
|
252
|
+
*/
|
|
253
|
+
export async function snapshotAt(token, owner, repo, commit, ref = "HEAD") {
|
|
254
|
+
const { paths, files } = await fetchRepoArchive(token, owner, repo, commit.sha, (path) => path.startsWith(`${TASK_DIR}/`) || path.includes(`/${TASK_DIR}/`));
|
|
255
|
+
const dirs = findBoardDirs(paths);
|
|
256
|
+
if (dirs.length === 0) {
|
|
257
|
+
throw new BoardSourceError(404, ref === "HEAD"
|
|
258
|
+
? "no .task directory in this repository — run `task init` and commit it"
|
|
259
|
+
: `no .task directory on ${ref}`, "no-boards");
|
|
260
|
+
}
|
|
261
|
+
const data = new Map();
|
|
262
|
+
for (const id of dirs) {
|
|
263
|
+
const base = id === "." ? TASK_DIR : `${id}/${TASK_DIR}`;
|
|
264
|
+
const configBytes = files.get(`${base}/${CONFIG_FILE}`);
|
|
265
|
+
if (!configBytes)
|
|
266
|
+
continue;
|
|
267
|
+
const prefix = `${base}/`;
|
|
268
|
+
const ticketPaths = paths.filter((path) => (path.startsWith(`${prefix}${TICKETS_DIR}/`) || path.startsWith(`${prefix}${GOALS_DIR}/`)) &&
|
|
269
|
+
path.endsWith(".md"));
|
|
270
|
+
// A config with no committed tickets can be two very different things: a
|
|
271
|
+
// real (empty) text board, or a pre-0.6 board whose state is a SQLite
|
|
272
|
+
// database this app no longer reads. The config's version says which —
|
|
273
|
+
// skip the legacy one rather than render an empty board that lies.
|
|
274
|
+
if (ticketPaths.length === 0 && parseConfig(configBytes, "").version < 2)
|
|
275
|
+
continue;
|
|
276
|
+
const tickets = new Map();
|
|
277
|
+
for (const path of ticketPaths) {
|
|
278
|
+
const bytes = files.get(path);
|
|
279
|
+
if (bytes)
|
|
280
|
+
tickets.set(path.slice(prefix.length), bytes);
|
|
281
|
+
}
|
|
282
|
+
const board = readTicketsBoard(id, configBytes, tickets);
|
|
283
|
+
data.set(board.info.id, board);
|
|
284
|
+
}
|
|
285
|
+
if (data.size === 0) {
|
|
286
|
+
throw new BoardSourceError(404, "found .task/config.json but no committed tickets — are .task/tickets/ gitignored, or is this a pre-0.6 SQLite board that still needs `task migrate` (@nickmeriano/task@0.6)?");
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
commit,
|
|
290
|
+
boards: [...data.values()].map((board) => board.info),
|
|
291
|
+
data,
|
|
292
|
+
truncated: false,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Every board at `ref`, cache-free: "what is HEAD" plus one tarball. Callers
|
|
297
|
+
* that serve more than one view put a cache between the two — see
|
|
298
|
+
* `SnapshotMemory` — because the second request is the expensive one and a
|
|
299
|
+
* commit never changes.
|
|
300
|
+
*/
|
|
301
|
+
export async function loadSnapshot(token, owner, repo, ref) {
|
|
302
|
+
const commit = await headCommit(token, owner, repo, ref);
|
|
303
|
+
return snapshotAt(token, owner, repo, commit, ref);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Parsed snapshots, keyed by `owner/repo@sha`, bounded.
|
|
307
|
+
*
|
|
308
|
+
* A commit is immutable, so this never serves anything stale: the only lookup
|
|
309
|
+
* that goes to GitHub on a warm instance is "what is HEAD right now", and a
|
|
310
|
+
* new commit is a new key. Bounded because an isolate's memory is not ours to
|
|
311
|
+
* fill; `Map` iterates in insertion order, so the first key is the oldest.
|
|
312
|
+
*/
|
|
313
|
+
export class SnapshotMemory {
|
|
314
|
+
snapshots = new Map();
|
|
315
|
+
max;
|
|
316
|
+
constructor(max = 24) {
|
|
317
|
+
this.max = max;
|
|
318
|
+
}
|
|
319
|
+
get(key) {
|
|
320
|
+
return this.snapshots.get(key);
|
|
321
|
+
}
|
|
322
|
+
remember(key, snapshot) {
|
|
323
|
+
this.snapshots.set(key, snapshot);
|
|
324
|
+
while (this.snapshots.size > this.max) {
|
|
325
|
+
const oldest = this.snapshots.keys().next().value;
|
|
326
|
+
if (oldest === undefined)
|
|
327
|
+
break;
|
|
328
|
+
this.snapshots.delete(oldest);
|
|
329
|
+
}
|
|
330
|
+
return snapshot;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
//# sourceMappingURL=source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/board/source.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hosted board and an exported board's live proxy serve the same SPA as
|
|
3
|
+
* `task serve`, so their payloads have to be the same payloads — not
|
|
4
|
+
* "compatible", the same. This diffs them against the CLI's own `FileStore`.
|
|
5
|
+
*
|
|
6
|
+
* That is the test that actually protects the design: if someone adds a field
|
|
7
|
+
* to the CLI's `toTask` and not here, the hosted board silently loses it, and
|
|
8
|
+
* the only symptom is a piece of the UI that renders blank for one deployment
|
|
9
|
+
* and not the other.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=source.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.test.d.ts","sourceRoot":"","sources":["../../src/board/source.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hosted board and an exported board's live proxy serve the same SPA as
|
|
3
|
+
* `task serve`, so their payloads have to be the same payloads — not
|
|
4
|
+
* "compatible", the same. This diffs them against the CLI's own `FileStore`.
|
|
5
|
+
*
|
|
6
|
+
* That is the test that actually protects the design: if someone adds a field
|
|
7
|
+
* to the CLI's `toTask` and not here, the hosted board silently loses it, and
|
|
8
|
+
* the only symptom is a piece of the UI that renders blank for one deployment
|
|
9
|
+
* and not the other.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { test } from "node:test";
|
|
17
|
+
import { FileStore, initProject } from "../file-store.js";
|
|
18
|
+
import { findBoardDirs, readTicketsBoard } from "./source.js";
|
|
19
|
+
const REPO_ROOT = fileURLToPath(new URL("../../../../..", import.meta.url));
|
|
20
|
+
/** Every board this repo actually commits, as `findBoards` would order them. */
|
|
21
|
+
const BOARDS = [".", "projects/inspo", "projects/phone/web", "projects/portal", "projects/task/cli"];
|
|
22
|
+
function tempRoot() {
|
|
23
|
+
const dir = mkdtempSync(join(tmpdir(), "board-source-test-"));
|
|
24
|
+
process.on("exit", () => rmSync(dir, { recursive: true, force: true }));
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
/** A board's `.task/` read off disk into the path→bytes map the worker sees. */
|
|
28
|
+
function ticketFiles(taskDir) {
|
|
29
|
+
const files = new Map();
|
|
30
|
+
for (const dir of ["goals", "goals/archive"]) {
|
|
31
|
+
const goalsDir = join(taskDir, dir);
|
|
32
|
+
if (!existsSync(goalsDir))
|
|
33
|
+
continue;
|
|
34
|
+
for (const entry of readdirSync(goalsDir, { withFileTypes: true })) {
|
|
35
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
36
|
+
continue;
|
|
37
|
+
files.set(`${dir}/${entry.name}`, new Uint8Array(readFileSync(join(goalsDir, entry.name))));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const ticketsDir = join(taskDir, "tickets");
|
|
41
|
+
if (!existsSync(ticketsDir))
|
|
42
|
+
return files;
|
|
43
|
+
for (const number of readdirSync(ticketsDir)) {
|
|
44
|
+
const ticket = join(ticketsDir, number, "ticket.md");
|
|
45
|
+
if (existsSync(ticket)) {
|
|
46
|
+
files.set(`tickets/${number}/ticket.md`, new Uint8Array(readFileSync(ticket)));
|
|
47
|
+
}
|
|
48
|
+
for (const kind of ["comments", "asks"]) {
|
|
49
|
+
const dir = join(ticketsDir, number, kind);
|
|
50
|
+
if (!existsSync(dir))
|
|
51
|
+
continue;
|
|
52
|
+
for (const entry of readdirSync(dir)) {
|
|
53
|
+
files.set(`tickets/${number}/${kind}/${entry}`, new Uint8Array(readFileSync(join(dir, entry))));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return files;
|
|
58
|
+
}
|
|
59
|
+
function loadTicketsBoard(id, root) {
|
|
60
|
+
const taskDir = join(root, ".task");
|
|
61
|
+
return readTicketsBoard(id, new Uint8Array(readFileSync(join(taskDir, "config.json"))), ticketFiles(taskDir));
|
|
62
|
+
}
|
|
63
|
+
/** `GET /api/tasks` + per-task comments, straight from the CLI store. */
|
|
64
|
+
function expectedPayloads(store) {
|
|
65
|
+
const counts = store.commentCounts();
|
|
66
|
+
const tasks = store.list().map((task) => ({
|
|
67
|
+
...task,
|
|
68
|
+
commentCount: counts.get(task.key) ?? 0,
|
|
69
|
+
}));
|
|
70
|
+
return { tasks, comments: new Map(tasks.map((t) => [t.key, store.comments(t.key)])) };
|
|
71
|
+
}
|
|
72
|
+
function assertSamePayloads(id, hosted, store) {
|
|
73
|
+
assert.equal(hosted.info.name, store.config.name, `${id}: project name`);
|
|
74
|
+
assert.equal(hosted.info.prefix, store.config.prefix, `${id}: prefix`);
|
|
75
|
+
const expected = expectedPayloads(store);
|
|
76
|
+
assert.equal(hosted.tasks.length, expected.tasks.length, `${id}: task count`);
|
|
77
|
+
for (let i = 0; i < expected.tasks.length; i++) {
|
|
78
|
+
assert.deepEqual(hosted.tasks[i], expected.tasks[i], `${id}: task ${expected.tasks[i].id}`);
|
|
79
|
+
}
|
|
80
|
+
for (const task of expected.tasks) {
|
|
81
|
+
assert.deepEqual(hosted.comments.get(task.key) ?? [], expected.comments.get(task.key), `${id}: comments on ${task.id}`);
|
|
82
|
+
}
|
|
83
|
+
assert.deepEqual(hosted.goals, [...store.goals(), ...store.goals(true)], `${id}: goals`);
|
|
84
|
+
}
|
|
85
|
+
test("finds the same boards the CLI would, in the same order", () => {
|
|
86
|
+
// The shape a recursive git-tree listing arrives in, plus the noise it
|
|
87
|
+
// really contains: nested boards, unrelated files, and a `.task` path that
|
|
88
|
+
// is a file rather than a board.
|
|
89
|
+
const paths = [
|
|
90
|
+
"README.md",
|
|
91
|
+
".task/config.json",
|
|
92
|
+
".task/tickets/1/ticket.md",
|
|
93
|
+
"projects/phone/web/.task/config.json",
|
|
94
|
+
"projects/task/cli/.task/config.json",
|
|
95
|
+
"docs/.taskrc",
|
|
96
|
+
"vendor/thing/.task/config.json.bak",
|
|
97
|
+
// A directory that merely *ends* in `.task` is not a board directory.
|
|
98
|
+
"lib/x.task/config.json",
|
|
99
|
+
];
|
|
100
|
+
assert.deepEqual(findBoardDirs(paths), [".", "projects/phone/web", "projects/task/cli"]);
|
|
101
|
+
});
|
|
102
|
+
test("ignores boards nested deeper than the CLI would walk", () => {
|
|
103
|
+
const deep = "a/b/c/d/e/f/g/.task/config.json";
|
|
104
|
+
assert.deepEqual(findBoardDirs([deep]), []);
|
|
105
|
+
assert.deepEqual(findBoardDirs(["a/b/c/.task/config.json"]), ["a/b/c"]);
|
|
106
|
+
});
|
|
107
|
+
test("text boards produce the same payloads the CLI's FileStore does", () => {
|
|
108
|
+
const root = tempRoot();
|
|
109
|
+
const store = initProject(root, { name: "fixture board", prefix: "FIX" });
|
|
110
|
+
store.createGoal({ title: "Launch", description: "the shared why" });
|
|
111
|
+
const one = store.create({
|
|
112
|
+
title: "Every field exercised",
|
|
113
|
+
description: "A *body* with\n\n# an inner heading\n\nand `code`.",
|
|
114
|
+
tags: ["api", "infra"],
|
|
115
|
+
goal: "launch",
|
|
116
|
+
prs: ["https://github.com/x/y/pull/3"],
|
|
117
|
+
});
|
|
118
|
+
store.addAsk(one.key, "Approve the launch window", "claude");
|
|
119
|
+
const two = store.create({ title: "Mover", status: "backlog" });
|
|
120
|
+
store.create({ title: "Plain" });
|
|
121
|
+
store.update(two.key, { status: "in_progress" }); // lands on top → negative position
|
|
122
|
+
store.link(two.key, "blocked_by", one.key);
|
|
123
|
+
store.addComment(one.key, "first, with **markdown**", "nick");
|
|
124
|
+
store.addComment(one.key, "second", "claude");
|
|
125
|
+
assertSamePayloads("fixture", loadTicketsBoard("fixture", root), store);
|
|
126
|
+
});
|
|
127
|
+
test("this repo's committed boards read as text, same as the CLI reads them", () => {
|
|
128
|
+
for (const id of BOARDS) {
|
|
129
|
+
const root = id === "." ? REPO_ROOT : join(REPO_ROOT, id);
|
|
130
|
+
assertSamePayloads(id, loadTicketsBoard(id, root), new FileStore(root));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
test("every committed board is non-empty and self-consistent", () => {
|
|
134
|
+
// A smoke check that the fixtures above are actually exercising something —
|
|
135
|
+
// boards that all happened to be empty would make the diff vacuous.
|
|
136
|
+
const loaded = BOARDS.map((id) => loadTicketsBoard(id, id === "." ? REPO_ROOT : join(REPO_ROOT, id)));
|
|
137
|
+
const total = loaded.reduce((sum, board) => sum + board.tasks.length, 0);
|
|
138
|
+
assert.ok(total > 10, `expected the repo's boards to hold real tasks, found ${total}`);
|
|
139
|
+
for (const board of loaded) {
|
|
140
|
+
for (const task of board.tasks) {
|
|
141
|
+
assert.match(task.id, /^[A-Z0-9]+-[a-z0-9]+$/, `${board.info.id}: display id`);
|
|
142
|
+
assert.ok(Array.isArray(task.tags), `${board.info.id}: tags parsed`);
|
|
143
|
+
assert.equal(typeof task.needsHuman, "boolean", `${board.info.id}: needsHuman`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
test("a broken ticket degrades instead of breaking the board", () => {
|
|
148
|
+
const config = new TextEncoder().encode(JSON.stringify({ name: "rough", prefix: "RGH", version: 2 }));
|
|
149
|
+
const encode = (s) => new TextEncoder().encode(s);
|
|
150
|
+
const board = readTicketsBoard("rough", config, new Map([
|
|
151
|
+
["tickets/bad2c/ticket.md", encode("---\nstatus: todoo\nposition: not-a-number\n---\n\nno heading")],
|
|
152
|
+
["tickets/gone3/ticket.md", encode("not even frontmatter")],
|
|
153
|
+
["tickets/fine4/ticket.md", encode("---\nstatus: todo\nposition: 1\ncreated: c\nupdated: u\n---\n\n# Fine\n")],
|
|
154
|
+
// A comment on a ticket that didn't parse must not surface anywhere.
|
|
155
|
+
["tickets/gone3/comments/2026-08-15T120000-x.md", encode("---\nauthor: \"x\"\ncreated: c\n---\n\nhi")],
|
|
156
|
+
// A pre-TAS-42 numeric directory is not a ticket to this reader at all.
|
|
157
|
+
["tickets/42/ticket.md", encode("---\nstatus: todo\nposition: 1\ncreated: c\nupdated: u\n---\n\n# Old format\n")],
|
|
158
|
+
]));
|
|
159
|
+
assert.deepEqual(board.tasks.map((t) => [t.key, t.status, t.title]), [
|
|
160
|
+
["bad2c", "backlog", ""],
|
|
161
|
+
["fine4", "todo", "Fine"],
|
|
162
|
+
]);
|
|
163
|
+
assert.equal(board.comments.size, 0);
|
|
164
|
+
});
|
|
165
|
+
//# sourceMappingURL=source.test.js.map
|