@glw907/cairn-cms 0.58.0 → 0.60.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 (97) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/components/CairnAdmin.svelte +3 -0
  3. package/dist/components/CairnMediaLibrary.svelte +1101 -27
  4. package/dist/components/CairnMediaLibrary.svelte.d.ts +10 -2
  5. package/dist/components/CairnTidySettings.svelte +553 -0
  6. package/dist/components/CairnTidySettings.svelte.d.ts +32 -0
  7. package/dist/components/EditPage.svelte +371 -2
  8. package/dist/components/MarkdownEditor.svelte +168 -1
  9. package/dist/components/MarkdownEditor.svelte.d.ts +44 -0
  10. package/dist/components/TidyReview.svelte +463 -0
  11. package/dist/components/TidyReview.svelte.d.ts +47 -0
  12. package/dist/components/admin-icons.d.ts +1 -0
  13. package/dist/components/admin-icons.js +1 -0
  14. package/dist/components/cairn-admin.css +913 -2
  15. package/dist/components/editor-tidy.d.ts +31 -0
  16. package/dist/components/editor-tidy.js +199 -0
  17. package/dist/components/index.d.ts +1 -0
  18. package/dist/components/index.js +1 -0
  19. package/dist/components/markdown-directives.d.ts +16 -0
  20. package/dist/components/markdown-directives.js +34 -0
  21. package/dist/components/objective-errors.d.ts +30 -0
  22. package/dist/components/objective-errors.js +113 -0
  23. package/dist/components/spellcheck-assets/dictionary-en-us.txt +104743 -0
  24. package/dist/components/spellcheck-assets/spellchecker-wasm-LICENSE.txt +21 -0
  25. package/dist/components/spellcheck-assets/spellchecker-wasm.wasm +0 -0
  26. package/dist/components/spellcheck-worker.d.ts +80 -0
  27. package/dist/components/spellcheck-worker.js +161 -0
  28. package/dist/components/spellcheck.d.ts +146 -0
  29. package/dist/components/spellcheck.js +541 -0
  30. package/dist/components/tidy-categorize.d.ts +67 -0
  31. package/dist/components/tidy-categorize.js +392 -0
  32. package/dist/components/tidy-diff.d.ts +60 -0
  33. package/dist/components/tidy-diff.js +147 -0
  34. package/dist/components/tidy-validate.d.ts +37 -0
  35. package/dist/components/tidy-validate.js +174 -0
  36. package/dist/content/compose.d.ts +1 -1
  37. package/dist/content/compose.js +11 -0
  38. package/dist/content/site-dictionary.d.ts +31 -0
  39. package/dist/content/site-dictionary.js +82 -0
  40. package/dist/content/types.d.ts +25 -0
  41. package/dist/doctor/checks-local.d.ts +1 -0
  42. package/dist/doctor/checks-local.js +55 -6
  43. package/dist/doctor/index.js +2 -1
  44. package/dist/log/events.d.ts +1 -1
  45. package/dist/media/bulk-delete-plan.d.ts +24 -0
  46. package/dist/media/bulk-delete-plan.js +25 -0
  47. package/dist/media/orphan-scan.d.ts +37 -0
  48. package/dist/media/orphan-scan.js +42 -0
  49. package/dist/media/reconcile.d.ts +3 -0
  50. package/dist/media/reconcile.js +3 -2
  51. package/dist/nav/site-config.d.ts +98 -0
  52. package/dist/nav/site-config.js +132 -0
  53. package/dist/sveltekit/admin-dispatch.d.ts +2 -0
  54. package/dist/sveltekit/admin-dispatch.js +6 -2
  55. package/dist/sveltekit/cairn-admin.d.ts +16 -1
  56. package/dist/sveltekit/cairn-admin.js +28 -3
  57. package/dist/sveltekit/content-routes.d.ts +171 -4
  58. package/dist/sveltekit/content-routes.js +597 -3
  59. package/dist/sveltekit/index.d.ts +1 -1
  60. package/dist/sveltekit/tidy-prompt.d.ts +11 -0
  61. package/dist/sveltekit/tidy-prompt.js +118 -0
  62. package/package.json +10 -1
  63. package/src/lib/components/CairnAdmin.svelte +3 -0
  64. package/src/lib/components/CairnMediaLibrary.svelte +1101 -27
  65. package/src/lib/components/CairnTidySettings.svelte +553 -0
  66. package/src/lib/components/EditPage.svelte +371 -2
  67. package/src/lib/components/MarkdownEditor.svelte +168 -1
  68. package/src/lib/components/TidyReview.svelte +463 -0
  69. package/src/lib/components/admin-icons.ts +1 -0
  70. package/src/lib/components/cairn-admin.css +25 -0
  71. package/src/lib/components/editor-tidy.ts +241 -0
  72. package/src/lib/components/index.ts +1 -0
  73. package/src/lib/components/markdown-directives.ts +35 -0
  74. package/src/lib/components/objective-errors.ts +155 -0
  75. package/src/lib/components/spellcheck-assets/dictionary-en-us.txt +104743 -0
  76. package/src/lib/components/spellcheck-assets/spellchecker-wasm-LICENSE.txt +21 -0
  77. package/src/lib/components/spellcheck-assets/spellchecker-wasm.wasm +0 -0
  78. package/src/lib/components/spellcheck-worker.ts +279 -0
  79. package/src/lib/components/spellcheck.ts +679 -0
  80. package/src/lib/components/tidy-categorize.ts +460 -0
  81. package/src/lib/components/tidy-diff.ts +196 -0
  82. package/src/lib/components/tidy-validate.ts +202 -0
  83. package/src/lib/content/compose.ts +11 -1
  84. package/src/lib/content/site-dictionary.ts +84 -0
  85. package/src/lib/content/types.ts +25 -0
  86. package/src/lib/doctor/checks-local.ts +59 -5
  87. package/src/lib/doctor/index.ts +2 -0
  88. package/src/lib/log/events.ts +9 -1
  89. package/src/lib/media/bulk-delete-plan.ts +54 -0
  90. package/src/lib/media/orphan-scan.ts +74 -0
  91. package/src/lib/media/reconcile.ts +3 -2
  92. package/src/lib/nav/site-config.ts +197 -0
  93. package/src/lib/sveltekit/admin-dispatch.ts +7 -3
  94. package/src/lib/sveltekit/cairn-admin.ts +38 -4
  95. package/src/lib/sveltekit/content-routes.ts +795 -7
  96. package/src/lib/sveltekit/index.ts +1 -0
  97. package/src/lib/sveltekit/tidy-prompt.ts +153 -0
