@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.
Files changed (50) hide show
  1. package/README.md +2 -2
  2. package/dist/init.d.ts +33 -7
  3. package/dist/init.d.ts.map +1 -1
  4. package/dist/init.js +75 -48
  5. package/dist/init.js.map +1 -1
  6. package/dist/kb/cli.d.ts.map +1 -1
  7. package/dist/kb/cli.js +53 -11
  8. package/dist/kb/cli.js.map +1 -1
  9. package/dist/kb/fold.d.ts +25 -1
  10. package/dist/kb/fold.d.ts.map +1 -1
  11. package/dist/kb/fold.js +39 -0
  12. package/dist/kb/fold.js.map +1 -1
  13. package/dist/kb/repair.d.ts +36 -0
  14. package/dist/kb/repair.d.ts.map +1 -0
  15. package/dist/kb/repair.js +116 -0
  16. package/dist/kb/repair.js.map +1 -0
  17. package/dist/kb/search.d.ts +3 -2
  18. package/dist/kb/search.d.ts.map +1 -1
  19. package/dist/kb/search.js +97 -7
  20. package/dist/kb/search.js.map +1 -1
  21. package/dist/kb/session-worklist.d.ts +10 -0
  22. package/dist/kb/session-worklist.d.ts.map +1 -0
  23. package/dist/kb/session-worklist.js +92 -0
  24. package/dist/kb/session-worklist.js.map +1 -0
  25. package/dist/kb/view-template.d.ts +4 -3
  26. package/dist/kb/view-template.d.ts.map +1 -1
  27. package/dist/kb/view-template.js +4 -3
  28. package/dist/kb/view-template.js.map +1 -1
  29. package/dist/kb/view.d.ts +4 -1
  30. package/dist/kb/view.d.ts.map +1 -1
  31. package/dist/kb/view.js +15 -2
  32. package/dist/kb/view.js.map +1 -1
  33. package/dist/kb/write-guide.d.ts +43 -1
  34. package/dist/kb/write-guide.d.ts.map +1 -1
  35. package/dist/kb/write-guide.js +69 -25
  36. package/dist/kb/write-guide.js.map +1 -1
  37. package/dist/kb/write.d.ts.map +1 -1
  38. package/dist/kb/write.js +10 -1
  39. package/dist/kb/write.js.map +1 -1
  40. package/dist/server/mcp.d.ts +8 -1
  41. package/dist/server/mcp.d.ts.map +1 -1
  42. package/dist/server/mcp.js +39 -5
  43. package/dist/server/mcp.js.map +1 -1
  44. package/dist/unwire.d.ts.map +1 -1
  45. package/dist/unwire.js +5 -2
  46. package/dist/unwire.js.map +1 -1
  47. package/hooks/capture-payload.mjs +73 -13
  48. package/hooks/note-shape.d.mts +61 -0
  49. package/hooks/note-shape.mjs +287 -0
  50. 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cstart/coldstart",
3
- "version": "2.2.9",
3
+ "version": "2.2.11",
4
4
  "mcpName": "io.github.AkashGoenka/coldstart",
5
5
  "publishConfig": {
6
6
  "access": "public"