@panaversity/ksor 0.0.39 → 0.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +965 -0
  2. package/README.md +29 -9
  3. package/dist/checker/check-main.mjs +14049 -0
  4. package/dist/cli.mjs +11528 -5213
  5. package/dist/gateway-api-CF4ED9_g-BQusM_dK.mjs +10895 -0
  6. package/dist/gateway.d.mts +52 -13
  7. package/dist/gateway.mjs +2 -2
  8. package/dist/index.d.mts +1 -1
  9. package/dist/index.mjs +1 -1
  10. package/dist/{src-pl4aOpVs.mjs → src-dqpI-p1a.mjs} +1 -0
  11. package/docs/authorization.md +8 -6
  12. package/docs/deploying.md +36 -25
  13. package/docs/index.md +26 -13
  14. package/docs/ingesting.md +70 -22
  15. package/docs/tool-surface.md +69 -16
  16. package/package.json +4 -3
  17. package/schema/migrations/2.4-2.5__okf-profile.sql +114 -0
  18. package/schema/schema.sql +77 -14
  19. package/templates/scaffold/.agents/skills/add-sources/SKILL.md +63 -18
  20. package/templates/scaffold/.agents/skills/format-checker/SKILL.md +42 -33
  21. package/templates/scaffold/.agents/skills/format-checker/check.mjs +13827 -1314
  22. package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +65 -27
  23. package/templates/scaffold/.agents/skills/make-slides/SKILL.md +7 -5
  24. package/templates/scaffold/.agents/skills/make-summary/SKILL.md +13 -6
  25. package/templates/scaffold/.claude/skills/add-sources/SKILL.md +63 -18
  26. package/templates/scaffold/.claude/skills/format-checker/SKILL.md +42 -33
  27. package/templates/scaffold/.claude/skills/format-checker/check.mjs +13827 -1314
  28. package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +65 -27
  29. package/templates/scaffold/.claude/skills/make-slides/SKILL.md +7 -5
  30. package/templates/scaffold/.claude/skills/make-summary/SKILL.md +13 -6
  31. package/templates/scaffold/.github/workflows/validate.yml +9 -1
  32. package/templates/scaffold/.ksor/governance.yaml +17 -0
  33. package/templates/scaffold/AGENTS.md +293 -119
  34. package/templates/scaffold/Dockerfile +5 -1
  35. package/templates/scaffold/README.md +160 -42
  36. package/templates/scaffold/env.example +37 -6
  37. package/templates/scaffold/gitignore +14 -6
  38. package/templates/scaffold/instance.md +21 -17
  39. package/templates/scaffold/knowledge/governance-ladder.md +11 -2
  40. package/templates/scaffold/knowledge/index.md +9 -0
  41. package/templates/scaffold/knowledge/surfaces/for-agents.md +13 -6
  42. package/templates/scaffold/knowledge/surfaces/for-people.md +7 -6
  43. package/templates/scaffold/knowledge/surfaces/index.md +4 -20
  44. package/templates/scaffold/knowledge/surfaces/overview.md +25 -0
  45. package/templates/scaffold/knowledge/what-is-a-ksor.md +6 -5
  46. package/templates/scaffold/knowledge/what-is-a-ksor.summary.md +4 -0
  47. package/templates/scaffold/package.json +3 -4
  48. package/templates/scaffold/pnpm-lock.yaml +3 -0
  49. package/templates/scaffold/system/gateways/content.ts +13 -0
  50. package/templates/scaffold/system/site/app/(home)/page.tsx +2 -2
  51. package/templates/scaffold/system/site/app/.well-known/mcp/server.json/route.ts +10 -0
  52. package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +134 -91
  53. package/templates/scaffold/system/site/app/global.css +198 -6
  54. package/templates/scaffold/system/site/app/layout.tsx +8 -3
  55. package/templates/scaffold/system/site/app/llms-full.txt/route.ts +13 -7
  56. package/templates/scaffold/system/site/app/llms.txt/route.ts +12 -7
  57. package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +26 -25
  58. package/templates/scaffold/system/site/components/code-block.tsx +87 -0
  59. package/templates/scaffold/system/site/components/embed.tsx +276 -0
  60. package/templates/scaffold/system/site/components/footer-mark.tsx +3 -2
  61. package/templates/scaffold/system/site/components/governance.tsx +219 -87
  62. package/templates/scaffold/system/site/components/mdx.tsx +10 -2
  63. package/templates/scaffold/system/site/components/record-index.tsx +5 -5
  64. package/templates/scaffold/system/site/components/record-stack.tsx +10 -9
  65. package/templates/scaffold/system/site/components/record-views.tsx +6 -17
  66. package/templates/scaffold/system/site/components/sidebar-status.tsx +19 -18
  67. package/templates/scaffold/system/site/lib/alert-rule.ts +214 -0
  68. package/templates/scaffold/system/site/lib/attachment-rule.ts +6 -1
  69. package/templates/scaffold/system/site/lib/attachments.ts +0 -28
  70. package/templates/scaffold/system/site/lib/audience-rule.ts +15 -21
  71. package/templates/scaffold/system/site/lib/audience.ts +42 -146
  72. package/templates/scaffold/system/site/lib/embed-rule.ts +255 -0
  73. package/templates/scaffold/system/site/lib/governance.ts +339 -225
  74. package/templates/scaffold/system/site/lib/index-routes.ts +125 -0
  75. package/templates/scaffold/system/site/lib/lifecycle-rule.ts +52 -0
  76. package/templates/scaffold/system/site/lib/lock.ts +282 -0
  77. package/templates/scaffold/system/site/lib/order-rule.ts +37 -0
  78. package/templates/scaffold/system/site/lib/record-href.ts +68 -0
  79. package/templates/scaffold/system/site/lib/record-link.tsx +26 -0
  80. package/templates/scaffold/system/site/lib/rules-version.ts +11 -0
  81. package/templates/scaffold/system/site/lib/shared.ts +67 -104
  82. package/templates/scaffold/system/site/lib/sim-rule.ts +49 -0
  83. package/templates/scaffold/system/site/lib/source.ts +256 -186
  84. package/templates/scaffold/system/site/lib/stage-knowledge.ts +638 -467
  85. package/templates/scaffold/system/site/lib/stage-manifest.ts +128 -0
  86. package/templates/scaffold/system/site/package.json +1 -0
  87. package/templates/scaffold/system/site/record/actor.ts +23 -0
  88. package/templates/scaffold/system/site/record/check.ts +571 -0
  89. package/templates/scaffold/system/site/record/citations.ts +312 -0
  90. package/templates/scaffold/system/site/record/frontmatter.ts +134 -0
  91. package/templates/scaffold/system/site/record/git-ledger.ts +171 -0
  92. package/templates/scaffold/system/site/record/hygiene.ts +320 -0
  93. package/templates/scaffold/system/site/record/index-file.ts +150 -0
  94. package/templates/scaffold/system/site/record/index.ts +103 -0
  95. package/templates/scaffold/system/site/record/instance.ts +257 -0
  96. package/templates/scaffold/system/site/record/instant.ts +43 -0
  97. package/templates/scaffold/system/site/record/ledger.ts +694 -0
  98. package/templates/scaffold/system/site/record/load.ts +129 -0
  99. package/templates/scaffold/system/site/record/lock.ts +306 -0
  100. package/templates/scaffold/system/site/record/near-miss.ts +37 -0
  101. package/templates/scaffold/system/site/record/policy.ts +414 -0
  102. package/templates/scaffold/system/site/record/profile.ts +535 -0
  103. package/templates/scaffold/system/site/record/refusal.ts +106 -0
  104. package/templates/scaffold/system/site/record/yaml-file.ts +103 -0
  105. package/templates/scaffold/system/site/source.config.ts +109 -23
  106. package/dist/gateway-api-CmIthmJS-IUA9qS-T.mjs +0 -3225
  107. package/templates/scaffold/system/site/lib/denial-rule.ts +0 -220
  108. package/templates/scaffold/system/site/lib/page-order.ts +0 -93
