@markstanden/opencode-docs-plugin 0.8.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
@@ -36,6 +36,23 @@ are RepoContext. A repo adds other local roots in
36
36
  `.opencode/docs-lakes.json`; a personal or managed base file lists the
37
37
  machine, global and wiki lakes.
38
38
 
39
+ Lakes are named, and several per class are supported. A root nested inside
40
+ 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.
47
+
48
+ Lake notes — a missing root, or a root outside version control — are computed
49
+ per call, not captured at startup: create, remove or version a root and the
50
+ next `check`, `search` and help reflect it without a restart.
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
+
39
56
  The excluded areas — never scanned, never validated in a knowledge lake — are
40
57
  `plans/` (scratch, gitignored, deleted when the work lands), `requirements/`
41
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.
@@ -181,10 +187,17 @@ writable repo lake may sit inside a larger read-only wiki lake, and search,
181
187
  only decides its own profile and defaults; `repo`, `machine`, `global` and
182
188
  `wiki` all skip a nested root the same way.
183
189
 
184
- The skip compares the configured roots as written, so reach the tree the same
185
- way from both declarations. If one lake names the real directory and the other
186
- reaches it through a symlink, junction or mapped drive, the two paths never
187
- match and the nested doc is indexed twice.
190
+ The skip is decided on real paths: every configured root is resolved through
191
+ symlinks, junctions and mapped drives before the comparison, so the two
192
+ declarations may reach the tree by different routes and the nested doc is
193
+ still indexed once.
194
+
195
+ 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.
188
201
 
189
202
  ```jsonc
190
203
  {
@@ -220,6 +233,10 @@ it `— missing`, and `add` refuses with the path rather than bootstrapping a
220
233
  tree. Create the directory deliberately. Area folders beneath an existing root
221
234
  are still created on write.
222
235
 
236
+ Root and git notes are computed per call, not captured at startup: a root that
237
+ appears, disappears or gains a git checkout is reflected by the next `check`,
238
+ `search` and help without a restart.
239
+
223
240
  `maxDocLines` (default 200, `0` disables) is a **soft** note on a long doc, not a
224
241
  refusal. `perLakeQuota` (default 3, `0` disables) is the per-source result
225
242
  reservation.
@@ -235,12 +252,15 @@ wiki's owners decide.
235
252
 
236
253
  ## Check
237
254
 
238
- `check` validates the knowledge lakes: required front-matter, an H1, at least
239
- 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`,
240
257
  `Open questions`), no hand-maintained `Date`, a distinctive H1+Keywords
