@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.
- package/CHANGELOG.md +47 -0
- package/README.md +24 -6
- package/dist/cli.mjs +345 -2
- package/docs/index.md +38 -6
- package/package.json +3 -1
- package/templates/LICENSE +23 -0
- package/templates/scaffold/.agents/skills/add-sources/SKILL.md +46 -0
- package/templates/scaffold/.agents/skills/format-checker/SKILL.md +46 -0
- package/templates/scaffold/.agents/skills/format-checker/check.mjs +991 -0
- package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +61 -0
- package/templates/scaffold/.claude/skills/add-sources/SKILL.md +46 -0
- package/templates/scaffold/.claude/skills/format-checker/SKILL.md +46 -0
- package/templates/scaffold/.claude/skills/format-checker/check.mjs +991 -0
- package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +61 -0
- package/templates/scaffold/.gemini/settings.json +5 -0
- package/templates/scaffold/.gitattributes +5 -0
- package/templates/scaffold/.github/workflows/validate.yml +23 -0
- package/templates/scaffold/AGENTS.md +133 -0
- package/templates/scaffold/CLAUDE.md +1 -0
- package/templates/scaffold/README.md +90 -0
- package/templates/scaffold/gitignore +16 -0
- package/templates/scaffold/instance.md +26 -0
- package/templates/scaffold/knowledge/example.md +23 -0
- package/templates/scaffold/package.json +15 -0
- package/templates/scaffold/pnpm-lock.yaml +4041 -0
- package/templates/scaffold/pnpm-workspace.yaml +19 -0
- package/templates/scaffold/system/site/app/(home)/layout.tsx +6 -0
- package/templates/scaffold/system/site/app/(home)/page.tsx +83 -0
- package/templates/scaffold/system/site/app/api/search/route.ts +11 -0
- package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +53 -0
- package/templates/scaffold/system/site/app/docs/layout.tsx +24 -0
- package/templates/scaffold/system/site/app/global.css +26 -0
- package/templates/scaffold/system/site/app/icon.png +0 -0
- package/templates/scaffold/system/site/app/layout.tsx +41 -0
- package/templates/scaffold/system/site/app/llms-full.txt/route.ts +10 -0
- package/templates/scaffold/system/site/app/llms.txt/route.ts +15 -0
- package/templates/scaffold/system/site/components/built-with.tsx +18 -0
- package/templates/scaffold/system/site/components/footer-mark.tsx +22 -0
- package/templates/scaffold/system/site/components/mdx.tsx +15 -0
- package/templates/scaffold/system/site/lib/audience.ts +178 -0
- package/templates/scaffold/system/site/lib/layout.shared.tsx +17 -0
- package/templates/scaffold/system/site/lib/shared.ts +60 -0
- package/templates/scaffold/system/site/lib/source.ts +119 -0
- package/templates/scaffold/system/site/lib/stage-knowledge.ts +301 -0
- package/templates/scaffold/system/site/next-env.d.ts +6 -0
- package/templates/scaffold/system/site/next.config.mjs +32 -0
- package/templates/scaffold/system/site/package.json +29 -0
- package/templates/scaffold/system/site/postcss.config.mjs +7 -0
- package/templates/scaffold/system/site/source.config.ts +41 -0
- package/templates/scaffold/system/site/tsconfig.json +35 -0
- 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 (
|
|
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.
|
|
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
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
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.
|