@ctxr/skill-llm-wiki 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +2 -2
  3. package/SKILL.md +7 -0
  4. package/guide/cli.md +6 -4
  5. package/guide/consumers/index.md +106 -0
  6. package/guide/consumers/quickstart.md +96 -0
  7. package/guide/consumers/recipes/ci-gate.md +125 -0
  8. package/guide/consumers/recipes/dated-wiki.md +131 -0
  9. package/guide/consumers/recipes/format-gate.md +126 -0
  10. package/guide/consumers/recipes/post-write-heal.md +125 -0
  11. package/guide/consumers/recipes/skill-absent.md +111 -0
  12. package/guide/consumers/recipes/subject-wiki.md +110 -0
  13. package/guide/consumers/recipes/testing.md +149 -0
  14. package/guide/index.md +9 -0
  15. package/guide/substrate/operators.md +1 -1
  16. package/guide/substrate/tiered-ai.md +6 -5
  17. package/guide/ux/user-intent.md +6 -5
  18. package/package.json +9 -3
  19. package/scripts/cli.mjs +565 -15
  20. package/scripts/lib/balance.mjs +579 -0
  21. package/scripts/lib/cluster-detect.mjs +482 -4
  22. package/scripts/lib/contract.mjs +257 -0
  23. package/scripts/lib/decision-log.mjs +121 -15
  24. package/scripts/lib/heal.mjs +167 -0
  25. package/scripts/lib/init.mjs +210 -0
  26. package/scripts/lib/intent.mjs +370 -4
  27. package/scripts/lib/join-constants.mjs +22 -0
  28. package/scripts/lib/join.mjs +917 -0
  29. package/scripts/lib/json-envelope.mjs +190 -0
  30. package/scripts/lib/nest-applier.mjs +395 -32
  31. package/scripts/lib/operators.mjs +472 -38
  32. package/scripts/lib/orchestrator.mjs +419 -12
  33. package/scripts/lib/root-containment.mjs +351 -0
  34. package/scripts/lib/similarity-cache.mjs +115 -20
  35. package/scripts/lib/similarity.mjs +11 -0
  36. package/scripts/lib/soft-dag.mjs +726 -0
  37. package/scripts/lib/templates.mjs +78 -0
  38. package/scripts/lib/tiered.mjs +42 -18
  39. package/scripts/lib/validate.mjs +22 -0
  40. package/scripts/lib/where.mjs +71 -0
  41. package/scripts/testkit/assert-frontmatter.mjs +171 -0
  42. package/scripts/testkit/cli-run.mjs +95 -0
  43. package/scripts/testkit/make-wiki-fixture.mjs +301 -0
  44. package/scripts/testkit/stub-skill.mjs +107 -0
  45. package/templates/adrs.llmwiki.layout.yaml +33 -0
  46. package/templates/plans.llmwiki.layout.yaml +34 -0
  47. package/templates/regressions.llmwiki.layout.yaml +34 -0
  48. package/templates/reports.llmwiki.layout.yaml +33 -0
  49. package/templates/runbooks.llmwiki.layout.yaml +33 -0
  50. package/templates/sessions.llmwiki.layout.yaml +34 -0