@@ -0,0 +1,257 @@
1
+ /**
2
+ * The instance document, `instance.md` format 2 (record spec §3): a
3
+ * profile-shaped document BESIDE the bundle, not a concept — identity is not
4
+ * knowledge, so it carries no `type`, `status` or `ksor.audience`, and the
5
+ * lifecycle table does not apply to it. ONE reader: the checker, `ksor build`
6
+ * and the kernel's `parseInstance` all go through this, so the file cannot
7
+ * mean two things (decision 26). The deployment keys (`database`, `embedding`,
8
+ * `retrieval`, `budgets`, `site`, `mcp_url`, `version`) are passed through as
9
+ * parsed YAML; the kernel binds and validates the ones it consumes.
10
+ */
11
+ import { z } from "zod";
12
+
13
+ import { splitFrontmatter } from "./frontmatter";
14
+ import type { Refusal } from "./refusal";
15
+
16
+ /** Keys that left the instance for the policy (audiences) or moved under `toolchain` (ksor). */
17
+ export const MOVED_INSTANCE_KEYS = ["audiences", "default_visibility", "ksor"] as const;
18
+
19
+ /** The closed key set: an unknown top-level key is refused, never ignored (a misspelled `retrieval:` would silently turn the abstention gate off). */
20
+ export const INSTANCE_KEYS = [
21
+ "format",
22
+ "name",
23
+ "title",
24
+ "description",
25
+ "toolchain",
26
+ "database",
27
+ "embedding",
28
+ "retrieval",
29
+ "budgets",
30
+ "site",
31
+ "mcp_url",
32
+ "version",
33
+ ] as const;
34
+
35
+ /**
36
+ * The closed key set INSIDE each group. A group is where a silently-ignored
37
+ * key does the most damage: a misspelled `vector_flor:` leaves the abstention
38
+ * gate off while the owner believes it is on (found live 2026-08-20). The
39
+ * kernel's `groupSchemas` is the authority for these names and
40
+ * `instance-keys-drift.test.ts` holds this map to it.
41
+ */
42
+ export const NESTED_INSTANCE_KEYS: Readonly<Record<string, readonly string[]>> = {
43
+ toolchain: ["requires", "scaffolded"],
44
+ database: ["dsn_env", "tenant_id"],
45
+ embedding: ["provider", "model", "dim"],
46
+ retrieval: ["text_search_config", "vector_floor", "floor_digest", "keyword_floor"],
47
+ budgets: ["maximum_response_characters"],
48
+ // The site's own group. The kernel does not consume it, but ONE reader now
49
+ // validates the instance for every surface (decision 26), and a key nobody
50
+ // reads is a setting the owner believes is in effect — `site.title` was
51
+ // tolerated while `title` moved to the top level, which is exactly that.
52
+ site: ["url", "governance"],
53
+ };
54
+
55
+ const OPEN_GROUPS: readonly string[] = [];
56
+
57
+ /**
58
+ * The map above, read backwards: which block a top-level key would have
59
+ * belonged to.
60
+ *
61
+ * A key of a block written one level out is not an unknown key — it is a known
62
+ * key in the wrong place, and the difference is the whole remedy. `ksor
63
+ * calibrate` printed `vector_floor:` and `floor_digest:` to paste, the paste
64
+ * was refused, and the refusal said to "nest it under the block it belongs to"
65
+ * without ever naming the block. It is `retrieval:`, and this file is where
66
+ * that is written down (first-hour walkthrough, 2026-08-26).
67
+ */
68
+ const GROUP_OF: ReadonlyMap<string, string> = new Map(
69
+ Object.entries(NESTED_INSTANCE_KEYS).flatMap(([group, keys]) =>
70
+ keys.map((key) => [key, group] as const),
71
+ ),
72
+ );
73
+
74
+ /** The value as it would be written back — so the remedy moves the value, not just the key. */
75
+ function asYaml(value: unknown): string {
76
+ if (typeof value === "string") return value;
77
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
78
+ if (value === null || value === undefined) return "null";
79
+ return JSON.stringify(value);
80
+ }
81
+
82
+ const NAME = /^[a-z0-9][a-z0-9-]{0,62}$/;
83
+
84
+ const schema = z
85
+ .object({
86
+ format: z.literal(2),
87
+ name: z
88
+ .string()
89
+ .regex(
90
+ NAME,
91
+ "the name is the corpus identity every citation carries (ascii lowercase, digits, hyphens)",
92
+ ),
93
+ title: z.string().min(1),
94
+ description: z.string().min(1),
95
+ toolchain: z
96
+ .object({ requires: z.string().min(1), scaffolded: z.string().min(1) })
97
+ .loose()
98
+ .optional(),
99
+ database: z.record(z.string(), z.unknown()).optional(),
100
+ embedding: z.record(z.string(), z.unknown()).optional(),
101
+ retrieval: z.record(z.string(), z.unknown()).optional(),
102
+ budgets: z.record(z.string(), z.unknown()).optional(),
103
+ site: z.record(z.string(), z.unknown()).optional(),
104
+ mcp_url: z.string().min(1).optional(),
105
+ version: z.string().min(1).optional(),
106
+ })
107
+ .strict();
108
+
109
+ export interface InstanceDocument {
110
+ readonly name: string;
111
+ readonly title: string;
112
+ readonly description: string;
113
+ readonly toolchain: { readonly requires: string; readonly scaffolded: string } | null;
114
+ readonly database: Readonly<Record<string, unknown>> | null;
115
+ readonly embedding: Readonly<Record<string, unknown>> | null;
116
+ readonly retrieval: Readonly<Record<string, unknown>> | null;
117
+ readonly budgets: Readonly<Record<string, unknown>> | null;
118
+ readonly site: Readonly<Record<string, unknown>> | null;
119
+ readonly mcpUrl: string | null;
120
+ readonly version: string | null;
121
+ /** The MCP server's instructions, in full — the body, edge-trimmed. */
122
+ readonly instructions: string;
123
+ }
124
+
125
+ export type InstanceResult =
126
+ | { readonly ok: true; readonly instance: InstanceDocument }
127
+ | { readonly ok: false; readonly refusals: readonly Refusal[] };
128
+
129
+ const PATH = "instance.md";
130
+ const FIX =
131
+ "run `ksor migrate --write`, which rewrites the instance to format 2 and moves the audience model into .ksor/governance.yaml";
132
+
133
+ export function parseInstanceDocument(text: string, path: string = PATH): InstanceResult {
134
+ const split = splitFrontmatter(text, path);
135
+ if (!split.ok) return { ok: false, refusals: [split.refusal] };
136
+ const fm = split.frontmatter;
137
+ const refuse = (why: string, fix: string = FIX): InstanceResult => ({
138
+ ok: false,
139
+ refusals: [{ slug: "ksor-instance-format", path, why, fix }],
140
+ });
141
+ if (fm === null) {
142
+ return refuse(
143
+ "instance.md has no frontmatter — the frontmatter is the machine half of the instance definition; without it nothing is declared",
144
+ "open the file with --- on line 1 and close the block with ---",
145
+ );
146
+ }
147
+ const moved = MOVED_INSTANCE_KEYS.filter((k) => k in fm);
148
+ if (moved.length > 0) {
149
+ return refuse(
150
+ `\`${moved.join("`, `")}\` no longer live on the instance — audiences and authority live in \`.ksor/governance.yaml\`, and the upgrade stamp under \`toolchain:\``,
151
+ );
152
+ }
153
+ if (fm["format"] !== 2) {
154
+ return refuse(
155
+ `\`format: ${fm["format"] === undefined ? "(missing)" : String(fm["format"])}\` is not the profile's instance (format 2)`,
156
+ );
157
+ }
158
+ const unknown = Object.keys(fm).filter((k) => !(INSTANCE_KEYS as readonly string[]).includes(k));
159
+ if (unknown.length > 0) {
160
+ // A key that belongs to a block gets that block by NAME, with the values it
161
+ // already carries — the remedy is a block to write, not a direction to
162
+ // search in. Grouped by the block they belong to, so two keys of one block
163
+ // are one edit.
164
+ const group = unknown.map((k) => GROUP_OF.get(k)).find((g) => g !== undefined);
165
+ if (group !== undefined) {
166
+ const here = unknown.filter((k) => GROUP_OF.get(k) === group);
167
+ const one = here.length === 1;
168
+ return refuse(
169
+ `instance.md declares an unknown top-level key: ${unknown.join(", ")} — ` +
170
+ `${here.map((k) => `\`${k}\``).join(" and ")} ${one ? "is a key" : "are keys"} of the ` +
171
+ `\`${group}:\` block written at the top level, where nothing reads ${one ? "it" : "them"}. ` +
172
+ "The key set is closed so a key never means two things (a misspelled " +
173
+ "`retrieval:` would otherwise turn the abstention gate off silently)",
174
+ `move ${one ? "it" : "them"} under \`${group}:\`, keeping the value${one ? "" : "s"} — ` +
175
+ `the value is the setting, so it moves rather than goes:\n${group}:\n` +
176
+ here.map((k) => ` ${k}: ${asYaml(fm[k])}`).join("\n"),
177
+ );
178
+ }
179
+ return refuse(
180
+ `instance.md declares an unknown top-level key: ${unknown.join(", ")} — the key set is closed so a key never means two things (a misspelled \`retrieval:\` would otherwise turn the abstention gate off silently)`,
181
+ `fix the spelling or remove it — the top level reads exactly: ${INSTANCE_KEYS.join(", ")}`,
182
+ );
183
+ }
184
+ // Every group is closed too, and a group written inline or as a scalar is
185
+ // not read as a group at all — every setting inside it would be dropped.
186
+ const nested: Refusal[] = [];
187
+ for (const [key, allowed] of [
188
+ ...Object.entries(NESTED_INSTANCE_KEYS),
189
+ ...OPEN_GROUPS.map((k) => [k, null] as const),
190
+ ]) {
191
+ const value = fm[key];
192
+ if (value === undefined) continue;
193
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
194
+ nested.push({
195
+ slug: "ksor-instance-format",
196
+ path,
197
+ why: `\`${key}:\` is not a block mapping — a group written inline or as a scalar is not read as a group, so every setting inside it is dropped`,
198
+ fix: `write it as an indented block:\n ${key}:\n <key>: <value>`,
199
+ });
200
+ continue;
201
+ }
202
+ if (allowed === null) continue;
203
+ for (const sub of Object.keys(value as Record<string, unknown>)) {
204
+ if (!allowed.includes(sub)) {
205
+ nested.push({
206
+ slug: "ksor-instance-format",
207
+ path,
208
+ why: `unknown key under \`${key}\`: \`${sub}\``,
209
+ fix: `remove \`${sub}:\` (allowed under ${key}: ${allowed.join(", ")})`,
210
+ });
211
+ }
212
+ }
213
+ }
214
+ const governance = (fm["site"] as Record<string, unknown> | undefined)?.["governance"];
215
+ if (governance !== undefined && typeof governance !== "boolean") {
216
+ nested.push({
217
+ slug: "ksor-instance-format",
218
+ path,
219
+ why: `\`site.governance\` is ${JSON.stringify(governance)} — it decides whether pages show the governance each document declares, and a value nobody can read is a setting the owner believes is in effect`,
220
+ fix: "write `governance: false` to keep pages plain, or remove the key (the default shows them)",
221
+ });
222
+ }
223
+ if (nested.length > 0) return { ok: false, refusals: nested };
224
+
225
+ const parsed = schema.safeParse(fm);
226
+ if (!parsed.success) {
227
+ return {
228
+ ok: false,
229
+ refusals: parsed.error.issues.map((issue) => ({
230
+ slug: "ksor-instance-format",
231
+ path,
232
+ why: `\`${issue.path.map(String).join(".") || "(root)"}\`: ${issue.message}`,
233
+ fix: "the instance's shape is record spec §3: `format: 2`, `name`, `title`, `description`, optional `toolchain` and the deployment keys",
234
+ })),
235
+ };
236
+ }
237
+ const d = parsed.data;
238
+ return {
239
+ ok: true,
240
+ instance: {
241
+ name: d.name,
242
+ title: d.title,
243
+ description: d.description,
244
+ toolchain: d.toolchain
245
+ ? { requires: d.toolchain.requires, scaffolded: d.toolchain.scaffolded }
246
+ : null,
247
+ database: d.database ?? null,
248
+ embedding: d.embedding ?? null,
249
+ retrieval: d.retrieval ?? null,
250
+ budgets: d.budgets ?? null,
251
+ site: d.site ?? null,
252
+ mcpUrl: d.mcp_url ?? null,
253
+ version: d.version ?? null,
254
+ instructions: split.body.trim(),
255
+ },
256
+ };
257
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Every timestamp in the record is an ISO 8601 instant with an explicit
3
+ * offset (record spec §2.3; upstream OKF made the same move at the pinned
4
+ * commit). A bare date has no instant to compare, and `Date.parse` would
5
+ * silently supply one in the checker's own time zone.
6
+ *
7
+ * The format is half the rule. `Date.parse` does not refuse a day the calendar
8
+ * does not have — it ROLLS it, so `2026-02-30T00:00Z` is accepted as
9
+ * `2026-03-02T00:00:00Z` and `T24:00` as the next day. A governance timestamp
10
+ * decides WHEN a document is in force, so an `effective_from` that means a date
11
+ * nobody wrote is the same silent substitution the offset rule exists to
12
+ * prevent, reached through the value instead of the format (2026-08-25 review).
13
+ */
14
+ const INSTANT =
15
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/;
16
+
17
+ /** Epoch milliseconds, or null when the value is not an instant with an offset. */
18
+ export function parseInstant(value: unknown): number | null {
19
+ if (typeof value !== "string") return null;
20
+ const fields = INSTANT.exec(value);
21
+ if (fields === null) return null;
22
+ const ms = Date.parse(value);
23
+ if (Number.isNaN(ms)) return null;
24
+ return rolled(fields) ? null : ms;
25
+ }
26
+
27
+ /**
28
+ * Did `Date.parse` move any field the author wrote? Asked by re-reading the
29
+ * date and time of day AT UTC — the offset cannot make a date legal or
30
+ * illegal, and reading them as UTC is the one way to render them back without
31
+ * a second calendar implementation of our own.
32
+ *
33
+ * The fraction is deliberately outside the comparison: it can only truncate
34
+ * (`Date.parse` drops precision below a millisecond), never carry into the
35
+ * second, so it changes no field and refusing it would refuse the six-digit
36
+ * form most languages' `isoformat()` emits.
37
+ */
38
+ function rolled(fields: RegExpExecArray): boolean {
39
+ const [, year, month, day, hour, minute, second = "00"] = fields;
40
+ const wrote = `${year}-${month}-${day}T${hour}:${minute}:${second}`;
41
+ const utc = Date.parse(`${wrote}Z`);
42
+ return Number.isNaN(utc) || new Date(utc).toISOString().slice(0, wrote.length) !== wrote;
43
+ }