@markstanden/opencode-docs-plugin 0.10.0 → 0.13.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/CONVENTION.md CHANGED
@@ -4,46 +4,72 @@ This plugin owns this convention: it is what the `docs` tool's `add` writes,
4
4
  what `check` validates, and what the registered `docs` skill teaches. A
5
5
  repository that adopts the plugin takes the convention with it.
6
6
 
7
- ## Classes and scope
8
-
9
- Every doc belongs to exactly one class. The class is fixed by the lake it lives
10
- in, so a doc cannot mix scopes.
11
-
12
- | Class | Where | Reader | Writes |
13
- | ------------------ | ----------------------------------- | ---------------------------- | ------------------------------------- |
14
- | **Instructions** | nested `README.md` files | code-local how-to | never by the tool |
15
- | **RepoContext** | `<repo>/docs` by default | this repo | yes |
16
- | **MachineContext** | a personal lake such as `~/doclake` | this person, across machines | yes |
17
- | **GlobalContext** | an organisation lake | every team, every repo | yes, lock down with `writable: false` |
18
- | **Wiki** | a human-readable wiki checkout | humans first, agents second | no |
19
-
20
- Place at the **narrowest scope where the whole claim is true**:
21
-
22
- 1. Command or step to build, run, test, deploy → Instructions.
23
- 2. True only for this repo → RepoContext.
24
- 3. True for you across machines → MachineContext.
25
- 4. True for the org across repos → GlobalContext.
26
- 5. Human-facing narrative → Wiki.
27
-
28
- Two answers mean two focused docs, one per class. A doc that needs a placement
29
- debate is not focused enough.
7
+ ## Scopes and classes
8
+
9
+ Every doc belongs to exactly one scope. The scope is fixed by the lake it
10
+ lives in, so a doc cannot mix scopes. The config values below never change;
11
+ the labels are how the tool presents them.
12
+
13
+ | Scope | Config value | Where | Reader | Meaning | Writes |
14
+ | ------------ | ------------------ | ----------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------- |
15
+ | Repository | `repo` | `<repo>/docs` by default | this repo | understanding or maintaining this particular repository | yes |
16
+ | Personal | `machine` | a personal lake such as `~/doclake` | one person, any machine | tied to a person's devices, accounts, preferences or individual setup — one named host counts | yes |
17
+ | Organisation | `global` | an organisation lake | the owning teams | established shared standards, services or integrations, with their boundary stated | yes, lock down with `writable: false` |
18
+ | Instructions | nested `README.md` | the project tree | code-local how-to | build, run, test, deploy this code | never by the tool |
19
+ | Wiki | `wiki` | a human-readable wiki checkout | humans first | human-facing reference and narrative | no |
20
+
21
+ Place at the **narrowest justified scope**:
22
+
23
+ 1. Identify the claim, what it depends on and its evidence. Where the session
24
+ happened is not itself a destination.
25
+ 2. Depends on this repository's implementation, architecture or decisions →
26
+ Repository.
27
+ 3. Depends on a person's device, account or individual setup → Personal.
28
+ 4. An established shared rule or integration → Organisation, recording its
29
+ actual team/service boundary. It need not apply to every team to belong
30
+ there.
31
+ 5. Reusability alone is not organisational ownership: a possible
32
+ generalisation from one repo stays with the repo that observed it, and its
33
+ conditions, until broader applicability or ownership is established.
34
+ 6. Operational procedures belong to the README editing workflow — `add`
35
+ cannot write them. A knowledge doc may keep the minimal diagnostic
36
+ commands that substantiate its claim and link to the maintained procedure;
37
+ commands are not banned from knowledge docs.
38
+ 7. Human narrative follows the wiki's owner workflow. The wiki being
39
+ read-only to the tool is not a reason to write a knowledge lake instead.
40
+ 8. Split only when there are independently useful claims with different
41
+ owners or topics. A shared rule plus a genuine repo exception may need two
42
+ linked docs; an uncertain owner does not make a doc unfocused.
43
+ 9. If ownership cannot be established, keep an explicitly unresolved proposal
44
+ in the handover and ask a focused question. Never guess a broader scope or
45
+ silently write to a substitute lake.
46
+
47
+ Two answers with different owners means two focused docs, one per scope.
48
+ Configured lakes may carry a `description` saying what belongs in that
49
+ particular destination; when several writable lakes exist, `add` requires an
50
+ explicit `lake` choice.
30
51
 