@@ -0,0 +1,149 @@
1
+ ---
2
+ id: recipe-testing
3
+ type: primary
4
+ depth_role: leaf
5
+ focus: "Use the shipped testkit in consumer test suites"
6
+ parents:
7
+ - ../index.md
8
+ tags:
9
+ - testing
10
+ - testkit
11
+ - fixtures
12
+ - consumers
13
+ activation:
14
+ keyword_matches:
15
+ - test harness
16
+ - consumer tests
17
+ - testkit
18
+ - stub skill
19
+ - wiki fixture
20
+ tag_matches:
21
+ - testing
22
+ - testkit
23
+
24
+ generator: "skill-llm-wiki/v1"
25
+ ---
26
+
27
+ # Recipe: testing with the shipped testkit
28
+
29
+ ## Trigger
30
+
31
+ Your consumer has tests that interact with `skill-llm-wiki` (e.g. your installer refuses without the skill present, your wiki-write workflow post-heal classifies verdicts). Use the shipped testkit instead of hand-rolling fixtures.
32
+
33
+ ## Commands to locate the testkit
34
+
35
+ ```bash
36
+ skill-llm-wiki where --json | jq -r '.testkit_dir'
37
+ ```
38
+
39
+ The returned absolute path contains:
40
+
41
+ - `stub-skill.mjs` — seed a presence-only skill under `.claude/skills/` or `.agents/skills/`.
42
+ - `make-wiki-fixture.mjs` — build a minimal hosted-mode wiki at a temp path using the shipped templates.
43
+ - `assert-frontmatter.mjs` — parse a leaf's frontmatter and assert expected fields.
44
+ - `cli-run.mjs` — spawn the CLI, capture stdout/stderr/status, auto-parse the envelope.
45
+
46
+ > **About `<testkit_dir>` in the code below:** the literal string
47
+ > `<testkit_dir>` is a placeholder. Resolve it at test-load time via
48
+ > the `where` probe, and convert the filesystem path to a `file://`
49
+ > URL before passing it to dynamic `import()` — Node ESM requires
50
+ > this on Windows (where `TESTKIT` looks like `C:\...`) and it is
51
+ > harmless on POSIX. Example:
52
+ >
53
+ > ```js
54
+ > import { spawnSync } from "node:child_process";
55
+ > import { pathToFileURL } from "node:url";
56
+ > const WHERE = JSON.parse(
57
+ > spawnSync("skill-llm-wiki", ["where", "--json"], { encoding: "utf8" }).stdout,
58
+ > );
59
+ > const TESTKIT = WHERE.testkit_dir;
60
+ > const { stubSkill } = await import(
61
+ > pathToFileURL(`${TESTKIT}/stub-skill.mjs`).href,
62
+ > );
63
+ > ```
64
+ >
65
+ > Do not copy the `<testkit_dir>/stub-skill.mjs` string verbatim
66
+ > into your imports — it will not resolve.
67
+
68
+ ## Consumer test code
69
+
70
+ ### Presence stub (replaces hand-rolled `wikiSkillStub`)
71
+
72
+ ```js
73
+ import { test } from "node:test";
74
+ import { mkdtempSync } from "node:fs";
75
+ import { tmpdir } from "node:os";
76
+ import { join } from "node:path";
77
+ import { stubSkill } from "<testkit_dir>/stub-skill.mjs";
78
+
79
+ test("installer refuses when skill is absent", async () => {
80
+ const home = mkdtempSync(join(tmpdir(), "test-"));
81
+ // Do NOT call stubSkill — simulate the absent case.
82
+ const r = await runInstaller({ env: { HOME: home } });
83
+ assert.equal(r.status, 1);
84
+ assert.match(r.stderr, /skill-llm-wiki is not installed/);
85
+ });
86
+
87
+ test("installer proceeds when stub is present", async () => {
88
+ const home = mkdtempSync(join(tmpdir(), "test-"));
89
+ await stubSkill({ home });
90
+ const r = await runInstaller({ env: { HOME: home } });
91
+ assert.equal(r.status, 0);
92
+ });
93
+ ```
94
+
95
+ ### Fixture wiki (for exercising write workflows)
96
+
97
+ ```js
98
+ import { makeWikiFixture } from "<testkit_dir>/make-wiki-fixture.mjs";
99
+
100
+ test("my consumer writes a report leaf in the right shape", async () => {
101
+ const wiki = await makeWikiFixture({
102
+ path: join(tmpdir(), `reports-${Date.now()}`),
103
+ kind: "dated",
104
+ template: "reports",
105
+ seedLeaves: ["2026/04/18/example.md"],
106
+ });
107
+ // Now drive your consumer write code against `wiki.path`.
108
+ await writeMyLeaf(wiki.path, "2026/04/18/new-report.md", "content");
109
+ assert.ok(existsSync(join(wiki.path, "2026/04/18/new-report.md")));
110
+ });
111
+ ```
112
+
113
+ ### CLI run (for end-to-end verdict-handling tests)
114
+
115
+ ```js
116
+ import { runCli } from "<testkit_dir>/cli-run.mjs";
117
+
118
+ test("heal on a fresh wiki returns verdict=ok", async () => {
119
+ const wiki = await makeWikiFixture({ path: tmpWiki(), kind: "dated" });
120
+ const r = runCli(["heal", wiki.path, "--json"]);
121
+ assert.equal(r.envelope.verdict, "ok");
122
+ });
123
+ ```
124
+
125
+ ### Frontmatter assertions
126
+
127
+ ```js
128
+ import { assertFrontmatterShape } from "<testkit_dir>/assert-frontmatter.mjs";
129
+
130
+ test("my consumer writes the expected frontmatter", async () => {
131
+ await writeMyLeaf(wiki.path, "2026/04/18/leaf.md", "content");
132
+ assertFrontmatterShape(join(wiki.path, "2026/04/18/leaf.md"), {
133
+ type: "primary",
134
+ depth_role: "leaf",
135
+ focus: "leaf",
136
+ });
137
+ });
138
+ ```
139
+
140
+ ## Failure modes
141
+
142
+ - `testkit_dir` is `null`: you are running against an old skill version without the testkit. Gate on `format_version >= 1` in CI.
143
+ - `stubSkill` fails with "unknown layout": pass one of `"claude-skills"` or `"agents-skills"`.
144
+ - `makeWikiFixture` fails on the template lookup: pass a name matching one of the shipped templates (see [dated-wiki.md](dated-wiki.md) / [subject-wiki.md](subject-wiki.md)).
145
+
146
+ ## Do not
147
+
148
+ - Import from `scripts/lib/` in your tests. Those are internal; only `scripts/testkit/` is part of the consumer contract.
149
+ - Hand-roll parallel stub helpers once this testkit exists. Drift between your stub and the skill's canonical shape is a real bug source.
package/guide/index.md CHANGED
@@ -74,6 +74,13 @@ entries:
74
74
  file: "ux/index.md"
