@glw907/cairn-cms 0.62.1 → 0.62.2
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
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are recorded here, most recent first.
|
|
4
4
|
|
|
5
|
+
## 0.62.2
|
|
6
|
+
|
|
7
|
+
The edit-load address-collision advisory now checks the published corpus only. It fires when an entry
|
|
8
|
+
you are editing collides with an entry already published on `main`, and it no longer reads sibling
|
|
9
|
+
`cairn/<concept>/<id>` branches when an editor opens an entry, so opening the editor adds no GitHub
|
|
10
|
+
reads. The publish-time re-check is unchanged: it stays full cross-branch and still emits the
|
|
11
|
+
`publish.address_collision` log event when a publish overrides another entry's address. No consumer
|
|
12
|
+
action is required.
|
|
13
|
+
|
|
5
14
|
## 0.62.1
|
|
6
15
|
|
|
7
16
|
The entry editor gains an advisory channel and its first notice: a cross-branch address-collision
|
|
@@ -32,6 +32,11 @@ export interface AddressEntry {
|
|
|
32
32
|
}
|
|
33
33
|
/** Permalink to the distinct entries that resolve to it, across main and every open branch. */
|
|
34
34
|
export type AddressIndex = Map<string, AddressEntry[]>;
|
|
35
|
+
/**
|
|
36
|
+
* The address index over main only: a synchronous reverse map of each manifest entry's resolved
|
|
37
|
+
* permalink. No backend read, so an edit-load can build it for free from the manifest it already holds.
|
|
38
|
+
*/
|
|
39
|
+
export declare function mainAddressIndex(manifest: Manifest): AddressIndex;
|
|
35
40
|
/**
|
|
36
41
|
* Build the permalink-keyed address index over main (from each manifest entry's resolved permalink)
|
|
37
42
|
* plus every open cairn/* branch (resolved from its edited markdown).
|
|
@@ -14,17 +14,11 @@ function push(index, permalink, entry) {
|
|
|
14
14
|
index.set(permalink, [entry]);
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* The build fails open: a branch read that throws and a permalink that cannot resolve are both caught
|
|
21
|
-
* and skipped, so a transient failure degrades to a thinner index, never a thrown editor or a blocked
|
|
22
|
-
* publish. The branches are read in one Promise.all, the way buildUsageIndex reads them.
|
|
17
|
+
* The address index over main only: a synchronous reverse map of each manifest entry's resolved
|
|
18
|
+
* permalink. No backend read, so an edit-load can build it for free from the manifest it already holds.
|
|
23
19
|
*/
|
|
24
|
-
export
|
|
20
|
+
export function mainAddressIndex(manifest) {
|
|
25
21
|
const index = new Map();
|
|
26
|
-
// The main arm: the manifest already carries each entry's resolved permalink, so this is a pure
|
|
27
|
-
// reverse map with no per-file read.
|
|
28
22
|
for (const entry of manifest.entries) {
|
|
29
23
|
push(index, entry.permalink, {
|
|
30
24
|
concept: entry.concept,
|
|
@@ -33,6 +27,20 @@ export async function buildAddressIndex(repo, token, concepts, manifest) {
|
|
|
33
27
|
source: 'main',
|
|
34
28
|
});
|
|
35
29
|
}
|
|
30
|
+
return index;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the permalink-keyed address index over main (from each manifest entry's resolved permalink)
|
|
34
|
+
* plus every open cairn/* branch (resolved from its edited markdown).
|
|
35
|
+
*
|
|
36
|
+
* The build fails open: a branch read that throws and a permalink that cannot resolve are both caught
|
|
37
|
+
* and skipped, so a transient failure degrades to a thinner index, never a thrown editor or a blocked
|
|
38
|
+
* publish. The branches are read in one Promise.all, the way buildUsageIndex reads them.
|
|
39
|
+
*/
|
|
40
|
+
export async function buildAddressIndex(repo, token, concepts, manifest) {
|
|
41
|
+
// The main arm: the manifest already carries each entry's resolved permalink, so seed from the
|
|
42
|
+
// synchronous main-only index and union the branch arm on top.
|
|
43
|
+
const index = mainAddressIndex(manifest);
|
|
36
44
|
// The branch arm: read each open cairn/* branch's one edited file and resolve its permalink. The
|
|
37
45
|
// path is derivable from the branch name, so no tree-listing is needed.
|
|
38
46
|
const names = await listBranches(repo, PENDING_PREFIX, token);
|
|
@@ -8,7 +8,7 @@ import { extractCairnLinks, formatCairnToken, rewriteCairnLink } from '../conten
|
|
|
8
8
|
import { frontmatterFromForm, parseMarkdown, dateInputValue, serializeMarkdown } from '../content/frontmatter.js';
|
|
9
9
|
import { deriveExcerpt } from '../content/excerpt.js';
|
|
10
10
|
import { asString, entryIdentity } from '../content/identity.js';
|
|
11
|
-
import { buildAddressIndex, addressCollision } from '../content/advisories.js';
|
|
11
|
+
import { buildAddressIndex, mainAddressIndex, addressCollision } from '../content/advisories.js';
|
|
12
12
|
import { isValidId, slugify, filenameFromId, composeDatedId, slugFromId, renameId } from '../content/ids.js';
|
|
13
13
|
import { appCredentials } from '../github/credentials.js';
|
|
14
14
|
import { listMarkdown, readRaw, commitFile, commitFiles } from '../github/repo.js';
|
|
@@ -502,14 +502,16 @@ export function createContentRoutes(runtime, deps = {}) {
|
|
|
502
502
|
}));
|
|
503
503
|
inbound = inboundLinks(manifest, concept.id, id);
|
|
504
504
|
}
|
|
505
|
-
// The
|
|
506
|
-
//
|
|
507
|
-
//
|
|
505
|
+
// The address-collision advisory: warn-and-allow, never a gate. At edit-load it checks the
|
|
506
|
+
// published corpus only, built synchronously from the same manifest read above (no extra GitHub
|
|
507
|
+
// read per editor open); publishAction re-checks the full cross-branch index before it lands. The
|
|
508
|
+
// try/catch degrades to no notice if entryIdentity throws on a malformed-date entry. Skip the build
|
|
509
|
+
// with no manifest to index.
|
|
508
510
|
let advisories = [];
|
|
509
511
|
if (manifest !== null) {
|
|
510
512
|
try {
|
|
511
513
|
const identity = entryIdentity(concept, path, parsed.frontmatter);
|
|
512
|
-
const addressIndex =
|
|
514
|
+
const addressIndex = mainAddressIndex(manifest);
|
|
513
515
|
const other = addressCollision(addressIndex, { concept: concept.id, id }, identity.permalink);
|
|
514
516
|
if (other) {
|
|
515
517
|
const otherConcept = findConcept(runtime.concepts, other.concept);
|
|
@@ -524,8 +526,8 @@ export function createContentRoutes(runtime, deps = {}) {
|
|
|
524
526
|
];
|
|
525
527
|
}
|
|
526
528
|
}
|
|
527
|
-
catch
|
|
528
|
-
|
|
529
|
+
catch {
|
|
530
|
+
// A malformed-date entry that cannot resolve its permalink degrades to no advisory, fail open.
|
|
529
531
|
}
|
|
530
532
|
}
|
|
531
533
|
// Project the one committed media manifest read two ways: the minimal resolver triple the preview
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// cairn-cms: the entry editor's internal advisory channel (editor-help pass 3). An advisory is a
|
|
2
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
|
|
4
|
-
//
|
|
3
|
+
// only and never a callback. Today's one notice is the address collision: a warning, not a gate,
|
|
4
|
+
// that another entry already resolves to the same public address (last-write-wins).
|
|
5
5
|
//
|
|
6
6
|
// The address index mirrors buildUsageIndex (src/lib/media/usage.ts): a main arm that reads each
|
|
7
7
|
// manifest entry's resolved permalink with no per-file read, and a branch arm that lists every open
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// and resolves its permalink. The map is keyed by permalink, so every entry that resolves to a given
|
|
10
10
|
// address shares one bucket. The build fails open: a branch read that throws, or a dated entry whose
|
|
11
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.
|
|
12
|
+
// notice and never blocks the editor or the publish. The scope splits by call site: the main arm at
|
|
13
|
+
// edit-load (synchronous, no extra GitHub read per open) and the full cross-branch check at publish.
|
|
13
14
|
import type { ConceptDescriptor } from './types.js';
|
|
14
15
|
import type { RepoRef } from '../github/types.js';
|
|
15
16
|
import type { Manifest } from './manifest.js';
|
|
@@ -63,6 +64,23 @@ function push(index: AddressIndex, permalink: string, entry: AddressEntry): void
|
|
|
63
64
|
else index.set(permalink, [entry]);
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The address index over main only: a synchronous reverse map of each manifest entry's resolved
|
|
69
|
+
* permalink. No backend read, so an edit-load can build it for free from the manifest it already holds.
|
|
70
|
+
*/
|
|
71
|
+
export function mainAddressIndex(manifest: Manifest): AddressIndex {
|
|
72
|
+
const index: AddressIndex = new Map();
|
|
73
|
+
for (const entry of manifest.entries) {
|
|
74
|
+
push(index, entry.permalink, {
|
|
75
|
+
concept: entry.concept,
|
|
76
|
+
id: entry.id,
|
|
77
|
+
title: entry.title,
|
|
78
|
+
source: 'main',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return index;
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
/**
|
|
67
85
|
* Build the permalink-keyed address index over main (from each manifest entry's resolved permalink)
|
|
68
86
|
* plus every open cairn/* branch (resolved from its edited markdown).
|
|
@@ -77,18 +95,9 @@ export async function buildAddressIndex(
|
|
|
77
95
|
concepts: ConceptDescriptor[],
|
|
78
96
|
manifest: Manifest,
|
|
79
97
|
): Promise<AddressIndex> {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
}
|
|
98
|
+
// The main arm: the manifest already carries each entry's resolved permalink, so seed from the
|
|
99
|
+
// synchronous main-only index and union the branch arm on top.
|
|
100
|
+
const index = mainAddressIndex(manifest);
|
|
92
101
|
|
|
93
102
|
// The branch arm: read each open cairn/* branch's one edited file and resolve its permalink. The
|
|
94
103
|
// path is derivable from the branch name, so no tree-listing is needed.
|
|
@@ -8,7 +8,7 @@ import { extractCairnLinks, formatCairnToken, rewriteCairnLink } from '../conten
|
|
|
8
8
|
import { frontmatterFromForm, parseMarkdown, dateInputValue, serializeMarkdown } from '../content/frontmatter.js';
|
|
9
9
|
import { deriveExcerpt } from '../content/excerpt.js';
|
|
10
10
|
import { asString, entryIdentity } from '../content/identity.js';
|
|
11
|
-
import { buildAddressIndex, addressCollision, type AdvisoryNotice, type AddressEntry } from '../content/advisories.js';
|
|
11
|
+
import { buildAddressIndex, mainAddressIndex, addressCollision, type AdvisoryNotice, type AddressEntry } from '../content/advisories.js';
|
|
12
12
|
import { isValidId, slugify, filenameFromId, composeDatedId, slugFromId, renameId } from '../content/ids.js';
|
|
13
13
|
import { appCredentials, type GithubKeyEnv } from '../github/credentials.js';
|
|
14
14
|
import { listMarkdown, readRaw, commitFile, commitFiles, type FileChange } from '../github/repo.js';
|
|
@@ -1072,14 +1072,16 @@ export function createContentRoutes(runtime: CairnRuntime, deps: ContentRoutesDe
|
|
|
1072
1072
|
inbound = inboundLinks(manifest, concept.id, id);
|
|
1073
1073
|
}
|
|
1074
1074
|
|
|
1075
|
-
// The
|
|
1076
|
-
//
|
|
1077
|
-
//
|
|
1075
|
+
// The address-collision advisory: warn-and-allow, never a gate. At edit-load it checks the
|
|
1076
|
+
// published corpus only, built synchronously from the same manifest read above (no extra GitHub
|
|
1077
|
+
// read per editor open); publishAction re-checks the full cross-branch index before it lands. The
|
|
1078
|
+
// try/catch degrades to no notice if entryIdentity throws on a malformed-date entry. Skip the build
|
|
1079
|
+
// with no manifest to index.
|
|
1078
1080
|
let advisories: AdvisoryNotice[] = [];
|
|
1079
1081
|
if (manifest !== null) {
|
|
1080
1082
|
try {
|
|
1081
1083
|
const identity = entryIdentity(concept, path, parsed.frontmatter);
|
|
1082
|
-
const addressIndex =
|
|
1084
|
+
const addressIndex = mainAddressIndex(manifest);
|
|
1083
1085
|
const other = addressCollision(addressIndex, { concept: concept.id, id }, identity.permalink);
|
|
1084
1086
|
if (other) {
|
|
1085
1087
|
const otherConcept = findConcept(runtime.concepts, other.concept);
|
|
@@ -1093,8 +1095,8 @@ export function createContentRoutes(runtime: CairnRuntime, deps: ContentRoutesDe
|
|
|
1093
1095
|
},
|
|
1094
1096
|
];
|
|
1095
1097
|
}
|
|
1096
|
-
} catch
|
|
1097
|
-
|
|
1098
|
+
} catch {
|
|
1099
|
+
// A malformed-date entry that cannot resolve its permalink degrades to no advisory, fail open.
|
|
1098
1100
|
}
|
|
1099
1101
|
}
|
|
1100
1102
|
|