@unpolarize/knowledge-planning 0.1.0 → 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.
@@ -1,231 +0,0 @@
1
- import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
2
- import { join, dirname, relative } from 'node:path';
3
- import {
4
- SCHEMA_BY_TYPE,
5
- EdgeSchema,
6
- ns,
7
- type Edge,
8
- type ObjectType,
9
- type PlanningObject,
10
- } from '../schema/entities.ts';
11
- import { parseMarkdown, serializeMarkdown } from '../schema/frontmatter.ts';
12
- import { DEFAULT_CONFIG, type Config } from '../types.ts';
13
-
14
- /** A loaded markdown object: validated `object` + raw `data`/`body` for round-trip writes. */
15
- export interface StoreDoc {
16
- id: string;
17
- type: ObjectType;
18
- path: string; // absolute
19
- relpath: string; // relative to store root
20
- object: PlanningObject;
21
- data: Record<string, unknown>;
22
- body: string;
23
- }
24
-
25
- const IGNORE_DIRS = new Set(['node_modules', '.git', '.index', 'src', 'test', 'dist', '.vscode']);
26
- const CONTAINER_TYPES = new Set<ObjectType>(['project']);
27
-
28
- /**
29
- * Markdown SSOT reader/writer. The directory tree IS the index: every `*.md` with
30
- * valid `id`+`type` frontmatter is an object, keyed by its `id`. Writes preserve
31
- * body text and strip empty frontmatter. Edges come from both `edges.jsonl` and
32
- * object frontmatter (the union is rebuildable, so a clobber is recoverable).
33
- */
34
- export class Store {
35
- readonly root: string;
36
- private docs: Map<string, StoreDoc> | null = null;
37
- private warnings: string[] = [];
38
-
39
- constructor(root: string) {
40
- this.root = root;
41
- }
42
-
43
- get configPath(): string {
44
- return join(this.root, 'config.json');
45
- }
46
-
47
- get edgesPath(): string {
48
- return join(this.root, 'edges.jsonl');
49
- }
50
-
51
- config(): Config {
52
- if (!existsSync(this.configPath)) return { ...DEFAULT_CONFIG };
53
- try {
54
- return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync(this.configPath, 'utf8')) };
55
- } catch {
56
- return { ...DEFAULT_CONFIG };
57
- }
58
- }
59
-
60
- /** Force a re-scan on next access. */
61
- invalidate(): void {
62
- this.docs = null;
63
- }
64
-
65
- private ensureLoaded(): Map<string, StoreDoc> {
66
- if (this.docs) return this.docs;
67
- const docs = new Map<string, StoreDoc>();
68
- this.warnings = [];
69
- for (const abs of walkMarkdown(this.root)) {
70
- let raw: string;
71
- try {
72
- raw = readFileSync(abs, 'utf8');
73
- } catch {
74
- continue;
75
- }
76
- const { data, body } = parseMarkdown(raw);
77
- const type = data.type as ObjectType | undefined;
78
- const id = data.id as string | undefined;
79
- if (!type || !id || !(type in SCHEMA_BY_TYPE)) continue;
80
- const schema = SCHEMA_BY_TYPE[type];
81
- const res = schema.safeParse(data);
82
- if (!res.success) {
83
- this.warnings.push(`${relative(this.root, abs)}: ${res.error.issues[0]?.message ?? 'invalid'}`);
84
- continue;
85
- }
86
- docs.set(id, {
87
- id,
88
- type,
89
- path: abs,
90
- relpath: relative(this.root, abs),
91
- object: res.data as PlanningObject,
92
- data,
93
- body,
94
- });
95
- }
96
- this.docs = docs;
97
- return docs;
98
- }
99
-
100
- getWarnings(): string[] {
101
- this.ensureLoaded();
102
- return this.warnings;
103
- }
104
-
105
- all(): StoreDoc[] {
106
- return [...this.ensureLoaded().values()];
107
- }
108
-
109
- get(id: string): StoreDoc | undefined {
110
- return this.ensureLoaded().get(id);
111
- }
112
-
113
- byType(type: ObjectType): StoreDoc[] {
114
- return this.all().filter((d) => d.type === type);
115
- }
116
-
117
- /** Default on-disk path for a new object id. Projects are folders (`<id>/index.md`). */
118
- pathForId(id: string, type: ObjectType): string {
119
- if (CONTAINER_TYPES.has(type)) return join(this.root, id, 'index.md');
120
- return join(this.root, `${id}.md`);
121
- }
122
-
123
- /**
124
- * Create or overwrite an object. Frontmatter is validated; body defaults to the
125
- * existing body when omitted for an existing object.
126
- */
127
- write(input: Record<string, unknown> & { id: string; type: ObjectType }, body?: string): StoreDoc {
128
- const { id, type } = input;
129
- if (!(type in SCHEMA_BY_TYPE)) throw new Error(`unknown type: ${type}`);
130
- const existing = this.get(id);
131
- const data: Record<string, unknown> = { schema: ns(type), ...input };
132
- const schema = SCHEMA_BY_TYPE[type];
133
- const parsed = schema.parse(data); // throws on invalid
134
- const path = existing?.path ?? this.pathForId(id, type);
135
- const finalBody = body ?? existing?.body ?? '';
136
- mkdirSync(dirname(path), { recursive: true });
137
- writeFileSync(path, serializeMarkdown(data, finalBody), 'utf8');
138
- const doc: StoreDoc = {
139
- id,
140
- type,
141
- path,
142
- relpath: relative(this.root, path),
143
- object: parsed as PlanningObject,
144
- data,
145
- body: finalBody,
146
- };
147
- this.ensureLoaded().set(id, doc);
148
- return doc;
149
- }
150
-
151
- /** Load an object, mutate its frontmatter via `mutator`, and write it back. */
152
- patch(id: string, mutator: (data: Record<string, unknown>) => void): StoreDoc {
153
- const existing = this.get(id);
154
- if (!existing) throw new Error(`object not found: ${id}`);
155
- const data = { ...existing.data };
156
- mutator(data);
157
- return this.write(data as { id: string; type: ObjectType }, existing.body);
158
- }
159
-
160
- // --- Edges ---------------------------------------------------------------
161
-
162
- appendEdge(edge: Edge): void {
163
- const e = EdgeSchema.parse(edge);
164
- appendFileSync(this.edgesPath, JSON.stringify(e) + '\n', 'utf8');
165
- }
166
-
167
- /** Explicit edges from edges.jsonl (append-only log). */
168
- fileEdges(): Edge[] {
169
- if (!existsSync(this.edgesPath)) return [];
170
- const out: Edge[] = [];
171
- for (const line of readFileSync(this.edgesPath, 'utf8').split('\n')) {
172
- const t = line.trim();
173
- if (!t) continue;
174
- try {
175
- out.push(EdgeSchema.parse(JSON.parse(t)));
176
- } catch {
177
- /* skip malformed line */
178
- }
179
- }
180
- return out;
181
- }
182
-
183
- /** Edges derived from object frontmatter (the rebuildable source of truth). */
184
- frontmatterEdges(): Edge[] {
185
- const edges: Edge[] = [];
186
- for (const d of this.all()) {
187
- const o = d.object;
188
- for (const ref of o.blocked_by ?? [])
189
- edges.push({ kind: 'blocked_by', from: o.id, to: ref.path, reason: ref.reason, status: ref.status });
190
- for (const c of o.cites ?? []) edges.push({ kind: 'cites', from: o.id, to: c });
191
- for (const dep of o.depends_on ?? []) edges.push({ kind: 'depends_on', from: o.id, to: dep });
192
- for (const r of o.related ?? []) edges.push({ kind: 'related', from: o.id, to: r });
193
- for (const s of o.linked_sessions ?? []) edges.push({ kind: 'evidence', from: `session:${s}`, to: o.id });
194
- if (o.type === 'idea' && o.promoted_to) edges.push({ kind: 'promoted_to', from: o.id, to: o.promoted_to });
195
- if (o.type === 'task' && o.plan) edges.push({ kind: 'parent_of', from: o.plan, to: o.id });
196
- if (o.type === 'task' && o.scheduled_for) edges.push({ kind: 'scheduled_for', from: o.id, to: o.scheduled_for });
197
- if (o.type === 'insight') for (const t of o.suggested_tasks ?? []) edges.push({ kind: 'surfaced_on', from: o.id, to: t });
198
- if (o.project) edges.push({ kind: 'parent_of', from: o.project, to: o.id });
199
- }
200
- return edges;
201
- }
202
-
203
- /** Union of frontmatter-derived and explicit edges. */
204
- allEdges(): Edge[] {
205
- return [...this.frontmatterEdges(), ...this.fileEdges()];
206
- }
207
- }
208
-
209
- /** Recursively yield absolute paths of `*.md` files, skipping IGNORE_DIRS. */
210
- export function walkMarkdown(root: string): string[] {
211
- const out: string[] = [];
212
- if (!existsSync(root)) return out;
213
- const walk = (dir: string) => {
214
- let entries;
215
- try {
216
- entries = readdirSync(dir, { withFileTypes: true });
217
- } catch {
218
- return;
219
- }
220
- for (const e of entries) {
221
- if (e.isDirectory()) {
222
- if (IGNORE_DIRS.has(e.name) || e.name.startsWith('.')) continue;
223
- walk(join(dir, e.name));
224
- } else if (e.isFile() && e.name.endsWith('.md')) {
225
- out.push(join(dir, e.name));
226
- }
227
- }
228
- };
229
- walk(root);
230
- return out;
231
- }
package/src/types.ts DELETED
@@ -1,18 +0,0 @@
1
- /** Store-level configuration (config.json at the planning repo root). */
2
- export interface Config {
3
- /** Top-level life/work areas. */
4
- domains: string[];
5
- /** Absolute path to the ~/docs knowledge base (KnowledgeRef resolution root). */
6
- kb_root: string;
7
- /** Absolute path to the sessions.git store (~/.sessions) for planning_refs. */
8
- sessions_root: string;
9
- /** Team-ready fields, dormant solo. */
10
- owner?: string;
11
- visibility?: 'private' | 'shared';
12
- }
13
-
14
- export const DEFAULT_CONFIG: Config = {
15
- domains: ['tech', 'career', 'finances', 'kids', 'relationship', 'relocation'],
16
- kb_root: `${process.env.HOME}/docs`,
17
- sessions_root: `${process.env.HOME}/.sessions`,
18
- };