@gmickel/gno 1.8.0 → 1.9.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.
Files changed (41) hide show
  1. package/README.md +6 -2
  2. package/assets/skill/SKILL.md +13 -0
  3. package/assets/skill/cli-reference.md +9 -0
  4. package/assets/skill/examples.md +21 -0
  5. package/assets/skill/mcp-reference.md +6 -0
  6. package/package.json +1 -1
  7. package/src/cli/commands/capture.ts +9 -6
  8. package/src/cli/commands/index-cmd.ts +17 -6
  9. package/src/cli/commands/shared.ts +17 -1
  10. package/src/cli/commands/update.ts +17 -6
  11. package/src/config/content-types.ts +140 -0
  12. package/src/config/defaults.ts +1 -0
  13. package/src/config/index.ts +14 -0
  14. package/src/config/loader.ts +11 -2
  15. package/src/config/types.ts +37 -1
  16. package/src/core/config-mutation.ts +14 -2
  17. package/src/core/note-presets.ts +61 -5
  18. package/src/ingestion/frontmatter.ts +77 -2
  19. package/src/ingestion/index.ts +2 -0
  20. package/src/ingestion/sync-options.ts +29 -0
  21. package/src/ingestion/sync.ts +104 -16
  22. package/src/ingestion/types.ts +14 -0
  23. package/src/mcp/tools/add-collection.ts +8 -5
  24. package/src/mcp/tools/capture.ts +5 -2
  25. package/src/mcp/tools/index-cmd.ts +13 -7
  26. package/src/mcp/tools/index.ts +8 -9
  27. package/src/mcp/tools/sync.ts +12 -6
  28. package/src/mcp/tools/workspace-write.ts +16 -10
  29. package/src/pipeline/hybrid.ts +2 -0
  30. package/src/pipeline/search.ts +2 -0
  31. package/src/pipeline/types.ts +2 -0
  32. package/src/pipeline/vsearch.ts +4 -0
  33. package/src/sdk/client.ts +51 -24
  34. package/src/serve/background-runtime.ts +18 -4
  35. package/src/serve/config-sync.ts +9 -2
  36. package/src/serve/routes/api.ts +50 -24
  37. package/src/serve/watch-service.ts +12 -2
  38. package/src/store/migrations/009-content-type-rule-fingerprint.ts +36 -0
  39. package/src/store/migrations/index.ts +12 -1
  40. package/src/store/sqlite/adapter.ts +29 -14
  41. package/src/store/types.ts +6 -0
package/README.md CHANGED
@@ -97,7 +97,11 @@ gno daemon --detach # headless continuous indexing (background; --status / --st
97
97
 
98
98
  - **Second-brain capture**: `gno capture`, REST `/api/capture`, SDK
99
99
  `client.capture()`, MCP `gno_capture`, and Web UI Quick Capture write
