@panaversity/ksor 0.0.19 → 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 (44) hide show
  1. package/CHANGELOG.md +501 -0
  2. package/dist/cli.mjs +99 -19
  3. package/docs/authorization.md +197 -0
  4. package/docs/index.md +4 -0
  5. package/package.json +1 -1
  6. package/templates/scaffold/.agents/skills/format-checker/check.mjs +232 -9
  7. package/templates/scaffold/.claude/skills/format-checker/check.mjs +232 -9
  8. package/templates/scaffold/AGENTS.md +52 -4
  9. package/templates/scaffold/instance.md +28 -20
  10. package/templates/scaffold/knowledge/governance-ladder.md +36 -0
  11. package/templates/scaffold/knowledge/surfaces/for-agents.md +29 -0
  12. package/templates/scaffold/knowledge/surfaces/for-people.md +35 -0
  13. package/templates/scaffold/knowledge/surfaces/index.md +21 -0
  14. package/templates/scaffold/knowledge/what-is-a-ksor.md +39 -0
  15. package/templates/scaffold/pnpm-lock.yaml +1198 -228
  16. package/templates/scaffold/system/site/app/(home)/layout.tsx +6 -0
  17. package/templates/scaffold/system/site/app/(home)/page.tsx +65 -70
  18. package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +122 -14
  19. package/templates/scaffold/system/site/app/docs/layout.tsx +2 -21
  20. package/templates/scaffold/system/site/app/global.css +552 -9
  21. package/templates/scaffold/system/site/app/layout.tsx +23 -4
  22. package/templates/scaffold/system/site/app/llms-full.txt/route.ts +4 -2
  23. package/templates/scaffold/system/site/app/llms.txt/route.ts +11 -9
  24. package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +51 -0
  25. package/templates/scaffold/system/site/components/copy-markdown.tsx +70 -0
  26. package/templates/scaffold/system/site/components/governance.tsx +262 -0
  27. package/templates/scaffold/system/site/components/home-cover.tsx +137 -0
  28. package/templates/scaffold/system/site/components/record-index.tsx +120 -0
  29. package/templates/scaffold/system/site/components/record-shell.tsx +68 -0
  30. package/templates/scaffold/system/site/components/record-stack.tsx +131 -0
  31. package/templates/scaffold/system/site/components/record-toc.tsx +160 -0
  32. package/templates/scaffold/system/site/components/search-dialog.tsx +130 -0
  33. package/templates/scaffold/system/site/components/sidebar-status.tsx +35 -0
  34. package/templates/scaffold/system/site/components/ui/badge.tsx +46 -0
  35. package/templates/scaffold/system/site/components/ui/button.tsx +62 -0
  36. package/templates/scaffold/system/site/components/ui/separator.tsx +28 -0
  37. package/templates/scaffold/system/site/components.json +25 -0
  38. package/templates/scaffold/system/site/lib/governance.ts +432 -0
  39. package/templates/scaffold/system/site/lib/layout.shared.tsx +1 -1
  40. package/templates/scaffold/system/site/lib/shared.ts +38 -0
  41. package/templates/scaffold/system/site/lib/source.ts +221 -5
  42. package/templates/scaffold/system/site/lib/utils.ts +6 -0
  43. package/templates/scaffold/system/site/package.json +9 -3
  44. 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",