@panaversity/ksor 0.0.39 → 0.0.41

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 (108) hide show
  1. package/CHANGELOG.md +965 -0
  2. package/README.md +29 -9
  3. package/dist/checker/check-main.mjs +14049 -0
  4. package/dist/cli.mjs +11528 -5213
  5. package/dist/gateway-api-CF4ED9_g-BQusM_dK.mjs +10895 -0
  6. package/dist/gateway.d.mts +52 -13
  7. package/dist/gateway.mjs +2 -2
  8. package/dist/index.d.mts +1 -1
  9. package/dist/index.mjs +1 -1
  10. package/dist/{src-pl4aOpVs.mjs → src-dqpI-p1a.mjs} +1 -0
  11. package/docs/authorization.md +8 -6
  12. package/docs/deploying.md +36 -25
  13. package/docs/index.md +26 -13
  14. package/docs/ingesting.md +70 -22
  15. package/docs/tool-surface.md +69 -16
  16. package/package.json +4 -3
  17. package/schema/migrations/2.4-2.5__okf-profile.sql +114 -0
  18. package/schema/schema.sql +77 -14
  19. package/templates/scaffold/.agents/skills/add-sources/SKILL.md +63 -18
  20. package/templates/scaffold/.agents/skills/format-checker/SKILL.md +42 -33
  21. package/templates/scaffold/.agents/skills/format-checker/check.mjs +13827 -1314
  22. package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +65 -27
  23. package/templates/scaffold/.agents/skills/make-slides/SKILL.md +7 -5
  24. package/templates/scaffold/.agents/skills/make-summary/SKILL.md +13 -6
  25. package/templates/scaffold/.claude/skills/add-sources/SKILL.md +63 -18
  26. package/templates/scaffold/.claude/skills/format-checker/SKILL.md +42 -33
  27. package/templates/scaffold/.claude/skills/format-checker/check.mjs +13827 -1314
  28. package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +65 -27
  29. package/templates/scaffold/.claude/skills/make-slides/SKILL.md +7 -5
  30. package/templates/scaffold/.claude/skills/make-summary/SKILL.md +13 -6
  31. package/templates/scaffold/.github/workflows/validate.yml +9 -1
  32. package/templates/scaffold/.ksor/governance.yaml +17 -0
  33. package/templates/scaffold/AGENTS.md +293 -119
  34. package/templates/scaffold/Dockerfile +5 -1
  35. package/templates/scaffold/README.md +160 -42
  36. package/templates/scaffold/env.example +37 -6
  37. package/templates/scaffold/gitignore +14 -6
  38. package/templates/scaffold/instance.md +21 -17
  39. package/templates/scaffold/knowledge/governance-ladder.md +11 -2
  40. package/templates/scaffold/knowledge/index.md +9 -0
  41. package/templates/scaffold/knowledge/surfaces/for-agents.md +13 -6
  42. package/templates/scaffold/knowledge/surfaces/for-people.md +7 -6
  43. package/templates/scaffold/knowledge/surfaces/index.md +4 -20
  44. package/templates/scaffold/knowledge/surfaces/overview.md +25 -0
  45. package/templates/scaffold/knowledge/what-is-a-ksor.md +6 -5
  46. package/templates/scaffold/knowledge/what-is-a-ksor.summary.md +4 -0
  47. package/templates/scaffold/package.json +3 -4
  48. package/templates/scaffold/pnpm-lock.yaml +3 -0
  49. package/templates/scaffold/system/gateways/content.ts +13 -0
  50. package/templates/scaffold/system/site/app/(home)/page.tsx +2 -2
  51. package/templates/scaffold/system/site/app/.well-known/mcp/server.json/route.ts +10 -0
  52. package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +134 -91
  53. package/templates/scaffold/system/site/app/global.css +198 -6
  54. package/templates/scaffold/system/site/app/layout.tsx +8 -3
  55. package/templates/scaffold/system/site/app/llms-full.txt/route.ts +13 -7
  56. package/templates/scaffold/system/site/app/llms.txt/route.ts +12 -7
  57. package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +26 -25
  58. package/templates/scaffold/system/site/components/code-block.tsx +87 -0
  59. package/templates/scaffold/system/site/components/embed.tsx +276 -0
  60. package/templates/scaffold/system/site/components/footer-mark.tsx +3 -2
  61. package/templates/scaffold/system/site/components/governance.tsx +219 -87
  62. package/templates/scaffold/system/site/components/mdx.tsx +10 -2
  63. package/templates/scaffold/system/site/components/record-index.tsx +5 -5
  64. package/templates/scaffold/system/site/components/record-stack.tsx +10 -9
  65. package/templates/scaffold/system/site/components/record-views.tsx +6 -17
  66. package/templates/scaffold/system/site/components/sidebar-status.tsx +19 -18
  67. package/templates/scaffold/system/site/lib/alert-rule.ts +214 -0
  68. package/templates/scaffold/system/site/lib/attachment-rule.ts +6 -1
  69. package/templates/scaffold/system/site/lib/attachments.ts +0 -28
  70. package/templates/scaffold/system/site/lib/audience-rule.ts +15 -21
  71. package/templates/scaffold/system/site/lib/audience.ts +42 -146
  72. package/templates/scaffold/system/site/lib/embed-rule.ts +255 -0
  73. package/templates/scaffold/system/site/lib/governance.ts +339 -225
  74. package/templates/scaffold/system/site/lib/index-routes.ts +125 -0
  75. package/templates/scaffold/system/site/lib/lifecycle-rule.ts +52 -0
  76. package/templates/scaffold/system/site/lib/lock.ts +282 -0
  77. package/templates/scaffold/system/site/lib/order-rule.ts +37 -0
  78. package/templates/scaffold/system/site/lib/record-href.ts +68 -0
  79. package/templates/scaffold/system/site/lib/record-link.tsx +26 -0
  80. package/templates/scaffold/system/site/lib/rules-version.ts +11 -0
  81. package/templates/scaffold/system/site/lib/shared.ts +67 -104
  82. package/templates/scaffold/system/site/lib/sim-rule.ts +49 -0
  83. package/templates/scaffold/system/site/lib/source.ts +256 -186
  84. package/templates/scaffold/system/site/lib/stage-knowledge.ts +638 -467
  85. package/templates/scaffold/system/site/lib/stage-manifest.ts +128 -0
  86. package/templates/scaffold/system/site/package.json +1 -0
  87. package/templates/scaffold/system/site/record/actor.ts +23 -0
  88. package/templates/scaffold/system/site/record/check.ts +571 -0
  89. package/templates/scaffold/system/site/record/citations.ts +312 -0
  90. package/templates/scaffold/system/site/record/frontmatter.ts +134 -0
  91. package/templates/scaffold/system/site/record/git-ledger.ts +171 -0
  92. package/templates/scaffold/system/site/record/hygiene.ts +320 -0
  93. package/templates/scaffold/system/site/record/index-file.ts +150 -0
  94. package/templates/scaffold/system/site/record/index.ts +103 -0
  95. package/templates/scaffold/system/site/record/instance.ts +257 -0
  96. package/templates/scaffold/system/site/record/instant.ts +43 -0
  97. package/templates/scaffold/system/site/record/ledger.ts +694 -0
  98. package/templates/scaffold/system/site/record/load.ts +129 -0
  99. package/templates/scaffold/system/site/record/lock.ts +306 -0
  100. package/templates/scaffold/system/site/record/near-miss.ts +37 -0
  101. package/templates/scaffold/system/site/record/policy.ts +414 -0
  102. package/templates/scaffold/system/site/record/profile.ts +535 -0
  103. package/templates/scaffold/system/site/record/refusal.ts +106 -0
  104. package/templates/scaffold/system/site/record/yaml-file.ts +103 -0
  105. package/templates/scaffold/system/site/source.config.ts +109 -23
  106. package/dist/gateway-api-CmIthmJS-IUA9qS-T.mjs +0 -3225
  107. package/templates/scaffold/system/site/lib/denial-rule.ts +0 -220
  108. package/templates/scaffold/system/site/lib/page-order.ts +0 -93
