@panaversity/ksor 0.0.1 → 0.0.2

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 (47) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +24 -6
  3. package/dist/cli.mjs +344 -2
  4. package/docs/index.md +34 -6
  5. package/package.json +3 -1
  6. package/templates/LICENSE +23 -0
  7. package/templates/scaffold/.agents/skills/add-sources/SKILL.md +43 -0
  8. package/templates/scaffold/.agents/skills/format-checker/SKILL.md +39 -0
  9. package/templates/scaffold/.agents/skills/format-checker/check.mjs +782 -0
  10. package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +46 -0
  11. package/templates/scaffold/.claude/skills/add-sources/SKILL.md +43 -0
  12. package/templates/scaffold/.claude/skills/format-checker/SKILL.md +39 -0
  13. package/templates/scaffold/.claude/skills/format-checker/check.mjs +782 -0
  14. package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +46 -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 +104 -0
  19. package/templates/scaffold/CLAUDE.md +1 -0
  20. package/templates/scaffold/README.md +63 -0
  21. package/templates/scaffold/gitignore +13 -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/mdx.tsx +15 -0
  39. package/templates/scaffold/system/site/lib/layout.shared.tsx +17 -0
  40. package/templates/scaffold/system/site/lib/shared.ts +51 -0
  41. package/templates/scaffold/system/site/lib/source.ts +119 -0
  42. package/templates/scaffold/system/site/next-env.d.ts +6 -0
  43. package/templates/scaffold/system/site/next.config.mjs +32 -0
  44. package/templates/scaffold/system/site/package.json +29 -0
  45. package/templates/scaffold/system/site/postcss.config.mjs +7 -0
  46. package/templates/scaffold/system/site/source.config.ts +35 -0
  47. package/templates/scaffold/system/site/tsconfig.json +35 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 54a8f5f: `ksor init` is implemented — the first working verb. One command emits a
8
+ complete governed knowledge project: the record (`knowledge/`), a working
9
+ Fumadocs site (`system/site/`, static export, hot reload, static search,
10
+ llms.txt), the agent kit (AGENTS.md constitution, CLAUDE.md pointer,
11
+ `.agents/skills` with byte-identical `.claude/skills` copies, Gemini
12
+ pointer), adopter CI, and a dependency-free format checker (`pnpm check`).
13
+ Deterministic (every emitted byte ships as template content, lockfile
14
+ included), atomic, offline. Refusals carry stable slugs with working
15
+ remedies; environment failures exit 3 with slugs, never raw stack traces.
16
+
17
+ The scaffold ships branded and self-explaining: the KSoR mark as the
18
+ default favicon, a real landing page led by the instance name with the
19
+ first document derived (never hardcoded), a deletable "Built with KSoR"
20
+ maker's mark, a README that explains every emitted file, and a governed
21
+ `order:` frontmatter key that drives the sidebar, `llms.txt`, and the
22
+ home page from one declaration. The site shell is replaceable behind a
23
+ four-clause surface contract, proven by a second (Docusaurus) shell and
24
+ a shell-agnostic conformance suite in the ksor repository.
25
+
3
26
  ## 0.0.1
4
27
 
