@panaversity/ksor 0.0.20 → 0.0.21

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 (42) hide show
  1. package/CHANGELOG.md +472 -0
  2. package/dist/cli.mjs +71 -19
  3. package/package.json +3 -3
  4. package/templates/scaffold/.agents/skills/format-checker/check.mjs +232 -9
  5. package/templates/scaffold/.claude/skills/format-checker/check.mjs +232 -9
  6. package/templates/scaffold/AGENTS.md +52 -4
  7. package/templates/scaffold/instance.md +28 -20
  8. package/templates/scaffold/knowledge/governance-ladder.md +36 -0
  9. package/templates/scaffold/knowledge/surfaces/for-agents.md +29 -0
  10. package/templates/scaffold/knowledge/surfaces/for-people.md +35 -0
  11. package/templates/scaffold/knowledge/surfaces/index.md +21 -0
  12. package/templates/scaffold/knowledge/what-is-a-ksor.md +39 -0
  13. package/templates/scaffold/pnpm-lock.yaml +1198 -228
  14. package/templates/scaffold/system/site/app/(home)/layout.tsx +6 -0
  15. package/templates/scaffold/system/site/app/(home)/page.tsx +65 -70
  16. package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +122 -14
  17. package/templates/scaffold/system/site/app/docs/layout.tsx +2 -21
  18. package/templates/scaffold/system/site/app/global.css +552 -9
  19. package/templates/scaffold/system/site/app/layout.tsx +23 -4
  20. package/templates/scaffold/system/site/app/llms-full.txt/route.ts +4 -2
  21. package/templates/scaffold/system/site/app/llms.txt/route.ts +11 -9
  22. package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +51 -0
  23. package/templates/scaffold/system/site/components/copy-markdown.tsx +70 -0
  24. package/templates/scaffold/system/site/components/governance.tsx +262 -0
  25. package/templates/scaffold/system/site/components/home-cover.tsx +137 -0
  26. package/templates/scaffold/system/site/components/record-index.tsx +120 -0
  27. package/templates/scaffold/system/site/components/record-shell.tsx +68 -0
  28. package/templates/scaffold/system/site/components/record-stack.tsx +131 -0
  29. package/templates/scaffold/system/site/components/record-toc.tsx +160 -0
  30. package/templates/scaffold/system/site/components/search-dialog.tsx +130 -0
  31. package/templates/scaffold/system/site/components/sidebar-status.tsx +35 -0
  32. package/templates/scaffold/system/site/components/ui/badge.tsx +46 -0
  33. package/templates/scaffold/system/site/components/ui/button.tsx +62 -0
  34. package/templates/scaffold/system/site/components/ui/separator.tsx +28 -0
  35. package/templates/scaffold/system/site/components.json +25 -0
  36. package/templates/scaffold/system/site/lib/governance.ts +432 -0
  37. package/templates/scaffold/system/site/lib/layout.shared.tsx +1 -1
  38. package/templates/scaffold/system/site/lib/shared.ts +38 -0
  39. package/templates/scaffold/system/site/lib/source.ts +221 -5
  40. package/templates/scaffold/system/site/lib/utils.ts +6 -0
  41. package/templates/scaffold/system/site/package.json +9 -3
  42. package/templates/scaffold/knowledge/example.md +0 -23
