sdtk-wiki-kit 0.1.0 → 0.1.1

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 CHANGED
@@ -36,13 +36,15 @@ Implemented in the Foundation/Beta package:
36
36
  | Run non-destructive wiki lint | `sdtk-wiki lint` |
37
37
  | Run stale-page prune dry-run report | `sdtk-wiki wiki prune --dry-run` |
38
38
  | Generate local discovery plan from gap evidence | `sdtk-wiki wiki discover --plan` |
39
- | Generate compile dry-run preview from a local plan | `sdtk-wiki wiki compile --dry-run` |
40
- | Ask grounded questions over built graph | `sdtk-wiki ask` |
41
- | Save one redacted query record after successful Ask | `sdtk-wiki ask --save-query` |
39
+ | Generate semantic extraction dry-run report | `sdtk-wiki wiki extract --dry-run` |
40
+ | Generate compile dry-run preview and JSON sidecar | `sdtk-wiki wiki compile --dry-run` |
41
+ | Apply an approved compile JSON sidecar | `sdtk-wiki wiki compile --apply --yes` |
42
+ | Search generated personal-brain pages locally | `sdtk-wiki search` |
43
+ | Ask grounded questions over built graph | `sdtk-wiki ask` with `wiki.ask` entitlement/runtime preconditions |
44
+ | Save one redacted query record after successful Ask | `sdtk-wiki ask --save-query` with `wiki.ask` entitlement/runtime preconditions |
42
45
 
43
46
  Not implemented in the Foundation/Beta runtime:
44
47
 
45
- - `sdtk-wiki wiki compile --apply`
46
48
  - automatic web discovery or web fetch
47
49
  - automatic source ingest from the web
48
50
  - destructive prune/delete/archive
@@ -60,6 +62,7 @@ Not implemented in the Foundation/Beta runtime:
60
62
  | `.sdtk/wiki/raw` | metadata-only raw/source registry |
61
63
  | `.sdtk/wiki/provenance` | source/build/ingest provenance |
62
64
  | `.sdtk/wiki/reports` | lint, prune, discover, and compile preview reports |
65
+ | `.sdtk/wiki/personal-brain` | generated semantic personal-brain pages from explicit apply |
63
66
  | `.sdtk/wiki/queries` | opt-in redacted Ask query records |
64
67
  | `.sdtk/atlas` | legacy Atlas compatibility output, readable only |
65
68
 
@@ -164,14 +167,40 @@ Safety:
164
167
  - no compile/apply
165
168
  - no prune/delete/archive
166
169
 
170
+ ### Semantic Extraction Dry-Run
171
+
172
+ ```powershell
173
+ sdtk-wiki wiki extract --project-path <path> --source-root <path> --dry-run
174
+ ```
175
+
176
+ Reads local Markdown sources and writes a semantic extraction JSON report under
177
+ `.sdtk/wiki/reports`. The report can identify local source records, GitHub
178
+ tool candidates, concept candidates, relations, comparisons, syntheses, and
179
+ source-quality findings.
180
+
181
+ Safety:
182
+
183
+ - local source roots only
184
+ - no web fetch
185
+ - no page generation
186
+ - no graph/viewer rebuild side effects
187
+ - no raw/provenance mutation
188
+ - no `.sdtk/atlas` mutation
189
+
167
190
  ### Compile Dry-Run Preview
168
191
 
169
192
  ```powershell
170
193
  sdtk-wiki wiki compile --plan <path> --project-path <path> --dry-run
171
194
  ```
172
195
 
173
- Reads a local markdown or JSON compile plan and writes a preview report under
174
- `.sdtk/wiki/reports`.
196
+ Reads a local structured markdown plan, JSON operation plan, or
197
+ `sdtk_wiki_semantic_extraction` JSON report and writes both:
198
+
199
+ - `.sdtk/wiki/reports/compile-dry-run-preview-YYYY-MM-DD.md`
200
+ - `.sdtk/wiki/reports/compile-apply-plan-YYYY-MM-DD.json`
201
+
202
+ The markdown report is for human review. The JSON sidecar is the only supported
203
+ source of truth for explicit apply.
175
204
 
