@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 +76 -42
- package/README.md +187 -38
- package/package.json +2 -2
- package/src/SKILL.md +60 -57
- package/src/index.ts +1549 -168
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
|
-
##
|
|
8
|
-
|
|
9
|
-
Every doc belongs to exactly one
|
|
10
|
-
in, so a doc cannot mix scopes.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
127
|
-
- The narrowest
|
|
128
|
-
rule; the narrower doc states only its delta and links up.
|
|
129
|
-
- Do not copy between knowledge
|
|
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
|
|
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
|
-
`
|
|
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 **
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
| **
|
|
14
|
-
| **
|
|
15
|
-
| **
|
|
16
|
-
| **
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
|
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": {
|
|
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
|
|
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
|
-
{
|
|
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
|
|
166
|
-
|
|
|
167
|
-
| `class`
|
|
168
|
-
| `root`
|
|
169
|
-
| `name`
|
|
170
|
-
| `
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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,
|
|
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.
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
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.
|
|
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
|
},
|