@panaversity/ksor 0.0.50 → 0.0.52
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 +131 -0
- package/dist/cli.mjs +229 -5
- package/dist/{gateway-api-D8HlLys2-Ca8OnwLn.mjs → gateway-api-Ck1l_Sgs-B-_wMYLB.mjs} +18 -11
- package/dist/gateway.mjs +1 -1
- package/docs/index.md +5 -0
- package/docs/ingesting.md +47 -0
- package/docs/tool-surface.md +25 -10
- package/docs/upgrading.md +106 -0
- package/package.json +3 -3
- package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +16 -1
- package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +16 -1
- package/templates/scaffold/.ksor/people.yaml +32 -0
- package/templates/scaffold/AGENTS.md +47 -1
- package/templates/scaffold/gitignore +1 -0
- package/templates/scaffold/pnpm-lock.yaml +304 -262
- package/templates/scaffold/system/site/components/governance.tsx +6 -4
- package/templates/scaffold/system/site/lib/actor-display.ts +53 -0
- package/templates/scaffold/system/site/lib/people.ts +82 -0
- package/templates/scaffold/system/site/lib/record-href.ts +1 -1
- package/templates/scaffold/system/site/next.config.mjs +13 -0
- package/templates/scaffold/system/site/package.json +4 -4
- package/templates/scaffold/system/site/source.config.ts +1 -1
|
@@ -4,6 +4,8 @@ import { DocumentActions } from "@/components/document-actions";
|
|
|
4
4
|
import { Clock } from "lucide-react";
|
|
5
5
|
import type { ReactElement } from "react";
|
|
6
6
|
|
|
7
|
+
import { displayActor } from "@/lib/actor-display";
|
|
8
|
+
import { peopleBook } from "@/lib/people";
|
|
7
9
|
import {
|
|
8
10
|
badgeAddsToStatus,
|
|
9
11
|
badgeText,
|
|
@@ -257,7 +259,7 @@ export function GovernanceMeta({
|
|
|
257
259
|
<Chip text={trust.tier} />
|
|
258
260
|
{trust.by === null ? null : (
|
|
259
261
|
<span className="font-normal text-fd-muted-foreground">
|
|
260
|
-
{trust.by}
|
|
262
|
+
{displayActor(trust.by, peopleBook())}
|
|
261
263
|
{trust.at === null ? null : <> · {day(trust.at)}</>}
|
|
262
264
|
</span>
|
|
263
265
|
)}
|
|
@@ -286,7 +288,7 @@ export function GovernanceMeta({
|
|
|
286
288
|
Links keep full strength: `Replaces` points at the document this one
|
|
287
289
|
superseded, and that is an action rather than a fact. */}
|
|
288
290
|
<dl className="mt-2.5 flex flex-wrap items-baseline gap-x-8 gap-y-2.5 empty:mt-0 [&_a]:text-fd-foreground [&_dd]:font-normal [&_dd]:text-fd-muted-foreground">
|
|
289
|
-
{owner === null ? null : <Fact label="Owner">{owner}</Fact>}
|
|
291
|
+
{owner === null ? null : <Fact label="Owner">{displayActor(owner, peopleBook())}</Fact>}
|
|
290
292
|
{/* Who let this into the record. `ksor.approval` is what makes a `stable`
|
|
291
293
|
document stable at all (record spec §2.2), so a page that showed the
|
|
292
294
|
word and not the signature would be publishing the claim without its
|
|
@@ -294,7 +296,7 @@ export function GovernanceMeta({
|
|
|
294
296
|
{approval === null ? null : (
|
|
295
297
|
<Fact label="Approved">
|
|
296
298
|
<>
|
|
297
|
-
{approval.by} · {day(approval.at)}
|
|
299
|
+
{displayActor(approval.by, peopleBook())} · {day(approval.at)}
|
|
298
300
|
</>
|
|
299
301
|
</Fact>
|
|
300
302
|
)}
|
|
@@ -307,7 +309,7 @@ export function GovernanceMeta({
|
|
|
307
309
|
{deprecated === null ? null : (
|
|
308
310
|
<Fact label="Withdrawn">
|
|
309
311
|
<>
|
|
310
|
-
{deprecated.by} · {day(deprecated.at)}
|
|
312
|
+
{displayActor(deprecated.by, peopleBook())} · {day(deprecated.at)}
|
|
311
313
|
</>
|
|
312
314
|
</Fact>
|
|
313
315
|
)}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn a stored actor identifier into the string the page prints.
|
|
3
|
+
*
|
|
4
|
+
* KSoR stores actors as `human:<handle>`, `process:<id>`, `team:<id>` or
|
|
5
|
+
* `<producer>/<version>`. Those forms are right for machines — the checker
|
|
6
|
+
* parses them, `.ksor/governance.yaml` lists actors by them, and the whole
|
|
7
|
+
* authority model depends on them — but a public-facing page shouldn't lead
|
|
8
|
+
* with a slug. This module bridges the two.
|
|
9
|
+
*
|
|
10
|
+
* ONE-WAY RULE. If `.ksor/people.yaml` maps this exact identifier to a name, we
|
|
11
|
+
* print `Human: Bashir Aziz`. If it does not, we print the identifier unchanged
|
|
12
|
+
* (`human:bashiraziz`, `ksor-starter/0.0.47`). No convention-based splitting,
|
|
13
|
+
* no camelCase / kebab-case guessing, and no derivation of a handle from a
|
|
14
|
+
* name: an owner is the only source of a display name.
|
|
15
|
+
*
|
|
16
|
+
* A producer actor (`<producer>/<version>`) has no `<kind>:` prefix and no
|
|
17
|
+
* natural name to look up, so it passes through unchanged — which is correct:
|
|
18
|
+
* a tool that approved a document should be named plainly, not humanised.
|
|
19
|
+
*
|
|
20
|
+
* NO IMPORTS: a leaf, like `lifecycle-rule.ts`. The phone book is handed in by
|
|
21
|
+
* the caller rather than reached for, so the rule can be exercised without a
|
|
22
|
+
* record on disk — and so nothing pulls a filesystem read in behind it.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** The prefixes KSoR's actor grammar defines, in the case the site prints. */
|
|
26
|
+
const KIND_LABELS: Record<string, string> = {
|
|
27
|
+
human: "Human",
|
|
28
|
+
process: "Process",
|
|
29
|
+
team: "Team",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The display form of an actor. An actor that does not appear in
|
|
34
|
+
* `.ksor/people.yaml` renders exactly as stored, so nothing regresses on a
|
|
35
|
+
* record that has declared no names — which is every record until an owner
|
|
36
|
+
* says otherwise.
|
|
37
|
+
*/
|
|
38
|
+
export function displayActor(actor: string, people: ReadonlyMap<string, string>): string {
|
|
39
|
+
const colonAt = actor.indexOf(":");
|
|
40
|
+
if (colonAt === -1) {
|
|
41
|
+
// No `<kind>:` prefix — this is a producer like `ksor-starter/0.0.47`.
|
|
42
|
+
// Print it unchanged: humanising a tool's identifier would misread.
|
|
43
|
+
return actor;
|
|
44
|
+
}
|
|
45
|
+
// Looked up by the WHOLE identifier, not the bare handle: `human:ops` and
|
|
46
|
+
// `team:ops` are different actors, and a phone book keyed on `ops` would
|
|
47
|
+
// print one of them under the other's name.
|
|
48
|
+
const name = people.get(actor) ?? null;
|
|
49
|
+
if (name === null) return actor;
|
|
50
|
+
const kind = actor.slice(0, colonAt);
|
|
51
|
+
const kindLabel = KIND_LABELS[kind] ?? kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
52
|
+
return `${kindLabel}: ${name}`;
|
|
53
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Natural names for the actors this record cites — the site's phone book.
|
|
3
|
+
*
|
|
4
|
+
* A MAP keyed by the actor as stored (`human:bashiraziz`, `team:legal-ops`),
|
|
5
|
+
* not a list of names a handle is derived from. The derivation was the defect:
|
|
6
|
+
* `name.replace(/\s+/g, "").toLowerCase()` can only ever match a handle that
|
|
7
|
+
* IS somebody's squashed full name, so `human:ciso`, `human:audit-lead` and
|
|
8
|
+
* `human:mjs` — most of the actors in a real record — had no expressible name
|
|
9
|
+
* at all. It also collided: "Bashir Aziz" and "Bashira Ziz" both derive
|
|
10
|
+
* `bashiraziz`, which would print one person's name on the other's governance
|
|
11
|
+
* act. A map has neither problem, and duplicate keys are refused by the parser
|
|
12
|
+
* rather than resolved by whichever came last.
|
|
13
|
+
*
|
|
14
|
+
* ONE-WAY. The identifier is what the record stores, cites and checks against
|
|
15
|
+
* the policy; this is only what a page prints. Nothing reads a name back into
|
|
16
|
+
* an actor, and no authority follows from appearing here — which is why this is
|
|
17
|
+
* a file of its own and not a block in `.ksor/governance.yaml`: that file is
|
|
18
|
+
* the root of authority, its key set is closed on purpose, and its digest is
|
|
19
|
+
* hashed into `build.lock.json`, so correcting the spelling of someone's name
|
|
20
|
+
* there would refuse the next site build as `ksor-lock-stale`.
|
|
21
|
+
*
|
|
22
|
+
* Read from the project root rather than the process's cwd: `next build` runs
|
|
23
|
+
* in `system/site`, so a cwd-relative path found nothing and the feature was
|
|
24
|
+
* inert in exactly the builds that publish.
|
|
25
|
+
*
|
|
26
|
+
* Read AT USE and memoised, not at module load. A module-load `readFileSync`
|
|
27
|
+
* makes importing this module a filesystem act — it runs wherever the module is
|
|
28
|
+
* pulled in, including from a test that wants nothing but the display rule, and
|
|
29
|
+
* it fixes the answer before anything has had a chance to say where the record
|
|
30
|
+
* is. That is the same defect the env-tuning knobs had (#149/#194), one file
|
|
31
|
+
* over.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { readFileSync } from "node:fs";
|
|
35
|
+
import path from "node:path";
|
|
36
|
+
|
|
37
|
+
import { parseAllDocuments } from "yaml";
|
|
38
|
+
|
|
39
|
+
import { projectRoot } from "./shared";
|
|
40
|
+
|
|
41
|
+
const PEOPLE_YAML = path.join(projectRoot, ".ksor", "people.yaml");
|
|
42
|
+
|
|
43
|
+
function loadPeople(): ReadonlyMap<string, string> {
|
|
44
|
+
let text: string;
|
|
45
|
+
try {
|
|
46
|
+
text = readFileSync(PEOPLE_YAML, "utf8");
|
|
47
|
+
} catch {
|
|
48
|
+
// Optional: its absence means "no natural names declared".
|
|
49
|
+
return new Map();
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const docs = parseAllDocuments(text.replace(/^/, ""), {
|
|
53
|
+
schema: "core",
|
|
54
|
+
uniqueKeys: true,
|
|
55
|
+
logLevel: "silent",
|
|
56
|
+
});
|
|
57
|
+
const value: unknown = docs[0]?.toJS();
|
|
58
|
+
if (typeof value !== "object" || value === null) return new Map();
|
|
59
|
+
const table = (value as { people?: unknown }).people;
|
|
60
|
+
if (typeof table !== "object" || table === null || Array.isArray(table)) return new Map();
|
|
61
|
+
const out = new Map<string, string>();
|
|
62
|
+
for (const [actor, name] of Object.entries(table as Record<string, unknown>)) {
|
|
63
|
+
// A blank value is an entry someone started and left; printing "" would
|
|
64
|
+
// erase the identifier rather than replace it.
|
|
65
|
+
if (typeof name === "string" && name.trim() !== "") out.set(actor.trim(), name.trim());
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
} catch {
|
|
69
|
+
return new Map();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let cached: ReadonlyMap<string, string> | null = null;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* What this record has declared. Memoised per process: the file is authored,
|
|
77
|
+
* not runtime state, and a static build renders many pages from one read.
|
|
78
|
+
*/
|
|
79
|
+
export function peopleBook(): ReadonlyMap<string, string> {
|
|
80
|
+
cached ??= loadPeople();
|
|
81
|
+
return cached;
|
|
82
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* bundle-absolute (`/policies/x.md`, resolved against `knowledge/`) and
|
|
6
6
|
* relative (`x.md`, `./x.md`, `../x.md`, against the document's own
|
|
7
7
|
* directory), with `.md` optional in each. The shell resolves only the `./`
|
|
8
|
-
* and `../` forms (fumadocs-core 16.
|
|
8
|
+
* and `../` forms (fumadocs-core 16.15.4, `resolveHref` returns anything else
|
|
9
9
|
* untouched), so a bundle-absolute link left the record's frame entirely and a
|
|
10
10
|
* bare `x.md` was resolved by the browser against the page's ROUTE rather than
|
|
11
11
|
* its directory: both 404'd from every page, found live 2026-08-25 as prefetch
|
|
@@ -46,6 +46,19 @@ const config = {
|
|
|
46
46
|
// Hosting under a sub-path (e.g. a GitHub Pages project site):
|
|
47
47
|
// KSOR_BASE_PATH="/my-repo" pnpm build
|
|
48
48
|
basePath: process.env.KSOR_BASE_PATH ?? "",
|
|
49
|
+
// Next >= 16.3 writes AGENTS.md and CLAUDE.md into the Next project root
|
|
50
|
+
// whenever `next dev` detects a coding agent (`agentRules`, default true).
|
|
51
|
+
// Here that root is system/site, and a markdown file there is refused by the
|
|
52
|
+
// record's own hygiene rule (`ksor-site-holds-content`): the site RENDERS the
|
|
53
|
+
// record, it never holds it, so content there silently forks it. Left on, an
|
|
54
|
+
// adopter's `pnpm dev` made their own `pnpm check` go red — reproduced by the
|
|
55
|
+
// scaffold walkthrough the hour Next 16.3.3 was pinned.
|
|
56
|
+
//
|
|
57
|
+
// The scaffold already answers what the feature is for: AGENTS.md at the repo
|
|
58
|
+
// root is the coding agent's first read, and one record must not speak with
|
|
59
|
+
// two voices about one thing. Turn it back on only if you also move those two
|
|
60
|
+
// files out of system/site.
|
|
61
|
+
agentRules: false,
|
|
49
62
|
turbopack: {
|
|
50
63
|
root: repoRoot,
|
|
51
64
|
},
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"class-variance-authority": "0.7.1",
|
|
12
12
|
"clsx": "2.1.1",
|
|
13
|
-
"fumadocs-core": "16.
|
|
14
|
-
"fumadocs-mdx": "15.
|
|
15
|
-
"fumadocs-ui": "16.
|
|
13
|
+
"fumadocs-core": "16.15.4",
|
|
14
|
+
"fumadocs-mdx": "15.4.0",
|
|
15
|
+
"fumadocs-ui": "16.15.4",
|
|
16
16
|
"lucide-react": "1.31.0",
|
|
17
|
-
"next": "16.
|
|
17
|
+
"next": "16.3.3",
|
|
18
18
|
"radix-ui": "1.6.7",
|
|
19
19
|
"react": "19.2.8",
|
|
20
20
|
"react-dom": "19.2.8",
|
|
@@ -230,7 +230,7 @@ export default defineConfig({
|
|
|
230
230
|
* apply to every group on the page and persist to the next visit. The
|
|
231
231
|
* `Tabs` branch drops the attribute silently, so a reader with a
|
|
232
232
|
* ten-section document would pick their tool ten times (verified against
|
|
233
|
-
* fumadocs-core 16.
|
|
233
|
+
* fumadocs-core 16.15.4, remark-code-tab.js).
|
|
234
234
|
*/
|
|
235
235
|
remarkPlugins: [[remarkCodeTab, { Tabs: "CodeBlockTabs" }]],
|
|
236
236
|
},
|