@@ -2,12 +2,13 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
4
  import { governanceVisible } from "./governance";
5
+ import { splitFrontmatter } from "../record/frontmatter";
5
6
 
6
- // The record's one identity source: instance.md's `name:` — the same file
7
- // every other shell reads, so renaming the instance renames every surface
8
- // at the next build, and no shell carries a baked-in copy (found live
9
- // 2026-08-18: a stamped constant survived a restore-from-templates as the
10
- // literal placeholder name, with every gate green).
7
+ // The record's one identity source: instance.md — the same file every other
8
+ // surface reads, so renaming the instance renames every surface at the next
9
+ // build, and no shell carries a baked-in copy (found live 2026-08-18: a
10
+ // stamped constant survived a restore-from-templates as the literal
11
+ // placeholder name, with every gate green).
11
12
  function findInstance(start: string): string {
12
13
  let dir = start;
13
14
  for (let i = 0; i < 5; i += 1) {
@@ -22,24 +23,42 @@ function findInstance(start: string): string {
22
23
  );
23
24
  }
24
25
 
26
+ /** The project root: the directory holding instance.md. */
27
+ export const projectRoot: string = path.dirname(findInstance(process.cwd()));
28
+
25
29
  /**
26
- * instance.md's frontmatter block the configuration every surface reads
27
- * (identity here, the audience model in lib/audience.ts). Only this block:
28
- * body prose that looks like a key must never become configuration (review
29
- * finding, 2026-08-18).
30
+ * instance.md's frontmatter, read with the record's own parser (decision 26:
31
+ * one YAML reader, never a line scanner the scanner this replaced could not
32
+ * read a nested block and failed silently on it). Only the frontmatter: the
33
+ * body is the MCP server's instructions in full (record spec §3), and prose
34
+ * that looks like a key must never become configuration (review finding,
35
+ * 2026-08-18).
30
36
  */
31
- export function instanceFrontmatter(): string {
32
- const text = readFileSync(findInstance(process.cwd()), "utf8");
33
- // The checker's boundary exactly: BOM stripped, CRLF normalized, lax close.
34
- const normalized = text.replace(/^\uFEFF/, "").replaceAll("\r\n", "\n");
35
- return /^---\n([\s\S]*?)\n---/.exec(normalized)?.[1] ?? "";
37
+ function readInstance(): Readonly<Record<string, unknown>> {
38
+ const split = splitFrontmatter(
39
+ readFileSync(path.join(projectRoot, "instance.md"), "utf8"),
40
+ "instance.md",
41
+ );
42
+ if (!split.ok) {
43
+ throw new Error(
44
+ `${split.refusal.slug}: instance.md — ${split.refusal.why}\n fix: ${split.refusal.fix}`,
45
+ );
46
+ }
47
+ return split.frontmatter ?? {};
48
+ }
49
+
50
+ export const instanceData: Readonly<Record<string, unknown>> = readInstance();
51
+
52
+ function scalar(key: string): string | null {
53
+ const value = instanceData[key];
54
+ if (typeof value !== "string") return null;
55
+ const trimmed = value.trim();
56
+ return trimmed === "" ? null : trimmed;
36
57
  }
37
58
 
38
59
  function readInstanceName(): string {
39
- const raw = /^name:[ \t]*(.*)$/m.exec(instanceFrontmatter())?.[1]?.trim() ?? "";
40
- const unquoted = /^(['"])(.*)\1$/.exec(raw);
41
- const name = unquoted?.[2] ?? raw;
42
- if (name === "") {
60
+ const name = scalar("name");
61
+ if (name === null) {
43
62
  throw new Error("instance.md carries no name: — it is the project's identity; run pnpm check.");
44
63
  }
45
64
  return name;
@@ -48,89 +67,33 @@ function readInstanceName(): string {
48
67
  export const appName: string = readInstanceName();
49
68
 
50
69
  /**
51
- * The record's DISPLAY TITLE: instance.md's first body heading. The slug in
52
- * `name:` is the machine identity (llms.txt, future citations); the H1 is
53
- * the human name every page leads with. A fresh scaffold reads "Knowledge
54
- * System of Record" until the intake interview writes the real one.
70
+ * The record's DISPLAY TITLE: instance.md's `title:` (record spec §3, format
71
+ * 2 what used to be the body's H1). The slug in `name:` is the machine
72
+ * identity (llms.txt, citations); the title is the human name every page leads
73
+ * with. Falls back to the name so a page always has one.
55
74
  */
56
- function readInstanceTitle(): string {
57
- const text = readFileSync(findInstance(process.cwd()), "utf8");
58
- const body = text.replace(/^\uFEFF?---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n?/, "");
59
- return /^#[ \t]+(.+)$/m.exec(body)?.[1]?.trim() ?? appName;
60
- }
61
-
62
- export const appTitle: string = readInstanceTitle();
75
+ export const appTitle: string = scalar("title") ?? appName;
63
76
 
64
77
  /**
65
- * Whether the pages show the governance each document declares
66
- * (`site.governance` in instance.md, default on). Read once at build/server
67
- * start, like the identity above restart `pnpm dev` after changing it.
68
- */
69
- export const showGovernance: boolean = governanceVisible(instanceFrontmatter());
70
-
71
- /**
72
- * The record's own statement of what it is authoritative for: the first
73
- * paragraph of instance.md's body, below the display title.
74
- *
75
- * The home page used to carry a line of ksor's marketing copy instead — the
76
- * framework's voice above somebody else's knowledge, which the project's own
77
- * critical rule 1 forbids. This is the record speaking for itself, and it is
78
- * the same prose `ksor serve` hands the MCP server as its instructions, so the
79
- * two surfaces open with one sentence.
78
+ * The record's own one-sentence statement of what it is authoritative for:
79
+ * instance.md's `description:`. It seeds the home page, `llms.txt` and
80
+ * `server.json`, and it is the record speaking for itself the framework's
81
+ * marketing copy used to sit where it goes (research/site-design.md F7).
80
82
  *
81
- * Null when the body says nothing yet; the page then renders nothing rather
82
- * than inventing a purpose.
83
+ * Null when the owner has not written one; the page then renders nothing
84
+ * rather than inventing a purpose.
83
85
  */
84
- function readInstancePurpose(): string | null {
85
- const text = readFileSync(findInstance(process.cwd()), "utf8");
86
- const body = text.replace(/^?---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n?/, "");
87
- const afterTitle = body.replace(/^#[ \t]+.*$/m, "");
88
- const paragraph = afterTitle
89
- .split(/\n\s*\n/)
90
- .map((block) => block.trim())
91
- .find((block) => block !== "");
92
- if (paragraph === undefined) return null;
93
- // One paragraph, as one line: the body wraps at 80 columns for the file's
94
- // sake, and those newlines are not sentence breaks.
95
- return paragraph.replaceAll(/\s*\n\s*/g, " ").trim();
96
- }
86
+ export const appDescription: string | null = scalar("description");
97
87
 
98
- export const appPurpose: string | null = readInstancePurpose();
88
+ /** The description, under the name the home page's cover takes it by. */
89
+ export const appPurpose: string | null = appDescription;
99
90
 
100
91
  /**
101
- * How this record describes ITSELF, in one line — what an agent reads in a
102
- * registry listing to decide whether this record can answer its question.
103
- *
104
- * It comes from the record's own prose (instance.md's first real paragraph,
105
- * which the intake interview writes) because the alternative is what shipped
106
- * before: one hard-coded sentence, byte-identical in every ksor record ever
107
- * scaffolded, telling a discovering agent nothing that distinguishes this record
108
- * from any other. "Discoverability determines whether agents find you at all" is
109
- * a product principle, and a description that cannot discriminate is not
110
- * discoverability (found live 2026-08-21).
111
- *
112
- * An UNDESCRIBED record says so rather than borrowing a confident sentence it
113
- * has not earned — the same answer the MCP door already gives an agent that
114
- * connects, so the two surfaces do not disagree about whether this record knows
115
- * what it is. The marker is the template's own unfilled placeholder, matched on
116
- * the WHOLE body: a scaffold's first paragraphs are authoring guidance, and
117
- * reading one of those as the record's scope is worse than admitting there is
118
- * none.
92
+ * Whether the pages show the governance each document declares
93
+ * (`site.governance` in instance.md, default on). Read once at build/server
94
+ * start, like the identity above — restart `pnpm dev` after changing it.
119
95
  */
120
- const TEMPLATE_MARKER = "_fill this in; it is";
121
-
122
- function readInstanceScope(): string | null {
123
- const text = readFileSync(findInstance(process.cwd()), "utf8");
124
- const body = text.replace(/^\uFEFF?---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n?/, "");
125
- if (body.includes(TEMPLATE_MARKER)) return null;
126
- const afterHeading = body.replace(/^[\s\S]*?^#[ \t]+.+$/m, "");
127
- for (const para of afterHeading.split(/\n[ \t]*\n/)) {
128
- const one = para.trim().replace(/\s+/g, " ");
129
- if (one === "" || one.startsWith("#") || one.startsWith("-") || one.startsWith(">")) continue;
130
- return sentence(one);
131
- }
132
- return null;
133
- }
96
+ export const showGovernance: boolean = governanceVisible(instanceData);
134
97
 
135
98
  /**
136
99
  * The MCP registry schema caps `ServerDetail.description` at **100 characters**
@@ -146,9 +109,10 @@ function readInstanceScope(): string | null {
146
109
  */
147
110
  const DESCRIPTION_MAX = 100;
148
111
 
149
- function sentence(one: string): string {
150
- const first = /^(.+?[.!?])(\s|$)/.exec(one)?.[1] ?? one;
151
- return first;
112
+ /** One paragraph as one line: a wrapped description is not several sentences. */
113
+ function sentence(text: string): string {
114
+ const one = text.replace(/\s+/g, " ").trim();
115
+ return /^(.+?[.!?])(\s|$)/.exec(one)?.[1] ?? one;
152
116
  }
153
117
 
154
118
  /** Fit `text` inside `max`, breaking on a word rather than mid-word. */
@@ -156,11 +120,11 @@ function fit(text: string, max: number): string {
156
120
  if (text.length <= max) return text;
157
121
  const cut = text.slice(0, max - 1);
158
122
  const at = cut.lastIndexOf(" ");
159
- return `${(at > max / 2 ? cut.slice(0, at) : cut).replace(/[,;:.\s]+$/, "")}\u2026`;
123
+ return `${(at > max / 2 ? cut.slice(0, at) : cut).replace(/[,;:.\s]+$/, "")}…`;
160
124
  }
161
125
 
162
- /** null until the owner has written one — never a guess. */
163
- export const appScope: string | null = readInstanceScope();
126
+ /** The first sentence of the description, or null until the owner has written one — never a guess. */
127
+ export const appScope: string | null = appDescription === null ? null : sentence(appDescription);
164
128
 
165
129
  /**
166
130
  * The one-line description every discovery surface publishes. Built here so the
@@ -181,9 +145,7 @@ export function recordDescription(): string {
181
145
  * agent would try it and conclude the record is down rather than unpublished.
182
146
  */
183
147
  export function mcpEndpoint(): string | null {
184
- const declared = /^mcp_url:[ \t]*(.*)$/m.exec(instanceFrontmatter())?.[1] ?? "";
185
- const value = declared.trim().replace(/^["']|["']$/g, "");
186
- return value === "" ? null : value;
148
+ return scalar("mcp_url");
187
149
  }
188
150
 
189
151
  /**
@@ -217,7 +179,8 @@ export function mcpNamespace(): string {
217
179
  * instance.md and falls back to a first-release default.
218
180
  */
219
181
  export function recordVersion(): string {
220
- const declared = /^version:[ \t]*(.*)$/m.exec(instanceFrontmatter())?.[1] ?? "";
221
- const value = declared.trim().replace(/^["']|["']$/g, "");
222
- return /^\d+\.\d+\.\d+/.test(value) ? value : "0.1.0";
182
+ const value = instanceData["version"];
183
+ const text =
184
+ typeof value === "number" ? String(value) : typeof value === "string" ? value.trim() : "";
185
+ return /^\d+\.\d+\.\d+/.test(text) ? text : "0.1.0";
223
186
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * What makes a file in the record a SIM — an interactive page the record
3
+ * CARRIES, served rather than bundled.
4
+ *
5
+ * A sim is an ASSET of its document, exactly like the figures beside it: many
6
+ * per document, named freely, linked from the prose the way a figure is, and
7
+ * staged only when a published document references it. It is deliberately NOT
8
+ * a study attachment (decision 24) — an attachment is named after its parent
9
+ * (`<doc>.quiz.yaml`), and seven sims cannot all be `index.sim.html`. So this
10
+ * rule is the SUFFIX and nothing else: there is no parent name to derive, and
11
+ * governance is inherited by POSITION — through the concept whose link is the
12
+ * only way a sim ever reaches the stage.
13
+ *
14
+ * The suffix is what separates a page the record MEANS to serve from an
15
+ * `.html` that fell into `knowledge/`. Both are bytes with the same extension,
16
+ * so without a marker the record would either serve every stray export or
17
+ * serve none of the sims — which is why the checker admits this shape by name
18
+ * and refuses the rest.
19
+ *
20
+ * A LEAF: no imports, so the record's checker and the site's embed rule can
21
+ * each take it without taking anything else with them. Duplicated by
22
+ * construction — the site cannot import the kernel (decision 18) — and the
23
+ * copy is asserted rather than trusted.
24
+ */
25
+
26
+ /** The suffix that marks an asset as a page to be SERVED rather than bundled. */
27
+ export const SIM_SUFFIX = ".sim.html";
28
+
29
+ /**
30
+ * True when this file name is a sim.
31
+ *
32
+ * Matched on the whole base name: a file called exactly `.sim.html` is a
33
+ * dotfile with no stem, so it names no page and is not one — the same
34
+ * boundary `attachmentKindOf` draws, and it keeps a stray dotfile falling
35
+ * through to the honest "unexpected file type" refusal.
36
+ */
37
+ export function isSim(baseName: string): boolean {
38
+ return baseName.length > SIM_SUFFIX.length && baseName.endsWith(SIM_SUFFIX);
39
+ }
40
+
41
+ /**
42
+ * Where a sim is served, derived from where it sits in the record.
43
+ *
44
+ * The record path is the identity (product principle 3), so two documents may
45
+ * each own a `goal-loop.sim.html` without colliding.
46
+ */
47
+ export function publicSimPath(recordRelative: string): string {
48
+ return recordRelative.slice(0, -SIM_SUFFIX.length).replaceAll("\\", "/") + ".html";
49
+ }