@@ -1,11 +1,14 @@
1
1
  import { fail } from '@sveltejs/kit';
2
2
  import { type GithubKeyEnv } from '../github/credentials.js';
3
3
  import { type LinkTarget, type InboundLink } from '../content/manifest.js';
4
+ import type { TidyConventions } from '../nav/site-config.js';
4
5
  import type { MediaEntry } from '../media/manifest.js';
5
6
  import type { MediaLibrary, MediaLibraryEntry } from '../media/library-entry.js';
6
7
  import type { UsageEntry } from '../media/usage.js';
8
+ import { type OrphanScan } from '../media/orphan-scan.js';
7
9
  import type { RepointPlacement, AltPlacement } from '../content/media-rewrite.js';
8
10
  import type { BranchRef } from '../media/rewrite-plan.js';
11
+ import type { BulkDeleteSkip } from '../media/bulk-delete-plan.js';
9
12
  import type { CookieJar, EventBase } from './types.js';
10
13
  import type { CairnRuntime, FrontmatterField, ResolvedPreview } from '../content/types.js';
11
14
  import type { Role } from '../auth/types.js';
@@ -114,6 +117,26 @@ export interface EditData {
114
117
  * when one exists, applied over the top-level values); null when the site sets none, which
115
118
  * leaves the frame rendering unstyled markup behind a hint. */
116
119
  preview: ResolvedPreview | null;
120
+ /** The spellcheck dictionary file for the site's configured dialect (default US English), resolved
121
+ * once at compose. The editor resolves it to a real asset URL on the main thread and hands that URL
122
+ * to the spellcheck Worker's `init`, the same way `mediaLibrary` is threaded in. Just the filename,
123
+ * e.g. "dictionary-en-us.txt". */
124
+ spellcheckDictionary: string;
125
+ /** The committed personal-dictionary words for the site (spec 1.6): the durable, shared, reviewable
126
+ * layer the editor seeds the spellcheck Worker's personal set from, the way `mediaLibrary` is handed
127
+ * in. Read from the git-committed `dictionary.txt` at editor load; empty when the file is absent or
128
+ * unreadable (the editor degrades to dialect-only). The dialect dictionary and the session ignore
129
+ * list are the other two layers; only this one is committed. */
130
+ siteDictionary: string[];
131
+ /** The editor-tier tidy facts the review surface needs (spec 2.5): whether tidy is enabled, the model
132
+ * that runs (for the head pill), and the RESOLVED conventions (the only data source for a
133
+ * normalization's because-line and the local category inference). The API key never appears here, it
134
+ * is a Worker secret. `enabled` false hides the Tidy control. */
135
+ tidy: {
136
+ enabled: boolean;
137
+ model: string;
138
+ conventions: TidyConventions;
139
+ };
117
140
  }
