@panaversity/ksor 0.0.1 → 0.0.3

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 (51) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +24 -6
  3. package/dist/cli.mjs +345 -2
  4. package/docs/index.md +38 -6
  5. package/package.json +3 -1
  6. package/templates/LICENSE +23 -0
  7. package/templates/scaffold/.agents/skills/add-sources/SKILL.md +46 -0
  8. package/templates/scaffold/.agents/skills/format-checker/SKILL.md +46 -0
  9. package/templates/scaffold/.agents/skills/format-checker/check.mjs +991 -0
  10. package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +61 -0
  11. package/templates/scaffold/.claude/skills/add-sources/SKILL.md +46 -0
  12. package/templates/scaffold/.claude/skills/format-checker/SKILL.md +46 -0
  13. package/templates/scaffold/.claude/skills/format-checker/check.mjs +991 -0
  14. package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +61 -0
  15. package/templates/scaffold/.gemini/settings.json +5 -0
  16. package/templates/scaffold/.gitattributes +5 -0
  17. package/templates/scaffold/.github/workflows/validate.yml +23 -0
  18. package/templates/scaffold/AGENTS.md +133 -0
  19. package/templates/scaffold/CLAUDE.md +1 -0
  20. package/templates/scaffold/README.md +90 -0
  21. package/templates/scaffold/gitignore +16 -0
  22. package/templates/scaffold/instance.md +26 -0
  23. package/templates/scaffold/knowledge/example.md +23 -0
  24. package/templates/scaffold/package.json +15 -0
  25. package/templates/scaffold/pnpm-lock.yaml +4041 -0
  26. package/templates/scaffold/pnpm-workspace.yaml +19 -0
  27. package/templates/scaffold/system/site/app/(home)/layout.tsx +6 -0
  28. package/templates/scaffold/system/site/app/(home)/page.tsx +83 -0
  29. package/templates/scaffold/system/site/app/api/search/route.ts +11 -0
  30. package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +53 -0
  31. package/templates/scaffold/system/site/app/docs/layout.tsx +24 -0
  32. package/templates/scaffold/system/site/app/global.css +26 -0
  33. package/templates/scaffold/system/site/app/icon.png +0 -0
  34. package/templates/scaffold/system/site/app/layout.tsx +41 -0
  35. package/templates/scaffold/system/site/app/llms-full.txt/route.ts +10 -0
  36. package/templates/scaffold/system/site/app/llms.txt/route.ts +15 -0
  37. package/templates/scaffold/system/site/components/built-with.tsx +18 -0
  38. package/templates/scaffold/system/site/components/footer-mark.tsx +22 -0
  39. package/templates/scaffold/system/site/components/mdx.tsx +15 -0
  40. package/templates/scaffold/system/site/lib/audience.ts +178 -0
  41. package/templates/scaffold/system/site/lib/layout.shared.tsx +17 -0
  42. package/templates/scaffold/system/site/lib/shared.ts +60 -0
  43. package/templates/scaffold/system/site/lib/source.ts +119 -0
  44. package/templates/scaffold/system/site/lib/stage-knowledge.ts +301 -0
  45. package/templates/scaffold/system/site/next-env.d.ts +6 -0
  46. package/templates/scaffold/system/site/next.config.mjs +32 -0
  47. package/templates/scaffold/system/site/package.json +29 -0
  48. package/templates/scaffold/system/site/postcss.config.mjs +7 -0
  49. package/templates/scaffold/system/site/source.config.ts +41 -0
  50. package/templates/scaffold/system/site/tsconfig.json +35 -0
  51. package/templates/scaffold/vercel.json +8 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 8e88899: The scaffold now answers Vercel's deploy interview: a shipped