100
- provenance-rich notes from text, stdin, or files
100
+ provenance-rich notes from text, stdin, or files, including typed presets for
101
+ ideas, people, company/projects, and meetings
102
+ - **Schema-lite content types**: optional `contentTypes` rules map configured
103
+ frontmatter `type` values or path prefixes to canonical `contentType` metadata
104
+ in JSON search/query results
101
105
  - **Publish to [gno.sh](https://gno.sh/publish)**: new `gno publish export` CLI and Web UI action produce a self-contained artifact you upload to the hosted reader — public, secret, invite-only, or locally encrypted before upload
102
106
  - **Retrieval Quality Upgrade**: stronger BM25 lexical handling, code-aware chunking, terminal result hyperlinks, and per-collection model overrides
103
107
  - **Code Embedding Benchmarks**: new benchmark workflow across canonical, real-GNO, and pinned OSS slices for comparing alternate embedding models
@@ -546,7 +550,7 @@ Open `http://localhost:3000` to:
546
550
  - **Browse**: Cross-collection tree workspace with folder detail panes and per-tab browse context
547
551
  - **Edit**: Create, edit, and delete documents with live preview
548
552
  - **Create in place**: New notes in the current folder/collection with presets and command-palette flows
549
- - **Capture with provenance**: `gno capture` and Web UI Quick Capture write quick notes to an editable collection with structured `source:` metadata and a receipt that separates write, sync, and embed state
553
+ - **Capture with provenance**: `gno capture` and Web UI Quick Capture write quick notes to an editable collection with structured `source:` metadata, typed preset scaffolds, and a receipt that separates write, sync, and embed state
550
554
  - **Same capture contract everywhere**: CLI, MCP `gno_capture`, REST `/api/capture`, SDK `client.capture()`, and Web UI Quick Capture return the same provenance receipt shape
551
555
  - **Ask**: AI-powered Q&A with citations
552
556
  - **Manage Collections**: Add, remove, and re-index collections
@@ -214,8 +214,21 @@ Use `gno capture` for quick second-brain writes into an editable collection:
214
214
  ```bash
215
215
  gno capture "thought to remember"
216
216
  gno capture --file ./clip.md --source-url https://example.com --source-kind web --json
217
+ gno capture --preset person --title "Jane Doe" --folder people/
218
+ gno capture --preset meeting --title "Weekly sync" --folder meetings/
217
219
  ```
218
220
 
221
+ Preset IDs: `blank`, `project-note`, `research-note`, `decision-note`,
222
+ `prompt-pattern`, `source-summary`, `idea-original`, `person`,
223
+ `company-project`, `meeting`.
224
+
225
+ For second-brain pages, prefer the typed presets:
226
+
227
+ - `idea-original`: exact idea phrasing, context, related concepts, publish potential.
228
+ - `person`: current state, relationship, assessment, open threads, timeline.
229
+ - `company-project`: state, changes, decisions, people, timeline.
230
+ - `meeting`: synthesis/action analysis above `## Timeline`; raw notes below.
231
+
219
232
  The JSON receipt reports write, sync, and embed status separately. Generated
220
233
  captures land under `inbox/YYYY-MM-DD/capture-<body-hash>.md` unless `--path`,
221
234
  `--folder`, or `--title` overrides the path. Capture does not imply embedding
@@ -124,6 +124,15 @@ gno capture --file ./clip.md --source-url https://example.com --source-kind web
124
124
  gno capture "meeting note" --quiet
125
125
  ```
126
126
 
127
+ Preset IDs: `blank`, `project-note`, `research-note`, `decision-note`,
128
+ `prompt-pattern`, `source-summary`, `idea-original`, `person`,
129
+ `company-project`, `meeting`.
130
+
131
+ Second-brain presets keep current synthesis above `## Timeline` and dated
132
+ evidence below it. Use `idea-original` for exact idea wording, `person` for
133
+ relationship/current-state notes, `company-project` for organizations or active
134
+ workstreams, and `meeting` for analysis above transcript/raw notes/action items.
135
+
127
136
  Important behavior:
128
137
 
129
138
  - Inline content, `--stdin`, and `--file` are mutually exclusive.
@@ -359,6 +359,27 @@ gno capture "thought to remember"
359
359
  gno capture --file ./clip.md --source-url https://example.com --source-kind web --json
360
360
  ```
361
361
 
362
+ ### Typed second-brain presets
363
+
364
+ ```bash
365
+ # Original idea: preserve exact phrasing and related concepts
366
+ gno capture --preset idea-original --title "Local-first inbox triage" --folder ideas/
367
+
368
+ # Person page: current synthesis plus relationship/open threads/timeline
369
+ gno capture --preset person --title "Jane Doe" --folder people/
370
+
371
+ # Company/project page: state, decisions, people, and timeline
372
+ gno capture --preset company-project --title "Acme renewal" --folder projects/
373
+
374
+ # Meeting page: analysis above raw notes, transcript, and action items
375
+ gno capture --preset meeting --title "Weekly sync" --folder meetings/
376
+ ```
377
+
378
+ For these presets, keep current synthesis above `## Timeline`; put dated evidence
379
+ and raw chronology below it. If `contentTypes` are configured in `index.yml`,
380
+ matching frontmatter `type` or path prefixes become `contentType` in JSON search
381
+ results.
382
+
362
383
  ### Web UI quick capture
363
384
 
364
385
  Press **N** in `gno serve`, write the note, and open **Source** only when you
@@ -76,6 +76,12 @@ frontmatter and returns the same provenance receipt shape as CLI, REST, and SDK
76
76
  capture, plus legacy MCP fields (`docid`, `absPath`, `overwritten`,
77
77
  `serverInstanceId`).
78
78
 
79
+ `presetId` accepts `blank`, `project-note`, `research-note`, `decision-note`,
80
+ `prompt-pattern`, `source-summary`, `idea-original`, `person`,
81
+ `company-project`, or `meeting`. The typed second-brain presets use flat
82
+ frontmatter (`type`, `category`, `tags`) and a synthesis/timeline page pattern;
83
+ provenance still comes from the capture `source` fields, not the preset.
84
+
79
85
  Use `collisionPolicy: "open_existing"` to return an existing note without
80
86
  rewriting, `create_with_suffix` to create the next available path, or legacy
81
87
  `overwrite: true` to replace the target path. Capture content must be text, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
@@ -26,7 +26,7 @@ import {
26
26
  } from "../../core/capture";
27
27
  import { writeCapturePlanFile } from "../../core/capture-write";
28
28
  import { withWriteLock } from "../../core/file-lock";
29
- import { defaultSyncService } from "../../ingestion";
29
+ import { defaultSyncService, withContentTypeRules } from "../../ingestion";
30
30
  import { CliError } from "../errors";
31
31
  import { initStore } from "./shared";
32
32
 
@@ -140,7 +140,7 @@ export async function capture(
140
140
  throw new CliError("VALIDATION", storeInit.error);
141
141
  }
142
142
 
143
- const { store, collections } = storeInit;
143
+ const { store, collections, config } = storeInit;
144
144
  try {
145
145
  const collectionName = options.collection?.trim();
146
146
  const collection = collectionName
@@ -222,10 +222,13 @@ export async function capture(
222
222
  collection,
223
223
  store,
224
224
  [plan.relPath],
225
- {
226
- runUpdateCmd: false,
227
- gitPull: false,
228
- }
225
+ withContentTypeRules(
226
+ {
227
+ runUpdateCmd: false,
228
+ gitPull: false,
229
+ },
230
+ config
231
+ )
229
232
  );
230
233
  const syncResult = syncResults[0];
231
234
  const docResult = await store.getDocument(collection.name, plan.relPath);
@@ -5,7 +5,11 @@
5
5
  * @module src/cli/commands/indexCmd
6
6
  */
7
7
 
8
- import { defaultSyncService, type SyncResult } from "../../ingestion";
8
+ import {
9
+ defaultSyncService,
10
+ type SyncResult,
11
+ withContentTypeRules,
12
+ } from "../../ingestion";
9
13
  import { formatSyncResultLines, initStore } from "./shared";
10
14
 
11
15
  /**
@@ -55,14 +59,21 @@ export async function index(options: IndexOptions = {}): Promise<IndexResult> {
55
59
  return { success: false, error: initResult.error };
56
60
  }
57
61
 
58
- const { store, collections } = initResult;
62
+ const { store, collections, config } = initResult;
59
63
 
60
64
  try {
61
65
  // Run sync service (update phase)
62
- const syncResult = await defaultSyncService.syncAll(collections, store, {
63
- gitPull: options.gitPull,
64
- runUpdateCmd: true,
65
- });
66
+ const syncResult = await defaultSyncService.syncAll(
67
+ collections,
68
+ store,
69
+ withContentTypeRules(
70
+ {
71
+ gitPull: options.gitPull,
72
+ runUpdateCmd: true,
73
+ },
74
+ config
75
+ )
76
+ );
66
77
 
67
78
  // Embedding phase
68
79
  const embedSkipped = options.noEmbed ?? false;
@@ -10,7 +10,12 @@ import type { SyncResult } from "../../ingestion";
10
10
  import type { SearchResults } from "../../pipeline/types";
11
11
 
12
12
  import { decorateUriForIndex, getIndexDbPath } from "../../app/constants";
13
- import { getConfigPaths, isInitialized, loadConfig } from "../../config";
13
+ import {
14
+ getConfigPaths,
15
+ isInitialized,
16
+ loadConfig,
17
+ writeConfigWarningsToStderr,
18
+ } from "../../config";
14
19
  import { SqliteAdapter } from "../../store/sqlite/adapter";
15
20
 
16
21
  /**
@@ -61,6 +66,7 @@ export async function initStore(
61
66
  if (!configResult.ok) {
62
67
  return { ok: false, error: configResult.error.message };
63
68
  }
69
+ writeConfigWarningsToStderr(configResult.warnings);
64
70
  const config = configResult.value;
65
71
 
66
72
  // Filter to single collection if specified
@@ -165,6 +171,16 @@ export function formatSyncResultLines(
165
171
  lines.push(` [${err.code}] ${err.relPath}: ${err.message}`);
166
172
  }
167
173
  }
174
+ if (options.verbose && c.files?.length) {
175
+ for (const file of c.files) {
176
+ if (!file.contentType || !file.contentTypeSource) {
177
+ continue;
178
+ }
179
+ lines.push(
180
+ ` [${file.status}] ${file.relPath}: contentType=${file.contentType} (${file.contentTypeSource})`
181
+ );
182
+ }
183
+ }
168
184
  }
169
185
 
170
186
  lines.push("");
@@ -5,7 +5,11 @@
5
5
  * @module src/cli/commands/update
6
6
  */
7
7
 
8
- import { defaultSyncService, type SyncResult } from "../../ingestion";
8
+ import {
9
+ defaultSyncService,
10
+ type SyncResult,
11
+ withContentTypeRules,
12
+ } from "../../ingestion";
9
13
  import { formatSyncResultLines, initStore } from "./shared";
10
14
 
11
15
  /**
@@ -43,14 +47,21 @@ export async function update(
43
47
  return { success: false, error: initResult.error };
44
48
  }
45
49
 
46
- const { store, collections } = initResult;
50
+ const { store, collections, config } = initResult;
47
51
 
48
52
  try {
49
53
  // Run sync service
50
- const result = await defaultSyncService.syncAll(collections, store, {
51
- gitPull: options.gitPull,
52
- runUpdateCmd: true,
53
- });
54
+ const result = await defaultSyncService.syncAll(
55
+ collections,
56
+ store,
57
+ withContentTypeRules(
58
+ {
59
+ gitPull: options.gitPull,
60
+ runUpdateCmd: true,
61
+ },
62
+ config
63
+ )
64
+ );
54
65
 
55
66
  return { success: true, result };
56
67
  } finally {
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Content type config normalization.
3
+ *
4
+ * @module src/config/content-types
5
+ */
6
+
7
+ import type { NotePresetId } from "../core/note-presets";
8
+ import type { Config, ContentTypeConfig } from "./types";
9
+
10
+ import { NOTE_PRESETS } from "../core/note-presets";
11
+
12
+ export type ConfigWarningCode =
13
+ | "UNKNOWN_CONTENT_TYPE_PRESET"
14
+ | "DUPLICATE_CONTENT_TYPE_PREFIX";
15
+
16
+ export interface ConfigWarning {
17
+ code: ConfigWarningCode;
18
+ message: string;
19
+ path: string;
20
+ }
21
+
22
+ export interface NormalizedContentTypeRule extends ContentTypeConfig {
23
+ preset: NotePresetId;
24
+ }
25
+
26
+ export interface ContentTypeNormalizationResult {
27
+ rules: NormalizedContentTypeRule[];
28
+ warnings: ConfigWarning[];
29
+ }
30
+
31
+ export interface ConfigNormalizationResult {
32
+ config: Config;
33
+ warnings: ConfigWarning[];
34
+ }
35
+
36
+ const NOTE_PRESET_IDS = new Set<NotePresetId>(
37
+ NOTE_PRESETS.map((preset) => preset.id)
38
+ );
39
+
40
+ function isNotePresetId(value: string): value is NotePresetId {
41
+ return NOTE_PRESET_IDS.has(value as NotePresetId);
42
+ }
43
+
44
+ export function normalizeContentTypes(
45
+ contentTypes: ContentTypeConfig[]
46
+ ): ContentTypeNormalizationResult {
47
+ const warnings: ConfigWarning[] = [];
48
+ const seenPrefixes = new Set<string>();
49
+ const rules: NormalizedContentTypeRule[] = [];
50
+
51
+ for (const [typeIndex, contentType] of contentTypes.entries()) {
52
+ const path = `contentTypes[${typeIndex}]`;
53
+ if (!isNotePresetId(contentType.preset)) {
54
+ warnings.push({
55
+ code: "UNKNOWN_CONTENT_TYPE_PRESET",
56
+ path: `${path}.preset`,
57
+ message: `Dropped content type "${contentType.id}" because preset "${contentType.preset}" is not a known note preset.`,
58
+ });
59
+ continue;
60
+ }
61
+
62
+ const prefixes: string[] = [];
63
+ for (const [prefixIndex, prefix] of contentType.prefixes.entries()) {
64
+ if (seenPrefixes.has(prefix)) {
65
+ warnings.push({
66
+ code: "DUPLICATE_CONTENT_TYPE_PREFIX",
67
+ path: `${path}.prefixes[${prefixIndex}]`,
68
+ message: `Dropped duplicate content type prefix "${prefix}" from "${contentType.id}".`,
69
+ });
70
+ continue;
71
+ }
72
+
73
+ seenPrefixes.add(prefix);
74
+ prefixes.push(prefix);
75
+ }
76
+
77
+ if (prefixes.length === 0) {
78
+ rules.push({
79
+ ...contentType,
80
+ preset: contentType.preset,
81
+ prefixes,
82
+ });
83
+ continue;
84
+ }
85
+
86
+ rules.push({
87
+ ...contentType,
88
+ preset: contentType.preset,
89
+ prefixes: [...prefixes].sort((a, b) => b.length - a.length),
90
+ });
91
+ }
92
+
93
+ rules.sort((a, b) => {
94
+ const aLongest = a.prefixes[0]?.length ?? 0;
95
+ const bLongest = b.prefixes[0]?.length ?? 0;
96
+ return bLongest - aLongest;
97
+ });
98
+
99
+ return { rules, warnings };
100
+ }
101
+
102
+ export function normalizeConfigContentTypes(
103
+ config: Config
104
+ ): ConfigNormalizationResult {
105
+ const normalized = normalizeContentTypes(config.contentTypes ?? []);
106
+ return {
107
+ config: {
108
+ ...config,
109
+ contentTypes: normalized.rules,
110
+ },
111
+ warnings: normalized.warnings,
112
+ };
113
+ }
114
+
115
+ export function fingerprintContentTypeRules(
116
+ rules: NormalizedContentTypeRule[]
117
+ ): string {
118
+ const canonical = rules.map((rule) => ({
119
+ id: rule.id,
120
+ preset: rule.preset,
121
+ prefixes: rule.prefixes,
122
+ }));
123
+ const hasher = new Bun.CryptoHasher("sha256");
124
+ hasher.update(JSON.stringify(canonical));
125
+ return hasher.digest("hex");
126
+ }
127
+
128
+ export function formatConfigWarning(warning: ConfigWarning): string {
129
+ return `[config] ${warning.path}: ${warning.message}`;
130
+ }
131
+
132
+ export function formatConfigWarnings(warnings?: ConfigWarning[]): string[] {
133
+ return (warnings ?? []).map(formatConfigWarning);
134
+ }
135
+
136
+ export function writeConfigWarningsToStderr(warnings?: ConfigWarning[]): void {
137
+ for (const warning of formatConfigWarnings(warnings)) {
138
+ process.stderr.write(`${warning}\n`);
139
+ }
140
+ }
@@ -16,5 +16,6 @@ export function createDefaultConfig(): Config {
16
16
  ftsTokenizer: DEFAULT_FTS_TOKENIZER,
17
17
  collections: [],
18
18
  contexts: [],
19
+ contentTypes: [],
19
20
  };
20
21
  }
@@ -5,6 +5,16 @@
5
5
  */
6
6
 
7
7
  export { createDefaultConfig } from "./defaults";
8
+ export {
9
+ type ConfigWarning,
10
+ fingerprintContentTypeRules,
11
+ formatConfigWarning,
12
+ formatConfigWarnings,
13
+ normalizeConfigContentTypes,
14
+ normalizeContentTypes,
15
+ type NormalizedContentTypeRule,
16
+ writeConfigWarningsToStderr,
17
+ } from "./content-types";
8
18
  // Loading
9
19
  export {
10
20
  isInitialized,
@@ -40,6 +50,10 @@ export {
40
50
  CollectionSchema,
41
51
  type Config,
42
52
  ConfigSchema,
53
+ CONTENT_TYPE_GRAPH_HINTS,
54
+ type ContentTypeConfig,
55
+ type ContentTypeGraphHint,
56
+ ContentTypeSchema,
43
57
  type Context,
44
58
  ContextSchema,
45
59
  DEFAULT_EXCLUDES,
@@ -7,6 +7,10 @@
7
7
 
8
8
  import type { ZodError } from "zod";
9
9
 
10
+ import {
11
+ normalizeConfigContentTypes,
12
+ type ConfigWarning,
13
+ } from "./content-types";
10
14
  import { configExists, expandPath, getConfigPaths } from "./paths";
11
15
  import { CONFIG_VERSION, type Config, ConfigSchema } from "./types";
12
16
 
@@ -15,7 +19,7 @@ import { CONFIG_VERSION, type Config, ConfigSchema } from "./types";
15
19
  // ─────────────────────────────────────────────────────────────────────────────
16
20
 
17
21
  export type LoadResult<T> =
18
- | { ok: true; value: T }
22
+ | { ok: true; value: T; warnings: ConfigWarning[] }
19
23
  | { ok: false; error: LoadError };
20
24
 
21
25
  export type LoadError =
@@ -128,7 +132,12 @@ export async function loadConfigFromPath(
128
132
  };
129
133
  }
130
134
 
131
- return { ok: true, value: result.data };
135
+ const normalized = normalizeConfigContentTypes(result.data);
136
+ return {
137
+ ok: true,
138
+ value: normalized.config,
139
+ warnings: normalized.warnings,
140
+ };
132
141
  }
133
142
 
134
143
  /**
@@ -245,6 +245,37 @@ export const ModelConfigSchema = z.object({
245
245
 
246
246
  export type ModelConfig = z.infer<typeof ModelConfigSchema>;
247
247
 
248
+ // ─────────────────────────────────────────────────────────────────────────────
249
+ // Content Type Schema
250
+ // ─────────────────────────────────────────────────────────────────────────────
251
+
252
+ export const CONTENT_TYPE_GRAPH_HINTS = [
253
+ "mentions",
254
+ "works_at",
255
+ "attended",
256
+ "decided",
257
+ "related_to",
258
+ ] as const;
259
+
260
+ export type ContentTypeGraphHint = (typeof CONTENT_TYPE_GRAPH_HINTS)[number];
261
+
262
+ export const ContentTypeSchema = z.object({
263
+ /** Stable content type identifier */
264
+ id: z.string().min(1),
265
+ /** Relative path prefixes that map to this content type */
266
+ prefixes: z.array(z.string().min(1)),
267
+ /** Note preset ID, resolved post-parse so unknown refs warn-and-drop */
268
+ preset: z.string().min(1),
269
+ /** Reserved for fn-84 typed graph hints; accepted but no-op in fn-83 */
270
+ graphHints: z.array(z.string().min(1)).optional(),
271
+ /** Reserved for future ranking; accepted but no-op in fn-83 */
272
+ searchBoost: z.number().finite().optional(),
273
+ /** Marks time-oriented content types; accepted but no-op in fn-83 */
274
+ temporal: z.boolean().optional(),
275
+ });
276
+
277
+ export type ContentTypeConfig = z.infer<typeof ContentTypeSchema>;
278
+
248
279
  // ─────────────────────────────────────────────────────────────────────────────
