@dmthepm/commune 0.1.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +468 -0
  3. package/bin/commune.mjs +29 -0
  4. package/lib/cli/check.d.ts +13 -0
  5. package/lib/cli/check.js +58 -0
  6. package/lib/cli/errors.d.ts +29 -0
  7. package/lib/cli/errors.js +41 -0
  8. package/lib/cli/gate.d.ts +34 -0
  9. package/lib/cli/gate.js +165 -0
  10. package/lib/cli/main.d.ts +20 -0
  11. package/lib/cli/main.js +175 -0
  12. package/lib/cli/query.d.ts +30 -0
  13. package/lib/cli/query.js +103 -0
  14. package/lib/cli/related.d.ts +17 -0
  15. package/lib/cli/related.js +177 -0
  16. package/lib/cli/render.d.ts +20 -0
  17. package/lib/cli/render.js +32 -0
  18. package/lib/cli/root.d.ts +11 -0
  19. package/lib/cli/root.js +28 -0
  20. package/lib/cli/usage.d.ts +3 -0
  21. package/lib/cli/usage.js +46 -0
  22. package/lib/cli/version.d.ts +24 -0
  23. package/lib/cli/version.js +29 -0
  24. package/lib/integration.d.ts +24 -0
  25. package/lib/integration.js +111 -0
  26. package/lib/lib/graph.d.ts +354 -0
  27. package/lib/lib/graph.js +774 -0
  28. package/lib/markdown.d.ts +30 -0
  29. package/lib/markdown.js +24 -0
  30. package/lib/rehype-external-links.d.ts +15 -0
  31. package/lib/rehype-external-links.js +46 -0
  32. package/lib/remark-wikilinks.d.ts +25 -0
  33. package/lib/remark-wikilinks.js +108 -0
  34. package/package.json +101 -0
  35. package/src/components/Backlinks.astro +17 -0
  36. package/src/components/BacklinksScript.astro +117 -0
  37. package/src/components/Footer.astro +35 -0
  38. package/src/components/Header.astro +250 -0
  39. package/src/components/HeaderStarScript.astro +380 -0
  40. package/src/components/HomeFooterCards.astro +155 -0
  41. package/src/components/MarkdownLink.astro +36 -0
  42. package/src/components/PlausibleScript.astro +13 -0
  43. package/src/components/RelatedNotes.astro +77 -0
  44. package/src/components/SearchModal.astro +213 -0
  45. package/src/components/StarredLinksScript.astro +92 -0
  46. package/src/components/panes.ts +61 -0
  47. package/src/styles/design-system.css +157 -0
  48. package/src/styles/notes.css +85 -0