176
205
  Supported operation types:
177
206
 
@@ -180,9 +209,41 @@ Supported operation types:
180
209
  - `add_relation`
181
210
  - `add_source_ref`
182
211
 
183
- Unknown operation types are reported as `unsupported_operation`. The current
184
- runtime has no `--apply` behavior and does not modify wiki pages, raw sources,
185
- provenance, or `.sdtk/atlas`.
212
+ Unknown operation types are reported as `unsupported_operation`. Dry-run does
213
+ not modify wiki pages, raw sources, provenance, or `.sdtk/atlas`.
214
+
215
+ ### Compile Apply
216
+
217
+ ```powershell
218
+ sdtk-wiki wiki compile --plan <compile-apply-plan-json> --project-path <path> --apply --yes
219
+ ```
220
+
221
+ Applies only a `record_type: "sdtk_wiki_compile_apply_plan"` JSON sidecar
222
+ generated by compile dry-run. Markdown plans and raw semantic extraction JSON
223
+ are rejected for apply.
224
+
225
+ Apply behavior:
226
+
227
+ - requires `--apply --yes`
228
+ - writes only under `.sdtk/wiki/personal-brain`
229
+ - create-only or same-content no-op
230
+ - no overwrite with different content
231
+ - no delete, archive, rewrite, or reorder
232
+ - no raw/provenance descriptor mutation
233
+ - no `.sdtk/atlas` mutation
234
+
235
+ ### Local Search
236
+
237
+ ```powershell
238
+ sdtk-wiki search --project-path <path> "<query>"
239
+ ```
240
+
241
+ Searches generated personal-brain Markdown pages under
242
+ `.sdtk/wiki/personal-brain/**/*.md`.
243
+
244
+ Search is deterministic, read-only, and non-premium. It does not require
245
+ `wiki.ask` entitlement, does not call an LLM/RAG runtime, does not write query
246
+ history, and does not mutate project files.
186
247
 
187
248
  ### Ask
188
249
 
@@ -190,7 +251,9 @@ provenance, or `.sdtk/atlas`.
190
251
  sdtk-wiki ask --question "<text>" [--project-path <path>] [--json] [--source <id-or-path>] [--max-sources <n>] [--save-query]
191
252
  ```
192
253
 
193
- Native `sdtk-wiki ask` is the canonical Q&A command for capability `wiki.ask`.
254
+ Native `sdtk-wiki ask` is implemented as the canonical Q&A command for
255
+ capability `wiki.ask`, but it is not a free local search command. It requires
256
+ valid `wiki.ask` entitlement and runtime preconditions.
194
257
 
195
258
  Preconditions:
196
259
 
@@ -237,6 +300,15 @@ Preview a compile plan without applying it:
237
300
  sdtk-wiki wiki compile --plan <local-plan.md-or-json> --project-path . --dry-run
238
301
  ```
239
302
 
303
+ Build a personal-brain from local Markdown sources and search it:
304
+
305
+ ```powershell
306
+ sdtk-wiki wiki extract --project-path . --source-root docs --dry-run
307
+ sdtk-wiki wiki compile --project-path . --plan .sdtk/wiki/reports/semantic-extraction-dry-run-<stamp>.json --dry-run
308
+ sdtk-wiki wiki compile --project-path . --plan .sdtk/wiki/reports/compile-apply-plan-<date>.json --apply --yes
309
+ sdtk-wiki search --project-path . "multi-agent"
310
+ ```
311
+
240
312
  Ask and save an opt-in redacted query record:
241
313
 
242
314
  ```powershell
@@ -244,6 +316,10 @@ sdtk-wiki atlas build --project-path .
244
316
  sdtk-wiki ask --project-path . --question "Which docs describe the deployment path?" --save-query
245
317
  ```
246
318
 
319
+ This flow requires valid `wiki.ask` entitlement/runtime preconditions. Use
320
+ `sdtk-wiki search` for non-premium local validation of generated
321
+ personal-brain pages.
322
+
247
323
  ## Foundation/Beta Boundaries
248
324
 
249
325
  This release is local-first and report-first. It is a foundation for a