249
280
  // Config Schema (root)
250
281
  // ─────────────────────────────────────────────────────────────────────────────
@@ -265,11 +296,16 @@ export const ConfigSchema = z.object({
265
296
  /** Context metadata */
266
297
  contexts: z.array(ContextSchema).default([]),
267
298
 
299
+ /** Opt-in schema-lite content type rules */
300
+ contentTypes: z.array(ContentTypeSchema).default([]),
301
+
268
302
  /** Model configuration */
269
303
  models: ModelConfigSchema.optional(),
270
304
  });
271
305
 
272
- export type Config = z.infer<typeof ConfigSchema>;
306
+ export type Config = Omit<z.infer<typeof ConfigSchema>, "contentTypes"> & {
307
+ contentTypes?: ContentTypeConfig[];
308
+ };
273
309
 
274
310
  // ─────────────────────────────────────────────────────────────────────────────
275
311
  // Scope Utilities
@@ -7,7 +7,12 @@
7
7
  import type { Config } from "../config/types";
8
8
  import type { SqliteAdapter } from "../store/sqlite/adapter";
9
9
 
10
- import { loadConfig, saveConfig } from "../config";
10
+ import {
11
+ formatConfigWarnings,
12
+ loadConfig,
13
+ normalizeConfigContentTypes,
14
+ saveConfig,
15
+ } from "../config";
11
16
 
