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 +87 -11
- package/assets/atlas/build_atlas.py +164 -79
- package/package.json +1 -1
- package/src/commands/help.js +10 -3
- package/src/commands/lint.js +2 -1
- package/src/commands/search.js +88 -0
- package/src/commands/wiki.js +83 -9
- package/src/index.js +4 -1
- package/src/lib/wiki-compile.js +694 -6
- package/src/lib/wiki-extract.js +637 -0
- package/src/lib/wiki-flags.js +8 -0
- package/src/lib/wiki-lint.js +179 -2
- package/src/lib/wiki-search.js +175 -0
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
|
|
40
|
-
|
|
|
41
|
-
|
|
|
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
|
|
174
|
-
|
|
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`.
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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,
|
|
649
|
-
prior_state = load_atlas_state(atlas_dir)
|
|
650
|
-
prior_documents = prior_state.get("documents", {})
|
|
651
|
-
|
|
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
|
-
"
|
|
716
|
-
"
|
|
717
|
-
"
|
|
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,
|
|
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"
|
|
858
|
-
f"
|
|
859
|
-
f"
|
|
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
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
package/src/commands/help.js
CHANGED
|
@@ -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
|
|
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
|
|
61
|
-
|
|
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
|
|
package/src/commands/lint.js
CHANGED
|
@@ -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
|
+
};
|