@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
|
@@ -1,108 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Pure lexical parsing - NO store/config access.
|
|
2
|
+
* CLI compatibility re-export for document ref parsing.
|
|
4
3
|
*
|
|
5
4
|
* @module src/cli/commands/ref-parser
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type: RefType;
|
|
16
|
-
/** Normalized ref (without :line suffix) */
|
|
17
|
-
value: string;
|
|
18
|
-
/** For collPath type */
|
|
19
|
-
collection?: string;
|
|
20
|
-
/** For collPath type */
|
|
21
|
-
relPath?: string;
|
|
22
|
-
/** Parsed :line suffix (1-indexed) */
|
|
23
|
-
line?: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export type ParseRefResult = ParsedRef | { error: string };
|
|
27
|
-
|
|
28
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
-
// Top-level regex patterns (perf: avoid recreating in functions)
|
|
30
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
const DOCID_PATTERN = /^#[a-f0-9]{6,}$/;
|
|
33
|
-
const LINE_SUFFIX_PATTERN = /:(\d+)$/;
|
|
34
|
-
const GLOB_PATTERN = /[*?[\]]/;
|
|
35
|
-
|
|
36
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
-
// Parser Functions
|
|
38
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Parse a single ref string.
|
|
42
|
-
* - Docid: starts with # (no :line suffix allowed)
|
|
43
|
-
* - URI: starts with gno:// (optional :N suffix)
|
|
44
|
-
* - Else: collection/path (optional :N suffix)
|
|
45
|
-
*/
|
|
46
|
-
export function parseRef(ref: string): ParseRefResult {
|
|
47
|
-
// 1. DocID: starts with #, validate pattern
|
|
48
|
-
if (ref.startsWith("#")) {
|
|
49
|
-
if (ref.includes(":")) {
|
|
50
|
-
return { error: "Docid refs cannot have :line suffix" };
|
|
51
|
-
}
|
|
52
|
-
if (!DOCID_PATTERN.test(ref)) {
|
|
53
|
-
return { error: `Invalid docid format: ${ref}` };
|
|
54
|
-
}
|
|
55
|
-
return { type: "docid", value: ref };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// 2. Parse optional :line suffix for URI and collPath
|
|
59
|
-
let line: number | undefined;
|
|
60
|
-
let baseRef = ref;
|
|
61
|
-
const lineMatch = ref.match(LINE_SUFFIX_PATTERN);
|
|
62
|
-
if (lineMatch?.[1]) {
|
|
63
|
-
const parsed = Number.parseInt(lineMatch[1], 10);
|
|
64
|
-
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
65
|
-
return { error: `Invalid line suffix (must be >= 1): ${ref}` };
|
|
66
|
-
}
|
|
67
|
-
line = parsed;
|
|
68
|
-
baseRef = ref.slice(0, -lineMatch[0].length);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 3. URI: starts with gno://
|
|
72
|
-
if (baseRef.startsWith("gno://")) {
|
|
73
|
-
return { type: "uri", value: baseRef, line };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// 4. Collection/path: must contain /
|
|
77
|
-
const slashIdx = baseRef.indexOf("/");
|
|
78
|
-
if (slashIdx === -1) {
|
|
79
|
-
return { error: `Invalid ref format (missing /): ${ref}` };
|
|
80
|
-
}
|
|
81
|
-
const collection = baseRef.slice(0, slashIdx);
|
|
82
|
-
const relPath = baseRef.slice(slashIdx + 1);
|
|
83
|
-
|
|
84
|
-
return { type: "collPath", value: baseRef, collection, relPath, line };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Split comma-separated refs. Does NOT expand globs.
|
|
89
|
-
*/
|
|
90
|
-
export function splitRefs(refs: string[]): string[] {
|
|
91
|
-
const result: string[] = [];
|
|
92
|
-
for (const r of refs) {
|
|
93
|
-
for (const part of r.split(",")) {
|
|
94
|
-
const trimmed = part.trim();
|
|
95
|
-
if (trimmed) {
|
|
96
|
-
result.push(trimmed);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return result;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Check if a ref contains glob characters.
|
|
105
|
-
*/
|
|
106
|
-
export function isGlobPattern(ref: string): boolean {
|
|
107
|
-
return GLOB_PATTERN.test(ref);
|
|
108
|
-
}
|
|
7
|
+
export type { ParsedRef, ParseRefResult, RefType } from "../../core/ref-parser";
|
|
8
|
+
export {
|
|
9
|
+
isGlobPattern,
|
|
10
|
+
parseRef,
|
|
11
|
+
resolveDocRef,
|
|
12
|
+
splitRefs,
|
|
13
|
+
} from "../../core/ref-parser";
|
|
@@ -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 {
|
|
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 {
|
|
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(
|
|
51
|
-
|
|
52
|
-
|
|
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 {
|
package/src/cli/options.ts
CHANGED
|
@@ -22,6 +22,7 @@ export const CMD = {
|
|
|
22
22
|
search: "search",
|
|
23
23
|
vsearch: "vsearch",
|
|
24
24
|
query: "query",
|
|
25
|
+
queryDiagnose: "query.diagnose",
|
|
25
26
|
bench: "bench",
|
|
26
27
|
ask: "ask",
|
|
27
28
|
get: "get",
|
|
@@ -38,6 +39,7 @@ export const CMD = {
|
|
|
38
39
|
backlinks: "backlinks",
|
|
39
40
|
similar: "similar",
|
|
40
41
|
graph: "graph",
|
|
42
|
+
graphQuery: "graph.query",
|
|
41
43
|
capture: "capture",
|
|
42
44
|
} as const;
|
|
43
45
|
|
|
@@ -47,6 +49,7 @@ const FORMAT_SUPPORT: Record<CommandId, OutputFormat[]> = {
|
|
|
47
49
|
[CMD.search]: ["terminal", "json", "files", "csv", "md", "xml"],
|
|
48
50
|
[CMD.vsearch]: ["terminal", "json", "files", "csv", "md", "xml"],
|
|
49
51
|
[CMD.query]: ["terminal", "json", "files", "csv", "md", "xml"],
|
|
52
|
+
[CMD.queryDiagnose]: ["terminal", "json"],
|
|
50
53
|
[CMD.bench]: ["terminal", "json"],
|
|
51
54
|
[CMD.ask]: ["terminal", "json", "md"],
|
|
52
55
|
[CMD.get]: ["terminal", "json", "md"],
|
|
@@ -64,6 +67,7 @@ const FORMAT_SUPPORT: Record<CommandId, OutputFormat[]> = {
|
|
|
64
67
|
[CMD.similar]: ["terminal", "json", "md"],
|
|
65
68
|
// graph uses custom --dot/--mermaid flags (not OutputFormat) and writes via terminal output
|
|
66
69
|
[CMD.graph]: ["json", "terminal"],
|
|
70
|
+
[CMD.graphQuery]: ["terminal", "json"],
|
|
67
71
|
[CMD.capture]: ["terminal", "json"],
|
|
68
72
|
};
|
|
69
73
|
|
package/src/cli/program.ts
CHANGED
|
@@ -508,7 +508,7 @@ function wireSearchCommands(program: Command): void {
|
|
|
508
508
|
|
|
509
509
|
// query - Hybrid search with expansion and reranking
|
|
510
510
|
program
|
|
511
|
-
.command("query <query
|
|
511
|
+
.command("query <query...>")
|
|
512
512
|
.description("Hybrid search with expansion and reranking")
|
|
513
513
|
.option("-n, --limit <num>", "max results")
|
|
514
514
|
.option("--min-score <num>", "minimum score threshold")
|
|
@@ -542,6 +542,7 @@ function wireSearchCommands(program: Command): void {
|
|
|
542
542
|
.option("--no-rerank", "disable reranking")
|
|
543
543
|
.option("--graph", "enable graph neighbor expansion")
|
|
544
544
|
.option("--no-graph", "compatibility no-op; graph is off by default")
|
|
545
|
+
.option("--target <doc>", "diagnose target document ref")
|
|
545
546
|
.option(
|
|
546
547
|
"--query-mode <mode:text>",
|
|
547
548
|
"structured mode entry (repeatable): term:<text>, intent:<text>, or hyde:<text>",
|
|
@@ -555,10 +556,16 @@ function wireSearchCommands(program: Command): void {
|
|
|
555
556
|
.option("--csv", "CSV output")
|
|
556
557
|
.option("--xml", "XML output")
|
|
557
558
|
.option("--files", "file paths only")
|
|
558
|
-
.action(async (
|
|
559
|
+
.action(async (queryParts: string[], cmdOpts: Record<string, unknown>) => {
|
|
559
560
|
const format = getFormat(cmdOpts);
|
|
560
|
-
|
|
561
|
+
const isDiagnose =
|
|
562
|
+
queryParts[0] === "diagnose" && Boolean(cmdOpts.target);
|
|
563
|
+
assertFormatSupported(isDiagnose ? CMD.queryDiagnose : CMD.query, format);
|
|
564
|
+
const diagnoseFormat = format === "json" ? "json" : "terminal";
|
|
561
565
|
const globals = getGlobals();
|
|
566
|
+
let queryText = isDiagnose
|
|
567
|
+
? queryParts.slice(1).join(" ")
|
|
568
|
+
: queryParts.join(" ");
|
|
562
569
|
|
|
563
570
|
// Validate empty query
|
|
564
571
|
if (!queryText.trim()) {
|
|
@@ -638,6 +645,44 @@ function wireSearchCommands(program: Command): void {
|
|
|
638
645
|
candidateLimit,
|
|
639
646
|
});
|
|
640
647
|
|
|
648
|
+
if (isDiagnose) {
|
|
649
|
+
const { queryDiagnose, formatQueryDiagnose } =
|
|
650
|
+
await import("./commands/query");
|
|
651
|
+
const result = await queryDiagnose(queryText, {
|
|
652
|
+
configPath: globals.config,
|
|
653
|
+
indexName: globals.index,
|
|
654
|
+
target: cmdOpts.target as string,
|
|
655
|
+
limit,
|
|
656
|
+
minScore,
|
|
657
|
+
collection: cmdOpts.collection as string | undefined,
|
|
658
|
+
lang: cmdOpts.lang as string | undefined,
|
|
659
|
+
since: cmdOpts.since as string | undefined,
|
|
660
|
+
until: cmdOpts.until as string | undefined,
|
|
661
|
+
categories,
|
|
662
|
+
author: cmdOpts.author as string | undefined,
|
|
663
|
+
intent: cmdOpts.intent as string | undefined,
|
|
664
|
+
exclude,
|
|
665
|
+
tagsAll,
|
|
666
|
+
tagsAny,
|
|
667
|
+
noExpand: depthPolicy.noExpand,
|
|
668
|
+
noRerank: depthPolicy.noRerank,
|
|
669
|
+
graph: Boolean(cmdOpts.graph),
|
|
670
|
+
noGraph: Boolean(cmdOpts.fast) || cmdOpts.graph === false,
|
|
671
|
+
candidateLimit: depthPolicy.candidateLimit,
|
|
672
|
+
queryModes,
|
|
673
|
+
json: diagnoseFormat === "json",
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
if (!result.success) {
|
|
677
|
+
throw new CliError("RUNTIME", result.error);
|
|
678
|
+
}
|
|
679
|
+
await writeOutput(
|
|
680
|
+
formatQueryDiagnose(result, { format: diagnoseFormat }),
|
|
681
|
+
diagnoseFormat
|
|
682
|
+
);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
641
686
|
const { query, formatQuery } = await import("./commands/query");
|
|
642
687
|
const result = await query(queryText, {
|
|
643
688
|
configPath: globals.config,
|
|
@@ -2153,6 +2198,8 @@ function wireLinksCommands(program: Command): void {
|
|
|
2153
2198
|
.command("list <doc>", { isDefault: true })
|
|
2154
2199
|
.description("List outgoing links from a document")
|
|
2155
2200
|
.option("--type <type>", "filter by link type (wiki, markdown)")
|
|
2201
|
+
.option("--edge-type <type>", "filter by semantic edge type")
|
|
2202
|
+
.option("--relation <type>", "alias for --edge-type")
|
|
2156
2203
|
.option("--json", "JSON output")
|
|
2157
2204
|
.option("--md", "Markdown output")
|
|
2158
2205
|
.action(async (doc: string, cmdOpts: Record<string, unknown>) => {
|
|
@@ -2168,11 +2215,27 @@ function wireLinksCommands(program: Command): void {
|
|
|
2168
2215
|
`Invalid link type: ${linkType}. Must be 'wiki' or 'markdown'.`
|
|
2169
2216
|
);
|
|
2170
2217
|
}
|
|
2218
|
+
const edgeType = cmdOpts.edgeType as string | undefined;
|
|
2219
|
+
const relation = cmdOpts.relation as string | undefined;
|
|
2220
|
+
if (linkType && (edgeType || relation)) {
|
|
2221
|
+
throw new CliError(
|
|
2222
|
+
"VALIDATION",
|
|
2223
|
+
"--type cannot be combined with --edge-type or --relation"
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
if (edgeType && relation && edgeType !== relation) {
|
|
2227
|
+
throw new CliError(
|
|
2228
|
+
"VALIDATION",
|
|
2229
|
+
"--edge-type and --relation are aliases and must match when both are provided"
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2171
2232
|
|
|
2172
2233
|
const { linksList, formatLinksList } = await import("./commands/links");
|
|
2173
2234
|
const result = await linksList(doc, {
|
|
2174
2235
|
configPath: globals.config,
|
|
2175
2236
|
type: linkType as "wiki" | "markdown" | undefined,
|
|
2237
|
+
edgeType,
|
|
2238
|
+
relation,
|
|
2176
2239
|
json: format === "json",
|
|
2177
2240
|
md: format === "md",
|
|
2178
2241
|
});
|
|
@@ -2196,6 +2259,8 @@ function wireLinksCommands(program: Command): void {
|
|
|
2196
2259
|
.command("backlinks <doc>")
|
|
2197
2260
|
.description("List documents linking to this document")
|
|
2198
2261
|
.option("-c, --collection <name>", "filter by collection")
|
|
2262
|
+
.option("--edge-type <type>", "filter by semantic edge type")
|
|
2263
|
+
.option("--relation <type>", "alias for --edge-type")
|
|
2199
2264
|
.option("--json", "JSON output")
|
|
2200
2265
|
.option("--md", "Markdown output")
|
|
2201
2266
|
.action(async (doc: string, cmdOpts: Record<string, unknown>) => {
|
|
@@ -2204,9 +2269,19 @@ function wireLinksCommands(program: Command): void {
|
|
|
2204
2269
|
const globals = getGlobals();
|
|
2205
2270
|
|
|
2206
2271
|
const { backlinks, formatBacklinks } = await import("./commands/links");
|
|
2272
|
+
const edgeType = cmdOpts.edgeType as string | undefined;
|
|
2273
|
+
const relation = cmdOpts.relation as string | undefined;
|
|
2274
|
+
if (edgeType && relation && edgeType !== relation) {
|
|
2275
|
+
throw new CliError(
|
|
2276
|
+
"VALIDATION",
|
|
2277
|
+
"--edge-type and --relation are aliases and must match when both are provided"
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2207
2280
|
const result = await backlinks(doc, {
|
|
2208
2281
|
configPath: globals.config,
|
|
2209
2282
|
collection: cmdOpts.collection as string | undefined,
|
|
2283
|
+
edgeType,
|
|
2284
|
+
relation,
|
|
2210
2285
|
json: format === "json",
|
|
2211
2286
|
md: format === "md",
|
|
2212
2287
|
});
|
|
@@ -2278,7 +2353,7 @@ function wireLinksCommands(program: Command): void {
|
|
|
2278
2353
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2279
2354
|
|
|
2280
2355
|
function wireGraphCommand(program: Command): void {
|
|
2281
|
-
program
|
|
2356
|
+
const graphCmd = program
|
|
2282
2357
|
.command("graph")
|
|
2283
2358
|
.description("Output knowledge graph of document links")
|
|
2284
2359
|
.option("-c, --collection <name>", "filter by collection")
|
|
@@ -2289,7 +2364,7 @@ function wireGraphCommand(program: Command): void {
|
|
|
2289
2364
|
.option("--include-isolated", "include nodes with no links")
|
|
2290
2365
|
.option("--similar-top-k <n>", "similar docs per node (default 5)")
|
|
2291
2366
|
.option("--neighbors <ref>", "show graph neighbors for document/node ref")
|
|
2292
|
-
.option("--direction <dir>", "neighbor direction: both, out, in"
|
|
2367
|
+
.option("--direction <dir>", "neighbor direction: both, out, in")
|
|
2293
2368
|
.option("--from <ref>", "path start document/node ref")
|
|
2294
2369
|
.option("--to <ref>", "path target document/node ref")
|
|
2295
2370
|
.option("--max-depth <n>", "max path hops (default 6)")
|
|
@@ -2332,10 +2407,11 @@ function wireGraphCommand(program: Command): void {
|
|
|
2332
2407
|
const maxDepth = cmdOpts.maxDepth
|
|
2333
2408
|
? parsePositiveInt("max-depth", cmdOpts.maxDepth)
|
|
2334
2409
|
: undefined;
|
|
2410
|
+
const graphDirection = (cmdOpts.direction ?? "both") as string;
|
|
2335
2411
|
if (
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2412
|
+
graphDirection !== "both" &&
|
|
2413
|
+
graphDirection !== "out" &&
|
|
2414
|
+
graphDirection !== "in"
|
|
2339
2415
|
) {
|
|
2340
2416
|
throw new CliError(
|
|
2341
2417
|
"VALIDATION",
|
|
@@ -2352,6 +2428,7 @@ function wireGraphCommand(program: Command): void {
|
|
|
2352
2428
|
const { graph, formatGraph } = await import("./commands/graph.js");
|
|
2353
2429
|
const result = await graph({
|
|
2354
2430
|
configPath: globals.config,
|
|
2431
|
+
indexName: globals.index,
|
|
2355
2432
|
collection: cmdOpts.collection as string | undefined,
|
|
2356
2433
|
limitNodes,
|
|
2357
2434
|
limitEdges,
|
|
@@ -2361,7 +2438,7 @@ function wireGraphCommand(program: Command): void {
|
|
|
2361
2438
|
similarTopK,
|
|
2362
2439
|
format,
|
|
2363
2440
|
neighbors: cmdOpts.neighbors as string | undefined,
|
|
2364
|
-
direction:
|
|
2441
|
+
direction: graphDirection as "both" | "out" | "in",
|
|
2365
2442
|
from: cmdOpts.from as string | undefined,
|
|
2366
2443
|
to: cmdOpts.to as string | undefined,
|
|
2367
2444
|
maxDepth,
|
|
@@ -2385,6 +2462,96 @@ function wireGraphCommand(program: Command): void {
|
|
|
2385
2462
|
|
|
2386
2463
|
await writeOutput(output, format === "json" ? "json" : "terminal");
|
|
2387
2464
|
});
|
|
2465
|
+
|
|
2466
|
+
graphCmd
|
|
2467
|
+
.command("query <doc>")
|
|
2468
|
+
.description("Run bounded typed-edge traversal from a document")
|
|
2469
|
+
.option("--direction <dir>", "direction: both, out, in")
|
|
2470
|
+
.option("--edge-type <type>", "filter by typed edge type")
|
|
2471
|
+
.option("--max-depth <n>", "max traversal depth (default 2)")
|
|
2472
|
+
.option("--max-nodes <n>", "max returned nodes (default 100)")
|
|
2473
|
+
.option(
|
|
2474
|
+
"--frontier-limit <n>",
|
|
2475
|
+
"max frontier width per depth (default 100)"
|
|
2476
|
+
)
|
|
2477
|
+
.option(
|
|
2478
|
+
"--visited-limit <n>",
|
|
2479
|
+
"max visited rows during traversal (default 500)"
|
|
2480
|
+
)
|
|
2481
|
+
.option("--json", "JSON output")
|
|
2482
|
+
.action(
|
|
2483
|
+
async (
|
|
2484
|
+
doc: string,
|
|
2485
|
+
cmdOpts: Record<string, unknown>,
|
|
2486
|
+
command: Command
|
|
2487
|
+
) => {
|
|
2488
|
+
const globals = getGlobals();
|
|
2489
|
+
const format = getFormat(cmdOpts);
|
|
2490
|
+
assertFormatSupported(CMD.graphQuery, format);
|
|
2491
|
+
if (format !== "terminal" && format !== "json") {
|
|
2492
|
+
throw new CliError(
|
|
2493
|
+
"VALIDATION",
|
|
2494
|
+
"gno graph query supports terminal and json output"
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
const argv = resolveCliArgv(command);
|
|
2498
|
+
const directionFlagIndex = argv.lastIndexOf("--direction");
|
|
2499
|
+
const rawDirection =
|
|
2500
|
+
directionFlagIndex >= 0 ? argv[directionFlagIndex + 1] : undefined;
|
|
2501
|
+
const direction = (rawDirection ??
|
|
2502
|
+
cmdOpts.direction ??
|
|
2503
|
+
"both") as string;
|
|
2504
|
+
if (direction !== "both" && direction !== "out" && direction !== "in") {
|
|
2505
|
+
throw new CliError(
|
|
2506
|
+
"VALIDATION",
|
|
2507
|
+
"--direction must be one of: both, out, in"
|
|
2508
|
+
);
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
const maxDepth = cmdOpts.maxDepth
|
|
2512
|
+
? parsePositiveInt("max-depth", cmdOpts.maxDepth)
|
|
2513
|
+
: undefined;
|
|
2514
|
+
const maxNodes = cmdOpts.maxNodes
|
|
2515
|
+
? parsePositiveInt("max-nodes", cmdOpts.maxNodes)
|
|
2516
|
+
: undefined;
|
|
2517
|
+
const frontierLimit = cmdOpts.frontierLimit
|
|
2518
|
+
? parsePositiveInt("frontier-limit", cmdOpts.frontierLimit)
|
|
2519
|
+
: undefined;
|
|
2520
|
+
const visitedLimit = cmdOpts.visitedLimit
|
|
2521
|
+
? parsePositiveInt("visited-limit", cmdOpts.visitedLimit)
|
|
2522
|
+
: undefined;
|
|
2523
|
+
|
|
2524
|
+
const { graphQuery, formatGraphQuery } =
|
|
2525
|
+
await import("./commands/graph.js");
|
|
2526
|
+
const result = await graphQuery(doc, {
|
|
2527
|
+
configPath: globals.config,
|
|
2528
|
+
indexName: globals.index,
|
|
2529
|
+
direction: direction as "both" | "out" | "in",
|
|
2530
|
+
edgeType: cmdOpts.edgeType as string | undefined,
|
|
2531
|
+
maxDepth,
|
|
2532
|
+
maxNodes,
|
|
2533
|
+
frontierLimit,
|
|
2534
|
+
visitedLimit,
|
|
2535
|
+
format,
|
|
2536
|
+
});
|
|
2537
|
+
|
|
2538
|
+
if (!result.success) {
|
|
2539
|
+
throw new CliError(
|
|
2540
|
+
result.isValidation ? "VALIDATION" : "RUNTIME",
|
|
2541
|
+
result.error
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
const output = formatGraphQuery(result, { format });
|
|
2546
|
+
if (result.data.meta.truncated) {
|
|
2547
|
+
for (const warning of result.data.meta.warnings) {
|
|
2548
|
+
console.error(`Warning: ${warning}`);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
await writeOutput(output, format);
|
|
2553
|
+
}
|
|
2554
|
+
);
|
|
2388
2555
|
}
|
|
2389
2556
|
|
|
2390
2557
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,154 @@
|
|
|
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 normalizeGraphHint(value: string): string {
|
|
41
|
+
return value
|
|
42
|
+
.trim()
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.replace(/[-\s]+/g, "_");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isNotePresetId(value: string): value is NotePresetId {
|
|
48
|
+
return NOTE_PRESET_IDS.has(value as NotePresetId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function normalizeContentTypes(
|
|
52
|
+
contentTypes: ContentTypeConfig[]
|
|
53
|
+
): ContentTypeNormalizationResult {
|
|
54
|
+
const warnings: ConfigWarning[] = [];
|
|
55
|
+
const seenPrefixes = new Set<string>();
|
|
56
|
+
const rules: NormalizedContentTypeRule[] = [];
|
|
57
|
+
|
|
58
|
+
for (const [typeIndex, contentType] of contentTypes.entries()) {
|
|
59
|
+
const path = `contentTypes[${typeIndex}]`;
|
|
60
|
+
if (!isNotePresetId(contentType.preset)) {
|
|
61
|
+
warnings.push({
|
|
62
|
+
code: "UNKNOWN_CONTENT_TYPE_PRESET",
|
|
63
|
+
path: `${path}.preset`,
|
|
64
|
+
message: `Dropped content type "${contentType.id}" because preset "${contentType.preset}" is not a known note preset.`,
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const prefixes: string[] = [];
|
|
70
|
+
for (const [prefixIndex, prefix] of contentType.prefixes.entries()) {
|
|
71
|
+
if (seenPrefixes.has(prefix)) {
|
|
72
|
+
warnings.push({
|
|
73
|
+
code: "DUPLICATE_CONTENT_TYPE_PREFIX",
|
|
74
|
+
path: `${path}.prefixes[${prefixIndex}]`,
|
|
75
|
+
message: `Dropped duplicate content type prefix "${prefix}" from "${contentType.id}".`,
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
seenPrefixes.add(prefix);
|
|
81
|
+
prefixes.push(prefix);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (prefixes.length === 0) {
|
|
85
|
+
rules.push({
|
|
86
|
+
...contentType,
|
|
87
|
+
preset: contentType.preset,
|
|
88
|
+
prefixes,
|
|
89
|
+
graphHints: contentType.graphHints
|
|
90
|
+
? contentType.graphHints.map(normalizeGraphHint).filter(Boolean)
|
|
91
|
+
: undefined,
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
rules.push({
|
|
97
|
+
...contentType,
|
|
98
|
+
preset: contentType.preset,
|
|
99
|
+
prefixes: [...prefixes].sort((a, b) => b.length - a.length),
|
|
100
|
+
graphHints: contentType.graphHints
|
|
101
|
+
? contentType.graphHints.map(normalizeGraphHint).filter(Boolean)
|
|
102
|
+
: undefined,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
rules.sort((a, b) => {
|
|
107
|
+
const aLongest = a.prefixes[0]?.length ?? 0;
|
|
108
|
+
const bLongest = b.prefixes[0]?.length ?? 0;
|
|
109
|
+
return bLongest - aLongest;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return { rules, warnings };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function normalizeConfigContentTypes(
|
|
116
|
+
config: Config
|
|
117
|
+
): ConfigNormalizationResult {
|
|
118
|
+
const normalized = normalizeContentTypes(config.contentTypes ?? []);
|
|
119
|
+
return {
|
|
120
|
+
config: {
|
|
121
|
+
...config,
|
|
122
|
+
contentTypes: normalized.rules,
|
|
123
|
+
},
|
|
124
|
+
warnings: normalized.warnings,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function fingerprintContentTypeRules(
|
|
129
|
+
rules: NormalizedContentTypeRule[]
|
|
130
|
+
): string {
|
|
131
|
+
const canonical = rules.map((rule) => ({
|
|
132
|
+
id: rule.id,
|
|
133
|
+
preset: rule.preset,
|
|
134
|
+
prefixes: rule.prefixes,
|
|
135
|
+
graphHints: rule.graphHints ?? [],
|
|
136
|
+
}));
|
|
137
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
138
|
+
hasher.update(JSON.stringify(canonical));
|
|
139
|
+
return hasher.digest("hex");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function formatConfigWarning(warning: ConfigWarning): string {
|
|
143
|
+
return `[config] ${warning.path}: ${warning.message}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function formatConfigWarnings(warnings?: ConfigWarning[]): string[] {
|
|
147
|
+
return (warnings ?? []).map(formatConfigWarning);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function writeConfigWarningsToStderr(warnings?: ConfigWarning[]): void {
|
|
151
|
+
for (const warning of formatConfigWarnings(warnings)) {
|
|
152
|
+
process.stderr.write(`${warning}\n`);
|
|
153
|
+
}
|
|
154
|
+
}
|