@glw907/cairn-cms 0.68.0 → 0.76.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 (177) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/ambient.d.ts +2 -0
  3. package/dist/components/CairnAdmin.svelte.d.ts +2 -7
  4. package/dist/components/ComponentForm.svelte +44 -27
  5. package/dist/components/ComponentInsertDialog.svelte +5 -5
  6. package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
  7. package/dist/components/EditPage.svelte +29 -107
  8. package/dist/components/EditPage.svelte.d.ts +2 -7
  9. package/dist/components/EntryPicker.svelte +117 -0
  10. package/dist/components/EntryPicker.svelte.d.ts +35 -0
  11. package/dist/components/FieldInput.svelte +218 -0
  12. package/dist/components/FieldInput.svelte.d.ts +51 -0
  13. package/dist/components/IconPicker.svelte +2 -2
  14. package/dist/components/IconPicker.svelte.d.ts +2 -0
  15. package/dist/components/LinkPicker.svelte +8 -75
  16. package/dist/components/LinkPicker.svelte.d.ts +4 -5
  17. package/dist/components/MediaHeroField.svelte +8 -5
  18. package/dist/components/MediaHeroField.svelte.d.ts +4 -0
  19. package/dist/components/ObjectGroupField.svelte +54 -0
  20. package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
  21. package/dist/components/ReferenceField.svelte +94 -0
  22. package/dist/components/ReferenceField.svelte.d.ts +27 -0
  23. package/dist/components/RepeatableField.svelte +221 -0
  24. package/dist/components/RepeatableField.svelte.d.ts +53 -0
  25. package/dist/components/cairn-admin.css +4 -0
  26. package/dist/components/preview-doc.js +5 -1
  27. package/dist/components/tidy-validate.js +1 -1
  28. package/dist/content/adapter.js +18 -0
  29. package/dist/content/advisories.d.ts +2 -2
  30. package/dist/content/advisories.js +3 -5
  31. package/dist/content/compose.d.ts +7 -6
  32. package/dist/content/compose.js +26 -20
  33. package/dist/content/concepts.d.ts +21 -15
  34. package/dist/content/concepts.js +55 -32
  35. package/dist/content/field-rules.js +3 -4
  36. package/dist/content/fields.d.ts +49 -1
  37. package/dist/content/fields.js +11 -0
  38. package/dist/content/fieldset.d.ts +31 -10
  39. package/dist/content/fieldset.js +262 -109
  40. package/dist/content/frontmatter-region.d.ts +38 -0
  41. package/dist/content/frontmatter-region.js +75 -0
  42. package/dist/content/frontmatter.d.ts +35 -2
  43. package/dist/content/frontmatter.js +232 -11
  44. package/dist/content/manifest.d.ts +34 -0
  45. package/dist/content/manifest.js +80 -4
  46. package/dist/content/media-refs.d.ts +2 -2
  47. package/dist/content/media-rewrite.js +1 -69
  48. package/dist/content/reference-index.d.ts +56 -0
  49. package/dist/content/reference-index.js +95 -0
  50. package/dist/content/references.d.ts +40 -0
  51. package/dist/content/references.js +0 -0
  52. package/dist/content/standard-schema.d.ts +30 -0
  53. package/dist/content/standard-schema.js +4 -0
  54. package/dist/content/types.d.ts +127 -178
  55. package/dist/delivery/data.d.ts +2 -2
  56. package/dist/delivery/data.js +1 -1
  57. package/dist/delivery/public-routes.d.ts +2 -5
  58. package/dist/delivery/public-routes.js +15 -1
  59. package/dist/delivery/site-descriptors.d.ts +5 -1
  60. package/dist/delivery/site-descriptors.js +8 -3
  61. package/dist/delivery/site-indexes.d.ts +2 -2
  62. package/dist/delivery/site-resolver.d.ts +25 -0
  63. package/dist/delivery/site-resolver.js +49 -0
  64. package/dist/doctor/checks-local.js +6 -11
  65. package/dist/github/backend.d.ts +83 -0
  66. package/dist/github/backend.js +76 -0
  67. package/dist/github/credentials.d.ts +11 -5
  68. package/dist/github/credentials.js +3 -3
  69. package/dist/github/repo.d.ts +8 -19
  70. package/dist/github/repo.js +69 -80
  71. package/dist/github/types.d.ts +1 -1
  72. package/dist/github/types.js +4 -4
  73. package/dist/index.d.ts +16 -12
  74. package/dist/index.js +7 -8
  75. package/dist/islands/index.d.ts +12 -0
  76. package/dist/islands/index.js +83 -0
  77. package/dist/islands/types.d.ts +7 -0
  78. package/dist/islands/types.js +1 -0
  79. package/dist/media/rewrite-plan.d.ts +2 -3
  80. package/dist/media/rewrite-plan.js +2 -3
  81. package/dist/media/usage.d.ts +2 -2
  82. package/dist/media/usage.js +3 -5
  83. package/dist/nav/site-config.d.ts +0 -6
  84. package/dist/nav/site-config.js +6 -4
  85. package/dist/render/component-grammar.js +11 -11
  86. package/dist/render/component-reference.js +5 -3
  87. package/dist/render/component-validate.d.ts +4 -1
  88. package/dist/render/component-validate.js +10 -35
  89. package/dist/render/pipeline.d.ts +0 -6
  90. package/dist/render/pipeline.js +1 -1
  91. package/dist/render/registry.d.ts +34 -34
  92. package/dist/render/registry.js +26 -5
  93. package/dist/render/rehype-dispatch.d.ts +4 -4
  94. package/dist/render/rehype-dispatch.js +36 -11
  95. package/dist/render/remark-directives.js +4 -5
  96. package/dist/render/sanitize-schema.js +1 -1
  97. package/dist/sveltekit/cairn-admin.d.ts +5 -5
  98. package/dist/sveltekit/cairn-admin.js +3 -4
  99. package/dist/sveltekit/content-routes.d.ts +10 -8
  100. package/dist/sveltekit/content-routes.js +269 -181
  101. package/dist/sveltekit/health.d.ts +7 -3
  102. package/dist/sveltekit/health.js +9 -3
  103. package/dist/sveltekit/index.d.ts +1 -1
  104. package/dist/sveltekit/nav-routes.d.ts +6 -5
  105. package/dist/sveltekit/nav-routes.js +22 -20
  106. package/dist/sveltekit/types.d.ts +2 -0
  107. package/dist/vite/index.d.ts +3 -3
  108. package/dist/vite/index.js +17 -8
  109. package/package.json +5 -1
  110. package/src/lib/ambient.ts +7 -0
  111. package/src/lib/components/CairnAdmin.svelte +2 -6
  112. package/src/lib/components/ComponentForm.svelte +48 -27
  113. package/src/lib/components/ComponentInsertDialog.svelte +9 -8
  114. package/src/lib/components/EditPage.svelte +43 -119
  115. package/src/lib/components/EntryPicker.svelte +154 -0
  116. package/src/lib/components/FieldInput.svelte +262 -0
  117. package/src/lib/components/IconPicker.svelte +4 -2
  118. package/src/lib/components/LinkPicker.svelte +10 -81
  119. package/src/lib/components/MediaHeroField.svelte +12 -5
  120. package/src/lib/components/ObjectGroupField.svelte +97 -0
  121. package/src/lib/components/ReferenceField.svelte +126 -0
  122. package/src/lib/components/RepeatableField.svelte +310 -0
  123. package/src/lib/components/preview-doc.ts +5 -1
  124. package/src/lib/components/tidy-validate.ts +1 -1
  125. package/src/lib/content/adapter.ts +21 -0
  126. package/src/lib/content/advisories.ts +4 -7
  127. package/src/lib/content/compose.ts +30 -23
  128. package/src/lib/content/concepts.ts +68 -40
  129. package/src/lib/content/field-rules.ts +3 -4
  130. package/src/lib/content/fields.ts +52 -1
  131. package/src/lib/content/fieldset.ts +291 -128
  132. package/src/lib/content/frontmatter-region.ts +90 -0
  133. package/src/lib/content/frontmatter.ts +231 -15
  134. package/src/lib/content/manifest.ts +101 -4
  135. package/src/lib/content/media-refs.ts +2 -2
  136. package/src/lib/content/media-rewrite.ts +7 -80
  137. package/src/lib/content/reference-index.ts +159 -0
  138. package/src/lib/content/references.ts +0 -0
  139. package/src/lib/content/standard-schema.ts +25 -0
  140. package/src/lib/content/types.ts +128 -195
  141. package/src/lib/delivery/data.ts +2 -2
  142. package/src/lib/delivery/public-routes.ts +17 -3
  143. package/src/lib/delivery/site-descriptors.ts +8 -3
  144. package/src/lib/delivery/site-indexes.ts +2 -2
  145. package/src/lib/delivery/site-resolver.ts +64 -0
  146. package/src/lib/doctor/checks-local.ts +6 -14
  147. package/src/lib/github/backend.ts +161 -0
  148. package/src/lib/github/credentials.ts +10 -7
  149. package/src/lib/github/repo.ts +79 -83
  150. package/src/lib/github/types.ts +5 -5
  151. package/src/lib/index.ts +38 -23
  152. package/src/lib/islands/index.ts +84 -0
  153. package/src/lib/islands/types.ts +11 -0
  154. package/src/lib/media/rewrite-plan.ts +4 -6
  155. package/src/lib/media/usage.ts +4 -7
  156. package/src/lib/nav/site-config.ts +8 -9
  157. package/src/lib/render/component-grammar.ts +10 -10
  158. package/src/lib/render/component-reference.ts +4 -3
  159. package/src/lib/render/component-validate.ts +10 -35
  160. package/src/lib/render/pipeline.ts +1 -7
  161. package/src/lib/render/registry.ts +58 -39
  162. package/src/lib/render/rehype-dispatch.ts +45 -10
  163. package/src/lib/render/remark-directives.ts +4 -5
  164. package/src/lib/render/sanitize-schema.ts +1 -1
  165. package/src/lib/sveltekit/cairn-admin.ts +8 -9
  166. package/src/lib/sveltekit/content-routes.ts +330 -221
  167. package/src/lib/sveltekit/health.ts +13 -6
  168. package/src/lib/sveltekit/index.ts +2 -2
  169. package/src/lib/sveltekit/nav-routes.ts +33 -29
  170. package/src/lib/sveltekit/types.ts +5 -1
  171. package/src/lib/vite/index.ts +20 -11
  172. package/dist/content/schema.d.ts +0 -87
  173. package/dist/content/schema.js +0 -85
  174. package/dist/content/validate.d.ts +0 -17
  175. package/dist/content/validate.js +0 -93
  176. package/src/lib/content/schema.ts +0 -163
  177. package/src/lib/content/validate.ts +0 -90