8
+ `vercel.json` declares the repo root as the deploy directory (pinning
9
+ `system/site` omits the record — the interview's natural answer breaks
10
+ the build), the static export as the deliverable, and matching trailing
11
+ slashes. The README gains a Deploying section documenting what was
12
+ always true but never written down: the built site is a folder of files
13
+ with zero host-specific dependencies — Vercel, GitHub Pages, nginx, or
14
+ `python3 -m http.server` all serve it, with `KSOR_BASE_PATH` for
15
+ sub-path hosts.
16
+ - 113fddd: The record can now declare its audience. A governed `visibility:` key
17
+ (one value, orthogonal to `status:`) against an `audiences:` model in
18
+ instance.md; per-audience **staged** builds enforce it — a build below a
19
+ document's tier carries no trace of it: no page, no search entry, no
20
+ llms.txt line, no sidebar title, no asset bytes, and nothing about the
21
+ filter itself in the client bundle. Non-public builds name themselves.
22
+ Seven checker rules guard the model, including the cross-audience link
23
+ no single build can catch. Absent `audiences:`, nothing changes —
24
+ purely additive. Evidence and the measured build-time-vs-per-request
25
+ decision: the ksor repository's research/visibility.md and issue #10.
26
+
27
+ ## 0.0.2
28
+
29
+ ### Patch Changes
30
+
31
+ - 54a8f5f: `ksor init` is implemented — the first working verb. One command emits a
32
+ complete governed knowledge project: the record (`knowledge/`), a working
33
+ Fumadocs site (`system/site/`, static export, hot reload, static search,
34
+ llms.txt), the agent kit (AGENTS.md constitution, CLAUDE.md pointer,
35
+ `.agents/skills` with byte-identical `.claude/skills` copies, Gemini
36
+ pointer), adopter CI, and a dependency-free format checker (`pnpm check`).
37
+ Deterministic (every emitted byte ships as template content, lockfile
38
+ included), atomic, offline. Refusals carry stable slugs with working
39
+ remedies; environment failures exit 3 with slugs, never raw stack traces.
40
+
41
+ The scaffold ships branded and self-explaining: the KSoR mark as the
42
+ default favicon, a real landing page led by the instance name with the
43
+ first document derived (never hardcoded), a deletable "Built with KSoR"
44
+ maker's mark, a README that explains every emitted file, and a governed
45
+ `order:` frontmatter key that drives the sidebar, `llms.txt`, and the
46
+ home page from one declaration. The site shell is replaceable behind a
47
+ four-clause surface contract, proven by a second (Docusaurus) shell and
48
+ a shell-agnostic conformance suite in the ksor repository.
49
+
3
50
  ## 0.0.1
4
51
 
5
52
  ### Patch Changes
package/README.md CHANGED
@@ -2,13 +2,31 @@
2
2
 
3
3
  **The Knowledge System of Record for humans and AI agents.**
4
4
 
5
- One governed source of markdown, published as a site people read and an MCP
6
- surface AI agents query with citations, and an honest refusal when the corpus
7
- does not cover the question.
5
+ Knowledge you can govern. Answers you can trace. Boundaries agents can
6
+ respect. One governed source of markdown, published as a site people read
7
+ and an MCP surface AI agents query — with citations, and an honest refusal
8
+ when the corpus does not cover the question.
8
9
 
