@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.
package/src/cli/index.ts DELETED
@@ -1,189 +0,0 @@
1
- #!/usr/bin/env node
2
- import { homedir } from 'node:os';
3
- import { join, resolve, basename, dirname } from 'node:path';
4
- import { Store } from '../store/store.ts';
5
- import { PlanningIndex } from '../index/db.ts';
6
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
7
- import * as cmd from './commands.ts';
8
-
9
- const HELP = `knowledge-planning (kp) — durable, git-backed planning over a markdown SSOT
10
-
11
- Usage: kp <command> [options]
12
-
13
- reindex Rebuild the .index/planning.db projection
14
- list [--type T] [--status S] List objects (idea/plan/task/...)
15
- daily [--date today|YYYY-MM-DD] [--carry-over] Render the daily board
16
- board Alias for 'daily'
17
- blocked Show objects blocked_by unresolved knowledge
18
- graph [--json] Graph data (nodes/edges) for the CSV webview
19
- capture "<text>" [--domain D] Quick-capture an idea (status=capture)
20
- promote <ideaId> [--to planId] Promote an idea to a plan
21
- link-session <objectId> <uuid> Bidirectional session link (+planning_refs)
22
- insight <file> --source S Ingest digest/brief bullets into an Insight
23
- ingest-gdoc [<file>] [--domain D] [--max N] Diff-ingest NEW Google-Doc bullets →
24
- capture ideas/inbox tasks (baselines on first run, caps per run)
25
- set-status <objectId> <status> Change an object's status (board lane / accept)
26
- export [--date today|DATE] One-shot JSON snapshot (for the CSV planning mode)
27
- show <objectId> Rich JSON detail (body + resolved refs + children) for the dashboard
28
- doctor Config, counts, fts, blocked, warnings
29
-
30
- Options:
31
- --root <path> Store root (default: package root or $KP_ROOT)
32
- --help This help
33
- `;
34
-
35
- function getFlag(argv: string[], name: string): string | undefined {
36
- const i = argv.indexOf(name);
37
- if (i >= 0 && i + 1 < argv.length && !argv[i + 1].startsWith('--')) return argv[i + 1];
38
- const eq = argv.find((a) => a.startsWith(`${name}=`));
39
- return eq ? eq.slice(name.length + 1) : undefined;
40
- }
41
- const hasFlag = (argv: string[], name: string): boolean => argv.includes(name);
42
-
43
- /**
44
- * Store root resolution. The DATA lives in the knowledge base (`~/docs/planning`),
45
- * not in this package — the package is tool source only. Override with `--root` or
46
- * `$KP_ROOT` (e.g. for tests or an alternate KB location).
47
- */
48
- export function defaultStoreRoot(): string {
49
- return join(homedir(), 'docs', 'planning');
50
- }
51
- function resolveRoot(argv: string[]): string {
52
- const flag = getFlag(argv, '--root');
53
- if (flag) return resolve(flag);
54
- if (process.env.KP_ROOT) return resolve(process.env.KP_ROOT);
55
- return defaultStoreRoot();
56
- }
57
-
58
- function positionals(argv: string[]): string[] {
59
- const out: string[] = [];
60
- for (let i = 0; i < argv.length; i++) {
61
- const a = argv[i];
62
- if (a.startsWith('--')) {
63
- if (i + 1 < argv.length && !argv[i + 1].startsWith('--') && !a.includes('=')) i++; // skip its value
64
- continue;
65
- }
66
- out.push(a);
67
- }
68
- return out;
69
- }
70
-
71
- function main(): void {
72
- const argv = process.argv.slice(2);
73
- const command = argv[0];
74
- if (!command || command === 'help' || hasFlag(argv, '--help')) {
75
- console.log(HELP);
76
- return;
77
- }
78
-
79
- const root = resolveRoot(argv);
80
- const store = new Store(root);
81
- const config = store.config();
82
- const rest = positionals(argv).slice(1);
83
- const idx = () => new PlanningIndex(store);
84
-
85
- switch (command) {
86
- case 'reindex':
87
- console.log(cmd.cmdReindex(store, root));
88
- break;
89
- case 'list':
90
- console.log(cmd.cmdList(store, { type: getFlag(argv, '--type'), status: getFlag(argv, '--status') }));
91
- break;
92
- case 'daily':
93
- case 'board': {
94
- const date = getFlag(argv, '--date');
95
- console.log(cmd.cmdDaily(store, idx(), !date || date === 'today' ? cmd.today() : date, hasFlag(argv, '--carry-over')));
96
- break;
97
- }
98
- case 'blocked':
99
- console.log(cmd.cmdBlocked(idx(), config.kb_root));
100
- break;
101
- case 'graph':
102
- console.log(cmd.cmdGraph(store, config.kb_root, hasFlag(argv, '--json')));
103
- break;
104
- case 'capture': {
105
- if (!rest[0]) throw new Error('usage: kp capture "<text>" [--domain D]');
106
- const res = cmd.cmdCapture(store, rest[0], getFlag(argv, '--domain'));
107
- console.log(`captured ${res.id}`);
108
- break;
109
- }
110
- case 'promote': {
111
- if (!rest[0]) throw new Error('usage: kp promote <ideaId> [--to planId]');
112
- const res = cmd.cmdPromote(store, rest[0], getFlag(argv, '--to'));
113
- console.log(`promoted ${res.ideaId} → ${res.planId}`);
114
- break;
115
- }
116
- case 'link-session': {
117
- if (!rest[0] || !rest[1]) throw new Error('usage: kp link-session <objectId> <session-uuid>');
118
- const res = cmd.cmdLinkSession(store, config.sessions_root, rest[0], rest[1]);
119
- console.log(
120
- `linked ${res.objectId} ↔ session ${res.uuid}\n envelope: ${
121
- res.envelopePath ? `${res.envelopeUpdated ? 'updated' : 'already linked'} (${res.envelopePath})` : 'not found in ' + config.sessions_root
122
- }`,
123
- );
124
- break;
125
- }
126
- case 'insight': {
127
- if (!rest[0]) throw new Error('usage: kp insight <file> --source S [--ref R]');
128
- const text = readFileSync(resolve(rest[0]), 'utf8');
129
- const res = cmd.cmdInsightIngest(store, text, getFlag(argv, '--source') ?? 'manual', getFlag(argv, '--ref'));
130
- console.log(`created ${res.id} with ${res.suggested} suggested item(s)`);
131
- break;
132
- }
133
- case 'export': {
134
- const date = getFlag(argv, '--date');
135
- console.log(JSON.stringify(cmd.cmdExport(store, idx(), config, !date || date === 'today' ? cmd.today() : date)));
136
- break;
137
- }
138
- case 'show': {
139
- if (!rest[0]) throw new Error('usage: kp show <objectId>');
140
- console.log(JSON.stringify(cmd.cmdShow(store, config.kb_root, rest[0])));
141
- break;
142
- }
143
- case 'ingest-gdoc': {
144
- const file = rest[0] ? resolve(rest[0]) : join(homedir(), 'docs/raw/google-docs/personal-todo-notes.md');
145
- const text = readFileSync(file, 'utf8');
146
- // diff against a per-source snapshot so only NEW bullets ingest (baseline on first run)
147
- const snapPath = join(root, '.ingest-state', `${basename(file)}.snapshot`);
148
- const prevText = existsSync(snapPath) ? readFileSync(snapPath, 'utf8') : null;
149
- const max = getFlag(argv, '--max');
150
- const res = cmd.cmdIngestGdoc(store, text, {
151
- domain: getFlag(argv, '--domain'),
152
- prevText,
153
- max: max ? Number(max) : undefined,
154
- });
155
- mkdirSync(dirname(snapPath), { recursive: true });
156
- writeFileSync(snapPath, text, 'utf8');
157
- if (res.baselined) {
158
- console.log(`baselined ${file} (first run — no ingest). New bullets will be ingested on the next change.`);
159
- } else {
160
- console.log(
161
- `ingested ${file}\n ${res.newBullets} new bullet(s) → created ${res.created.length}, skipped ${res.skipped}` +
162
- (res.capped ? `, capped ${res.capped} (raise with --max)` : ''),
163
- );
164
- if (res.created.length) console.log(' ' + res.created.join('\n '));
165
- }
166
- break;
167
- }
168
- case 'set-status': {
169
- if (!rest[0] || !rest[1]) throw new Error('usage: kp set-status <objectId> <status>');
170
- cmd.cmdSetStatus(store, rest[0], rest[1]);
171
- console.log(`set ${rest[0]} → ${rest[1]}`);
172
- break;
173
- }
174
- case 'doctor':
175
- console.log(cmd.cmdDoctor(store, idx(), config));
176
- break;
177
- default:
178
- console.error(`unknown command: ${command}\n`);
179
- console.log(HELP);
180
- process.exitCode = 1;
181
- }
182
- }
183
-
184
- try {
185
- main();
186
- } catch (err) {
187
- console.error(`error: ${(err as Error).message}`);
188
- process.exitCode = 1;
189
- }
@@ -1,40 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
-
4
- /** Locate a session envelope (`session.json`) by uuid under ~/.sessions/hosts/<host>/<month>/<uuid>/. */
5
- export function findSessionEnvelope(sessionsRoot: string, uuid: string): string | undefined {
6
- const hostsDir = join(sessionsRoot, 'hosts');
7
- if (!existsSync(hostsDir)) return undefined;
8
- for (const host of safeDirs(hostsDir)) {
9
- const hostDir = join(hostsDir, host);
10
- for (const month of safeDirs(hostDir)) {
11
- const candidate = join(hostDir, month, uuid, 'session.json');
12
- if (existsSync(candidate)) return candidate;
13
- }
14
- }
15
- return undefined;
16
- }
17
-
18
- function safeDirs(dir: string): string[] {
19
- try {
20
- return readdirSync(dir, { withFileTypes: true })
21
- .filter((e) => e.isDirectory())
22
- .map((e) => e.name);
23
- } catch {
24
- return [];
25
- }
26
- }
27
-
28
- /**
29
- * Add a planning_ref to a session envelope (idempotent). This is the session side of
30
- * the bidirectional link — the planning object's `linked_sessions` is the other side.
31
- */
32
- export function addPlanningRef(envelopePath: string, planningId: string): boolean {
33
- const env = JSON.parse(readFileSync(envelopePath, 'utf8')) as { planning_refs?: string[] };
34
- const refs = new Set(env.planning_refs ?? []);
35
- if (refs.has(planningId)) return false;
36
- refs.add(planningId);
37
- env.planning_refs = [...refs];
38
- writeFileSync(envelopePath, JSON.stringify(env, null, 2) + '\n', 'utf8');
39
- return true;
40
- }
@@ -1,67 +0,0 @@
1
- import { existsSync } from 'node:fs';
2
- import { Store } from '../store/store.ts';
3
- import { resolveKnowledgeRef, readTitle } from '../kb-bridge/kb-bridge.ts';
4
-
5
- export interface GraphNode {
6
- id: string;
7
- label: string;
8
- type: string; // domain|project|idea|plan|task|...|knowledge|session
9
- group: string;
10
- status?: string;
11
- blocked?: boolean;
12
- }
13
-
14
- export interface GraphEdge {
15
- from: string;
16
- to: string;
17
- kind: string;
18
- status?: string;
19
- }
20
-
21
- export interface GraphData {
22
- nodes: GraphNode[];
23
- edges: GraphEdge[];
24
- }
25
-
26
- /**
27
- * GraphData for the CSV webview: planning objects + knowledge targets (red when an
28
- * unresolved blocker) + session evidence nodes, connected by typed edges.
29
- */
30
- export function buildGraph(store: Store, kbRoot: string): GraphData {
31
- const nodes = new Map<string, GraphNode>();
32
- const edges: GraphEdge[] = [];
33
- const add = (n: GraphNode) => {
34
- if (!nodes.has(n.id)) nodes.set(n.id, n);
35
- };
36
-
37
- for (const d of store.all()) {
38
- const o = d.object as Record<string, unknown>;
39
- add({
40
- id: d.id,
41
- label: (o.title as string) ?? d.id,
42
- type: d.type,
43
- group: (o.domain as string) ?? (o.project as string) ?? d.type,
44
- status: (o.status as string) ?? undefined,
45
- });
46
- }
47
-
48
- for (const e of store.allEdges()) {
49
- if (e.kind === 'blocked_by' || e.kind === 'cites' || e.kind === 'implements') {
50
- const abs = resolveKnowledgeRef(e.to, kbRoot);
51
- const exists = existsSync(abs);
52
- add({
53
- id: e.to,
54
- label: (exists ? readTitle(abs) : undefined) ?? e.to.split('/').pop() ?? e.to,
55
- type: 'knowledge',
56
- group: 'knowledge',
57
- blocked: e.kind === 'blocked_by' && e.status !== 'resolved',
58
- });
59
- }
60
- if (e.kind === 'evidence' && e.from.startsWith('session:')) {
61
- add({ id: e.from, label: e.from.replace('session:', '').slice(0, 8), type: 'session', group: 'sessions' });
62
- }
63
- edges.push({ from: e.from, to: e.to, kind: e.kind, status: e.status });
64
- }
65
-
66
- return { nodes: [...nodes.values()], edges };
67
- }
@@ -1,91 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { Store } from '../store/store.ts';
6
- import { PlanningIndex, persistIndex } from './db.ts';
7
- import { buildGraph } from '../graph/graph.ts';
8
-
9
- let dir: string;
10
- let kb: string;
11
-
12
- function seed(): Store {
13
- const s = new Store(dir);
14
- s.write({ id: 'projects/cb', type: 'project', title: 'Code Build' });
15
- s.write({ id: 'ideas/mm', type: 'idea', title: 'Memory maps', status: 'accepted', project: 'projects/cb' });
16
- s.write({ id: 'ideas/raw', type: 'idea', title: 'Raw dump', status: 'capture' });
17
- s.write({
18
- id: 'plans/reloc',
19
- type: 'plan',
20
- title: 'Relocation decision',
21
- kind: 'life',
22
- status: 'plan',
23
- blocked_by: [{ path: 'knowledge/life/relocation/x.md', reason: 'tax compare', status: 'open' }],
24
- });
25
- s.write({ id: 'daily/2026-06-21', type: 'daily_plan', title: 'Today', date: '2026-06-21' });
26
- s.write({ id: 'tasks/scaffold', type: 'task', title: 'Scaffold repo', status: 'today', daily: 'daily/2026-06-21' });
27
- s.write({ id: 'tasks/review', type: 'task', title: 'Review brief', status: 'done', daily: 'daily/2026-06-21' });
28
- return s;
29
- }
30
-
31
- beforeEach(() => {
32
- dir = mkdtempSync(join(tmpdir(), 'kp-idx-'));
33
- kb = mkdtempSync(join(tmpdir(), 'kp-idxkb-'));
34
- mkdirSync(join(kb, 'knowledge/life/relocation'), { recursive: true });
35
- writeFileSync(join(kb, 'knowledge/life/relocation/x.md'), '# Reloc\n\nopen\n');
36
- });
37
- afterEach(() => {
38
- rmSync(dir, { recursive: true, force: true });
39
- rmSync(kb, { recursive: true, force: true });
40
- });
41
-
42
- describe('PlanningIndex', () => {
43
- it('projects objects and counts by type', () => {
44
- const idx = new PlanningIndex(seed());
45
- const counts = idx.counts();
46
- expect(counts.idea).toBe(2);
47
- expect(counts.task).toBe(2);
48
- expect(counts.plan).toBe(1);
49
- expect(idx.listByStatus('capture').map((r) => r.id)).toContain('ideas/raw');
50
- });
51
-
52
- it('searches titles', () => {
53
- const idx = new PlanningIndex(seed());
54
- expect(idx.search('memory').some((r) => r.id === 'ideas/mm')).toBe(true);
55
- });
56
-
57
- it('reports blocked_by knowledge edges with decision status', () => {
58
- const idx = new PlanningIndex(seed());
59
- const blocked = idx.blockedByKnowledge(kb);
60
- expect(blocked.length).toBe(1);
61
- expect(blocked[0]?.exists).toBe(true);
62
- expect(blocked[0]?.decided).toBe(false);
63
- });
64
-
65
- it('builds a daily board grouped by lane', () => {
66
- const idx = new PlanningIndex(seed());
67
- const board = idx.dailyBoard('2026-06-21');
68
- expect(board.daily_id).toBe('daily/2026-06-21');
69
- expect(board.lanes.today.map((r) => r.id)).toContain('tasks/scaffold');
70
- expect(board.lanes.done.map((r) => r.id)).toContain('tasks/review');
71
- });
72
-
73
- it('persists a file DB via reindex, idempotently (rebuild over existing fts)', () => {
74
- const dbPath = join(dir, '.index/planning.db');
75
- const res = persistIndex(seed(), dbPath);
76
- expect(existsSync(dbPath)).toBe(true);
77
- expect(res.objects).toBeGreaterThan(0);
78
- // second reindex over the same file must not throw on objects_fts
79
- expect(() => persistIndex(seed(), dbPath)).not.toThrow();
80
- });
81
- });
82
-
83
- describe('buildGraph', () => {
84
- it('includes planning + knowledge nodes and marks unresolved blockers', () => {
85
- const g = buildGraph(seed(), kb);
86
- expect(g.nodes.some((n) => n.id === 'ideas/mm' && n.type === 'idea')).toBe(true);
87
- const knode = g.nodes.find((n) => n.type === 'knowledge');
88
- expect(knode?.blocked).toBe(true);
89
- expect(g.edges.some((e) => e.kind === 'blocked_by')).toBe(true);
90
- });
91
- });
package/src/index/db.ts DELETED
@@ -1,196 +0,0 @@
1
- import { mkdirSync } from 'node:fs';
2
- import { dirname } from 'node:path';
3
- import { Store } from '../store/store.ts';
4
- import { inspectKnowledgeRef, type BlockedInspection } from '../kb-bridge/kb-bridge.ts';
5
- import { TASK_STATUS } from '../schema/entities.ts';
6
-
7
- // `node:sqlite` is a prefix-only builtin that bundler-based runners (vitest/vite)
8
- // fail to resolve through a static import (they strip `node:` and hunt for a bare
9
- // `sqlite` package). `process.getBuiltinModule` loads it directly under both the
10
- // node-run CLI and vitest. A minimal local interface keeps us off @types/node.
11
- interface SqliteStatement {
12
- run(...params: unknown[]): unknown;
13
- all(...params: unknown[]): unknown[];
14
- get(...params: unknown[]): unknown;
15
- }
16
- interface SqliteDB {
17
- exec(sql: string): void;
18
- prepare(sql: string): SqliteStatement;
19
- close(): void;
20
- }
21
- const getBuiltinModule = (globalThis as { process: { getBuiltinModule: (id: string) => unknown } }).process
22
- .getBuiltinModule;
23
- const { DatabaseSync } = getBuiltinModule('node:sqlite') as { DatabaseSync: new (path: string) => SqliteDB };
24
-
25
- export interface ObjectRow {
26
- id: string;
27
- type: string;
28
- title: string | null;
29
- status: string | null;
30
- domain: string | null;
31
- project: string | null;
32
- path: string;
33
- updated: string | null;
34
- }
35
-
36
- export interface DailyBoard {
37
- date: string;
38
- daily_id: string | null;
39
- body: string;
40
- lanes: Record<string, ObjectRow[]>;
41
- }
42
-
43
- /**
44
- * Read-side SQLite projection of the markdown store (node:sqlite, no native build).
45
- * Rebuilt from the store on construction, so it is always fresh and a clobber of the
46
- * persisted `.index/planning.db` is recoverable. FTS5 is used when the runtime has it,
47
- * else search degrades to LIKE.
48
- */
49
- export class PlanningIndex {
50
- readonly db: SqliteDB;
51
- readonly fts: boolean;
52
- private store: Store;
53
-
54
- constructor(store: Store, dbPath = ':memory:') {
55
- this.store = store;
56
- if (dbPath !== ':memory:') mkdirSync(dirname(dbPath), { recursive: true });
57
- this.db = new DatabaseSync(dbPath);
58
- this.fts = this.detectFts();
59
- this.build();
60
- }
61
-
62
- private detectFts(): boolean {
63
- try {
64
- this.db.exec('CREATE VIRTUAL TABLE __fts_probe USING fts5(x); DROP TABLE __fts_probe;');
65
- return true;
66
- } catch {
67
- return false;
68
- }
69
- }
70
-
71
- private build(): void {
72
- this.db.exec(`
73
- DROP TABLE IF EXISTS objects;
74
- DROP TABLE IF EXISTS edges;
75
- DROP TABLE IF EXISTS objects_fts;
76
- CREATE TABLE objects (
77
- id TEXT PRIMARY KEY, type TEXT, title TEXT, status TEXT,
78
- domain TEXT, project TEXT, path TEXT, updated TEXT
79
- );
80
- CREATE TABLE edges (kind TEXT, from_id TEXT, to_id TEXT, reason TEXT, status TEXT);
81
- CREATE INDEX idx_obj_type ON objects(type);
82
- CREATE INDEX idx_obj_status ON objects(status);
83
- CREATE INDEX idx_edge_kind ON edges(kind);
84
- `);
85
- if (this.fts) this.db.exec('CREATE VIRTUAL TABLE objects_fts USING fts5(id UNINDEXED, title, body);');
86
-
87
- const insObj = this.db.prepare(
88
- 'INSERT OR REPLACE INTO objects (id,type,title,status,domain,project,path,updated) VALUES (?,?,?,?,?,?,?,?)',
89
- );
90
- const insFts = this.fts ? this.db.prepare('INSERT INTO objects_fts (id,title,body) VALUES (?,?,?)') : null;
91
- for (const d of this.store.all()) {
92
- const o = d.object as Record<string, unknown>;
93
- insObj.run(
94
- d.id,
95
- d.type,
96
- (o.title as string) ?? null,
97
- (o.status as string) ?? null,
98
- (o.domain as string) ?? null,
99
- (o.project as string) ?? null,
100
- d.relpath,
101
- (o.updated as string) ?? null,
102
- );
103
- insFts?.run(d.id, (o.title as string) ?? '', d.body ?? '');
104
- }
105
-
106
- const insEdge = this.db.prepare('INSERT INTO edges (kind,from_id,to_id,reason,status) VALUES (?,?,?,?,?)');
107
- for (const e of this.store.allEdges()) insEdge.run(e.kind, e.from, e.to, e.reason ?? null, e.status ?? null);
108
- }
109
-
110
- close(): void {
111
- this.db.close();
112
- }
113
-
114
- // --- Queries -------------------------------------------------------------
115
-
116
- all(): ObjectRow[] {
117
- return this.db.prepare('SELECT * FROM objects ORDER BY type, id').all() as unknown as ObjectRow[];
118
- }
119
-
120
- byType(type: string): ObjectRow[] {
121
- return this.db.prepare('SELECT * FROM objects WHERE type = ? ORDER BY id').all(type) as unknown as ObjectRow[];
122
- }
123
-
124
- listByStatus(status: string): ObjectRow[] {
125
- return this.db.prepare('SELECT * FROM objects WHERE status = ? ORDER BY type, id').all(status) as unknown as ObjectRow[];
126
- }
127
-
128
- counts(): Record<string, number> {
129
- const rows = this.db.prepare('SELECT type, COUNT(*) n FROM objects GROUP BY type').all() as Array<{
130
- type: string;
131
- n: number;
132
- }>;
133
- return Object.fromEntries(rows.map((r) => [r.type, r.n]));
134
- }
135
-
136
- /** All `blocked_by` knowledge edges, inspected against the KB for decision status. */
137
- blockedByKnowledge(kbRoot: string): BlockedInspection[] {
138
- const out: BlockedInspection[] = [];
139
- for (const d of this.store.all()) {
140
- for (const ref of d.object.blocked_by ?? []) out.push(inspectKnowledgeRef(d.id, ref, kbRoot));
141
- }
142
- return out;
143
- }
144
-
145
- /** Tasks linked to a day, grouped by lane, plus the daily plan body. */
146
- dailyBoard(date: string): DailyBoard {
147
- const dailyId = `daily/${date}`;
148
- const daily = this.store.get(dailyId);
149
- const tasks = this.store
150
- .byType('task')
151
- .filter((t) => {
152
- const o = t.object as Record<string, unknown>;
153
- return o.daily === dailyId || o.scheduled_for === dailyId;
154
- })
155
- .map((t) => this.rowOf(t.id))
156
- .filter((r): r is ObjectRow => !!r);
157
- const lanes: Record<string, ObjectRow[]> = {};
158
- for (const lane of TASK_STATUS) lanes[lane] = [];
159
- for (const t of tasks) (lanes[t.status ?? 'inbox'] ??= []).push(t);
160
- return { date, daily_id: daily ? dailyId : null, body: daily?.body ?? '', lanes };
161
- }
162
-
163
- rowOf(id: string): ObjectRow | undefined {
164
- return this.db.prepare('SELECT * FROM objects WHERE id = ?').get(id) as unknown as ObjectRow | undefined;
165
- }
166
-
167
- search(q: string): ObjectRow[] {
168
- if (this.fts) {
169
- try {
170
- const ids = this.db.prepare('SELECT id FROM objects_fts WHERE objects_fts MATCH ? LIMIT 50').all(q) as Array<{
171
- id: string;
172
- }>;
173
- if (ids.length === 0) return [];
174
- const placeholders = ids.map(() => '?').join(',');
175
- return this.db
176
- .prepare(`SELECT * FROM objects WHERE id IN (${placeholders})`)
177
- .all(...ids.map((r) => r.id)) as unknown as ObjectRow[];
178
- } catch {
179
- /* fall through to LIKE on malformed MATCH query */
180
- }
181
- }
182
- const like = `%${q}%`;
183
- return this.db
184
- .prepare('SELECT * FROM objects WHERE title LIKE ? OR id LIKE ? ORDER BY type, id LIMIT 50')
185
- .all(like, like) as unknown as ObjectRow[];
186
- }
187
- }
188
-
189
- /** Persist the projection to a file DB (the `reindex` command). */
190
- export function persistIndex(store: Store, dbPath: string): { objects: number; edges: number } {
191
- const idx = new PlanningIndex(store, dbPath);
192
- const objects = store.all().length;
193
- const edges = store.allEdges().length;
194
- idx.close();
195
- return { objects, edges };
196
- }
package/src/index.ts DELETED
@@ -1,14 +0,0 @@
1
- // Public API of @unpolarize/knowledge-planning — consumed by the CLI, agent skills,
2
- // and the Code Sessions VS Code planning mode.
3
- export * from './schema/index.ts';
4
- export * from './types.ts';
5
- export { Store, walkMarkdown } from './store/store.ts';
6
- export type { StoreDoc } from './store/store.ts';
7
- export { PlanningIndex, persistIndex } from './index/db.ts';
8
- export type { ObjectRow, DailyBoard } from './index/db.ts';
9
- export { buildGraph } from './graph/graph.ts';
10
- export type { GraphData, GraphNode, GraphEdge } from './graph/graph.ts';
11
- export * from './kb-bridge/kb-bridge.ts';
12
- export * from './migrate/gdoc.ts';
13
- export { findSessionEnvelope, addPlanningRef } from './cli/sessions.ts';
14
- export * as commands from './cli/commands.ts';
@@ -1,44 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { inspectKnowledgeRef, readTitle, hasDecision } from './kb-bridge.ts';
6
-
7
- let kb: string;
8
- beforeEach(() => {
9
- kb = mkdtempSync(join(tmpdir(), 'kp-kb-'));
10
- mkdirSync(join(kb, 'knowledge/life/relocation'), { recursive: true });
11
- writeFileSync(join(kb, 'knowledge/life/relocation/x.md'), '# Reloc research\n\nopen questions remain\n');
12
- writeFileSync(join(kb, 'knowledge/life/relocation/y.md'), '---\ndecision: chose SLC\n---\n# Y\n');
13
- writeFileSync(join(kb, 'knowledge/life/relocation/z.md'), '# Z\n\n## Decision\n\nGo with Coimbra.\n');
14
- });
15
- afterEach(() => rmSync(kb, { recursive: true, force: true }));
16
-
17
- describe('kb-bridge', () => {
18
- it('reads titles from heading', () => {
19
- expect(readTitle(join(kb, 'knowledge/life/relocation/x.md'))).toBe('Reloc research');
20
- });
21
-
22
- it('detects decision via frontmatter and via ## Decision section', () => {
23
- expect(hasDecision(join(kb, 'knowledge/life/relocation/x.md'))).toBe(false);
24
- expect(hasDecision(join(kb, 'knowledge/life/relocation/y.md'))).toBe(true);
25
- expect(hasDecision(join(kb, 'knowledge/life/relocation/z.md'))).toBe(true);
26
- });
27
-
28
- it('marks a blocked_by edge resolvable only once the KB records a decision', () => {
29
- const open = inspectKnowledgeRef('plans/reloc', { path: 'knowledge/life/relocation/x.md', status: 'open' }, kb);
30
- expect(open.exists).toBe(true);
31
- expect(open.decided).toBe(false);
32
- expect(open.resolvable).toBe(false);
33
-
34
- const decided = inspectKnowledgeRef('plans/reloc', { path: 'knowledge/life/relocation/y.md', status: 'open' }, kb);
35
- expect(decided.decided).toBe(true);
36
- expect(decided.resolvable).toBe(true);
37
- });
38
-
39
- it('flags missing KB targets', () => {
40
- const miss = inspectKnowledgeRef('plans/x', { path: 'knowledge/nope.md', status: 'open' }, kb);
41
- expect(miss.exists).toBe(false);
42
- expect(miss.resolvable).toBe(false);
43
- });
44
- });