@glw907/cairn-cms 0.60.0 → 0.62.1

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 (281) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/components/AdminLayout.svelte +152 -229
  3. package/dist/components/CairnAdmin.svelte +13 -42
  4. package/dist/components/CairnLogo.svelte +1 -6
  5. package/dist/components/CairnMediaLibrary.svelte +821 -1210
  6. package/dist/components/CairnTidySettings.svelte +194 -261
  7. package/dist/components/CairnTidySettings.svelte.d.ts +1 -1
  8. package/dist/components/ComponentForm.svelte +110 -185
  9. package/dist/components/ComponentInsertDialog.svelte +163 -283
  10. package/dist/components/ConceptList.svelte +111 -191
  11. package/dist/components/ConfirmPage.svelte +5 -12
  12. package/dist/components/CsrfField.svelte +5 -11
  13. package/dist/components/DeleteDialog.svelte +15 -42
  14. package/dist/components/EditPage.svelte +781 -1205
  15. package/dist/components/EditorToolbar.svelte +108 -170
  16. package/dist/components/HelpHome.svelte +824 -0
  17. package/dist/components/HelpHome.svelte.d.ts +22 -0
  18. package/dist/components/IconPicker.svelte +23 -53
  19. package/dist/components/LinkPicker.svelte +34 -58
  20. package/dist/components/LoginPage.svelte +14 -27
  21. package/dist/components/ManageEditors.svelte +3 -15
  22. package/dist/components/MarkdownEditor.svelte +689 -957
  23. package/dist/components/MarkdownHelpDialog.svelte +12 -27
  24. package/dist/components/MediaCaptureCard.svelte +18 -57
  25. package/dist/components/MediaFigureControl.svelte +32 -71
  26. package/dist/components/MediaHeroField.svelte +210 -329
  27. package/dist/components/MediaInsertPopover.svelte +156 -283
  28. package/dist/components/MediaPicker.svelte +67 -131
  29. package/dist/components/NavTree.svelte +46 -78
  30. package/dist/components/RenameDialog.svelte +16 -43
  31. package/dist/components/ShortcutsDialog.svelte +9 -13
  32. package/dist/components/ShortcutsGrid.svelte +1 -2
  33. package/dist/components/TidyReview.svelte +140 -248
  34. package/dist/components/WebLinkDialog.svelte +19 -40
  35. package/dist/components/cairn-admin.css +4 -0
  36. package/dist/components/client-ingest.d.ts +16 -8
  37. package/dist/components/client-ingest.js +12 -6
  38. package/dist/components/editor-media.js +16 -8
  39. package/dist/components/editor-placeholder.d.ts +4 -2
  40. package/dist/components/editor-tidy.d.ts +24 -12
  41. package/dist/components/editor-tidy.js +8 -4
  42. package/dist/components/index.d.ts +1 -0
  43. package/dist/components/index.js +1 -0
  44. package/dist/components/link-completion.d.ts +12 -6
  45. package/dist/components/link-completion.js +12 -6
  46. package/dist/components/markdown-directives.d.ts +9 -6
  47. package/dist/components/markdown-directives.js +9 -6
  48. package/dist/components/markdown-format.d.ts +7 -2
  49. package/dist/components/markdown-format.js +59 -28
  50. package/dist/components/markdown-reference.d.ts +8 -0
  51. package/dist/components/markdown-reference.js +22 -0
  52. package/dist/components/media-upload-outcome.d.ts +12 -6
  53. package/dist/components/objective-errors.d.ts +8 -4
  54. package/dist/components/objective-errors.js +8 -4
  55. package/dist/components/preview-doc.d.ts +4 -2
  56. package/dist/components/preview-doc.js +4 -2
  57. package/dist/components/spellcheck.d.ts +57 -29
  58. package/dist/components/spellcheck.js +50 -20
  59. package/dist/components/tidy-categorize.d.ts +20 -10
  60. package/dist/components/tidy-categorize.js +16 -8
  61. package/dist/components/tidy-validate.d.ts +12 -6
  62. package/dist/components/tidy-validate.js +20 -10
  63. package/dist/components/topbar-context.d.ts +4 -2
  64. package/dist/content/advisories.d.ts +51 -0
  65. package/dist/content/advisories.js +79 -0
  66. package/dist/content/compose.d.ts +4 -2
  67. package/dist/content/compose.js +1 -0
  68. package/dist/content/excerpt.js +4 -2
  69. package/dist/content/getting-started.d.ts +18 -0
  70. package/dist/content/getting-started.js +12 -0
  71. package/dist/content/links.d.ts +16 -8
  72. package/dist/content/links.js +12 -6
  73. package/dist/content/manifest.d.ts +36 -18
  74. package/dist/content/manifest.js +32 -16
  75. package/dist/content/media-refs.d.ts +4 -2
  76. package/dist/content/media-refs.js +4 -2
  77. package/dist/content/media-rewrite.d.ts +8 -4
  78. package/dist/content/media-rewrite.js +76 -38
  79. package/dist/content/schema.d.ts +20 -10
  80. package/dist/content/site-dictionary.d.ts +4 -2
  81. package/dist/content/site-dictionary.js +8 -4
  82. package/dist/content/types.d.ts +97 -42
  83. package/dist/delivery/CairnHead.svelte +8 -11
  84. package/dist/delivery/content-index.d.ts +16 -8
  85. package/dist/delivery/feeds.js +4 -2
  86. package/dist/delivery/json-ld.d.ts +3 -0
  87. package/dist/delivery/json-ld.js +3 -0
  88. package/dist/delivery/manifest.d.ts +4 -2
  89. package/dist/delivery/manifest.js +4 -2
  90. package/dist/delivery/public-routes.d.ts +12 -6
  91. package/dist/delivery/public-routes.js +4 -2
  92. package/dist/delivery/seo-fields.d.ts +12 -6
  93. package/dist/delivery/seo-fields.js +8 -4
  94. package/dist/delivery/site-indexes.d.ts +4 -2
  95. package/dist/delivery/site-resolver.d.ts +4 -2
  96. package/dist/delivery/site-resolver.js +4 -2
  97. package/dist/doctor/cloudflare-api.d.ts +6 -0
  98. package/dist/doctor/cloudflare-api.js +6 -0
  99. package/dist/doctor/index.d.ts +12 -6
  100. package/dist/doctor/report.d.ts +3 -0
  101. package/dist/doctor/report.js +3 -0
  102. package/dist/doctor/run.d.ts +3 -0
  103. package/dist/doctor/run.js +3 -0
  104. package/dist/doctor/types.d.ts +10 -2
  105. package/dist/doctor/types.js +6 -0
  106. package/dist/doctor/wrangler-config.d.ts +7 -2
  107. package/dist/doctor/wrangler-config.js +3 -0
  108. package/dist/email.d.ts +4 -2
  109. package/dist/env.d.ts +0 -3
  110. package/dist/env.js +0 -3
  111. package/dist/github/branches.d.ts +4 -2
  112. package/dist/github/branches.js +4 -2
  113. package/dist/github/signing.d.ts +1 -1
  114. package/dist/github/signing.js +2 -2
  115. package/dist/log/events.d.ts +1 -1
  116. package/dist/media/bulk-delete-plan.d.ts +8 -4
  117. package/dist/media/config.d.ts +12 -6
  118. package/dist/media/config.js +16 -8
  119. package/dist/media/delivery-bucket.d.ts +4 -2
  120. package/dist/media/library-entry.d.ts +4 -2
  121. package/dist/media/library-entry.js +4 -2
  122. package/dist/media/manifest.d.ts +29 -15
  123. package/dist/media/manifest.js +29 -16
  124. package/dist/media/naming.d.ts +12 -6
  125. package/dist/media/naming.js +24 -12
  126. package/dist/media/orphan-scan.d.ts +4 -2
  127. package/dist/media/reconcile.d.ts +21 -11
  128. package/dist/media/reconcile.js +12 -6
  129. package/dist/media/reference.d.ts +8 -4
  130. package/dist/media/reference.js +12 -6
  131. package/dist/media/rewrite-plan.d.ts +12 -6
  132. package/dist/media/sniff.d.ts +4 -2
  133. package/dist/media/sniff.js +28 -14
  134. package/dist/media/store.d.ts +16 -8
  135. package/dist/media/store.js +4 -2
  136. package/dist/media/transform-url.d.ts +12 -6
  137. package/dist/media/transform-url.js +8 -4
  138. package/dist/media/usage.d.ts +8 -4
  139. package/dist/nav/site-config.d.ts +16 -8
  140. package/dist/render/component-grammar.d.ts +23 -10
  141. package/dist/render/component-grammar.js +19 -8
  142. package/dist/render/component-insert.d.ts +8 -4
  143. package/dist/render/component-insert.js +4 -2
  144. package/dist/render/component-reference.d.ts +4 -2
  145. package/dist/render/component-reference.js +4 -2
  146. package/dist/render/component-validate.d.ts +3 -0
  147. package/dist/render/component-validate.js +3 -0
  148. package/dist/render/glyph.d.ts +4 -2
  149. package/dist/render/glyph.js +4 -2
  150. package/dist/render/pipeline.d.ts +20 -10
  151. package/dist/render/pipeline.js +4 -2
  152. package/dist/render/registry.d.ts +40 -20
  153. package/dist/render/registry.js +16 -8
  154. package/dist/render/rehype-dispatch.d.ts +22 -8
  155. package/dist/render/rehype-dispatch.js +22 -8
  156. package/dist/render/remark-directives.d.ts +3 -0
  157. package/dist/render/remark-directives.js +3 -0
  158. package/dist/render/remark-figure.d.ts +4 -2
  159. package/dist/render/remark-figure.js +4 -2
  160. package/dist/render/resolve-links.d.ts +4 -2
  161. package/dist/render/resolve-links.js +4 -2
  162. package/dist/render/resolve-media.d.ts +16 -8
  163. package/dist/render/resolve-media.js +12 -6
  164. package/dist/sveltekit/admin-dispatch.d.ts +2 -0
  165. package/dist/sveltekit/admin-dispatch.js +9 -3
  166. package/dist/sveltekit/auth-routes.d.ts +3 -0
  167. package/dist/sveltekit/auth-routes.js +3 -0
  168. package/dist/sveltekit/cairn-admin.d.ts +16 -5
  169. package/dist/sveltekit/cairn-admin.js +26 -10
  170. package/dist/sveltekit/content-routes.d.ts +191 -86
  171. package/dist/sveltekit/content-routes.js +295 -107
  172. package/dist/sveltekit/editors-routes.d.ts +3 -0
  173. package/dist/sveltekit/editors-routes.js +3 -0
  174. package/dist/sveltekit/guard.d.ts +4 -2
  175. package/dist/sveltekit/guard.js +4 -2
  176. package/dist/sveltekit/https-required-page.d.ts +1 -1
  177. package/dist/sveltekit/https-required-page.js +1 -1
  178. package/dist/sveltekit/index.d.ts +1 -1
  179. package/dist/sveltekit/media-route.d.ts +1 -2
  180. package/dist/sveltekit/media-route.js +13 -8
  181. package/dist/sveltekit/nav-routes.d.ts +7 -2
  182. package/dist/sveltekit/nav-routes.js +3 -0
  183. package/dist/sveltekit/types.d.ts +4 -2
  184. package/dist/vite/index.d.ts +32 -16
  185. package/dist/vite/index.js +52 -26
  186. package/dist/vite/resolve-root.d.ts +8 -4
  187. package/dist/vite/resolve-root.js +4 -2
  188. package/package.json +8 -2
  189. package/src/lib/components/AdminLayout.svelte +22 -0
  190. package/src/lib/components/CairnAdmin.svelte +3 -0
  191. package/src/lib/components/CairnTidySettings.svelte +2 -2
  192. package/src/lib/components/ComponentForm.svelte +0 -1
  193. package/src/lib/components/EditPage.svelte +133 -41
  194. package/src/lib/components/HelpHome.svelte +850 -0
  195. package/src/lib/components/MarkdownHelpDialog.svelte +4 -15
  196. package/src/lib/components/client-ingest.ts +20 -10
  197. package/src/lib/components/editor-media.ts +20 -10
  198. package/src/lib/components/editor-placeholder.ts +12 -6
  199. package/src/lib/components/editor-tidy.ts +28 -14
  200. package/src/lib/components/index.ts +1 -0
  201. package/src/lib/components/link-completion.ts +12 -6
  202. package/src/lib/components/markdown-directives.ts +13 -8
  203. package/src/lib/components/markdown-format.ts +63 -30
  204. package/src/lib/components/markdown-reference.ts +30 -0
  205. package/src/lib/components/media-upload-outcome.ts +12 -6
  206. package/src/lib/components/objective-errors.ts +16 -8
  207. package/src/lib/components/preview-doc.ts +4 -2
  208. package/src/lib/components/spellcheck.ts +92 -40
  209. package/src/lib/components/tidy-categorize.ts +28 -14
  210. package/src/lib/components/tidy-validate.ts +28 -14
  211. package/src/lib/components/topbar-context.ts +4 -2
  212. package/src/lib/content/advisories.ts +141 -0
  213. package/src/lib/content/compose.ts +5 -2
  214. package/src/lib/content/excerpt.ts +4 -2
  215. package/src/lib/content/getting-started.ts +31 -0
  216. package/src/lib/content/links.ts +16 -8
  217. package/src/lib/content/manifest.ts +36 -18
  218. package/src/lib/content/media-refs.ts +4 -2
  219. package/src/lib/content/media-rewrite.ts +100 -50
  220. package/src/lib/content/schema.ts +20 -10
  221. package/src/lib/content/site-dictionary.ts +8 -4
  222. package/src/lib/content/types.ts +97 -42
  223. package/src/lib/delivery/content-index.ts +16 -8
  224. package/src/lib/delivery/feeds.ts +4 -2
  225. package/src/lib/delivery/json-ld.ts +3 -0
  226. package/src/lib/delivery/manifest.ts +4 -2
  227. package/src/lib/delivery/public-routes.ts +16 -8
  228. package/src/lib/delivery/seo-fields.ts +12 -6
  229. package/src/lib/delivery/site-indexes.ts +4 -2
  230. package/src/lib/delivery/site-resolver.ts +4 -2
  231. package/src/lib/doctor/cloudflare-api.ts +6 -0
  232. package/src/lib/doctor/index.ts +12 -6
  233. package/src/lib/doctor/report.ts +3 -0
  234. package/src/lib/doctor/run.ts +3 -0
  235. package/src/lib/doctor/types.ts +10 -2
  236. package/src/lib/doctor/wrangler-config.ts +7 -2
  237. package/src/lib/email.ts +4 -2
  238. package/src/lib/env.ts +0 -3
  239. package/src/lib/github/branches.ts +4 -2
  240. package/src/lib/github/signing.ts +2 -2
  241. package/src/lib/log/events.ts +1 -0
  242. package/src/lib/media/bulk-delete-plan.ts +8 -4
  243. package/src/lib/media/config.ts +24 -12
  244. package/src/lib/media/delivery-bucket.ts +4 -2
  245. package/src/lib/media/library-entry.ts +4 -2
  246. package/src/lib/media/manifest.ts +33 -18
  247. package/src/lib/media/naming.ts +24 -12
  248. package/src/lib/media/orphan-scan.ts +4 -2
  249. package/src/lib/media/reconcile.ts +21 -11
  250. package/src/lib/media/reference.ts +12 -6
  251. package/src/lib/media/rewrite-plan.ts +12 -6
  252. package/src/lib/media/sniff.ts +28 -14
  253. package/src/lib/media/store.ts +16 -8
  254. package/src/lib/media/transform-url.ts +12 -6
  255. package/src/lib/media/usage.ts +8 -4
  256. package/src/lib/nav/site-config.ts +16 -8
  257. package/src/lib/render/component-grammar.ts +23 -10
  258. package/src/lib/render/component-insert.ts +8 -4
  259. package/src/lib/render/component-reference.ts +4 -2
  260. package/src/lib/render/component-validate.ts +3 -0
  261. package/src/lib/render/glyph.ts +4 -2
  262. package/src/lib/render/pipeline.ts +20 -10
  263. package/src/lib/render/registry.ts +44 -22
  264. package/src/lib/render/rehype-dispatch.ts +22 -8
  265. package/src/lib/render/remark-directives.ts +3 -0
  266. package/src/lib/render/remark-figure.ts +4 -2
  267. package/src/lib/render/resolve-links.ts +4 -2
  268. package/src/lib/render/resolve-media.ts +16 -8
  269. package/src/lib/sveltekit/admin-dispatch.ts +10 -4
  270. package/src/lib/sveltekit/auth-routes.ts +3 -0
  271. package/src/lib/sveltekit/cairn-admin.ts +37 -15
  272. package/src/lib/sveltekit/content-routes.ts +492 -197
  273. package/src/lib/sveltekit/editors-routes.ts +3 -0
  274. package/src/lib/sveltekit/guard.ts +4 -2
  275. package/src/lib/sveltekit/https-required-page.ts +1 -1
  276. package/src/lib/sveltekit/index.ts +3 -0
  277. package/src/lib/sveltekit/media-route.ts +13 -8
  278. package/src/lib/sveltekit/nav-routes.ts +7 -2
  279. package/src/lib/sveltekit/types.ts +4 -2
  280. package/src/lib/vite/index.ts +60 -30
  281. package/src/lib/vite/resolve-root.ts +8 -4
