@markstanden/opencode-docs-plugin 0.10.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,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
 
@@ -285,6 +414,17 @@ Tests run the plugin source directly under Node's type-stripping (`noEmit`,
285
414
  ranking harness pins behaviour against — edit it directly. It holds invented
286
415
  paths and summaries only; never snapshot a real docs tree into it.
287
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
+
288
428
  ## Licence
289
429
 
290
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.10.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
  },
package/src/SKILL.md CHANGED
@@ -1,79 +1,79 @@
1
1
  # Docs
2
2
 
3
- Durable knowledge lives in **lakes**: **find** it (search, then read), or
4
- **record** one focused doc at the narrowest true scope. Call the `docs` tool;
5
- inside `execute` it is `tools.docs.docs_search(…)`, same verbs.
6
-
7
- ## Classes
8
-
9
- | Class | Where | Use it for | Writes |
10
- | ------------------ | --------------------------- | --------------------------------------- | ------------- |
11
- | **Instructions** | nested `README.md` | build, run, test, deploy this code | never |
12
- | **RepoContext** | `<repo>/docs` | this repo's traps, decisions, landmines | yes |
13
- | **MachineContext** | personal lake (`~/doclake`) | tooling and traps across your machines | yes |
14
- | **GlobalContext** | organisation lake | standards and integrations across repos | yes, reviewed |
15
- | **Wiki** | human-readable wiki | narrative for humans | read-only |
16
-
17
- Record at the **narrowest scope where the whole claim is true**. Two scopes
18
- means two docs; one omnibus doc is out of place everywhere.
3
+ Durable knowledge lives in **lakes**: **find** it, or **record** one focused
4
+ doc at the narrowest justified scope. Call the `docs` tool; inside `execute`
5
+ it is `tools.docs.docs_search(…)`, same verbs.
6
+
7
+ ## Scopes
8
+
9
+ | Scope | Config | Where | Use it for | Writes |
10
+ | ---------------- | --------- | --------------------------- | --------------------------------- | ------------- |
11
+ | **Repository** | `repo` | `<repo>/docs` | this repo's traps and decisions | yes |
12
+ | **Personal** | `machine` | personal lake (`~/doclake`) | a person's devices and setup | yes |
13
+ | **Organisation** | `global` | organisation lake | shared standards and integrations | yes, reviewed |
14
+ | **Instructions** | implicit | nested `README.md` | build, run, test, deploy | never |
15
+ | **Wiki** | `wiki` | human-readable wiki | narrative for humans | read-only |
16
+
17
+ Place by dependency: this repo's code or decisions → Repository; a person's
18
+ device, account or setup (a named host counts) → Personal; an established
19
+ shared rule → Organisation, boundary stated — reusability alone is not
20
+ ownership; operational steps → the README workflow; human narrative → the
21
+ wiki's owners. Split only for claims with different owners; unresolved
22
+ ownership stays an explicit proposal, never a guessed destination.
23
+
24
+ `help` lists each lake's scope and description; `add` writes the sole writable
25
+ lake without `lake`, else refuses with candidates — pass `lake`.
19
26
 
20
27
  ## Find it
21
28
 
22
- A **blank `search`** lists recent docs, one line each; a **`search`** ranks
23
- them, falling back to `Closest matches` and matching terms as **substrings** —
24
- so a typo finds nothing. Terms shorter than `MIN_TERM_LENGTH` (3) are dropped
25
- when longer terms exist; otherwise all query terms are kept. `help` lists the
26
- lakes and ranking.
29
+ A **blank `search`** lists recent docs; a **`search`** ranks them, matching
30
+ terms as **substrings** (a typo finds nothing) and falling back to
31
+ `Closest matches`. Terms shorter than `MIN_TERM_LENGTH` (3) are dropped
32
+ when longer terms exist; otherwise all query terms are kept.
27
33
 
28
- Open the match with `read`: the root-relative path, or `lake:path` when several
29
- lakes exist. A README result is `instructions:path` and opens with the built-in
30
- `read`. Consult matching docs before a broad grep; a tie ranks repo,
31
- instructions, global, machine, wiki. Knowledge lakes exclude `plans/`,
32
- `requirements/` and `wiki/` at the root; wiki lakes exclude only
33
- dot-directories and `node_modules`.
34
+ Open the match with `read` (root-relative, or `lake:path`); a README
35
+ result is `instructions:path` for the built-in `read`. Consult
36
+ matching docs before a broad grep; a tie ranks repo, instructions, global,
37
+ machine, wiki. Knowledge lakes exclude `plans/`, `requirements/` and `wiki/`
38
+ at the root; wiki lakes only dot-directories and `node_modules`.
34
39
 
35
40
  ## Record it
36
41
 