31
52
  ## Where knowledge lives
32
53
 
33
54
  Durable knowledge is one Markdown file per topic under a lake root, grouped by
34
55
  area: `<area>/<name>.md`. The default repo lake is `<location>/docs`; its docs
35
- are RepoContext. A repo adds other local roots in
56
+ are Repository scope. A repo adds other local roots in
36
57
  `.opencode/docs-lakes.json`; a personal or managed base file lists the
37
- machine, global and wiki lakes.
58
+ personal, organisation and wiki lakes.
38
59
 
39
60
  Lakes are named, and several per class are supported. A root nested inside
40
61
  another lake's root belongs to the **narrowest enclosing lake**: the outer lake
41
- skips it, search returns the inner lake's entry, a bare `read` resolves there,
42
- and `add` writes to the deepest writable knowledge lake. Nesting is decided on
43
- real paths, so reaching one root through a symlink cannot defeat containment.
44
- Two lakes that do not nest holding the same relative path stay a refusal —
45
- pass `lake`. Near-twin checks apply inside one lake and never pair across the
46
- nesting boundary.
62
+ skips it, search returns the inner lake's entry, and a bare `read` resolves
63
+ there. Nesting resolves the physical owner of an already identified target; it
64
+ never infers the semantic owner of a new claim — `add` without a `lake`
65
+ selector requires exactly one configured writable knowledge lake, and any
66
+ other number refuses with the candidate list. Nesting is decided on
67
+ real paths, so reaching one root through a symlink cannot defeat containment,
68
+ and a write addressed through an outer route that physically resolves inside
69
+ a nested lake's tree refuses and names its physical owner. Two lakes that do
70
+ not nest holding the same relative path stay a refusal — pass `lake`.
71
+ Near-twin checks apply inside one lake and never pair across the nesting
72
+ boundary.
47
73
 
48
74
  Lake notes — a missing root, or a root outside version control — are computed
49
75
  per call, not captured at startup: create, remove or version a root and the
@@ -115,11 +141,12 @@ git. Optional invisible metadata may be added later; it is not required.
115
141
  `add` refuses a near-twin write and returns the candidates to reconcile
116
142
  against — edit before add, merge near-twins, split a bloated doc into
117
143
  per-topic files.
118
- - The reconcile applies to a **create**: with `path` omitted, the target is
119
- derived and the near-twin guard runs. An explicit `path` is the update
120
- contract and skips it, which is how you replace a doc in place. Passing an
121
- explicit _new_ path is therefore a deliberate override, not a protected
122
- write.
144
+ - The reconcile applies to every **create**, derived and explicit paths alike:
145
+ `add` refuses a near-twin and returns the candidates to reconcile against.
146
+ A non-blank `allowNearTwinReason` overrides that guard alone, for a
147
+ deliberate distinct doc; it never bypasses target existence or the
148
+ structural checks. Replacement is `mode: "replace"` with the existing
149
+ doc's path — wholesale, and never an implicit create.
123
150
  - Near-twins are per lake. The same topic at repo and global scope is
124
151
  legitimate; the narrower doc states its delta and links up.
125
152
 
@@ -127,10 +154,10 @@ git. Optional invisible metadata may be added later; it is not required.
127
154
 
128
155
  - One question per doc. One topic per doc.
129
156
  - More than one H2 aimed at different topics or audiences means split.
130
- - Never write an omnibus notes doc to avoid choosing a class.
131
- - The narrowest true scope owns the claim. A broader class owns the general
132
- rule; the narrower doc states only its delta and links up.
133
- - Do not copy between knowledge classes. Move or link.
157
+ - Never write an omnibus notes doc to avoid choosing a scope.
158
+ - The narrowest justified scope owns the claim. A broader scope owns the
159
+ general rule; the narrower doc states only its delta and links up.
160
+ - Do not copy between knowledge scopes. Move or link.
134
161
  - Instructions own commands. Knowledge docs own reasons and link to the
