@markstanden/opencode-docs-plugin 0.9.0 → 0.10.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
@@ -49,6 +49,10 @@ Lake notes — a missing root, or a root outside version control — are compute
49
49
  per call, not captured at startup: create, remove or version a root and the
50
50
  next `check`, `search` and help reflect it without a restart.
51
51
 
52
+ Validation follows writability: `check` validates only writable knowledge lakes.
53
+ A read-only lake is indexed for search and never validated, and the report names
54
+ it as skipped.
55
+
52
56
  The excluded areas — never scanned, never validated in a knowledge lake — are
53
57
  `plans/` (scratch, gitignored, deleted when the work lands), `requirements/`
54
58
  and `wiki/` (vendored mirrors). The wiki profile has no such exclusions; only
package/README.md CHANGED
@@ -169,6 +169,12 @@ Lake entries:
169
169
  | `name` | Lower-case dashes; defaults to the root folder name. `instructions` is reserved. |
170
170
  | `writable` | Defaults true for knowledge classes, fixed false for `wiki`. |
171
171
 
172
+ Validation follows writability: `check` validates a writable knowledge lake
173
+ only. A read-only knowledge lake, and every `wiki` lake, is indexed for search
174
+ and never validated — the plugin cannot write there, so findings could not be
175
+ fixed through it. Each skipped lake is named in the report, so "no findings" is
176
+ never mistaken for compliance.
177
+
172
178
  Lake paths never follow symlinks: a symlinked doc is skipped by search and
173
179
  `check`, refused by `read`, and refused by `add`. The configured `root` itself
174
180
  may be a symlink; the content beneath it must be real.
@@ -246,12 +252,15 @@ wiki's owners decide.
246
252
 
247
253
  ## Check
248
254
 
