@cstart/coldstart 2.2.9 → 2.2.11
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/README.md +2 -2
- package/dist/init.d.ts +33 -7
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +75 -48
- package/dist/init.js.map +1 -1
- package/dist/kb/cli.d.ts.map +1 -1
- package/dist/kb/cli.js +53 -11
- package/dist/kb/cli.js.map +1 -1
- package/dist/kb/fold.d.ts +25 -1
- package/dist/kb/fold.d.ts.map +1 -1
- package/dist/kb/fold.js +39 -0
- package/dist/kb/fold.js.map +1 -1
- package/dist/kb/repair.d.ts +36 -0
- package/dist/kb/repair.d.ts.map +1 -0
- package/dist/kb/repair.js +116 -0
- package/dist/kb/repair.js.map +1 -0
- package/dist/kb/search.d.ts +3 -2
- package/dist/kb/search.d.ts.map +1 -1
- package/dist/kb/search.js +97 -7
- package/dist/kb/search.js.map +1 -1
- package/dist/kb/session-worklist.d.ts +10 -0
- package/dist/kb/session-worklist.d.ts.map +1 -0
- package/dist/kb/session-worklist.js +92 -0
- package/dist/kb/session-worklist.js.map +1 -0
- package/dist/kb/view-template.d.ts +4 -3
- package/dist/kb/view-template.d.ts.map +1 -1
- package/dist/kb/view-template.js +4 -3
- package/dist/kb/view-template.js.map +1 -1
- package/dist/kb/view.d.ts +4 -1
- package/dist/kb/view.d.ts.map +1 -1
- package/dist/kb/view.js +15 -2
- package/dist/kb/view.js.map +1 -1
- package/dist/kb/write-guide.d.ts +43 -1
- package/dist/kb/write-guide.d.ts.map +1 -1
- package/dist/kb/write-guide.js +69 -25
- package/dist/kb/write-guide.js.map +1 -1
- package/dist/kb/write.d.ts.map +1 -1
- package/dist/kb/write.js +10 -1
- package/dist/kb/write.js.map +1 -1
- package/dist/server/mcp.d.ts +8 -1
- package/dist/server/mcp.d.ts.map +1 -1
- package/dist/server/mcp.js +39 -5
- package/dist/server/mcp.js.map +1 -1
- package/dist/unwire.d.ts.map +1 -1
- package/dist/unwire.js +5 -2
- package/dist/unwire.js.map +1 -1
- package/hooks/capture-payload.mjs +73 -13
- package/hooks/note-shape.d.mts +61 -0
- package/hooks/note-shape.mjs +287 -0
- package/package.json +1 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* note-shape.mjs — THE single source of truth for what a notebook note must
|
|
3
|
+
* carry to be findable, and the one place the spec shapes are written down.
|
|
4
|
+
*
|
|
5
|
+
* WHY IT EXISTS: this list used to be hand-maintained in three places — the
|
|
6
|
+
* `kb write` guide, the capture prompt's inlined shapes, and the MCP kb_write
|
|
7
|
+
* tool description — and they drifted, silently, for months. The capture
|
|
8
|
+
* prompt's flow shape lost "aliases" and every flow written from it went into
|
|
9
|
+
* the notebook reachable only by its exact title. The MCP description has never
|
|
10
|
+
* mentioned "aliases" at all, which is why notebooks driven by no-shell clients
|
|
11
|
+
* are the worst affected. Nothing ever checked, because there was nothing to
|
|
12
|
+
* check against. Now there is: every surface renders from the tables below, and
|
|
13
|
+
* `tests/kb-shape-parity.test.ts` fails if a consumer stops doing so.
|
|
14
|
+
*
|
|
15
|
+
* WHY IT LIVES IN hooks/ AND NOT src/: the capture hooks are plain .mjs that
|
|
16
|
+
* run straight from the package with no build step and never import from dist/.
|
|
17
|
+
* The TypeScript side already reaches into hooks/ for exactly this reason (see
|
|
18
|
+
* lint.ts and hooks/ignore.mjs), so putting the table here is what lets ONE file
|
|
19
|
+
* serve the hook, the CLI, and the MCP server. Types are in note-shape.d.mts.
|
|
20
|
+
*
|
|
21
|
+
* ADDING A REQUIREMENT LATER (the forward-ready path the design asks for):
|
|
22
|
+
* append to NOTE_CHECKS. Every consumer picks it up — the guide text, the
|
|
23
|
+
* capture prompt, the MCP description, the write-time warning, and `kb repair`'s
|
|
24
|
+
* detection. Never delete a `check` id that has shipped: `kb repair --json`
|
|
25
|
+
* consumers filter by it, so retire one by making its predicate always return
|
|
26
|
+
* false rather than by removing the entry.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Bumped when the shape of a `kb repair --json` finding changes, never when a
|
|
30
|
+
* check is added or retired — consumers filter by `check` and ignore unknowns. */
|
|
31
|
+
export const REPAIR_CONTRACT_VERSION = 1;
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// The spec shapes — rendered by the write guide, the capture prompt, and the
|
|
35
|
+
// MCP tool description. One example string per spec type, used verbatim by all
|
|
36
|
+
// three, so the copies cannot disagree about a field.
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export const SPEC_SHAPES = [
|
|
40
|
+
{
|
|
41
|
+
spec: 'file-single',
|
|
42
|
+
noteType: 'file',
|
|
43
|
+
headline: 'file (single purpose — the DEFAULT for file notes):',
|
|
44
|
+
example: `
|
|
45
|
+
{"type":"file-single","path":"src/x.py",
|
|
46
|
+
"summary":"its one purpose + how (1-3 sentences)",
|
|
47
|
+
"aliases":["symptom or search words"],
|
|
48
|
+
"anchors":[{"path":"src/x.py","symbols":["TheFnYouWorkedWith"]}]}`,
|
|
49
|
+
/** Prose the full (guide) rendering adds; the compact rendering drops it. */
|
|
50
|
+
note: `
|
|
51
|
+
"symbols" is how a note answers a search for the NAME of a thing. Agents
|
|
52
|
+
search kb with identifiers far more than with prose, and those land in the
|
|
53
|
+
anchor channel — which holds nothing but the path unless you fill it. Name
|
|
54
|
+
the symbols you actually worked with; omit only for a file that declares
|
|
55
|
+
none (config, css, markdown).`,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
spec: 'file-hub',
|
|
59
|
+
noteType: 'file',
|
|
60
|
+
headline: `
|
|
61
|
+
file (hub — ONLY for grab-bag files with NO single purpose: models.py,
|
|
62
|
+
utils, helpers. One facet PER SYMBOL you worked with. Touching many
|
|
63
|
+
symbols does not make a file a hub; a single-purpose file stays
|
|
64
|
+
file-single):`,
|
|
65
|
+
example: `
|
|
66
|
+
{"type":"file-hub","path":"src/y.py","aliases":["search words"],
|
|
67
|
+
"facets":[{"symbol":"ClassOrFn","detail":"the non-obvious thing about THIS symbol",
|
|
68
|
+
"flows":["<flow id or the flow's exact title>"]}]}`,
|
|
69
|
+
note: '',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
spec: 'flow',
|
|
73
|
+
noteType: 'flow',
|
|
74
|
+
headline: 'flow (product-level mechanism — see the capture checklist\'s gate):',
|
|
75
|
+
example: `
|
|
76
|
+
{"type":"flow","title":"how X happens","aliases":["other words for X"],
|
|
77
|
+
"summary":"first sentence = the product-level fact the file notes miss",
|
|
78
|
+
"steps":[{"path":"src/a.py","symbols":["entry"],"role":"receives the request"}],
|
|
79
|
+
"invariants":["what must hold"],"verified":["src/a.py"]}`,
|
|
80
|
+
note: `
|
|
81
|
+
"steps" is the chain. "verified" is what FILES the note — anchors come from
|
|
82
|
+
"verified" (plus any explicit "anchors"), NOT from the steps. List in
|
|
83
|
+
"verified" every step file you actually opened this session, or the note is
|
|
84
|
+
not filed at those addresses: kb lookup on them returns nothing and they are
|
|
85
|
+
never freshness-stamped. Do not reuse the file-note shape (title + summary)
|
|
86
|
+
from memory for a flow.`,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
spec: 'lesson',
|
|
90
|
+
noteType: 'lesson',
|
|
91
|
+
headline: 'lesson (confirmed ABSENCE only — one file/symbol facts are facets, not lessons):',
|
|
92
|
+
example: `
|
|
93
|
+
{"type":"lesson","kind":"absence","title":"no retry logic in this repo",
|
|
94
|
+
"body":"what you looked for + that it is not there",
|
|
95
|
+
"scope":{"terms":["search","terms"]}}`,
|
|
96
|
+
note: '',
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// The requirements — one entry per way a note can end up unfindable.
|
|
102
|
+
//
|
|
103
|
+
// Each carries TWO predicates on purpose. `missingInSpec` runs at write time on
|
|
104
|
+
// the JSON the agent just sent; `missingInNote` runs in `kb repair` on the
|
|
105
|
+
// folded note as it exists on disk. They read different shapes and cannot be
|
|
106
|
+
// collapsed into one, so they sit adjacent — a change to what "missing" means
|
|
107
|
+
// has to be made in both, in the same edit, in view of each other.
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
/** A folded note as `kb repair` sees it (subset of KbNote used by the checks). */
|
|
111
|
+
const noteAliases = (n) => (n.aliases ?? []).filter(Boolean);
|
|
112
|
+
|
|
113
|
+
export const NOTE_CHECKS = [
|
|
114
|
+
{
|
|
115
|
+
check: 'missing-aliases',
|
|
116
|
+
field: 'aliases',
|
|
117
|
+
/** Folded note types this applies to. */
|
|
118
|
+
noteTypes: ['file', 'flow'],
|
|
119
|
+
/** Spec types this applies to at write time. */
|
|
120
|
+
specTypes: ['file', 'file-single', 'file-hub', 'flow'],
|
|
121
|
+
why: '"aliases" — the words someone would SEARCH for this; without them the note is reachable only by its exact title',
|
|
122
|
+
/** What the agent has to do about it, in `kb repair`'s worklist. */
|
|
123
|
+
repairHint:
|
|
124
|
+
'Open the note and name 2-6 phrases someone would type when they hit this — SYMPTOMS and '
|
|
125
|
+
+ 'the words in the code (identifiers, error strings), not restatements of the title.',
|
|
126
|
+
fix: () => '"aliases":["2-5 word search keys"]',
|
|
127
|
+
missingInSpec: (s) => !(Array.isArray(s.aliases) && s.aliases.filter(Boolean).length),
|
|
128
|
+
missingInNote: (n) => !noteAliases(n).length,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
// `kb write` REJECTS an absence lesson with no scope.terms, so this can only
|
|
132
|
+
// fire on notes already on disk from before that validation. Kept for them.
|
|
133
|
+
check: 'missing-scope-terms',
|
|
134
|
+
field: 'scope.terms',
|
|
135
|
+
noteTypes: ['lesson'],
|
|
136
|
+
specTypes: ['lesson'],
|
|
137
|
+
why: '"scope.terms" — a lesson has no anchors, so these terms are its only retrieval surface',
|
|
138
|
+
repairHint:
|
|
139
|
+
'Add the terms you would search for to conclude the thing is absent. The keeper re-runs '
|
|
140
|
+
+ 'them, so they double as the tripwire that flags the lesson when the absence ends.',
|
|
141
|
+
fix: () => '"scope":{"terms":["what you searched for"]}',
|
|
142
|
+
missingInSpec: (s) => !(Array.isArray(s.scope?.terms) && s.scope.terms.filter(Boolean).length),
|
|
143
|
+
missingInNote: (n) => !(n.scope?.terms ?? []).filter(Boolean).length,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
check: 'file-no-symbols',
|
|
147
|
+
field: 'anchors[].symbols',
|
|
148
|
+
/** A hub reaches the same channel through its facets — write.ts unions every
|
|
149
|
+
* facet symbol into the anchor — so name the field it actually writes. */
|
|
150
|
+
fieldBySpec: { 'file-hub': 'facets[].symbol' },
|
|
151
|
+
noteTypes: ['file'],
|
|
152
|
+
specTypes: ['file', 'file-single', 'file-hub'],
|
|
153
|
+
why:
|
|
154
|
+
'"anchors[].symbols" (or "facets[].symbol" on a hub) — the identifiers you worked with, if '
|
|
155
|
+
+ 'this file declares any; agents search kb by NAME far more than by prose, and those '
|
|
156
|
+
+ 'queries only match this channel',
|
|
157
|
+
repairHint:
|
|
158
|
+
'Run `coldstart gs <path>` and anchor the symbols the note is actually ABOUT — not every '
|
|
159
|
+
+ 'symbol the file declares. A file that declares none (css, markdown, config) is fine as is.',
|
|
160
|
+
fix: (ctx) => `"anchors":[{"path":"${ctx.path ?? '<path>'}","symbols":["TheFnYouWorkedWith"]}]`,
|
|
161
|
+
missingInSpec: (s) => !(
|
|
162
|
+
(Array.isArray(s.anchors)
|
|
163
|
+
&& s.anchors.some((a) => Array.isArray(a?.symbols) && a.symbols.filter(Boolean).length))
|
|
164
|
+
|| (Array.isArray(s.facets) && s.facets.some((f) => f?.symbol))),
|
|
165
|
+
missingInNote: (n) => !(n.anchors ?? []).some((a) => (a.symbols ?? []).filter(Boolean).length),
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
check: 'flow-no-steps',
|
|
169
|
+
field: 'steps',
|
|
170
|
+
noteTypes: ['flow'],
|
|
171
|
+
specTypes: ['flow'],
|
|
172
|
+
why:
|
|
173
|
+
'"steps" — the chain that makes a flow a flow; without it the note is a title and a '
|
|
174
|
+
+ 'paragraph, and there is nothing for a reader to follow into the code',
|
|
175
|
+
repairHint:
|
|
176
|
+
'Re-read the chain and record each hop as {path, symbols, role}. Then list in "verified" '
|
|
177
|
+
+ 'the files you opened doing it — that is what files the note.',
|
|
178
|
+
fix: () => '"steps":[{"path":"src/a.py","symbols":["entry"],"role":"receives the request"}]',
|
|
179
|
+
/** Spec-side this is judged against the FOLDED result, not the spec: an
|
|
180
|
+
* update that touches only aliases legitimately omits steps and the fold
|
|
181
|
+
* keeps the ones already there. See stepsMissingAfterWrite below. */
|
|
182
|
+
missingInSpec: () => false,
|
|
183
|
+
missingInNote: (n) => !(n.steps ?? []).length,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
check: 'flow-steps-unanchored',
|
|
187
|
+
field: 'verified',
|
|
188
|
+
noteTypes: ['flow'],
|
|
189
|
+
/** Named in the shapes and the tool description so writers list it up front,
|
|
190
|
+
* but never warned about at write time — a spec's `verified` is a claim
|
|
191
|
+
* about what the agent read, and the write path has no way to falsify it.
|
|
192
|
+
* It is caught later, on the folded note, by `kb repair`. */
|
|
193
|
+
specTypes: ['flow'],
|
|
194
|
+
why:
|
|
195
|
+
'"verified" does not cover every step file — a flow is FILED at its verified paths, so a '
|
|
196
|
+
+ 'step file missing from them is invisible to `kb lookup` on that file and is never '
|
|
197
|
+
+ 'freshness-stamped, even though the note describes what it does',
|
|
198
|
+
repairHint:
|
|
199
|
+
'Open each unfiled step file and confirm the step is real. Re-put the note with those '
|
|
200
|
+
+ 'paths in "verified" — only the ones you actually read. Leaving a step unfiled is a fine '
|
|
201
|
+
+ 'answer for a file the flow only mentions in passing; say so and move on.',
|
|
202
|
+
fix: (ctx) => `"verified":[${(ctx.unanchored ?? []).map((p) => `"${p}"`).join(',')}]`,
|
|
203
|
+
missingInSpec: () => false,
|
|
204
|
+
missingInNote: (n) => unanchoredSteps(n).length > 0,
|
|
205
|
+
/** Extra context this check contributes to its finding. */
|
|
206
|
+
context: (n) => ({ unanchored: unanchoredSteps(n) }),
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
/** Step paths the note is not filed at. Anchors come from `verified` + explicit
|
|
211
|
+
* `anchors`; steps are only the story. */
|
|
212
|
+
export function unanchoredSteps(n) {
|
|
213
|
+
const anchored = new Set((n.anchors ?? []).map((a) => a.path));
|
|
214
|
+
const seen = new Set();
|
|
215
|
+
const out = [];
|
|
216
|
+
for (const s of n.steps ?? []) {
|
|
217
|
+
if (!s?.path || anchored.has(s.path) || seen.has(s.path)) continue;
|
|
218
|
+
seen.add(s.path);
|
|
219
|
+
out.push(s.path);
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** A flow write leaves the note stepless. Judged on the FOLDED note (passed in
|
|
225
|
+
* as `after`) because the spec alone cannot tell an alias-only update from a
|
|
226
|
+
* flow written without a chain. */
|
|
227
|
+
export function stepsMissingAfterWrite(spec, after) {
|
|
228
|
+
if (!spec || spec.type !== 'flow' || spec.op === 'retract') return false;
|
|
229
|
+
if (after) return !(after.steps ?? []).length;
|
|
230
|
+
return !(Array.isArray(spec.steps) && spec.steps.length);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Renderers — every surface goes through one of these.
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
/** Checks that apply to a given SPEC type (what the writer just sent). */
|
|
238
|
+
export function checksForSpec(specType) {
|
|
239
|
+
return NOTE_CHECKS.filter((c) => c.specTypes.includes(specType));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Checks that apply to a given folded NOTE type. */
|
|
243
|
+
export function checksForNote(noteType) {
|
|
244
|
+
return NOTE_CHECKS.filter((c) => c.noteTypes.includes(noteType));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The spec shapes as a block of text.
|
|
249
|
+
* `compact` drops the explanatory prose (the capture prompt pays per token and
|
|
250
|
+
* carries its own rules section); both renderings emit the SAME examples.
|
|
251
|
+
*/
|
|
252
|
+
export function shapesBlock({ compact = false } = {}) {
|
|
253
|
+
return SPEC_SHAPES.map((s) => {
|
|
254
|
+
const example = indent(dedent(s.example), compact ? ' ' : ' ');
|
|
255
|
+
if (compact) return example;
|
|
256
|
+
return `${indent(dedent(s.headline), ' ')}\n${example}`
|
|
257
|
+
+ (s.note ? `\n${indent(dedent(s.note), ' ')}` : '');
|
|
258
|
+
}).join('\n\n');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Template literals in this file are written at their source indentation and
|
|
262
|
+
* open with a newline, so every line — including the first — carries the same
|
|
263
|
+
* prefix. Strip the common prefix and RELATIVE indentation survives, which is
|
|
264
|
+
* what makes the continuation lines of a JSON example line up under it. */
|
|
265
|
+
function dedent(text) {
|
|
266
|
+
const lines = text.split('\n');
|
|
267
|
+
while (lines.length && !lines[0].trim()) lines.shift();
|
|
268
|
+
while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
|
|
269
|
+
const pad = Math.min(...lines.filter((l) => l.trim()).map((l) => l.match(/^ */)[0].length), Infinity);
|
|
270
|
+
return Number.isFinite(pad) ? lines.map((l) => l.slice(pad)).join('\n') : lines.join('\n');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function indent(text, prefix) {
|
|
274
|
+
return text.split('\n').map((l) => (l.trim() ? prefix + l : l)).join('\n');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** One line naming the fields a note cannot be retrieved without — for the MCP
|
|
278
|
+
* tool description and anywhere else that needs the rule without the shapes. */
|
|
279
|
+
export function requiredFieldsLine() {
|
|
280
|
+
const byType = SPEC_SHAPES.map((s) => {
|
|
281
|
+
const fields = checksForSpec(s.spec).map((c) => c.fieldBySpec?.[s.spec] ?? c.field);
|
|
282
|
+
return `${s.spec}: ${fields.join(' + ')}`;
|
|
283
|
+
});
|
|
284
|
+
return `REQUIRED for the note to be findable at all — ${byType.join('; ')}. `
|
|
285
|
+
+ `A note missing these is written but unreachable: aliases are the only search surface besides `
|
|
286
|
+
+ `the exact title, and anchor symbols are the only channel that answers a query typed as an identifier.`;
|
|
287
|
+
}
|