@mulmoclaude/core 4.2.0 → 4.4.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.
Files changed (41) hide show
  1. package/assets/helps/collection-skills.md +17 -6
  2. package/assets/helps/custom-view-remote.md +28 -11
  3. package/assets/helps/error-recovery.md +73 -3
  4. package/dist/collection/registry/server/index.cjs +1 -1
  5. package/dist/collection/registry/server/index.js +1 -1
  6. package/dist/collection/server/index.cjs +2 -1
  7. package/dist/collection/server/index.js +2 -2
  8. package/dist/collection/server/manageTool.d.ts +24 -0
  9. package/dist/collection-watchers/index.cjs +1 -1
  10. package/dist/collection-watchers/index.js +1 -1
  11. package/dist/feeds/server/index.cjs +1 -1
  12. package/dist/feeds/server/index.js +1 -1
  13. package/dist/{graph-BZJxiv1E.cjs → graph-CURotEkH.cjs} +88 -37
  14. package/dist/graph-CURotEkH.cjs.map +1 -0
  15. package/dist/{graph-Ce7xsDTt.js → graph-DBxSOx3v.js} +77 -38
  16. package/dist/graph-DBxSOx3v.js.map +1 -0
  17. package/dist/{server-C-Ngd3g2.js → server-BvXO49Vj.js} +80 -13
  18. package/dist/{server-C-Ngd3g2.js.map → server-BvXO49Vj.js.map} +1 -1
  19. package/dist/{server-DZkN0GGh.cjs → server-CF1qllGV.cjs} +85 -12
  20. package/dist/{server-DZkN0GGh.cjs.map → server-CF1qllGV.cjs.map} +1 -1
  21. package/dist/{slug-CdN-pQX1.js → slug-BpjumQfe.js} +26 -5
  22. package/dist/{slug-CdN-pQX1.js.map → slug-BpjumQfe.js.map} +1 -1
  23. package/dist/{slug-DbwORN9z.cjs → slug-JxgB0ru9.cjs} +31 -4
  24. package/dist/{slug-DbwORN9z.cjs.map → slug-JxgB0ru9.cjs.map} +1 -1
  25. package/dist/wiki/graph.d.ts +0 -5
  26. package/dist/wiki/index.cjs +5 -2
  27. package/dist/wiki/index.d.ts +3 -2
  28. package/dist/wiki/index.js +3 -3
  29. package/dist/wiki/lint.d.ts +8 -9
  30. package/dist/wiki/paths.cjs +1 -1
  31. package/dist/wiki/paths.js +1 -1
  32. package/dist/wiki/resolve.d.ts +24 -0
  33. package/dist/wiki/server/engine.d.ts +12 -4
  34. package/dist/wiki/server/index.cjs +46 -34
  35. package/dist/wiki/server/index.cjs.map +1 -1
  36. package/dist/wiki/server/index.js +46 -34
  37. package/dist/wiki/server/index.js.map +1 -1
  38. package/dist/wiki/slug.d.ts +14 -3
  39. package/package.json +1 -1
  40. package/dist/graph-BZJxiv1E.cjs.map +0 -1
  41. package/dist/graph-Ce7xsDTt.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { n as wikiSlugify } from "./slug-CdN-pQX1.js";
1
+ import { n as wikiPageStem, r as wikiSlugify } from "./slug-BpjumQfe.js";
2
2
  var WIKI_LINK_PATTERN = new RegExp(`\\[\\[([^\\][\\r\\n]{1,200})\\]\\]`, "g");
3
3
  /** Split the raw text inside `[[...]]` into target + display.
4
4
  *
@@ -31,6 +31,44 @@ function parseWikiLink(inner) {
31
31
  };
32
32
  }
33
33
  //#endregion
34
+ //#region src/wiki/resolve.ts
35
+ /** Match a raw link target against known page slugs, returning the
36
+ * matched slug or null.
37
+ *
38
+ * Literal first: page slugs ARE filename stems, so a non-ASCII page
39
+ * only ever matches its own untouched name — `wikiSlugify` strips
40
+ * every non-ASCII character and would reduce `不耕起栽培-カバークロップ4年計画`
41
+ * to `-4` (#2940). The slugified form still runs second so
42
+ * `[[Sakura Internet]]` keeps resolving to `sakura-internet.md`. */
43
+ function matchWikiSlug(target, known) {
44
+ const literal = target.trim();
45
+ if (literal.length === 0) return null;
46
+ if (known.has(literal)) return literal;
47
+ const slug = wikiSlugify(literal);
48
+ if (slug.length > 0 && known.has(slug)) return slug;
49
+ return null;
50
+ }
51
+ /** index.md title → slug, first entry wins. The fallback for links
52
+ * written as the display title rather than the page's own name. */
53
+ function slugByIndexTitle(entries) {
54
+ const map = /* @__PURE__ */ new Map();
55
+ entries.forEach((entry) => {
56
+ if (entry.title.length > 0 && !map.has(entry.title)) map.set(entry.title, entry.slug);
57
+ });
58
+ return map;
59
+ }
60
+ /** Resolve a raw `[[link]]` target to an existing page slug, or null:
61
+ * known slug first, then an index entry's title. Every consumer that
62
+ * judges whether a link points at a real page — the graph and the
63
+ * lint — shares this, so a link cannot be an edge in one and a broken
64
+ * link in the other. */
65
+ function resolveLinkTarget(target, fileSlugs, slugByTitle) {
66
+ const matched = matchWikiSlug(target, fileSlugs);
67
+ if (matched !== null) return matched;
68
+ const byTitle = slugByTitle.get(target.trim());
69
+ return byTitle !== void 0 && fileSlugs.has(byTitle) ? byTitle : null;
70
+ }
71
+ //#endregion
34
72
  //#region src/wiki/index-parse.ts
35
73
  var BULLET_LINK_PATTERN = /^[-*]\s+\[([^\]]+)\]\(([^)]*)\)(?:\s*[—–-]\s*(.*))?/;
36
74
  var BULLET_WIKI_LINK_PATTERN = /^[-*]\s+\[\[([^\]]+)\]\](?:\s*[—–-]\s*(.*))?/;