@@ -252,10 +328,10 @@ second-brain workflow, not a fully autonomous second brain.
252
328
  Do not claim the Foundation/Beta runtime includes:
253
329
 
254
330
  - web fetch/discover
255
- - compile `--apply`
256
331
  - destructive prune/delete/archive
257
332
  - query list/show/delete
258
333
  - default full prompt/full answer query persistence
334
+ - premium Ask without valid `wiki.ask` entitlement/runtime preconditions
259
335
  - `.sdtk/atlas` as canonical storage
260
336
 
261
337
  See `products/sdtk-wiki/governance/SDTK_WIKI_USAGE_GUIDE.md` for the fuller
@@ -147,20 +147,48 @@ def _assert_inside(base: Path, target: Path) -> None:
147
147
  raise ValueError(f"Refusing to write outside SDTK-WIKI workspace: {resolved_target}")
148
148
 
149
149
 
150
- def _is_excluded(
151
- path: Path,
152
- root: Path,
153
- exclude_frags: list[str],
154
- ) -> bool:
155
- try:
156
- rel = path.relative_to(root).as_posix().lower()
157
- except ValueError:
158
- rel = path.as_posix().lower()
159
- for frag in exclude_frags:
160
- norm_frag = frag.replace("\\", "/").lower()
161
- if norm_frag in rel:
162
- return True
163
- return False
150
+ def _is_excluded(
151
+ path: Path,
152
+ root: Path,
153
+ exclude_frags: list[str],
154
+ ) -> bool:
155
+ return _match_exclude(path=path, root=root, exclude_frags=exclude_frags) is not None
156
+
157
+
158
+ def _display_scan_path(path: Path, root: Path) -> str:
159
+ try:
160
+ return path.relative_to(root).as_posix()
161
+ except ValueError:
162
+ return path.as_posix()
163
+
164
+
165
+ def _normalise_exclude_fragment(frag: str) -> list[str]:
166
+ norm_frag = frag.replace("\\", "/").strip("/").lower()
167
+ return [part for part in norm_frag.split("/") if part and part != "."]
168
+
169
+
170
+ def _match_exclude(
171
+ path: Path,
172
+ root: Path,
173
+ exclude_frags: list[str],
174
+ ) -> str | None:
175
+ rel = _display_scan_path(path, root).lower()
176
+ rel_parts = [part for part in rel.split("/") if part and part != "."]
177
+
178
+ for frag in exclude_frags:
179
+ frag_parts = _normalise_exclude_fragment(frag)
180
+ if not frag_parts:
181
+ continue
182
+ if len(frag_parts) == 1:
183
+ if frag_parts[0] in rel_parts:
184
+ return frag
185
+ continue
186
+
187
+ for idx in range(0, len(rel_parts) - len(frag_parts) + 1):
188
+ if rel_parts[idx : idx + len(frag_parts)] == frag_parts:
189
+ return frag
190
+
191
+ return None
164
192
 
165
193
 
166
194
  def _extract_title(text: str) -> str:
@@ -322,9 +350,9 @@ def _compute_file_hash(md_file: Path) -> str:
322
350
  return hashlib.sha256(content).hexdigest()
323
351
 
324
352
 
325
- def _parse_doc_record(md_file: Path, root: Path) -> dict[str, Any]:
326
- rel = md_file.relative_to(root).as_posix()
327
- text = md_file.read_text(encoding="utf-8", errors="replace")
353
+ def _parse_doc_record(md_file: Path, root: Path) -> dict[str, Any]:
354
+ rel = _display_scan_path(md_file, root)
355
+ text = md_file.read_text(encoding="utf-8", errors="replace")
328
356
  frontmatter_fields, body_text = _parse_frontmatter(text)
329
357
  title = str(
330
358
  frontmatter_fields.get("title")
@@ -363,39 +391,70 @@ def _parse_doc_record(md_file: Path, root: Path) -> dict[str, Any]:
363
391
  }
364
392
 
365
393
 