75
75
  type: index
76
76
  focus: User-facing intent resolution and preflight failure messaging.
77
+ - id: consumers
78
+ file: "consumers/index.md"
79
+ type: index
80
+ focus: "Integrating another skill or agent as a consumer of skill-llm-wiki."
81
+ tags:
82
+ - consumers
83
+ - integration
77
84
  children:
78
85
  - "basics/index.md"
79
86
  - "correctness/index.md"
@@ -83,6 +90,7 @@ children:
83
90
  - "operations/index.md"
84
91
  - "substrate/index.md"
85
92
  - "ux/index.md"
93
+ - "consumers/index.md"
86
94
  ---
87
95
  <!-- BEGIN AUTO-GENERATED NAVIGATION -->
88
96
 
@@ -110,6 +118,7 @@ children:
110
118
  | [operations/index.md](operations/index.md) | 📁 index | per-operation phase pipelines for Build, Extend, Validate, Rebuild, Fix, and Join |
111
119
  | [substrate/index.md](substrate/index.md) | 📁 index | Decision machinery — rewrite operators and the tiered AI ladder driving them. |
112
120
  | [ux/index.md](ux/index.md) | 📁 index | User-facing intent resolution and preflight failure messaging. |
121
+ | [consumers/index.md](consumers/index.md) | 📁 index | Integrating another skill or agent as a consumer of skill-llm-wiki. |
113
122
 
114
123
  <!-- END AUTO-GENERATED NAVIGATION -->
115
124
 
@@ -61,7 +61,7 @@ NEST fires in two modes:
61
61
 
62
62
  **Cluster-based application.** Each accepted cluster is named via a Tier 2 `cluster_name` request (slug + purpose) — or receives a slug directly from a `propose_structure` Tier 2 response. Names are NEVER shortcut from shared tags; if the sub-agent cannot name a cluster, that cluster does not nest. The NEST applier (`scripts/lib/nest-applier.mjs`) then:
63
63
 