@@ -0,0 +1,141 @@
1
+ // cairn-cms: the entry editor's internal advisory channel (editor-help pass 3). An advisory is a
2
+ // non-blocking, serializable notice that rides EditData across the SSR boundary, so it carries data
3
+ // only and never a callback. Today's one notice is the cross-branch address collision: a warning,
4
+ // not a gate, that another entry already resolves to the same public address (last-write-wins).
5
+ //
6
+ // The address index mirrors buildUsageIndex (src/lib/media/usage.ts): a main arm that reads each
7
+ // manifest entry's resolved permalink with no per-file read, and a branch arm that lists every open
8
+ // cairn/* branch, reconstructs each edited entry's path from the branch name, reads that one file,
9
+ // and resolves its permalink. The map is keyed by permalink, so every entry that resolves to a given
10
+ // address shares one bucket. The build fails open: a branch read that throws, or a dated entry whose
11
+ // permalink cannot resolve, is skipped rather than thrown, so a transient failure degrades to no
12
+ // notice and never blocks the editor or the publish.
13
+ import type { ConceptDescriptor } from './types.js';
14
+ import type { RepoRef } from '../github/types.js';
15
+ import type { Manifest } from './manifest.js';
16
+ import { listBranches } from '../github/branches.js';
17
+ import { readRaw } from '../github/repo.js';
18
+ import { PENDING_PREFIX, parsePendingBranch } from './pending.js';
19
+ import { findConcept } from './concepts.js';
20
+ import { isValidId, filenameFromId } from './ids.js';
21
+ import { parseMarkdown } from './frontmatter.js';
22
+ import { entryIdentity } from './identity.js';
23
+
24
+ /** One action an advisory offers, as a label and an optional link target. */
25
+ export interface AdvisoryAction {
26
+ /** The action's button or link label. */
27
+ label: string;
28
+ /** The link target, when the action navigates. */
29
+ href?: string;
30
+ }
31
+
32
+ /** A non-blocking editor notice, serializable so it can ride EditData across the SSR boundary. */
33
+ export interface AdvisoryNotice {
34
+ /** The notice kind, e.g. "address-collision". */
35
+ kind: string;
36
+ /** The advisory severity; warn-and-allow, never a gate. */
37
+ severity: 'warn';
38
+ /** The notice text shown to the editor. */
39
+ message: string;
40
+ /** The notice's offered actions, when any. */
41
+ actions?: AdvisoryAction[];
42
+ }
43
+
44
+ /** One entry that resolves to an address, in a shape the collision check and the message read. */
45
+ export interface AddressEntry {
46
+ /** The concept id, e.g. "pages". */
47
+ concept: string;
48
+ /** The entry id (its filename stem). */
49
+ id: string;
50
+ /** The entry title for display, from the manifest (main) or frontmatter (branch). */
51
+ title: string;
52
+ /** The published corpus on main, or an open cairn/* edit branch. */
53
+ source: 'main' | 'branch';
54
+ }
55
+
56
+ /** Permalink to the distinct entries that resolve to it, across main and every open branch. */
57
+ export type AddressIndex = Map<string, AddressEntry[]>;
58
+
59
+ /** Append a row under its permalink, creating the bucket on first use. */
60
+ function push(index: AddressIndex, permalink: string, entry: AddressEntry): void {
61
+ const rows = index.get(permalink);
62
+ if (rows) rows.push(entry);
63
+ else index.set(permalink, [entry]);
64
+ }
65
+
66
+ /**
67
+ * Build the permalink-keyed address index over main (from each manifest entry's resolved permalink)
68
+ * plus every open cairn/* branch (resolved from its edited markdown).
69
+ *
70
+ * The build fails open: a branch read that throws and a permalink that cannot resolve are both caught
71
+ * and skipped, so a transient failure degrades to a thinner index, never a thrown editor or a blocked
72
+ * publish. The branches are read in one Promise.all, the way buildUsageIndex reads them.
73
+ */
74
+ export async function buildAddressIndex(
75
+ repo: RepoRef,
76
+ token: string,
77
+ concepts: ConceptDescriptor[],
78
+ manifest: Manifest,
79
+ ): Promise<AddressIndex> {
80
+ const index: AddressIndex = new Map();
81
+
82
+ // The main arm: the manifest already carries each entry's resolved permalink, so this is a pure
83
+ // reverse map with no per-file read.
84
+ for (const entry of manifest.entries) {
85
+ push(index, entry.permalink, {
86
+ concept: entry.concept,
87
+ id: entry.id,
88
+ title: entry.title,
89
+ source: 'main',
90
+ });
91
+ }
92
+
93
+ // The branch arm: read each open cairn/* branch's one edited file and resolve its permalink. The
94
+ // path is derivable from the branch name, so no tree-listing is needed.
95
+ const names = await listBranches(repo, PENDING_PREFIX, token);
96
+ const perBranch = await Promise.all(
97
+ names.map(async (name): Promise<{ permalink: string; entry: AddressEntry } | null> => {
98
+ // Resolve the branch name with the branch tooling's guard: a malformed name, an id that fails
99
+ // the slug rule, or an unconfigured concept is skipped with no read attempted.
100
+ const ref = parsePendingBranch(name);
101
+ if (!ref || !isValidId(ref.id)) return null;
102
+ const concept = findConcept(concepts, ref.concept);
103
+ if (!concept) return null;
104
+
105
+ const path = `${concept.dir}/${filenameFromId(ref.id)}`;
106
+ try {
107
+ const raw = await readRaw({ ...repo, branch: name }, path, token);
108
+ if (raw === null) return null; // The file is absent on the branch: nothing to resolve.
109
+ const { frontmatter } = parseMarkdown(raw);
110
+ const fmTitle = frontmatter.title;
111
+ const title = typeof fmTitle === 'string' && fmTitle.trim() ? fmTitle : ref.id;
112
+ // entryIdentity throws for a dated entry with no date; that branch is caught and skipped.
113
+ const { permalink } = entryIdentity(concept, path, frontmatter);
114
+ return { permalink, entry: { concept: concept.id, id: ref.id, title, source: 'branch' } };
115
+ } catch {
116
+ // A failed branch read or an unresolvable permalink degrades this one branch, fail open.
117
+ return null;
118
+ }
119
+ }),
120
+ );
121
+
122
+ // Fold the per-branch rows back in, preserving the branch order so the index reads stably.
123
+ for (const row of perBranch) {
124
+ if (row) push(index, row.permalink, row.entry);
125
+ }
126
+
127
+ return index;
128
+ }
129
+
130
+ /**
131
+ * Find the first other entry that already resolves to an address, or null when the address is free
132
+ * or holds only the entry itself. The self entry is identified by its concept and id together.
133
+ */
134
+ export function addressCollision(
135
+ index: AddressIndex,
136
+ self: { concept: string; id: string },
137
+ address: string,
138
+ ): AddressEntry | null {
139
+ const rows = index.get(address) ?? [];
140
+ return rows.find((row) => row.concept !== self.concept || row.id !== self.id) ?? null;
141
+ }
@@ -8,9 +8,11 @@ import { resolveConcepts } from './concepts.js';
8
8
  import { normalizeAssets } from '../media/config.js';