@@ -115,6 +115,7 @@ function parseFrontmatter(text) {
115
115
  const malformedQuote = new Map();
116
116
  const malformed = [];
117
117
  const duplicates = [];
118
+ const truncated = [];
118
119
  const tightColons = [];
119
120
  const tabIndents = [];
120
121
  let current = null;
@@ -157,6 +158,12 @@ function parseFrontmatter(text) {
157
158
  }
158
159
  const nested = /^[ \t]+([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(line);
159
160
  if (nested && current !== null) {
161
+ // Same hazard as the top-level duplicate above, one level down: this Map
162
+ // keeps the LAST write while the surfaces read the FIRST occurrence, so
163
+ // the check validates one value and the build publishes the other
164
+ // (found 2026-08-20: `governance: nope` then `governance: false` passed
165
+ // the check and crashed the build).
166
+ if (children.get(current).has(nested[1])) duplicates.push(`${current}.${nested[1]}`);
160
167
  children.get(current).set(nested[1], unquote(nested[2]));
161
168
  continue;
162
169
  }
@@ -168,7 +175,16 @@ function parseFrontmatter(text) {
168
175
  // entry, and reading it as one refuses the documents instead of the
169
176
  // list (found live 2026-08-18: `- public # the default` made every
170
177
  // public document's visibility undeclared).
171
- const value = /^["']/.test(item[1]) ? item[1] : item[1].replace(/\s+#.*$/, "");
178
+ const quotedItem = /^["']/.test(item[1]);
179
+ const value = quotedItem ? item[1] : item[1].replace(/\s+#.*$/, "");
180
+ // A `#` is a comment to YAML and an invoice or issue number to a person.
181
+ // In `provenance` — free text naming a real source — the truncation is
182
+ // silent data loss: `- Invoice #4471 from Acme Ltd` publishes as
183
+ // "Invoice" (found 2026-08-20). NOT flagged for `audiences`, where a
184
+ // trailing comment is an intended and documented use.
185
+ if (!quotedItem && current === "provenance" && value !== item[1]) {
186
+ truncated.push({ key: current, raw: item[1].trim(), kept: value.trim() });
187
+ }
172
188
  lists.get(current).push(unquote(value));
173
189
  continue;
174
190
  }
@@ -180,7 +196,19 @@ function parseFrontmatter(text) {
180
196
  malformed.push(line.trim());
181
197
  continue;
182
198
  }
183
- if (/^[ \t]*-([ \t]|$)/.test(line) || /^[ \t]+\S/.test(line)) continue;
199
+ if (/^[ \t]*-([ \t]|$)/.test(line)) continue;
200
+ // A whole-line comment is YAML, not a wrapped value — an author annotating
201
+ // their own frontmatter ("# add the signed PDF once it lands") is doing
202
+ // nothing wrong, and refusing it turned the adopter's shipped CI red on a
203
+ // record both surfaces read perfectly (found 2026-08-21).
204
+ if (/^[ \t]*#/.test(line)) continue;
205
+ // An indented line that is neither a nested key nor a list item is a value
206
+ // WRAPPED onto a second line. YAML folds it back into the value above;
207
+ // this parser used to skip it, so every rule here inspected a string that
208
+ // was not what the surfaces publish — `effective: 2026-04-01` continued by
209
+ // ` 00:00:00 +05:00` passed the date rule and shipped the day before
210
+ // (found 2026-08-20). A value the checker cannot see is a value it cannot
211
+ // govern.
184
212
  malformed.push(line.trim());
185
213
  }
186
214
  return {
@@ -190,6 +218,7 @@ function parseFrontmatter(text) {
190
218
  quoted,
191
219
  malformedQuote,
192
220
  duplicates,
221
+ truncated,
193
222
  malformed,
194
223
  tightColons,
195
224
  tabIndents,
@@ -472,9 +501,16 @@ if (!existsSync(knowledgeDir)) {
472
501
 
473
502
  // frontmatter + links per document
474
503
  const visibilityByPath = new Map();
504
+ const documentPaths = new Set();
475
505
  const crossings = [];
476
506
  for (const p of mdFiles) {
477
507
  const rel = path.relative(root, p);
508
+ // Every markdown file under knowledge/ IS a document of the record, whether
509
+ // or not its own frontmatter parses. Counting it only after a clean parse
510
+ // made a correct `superseded_by` be reported as a capitalisation mistake
511
+ // whenever its successor was the document still being written (round 3) —
512
+ // two problems for one cause, and the second one false.
513
+ documentPaths.add(p);
478
514
  const text = readFileSync(p, "utf8");
479
515
  const fm = parseFrontmatter(text);
480
516
  if (fm === null) {
@@ -490,7 +526,7 @@ if (!existsSync(knowledgeDir)) {
490
526
  problem(
491
527
  rel,
492
528
  `unclosed or malformed frontmatter — this is not a frontmatter line: "${fm.malformed[0]}"`,
493
- "an unclosed block swallows the body: the checker reads prose as governance, and the site renders a document with no title",
529
+ "an unclosed block swallows the body, and a value wrapped onto a second line is folded back by YAML but invisible here — either way this check inspects something the site does not publish",
494
530
  "close the block with --- on its own line; every line inside it is `key: value` or a `- list item` — no prose, no comments",
495
531
  );
496
532
  } else {
@@ -618,9 +654,28 @@ if (!existsSync(knowledgeDir)) {
618
654
  "add superseded_by: ./<successor>.md",
619
655
  );
620
656
  }
621
- // A successor that names a path must be a document that exists: the
622
- // pointer is the whole value of marking something superseded.
623
- if (successor && (/^\.{1,2}\//.test(successor) || successor.toLowerCase().endsWith(".md"))) {
657
+ if (successor && status !== "superseded") {
658
+ problem(
659
+ rel,
660
+ `superseded_by on a document that is status: ${status || "(none)"}`,
661
+ "the two keys are one statement — a successor pointer says this document was replaced, so a record that keeps the pointer while calling the document current contradicts itself, and the site publishes a Superseded notice over a live document",
662
+ "set status: superseded, or remove superseded_by if this document is still current",
663
+ );
664
+ }
665
+ // superseded_by must be a document pointer, and EVERY successor is
666
+ // validated. This was gated behind a shape test until 2026-08-20, so a
667
+ // value matching neither shape (`hr/refunds-2026`) skipped existence,
668
+ // escape-the-record AND the cross-audience rule — and then the site
669
+ // published the raw pointer, naming a document a lower tier must not know
670
+ // exists.
671
+ if (successor && !successor.split("#")[0].toLowerCase().endsWith(".md")) {
672
+ problem(
673
+ rel,
674
+ `superseded_by is not a document pointer: ${successor}`,
675
+ "unless it names a markdown document this check cannot tell whether the successor exists, stays inside the record, or is readable by this document's audience — and the site publishes the raw text of it",
676
+ "write it as a relative path to the successor, e.g. superseded_by: ./<successor>.md",
677
+ );
678
+ } else if (successor) {
624
679
  const resolved = path.resolve(path.dirname(p), successor.split("#")[0]);
625
680
  if (!resolved.startsWith(knowledgeDir + path.sep)) {
626
681
  problem(
@@ -629,7 +684,7 @@ if (!existsSync(knowledgeDir)) {
629
684
  "the successor is what readers are sent to instead — outside knowledge/ it is not a governed document",
630
685
  "point superseded_by at a document inside knowledge/",
631
686
  );
632
- } else if (!existsSync(resolved)) {
687
+ } else if (!existsSync(resolved) || !lstatSync(resolved).isFile()) {
633
688
  problem(
634
689
  rel,
635
690
  `superseded_by points at a document that does not exist: ${successor}`,
@@ -640,6 +695,76 @@ if (!existsSync(knowledgeDir)) {
640
695
  crossings.push({ kind: "superseded_by", rel, from: p, to: resolved, target: successor });
641
696
  }
642
697
  }
698
+ for (const cut of fm.truncated) {
699
+ problem(
700
+ rel,
701
+ `a ${cut.key} entry is cut short at a #: ${cut.raw}`,
702
+ `YAML ends an unquoted value at " #", so the record stores only "${cut.kept}" and that is what the page publishes as the source — the rest is lost without a word`,
703
+ `quote it to keep the whole thing: - "${cut.raw.replace(/"/g, "'")}"`,
704
+ );
705
+ }
706
+ // Every governed key except `provenance` is ONE value. Written as a
707
+ // block sequence or a nested map it parses to an array or an object, and
708
+ // the page — which asks for text — renders nothing at all: the record
709
+ // declares the fact and the surface silently omits it (found 2026-08-21
710
+ // with `effective:` followed by ` - 2026-04-01`). `visibility` has its
711
+ // own list rule with a better message, so it is left to it.
712
+ for (const scalarKey of [
713
+ "title",
714
+ "description",
715
+ "status",
716
+ "owner",
717
+ "effective",
718
+ "superseded",
719
+ "superseded_by",
720
+ "order",
721
+ ]) {
722
+ const asList = (fm.lists.get(scalarKey) ?? []).length > 0;
723
+ const asMap = (fm.children.get(scalarKey)?.size ?? 0) > 0;
724
+ if (asList || asMap) {
725
+ problem(
726
+ rel,
727
+ `${scalarKey} is written as ${asList ? "a list" : "a nested block"}, not a value`,
728
+ "the surfaces read this key as one piece of text; as a list or a map it reaches them as neither, so the page publishes nothing where the record declares something",
729
+ `put the value on the key's own line: ${scalarKey}: <value>`,
730
+ );
731
+ }
732
+ }
733
+ // `effective` is the DAY a document takes effect. Written unquoted with a
734
+ // time, YAML makes it a timestamp, and normalizing that to a UTC day
735
+ // prints the day before the record's for any positive offset (found
736
+ // 2026-08-20: `2026-04-01 00:00:00 +05:00` rendered 2026-03-31).
737
+ // `effective` is the DAY a document takes effect, and the page publishes
738
+ // it as fact inside <time datetime>. Unquoted it must be a calendar-valid
739
+ // YYYY-MM-DD and nothing else, because every other shape YAML accepts
740
+ // here publishes something the record does not say: `2026-06-31` rolls
741
+ // silently to July 1st (js-yaml's date path is `Date.UTC(y, m, d)` with
742
+ // no validation), a value carrying a time reads back in a timezone and
743
+ // can land a day early, and a bare `2026` types as a number that never
744
+ // reaches the page at all. Three rounds of narrower rules each leaked a
745
+ // different one of those, so the rule is now the whole contract: a plain
746
+ // date, or QUOTED text that is published verbatim and never parsed.
747
+ const effective = fm.keys.get("effective");
748
+ if (effective !== undefined && effective !== "" && !fm.quoted.has("effective")) {
749
+ const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(effective);
750
+ let calendarValid = false;
751
+ if (iso !== null) {
752
+ const [year, month, day] = [Number(iso[1]), Number(iso[2]), Number(iso[3])];
753
+ const probe = new Date(Date.UTC(year, month - 1, day));
754
+ calendarValid =
755
+ probe.getUTCFullYear() === year &&
756
+ probe.getUTCMonth() === month - 1 &&
757
+ probe.getUTCDate() === day;
758
+ }
759
+ if (!calendarValid) {
760
+ problem(
761
+ rel,
762
+ `effective is not a calendar date: ${effective}`,
763
+ "the page publishes this as the day the document takes effect, in a <time> element a machine reads as fact — YAML rolls an impossible date to the next month without a word, reads a value carrying a time in a timezone, and types a bare year as a number that never reaches the page",
764
+ `write a real date (effective: 2026-04-01), or quote it to publish it as text (effective: "${effective.replace(/"/g, "'")}")`,
765
+ );
766
+ }
767
+ }
643
768
  // visibility: one audience per document, from the set instance.md declares.
644
769
  // Through scalarValue, so the comparison sees what the READERS see: a raw
645
770
  // read reported `visibility "internal # narrowed 2026-08" is not a
@@ -684,6 +809,58 @@ if (!existsSync(knowledgeDir)) {
684
809
  }
685
810
  }
686
811
 
812
+ for (const { kind, rel, to, target } of crossings) {
813
+ if (kind !== "superseded_by" || documentPaths.has(to)) continue;
814
+ problem(
815
+ rel,
816
+ `superseded_by does not name a document in the record: ${target}`,
817
+ "it resolves to a file the record does not govern — on a case-insensitive filesystem a mis-typed capitalisation resolves happily here and then misses every rule keyed by the real path, the cross-audience check included",
818
+ "match the successor's path exactly as it appears under knowledge/ (ascii lowercase)",
819
+ );
820
+ }
821
+
822
+ // A supersession must lead somewhere. A document that supersedes ITSELF, or a
823
+ // pair that supersede each other, published a notice telling the reader this
824
+ // page is replaced — by a link back to a page saying the same thing. The
825
+ // reader is sent in a circle and never reaches a current document (found
826
+ // 2026-08-20).
827
+ {
828
+ const successorOf = new Map();
829
+ for (const { kind, from, to } of crossings) {
830
+ if (kind === "superseded_by" && !successorOf.has(from)) successorOf.set(from, to);
831
+ }
832
+ const walked = new Set();
833
+ for (const start of successorOf.keys()) {
834
+ if (walked.has(start)) continue;
835
+ // An ORDERED path, not a set: the documents visited before the loop
836
+ // closes are not part of the cycle, and reporting them made the check
837
+ // blame a document whose pointer was correct while printing an edge the
838
+ // record does not contain (found 2026-08-20 — a → b → c → b was reported
839
+ // as "a → b → c → a").
840
+ const trail = [start];
841
+ const onTrail = new Set([start]);
842
+ let cursor = successorOf.get(start);
843
+ while (cursor !== undefined && !onTrail.has(cursor)) {
844
+ onTrail.add(cursor);
845
+ trail.push(cursor);
846
+ cursor = successorOf.get(cursor);
847
+ }
848
+ for (const node of trail) walked.add(node);
849
+ if (cursor === undefined) continue; // the chain ends at a current document
850
+ // The cycle is the trail from the document the walk returned to.
851
+ const cycle = trail.slice(trail.indexOf(cursor));
852
+ const names = cycle.map((file) => path.relative(root, file));
853
+ problem(
854
+ names[0],
855
+ cycle.length === 1
856
+ ? "superseded_by points at this document itself"
857
+ : `supersession cycle: ${names.join(" → ")} → ${names[0]}`,
858
+ "a supersession sends the reader to the successor that replaced this one — a pointer that comes back here sends them in a circle and never reaches a current document",
859
+ "point superseded_by at the document that actually replaces this one, or set status back if nothing does",
860
+ );
861
+ }
862
+ }
863
+
687
864
  // Pointers across audiences: the leak no single build can catch, because the
688
865
  // build that publishes the pointer has already dropped its target and cannot
689
866
  // know it ever existed. Only the whole record sees both ends.
@@ -750,7 +927,7 @@ const INSTANCE_KEYS = new Set([
750
927
  "budgets",
751
928
  ]);
752
929
  const INSTANCE_KSOR_KEYS = new Set(["requires", "scaffolded"]);
753
- const INSTANCE_SITE_KEYS = new Set(["url"]);
930
+ const INSTANCE_SITE_KEYS = new Set(["url", "governance"]);
754
931
  // Nested field names mirror the kernel's instance schema; the kernel validates
755
932
  // their values (this checker stays dependency-free and cannot import it).
756
933
  const INSTANCE_DATABASE_KEYS = new Set(["dsn_env", "tenant_id"]);
@@ -778,7 +955,7 @@ if (!existsSync(instanceMd)) {
778
955
  problem(
779
956
  "instance.md",
780
957
  `unclosed or malformed frontmatter — this is not a frontmatter line: "${fm.malformed[0]}"`,
781
- "an unclosed block swallows the identity prose and turns it into unreadable configuration",
958
+ "an unclosed block swallows the identity prose and turns it into unreadable configuration; a value wrapped onto a second line is folded back by YAML but invisible here, so this check governs a string the surfaces never see",
782
959
  "close the block with --- on its own line; every line inside it is `key: value` — the identity prose belongs below it",
783
960
  );
784
961
  } else {
@@ -920,6 +1097,31 @@ if (!existsSync(instanceMd)) {
920
1097
  "add audiences: (ordered least- to most-restricted, public first), or remove default_visibility:",
921
1098
  );
922
1099
  }
1100
+ // A group written as a flow mapping (`site: { governance: false }`) lands
1101
+ // as a scalar with NO children, so every nested rule below — the closed key
1102
+ // set included — silently skips it: the owner's setting is dropped without
1103
+ // a word (found 2026-08-20). The groups are block mappings, always.
1104
+ for (const parent of ["ksor", "site", "database", "embedding", "retrieval", "budgets"]) {
1105
+ // A trailing ` #` comment is part of this checker's own grammar — it is
1106
+ // stripped for values and for list items, and `audiences: # who may read`
1107
+ // passes on the same file. Reading the raw value called `site: # notes` an
1108
+ // inline mapping and refused a well-formed record, with a why that was
1109
+ // factually false: the surfaces read that file perfectly (found
1110
+ // 2026-08-20).
1111
+ const rawGroup = fm.keys.get(parent);
1112
+ const inline =
1113
+ rawGroup === undefined || rawGroup.trim().startsWith("#")
1114
+ ? ""
1115
+ : rawGroup.replace(/\s+#.*$/, "").trim();
1116
+ if (inline !== "") {
1117
+ problem(
1118
+ "instance.md",
1119
+ `${parent}: has an inline value: ${inline}`,
1120
+ "a group written on one line is not read as a group — every key inside it is skipped by this check AND by the surfaces, so the settings the owner wrote are silently dropped",
1121
+ `write it as an indented block:\n ${parent}:\n <key>: <value>`,
1122
+ );
1123
+ }
1124
+ }
923
1125
  for (const [parent, allowed] of [
924
1126
  ["ksor", INSTANCE_KSOR_KEYS],
925
1127
  ["site", INSTANCE_SITE_KEYS],
@@ -929,6 +1131,27 @@ if (!existsSync(instanceMd)) {
929
1131
  ["budgets", INSTANCE_BUDGETS_KEYS],
930
1132
  ]) {
931
1133
  for (const key of fm.children.get(parent)?.keys() ?? []) {
1134
+ // site.governance is a switch, so its VALUE is checked here: a typo
1135
+ // that silently defaulted would publish the governance the owner asked
1136
+ // to hide, or hide what they asked to publish.
1137
+ if (parent === "site" && key === "governance") {
1138
+ const raw = (fm.children.get("site")?.get("governance") ?? "").trim();
1139
+ const value = (/^["']/.test(raw) ? raw : raw.replace(/\s+#.*$/, ""))
1140
+ .trim()
1141
+ .replace(/^(['"])(.*)\1$/, "$2")
1142
+ // Case-folded: js-yaml reads `False` as false, and the site's own
1143
+ // reader lowercases before comparing. A checker stricter than both
1144
+ // surfaces is the very divergence this rule exists to stop.
1145
+ .toLowerCase();
1146
+ if (value !== "true" && value !== "false") {
1147
+ problem(
1148
+ "instance.md",
1149
+ `site.governance is "${value}" — it must be true or false`,
1150
+ "it decides whether pages show the owner, effective date and sources each document declares; a value nobody can read is a setting the owner believes is in effect",
1151
+ 'write "governance: false" to keep pages plain, or remove the key (the default shows them)',
1152
+ );
1153
+ }
1154
+ }
932
1155
  if (!allowed.has(key)) {
933
1156
  problem(
934
1157
  "instance.md",
@@ -15,6 +15,12 @@ The record survives the system: `knowledge/` must stay readable and complete
15
15
  even if `system/` is deleted. Dependency flows one way — the system reads the
16
16
  record; the record never references the system.
17
17
 
18
+ Every grouped key in `instance.md` (`ksor`, `site`, `database`, `embedding`,
19
+ `retrieval`, `budgets`) is written as an indented block, never inline on one
20
+ line: a group written as `site: { governance: false }` is not read as a group
21
+ at all, so every setting inside it is silently dropped. `pnpm check` refuses
22
+ that shape, and refuses a key repeated inside a group.
23
+
18
24
  `instance.md` carries a closed key set — `format`, `name`, `ksor`, `site`,
19
25
  the optional pair `audiences` + `default_visibility` (the record's reader
20
26
  audiences, ordered least- to most-restricted with `public` first, and the one
@@ -364,9 +370,37 @@ Details in README → Deploying.
364
370
  are required. `owner` and `provenance` (a list naming real sources) are
365
371
  strongly encouraged — they become required as this project climbs the
366
372
  governance ladder. `description`, `visibility` (below), `order` (sidebar
367
- position), `effective` (the date the document takes effect) and `superseded`
368
- (a legacy marker prefer `status`) are available. No other keys; never
373
+ position), `effective` (the date the document takes effect a real
374
+ `YYYY-MM-DD` date and nothing else, or **quote it** to publish it as text:
375
+ `effective: "Q1 2026"`. Unquoted, YAML turns `2026-06-31` into July 1st and
376
+ `2026-04-01 09:00 +05:00` into the day before, without a word, and the page
377
+ publishes that as fact) and `superseded` (a legacy marker — prefer `status`)
378
+ are available. No other keys; never
369
379
  `id:` or `name:` — the path is the identity.
380
+ - **The governance keys are rendered, so they are worth filling in.** Each
381
+ page shows its owner and effective date under the title, lists every
382
+ `provenance` entry separately at the foot, and — for a superseded document —
383
+ carries a notice above the title naming its successor and linking to it. A
384
+ key you leave off renders nothing at all: the site never invents a value, so
385
+ a missing owner reads as missing rather than as unowned.
386
+ - **The agent surface carries them too.** `llms.txt` marks a document whose
387
+ status is a caveat and names the route that replaced a superseded one;
388
+ `llms-full.txt` puts the keys back as frontmatter above each document. An
389
+ agent reading the record therefore sees what a reader sees — a withdrawn
390
+ document is never handed over as plain prose.
391
+ - **Don't want any of it on the published pages?** Set `governance: false`
392
+ under `site:` in `instance.md`. The record keeps every key — the agent
393
+ surface and your audit trail still read them, and `llms.txt`/`llms-full.txt`
394
+ keep publishing them — and the pages simply stay plain. One consequence worth
395
+ knowing: the home page shows the agent surface VERBATIM in its panel, so the
396
+ keys stay visible there even with this off. That panel's whole claim is that
397
+ it is the bytes an agent is served, and editing them to match a page setting
398
+ would make it lie. Remove the panel if you need the front page silent too. The supersession notice is the one thing it does not hide: a reader
399
+ handed a replaced document with no word of its successor has been misled.
400
+ - **`status` is shown only when it is a caveat.** `draft`, `review` and
401
+ `superseded` appear as a small label; `approved` shows nothing, because a
402
+ reader already assumes a document in the record is current — so the label
403
+ stays rare enough to be noticed on the pages where it matters.
370
404
  - `visibility:` names the one audience a document belongs to — a single value
371
405
  from `instance.md`'s `audiences:`, never a list, and orthogonal to `status:`
372
406
  (an approved document can be restricted, and a draft is not hidden). Leave
@@ -385,7 +419,14 @@ Details in README → Deploying.
385
419
  document and can clone, the answer is a second repository.**
386
420
 
387
421
  - A replaced document is marked `status: superseded` with `superseded_by:`
388
- pointing at its successor — superseded documents are never deleted.
422
+ pointing at its successor — superseded documents are never deleted. The two
423
+ keys are one statement, so `pnpm check` refuses each without the other: a
424
+ successor pointer left on a document you have set back to `approved` would
425
+ publish a "Superseded" banner over a live document. The pointer must name a
426
+ markdown document (`./<successor>.md`), exactly as it is capitalised under
427
+ `knowledge/`, and it must lead somewhere: a document that supersedes itself,
428
+ or a pair that supersede each other, sends the reader in a circle and is
429
+ refused.
389
430
  - Images and assets live in `knowledge/` beside the document that uses them,
390
431
  referenced by relative links. A relative link must never leave `knowledge/`.
391
432
  - Copy load-bearing values (numbers, thresholds, dates) exactly from their
@@ -420,9 +461,16 @@ You own `system/site/` outright — these are the seams, cheapest first:
420
461
  - **Display title** — `instance.md`'s body `# H1` (the intake interview
421
462
  writes it). Headline, navbar, and browser title follow on restart.
422
463
  - **Accent color** — the one brand pair in `system/site/app/global.css`
423
- (`--color-fd-primary`, light and dark); every accented element follows.
464
+ (`--primary` and `--primary-foreground`, light and dark); every accented
465
+ element follows, in the shell and in every shadcn component alike.
424
466
  - **Logo and favicon** — replace `system/site/app/icon.png`; the tab icon
425
467
  and the home-page mark are the same file.
468
+ - **Components** — the site is a shadcn/ui project (`system/site/components.json`),
469
+ so `pnpm dlx shadcn@latest add <name>` writes a component into
470
+ `system/site/components/ui/` that you then own like everything else here.
471
+ Fumadocs reads the same tokens (its `shadcn` preset maps every `--color-fd-*`
472
+ onto the shadcn variable of the same role), so a registry component and the
473
+ documentation shell around it stay one palette.
426
474
  - **Anything deeper** — edit the site like the Next.js app it is; the only
427
475
  rule that survives customization is critical rule 1. The whole shell is
428
476
  replaceable behind a five-clause contract (a themed Docusaurus shell with
@@ -23,29 +23,37 @@ ksor:
23
23
  # version: 0.1.0
24
24
  ---
25
25
 
26
- # Knowledge System of Record
26
+ # KSoR
27
27
 
28
- The heading above is this record's **display title** the human name every
29
- page leads with. The intake interview replaces it with the real one
30
- ("Acme Operations Handbook"); the machine identity stays `KSOR-STAMP-NAME`
31
- in the frontmatter, and that is what agents and citations use.
28
+ This record is authoritative for what a Knowledge System of Record is, how a
29
+ project climbs the governance ladder, and which surfaces the same governed
30
+ knowledge is published through. It does not cover the CLI's release history or
31
+ the internals of the retrieval kernel.
32
32
 
33
- This Knowledge System of Record is authoritative for _fill this in; it is
34
- the single most important sentence in the project._
33
+ Write and govern the knowledge once; every surface here derives from it. When a
34
+ slide deck, a wiki page or a model's memory disagrees with this record, this
35
+ record wins.
35
36
 
36
- Everything below this frontmatter is the identity of this instance: what the
37
- corpus covers, who it serves, and how strictly it should decline questions it
38
- does not cover. This prose IS the agent surface's system prompt `ksor serve`
39
- wires it into the MCP server's instructions so write it for a reader who must
40
- act on it.
37
+ ## This is a starter, and it is yours to replace
38
+
39
+ Everything above describes KSoR itself. It ships filled in so that a fresh
40
+ project has a real governed corpus on the first `pnpm dev` statuses, owners,
41
+ provenance, a folder and a draft — instead of an empty shelf and a placeholder.
42
+ The documents live in `knowledge/`; delete them as your own knowledge arrives.
43
+
44
+ Be deliberate about replacing it, because a starter that describes the wrong
45
+ thing describes it _everywhere_. Two lines here are read by every surface:
46
+
47
+ - **The heading** is the display title — the human name every page leads with.
48
+ The machine identity stays `KSOR-STAMP-NAME` in the frontmatter, and that is
49
+ what citations and `llms.txt` use.
50
+ - **The first paragraph** is this record's scope. The site publishes it, and
51
+ `ksor serve` hands it to a connecting agent as the MCP server's instructions.
52
+ A record published with this paragraph unchanged will tell an agent — quite
53
+ accurately, and quite uselessly for you — that it is authoritative for what a
54
+ Knowledge System of Record is.
41
55
 
42
56
  Ask your coding agent to run the **intake interview** (it knows how — see
43
57
  `.agents/skills/intake-interview/`), answer its questions, and let it write
44
- this document with you.
45
-
46
- Until you do, `ksor serve` says so — at boot, and to every agent that connects:
47
- the MCP surface replaces this template with a plain statement that the record's
48
- scope is unstated, rather than passing authoring guidance to a runtime agent as
49
- if it were instructions. Nothing breaks, and the record still answers with
50
- citations; it just cannot tell an agent what it is authoritative FOR, which is
51
- the one thing that makes an answer worth trusting.
58
+ this document with you. Replace those two lines and every surface follows,
59
+ because every surface reads them from here.
@@ -0,0 +1,36 @@
1
+ ---
2
+ title: The governance ladder
3
+ description: Level 0 works immediately; a project climbs only as far as its domain needs.
4
+ status: draft
5
+ owner: Product
6
+ order: 3
7
+ ---
8
+
9
+ Governance here is a ladder, not a gate. Demanding the top rung of a project on
10
+ the bottom one is a bug, not rigour.
11
+
12
+ ## Where every record starts
13
+
14
+ A document needs a title and a status. That is the whole requirement, and a
15
+ record of such documents is already publishable, searchable and citable.
16
+
17
+ ## The rungs above it
18
+
19
+ Each rung is worth climbing when the domain asks for it, and not before.
20
+
21
+ ### Owners and sources
22
+
23
+ An owner names who stands behind a document. Provenance names where its claims
24
+ came from, one entry per source.
25
+
26
+ ### Effective dates and supersession
27
+
28
+ An effective date says when a document began to apply. A replaced document is
29
+ marked and points at its successor rather than being deleted, so the history
30
+ stays readable.
31
+
32
+ ### A measured floor for abstention
33
+
34
+ The served rung answers questions from the record and declines the ones it does
35
+ not cover. The threshold for declining is measured against this corpus, never
36
+ copied from another one, and it is recorded beside the number with its date.
@@ -0,0 +1,29 @@
1
+ ---
2
+ title: The agent surface
3
+ description: MCP for retrieval with citations, and machine-readable files beside it.
4
+ status: approved
5
+ owner: Product
6
+ order: 2
7
+ effective: 2026-08-22
8
+ provenance:
9
+ - KSoR README, "an agent interface through MCP for search, retrieval, citation, reasoning, and action"
10
+ ---
11
+
12
+ Agents reach the record through MCP — an open standard, so one corpus answers in
13
+ any assistant or framework its owner points at it.
14
+
15
+ ## Retrieval that cites
16
+
17
+ Search and retrieval answer with citations back into the record, so a claim can
18
+ be checked against the document that carries it rather than taken on trust.
19
+
20
+ ### Abstention is a feature
21
+
22
+ "Not in this record" is a correct answer. It is never an error, and never a
23
+ licence to fall back on what a model happens to remember.
24
+
25
+ ## Files beside the interface
26
+
27
+ The build publishes the same knowledge as plain files an agent can fetch without
28
+ a server: `llms.txt` indexes the record, `llms-full.txt` carries every document
29
+ in one file, and each document has a markdown twin at its own address.
@@ -0,0 +1,35 @@
1
+ ---
2
+ title: The human surface
3
+ description: Pages for reading, reviewing and sharing the record.
4
+ status: approved
5
+ owner: Product
6
+ order: 1
7
+ effective: 2026-08-22
8
+ provenance:
9
+ - KSoR README, "a human experience for reading, learning, reviewing, and sharing"
10
+ ---
11
+
12
+ A static site renders every governed document at a route derived from its path,
13
+ with the governance it declares.
14
+
15
+ ## What a page carries
16
+
17
+ Under each title the page shows the facts the document declares about itself —
18
+ who owns it, when it took effect — and lists every source it came from at the
19
+ foot, one entry per source, so a citation can point at exactly one of them.
20
+
21
+ ### A status appears only when it is a caveat
22
+
23
+ A draft or a withdrawn document says so. An approved one shows nothing, because
24
+ a reader already assumes a document in the record is current, and a label that
25
+ never varies stops being read.
26
+
27
+ #### A withdrawn document names its successor
28
+
29
+ Supersession runs both ways: the retired document points at what replaced it,
30
+ and the current one names what it replaced. Nothing is deleted.
31
+
32
+ ## The record without the site
33
+
34
+ Nothing here is authored in the site. Delete the site and the record is intact —
35
+ it is CommonMark in a folder, readable in any editor.
@@ -0,0 +1,21 @@
1
+ ---
2
+ title: Surfaces
3
+ description: One source, published through several synchronized projections.
4
+ status: approved
5
+ owner: Product
6
+ order: 2
7
+ ---
8
+
9
+ The record is written once and published through surfaces that cannot disagree,
10
+ because each derives from the same source rather than from a copy of it.
11
+
12
+ ## Why they cannot drift
13
+
14
+ A surface is generated from the record at build time. Adding one never means
15
+ editing the knowledge, and a document changed in one place changes everywhere
16
+ the next time the record is published.
17
+
18
+ ## The two audiences
19
+
20
+ A person reads pages. An agent reads bytes. Both are served from the same
21
+ governed markdown, which is what makes them answer alike.
@@ -0,0 +1,39 @@
1
+ ---
2
+ title: What a Knowledge System of Record is
3
+ description: The authoritative, governed knowledge layer humans, agents and software operate from.
4
+ status: approved
5
+ owner: Product
6
+ order: 1
7
+ effective: 2026-08-22
8
+ provenance:
9
+ - KSoR README, "What Is a Knowledge System of Record?"
10
+ ---
11
+
12
+ A traditional system of record establishes what is true about the current state
13
+ of a business: the ledger is authoritative for transactions, the HRIS for
14
+ employee records. When a spreadsheet disagrees with the ledger, the ledger wins.
15
+
16
+ A Knowledge System of Record establishes something else — what the organization
17
+ knows and how it should operate. Which policies apply, which thresholds are
18
+ approved, what a term means here, and what to do when the answer is not known.
19
+
20
+ ## The problem it solves
21
+
22
+ That knowledge is usually scattered across wikis, decks, PDFs, prompts and
23
+ someone's memory, with no authoritative answer to the question an agent has to
24
+ ask: which knowledge should I trust?
25
+
26
+ ### Why an assistant cannot answer that question
27
+
28
+ An assistant answers from everything it has ever read, which is exactly why it
29
+ cannot tell you which of its sentences were checked. Businesses have had a
30
+ system of record for decades. AI never did.
31
+
32
+ ## What this record settles, and what it does not
33
+
34
+ It settles which copy governs. Every answer traces to a document here, and that
35
+ document names who stands behind it and when it took effect.
36
+
37
+ It does not settle whether that document is right. Provenance proves who said
38
+ what, and when — the judgement of whether a source is any good is a separate
39
+ matter, and this record never claims otherwise.