@@ -104,6 +142,15 @@ function extractSlugFromBulletHref(rawHref) {
104
142
  if (/^[a-z]+:\/\//i.test(href)) return "";
105
143
  return (href.split("/").pop() ?? href).replace(/\.md$/i, "");
106
144
  }
145
+ /** Slug for an entry whose only identifier is human-written text.
146
+ * A page file may be named in Japanese, and its slug IS that filename
147
+ * stem — slugifying would index `-4` and leave the real file reported
148
+ * as both missing and orphaned (#2944). `wikiPageStem` keeps the
149
+ * hyphenated convention for ASCII and returns null for a name no file
150
+ * could carry, where the old slug is still the best guess. */
151
+ function entrySlugFor(name) {
152
+ return wikiPageStem(name) ?? wikiSlugify(name);
153
+ }
107
154
  function parseBulletLinkRow(trimmed) {
108
155
  const match = BULLET_LINK_PATTERN.exec(trimmed);
109
156
  if (!match) return null;
@@ -112,7 +159,7 @@ function parseBulletLinkRow(trimmed) {
112
159
  const { description, tags } = extractHashTags(match[3]?.trim() ?? "");
113
160
  return {
114
161
  title,
115
- slug: extractSlugFromBulletHref(href) || wikiSlugify(title),
162
+ slug: extractSlugFromBulletHref(href) || entrySlugFor(title),
116
163
  description,
117
164
  tags
118
165
  };
@@ -122,7 +169,7 @@ function parseBulletWikiLinkRow(trimmed) {
122
169
  if (!match) return null;
123
170
  const { target, display } = parseWikiLink(match[1] ?? "");
124
171
  const title = (display || target).trim();
125
- const slug = target ? wikiSlugify(target) : wikiSlugify(title);
172
+ const slug = entrySlugFor(target);
126
173
  const { description, tags } = extractHashTags(match[2]?.trim() ?? "");
127
174
  return {
128
175
  title,
@@ -176,31 +223,36 @@ function findOrphanPages(fileSlugs, indexedSlugs) {
176
223
  /** Slugs referenced by index.md that have no corresponding file. */
177
224
  function findMissingFiles(pageEntries, fileSlugs) {
178
225
  const issues = [];
179
- for (const entry of pageEntries) if (!fileSlugs.has(entry.slug)) issues.push(`- **Missing file**: index.md references \`${entry.slug}\` but the file does not exist`);
226
+ for (const entry of pageEntries) if (entry.slug.length === 0) issues.push(`- **Malformed entry**: index.md lists \`${entry.title}\` with no page name`);
227
+ else if (!fileSlugs.has(entry.slug)) issues.push(`- **Missing file**: index.md references \`${entry.slug}\` but the file does not exist`);
180
228
  return issues;
181
229
  }
182
- /** Walk a page's body for `[[…]]` links and flag any whose
183
- * resolved slug doesn't exist in the file set.
230
+ /** One `[[…]]` body's diagnostic, or null when the link resolves.
184
231
  *
185
232
  * Critically: this routes through `parseWikiLink` so
186
- * `[[slug|display]]` is split correctly — the lint slugifies the
233
+ * `[[slug|display]]` is split correctly — the lint resolves the
187
234
  * TARGET, not the full bracket body. Pre-#1297 the lint
188
235
  * slugified the entire content (`slug|display`), which collapsed
189
236
  * to a slug that always missed and produced ~168 false-positive
190
237
  * "broken link" warnings. */
191
- function findBrokenLinksInPage(fileName, content, fileSlugs) {
192
- const issues = [];
193
- const matches = [...content.matchAll(WIKI_LINK_PATTERN)];
194
- for (const [, body = ""] of matches) {
195
- const { target } = parseWikiLink(body);
196
- const linkSlug = wikiSlugify(target);
197
- if (linkSlug.length === 0) {
198
- issues.push(`- **Broken link** in \`${fileName}\`: [[${body}]] → empty target`);
199
- continue;
200
- }
201
- if (!fileSlugs.has(linkSlug)) issues.push(`- **Broken link** in \`${fileName}\`: [[${body}]] → \`${linkSlug}.md\` not found`);
202
- }
203
- return issues;
238
+ function brokenLinkIssue(fileName, body, fileSlugs, slugByTitle) {
239
+ const { target } = parseWikiLink(body);
240
+ if (target.trim().length === 0) return `- **Broken link** in \`${fileName}\`: [[${body}]] → empty target`;
241
+ if (resolveLinkTarget(target, fileSlugs, slugByTitle) !== null) return null;
242
+ const stem = wikiPageStem(target);
243
+ if (stem === null) return `- **Broken link** in \`${fileName}\`: [[${body}]] → \`${target.trim()}\` cannot be a page filename`;
244
+ return `- **Broken link** in \`${fileName}\`: [[${body}]] → \`${stem}.md\` not found`;
245
+ }
246
+ /** Walk a page's body for `[[…]]` links and flag any whose target
247
+ * doesn't resolve to a file.
248
+ *
249
+ * `slugByTitle` (from `slugByIndexTitle`) is what lets a link written
250
+ * as an index.md display title count as resolved — without it the
251
+ * lint calls broken exactly the links the page resolver and the graph
252
+ * follow happily. It defaults to empty so a caller linting an
253
+ * in-memory snapshot with no index still works. */
254
+ function findBrokenLinksInPage(fileName, content, fileSlugs, slugByTitle = /* @__PURE__ */ new Map()) {
255
+ return [...content.matchAll(WIKI_LINK_PATTERN)].map(([, body = ""]) => brokenLinkIssue(fileName, body, fileSlugs, slugByTitle)).filter((issue) => issue !== null);
204
256
  }
205
257
  function formatTagList(tags) {
206
258
  return `[${[...tags].sort().join(", ")}]`;
@@ -232,17 +284,6 @@ function formatLintReport(issues) {
232
284
  }
233
285
  //#endregion
234
286
  //#region src/wiki/graph.ts
235
- /** Resolve a raw `[[link]]` target to an existing page slug, or null.
236
- * Mirrors the route resolver's strategy: slugify first, then fall
237
- * back to matching an index entry title so non-ASCII targets like
238
- * `[[さくらインターネット]]` resolve to their ASCII file slug. */
239
- function resolveLinkTarget(target, fileSlugs, slugByTitle) {
240
- const slug = wikiSlugify(target);
241
- if (slug.length > 0 && fileSlugs.has(slug)) return slug;
242
- const byTitle = slugByTitle.get(target.trim());
243
- if (byTitle !== void 0 && fileSlugs.has(byTitle)) return byTitle;
244
- return null;
245
- }
246
287
  /** Resolved, deduped outgoing slugs for one page body. Self-links and
247
288
  * links to non-existent pages are dropped (the latter are already a
248
289
  * lint "broken link", not a graph edge). */
@@ -257,14 +298,12 @@ function pageOutgoingSlugs(fromSlug, content, fileSlugs, slugByTitle) {
257
298
  }
258
299
  function buildTitleMaps(entries) {
259
300
  const titleBySlug = /* @__PURE__ */ new Map();
260
- const slugByTitle = /* @__PURE__ */ new Map();
261
- for (const entry of entries) {
301
+ entries.forEach((entry) => {
262
302
  if (!titleBySlug.has(entry.slug)) titleBySlug.set(entry.slug, entry.title);
263
- if (entry.title.length > 0 && !slugByTitle.has(entry.title)) slugByTitle.set(entry.title, entry.slug);
264
- }
303
+ });
265
304
  return {
266
305
  titleBySlug,
267
- slugByTitle
306
+ slugByTitle: slugByIndexTitle(entries)
268
307
  };
269
308
  }
270
309
  /** Build the full page→page graph. Nodes are the existing page files
@@ -307,6 +346,6 @@ function incomingLinks(graph, slug) {
307
346
  return result;
308
347
  }
309
348
  //#endregion
310
- export { WIKI_LINK_PATTERN as _, findBrokenLinksInPage as a, findTagDrift as c, BULLET_WIKI_LINK_PATTERN as d, buildTableColumnMap as f, parseTagsCell as g, parseIndexEntries as h, resolveLinkTarget as i, formatLintReport as l, extractSlugFromBulletHref as m, incomingLinks as n, findMissingFiles as o, extractHashTags as p, pageOutgoingSlugs as r, findOrphanPages as s, buildWikiGraph as t, BULLET_LINK_PATTERN as u, parseWikiLink as v };
349
+ export { resolveLinkTarget as _, findMissingFiles as a, parseWikiLink as b, formatLintReport as c, buildTableColumnMap as d, extractHashTags as f, matchWikiSlug as g, parseTagsCell as h, findBrokenLinksInPage as i, BULLET_LINK_PATTERN as l, parseIndexEntries as m, incomingLinks as n, findOrphanPages as o, extractSlugFromBulletHref as p, pageOutgoingSlugs as r, findTagDrift as s, buildWikiGraph as t, BULLET_WIKI_LINK_PATTERN as u, slugByIndexTitle as v, WIKI_LINK_PATTERN as y };
311
350
 
312
- //# sourceMappingURL=graph-Ce7xsDTt.js.map
351
+ //# sourceMappingURL=graph-DBxSOx3v.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-DBxSOx3v.js","names":[],"sources":["../src/wiki/link.ts","../src/wiki/resolve.ts","../src/wiki/index-parse.ts","../src/wiki/lint.ts","../src/wiki/graph.ts"],"sourcesContent":["// Pure helpers for `[[wiki-link]]` syntax. Used by the lint\n// (`server/api/routes/wiki.ts`), the page resolver, and the\n// frontend renderer (`src/plugins/wiki/helpers.ts`) — before this\n// module the three sites each had their own implementation and\n// none of them handled the `[[target|display]]` form, producing\n// false-positive \"broken link\" warnings on slug-aliased links\n// (issue #1297).\n//\n// All functions are pure string operations; safe to import from\n// browser bundles. No `node:*` imports anywhere in this file.\n\n/** Inline `[[...]]` link pattern. The capture group is the raw\n * text between the brackets — the caller is expected to feed it\n * through `parseWikiLink` to split off any `|display` suffix.\n *\n * Body length is capped at 200 chars to keep the regex linear:\n * catastrophic backtracking isn't possible (no nested\n * alternation), but a malicious page with thousands of\n * unmatched `[[` could still pin the CPU. 200 is well above the\n * longest legitimate title we've seen and matches the cap the\n * server-side `WIKI_LINK_PATTERN` constant has carried since the\n * original implementation in #951.\n *\n * Shared with the renderer's hand-rolled scanner so the two can't\n * drift: the renderer must reject the same newline-bearing and\n * over-length bodies, or a link would render clickable while the\n * graph / backlinks / lint (which use this pattern) never see it. */\nexport const WIKI_LINK_MAX_LEN = 200;\nexport const WIKI_LINK_PATTERN = new RegExp(`\\\\[\\\\[([^\\\\][\\\\r\\\\n]{1,${WIKI_LINK_MAX_LEN}})\\\\]\\\\]`, \"g\");\n\n/** The structural shape of an inline `[[target|display]]` link.\n * `target` is what the resolver / lint compares against page\n * slugs; `display` is what the renderer shows to the user.\n * When the user writes `[[foo]]` (no pipe), both fields hold\n * `foo`. */\nexport interface WikiLink {\n target: string;\n display: string;\n}\n\n/** Split the raw text inside `[[...]]` into target + display.\n *\n * - `[[foo|Bar Baz]]` → `{ target: \"foo\", display: \"Bar Baz\" }`\n * - `[[foo]]` → `{ target: \"foo\", display: \"foo\" }`\n * - `[[|empty target]]` → `{ target: \"\", display: \"empty target\" }`\n * (caller decides whether an empty target is meaningful;\n * typically lint flags it, renderer shows the display text raw)\n * - `[[foo|]]` → `{ target: \"foo\", display: \"\" }`\n * (same — caller decides)\n *\n * Whitespace around the pipe is preserved on the display side\n * (so the user can write `[[foo| ある記事 ]]` and get the\n * spacing intentionally) but TRIMMED on the target side, since\n * a slug-comparable target with leading/trailing whitespace is\n * always a typo.\n *\n * Only the FIRST pipe splits. `[[a|b|c]]` becomes\n * `{ target: \"a\", display: \"b|c\" }` so display strings can\n * legitimately contain a literal `|`. */\nexport function parseWikiLink(inner: string): WikiLink {\n const pipeIdx = inner.indexOf(\"|\");\n if (pipeIdx === -1) {\n return { target: inner, display: inner };\n }\n const target = inner.slice(0, pipeIdx).trim();\n const display = inner.slice(pipeIdx + 1);\n return { target, display };\n}\n","// The one rule for \"does this `[[link]]` target name an existing page?\".\n// Shared by the page resolver, the lint, and the graph so the three\n// cannot disagree on which files a target can reach.\n//\n// Pure string + lookup work; safe to import from browser bundles.\n\nimport type { WikiPageEntry } from \"./index-parse.js\";\nimport { wikiSlugify } from \"./slug.js\";\n\n/** Anything that can answer \"do you hold this slug?\" — the page index\n * (`Map<slug, filename>`) and the lint's `Set<slug>` both qualify. */\nexport interface SlugLookup {\n has: (slug: string) => boolean;\n}\n\n/** Match a raw link target against known page slugs, returning the\n * matched slug or null.\n *\n * Literal first: page slugs ARE filename stems, so a non-ASCII page\n * only ever matches its own untouched name — `wikiSlugify` strips\n * every non-ASCII character and would reduce `不耕起栽培-カバークロップ4年計画`\n * to `-4` (#2940). The slugified form still runs second so\n * `[[Sakura Internet]]` keeps resolving to `sakura-internet.md`. */\nexport function matchWikiSlug(target: string, known: SlugLookup): string | null {\n const literal = target.trim();\n if (literal.length === 0) return null;\n if (known.has(literal)) return literal;\n const slug = wikiSlugify(literal);\n if (slug.length > 0 && known.has(slug)) return slug;\n return null;\n}\n\n/** index.md title → slug, first entry wins. The fallback for links\n * written as the display title rather than the page's own name. */\nexport function slugByIndexTitle(entries: readonly WikiPageEntry[]): Map<string, string> {\n const map = new Map<string, string>();\n entries.forEach((entry) => {\n if (entry.title.length > 0 && !map.has(entry.title)) map.set(entry.title, entry.slug);\n });\n return map;\n}\n\n/** Resolve a raw `[[link]]` target to an existing page slug, or null:\n * known slug first, then an index entry's title. Every consumer that\n * judges whether a link points at a real page — the graph and the\n * lint — shares this, so a link cannot be an edge in one and a broken\n * link in the other. */\nexport function resolveLinkTarget(target: string, fileSlugs: SlugLookup, slugByTitle: ReadonlyMap<string, string>): string | null {\n const matched = matchWikiSlug(target, fileSlugs);\n if (matched !== null) return matched;\n const byTitle = slugByTitle.get(target.trim());\n return byTitle !== undefined && fileSlugs.has(byTitle) ? byTitle : null;\n}\n","// Pure parsers for `data/wiki/index.md` content. Used by:\n// - server/api/routes/wiki.ts — page resolver + lint\n// - frontend lint preview (future) — same parser, no fork\n//\n// All exports here are pure string ops; no `node:*` imports allowed.\n// If a future caller needs to walk the filesystem, that wrapper goes\n// under `@mulmoclaude/core/wiki/server` (today only `server/paths.ts`\n// touches `node:path`; nothing here touches disk).\n\nimport { parseWikiLink } from \"./link.js\";\nimport { wikiPageStem, wikiSlugify } from \"./slug.js\";\n\nexport interface WikiPageEntry {\n title: string;\n slug: string;\n description: string;\n tags: string[];\n}\n\n// ── Patterns ──────────────────────────────────────────────────\n// Bullet patterns live here (not server/utils/regex.ts) because the\n// index parser is the only consumer and the regexes are part of the\n// public surface that callers may want to reason about in tests.\n// Linear in line length: every `[^x]+` runs over a fixed exclusion\n// set with a hard delimiter; the optional summary group's `(.*)` is\n// a single greedy run with no nested overlap.\n\n// eslint-disable-next-line security/detect-unsafe-regex -- bullet-link parser; bounded captures with hard delimiters\nexport const BULLET_LINK_PATTERN = /^[-*]\\s+\\[([^\\]]+)\\]\\(([^)]*)\\)(?:\\s*[—–-]\\s*(.*))?/;\n// eslint-disable-next-line security/detect-unsafe-regex -- same shape as BULLET_LINK_PATTERN\nexport const BULLET_WIKI_LINK_PATTERN = /^[-*]\\s+\\[\\[([^\\]]+)\\]\\](?:\\s*[—–-]\\s*(.*))?/;\n\nconst TABLE_SEPARATOR_PATTERN = /^\\|[\\s|:-]+\\|$/;\n\n// Unicode-aware tag body: any letter or number in any script (so\n// Japanese / Chinese / Korean tags like `#クラウド` or `#可視化` work),\n// plus `-` and `_` as internal joiners. First char is a letter or\n// number only — no leading punctuation.\nconst HASHTAG_PATTERN = /(?:^|\\s)#([\\p{L}\\p{N}][\\p{L}\\p{N}_-]*)/gu;\n\n// ── Tag extraction ────────────────────────────────────────────\n\n/** Extract `#tag` tokens from a bullet description, returning the\n * stripped description and a sorted, deduped, lowercased tag list.\n * Only matches at word boundaries so mid-word `#` (e.g. anchor\n * URLs) is left alone. */\nexport function extractHashTags(text: string): { description: string; tags: string[] } {\n HASHTAG_PATTERN.lastIndex = 0;\n const tags = [...text.matchAll(HASHTAG_PATTERN)].flatMap(([, tag]) => (tag ? [tag.toLowerCase()] : []));\n const description = text.replace(HASHTAG_PATTERN, \"\").replace(/\\s+/g, \" \").trim();\n const deduped = [...new Set(tags)].sort();\n return { description, tags: deduped };\n}\n\n/** Split a table Tags cell — tolerates comma, whitespace, or `#`\n * prefixes. Empty cell yields an empty list. */\nexport function parseTagsCell(cell: string): string[] {\n const tokens = cell\n .split(/[,\\s]+/)\n .map((token) => token.trim().replace(/^#/, \"\").toLowerCase())\n .filter((token) => token.length > 0);\n return [...new Set(tokens)].sort();\n}\n\n// ── Table parser ──────────────────────────────────────────────\n\n/** Map header cell names → column indices, case- and whitespace-\n * tolerant. Used by the row parser to locate the Tags column (and\n * any other named column) without assuming a fixed position, so\n * older 3- and 4-column tables keep working. */\nexport function buildTableColumnMap(headerRow: string): Map<string, number> {\n const cells = headerRow\n .split(\"|\")\n .slice(1, -1)\n // Mirror parseTableRow's cell-normalising: strip the surrounding\n // backticks that commonly wrap cell values in wiki tables.\n // Without this, a `| \\`tags\\` |` header maps to the key \"`tags`\"\n // and the subsequent `columnMap.get(\"tags\")` lookup silently\n // misses the column, falling back to `tags: []`.\n .map((cell) => cell.trim().replace(/^`|`$/g, \"\").toLowerCase());\n const map = new Map<string, number>();\n cells.forEach((cell, i) => {\n if (cell) map.set(cell, i);\n });\n return map;\n}\n\ninterface TableColumnIndices {\n slug: number;\n title: number;\n summary: number;\n /** Undefined when the table has no `tags` column — caller skips\n * the tags lookup entirely and the row gets `tags: []`. */\n tags: number | undefined;\n}\n\n/** Resolve the per-column indices the row parser needs. Falls back\n * to positional defaults (0/1/2) when the table has no header map.\n * \"summary\" is the canonical column name; \"description\" is accepted\n * as a legacy alias used by older fixtures. */\nfunction resolveTableColumnIndices(columnMap: Map<string, number> | null): TableColumnIndices {\n return {\n slug: columnMap?.get(\"slug\") ?? 0,\n title: columnMap?.get(\"title\") ?? 1,\n summary: columnMap?.get(\"summary\") ?? columnMap?.get(\"description\") ?? 2,\n tags: columnMap?.get(\"tags\"),\n };\n}\n\nfunction parseTableRow(trimmed: string, columnMap: Map<string, number> | null): WikiPageEntry | null {\n const cols = trimmed\n .split(\"|\")\n .slice(1, -1)\n .map((column) => column.trim().replace(/^`|`$/g, \"\"));\n if (cols.length < 2) return null;\n\n const idx = resolveTableColumnIndices(columnMap);\n const slug = cols[idx.slug] ?? \"\";\n const title = cols[idx.title] || slug;\n if (!slug || !title) return null;\n\n const description = cols[idx.summary] ?? \"\";\n const tags = idx.tags !== undefined ? parseTagsCell(cols[idx.tags] ?? \"\") : [];\n return { title, slug, description, tags };\n}\n\n// ── Bullet-row parsers ────────────────────────────────────────\n\n/** Extract the slug segment from a bullet link's href. Accepts the\n * canonical `pages/<slug>.md`, a bare `<slug>.md`, or just `<slug>`\n * — the three forms produced by different historical writers of\n * index.md. Returns \"\" for hrefs that don't look like a wiki page\n * reference (e.g. `https://example.com`) so the caller can fall\n * back to title-based slugification. */\nexport function extractSlugFromBulletHref(rawHref: string): string {\n const href = rawHref.trim();\n if (!href) return \"\";\n if (/^[a-z]+:\\/\\//i.test(href)) return \"\";\n const lastSegment = href.split(\"/\").pop() ?? href;\n return lastSegment.replace(/\\.md$/i, \"\");\n}\n\n/** Slug for an entry whose only identifier is human-written text.\n * A page file may be named in Japanese, and its slug IS that filename\n * stem — slugifying would index `-4` and leave the real file reported\n * as both missing and orphaned (#2944). `wikiPageStem` keeps the\n * hyphenated convention for ASCII and returns null for a name no file\n * could carry, where the old slug is still the best guess. */\nfunction entrySlugFor(name: string): string {\n return wikiPageStem(name) ?? wikiSlugify(name);\n}\n\nfunction parseBulletLinkRow(trimmed: string): WikiPageEntry | null {\n const match = BULLET_LINK_PATTERN.exec(trimmed);\n if (!match) return null;\n const title = match[1]?.trim() ?? \"\";\n const href = match[2] ?? \"\";\n const raw = match[3]?.trim() ?? \"\";\n const { description, tags } = extractHashTags(raw);\n // Prefer the slug embedded in the href so non-ASCII titles keep\n // a navigable slug. Fall back to the title only when the href has\n // no recognisable slug (rare — usually means the author put an\n // external URL here).\n const slug = extractSlugFromBulletHref(href) || entrySlugFor(title);\n return { title, slug, description, tags };\n}\n\nfunction parseBulletWikiLinkRow(trimmed: string): WikiPageEntry | null {\n const match = BULLET_WIKI_LINK_PATTERN.exec(trimmed);\n if (!match) return null;\n // Split `[[target|display]]` so the bullet entry's slug derives\n // from the TARGET (which is already a slug-shaped identifier\n // when the author wrote the alias form) and the title shows\n // the DISPLAY half. Pre-#1297 the parser slugified the full\n // `target|display` body, which collapses `|` to \"\" and\n // produces a wrong slug — Codex review on PR #1312.\n const { target, display } = parseWikiLink(match[1] ?? \"\");\n const title = (display || target).trim();\n // No title fallback: an explicitly empty target (`- [[|display]]`) is\n // malformed, and borrowing the display half would let it name a real\n // page — silently accepting the typo instead of reporting it. An\n // empty slug reaches `findMissingFiles`, which says so (#2944).\n const slug = entrySlugFor(target);\n const raw = match[2]?.trim() ?? \"\";\n const { description, tags } = extractHashTags(raw);\n return { title, slug, description, tags };\n}\n\n// ── Top-level parser ──────────────────────────────────────────\n\n/** Parse entries from index.md. Supports three formats:\n * 1. Table: `| slug | Title | Summary | (Tags) |`\n * 2. Bullet link: `- [Title](pages/slug.md) — description`\n * 3. Wiki link: `- [[Title]] — description`\n *\n * Returns entries in source order. Any unrecognised line is\n * silently skipped — index.md is freeform markdown otherwise. */\nexport function parseIndexEntries(content: string): WikiPageEntry[] {\n const entries: WikiPageEntry[] = [];\n let inTable = false;\n let columnMap: Map<string, number> | null = null;\n\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n\n if (trimmed.startsWith(\"|\")) {\n if (TABLE_SEPARATOR_PATTERN.test(trimmed)) {\n inTable = true;\n continue;\n }\n if (!inTable) {\n // First `|`-line before the separator is the header. Capture\n // the column map so parseTableRow can locate the Tags\n // column (if any) by name rather than position.\n columnMap = buildTableColumnMap(trimmed);\n inTable = true;\n continue;\n }\n const entry = parseTableRow(trimmed, columnMap);\n if (entry) entries.push(entry);\n continue;\n }\n\n inTable = false;\n columnMap = null;\n\n const bullet = parseBulletLinkRow(trimmed) ?? parseBulletWikiLinkRow(trimmed);\n if (bullet) entries.push(bullet);\n }\n return entries;\n}\n","// Pure wiki lint rules.\n//\n// Each rule takes plain inputs (Sets, page-entry arrays, raw file\n// contents) so the whole pipeline can be unit-tested without\n// filesystem fixtures. The Express route at\n// `server/api/routes/wiki.ts` is a thin shell that reads the disk,\n// calls these rules, formats the result. A future frontend \"lint\n// preview before save\" would call the same functions over an\n// in-memory snapshot.\n\nimport type { WikiPageEntry } from \"./index-parse.js\";\nimport { WIKI_LINK_PATTERN, parseWikiLink } from \"./link.js\";\nimport { resolveLinkTarget } from \"./resolve.js\";\nimport { wikiPageStem } from \"./slug.js\";\n\n/** Files on disk that aren't referenced by index.md. */\nexport function findOrphanPages(fileSlugs: ReadonlySet<string>, indexedSlugs: ReadonlySet<string>): string[] {\n const issues: string[] = [];\n for (const slug of fileSlugs) {\n if (!indexedSlugs.has(slug)) {\n issues.push(`- **Orphan page**: \\`${slug}.md\\` exists but is missing from index.md`);\n }\n }\n return issues;\n}\n\n/** Slugs referenced by index.md that have no corresponding file. */\nexport function findMissingFiles(pageEntries: readonly WikiPageEntry[], fileSlugs: ReadonlySet<string>): string[] {\n const issues: string[] = [];\n for (const entry of pageEntries) {\n if (entry.slug.length === 0) {\n issues.push(`- **Malformed entry**: index.md lists \\`${entry.title}\\` with no page name`);\n } else if (!fileSlugs.has(entry.slug)) {\n issues.push(`- **Missing file**: index.md references \\`${entry.slug}\\` but the file does not exist`);\n }\n }\n return issues;\n}\n\n/** One `[[…]]` body's diagnostic, or null when the link resolves.\n *\n * Critically: this routes through `parseWikiLink` so\n * `[[slug|display]]` is split correctly — the lint resolves the\n * TARGET, not the full bracket body. Pre-#1297 the lint\n * slugified the entire content (`slug|display`), which collapsed\n * to a slug that always missed and produced ~168 false-positive\n * \"broken link\" warnings. */\nfunction brokenLinkIssue(fileName: string, body: string, fileSlugs: ReadonlySet<string>, slugByTitle: ReadonlyMap<string, string>): string | null {\n const { target } = parseWikiLink(body);\n // Empty target is its own diagnostic — `[[|display]]` has nothing to\n // resolve and would otherwise be flagged identically to a real broken\n // link. Keep the original raw bracket body in the report so the user\n // can grep their pages for the malformed link. (`[[]]` never reaches\n // here: a zero-length body matches neither `WIKI_LINK_PATTERN` nor the\n // renderer's scanner, so it stays literal text everywhere.)\n if (target.trim().length === 0) return `- **Broken link** in \\`${fileName}\\`: [[${body}]] → empty target`;\n if (resolveLinkTarget(target, fileSlugs, slugByTitle) !== null) return null;\n const stem = wikiPageStem(target);\n // A target the write guard would reject has no file to suggest —\n // saying `../secrets.md not found` invites creating what cannot\n // exist (CodeRabbit review).\n if (stem === null) return `- **Broken link** in \\`${fileName}\\`: [[${body}]] → \\`${target.trim()}\\` cannot be a page filename`;\n return `- **Broken link** in \\`${fileName}\\`: [[${body}]] → \\`${stem}.md\\` not found`;\n}\n\n/** Walk a page's body for `[[…]]` links and flag any whose target\n * doesn't resolve to a file.\n *\n * `slugByTitle` (from `slugByIndexTitle`) is what lets a link written\n * as an index.md display title count as resolved — without it the\n * lint calls broken exactly the links the page resolver and the graph\n * follow happily. It defaults to empty so a caller linting an\n * in-memory snapshot with no index still works. */\nexport function findBrokenLinksInPage(\n fileName: string,\n content: string,\n fileSlugs: ReadonlySet<string>,\n slugByTitle: ReadonlyMap<string, string> = new Map(),\n): string[] {\n return [...content.matchAll(WIKI_LINK_PATTERN)]\n .map(([, body = \"\"]) => brokenLinkIssue(fileName, body, fileSlugs, slugByTitle))\n .filter((issue): issue is string => issue !== null);\n}\n\nfunction formatTagList(tags: readonly string[]): string {\n return `[${[...tags].sort().join(\", \")}]`;\n}\n\n/** Flag any slug whose index.md tags differ from the page's own\n * frontmatter `tags:` field. Comparison is set-based and order-\n * insensitive; both sides are lowercased at parse time. Slugs\n * missing from `frontmatterTagsBySlug` are ignored here — the\n * missing file itself is already reported by `findMissingFiles`. */\nexport function findTagDrift(pageEntries: readonly WikiPageEntry[], frontmatterTagsBySlug: ReadonlyMap<string, readonly string[]>): string[] {\n const issues: string[] = [];\n for (const entry of pageEntries) {\n // Lowercase on lookup — the caller keys the map with\n // lowercased slugs, so a `MyPage.md` filename still matches\n // an `entry.slug` of `mypage` produced by `wikiSlugify` on the\n // wiki-link parser path.\n const pageTags = frontmatterTagsBySlug.get(entry.slug.toLowerCase());\n if (pageTags === undefined) continue;\n const pageSet = new Set(pageTags);\n const indexSet = new Set(entry.tags);\n if (pageSet.size !== indexSet.size || [...pageSet].some((tag) => !indexSet.has(tag))) {\n issues.push(`- **Tag drift**: \\`${entry.slug}.md\\` frontmatter has ${formatTagList(pageTags)} but index.md has ${formatTagList(entry.tags)}`);\n }\n }\n return issues;\n}\n\n/** Render the final markdown report from the accumulated issues\n * list. Empty input yields the \"wiki is healthy\" sentinel so the\n * caller can write the same file every time without branching on\n * presence. */\nexport function formatLintReport(issues: readonly string[]): string {\n if (issues.length === 0) {\n return \"# Wiki Lint Report\\n\\n✓ No issues found. Wiki is healthy.\";\n }\n const noun = `issue${issues.length !== 1 ? \"s\" : \"\"}`;\n return `# Wiki Lint Report\\n\\n${issues.length} ${noun} found:\\n\\n${issues.join(\"\\n\")}`;\n}\n","// Pure builder for the wiki page→page link graph. Used by:\n// - server/api/routes/wiki.ts — the `graph` action endpoint\n// - src/plugins/wiki/View.vue — backlinks (\"linked references\") +\n// the Graph tab\n//\n// All functions are pure string / collection ops; no `node:*` imports,\n// so the frontend bundle can import them directly (same discipline as\n// the sibling `link.ts` / `lint.ts` / `index-parse.ts` modules).\n//\n// NOTE: this is page→page link structure, distinct from the\n// `server/workspace/wiki-backlinks/` module, which appends *session*\n// backlinks (a page → the chat that edited it, #109). Different concept\n// — do not conflate.\n\nimport { WIKI_LINK_PATTERN, parseWikiLink } from \"./link.js\";\nimport { resolveLinkTarget, slugByIndexTitle } from \"./resolve.js\";\nimport type { WikiPageEntry } from \"./index-parse.js\";\n\nexport interface WikiGraphNode {\n slug: string;\n title: string;\n}\n\nexport interface WikiGraphEdge {\n from: string;\n to: string;\n}\n\nexport interface WikiGraph {\n nodes: WikiGraphNode[];\n edges: WikiGraphEdge[];\n}\n\n/** One page's raw body plus its canonical (file-derived) slug. */\nexport interface WikiPageContent {\n slug: string;\n content: string;\n}\n\n/** Resolved, deduped outgoing slugs for one page body. Self-links and\n * links to non-existent pages are dropped (the latter are already a\n * lint \"broken link\", not a graph edge). */\nexport function pageOutgoingSlugs(fromSlug: string, content: string, fileSlugs: ReadonlySet<string>, slugByTitle: ReadonlyMap<string, string>): string[] {\n const out = new Set<string>();\n for (const [, body = \"\"] of content.matchAll(WIKI_LINK_PATTERN)) {\n const { target } = parseWikiLink(body);\n const resolved = resolveLinkTarget(target, fileSlugs, slugByTitle);\n if (resolved !== null && resolved !== fromSlug) out.add(resolved);\n }\n return [...out];\n}\n\nfunction buildTitleMaps(entries: readonly WikiPageEntry[]): { titleBySlug: Map<string, string>; slugByTitle: Map<string, string> } {\n const titleBySlug = new Map<string, string>();\n entries.forEach((entry) => {\n if (!titleBySlug.has(entry.slug)) titleBySlug.set(entry.slug, entry.title);\n });\n return { titleBySlug, slugByTitle: slugByIndexTitle(entries) };\n}\n\n/** Build the full page→page graph. Nodes are the existing page files\n * (titled from index.md, falling back to the slug for un-indexed\n * pages); edges are the resolved `[[links]]`, deduped per (from,to). */\nexport function buildWikiGraph(pages: readonly WikiPageContent[], entries: readonly WikiPageEntry[]): WikiGraph {\n const fileSlugs = new Set(pages.map((page) => page.slug));\n const { titleBySlug, slugByTitle } = buildTitleMaps(entries);\n const nodes: WikiGraphNode[] = pages.map((page) => ({ slug: page.slug, title: titleBySlug.get(page.slug) ?? page.slug }));\n const edges: WikiGraphEdge[] = [];\n const seen = new Set<string>();\n for (const page of pages) {\n for (const toSlug of pageOutgoingSlugs(page.slug, page.content, fileSlugs, slugByTitle)) {\n // Newline cannot appear in a wiki slug, so it is a safe pair key.\n const key = [page.slug, toSlug].join(\"\\n\");\n if (seen.has(key)) continue;\n seen.add(key);\n edges.push({ from: page.slug, to: toSlug });\n }\n }\n return { nodes, edges };\n}\n\n/** Pages that link TO `slug` (incoming edges), deduped, as nodes. */\nexport function incomingLinks(graph: WikiGraph, slug: string): WikiGraphNode[] {\n const nodeBySlug = new Map(graph.nodes.map((node) => [node.slug, node]));\n const result: WikiGraphNode[] = [];\n const seen = new Set<string>();\n for (const edge of graph.edges) {\n if (edge.to !== slug || seen.has(edge.from)) continue;\n seen.add(edge.from);\n const node = nodeBySlug.get(edge.from);\n if (node) result.push(node);\n }\n return result;\n}\n"],"mappings":"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"}
@@ -1566,6 +1566,22 @@ var MAX_SCHEMA_ISSUES = 20;
1566
1566
  * write that reported success would leave a half-filled collection nobody
1567
1567
  * knows is half-filled. */
1568
1568
  var MAX_PUT_ITEMS = 1e3;
1569
+ /** Cap the strict-tier findings one putItems call reports back. The rows were
1570
+ * WRITTEN, so this is a report and not a refusal — and a generated batch is
1571
+ * wrong the same way in every row, which means ten samples plus the total say
1572
+ * everything a thousand would. */
1573
+ var MAX_PUT_LINT = 10;
1574
+ /** What the `lint` block SAYS, once per call. A bare list of findings on rows the
1575
+ * call also reports as `written` reads as noise; the reason to act is that the
1576
+ * next reader of these rows is stricter than the write was.
1577
+ *
1578
+ * Like everything else the tool says about validation, this describes the tool
1579
+ * with its gate ON. Under `ablateValidation` nothing is checked — `rejected`
1580
+ * stays empty too — and the caveat is deliberately NOT written into the
1581
+ * agent-facing text: ablation exists to measure what a model does without the
1582
+ * guardrails, and a model told "unless ablation is on" is no longer without
1583
+ * them (CodeRabbit on #2925). */
1584
+ var PUT_LINT_NOTE = "These rows WERE written. The write gate refuses what would make a record unopenable — a missing required field, a value outside an enum, a mismatched primaryKey, a computed key, a colliding id under `create` — but it does not check the SHAPE of a value, so a wrong-shaped one is reported here rather than rejected. `getItems` surfaces the same finding as a `warning` (on a full listing, or when a requested id is missing), and publishing a shared app REFUSES the row outright. Fix the generator and rewrite them before writing the rest of the set.";
1569
1585
  /** Refuse an `itemsFile` larger than this, from `stat` and before any read.
1570
1586
  * The row cap alone cannot bound the work: the file has to be read and parsed
1571
1587
  * WHOLE before there are rows to count, so a huge blob is paid for in full
@@ -1734,6 +1750,26 @@ async function mergeWithExisting(collection, store, record, itemId) {
1734
1750
  ...record
1735
1751
  };
1736
1752
  }
1753
+ /** The strict-tier finding on a row the write gate LET THROUGH, or nothing.
1754
+ *
1755
+ * The gate stays exactly where it was — `lint-not-lock` is why the strict tier
1756
+ * exists (see `../core/recordZ`), and turning these into rejections would make
1757
+ * a collection whose legacy rows predate the typed rules unwritable. What was
1758
+ * missing is that the row's author never HEARD about it: `getItems` reports the
1759
+ * same finding and publishing a shared app refuses exactly these rows, so the
1760
+ * one surface that stayed silent was the one that could still fix the generator
1761
+ * before it wrote the other 719 rows (mulmoterminal#1763).
1762
+ *
1763
+ * Run against the record as WRITTEN (`toWrite`, post-merge, post-defaults) —
1764
+ * what a later read will lint is what landed, not what the caller sent. */
1765
+ function lintOf(record, itemId, schema, deps) {
1766
+ if (deps.ablateValidation) return {};
1767
+ const problem = validateRecordObject(record, itemId, schema, "strict");
1768
+ return problem ? { lint: {
1769
+ id: defangForPrompt(itemId),
1770
+ problem: defangForPrompt(problem)
1771
+ } } : {};
1772
+ }
1737
1773
  async function putOneItem(collection, store, write, record, mode, deps) {
1738
1774
  const { schema } = collection;
1739
1775
  const created = mode === "create" ? {
@@ -1759,7 +1795,10 @@ async function putOneItem(collection, store, write, record, mode, deps) {
1759
1795
  if (invalid) return reject(itemId, invalid);
1760
1796
  }
1761
1797
  const result = await write(itemId, toWrite, { refuseOverwrite: mode === "create" });
1762
- if (result.kind === "ok") return { written: result.itemId };
1798
+ if (result.kind === "ok") return {
1799
+ written: result.itemId,
1800
+ ...lintOf(toWrite, result.itemId, schema, deps)
1801
+ };
1763
1802
  if (result.kind === "invalid-id") return reject(itemId, `'${itemId}' is not a valid record id (letters/digits at the ends; -, _, or . inside; no '..' or path characters)`);
1764
1803
  if (result.kind === "conflict") return reject(itemId, `'${itemId}' already exists — mode "create" refuses overwrite; use "upsert" to update it`);
1765
1804
  return reject(itemId, "write refused: the collection's data dir escapes the workspace");
@@ -1932,23 +1971,51 @@ async function resolvePutRows(args, deps) {
1932
1971
  if (rows.length > 1e3) return `manageCollection: refused — ${rows.length} rows is over the putItems limit of ${MAX_PUT_ITEMS}. Nothing was written; split them across several calls.`;
1933
1972
  return rows;
1934
1973
  }
1935
- async function handlePutItems(collection, args, deps) {
1936
- const store = storeFor(collection, { workspaceRoot: deps.workspaceRoot });
1937
- const { write } = store;
1938
- if (!write) return `manageCollection: ${readOnlyRefusal(collection.slug)} (its records are the rows of '${collection.schema.dataSource?.path}'; edit that file to change the data).`;
1939
- const rows = await resolvePutRows(args, deps);
1940
- if (typeof rows === "string") return rows;
1974
+ /** The `lint` block of a putItems result, or nothing when every written row is
1975
+ * clean — absent rather than empty, so a clean call reads exactly as it always
1976
+ * did and the key's presence is itself the signal.
1977
+ *
1978
+ * `total` counts every flagged row and `rows` shows the first
1979
+ * `MAX_PUT_LINT` of them, so a capped report can never be read as a total. */
1980
+ function lintReport(lint) {
1981
+ if (lint.length === 0) return {};
1982
+ return { lint: {
1983
+ total: lint.length,
1984
+ note: PUT_LINT_NOTE,
1985
+ rows: lint.slice(0, 10)
1986
+ } };
1987
+ }
1988
+ /** Write the batch row by row, sorting each outcome into its own list. One row
1989
+ * at a time on purpose: a rejection is per row, so a bad row must not take the
1990
+ * batch with it. */
1991
+ async function putEachRow(collection, store, write, rows, mode, deps) {
1941
1992
  const written = [];
1942
1993
  const rejected = [];
1994
+ const lint = [];
1943
1995
  for (const record of rows) {
1944
- const outcome = await putOneItem(collection, store, write, record, args.mode, deps);
1996
+ const outcome = await putOneItem(collection, store, write, record, mode, deps);
1945
1997
  if (outcome.written) written.push(outcome.written);
1946
1998
  if (outcome.rejected) rejected.push(outcome.rejected);
1999
+ if (outcome.lint) lint.push(outcome.lint);
1947
2000
  }
2001
+ return {
2002
+ written,
2003
+ rejected,
2004
+ lint
2005
+ };
2006
+ }
2007
+ async function handlePutItems(collection, args, deps) {
2008
+ const store = storeFor(collection, { workspaceRoot: deps.workspaceRoot });
2009
+ const { write } = store;
2010
+ if (!write) return `manageCollection: ${readOnlyRefusal(collection.slug)} (its records are the rows of '${collection.schema.dataSource?.path}'; edit that file to change the data).`;
2011
+ const rows = await resolvePutRows(args, deps);
2012
+ if (typeof rows === "string") return rows;
2013
+ const { written, rejected, lint } = await putEachRow(collection, store, write, rows, args.mode, deps);
1948
2014
  return JSON.stringify({
1949
2015
  collection: collection.slug,
1950
2016
  written,
1951
- rejected
2017
+ rejected,
2018
+ ...lintReport(lint)
1952
2019
  });
1953
2020
  }
1954
2021
  /** Delete records by id, through the store — so it works on any writable
@@ -2158,7 +2225,7 @@ async function handlePutSchema(slug, schemaArg, deps) {
2158
2225
  written: true
2159
2226
  });
2160
2227
  }
2161
- var MANAGE_COLLECTION_PROMPT = "Use `manageCollection` instead of raw Read/Write/Edit when working with a collection's records OR its schema (raw file I/O stays available as the escape hatch). Before authoring or changing a collection's `schema.json`, call `schemaDocs` to load the field/DSL reference — the default reply is the core authoring guide plus a table of contents; fetch advanced sections (actions, bells, calendar/kanban views, dataSource, storage) by passing their heading as `topic` rather than dumping `topic: \"all\"`. Then read with `getSchema` and write with `putSchema` — `putSchema` validates the whole schema before writing and returns actionable errors instead of silently failing discovery's validation. `getItems` is the only way to see computed values — `derived` fields (e.g. a portfolio's value), `toggle` projections, and `embed` records are host-computed and never present in the stored JSON files. On large collections pass `ids` and/or `fields` to keep the result small. For a question that spans collections (\"which clients have unpaid invoices?\"), start with `getOntology`: it lists every collection with its primaryKey, record count, and outbound `ref`/`embed` relations, so you know which collections to join before reading any records. `putItems` validates every row against the schema before writing (required fields, enum values, primaryKey = record id) and returns `{ written, rejected }`; fix each rejected row using its `problem` text and retry just those rows. Never include computed fields in a row you write. When the rows come from a script rather than from you (a generated schedule, an imported set, anything past a few dozen records), write them to a JSON file UNDER THE WORKSPACE and pass its absolute path as `itemsFile` instead of `items` — the host reads the file, so the rows never pass through your context. Do NOT hand-transcribe a generated file into `items`, and never drive the collection by spawning the MCP bridge yourself. To update a few fields of an existing record, use `mode: \"merge\"` with a partial row ({ id, <changed fields> }) — the default upsert replaces the WHOLE record, so a partial upsert would silently erase every optional field it omits. `deleteItems` removes records by id and returns `{ deleted, rejected }`; an id that doesn't exist comes back rejected rather than counted as deleted, so check `rejected` before reporting a deletion as done. Answer aggregation questions (counts, sums, averages, group-bys) with `queryItems` on ANY collection — on a dataSource (CSV) collection it scans the whole file (getItems is row-capped, so aggregates computed from its output can be silently wrong on large files); on a file-backed collection it aggregates the enriched records, so computed fields (derived/rollup/toggle) are queryable columns.";
2228
+ var MANAGE_COLLECTION_PROMPT = "Use `manageCollection` instead of raw Read/Write/Edit when working with a collection's records OR its schema (raw file I/O stays available as the escape hatch). Before authoring or changing a collection's `schema.json`, call `schemaDocs` to load the field/DSL reference — the default reply is the core authoring guide plus a table of contents; fetch advanced sections (actions, bells, calendar/kanban views, dataSource, storage) by passing their heading as `topic` rather than dumping `topic: \"all\"`. Then read with `getSchema` and write with `putSchema` — `putSchema` validates the whole schema before writing and returns actionable errors instead of silently failing discovery's validation. `getItems` is the only way to see computed values — `derived` fields (e.g. a portfolio's value), `toggle` projections, and `embed` records are host-computed and never present in the stored JSON files. On large collections pass `ids` and/or `fields` to keep the result small. For a question that spans collections (\"which clients have unpaid invoices?\"), start with `getOntology`: it lists every collection with its primaryKey, record count, and outbound `ref`/`embed` relations, so you know which collections to join before reading any records. `putItems` GATES every row on what would make the record unopenable — required fields, enum values, primaryKey = record id — and returns `{ written, rejected }`; fix each rejected row using its `problem` text and retry just those rows. Never include computed fields in a row you write. That gate does NOT check the SHAPE of a value: a `datetime` written as an instant (`2026-08-17T15:00:00.000Z`, what `toISOString()` produces) rather than as a wall clock, a `number` holding something that is not a number at all, a `date` that is not a real day are all WRITTEN, and come back in a `lint` block beside `written`. Read it — a full `getItems` listing warns about the same rows and publishing a shared app REFUSES them, so a silent `lint` is the only proof the values are right. Before generating a large set, read the exact stored form of each type in the `Field types` section of `schemaDocs` (`topic: \"Field types\"` fetches just that one), then write ONE batch and check that `lint` is absent. `datetime` in particular is a local wall clock (`YYYY-MM-DDTHH:MM`, no timezone suffix), so `new Date(...).toISOString()` is wrong twice over — the suffix, and the hours the conversion moved. The one `Z`-suffixed datetime the strict tier accepts is a shared app's server-stamped instant, written by the SERVER with nine fractional digits; you never produce that value. When the rows come from a script rather than from you (a generated schedule, an imported set, anything past a few dozen records), write them to a JSON file UNDER THE WORKSPACE and pass its absolute path as `itemsFile` instead of `items` — the host reads the file, so the rows never pass through your context. Do NOT hand-transcribe a generated file into `items`, and never drive the collection by spawning the MCP bridge yourself. To update a few fields of an existing record, use `mode: \"merge\"` with a partial row ({ id, <changed fields> }) — the default upsert replaces the WHOLE record, so a partial upsert would silently erase every optional field it omits. `deleteItems` removes records by id and returns `{ deleted, rejected }`; an id that doesn't exist comes back rejected rather than counted as deleted, so check `rejected` before reporting a deletion as done. Answer aggregation questions (counts, sums, averages, group-bys) with `queryItems` on ANY collection — on a dataSource (CSV) collection it scans the whole file (getItems is row-capped, so aggregates computed from its output can be silently wrong on large files); on a file-backed collection it aggregates the enriched records, so computed fields (derived/rollup/toggle) are queryable columns.";
2162
2229
  /** Validate getItems' optional `ids`/`fields` args, then delegate. */
2163
2230
  async function dispatchGetItems(collection, args, deps) {
2164
2231
  const ids = optionalStringArray(args.ids, "ids");
@@ -2214,7 +2281,7 @@ async function dispatchManageCollection(deps, args) {
2214
2281
  }
2215
2282
  var MANAGE_COLLECTION_DEFINITION = {
2216
2283
  name: "manageCollection",
2217
- description: "Read and write a schema-driven collection through the host — both its records and its structure. getItems returns records WITH computed values (derived formulas, toggles, embeds) the stored JSON files don't contain; putItems validates each row against the schema before writing; deleteItems removes records by id. getOntology maps the whole workspace: every collection with its record count and outbound ref/embed relations — call it first for cross-collection questions. schemaDocs returns the collection-authoring reference — the core guide plus a table of contents by default; pass `topic` for a specific section. getSchema/putSchema read and validate-then-write the collection's schema.json. Prefer it over raw file I/O on collections.",
2284
+ description: "Read and write a schema-driven collection through the host — both its records and its structure. getItems returns records WITH computed values (derived formulas, toggles, embeds) the stored JSON files don't contain; putItems gates each row on required/enum/primaryKey before writing and lints the value shapes it does not gate; deleteItems removes records by id. getOntology maps the whole workspace: every collection with its record count and outbound ref/embed relations — call it first for cross-collection questions. schemaDocs returns the collection-authoring reference — the core guide plus a table of contents by default; pass `topic` for a specific section. getSchema/putSchema read and validate-then-write the collection's schema.json. Prefer it over raw file I/O on collections.",
2218
2285
  inputSchema: {
2219
2286
  type: "object",
2220
2287
  properties: {
@@ -2289,6 +2356,6 @@ function makeManageCollectionTool(deps = {}) {
2289
2356
  };
2290
2357
  }
2291
2358
  //#endregion
2292
- export { runCollectionQuery as A, firstMutateParamProblem as C, validateRecordObject as D, validateCollectionRecords as E, promptPathsFor as F, readCustomViewHtml as I, readCustomViewI18n as L, runQueryOverRows as M, buildActionSeedPrompt as N, compileRecordZ as O, buildCollectionActionSeedPrompt as P, readSkillTemplate as R, applyMutateAction as S, STORE_UNREADABLE as T, successorId as _, makeManageCollectionTool as a, buildWorkspaceOntology as b, deleteCollectionRefusalMessage as c, daysInMonth as d, formatCivil as f, resolveEvery as g, parseCivil as h, MAX_UNSELECTIVE_ITEMS as i, enrichItems as j, recordFieldProblem as k, advanceTriggerDate as l, maybeSpawnSuccessor as m, MAX_PUT_ITEMS as n, deleteCustomView as o, isTriggerDue as p, MAX_SCHEMA_ISSUES as r, deleteCollection as s, MAX_ITEMS_FILE_BYTES as t, computeSuccessor as u, ONE_SECOND_MS as v, MAX_RECORD_ISSUES as w, schemaRelations as x, computeCollectionIcon as y };
2359
+ export { recordFieldProblem as A, applyMutateAction as C, validateCollectionRecords as D, STORE_UNREADABLE as E, buildCollectionActionSeedPrompt as F, promptPathsFor as I, readCustomViewHtml as L, enrichItems as M, runQueryOverRows as N, validateRecordObject as O, buildActionSeedPrompt as P, readCustomViewI18n as R, schemaRelations as S, MAX_RECORD_ISSUES as T, resolveEvery as _, MAX_UNSELECTIVE_ITEMS as a, computeCollectionIcon as b, deleteCollection as c, computeSuccessor as d, daysInMonth as f, parseCivil as g, maybeSpawnSuccessor as h, MAX_SCHEMA_ISSUES as i, runCollectionQuery as j, compileRecordZ as k, deleteCollectionRefusalMessage as l, isTriggerDue as m, MAX_PUT_ITEMS as n, makeManageCollectionTool as o, formatCivil as p, MAX_PUT_LINT as r, deleteCustomView as s, MAX_ITEMS_FILE_BYTES as t, advanceTriggerDate as u, successorId as v, firstMutateParamProblem as w, buildWorkspaceOntology as x, ONE_SECOND_MS as y, readSkillTemplate as z };
2293
2360
 
2294
- //# sourceMappingURL=server-C-Ngd3g2.js.map
2361
+ //# sourceMappingURL=server-BvXO49Vj.js.map