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