118
141
  /** One asset's where-used overlay, kept separate from MediaLibraryEntry so the picker's shared
119
142
  * projection stays decoupled from the Library-only usage facts. */
@@ -135,13 +158,49 @@ export interface MediaLibraryData {
135
158
  * redirected commit conflict never overwrite each other. */
136
159
  error: string | null;
137
160
  /** The success flash a redirected action carries: `deleted` from `?deleted=1`, `updated` from
138
- * `?updated=1`, `replaced` from `?replaced=1`, `altPropagated` from `?altPropagated=1`, null
139
- * otherwise. The component renders a polite success strip for each. */
140
- flash: 'deleted' | 'updated' | 'replaced' | 'altPropagated' | null;
161
+ * `?updated=1`, `replaced` from `?replaced=1`, `altPropagated` from `?altPropagated=1`,
162
+ * `bulkDeleted` from `?bulkDeleted=1`, `orphansPurged` from `?orphansPurged=1`, null otherwise.
163
+ * The component renders a polite success strip for each. */
164
+ flash: 'deleted' | 'updated' | 'replaced' | 'altPropagated' | 'bulkDeleted' | 'orphansPurged' | null;
141
165
  /** A redirected action's conflict error read from `?error=` (a commit-conflict bounce). Kept in
142
166
  * its own slot rather than the degraded-load `error` above, so the two never collide. */
143
167
  flashError: string | null;
144
168
  }
169
+ /** The two-tier tidy settings load (spec 2.8, Task 15). The developer tier is read-only: `enabled`,
170
+ * `keyConfigured`, and `model`/`modelLabel` are deploy-time facts the editor sees but cannot change.
171
+ * The editor tier is the resolved `conventions` block, written back through the save. The visibility
172
+ * gate is truthful: `enabled` is true only when `tidy.enabled` is set AND the API key is present, so
173
+ * the screen renders the convention list only then and the honest gate note otherwise. The key is a
174
+ * Worker secret, so `keyConfigured` is the presence of `ANTHROPIC_API_KEY` in the load's env, never
175
+ * the key itself; nothing here returns or logs the secret. */
176
+ export interface SettingsData {
177
+ /** The truthful gate: tidy is enabled AND the API key is present. The screen renders the editor
178
+ * tier only when this is true, and the honest gate note (a labelled region, no disabled controls)
179
+ * otherwise. */
180
+ enabled: boolean;
181
+ /** Whether `tidy.enabled` is set in the site config, independent of the key. The gate note's
182
+ * checklist reads this to show which deploy-time step is still open. */
183
+ tidyEnabled: boolean;
184
+ /** Whether the API key secret is present in the Worker env. A presence flag, never the key. */
185
+ keyConfigured: boolean;
186
+ /** The model id (a developer-tier fact, read-only on the screen). */
187
+ model: string;
188
+ /** A plain-language label for the model id ("Claude Sonnet"), so the read-only fact is not a bare
189
+ * jargon token. Falls back to the raw id for an unknown model. */
190
+ modelLabel: string;
191
+ /** The resolved editor-tier conventions: every field concrete, the screen's initial control state.
192
+ * Present only when the gate is open; the gate state needs no conventions. */
193
+ conventions: TidyConventions;
194
+ /** The success flash a redirected save carries (`?saved=1`). */
195
+ saved: boolean;
196
+ /** A redirected save's validation or conflict error read from `?error=`. */
197
+ error: string | null;
198
+ }
199
+ /** A refused settings save: a conflict bounce or a malformed conventions payload. Just the one-line
200
+ * summary; the save commits nothing on a refusal. */
201
+ export interface SettingsSaveFailure {
202
+ error: string;
203
+ }
145
204
  /** The structural event the content routes read; a real SvelteKit RequestEvent satisfies it. */
