@panaversity/ksor 0.0.40 → 0.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +869 -0
  2. package/README.md +11 -7
  3. package/dist/checker/check-main.mjs +14049 -0
  4. package/dist/cli.mjs +11528 -5213
  5. package/dist/gateway-api-CF4ED9_g-BQusM_dK.mjs +10895 -0
  6. package/dist/gateway.d.mts +52 -13
  7. package/dist/gateway.mjs +2 -2
  8. package/dist/index.d.mts +1 -1
  9. package/dist/index.mjs +1 -1
  10. package/dist/{src-pl4aOpVs.mjs → src-dqpI-p1a.mjs} +1 -0
  11. package/docs/authorization.md +8 -6
  12. package/docs/deploying.md +36 -25
  13. package/docs/index.md +26 -13
  14. package/docs/ingesting.md +70 -22
  15. package/docs/tool-surface.md +69 -16
  16. package/package.json +4 -3
  17. package/schema/migrations/2.4-2.5__okf-profile.sql +114 -0
  18. package/schema/schema.sql +77 -14
  19. package/templates/scaffold/.agents/skills/add-sources/SKILL.md +63 -18
  20. package/templates/scaffold/.agents/skills/format-checker/SKILL.md +42 -33
  21. package/templates/scaffold/.agents/skills/format-checker/check.mjs +13827 -1314
  22. package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +65 -27
  23. package/templates/scaffold/.agents/skills/make-slides/SKILL.md +7 -5
  24. package/templates/scaffold/.agents/skills/make-summary/SKILL.md +13 -6
  25. package/templates/scaffold/.claude/skills/add-sources/SKILL.md +63 -18
  26. package/templates/scaffold/.claude/skills/format-checker/SKILL.md +42 -33
  27. package/templates/scaffold/.claude/skills/format-checker/check.mjs +13827 -1314
  28. package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +65 -27
  29. package/templates/scaffold/.claude/skills/make-slides/SKILL.md +7 -5
  30. package/templates/scaffold/.claude/skills/make-summary/SKILL.md +13 -6
  31. package/templates/scaffold/.github/workflows/validate.yml +9 -1
  32. package/templates/scaffold/.ksor/governance.yaml +17 -0
  33. package/templates/scaffold/AGENTS.md +234 -113
  34. package/templates/scaffold/Dockerfile +5 -1
  35. package/templates/scaffold/README.md +160 -42
  36. package/templates/scaffold/env.example +37 -6
  37. package/templates/scaffold/gitignore +13 -8
  38. package/templates/scaffold/instance.md +21 -17
  39. package/templates/scaffold/knowledge/governance-ladder.md +6 -2
  40. package/templates/scaffold/knowledge/index.md +9 -0
  41. package/templates/scaffold/knowledge/surfaces/for-agents.md +7 -6
  42. package/templates/scaffold/knowledge/surfaces/for-people.md +7 -6
  43. package/templates/scaffold/knowledge/surfaces/index.md +4 -20
  44. package/templates/scaffold/knowledge/surfaces/overview.md +25 -0
  45. package/templates/scaffold/knowledge/what-is-a-ksor.md +6 -5
  46. package/templates/scaffold/knowledge/what-is-a-ksor.summary.md +4 -0
  47. package/templates/scaffold/package.json +3 -4
  48. package/templates/scaffold/pnpm-lock.yaml +3 -0
  49. package/templates/scaffold/system/gateways/content.ts +13 -0
  50. package/templates/scaffold/system/site/app/(home)/page.tsx +2 -2
  51. package/templates/scaffold/system/site/app/.well-known/mcp/server.json/route.ts +10 -0
  52. package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +126 -91
  53. package/templates/scaffold/system/site/app/global.css +13 -5
  54. package/templates/scaffold/system/site/app/layout.tsx +8 -3
  55. package/templates/scaffold/system/site/app/llms-full.txt/route.ts +13 -7
  56. package/templates/scaffold/system/site/app/llms.txt/route.ts +12 -7
  57. package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +26 -25
  58. package/templates/scaffold/system/site/components/footer-mark.tsx +3 -2
  59. package/templates/scaffold/system/site/components/governance.tsx +205 -87
  60. package/templates/scaffold/system/site/components/record-index.tsx +5 -5
  61. package/templates/scaffold/system/site/components/record-stack.tsx +10 -9
  62. package/templates/scaffold/system/site/components/sidebar-status.tsx +19 -18
  63. package/templates/scaffold/system/site/lib/attachment-rule.ts +6 -1
  64. package/templates/scaffold/system/site/lib/attachments.ts +0 -28
  65. package/templates/scaffold/system/site/lib/audience-rule.ts +15 -21
  66. package/templates/scaffold/system/site/lib/audience.ts +42 -146
  67. package/templates/scaffold/system/site/lib/embed-rule.ts +9 -0
  68. package/templates/scaffold/system/site/lib/governance.ts +339 -225
  69. package/templates/scaffold/system/site/lib/index-routes.ts +125 -0
  70. package/templates/scaffold/system/site/lib/lifecycle-rule.ts +52 -0
  71. package/templates/scaffold/system/site/lib/lock.ts +282 -0
  72. package/templates/scaffold/system/site/lib/order-rule.ts +37 -0
  73. package/templates/scaffold/system/site/lib/record-href.ts +68 -0
  74. package/templates/scaffold/system/site/lib/record-link.tsx +26 -0
  75. package/templates/scaffold/system/site/lib/rules-version.ts +11 -0
  76. package/templates/scaffold/system/site/lib/shared.ts +67 -104
  77. package/templates/scaffold/system/site/lib/sim-rule.ts +49 -0
  78. package/templates/scaffold/system/site/lib/source.ts +256 -186
  79. package/templates/scaffold/system/site/lib/stage-knowledge.ts +566 -492
  80. package/templates/scaffold/system/site/lib/stage-manifest.ts +128 -0
  81. package/templates/scaffold/system/site/package.json +1 -0
  82. package/templates/scaffold/system/site/record/actor.ts +23 -0
  83. package/templates/scaffold/system/site/record/check.ts +571 -0
  84. package/templates/scaffold/system/site/record/citations.ts +312 -0
  85. package/templates/scaffold/system/site/record/frontmatter.ts +134 -0
  86. package/templates/scaffold/system/site/record/git-ledger.ts +171 -0
  87. package/templates/scaffold/system/site/record/hygiene.ts +320 -0
  88. package/templates/scaffold/system/site/record/index-file.ts +150 -0
  89. package/templates/scaffold/system/site/record/index.ts +103 -0
  90. package/templates/scaffold/system/site/record/instance.ts +257 -0
  91. package/templates/scaffold/system/site/record/instant.ts +43 -0
  92. package/templates/scaffold/system/site/record/ledger.ts +694 -0
  93. package/templates/scaffold/system/site/record/load.ts +129 -0
  94. package/templates/scaffold/system/site/record/lock.ts +306 -0
  95. package/templates/scaffold/system/site/record/near-miss.ts +37 -0
  96. package/templates/scaffold/system/site/record/policy.ts +414 -0
  97. package/templates/scaffold/system/site/record/profile.ts +535 -0
  98. package/templates/scaffold/system/site/record/refusal.ts +106 -0
  99. package/templates/scaffold/system/site/record/yaml-file.ts +103 -0
  100. package/templates/scaffold/system/site/source.config.ts +77 -22
  101. package/dist/gateway-api-CmIthmJS-IUA9qS-T.mjs +0 -3225
  102. package/templates/scaffold/system/site/lib/denial-rule.ts +0 -220
  103. package/templates/scaffold/system/site/lib/page-order.ts +0 -93
