@shipbench/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/dist/chunk-7T2E4KSJ.js +68 -0
- package/dist/index.d.ts +252 -0
- package/dist/index.js +1763 -0
- package/dist/layout-BS-84zwS.d.ts +228 -0
- package/dist/layout.d.ts +1 -0
- package/dist/layout.js +14 -0
- package/package.json +59 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only surface. Consumers that only display data (e.g. Harbor's
|
|
3
|
+
* remote board mode) hold this type so the compiler prevents accidental
|
|
4
|
+
* write attempts.
|
|
5
|
+
*/
|
|
6
|
+
interface ReadableStorageAdapter {
|
|
7
|
+
readFile(path: string): Promise<string>;
|
|
8
|
+
readFileIfExists(path: string): Promise<string | null>;
|
|
9
|
+
listFiles(directory: string): Promise<string[]>;
|
|
10
|
+
readFiles(paths: string[]): Promise<Map<string, string>>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Full read/write surface. Used by anything that mutates the `.shipbench/`
|
|
14
|
+
* directory — the CLI and in-repo agents.
|
|
15
|
+
*/
|
|
16
|
+
interface StorageAdapter extends ReadableStorageAdapter {
|
|
17
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
18
|
+
writeFiles(files: Map<string, string>): Promise<void>;
|
|
19
|
+
deleteFile(path: string): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
interface ColumnDef {
|
|
22
|
+
id: string;
|
|
23
|
+
label: string;
|
|
24
|
+
}
|
|
25
|
+
interface PriorityConfig {
|
|
26
|
+
values: string[];
|
|
27
|
+
default: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Per-column ordered list of task slugs. Tasks present in a column's array
|
|
31
|
+
* render in that order; tasks whose slug isn't listed render below the
|
|
32
|
+
* ordered set (sub-sorted by `created` desc for stable tiebreak). The
|
|
33
|
+
* Uncategorized column ignores layout entirely.
|
|
34
|
+
*/
|
|
35
|
+
type BoardLayout = Record<string, string[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Controls how the done column is rendered. Cap is applied only when no
|
|
38
|
+
* search query is active — search always bypasses the cap so hidden matches
|
|
39
|
+
* aren't invisible.
|
|
40
|
+
*/
|
|
41
|
+
interface DoneDisplayConfig {
|
|
42
|
+
/**
|
|
43
|
+
* Max number of done tasks shown by default. `0` or negative disables the
|
|
44
|
+
* cap entirely (show all).
|
|
45
|
+
*/
|
|
46
|
+
max: number;
|
|
47
|
+
}
|
|
48
|
+
interface ShipbenchConfig {
|
|
49
|
+
version: number;
|
|
50
|
+
name: string;
|
|
51
|
+
columns: ColumnDef[];
|
|
52
|
+
default_column: string;
|
|
53
|
+
done_column: string;
|
|
54
|
+
done_display: DoneDisplayConfig;
|
|
55
|
+
priority: PriorityConfig;
|
|
56
|
+
schema: {
|
|
57
|
+
custom_fields: Record<string, unknown>;
|
|
58
|
+
};
|
|
59
|
+
layout: BoardLayout;
|
|
60
|
+
}
|
|
61
|
+
/** A non-fatal problem encountered while resolving project configuration. */
|
|
62
|
+
interface ConfigLoadWarning {
|
|
63
|
+
path: string;
|
|
64
|
+
message: string;
|
|
65
|
+
}
|
|
66
|
+
interface LoadConfigOptions {
|
|
67
|
+
/** Receives recoverable diagnostics without coupling core to an output stream. */
|
|
68
|
+
onWarning?: (warning: ConfigLoadWarning) => void;
|
|
69
|
+
}
|
|
70
|
+
interface TaskFrontmatter {
|
|
71
|
+
title: string;
|
|
72
|
+
status: string;
|
|
73
|
+
priority?: string;
|
|
74
|
+
assignee?: string;
|
|
75
|
+
tags?: string[];
|
|
76
|
+
/**
|
|
77
|
+
* Slugs of tasks that must be finished first. Data only — nothing in core
|
|
78
|
+
* gates mutations or moves a task's column based on this. An absent field
|
|
79
|
+
* and an empty array mean the same thing: no dependencies.
|
|
80
|
+
*/
|
|
81
|
+
depends_on?: string[];
|
|
82
|
+
created: string;
|
|
83
|
+
updated: string;
|
|
84
|
+
}
|
|
85
|
+
interface TaskComment {
|
|
86
|
+
timestamp: string;
|
|
87
|
+
text: string;
|
|
88
|
+
}
|
|
89
|
+
interface Task {
|
|
90
|
+
slug: string;
|
|
91
|
+
frontmatter: TaskFrontmatter;
|
|
92
|
+
/** Timeless task description, excluding the reserved trailing Task Updates section. */
|
|
93
|
+
body: string;
|
|
94
|
+
/** Time-anchored entries parsed from the trailing `## Task Updates` section. */
|
|
95
|
+
comments: TaskComment[];
|
|
96
|
+
}
|
|
97
|
+
interface TaskValidationWarning {
|
|
98
|
+
slug: string;
|
|
99
|
+
field: string;
|
|
100
|
+
message: string;
|
|
101
|
+
}
|
|
102
|
+
/** Diagnostics an initialization check can surface without rejecting a project. */
|
|
103
|
+
type ProjectWarning = ConfigLoadWarning | TaskValidationWarning;
|
|
104
|
+
interface TaskReadResult {
|
|
105
|
+
tasks: Task[];
|
|
106
|
+
warnings: TaskValidationWarning[];
|
|
107
|
+
}
|
|
108
|
+
interface BoardAPI {
|
|
109
|
+
/** When true, the Board hides create/edit/drag affordances and renders a viewing experience only. */
|
|
110
|
+
readonly readOnly?: boolean;
|
|
111
|
+
getConfig(): Promise<ShipbenchConfig>;
|
|
112
|
+
listTasks(): Promise<TaskReadResult>;
|
|
113
|
+
listArchivedTasks(): Promise<TaskReadResult>;
|
|
114
|
+
createTask(title: string, fields?: Partial<TaskFrontmatter>): Promise<Task>;
|
|
115
|
+
/**
|
|
116
|
+
* Update a task's fields and/or body. When the update changes `status`, the
|
|
117
|
+
* task moves columns, so implementations also maintain layout and return the
|
|
118
|
+
* authoritative new `layout` (mirroring {@link BoardAPI.reorderTask}); `layout`
|
|
119
|
+
* is omitted when the status did not change.
|
|
120
|
+
*/
|
|
121
|
+
updateTask(slug: string, fields: Partial<TaskFrontmatter>, body?: string): Promise<{
|
|
122
|
+
task: Task;
|
|
123
|
+
layout?: BoardLayout;
|
|
124
|
+
}>;
|
|
125
|
+
/** Append a time-stamped entry to the task's trailing Task Updates section. */
|
|
126
|
+
addComment(slug: string, text: string): Promise<Task>;
|
|
127
|
+
/** Edit an entry's text by zero-based index while preserving its timestamp. */
|
|
128
|
+
editComment(slug: string, index: number, text: string): Promise<Task>;
|
|
129
|
+
/** Delete an entry by zero-based index. */
|
|
130
|
+
deleteComment(slug: string, index: number): Promise<Task>;
|
|
131
|
+
moveTask(slug: string, toStatus: string): Promise<Task>;
|
|
132
|
+
/**
|
|
133
|
+
* Move (and reorder) a task. `position` is a 0-based index into the
|
|
134
|
+
* destination column's `layout` entry; `-1` means "append to end".
|
|
135
|
+
* Implementations are expected to return both the (possibly status-changed)
|
|
136
|
+
* task and the authoritative new layout so consumers can reconcile.
|
|
137
|
+
*/
|
|
138
|
+
reorderTask(slug: string, toStatus: string, position: number): Promise<{
|
|
139
|
+
task: Task;
|
|
140
|
+
layout: BoardLayout;
|
|
141
|
+
}>;
|
|
142
|
+
archiveTask(slug: string, options?: {
|
|
143
|
+
force?: boolean;
|
|
144
|
+
}): Promise<void>;
|
|
145
|
+
unarchiveTask(slug: string): Promise<Task>;
|
|
146
|
+
deleteTask(slug: string): Promise<void>;
|
|
147
|
+
onTasksChanged?(callback: () => void): () => void;
|
|
148
|
+
/**
|
|
149
|
+
* Turn a repo-root-relative path (e.g. `docs/spec.md`) into a URL the host can
|
|
150
|
+
* actually serve, so Markdown links to repo files in a task body resolve to
|
|
151
|
+
* something real. Return `null` when the path has no reachable destination.
|
|
152
|
+
*
|
|
153
|
+
* Optional, like {@link BoardAPI.onTasksChanged}: hosts that cannot point at
|
|
154
|
+
* repo files omit it, and the Board renders those links as plain, visible
|
|
155
|
+
* paths rather than dead anchors.
|
|
156
|
+
*/
|
|
157
|
+
resolveRepoLink?(repoRelativePath: string): string | null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Pure layout algebra — the single definition of manual task ordering.
|
|
162
|
+
*
|
|
163
|
+
* `layout.json` is machine-managed, and for a while two implementations of these
|
|
164
|
+
* rules existed: core's (authoritative, applied on write) and the Board store's
|
|
165
|
+
* optimistic copy, kept in sync by hand and by comment. They had already begun
|
|
166
|
+
* to diverge. Both now call these functions, so there is no mirror left to
|
|
167
|
+
* drift — see docs/audits/board-move-algorithm-audit.md.
|
|
168
|
+
*
|
|
169
|
+
* Nothing here does I/O. Callers supply the current layout and the task list.
|
|
170
|
+
*/
|
|
171
|
+
/**
|
|
172
|
+
* `created` desc — the deterministic fallback order for unpositioned tasks.
|
|
173
|
+
* Newest first, because rendering an unordered column is most useful with the
|
|
174
|
+
* freshest work on top. `compareTaskReadiness` in `availability.ts` breaks its
|
|
175
|
+
* `created` tie the other way; the two serve different questions and are not
|
|
176
|
+
* expected to agree.
|
|
177
|
+
*/
|
|
178
|
+
declare function byCreatedDesc(a: Task, b: Task): number;
|
|
179
|
+
/** `updated` desc — how the done column always sorts. */
|
|
180
|
+
declare function byUpdatedDesc(a: Task, b: Task): number;
|
|
181
|
+
/**
|
|
182
|
+
* Drop a slug from every column.
|
|
183
|
+
*
|
|
184
|
+
* When `existingSlugs` is given, slugs whose task no longer exists are pruned
|
|
185
|
+
* too, which is how a stale `layout.json` heals over time. Omit it to touch
|
|
186
|
+
* nothing but `slug` — rollback paths want that, since they are restoring state
|
|
187
|
+
* rather than reconciling it.
|
|
188
|
+
*/
|
|
189
|
+
declare function layoutWithoutTask(layout: BoardLayout, slug: string, existingSlugs?: ReadonlySet<string>): BoardLayout;
|
|
190
|
+
/**
|
|
191
|
+
* The layout after moving `slug` into `toStatus` at `position`.
|
|
192
|
+
*
|
|
193
|
+
* `position` is a 0-based index; `-1` (or past the end) appends. It is computed
|
|
194
|
+
* by callers against the *visible* column — layout order followed by
|
|
195
|
+
* unpositioned tasks in `created` desc order — so this materializes those
|
|
196
|
+
* leftovers into the column array before splicing, keeping the layout a superset
|
|
197
|
+
* of what the user is looking at. Without that step a position computed on
|
|
198
|
+
* screen would land somewhere else in the file.
|
|
199
|
+
*
|
|
200
|
+
* The done column never carries manual order: it time-sorts by `updated`, so its
|
|
201
|
+
* key is dropped entirely.
|
|
202
|
+
*/
|
|
203
|
+
declare function layoutAfterMove({ layout, tasks, slug, toStatus, position, doneColumn, }: {
|
|
204
|
+
layout: BoardLayout;
|
|
205
|
+
/** Every live task. Used for leftovers and for pruning stale slugs. */
|
|
206
|
+
tasks: Task[];
|
|
207
|
+
slug: string;
|
|
208
|
+
toStatus: string;
|
|
209
|
+
position: number;
|
|
210
|
+
doneColumn: string;
|
|
211
|
+
}): BoardLayout;
|
|
212
|
+
/**
|
|
213
|
+
* The tasks belonging to one column, in render order.
|
|
214
|
+
*
|
|
215
|
+
* - Regular column: `layout[columnId]` order first, then tasks with that status
|
|
216
|
+
* and no layout entry, `created` desc.
|
|
217
|
+
* - Done column (`columnId === doneColumn`): layout ignored, `updated` desc.
|
|
218
|
+
* Manual ordering stops meaning anything once work is finished.
|
|
219
|
+
* - Uncategorized (any `columnId` not in `validStatuses`): every task whose
|
|
220
|
+
* status matches no column, `created` desc. Layout ignored.
|
|
221
|
+
*
|
|
222
|
+
* `doneColumn` is deliberately required. It was optional, and `DetailView`
|
|
223
|
+
* omitted it — so keyboard navigation through the done column ran in a different
|
|
224
|
+
* order than the column being looked at.
|
|
225
|
+
*/
|
|
226
|
+
declare function orderedTasksForColumn(tasks: Task[], layout: BoardLayout | undefined, columnId: string, validStatuses: ReadonlySet<string>, doneColumn: string): Task[];
|
|
227
|
+
|
|
228
|
+
export { type BoardLayout as B, type ColumnDef as C, type DoneDisplayConfig as D, type LoadConfigOptions as L, type ProjectWarning as P, type ReadableStorageAdapter as R, type StorageAdapter as S, type Task as T, type ShipbenchConfig as a, type TaskFrontmatter as b, type TaskReadResult as c, type BoardAPI as d, type ConfigLoadWarning as e, type PriorityConfig as f, type TaskComment as g, type TaskValidationWarning as h, byCreatedDesc as i, byUpdatedDesc as j, layoutWithoutTask as k, layoutAfterMove as l, orderedTasksForColumn as o };
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { i as byCreatedDesc, j as byUpdatedDesc, l as layoutAfterMove, k as layoutWithoutTask, o as orderedTasksForColumn } from './layout-BS-84zwS.js';
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
byCreatedDesc,
|
|
3
|
+
byUpdatedDesc,
|
|
4
|
+
layoutAfterMove,
|
|
5
|
+
layoutWithoutTask,
|
|
6
|
+
orderedTasksForColumn
|
|
7
|
+
} from "./chunk-7T2E4KSJ.js";
|
|
8
|
+
export {
|
|
9
|
+
byCreatedDesc,
|
|
10
|
+
byUpdatedDesc,
|
|
11
|
+
layoutAfterMove,
|
|
12
|
+
layoutWithoutTask,
|
|
13
|
+
orderedTasksForColumn
|
|
14
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shipbench/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless library for the ShipBench project convention — parse, validate, and write Markdown task files through a pluggable storage adapter.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/gmmurray/shipbench.git",
|
|
9
|
+
"directory": "packages/core"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://shipbench.dev",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/gmmurray/shipbench/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"shipbench",
|
|
17
|
+
"project-management",
|
|
18
|
+
"tasks",
|
|
19
|
+
"kanban",
|
|
20
|
+
"markdown",
|
|
21
|
+
"frontmatter",
|
|
22
|
+
"git",
|
|
23
|
+
"agents"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./layout": {
|
|
37
|
+
"types": "./dist/layout.d.ts",
|
|
38
|
+
"import": "./dist/layout.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^26.0.1",
|
|
46
|
+
"tsup": "^8.0.0",
|
|
47
|
+
"typescript": "^5.5.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"gray-matter": "^4.0.3"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup src/index.ts src/layout.ts --format esm --dts",
|
|
54
|
+
"dev": "tsup src/index.ts src/layout.ts --format esm --dts --watch",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:watch": "vitest"
|
|
58
|
+
}
|
|
59
|
+
}
|