135
162
  command. Knowledge docs link to the wiki page for human narrative.
136
163
  - Wiki duplication is expected, not a defect. If a wiki page drifts from a
@@ -145,13 +172,16 @@ git. Optional invisible metadata may be added later; it is not required.
145
172
 
146
173
  - No secrets, credentials, tokens, or customer data. Name the vault or variable,
147
174
  never the value.
148
- - No personal data in GlobalContext or Wiki.
175
+ - No personal data in Organisation or Wiki.
149
176
  - The wiki profile is read-only to the tool. Its own owners review changes.
150
177
 
151
178
  ## Lifecycle
152
179
 
153
180
  A doc is current or deleted. Correct a wrong doc by replacing it in place with
154
- `add` and an explicit `path`. Delete an obsolete doc through the shell with
181
+ `mode: "replace"`, the doc's path, and the revision token your last read or
182
+ write returned in its result text — a stale token refuses rather than
183
+ overwriting a newer edit. Delete
184
+ an obsolete doc through the shell with
155
185
  explicit approval; there is no `delete` verb. A failed attempt is still valid
156
186
  knowledge when it records the outcome and the current truth.
157
187
 
package/README.md CHANGED
@@ -5,20 +5,25 @@ knowledge **lakes** it can actually use: a `docs` tool and a `/docs` command
5
5
  over configured Markdown trees, nested `README.md` files in the project, and the
6
6
  writing convention those docs follow.
7
7
 
8
- Docs are grouped by **class**:
9
-
10
- | Class | Where | Use it for | Writes |
11
- | ------------------ | ------------------------------- | --------------------------------------- | ------------- |
12
- | **Instructions** | nested `README.md` | build, run, test, deploy this code | never |
13
- | **RepoContext** | `<repo>/docs` by default | this repo's traps, decisions, landmines | yes |
14
- | **MachineContext** | personal lake, e.g. `~/doclake` | tooling and traps across your machines | yes |
15
- | **GlobalContext** | organisation lake | standards and integrations across repos | yes, reviewed |
16
- | **Wiki** | human-readable wiki checkout | narrative for humans | read-only |
17
-
18
- Place knowledge at the narrowest scope where the whole claim is true. A doc that
19
- spans scopes is split into focused docs, one per class. The full rulebook is
20
- [CONVENTION.md](CONVENTION.md); the registered `docs` skill carries the working
21
- version.
8
+ Docs are grouped by **scope** (config values never change; labels are how the
9
+ tool presents them):
10
+
11
+ | Scope | Config value | Where | Use it for | Writes |
12
+ | ---------------- | --------------------------- | ------------------------------- | ----------------------------------------------- | ------------- |
13
+ | **Repository** | `repo` / nested `README.md` | `<repo>/docs` by default | this repo's traps, decisions, landmines | yes |
14
+ | **Personal** | `machine` | personal lake, e.g. `~/doclake` | a person's devices, accounts and setup | yes |
15
+ | **Organisation** | `global` | organisation lake | standards and integrations, with their boundary | yes, reviewed |
16
+ | **Instructions** | implicit | nested `README.md` | build, run, test, deploy this code | never |
17
+ | **Wiki** | `wiki` | human-readable wiki checkout | narrative for humans | read-only |
18
+
19
+ Place knowledge at the **narrowest justified scope**. Identify the claim and
20
+ what it depends on: this repo → Repository; a person's device, account or
21
+ setup → Personal; an established shared rule → Organisation, stating its
22
+ actual team boundary; operational steps → the README workflow; human
23
+ narrative → the wiki's owners. Reusability alone is not organisational
24
+ ownership, and unresolved ownership stays an explicit question — never a
25
+ guessed destination. The full rulebook is [CONVENTION.md](CONVENTION.md); the
26
+ registered `docs` skill carries the working version.
22
27
 
23
28
  ## The tool and command
24
29
 
@@ -26,7 +31,8 @@ version.
26
31
  (`path — Area — date — Summary`); a search ranks docs over their front-matter,