9
- > **`0.x` status:** this package currently reserves the name and states the
10
- > intent. The CLI answers honestly (`exit 2`, "designed but not implemented")
11
- > rather than pretending. [`docs/status.md`](https://github.com/panaversity/ksor/blob/main/docs/status.md)
10
+ ## Start
11
+
12
+ ```bash
13
+ npx @panaversity/ksor init my-sor
14
+ cd my-sor
15
+ pnpm install
16
+ pnpm dev # the site, live at http://localhost:3000
17
+ ```
18
+
19
+ One command emits a complete governed project: the record (`knowledge/`,
20
+ plain CommonMark), a working documentation site with hot reload, offline
21
+ search and `llms.txt`, adopter CI, a dependency-free format checker
22
+ (`pnpm check`), and the instructions and skills any coding agent needs to
23
+ operate it. Everything emitted is yours (the templates are MIT-0), the
24
+ scaffold is deterministic and offline, and every refusal explains itself.
25
+
26
+ > **`0.x` status:** `init` works; `dev`, `build` and `serve` are designed,
27
+ > not implemented — each prints an honest notice and exits `2` (inside a
28
+ > scaffolded project, `pnpm dev` / `pnpm build` cover local work).
29
+ > [`docs/status.md`](https://github.com/panaversity/ksor/blob/main/docs/status.md)
12
30
  > and the released version number are authoritative for implemented
13
31
  > functionality.
14
32
 
package/dist/cli.mjs CHANGED
@@ -1,6 +1,335 @@
1
1
  #!/usr/bin/env node
2
2
  import { exitCodes, resolveCommand, verbs } from "./index.mjs";
3
- import { readFileSync } from "node:fs";
3
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { spawnSync } from "node:child_process";
6
+ import path from "node:path";
7
+ //#region src/init/errors.ts
8
+ /** The `code` of a Node system error, or null for anything that carries none. */
9
+ function errnoCode(value) {
10
+ if (typeof value !== "object" || value === null) return null;
11
+ const { code } = value;
12
+ return typeof code === "string" ? code : null;
13
+ }
14
+ /**
15
+ * Failures the environment caused and the environment must fix — exit 3. Any
16
+ * other throw is a ksor bug and keeps its stack rather than being dressed up
17
+ * as the operator's fault.
18
+ */
19
+ const ENVIRONMENT_CODES = /* @__PURE__ */ new Set([
20
+ "EACCES",
21
+ "EAGAIN",
22
+ "EBUSY",
23
+ "EDQUOT",
24
+ "EEXIST",
25
+ "EIO",
26
+ "EISDIR",
27
+ "ELOOP",
28
+ "EMFILE",
29
+ "ENAMETOOLONG",
30
+ "ENFILE",
31
+ "ENOENT",
32
+ "ENOSPC",
33
+ "ENOTDIR",
34
+ "ENOTEMPTY",
35
+ "EPERM",
36
+ "EROFS",
37
+ "EXDEV"
38
+ ]);
39
+ function isEnvironmentError(value) {
40
+ const code = errnoCode(value);
41
+ return code !== null && ENVIRONMENT_CODES.has(code);
42
+ }
43
+ //#endregion
44
+ //#region src/init/materialize.ts
45
+ const EMITTED_NAMES = /* @__PURE__ */ new Map([["gitignore", ".gitignore"]]);
46
+ const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
47
+ ".md",
48
+ ".json",
49
+ ".yaml",
50
+ ".yml",
51
+ ".ts",
52
+ ".tsx",
53
+ ".mjs",
54
+ ".js",
55
+ ".css",
56
+ ".txt"
57
+ ]);
58
+ function isTextFile(file) {
59
+ const base = path.basename(file);
60
+ if (base === "gitignore" || base === ".gitattributes") return true;
61
+ return TEXT_EXTENSIONS.has(path.extname(file));
62
+ }
63
+ /**
64
+ * Copy the template tree into targetDir, applying the two stamps to text
65
+ * files. Byte-determinism holds because templates are shipped bytes and the
66
+ * stamps are pure string substitution.
67
+ *
68
+ * Every path it brings into existence is appended to `created`, parents before
69
+ * children, so a caller that cannot rename-over (the `init .` form) can undo
70
+ * a half-written tree in reverse order.
71
+ */
72
+ function materialize(templateDir, targetDir, stamps, created = []) {
73
+ for (const entry of readdirSync(templateDir, { withFileTypes: true })) {
74
+ if (entry.name === "node_modules") continue;
75
+ const from = path.join(templateDir, entry.name);
76
+ const to = path.join(targetDir, EMITTED_NAMES.get(entry.name) ?? entry.name);
77
+ if (entry.isDirectory()) {
78
+ if (!existsSync(to)) {
79
+ mkdirSync(to, { recursive: true });
80
+ created.push(to);
81
+ }
82
+ materialize(from, to, stamps, created);
83
+ } else if (isTextFile(from)) {
84
+ const text = readFileSync(from, "utf8").replaceAll("KSOR-STAMP-NAME", stamps.name).replaceAll("KSOR-STAMP-VERSION", stamps.version);
85
+ created.push(to);
86
+ writeFileSync(to, text);
87
+ } else {
88
+ created.push(to);
89
+ copyFileSync(from, to);
90
+ }
91
+ }
92
+ return created;
93
+ }
94
+ //#endregion
95
+ //#region src/init/name.ts
96
+ /** Project-name grammar (spec: `^[a-z0-9][a-z0-9-]{0,62}$`). */
97
+ const NAME_GRAMMAR = /^[a-z0-9][a-z0-9-]{0,62}$/;
98
+ /**
99
+ * Windows refuses these as directory names at the filesystem layer, whatever
100
+ * the grammar says — and the spec runs its acceptance on windows-latest, so a
101
+ * name accepted here must be a directory everywhere.
102
+ */
103
+ const WINDOWS_RESERVED = /* @__PURE__ */ new Set([
104
+ "con",
105
+ "prn",
106
+ "aux",
107
+ "nul",
108
+ ...Array.from({ length: 9 }, (_, index) => `com${index + 1}`),
109
+ ...Array.from({ length: 9 }, (_, index) => `lpt${index + 1}`)
110
+ ]);
111
+ function nameProblem(name) {
112
+ if (!NAME_GRAMMAR.test(name)) return "grammar";
113
+ if (WINDOWS_RESERVED.has(name)) return "windows-reserved";
114
+ return null;
115
+ }
116
+ function isValidName(name) {
117
+ return nameProblem(name) === null;
118
+ }
119
+ /** A best-effort valid slug from an invalid name, for the remedy line. */
120
+ function suggestName(input) {
121
+ const slug = input.toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/-+/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 63);
122
+ const usable = WINDOWS_RESERVED.has(slug) ? `${slug}-sor` : slug;
123
+ return isValidName(usable) ? usable : null;
124
+ }
125
+ //#endregion
126
+ //#region src/init/walk.ts
127
+ /** Nearest ancestor (inclusive) containing instance.md, or null. */
128
+ function findAncestorProject(startDir) {
129
+ let dir = path.resolve(startDir);
130
+ for (;;) {
131
+ if (existsSync(path.join(dir, "instance.md"))) return dir;
132
+ const parent = path.dirname(dir);
133
+ if (parent === dir) return null;
134
+ dir = parent;
135
+ }
136
+ }
137
+ /**
138
+ * Nearest ancestor (exclusive of startDir) that is a pnpm workspace root
139
+ * whose globs could swallow a nested project. Presence of the file is enough
140
+ * to warn — glob analysis would promise precision the warning doesn't need.
141
+ */
142
+ function findAncestorWorkspace(startDir) {
143
+ let dir = path.dirname(path.resolve(startDir));
144
+ for (;;) {
145
+ const manifest = path.join(dir, "pnpm-workspace.yaml");
146
+ if (existsSync(manifest)) return dir;
147
+ const pkg = path.join(dir, "package.json");
148
+ if (existsSync(pkg)) try {
149
+ if (JSON.parse(readFileSync(pkg, "utf8")).workspaces !== void 0) return dir;
150
+ } catch {}
151
+ const parent = path.dirname(dir);
152
+ if (parent === dir) return null;
153
+ dir = parent;
154
+ }
155
+ }
156
+ //#endregion
157
+ //#region src/init/index.ts
158
+ const STAGE_PREFIX = ".ksor-init-";
159
+ const GRAMMAR = "^[a-z0-9][a-z0-9-]{0,62}$";
160
+ function fail(io, slug, lines, code) {
161
+ io.err(`error: ${slug}\n${lines.join("\n")}\n`);
162
+ return code;
163
+ }
164
+ function refuse(io, slug, lines) {
165
+ return fail(io, slug, lines, exitCodes.refused);
166
+ }
167
+ function refuseExists(io, word) {
168
+ return refuse(io, "exists", [`${word}/ already exists here.`, `pick another name, or remove ${word}/ first if it is disposable.`]);
169
+ }
170
+ function usage$1(io) {
171
+ io.out(`ksor init <name> create a new Knowledge System of Record in ./<name>
172
+ ksor init . scaffold into the current directory (must be empty)
173
+
174
+ Nothing was scaffolded: bare \`ksor init\` never writes — an unattended
175
+ agent must not scaffold into an unknown directory by accident.
176
+ The name must match ${GRAMMAR} (e.g. accounting-sor).\n`);
177
+ return 0;
178
+ }
179
+ /**
180
+ * Spec: stale stage dirs are reported, never deleted — they may hold work.
181
+ *
182
+ * Reported only once this run has written its own tree: a note ahead of a
183
+ * refusal would take the first stderr line, which belongs to the slug (found
184
+ * live: 25 concurrent init pairs, where the loser's refusal was pushed to line
185
+ * two by a note about the winner's live stage — 2026-08-18).
186
+ */
187
+ function noteStaleStages(dir, io) {
188
+ for (const entry of readdirSync(dir).sort()) if (entry.startsWith(STAGE_PREFIX)) io.err(`note: found ${entry} — left by an interrupted init; inspect and remove it\n`);
189
+ }
190
+ /** Undo a half-written tree, children before parents. */
191
+ function rollback(created) {
192
+ for (const target of [...created].reverse()) try {
193
+ rmSync(target, {
194
+ recursive: true,
195
+ force: true
196
+ });
197
+ } catch {}
198
+ }
199
+ /** found live: git may be absent on minimal CI images — warn, never fail. */
200
+ function gitInit(dir, io) {
201
+ if (spawnSync("git", ["rev-parse", "--git-dir"], {
202
+ cwd: dir,
203
+ stdio: "ignore"
204
+ }).status === 0) return;
205
+ const result = spawnSync("git", ["init", "--quiet"], {
206
+ cwd: dir,
207
+ stdio: [
208
+ "ignore",
209
+ "ignore",
210
+ "pipe"
211
+ ],
212
+ encoding: "utf8"
213
+ });
214
+ if (result.error !== void 0) {
215
+ io.err(errnoCode(result.error) === "ENOENT" ? "note: git was not found — initialize the repository yourself when convenient\n" : `note: git init failed: ${result.error.message}\n`);
216
+ return;
217
+ }
218
+ if (result.status !== 0) {
219
+ const detail = (result.stderr ?? "").trim().split("\n")[0] || `git exited ${result.status}`;
220
+ io.err(`note: git init failed: ${detail}\n`);
221
+ }
222
+ }
223
+ function handoff(io, name, targetWasDot) {
224
+ const enter = targetWasDot ? "" : ` cd ${name}\n`;
225
+ io.out(`${name} is ready — your knowledge, your repo, yours outright.\n
226
+ Next (or just tell your coding agent to take it from here):
227
+ ` + enter + " pnpm install\n pnpm dev # the site, live at http://localhost:3000\n\nno pnpm? run: npm install -g pnpm — or `corepack enable pnpm` on Nodes that bundle corepack\n\nStart in knowledge/ — AGENTS.md carries the working rules.\n");
228
+ }
229
+ function init(args, cwd, io, env) {
230
+ const { version, templatesDir } = env;
231
+ if (!existsSync(templatesDir)) return fail(io, "broken-install", [`the ksor package is missing its templates: ${templatesDir}`, "reinstall it — `pnpm add -D @panaversity/ksor`, or `npm i -g @panaversity/ksor`."], exitCodes.environment);
232
+ const word = args[0] ?? null;
233
+ if (word === null) return usage$1(io);
234
+ if (args.length > 1) {
235
+ const joined = suggestName(args.join("-"));
236
+ return refuse(io, "bad-name", [`a project name is one word — ${args.length} were given: ${args.join(" ")}`, joined !== null ? `try: ksor init ${joined}` : `pick a short name matching ${GRAMMAR}: ksor init <name>.`]);
237
+ }
238
+ const isDot = word === ".";
239
+ const targetDir = isDot ? path.resolve(cwd) : path.resolve(cwd, word);
240
+ const name = isDot ? path.basename(targetDir) : word;
241
+ const problem = nameProblem(name);
242
+ if (problem !== null) {
243
+ const reason = problem === "windows-reserved" ? "Windows reserves it as a device name, so no directory can carry it there" : `it must match ${GRAMMAR} — lowercase letters, digits, hyphens`;
244
+ const suggestion = suggestName(name);
245
+ return refuse(io, "bad-name", isDot ? [`\`ksor init .\` takes the project name from this directory, and "${name}" cannot be one: ${reason}.`, suggestion !== null ? `run \`ksor init ${suggestion}\` from the parent directory, or rename this directory first.` : "rename this directory to lowercase letters, digits and hyphens, then re-run."] : [`"${name}" is not a usable project name: ${reason}.`, suggestion !== null ? `try: ksor init ${suggestion}` : "pick a short lowercase name."]);
246
+ }
247
+ const ancestorProject = findAncestorProject(isDot ? path.dirname(targetDir) : cwd);
248
+ if (ancestorProject !== null) return refuse(io, "nested", [`an existing ksor project owns this path: ${ancestorProject}`, "a corpus lives inside exactly one project — create the new one outside it."]);
249
+ if (isDot) {
250
+ const contents = existsSync(targetDir) ? readdirSync(targetDir).filter((e) => e !== ".git").sort() : [];
251
+ if (contents.length > 0) {
252
+ const listed = contents.slice(0, 5).join(", ") + (contents.length > 5 ? ", …" : "");
253
+ return refuse(io, "blocked", [`the current directory is not empty (${contents.length} entr${contents.length === 1 ? "y" : "ies"}: ${listed}).`, "run `ksor init .` in an empty directory, or `ksor init <name>` to create one."]);
254
+ }
255
+ } else if (existsSync(targetDir)) return refuseExists(io, word);
256
+ const ancestorWorkspace = findAncestorWorkspace(targetDir);
257
+ if (isDot) {
258
+ const created = [];
259
+ try {
260
+ materialize(templatesDir, targetDir, {
261
+ name,
262
+ version
263
+ }, created);
264
+ } catch (error) {
265
+ rollback(created);
266
+ throw error;
267
+ }
268
+ } else {
269
+ const stage = mkdtempSync(path.join(path.dirname(targetDir), STAGE_PREFIX));
270
+ try {
271
+ materialize(templatesDir, stage, {
272
+ name,
273
+ version
274
+ });
275
+ } catch (error) {
276
+ rmSync(stage, {
277
+ recursive: true,
278
+ force: true
279
+ });
280
+ throw error;
281
+ }
282
+ try {
283
+ renameSync(stage, targetDir);
284
+ } catch (error) {
285
+ rmSync(stage, {
286
+ recursive: true,
287
+ force: true
288
+ });
289
+ const code = errnoCode(error);
290
+ if (code === "ENOTEMPTY" || code === "EEXIST" || code === "EPERM") return refuseExists(io, word);
291
+ throw error;
292
+ }
293
+ }
294
+ try {
295
+ if (ancestorWorkspace !== null) io.err(`warning: parent pnpm workspace at ${ancestorWorkspace} — its globs may enroll this\nproject's packages into the parent install. Exclude it there if builds misbehave.
296
+ `);
297
+ if (!isDot) {
298
+ chmodSync(targetDir, statSync(path.join(targetDir, "knowledge")).mode & 511);
299
+ noteStaleStages(path.dirname(targetDir), io);
300
+ }
301
+ gitInit(targetDir, io);
302
+ } catch (error) {
303
+ if (!isEnvironmentError(error)) throw error;
304
+ const detail = error instanceof Error ? error.message : String(error);
305
+ io.err(`note: the project was created, but a follow-up step failed: ${detail}\n`);
306
+ }
307
+ handoff(io, name, isDot);
308
+ return 0;
309
+ }
310
+ function runInit(args, cwd, io, env) {
311
+ try {
312
+ return init(args, cwd, io, env);
313
+ } catch (error) {
314
+ if (!isEnvironmentError(error)) throw error;
315
+ return fail(io, "environment", [`the filesystem refused: ${error instanceof Error ? error.message : String(error)}`, "fix the environment and re-run — nothing was kept."], exitCodes.environment);
316
+ }
317
+ }
318
+ //#endregion
319
+ //#region src/init/platform.ts
320
+ /** The scaffold's toolchain requires it (decision 5, and `engines` here). */
321
+ const MINIMUM_NODE_MAJOR = 24;
322
+ /**
323
+ * The remedy for a runtime ksor cannot run on, or null when it can. Pure so
324
+ * the refusal is testable without installing a second Node.
325
+ */
326
+ function unsupportedPlatform(nodeVersion) {
327
+ const version = nodeVersion.replace(/^v/, "");
328
+ const major = Number.parseInt(version, 10);
329
+ if (Number.isNaN(major) || major >= MINIMUM_NODE_MAJOR) return null;
330
+ return `ksor requires Node >= ${MINIMUM_NODE_MAJOR} — you are on v${version}; install a current Node and re-run.`;
331
+ }
332
+ //#endregion
4
333
  //#region src/cli.ts