@@ -1,220 +0,0 @@
1
- /**
2
- * The denial rule, alone, with no imports and no side effects.
3
- *
4
- * CANONICAL COPY: `packages/content/src/lib/denial-rule.ts`. The scaffold's
5
- * site carries a byte-identical copy at `system/site/lib/denial-rule.ts`, and
6
- * `denial-rule-drift.test.ts` fails if the two ever differ — the same
7
- * arrangement decision 18 made for the audience rule, and for the same reason:
8
- * the site cannot import the kernel, whose package carries pg and the embedding
9
- * providers.
10
- *
11
- * Why it is a leaf: these functions decide whether a withdrawn document gets
12
- * published, and they lived inside a module that reads `instance.md` at import
13
- * time — so none of them could be tested as rules. A round-8 mutation made
14
- * `isDenied` return false unconditionally, which publishes every withdrawn
15
- * document to `/docs` and `llms.txt`, and the entire suite stayed green.
16
- */
17
-
18
- /** The shape `ksor takedown --export` writes. */
19
- export interface DenylistManifest {
20
- format?: number;
21
- corpus_id?: string;
22
- source?: string;
23
- denied?: { stable_id?: string; scope?: string }[];
24
- denied_subtrees?: string[];
25
- }
26
-
27
- /**
28
- * Is this document denied? Exact ids, plus the directories a `--subtree`
29
- * takedown governs.
30
- *
31
- * `ksor takedown --export` expands a `--subtree` denial to its actual
32
- * descendants by walking parent_id, where the tree lives. Interpreting SCOPE
33
- * here meant prefix-matching stable_ids, and a section's stable_id ends in
34
- * `/index` (or `#section`), so the prefix never matched its children and every
35
- * descendant kept publishing — the failure decision 14 records as the reason
36
- * its own walk uses parent_id rather than a prefix.
37
- *
38
- * But an expanded list can only name what the ACTIVE GENERATION contains, and
39
- * the site reads DISK. A document added under a withdrawn section after the
40
- * last ingest is on disk and not in the database, so subtree denials also
41
- * arrive as DIRECTORIES. That is not the rejected prefix match: those paths
42
- * come from `sources.origin_path`, so they are real locations on disk, and a
43
- * document's location cannot be decoupled from itself by a frontmatter
44
- * `sor_id:` the way its id can.
45
- *
46
- * `recordPath` is in the record's own frame (it starts with the record
47
- * directory's name), because that is the frame `origin_path` uses.
48
- */
49
- export function isDenied(
50
- manifest: DenylistManifest,
51
- stableId: string,
52
- recordPath: string,
53
- ): boolean {
54
- if ((manifest.denied ?? []).some((d) => String(d.stable_id) === stableId)) return true;
55
- return (manifest.denied_subtrees ?? []).some((dir) => {
56
- const prefix = String(dir).replace(/\\/g, "/");
57
- if (prefix === "/") return true;
58
- return recordPath.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`);
59
- });
60
- }
61
-
62
- /**
63
- * A plain scalar, read the way the kernel's frontmatter reader reads one.
64
- *
65
- * The two diverged on a TRAILING COMMENT: the kernel strips `# …` from an
66
- * unquoted scalar and the site kept it, so `sor_id: hr/policy # renamed 2026`
67
- * gave the kernel `hr/policy` and the site `hr/policy # renamed 2026`. A
68
- * takedown on the id the MCP door reports as `provenance.stable_id` was then
69
- * denied by the door and silently ignored by the site build, which kept
70
- * publishing the document.
71
- *
72
- * A comment cannot appear inside a QUOTED scalar's value, so quoting is
73
- * resolved first — exactly the kernel's order.
74
- *
75
- * `ok: false` marks a value the kernel's reader REFUSES rather than reads: a
76
- * flow collection, an anchor, a block scalar, or anything with a `: ` in it.
77
- * That matters because refusing one line poisons the whole map — see
78
- * `frontmatterMap`.
79
- */
80
- export function scalarLike(raw: string | undefined): string | undefined {
81
- if (raw === undefined) return undefined;
82
- const parsed = readScalar(raw.trim());
83
- return parsed.kind === "string" ? parsed.value : undefined;
84
- }
85
-
86
- /**
87
- * Plain scalars the kernel's reader converts to a bool, null, int or float —
88
- * never a string, so they can never be an id.
89
- */
90
- const YAML_TYPED =
91
- /^(?:true|True|TRUE|false|False|FALSE|yes|Yes|YES|no|No|NO|on|On|ON|off|Off|OFF|~|null|Null|NULL|[-+]?[0-9][0-9_]*|[-+]?(?:\.[0-9]+|[0-9][0-9_]*\.[0-9_]*)(?:[eE][-+]?[0-9]+)?)$/;
92
-
93
- /**
94
- * Three outcomes, because the kernel's reader has three:
95
- *
96
- * string a plain or quoted string — usable as an id.
97
- * typed a bool, null, int or float. The kernel KEEPS the key with a
98
- * non-string value, and `stableIdOf` requires a string, so the
99
- * override is dropped. The key exists; it just is not an id.
100
- * refused a shape the reader will not read at all. The kernel POISONS the
101
- * whole map on one of these.
102
- *
103
- * Collapsing `typed` into `refused` would empty the map for a document whose
104
- * `order: 3` is perfectly ordinary — which the kernel does not do.
105
- */
106
- interface ScalarRead {
107
- readonly kind: "string" | "typed" | "refused";
108
- readonly value: string;
109
- }
110
-
111
- function readScalar(raw: string): ScalarRead {
112
- // An EMPTY value is `null` to the kernel — the key exists and is not a
113
- // string, exactly like a bool or a number.
114
- if (raw === "") return { kind: "typed", value: "" };
115
- const dq = /^"(.*)"$/.exec(raw);
116
- if (dq !== null)
117
- return { kind: "string", value: (dq[1] ?? "").replace(/\\"/g, '"').replace(/\\\\/g, "\\") };
118
- const sq = /^'(.*)'$/.exec(raw);
119
- if (sq !== null) return { kind: "string", value: (sq[1] ?? "").replace(/''/g, "'") };
120
- const plain = raw.replace(/[ \t]+#.*$/, "").trim();
121
- // The shapes the kernel's reader does not hand back as a STRING. Two groups,
122
- // and both matter for the same reason:
123
- //
124
- // refused a flow collection, an anchor, a block scalar, a plain value
125
- // containing ": " — the kernel returns ok:false and poisons the
126
- // whole map.
127
- // typed a YAML bool, null, int or float — the kernel returns them as
128
- // non-strings, and `stableIdOf` requires a string, so it DROPS
129
- // the override. `sor_id: 4711` therefore resolved to the path on
130
- // the kernel and to "4711" here: a takedown honoured by the door
131
- // and ignored by the site build, the same divergence round 9
132
- // closed for comments and flow lists, in the same function
133
- // (round-10 review of PR 43).
134
- //
135
- // Both are `ok: false` here because both end with the site NOT taking an
136
- // override — which is what the kernel does. Kept in step with `scalarValue`
137
- // in ingest/adapters/plain-tree.ts and bound to it by
138
- // `stable-id-conformance.test.ts`.
139
- // VALID YAML this reader does not model: a flow sequence or mapping, a block
140
- // scalar, an anchor/alias/tag. PyYAML parses every one — the DOCUMENT is fine
141
- // and only this KEY is beyond the reader, so it must not empty the map.
142
- // Checked BEFORE the ": " test, because a flow mapping legitimately contains
143
- // one (`meta: {a: 1}`).
144
- // `typed` rather than `refused`: the key exists but is not a string, so this
145
- // map (which holds strings) omits it and no override is taken — exactly what
146
- // the kernel now does with `value: null` (issue #78).
147
- if (/^[|>&*!{[]/.test(plain)) return { kind: "typed", value: "" };
148
- if (/:[ \t]/.test(plain) || plain.endsWith(":")) return { kind: "refused", value: "" };
149
- if (YAML_TYPED.test(plain)) return { kind: "typed", value: "" };
150
- return { kind: "string", value: plain };
151
- }
152
-
153
- /**
154
- * The frontmatter block as a map, read the way the KERNEL reads it — including
155
- * the part that looks like a bug and is load-bearing: if ANY top-level line is
156
- * a shape the reader refuses, the WHOLE map comes back empty.
157
- *
158
- * That behaviour is inherited from the oracle's PyYAML path, and mirroring it
159
- * is not optional. The site read `sor_id:` with a bare regex, so a document
160
- * carrying an ordinary flow list —
161
- *
162
- * title: Policy
163
- * tags: [hr, payroll]
164
- * sor_id: hr/policy
165
- *
166
- * — got `hr/policy` here and `knowledge/policies/policy` from the kernel, which
167
- * drops the override with the poisoned map. A takedown then matched on exactly
168
- * one surface: denied by the MCP door, ignored by the site build, published to
169
- * /docs and llms.txt. That is the failure decisions 14 and 18 exist to stop,
170
- * re-entered through the denial rule (round-9 review of PR 43).
171
- */
172
- export function frontmatterMap(block: string): Record<string, string> {
173
- const map: Record<string, string> = {};
174
- for (const line of block.split(/\r?\n/)) {
175
- if (line.trim() === "" || line.trimStart().startsWith("#")) continue;
176
- if (/^[ \t]/.test(line)) continue; // nested structure — no top-level scalar
177
- const kv = /^([^\s:]+):(?:[ \t]+(.*))?$/.exec(line);
178
- const key = kv?.[1];
179
- if (key === undefined) return {};
180
- const parsed = readScalar((kv?.[2] ?? "").trim());
181
- if (parsed.kind === "refused") return {};
182
- // A typed value is present in the kernel's map and is not a string; this
183
- // map holds strings, so the key is simply absent — which is what every
184
- // consumer here needs to know about it.
185
- if (parsed.kind === "string") map[key] = parsed.value;
186
- }
187
- return map;
188
- }
189
-
190
- /**
191
- * The file's path in the frame the RECORD uses — `sources.origin_path`, which
192
- * is project-root relative and therefore starts with the record directory's
193
- * own name. `relPath` is the file's path relative to the record directory,
194
- * with forward slashes.
195
- */
196
- export function recordPathFrom(recordName: string, relPath: string): string {
197
- return `${recordName}/${relPath}`;
198
- }
199
-
200
- /**
201
- * The record's stable_id for a file, mirroring the kernel's adapter —
202
- * INCLUDING the `sor_id:` frontmatter override.
203
- *
204
- * Deriving it from the path alone meant a takedown of any document carrying an
205
- * `sor_id:` never matched here and it stayed published, while the MCP door
206
- * denied it: the same decoupling decision 14 records as the reason the subtree
207
- * walk uses parent_id rather than a prefix.
208
- */
209
- export function stableIdFrom(
210
- recordName: string,
211
- relPath: string,
212
- frontmatterBlock: string,
213
- ): string {
214
- // Through the MAP, not a bare regex on the block: the kernel drops the whole
215
- // map when any line is a shape it refuses, and an id the two surfaces read
216
- // differently is a takedown that lands on one of them.
217
- const override = frontmatterMap(frontmatterBlock)["sor_id"];
218
- if (override !== undefined && override !== "") return override;
219
- return `${recordName}/${relPath.replace(/\.md$/i, "")}`;
220
- }
@@ -1,93 +0,0 @@
1
- /**
2
- * Turning the reading-order rule into a sorted page tree — the SITE's half of
3
- * decision 18, and canonical here so it can be tested.
4
- *
5
- * It names no framework type — the page-tree node shape is declared
6
- * structurally below — for two reasons: the file is byte-copied into the
7
- * scaffold's site, where it runs against Fumadocs's `Node`; and a rule that can
8
- * only be exercised inside a Next build is a rule nobody exercises. The kernel
9
- * has no website — it carries this so the SAME ORDER_CASES rows can be run
10
- * against the tree sort a reader actually sees.
11
- *
12
- * Structural, not nominal: any node shaped like this sorts, which is what makes
13
- * one file serve both the real page tree and the table's fixtures.
14
- */
15
-
16
- import { compareSiblings, UNORDERED } from "./order-rule";
17
-
18
- /** The members of a page-tree node that reading order depends on. */
19
- export interface OrderNode {
20
- readonly type: string;
21
- /** A page's route. Absent on folders. */
22
- readonly url?: string;
23
- /** A folder's index document, when it has one. */
24
- readonly index?: { readonly url: string } | undefined;
25
- readonly children?: readonly OrderNode[] | undefined;
26
- }
27
-
28
- function nodeOrder(node: OrderNode, orders: ReadonlyMap<string, number>): number {
29
- if (node.type === "page") return orders.get(node.url ?? "") ?? UNORDERED;
30
- if (node.type === "folder" && node.index) return orders.get(node.index.url) ?? UNORDERED;
31
- return UNORDERED;
32
- }
33
-
34
- // A url that identifies this node: its own, its index's, or its first
35
- // descendant's — a folder with no index document has no url of its own.
36
- function nodeUrl(node: OrderNode): string {
37
- if (node.type === "page") return node.url ?? "";
38
- if (node.type === "folder") {
39
- if (node.index) return node.index.url;
40
- for (const child of node.children ?? []) {
41
- const url = nodeUrl(child);
42
- if (url !== "") return url;
43
- }
44
- }
45
- return "";
46
- }
47
-
48
- // The index of a top-level document's own segment: "/docs/x" splits to
49
- // ["", "docs", "x"], so its segment is at 2 — the baseUrl's segment count.
50
- const BASE_SEGMENTS = "/docs".split("/").length;
51
-
52
- /**
53
- * The tie key: the ONE path segment that distinguishes this node from its
54
- * siblings — never the whole url.
55
- *
56
- * The tree adapter ties on the entry's own name, so a folder with no index
57
- * document must tie on its folder name too. Its url comes from a descendant
58
- * (`/docs/guides/first`), and comparing that whole url against a sibling
59
- * `/docs/guides-x` puts them in the opposite order, because the separator `/`
60
- * (47) sorts after `-` (45). Taking the segment at this depth is exactly the
61
- * adapter's key.
62
- */
63
- function tieAt(node: OrderNode, depth: number): string {
64
- return nodeUrl(node).split("/")[BASE_SEGMENTS + depth] ?? "";
65
- }
66
-
67
- /**
68
- * Children of one parent, in reading order, recursively.
69
- *
70
- * Generic in the node so the caller keeps its own richer type: the site passes
71
- * Fumadocs `Node`s and gets `Node`s back. The one cast is the rebuilt folder —
72
- * spreading a node and replacing `children` produces a value TypeScript can no
73
- * longer prove is the same subtype, though it is: every other member is copied
74
- * verbatim. `depth` starts at 0 for the tree's own children.
75
- */
76
- export function sortNodes<T extends OrderNode>(
77
- nodes: readonly T[],
78
- orders: ReadonlyMap<string, number>,
79
- depth: number,
80
- ): T[] {
81
- return nodes
82
- .map((node) =>
83
- node.type === "folder"
84
- ? ({ ...node, children: sortNodes(node.children ?? [], orders, depth + 1) } as T)
85
- : node,
86
- )
87
- .sort((a, b) =>
88
- compareSiblings(
89
- { order: nodeOrder(a, orders), tie: tieAt(a, depth) },
90
- { order: nodeOrder(b, orders), tie: tieAt(b, depth) },
91
- ),
92
- );
93
- }