64
- 1. **Atomic slug resolution.** Before touching the filesystem, `resolveNestSlug(slug, proposal)` checks whether the proposed slug collides with (a) any member leaf's id, (b) any non-member sibling leaf's id in the same parent, or (c) an existing sibling subdirectory name. On collision the slug is auto-suffixed deterministically (`<slug>-group`, then `<slug>-group-2`, `-group-3`, …) until it's non-colliding. The rename is audited in `decisions.yaml` as `decision: slug-renamed`. This pre-empts the DUP-ID class of validation failure that would otherwise rollback the entire NEST after apply.
64
+ 1. **Atomic slug resolution.** Before touching the filesystem, `resolveNestSlug(slug, proposal, wikiRoot, opts)` checks whether the proposed slug collides with (a) any member leaf's id, (b) any non-member sibling leaf's id in the same parent, (c) an existing sibling subdirectory name, or (d) any live leaf id or subdirectory basename elsewhere in the tree (full-tree walk, activated whenever `wikiRoot` is provided). On collision the slug is auto-suffixed deterministically (`<slug>-group`, then `<slug>-group-2`, `-group-3`, …) until it's non-colliding. The rename is audited in `decisions.yaml` as `decision: slug-renamed`. This pre-empts the DUP-ID class of validation failure that would otherwise rollback the entire NEST after apply — including cross-depth collisions (e.g. a cluster slug `event-patterns` proposed under `design-patterns-group/` that would collide with an existing `arch/event-patterns/` in a different branch of the tree). The optional `opts.wikiIndex` argument accepts a precomputed `Set` from `buildWikiForbiddenIndex(wikiRoot)` — the convergence loop builds it once per iteration and mutates it with `wikiIndex.add(resolvedSlug)` after each successful apply, dropping per-proposal cost from O(full-tree) to O(parent-dir). `wikiRoot` is itself optional: legacy callers that omit it get the parent-dir-only walk preserved from v1.0.0 (modulo the dot-skip rule described in the module source).
65
65
  2. Creates `<parent>/<slug>/` (using the resolved slug).
66
66
  3. Moves each cluster member into the new directory and rewrites its `parents[]` to `["index.md"]`.
67
67
  4. Writes a minimal `index.md` stub carrying `id` (= resolved slug), `type: index`, `depth_role: subcategory`, a `focus:` line from the cluster purpose, and — when the members share them — `shared_covers[]` (intersection of member covers) and `tags[]` (intersection of member tags). The stub does NOT carry aggregated `activation_defaults`: routing is semantic, and descent decisions are made against the stub's `focus` + `shared_covers`, not against a literal keyword union.
@@ -9,7 +9,7 @@ covers:
9
9
  - "Tier 0 is TF-IDF over frontmatter (focus + covers + tags) with fixed thresholds"
10
10
  - "Tier 1 is local embeddings via @xenova/transformers (MiniLM, REQUIRED dep)"
11
11
  - "Tier 2 is a sub-agent, executed via the CLI exit-7 handshake (never inline)"
12
- - default quality mode is tiered-fast; claude-first and tier0-only are opt-in
12
+ - default quality mode is tiered-fast; claude-first and deterministic are opt-in
13
13
  - "similarity-cache at <wiki>/.llmwiki/similarity-cache/ memoises pairwise results"
14
14
  - "decision-log at <wiki>/.llmwiki/decisions.yaml records every non-trivial decision"
15
15
  - operator-convergence routes every MERGE similarity check through tiered.decide
@@ -85,8 +85,9 @@ well-structured corpora — pairs of near-duplicate entries
85
85
  should collapse as SAME, obviously unrelated pairs as DIFFERENT
86
86
  — leaving only genuinely ambiguous pairs to escalate. The actual
87
87
  Tier 0 hit rate on a given wiki depends on how informative the
88
- frontmatter is; run with `--quality-mode tier0-only` and inspect
89
- `decisions.yaml` to measure the tier distribution for your corpus.
88
+ frontmatter is; inspect `decisions.yaml` after a build to measure
89
+ the tier distribution for your corpus (grep the `tier:` field on
90
+ every decision entry).
90
91
 
91
92
  ## Tier 1 — local embeddings (scripts/lib/embeddings.mjs)
92
93
 
@@ -275,13 +276,13 @@ all of its Tier 2 cost on subsequent rebuilds.
275
276
 
276
277
  ## Quality modes
277
278
 
278
- Choose via `--quality-mode` or the `LLM_WIKI_QUALITY_MODE` env var.
279
+ Choose via `--quality-mode` (flag) or `LLM_WIKI_QUALITY_MODE` (env var). The flag wins when both are set. Invalid values on EITHER path raise `INT-13` at the intent layer with the same valid-values suggestions — a stale env value from an obsolete shell profile fails loud on the next `skill-llm-wiki` invocation rather than silently falling through to a plain throw at convergence time. Env-var validation is gated to subcommands that consume quality mode (build / extend / rebuild / fix / join); recovery paths like `rollback` are unaffected.
279
280
 
280
281
  | Mode | Behaviour | Use when |
281
282
  |------|-----------|----------|
