@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
@@ -16,29 +16,23 @@
16
16
  * this file is the shared implementation of the TypeScript half.
17
17
  */
18
18
 
19
- export interface AudienceModel {
20
- /** Least- to most-restricted, `public` first. */
21
- readonly audiences: readonly string[];
22
- /** The tier of a document that declares no `visibility:`. */
23
- readonly defaultVisibility: string;
19
+ /**
20
+ * The overlap rule (record spec §2.4): a concept holds a LIST of audience
21
+ * identifiers, a viewer holds a list that always includes `public`, and the
22
+ * concept is visible when the two overlap. Rank moves to the viewer,
23
+ * membership stays on the document. Omission is a refusal upstream, never a
24
+ * default here — an empty list on either side is visible to nobody.
25
+ */
26
+ export function overlaps(viewer: readonly string[], audience: readonly string[]): boolean {
27
+ return audience.some((a) => viewer.includes(a));
24
28
  }
25
29
 
26
30
  /**
27
- * May a build FOR `audience` publish a document of this `visibility`?
28
- *
29
- * `model === null` is a record that declares no audience model: nothing to
30
- * filter, everything publishes the level-0 shape.
31
+ * The widening rule: a link, a `ksor.superseded_by` pointer or a companion
32
+ * body may reach a target whose audience contains `public` or contains every
33
+ * identifier in the source's then every reader of the source can read the
34
+ * target. `[internal]` `[public]` passes; `[public]` → `[internal]` refuses.
31
35
  */
32
- export function decideVisible(
33
- model: AudienceModel | null,
34
- audience: string,
35
- visibility: string | null,
36
- ): boolean {
37
- if (model === null) return true;
38
- const value = visibility === null || visibility === "" ? model.defaultVisibility : visibility;
39
- const rank = model.audiences.indexOf(value);
40
- // An undeclared visibility is refused, never published: a value no build
41
- // understands is a typo, and a typo reads as a restriction.
42
- if (rank === -1) return false;
43
- return rank <= model.audiences.indexOf(audience);
36
+ export function mayReach(source: readonly string[], target: readonly string[]): boolean {
37
+ return target.includes("public") || source.every((a) => target.includes(a));
44
38
  }
@@ -1,168 +1,64 @@
1
- import { decideVisible, type AudienceModel } from "./audience-rule";
2
- import { instanceFrontmatter } from "./shared";
3
-
4
1
  /**
5
- * The audience model, declared in instance.md the record says who its
6
- * readers are, and the build enforces it:
2
+ * The viewer this build publishes for (record spec §2.4, build spec §3).
3
+ *
4
+ * A concept holds a LIST of audience identifiers; a viewer holds a list that
5
+ * always includes `public`; the concept is visible when the two overlap. The
6
+ * rule itself is `overlaps` in `./audience-rule` — the kernel's copy, asserted
7
+ * byte-identical — and this module only reads the viewer from the environment:
7
8
  *
8
- * audiences:
9
- * - public
10
- * - internal
11
- * - restricted
12
- * default_visibility: public
9
+ * KSOR_AUDIENCE=public,internal pnpm build
13
10
  *
14
- * Ordered least- to most-restricted, so "build the internal site" means
15
- * "public and internal included" with no further configuration. A record
16
- * that declares no audiences has no model and publishes every document
17
- * the behaviour of every instance written before this key existed.
11
+ * Unset means `[public]`, the only default that cannot leak. Whether each
12
+ * identifier is REGISTERED is the staging's question, because the registry
13
+ * lives in the policy (or the lock) and this module reads neither.
18
14
  */
19
- export type { AudienceModel };
20
- export { decideVisible };
21
-
22
- function unquote(raw: string): string {
23
- const trimmed = raw.trim();
24
- return /^(['"])(.*)\1$/.exec(trimmed)?.[2] ?? trimmed;
25
- }
26
15
 
27
- /** Every refusal this feature makes: a slug a pipeline can match, then the remedy. */
16
+ /** Every refusal the site makes: a slug a pipeline can match, then the remedy. */
28
17
  export function refuse(slug: string, what: string, why: string, fix: string): never {
29
18
  // The slug leads, so a pipeline can match on it, and the three lines below
30
19
  // it are the whole remedy — an operator never has to read this file.
31
20
  throw new Error(`${slug}: ${what}\n why: ${why}\n fix: ${fix}`);
32
21
  }
33
22
 
34
- function readAudienceModel(): AudienceModel | null {
35
- const block = instanceFrontmatter();
36
- // Top-level key only: `^` under /m cannot match an indented child.
37
- if (!/^audiences:/m.test(block)) return null;
38
-
39
- // The grammar mirrors the checker's exactly — CRLF-tolerant, list items at
40
- // ANY indent (YAML allows unindented block sequences), and a ` #` comment
41
- // ends an unquoted entry (all three found live 2026-08-18: records the
42
- // checker blessed either failed this build or silently lost a tier).
43
- const stripComment = (value: string): string =>
44
- /^["']/.test(value.trim()) ? value : value.replace(/\s+#.*$/, "");
45
- // A line scanner, not a block regex: a blank line among the items or a
46
- // comment on the key line broke the block capture and refused every build
47
- // of a checker-green record (review finding, 2026-08-19).
48
- const flow = /^audiences:[ \t]*\[(.*)\][ \t]*(?:#.*)?$/m.exec(block)?.[1];
49
- let items: string[] = [];
50
- if (flow !== undefined) {
51
- items = flow.split(",");
52
- } else {
53
- const lines = block.split("\n");
54
- const start = lines.findIndex((line) => /^audiences:[ \t]*(?:#.*)?$/.test(line));
55
- if (start !== -1) {
56
- for (const line of lines.slice(start + 1)) {
57
- if (line.trim() === "") continue;
58
- const item = /^[ \t]*-[ \t]+(.*)$/.exec(line);
59
- if (item === null) break;
60
- items.push(item[1] ?? "");
61
- }
62
- }
63
- }
64
- const audiences = items
65
- .map(stripComment)
66
- .map(unquote)
67
- .filter((value) => value !== "");
68
-
69
- // A declared-but-unreadable model must never read as "no model": that is
70
- // the one parse failure that publishes the whole record.
71
- if (audiences.length === 0) {
72
- refuse(
73
- "ksor-audiences-unreadable",
74
- "instance.md declares `audiences:` but no audience could be read from it",
75
- "an unreadable model reads as no model, and no model publishes every document — the one parse failure that leaks",
76
- "write the audiences as a list, least-restricted first:\n audiences:\n - public\n - internal",
77
- );
78
- }
23
+ let resolved: readonly string[] | null = null;
79
24
 
80
- // The staging never depends on the checker having run: a
81
- // most-restrictive-first model would make plain `pnpm build` publish
82
- // every restricted document with no label (review finding, 2026-08-18).
83
- if (audiences[0] !== "public") {
84
- refuse(
85
- "ksor-audiences-misordered",
86
- `audiences: must start with public (it starts with "${audiences[0]}")`,
87
- "the list is ordered least- to most-restricted, and an unset KSOR_AUDIENCE builds the FIRST entry — any other first entry makes the default build the leak",
88
- "reorder audiences: with public first",
89
- );
90
- }
91
- if (new Set(audiences).size !== audiences.length) {
92
- refuse(
93
- "ksor-audiences-duplicate",
94
- `audiences: declares a tier twice (${audiences.join(", ")})`,
95
- "a duplicated tier has two positions in the ordering, and which one a build honours is undefined",
96
- "remove the duplicate entry",
97
- );
98
- }
99
- const defaultVisibility = unquote(
100
- stripComment(/^default_visibility:[ \t]*(.*)$/m.exec(block)?.[1] ?? ""),
101
- );
102
- if (defaultVisibility === "") {
103
- refuse(
104
- "ksor-default-visibility-missing",
105
- "instance.md declares `audiences:` without `default_visibility:`",
106
- "there is no safe guess: assuming the widest tier leaks on the first document that forgets the key, assuming the narrowest hides the record",
107
- `add the tier a document without a visibility: key belongs to, e.g. default_visibility: ${audiences[0]}`,
108
- );
109
- }
110
- if (!audiences.includes(defaultVisibility)) {
111
- refuse(
112
- "ksor-default-visibility-undeclared",
113
- `default_visibility: ${defaultVisibility} is not one of the declared audiences (${audiences.join(", ")})`,
114
- "every document without a visibility: key belongs to this tier — a tier no build understands is a record no build can publish honestly",
115
- `set default_visibility: to one of ${audiences.join(", ")}, or declare ${defaultVisibility} in audiences:`,
116
- );
117
- }
118
-
119
- return { audiences, defaultVisibility };
25
+ /**
26
+ * The viewer list, `public` always among it. A function rather than a
27
+ * module constant so the refusal is raised where staging can clean up after
28
+ * it: thrown at import time it escaped the stage lock and left the previous,
29
+ * wider stage on disk (found by the staging suite, 2026-08-25).
30
+ */
31
+ export function viewer(): readonly string[] {
32
+ if (resolved === null) resolved = readViewer();
33
+ return resolved;
120
34
  }
121
35
 
122
- /** The declared model, or null when this record declares none. */
123
- export const audienceModel: AudienceModel | null = readAudienceModel();
124
-
125
- function resolveBuildAudience(model: AudienceModel | null): string {
126
- const requested = process.env.KSOR_AUDIENCE?.trim() ?? "";
127
- if (model === null) {
128
- if (requested !== "") {
129
- refuse(
130
- "ksor-audiences-not-declared",
131
- `KSOR_AUDIENCE="${requested}" was requested, but instance.md declares no audiences`,
132
- "this build would publish every document — a build that cannot filter must never look like one that did",
133
- "declare the model in instance.md (audiences: + default_visibility:), or build without KSOR_AUDIENCE",
134
- );
135
- }
136
- return "";
137
- }
138
- // Unset means the least-restricted tier: the only default that cannot leak,
139
- // so `pnpm build` keeps publishing the public site out of the box.
140
- if (requested === "") return model.audiences[0] as string;
141
- if (!model.audiences.includes(requested)) {
36
+ function readViewer(): readonly string[] {
37
+ const raw = process.env.KSOR_AUDIENCE?.trim() ?? "";
38
+ if (raw === "") return ["public"];
39
+ const list = raw
40
+ .split(",")
41
+ .map((item) => item.trim())
42
+ .filter((item) => item !== "");
43
+ if (!list.includes("public")) {
142
44
  refuse(
143
- "ksor-audience-undeclared",
144
- `KSOR_AUDIENCE="${requested}" is not an audience this record declares (${model.audiences.join(", ")})`,
145
- "an unrecognized audience could only be honoured by publishing more than the record names so it refuses instead of widening",
146
- `build with one of ${model.audiences.join(", ")}, or add "${requested}" to instance.md's audiences: list`,
45
+ "ksor-viewer-omits-public",
46
+ `KSOR_AUDIENCE="${raw}" does not include public`,
47
+ "a viewer list always includes public every reader of a restricted build is also a reader of the open one, and a build for a restricted audience alone would silently drop every public concept",
48
+ `build with KSOR_AUDIENCE=public,${list.join(",")}`,
147
49
  );
148
50
  }
149
- return requested;
150
- }
151
-
152
- /** The audience this build publishes for; "" when the record has no model. */
153
- export const buildAudience: string = resolveBuildAudience(audienceModel);
154
-
155
- /** Whether a document of this visibility belongs in THIS build. */
156
- export function visibleInBuild(visibility: string | null): boolean {
157
- return decideVisible(audienceModel, buildAudience, visibility);
51
+ return [...new Set(list)];
158
52
  }
159
53
 
160
54
  /**
161
- * What a non-public build calls itself, in the site chrome — so a leaked
162
- * screenshot of an internal site says which audience it was built for. The
163
- * public build (the least-restricted tier) says nothing new.
55
+ * What a restricted build calls itself, in the site chrome — so a leaked
56
+ * screenshot of an internal site says which audiences it was built for. The
57
+ * public build says nothing new.
164
58
  */
165
- export const audienceNotice: string | null =
166
- audienceModel === null || buildAudience === audienceModel.audiences[0]
59
+ export function audienceNotice(): string | null {
60
+ const list = viewer();
61
+ return list.length === 1
167
62
  ? null
168
- : `${buildAudience} build — not for publication`;
63
+ : `${list.filter((a) => a !== "public").join(", ")} build — not for publication`;
64
+ }
@@ -69,6 +69,15 @@ export function hostOf(value: string): string | null {
69
69
  * per document, named freely, staged only when a published document links to
70
70
  * it. It is deliberately NOT an attachment — an attachment is named after its
71
71
  * parent (`<doc>.quiz.yaml`), and seven sims cannot all be `index.sim.html`.
72
+ *
73
+ * CANONICAL in `packages/content/src/lib/sim-rule.ts`, because the record's
74
+ * checker decides which files are admitted at all and this file only decides
75
+ * which links become a frame — one rule, two readers. The definition is
76
+ * repeated here rather than imported to keep this file a LEAF (above), which
77
+ * is not a preference: a relative import here needs `./sim-rule.js` under the
78
+ * package's node16 typecheck and `./sim-rule` under the site's bundler, and
79
+ * no one spelling satisfies both. So the two copies are PINNED to each other
80
+ * in `embed-rule.test.ts` instead — decision 18's shape, on a smaller rule.
72
81
  */
73
82
  export const SIM_SUFFIX = ".sim.html";
74
83