@gmickel/gno 1.8.0 → 1.10.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/README.md +6 -2
- package/assets/skill/SKILL.md +41 -16
- package/assets/skill/cli-reference.md +39 -0
- package/assets/skill/examples.md +41 -0
- package/assets/skill/mcp-reference.md +13 -1
- package/package.json +1 -1
- package/src/cli/commands/capture.ts +9 -6
- package/src/cli/commands/graph.ts +89 -2
- package/src/cli/commands/index-cmd.ts +17 -6
- package/src/cli/commands/links.ts +237 -54
- package/src/cli/commands/query.ts +212 -0
- package/src/cli/commands/ref-parser.ts +8 -103
- package/src/cli/commands/shared.ts +17 -1
- package/src/cli/commands/update.ts +17 -6
- package/src/cli/options.ts +4 -0
- package/src/cli/program.ts +176 -9
- package/src/config/content-types.ts +154 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/index.ts +14 -0
- package/src/config/loader.ts +11 -2
- package/src/config/types.ts +37 -1
- package/src/core/config-mutation.ts +14 -2
- package/src/core/graph-query.ts +137 -0
- package/src/core/graph-resolver.ts +117 -0
- package/src/core/note-presets.ts +61 -5
- package/src/core/ref-parser.ts +145 -0
- package/src/ingestion/frontmatter.ts +170 -2
- package/src/ingestion/index.ts +2 -0
- package/src/ingestion/sync-options.ts +29 -0
- package/src/ingestion/sync.ts +385 -17
- package/src/ingestion/types.ts +14 -0
- package/src/mcp/tools/add-collection.ts +8 -5
- package/src/mcp/tools/capture.ts +5 -2
- package/src/mcp/tools/get.ts +1 -1
- package/src/mcp/tools/index-cmd.ts +13 -7
- package/src/mcp/tools/index.ts +97 -10
- package/src/mcp/tools/links.ts +83 -1
- package/src/mcp/tools/multi-get.ts +1 -1
- package/src/mcp/tools/query.ts +207 -0
- package/src/mcp/tools/sync.ts +12 -6
- package/src/mcp/tools/workspace-write.ts +16 -10
- package/src/pipeline/diagnose.ts +302 -0
- package/src/pipeline/filters.ts +119 -0
- package/src/pipeline/hybrid.ts +92 -17
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +34 -0
- package/src/pipeline/vsearch.ts +4 -0
- package/src/publish/export-service.ts +1 -1
- package/src/sdk/client.ts +51 -24
- package/src/sdk/documents.ts +2 -2
- package/src/serve/background-runtime.ts +18 -4
- package/src/serve/config-sync.ts +9 -2
- package/src/serve/routes/api.ts +244 -24
- package/src/serve/routes/graph.ts +173 -1
- package/src/serve/server.ts +24 -1
- package/src/serve/watch-service.ts +12 -2
- package/src/store/migrations/009-content-type-rule-fingerprint.ts +36 -0
- package/src/store/migrations/010-typed-edges.ts +67 -0
- package/src/store/migrations/011-doc-edge-traversal-indexes.ts +30 -0
- package/src/store/migrations/index.ts +16 -1
- package/src/store/sqlite/adapter.ts +853 -114
- package/src/store/types.ts +147 -0
package/src/config/index.ts
CHANGED
|
@@ -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,
|
package/src/config/loader.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|
package/src/config/types.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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 {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared bounded typed-edge graph traversal.
|
|
3
|
+
*
|
|
4
|
+
* @module src/core/graph-query
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { NormalizedContentTypeRule } from "../config";
|
|
8
|
+
import type {
|
|
9
|
+
DocumentRow,
|
|
10
|
+
GraphQueryNode,
|
|
11
|
+
GraphQueryOptions,
|
|
12
|
+
GraphQueryResult,
|
|
13
|
+
StorePort,
|
|
14
|
+
} from "../store/types";
|
|
15
|
+
|
|
16
|
+
import { resolveDocRef } from "./ref-parser";
|
|
17
|
+
|
|
18
|
+
export type DiagnoseGraphQueryResult =
|
|
19
|
+
| { success: true; data: GraphQueryResult }
|
|
20
|
+
| { success: false; error: string; isValidation?: boolean };
|
|
21
|
+
|
|
22
|
+
export interface DiagnoseGraphQueryOptions extends GraphQueryOptions {
|
|
23
|
+
contentTypeRules?: NormalizedContentTypeRule[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function graphHintsForDoc(
|
|
27
|
+
doc: DocumentRow,
|
|
28
|
+
rules: NormalizedContentTypeRule[]
|
|
29
|
+
): string[] {
|
|
30
|
+
if (!doc.contentType) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return rules.find((rule) => rule.id === doc.contentType)?.graphHints ?? [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toNode(
|
|
37
|
+
doc: DocumentRow,
|
|
38
|
+
depth: number,
|
|
39
|
+
rules: NormalizedContentTypeRule[]
|
|
40
|
+
): GraphQueryNode {
|
|
41
|
+
return {
|
|
42
|
+
id: doc.docid,
|
|
43
|
+
uri: doc.uri,
|
|
44
|
+
title: doc.title,
|
|
45
|
+
collection: doc.collection,
|
|
46
|
+
relPath: doc.relPath,
|
|
47
|
+
depth,
|
|
48
|
+
graphHints: graphHintsForDoc(doc, rules),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function diagnoseGraphQuery(
|
|
53
|
+
store: StorePort,
|
|
54
|
+
rootRef: string,
|
|
55
|
+
options: DiagnoseGraphQueryOptions = {}
|
|
56
|
+
): Promise<DiagnoseGraphQueryResult> {
|
|
57
|
+
const resolved = await resolveDocRef(store, rootRef);
|
|
58
|
+
if ("error" in resolved) {
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
error: resolved.error,
|
|
62
|
+
isValidation: resolved.isValidation,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const rootDoc = resolved.doc;
|
|
67
|
+
if (!rootDoc.active) {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
error: `Document is inactive: ${rootRef}`,
|
|
71
|
+
isValidation: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const direction = options.direction ?? "both";
|
|
76
|
+
const traversal = await store.queryGraphTraversal(rootDoc.id, {
|
|
77
|
+
direction,
|
|
78
|
+
edgeType: options.edgeType,
|
|
79
|
+
maxDepth: options.maxDepth,
|
|
80
|
+
maxNodes: options.maxNodes,
|
|
81
|
+
frontierLimit: options.frontierLimit,
|
|
82
|
+
visitedLimit: options.visitedLimit,
|
|
83
|
+
});
|
|
84
|
+
if (!traversal.ok) {
|
|
85
|
+
return { success: false, error: traversal.error.message };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rules = options.contentTypeRules ?? [];
|
|
89
|
+
const nodes = traversal.value.nodes
|
|
90
|
+
.map(({ doc, depth }) => toNode(doc, depth, rules))
|
|
91
|
+
.sort((a, b) => a.depth - b.depth || a.uri.localeCompare(b.uri));
|
|
92
|
+
const root =
|
|
93
|
+
nodes.find((node) => node.id === rootDoc.docid) ??
|
|
94
|
+
toNode(rootDoc, 0, rules);
|
|
95
|
+
const edges = traversal.value.edges
|
|
96
|
+
.map(({ edge, depth }) => ({
|
|
97
|
+
source: edge.sourceDocid,
|
|
98
|
+
target: edge.targetDocid,
|
|
99
|
+
edgeType: edge.edgeType,
|
|
100
|
+
relationType: edge.relationType,
|
|
101
|
+
confidence: edge.confidence,
|
|
102
|
+
edgeSource: edge.edgeSource,
|
|
103
|
+
depth,
|
|
104
|
+
}))
|
|
105
|
+
.sort(
|
|
106
|
+
(a, b) =>
|
|
107
|
+
a.depth - b.depth ||
|
|
108
|
+
a.edgeType.localeCompare(b.edgeType) ||
|
|
109
|
+
a.source.localeCompare(b.source) ||
|
|
110
|
+
a.target.localeCompare(b.target)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
data: {
|
|
116
|
+
schemaVersion: "1.0",
|
|
117
|
+
root,
|
|
118
|
+
nodes,
|
|
119
|
+
edges,
|
|
120
|
+
meta: {
|
|
121
|
+
direction,
|
|
122
|
+
edgeType: options.edgeType ?? null,
|
|
123
|
+
maxDepth: Math.max(1, Math.min(options.maxDepth ?? 2, 6)),
|
|
124
|
+
maxNodes: Math.max(1, Math.min(options.maxNodes ?? 100, 1_000)),
|
|
125
|
+
frontierLimit: Math.max(
|
|
126
|
+
1,
|
|
127
|
+
Math.min(options.frontierLimit ?? 100, 1_000)
|
|
128
|
+
),
|
|
129
|
+
visitedLimit: Math.max(1, Math.min(options.visitedLimit ?? 500, 5_000)),
|
|
130
|
+
returnedNodes: nodes.length,
|
|
131
|
+
returnedEdges: edges.length,
|
|
132
|
+
truncated: traversal.value.truncated,
|
|
133
|
+
warnings: traversal.value.warnings,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared SQL expressions for graph link target resolution.
|
|
3
|
+
*
|
|
4
|
+
* These helpers preserve getGraph's existing wiki matching order so future
|
|
5
|
+
* typed-edge projection/backfill can reuse the exact resolver.
|
|
6
|
+
*
|
|
7
|
+
* @module src/core/graph-resolver
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const suffixMatch = (targetExpr: string, valueExpr: string): string =>
|
|
11
|
+
`(substr(${targetExpr}, -length(${valueExpr})) = ${valueExpr}
|
|
12
|
+
AND (length(${targetExpr}) = length(${valueExpr})
|
|
13
|
+
OR substr(${targetExpr}, -length(${valueExpr}) - 1, 1) = '/'))`;
|
|
14
|
+
|
|
15
|
+
const wikiTitleExpr = (alias: string): string => `lower(trim(${alias}.title))`;
|
|
16
|
+
const wikiRelPathExpr = (alias: string): string => `lower(${alias}.rel_path)`;
|
|
17
|
+
|
|
18
|
+
function wikiTargetExpressions(targetRefExpr: string): {
|
|
19
|
+
targetBaseExpr: string;
|
|
20
|
+
targetMdExpr: string;
|
|
21
|
+
} {
|
|
22
|
+
const targetBaseExpr = `CASE
|
|
23
|
+
WHEN ${targetRefExpr} LIKE '%.md' THEN substr(${targetRefExpr}, 1, length(${targetRefExpr}) - 3)
|
|
24
|
+
ELSE ${targetRefExpr}
|
|
25
|
+
END`;
|
|
26
|
+
return { targetBaseExpr, targetMdExpr: `${targetBaseExpr} || '.md'` };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildWikiMatchExpression(
|
|
30
|
+
alias: string,
|
|
31
|
+
targetRefExpr: string
|
|
32
|
+
): string {
|
|
33
|
+
const titleExpr = wikiTitleExpr(alias);
|
|
34
|
+
const relExpr = wikiRelPathExpr(alias);
|
|
35
|
+
const { targetBaseExpr, targetMdExpr } = wikiTargetExpressions(targetRefExpr);
|
|
36
|
+
return `(
|
|
37
|
+
${titleExpr} = ${targetBaseExpr}
|
|
38
|
+
OR ${titleExpr} = ${targetMdExpr}
|
|
39
|
+
OR ${suffixMatch(targetBaseExpr, titleExpr)}
|
|
40
|
+
OR ${suffixMatch(targetMdExpr, `${titleExpr} || '.md'`)}
|
|
41
|
+
OR ${relExpr} = ${targetBaseExpr}
|
|
42
|
+
OR ${relExpr} = ${targetMdExpr}
|
|
43
|
+
OR ${suffixMatch(relExpr, targetMdExpr)}
|
|
44
|
+
OR ${suffixMatch(relExpr, targetBaseExpr)}
|
|
45
|
+
OR ${suffixMatch(targetMdExpr, relExpr)}
|
|
46
|
+
OR ${suffixMatch(targetBaseExpr, relExpr)}
|
|
47
|
+
)`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildWikiOrderExpression(
|
|
51
|
+
alias: string,
|
|
52
|
+
targetRefExpr: string
|
|
53
|
+
): string {
|
|
54
|
+
const titleExpr = wikiTitleExpr(alias);
|
|
55
|
+
const relExpr = wikiRelPathExpr(alias);
|
|
56
|
+
const { targetBaseExpr, targetMdExpr } = wikiTargetExpressions(targetRefExpr);
|
|
57
|
+
return `CASE
|
|
58
|
+
WHEN ${titleExpr} = ${targetBaseExpr} THEN 1
|
|
59
|
+
WHEN ${titleExpr} = ${targetMdExpr} THEN 2
|
|
60
|
+
WHEN ${suffixMatch(targetBaseExpr, titleExpr)} THEN 3
|
|
61
|
+
WHEN ${suffixMatch(targetMdExpr, `${titleExpr} || '.md'`)} THEN 4
|
|
62
|
+
WHEN ${relExpr} = ${targetBaseExpr} THEN 5
|
|
63
|
+
WHEN ${relExpr} = ${targetMdExpr} THEN 6
|
|
64
|
+
WHEN ${suffixMatch(relExpr, targetMdExpr)} THEN 7
|
|
65
|
+
WHEN ${suffixMatch(relExpr, targetBaseExpr)} THEN 8
|
|
66
|
+
WHEN ${suffixMatch(targetMdExpr, relExpr)} THEN 9
|
|
67
|
+
WHEN ${suffixMatch(targetBaseExpr, relExpr)} THEN 10
|
|
68
|
+
ELSE 11
|
|
69
|
+
END`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildWikiBestMatchSubquery(
|
|
73
|
+
collectionExpr: string,
|
|
74
|
+
targetRefExpr: string
|
|
75
|
+
): string {
|
|
76
|
+
return `
|
|
77
|
+
SELECT t.id FROM documents t
|
|
78
|
+
WHERE t.active = 1
|
|
79
|
+
AND t.collection = ${collectionExpr}
|
|
80
|
+
AND ${buildWikiMatchExpression("t", targetRefExpr)}
|
|
81
|
+
AND ${buildWikiOrderExpression("t", targetRefExpr)} = (
|
|
82
|
+
SELECT MIN(${buildWikiOrderExpression("t2", targetRefExpr)}) FROM documents t2
|
|
83
|
+
WHERE t2.active = 1
|
|
84
|
+
AND t2.collection = ${collectionExpr}
|
|
85
|
+
AND ${buildWikiMatchExpression("t2", targetRefExpr)}
|
|
86
|
+
)
|
|
87
|
+
ORDER BY t.id LIMIT 1
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function buildWikiBestRankSubquery(
|
|
92
|
+
collectionExpr: string,
|
|
93
|
+
targetRefExpr: string
|
|
94
|
+
): string {
|
|
95
|
+
return `
|
|
96
|
+
SELECT MIN(${buildWikiOrderExpression("t", targetRefExpr)}) FROM documents t
|
|
97
|
+
WHERE t.active = 1
|
|
98
|
+
AND t.collection = ${collectionExpr}
|
|
99
|
+
AND ${buildWikiMatchExpression("t", targetRefExpr)}
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function buildWikiBestRankMatchCountSubquery(
|
|
104
|
+
collectionExpr: string,
|
|
105
|
+
targetRefExpr: string
|
|
106
|
+
): string {
|
|
107
|
+
return `
|
|
108
|
+
SELECT COUNT(*) FROM documents t
|
|
109
|
+
WHERE t.active = 1
|
|
110
|
+
AND t.collection = ${collectionExpr}
|
|
111
|
+
AND ${buildWikiMatchExpression("t", targetRefExpr)}
|
|
112
|
+
AND ${buildWikiOrderExpression("t", targetRefExpr)} = (${buildWikiBestRankSubquery(
|
|
113
|
+
collectionExpr,
|
|
114
|
+
targetRefExpr
|
|
115
|
+
)})
|
|
116
|
+
`;
|
|
117
|
+
}
|
package/src/core/note-presets.ts
CHANGED
|
@@ -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: "
|
|
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##
|
|
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
|
|
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##
|
|
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
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference parser and resolver for document refs.
|
|
3
|
+
* Pure parsing helpers stay store-free; resolution depends only on StorePort.
|
|
4
|
+
*
|
|
5
|
+
* @module src/core/ref-parser
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DocumentRow, StorePort } from "../store/types";
|
|
9
|
+
|
|
10
|
+
export type RefType = "docid" | "uri" | "collPath";
|
|
11
|
+
|
|
12
|
+
export interface ParsedRef {
|
|
13
|
+
type: RefType;
|
|
14
|
+
/** Normalized ref (without :line suffix) */
|
|
15
|
+
value: string;
|
|
16
|
+
/** For collPath type */
|
|
17
|
+
collection?: string;
|
|
18
|
+
/** For collPath type */
|
|
19
|
+
relPath?: string;
|
|
20
|
+
/** Parsed :line suffix (1-indexed) */
|
|
21
|
+
line?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ParseRefResult = ParsedRef | { error: string };
|
|
25
|
+
|
|
26
|
+
export type ResolvedDocRef =
|
|
27
|
+
| { doc: DocumentRow }
|
|
28
|
+
| { error: string; isValidation: boolean };
|
|
29
|
+
|
|
30
|
+
const DOCID_PATTERN = /^#[a-f0-9]{6,}$/;
|
|
31
|
+
const LINE_SUFFIX_PATTERN = /:(\d+)$/;
|
|
32
|
+
const GLOB_PATTERN = /[*?[\]]/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a single ref string.
|
|
36
|
+
* - Docid: starts with # (no :line suffix allowed)
|
|
37
|
+
* - URI: starts with gno:// (optional :N suffix)
|
|
38
|
+
* - Else: collection/path (optional :N suffix)
|
|
39
|
+
*/
|
|
40
|
+
export function parseRef(ref: string): ParseRefResult {
|
|
41
|
+
if (ref.startsWith("#")) {
|
|
42
|
+
if (ref.includes(":")) {
|
|
43
|
+
return { error: "Docid refs cannot have :line suffix" };
|
|
44
|
+
}
|
|
45
|
+
if (!DOCID_PATTERN.test(ref)) {
|
|
46
|
+
return { error: `Invalid docid format: ${ref}` };
|
|
47
|
+
}
|
|
48
|
+
return { type: "docid", value: ref };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let line: number | undefined;
|
|
52
|
+
let baseRef = ref;
|
|
53
|
+
const lineMatch = ref.match(LINE_SUFFIX_PATTERN);
|
|
54
|
+
if (lineMatch?.[1]) {
|
|
55
|
+
const parsed = Number.parseInt(lineMatch[1], 10);
|
|
56
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
57
|
+
return { error: `Invalid line suffix (must be >= 1): ${ref}` };
|
|
58
|
+
}
|
|
59
|
+
line = parsed;
|
|
60
|
+
baseRef = ref.slice(0, -lineMatch[0].length);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (baseRef.startsWith("gno://")) {
|
|
64
|
+
return { type: "uri", value: baseRef, line };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const slashIdx = baseRef.indexOf("/");
|
|
68
|
+
if (slashIdx === -1) {
|
|
69
|
+
return { error: `Invalid ref format (missing /): ${ref}` };
|
|
70
|
+
}
|
|
71
|
+
const collection = baseRef.slice(0, slashIdx);
|
|
72
|
+
const relPath = baseRef.slice(slashIdx + 1);
|
|
73
|
+
|
|
74
|
+
return { type: "collPath", value: baseRef, collection, relPath, line };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a document reference against the store.
|
|
79
|
+
*/
|
|
80
|
+
export async function resolveDocRef(
|
|
81
|
+
store: StorePort,
|
|
82
|
+
docRef: string
|
|
83
|
+
): Promise<ResolvedDocRef> {
|
|
84
|
+
const parsed = parseRef(docRef);
|
|
85
|
+
|
|
86
|
+
if ("error" in parsed) {
|
|
87
|
+
return { error: parsed.error, isValidation: true };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let doc: DocumentRow | null = null;
|
|
91
|
+
|
|
92
|
+
switch (parsed.type) {
|
|
93
|
+
case "docid": {
|
|
94
|
+
const result = await store.getDocumentByDocid(parsed.value);
|
|
95
|
+
if (result.ok && result.value) {
|
|
96
|
+
doc = result.value;
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case "uri": {
|
|
101
|
+
const result = await store.getDocumentByUri(parsed.value);
|
|
102
|
+
if (result.ok && result.value) {
|
|
103
|
+
doc = result.value;
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "collPath": {
|
|
108
|
+
const uri = `gno://${parsed.collection}/${parsed.relPath}`;
|
|
109
|
+
const result = await store.getDocumentByUri(uri);
|
|
110
|
+
if (result.ok && result.value) {
|
|
111
|
+
doc = result.value;
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!doc) {
|
|
118
|
+
return { error: `Document not found: ${docRef}`, isValidation: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { doc };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Split comma-separated refs. Does NOT expand globs.
|
|
126
|
+
*/
|
|
127
|
+
export function splitRefs(refs: string[]): string[] {
|
|
128
|
+
const result: string[] = [];
|
|
129
|
+
for (const r of refs) {
|
|
130
|
+
for (const part of r.split(",")) {
|
|
131
|
+
const trimmed = part.trim();
|
|
132
|
+
if (trimmed) {
|
|
133
|
+
result.push(trimmed);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if a ref contains glob characters.
|
|
142
|
+
*/
|
|
143
|
+
export function isGlobPattern(ref: string): boolean {
|
|
144
|
+
return GLOB_PATTERN.test(ref);
|
|
145
|
+
}
|