366
- def list_indexable_markdown_files(
367
- root: Path,
368
- scan_roots: list[Path],
369
- exclude_frags: list[str],
370
- ) -> list[Path]:
371
- files: list[Path] = []
372
- seen_paths: set[str] = set()
373
-
374
- for scan_root in scan_roots:
375
- if not scan_root.exists():
376
- print(f"[atlas] Warning: scan root does not exist, skipping: {scan_root}", file=sys.stderr)
377
- continue
394
+ def list_indexable_markdown_files(
395
+ root: Path,
396
+ scan_roots: list[Path],
397
+ exclude_frags: list[str],
398
+ ) -> list[Path]:
399
+ return collect_indexable_markdown_files(root, scan_roots, exclude_frags)["files"]
400
+
401
+
402
+ def collect_indexable_markdown_files(
403
+ root: Path,
404
+ scan_roots: list[Path],
405
+ exclude_frags: list[str],
406
+ ) -> dict[str, Any]:
407
+ files: list[Path] = []
408
+ seen_paths: set[str] = set()
409
+ skipped_files: list[dict[str, str]] = []
410
+ scanned_count = 0
411
+
412
+ for scan_root in scan_roots:
413
+ if not scan_root.exists():
414
+ print(f"[atlas] Warning: scan root does not exist, skipping: {scan_root}", file=sys.stderr)
415
+ continue
378
416
  if scan_root.is_file() and scan_root.suffix.lower() == ".md":
379
417
  candidates = [scan_root]
380
418
  elif scan_root.is_dir():
381
419
  candidates = [p for p in sorted(scan_root.rglob("*.md")) if p.is_file()]
382
420
  else:
383
- candidates = []
384
-
385
- for md_file in candidates:
386
- if _is_excluded(md_file, root=root, exclude_frags=exclude_frags):
387
- continue
388
- try:
389
- rel = md_file.relative_to(root).as_posix()
390
- except ValueError:
391
- rel = md_file.as_posix()
392
- if rel in seen_paths:
393
- continue
394
- seen_paths.add(rel)
395
- files.append(md_file)
396
-
397
- files.sort(key=lambda p: p.as_posix())
398
- return files
421
+ candidates = []
422
+
423
+ for md_file in candidates:
424
+ scanned_count += 1
425
+ matched_exclude = _match_exclude(md_file, root=root, exclude_frags=exclude_frags)
426
+ display_path = _display_scan_path(md_file, root)
427
+ if matched_exclude is not None:
428
+ skipped_files.append(
429
+ {
430
+ "path": display_path,
431
+ "reason": f"exclude:{matched_exclude}",
432
+ }
433
+ )
434
+ continue
435
+ try:
436
+ rel = md_file.relative_to(root).as_posix()
437
+ except ValueError:
438
+ rel = md_file.as_posix()
439
+ if rel in seen_paths:
440
+ skipped_files.append(
441
+ {
442
+ "path": display_path,
443
+ "reason": "duplicate_scan_root",
444
+ }
445
+ )
446
+ continue
447
+ seen_paths.add(rel)
448
+ files.append(md_file)
449
+
450
+ files.sort(key=lambda p: p.as_posix())
451
+ return {
452
+ "files": files,
453
+ "scanned_count": scanned_count,
454
+ "indexed_count": len(files),
455
+ "skipped_count": len(skipped_files),
456
+ "skipped_files": skipped_files,
457
+ }
399
458
 
400
459
 
401
460
  # ---------------------------------------------------------------------------
@@ -639,16 +698,17 @@ def write_wiki_pages_and_provenance(
639
698
  }
640
699
 
641
700
 
642
- def build_docs_incremental(
643
- root: Path,
644
- atlas_dir: Path,
645
- generated: str,
646
- scan_roots: list[Path],
647
- exclude_frags: list[str],
648
- ) -> tuple[list[dict[str, Any]], dict[str, Any], dict[str, int]]:
649
- prior_state = load_atlas_state(atlas_dir)
650
- prior_documents = prior_state.get("documents", {})
651
- current_files = list_indexable_markdown_files(root, scan_roots, exclude_frags)
701
+ def build_docs_incremental(
702
+ root: Path,
703
+ atlas_dir: Path,
704
+ generated: str,
705
+ scan_roots: list[Path],
706
+ exclude_frags: list[str],
707
+ ) -> tuple[list[dict[str, Any]], dict[str, Any], dict[str, Any]]:
708
+ prior_state = load_atlas_state(atlas_dir)
709
+ prior_documents = prior_state.get("documents", {})
710
+ scan_result = collect_indexable_markdown_files(root, scan_roots, exclude_frags)
711
+ current_files = scan_result["files"]
652
712
 
