@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.
- package/CHANGELOG.md +118 -0
- package/README.md +2 -2
- package/SKILL.md +7 -0
- package/guide/cli.md +6 -4
- package/guide/consumers/index.md +106 -0
- package/guide/consumers/quickstart.md +96 -0
- package/guide/consumers/recipes/ci-gate.md +125 -0
- package/guide/consumers/recipes/dated-wiki.md +131 -0
- package/guide/consumers/recipes/format-gate.md +126 -0
- package/guide/consumers/recipes/post-write-heal.md +125 -0
- package/guide/consumers/recipes/skill-absent.md +111 -0
- package/guide/consumers/recipes/subject-wiki.md +110 -0
- package/guide/consumers/recipes/testing.md +149 -0
- package/guide/index.md +9 -0
- package/guide/substrate/operators.md +1 -1
- package/guide/substrate/tiered-ai.md +6 -5
- package/guide/ux/user-intent.md +6 -5
- package/package.json +9 -3
- package/scripts/cli.mjs +565 -15
- package/scripts/lib/balance.mjs +579 -0
- package/scripts/lib/cluster-detect.mjs +482 -4
- package/scripts/lib/contract.mjs +257 -0
- package/scripts/lib/decision-log.mjs +121 -15
- package/scripts/lib/heal.mjs +167 -0
- package/scripts/lib/init.mjs +210 -0
- package/scripts/lib/intent.mjs +370 -4
- package/scripts/lib/join-constants.mjs +22 -0
- package/scripts/lib/join.mjs +917 -0
- package/scripts/lib/json-envelope.mjs +190 -0
- package/scripts/lib/nest-applier.mjs +395 -32
- package/scripts/lib/operators.mjs +472 -38
- package/scripts/lib/orchestrator.mjs +419 -12
- package/scripts/lib/root-containment.mjs +351 -0
- package/scripts/lib/similarity-cache.mjs +115 -20
- package/scripts/lib/similarity.mjs +11 -0
- package/scripts/lib/soft-dag.mjs +726 -0
- package/scripts/lib/templates.mjs +78 -0
- package/scripts/lib/tiered.mjs +42 -18
- package/scripts/lib/validate.mjs +22 -0
- package/scripts/lib/where.mjs +71 -0
- package/scripts/testkit/assert-frontmatter.mjs +171 -0
- package/scripts/testkit/cli-run.mjs +95 -0
- package/scripts/testkit/make-wiki-fixture.mjs +301 -0
- package/scripts/testkit/stub-skill.mjs +107 -0
- package/templates/adrs.llmwiki.layout.yaml +33 -0
- package/templates/plans.llmwiki.layout.yaml +34 -0
- package/templates/regressions.llmwiki.layout.yaml +34 -0
- package/templates/reports.llmwiki.layout.yaml +33 -0
- package/templates/runbooks.llmwiki.layout.yaml +33 -0
- 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,
|
|
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
|
|
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;
|
|
89
|
-
|
|
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
|
|
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
|
-
| `
|
|
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
|
|
package/guide/ux/user-intent.md
CHANGED
|
@@ -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` / `
|
|
83
|
+
| INT-13 | Unknown `--quality-mode` value | use `tiered-fast` / `claude-first` / `deterministic` |
|
|
84
84
|
|
|
85
|
-
## `--json
|
|
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
|
|
89
|
-
object on
|
|
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
|
|
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
|
}
|