146
205
  export interface ContentEvent extends EventBase<GithubKeyEnv> {
147
206
  params: Record<string, string>;
@@ -150,10 +209,71 @@ export interface ContentEvent extends EventBase<GithubKeyEnv> {
150
209
  cookies?: CookieJar;
151
210
  }
152
211
  /** Injectable dependencies; tests stub the token mint to avoid signing a real key. */
212
+ /** The minimal Anthropic client surface the tidy action uses, typed structurally so the SDK's deep
213
+ * generics never reach a public signature and so the integration test can inject a fake whose
214
+ * `messages.create` it stubs. The real factory builds `new Anthropic({ apiKey })`, which satisfies
215
+ * this shape. The success path reads only the text blocks, the model, the stop reason, and the usage
216
+ * counts. */
217
+ export interface TidyClient {
218
+ messages: {
219
+ create(body: {
220
+ model: string;
221
+ max_tokens: number;
222
+ system: string;
223
+ messages: {
224
+ role: 'user';
225
+ content: string;
226
+ }[];
227
+ }, options?: {
228
+ signal?: AbortSignal;
229
+ }): Promise<{
230
+ content: {
231
+ type: string;
232
+ text?: string;
233
+ }[];
234
+ model: string;
235
+ stop_reason: string | null;
236
+ usage: {
237
+ input_tokens: number;
238
+ output_tokens: number;
239
+ };
240
+ }>;
241
+ };
242
+ }
153
243
  export interface ContentRoutesDeps {
154
244
  /** Mint a GitHub App installation token from the Worker env. Defaults to the real signer.
155
245
  * A bare string works too; the routes await whatever comes back. */
156
246
  mintToken?: (env: GithubKeyEnv) => string | Promise<string>;
247
+ /** Build the Anthropic client for the tidy action from the resolved API key. Defaults to the real
248
+ * SDK client. Injected in tests so `messages.create` is stubbed and no network call (or real key)
249
+ * is ever needed. The factory runs only after the key is read from the env, so a disabled or
250
+ * unconfigured site never constructs a client. */
251
+ anthropic?: (opts: {
252
+ apiKey: string;
253
+ }) => TidyClient;
254
+ /** The tidy action's own request deadline in milliseconds, set shorter than the platform limit so a
255
+ * slow model call becomes a clean retryable fail(502) rather than a platform timeout. Defaults to
256
+ * {@link DEFAULT_TIDY_TIMEOUT_MS}. Overridable in tests to assert the deadline path without waiting. */
257
+ tidyTimeoutMs?: number;
258
+ }
259
+ /** The successful tidy outcome (spec 2.1): the corrected markdown, the model that produced it, and the
260
+ * token usage. The diff is computed on the client (Task 12), so the server returns the plain text and
261
+ * commits nothing. Admin-internal: consumed by the editor's review surface, not on the package's
262
+ * sveltekit subpath, so it carries no reference page. */
263
+ export interface TidyResult {
264
+ corrected: string;
265
+ model: string;
266
+ usage: {
267
+ input_tokens: number;
268
+ output_tokens: number;
269
+ };
270
+ }
271
+ /** A refused tidy: `fail(403)` on a failed CSRF check, `fail(503)` when tidy is disabled or the API
272
+ * key is missing, `fail(413)` for an over-long body, `fail(502)` for a deadline overrun, abort, or
273
+ * model error (all retryable), `fail(422)` for a model refusal, `fail(400)` for a malformed body. Just
274
+ * the one-line summary; the action commits nothing, so a refusal can never corrupt the entry. */
275
+ export interface TidyFailure {
276
+ error: string;
157
277
  }
158
278
  /** A blocked save or publish: `fail(400)` when the body links to a target absent from main. */
159
279
  export interface SaveFailure {
@@ -212,6 +332,46 @@ export interface MediaReplaceFailure {
212
332
  export interface MediaAltPropagateFailure {
213
333
  error: string;
214
334
  }
335
+ /** The personal-dictionary add outcome (spec 1.6): the merged, canonical sorted word list after the
336
+ * add landed. The client reconciles its pending-additions set against this (a word now in the list is
337
+ * committed and dropped from pending). Admin-internal: exported for the editor host's reconcile, not
338
+ * on the package's sveltekit subpath, so it carries no reference page. */
339
+ export interface DictionaryAddResult {
340
+ words: string[];
341
+ }
342
+ /** A refused personal-dictionary add: `fail(403)` on a failed CSRF check, `fail(400)` on a body that
343
+ * carries no valid word. The client keeps its pending additions for the session and re-attempts on
344
+ * the next save, so the word is never silently dropped. Just the one-line summary. */
345
+ export interface DictionaryAddFailure {
346
+ error: string;
347
+ }
348
+ /** A refused media bulk delete or orphan purge: `fail(503)` for the fail-closed strict-usage refusal
349
+ * (the whole batch refuses) or media-off / a missing bucket binding. The per-item outcomes ride the
350
+ * returned summary, not a fail. */
351
+ export interface MediaBulkFailure {
352
+ error: string;
353
+ }
354
+ /** The bulk-delete outcome the component renders: the deleted hashes, the skipped rows from the
355
+ * partition (with their reason and where-used), and any per-object R2 delete failure. Admin-internal,
356
+ * not on the package subpath, so no reference page. */
357
+ export interface MediaBulkDeleteResult {
358
+ deleted: string[];
359
+ skipped: BulkDeleteSkip[];
360
+ failed: {
361
+ hash: string;
362
+ error: string;
363
+ }[];
364
+ }
365
+ /** The orphan-purge outcome: the purged R2 keys, the keys skipped because their hash was claimed by a
366
+ * manifest row since the scan, and any per-object delete failure. Admin-internal, no reference page. */
367
+ export interface MediaOrphanPurgeResult {
368
+ purged: string[];
369
+ skippedClaimed: string[];
370
+ failed: {
371
+ key: string;
372
+ error: string;
373
+ }[];
374
+ }
215
375
  /** One entry the replace preview will rewrite, enriched with its display title and permalink from the
216
376
  * content manifest (the planner's PlannedEntry carries neither). The screen lists these as the
217
377
  * confirm dialog's where-touched preview, and the apply re-derives its own plan rather than trusting
@@ -275,7 +435,7 @@ export interface MediaAltPreviewPlan {
275
435
  * keys identify which guard refused. The media refusals ride here too, so the Media Library's one
276
436
  * `form` prop carries a `?/mediaDelete`, `?/mediaUpdate`, `?/mediaReplace`, or `?/mediaAltPropagate`
277
437
  * refusal without a second type. */
278
- export type ContentFormFailure = Partial<SaveFailure & DeleteRefusal & RenameFailure & MediaDeleteRefusal & MediaUpdateFailure & MediaReplaceFailure & MediaAltPropagateFailure>;
438
+ export type ContentFormFailure = Partial<SaveFailure & DeleteRefusal & RenameFailure & MediaDeleteRefusal & MediaUpdateFailure & MediaReplaceFailure & MediaAltPropagateFailure & MediaBulkFailure & TidyFailure>;
279
439
  /** The successful upload's response (`uploadAction`). The server-owned `record` rides the editor's
280
440
  * optimistic client state and commits with the entry at Save (the upload itself commits nothing).
281
441
  * `reused` is true when identical bytes were already stored, so the second upload did no second put;
@@ -291,6 +451,8 @@ export declare function createContentRoutes(runtime: CairnRuntime, deps?: Conten
291
451
  indexRedirect: () => never;
292
452
  listLoad: (event: ContentEvent) => Promise<ListData>;
293
453
  mediaLibraryLoad: (event: ContentEvent) => Promise<MediaLibraryData>;
454
+ settingsLoad: (event: ContentEvent) => SettingsData;
455
+ settingsSave: (event: ContentEvent) => Promise<never>;
294
456
  createAction: (event: ContentEvent) => Promise<never>;
295
457
  editLoad: (event: ContentEvent) => Promise<EditData>;
296
458
  saveAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
@@ -302,10 +464,15 @@ export declare function createContentRoutes(runtime: CairnRuntime, deps?: Conten
302
464
  renameAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
303
465
  uploadAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | UploadResult>;
304
466
  mediaDeleteAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
467
+ mediaBulkDelete: (event: ContentEvent) => Promise<ReturnType<typeof fail> | MediaBulkDeleteResult>;
468
+ mediaOrphanScan: (event: ContentEvent) => Promise<ReturnType<typeof fail> | OrphanScan>;
469
+ mediaPurgeOrphans: (event: ContentEvent) => Promise<ReturnType<typeof fail> | MediaOrphanPurgeResult>;
305
470
  mediaUpdateAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
306
471
  mediaReplacePreview: (event: ContentEvent) => Promise<ReturnType<typeof fail> | MediaReplacePreviewPlan>;
307
472
  mediaReplaceApply: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
308
473
  mediaAltPreview: (event: ContentEvent) => Promise<ReturnType<typeof fail> | MediaAltPreviewPlan>;
309
474
  mediaAltApply: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
475
+ addDictionaryWord: (event: ContentEvent) => Promise<ReturnType<typeof fail> | DictionaryAddResult>;
476
+ tidyAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | TidyResult>;
310
477
  mintToken: (env: GithubKeyEnv) => string | Promise<string>;
311
478
  };