@@ -0,0 +1,354 @@
1
+ /**
2
+ * The content graph core.
3
+ *
4
+ * Every mechanism that needs to know "what content exists and where does it
5
+ * live" reads from here: the remark WikiLinks plugin, the backlinks Astro
6
+ * integration, and the search-index check. Before this module those three
7
+ * carried hand-synced copies of the same directory scan, the same visibility
8
+ * rules, and three subtly different opinions about trailing slashes.
9
+ *
10
+ * The rules this module owns:
11
+ * - which collections participate (notes, research, pages)
12
+ * - visibility (notes opt in with `visibility: public`; research and pages
13
+ * are always public)
14
+ * - canonical URLs, always with a trailing slash, matching Astro's
15
+ * directory build format
16
+ * - the title/alias lookup used to resolve `[[WikiLinks]]`
17
+ * - which link forms count as an edge, and how each one resolves
18
+ */
19
+ export type CollectionName = 'notes' | 'research' | 'pages';
20
+ /** Where each collection's markdown lives, relative to the project root. */
21
+ export declare const CONTENT_DIRS: Record<CollectionName, string>;
22
+ /** Collection scan order. Stable so derived artifacts are deterministic. */
23
+ export declare const COLLECTIONS: CollectionName[];
24
+ /** One piece of public content, as the graph sees it. */
25
+ export interface ContentEntry {
26
+ /** Collection-relative slug, e.g. `evergreen-notes`. Not unique across collections. */
27
+ slug: string;
28
+ /** Canonical site path with a trailing slash, e.g. `/notes/evergreen-notes/`. Unique. */
29
+ urlPath: string;
30
+ title: string;
31
+ collection: CollectionName;
32
+ aliases: string[];
33
+ tags: string[];
34
+ status: string;
35
+ summary?: string;
36
+ updated?: string;
37
+ /** Markdown body with frontmatter stripped. */
38
+ body: string;
39
+ /** Parsed frontmatter. Kept because links can live in it. */
40
+ frontmatter: Record<string, unknown>;
41
+ /** Source file path, relative to the project root. */
42
+ file: string;
43
+ }
44
+ /** What a resolved `[[WikiLink]]` points at. */
45
+ export interface LinkTarget {
46
+ slug: string;
47
+ collection: CollectionName;
48
+ urlPath: string;
49
+ }
50
+ /**
51
+ * Convert a content file path to its collection-relative slug and canonical URL.
52
+ *
53
+ * Files are named for their titles, so that `[[Evergreen Notes]]` resolves in
54
+ * Obsidian (which matches on filename) and in Astro (which matches on title)
55
+ * without a piped alias. The URL is therefore *derived* from the filename, not
56
+ * equal to it, using the same `github-slugger` call Astro's own content layer
57
+ * uses — so the graph and Astro's `entry.slug` can never disagree.
58
+ *
59
+ * Two escape hatches, in priority order:
60
+ * - `slug` frontmatter pins the URL when a rename would otherwise move it.
61
+ * Astro treats this key as reserved and strips it before Zod validation, so
62
+ * it must not appear in any collection schema.
63
+ * - `pages` declare a whole `url`, since they render at arbitrary routes.
64
+ */
65
+ export declare function toUrlPath(file: string, collection: CollectionName, data?: Record<string, unknown>): {
66
+ slug: string;
67
+ urlPath: string;
68
+ };
69
+ /**
70
+ * Convert a canonical URL path to the *file* path of its markdown twin.
71
+ *
72
+ * Every note is served twice at one URL: `/notes/cake/` renders the page,
73
+ * `/notes/cake.md` returns the source file. The mapping is pure string work —
74
+ * drop the trailing slash, append `.md` — and it lives here rather than in the
75
+ * build writer so the page templates cannot drift from the files that writer
76
+ * actually emits. The home page (`/`) has no name to hang the suffix on, so it
77
+ * gets `index.md`.
78
+ *
79
+ * The result is relative, with no leading slash: the caller joins it onto its
80
+ * output directory. For the URL a rendered page should link to, use
81
+ * `toMarkdownHref`. Traversal segments are rejected rather than normalized,
82
+ * since a `url` frontmatter is author-controlled and a `..` in it would
83
+ * otherwise let a build write outside its output directory.
84
+ */
85
+ export declare function toMarkdownPath(urlPath: string): string;
86
+ /**
87
+ * The site-absolute URL of an entry's markdown twin — what a rendered page links to.
88
+ *
89
+ * Derived from `toMarkdownPath` rather than computed alongside it, so a page's
90
+ * "view as markdown" link and the file the build writes can only ever be the
91
+ * same string with a leading slash.
92
+ */
93
+ export declare function toMarkdownHref(urlPath: string): string;
94
+ /** Coerce a frontmatter date to `yyyy-mm-dd`, so build output has no timestamp drift. */
95
+ export declare function normalizeDate(value: unknown): string | undefined;
96
+ /** Where to read content from. */
97
+ export interface GraphOptions {
98
+ /**
99
+ * The project root: the directory that *contains* `src/content`, not
100
+ * `src/content` itself.
101
+ *
102
+ * Every path the graph exposes is derived from this directory — `file` is
103
+ * relative to it, and `toUrlPath` strips `CONTENT_DIRS[collection]` off the
104
+ * front of `file` to get a slug. Point it one level too deep and every slug
105
+ * silently changes. Defaults to `process.cwd()`.
106
+ */
107
+ root?: string;
108
+ }
109
+ /**
110
+ * Read every public content entry, in a stable order.
111
+ *
112
+ * Deliberately uncached: the backlinks integration runs this on each build hook
113
+ * and the dev server must see content edits without a restart.
114
+ *
115
+ * The root is a parameter rather than a `process.chdir`, so one process can
116
+ * read several vaults — which is what the CLI does when it is pointed at
117
+ * another wiki — without the cwd becoming shared mutable state.
118
+ */
119
+ export declare function loadContentEntries(options?: GraphOptions): Promise<ContentEntry[]>;
120
+ /**
121
+ * Build the `[[WikiLink]]` resolution table: lowercased title or alias → target.
122
+ *
123
+ * Titles win over aliases, and within each pass later entries win — which only
124
+ * matters when two pieces of content claim the same name, and is a content bug
125
+ * either way.
126
+ */
127
+ export declare function buildLinkLookup(entries: ContentEntry[]): Map<string, LinkTarget>;
128
+ /**
129
+ * Build the url resolution table: canonical `urlPath` → target.
130
+ *
131
+ * Separate from the title/alias lookup on purpose. A path and a title are
132
+ * different namespaces, and mixing them is what let `/notes/atomic-notes`
133
+ * resolve through an alias rather than through the route it actually names.
134
+ */
135
+ export declare function buildUrlLookup(entries: ContentEntry[]): Map<string, LinkTarget>;
136
+ /**
137
+ * Build the filename resolution table: lowercased basename → target.
138
+ *
139
+ * Not a resolution table — nothing resolves through it. It exists so `check`
140
+ * can notice when a name means two different things depending on which table
141
+ * you ask: `[[Index]]` resolves by *title*, but `[index](./Index.md)` names a
142
+ * *file*, and in a vault where those disagree one of the two is silently wrong.
143
+ * Obsidian resolves by filename and Astro by title, so this is the seam where
144
+ * the two tools stop agreeing about what a link points at.
145
+ */
146
+ export declare function buildBasenameLookup(entries: ContentEntry[]): Map<string, LinkTarget[]>;
147
+ /**
148
+ * Resolve one extracted edge, using the table its kind belongs to.
149
+ *
150
+ * Deliberately does nothing clever: no basename fallback, no source-relative
151
+ * disambiguation, no collision policy. Those are resolution questions, tracked
152
+ * on #18; this is extraction's other half and nothing more.
153
+ */
154
+ export declare function resolveLink(link: ExtractedLink, byName: Map<string, LinkTarget>, byUrl: Map<string, LinkTarget>): LinkTarget | undefined;
155
+ /**
156
+ * The link lookup for one project root, built once and reused.
157
+ *
158
+ * The remark plugin runs per markdown file, so rescanning the content tree on
159
+ * every transform would make builds quadratic. Call `resetGraphCache()` if a
160
+ * caller needs to see content written during the same process.
161
+ */
162
+ export declare function getLinkLookup(options?: GraphOptions): Promise<Map<string, LinkTarget>>;
163
+ export declare function resetGraphCache(): void;
164
+ /**
165
+ * Blank out fenced blocks and inline code, preserving offsets.
166
+ *
167
+ * A `[[WikiLink]]` written inside backticks is documentation *about* the
168
+ * syntax, not a link. The remark plugin gets this right for free because it
169
+ * only visits `text` nodes and never `inlineCode` — anything scanning raw
170
+ * markdown has to strip code itself or it reports phantom broken links.
171
+ */
172
+ export declare function stripCode(content: string): string;
173
+ /**
174
+ * How a link target should be resolved.
175
+ *
176
+ * `name` is title-shaped: the raw text of a `[[WikiLink]]`, or the basename of
177
+ * a relative file link, resolved against the title/alias lookup.
178
+ * `url` is path-shaped: an absolute site path, resolved against `urlPath`.
179
+ *
180
+ * The two cannot be collapsed into one string. A bare `atomic-notes` is not a
181
+ * title and matching it against one only works by coincidence; `/notes/atomic-notes/`
182
+ * is not a title either and must never be looked up as one.
183
+ */
184
+ export type LinkKind = 'name' | 'url';
185
+ /** One outbound edge, with enough information to resolve it. */
186
+ export interface ExtractedLink {
187
+ kind: LinkKind;
188
+ target: string;
189
+ }
190
+ /**
191
+ * Extract every outbound link target from a markdown body and its frontmatter.
192
+ *
193
+ * Returns the edges Obsidian's `resolvedLinks` would record: wikilinks, embeds,
194
+ * internal markdown links, and `frontmatterLinks`. Code is excluded.
195
+ *
196
+ * Frontmatter is passed separately because it is already parsed by the time it
197
+ * reaches here — `loadContentEntries()` splits it off with gray-matter, so the
198
+ * body alone can never contain it.
199
+ */
200
+ export declare function extractLinks(content: string, frontmatter?: Record<string, unknown>): ExtractedLink[];
201
+ /**
202
+ * The shape of the star calculation strategy.
203
+ *
204
+ * Declared as an interface rather than inferred from the value, because the
205
+ * value picks one member of each union and `calculateStars` switches over all
206
+ * of them. With `as const` on the fields, TypeScript narrows `strategy` to the
207
+ * literal `'top-percent'` and every other arm of that switch becomes an error
208
+ * — the config would be untunable without editing the code that reads it,
209
+ * which is the opposite of what a config is for.
210
+ */
211
+ export interface StarConfig {
212
+ strategy: 'top-percent' | 'top-absolute' | 'threshold';
213
+ /** For 'top-percent': what percentage gets stars. */
214
+ topPercent: number;
215
+ /** For 'top-absolute': how many notes get stars. */
216
+ topAbsolute: number;
217
+ /** For 'threshold': minimum backlinks to get a star. */
218
+ threshold: number;
219
+ /** Below this many notes, nobody is starred. */
220
+ minNotesForStars: number;
221
+ rankBy: 'backlinks' | 'revisions' | 'cross-theme' | 'weighted';
222
+ /** For 'weighted' ranking (future). */
223
+ weights: {
224
+ backlinks: number;
225
+ revisions: number;
226
+ crossTheme: number;
227
+ };
228
+ }
229
+ /**
230
+ * Star calculation strategy.
231
+ *
232
+ * Lives in the core rather than the Astro integration because `isStarred` is a
233
+ * field of the public artifact, and the CLI has to be able to produce the same
234
+ * artifact without loading Astro.
235
+ */
236
+ export declare const STAR_CONFIG: StarConfig;
237
+ /**
238
+ * One node of the public graph, and one entry of `backlinks.json`.
239
+ *
240
+ * The property order here is a serialization contract: `backlinks.json` is
241
+ * committed, and a reordered object is a diff on every build.
242
+ */
243
+ export interface NoteMetadata {
244
+ /** Canonical URL path. The graph is keyed by URL, so this is the identity. */
245
+ slug: string;
246
+ title: string;
247
+ collection: CollectionName;
248
+ aliases: string[];
249
+ outbound: string[];
250
+ inbound: string[];
251
+ tags: string[];
252
+ status: string;
253
+ summary?: string;
254
+ updated?: string;
255
+ isStarred?: boolean;
256
+ }
257
+ /** Which rules the graph and `check` can report. */
258
+ export type DiagnosticRule = 'broken-link' | 'ambiguous-target' | 'duplicate-name' | 'noncanonical-title';
259
+ /**
260
+ * One finding, as data.
261
+ *
262
+ * The Astro integration used to print broken links inline, which meant the only
263
+ * way to get at them was to scrape build logs. Findings are values now, so the
264
+ * integration, `check` and the search-index gate can each render the same set
265
+ * their own way.
266
+ */
267
+ export interface Diagnostic {
268
+ rule: DiagnosticRule;
269
+ severity: 'error' | 'warning';
270
+ /** Source file, relative to the project root. */
271
+ file: string;
272
+ /** Not tracked yet: `extractLinks` records no offsets. Added when #24 needs them. */
273
+ line?: number;
274
+ message: string;
275
+ /** The link text or name involved. */
276
+ target?: string;
277
+ /** Canonical URL of the entry the finding is in. Carried so build output can name the route. */
278
+ urlPath?: string;
279
+ /** How the offending link was spelled, which decides `[[name]]` vs `(url)` rendering. */
280
+ kind?: LinkKind;
281
+ /** urlPaths in contention, for `ambiguous-target` and `duplicate-name`. */
282
+ candidates?: string[];
283
+ /** The spelling a `noncanonical-title` finding should have used. */
284
+ canonical?: string;
285
+ }
286
+ /** The resolved content graph. */
287
+ export interface Graph {
288
+ /** Keyed by urlPath; insertion order is entry order. */
289
+ nodes: Record<string, NoteMetadata>;
290
+ diagnostics: Diagnostic[];
291
+ totalBacklinks: number;
292
+ }
293
+ /**
294
+ * Calculate which notes get stars based on STAR_CONFIG.
295
+ * Returns a Set of slugs that should be starred.
296
+ */
297
+ export declare function calculateStars(notes: Map<string, NoteMetadata>): Set<string>;
298
+ /**
299
+ * Build the resolved graph from loaded entries.
300
+ *
301
+ * Pure: no filesystem, no logging, no Astro. Everything a caller might want to
302
+ * print comes back in `diagnostics`, so the same construction serves the build
303
+ * (which warns), `check` (which reports) and `related` (which does neither).
304
+ */
305
+ export declare function buildGraph(entries: ContentEntry[]): Graph;
306
+ /**
307
+ * Render a diagnostic as the build has always rendered it.
308
+ *
309
+ * `public/backlinks.json` is not the only committed contract — build output is
310
+ * read by humans and diffed by reviewers, so the warning line is kept exactly
311
+ * as it was when the integration formatted it itself.
312
+ */
313
+ export declare function formatDiagnostic(diagnostic: Diagnostic): string;
314
+ /**
315
+ * The public artifact, exactly as `public/backlinks.json` stores it.
316
+ *
317
+ * Separate from `Graph` because the file is a runtime contract with four
318
+ * client-side readers: the graph can grow fields, the file cannot.
319
+ */
320
+ export declare function toBacklinksJson(graph: Graph): Record<string, NoteMetadata>;
321
+ /**
322
+ * Names claimed by more than one entry.
323
+ *
324
+ * Two triggers, because a name lives in two namespaces: a title or alias that
325
+ * two entries both answer to, and a filename that two entries share. Neither
326
+ * is an error the graph can resolve — `buildLinkLookup` keeps last-wins, which
327
+ * is what it has always done — but both mean a `[[WikiLink]]` reaches somewhere
328
+ * its author did not choose, and silently.
329
+ *
330
+ * Reported against the *first* claimant, because that is the entry the
331
+ * collision makes unreachable.
332
+ */
333
+ export declare function findDuplicateNames(entries: ContentEntry[]): Diagnostic[];
334
+ /**
335
+ * WikiLinks that resolve but do not name their target exactly.
336
+ *
337
+ * The rule this implements used to live in the build gate script, with its own
338
+ * lookup table and its own regex — a second copy of a rule the graph core
339
+ * already had the ingredients for, which is the bug class #3 was opened to
340
+ * kill. One implementation now, two callers: `check` reports it and
341
+ * `commune gate` fails on it.
342
+ *
343
+ * Worth checking precisely because it is invisible: a piped link or a
344
+ * near-miss title still renders, and the vault quietly decouples from the site.
345
+ */
346
+ export declare function findNoncanonicalTitles(entries: ContentEntry[]): Diagnostic[];
347
+ /**
348
+ * Every finding `check` reports.
349
+ *
350
+ * The graph's own diagnostics come first and in entry order, so the list reads
351
+ * the same way the build log does, then the two whole-corpus rules that need
352
+ * every entry in hand before they can fire.
353
+ */
354
+ export declare function checkEntries(entries: ContentEntry[], graph: Graph): Diagnostic[];