@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.
- package/CHANGELOG.md +82 -0
- package/dist/ambient.d.ts +2 -0
- package/dist/components/CairnAdmin.svelte.d.ts +2 -7
- package/dist/components/ComponentForm.svelte +44 -27
- package/dist/components/ComponentInsertDialog.svelte +5 -5
- package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
- package/dist/components/EditPage.svelte +29 -107
- package/dist/components/EditPage.svelte.d.ts +2 -7
- package/dist/components/EntryPicker.svelte +117 -0
- package/dist/components/EntryPicker.svelte.d.ts +35 -0
- package/dist/components/FieldInput.svelte +218 -0
- package/dist/components/FieldInput.svelte.d.ts +51 -0
- package/dist/components/IconPicker.svelte +2 -2
- package/dist/components/IconPicker.svelte.d.ts +2 -0
- package/dist/components/LinkPicker.svelte +8 -75
- package/dist/components/LinkPicker.svelte.d.ts +4 -5
- package/dist/components/MediaHeroField.svelte +8 -5
- package/dist/components/MediaHeroField.svelte.d.ts +4 -0
- package/dist/components/ObjectGroupField.svelte +54 -0
- package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
- package/dist/components/ReferenceField.svelte +94 -0
- package/dist/components/ReferenceField.svelte.d.ts +27 -0
- package/dist/components/RepeatableField.svelte +221 -0
- package/dist/components/RepeatableField.svelte.d.ts +53 -0
- package/dist/components/cairn-admin.css +4 -0
- package/dist/components/preview-doc.js +5 -1
- package/dist/components/tidy-validate.js +1 -1
- package/dist/content/adapter.js +18 -0
- package/dist/content/advisories.d.ts +2 -2
- package/dist/content/advisories.js +3 -5
- package/dist/content/compose.d.ts +7 -6
- package/dist/content/compose.js +26 -20
- package/dist/content/concepts.d.ts +21 -15
- package/dist/content/concepts.js +55 -32
- package/dist/content/field-rules.js +3 -4
- package/dist/content/fields.d.ts +49 -1
- package/dist/content/fields.js +11 -0
- package/dist/content/fieldset.d.ts +31 -10
- package/dist/content/fieldset.js +262 -109
- package/dist/content/frontmatter-region.d.ts +38 -0
- package/dist/content/frontmatter-region.js +75 -0
- package/dist/content/frontmatter.d.ts +35 -2
- package/dist/content/frontmatter.js +232 -11
- package/dist/content/manifest.d.ts +34 -0
- package/dist/content/manifest.js +80 -4
- package/dist/content/media-refs.d.ts +2 -2
- package/dist/content/media-rewrite.js +1 -69
- package/dist/content/reference-index.d.ts +56 -0
- package/dist/content/reference-index.js +95 -0
- package/dist/content/references.d.ts +40 -0
- package/dist/content/references.js +0 -0
- package/dist/content/standard-schema.d.ts +30 -0
- package/dist/content/standard-schema.js +4 -0
- package/dist/content/types.d.ts +127 -178
- package/dist/delivery/data.d.ts +2 -2
- package/dist/delivery/data.js +1 -1
- package/dist/delivery/public-routes.d.ts +2 -5
- package/dist/delivery/public-routes.js +15 -1
- package/dist/delivery/site-descriptors.d.ts +5 -1
- package/dist/delivery/site-descriptors.js +8 -3
- package/dist/delivery/site-indexes.d.ts +2 -2
- package/dist/delivery/site-resolver.d.ts +25 -0
- package/dist/delivery/site-resolver.js +49 -0
- package/dist/doctor/checks-local.js +6 -11
- package/dist/github/backend.d.ts +83 -0
- package/dist/github/backend.js +76 -0
- package/dist/github/credentials.d.ts +11 -5
- package/dist/github/credentials.js +3 -3
- package/dist/github/repo.d.ts +8 -19
- package/dist/github/repo.js +69 -80
- package/dist/github/types.d.ts +1 -1
- package/dist/github/types.js +4 -4
- package/dist/index.d.ts +16 -12
- package/dist/index.js +7 -8
- package/dist/islands/index.d.ts +12 -0
- package/dist/islands/index.js +83 -0
- package/dist/islands/types.d.ts +7 -0
- package/dist/islands/types.js +1 -0
- package/dist/media/rewrite-plan.d.ts +2 -3
- package/dist/media/rewrite-plan.js +2 -3
- package/dist/media/usage.d.ts +2 -2
- package/dist/media/usage.js +3 -5
- package/dist/nav/site-config.d.ts +0 -6
- package/dist/nav/site-config.js +6 -4
- package/dist/render/component-grammar.js +11 -11
- package/dist/render/component-reference.js +5 -3
- package/dist/render/component-validate.d.ts +4 -1
- package/dist/render/component-validate.js +10 -35
- package/dist/render/pipeline.d.ts +0 -6
- package/dist/render/pipeline.js +1 -1
- package/dist/render/registry.d.ts +34 -34
- package/dist/render/registry.js +26 -5
- package/dist/render/rehype-dispatch.d.ts +4 -4
- package/dist/render/rehype-dispatch.js +36 -11
- package/dist/render/remark-directives.js +4 -5
- package/dist/render/sanitize-schema.js +1 -1
- package/dist/sveltekit/cairn-admin.d.ts +5 -5
- package/dist/sveltekit/cairn-admin.js +3 -4
- package/dist/sveltekit/content-routes.d.ts +10 -8
- package/dist/sveltekit/content-routes.js +269 -181
- package/dist/sveltekit/health.d.ts +7 -3
- package/dist/sveltekit/health.js +9 -3
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/nav-routes.d.ts +6 -5
- package/dist/sveltekit/nav-routes.js +22 -20
- package/dist/sveltekit/types.d.ts +2 -0
- package/dist/vite/index.d.ts +3 -3
- package/dist/vite/index.js +17 -8
- package/package.json +5 -1
- package/src/lib/ambient.ts +7 -0
- package/src/lib/components/CairnAdmin.svelte +2 -6
- package/src/lib/components/ComponentForm.svelte +48 -27
- package/src/lib/components/ComponentInsertDialog.svelte +9 -8
- package/src/lib/components/EditPage.svelte +43 -119
- package/src/lib/components/EntryPicker.svelte +154 -0
- package/src/lib/components/FieldInput.svelte +262 -0
- package/src/lib/components/IconPicker.svelte +4 -2
- package/src/lib/components/LinkPicker.svelte +10 -81
- package/src/lib/components/MediaHeroField.svelte +12 -5
- package/src/lib/components/ObjectGroupField.svelte +97 -0
- package/src/lib/components/ReferenceField.svelte +126 -0
- package/src/lib/components/RepeatableField.svelte +310 -0
- package/src/lib/components/preview-doc.ts +5 -1
- package/src/lib/components/tidy-validate.ts +1 -1
- package/src/lib/content/adapter.ts +21 -0
- package/src/lib/content/advisories.ts +4 -7
- package/src/lib/content/compose.ts +30 -23
- package/src/lib/content/concepts.ts +68 -40
- package/src/lib/content/field-rules.ts +3 -4
- package/src/lib/content/fields.ts +52 -1
- package/src/lib/content/fieldset.ts +291 -128
- package/src/lib/content/frontmatter-region.ts +90 -0
- package/src/lib/content/frontmatter.ts +231 -15
- package/src/lib/content/manifest.ts +101 -4
- package/src/lib/content/media-refs.ts +2 -2
- package/src/lib/content/media-rewrite.ts +7 -80
- package/src/lib/content/reference-index.ts +159 -0
- package/src/lib/content/references.ts +0 -0
- package/src/lib/content/standard-schema.ts +25 -0
- package/src/lib/content/types.ts +128 -195
- package/src/lib/delivery/data.ts +2 -2
- package/src/lib/delivery/public-routes.ts +17 -3
- package/src/lib/delivery/site-descriptors.ts +8 -3
- package/src/lib/delivery/site-indexes.ts +2 -2
- package/src/lib/delivery/site-resolver.ts +64 -0
- package/src/lib/doctor/checks-local.ts +6 -14
- package/src/lib/github/backend.ts +161 -0
- package/src/lib/github/credentials.ts +10 -7
- package/src/lib/github/repo.ts +79 -83
- package/src/lib/github/types.ts +5 -5
- package/src/lib/index.ts +38 -23
- package/src/lib/islands/index.ts +84 -0
- package/src/lib/islands/types.ts +11 -0
- package/src/lib/media/rewrite-plan.ts +4 -6
- package/src/lib/media/usage.ts +4 -7
- package/src/lib/nav/site-config.ts +8 -9
- package/src/lib/render/component-grammar.ts +10 -10
- package/src/lib/render/component-reference.ts +4 -3
- package/src/lib/render/component-validate.ts +10 -35
- package/src/lib/render/pipeline.ts +1 -7
- package/src/lib/render/registry.ts +58 -39
- package/src/lib/render/rehype-dispatch.ts +45 -10
- package/src/lib/render/remark-directives.ts +4 -5
- package/src/lib/render/sanitize-schema.ts +1 -1
- package/src/lib/sveltekit/cairn-admin.ts +8 -9
- package/src/lib/sveltekit/content-routes.ts +330 -221
- package/src/lib/sveltekit/health.ts +13 -6
- package/src/lib/sveltekit/index.ts +2 -2
- package/src/lib/sveltekit/nav-routes.ts +33 -29
- package/src/lib/sveltekit/types.ts +5 -1
- package/src/lib/vite/index.ts +20 -11
- package/dist/content/schema.d.ts +0 -87
- package/dist/content/schema.js +0 -85
- package/dist/content/validate.d.ts +0 -17
- package/dist/content/validate.js +0 -93
- package/src/lib/content/schema.ts +0 -163
- 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> }> };
|