27
32
  path and body identifiers (plus wiki prose), falling back to the closest
28
33
  matches rather than a dead end; `read` returns a doc verbatim and accepts a
29
- `lake` or `lake:path`; `add` writes knowledge through a typed DTO; `check`
34
+ `lake` or `lake:path`; `add` writes knowledge through a typed DTO with an
35
+ explicit `mode`; `check`
30
36
  validates the knowledge lakes. Nested project `README.md` files are indexed
31
37
  too — `.gitignore` rules decide what is skipped — but only for search: `add`
32
38
  and `check` never touch them, and their results are prefixed `instructions:`
@@ -89,21 +95,27 @@ The simplest setup uses `root` — one repo lake:
89
95
 
90
96
  `root` expands `~`, resolves a relative path against the location, and defaults
91
97
  to `<location>/docs`. A blank value counts as unset. `root` declares a **repo**
92
- lake; for a personal or global lake, use `lakes` with the class instead. Its
93
- name follows the root folder unless you set the `name` option beside it:
98
+ lake; for a personal or organisation lake, use `lakes` with the class instead.
99
+ Its name follows the root folder unless you set the `name` option beside it,
100
+ and `description` documents what belongs there the same way:
94
101
 
95
102
  ```jsonc
96
103
  {
97
104
  "plugins": [
98
105
  {
99
106
  "package": "@markstanden/opencode-docs-plugin",
100
- "options": { "root": "documentation", "name": "doclake" },
107
+ "options": {
108
+ "root": "documentation",
109
+ "name": "doclake",
110
+ "description": "This repository's decisions and traps.",
111
+ },
101
112
  },
102
113
  ],
103
114
  }
104
115
  ```
105
116
 
106
- `name` only applies to `root`; inside `lakes`, set it on the entry.
117
+ `name` and `description` only apply to `root`; inside `lakes`, set them on the
118
+ entry.
107
119
 
108
120
  For several lakes, list them inline, in a base file, or both. The base file is
109
121
  usually a personal or managed file; the repo file adds local lakes:
@@ -129,7 +141,12 @@ usually a personal or managed file; the repo file adds local lakes:
129
141
  ```jsonc
130
142
  {
131
143
  "lakes": [
132
- { "class": "machine", "root": "~/doclake", "name": "doclake" },
144
+ {
145
+ "class": "machine",
146
+ "root": "~/doclake",
147
+ "name": "doclake",
148
+ "description": "Personal devices, workstation configuration and individual tooling preferences.",
149
+ },
133
150
  { "class": "global", "root": "~/org-docs", "name": "global" },
134
151
  { "class": "wiki", "root": "~/org-wiki", "name": "wiki" },
135
152
  ],
@@ -162,12 +179,13 @@ docs live elsewhere.
162
179
 
163
180
  Lake entries:
164
181
 
165
- | Field | Meaning |
166
- | ---------- | ------------------------------------------------------------------------------------------------ |
167
- | `class` | `repo`, `machine`, `global` or `wiki`; default `repo`. |
168
- | `root` | The lake root. Required for `machine`/`global`/`wiki`; defaults to `<location>/docs` for `repo`. |
169
- | `name` | Lower-case dashes; defaults to the root folder name. `instructions` is reserved. |
170
- | `writable` | Defaults true for knowledge classes, fixed false for `wiki`. |
182
+ | Field | Meaning |
183
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
184
+ | `class` | `repo`, `machine`, `global` or `wiki`; default `repo`. Presented as Repository, Personal, Organisation, Wiki. |
185
+ | `root` | The lake root. Required for `machine`/`global`/`wiki`; defaults to `<location>/docs` for `repo`. |
186
+ | `name` | Lower-case dashes; defaults to the root folder name. `instructions` is reserved. |
187
+ | `description` | Optional. What belongs in this particular destination; shown in `help` and refusal candidate lists. Trimmed, blank counts as absent, non-string fails at startup. Descriptive data only — never indexed for search. |
188
+ | `writable` | Defaults true for knowledge classes, fixed false for `wiki`. |
171
189
 
172
190
  Validation follows writability: `check` validates a writable knowledge lake
173
191
  only. A read-only knowledge lake, and every `wiki` lake, is indexed for search
@@ -193,11 +211,13 @@ declarations may reach the tree by different routes and the nested doc is
193
211
  still indexed once.
194
212
 
195
213
  A doc belongs to the **narrowest enclosing lake**. With nested roots, `search`
196
- returns the inner lake's entry and a bare `read` resolves to it; `add` writes
197
- to the deepest writable knowledge lake, so an outer read-only wrapper never
198
- hijacks the write. Two lakes that do not nest holding the same relative path
199
- stay a refusal — pass `lake`. Near-twin checks apply inside one lake and never
200
- pair across the nesting boundary.
214
+ returns the inner lake's entry and a bare `read` resolves to it. `add` never
215
+ infers a destination from nesting: an omitted `lake` needs exactly one
216
+ configured writable knowledge lake (see below), and a write addressed through
217
+ an outer route that physically resolves inside a nested lake's tree refuses,
218
+ naming its physical owner. Two lakes that do not nest holding the same
219
+ relative path stay a refusal — pass `lake`. Near-twin checks apply inside one
220
+ lake and never pair across the nesting boundary.
201
221
 
202
222
  ```jsonc
