@markstanden/opencode-docs-plugin 0.9.0 → 0.12.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,51 +4,81 @@ 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
50
76
  next `check`, `search` and help reflect it without a restart.
51
77
 
78
+ Validation follows writability: `check` validates only writable knowledge lakes.
79
+ A read-only lake is indexed for search and never validated, and the report names
80
+ it as skipped.
81
+
52
82
  The excluded areas — never scanned, never validated in a knowledge lake — are
53
83
  `plans/` (scratch, gitignored, deleted when the work lands), `requirements/`
54
84
  and `wiki/` (vendored mirrors). The wiki profile has no such exclusions; only
@@ -111,11 +141,12 @@ git. Optional invisible metadata may be added later; it is not required.
111
141
  `add` refuses a near-twin write and returns the candidates to reconcile
112
142
  against — edit before add, merge near-twins, split a bloated doc into
113
143
  per-topic files.
114
- - The reconcile applies to a **create**: with `path` omitted, the target is
115
- derived and the near-twin guard runs. An explicit `path` is the update
116
- contract and skips it, which is how you replace a doc in place. Passing an
117
- explicit _new_ path is therefore a deliberate override, not a protected
118
- 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.
119
150
  - Near-twins are per lake. The same topic at repo and global scope is
120
151
  legitimate; the narrower doc states its delta and links up.
121
152
 
@@ -123,10 +154,10 @@ git. Optional invisible metadata may be added later; it is not required.
123
154
 
124
155
  - One question per doc. One topic per doc.
125
156
  - More than one H2 aimed at different topics or audiences means split.
126
- - Never write an omnibus notes doc to avoid choosing a class.
127
- - The narrowest true scope owns the claim. A broader class owns the general
128
- rule; the narrower doc states only its delta and links up.
129
- - 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.
130
161
  - Instructions own commands. Knowledge docs own reasons and link to the
131
162
  command. Knowledge docs link to the wiki page for human narrative.
132
163
  - Wiki duplication is expected, not a defect. If a wiki page drifts from a
@@ -141,13 +172,16 @@ git. Optional invisible metadata may be added later; it is not required.
141
172
 
142
173
  - No secrets, credentials, tokens, or customer data. Name the vault or variable,
143
174
  never the value.
144
- - No personal data in GlobalContext or Wiki.
175
+ - No personal data in Organisation or Wiki.
145
176
  - The wiki profile is read-only to the tool. Its own owners review changes.
146
177
 
147
178
  ## Lifecycle
148
179
 
149
180
  A doc is current or deleted. Correct a wrong doc by replacing it in place with
150
- `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
151
185
  explicit approval; there is no `delete` verb. A failed attempt is still valid
152
186
  knowledge when it records the outcome and the current truth.
153
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,19 @@ 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`. |
189
+
190
+ Validation follows writability: `check` validates a writable knowledge lake
191
+ only. A read-only knowledge lake, and every `wiki` lake, is indexed for search
192
+ and never validated — the plugin cannot write there, so findings could not be
193
+ fixed through it. Each skipped lake is named in the report, so "no findings" is
194
+ never mistaken for compliance.
171
195
 
172
196
  Lake paths never follow symlinks: a symlinked doc is skipped by search and
173
197
  `check`, refused by `read`, and refused by `add`. The configured `root` itself
@@ -187,11 +211,13 @@ declarations may reach the tree by different routes and the nested doc is
187
211
  still indexed once.
188
212
 
189
213
  A doc belongs to the **narrowest enclosing lake**. With nested roots, `search`
190
- returns the inner lake's entry and a bare `read` resolves to it; `add` writes
191
- to the deepest writable knowledge lake, so an outer read-only wrapper never
192
- hijacks the write. Two lakes that do not nest holding the same relative path
193
- stay a refusal — pass `lake`. Near-twin checks apply inside one lake and never
194
- 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.
195
221
 
196
222
  ```jsonc
197
223
  {
@@ -227,6 +253,26 @@ it `— missing`, and `add` refuses with the path rather than bootstrapping a
227
253
  tree. Create the directory deliberately. Area folders beneath an existing root
228
254
  are still created on write.
229
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
+
230
276
  Root and git notes are computed per call, not captured at startup: a root that
231
277
  appears, disappears or gains a git checkout is reflected by the next `check`,
232
278
  `search` and help without a restart.
@@ -246,16 +292,108 @@ wiki's owners decide.
246
292
 
247
293
  ## Check
248
294
 
249
- `check` validates the knowledge lakes: required front-matter, an H1, at least
250
- one claim section (`Facts`, `Inferred`, `Tested`, `Failed approaches`,
295
+ `check` validates the **writable** knowledge lakes: required front-matter, an H1,
296
+ at least one claim section (`Facts`, `Inferred`, `Tested`, `Failed approaches`,
251
297
  `Open questions`), no hand-maintained `Date`, a distinctive H1+Keywords
252
- identity, same-area near-twins, and broken relative `.md` links. Obsolete
253
- `Status`/`Supersedes` fields, long docs, and unversioned or missing roots are
254
- **notes**, not findings. An empty findings list means compliant.
298
+ identity, same-area near-twins, and broken relative `.md` links. Read-only
299
+ knowledge lakes and wiki lakes are searched in full but never validated; the
300
+ report names each skipped lake and why. Obsolete `Status`/`Supersedes` fields,
301
+ long docs, and unversioned or missing roots are **notes**, not findings. An
302
+ empty findings list means compliant. A very large report is capped with an
303
+ `and N more lines not shown` tail; narrow it with `lake`.
255
304
 
256
305
  There is no lifecycle field. A doc is current or deleted; git is the archive.
257
- Correct a wrong doc by replacing it in place. A failed attempt is valid
258
- 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.
259
397
 
260
398
  ## Status
261
399
 
@@ -276,6 +414,17 @@ Tests run the plugin source directly under Node's type-stripping (`noEmit`,
276
414
  ranking harness pins behaviour against — edit it directly. It holds invented
277
415
  paths and summaries only; never snapshot a real docs tree into it.
278
416
 
417
+ `tests/fixtures/placement-cases.json` is a synthetic evaluation fixture for
418
+ the placement guidance (cases E01–E16, each with a claim, evidence,
419
+ destinations, expected scope or action, acceptable rationale and tempting
420
+ wrong answer). To evaluate the guidance behaviourally: run the cases through
421
+ a model with the docs tools mounted, holding model, settings and tool
422
+ configuration constant, and score first-choice accuracy, unnecessary
423
+ splitting, inappropriate fallback writes and the tool calls needed to correct
424
+ recording. No behavioural trials have been run with the current wording; a
425
+ fixture that merely asserts expected answers in Node is not evidence a model
426
+ follows the guidance.
427
+
279
428
  ## Licence
280
429
 
281
430
  MIT — see [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markstanden/opencode-docs-plugin",
3
- "version": "0.9.0",
3
+ "version": "0.12.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
  },