@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/LICENSE +21 -0
- package/README.md +109 -62
- package/dist/chunk-6GL2IYUE.js +2020 -0
- package/dist/cli.js +385 -0
- package/dist/index.d.ts +4532 -0
- package/dist/index.js +94 -0
- package/dist/server-NXAKSD5Z.js +862 -0
- package/package.json +19 -9
- package/src/cli/commands.test.ts +0 -105
- package/src/cli/commands.ts +0 -341
- package/src/cli/index.ts +0 -189
- package/src/cli/sessions.ts +0 -40
- package/src/graph/graph.ts +0 -67
- package/src/index/db.test.ts +0 -91
- package/src/index/db.ts +0 -196
- package/src/index.ts +0 -14
- package/src/kb-bridge/kb-bridge.test.ts +0 -44
- package/src/kb-bridge/kb-bridge.ts +0 -63
- package/src/migrate/gdoc.test.ts +0 -28
- package/src/migrate/gdoc.ts +0 -68
- package/src/schema/entities.test.ts +0 -60
- package/src/schema/entities.ts +0 -233
- package/src/schema/frontmatter.ts +0 -44
- package/src/schema/index.ts +0 -2
- package/src/store/store.test.ts +0 -64
- package/src/store/store.ts +0 -231
- package/src/types.ts +0 -18
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unpolarize/knowledge-planning",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "KP (Knowledge Planning) \u2014 a durable, git-backed planning CLI and library over a markdown SSOT (tasks, ideas, plans, thoughts; kanban, calendar, sessions links). Point it at any store directory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"publishConfig": {
|
|
@@ -12,30 +12,40 @@
|
|
|
12
12
|
"url": "git+https://github.com/unpolarize/knowledge-planning.git"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"
|
|
16
|
-
"README.md"
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
17
18
|
],
|
|
18
19
|
"bin": {
|
|
19
|
-
"knowledge-planning": "./
|
|
20
|
-
"kp": "./
|
|
20
|
+
"knowledge-planning": "./dist/cli.js",
|
|
21
|
+
"kp": "./dist/cli.js"
|
|
21
22
|
},
|
|
22
23
|
"exports": {
|
|
23
|
-
".":
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
}
|
|
24
28
|
},
|
|
25
29
|
"scripts": {
|
|
26
30
|
"kp": "node ./src/cli/index.ts",
|
|
27
31
|
"test": "vitest run",
|
|
28
32
|
"test:watch": "vitest",
|
|
29
|
-
"typecheck": "tsc --noEmit"
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"prepublishOnly": "npm run build"
|
|
30
36
|
},
|
|
31
37
|
"dependencies": {
|
|
32
38
|
"yaml": "^2.9.0",
|
|
33
39
|
"zod": "^3.25.0"
|
|
34
40
|
},
|
|
35
41
|
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.20.0",
|
|
43
|
+
"tsup": "^8.5.1",
|
|
44
|
+
"typescript": "^5.9.3",
|
|
36
45
|
"vitest": "^2.1.8"
|
|
37
46
|
},
|
|
38
47
|
"engines": {
|
|
39
48
|
"node": ">=22"
|
|
40
|
-
}
|
|
49
|
+
},
|
|
50
|
+
"types": "./dist/index.d.ts"
|
|
41
51
|
}
|
package/src/cli/commands.test.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync } 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 } from '../index/db.ts';
|
|
7
|
-
import * as cmd from './commands.ts';
|
|
8
|
-
|
|
9
|
-
let dir: string;
|
|
10
|
-
let kb: string;
|
|
11
|
-
let sessions: string;
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
dir = mkdtempSync(join(tmpdir(), 'kp-cmd-'));
|
|
15
|
-
kb = mkdtempSync(join(tmpdir(), 'kp-cmdkb-'));
|
|
16
|
-
sessions = mkdtempSync(join(tmpdir(), 'kp-cmdsess-'));
|
|
17
|
-
});
|
|
18
|
-
afterEach(() => {
|
|
19
|
-
for (const d of [dir, kb, sessions]) rmSync(d, { recursive: true, force: true });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe('cli commands', () => {
|
|
23
|
-
it('captures an idea', () => {
|
|
24
|
-
const s = new Store(dir);
|
|
25
|
-
const res = cmd.cmdCapture(s, 'Memory maps in CB', 'tech');
|
|
26
|
-
expect(res.id).toBe('ideas/memory-maps-in-cb');
|
|
27
|
-
expect(new Store(dir).get(res.id)?.object.status).toBe('capture');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('promotes an idea to a plan and marks lineage', () => {
|
|
31
|
-
const s = new Store(dir);
|
|
32
|
-
cmd.cmdCapture(s, 'Knowledge planning MVP', 'tech');
|
|
33
|
-
const res = cmd.cmdPromote(s, 'ideas/knowledge-planning-mvp');
|
|
34
|
-
const fresh = new Store(dir);
|
|
35
|
-
expect(fresh.get(res.planId)?.type).toBe('plan');
|
|
36
|
-
expect(fresh.get('ideas/knowledge-planning-mvp')?.data.status).toBe('done');
|
|
37
|
-
expect(fresh.get('ideas/knowledge-planning-mvp')?.data.promoted_to).toBe(res.planId);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('links a session both ways (linked_sessions + planning_refs)', () => {
|
|
41
|
-
const s = new Store(dir);
|
|
42
|
-
s.write({ id: 'ideas/mm', type: 'idea', title: 'MM' });
|
|
43
|
-
const uuid = '4992a4a4-5632-4048-a55e-ab2be5857f2e';
|
|
44
|
-
const envDir = join(sessions, 'hosts/h1/2026-06', uuid);
|
|
45
|
-
mkdirSync(envDir, { recursive: true });
|
|
46
|
-
writeFileSync(
|
|
47
|
-
join(envDir, 'session.json'),
|
|
48
|
-
JSON.stringify({ schema: 'session-store/session@1', session_id: uuid, host: 'h1', agent: 'claude-code' }),
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const res = cmd.cmdLinkSession(s, sessions, 'ideas/mm', uuid);
|
|
52
|
-
expect(res.envelopeUpdated).toBe(true);
|
|
53
|
-
|
|
54
|
-
const env = JSON.parse(readFileSync(join(envDir, 'session.json'), 'utf8'));
|
|
55
|
-
expect(env.planning_refs).toContain('ideas/mm');
|
|
56
|
-
expect(new Store(dir).get('ideas/mm')?.object.linked_sessions).toContain(uuid);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('baselines on first run, then ingests only new substantial bullets', () => {
|
|
60
|
-
const s = new Store(dir);
|
|
61
|
-
const base = '- brainstorm a thing\n- [ ] call the contractor\n';
|
|
62
|
-
// first run: no prior snapshot → baseline, ingest nothing (never flood a brain-dump)
|
|
63
|
-
const b = cmd.cmdIngestGdoc(s, base, { prevText: null });
|
|
64
|
-
expect(b.baselined).toBe(true);
|
|
65
|
-
expect(b.created.length).toBe(0);
|
|
66
|
-
// next run: one new substantial bullet added → only it ingests ('tiny' filtered by minLen)
|
|
67
|
-
const r = cmd.cmdIngestGdoc(s, base + '- a brand new idea here\n- tiny\n', { prevText: base, domain: 'tech' });
|
|
68
|
-
expect(r.baselined).toBe(false);
|
|
69
|
-
expect(r.created).toEqual(['ideas/a-brand-new-idea-here']);
|
|
70
|
-
expect(new Store(dir).get('ideas/a-brand-new-idea-here')?.object.status).toBe('capture');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('caps new captures per run', () => {
|
|
74
|
-
const s = new Store(dir);
|
|
75
|
-
const lines = Array.from({ length: 10 }, (_, i) => `- new substantial bullet number ${i}`).join('\n');
|
|
76
|
-
const r = cmd.cmdIngestGdoc(s, lines, { prevText: '', max: 3 });
|
|
77
|
-
expect(r.created.length).toBe(3);
|
|
78
|
-
expect(r.capped).toBe(7);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('ingests insight bullets into an Insight object', () => {
|
|
82
|
-
const s = new Store(dir);
|
|
83
|
-
const res = cmd.cmdInsightIngest(s, '- ship the daemon\n- [ ] email outreach\n', 'daily-digest', 'raw/x.md');
|
|
84
|
-
expect(res.suggested).toBe(2);
|
|
85
|
-
expect(new Store(dir).get(res.id)?.type).toBe('insight');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('renders list / daily / blocked / graph as strings', () => {
|
|
89
|
-
const s = new Store(dir);
|
|
90
|
-
s.write({ id: 'ideas/a', type: 'idea', title: 'A', status: 'capture' });
|
|
91
|
-
s.write({ id: 'daily/2026-06-21', type: 'daily_plan', date: '2026-06-21' }, '## Top 3\n- [ ] ship');
|
|
92
|
-
s.write({ id: 'tasks/t', type: 'task', title: 'Ship', status: 'today', daily: 'daily/2026-06-21' });
|
|
93
|
-
s.write({
|
|
94
|
-
id: 'plans/r',
|
|
95
|
-
type: 'plan',
|
|
96
|
-
title: 'R',
|
|
97
|
-
blocked_by: [{ path: 'knowledge/x.md', reason: 'decide' }],
|
|
98
|
-
});
|
|
99
|
-
const idx = new PlanningIndex(s);
|
|
100
|
-
expect(cmd.cmdList(s, { status: 'capture' })).toContain('ideas/a');
|
|
101
|
-
expect(cmd.cmdDaily(s, idx, '2026-06-21', false)).toContain('Ship');
|
|
102
|
-
expect(cmd.cmdBlocked(idx, kb)).toContain('plans/r');
|
|
103
|
-
expect(cmd.cmdGraph(s, kb, true)).toContain('"nodes"');
|
|
104
|
-
});
|
|
105
|
-
});
|
package/src/cli/commands.ts
DELETED
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
import { Store } from '../store/store.ts';
|
|
3
|
-
import { PlanningIndex, persistIndex } from '../index/db.ts';
|
|
4
|
-
import { buildGraph } from '../graph/graph.ts';
|
|
5
|
-
import { extractBullets, toSeeds, slugify } from '../migrate/gdoc.ts';
|
|
6
|
-
import { findSessionEnvelope, addPlanningRef } from './sessions.ts';
|
|
7
|
-
import { inspectKnowledgeRef, resolveKnowledgeRef, readTitle } from '../kb-bridge/kb-bridge.ts';
|
|
8
|
-
import { existsSync } from 'node:fs';
|
|
9
|
-
import type { Config } from '../types.ts';
|
|
10
|
-
|
|
11
|
-
export const today = (): string => new Date().toISOString().slice(0, 10);
|
|
12
|
-
|
|
13
|
-
export interface ListOpts {
|
|
14
|
-
status?: string;
|
|
15
|
-
type?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function cmdList(store: Store, opts: ListOpts): string {
|
|
19
|
-
let rows = store.all();
|
|
20
|
-
if (opts.type) rows = rows.filter((d) => d.type === opts.type);
|
|
21
|
-
if (opts.status) rows = rows.filter((d) => (d.object as { status?: string }).status === opts.status);
|
|
22
|
-
if (rows.length === 0) return '(no matching objects)';
|
|
23
|
-
return rows
|
|
24
|
-
.sort((a, b) => a.type.localeCompare(b.type) || a.id.localeCompare(b.id))
|
|
25
|
-
.map((d) => {
|
|
26
|
-
const o = d.object as { title?: string; status?: string };
|
|
27
|
-
return `${pad(d.type, 12)} ${pad(o.status ?? '-', 10)} ${d.id}${o.title ? ` — ${o.title}` : ''}`;
|
|
28
|
-
})
|
|
29
|
-
.join('\n');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function cmdDaily(store: Store, idx: PlanningIndex, date: string, carryOver: boolean): string {
|
|
33
|
-
const board = idx.dailyBoard(date);
|
|
34
|
-
const lines: string[] = [`# Daily — ${date}`];
|
|
35
|
-
if (!board.daily_id) lines.push(`(no daily plan file at daily/${date}; run /planning-daily or 'kp capture')`);
|
|
36
|
-
if (board.body.trim()) lines.push('', board.body.trim());
|
|
37
|
-
lines.push('', '## Tasks by lane');
|
|
38
|
-
for (const [lane, rows] of Object.entries(board.lanes)) {
|
|
39
|
-
if (rows.length === 0) continue;
|
|
40
|
-
lines.push(` ${lane}:`);
|
|
41
|
-
for (const r of rows) lines.push(` - [${r.status === 'done' ? 'x' : ' '}] ${r.title ?? r.id} (${r.id})`);
|
|
42
|
-
}
|
|
43
|
-
if (carryOver) {
|
|
44
|
-
const prev = previousDay(date);
|
|
45
|
-
const prevBoard = idx.dailyBoard(prev);
|
|
46
|
-
const unfinished = [...prevBoard.lanes.today, ...prevBoard.lanes.in_progress, ...prevBoard.lanes.inbox];
|
|
47
|
-
if (unfinished.length) {
|
|
48
|
-
lines.push('', `## Carry-over from ${prev}`);
|
|
49
|
-
for (const r of unfinished) lines.push(` - ${r.title ?? r.id} (${r.id})`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return lines.join('\n');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function cmdBlocked(idx: PlanningIndex, kbRoot: string): string {
|
|
56
|
-
const blocked = idx.blockedByKnowledge(kbRoot).filter((b) => b.status !== 'resolved');
|
|
57
|
-
if (blocked.length === 0) return '(nothing blocked by knowledge)';
|
|
58
|
-
return blocked
|
|
59
|
-
.map((b) => {
|
|
60
|
-
const flag = !b.exists ? 'MISSING' : b.resolvable ? 'RESOLVABLE (decision recorded)' : 'open';
|
|
61
|
-
return `${flag.padEnd(30)} ${b.id}\n → ${b.path}${b.reason ? `\n reason: ${b.reason}` : ''}`;
|
|
62
|
-
})
|
|
63
|
-
.join('\n');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function cmdGraph(store: Store, kbRoot: string, json: boolean): string {
|
|
67
|
-
const g = buildGraph(store, kbRoot);
|
|
68
|
-
if (json) return JSON.stringify(g, null, 2);
|
|
69
|
-
return `graph: ${g.nodes.length} nodes, ${g.edges.length} edges\n by type: ${countBy(g.nodes.map((n) => n.type))}`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function cmdReindex(store: Store, root: string): string {
|
|
73
|
-
const res = persistIndex(store, join(root, '.index/planning.db'));
|
|
74
|
-
const warnings = store.getWarnings();
|
|
75
|
-
return `reindexed → ${join(root, '.index/planning.db')}\n ${res.objects} objects, ${res.edges} edges${
|
|
76
|
-
warnings.length ? `\n ${warnings.length} warning(s):\n ${warnings.join('\n ')}` : ''
|
|
77
|
-
}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface CaptureResult {
|
|
81
|
-
id: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function cmdCapture(store: Store, text: string, domain?: string): CaptureResult {
|
|
85
|
-
const id = uniqueId(store, `ideas/${slugify(text)}`);
|
|
86
|
-
store.write({ id, type: 'idea', title: text, status: 'capture', domain, created: today() });
|
|
87
|
-
return { id };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface PromoteResult {
|
|
91
|
-
ideaId: string;
|
|
92
|
-
planId: string;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function cmdPromote(store: Store, ideaId: string, toId?: string): PromoteResult {
|
|
96
|
-
const idea = store.get(ideaId);
|
|
97
|
-
if (!idea || idea.type !== 'idea') throw new Error(`not an idea: ${ideaId}`);
|
|
98
|
-
const o = idea.object as {
|
|
99
|
-
title?: string;
|
|
100
|
-
domain?: string;
|
|
101
|
-
project?: string;
|
|
102
|
-
blocked_by?: unknown[];
|
|
103
|
-
cites?: string[];
|
|
104
|
-
};
|
|
105
|
-
const planId = toId ?? uniqueId(store, `plans/${ideaId.split('/').pop()}`);
|
|
106
|
-
const isLife = o.domain && o.domain !== 'tech';
|
|
107
|
-
store.write(
|
|
108
|
-
{
|
|
109
|
-
id: planId,
|
|
110
|
-
type: 'plan',
|
|
111
|
-
title: o.title ?? planId,
|
|
112
|
-
status: 'plan',
|
|
113
|
-
kind: isLife ? 'life' : 'dev',
|
|
114
|
-
domain: o.domain,
|
|
115
|
-
project: o.project,
|
|
116
|
-
promoted_from: ideaId,
|
|
117
|
-
blocked_by: o.blocked_by ?? [],
|
|
118
|
-
cites: o.cites ?? [],
|
|
119
|
-
created: today(),
|
|
120
|
-
},
|
|
121
|
-
`# ${o.title ?? planId}\n\n## Goal\n\n_(promoted from [[${ideaId}]])_\n\n## Steps\n\n- [ ] …\n`,
|
|
122
|
-
);
|
|
123
|
-
store.patch(ideaId, (d) => {
|
|
124
|
-
d.status = 'done';
|
|
125
|
-
d.promoted_to = planId;
|
|
126
|
-
});
|
|
127
|
-
store.appendEdge({ kind: 'promoted_to', from: ideaId, to: planId, created_at: new Date().toISOString() });
|
|
128
|
-
return { ideaId, planId };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export interface LinkResult {
|
|
132
|
-
objectId: string;
|
|
133
|
-
uuid: string;
|
|
134
|
-
envelopeUpdated: boolean;
|
|
135
|
-
envelopePath?: string;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function cmdLinkSession(store: Store, sessionsRoot: string, objectId: string, uuid: string): LinkResult {
|
|
139
|
-
const obj = store.get(objectId);
|
|
140
|
-
if (!obj) throw new Error(`object not found: ${objectId}`);
|
|
141
|
-
store.patch(objectId, (d) => {
|
|
142
|
-
const refs = new Set((d.linked_sessions as string[]) ?? []);
|
|
143
|
-
refs.add(uuid);
|
|
144
|
-
d.linked_sessions = [...refs];
|
|
145
|
-
});
|
|
146
|
-
store.appendEdge({ kind: 'evidence', from: `session:${uuid}`, to: objectId, created_at: new Date().toISOString() });
|
|
147
|
-
const envelopePath = findSessionEnvelope(sessionsRoot, uuid);
|
|
148
|
-
let envelopeUpdated = false;
|
|
149
|
-
if (envelopePath) envelopeUpdated = addPlanningRef(envelopePath, objectId);
|
|
150
|
-
return { objectId, uuid, envelopeUpdated, envelopePath };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export interface InsightResult {
|
|
154
|
-
id: string;
|
|
155
|
-
suggested: number;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Ingest digest/brief bullets (a markdown file) into an Insight with suggested tasks. */
|
|
159
|
-
export function cmdInsightIngest(store: Store, text: string, source: string, sourceRef?: string): InsightResult {
|
|
160
|
-
const bullets = extractBullets(text).filter((b) => b.text.length > 3);
|
|
161
|
-
const date = today();
|
|
162
|
-
const id = uniqueId(store, `insights/${date}-${slugify(source)}`);
|
|
163
|
-
const suggested = bullets.map((b) => b.text);
|
|
164
|
-
store.write(
|
|
165
|
-
{
|
|
166
|
-
id,
|
|
167
|
-
type: 'insight',
|
|
168
|
-
title: `${source} — ${date}`,
|
|
169
|
-
source,
|
|
170
|
-
source_ref: sourceRef,
|
|
171
|
-
generated_at: new Date().toISOString(),
|
|
172
|
-
status: 'new',
|
|
173
|
-
},
|
|
174
|
-
`# ${source} — ${date}\n\n## Suggested\n\n${suggested.map((s) => `- [ ] ${s}`).join('\n')}\n`,
|
|
175
|
-
);
|
|
176
|
-
return { id, suggested: suggested.length };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Rich detail for one object: body + resolved references + parent/children + lineage. */
|
|
180
|
-
export function cmdShow(store: Store, kbRoot: string, id: string): unknown {
|
|
181
|
-
const doc = store.get(id);
|
|
182
|
-
if (!doc) throw new Error(`object not found: ${id}`);
|
|
183
|
-
const o = doc.object as Record<string, unknown>;
|
|
184
|
-
const all = store.all();
|
|
185
|
-
const ref = (oid: string) => {
|
|
186
|
-
const d = store.get(oid);
|
|
187
|
-
return d ? { id: d.id, type: d.type, title: (d.object as { title?: string }).title ?? d.id, status: (d.object as { status?: string }).status } : { id: oid, missing: true };
|
|
188
|
-
};
|
|
189
|
-
const cites = (o.cites as string[] | undefined ?? []).map((p) => {
|
|
190
|
-
const abs = resolveKnowledgeRef(p, kbRoot);
|
|
191
|
-
return { path: p, exists: existsSync(abs), title: existsSync(abs) ? readTitle(abs) : undefined };
|
|
192
|
-
});
|
|
193
|
-
const blocked_by = (doc.object.blocked_by ?? []).map((r) => inspectKnowledgeRef(id, r, kbRoot));
|
|
194
|
-
// children: objects that point at this one as their project or plan parent
|
|
195
|
-
const children = all
|
|
196
|
-
.filter((d) => {
|
|
197
|
-
const c = d.object as Record<string, unknown>;
|
|
198
|
-
return d.id !== id && (c.project === id || c.plan === id || c.daily === id);
|
|
199
|
-
})
|
|
200
|
-
.map((d) => ref(d.id));
|
|
201
|
-
const parentId = (o.plan as string) || (o.project as string) || undefined;
|
|
202
|
-
return {
|
|
203
|
-
id: doc.id,
|
|
204
|
-
type: doc.type,
|
|
205
|
-
relpath: doc.relpath,
|
|
206
|
-
path: doc.path,
|
|
207
|
-
frontmatter: o,
|
|
208
|
-
title: o.title ?? doc.id,
|
|
209
|
-
status: o.status,
|
|
210
|
-
domain: o.domain,
|
|
211
|
-
body: doc.body,
|
|
212
|
-
cites,
|
|
213
|
-
blocked_by,
|
|
214
|
-
depends_on: (o.depends_on as string[] | undefined ?? []).map(ref),
|
|
215
|
-
related: (o.related as string[] | undefined ?? []).map(ref),
|
|
216
|
-
linked_sessions: (o.linked_sessions as string[] | undefined ?? []),
|
|
217
|
-
parent: parentId ? ref(parentId) : null,
|
|
218
|
-
children,
|
|
219
|
-
promoted_to: o.promoted_to ? ref(o.promoted_to as string) : null,
|
|
220
|
-
promoted_from: o.promoted_from ? ref(o.promoted_from as string) : null,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** One-shot JSON snapshot for external consumers (the CSV planning mode). */
|
|
225
|
-
export function cmdExport(store: Store, idx: PlanningIndex, config: Config, date: string): unknown {
|
|
226
|
-
return {
|
|
227
|
-
generated_at: new Date().toISOString(),
|
|
228
|
-
root: store.root,
|
|
229
|
-
kb_root: config.kb_root,
|
|
230
|
-
sessions_root: config.sessions_root,
|
|
231
|
-
counts: idx.counts(),
|
|
232
|
-
objects: idx.all(),
|
|
233
|
-
inbox: idx.listByStatus('capture'),
|
|
234
|
-
board: idx.dailyBoard(date),
|
|
235
|
-
blocked: idx.blockedByKnowledge(config.kb_root).filter((b) => b.status !== 'resolved'),
|
|
236
|
-
graph: buildGraph(store, config.kb_root),
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** Set an object's status (board lane moves, idea accept/park). */
|
|
241
|
-
export function cmdSetStatus(store: Store, id: string, status: string): void {
|
|
242
|
-
if (!store.get(id)) throw new Error(`object not found: ${id}`);
|
|
243
|
-
store.patch(id, (d) => {
|
|
244
|
-
d.status = status;
|
|
245
|
-
d.updated = today();
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export interface IngestResult {
|
|
250
|
-
created: string[];
|
|
251
|
-
skipped: number;
|
|
252
|
-
newBullets: number;
|
|
253
|
-
baselined: boolean;
|
|
254
|
-
capped: number;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Ingest the synced Google-Doc todo mirror into the planning inbox: checkbox items →
|
|
259
|
-
* inbox tasks, plain bullets → capture ideas.
|
|
260
|
-
*
|
|
261
|
-
* Safe by construction for a large brain-dump outline (the real doc is thousands of
|
|
262
|
-
* nested bullets), per the design's "diff triggers append of NEW bullets, human approves":
|
|
263
|
-
* - **Diff-based:** ingests only bullets absent from `prevText` (the prior snapshot).
|
|
264
|
-
* - **Baseline first run:** when `prevText` is null (no snapshot yet), ingest nothing —
|
|
265
|
-
* just establish the baseline so a years-long doc never floods the store.
|
|
266
|
-
* - **Filtered:** only substantial (`minLen`) top-level bullets (`depth <= 2`).
|
|
267
|
-
* - **Capped:** at most `max` new captures per run (the rest wait for the next run).
|
|
268
|
-
* - **Deduped:** an id that already exists is skipped.
|
|
269
|
-
* Captures land as `capture`/`inbox` (triage state); the human promotes/parks them.
|
|
270
|
-
*/
|
|
271
|
-
export function cmdIngestGdoc(
|
|
272
|
-
store: Store,
|
|
273
|
-
text: string,
|
|
274
|
-
opts: { domain?: string; source?: string; prevText?: string | null; max?: number; minLen?: number } = {},
|
|
275
|
-
): IngestResult {
|
|
276
|
-
const minLen = opts.minLen ?? 12;
|
|
277
|
-
const max = opts.max ?? 25;
|
|
278
|
-
const source = opts.source ?? 'gdoc';
|
|
279
|
-
if (opts.prevText == null) {
|
|
280
|
-
return { created: [], skipped: 0, newBullets: 0, baselined: true, capped: 0 };
|
|
281
|
-
}
|
|
282
|
-
const prevSet = new Set(extractBullets(opts.prevText).map((b) => b.text));
|
|
283
|
-
const fresh = extractBullets(text).filter((b) => b.text.length >= minLen && b.depth <= 2 && !prevSet.has(b.text));
|
|
284
|
-
const seeds = toSeeds(fresh.slice(0, max), { domain: opts.domain, source });
|
|
285
|
-
const created: string[] = [];
|
|
286
|
-
let skipped = 0;
|
|
287
|
-
for (const seed of seeds) {
|
|
288
|
-
if (store.get(seed.id)) {
|
|
289
|
-
skipped++;
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
store.write(
|
|
293
|
-
{ id: seed.id, type: seed.type, title: seed.title, status: seed.status, domain: seed.domain, source: seed.source, created: today() },
|
|
294
|
-
seed.title,
|
|
295
|
-
);
|
|
296
|
-
created.push(seed.id);
|
|
297
|
-
}
|
|
298
|
-
return { created, skipped, newBullets: fresh.length, baselined: false, capped: Math.max(0, fresh.length - seeds.length) };
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
export function cmdDoctor(store: Store, idx: PlanningIndex, config: Config): string {
|
|
302
|
-
const counts = idx.counts();
|
|
303
|
-
const blocked = idx.blockedByKnowledge(config.kb_root).filter((b) => b.status !== 'resolved');
|
|
304
|
-
const warnings = store.getWarnings();
|
|
305
|
-
return [
|
|
306
|
-
`store root: ${store.root}`,
|
|
307
|
-
`kb_root: ${config.kb_root}`,
|
|
308
|
-
`sessions: ${config.sessions_root}`,
|
|
309
|
-
`objects: ${Object.entries(counts).map(([k, v]) => `${k}=${v}`).join(' ') || '(none)'}`,
|
|
310
|
-
`fts5: ${idx.fts ? 'yes' : 'no (LIKE fallback)'}`,
|
|
311
|
-
`blocked: ${blocked.length} (${blocked.filter((b) => b.resolvable).length} resolvable, ${blocked.filter((b) => !b.exists).length} missing)`,
|
|
312
|
-
`warnings: ${warnings.length}${warnings.length ? '\n ' + warnings.join('\n ') : ''}`,
|
|
313
|
-
].join('\n');
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// --- helpers ---------------------------------------------------------------
|
|
317
|
-
|
|
318
|
-
function pad(s: string, n: number): string {
|
|
319
|
-
return (s ?? '').padEnd(n).slice(0, n);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function countBy(items: string[]): string {
|
|
323
|
-
const m: Record<string, number> = {};
|
|
324
|
-
for (const i of items) m[i] = (m[i] ?? 0) + 1;
|
|
325
|
-
return Object.entries(m)
|
|
326
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
327
|
-
.join(' ');
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function uniqueId(store: Store, base: string): string {
|
|
331
|
-
if (!store.get(base)) return base;
|
|
332
|
-
let n = 2;
|
|
333
|
-
while (store.get(`${base}-${n}`)) n++;
|
|
334
|
-
return `${base}-${n}`;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function previousDay(date: string): string {
|
|
338
|
-
const d = new Date(date + 'T00:00:00Z');
|
|
339
|
-
d.setUTCDate(d.getUTCDate() - 1);
|
|
340
|
-
return d.toISOString().slice(0, 10);
|
|
341
|
-
}
|