5
334
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
6
335
  const notice = `
@@ -12,7 +341,7 @@ Follow along: ${pkg.homepage}\n`;
12
341
  const usage = `ksor ${pkg.version} — Knowledge System of Record\n
13
342
  Usage: ksor <verb>
14
343
 
15
- Verbs (designed; none implemented in ${pkg.version} each exits 2 until it ships):\n init create a new KSoR project
344
+ Verbs (init is implemented; the rest exit 2 until they ship):\n init create a new KSoR project (implemented)
16
345
  dev run the human surface locally, watching
17
346
  build validate and build both surfaces
18
347
  serve expose the MCP agent surface
@@ -29,6 +358,20 @@ function main(args) {
29
358
  return 0;
30
359
  }
31
360
  const { word, verb } = resolveCommand(args);
361
+ if (verb === "init") {
362
+ const remedy = unsupportedPlatform(process.versions.node);
363
+ if (remedy !== null) {
364
+ process.stderr.write(`error: unsupported-platform\n${remedy}\n`);
365
+ return exitCodes.environment;
366
+ }
367
+ return runInit(args.slice(args.indexOf("init") + 1), process.cwd(), {
368
+ out: (text) => process.stdout.write(text),
369
+ err: (text) => process.stderr.write(text)
370
+ }, {
371
+ version: pkg.version,
372
+ templatesDir: fileURLToPath(new URL("../templates/scaffold", import.meta.url))
373
+ });
374
+ }
32
375
  if (word !== null && verb === null) {
33
376
  process.stderr.write(`error: unknown-verb\n"${word}" is not a ksor verb. The vocabulary is: ${verbs.join(", ")}.\n`);
34
377
  return exitCodes.refused;
package/docs/index.md CHANGED
@@ -7,15 +7,47 @@ status: draft
7
7
 
8
8
  These docs ship inside the npm package (`node_modules/@panaversity/ksor/docs/`)
9
9
  so that coding agents read documentation matching the **installed** version
10
- instead of their training memory. That mechanism is live from 0.x on, even
11
- though the docs are still small — the corpus grows with each implemented verb.
10
+ instead of their training memory. The corpus grows with each implemented verb.
12
11
 
13
12
  ## What exists in this build
14
13
 
15
- - The `ksor` CLI answers honestly: every designed verb (`init`, `dev`, `build`,
16
- `serve`) reports "designed but not implemented" and exits `2`.
17
- - The package root exports the CLI contract: `exitCodes` (1 refused,
18
- 2 not implemented, 3 environment), `verbs`, and `resolveCommand`.
14
+ - **`ksor init <name>` works.** One command emits a complete governed
15
+ project: the record (`knowledge/`, CommonMark only), a working
16
+ documentation site (`system/site/`, Next.js + Fumadocs static export,
17
+ hot reload, static search, `llms.txt`), the agent kit (`AGENTS.md`,
18
+ a `CLAUDE.md` pointer, skills under `.agents/skills/` with byte-identical
19
+ `.claude/skills/` copies), adopter CI, and a dependency-free format
20
+ checker (`pnpm check`). `ksor init .` scaffolds into an empty directory
21
+ whose name passes the project-name grammar. Everything emitted belongs to
22
+ the adopter (templates are MIT-0).
23
+ - Inside a scaffolded project, `pnpm install && pnpm dev` serves the record
24
+ at `http://localhost:3000`; `pnpm build` writes a fully static export to
25
+ `system/site/out/`. `KSOR_BASE_PATH=/repo pnpm build` targets sub-path
26
+ hosting.
27
+ - The remaining verbs (`dev`, `build`, `serve`) are designed, not
28
+ implemented: each prints an honest notice and exits `2`.
29
+ - Exit codes are a contract: `1` refused (first stderr line is a stable
30
+ slug such as `error: bad-name`, followed by a remedy), `2` designed but
31
+ not implemented, `3` the environment cannot run ksor
32
+ (`error: unsupported-platform`, `error: broken-install`,
33
+ `error: environment`).
34
+ - The package root exports the CLI contract: `exitCodes`, `verbs`, and
35
+ `resolveCommand`.
36
+
37
+ ## For the agent operating a scaffolded project
38
+
39
+ Read the scaffold's own `AGENTS.md` first — it is the working contract.
40
+ Knowledge lives in `knowledge/` and never inside the site; frontmatter uses
41
+ a closed key set (`title` + `status` required); `pnpm check` explains any
42
+ violation and how to fix it. Sidebar order is the governed `order:`
43
+ frontmatter key — never `meta.json` or `sidebar_position`. If the
44
+ instance declares an `audiences:` model, documents may carry a
45
+ `visibility:` key and per-audience builds (`KSOR_AUDIENCE=<tier> pnpm
46
+ build`) stage only what that tier may see — publication, not authorship:
47
+ anyone who can clone reads everything. The site shell
48
+ at `system/site/` is replaceable behind a five-clause surface contract; a
49
+ Docusaurus conformance shell lives in the ksor repository under
50
+ `workbench/shells/docusaurus/` with its swap recipe.
19
51
 
