@frehilm/ordna-core 0.1.4 → 0.2.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.
Files changed (65) hide show
  1. package/README.md +34 -0
  2. package/dist/config.d.ts +26 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +41 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/git.d.ts +10 -0
  7. package/dist/git.d.ts.map +1 -1
  8. package/dist/git.js +14 -27
  9. package/dist/git.js.map +1 -1
  10. package/dist/index.d.ts +3 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/schema.d.ts +18 -1
  15. package/dist/schema.d.ts.map +1 -1
  16. package/dist/schema.js.map +1 -1
  17. package/dist/storage/auto-detect.d.ts +82 -0
  18. package/dist/storage/auto-detect.d.ts.map +1 -0
  19. package/dist/storage/auto-detect.js +158 -0
  20. package/dist/storage/auto-detect.js.map +1 -0
  21. package/dist/storage/auto-push.d.ts +36 -0
  22. package/dist/storage/auto-push.d.ts.map +1 -0
  23. package/dist/storage/auto-push.js +100 -0
  24. package/dist/storage/auto-push.js.map +1 -0
  25. package/dist/storage/backend.d.ts +86 -0
  26. package/dist/storage/backend.d.ts.map +1 -0
  27. package/dist/storage/backend.js +14 -0
  28. package/dist/storage/backend.js.map +1 -0
  29. package/dist/storage/backends/file.d.ts +31 -0
  30. package/dist/storage/backends/file.d.ts.map +1 -0
  31. package/dist/storage/backends/file.js +213 -0
  32. package/dist/storage/backends/file.js.map +1 -0
  33. package/dist/storage/backends/hybrid.d.ts +39 -0
  34. package/dist/storage/backends/hybrid.d.ts.map +1 -0
  35. package/dist/storage/backends/hybrid.js +270 -0
  36. package/dist/storage/backends/hybrid.js.map +1 -0
  37. package/dist/storage/backends/namespace.d.ts +55 -0
  38. package/dist/storage/backends/namespace.d.ts.map +1 -0
  39. package/dist/storage/backends/namespace.js +907 -0
  40. package/dist/storage/backends/namespace.js.map +1 -0
  41. package/dist/storage/file-io.d.ts +38 -0
  42. package/dist/storage/file-io.d.ts.map +1 -0
  43. package/dist/storage/file-io.js +69 -0
  44. package/dist/storage/file-io.js.map +1 -0
  45. package/dist/storage/git-ref.d.ts +77 -0
  46. package/dist/storage/git-ref.d.ts.map +1 -0
  47. package/dist/storage/git-ref.js +184 -0
  48. package/dist/storage/git-ref.js.map +1 -0
  49. package/dist/storage/markdown.d.ts +17 -0
  50. package/dist/storage/markdown.d.ts.map +1 -0
  51. package/dist/storage/markdown.js +17 -0
  52. package/dist/storage/markdown.js.map +1 -0
  53. package/dist/storage/sync-ref.d.ts +82 -0
  54. package/dist/storage/sync-ref.d.ts.map +1 -0
  55. package/dist/storage/sync-ref.js +191 -0
  56. package/dist/storage/sync-ref.js.map +1 -0
  57. package/dist/store.d.ts +56 -8
  58. package/dist/store.d.ts.map +1 -1
  59. package/dist/store.js +93 -115
  60. package/dist/store.js.map +1 -1
  61. package/dist/watcher.d.ts +33 -1
  62. package/dist/watcher.d.ts.map +1 -1
  63. package/dist/watcher.js +12 -30
  64. package/dist/watcher.js.map +1 -1
  65. package/package.json +1 -1
