@panaversity/ksor 0.0.39 → 0.0.40

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.
@@ -0,0 +1,246 @@
1
+ /**
2
+ * An interactive page the document points at, as a CLICK-TO-LOAD frame.
3
+ *
4
+ * A record often wants to show something running — a simulation, a dashboard,
5
+ * a player — at one exact point in the prose. A deck is one per document and
6
+ * lives in an attachment (`<doc>.slides.yaml`); this is the other shape: many
7
+ * per document, each where the sentence before it puts it.
8
+ *
9
+ * The form is an ordinary CommonMark link whose TITLE is the word `embed`:
10
+ *
11
+ * [Play run-until-done](https://example.org/sims/goal-loop "embed")
12
+ *
13
+ * Chosen the way the alert syntax was chosen — for what it does everywhere the
14
+ * record is read that is NOT this site. GitHub renders a link with a tooltip.
15
+ * `/md/` and `llms-full.txt` carry the author's link. A plain editor shows a
16
+ * link. No grammar is added to `knowledge/`, so critical rule 2 holds.
17
+ *
18
+ * The TITLE, rather than a lone link on its own line, because the opt-in has
19
+ * to be something an author WROTE. A rule that reframes any link standing
20
+ * alone would silently pull a third party's page into the record the first
21
+ * time someone put a citation on its own line.
22
+ *
23
+ * REHYPE, not remark, for the reason `alert-rule.ts` records: this record's
24
+ * markdown is serialized from the mdast, so rewriting there would publish this
25
+ * site's React component to the agent surface in place of the author's link.
26
+ *
27
+ * A LEAF: no imports, so the rule can be tested on its own.
28
+ */
29
+
30
+ /** The link title that opts a link in. Anything else is an ordinary link. */
31
+ export const EMBED_TITLE = "embed";
32
+
33
+ /** What the panel names when the page is the record's own. */
34
+ export const SELF_HOST = "this record";
35
+
36
+ export interface EmbedMatch {
37
+ readonly url: string;
38
+ readonly host: string;
39
+ }
40
+
41
+ /**
42
+ * The host a frame would reach, or null when the value is not a url.
43
+ *
44
+ * Surfaced rather than hidden: the placeholder names it, so a reader who
45
+ * clicks has already been told whose page is about to load. That naming is
46
+ * half of why there is no host allowlist here — the other half is that an
47
+ * allowlist written into a scaffold would be one adopter's hosts shipped to
48
+ * every other adopter.
49
+ */
50
+ export function hostOf(value: string): string | null {
51
+ try {
52
+ return new URL(value).hostname;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * The frame this link asks for, or null when it is an ordinary link.
60
+ *
61
+ * https only. A browser blocks an http frame inside a secure page as mixed
62
+ * content, so an http embed publishes a panel that silently never loads —
63
+ * worse than refusing it, because nothing goes red.
64
+ */
65
+ /**
66
+ * The suffix that marks an asset as a page to be SERVED rather than bundled.
67
+ *
68
+ * A sim is an asset of its document, exactly like the figures beside it: many
69
+ * per document, named freely, staged only when a published document links to
70
+ * it. It is deliberately NOT an attachment — an attachment is named after its
71
+ * parent (`<doc>.quiz.yaml`), and seven sims cannot all be `index.sim.html`.
72
+ */
73
+ export const SIM_SUFFIX = ".sim.html";
74
+
75
+ /**
76
+ * Where a sim is served, derived from where it sits in the record.
77
+ *
78
+ * The record path is the identity (product principle 3), so two documents
79
+ * may each own a `goal-loop.sim.html` without colliding.
80
+ */
81
+ export function publicSimPath(recordRelative: string): string {
82
+ return recordRelative.slice(0, -SIM_SUFFIX.length).replaceAll("\\", "/") + ".html";
83
+ }
84
+
85
+ export function matchEmbed(url: string, title: string | undefined): EmbedMatch | null {
86
+ if (title !== EMBED_TITLE) return null;
87
+
88
+ // A sim carried IN the record. Same origin, so no third party learns
89
+ // anything, it works offline, and no host can refuse to be framed — which
90
+ // is not hypothetical: every sim this was first built for answers
91
+ // `x-frame-options: SAMEORIGIN`, so a cross-origin frame to them can never
92
+ // render (measured 2026-08-24, all seven).
93
+ if (url.endsWith(SIM_SUFFIX) && !url.includes(":")) {
94
+ return { url, host: SELF_HOST };
95
+ }
96
+
97
+ let parsed: URL;
98
+ try {
99
+ parsed = new URL(url);
100
+ } catch {
101
+ return null;
102
+ }
103
+ if (parsed.protocol !== "https:") return null;
104
+
105
+ return { url, host: parsed.hostname };
106
+ }
107
+
108
+ export interface EmbedCase {
109
+ readonly url: string;
110
+ /** `undefined` is the ordinary link — no title at all. */
111
+ readonly title: string | undefined;
112
+ readonly embeds: boolean;
113
+ }
114
+
115
+ /** The rule, as a table — what embeds, and what deliberately does not. */
116
+ export const EMBED_CASES: readonly EmbedCase[] = [
117
+ { url: "https://example.org/sims/goal-loop", title: EMBED_TITLE, embeds: true },
118
+ // Carried in the record, beside its document.
119
+ { url: "goal-loop.sim.html", title: EMBED_TITLE, embeds: true },
120
+ { url: "sims/goal-loop.sim.html", title: EMBED_TITLE, embeds: true },
121
+ // Still opt-in: an ordinary link to a sim is an ordinary link.
122
+ { url: "goal-loop.sim.html", title: undefined, embeds: false },
123
+ { url: "https://example.org/sims/goal-loop?v=3", title: EMBED_TITLE, embeds: true },
124
+ // No title is the ordinary case, and by far the commonest link in a record.
125
+ { url: "https://example.org/sims/goal-loop", title: undefined, embeds: false },
126
+ // A title an author wrote for a reader is a tooltip, not an instruction.
127
+ { url: "https://example.org/sims/goal-loop", title: "The run-until-done sim", embeds: false },
128
+ // Near misses on the marker, which must stay links.
129
+ { url: "https://example.org/sim", title: "Embed", embeds: false },
130
+ { url: "https://example.org/sim", title: "embedded", embeds: false },
131
+ { url: "https://example.org/sim", title: " embed", embeds: false },
132
+ // No height token: the frame measures the page it holds, so a record never
133
+ // carries a number that a different measure would make wrong.
134
+ { url: "goal-loop.sim.html", title: "embed 660", embeds: false },
135
+ // http would be blocked as mixed content, so it stays a link that works.
136
+ { url: "http://example.org/sim", title: EMBED_TITLE, embeds: false },
137
+ // Neither of these is a url a frame could reach.
138
+ { url: "/sims/goal-loop", title: EMBED_TITLE, embeds: false },
139
+ { url: "mailto:records@example.org", title: EMBED_TITLE, embeds: false },
140
+ ];
141
+
142
+ interface EmbedNode {
143
+ type?: string;
144
+ tagName?: string;
145
+ name?: string;
146
+ value?: string;
147
+ properties?: { href?: unknown; title?: unknown };
148
+ attributes?: readonly { type: string; name: string; value: string }[];
149
+ children?: EmbedNode[];
150
+ }
151
+
152
+ /** The link's own text, which becomes the frame's title and the link out. */
153
+ function labelOf(node: EmbedNode): string {
154
+ return (node.children ?? [])
155
+ .map((child) => (child.type === "text" ? (child.value ?? "") : labelOf(child)))
156
+ .join("")
157
+ .trim();
158
+ }
159
+
160
+ /** The record-relative directory a staged document sits in, or "". */
161
+ /**
162
+ * BOTH roots, because there are two. A record that declares `audiences:` or
163
+ * carries a takedown is read from `.staged-knowledge/`; every other record is
164
+ * read from `knowledge/` directly, which is the level-0 fast path and the
165
+ * common case. Keying on the staged one alone dropped the directory from
166
+ * every url on exactly the records most people have (found live 2026-08-24:
167
+ * `/sims/goal-loop.html` for a sim that lives in `loop-engineering/`).
168
+ */
169
+ const RECORD_ROOTS = [".staged-knowledge/", "knowledge/"] as const;
170
+
171
+ export function recordDirOf(filePath: string | undefined): string {
172
+ if (!filePath) return "";
173
+ const normalized = filePath.replaceAll("\\", "/");
174
+ for (const marker of RECORD_ROOTS) {
175
+ const at = normalized.lastIndexOf(marker);
176
+ if (at === -1) continue;
177
+ const rel = normalized.slice(at + marker.length);
178
+ const cut = rel.lastIndexOf("/");
179
+ return cut === -1 ? "" : rel.slice(0, cut);
180
+ }
181
+ return "";
182
+ }
183
+
184
+ /** The served url for a sim linked from a document in `dir`. */
185
+ export function servedSimUrl(dir: string, target: string): string {
186
+ const joined = dir === "" ? target : `${dir}/${target}`;
187
+ return "/sims/" + publicSimPath(joined);
188
+ }
189
+
190
+ function embedFor(node: EmbedNode, dir: string): EmbedNode | null {
191
+ if (node.type !== "element" || node.tagName !== "p") return null;
192
+
193
+ // ALONE in its paragraph. hast keeps the source's whitespace between inline
194
+ // children, so a link that shares a sentence still has text nodes beside it
195
+ // — and reframing that would swallow the sentence around the link.
196
+ const meaningful = (node.children ?? []).filter(
197
+ (child) => child.type !== "text" || (child.value ?? "").trim() !== "",
198
+ );
199
+ const [link] = meaningful;
200
+ if (meaningful.length !== 1 || !link || link.type !== "element" || link.tagName !== "a") {
201
+ return null;
202
+ }
203
+
204
+ const { href, title } = link.properties ?? {};
205
+ if (typeof href !== "string") return null;
206
+ const match = matchEmbed(href, typeof title === "string" ? title : undefined);
207
+ if (match === null) return null;
208
+
209
+ // A sim is written as a link to the file BESIDE the document, exactly like
210
+ // a figure. The url it is served at is derived here rather than authored,
211
+ // so the record never contains a path into the site's own build output.
212
+ const url = href.endsWith(SIM_SUFFIX) ? servedSimUrl(dir, href) : match.url;
213
+
214
+ return {
215
+ type: "mdxJsxFlowElement",
216
+ name: "Embed",
217
+ attributes: [
218
+ { type: "mdxJsxAttribute", name: "url", value: url },
219
+ { type: "mdxJsxAttribute", name: "host", value: match.host },
220
+ // Carried in the record, or someone else's page. The two say very
221
+ // different things to a reader, so the panel is told which.
222
+ { type: "mdxJsxAttribute", name: "owned", value: String(match.host === SELF_HOST) },
223
+ { type: "mdxJsxAttribute", name: "label", value: labelOf(link) },
224
+ ],
225
+ children: [],
226
+ };
227
+ }
228
+
229
+ function convertEmbeds(node: EmbedNode, dir: string): void {
230
+ const children = node.children;
231
+ if (!children) return;
232
+
233
+ for (let i = 0; i < children.length; i++) {
234
+ const child = children[i];
235
+ if (!child) continue;
236
+ convertEmbeds(child, dir);
237
+ const embed = embedFor(child, dir);
238
+ if (embed) children[i] = embed;
239
+ }
240
+ }
241
+
242
+ export function rehypeEmbeds(): (tree: EmbedNode, file?: { path?: string }) => void {
243
+ return (tree: EmbedNode, file?: { path?: string }): void => {
244
+ convertEmbeds(tree, recordDirOf(file?.path));
245
+ };
246
+ }
@@ -14,6 +14,7 @@ import path from "node:path";
14
14
  import { ATTACHMENT_SUFFIXES, isAttachment, parentDocumentOf } from "./attachment-rule";
15
15
  import { audienceModel, buildAudience, refuse, visibleInBuild } from "./audience";
16
16
  import { isDenied, recordPathFrom, stableIdFrom, type DenylistManifest } from "./denial-rule";
17
+ import { publicSimPath, SIM_SUFFIX } from "./embed-rule";
17
18
  import { appName, instanceFrontmatter } from "./shared";
18
19
 
19
20
  // Both relative to the site directory — the directory every build runs from
@@ -21,6 +22,8 @@ import { appName, instanceFrontmatter } from "./shared";
21
22
  // resolves a collection's `dir`.
22
23
  const RECORD_DIR = "../../knowledge";
23
24
  const STAGE_DIR = "./.staged-knowledge";
25
+ // Served, not bundled. Next copies public/ into the export as-is.
26
+ const PUBLIC_SIM_DIR = "./public/sims";
24
27
 
25
28
  // ONE frontmatter boundary, the checker's exactly: BOM stripped, CRLF
26
29
  // normalized, lax close (a `----` line closes — review finding 2026-08-19:
@@ -444,7 +447,20 @@ function withStageLock<T>(stageDir: string, work: () => T): T {
444
447
  writeFileSync(lockFile, String(process.pid), { flag: "wx" });
445
448
  break;
446
449
  } catch (error) {
447
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
450
+ // EEXIST is "someone holds it". EPERM is the SAME THING on Windows: a
451
+ // create against a path whose file is in the pending-delete state — the
452
+ // window between another process calling `rmSync` and the filesystem
453
+ // actually releasing the name — raises EPERM, not EEXIST. Rethrowing it
454
+ // failed the build for the ordinary contended case, and only on Windows,
455
+ // and only sometimes: green on five CI runs of this same code and red on
456
+ // the next two, because it depends on landing inside a window a few
457
+ // milliseconds wide (2026-08-25, `Init acceptance (Windows)`).
458
+ //
459
+ // Waiting is safe for both: a holder that has died leaves a lock
460
+ // `lockIsAbandoned` breaks, so neither code can wait forever on a
461
+ // process that is gone.
462
+ const code = (error as NodeJS.ErrnoException).code;
463
+ if (code !== "EEXIST" && code !== "EPERM") throw error;
448
464
  if (lockIsAbandoned(lockFile)) {
449
465
  rmSync(lockFile, { force: true });
450
466
  continue;
@@ -541,13 +557,17 @@ function fillStage(recordDir: string, stageDir: string, denied: DenylistManifest
541
557
  removeStage(stageDir);
542
558
  throw error;
543
559
  }
544
- if (stageHolds(recordDir, stageDir, plan)) return;
560
+ if (stageHolds(recordDir, stageDir, plan)) {
561
+ publishSims(stageDir);
562
+ return;
563
+ }
545
564
  removeStage(stageDir);
546
565
  for (const from of plan.files) {
547
566
  const to = path.join(stageDir, path.relative(recordDir, from));
548
567
  mkdirSync(path.dirname(to), { recursive: true });
549
568
  copyFileSync(from, to);
550
569
  }
570
+ publishSims(stageDir);
551
571
  });
552
572
  }
553
573
 
@@ -686,6 +706,73 @@ function watchRecord(recordDir: string, stageDir: string): void {
686
706
  * per-request filter leaked on the fifth and sixth consumer of the record
687
707
  * its own author had not enumerated (research/visibility.md §4–§5).
688
708
  */
709
+
710
+ /**
711
+ * A sim is the one asset that has to be SERVED rather than bundled: it is a
712
+ * page, and a page needs a url before anything can frame it. Next copies
713
+ * `public/` into the export as-is, so that is where it goes.
714
+ *
715
+ * A PASS OF ITS OWN, over the directory the collection actually reads — not a
716
+ * rider on staging. Staging runs only for a record that declares `audiences:`
717
+ * or carries a takedown, and most records declare neither, so a sim hung off
718
+ * it published for the rare record and silently vanished for the common one
719
+ * (found live 2026-08-24: nothing reached `public/` on the level-0 path).
720
+ * Reading the SOURCE dir inherits the filtering when there is any, and works
721
+ * when there is none. What it does NOT inherit at level 0 is the staging
722
+ * plan's rule that only a REFERENCED asset ships: a record with no audiences
723
+ * and no takedowns publishes every document anyway, so an unreferenced sim
724
+ * ships its bytes there. Said rather than fixed, because the moment either
725
+ * governance exists the staged dir is what this walks and the rule applies.
726
+ */
727
+ /** Whether the record carries a sim at all — a lock nobody needs is churn. */
728
+ function hasSims(dir: string): boolean {
729
+ return walkFiles(dir).some((file) => file.endsWith(SIM_SUFFIX));
730
+ }
731
+
732
+ function publishSims(sourceDir: string): void {
733
+ const target = path.resolve(process.cwd(), PUBLIC_SIM_DIR);
734
+
735
+ const walk = (dir: string, rel: string): void => {
736
+ let entries;
737
+ try {
738
+ entries = readdirSync(dir, { withFileTypes: true });
739
+ } catch {
740
+ return;
741
+ }
742
+ for (const entry of entries) {
743
+ const from = path.join(dir, entry.name);
744
+ const next = rel === "" ? entry.name : `${rel}/${entry.name}`;
745
+ if (entry.isDirectory()) {
746
+ walk(from, next);
747
+ continue;
748
+ }
749
+ if (!entry.name.endsWith(SIM_SUFFIX)) continue;
750
+ const to = path.join(target, publicSimPath(next));
751
+ // Same size AND same mtime is this file's own definition of unchanged
752
+ // (see `stageHolds`). Skipping the write is what keeps the common
753
+ // build from touching the tree at all.
754
+ try {
755
+ const source = statSync(from);
756
+ const published = statSync(to);
757
+ if (published.size === source.size && published.mtimeMs >= source.mtimeMs) continue;
758
+ } catch {
759
+ // Not published yet, which is the ordinary first-build case.
760
+ }
761
+ mkdirSync(path.dirname(to), { recursive: true });
762
+ copyFileSync(from, to);
763
+ }
764
+ };
765
+
766
+ // The CALLER holds the stage lock, and this takes none of its own — for the
767
+ // reason `withStageLock` records at length, plus one this change learned on
768
+ // Windows: taking it a SECOND time per evaluation doubles the create/delete
769
+ // churn on one lock file, and `wx` create against a file in Windows'
770
+ // pending-delete state fails as `EPERM`, which is not `EEXIST` and so is
771
+ // rethrown. Green on macOS and Linux, red on Windows CI, from a pass that
772
+ // was correct about needing the lock and wrong about taking it again.
773
+ walk(sourceDir, "");
774
+ }
775
+
689
776
  export function knowledgeSourceDir(): string {
690
777
  const stageDir = path.resolve(process.cwd(), STAGE_DIR);
691
778
  const recordDir = path.resolve(process.cwd(), RECORD_DIR);
@@ -704,9 +791,19 @@ export function knowledgeSourceDir(): string {
704
791
  // because two evaluations removing one tree is the `ENOTEMPTY` shape of the
705
792
  // same race; the existence check keeps a record that never stages from
706
793
  // taking a lock on every build.
707
- if (existsSync(stageDir)) withStageLock(stageDir, () => removeStage(stageDir));
708
794
  refuseVisibilityWithoutAudiences(recordDir);
709
795
  assertAttachmentsWellFormed(recordDir);
796
+ // ONE acquisition on this path too, doing both jobs — and keyed on the
797
+ // stage path rather than the record's, because `${recordDir}.lock` would
798
+ // drop a lock file beside `knowledge/`, in the adopter's repo, for a build
799
+ // that never stages anything.
800
+ const stale = existsSync(stageDir);
801
+ if (stale || hasSims(recordDir)) {
802
+ withStageLock(stageDir, () => {
803
+ if (stale) removeStage(stageDir);
804
+ publishSims(recordDir);
805
+ });
806
+ }
710
807
  return RECORD_DIR;
711
808
  }
712
809
  if (audienceModel === null) refuseVisibilityWithoutAudiences(recordDir);
@@ -1,5 +1,7 @@
1
1
  import { defineCollections, defineConfig, defineDocs } from "fumadocs-mdx/config";
2
2
  import { remarkCodeTab } from "fumadocs-core/mdx-plugins/remark-code-tab";
3
+ import { rehypeGithubAlerts } from "./lib/alert-rule";
4
+ import { rehypeEmbeds } from "./lib/embed-rule";
3
5
  import { metaSchema, pageSchema } from "fumadocs-core/source/schema";
4
6
  import { z } from "zod";
5
7
  import { DeckSchema } from "./lib/deck";
@@ -123,7 +125,36 @@ export default defineConfig({
123
125
  * place; the page decides whether there is a deck to put there. See
124
126
  * lib/teaching-aid-rule.ts.
125
127
  */
126
- rehypePlugins: [[rehypeTeachingAid, { isAttachment }]],
128
+ rehypePlugins: [
129
+ [rehypeTeachingAid, { isAttachment }],
130
+ /**
131
+ * A passage the reader must not miss, as a CALLOUT.
132
+ *
133
+ * GitHub's alert syntax: a blockquote whose first line is `[!WARNING]`.
134
+ * GitHub renders it as a styled alert, every other viewer renders an
135
+ * ordinary blockquote carrying a visible label, and it reaches `/md/`
136
+ * and `llms-full.txt` as the author's blockquote rather than as markup.
137
+ *
138
+ * Not `:::warning`. fumadocs ships `remarkAdmonition` for that form and
139
+ * deprecates it in favour of a `remark-directive` setup — but the deeper
140
+ * objection is that `:::` is a dialect, and a record written in one
141
+ * renders as literal punctuation everywhere it is read outside this site.
142
+ *
143
+ * REHYPE rather than remark, and lib/alert-rule.ts records why: the
144
+ * record's markdown is serialized from the mdast, so converting there
145
+ * publishes this site's React component to the agent surface.
146
+ */
147
+ rehypeGithubAlerts,
148
+ /**
149
+ * An interactive page the document points at, as a click-to-load
150
+ * frame. Authored as an ordinary link titled `embed`, so the record
151
+ * stays CommonMark and every other reader of it sees a link.
152
+ *
153
+ * REHYPE, so `/md/` and `llms-full.txt` keep the author's link
154
+ * rather than this site's component. See lib/embed-rule.ts.
155
+ */
156
+ rehypeEmbeds,
157
+ ],
127
158
  /**
128
159
  * Alternative versions of the same instruction, as TABS.
129
160
  *