@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.
- package/assets/helps/collection-skills.md +17 -6
- package/assets/helps/custom-view-remote.md +28 -11
- package/assets/helps/error-recovery.md +73 -3
- package/dist/collection/registry/server/index.cjs +1 -1
- package/dist/collection/registry/server/index.js +1 -1
- package/dist/collection/server/index.cjs +2 -1
- package/dist/collection/server/index.js +2 -2
- package/dist/collection/server/manageTool.d.ts +24 -0
- package/dist/collection-watchers/index.cjs +1 -1
- package/dist/collection-watchers/index.js +1 -1
- package/dist/feeds/server/index.cjs +1 -1
- package/dist/feeds/server/index.js +1 -1
- package/dist/{graph-BZJxiv1E.cjs → graph-CURotEkH.cjs} +88 -37
- package/dist/graph-CURotEkH.cjs.map +1 -0
- package/dist/{graph-Ce7xsDTt.js → graph-DBxSOx3v.js} +77 -38
- package/dist/graph-DBxSOx3v.js.map +1 -0
- package/dist/{server-C-Ngd3g2.js → server-BvXO49Vj.js} +80 -13
- package/dist/{server-C-Ngd3g2.js.map → server-BvXO49Vj.js.map} +1 -1
- package/dist/{server-DZkN0GGh.cjs → server-CF1qllGV.cjs} +85 -12
- package/dist/{server-DZkN0GGh.cjs.map → server-CF1qllGV.cjs.map} +1 -1
- package/dist/{slug-CdN-pQX1.js → slug-BpjumQfe.js} +26 -5
- package/dist/{slug-CdN-pQX1.js.map → slug-BpjumQfe.js.map} +1 -1
- package/dist/{slug-DbwORN9z.cjs → slug-JxgB0ru9.cjs} +31 -4
- package/dist/{slug-DbwORN9z.cjs.map → slug-JxgB0ru9.cjs.map} +1 -1
- package/dist/wiki/graph.d.ts +0 -5
- package/dist/wiki/index.cjs +5 -2
- package/dist/wiki/index.d.ts +3 -2
- package/dist/wiki/index.js +3 -3
- package/dist/wiki/lint.d.ts +8 -9
- package/dist/wiki/paths.cjs +1 -1
- package/dist/wiki/paths.js +1 -1
- package/dist/wiki/resolve.d.ts +24 -0
- package/dist/wiki/server/engine.d.ts +12 -4
- package/dist/wiki/server/index.cjs +46 -34
- package/dist/wiki/server/index.cjs.map +1 -1
- package/dist/wiki/server/index.js +46 -34
- package/dist/wiki/server/index.js.map +1 -1
- package/dist/wiki/slug.d.ts +14 -3
- package/package.json +1 -1
- package/dist/graph-BZJxiv1E.cjs.map +0 -1
- package/dist/graph-Ce7xsDTt.js.map +0 -1
|
@@ -193,12 +193,23 @@ skipped, never crashes the host):
|
|
|
193
193
|
|
|
194
194
|
Every field spec needs a `type` and a `label`. Extra keys by type:
|
|
195
195
|
|
|
196
|
-
- **`datetime`** — no extra keys. Stored as a `YYYY-MM-DDTHH:MM` string
|
|
197
|
-
edited with a native date+time picker.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
`
|
|
196
|
+
- **`datetime`** — no extra keys. Stored as a `YYYY-MM-DDTHH:MM` string (seconds
|
|
197
|
+
optional) and edited with a native date+time picker. It is a **local wall
|
|
198
|
+
clock, not an instant**: no `Z`, no `+09:00` offset. `08:00` means eight in the
|
|
199
|
+
morning wherever the records are read, which is what a schedule means and what
|
|
200
|
+
the calendar can place. So a generated value must be FORMATTED, never
|
|
201
|
+
converted — `new Date(...).toISOString()` is wrong twice over: it appends the
|
|
202
|
+
`Z` (`putItems` writes the row but reports it in `lint`, a full `getItems`
|
|
203
|
+
listing warns about it, and a shared app's publish refuses it outright), and
|
|
204
|
+
it shifts the hours by the generating machine's offset, so a Tokyo 08:00 written on a US laptop reads back as the afternoon.
|
|
205
|
+
Build the string from the parts you already have (`${date}T${hh}:${mm}`). The
|
|
206
|
+
one `Z`-suffixed datetime that is legal is a shared app's server-stamped
|
|
207
|
+
field — nine fractional digits, written by the SERVER, never by you. Anything
|
|
208
|
+
else ending in `Z` (including `toISOString()`'s three digits) is linted.
|
|
209
|
+
Use it (as `calendarField` / `calendarEndField`) when an event has a real
|
|
210
|
+
start/end clock — the calendar's day view then draws each record as a
|
|
211
|
+
proportional time block. For the common "date column + separate time column"
|
|
212
|
+
shape, keep `date` and point `calendarTimeField` at the time string instead.
|
|
202
213
|
- **`enum`** — `values: ["draft", "sent", "paid"]` (non-empty strings). Renders
|
|
203
214
|
a `<select>`; stored as a plain string. Optional `default: "draft"` pre-fills
|
|
204
215
|
a NEW record — the Add form opens on it, and a `putItems` row in `create` mode
|
|
@@ -85,6 +85,13 @@ const page = await window.__MC_VIEW.getItems({
|
|
|
85
85
|
size-capped channel; you can never assume one call returns everything.
|
|
86
86
|
Render the first page, then offer a **"Load more"** affordance while
|
|
87
87
|
`items.length < total` (see the example).
|
|
88
|
+
- **A page may come back SHORTER than the `limit` you asked for.** When inlined
|
|
89
|
+
thumbnails would overflow the channel budget, the host ends the page early and
|
|
90
|
+
the remaining records arrive on the next one — with their images intact. So
|
|
91
|
+
advance by what you RECEIVED, never by the limit you requested:
|
|
92
|
+
`offset: loaded.length` (both examples below do this), and stop on
|
|
93
|
+
`loaded.length >= total`. `total` always counts every record behind the view,
|
|
94
|
+
never just this page. A short page is normal, not an error.
|
|
88
95
|
- **Always pass `fields`.** The projection keeps pages small (the primary key
|
|
89
96
|
is always included). List the columns your view actually renders — including
|
|
90
97
|
any `image`-type field you want inlined (see **Displaying images**).
|
|
@@ -180,19 +187,29 @@ const page = await window.__MC_VIEW.getItems({ offset: 0, limit: 20, fields: ["t
|
|
|
180
187
|
- **Only `image`-type fields listed in `imageFields` are inlined.** A non-image
|
|
181
188
|
field name is ignored; a field the page's `fields` projection dropped is not
|
|
182
189
|
inlined (so paging without the image column costs nothing).
|
|
183
|
-
- **Thumbnails are downscaled**, longest edge `imageMaxEdge` (default 512).
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
- **Thumbnails are downscaled**, longest edge `imageMaxEdge` (default 512). Every
|
|
191
|
+
inlined image travels inside the size-capped page, so the host enforces a
|
|
192
|
+
**per-page byte budget** — when the next thumbnail would overflow it, the page
|
|
193
|
+
**ends there** and those records arrive on the following page with their images.
|
|
194
|
+
You get a short page, never a page with a hole in it. A smaller `imageMaxEdge`
|
|
195
|
+
fits more thumbnails per page, so it costs FEWER round-trips; a smaller `limit`
|
|
196
|
+
costs more. Neither changes whether an image eventually appears — that is no
|
|
197
|
+
longer something the budget can take away.
|
|
198
|
+
For scale: a real photo at `imageMaxEdge: 384` inlines to roughly 15 KB, so a
|
|
199
|
+
page holds on the order of 50 of them before it is cut.
|
|
200
|
+
- **A field may still come back as a path, not a `data:` URL** — for a missing
|
|
201
|
+
file or an undecodable source, which a smaller page cannot fix; and, on the
|
|
202
|
+
FIRST item of a page only, for an image so large it does not fit an empty
|
|
203
|
+
budget (deferring that one would return an empty page and stall paging).
|
|
204
|
+
Handle both: `onerror` a placeholder, or check for the `data:` prefix before
|
|
205
|
+
rendering.
|
|
192
206
|
- **Cost**: inlined thumbnails are the one thing that grows a page's bytes;
|
|
193
207
|
they're opt-in per view and per projection precisely so a view pays only for the
|
|
194
|
-
images it shows. The preview's caption reports `N images (M
|
|
195
|
-
|
|
208
|
+
images it shows. The preview's caption reports `N images (M placeholders)` —
|
|
209
|
+
`M` counts every image the page hands back as a path, which is what you will
|
|
210
|
+
see as a placeholder. An image that merely did not fit is deferred to the next
|
|
211
|
+
page and is NOT counted; what remains is the unresolvable ones, plus the rare
|
|
212
|
+
first-item-too-large case above.
|
|
196
213
|
|
|
197
214
|
### Starting a chat — `startChat`
|
|
198
215
|
|
|
@@ -859,7 +859,9 @@ so read these three before forming a theory:
|
|
|
859
859
|
|---|---|
|
|
860
860
|
| `spawning agent … broker=tsx` | This install is on the SLOW path: the bundle is missing, so the broker is transcoded from source on every spawn (seconds to tens of seconds over a Windows/macOS bind mount). A `broker bundle missing` warn accompanies it once per process. |
|
|
861
861
|
| `[mcp] broker ready bootMs=… initializeMs=…` | The broker DID connect, and how long it took. `broker cold boot is slow` replaces it past 5 s. |
|
|
862
|
-
| `brokerEverReady=false` on the retry warn | No beacon
|
|
862
|
+
| `brokerEverReady=false reason=never-ready` on the retry warn | No beacon arrived for that chat, and the host kept looking until the beacon's own delivery budget was spent — the broker did not come up at all. The turn is NOT replayed: a replay would sit out another full connect wait and end in the same error. |
|
|
863
|
+
| `reason=ready-during-wait` on the retry warn | The beacon arrived while the host waited, so the broker lost the race by a moment and IS connected now. The turn is replayed, which is what fixes this one. |
|
|
864
|
+
| `brokerEverStarted=` on the `MCP tools were unavailable` warn | Whether the broker PROCESS ever existed, which is a different question from whether it answered. `true` with `brokerEverReady=false` means it launched and never finished booting — the boot is the problem (the mount, the `tsx` path). `false` means it never launched — the spawn is the problem. Diagnostic only, and not authenticated: under Docker everything needed to forge it sits in the per-session MCP config inside the workspace mount, so read it as evidence about a healthy install rather than as proof against a hostile one. |
|
|
863
865
|
|
|
864
866
|
### Fix
|
|
865
867
|
|
|
@@ -868,8 +870,23 @@ so read these three before forming a theory:
|
|
|
868
870
|
published launchers have shipped the bundle since 1.9.0.
|
|
869
871
|
- `broker ready` present with a small `initializeMs`, failing anyway → it is the startup race,
|
|
870
872
|
not the boot. See the scheduled-run section above.
|
|
871
|
-
- `
|
|
872
|
-
|
|
873
|
+
- `brokerEverStarted=false` → the broker process never launched. That is the permanent load
|
|
874
|
+
failure; check the `Cannot find module` section. This one surfaces fast on its own: the CLI
|
|
875
|
+
notices a broker it could not spawn within seconds rather than waiting out its connect ceiling,
|
|
876
|
+
so a turn that fails QUICKLY in this shape is the diagnosis, not a second symptom.
|
|
877
|
+
- `brokerEverStarted=true` with `brokerEverReady=false` → the process launched and the BOOT is
|
|
878
|
+
what did not finish. Two sub-cases, and the elapsed time separates them: a boot still running
|
|
879
|
+
when the CLI gave up eats the whole connect wait (fix the boot — the `broker=tsx` bullet above
|
|
880
|
+
— rather than waiting longer), while a boot that CRASHED fails as fast as a missing binary. The
|
|
881
|
+
crash reason is not in this log by construction: Claude CLI owns the broker's stderr, so read
|
|
882
|
+
the `Cannot find module` section for what to check.
|
|
883
|
+
- Either way the turn is NOT replayed, so the cost is ONE connect wait rather than two. The first
|
|
884
|
+
wait still happens — nothing can tell the broker is not coming until the CLI gives up on it —
|
|
885
|
+
and the host then keeps looking for the beacon a few seconds longer before concluding it never
|
|
886
|
+
will, so that a beacon merely still in flight is not mistaken for a broker that never answered.
|
|
887
|
+
- Do NOT read a missing `broker ready` line on its own as "the broker never started" — before
|
|
888
|
+
`brokerEverStarted` existed, that inference was the only one available and it is wrong for
|
|
889
|
+
exactly the case that costs the most time.
|
|
873
890
|
- Old or unrecorded `claudeCodeVersion` alongside any of these → rule out the frozen CLI first.
|
|
874
891
|
|
|
875
892
|
---
|
|
@@ -1071,6 +1088,59 @@ Reading the refusal you got:
|
|
|
1071
1088
|
Do NOT spawn the MCP bridge yourself. A hand-written JSON-RPC client fails
|
|
1072
1089
|
invisibly and can leave a partially written collection that nothing reports.
|
|
1073
1090
|
|
|
1091
|
+
## `putItems` came back with a `lint` block — the rows were written, the values are the wrong shape
|
|
1092
|
+
|
|
1093
|
+
### Symptoms
|
|
1094
|
+
|
|
1095
|
+
- A `putItems` result carries `lint` beside `written`: `{ total, note, rows: [{ id, problem }] }`.
|
|
1096
|
+
- The problem text reads like a type complaint rather than a missing field —
|
|
1097
|
+
`not a YYYY-MM-DDTHH:MM datetime`, `is not numeric`, `is not a real YYYY-MM-DD date`,
|
|
1098
|
+
`is not a boolean`.
|
|
1099
|
+
- `rejected` is empty, and the records are on disk, so nothing looks broken yet.
|
|
1100
|
+
|
|
1101
|
+
### Cause
|
|
1102
|
+
|
|
1103
|
+
The write gate refuses only what makes a record unopenable: required fields,
|
|
1104
|
+
enum values, `primaryKey` = record id. The SHAPE of a value is checked by a
|
|
1105
|
+
second, report-only tier — refusing there would make a collection whose older
|
|
1106
|
+
rows predate the typed rules unwritable. So the row lands, and `lint` is the
|
|
1107
|
+
report.
|
|
1108
|
+
|
|
1109
|
+
It is not cosmetic. Every later reader runs the strict tier: a full `getItems`
|
|
1110
|
+
listing (and a selective one that missed an id) returns the same finding as a
|
|
1111
|
+
`warning` beside the rows, and publishing a shared app REFUSES these rows
|
|
1112
|
+
outright. A batch generated by a script is wrong in every row the same way, so
|
|
1113
|
+
this is the moment it costs the least to fix.
|
|
1114
|
+
|
|
1115
|
+
### Fix
|
|
1116
|
+
|
|
1117
|
+
Correct the generator, then rewrite the rows (`mode: "upsert"`, same ids) and
|
|
1118
|
+
check that `lint` is gone. `lint.total` is the real count; `lint.rows` shows the
|
|
1119
|
+
first ten.
|
|
1120
|
+
|
|
1121
|
+
The one that recurs is `datetime`. It stores a **local wall clock** —
|
|
1122
|
+
`YYYY-MM-DDTHH:MM`, seconds optional, **no `Z` and no offset** — because `08:00`
|
|
1123
|
+
in a schedule means eight in the morning wherever it is read, and that is what
|
|
1124
|
+
the calendar can place. So format the string, never convert it:
|
|
1125
|
+
|
|
1126
|
+
```js
|
|
1127
|
+
// WRONG — appends `Z` and shifts the hours by the generating machine's offset
|
|
1128
|
+
{ startAt: new Date(`${day}T08:00`).toISOString() } // "2026-08-17T15:00:00.000Z"
|
|
1129
|
+
|
|
1130
|
+
// RIGHT — the clock you meant, written as the clock
|
|
1131
|
+
{ startAt: `${day}T08:00` } // "2026-08-17T08:00"
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
The conversion is the more expensive half: had the format passed, a Tokyo court's
|
|
1135
|
+
08:00 seeded on a US laptop would have published every slot seven hours out
|
|
1136
|
+
(eight, on the dates the other side of a DST change) with nothing to point at.
|
|
1137
|
+
The one `Z`-suffixed datetime the strict tier accepts is a shared app's
|
|
1138
|
+
server-stamped instant — nine fractional digits
|
|
1139
|
+
(`2026-08-15T01:45:54.605987654Z`), written by the server, not by you.
|
|
1140
|
+
`toISOString()`'s three digits are not that shape and are linted.
|
|
1141
|
+
|
|
1142
|
+
Field-by-field stored forms: `schemaDocs` with `topic: "Field types"`.
|
|
1143
|
+
|
|
1074
1144
|
## A tool your own instructions describe returns "No such tool available"
|
|
1075
1145
|
|
|
1076
1146
|
### Symptoms
|
|
@@ -6,7 +6,7 @@ const require_relPath = require("../../../relPath-CTAjGdCL.cjs");
|
|
|
6
6
|
const require_utils_fetch = require("../../../utils/fetch.cjs");
|
|
7
7
|
const require_discovery = require("../../../discovery-Dl9nRa-m.cjs");
|
|
8
8
|
const require_templatePath = require("../../../templatePath-27vUZowm.cjs");
|
|
9
|
-
const require_server = require("../../../server-
|
|
9
|
+
const require_server = require("../../../server-CF1qllGV.cjs");
|
|
10
10
|
const require_skill_bridge_index = require("../../../skill-bridge/index.cjs");
|
|
11
11
|
const require_types = require("../../../types-CNqkLT4M.cjs");
|
|
12
12
|
let node_fs = require("node:fs");
|
|
@@ -4,7 +4,7 @@ import { n as toPosixRelPath } from "../../../relPath-DW8MC8VO.js";
|
|
|
4
4
|
import { fetchWithTimeout } from "../../../utils/fetch.js";
|
|
5
5
|
import { $ as collectionsRegistriesConfigPath, G as safeRecordId, ct as projectSkillsDir, mt as skillsStagingDir, nt as getWorkspaceRoot, ot as log, r as loadCollection, s as CollectionSchemaZ, t as acceptParsedSchema } from "../../../discovery-P64fgtuD.js";
|
|
6
6
|
import { t as isSafeActionTemplatePath } from "../../../templatePath-k_WNbL_Q.js";
|
|
7
|
-
import {
|
|
7
|
+
import { y as ONE_SECOND_MS } from "../../../server-BvXO49Vj.js";
|
|
8
8
|
import { claudeSkillDir, dataSkillDir, mirrorSkillWrite } from "../../../skill-bridge/index.js";
|
|
9
9
|
import { n as parseRegistryIndex, r as isRecord, t as OFFICIAL_REGISTRY_NAME } from "../../../types-BKVZrtyc.js";
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
@@ -3,7 +3,7 @@ const require_root = require("../../root-rPH6FGDT.cjs");
|
|
|
3
3
|
const require_itemId = require("../../itemId-CEQdK01z.cjs");
|
|
4
4
|
const require_discovery = require("../../discovery-Dl9nRa-m.cjs");
|
|
5
5
|
const require_templatePath = require("../../templatePath-27vUZowm.cjs");
|
|
6
|
-
const require_server = require("../../server-
|
|
6
|
+
const require_server = require("../../server-CF1qllGV.cjs");
|
|
7
7
|
exports.APP_MANIFEST_FILE = require_discovery.APP_MANIFEST_FILE;
|
|
8
8
|
exports.BackendUnavailableError = require_discovery.BackendUnavailableError;
|
|
9
9
|
exports.COLLECTION_ROOT_REQUIRED = require_discovery.COLLECTION_ROOT_REQUIRED;
|
|
@@ -14,6 +14,7 @@ exports.DEFAULT_QUERY_ROWS = require_discovery.DEFAULT_QUERY_ROWS;
|
|
|
14
14
|
exports.MAX_CSV_ROWS = require_discovery.MAX_CSV_ROWS;
|
|
15
15
|
exports.MAX_ITEMS_FILE_BYTES = require_server.MAX_ITEMS_FILE_BYTES;
|
|
16
16
|
exports.MAX_PUT_ITEMS = require_server.MAX_PUT_ITEMS;
|
|
17
|
+
exports.MAX_PUT_LINT = require_server.MAX_PUT_LINT;
|
|
17
18
|
exports.MAX_QUERY_ROWS = require_discovery.MAX_QUERY_ROWS;
|
|
18
19
|
exports.MAX_RECORD_ISSUES = require_server.MAX_RECORD_ISSUES;
|
|
19
20
|
exports.MAX_SCHEMA_ISSUES = require_server.MAX_SCHEMA_ISSUES;
|
|
@@ -2,5 +2,5 @@ import { t as canonicalRoot } from "../../root-BMroU_mB.js";
|
|
|
2
2
|
import { B as COMPUTED_TYPES } from "../../itemId-BY2YPBkT.js";
|
|
3
3
|
import { A as readItem, B as SCHEMA_FILE, C as CollectionQueryZ, D as generateItemId, E as deleteItem, F as loadAppManifest, G as safeRecordId, H as itemFilePath, I as parseAppManifest, J as isBackendUnavailable, K as safeSlugName, L as sharedItemsPath, M as writeItem, N as APP_MANIFEST_FILE, O as isRegularFile, P as appManifestReason, Q as collectionChangePayload, R as pageFromFullRead, S as compileJsonlQuery, T as MAX_QUERY_ROWS, U as resolveDataDir, V as isContainedInRoot, W as resolveTemplatePath, Y as COLLECTION_ROOT_REQUIRED, Z as collectionChangeKey, _ as dedupeByRecordId, a as toDetail, at as localCollectionKey, d as storeFor, dt as setFirestoreAccessor, et as configureCollectionHost, ft as setSharedCollectionsSupport, g as decodeCsvRecordId, h as csvRowToItem, i as resolvePrimaryField, j as resolveCreateItemId, k as listItems, l as collectionWritable, lt as publishCollectionChange, n as discoverCollections, nt as getWorkspaceRoot, o as toSummary, ot as log, p as MAX_CSV_ROWS, pt as sharedCollectionChangePayload, q as BackendUnavailableError, r as loadCollection, rt as hostSupportsSharedCollections, s as CollectionSchemaZ, st as peekWorkspaceRoot, t as acceptParsedSchema, tt as firestoreHandle, u as readOnlyRefusal, ut as setCollectionChangePublisher, v as encodeCsvRecordId, w as DEFAULT_QUERY_ROWS, x as compileCsvQuery, y as normalizeCsvValue, z as projectItemFields } from "../../discovery-P64fgtuD.js";
|
|
4
4
|
import { i as isSafeTemplatePath, n as isSafeCustomViewI18nPath, r as isSafeCustomViewPath, t as isSafeActionTemplatePath } from "../../templatePath-k_WNbL_Q.js";
|
|
5
|
-
import { A as
|
|
6
|
-
export { APP_MANIFEST_FILE, BackendUnavailableError, COLLECTION_ROOT_REQUIRED, COMPUTED_TYPES, CollectionQueryZ, CollectionSchemaZ, DEFAULT_QUERY_ROWS, MAX_CSV_ROWS, MAX_ITEMS_FILE_BYTES, MAX_PUT_ITEMS, MAX_QUERY_ROWS, MAX_RECORD_ISSUES, MAX_SCHEMA_ISSUES, MAX_UNSELECTIVE_ITEMS, SCHEMA_FILE, STORE_UNREADABLE, acceptParsedSchema, advanceTriggerDate, appManifestReason, applyMutateAction, buildActionSeedPrompt, buildCollectionActionSeedPrompt, buildWorkspaceOntology, canonicalRoot, collectionChangeKey, collectionChangePayload, collectionWritable, compileCsvQuery, compileJsonlQuery, compileRecordZ, computeCollectionIcon, computeSuccessor, configureCollectionHost, csvRowToItem, daysInMonth, decodeCsvRecordId, dedupeByRecordId, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, encodeCsvRecordId, enrichItems, firestoreHandle, firstMutateParamProblem, formatCivil, generateItemId, getWorkspaceRoot, hostSupportsSharedCollections, isBackendUnavailable, isContainedInRoot, isRegularFile, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadAppManifest, loadCollection, localCollectionKey, log, makeManageCollectionTool, maybeSpawnSuccessor, normalizeCsvValue, pageFromFullRead, parseAppManifest, parseCivil, peekWorkspaceRoot, projectItemFields, promptPathsFor, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readOnlyRefusal, readSkillTemplate, recordFieldProblem, resolveCreateItemId, resolveDataDir, resolveEvery, resolvePrimaryField, resolveTemplatePath, runCollectionQuery, runQueryOverRows, safeRecordId, safeSlugName, schemaRelations, setCollectionChangePublisher, setFirestoreAccessor, setSharedCollectionsSupport, sharedCollectionChangePayload, sharedItemsPath, storeFor, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
|
|
5
|
+
import { A as recordFieldProblem, C as applyMutateAction, D as validateCollectionRecords, E as STORE_UNREADABLE, F as buildCollectionActionSeedPrompt, I as promptPathsFor, L as readCustomViewHtml, M as enrichItems, N as runQueryOverRows, O as validateRecordObject, P as buildActionSeedPrompt, R as readCustomViewI18n, S as schemaRelations, T as MAX_RECORD_ISSUES, _ as resolveEvery, a as MAX_UNSELECTIVE_ITEMS, b as computeCollectionIcon, c as deleteCollection, d as computeSuccessor, f as daysInMonth, g as parseCivil, h as maybeSpawnSuccessor, i as MAX_SCHEMA_ISSUES, j as runCollectionQuery, k as compileRecordZ, l as deleteCollectionRefusalMessage, m as isTriggerDue, n as MAX_PUT_ITEMS, o as makeManageCollectionTool, p as formatCivil, r as MAX_PUT_LINT, s as deleteCustomView, t as MAX_ITEMS_FILE_BYTES, u as advanceTriggerDate, v as successorId, w as firstMutateParamProblem, x as buildWorkspaceOntology, z as readSkillTemplate } from "../../server-BvXO49Vj.js";
|
|
6
|
+
export { APP_MANIFEST_FILE, BackendUnavailableError, COLLECTION_ROOT_REQUIRED, COMPUTED_TYPES, CollectionQueryZ, CollectionSchemaZ, DEFAULT_QUERY_ROWS, MAX_CSV_ROWS, MAX_ITEMS_FILE_BYTES, MAX_PUT_ITEMS, MAX_PUT_LINT, MAX_QUERY_ROWS, MAX_RECORD_ISSUES, MAX_SCHEMA_ISSUES, MAX_UNSELECTIVE_ITEMS, SCHEMA_FILE, STORE_UNREADABLE, acceptParsedSchema, advanceTriggerDate, appManifestReason, applyMutateAction, buildActionSeedPrompt, buildCollectionActionSeedPrompt, buildWorkspaceOntology, canonicalRoot, collectionChangeKey, collectionChangePayload, collectionWritable, compileCsvQuery, compileJsonlQuery, compileRecordZ, computeCollectionIcon, computeSuccessor, configureCollectionHost, csvRowToItem, daysInMonth, decodeCsvRecordId, dedupeByRecordId, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, encodeCsvRecordId, enrichItems, firestoreHandle, firstMutateParamProblem, formatCivil, generateItemId, getWorkspaceRoot, hostSupportsSharedCollections, isBackendUnavailable, isContainedInRoot, isRegularFile, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadAppManifest, loadCollection, localCollectionKey, log, makeManageCollectionTool, maybeSpawnSuccessor, normalizeCsvValue, pageFromFullRead, parseAppManifest, parseCivil, peekWorkspaceRoot, projectItemFields, promptPathsFor, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readOnlyRefusal, readSkillTemplate, recordFieldProblem, resolveCreateItemId, resolveDataDir, resolveEvery, resolvePrimaryField, resolveTemplatePath, runCollectionQuery, runQueryOverRows, safeRecordId, safeSlugName, schemaRelations, setCollectionChangePublisher, setFirestoreAccessor, setSharedCollectionsSupport, sharedCollectionChangePayload, sharedItemsPath, storeFor, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
|
|
@@ -11,6 +11,11 @@ export declare const MAX_SCHEMA_ISSUES = 20;
|
|
|
11
11
|
* write that reported success would leave a half-filled collection nobody
|
|
12
12
|
* knows is half-filled. */
|
|
13
13
|
export declare const MAX_PUT_ITEMS = 1000;
|
|
14
|
+
/** Cap the strict-tier findings one putItems call reports back. The rows were
|
|
15
|
+
* WRITTEN, so this is a report and not a refusal — and a generated batch is
|
|
16
|
+
* wrong the same way in every row, which means ten samples plus the total say
|
|
17
|
+
* everything a thousand would. */
|
|
18
|
+
export declare const MAX_PUT_LINT = 10;
|
|
14
19
|
/** Refuse an `itemsFile` larger than this, from `stat` and before any read.
|
|
15
20
|
* The row cap alone cannot bound the work: the file has to be read and parsed
|
|
16
21
|
* WHOLE before there are rows to count, so a huge blob is paid for in full
|
|
@@ -70,6 +75,25 @@ export type ManageCollectionDeps = DiscoveryOptions & {
|
|
|
70
75
|
* the sandbox is off — the prefix simply never appears. */
|
|
71
76
|
sandboxWorkspacePath?: string | undefined;
|
|
72
77
|
};
|
|
78
|
+
/** One row the call did not take, or took and flagged: the id it was about and
|
|
79
|
+
* what to fix. Exported alongside `PutItemsLint`, whose `rows` are these. */
|
|
80
|
+
export interface RejectedRow {
|
|
81
|
+
id: string;
|
|
82
|
+
problem: string;
|
|
83
|
+
}
|
|
84
|
+
/** The `lint` block of a putItems result — the strict-tier findings on rows the
|
|
85
|
+
* call WROTE. Exported because it is part of the tool's answer, not an
|
|
86
|
+
* internal: a caller (or a test) that re-declares the shape inline is a copy
|
|
87
|
+
* that stops matching the day the block gains a field. */
|
|
88
|
+
export interface PutItemsLint {
|
|
89
|
+
/** Every flagged row, not just the shown ones — a capped `rows` must never be
|
|
90
|
+
* readable as a total. */
|
|
91
|
+
total: number;
|
|
92
|
+
/** What the reader has to know to act on findings sitting beside `written`. */
|
|
93
|
+
note: string;
|
|
94
|
+
/** The first `MAX_PUT_LINT` findings. */
|
|
95
|
+
rows: RejectedRow[];
|
|
96
|
+
}
|
|
73
97
|
export declare function makeManageCollectionTool(deps?: ManageCollectionDeps): {
|
|
74
98
|
definition: {
|
|
75
99
|
name: string;
|
|
@@ -4,7 +4,7 @@ const require_itemId = require("../itemId-CEQdK01z.cjs");
|
|
|
4
4
|
const require_promptSafety = require("../promptSafety-C0BWDsfT.cjs");
|
|
5
5
|
require("../collection/index.cjs");
|
|
6
6
|
const require_discovery = require("../discovery-Dl9nRa-m.cjs");
|
|
7
|
-
const require_server = require("../server-
|
|
7
|
+
const require_server = require("../server-CF1qllGV.cjs");
|
|
8
8
|
const require_notifier = require("../notifier-DZJj0sOh.cjs");
|
|
9
9
|
let node_fs_promises = require("node:fs/promises");
|
|
10
10
|
//#region src/collection-watchers/config.ts
|
|
@@ -3,7 +3,7 @@ import { O as isValidCollectionName, q as fieldText } from "../itemId-BY2YPBkT.j
|
|
|
3
3
|
import { E as whenMatches, f as itemIsDone } from "../promptSafety-Bp780ezk.js";
|
|
4
4
|
import "../collection/index.js";
|
|
5
5
|
import { H as itemFilePath, Q as collectionChangePayload, d as storeFor, lt as publishCollectionChange, n as discoverCollections, pt as sharedCollectionChangePayload, r as loadCollection, st as peekWorkspaceRoot, tt as firestoreHandle } from "../discovery-P64fgtuD.js";
|
|
6
|
-
import {
|
|
6
|
+
import { h as maybeSpawnSuccessor, m as isTriggerDue } from "../server-BvXO49Vj.js";
|
|
7
7
|
import { d as publish, m as updateForPlugin, n as clear, s as listAll } from "../notifier-DhnwR82U.js";
|
|
8
8
|
import { access } from "node:fs/promises";
|
|
9
9
|
//#region src/collection-watchers/config.ts
|
|
@@ -5,7 +5,7 @@ const require_root = require("../../root-rPH6FGDT.cjs");
|
|
|
5
5
|
const require_itemId = require("../../itemId-CEQdK01z.cjs");
|
|
6
6
|
const require_project = require("../../project-C1ep9pvo.cjs");
|
|
7
7
|
const require_discovery = require("../../discovery-Dl9nRa-m.cjs");
|
|
8
|
-
const require_server = require("../../server-
|
|
8
|
+
const require_server = require("../../server-CF1qllGV.cjs");
|
|
9
9
|
const require_feeds_paths = require("../paths.cjs");
|
|
10
10
|
const require_ingestTypes = require("../../ingestTypes-CaX7_s-K.cjs");
|
|
11
11
|
const require_notifier = require("../../notifier-DZJj0sOh.cjs");
|
|
@@ -3,7 +3,7 @@ import { t as canonicalRoot } from "../../root-BMroU_mB.js";
|
|
|
3
3
|
import { H as INGEST_KINDS, V as FEED_SCHEDULES } from "../../itemId-BY2YPBkT.js";
|
|
4
4
|
import { t as mergeIntoExisting } from "../../project-B0j_Vf_Q.js";
|
|
5
5
|
import { K as safeSlugName, U as resolveDataDir, _t as createHostSlot, d as storeFor, gt as createForwardingLogger, k as listItems, n as discoverCollections } from "../../discovery-P64fgtuD.js";
|
|
6
|
-
import { F as
|
|
6
|
+
import { F as buildCollectionActionSeedPrompt, I as promptPathsFor, z as readSkillTemplate } from "../../server-BvXO49Vj.js";
|
|
7
7
|
import { FEEDS_DIR, feedDir, feedStatePath, feedsRoot, ingestStateDir, ingestStatePath } from "../paths.js";
|
|
8
8
|
import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../../ingestTypes-CIy1lHjf.js";
|
|
9
9
|
import { d as publish, n as clear } from "../../notifier-DhnwR82U.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_slug = require("./slug-
|
|
1
|
+
const require_slug = require("./slug-JxgB0ru9.cjs");
|
|
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 = require_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 require_slug.wikiPageStem(name) ?? require_slug.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) ||
|
|
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 =
|
|
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 (
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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 = require_slug.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 = require_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
|
-
|
|
261
|
-
for (const entry of entries) {
|
|
301
|
+
entries.forEach((entry) => {
|
|
262
302
|
if (!titleBySlug.has(entry.slug)) titleBySlug.set(entry.slug, entry.title);
|
|
263
|
-
|
|
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
|
|
@@ -385,6 +424,12 @@ Object.defineProperty(exports, "incomingLinks", {
|
|
|
385
424
|
return incomingLinks;
|
|
386
425
|
}
|
|
387
426
|
});
|
|
427
|
+
Object.defineProperty(exports, "matchWikiSlug", {
|
|
428
|
+
enumerable: true,
|
|
429
|
+
get: function() {
|
|
430
|
+
return matchWikiSlug;
|
|
431
|
+
}
|
|
432
|
+
});
|
|
388
433
|
Object.defineProperty(exports, "pageOutgoingSlugs", {
|
|
389
434
|
enumerable: true,
|
|
390
435
|
get: function() {
|
|
@@ -415,5 +460,11 @@ Object.defineProperty(exports, "resolveLinkTarget", {
|
|
|
415
460
|
return resolveLinkTarget;
|
|
416
461
|
}
|
|
417
462
|
});
|
|
463
|
+
Object.defineProperty(exports, "slugByIndexTitle", {
|
|
464
|
+
enumerable: true,
|
|
465
|
+
get: function() {
|
|
466
|
+
return slugByIndexTitle;
|
|
467
|
+
}
|
|
468
|
+
});
|
|
418
469
|
|
|
419
|
-
//# sourceMappingURL=graph-
|
|
470
|
+
//# sourceMappingURL=graph-CURotEkH.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-CURotEkH.cjs","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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"}
|