@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.
@@ -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 };
@@ -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
+ }