20
52
  ## Where truth lives
21
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panaversity/ksor",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Knowledge System of Record — the authoritative, governed source of knowledge that humans and AI agents operate from. Name reserved; implementation in progress.",
5
5
  "keywords": [
6
6
  "abstention",
@@ -31,6 +31,7 @@
31
31
  "files": [
32
32
  "dist",
33
33
  "docs",
34
+ "templates",
34
35
  "CHANGELOG.md",
35
36
  "NOTICE"
36
37
  ],
@@ -50,6 +51,7 @@
50
51
  },
51
52
  "devDependencies": {
52
53
  "@types/node": "^24.13.3",
54
+ "playwright": "^1.58.0",
53
55
  "publint": "0.3.23",
54
56
  "tsdown": "0.22.14",
55
57
  "typescript": "7.0.2",
@@ -0,0 +1,23 @@
1
+ MIT No Attribution (MIT-0)
2
+
3
+ Copyright 2026 Panaversity
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
18
+ SOFTWARE.
19
+
20
+ (This licence covers the scaffold templates in this directory — the files
21
+ `ksor init` copies into an adopter's repository. Everything init emits is the
22
+ adopter's, with no attribution or licence-propagation obligations. See
23
+ AGENTS.md decision 10.)
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: add-sources
3
+ description: Turn source material — documents, pages, pasted text, notes — into governed knowledge in knowledge/. Use when the owner shares material to add, says "add this to the knowledge base", or asks how to get existing content in. Not for editing the site.
4
+ metadata:
5
+ version: "1.2.0"
6
+ ---
7
+
8
+ # Add sources
9
+
10
+ Converting material into the record is the everyday work of this project.
11
+ The rules that make it _governed_ rather than merely stored:
12
+
13
+ ## Placement and shape
14
+
15
+ - One document per topic, under `knowledge/`, path = identity: lowercase,
16
+ hyphens, a folder per natural grouping. Plain CommonMark `.md` — if the
17
+ source is rich (tables, images), tables become markdown tables and images
18
+ land _beside the document_ with relative links.
19
+ - A folder's front page is `<folder>/index.md`; reading order is the
20
+ `order:` frontmatter key (ordered documents first, ascending; the rest
21
+ follow alphabetically) — never `meta.json` or `sidebar_position`.
22
+ - Frontmatter: `title` and `status: draft` always; add `owner` (who stands
23
+ behind this content) and `provenance` (a list naming the actual sources —
24
+ file names, systems, people, dates) whenever the owner can tell you.
25
+ Precision matters: "Finance policy manual §4.2, 2025 edition" governs;
26
+ "internal docs" does not.
27
+ - When `instance.md` declares `audiences:`, ask the owner which audience the
28
+ new material belongs to and write it as `visibility:` — never guess that
29
+ restricted material is public.
30
+
31
+ ## Fidelity rules
32
+
33
+ - **Copy load-bearing values exactly** — numbers, thresholds, dates, names.
34
+ Never round, never paraphrase a figure.
35
+ - **Two disagreeing sources stay two statements**, each with its provenance
36
+ — never smooth a conflict into one invented truth; flag it to the owner.
37
+ - **Do not fill gaps from general knowledge.** If the source doesn't cover
38
+ something, the record doesn't either — that boundary is the product.
39
+ - A document replacing an older one: mark the old one `status: superseded`
40
+ with `superseded_by:` pointing at the new — never delete it.
41
+
42
+ ## Finish every batch
43
+
44
+ Run `pnpm check` and fix what it reports (its errors explain themselves),
45
+ then show the owner the rendered result (`pnpm dev`) — the site is the
46
+ review surface: you write, they check.
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: format-checker
3
+ description: The record's format rules as a runnable check — frontmatter, filenames, links, structure. Use before handing off any change to knowledge/, when a check fails and you need to fix it, or when unsure whether a document is well-formed. Run with `pnpm check` (or node .agents/skills/format-checker/check.mjs).
4
+ metadata:
5
+ version: "1.2.0"
6
+ ---
7
+
8
+ # Format checker
9
+
10
+ `pnpm check` runs `check.mjs` — dependency-free Node, owned by this repo. It
11
+ enforces what AGENTS.md states in prose:
12
+
13
+ - `knowledge/` holds CommonMark `.md` and images only — no `.mdx`, no
14
+ `meta.json`, no other file types — and it is never empty; a record with no
15
+ documents stands behind nothing.
16
+ - Every document carries `title` + `status` (level-0 requirement); the full
17
+ allowed key set is closed; a `superseded` document names a `superseded_by`
18
+ that resolves to a document that exists.
19
+ - Filenames are portable identities: lowercase, Windows-safe, no spaces, no
20
+ case-collisions, no `foo.md` + `foo/index.md` pairs, no parentheses.
21
+ (`.DS_Store` and friends are skipped, never reported.)
22
+ - Relative links resolve and never leave `knowledge/` — inline,
23
+ `<angle-bracketed>`, and reference-style (`[text][label]` with its
24
+ `[label]: target` definition) alike. Links inside code spans and fenced
25
+ blocks are code, not links, and are ignored.
26
+ - `instance.md` exists, is `format: 1`, and carries only the keys the format
27
+ defines — an unknown key is named, never ignored.
28
+ - The audience model, when there is one: `audiences:` is ordered least- to
29
+ most-restricted with `public` first, no duplicates, and never without
30
+ `default_visibility:`; a document's `visibility:` names one of the declared
31
+ audiences; and no link or `superseded_by:` points from a wider audience at a
32
+ narrower one — the leak no single build can catch, since the build that
33
+ publishes the pointer has already dropped its target. A record that declares
34
+ no `audiences:` is checked exactly as it was before the key existed.
35
+ - `CLAUDE.md` stays a one-line pointer; `.agents/skills/` and
36
+ `.claude/skills/` hold the same files byte for byte **in both directions**
37
+ (a file only one tree carries is a rule nobody reviewed); the site contains
38
+ no content files.
39
+
40
+ Every failure prints what is wrong, why the rule exists, and the fix — obey
41
+ the printed fix literally; if it doesn't resolve the problem, that is a bug
42
+ worth reporting to ksor.
43
+
44
+ When you edit any skill under `.agents/skills/`, re-copy it to
45
+ `.claude/skills/` — the checker holds the two trees identical, and it now
46
+ notices a file added on either side.