249
- `check` validates the knowledge lakes: required front-matter, an H1, at least
250
- one claim section (`Facts`, `Inferred`, `Tested`, `Failed approaches`,
255
+ `check` validates the **writable** knowledge lakes: required front-matter, an H1,
256
+ at least one claim section (`Facts`, `Inferred`, `Tested`, `Failed approaches`,
251
257
  `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.
258
+ identity, same-area near-twins, and broken relative `.md` links. Read-only
259
+ knowledge lakes and wiki lakes are searched in full but never validated; the
260
+ report names each skipped lake and why. Obsolete `Status`/`Supersedes` fields,
261
+ long docs, and unversioned or missing roots are **notes**, not findings. An
262
+ empty findings list means compliant. A very large report is capped with an
263
+ `and N more lines not shown` tail; narrow it with `lake`.
255
264
 
256
265
  There is no lifecycle field. A doc is current or deleted; git is the archive.
257
266
  Correct a wrong doc by replacing it in place. A failed attempt is valid
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.10.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": {
package/src/SKILL.md CHANGED
@@ -64,12 +64,15 @@ variable, never the value — and no personal data in GlobalContext or Wiki.
64
64
 
65
65
  ## Check it
66
66
 
67
- `check` audits the lakes: front-matter, an `H1`, a claim section, no
68
- hand-maintained `Date`, a distinctive identity, near-twins and broken relative
69
- links. Obsolete fields, long docs and missing roots are **notes**, not findings;
70
- wiki lakes are not validated. `add` enforces the structural rules at the write
71
- boundary; cross-document audits stay with `check`. `maxDocLines` (default 200,
72
- `0` disables) is a soft size signal, not a refusal.
67
+ `check` audits the **writable** knowledge lakes: front-matter, an `H1`, a claim
68
+ 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.
73
76
 
74
77
  Run the project's tests and typecheck before proposing a commit, fixing
75
78
  failures rather than papering over them. Prepare the commit and **wait for
package/src/index.ts CHANGED
@@ -126,7 +126,7 @@ export const DOCS_INPUT = Schema.Struct({
126
126
  check: Schema.optional(
127
127
  Schema.Boolean.annotate({
128
128
  description:
129
- "Validate every knowledge doc: front-matter, an H1, a claim section, no hand-maintained Date, a distinctive H1+Keywords identity, near-twins, broken relative links, a soft length note. Wiki lakes and READMEs are not validated.",
129
+ "Validate every knowledge doc: front-matter, an H1, a claim section, no hand-maintained Date, a distinctive H1+Keywords identity, near-twins, broken relative links, a soft length note. Only writable knowledge lakes are validated; read-only lakes, wiki lakes and READMEs are not, and skipped lakes are named.",
130
130
  }),
131
131
  ),
132
132
  read: Schema.optional(
@@ -541,6 +541,13 @@ export const MIN_TERM_LENGTH = 3;
541
541
  */
542
542
  export const MAX_RESULTS = 10;
543
543
 
544
+ /**
545
+ * The most characters a `check` report returns before it is truncated with an
546
+ * `and N more` tail. A finding-heavy run once returned 39,871 characters in one
547
+ * tool result; the cap keeps a report readable. The single knob for the cap.
548
+ */
549
+ export const MAX_CHECK_CHARS = 8000;
550
+
544
551
  /** Split a raw query into lower-cased, non-empty terms, dropping filler. */
545
552
  function termsOf(query: string): string[] {
546
553
  const terms = query
@@ -2214,6 +2221,17 @@ export interface LakeSpec {
2214
2221
  writable: boolean;
2215
2222
  }
2216
2223
 
2224
+ /**
2225
+ * Whether `check` validates a lake: validation follows writability, because a
2226
+ * lake the plugin cannot write is out of its control. A wiki lake is never
2227
+ * validated. One rule, no per-lake override.
2228
+ * @param lake The configured lake.
2229
+ * @returns True when the lake's docs are validated.
2230
+ */
2231
+ function lakeValidated(lake: LakeSpec): boolean {
2232
+ return lake.profile === "knowledge" && lake.writable;
2233
+ }
2234
+
2217
2235
  /** A lake with its runtime seams. */
2218
2236
  export interface Lake extends LakeSpec {
2219
2237
  /** Collects this lake's docs. */
@@ -2971,6 +2989,80 @@ function searchDocs(
2971
2989
  };
2972
2990
  }
2973
2991
 
2992
+ /** Why a lake is skipped by `check`, in one short line. */
2993
+ function skippedLakeLine(lake: LakeSpec): string {
2994
+ const why = lake.profile === "wiki" ? "wiki class, read-only" : "read-only";
2995
+ return `${lake.name}: ${why}, not validated.`;
2996
+ }
2997
+
2998
+ /**
2999
+ * Truncate a check report at a line boundary, counting the lines cut and
3000
+ * pointing at the narrower report. A finding-heavy lake otherwise floods a
3001
+ * tool result.
3002
+ * @param content The full report text.
3003
+ * @returns The report, within {@link MAX_CHECK_CHARS}, or its capped form.
3004
+ */
3005
+ function cappedCheck(content: string): string {
3006
+ if (content.length <= MAX_CHECK_CHARS) {
3007
+ return content;
3008
+ }
3009
+ const lines = content.split("\n");
3010
+ const kept: string[] = [];
3011
+ let length = 0;
3012
+ for (const line of lines) {
3013
+ const next = length + (kept.length > 0 ? 1 : 0) + line.length;
3014
+ if (next > MAX_CHECK_CHARS) {
3015
+ break;
3016
+ }
3017
+ kept.push(line);
3018
+ length = next;
3019
+ }
3020
+ const omitted = lines.length - kept.length;
3021
+ const head = kept.length > 0 ? `${kept.join("\n")}\n\n` : "";
3022
+ return (
3023
+ `${head}… and ${omitted} more lines not shown. ` +
3024
+ "Narrow it per lake with the `lake` selector."
3025
+ );
3026
+ }
3027
+
3028
+ /** Run one lake's check report and count the docs looked at. */
3029
+ function lakeReport(
3030
+ runtime: Runtime,
3031
+ lake: Lake,
3032
+ ): { report: string; checked: number } {
3033
+ const entries = lake.collect();
3034
+ return {
3035
+ report: checkDocs(entries, lake.read, lakeNotes(lake), {
3036
+ maxDocLines: runtime.maxDocLines,
3037
+ ...(lake.linkCheck ? { exists: lake.exists } : {}),
3038
+ }),
3039
+ checked: entries.length,
3040
+ };
3041
+ }
3042
+
3043
+ /**
3044
+ * Join the validated lakes' reports and the skipped-lake lines. Only the
3045
+ * reports are capped: "no findings" must never read as "compliant" when a lake
3046
+ * was simply not looked at.
3047
+ * @param reports The validated lakes' reports, in configuration order.
3048
+ * @param skipped One-line reasons for each lake `check` did not validate.
3049
+ * @returns The check result text.
3050
+ */
3051
+ function assembleCheck(
3052
+ reports: readonly string[],
3053
+ skipped: readonly string[],
3054
+ ): string {
3055
+ const report = reports.length > 0 ? cappedCheck(reports.join("\n\n")) : "";
3056
+ const tail = skipped.join("\n");
3057
+ if (report === "" && tail === "") {
3058
+ return "No knowledge lakes configured.";
3059
+ }
3060
+ if (report === "" || tail === "") {
3061
+ return report || tail;
3062
+ }
3063
+ return `${report}\n\n${tail}`;
3064
+ }
3065
+
2974
3066
  /** Check the knowledge lakes, optionally limited to one. */
2975
3067
  function checkLakes(
2976
3068
  runtime: Runtime,
@@ -2981,54 +3073,24 @@ function checkLakes(
2981
3073
  if (lake === undefined) {
2982
3074
  return { content: `No lake named '${lakeName}'.`, checked: 0 };
2983
3075
  }
2984
- if (lake.profile === "wiki") {
2985
- return {
2986
- content:
2987
- `Lake '${lake.name}' is a wiki lake — read-only reference, ` +
2988
- "not validated.",
2989
- checked: 0,
2990
- };
3076
+ if (!lakeValidated(lake)) {
3077
+ return { content: skippedLakeLine(lake), checked: 0 };
2991
3078
  }
2992
- const entries = lake.collect();
2993
- return {
2994
- content: checkDocs(entries, lake.read, lakeNotes(lake), {
2995
- maxDocLines: runtime.maxDocLines,
2996
- ...(lake.linkCheck ? { exists: lake.exists } : {}),
2997
- }),
2998
- checked: entries.length,
2999
- };
3079
+ const { report, checked } = lakeReport(runtime, lake);
3080
+ return { content: cappedCheck(report), checked };
3000
3081
  }
3001
- const knowledge = runtime.lakes.filter(
3002
- (lake) => lake.profile === "knowledge",
3003
- );
3004
3082
  let checked = 0;
3005
- const reports = knowledge.map((lake) => {
3006
- const entries = lake.collect();
3007
- checked += entries.length;
3008
- const report = checkDocs(entries, lake.read, lakeNotes(lake), {
3009
- maxDocLines: runtime.maxDocLines,
3010
- ...(lake.linkCheck ? { exists: lake.exists } : {}),
3011
- });
3012
- return knowledge.length > 1 || runtime.lakes.length > 1
3013
- ? `${lake.name}:\n${report}`
3014
- : report;
3083
+ const reports = runtime.lakes.filter(lakeValidated).map((lake) => {
3084
+ const outcome = lakeReport(runtime, lake);
3085
+ checked += outcome.checked;
3086
+ return runtime.lakes.length > 1
3087
+ ? `${lake.name}:\n${outcome.report}`
3088
+ : outcome.report;
3015
3089
  });
3016
- const wiki = runtime.lakes
3017
- .filter((lake) => lake.profile === "wiki")
3018
- .map((lake) => lake.name);
3019
- if (wiki.length > 0) {
3020
- reports.push(
3021
- `note: wiki lakes are read-only reference and are not validated: ` +
3022
- `${wiki.join(", ")}.`,
3023
- );
3024
- }
3025
- return {
3026
- content:
3027
- reports.length > 0
3028
- ? reports.join("\n\n")
3029
- : "No knowledge lakes configured.",
3030
- checked,
3031
- };
3090
+ const skipped = runtime.lakes
3091
+ .filter((lake) => !lakeValidated(lake))
3092
+ .map(skippedLakeLine);
3093
+ return { content: assembleCheck(reports, skipped), checked };
3032
3094
  }
3033
3095
 
3034
3096
  /** Add a doc to the target lake. */
@@ -3202,7 +3264,7 @@ function lakeInfo(lakes: readonly Lake[]): LakeInfo[] {
3202
3264
  class: lake.class,
3203
3265
  root: lake.root,
3204
3266
  writable: lake.writable,
3205
- validated: lake.profile === "knowledge",
3267
+ validated: lakeValidated(lake),
3206
3268
  missing: lakeMissing(lake),
3207
3269
  }));
3208
3270
  }