12
17
  export interface ConfigMutationContext {
13
18
  store: SqliteAdapter;
@@ -53,6 +58,9 @@ export async function applyConfigChange<T = void>(
53
58
  code: "LOAD_ERROR",
54
59
  };
55
60
  }
61
+ for (const warning of formatConfigWarnings(loadResult.warnings)) {
62
+ console.warn(warning);
63
+ }
56
64
 
57
65
  const mutationResult = await mutate(loadResult.value);
58
66
  if (!mutationResult.ok) {
@@ -63,7 +71,11 @@ export async function applyConfigChange<T = void>(
63
71
  };
64
72
  }
65
73
 
66
- const newConfig = mutationResult.config;
74
+ const normalized = normalizeConfigContentTypes(mutationResult.config);
75
+ for (const warning of formatConfigWarnings(normalized.warnings)) {
76
+ console.warn(warning);
77
+ }
78
+ const newConfig = normalized.config;
67
79
  const saveResult = await saveConfig(newConfig, ctx.configPath);
68
80
  if (!saveResult.ok) {
69
81
  return {
@@ -14,7 +14,11 @@ export type NotePresetId =
14
14
  | "research-note"
15
15
  | "decision-note"
16
16
  | "prompt-pattern"
17
- | "source-summary";
17
+ | "source-summary"
18
+ | "idea-original"
19
+ | "person"
20
+ | "company-project"
21
+ | "meeting";
18
22
 
19
23
  export interface NotePresetDefinition {
20
24
  id: NotePresetId;
@@ -104,14 +108,14 @@ export const NOTE_PRESETS: NotePresetDefinition[] = [
104
108
  {
105
109
  id: "decision-note",
106
110
  label: "Decision Note",
107
- description: "Context, decision, rationale, and consequences.",
111
+ description: "Current synthesis, decision, rationale, and timeline.",
108
112
  defaultTags: ["decision"],
109
113
  frontmatter: {
110
114
  category: "decision",
111
115
  status: "proposed",
112
116
  },
113
117
  body: (title) =>
114
- `# ${title}\n\n## Context\n\n## Decision\n\n## Rationale\n\n## Consequences\n`,
118
+ `# ${title}\n\n## Current Synthesis\n\n## Decision\n\n## Rationale\n\n## Consequences\n\n## Timeline\n`,
115
119
  },
116
120
  {
117
121
  id: "prompt-pattern",
@@ -127,14 +131,66 @@ export const NOTE_PRESETS: NotePresetDefinition[] = [
127
131
  {
128
132
  id: "source-summary",
129
133
  label: "Source Summary",
130
- description: "Summarize an article, paper, or external source cleanly.",
134
+ description: "Summarize a source with synthesis above evidence timeline.",
131
135
  defaultTags: ["source", "summary"],
132
136
  frontmatter: {
133
137
  category: "source-summary",
134
138
  sources: [],
135
139
  },
136
140
  body: (title) =>
137
- `# ${title}\n\n## Summary\n\n## Important Claims\n\n## Evidence / Quotes\n\n## Takeaways\n`,
141
+ `# ${title}\n\n## Current Synthesis\n\n## Important Claims\n\n## Assessment\n\n## Timeline\n\n## Evidence / Quotes\n`,
142
+ },
143
+ {
144
+ id: "idea-original",
145
+ label: "Original Idea",
146
+ description:
147
+ "Capture exact phrasing, context, related concepts, and publish potential.",
148
+ frontmatter: {
149
+ type: "idea-original",
150
+ category: "idea-original",
151
+ tags: [],
152
+ },
153
+ body: (title) =>
154
+ `# ${title}\n\n## Current Synthesis\n\n## Open Threads\n\n## Assessment\n\n## Timeline\n\n## Exact Phrasing\n\n## Related Concepts\n`,
155
+ },
156
+ {
157
+ id: "person",
158
+ label: "Person",
159
+ description:
160
+ "Track current state, relationship, assessment, open threads, and timeline.",
161
+ frontmatter: {
162
+ type: "person",
163
+ category: "person",
164
+ tags: [],
165
+ },
166
+ body: (title) =>
167
+ `# ${title}\n\n## Current Synthesis\n\n## Open Threads\n\n## Assessment\n\n## Timeline\n\n## Relationship\n`,
168
+ },
169
+ {
170
+ id: "company-project",
171
+ label: "Company / Project",
172
+ description:
173
+ "Track state, changes, decisions, people, open threads, and timeline.",
174
+ frontmatter: {
175
+ type: "company-project",
176
+ category: "company-project",
177
+ tags: [],
178
+ },
179
+ body: (title) =>
180
+ `# ${title}\n\n## Current Synthesis\n\n## Open Threads\n\n## Assessment\n\n## Timeline\n\n## Decisions\n\n## People\n`,
181
+ },
182
+ {
183
+ id: "meeting",
184
+ label: "Meeting",
185
+ description:
186
+ "Put analysis above the timeline; keep transcript, notes, and actions below.",
187
+ frontmatter: {
188
+ type: "meeting",
189
+ category: "meeting",
190
+ tags: [],
191
+ },
192
+ body: (title) =>
193
+ `# ${title}\n\n## Current Synthesis\n\n## Open Threads\n\n## Assessment\n\n## Timeline\n\n## Transcript / Notes\n\n## Action Items\n`,
138
194
  },
139
195
  ];
140
196