@@ -25,6 +25,13 @@ import type { Image, Root } from 'mdast';
25
25
  import type { ContainerDirective } from 'mdast-util-directive';
26
26
  import { parseMediaToken } from '../media/reference.js';
27
27
  import { escapeLinkText } from './links.js';
28
+ import {
29
+ type FmLine,
30
+ splitFrontmatter,
31
+ fmLines,
32
+ frontmatterKeyRange,
33
+ escapeForRegExp,
34
+ } from './frontmatter-region.js';
28
35
 
29
36
  /** One repointed reference: which surface it lived on, the old token as written, and the new token. */
30
37
  export interface RepointPlacement {
@@ -73,18 +80,6 @@ function dropOverlappingEdits<T extends { start: number; end: number }>(edits: T
73
80
  */
74
81
  const MEDIA_TOKEN_SCAN = /media:[A-Za-z0-9._-]+/g;
75
82
 
76
- /**
77
- * Split a leading frontmatter block off the markdown. `fmBlock` is the `---` fenced block including
78
- * both fences and the trailing newline (empty when there is none); `body` is everything after it.
79
- * The block leads the document, so a frontmatter offset is already absolute and a body offset needs
80
- * `fmBlock.length` added. Shared by every arm so they agree on the boundary.
81
- */
82
- function splitFrontmatter(markdown: string): { fmBlock: string; body: string } {
83
- const m = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
84
- const fmBlock = m ? m[0] : '';
85
- return { fmBlock, body: markdown.slice(fmBlock.length) };
86
- }
87
-
88
83
  /**
89
84
  * Parse a doc with the figure-aware pipeline, so the body arm agrees with what remarkFigure renders
90
85
  * and can see the enclosing `:::figure` container. Mirrors parseFigureDoc in markdown-format.ts.
@@ -109,66 +104,6 @@ function inFigure(tree: Root, target: Image): boolean {
109
104
  return found;
110
105
  }
111
106
 
112
- /**
113
- * The split of fmBlock into its lines, each with its block-relative start and end offsets (the end
114
- * is the index of the trailing newline, or the block length for the last line). Block offsets are
115
- * already absolute since the frontmatter leads the document.
116
- */
117
- interface FmLine {
118
- start: number;
119
- end: number;
120
- }
121
-
122
- /**
123
- * Split fmBlock into lines once, so the locator helpers walk a shared structure instead of
124
- * re-scanning the block per call.
125
- */
126
- function fmLines(fmBlock: string): FmLine[] {
127
- const lines: FmLine[] = [];
128
- let pos = 0;
129
- while (pos <= fmBlock.length) {
130
- const nl = fmBlock.indexOf('\n', pos);
131
- const end = nl === -1 ? fmBlock.length : nl;
132
- lines.push({ start: pos, end });
133
- if (nl === -1) break;
134
- pos = nl + 1;
135
- }
136
- return lines;
137
- }
138
-
139
- /**
140
- * The inclusive line-index range `[lo, hi]` of the block-style mapping a top-level key opens: the
141
- * line `^<key>:` at indent 0 through the last line before the next top-level key (or the document
142
- * end). A flow-style value (`key: { ... }` all on one line) yields a single-line range. Returns null
143
- * when the key has no top-level line, which a malformed or non-canonical block can cause. Scoping the
144
- * per-key search to this range is what lets two image fields that share one hash, or an image field
145
- * whose hash also appears in a sibling text value, resolve to distinct, correct spans.
146
- */
147
- function frontmatterKeyRange(lines: FmLine[], fmBlock: string, key: string): [number, number] | null {
148
- const opener = new RegExp(`^${escapeForRegExp(key)}:`);
149
- const topLevelKey = /^[^\s#][^:]*:/;
150
- const isBoundary = (i: number) => {
151
- const text = fmBlock.slice(lines[i].start, lines[i].end);
152
- // A new top-level key or the closing `---` fence ends the current key's block.
153
- return topLevelKey.test(text) || text === '---';
154
- };
155
- let lo = -1;
156
- for (let i = 1; i < lines.length - 1; i += 1) {
157
- // Skip the leading `---` fence (line 0) and the trailing empty line after the closing fence.
158
- if (opener.test(fmBlock.slice(lines[i].start, lines[i].end))) {
159
- lo = i;
160
- break;
161
- }
162
- }
163
- if (lo === -1) return null;
164
- let hi = lo;
165
- for (let i = lo + 1; i < lines.length - 1; i += 1) {
166
- if (isBoundary(i)) break;
167
- hi = i;
168
- }
169
- return [lo, hi];
170
- }
171
-
172
107
  /**
173
108
  * A located `src:` line inside a block-style mapping: the line's start and end, its leading indent,
174
109
  * and the exact `media:` token's block-relative offsets and text.
@@ -470,14 +405,6 @@ function bodyAltEdits(body: string, blockLength: number, hash: string, defaultAl
470
405
  return edits;
471
406
  }
472
407
 
473
- /**
474
- * Escape a literal string for safe interpolation into a RegExp source. A key name or an indent is
475
- * matched literally, so its characters must not act as metacharacters.
476
- */
477
- function escapeForRegExp(literal: string): string {
478
- return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
479
- }
480
-
481
408
  /**
482
409
  * Find a sibling key line (`alt:` or `decorative:`) at exactly `indent` within the inclusive
483
410
  * line-index range `[lo, hi]` of one mapping. The range is the mapping's own block, so the search
@@ -0,0 +1,159 @@
1
+ // cairn-cms: the cross-branch reference index, the where-referenced core of the rename and delete
2
+ // gates. It answers "which entries reference this target entry" for every reference edge, keyed by the
3
+ // target's (concept, id) PAIR. The key is the pair and never an id alone (unlike media/usage.ts, which
4
+ // keys on a globally-unique content hash), because an id is unique only within a concept: pages/about
5
+ // and posts/about are distinct targets, and reverse-mapping by id alone would cross that boundary and
6
+ // refuse a rename or delete on a phantom inbound. The map unions two sources: the published corpus on
7
+ // main and every open cairn/* edit branch, so a target referenced only in an unpublished draft still
8
+ // counts as referenced and is not mistaken for safe to delete or freely rename.
9
+ //
10
+ // The main arm reads the content manifest's per-entry references (the edges manifestEntryFromFile
11
+ // records) and builds the reverse map; it never crawls the files, since the manifest already carries
12
+ // the edges. The branch arm cannot use a manifest (the content manifest is never committed to a
13
+ // branch), so it reconstructs each edited entry's path from the branch name, reads that one file, and
14
+ // runs the schema extractor directly.
15
+ import type { ConceptDescriptor } from './types.js';
16
+ import type { Backend } from '../github/backend.js';
17
+ import type { Manifest } from './manifest.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 { extractReferenceEdges } from './references.js';
23
+
24
+ /**
25
+ * Where a reference lives: the published corpus on main, or a named open edit branch. Re-declared here
26
+ * (rather than imported from media/usage.ts) so the content layer does not depend on the media layer.
27
+ */
28
+ export type UsageOrigin = { kind: 'published' } | { kind: 'branch'; branch: string };
29
+
30
+ /** One entry that references a target, in a shape the rename and delete gates name and group by. */
31
+ export interface ReferenceUsageEntry {
32
+ /** The referencing (source) entry's concept id, e.g. "posts". */
33
+ concept: string;
34
+ /** The referencing (source) entry's id (its filename stem). */
35
+ id: string;
36
+ /** The referencing entry's title for display, from the manifest (published) or frontmatter (branch). */
37
+ title: string;
38
+ /** The referencing entry's public permalink, present for a published entry (carried from the manifest). */
39
+ permalink?: string;
40
+ /** The frontmatter field the edge was declared on. */
41
+ field: string;
42
+ /** Published vs the cairn/* branch the edit lives on. */
43
+ origin: UsageOrigin;
44
+ }
45
+
46
+ /**
47
+ * The target's `${concept}/${id}` pair to the distinct entries that reference it. A pair with no row is
48
+ * not referenced anywhere the index could read (main plus the listed open branches).
49
+ */
50
+ export type ReferenceIndex = Map<string, ReferenceUsageEntry[]>;
51
+
52
+ /**
53
+ * Build options. `branches` lets a caller that already listed the open cairn/* branches pass them in so
54
+ * the index does not list them a second time. `strict` flips the per-branch read from degrade-and-skip
55
+ * to fail-closed: a delete or rename gate must not treat a transient branch-read failure as an absent
56
+ * reference, so it rethrows instead.
57
+ */
58
+ export interface BuildReferenceOptions {
59
+ /** The open cairn/* branch names, already listed. When present the index skips its own listing. */
60
+ branches?: string[];
61
+ /** When true a branch read that throws rejects the whole build, so the caller can fail closed. */
62
+ strict?: boolean;
63
+ }
64
+
65
+ /** Append a row under its target pair key, creating the bucket on first use. */
66
+ function push(index: ReferenceIndex, key: string, entry: ReferenceUsageEntry): void {
67
+ const rows = index.get(key);
68
+ if (rows) rows.push(entry);
69
+ else index.set(key, [entry]);
70
+ }
71
+
72
+ /**
73
+ * Build the pair-keyed reference index over main (from the manifest's per-entry references) plus every
74
+ * open cairn/* branch (parsed from its edited markdown).
75
+ *
76
+ * By default a single branch read that throws degrades that one branch and is skipped, the way the
77
+ * admin loaders degrade a failed read. That tolerance is wrong for the rename and delete gates: a
78
+ * transient branch-read failure would make a still-referenced target look free. Pass `strict: true` to
79
+ * rethrow a branch failure so the caller fails closed. Pass `branches` to reuse a branch list the
80
+ * caller already has rather than listing them a second time.
81
+ */
82
+ export async function buildReferenceIndex(
83
+ backend: Backend,
84
+ concepts: ConceptDescriptor[],
85
+ manifest: Manifest,
86
+ opts: BuildReferenceOptions = {},
87
+ ): Promise<ReferenceIndex> {
88
+ const index: ReferenceIndex = new Map();
89
+
90
+ // The main arm: the manifest already carries each entry's reference edges, so this is a pure reverse
91
+ // map with no per-file read. The KEY is the edge's TARGET (concept, id); the ROW is the source entry.
92
+ for (const entry of manifest.entries) {
93
+ for (const edge of entry.references ?? []) {
94
+ push(index, `${edge.concept}/${edge.id}`, {
95
+ concept: entry.concept,
96
+ id: entry.id,
97
+ title: entry.title,
98
+ permalink: entry.permalink,
99
+ field: edge.field,
100
+ origin: { kind: 'published' },
101
+ });
102
+ }
103
+ }
104
+
105
+ // The branch arm: read each open cairn/* branch's one edited file. The path is derivable from the
106
+ // branch name, so no tree-listing is needed. The branch list is reused when the caller passes it.
107
+ const names = opts.branches ?? (await backend.listBranches(PENDING_PREFIX));
108
+ // Read the branches in parallel rather than one at a time, so the latency floor is one round trip
109
+ // instead of N. workerd self-throttles to 6 simultaneous outbound connections, so this batch and
110
+ // the load path's media-union and linker reads each stay under the limit; do NOT run this fan-out
111
+ // concurrently with those (a future combined safety gate must not wrap them in one Promise.all),
112
+ // since the merged fan-out would queue behind that throttle.
113
+ const perBranch = await Promise.all(
114
+ names.map(async (name): Promise<{ key: string; entry: ReferenceUsageEntry }[]> => {
115
+ // Resolve the branch name to a configured entry with the same guard the branch tooling uses: a
116
+ // malformed name, an id that fails the slug rule (entry paths are built from it, so this is the
117
+ // path confinement), or a concept this site does not configure is skipped, no read attempted.
118
+ const ref = parsePendingBranch(name);
119
+ if (!ref || !isValidId(ref.id)) return [];
120
+ const concept = findConcept(concepts, ref.concept);
121
+ if (!concept) return [];
122
+
123
+ const path = `${concept.dir}/${filenameFromId(ref.id)}`;
124
+ try {
125
+ const raw = await backend.readFile(path, name);
126
+ if (raw === null) return []; // The file is absent on the branch: nothing to extract.
127
+ const { frontmatter } = parseMarkdown(raw);
128
+ const fmTitle = frontmatter.title;
129
+ const title = typeof fmTitle === 'string' && fmTitle.trim() ? fmTitle : ref.id;
130
+ const rows: { key: string; entry: ReferenceUsageEntry }[] = [];
131
+ for (const edge of extractReferenceEdges(frontmatter, concept.fields)) {
132
+ rows.push({
133
+ key: `${edge.concept}/${edge.id}`,
134
+ entry: {
135
+ concept: concept.id,
136
+ id: ref.id,
137
+ title,
138
+ field: edge.field,
139
+ origin: { kind: 'branch', branch: name },
140
+ },
141
+ });
142
+ }
143
+ return rows;
144
+ } catch (err) {
145
+ // In strict mode a branch failure fails the whole build so the gate can fail closed; otherwise
146
+ // degrade this one branch rather than sinking the screen.
147
+ if (opts.strict) throw err;
148
+ return [];
149
+ }
150
+ }),
151
+ );
152
+
153
+ // Fold the per-branch rows back in, preserving the branch order so the index reads stably.
154
+ for (const rows of perBranch) {
155
+ for (const { key, entry } of rows) push(index, key, entry);
156
+ }
157
+
158
+ return index;
159
+ }
Binary file
@@ -0,0 +1,25 @@
1
+ // cairn-cms: the Standard Schema conformance types, shared by both the v1 schema and the v2
2
+ // fieldset validators. They live here, apart from either validator, so the v2 `fieldset` keeps
3
+ // importing them once the v1 `schema.ts` is removed at the Contract v2 cutover.
4
+
5
+ /** The validate input the cairn adapter takes: the raw frontmatter and the body. */
6
+ export interface StandardInput {
7
+ frontmatter: Record<string, unknown>;
8
+ body: string;
9
+ }
10
+
11
+ /**
12
+ * A minimal local copy of the Standard Schema v1 interface (https://standardschema.dev), so the
13
+ * schema is a drop-in where the ecosystem accepts a validator, with no runtime dependency.
14
+ */
15
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
16
+ readonly '~standard': {
17
+ readonly version: 1;
18
+ readonly vendor: string;
19
+ readonly validate: (value: unknown) => StandardResult<Output>;
20
+ readonly types?: { readonly input: Input; readonly output: Output };
21
+ };
22
+ }
23
+ type StandardResult<Output> =
24
+ | { readonly value: Output; readonly issues?: undefined }
25
+ | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray<PropertyKey> }> };