37
- Write to `<area>/<title>.md` under the right lake; `CONVENTION.md` is
38
- authoritative.
39
-
40
- 1. **Read first, never duplicate.** Read the docs relevant to the task: the same
41
- area, keyword hits on its entities, or links from touched docs.
42
- 2. **Choose one class** from the table — the narrowest that holds the claim.
43
- 3. **Reconcile, then add.** Edit before add; merge near-twins; split a bloated
44
- doc per topic; after a split, merge or move, repoint inbound references and
45
- delete the emptied doc — no superseded stubs.
42
+ Record to `<area>/<title>.md` under the right lake (`CONVENTION.md` is
43
+ authoritative):
46
44
 
47
- One artifact per topic and class, never one dump; the compaction nudge shapes
48
- `Record with docs add:` blocks for this.
45
+ 1. **Read first, never duplicate:** the same area, keyword hits on its
46
+ entities, or links from touched docs.
47
+ 2. **Choose one scope** from the table — the narrowest that holds the claim;
48
+ one artifact per topic and scope.
49
+ 3. **Reconcile, then add:** edit before add; merge near-twins; split per topic,
50
+ repoint references, delete the emptied doc — no superseded stubs.
49
51
 
50
- Record through the typed `add`: metadata (`Area`, `Keywords`, `Summary`) and a
51
- body with an `H1` and a claim section — Facts, Inferred, Tested, Failed approaches, Open questions. Omit `path` to create `<Area>/<slug>.md`; pass `path` to replace **wholesale**, so omitted body content is lost and the near-twin reconcile is skipped. Refused: blank metadata, traversal, a non-`.md` or symlinked target, no claim section, and — only when `path` is omitted — a same-area near-twin.
52
+ Record through the typed `add`: metadata (`Area`, `Keywords`, `Summary`) plus a
53
+ body with an `H1` and a claim section — Facts, Inferred, Tested, Failed approaches, Open questions. `mode:"create"` writes a new doc (never overwrites; near-twins refuse unless `allowNearTwinReason` explains the distinction). `mode:"replace"` wholesale-replaces the doc at `path` against your `expectedRevision` (omitted content is lost; a missing target or stale token refuses, never creates). Refused always: blank metadata, traversal, a non-`.md` or symlinked target, no claim section.
52
54
 
53
- A doc is current or deleted; no `Status` or `Supersedes`. A failed attempt is
54
- valid knowledge when it records what was tried, why it failed, what is true now,
55
- and what not to retry.
55
+ A doc is current or deleted; a failed attempt is valid knowledge (what was
56
+ tried, why it failed, what is true now). Knowledge reads and writes end with a
57
+ `docs: revision` line: the replace token.
56
58
 
57
- Lead with the actionable fact, keeping the reason, limits and evidence that
58
- affect using it; drop chronology, repeated conclusions and empty sections. Keep
59
- the service names, errors, paths and commands someone will search for. A
59
+ Lead with the actionable fact, its reason, limits and evidence; drop
60
+ chronology and empty sections. Keep the
61
+ service names, errors, paths and commands someone will search for; the
60
62
  `Summary` decides whether to open the file.
61
63
 
62
64
  Never record secrets, credentials, tokens or customer data — name the vault or
63
- variable, never the value — and no personal data in GlobalContext or Wiki.
65
+ variable, never the value — and no personal data in Organisation or Wiki.
64
66
 
65
67
  ## Check it
66
68
 
67
69
  `check` audits the **writable** knowledge lakes: front-matter, an `H1`, a claim
68
70
  section, no hand-maintained `Date`, a distinctive identity, near-twins and broken
69
- relative links. Obsolete fields, long docs and missing roots are **notes**, not
70
- findings. Validation follows writability — read-only lakes, wiki lakes and
71
- READMEs are not validated, and skipped lakes are named in the report. A very
72
- large report is capped with an `and N more lines not shown` tail; narrow it with
73
- `lake`. `add` enforces the structural rules at the write boundary;
74
- cross-document audits stay with `check`. `maxDocLines` (default 200, `0`
75
- disables) is a soft size signal, not a refusal.
76
-
77
- Run the project's tests and typecheck before proposing a commit, fixing
78
- failures rather than papering over them. Prepare the commit and **wait for
79
- explicit approval** — recording findings is not commit approval. Never push.
71
+ relative links. Obsolete fields, long docs (`maxDocLines`, default 200, `0`
72
+ disables) and missing roots are **notes**, not findings. Read-only lakes, wiki
73
+ lakes and READMEs are searched but not validated; skipped lakes are named, a
74
+ very large report is capped with an `and N more lines not shown` tail. `add`
75
+ enforces the structural rules at the write boundary; cross-document audits
76
+ stay with `check`.
77
+
78
+ Run the project's tests and typecheck before proposing a commit; prepare it
79
+ and **wait for explicit approval** — recording is not approval. Never push.