@@ -0,0 +1,270 @@
1
+ import { join } from "node:path";
2
+ import chokidar, {} from "chokidar";
3
+ import { ARCHIVED_STATUS, isKnownStatus, } from "../backend.js";
4
+ import { deleteTaskFile, ensureTasksDir, filenameFor, listTaskBytes, writeTaskBytes, } from "../file-io.js";
5
+ import { GitRunner } from "../git-ref.js";
6
+ import { defaultSectionsFor, parseTask, parseTaskFile, serializeTask, } from "../markdown.js";
7
+ import { PushQueue } from "../auto-push.js";
8
+ import { SyncRef } from "../sync-ref.js";
9
+ const SYNC_REF_NAME = "refs/ordna/state";
10
+ const SYNC_PUSH_REFSPEC = `+${SYNC_REF_NAME}:${SYNC_REF_NAME}`;
11
+ function today() {
12
+ return new Date().toISOString().slice(0, 10);
13
+ }
14
+ function nowIso() {
15
+ return new Date().toISOString();
16
+ }
17
+ /**
18
+ * Hybrid storage backend.
19
+ *
20
+ * Tasks stay as markdown files in `tasks/*.md` — same on-disk layout
21
+ * as `FileBackend`, so existing tooling that reads task files Just
22
+ * Works. The difference: shared metadata moves into a single git ref
23
+ * at `refs/ordna/state`. The blob there holds:
24
+ *
25
+ * - `next_id`: a CAS-bumped allocator that prevents two offline
26
+ * collaborators from both picking the same task number
27
+ * - `ops`: an append-only audit log of every create / update /
28
+ * archive / delete
29
+ *
30
+ * Every mutation does two atomic writes (one to disk, one to the
31
+ * ref) and fires a best-effort coalesced push to keep `origin` in
32
+ * sync. Reads stay file-system-only (no ref involvement in v1).
33
+ */
34
+ export class HybridBackend {
35
+ cwd;
36
+ config;
37
+ tasksDir;
38
+ kind = "hybrid";
39
+ #initPromise = null;
40
+ #activeWatchers = new Set();
41
+ #git;
42
+ #sync = null;
43
+ #pushQueue = null;
44
+ #cachedActor = null;
45
+ constructor(cwd, config, tasksDir) {
46
+ this.cwd = cwd;
47
+ this.config = config;
48
+ this.tasksDir = tasksDir;
49
+ this.#git = new GitRunner(cwd);
50
+ }
51
+ async init() {
52
+ await this.#git.ensureRepository();
53
+ ensureTasksDir(this.tasksDir);
54
+ this.#sync = new SyncRef(this.#git, SYNC_REF_NAME);
55
+ this.#pushQueue = new PushQueue(this.#git, SYNC_PUSH_REFSPEC, "ordna-hybrid");
56
+ }
57
+ async #ensureInit() {
58
+ if (!this.#initPromise)
59
+ this.#initPromise = this.init();
60
+ return this.#initPromise;
61
+ }
62
+ async dispose() {
63
+ // Close all watchers first so a watcher event doesn't fire
64
+ // another mutation while we're flushing.
65
+ const closing = [];
66
+ for (const w of this.#activeWatchers)
67
+ closing.push(w.close());
68
+ this.#activeWatchers.clear();
69
+ await Promise.all(closing);
70
+ if (this.#pushQueue) {
71
+ await this.#pushQueue.flush();
72
+ }
73
+ }
74
+ // ---------------- reads ----------------
75
+ async list(options = {}) {
76
+ await this.#ensureInit();
77
+ const entries = await listTaskBytes(this.tasksDir);
78
+ const tasks = [];
79
+ for (const { filePath, raw } of entries) {
80
+ try {
81
+ tasks.push(parseTask(raw, filePath));
82
+ }
83
+ catch {
84
+ // Skip malformed tasks silently (same posture as file mode).
85
+ }
86
+ }
87
+ let filtered = tasks;
88
+ if (options.status)
89
+ filtered = filtered.filter((t) => t.status === options.status);
90
+ if (options.assignee)
91
+ filtered = filtered.filter((t) => t.assignee === options.assignee);
92
+ if (options.tag) {
93
+ const tag = options.tag;
94
+ filtered = filtered.filter((t) => t.tags.includes(tag));
95
+ }
96
+ filtered.sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true }));
97
+ return filtered;
98
+ }
99
+ async get(id) {
100
+ const tasks = await this.list();
101
+ return tasks.find((t) => t.id === id) ?? null;
102
+ }
103
+ // ---------------- writes ----------------
104
+ async create(input) {
105
+ await this.#ensureInit();
106
+ const sync = this.#sync;
107
+ const pushQueue = this.#pushQueue;
108
+ const status = input.status ?? this.config.statuses[0];
109
+ if (!status)
110
+ throw new Error("Config has no statuses defined.");
111
+ if (!isKnownStatus(this.config, status)) {
112
+ throw new Error(`Status "${status}" is not in configured statuses.`);
113
+ }
114
+ // CAS-allocate the next id from the sync ref. This is what
115
+ // makes hybrid mode safe across offline collaborators.
116
+ const id = await sync.allocateNextId(this.config);
117
+ const now = today();
118
+ const task = {
119
+ id,
120
+ title: input.title,
121
+ status,
122
+ assignee: input.assignee ?? null,
123
+ priority: input.priority ?? null,
124
+ tags: input.tags ?? [],
125
+ depends_on: input.depends_on ?? [],
126
+ created_at: now,
127
+ updated_at: now,
128
+ sections: defaultSectionsFor(this.config.schema),
129
+ extra_frontmatter: {},
130
+ filePath: "",
131
+ rawContent: "",
132
+ };
133
+ const filename = filenameFor(id, task.title, this.config.schema, this.config);
134
+ task.filePath = join(this.tasksDir, filename);
135
+ const serialized = serializeTask(task, this.config.schema);
136
+ task.rawContent = serialized;
137
+ await writeTaskBytes(task.filePath, serialized);
138
+ await sync.appendOp(await this.#buildOp("create", id));
139
+ pushQueue.schedule();
140
+ return task;
141
+ }
142
+ async update(id, patch) {
143
+ await this.#ensureInit();
144
+ const sync = this.#sync;
145
+ const pushQueue = this.#pushQueue;
146
+ const existing = await this.get(id);
147
+ if (!existing)
148
+ throw new Error(`Task ${id} not found.`);
149
+ const next = {
150
+ ...existing,
151
+ title: patch.title ?? existing.title,
152
+ status: patch.status ?? existing.status,
153
+ assignee: patch.assignee !== undefined ? patch.assignee : existing.assignee,
154
+ priority: patch.priority !== undefined ? patch.priority : existing.priority,
155
+ tags: patch.tags ?? existing.tags,
156
+ depends_on: patch.depends_on ?? existing.depends_on,
157
+ sections: patch.sections ?? existing.sections,
158
+ updated_at: today(),
159
+ };
160
+ if (next.status !== existing.status &&
161
+ !isKnownStatus(this.config, next.status)) {
162
+ throw new Error(`Status "${next.status}" is not in configured statuses.`);
163
+ }
164
+ const serialized = serializeTask(next, this.config.schema);
165
+ next.rawContent = serialized;
166
+ if (!existing.filePath) {
167
+ throw new Error(`Task ${id} has no filePath; cannot update in hybrid mode.`);
168
+ }
169
+ await writeTaskBytes(existing.filePath, serialized);
170
+ // Light op classification: a status-change to "archived" gets
171
+ // its own op kind; everything else is a generic update. Richer
172
+ // classification (move via status, changed-field lists) is
173
+ // deferred to the follow-up that adds op-specific fields.
174
+ const opKind = patch.status === ARCHIVED_STATUS ? "archive" : "update";
175
+ await sync.appendOp(await this.#buildOp(opKind, id));
176
+ pushQueue.schedule();
177
+ return next;
178
+ }
179
+ async delete(id) {
180
+ await this.#ensureInit();
181
+ const sync = this.#sync;
182
+ const pushQueue = this.#pushQueue;
183
+ const task = await this.get(id);
184
+ if (!task)
185
+ throw new Error(`Task ${id} not found.`);
186
+ if (!task.filePath) {
187
+ throw new Error(`Task ${id} has no filePath; cannot delete in hybrid mode.`);
188
+ }
189
+ await deleteTaskFile(task.filePath);
190
+ await sync.appendOp(await this.#buildOp("delete", id));
191
+ pushQueue.schedule();
192
+ }
193
+ // ---------------- watch ----------------
194
+ watch(listener) {
195
+ // Same chokidar setup as FileBackend. Sync ref doesn't drive
196
+ // watcher events in v1 — the file watcher is fast and reliable
197
+ // for the task-content changes that the TUI / web care about.
198
+ const watcher = chokidar.watch(this.tasksDir, {
199
+ ignoreInitial: true,
200
+ depth: 0,
201
+ persistent: true,
202
+ });
203
+ this.#activeWatchers.add(watcher);
204
+ const emitIfMarkdown = async (type, filePath) => {
205
+ if (!filePath.endsWith(".md"))
206
+ return;
207
+ try {
208
+ const task = await parseTaskFile(filePath);
209
+ listener({ type, task });
210
+ }
211
+ catch {
212
+ // Ignore partial writes or malformed files.
213
+ }
214
+ };
215
+ watcher.on("add", (path) => void emitIfMarkdown("added", path));
216
+ watcher.on("change", (path) => void emitIfMarkdown("changed", path));
217
+ watcher.on("unlink", (path) => {
218
+ if (!path.endsWith(".md"))
219
+ return;
220
+ listener({ type: "removed", filePath: path });
221
+ });
222
+ return async () => {
223
+ this.#activeWatchers.delete(watcher);
224
+ await watcher.close();
225
+ };
226
+ }
227
+ // ---------------- commit (file-mode parity) ----------------
228
+ async commit(message = "chore(tasks): update") {
229
+ await this.#ensureInit();
230
+ const tasksDirArg = this.config.tasksDir;
231
+ await this.#git.run(["add", "--", tasksDirArg]);
232
+ const status = await this.#git.run([
233
+ "status",
234
+ "--porcelain",
235
+ "--",
236
+ tasksDirArg,
237
+ ]);
238
+ if (status.trim().length === 0) {
239
+ throw new Error("No task changes to commit.");
240
+ }
241
+ await this.#git.run(["commit", "-m", message, "--", tasksDirArg]);
242
+ }
243
+ // ---------------- internals ----------------
244
+ async #buildOp(op, id) {
245
+ return {
246
+ ts: nowIso(),
247
+ actor: await this.#resolveActor(),
248
+ op,
249
+ id,
250
+ };
251
+ }
252
+ async #resolveActor() {
253
+ if (this.#cachedActor !== null)
254
+ return this.#cachedActor;
255
+ // Order: git config user.email → ORDNA_ACTOR env → "unknown".
256
+ const fromGit = await this.#git.userEmail();
257
+ if (fromGit) {
258
+ this.#cachedActor = fromGit;
259
+ return fromGit;
260
+ }
261
+ const fromEnv = process.env.ORDNA_ACTOR;
262
+ if (fromEnv && fromEnv.trim().length > 0) {
263
+ this.#cachedActor = fromEnv.trim();
264
+ return this.#cachedActor;
265
+ }
266
+ this.#cachedActor = "unknown";
267
+ return this.#cachedActor;
268
+ }
269
+ }
270
+ //# sourceMappingURL=hybrid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hybrid.js","sourceRoot":"","sources":["../../../src/storage/backends/hybrid.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import type { OrdnaConfig } from "../../config.js";
2
+ import type { Task, TaskCreateInput, TaskUpdateInput } from "../../schema.js";
3
+ import type { TaskEventListener } from "../../watcher.js";
4
+ import { type Backend, type FetchResult, type ListOptions } from "../backend.js";
5
+ /**
6
+ * Namespace storage backend.
7
+ *
8
+ * Tasks live as git **blobs** under `refs/ordna/tasks/<id>` — one ref
9
+ * per task, no working-tree files. `git status` stays clean. `git log`
10
+ * on branches doesn't see task mutations.
11
+ *
12
+ * **ID allocation.** A shared `refs/ordna/state` ref carries a
13
+ * `SyncRef`-managed JSON blob `{next_id, ops}`. Same primitive as
14
+ * hybrid: CAS in-process, auto-fetch-and-retry on conflict.
15
+ *
16
+ * **Bootstrap.** On `init()` if the state ref is missing, we scan
17
+ * existing `refs/ordna/tasks/*` and seed `next_id` from the max
18
+ * numeric id. Safe across concurrent processes (CAS).
19
+ *
20
+ * **Sync.** Every mutation schedules a per-ref push with
21
+ * `--force-with-lease` (per-ref CAS at the protocol level). On a
22
+ * rejected `create` (offline collision), the backend fetches,
23
+ * reallocates a fresh id via `SyncRef`, rewrites the local blob's
24
+ * `id:` field, cascades the rewrite through any local `depends_on`
25
+ * references to the old id, and emits a `renamed` event. Update
26
+ * collisions are deliberately loud — silently picking a winner would
27
+ * lose user edits.
28
+ *
29
+ * **Auto-fetch.** A configurable timer (default 60s) keeps the local
30
+ * snapshot fresh without manual pulls. A `fetch()` method exposes it
31
+ * manually for the TUI key / web button.
32
+ *
33
+ * **Audit log.** The `ops` array in the state blob records every
34
+ * `create`/`update`/`archive`/`delete`/`rename`. `rename` entries
35
+ * carry `renamedFrom` so the UIs can show a "previously known as X"
36
+ * banner on the affected task.
37
+ */
38
+ export declare class NamespaceBackend implements Backend {
39
+ #private;
40
+ private readonly cwd;
41
+ private readonly config;
42
+ readonly kind = "namespace";
43
+ constructor(cwd: string, config: OrdnaConfig);
44
+ init(): Promise<void>;
45
+ dispose(): Promise<void>;
46
+ list(options?: ListOptions): Promise<Task[]>;
47
+ get(id: string): Promise<Task | null>;
48
+ create(input: TaskCreateInput): Promise<Task>;
49
+ update(id: string, patch: TaskUpdateInput): Promise<Task>;
50
+ delete(id: string): Promise<void>;
51
+ watch(listener: TaskEventListener): () => Promise<void>;
52
+ commit(_message?: string): Promise<void>;
53
+ fetch(): Promise<FetchResult>;
54
+ }
55
+ //# sourceMappingURL=namespace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"namespace.d.ts","sourceRoot":"","sources":["../../../src/storage/backends/namespace.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,KAAK,EACX,IAAI,EACJ,eAAe,EACf,eAAe,EACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAa,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAEN,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,WAAW,EAEhB,MAAM,eAAe,CAAC;AAqFvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qBAAa,gBAAiB,YAAW,OAAO;;IA+B9C,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IA/BxB,QAAQ,CAAC,IAAI,eAAe;gBA8BV,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW;IAS/B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoCrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA4BxB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAsChD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAoBrC,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA+D7C,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA+DzD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+CvC,KAAK,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;IAuBjD,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUxC,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC;CAofnC"}