282
283
  | **`tiered-fast`** (default) | Full ladder. Tier 0 → Tier 1 → Tier 2 on mid-band escalations. | General-purpose builds. |
283
284
  | `claude-first` | Tier 0 is still consulted for decisive cases. Mid-band Tier 0 skips Tier 1 and goes directly to Tier 2. | When the user values Claude's judgment over speed/cost. |
284
- | `tier0-only` | Tier 0 only. Mid-band decisions become "undecidable" and the caller must resolve manually. | Air-gapped, hermetic CI, and smoke tests that must not reach out to Claude. |
285
+ | `deterministic` | Tier 0 Tier 1 ladder with a static threshold resolving mid-band Tier 1 pairs. No LLM/sub-agent is ever consulted. Cluster naming comes from `generateDeterministicSlug` + `deterministicPurpose`; Tier 2 escalations are skipped entirely. Repeated runs on the same inputs produce byte-reproducible output. | Hermetic CI; large deterministic corpus builds where reproducibility matters more than Tier 2's naming nuance. For air-gapped use, pre-warm the Tier 1 MiniLM model cache on a networked host — `@xenova/transformers` downloads the model on first use otherwise. |
285
286
 
286
287
  ## Similarity cache
287
288
 
@@ -9,7 +9,7 @@ covers:
9
9
  - the CLI refuses every ambiguous invocation with a structured INT-NN code
10
10
  - Claude MUST ask the user before running the skill when intent is unclear
11
11
  - ambiguity scenarios each have a fixed resolving flag the user can pick
12
- - "--json-errors makes the ambiguity body machine-parseable for Claude to read"
12
+ - "--json (canonical) or --json-errors (legacy alias) makes the ambiguity body machine-parseable for Claude to read"
13
13
  - "--no-prompt / LLM_WIKI_NO_PROMPT=1 disables interactive fallback; failures become hard errors"
14
14
  - never silently default — the cost of a wrong guess is always higher than a clarifying question
15
15
  tags:
@@ -80,13 +80,14 @@ in the wrong place) is always higher than a one-sentence clarifying question.
80
80
  | INT-10 | Unknown `--layout-mode` value | use `sibling` / `in-place` / `hosted` |
81
81
  | INT-11 | Unknown flag / malformed flag value | correct the flag |
82
82
  | INT-12 | Prompt required in non-interactive mode | supply the flag the prompt was asking for, or re-run in a TTY |
83
- | INT-13 | Unknown `--quality-mode` value | use `tiered-fast` / `claude-first` / `tier0-only` |
83
+ | INT-13 | Unknown `--quality-mode` value | use `tiered-fast` / `claude-first` / `deterministic` |
84
84
 
85
- ## `--json-errors` for programmatic consumption
85
+ ## `--json` for programmatic consumption
86
86
 
87
87
  When the skill is called from a script or from another Claude session, pass
88
- `--json-errors` on every invocation. The ambiguity body becomes a single JSON
89
- object on stderr:
88
+ `--json` (canonical) on every invocation; `--json-errors` is the legacy alias
89
+ and continues to work. The ambiguity body becomes a single JSON object on
90
+ stderr:
90
91
 
91
92
  ```json
92
93
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctxr/skill-llm-wiki",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Claude Code skill — build, extend, validate, rebuild, fix, and join LLM wikis from any knowledge corpus. Token-efficient retrieval via hierarchical indices, DAG parents, and deterministic rewrite operators.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,12 +31,16 @@
31
31
  "LICENSE",
32
32
  "CHANGELOG.md",
33
33
  "scripts/",
34
- "guide/"
34
+ "guide/",
35
+ "templates/"
35
36
  ],
36
37
  "ctxr": {
37
38
  "type": "skill",
38
39
  "target": "folder"
39
40
  },
41
+ "bin": {
42
+ "skill-llm-wiki": "scripts/cli.mjs"
43
+ },
40
44
  "scripts": {
41
45
  "test": "node --test",
42
46
  "validate": "node scripts/cli.mjs --version",
@@ -50,6 +54,8 @@
50
54
  },
51
55
  "dependencies": {
52
56
  "@xenova/transformers": "2.17.2",
53
- "gray-matter": "^4.0.3"
57
+ "gray-matter": "^4.0.3",
58
+ "p-retry": "^6.2.0",
59
+ "p-timeout": "^6.1.3"
54
60
  }
55
61
  }