241
- identity, same-area near-twins, and broken relative `.md` links. Obsolete
242
- `Status`/`Supersedes` fields, long docs, and unversioned or missing roots are
243
- **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`.
244
264
 
245
265
  There is no lifecycle field. A doc is current or deleted; git is the archive.
246
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.8.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. */
@@ -2227,8 +2245,12 @@ export interface Lake extends LakeSpec {
2227
2245
  * existing root; throws when the root is absent rather than creating it.
2228
2246
  */
2229
2247
  write: (path: string, contents: string) => void;
2230
- /** Environment notes for `check` (missing root, unversioned root). */
2231
- notes: readonly string[];
2248
+ /**
2249
+ * Legacy seam notes only. Real lakes compute their environment notes at
2250
+ * call time — a root that appears, disappears or gains a git checkout is
2251
+ * re-probed on the next `check`, `search` and help, never captured here.
2252
+ */
2253
+ notes?: readonly string[];
2232
2254
  /** Whether `check` should verify relative links; false for the legacy seam. */
2233
2255
  linkCheck: boolean;
2234
2256
  }
@@ -2314,7 +2336,6 @@ export function createLake(
2314
2336
  };
2315
2337
  return {
2316
2338
  ...spec,
2317
- notes: unversionedNote(spec.root, spec.name),
2318
2339
  linkCheck: true,
2319
2340
  collect: () =>
2320
2341
  collectDocs(spec.root, (paths) => fallbackDates(spec.root, paths), {
@@ -2346,18 +2367,27 @@ export function createLake(
2346
2367
 
2347
2368
  /**
2348
2369
  * Build every configured lake, each aware of the others so a nested root is
2349
- * collected once.
2370
+ * collected once. Nesting is decided on real paths, so a symlinked root cannot
2371
+ * defeat containment: the skip paths handed to the walker are re-anchored
2372
+ * under the collecting lake's configured root.
2350
2373
  * @param specs The configured lakes.
2351
2374
  * @returns The runtime lakes.
2352
2375
  */
2353
2376
  export function createLakes(specs: readonly LakeSpec[]): Lake[] {
2354
- return specs.map((spec) =>
2355
- createLake(spec, {
2356
- skip: specs
2357
- .filter((other) => other !== spec)
2358
- .map((other) => other.root),
2359
- }),
2360
- );
2377
+ const realRoots = specs.map((spec) => realPathOf(spec.root));
2378
+ return specs.map((spec, index) => {
2379
+ const own = realRoots[index] ?? spec.root;
2380
+ const skip = specs.flatMap((other, otherIndex) => {
2381
+ if (otherIndex === index) {
2382
+ return [];
2383
+ }
2384
+ const otherRoot = realRoots[otherIndex] ?? other.root;
2385
+ return otherRoot !== own && isUnder(otherRoot, own)
2386
+ ? [join(spec.root, relative(own, otherRoot))]
2387
+ : [];
2388
+ });
2389
+ return createLake(spec, { skip });
2390
+ });
2361
2391
  }
2362
2392
 
2363
2393
  /** The lake classes the configuration accepts. */
@@ -2819,6 +2849,44 @@ function findLake(lakes: readonly Lake[], name: string): Lake | undefined {
2819
2849
  return lakes.find((lake) => lake.name === name);
2820
2850
  }
2821
2851
 
2852
+ /**
2853
+ * The environment notes for one lake, probed at call time. Legacy seams keep
2854
+ * their captured notes; every real lake re-probes, so a root that appears,
2855
+ * disappears or gains a git checkout is reported by the next surface that
2856
+ * looks — `check`, `search` and help never disagree with a stale capture.
2857
+ * @param lake The lake to describe.
2858
+ * @returns The note lines.
2859
+ */
2860
+ function lakeNotes(lake: Lake): readonly string[] {
2861
+ return lake.root === ""
2862
+ ? (lake.notes ?? [])
2863
+ : unversionedNote(lake.root, lake.name);
2864
+ }
2865
+
2866
+ /** Whether a lake root is absent, probed at call time. */
2867
+ function lakeMissing(lake: Lake): boolean {
2868
+ return lake.root !== "" && !existsSync(lake.root);
2869
+ }
2870
+
2871
+ /**
2872
+ * How many knowledge lakes enclose a lake — the nesting depth used to pick the
2873
+ * narrowest owner of a path and the deepest writable target for `add`. Wiki
2874
+ * lakes never enclose anything.
2875
+ * @param lake The lake to measure.
2876
+ * @param lakes The configured lakes.
2877
+ * @returns The number of enclosing knowledge lakes.
2878
+ */
2879
+ function nestingDepth(lake: Lake, lakes: readonly Lake[]): number {
2880
+ const root = realPathOf(lake.root);
2881
+ return lakes.filter((other) => {
2882
+ if (other === lake || other.profile === "wiki") {
2883
+ return false;
2884
+ }
2885
+ const otherRoot = realPathOf(other.root);
2886
+ return otherRoot !== root && isUnder(root, otherRoot);
2887
+ }).length;
2888
+ }
2889
+
2822
2890
  /** The lake `add` writes to, or the refusal reason. */
2823
2891
  function writableLake(
2824
2892
  lakes: readonly Lake[],
@@ -2836,14 +2904,34 @@ function writableLake(
2836
2904
  }
2837
2905
  return lake;
2838
2906
  }
2839
- return (
2840
- lakes.find((lake) => lake.class === "repo" && lake.writable) ??
2841
- lakes.find((lake) => lake.profile === "knowledge" && lake.writable) ?? {
2907
+ // The narrowest writable knowledge lake wins: with nested roots the inner
2908
+ // lake owns the file, so an outer read-only lake must not hijack the write.
2909
+ const repos = lakes.filter(
2910
+ (lake) => lake.class === "repo" && lake.writable,
2911
+ );
2912
+ const pool =
2913
+ repos.length > 0
2914
+ ? repos
2915
+ : lakes.filter(
2916
+ (lake) => lake.profile === "knowledge" && lake.writable,
2917
+ );
2918
+ if (pool.length === 0) {
2919
+ return {
2842
2920
  reason:
2843
2921
  `${REFUSE} no writable knowledge lake is configured; ` +
2844
2922
  "add a repo, machine, or global lake.",
2923
+ };
2924
+ }
2925
+ let target = pool[0] as Lake;
2926
+ let depth = nestingDepth(target, lakes);
2927
+ for (const candidate of pool.slice(1)) {
2928
+ const candidateDepth = nestingDepth(candidate, lakes);
2929
+ if (candidateDepth > depth) {
2930
+ target = candidate;
2931
+ depth = candidateDepth;
2845
2932
  }
2846
- );
2933
+ }
2934
+ return target;
2847
2935
  }
2848
2936
 
2849
2937
  /** Qualify a path with its lake when more than one lake is configured. */
@@ -2887,9 +2975,8 @@ function searchDocs(
2887
2975
  const missing = runtime.lakes
2888
2976
  .filter(
2889
2977
  (lake) =>
2890
- lake.root !== "" &&
2891
2978
  (lakeName === undefined || lakeName === lake.name) &&
2892
- !existsSync(lake.root),
2979
+ lakeMissing(lake),
2893
2980
  )
2894
2981
  .map((lake) => missingRootNote(lake.root, lake.name));
2895
2982
  return {
@@ -2902,6 +2989,80 @@ function searchDocs(
2902
2989
  };
2903
2990
  }
2904
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
+
2905
3066
  /** Check the knowledge lakes, optionally limited to one. */
2906
3067
  function checkLakes(
2907
3068
  runtime: Runtime,
@@ -2912,54 +3073,24 @@ function checkLakes(
2912
3073
  if (lake === undefined) {
2913
3074
  return { content: `No lake named '${lakeName}'.`, checked: 0 };
2914
3075
  }
2915
- if (lake.profile === "wiki") {
2916
- return {
2917
- content:
2918
- `Lake '${lake.name}' is a wiki lake — read-only reference, ` +
2919
- "not validated.",
2920
- checked: 0,
2921
- };
3076
+ if (!lakeValidated(lake)) {
3077
+ return { content: skippedLakeLine(lake), checked: 0 };
2922
3078
  }
2923
- const entries = lake.collect();
2924
- return {
2925
- content: checkDocs(entries, lake.read, lake.notes, {
2926
- maxDocLines: runtime.maxDocLines,
2927
- ...(lake.linkCheck ? { exists: lake.exists } : {}),
2928
- }),
2929
- checked: entries.length,
2930
- };
3079
+ const { report, checked } = lakeReport(runtime, lake);
3080
+ return { content: cappedCheck(report), checked };
2931
3081
  }
2932
- const knowledge = runtime.lakes.filter(
2933
- (lake) => lake.profile === "knowledge",
2934
- );
2935
3082
  let checked = 0;
2936
- const reports = knowledge.map((lake) => {
2937
- const entries = lake.collect();
2938
- checked += entries.length;
2939
- const report = checkDocs(entries, lake.read, lake.notes, {
2940
- maxDocLines: runtime.maxDocLines,
2941
- ...(lake.linkCheck ? { exists: lake.exists } : {}),
2942
- });
2943
- return knowledge.length > 1 || runtime.lakes.length > 1
2944
- ? `${lake.name}:\n${report}`
2945
- : 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;
2946
3089
  });
2947
- const wiki = runtime.lakes
2948
- .filter((lake) => lake.profile === "wiki")
2949
- .map((lake) => lake.name);
2950
- if (wiki.length > 0) {
2951
- reports.push(
2952
- `note: wiki lakes are read-only reference and are not validated: ` +
2953
- `${wiki.join(", ")}.`,
2954
- );
2955
- }
2956
- return {
2957
- content:
2958
- reports.length > 0
2959
- ? reports.join("\n\n")
2960
- : "No knowledge lakes configured.",
2961
- checked,
2962
- };
3090
+ const skipped = runtime.lakes
3091
+ .filter((lake) => !lakeValidated(lake))
3092
+ .map(skippedLakeLine);
3093
+ return { content: assembleCheck(reports, skipped), checked };
2963
3094
  }
2964
3095
 
2965
3096
  /** Add a doc to the target lake. */
@@ -3064,14 +3195,26 @@ function uniqueHolder(
3064
3195
  if (holders.length === 0) {
3065
3196
  return { reason: `No doc at ${path}.` };
3066
3197
  }
3067
- if (holders.length > 1) {
3068
- return {
3069
- reason:
3070
- `${REFUSE} ${path} exists in multiple lakes ` +
3071
- `(${holders.map((lake) => lake.name).join(", ")}); pass lake.`,
3072
- };
3198
+ if (holders.length === 1) {
3199
+ return holders[0] as Lake;
3200
+ }
3201
+ // Nested lakes both hold an inner path: the narrowest enclosing lake owns
3202
+ // it. Non-nested holders tie on depth, which stays a documented refusal —
3203
+ // never a silent pick by collection order.
3204
+ const deepest = holders.map((lake) => ({
3205
+ lake,
3206
+ depth: nestingDepth(lake, holders),
3207
+ }));
3208
+ const max = Math.max(...deepest.map((item) => item.depth));
3209
+ const winners = deepest.filter((item) => item.depth === max);
3210
+ if (winners.length === 1) {
3211
+ return winners[0]?.lake as Lake;
3073
3212
  }
3074
- return holders[0] as Lake;
3213
+ return {
3214
+ reason:
3215
+ `${REFUSE} ${path} exists in multiple lakes ` +
3216
+ `(${holders.map((lake) => lake.name).join(", ")}); pass lake.`,
3217
+ };
3075
3218
  }
3076
3219
 
3077
3220
  /** Read one doc, resolving a `lake:path` prefix or the unique match. */
@@ -3121,8 +3264,8 @@ function lakeInfo(lakes: readonly Lake[]): LakeInfo[] {
3121
3264
  class: lake.class,
3122
3265
  root: lake.root,
3123
3266
  writable: lake.writable,
3124
- validated: lake.profile === "knowledge",
3125
- missing: lake.root !== "" && !existsSync(lake.root),
3267
+ validated: lakeValidated(lake),
3268
+ missing: lakeMissing(lake),
3126
3269
  }));
3127
3270
  }
3128
3271