9
9
  import { dictionaryFileForDialect, type SiteConfig } from '../nav/site-config.js';
10
10
 
11
- /** The input to {@link composeRuntime}. `siteConfig` is required so the per-concept URL policy is
11
+ /**
12
+ * The input to {@link composeRuntime}. `siteConfig` is required so the per-concept URL policy is
12
13
  * always derived from one source and can never be silently dropped. `extensions` fold in after the
13
- * adapter's concepts. */
14
+ * adapter's concepts.
15
+ */
14
16
  export interface ComposeInput {
15
17
  adapter: CairnAdapter;
16
18
  siteConfig: SiteConfig;
@@ -40,6 +42,7 @@ export function composeRuntime({ adapter, siteConfig, extensions = [] }: Compose
40
42
  concepts: resolveConcepts(content, siteConfig),
41
43
  backend: adapter.backend,
42
44
  sender: adapter.sender,
45
+ supportContact: adapter.supportContact,
43
46
  render: adapter.render,
44
47
  manifestPath: adapter.manifestPath ?? 'src/content/.cairn/index.json',
45
48
  registry: adapter.registry,
@@ -2,8 +2,10 @@
2
2
  // 5). A light markdown strip keeps summaries cheap, so a list card, an og:description, and a
3
3
  // summary-mode feed read one derived excerpt without a full render.
4
4
 
5
- /** Reduce markdown to readable plain text: drop fenced code, images, and markup; unwrap inline
6
- * code and links to their text; collapse whitespace. */
5
+ /**
6
+ * Reduce markdown to readable plain text: drop fenced code, images, and markup; unwrap inline
7
+ * code and links to their text; collapse whitespace.
8
+ */
7
9
  function toPlainText(md: string): string {
8
10
  return md
9
11
  .replace(/```[\s\S]*?```/g, ' ')
@@ -0,0 +1,31 @@
1
+ // cairn-cms: the Help home getting-started progress, derived from the committed manifest and the
2
+ // pending-branch list rather than stored. An entry in the manifest is published to main; a pending
3
+ // item is written on an open cairn/ branch. The three step states fall out of those two inputs, so
4
+ // the count the editor sees is always the real state of the corpus.
5
+ import type { Manifest } from './manifest.js';
6
+
7
+ /** The three getting-started steps, their completion count, and the fixed step total. */
8
+ export interface GettingStarted {
9
+ wrotePost: boolean;
10
+ publishedPost: boolean;
11
+ createdPage: boolean;
12
+ doneCount: number;
13
+ total: 3;
14
+ }
15
+
16
+ /**
17
+ * Map the manifest and the pending-branch list to the three getting-started step states. Writing a
18
+ * post (published or pending) completes the first step; publishing one completes the second; a page
19
+ * (published or pending) completes the third.
20
+ */
21
+ export function deriveGettingStarted(
22
+ manifest: Manifest,
23
+ pending: { concept: string; id: string }[],
24
+ ): GettingStarted {
25
+ const publishedPost = manifest.entries.some((e) => e.concept === 'posts');
26
+ const wrotePost = publishedPost || pending.some((p) => p.concept === 'posts');
27
+ const createdPage =
28
+ manifest.entries.some((e) => e.concept === 'pages') || pending.some((p) => p.concept === 'pages');
29
+ const doneCount = Number(wrotePost) + Number(publishedPost) + Number(createdPage);
30
+ return { wrotePost, publishedPost, createdPage, doneCount, total: 3 };
31
+ }
@@ -15,8 +15,10 @@ export interface CairnRef {
15
15
  id: string;
16
16
  }
17
17
 
18
- /** Resolve a reference to its live permalink. Returns undefined when the target is missing (the
19
- * preview marks it); the build resolver throws instead, so a dangling token fails the build. */
18
+ /**
19
+ * Resolve a reference to its live permalink. Returns undefined when the target is missing (the
20
+ * preview marks it); the build resolver throws instead, so a dangling token fails the build.
21
+ */
20
22
  export type LinkResolve = (ref: CairnRef) => string | undefined;
21
23
 
22
24
  /** Parse a `cairn:<concept>/<id>` href, or null for any other href or a malformed token. */
@@ -31,21 +33,27 @@ export function parseCairnToken(href: string): CairnRef | null {
31
33
  return { concept, id };
32
34
  }
33
35
 
34
- /** Write the `cairn:<concept>/<id>` token for a ref. The inverse of parseCairnToken, so the editor
35
- * link picker and the autocomplete write exactly the form the resolver reads back. */
36
+ /**
37
+ * Write the `cairn:<concept>/<id>` token for a ref. The inverse of parseCairnToken, so the editor
38
+ * link picker and the autocomplete write exactly the form the resolver reads back.
39
+ */
36
40
  export function formatCairnToken(ref: CairnRef): string {
37
41
  return `cairn:${ref.concept}/${ref.id}`;
38
42
  }
39
43
 
40
- /** Escape the characters that would break a markdown link's display text: a backslash and the
44
+ /**
45
+ * Escape the characters that would break a markdown link's display text: a backslash and the
41
46
  * square brackets that delimit the text. Used where a content title becomes link display text,
42
- * so an unbalanced bracket in a title cannot truncate the generated link. */
47
+ * so an unbalanced bracket in a title cannot truncate the generated link.
48
+ */
43
49
  export function escapeLinkText(text: string): string {
44
50
  return text.replace(/[\\[\]]/g, (ch) => `\\${ch}`);
45
51
  }
46
52
 
47
- /** The cairn links a markdown body points at, in first-occurrence order, deduped by concept/id.
48
- * Parses the body as mdast, so a token inside a code span or fence is never matched. */
53
+ /**
54
+ * The cairn links a markdown body points at, in first-occurrence order, deduped by concept/id.
55
+ * Parses the body as mdast, so a token inside a code span or fence is never matched.
56
+ */
49
57
  export function extractCairnLinks(body: string): CairnRef[] {
50
58
  const tree = unified().use(remarkParse).use(remarkGfm).parse(body);
51
59
  const seen = new Set<string>();
@@ -20,9 +20,11 @@ export interface ManifestEntry {
20
20
  summary?: string;
21
21
  draft: boolean;
22
22
  links: CairnRef[];
23
- /** The content hashes of the media this entry references (its hero plus its body images). The
23
+ /**
24
+ * The content hashes of the media this entry references (its hero plus its body images). The
24
25
  * main side of the media where-used index. Additive and optional: an entry with no media omits
25
- * the key, and a manifest committed before this field still parses (absent reads as no refs). */
26
+ * the key, and a manifest committed before this field still parses (absent reads as no refs).
27
+ */
26
28
  mediaRefs?: string[];
27
29
  }
28
30
 
@@ -42,9 +44,11 @@ export interface LinkTarget {
42
44
  draft: boolean;
43
45
  }
44
46
 
45
- /** Build one manifest entry from a content file. Drafts are included and flagged. The id, date, and
47
+ /**
48
+ * Build one manifest entry from a content file. Drafts are included and flagged. The id, date, and
46
49
  * permalink come from entryIdentity, the same source content-index uses, so a cairn: link resolves to
47
- * one URL whether the admin preview reads the manifest or the public build reads the content index. */
50
+ * one URL whether the admin preview reads the manifest or the public build reads the content index.
51
+ */
48
52
  export function manifestEntryFromFile(descriptor: ConceptDescriptor, file: { path: string; raw: string }): ManifestEntry {
49
53
  const { frontmatter, body } = parseMarkdown(file.raw);
50
54
  const { id, date, permalink } = entryIdentity(descriptor, file.path, frontmatter);
@@ -75,8 +79,10 @@ function compareRef(a: CairnRef, b: CairnRef): number {
75
79
  return a.concept.localeCompare(b.concept) || a.id.localeCompare(b.id);
76
80
  }
77
81
 
78
- /** Serialize canonically: entries sorted by concept then id, links sorted and deduped, a fixed key
79
- * order, two-space pretty, and a trailing newline, so the committed file diffs cleanly in a PR. */
82
+ /**
83
+ * Serialize canonically: entries sorted by concept then id, links sorted and deduped, a fixed key
84
+ * order, two-space pretty, and a trailing newline, so the committed file diffs cleanly in a PR.
85
+ */
80
86
  export function serializeManifest(manifest: Manifest): string {
81
87
  const entries = [...manifest.entries].sort(compareRef).map((e) => ({
82
88
  id: e.id,
@@ -92,10 +98,12 @@ export function serializeManifest(manifest: Manifest): string {
92
98
  return `${JSON.stringify({ version: 1, entries }, null, 2)}\n`;
93
99
  }
94
100
 
95
- /** Parse a committed manifest. Throws on malformed JSON, a wrong version, or a malformed entry, so
101
+ /**
102
+ * Parse a committed manifest. Throws on malformed JSON, a wrong version, or a malformed entry, so
96
103
  * every reader (the save guard, the delete path, the preview) sees a well-formed graph or a clear
97
104
  * error. The build regenerates the manifest, so a real file is always canonical; this guards a
98
- * hand-edited or truncated one. */
105
+ * hand-edited or truncated one.
106
+ */
99
107
  export function parseManifest(raw: string): Manifest {
100
108
  const data = JSON.parse(raw) as unknown;
101
109
  if (!data || typeof data !== 'object') {
@@ -163,9 +171,11 @@ export interface ManifestDiff {
163
171
 
164
172
  const keyOf = (e: ManifestEntry) => `${e.concept}/${e.id}`;
165
173
 
166
- /** Compare a built manifest against a committed one, keyed by concept+id (the same identity
174
+ /**
175
+ * Compare a built manifest against a committed one, keyed by concept+id (the same identity
167
176
  * upsertEntry and removeEntry use). A changed entry names the fields that differ. Pure, so it is
168
- * unit-tested apart from any build. */
177
+ * unit-tested apart from any build.
178
+ */
169
179
  export function diffManifests(built: Manifest, committed: Manifest): ManifestDiff {
170
180
  const builtByKey = new Map(built.entries.map((e) => [keyOf(e), e]));
171
181
  const committedByKey = new Map(committed.entries.map((e) => [keyOf(e), e]));
@@ -195,10 +205,12 @@ function formatDiff(d: ManifestDiff): string {
195
205
  return lines.join('\n');
196
206
  }
197
207
 
198
- /** Throw if the committed manifest drifts from what the corpus says. The canonical serialized form
208
+ /**
209
+ * Throw if the committed manifest drifts from what the corpus says. The canonical serialized form
199
210
  * is the fast-path equality guard, so semantic equality never spuriously fails. On a mismatch the
200
211
  * error names the added, removed, and changed entries, so a raw-git content edit that leaves the
201
- * committed manifest stale fails the build loudly with what drifted. */
212
+ * committed manifest stale fails the build loudly with what drifted.
213
+ */
202
214
  export function verifyManifest(built: Manifest, committedRaw: string): void {
203
215
  const builtRaw = serializeManifest(built);
204
216
  if (committedRaw === builtRaw) return;
@@ -236,8 +248,10 @@ export function verifyManifest(built: Manifest, committedRaw: string): void {
236
248
  );
237
249
  }
238
250
 
239
- /** Replace the entry with the same concept and id, or add it. Order does not matter, since
240
- * serializeManifest sorts. This is the save path's incremental patch. */
251
+ /**
252
+ * Replace the entry with the same concept and id, or add it. Order does not matter, since
253
+ * serializeManifest sorts. This is the save path's incremental patch.
254
+ */
241
255
  export function upsertEntry(manifest: Manifest, entry: ManifestEntry): Manifest {
242
256
  const entries = manifest.entries.filter((e) => !(e.concept === entry.concept && e.id === entry.id));
243
257
  entries.push(entry);
@@ -257,9 +271,11 @@ export interface InboundLink {
257
271
  permalink: string;
258
272
  }
259
273
 
260
- /** Every entry whose outbound edges point at the target, excluding the target itself. The delete
274
+ /**
275
+ * Every entry whose outbound edges point at the target, excluding the target itself. The delete
261
276
  * guard reads this to name "what links here"; the backlinks panel will reuse it. Pure over the
262
- * manifest, so the request-time delete path and a unit test call it the same way. */
277
+ * manifest, so the request-time delete path and a unit test call it the same way.
278
+ */
263
279
  export function inboundLinks(manifest: Manifest, concept: string, id: string): InboundLink[] {
264
280
  return manifest.entries
265
281
  .filter((e) => !(e.concept === concept && e.id === id))
@@ -267,8 +283,10 @@ export function inboundLinks(manifest: Manifest, concept: string, id: string): I
267
283
  .map((e) => ({ concept: e.concept, id: e.id, title: e.title, permalink: e.permalink }));
268
284
  }
269
285
 
270
- /** A resolver backed by manifest targets, for the admin preview. A miss returns undefined, so the
271
- * render step marks the link broken rather than throwing. The build resolver throws instead. */
286
+ /**
287
+ * A resolver backed by manifest targets, for the admin preview. A miss returns undefined, so the
288
+ * render step marks the link broken rather than throwing. The build resolver throws instead.
289
+ */
272
290
  export function manifestLinkResolver(targets: { concept: string; id: string; permalink: string }[]): LinkResolve {
273
291
  const byKey = new Map(targets.map((t) => [`${t.concept}/${t.id}`, t.permalink]));
274
292
  return (ref) => byKey.get(`${ref.concept}/${ref.id}`);
@@ -20,11 +20,13 @@ import { visit } from 'unist-util-visit';
20
20
  import { parseMediaToken } from '../media/reference.js';
21
21
  import type { FrontmatterField, ImageValue } from './types.js';
22
22
 
23
- /** The content hashes one entry references, in first-occurrence order, deduped by hash. Reads the
23
+ /**
24
+ * The content hashes one entry references, in first-occurrence order, deduped by hash. Reads the
24
25
  * frontmatter hero `image.src` for each `image`-typed field plus every body image node. A
25
26
  * non-media or malformed token is skipped, never thrown, so a stray `![](/x.png)` does not break
26
27
  * the manifest build. The body is parsed as mdast, so a `media:` token inside a code span or fence
27
- * is never matched. */
28
+ * is never matched.
29
+ */
28
30
  export function extractMediaRefs(
29
31
  frontmatter: Record<string, unknown>,
30
32
  body: string,