5
28
  ### 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,334 @@
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
+ const from = path.join(templateDir, entry.name);
75
+ const to = path.join(targetDir, EMITTED_NAMES.get(entry.name) ?? entry.name);
76
+ if (entry.isDirectory()) {
77
+ if (!existsSync(to)) {
78
+ mkdirSync(to, { recursive: true });
79
+ created.push(to);
80
+ }
81
+ materialize(from, to, stamps, created);
82
+ } else if (isTextFile(from)) {
83
+ const text = readFileSync(from, "utf8").replaceAll("KSOR-STAMP-NAME", stamps.name).replaceAll("KSOR-STAMP-VERSION", stamps.version);
84
+ created.push(to);
85
+ writeFileSync(to, text);
86
+ } else {
87
+ created.push(to);
88
+ copyFileSync(from, to);
89
+ }
90
+ }
91
+ return created;
92
+ }
93
+ //#endregion
94
+ //#region src/init/name.ts
95
+ /** Project-name grammar (spec: `^[a-z0-9][a-z0-9-]{0,62}$`). */
96
+ const NAME_GRAMMAR = /^[a-z0-9][a-z0-9-]{0,62}$/;
97
+ /**
98
+ * Windows refuses these as directory names at the filesystem layer, whatever
99
+ * the grammar says — and the spec runs its acceptance on windows-latest, so a
100
+ * name accepted here must be a directory everywhere.
101
+ */
102
+ const WINDOWS_RESERVED = /* @__PURE__ */ new Set([
103
+ "con",
104
+ "prn",
105
+ "aux",
106
+ "nul",
107
+ ...Array.from({ length: 9 }, (_, index) => `com${index + 1}`),
108
+ ...Array.from({ length: 9 }, (_, index) => `lpt${index + 1}`)
109
+ ]);
110
+ function nameProblem(name) {
111
+ if (!NAME_GRAMMAR.test(name)) return "grammar";
112
+ if (WINDOWS_RESERVED.has(name)) return "windows-reserved";
113
+ return null;
114
+ }
115
+ function isValidName(name) {
116
+ return nameProblem(name) === null;
117
+ }
118
+ /** A best-effort valid slug from an invalid name, for the remedy line. */
119
+ function suggestName(input) {
120
+ const slug = input.toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/-+/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 63);
121
+ const usable = WINDOWS_RESERVED.has(slug) ? `${slug}-sor` : slug;
122
+ return isValidName(usable) ? usable : null;
123
+ }
124
+ //#endregion
125
+ //#region src/init/walk.ts
126
+ /** Nearest ancestor (inclusive) containing instance.md, or null. */
127
+ function findAncestorProject(startDir) {
128
+ let dir = path.resolve(startDir);
129
+ for (;;) {
130
+ if (existsSync(path.join(dir, "instance.md"))) return dir;
131
+ const parent = path.dirname(dir);
132
+ if (parent === dir) return null;
133
+ dir = parent;
134
+ }
135
+ }
136
+ /**
137
+ * Nearest ancestor (exclusive of startDir) that is a pnpm workspace root
138
+ * whose globs could swallow a nested project. Presence of the file is enough
139
+ * to warn — glob analysis would promise precision the warning doesn't need.
140
+ */
141
+ function findAncestorWorkspace(startDir) {
142
+ let dir = path.dirname(path.resolve(startDir));
143
+ for (;;) {
144
+ const manifest = path.join(dir, "pnpm-workspace.yaml");
145
+ if (existsSync(manifest)) return dir;
146
+ const pkg = path.join(dir, "package.json");
147
+ if (existsSync(pkg)) try {
148
+ if (JSON.parse(readFileSync(pkg, "utf8")).workspaces !== void 0) return dir;
149
+ } catch {}
150
+ const parent = path.dirname(dir);
151
+ if (parent === dir) return null;
152
+ dir = parent;
153
+ }
154
+ }
155
+ //#endregion
156
+ //#region src/init/index.ts
157
+ const STAGE_PREFIX = ".ksor-init-";
158
+ const GRAMMAR = "^[a-z0-9][a-z0-9-]{0,62}$";
159
+ function fail(io, slug, lines, code) {
160
+ io.err(`error: ${slug}\n${lines.join("\n")}\n`);
161
+ return code;
162
+ }
163
+ function refuse(io, slug, lines) {
164
+ return fail(io, slug, lines, exitCodes.refused);
165
+ }
166
+ function refuseExists(io, word) {
167
+ return refuse(io, "exists", [`${word}/ already exists here.`, `pick another name, or remove ${word}/ first if it is disposable.`]);
168
+ }
169
+ function usage$1(io) {
170
+ io.out(`ksor init <name> create a new Knowledge System of Record in ./<name>
171
+ ksor init . scaffold into the current directory (must be empty)
172
+
173
+ Nothing was scaffolded: bare \`ksor init\` never writes — an unattended
174
+ agent must not scaffold into an unknown directory by accident.
175
+ The name must match ${GRAMMAR} (e.g. accounting-sor).\n`);
176
+ return 0;
177
+ }
178
+ /**
179
+ * Spec: stale stage dirs are reported, never deleted — they may hold work.
180
+ *
181
+ * Reported only once this run has written its own tree: a note ahead of a
182
+ * refusal would take the first stderr line, which belongs to the slug (found
183
+ * live: 25 concurrent init pairs, where the loser's refusal was pushed to line
184
+ * two by a note about the winner's live stage — 2026-08-18).
185
+ */
186
+ function noteStaleStages(dir, io) {
187
+ 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`);
188
+ }
189
+ /** Undo a half-written tree, children before parents. */
190
+ function rollback(created) {
191
+ for (const target of [...created].reverse()) try {
192
+ rmSync(target, {
193
+ recursive: true,
194
+ force: true
195
+ });
196
+ } catch {}
197
+ }
198
+ /** found live: git may be absent on minimal CI images — warn, never fail. */
199
+ function gitInit(dir, io) {
200
+ if (spawnSync("git", ["rev-parse", "--git-dir"], {
201
+ cwd: dir,
202
+ stdio: "ignore"
203
+ }).status === 0) return;
204
+ const result = spawnSync("git", ["init", "--quiet"], {
205
+ cwd: dir,
206
+ stdio: [
207
+ "ignore",
208
+ "ignore",
209
+ "pipe"
210
+ ],
211
+ encoding: "utf8"
212
+ });
213
+ if (result.error !== void 0) {
214
+ 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`);
215
+ return;
216
+ }
217
+ if (result.status !== 0) {
218
+ const detail = (result.stderr ?? "").trim().split("\n")[0] || `git exited ${result.status}`;
219
+ io.err(`note: git init failed: ${detail}\n`);
220
+ }
221
+ }
222
+ function handoff(io, name, targetWasDot) {
223
+ const enter = targetWasDot ? "" : ` cd ${name}\n`;
224
+ io.out(`${name} is ready — your knowledge, your repo, yours outright.\n
225
+ Next (or just tell your coding agent to take it from here):
226
+ ` + 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");
227
+ }
228
+ function init(args, cwd, io, env) {
229
+ const { version, templatesDir } = env;
230
+ 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);
231
+ const word = args[0] ?? null;
232
+ if (word === null) return usage$1(io);
233
+ if (args.length > 1) {
234
+ const joined = suggestName(args.join("-"));
235
+ 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>.`]);
236
+ }
237
+ const isDot = word === ".";
238
+ const targetDir = isDot ? path.resolve(cwd) : path.resolve(cwd, word);
239
+ const name = isDot ? path.basename(targetDir) : word;
240
+ const problem = nameProblem(name);
241
+ if (problem !== null) {
242
+ 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`;
243
+ const suggestion = suggestName(name);
244
+ 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."]);
245
+ }
246
+ const ancestorProject = findAncestorProject(isDot ? path.dirname(targetDir) : cwd);
247
+ 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."]);
248
+ if (isDot) {
249
+ const contents = existsSync(targetDir) ? readdirSync(targetDir).filter((e) => e !== ".git").sort() : [];
250
+ if (contents.length > 0) {
251
+ const listed = contents.slice(0, 5).join(", ") + (contents.length > 5 ? ", …" : "");
252
+ 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."]);
253
+ }
254
+ } else if (existsSync(targetDir)) return refuseExists(io, word);
255
+ const ancestorWorkspace = findAncestorWorkspace(targetDir);
256
+ if (isDot) {
257
+ const created = [];
258
+ try {
259
+ materialize(templatesDir, targetDir, {
260
+ name,
261
+ version
262
+ }, created);
263
+ } catch (error) {
264
+ rollback(created);
265
+ throw error;
266
+ }
267
+ } else {
268
+ const stage = mkdtempSync(path.join(path.dirname(targetDir), STAGE_PREFIX));
269
+ try {
270
+ materialize(templatesDir, stage, {
271
+ name,
272
+ version
273
+ });
274
+ } catch (error) {
275
+ rmSync(stage, {
276
+ recursive: true,
277
+ force: true
278
+ });
279
+ throw error;
280
+ }
281
+ try {
282
+ renameSync(stage, targetDir);
283
+ } catch (error) {
284
+ rmSync(stage, {
285
+ recursive: true,
286
+ force: true
287
+ });
288
+ const code = errnoCode(error);
289
+ if (code === "ENOTEMPTY" || code === "EEXIST" || code === "EPERM") return refuseExists(io, word);
290
+ throw error;
291
+ }
292
+ }
293
+ try {
294
+ 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.
295
+ `);
296
+ if (!isDot) {
297
+ chmodSync(targetDir, statSync(path.join(targetDir, "knowledge")).mode & 511);
298
+ noteStaleStages(path.dirname(targetDir), io);
299
+ }
300
+ gitInit(targetDir, io);
301
+ } catch (error) {
302
+ if (!isEnvironmentError(error)) throw error;
303
+ const detail = error instanceof Error ? error.message : String(error);
304
+ io.err(`note: the project was created, but a follow-up step failed: ${detail}\n`);
305
+ }
306
+ handoff(io, name, isDot);
307
+ return 0;
308
+ }
309
+ function runInit(args, cwd, io, env) {
310
+ try {
311
+ return init(args, cwd, io, env);
312
+ } catch (error) {
313
+ if (!isEnvironmentError(error)) throw error;
314
+ 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);
315
+ }
316
+ }
317
+ //#endregion
318
+ //#region src/init/platform.ts
319
+ /** The scaffold's toolchain requires it (decision 5, and `engines` here). */
320
+ const MINIMUM_NODE_MAJOR = 24;
321
+ /**
322
+ * The remedy for a runtime ksor cannot run on, or null when it can. Pure so
323
+ * the refusal is testable without installing a second Node.
324
+ */
325
+ function unsupportedPlatform(nodeVersion) {
326
+ const version = nodeVersion.replace(/^v/, "");
327
+ const major = Number.parseInt(version, 10);
328
+ if (Number.isNaN(major) || major >= MINIMUM_NODE_MAJOR) return null;
329
+ return `ksor requires Node >= ${MINIMUM_NODE_MAJOR} — you are on v${version}; install a current Node and re-run.`;
330
+ }
331
+ //#endregion
4
332
  //#region src/cli.ts
5
333
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
6
334
  const notice = `
@@ -12,7 +340,7 @@ Follow along: ${pkg.homepage}\n`;
12
340
  const usage = `ksor ${pkg.version} — Knowledge System of Record\n
13
341
  Usage: ksor <verb>
14
342
 
15
- Verbs (designed; none implemented in ${pkg.version} each exits 2 until it ships):\n init create a new KSoR project
343
+ Verbs (init is implemented; the rest exit 2 until they ship):\n init create a new KSoR project (implemented)
16
344
  dev run the human surface locally, watching
17
345
  build validate and build both surfaces
18
346
  serve expose the MCP agent surface
@@ -29,6 +357,20 @@ function main(args) {
29
357
  return 0;
30
358
  }
31
359
  const { word, verb } = resolveCommand(args);
360
+ if (verb === "init") {
361
+ const remedy = unsupportedPlatform(process.versions.node);
362
+ if (remedy !== null) {
363
+ process.stderr.write(`error: unsupported-platform\n${remedy}\n`);
364
+ return exitCodes.environment;
365
+ }
366
+ return runInit(args.slice(args.indexOf("init") + 1), process.cwd(), {
367
+ out: (text) => process.stdout.write(text),
368
+ err: (text) => process.stderr.write(text)
369
+ }, {
370
+ version: pkg.version,
371
+ templatesDir: fileURLToPath(new URL("../templates/scaffold", import.meta.url))
372
+ });
373
+ }
32
374
  if (word !== null && verb === null) {
33
375
  process.stderr.write(`error: unknown-verb\n"${word}" is not a ksor verb. The vocabulary is: ${verbs.join(", ")}.\n`);
34
376
  return exitCodes.refused;
package/docs/index.md CHANGED
@@ -7,15 +7,43 @@ 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`. The site shell
44
+ at `system/site/` is replaceable behind a four-clause surface contract; a
45
+ Docusaurus conformance shell lives in the ksor repository under
46
+ `workbench/shells/docusaurus/` with its swap recipe.
19
47
 
20
48
  ## Where truth lives
21
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panaversity/ksor",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
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,43 @@
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.1.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
+
28
+ ## Fidelity rules
29
+
30
+ - **Copy load-bearing values exactly** — numbers, thresholds, dates, names.
31
+ Never round, never paraphrase a figure.
32
+ - **Two disagreeing sources stay two statements**, each with its provenance
33
+ — never smooth a conflict into one invented truth; flag it to the owner.
34
+ - **Do not fill gaps from general knowledge.** If the source doesn't cover
35
+ something, the record doesn't either — that boundary is the product.
36
+ - A document replacing an older one: mark the old one `status: superseded`
37
+ with `superseded_by:` pointing at the new — never delete it.
38
+
39
+ ## Finish every batch
40
+
41
+ Run `pnpm check` and fix what it reports (its errors explain themselves),
42
+ then show the owner the rendered result (`pnpm dev`) — the site is the
43
+ review surface: you write, they check.
@@ -0,0 +1,39 @@
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.1.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
+ - `CLAUDE.md` stays a one-line pointer; `.agents/skills/` and
29
+ `.claude/skills/` hold the same files byte for byte **in both directions**
30
+ (a file only one tree carries is a rule nobody reviewed); the site contains
31
+ no content files.
32
+
33
+ Every failure prints what is wrong, why the rule exists, and the fix — obey
34
+ the printed fix literally; if it doesn't resolve the problem, that is a bug
35
+ worth reporting to ksor.
36
+
37
+ When you edit any skill under `.agents/skills/`, re-copy it to
38
+ `.claude/skills/` — the checker holds the two trees identical, and it now
39
+ notices a file added on either side.