203
223
  {
@@ -233,6 +253,26 @@ it `— missing`, and `add` refuses with the path rather than bootstrapping a
233
253
  tree. Create the directory deliberately. Area folders beneath an existing root
234
254
  are still created on write.
235
255
 
256
+ ### Choosing a write destination
257
+
258
+ `add` writes through the `lake` selector deliberately; it never guesses.
259
+
260
+ - An explicit `lake` selects exactly that lake: an unknown, read-only, or
261
+ missing-root lake refuses — it never falls back to another.
262
+ - With exactly one configured writable knowledge lake, an omitted `lake` uses
263
+ it. That is a convenience, not a classification: check the destination fits
264
+ before writing.
265
+ - With several writable knowledge lakes (two `machine` lakes included), an
266
+ omitted `lake` refuses before writing and lists **every** writable
267
+ candidate — name, scope label and config value, root, availability,
268
+ description. Nesting, class, configuration order and missing roots change
269
+ nothing: folder depth cannot infer the semantic owner of a claim, and no
270
+ candidate is auto-selected for being the deepest.
271
+ - With no writable knowledge lake, `add` refuses; wiki and read-only lakes are
272
+ never written. A temporarily missing root still counts as a candidate, so
273
+ dormancy cannot silently change the destination.
274
+ - Searching and checking never need a destination; they run across all lakes.
275
+
236
276
  Root and git notes are computed per call, not captured at startup: a root that
237
277
  appears, disappears or gains a git checkout is reflected by the next `check`,
238
278
  `search` and help without a restart.
@@ -263,8 +303,97 @@ empty findings list means compliant. A very large report is capped with an
263
303
  `and N more lines not shown` tail; narrow it with `lake`.
264
304
 
265
305
  There is no lifecycle field. A doc is current or deleted; git is the archive.
266
- Correct a wrong doc by replacing it in place. A failed attempt is valid
267
- knowledge when it records the outcome and what is true now.
306
+ Correct a wrong doc by replacing it in place with `mode: "replace"`. A failed
307
+ attempt is valid knowledge when it records the outcome and what is true now.
308
+
309
+ ## Creating and replacing
310
+
311
+ `add` carries an explicit mutation `mode`; the implicit path-based update of
312
+ earlier versions is intentionally unsupported.
313
+
314
+ ```jsonc
315
+ // Create: path is optional — omitted derives <Area>/<slug>.md from the H1.
316
+ {
317
+ "add": {
318
+ "mode": "create",
319
+ "metadata": { "Area": "decisions", "Keywords": ["queue"], "Summary": "Why Kafka." },
320
+ "body": "# Queue choice\n\n### Facts\n\n- Kafka, for the replay window.\n"
321
+ }
322
+ }
323
+
324
+ // Replace: path is required and must name an existing doc, and
325
+ // expectedRevision must match the doc's current bytes — the token your last
326
+ // read (or write) returned. The body is wholesale: omitted content is lost.
327
+ // A stale token refuses; read again, reconcile, resubmit.
328
+ {
329
+ "add": {
330
+ "mode": "replace",
331
+ "path": "decisions/queue-choice.md",
332
+ "expectedRevision": "<sha256 from the read>",
333
+ "metadata": { "Area": "decisions", "Keywords": ["queue"], "Summary": "Why Kafka." },
334
+ "body": "# Queue choice\n\n### Facts\n\n- Kafka, for the replay window.\n"
335
+ }
336
+ }
337
+ ```
338
+
339
+ A `read` of a knowledge lake doc carries the token in its metadata alongside
340
+ the resolved target — `{ read, lake, path, revision }` — and a successful
341
+ create or replace returns the new content's revision the same way. The result
342
+ text also ends with a `docs: revision …` line, so the token is visible to a
343
+ model call and to Code Mode JavaScript, not only in the stored metadata a
344
+ host may treat as UI-only. Wiki reads carry no revision: they promise no
345
+ write-capable workflow.
346
+
347
+ - **Create never overwrites.** A target that already exists refuses, even with
348
+ an `allowNearTwinReason`; pass `mode: "replace"` deliberately.
349
+ - **Every create reconciles.** An explicit path gets the same structural and
350
+ same-area near-twin checks as a derived path. A genuine near-twin refusal is
351
+ answered by `mode: "replace"` — or, for a deliberate distinct doc, a
352
+ non-blank `allowNearTwinReason`, which bypasses only that semantic guard,
353
+ never target existence, path safety, metadata validation, writability or
354
+ destination ambiguity.
355
+ - **Replace never creates.** A missing target refuses; it is never an
356
+ implicit create.
357
+ - **Revisions are content, not history.** A revision is the SHA-256 of the
358
+ doc's exact stored bytes; if the same bytes return later, the token matches
359
+ again. Create takes no `expectedRevision` — its precondition is that the
360
+ target does not exist.
361
+
362
+ ## Concurrency limits
363
+
364
+ Publications of one target are serialised by a per-target lock file in a
365
+ hidden `.docs-lock/` directory **beside the target's directory on the real
366
+ filesystem**, keyed on the target's canonical physical path: two
367
+ configurations reaching the same tree by different routes — including a
368
+ permitted symlinked configured root — resolve the same lock location, however
369
+ each runtime's lakes are configured. The lock directory is only ever created
370
+ inside an already-existing ancestor, existing symlinked or non-directory
371
+ objects refuse without being touched, and abandoned locks are never stolen
372
+ automatically (confirm the owner is gone, then remove the named file). Older
373
+ plugin versions lock at the configured root instead: quiesce or restart them
374
+ before relying on this protocol — no automatic migration, and any unknown
375
+ existing locks are left alone.
376
+
377
+ A replacement re-reads and re-checks the revision under the lock and publishes
378
+ through a **temp file this operation exclusively creates** (an ordinary
379
+ pathname create that never follows a pre-existing symlink or truncates a
380
+ foreign file) and an atomic whole-file rename — the all-or-nothing swap is a
381
+ **replacement-only guarantee**. A create opens the target exclusively and then
382
+ writes its bytes, so a reader inside that window can observe the new file
383
+ before every byte is stored; a create's promise is exclusive naming plus
384
+ eventually-complete bytes, not atomic first visibility. A physically
385
+ addressed write that reaches a nested lake's tree through an outer route is
386
+ refused with that lake named, so a writable outer lake cannot make read-only
387
+ nested content writable.
388
+
389
+ The guarantee covers **cooperating plugin writers** and pre-existing
390
+ operational-path symlinks — the native tool, the Code Mode twin and `/docs`
391
+ all use the same guarded route. It is not armour against an arbitrary local
392
+ attacker who can swap directories between individual syscalls, and a manual
393
+ editor or unrelated program that ignores the protocol can still race a
394
+ publication; changes visible before the guarded commit are detected, but this
395
+ is not a filesystem-wide compare-and-swap. A busy lock refuses with its
396
+ recorded owner instead of waiting indefinitely.
268
397
 
269
398
  ## Status
270
399
 
@@ -278,6 +407,9 @@ npm test # node --test
278
407
  npm run typecheck
279
408
  ```
280
409
 
410
+ Testing instructions, the behavioural placement trials and their container
411
+ live in [TESTING.md](TESTING.md).
412
+
281
413
  Tests run the plugin source directly under Node's type-stripping (`noEmit`,
282
414
  `erasableSyntaxOnly`), so the file OpenCode loads is the file under test.
283
415
 
@@ -285,6 +417,39 @@ Tests run the plugin source directly under Node's type-stripping (`noEmit`,
285
417
  ranking harness pins behaviour against — edit it directly. It holds invented
286
418
  paths and summaries only; never snapshot a real docs tree into it.
287
419
 
420
+ `tests/fixtures/placement-cases.json` is a synthetic evaluation fixture for
421
+ the placement guidance (cases E01–E18, each with a claim, evidence,
422
+ destinations, expected scope or action, acceptable rationale and tempting
423
+ wrong answer). `tests/fixtures/placement-harness/harness.mts` runs the cases
424
+ behaviourally: `generate` materialises one prompt, one pristine fixture tree
425
+ and one out-of-band rubric per case (defaults `~/tmp/docs-placement/` for
426
+ prompts, rubrics and results, `~/test-dir/docs-placement/` for fixtures),
427
+ `run` copies one case into a stamped results directory and drives a headless
428
+ `opencode run` session there, `judge` scores each answer against the case
429
+ rubric with a second, plugin-free model call (`--judge-model` names a stronger
430
+ judge than the workhorse), and `export` bundles a sweep into one `dossier.md`
431
+ for deep review by an external model. Trials judge placement from the supplied
432
+ finding: a per-case run container sees only that case's sandbox, prompt, an
433
+ id-only case list and the result, and the requested shell/edit/read denials
434
+ are defence in depth rather than guarantees — each case's trail exposure is
435
+ classified and reported in the sweep summary.
436
+ `container-run.sh` runs the harness inside a Debian-slim trial container
437
+ (Node LTS, current `@opencode/cli`, read-only root filesystem) so trials see
438
+ no host filesystem at all; the harness can also run directly on the host.
439
+ Runs modify neither the repository nor the fixture trees. The first
440
+ behavioural sweep is recorded: sweep-1 (2026-11-07, plugin 0.12.0 at main
441
+ `f5a5882`), decision-only prompts with the `docs` skill in session, workhorse
442
+ `opencode-go/deepseek-v4.1-flash` in the trial container — 14/17 under the
443
+ workhorse judge, 17/17 under `zai-coding-plan/glm-5.3-flash`, with the three
444
+ disagreements resolved by manual review in favour of rewording the fixture
445
+ and rubric (E09, E06) rather than the guidance; see [TESTING.md](TESTING.md).
446
+ The principle stands: a fixture that merely asserts expected answers in Node
447
+ is not evidence a model follows the guidance. In this repository,
448
+ `/placement` (workflow in
449
+ `.opencode/skills/placement-trials/`) drives the harness: trials run on the
450
+ workhorse model, judging defaults to the current session model, and `export`
451
+ bundles a sweep for a stronger external reviewer.
452
+
288
453
  ## Licence
289
454
 
290
455
  MIT — see [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markstanden/opencode-docs-plugin",
3
- "version": "0.10.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "OpenCode V2 plugin: a `docs` tool and `/docs` command over configurable knowledge lakes (repo, machine, global and read-only wiki), nested README.md search, and the writing convention they share",
6
6
  "repository": {
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "scripts": {
23
23
  "test": "node --test 'tests/**/*.test.ts'",
24
- "test:coverage": "mkdir -p coverage && node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info 'tests/**/*.test.ts'",
24
+ "test:coverage": "mkdir -p coverage && node --test --experimental-test-coverage --test-coverage-include='src/**/*.ts' --test-coverage-exclude='**/*.test.ts' --test-reporter=lcov --test-reporter-destination=coverage/lcov.info 'tests/**/*.test.ts'",
25
25
  "test:watch": "node --test --watch 'tests/**/*.test.ts'",
26
26
  "typecheck": "tsc --noEmit"
27
27
  },