653
713
  current_rel_paths = {}
654
714
  for md_file in current_files:
@@ -710,12 +770,16 @@ def build_docs_incremental(
710
770
  "generated": generated,
711
771
  "documents": next_documents,
712
772
  }
713
- build_stats = {
714
- "discovered_count": len(current_rel_paths),
715
- "reused_count": reused_count,
716
- "reparsed_count": reparsed_count,
717
- "removed_count": removed_count,
718
- }
773
+ build_stats = {
774
+ "discovered_count": len(current_rel_paths),
775
+ "scanned_count": scan_result["scanned_count"],
776
+ "indexed_count": len(current_rel_paths),
777
+ "skipped_count": scan_result["skipped_count"],
778
+ "skipped_files": scan_result["skipped_files"],
779
+ "reused_count": reused_count,
780
+ "reparsed_count": reparsed_count,
781
+ "removed_count": removed_count,
782
+ }
719
783
  return docs, next_state, build_stats
720
784
 
721
785
 
@@ -814,11 +878,11 @@ def build_graph(docs: list[dict[str, Any]]) -> dict[str, Any]:
814
878
  # ---------------------------------------------------------------------------
815
879
  # Summary markdown
816
880
  # ---------------------------------------------------------------------------
817
- def build_summary(
881
+ def build_summary(
818
882
  docs: list[dict[str, Any]],
819
883
  graph: dict[str, Any],
820
884
  generated: str,
821
- stats: dict[str, int] | None,
885
+ stats: dict[str, Any] | None,
822
886
  root: Path,
823
887
  scan_roots: list[Path],
824
888
  exclude_frags: list[str],
@@ -848,16 +912,30 @@ def build_summary(
848
912
  for fam, cnt in sorted(family_counts.items(), key=lambda x: -x[1]):
849
913
  lines.append(f"| {fam} | {cnt} |")
850
914
 
851
- if stats is not None:
852
- lines += [
853
- "",
854
- "## Incremental Build",
855
- "",
856
- f"Discovered markdown docs: {stats['discovered_count']}",
857
- f"Reused cached docs: {stats['reused_count']}",
858
- f"Reparsed docs: {stats['reparsed_count']}",
859
- f"Removed stale docs: {stats['removed_count']}",
860
- ]
915
+ if stats is not None:
916
+ lines += [
917
+ "",
918
+ "## Incremental Build",
919
+ "",
920
+ f"Discovered markdown docs: {stats['discovered_count']}",
921
+ f"Scanned markdown candidates: {stats.get('scanned_count', stats['discovered_count'])}",
922
+ f"Indexed markdown docs: {stats.get('indexed_count', stats['discovered_count'])}",
923
+ f"Skipped markdown docs: {stats.get('skipped_count', 0)}",
924
+ f"Reused cached docs: {stats['reused_count']}",
925
+ f"Reparsed docs: {stats['reparsed_count']}",
926
+ f"Removed stale docs: {stats['removed_count']}",
927
+ ]
928
+ skipped_files = stats.get("skipped_files") or []
929
+ if skipped_files:
930
+ lines += [
931
+ "",
932
+ "## Skipped Markdown Files",
933
+ "",
934
+ "| Path | Reason |",
935
+ "|------|--------|",
936
+ ]
937
+ for skipped in skipped_files:
938
+ lines.append(f"| {skipped['path']} | {skipped['reason']} |")
861
939
 
862
940
  lines += [
863
941
  "",
@@ -959,12 +1037,19 @@ def build_atlas(
959
1037
  scan_roots=roots,
960
1038
  exclude_frags=frags,
961
1039
  )
962
- print(f"[atlas] Indexed {len(docs)} documents.")
963
- if verbose:
964
- print(
965
- f"[atlas] Incremental build: reused {stats['reused_count']} cached, "
966
- f"reparsed {stats['reparsed_count']}, removed {stats['removed_count']}."
967
- )
1040
+ print(f"[atlas] Indexed {len(docs)} documents.")
1041
+ print(
1042
+ f"[atlas] Scan coverage: scanned {stats.get('scanned_count', len(docs))}, "
1043
+ f"indexed {stats.get('indexed_count', len(docs))}, "
1044
+ f"skipped {stats.get('skipped_count', 0)}."
1045
+ )
1046
+ if verbose:
1047
+ print(
1048
+ f"[atlas] Incremental build: reused {stats['reused_count']} cached, "
1049
+ f"reparsed {stats['reparsed_count']}, removed {stats['removed_count']}."
1050
+ )
1051
+ for skipped in stats.get("skipped_files", []):
1052
+ print(f"[atlas] Skipped markdown: {skipped['path']} ({skipped['reason']})")
968
1053
 
969
1054
  print("[atlas] Building graph...")
970
1055
  graph = build_graph(docs)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdtk-wiki-kit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Project-local wiki and knowledge graph toolkit for SDTK workspaces.",
5
5
  "bin": {
6
6
  "sdtk-wiki": "bin/sdtk-wiki.js"
@@ -16,6 +16,7 @@ Usage:
16
16
  sdtk-wiki wiki discover --help
17
17
  sdtk-wiki wiki compile --help
18
18
  sdtk-wiki ask --help
19
+ sdtk-wiki search --help
19
20
  sdtk-wiki lint --help
20
21
 
21
22
  R1 command model:
@@ -27,8 +28,9 @@ R1 command model:
27
28
  wiki ingest Register one local source in metadata-only raw/provenance state.
28
29
  wiki prune Write a report-only dry-run stale managed-page review.
29
30
  wiki discover Write a local-only discovery plan from WIKI gap evidence.
30
- wiki compile Write a compile dry-run preview from a local plan.
31
+ wiki compile Preview or explicitly apply local personal-brain compile plans.
31
32
  ask Ask grounded questions over the built SDTK-WIKI graph.
33
+ search Search generated personal-brain pages locally without premium Ask.
32
34
  lint Write a report-first, non-destructive wiki lint report.
33
35
 
34
36
  Workspace paths:
@@ -50,6 +52,10 @@ Premium Ask:
50
52
  Requires .sdtk/wiki/graph plus local entitlement/runtime preconditions.
51
53
  Query history, discover, compile, and cleanup automation are not enabled in R1.
52
54
 
55
+ Local Search:
56
+ sdtk-wiki search Deterministic, read-only local search over .sdtk/wiki/personal-brain.
57
+ Does not require wiki.ask entitlement and does not perform LLM/RAG behavior.
58
+
53
59
  Maintenance:
54
60
  sdtk-wiki wiki prune --dry-run is report-only and writes under .sdtk/wiki/reports.
55
61
  It never deletes, archives, applies, or mutates .sdtk/atlas.`);
@@ -57,8 +63,9 @@ Maintenance:
57
63
  sdtk-wiki wiki discover --plan is plan-only and writes under .sdtk/wiki/reports.
58
64
  It never fetches web sources, ingests sources, compiles pages, applies edits, prunes, or mutates .sdtk/atlas.`);
59
65
  console.log(`
60
- sdtk-wiki wiki compile --dry-run writes a compile dry-run preview under .sdtk/wiki/reports.
61
- It never applies changes, rewrites pages, mutates raw/provenance files, or mutates .sdtk/atlas.`);
66
+ sdtk-wiki wiki compile --dry-run writes a markdown preview plus JSON sidecar under .sdtk/wiki/reports.
67
+ sdtk-wiki wiki compile --apply --yes consumes only the JSON sidecar and writes create-only personal-brain pages.
68
+ It never rewrites pages, mutates raw/provenance files, or mutates .sdtk/atlas.`);
62
69
  return 0;
63
70
  }
64
71
 
@@ -13,13 +13,14 @@ function cmdLintHelp() {
13
13
  sdtk-wiki lint [--project-path <path>]
14
14
 
15
15
  Purpose:
16
- Run report-first, non-destructive lint checks over canonical .sdtk/wiki content.
16
+ Run report-first, non-destructive lint checks over canonical .sdtk/wiki content and local source-quality evidence.
17
17
 
18
18
  Output:
19
19
  .sdtk/wiki/reports/lint-report-YYYY-MM-DD.md
20
20
 
21
21
  Behavior:
22
22
  Findings are written to the report and do not auto-modify wiki or source files.
23
+ Source-quality checks report mojibake-like text, missing source URLs, weak titles, duplicate repo/source candidates, low-confidence extraction, and raw/graph/provenance coverage mismatch.
23
24
  Completed lint runs exit 0 even when findings exist.
24
25
  Missing workspace or fatal report-write failures exit non-zero.
25
26
 
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+
3
+ const { parseFlags } = require("../lib/args");
4
+ const { runWikiSearch } = require("../lib/wiki-search");
5
+
6
+ const SEARCH_FLAG_DEFS = {
7
+ help: { type: "boolean", alias: "h" },
8
+ "project-path": { type: "string" },
9
+ json: { type: "boolean" },
10
+ limit: { type: "string" },
11
+ };
12
+
13
+ function parseSearchFlags(args) {
14
+ return parseFlags(args || [], SEARCH_FLAG_DEFS);
15
+ }
16
+
17
+ function printSearchHelp() {
18
+ console.log(`SDTK-WIKI Local Search
19
+
20
+ Usage:
21
+ sdtk-wiki search --project-path <path> "multi-agent"
22
+ sdtk-wiki search --project-path <path> --json --limit 10 "Claude Code"
23
+
24
+ Purpose:
25
+ Deterministically search generated personal-brain Markdown pages.
26
+
27
+ Inputs:
28
+ .sdtk/wiki/personal-brain/**/*.md
29
+
30
+ Behavior:
31
+ Read-only and non-premium.
32
+ No wiki.ask entitlement is required.
33
+ No LLM, RAG, web search, query history, compile/apply, prune, or project mutation is performed.`);
34
+ return 0;
35
+ }
36
+
37
+ function printHumanResult(result) {
38
+ const lines = [
39
+ `Query: ${result.query}`,
40
+ `Search mode: ${result.searchMode}`,
41
+ `Personal brain: ${result.personalBrainPath}`,
42
+ `Scanned files: ${result.scannedFiles}`,
43
+ `Matches: ${result.totalMatches}`,
44
+ "",
45
+ ];
46
+
47
+ if (result.matches.length === 0) {
48
+ lines.push("No local personal-brain matches found.");
49
+ } else {
50
+ result.matches.forEach((match, index) => {
51
+ lines.push(`${index + 1}. ${match.path}`);
52
+ lines.push(` title: ${match.title}`);
53
+ lines.push(` score: ${match.score}`);
54
+ lines.push(` why: ${match.why}`);
55
+ lines.push(` snippet: ${match.snippet}`);
56
+ lines.push("");
57
+ });
58
+ }
59
+
60
+ lines.push("No entitlement, LLM/RAG runtime, query history, or project mutation was used.");
61
+ console.log(lines.join("\n").trimEnd());
62
+ }
63
+
64
+ function cmdSearch(args) {
65
+ const { flags, positional } = parseSearchFlags(args || []);
66
+ if (flags.help) {
67
+ return printSearchHelp();
68
+ }
69
+ const query = positional.join(" ");
70
+ const result = runWikiSearch({
71
+ projectPath: flags["project-path"],
72
+ query,
73
+ limit: flags.limit,
74
+ });
75
+
76
+ if (flags.json) {
77
+ console.log(JSON.stringify(result, null, 2));
78
+ } else {
79
+ printHumanResult(result);
80
+ }
81
+ return 0;
82
+ }
83
+
84
+ module.exports = {
85
+ cmdSearch,
86
+ parseSearchFlags,
87
+ printSearchHelp,
88
+ };