okfcli 0.0.0-stage → 0.1.0

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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +101 -2
  3. package/SPEC.md +243 -0
  4. package/dist/bin/okfcli.d.ts +2 -0
  5. package/dist/bin/okfcli.js +9 -0
  6. package/dist/src/cli.d.ts +7 -0
  7. package/dist/src/cli.js +122 -0
  8. package/dist/src/commands/init.d.ts +13 -0
  9. package/dist/src/commands/init.js +78 -0
  10. package/dist/src/commands/new.d.ts +10 -0
  11. package/dist/src/commands/new.js +53 -0
  12. package/dist/src/commands/validate.d.ts +7 -0
  13. package/dist/src/commands/validate.js +49 -0
  14. package/dist/src/commands/verify.d.ts +12 -0
  15. package/dist/src/commands/verify.js +77 -0
  16. package/dist/src/core/frontmatter.d.ts +7 -0
  17. package/dist/src/core/frontmatter.js +27 -0
  18. package/dist/src/core/profile.d.ts +12 -0
  19. package/dist/src/core/profile.js +101 -0
  20. package/dist/src/core/trust.d.ts +10 -0
  21. package/dist/src/core/trust.js +42 -0
  22. package/dist/src/core/types.d.ts +61 -0
  23. package/dist/src/core/types.js +1 -0
  24. package/dist/src/core/util.d.ts +17 -0
  25. package/dist/src/core/util.js +67 -0
  26. package/dist/src/generate/agents.d.ts +24 -0
  27. package/dist/src/generate/agents.js +128 -0
  28. package/dist/src/generate/targets.d.ts +26 -0
  29. package/dist/src/generate/targets.js +18 -0
  30. package/dist/src/generate/templates.d.ts +13 -0
  31. package/dist/src/generate/templates.js +54 -0
  32. package/dist/src/index.d.ts +6 -0
  33. package/dist/src/index.js +7 -0
  34. package/dist/src/wiki/fixes.d.ts +2 -0
  35. package/dist/src/wiki/fixes.js +45 -0
  36. package/dist/src/wiki/indexer.d.ts +5 -0
  37. package/dist/src/wiki/indexer.js +47 -0
  38. package/dist/src/wiki/pages.d.ts +19 -0
  39. package/dist/src/wiki/pages.js +70 -0
  40. package/dist/src/wiki/root.d.ts +1 -0
  41. package/dist/src/wiki/root.js +29 -0
  42. package/dist/src/wiki/rules.d.ts +3 -0
  43. package/dist/src/wiki/rules.js +150 -0
  44. package/package.json +65 -4
@@ -0,0 +1,53 @@
1
+ import { join } from 'node:path';
2
+ import { findFolderByType, loadProfile, typeAlias } from '../core/profile.js';
3
+ import { regenerateIndexes } from '../wiki/indexer.js';
4
+ import { renderPage } from '../generate/templates.js';
5
+ import { UserError, assertKebabPath, createText, errorCode, humanize } from '../core/util.js';
6
+ export const newHelp = `okfcli new <type> <name> --description "..." [--title "..."]
7
+
8
+ Creates a page with frontmatter and a section skeleton, and registers it in
9
+ the index.md of its folder. The name is relative to the folder of the type
10
+ (e.g. "state-management" or "ui/forms"). The type is the singular name: overview, architecture, pattern, convention, workflow,
11
+ testing, feature, module, interface, operation, troubleshooting.`;
12
+ export function newPage({ root, type, name, title, description }) {
13
+ const profile = loadProfile(root);
14
+ const folder = findFolderByType(profile, type);
15
+ if (!folder) {
16
+ const valid = profile.folders.map((f) => typeAlias(f.type)).join(', ');
17
+ throw new UserError(`Unknown type "${type}". Available types: ${valid}.`);
18
+ }
19
+ if (!description?.trim()) {
20
+ throw new UserError('Missing --description: one sentence saying what the page is about.');
21
+ }
22
+ const slug = name.replace(/\.md$/, '').replace(/^\/+|\/+$/g, '');
23
+ assertKebabPath(slug);
24
+ const rel = `${folder.name}/${slug}.md`;
25
+ const file = join(root, rel);
26
+ const pageTitle = title?.trim() || humanize(slug.split('/').pop() ?? slug);
27
+ const text = renderPage({
28
+ folder,
29
+ title: pageTitle,
30
+ description: description.trim(),
31
+ });
32
+ try {
33
+ createText(file, text);
34
+ }
35
+ catch (err) {
36
+ if (errorCode(err) === 'EEXIST')
37
+ throw new UserError(`${rel} already exists. Edit it instead of creating it again.`);
38
+ throw err;
39
+ }
40
+ const indexes = regenerateIndexes(root, profile);
41
+ const sections = folder.sections.map((s) => `# ${s}`);
42
+ return {
43
+ path: rel,
44
+ type: folder.type,
45
+ sections,
46
+ updated: indexes,
47
+ text: [
48
+ `Created ${rel} (type: ${folder.type}).`,
49
+ `Indexes updated: ${indexes.join(', ') || 'none'}.`,
50
+ `Fill in these sections: ${sections.join(', ')}.`,
51
+ ].join('\n'),
52
+ };
53
+ }
@@ -0,0 +1,7 @@
1
+ import type { CommandResult } from '../core/types.js';
2
+ export declare const validateHelp = "okfcli validate [--fix] [--strict]\n\nChecks the wiki against OKF v0.2 and the base profile (see SPEC.md section 8):\nstructure, sections, links, indexes and review state (which pages nobody has\nreviewed, or that changed after being verified). Exits with code 1 if there is\nany error (with --strict, any warning too).\n --fix repair what can be decided mechanically (folders, required pages,\n missing section skeletons, indexes), then validate again\n --strict treat warnings as failures\n --json print { ok, errors, warnings, fixed, review, issues }, each issue as\n { rule, severity, file, message, fix }";
3
+ export declare function validateCommand({ root, fix, strict }: {
4
+ root: string;
5
+ fix?: boolean | undefined;
6
+ strict?: boolean | undefined;
7
+ }): CommandResult;
@@ -0,0 +1,49 @@
1
+ import { folderByName, loadProfile } from '../core/profile.js';
2
+ import { validateWiki } from '../wiki/rules.js';
3
+ import { applyFixes } from '../wiki/fixes.js';
4
+ import { loadWiki } from '../wiki/pages.js';
5
+ import { trustState } from '../core/trust.js';
6
+ export const validateHelp = `okfcli validate [--fix] [--strict]
7
+
8
+ Checks the wiki against OKF v0.2 and the base profile (see SPEC.md section 8):
9
+ structure, sections, links, indexes and review state (which pages nobody has
10
+ reviewed, or that changed after being verified). Exits with code 1 if there is
11
+ any error (with --strict, any warning too).
12
+ --fix repair what can be decided mechanically (folders, required pages,
13
+ missing section skeletons, indexes), then validate again
14
+ --strict treat warnings as failures
15
+ --json print { ok, errors, warnings, fixed, review, issues }, each issue as
16
+ { rule, severity, file, message, fix }`;
17
+ // How many pages are at each trust tier. Deprecated pages are not asked to be
18
+ // reviewed, so they are counted apart.
19
+ function reviewSummary(root, profile) {
20
+ const pages = loadWiki(root).concepts.filter((c) => c.data && c.folder && folderByName(profile, c.folder));
21
+ const active = pages.filter((p) => p.data?.status !== 'deprecated');
22
+ const states = active.map((p) => trustState(p.data, p.body));
23
+ const count = (pred) => states.filter(pred).length;
24
+ return {
25
+ pages: pages.length,
26
+ deprecated: pages.length - active.length,
27
+ 'human-reviewed': count((s) => s.tier === 'human-reviewed'),
28
+ 'machine-confirmed': count((s) => s.tier === 'machine-confirmed'),
29
+ unverified: count((s) => s.tier === 'unverified' && !s.outdated),
30
+ outdated: count((s) => s.outdated),
31
+ };
32
+ }
33
+ export function validateCommand({ root, fix = false, strict = false }) {
34
+ const profile = loadProfile(root);
35
+ const fixed = fix ? applyFixes(root, profile) : [];
36
+ const issues = validateWiki(root, profile);
37
+ const errors = issues.filter((i) => i.severity === 'error').length;
38
+ const warnings = issues.length - errors;
39
+ const ok = errors === 0 && !(strict && warnings > 0);
40
+ const review = reviewSummary(root, profile);
41
+ const lines = [];
42
+ if (fixed.length)
43
+ lines.push('Fixed:', ...fixed.map((f) => ` ${f}`), '');
44
+ for (const i of issues) {
45
+ lines.push(`${i.severity.padEnd(7)} ${i.rule} ${i.file}`, ` ${i.message} ${i.fix}`);
46
+ }
47
+ lines.push(issues.length ? '' : null, `Review: ${review['human-reviewed']} human-reviewed, ${review['machine-confirmed']} machine-confirmed, ${review.unverified} unverified, ${review.outdated} outdated (${review.deprecated} deprecated pages not counted).`, `${errors} error${errors === 1 ? '' : 's'}, ${warnings} warning${warnings === 1 ? '' : 's'}.${ok ? '' : ' Validation failed.'}`);
48
+ return { ok, errors, warnings, fixed, review, issues, text: lines.filter((l) => l !== null).join('\n') };
49
+ }
@@ -0,0 +1,12 @@
1
+ import type { CommandResult } from '../core/types.js';
2
+ export declare const verifyHelp = "okfcli verify <path> [--by <actor>] [--force]\n\nRecords that a page has been reviewed: adds { by, at, hash } to \"verified\".\nThe hash fingerprints the content that was read; if the page is edited later,\nthe verification is reported as outdated until it is verified again.\nOne entry per actor: a repeat verification replaces the previous one.\nThe path is relative to the wiki, with or without \".md\".\n --by actor: human:<id>, process:<id> or <tool>/<version>.\n Default: $OKFCLI_ACTOR, else okfcli/<version> (machine-confirmed).\n Use human:<id> only when a person has actually reviewed the page.\n --force verify even if the page still has unfilled TODO placeholders\n\nA person can also verify by hand, by adding to the page frontmatter:\n verified:\n - { by: human:<id>, at: <ISO date-time> }\n(entries without a hash are never reported as outdated).";
3
+ export interface VerifyOptions {
4
+ root: string;
5
+ cwd: string;
6
+ path: string | undefined;
7
+ by?: string | undefined;
8
+ force?: boolean | undefined;
9
+ now?: Date;
10
+ version: string;
11
+ }
12
+ export declare function verifyCommand({ root, cwd, path, by, force, now, version }: VerifyOptions): CommandResult;
@@ -0,0 +1,77 @@
1
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
2
+ import { join, relative, resolve, isAbsolute, sep } from 'node:path';
3
+ import { isMap } from 'yaml';
4
+ import { loadProfile } from '../core/profile.js';
5
+ import { editFrontmatter, parseFrontmatter } from '../core/frontmatter.js';
6
+ import { contentHash, countTodos, trustState, validVerifications } from '../core/trust.js';
7
+ import { UserError, isoNow, toPosix, writeText } from '../core/util.js';
8
+ const ACTOR = /^(?:(?:human|process):\S+|[^\s/:]+\/\S+)$/;
9
+ export const verifyHelp = `okfcli verify <path> [--by <actor>] [--force]
10
+
11
+ Records that a page has been reviewed: adds { by, at, hash } to "verified".
12
+ The hash fingerprints the content that was read; if the page is edited later,
13
+ the verification is reported as outdated until it is verified again.
14
+ One entry per actor: a repeat verification replaces the previous one.
15
+ The path is relative to the wiki, with or without ".md".
16
+ --by actor: human:<id>, process:<id> or <tool>/<version>.
17
+ Default: $OKFCLI_ACTOR, else okfcli/<version> (machine-confirmed).
18
+ Use human:<id> only when a person has actually reviewed the page.
19
+ --force verify even if the page still has unfilled TODO placeholders
20
+
21
+ A person can also verify by hand, by adding to the page frontmatter:
22
+ verified:
23
+ - { by: human:<id>, at: <ISO date-time> }
24
+ (entries without a hash are never reported as outdated).`;
25
+ // Accepts a path relative to the wiki or to the current directory.
26
+ function locatePage(root, cwd, arg) {
27
+ const withExt = arg.endsWith('.md') ? arg : `${arg}.md`;
28
+ const candidates = [join(root, withExt)];
29
+ if (!isAbsolute(withExt))
30
+ candidates.push(resolve(cwd, withExt));
31
+ for (const file of candidates) {
32
+ if (!existsSync(file))
33
+ continue;
34
+ // Resolve symlinks too, so a link inside the wiki cannot point at a file outside it.
35
+ const rel = relative(realpathSync(root), realpathSync(file));
36
+ if (rel && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
37
+ return { file, rel: toPosix(rel) };
38
+ }
39
+ throw new UserError(`Page not found: ${arg}. Use a path relative to the wiki, e.g. patterns/state-management.md.`);
40
+ }
41
+ export function verifyCommand({ root, cwd, path, by, force = false, now = new Date(), version }) {
42
+ loadProfile(root);
43
+ if (!path)
44
+ throw new UserError('Usage: okfcli verify <path> [--by <actor>]');
45
+ const actor = by ?? process.env.OKFCLI_ACTOR ?? `okfcli/${version}`;
46
+ if (!ACTOR.test(actor)) {
47
+ throw new UserError(`Invalid actor "${actor}". Use human:<id>, process:<id> or <tool>/<version>.`);
48
+ }
49
+ const { file, rel } = locatePage(root, cwd, path);
50
+ const text = readFileSync(file, 'utf8');
51
+ const { data, body } = parseFrontmatter(text);
52
+ if (!data)
53
+ throw new UserError(`${rel} has no parseable frontmatter; fix it first (okfcli validate).`);
54
+ const todos = countTodos(body);
55
+ if (todos && !force) {
56
+ throw new UserError(`${rel} still has ${todos} unfilled TODO placeholder${todos > 1 ? 's' : ''}. Fill them in before verifying (or use --force).`);
57
+ }
58
+ const entry = { by: actor, at: isoNow(now), hash: contentHash(data, body) };
59
+ // Keep the verifications that still describe this content; drop outdated ones and the actor's previous entry.
60
+ const kept = validVerifications(data, body).filter((e) => e.by !== actor);
61
+ const next = editFrontmatter(text, (doc) => {
62
+ const seq = doc.createNode([...kept, entry]);
63
+ for (const item of seq.items)
64
+ if (isMap(item))
65
+ item.flow = true;
66
+ doc.set('verified', seq);
67
+ });
68
+ writeText(file, next);
69
+ const after = parseFrontmatter(next);
70
+ const { tier } = trustState(after.data, after.body);
71
+ return {
72
+ path: rel,
73
+ tier,
74
+ verified: [...kept, entry],
75
+ text: `Verified ${rel} as ${actor} (${tier}).`,
76
+ };
77
+ }
@@ -0,0 +1,7 @@
1
+ import { type Document } from 'yaml';
2
+ import type { Frontmatter } from './types.js';
3
+ export declare function parseFrontmatter(text: string): {
4
+ data: Frontmatter | null;
5
+ body: string;
6
+ };
7
+ export declare function editFrontmatter(text: string, mutate: (doc: Document) => void): string;
@@ -0,0 +1,27 @@
1
+ import { parse, parseDocument } from 'yaml';
2
+ import { UserError } from './util.js';
3
+ const FENCE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
4
+ // Returns { data, body }. data is null when there is no parseable frontmatter.
5
+ export function parseFrontmatter(text) {
6
+ const match = FENCE.exec(text);
7
+ if (!match)
8
+ return { data: null, body: text };
9
+ const body = text.slice(match[0].length);
10
+ try {
11
+ const data = parse(match[1] ?? '');
12
+ return { data: data && typeof data === 'object' && !Array.isArray(data) ? data : null, body };
13
+ }
14
+ catch {
15
+ return { data: null, body };
16
+ }
17
+ }
18
+ // Edits the frontmatter through a YAML document, keeping comments, order and
19
+ // the rest of the file untouched. Returns the new text.
20
+ export function editFrontmatter(text, mutate) {
21
+ const match = FENCE.exec(text);
22
+ if (!match)
23
+ throw new UserError('The page has no frontmatter to edit.');
24
+ const doc = parseDocument(match[1] ?? '');
25
+ mutate(doc);
26
+ return `---\n${doc.toString({ lineWidth: 0 }).trimEnd()}\n---\n${text.slice(match[0].length)}`;
27
+ }
@@ -0,0 +1,12 @@
1
+ import type { Folder, Profile } from './types.js';
2
+ export declare const PROFILE_ID = "base@0.1";
3
+ export declare const PROFILE_FILE = ".okf-profile.yaml";
4
+ export declare const OKF_VERSION = "0.2";
5
+ export declare const CORE_FOLDERS: Folder[];
6
+ export declare const OPTIONAL_FOLDERS: Folder[];
7
+ export declare const typeAlias: (type: string) => string;
8
+ export declare function loadProfile(wikiRoot: string): Profile;
9
+ export declare const folderByName: (profile: Profile, name: string) => Folder | undefined;
10
+ /** One of the base profile folders; they always exist. */
11
+ export declare function coreFolder(name: string): Folder;
12
+ export declare function findFolderByType(profile: Profile, alias: string): Folder | undefined;
@@ -0,0 +1,101 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { parse } from 'yaml';
4
+ import { KEBAB, UserError } from './util.js';
5
+ export const PROFILE_ID = 'base@0.1';
6
+ export const PROFILE_FILE = '.okf-profile.yaml';
7
+ export const OKF_VERSION = '0.2';
8
+ const SEVERITIES = ['error', 'warning', 'off'];
9
+ // Base profile folders. Names and `type` are fixed; see SPEC.md section 3.
10
+ const folder = (core, name, type, title, description, sections) => ({
11
+ name,
12
+ type,
13
+ title,
14
+ description,
15
+ sections,
16
+ core,
17
+ });
18
+ const core = (...args) => folder(true, ...args);
19
+ const optional = (...args) => folder(false, ...args);
20
+ export const CORE_FOLDERS = [
21
+ core('overview', 'Overview', 'Overview', 'What the project is, its scope and its glossary.', ['Purpose', 'Scope']),
22
+ core('architecture', 'Architecture', 'Architecture', 'Structure, layers and dependency rules.', ['Overview', 'Rules', 'Why']),
23
+ core('patterns', 'Pattern', 'Patterns', 'How each thing is implemented, with a real example.', ['When to use', 'How', 'Example', 'Why']),
24
+ core('conventions', 'Convention', 'Conventions', 'Style, naming and working conventions.', ['Rule', 'Examples']),
25
+ core('workflows', 'Workflow', 'Workflows', 'How we work: setup, build, PRs, releases, onboarding.', ['Goal', 'Steps']),
26
+ core('testing', 'Testing', 'Testing', 'Testing strategy, how to run it and rules.', ['Scope', 'How to run', 'Rules']),
27
+ ];
28
+ export const OPTIONAL_FOLDERS = [
29
+ optional('features', 'Feature', 'Features', 'Business capabilities and flows.', ['Summary', 'Behavior']),
30
+ optional('modules', 'Module', 'Modules', 'Internal packages and components.', ['Responsibility', 'Public API']),
31
+ optional('interfaces', 'Interface', 'Interfaces', 'External contracts: APIs, events, integrations.', ['Contract', 'Consumers']),
32
+ optional('operations', 'Operation', 'Operations', 'Deployment, runbooks and incidents.', ['When', 'Steps']),
33
+ optional('troubleshooting', 'Troubleshooting', 'Troubleshooting', 'Known problems and how to fix them.', ['Symptom', 'Cause', 'Fix']),
34
+ ];
35
+ // Command-line alias for a type: "Pattern" -> "pattern".
36
+ export const typeAlias = (type) => type.toLowerCase().replace(/\s+/g, '-');
37
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
38
+ export function loadProfile(wikiRoot) {
39
+ const file = join(wikiRoot, PROFILE_FILE);
40
+ if (!existsSync(file)) {
41
+ throw new UserError(`${PROFILE_FILE} not found in ${wikiRoot}. Is this an okfcli wiki? Run "okfcli init".`);
42
+ }
43
+ let config;
44
+ try {
45
+ config = parse(readFileSync(file, 'utf8')) ?? {};
46
+ }
47
+ catch (err) {
48
+ throw new UserError(`${PROFILE_FILE} is not valid YAML: ${err instanceof Error ? err.message : String(err)}`);
49
+ }
50
+ if (!isRecord(config)) {
51
+ throw new UserError(`${PROFILE_FILE} must be a YAML mapping.`);
52
+ }
53
+ if (config.extra_folders != null && !Array.isArray(config.extra_folders)) {
54
+ throw new UserError(`${PROFILE_FILE}: "extra_folders" must be a list.`);
55
+ }
56
+ const severity = config.severity ?? {};
57
+ if (!isRecord(severity)) {
58
+ throw new UserError(`${PROFILE_FILE}: "severity" must be a mapping of rule to ${SEVERITIES.join(', ')}.`);
59
+ }
60
+ for (const [rule, level] of Object.entries(severity)) {
61
+ if (!SEVERITIES.includes(level)) {
62
+ throw new UserError(`${PROFILE_FILE}: severity of "${rule}" is "${String(level)}"; use ${SEVERITIES.join(', ')}.`);
63
+ }
64
+ }
65
+ const extra = (config.extra_folders ?? []).map((entry) => {
66
+ const f = isRecord(entry) ? entry : {};
67
+ if (!f.name || !f.type) {
68
+ throw new UserError(`${PROFILE_FILE}: every extra_folders entry needs "name" and "type".`);
69
+ }
70
+ if (typeof f.name !== 'string' || !KEBAB.test(f.name)) {
71
+ throw new UserError(`${PROFILE_FILE}: extra_folders name "${String(f.name)}" must be kebab-case (lowercase letters, digits and hyphens).`);
72
+ }
73
+ const type = String(f.type);
74
+ const sections = (Array.isArray(f.sections) ? f.sections : []).map((s) => String(s).replace(/^#+\s*/, ''));
75
+ return {
76
+ name: f.name,
77
+ type,
78
+ title: f.title ? String(f.title) : type,
79
+ description: f.description ? String(f.description) : '',
80
+ sections,
81
+ core: false,
82
+ extra: true,
83
+ };
84
+ });
85
+ return {
86
+ config,
87
+ severity: severity,
88
+ folders: [...CORE_FOLDERS, ...OPTIONAL_FOLDERS, ...extra],
89
+ };
90
+ }
91
+ export const folderByName = (profile, name) => profile.folders.find((f) => f.name === name);
92
+ /** One of the base profile folders; they always exist. */
93
+ export function coreFolder(name) {
94
+ const folder = CORE_FOLDERS.find((f) => f.name === name);
95
+ if (!folder)
96
+ throw new Error(`Unknown core folder: ${name}`);
97
+ return folder;
98
+ }
99
+ export function findFolderByType(profile, alias) {
100
+ return profile.folders.find((f) => typeAlias(f.type) === alias.toLowerCase());
101
+ }
@@ -0,0 +1,10 @@
1
+ import type { Frontmatter, TrustTier, Verification } from './types.js';
2
+ export declare function verifiedList(data: Frontmatter | null): Verification[];
3
+ export declare function contentHash(data: Frontmatter | null, body: string): string;
4
+ export declare function validVerifications(data: Frontmatter | null, body: string): Verification[];
5
+ export declare function trustState(data: Frontmatter | null, body: string): {
6
+ tier: TrustTier;
7
+ outdated: boolean;
8
+ last: Verification | undefined;
9
+ };
10
+ export declare const countTodos: (body: string) => number;
@@ -0,0 +1,42 @@
1
+ import { createHash } from 'node:crypto';
2
+ // Trust signals of a page, derived from its frontmatter (OKF v0.2, section 5).
3
+ // `verified` may be a list or, as OKF allows, a single { by, at } mapping.
4
+ export function verifiedList(data) {
5
+ const v = data?.verified;
6
+ if (!v)
7
+ return [];
8
+ return (Array.isArray(v) ? v : [v]).filter((e) => !!e && typeof e === 'object');
9
+ }
10
+ const canonical = (value) => {
11
+ if (Array.isArray(value))
12
+ return value.map(canonical);
13
+ if (value && typeof value === 'object') {
14
+ const obj = value;
15
+ return Object.fromEntries(Object.keys(obj).sort().map((k) => [k, canonical(obj[k])]));
16
+ }
17
+ return value;
18
+ };
19
+ // Hash of what a reviewer read: the frontmatter (except verified and status,
20
+ // which do not change the content) and the body.
21
+ export function contentHash(data, body) {
22
+ const { verified: _verified, status: _status, ...rest } = data ?? {};
23
+ const text = `${JSON.stringify(canonical(rest))}\n${body.replace(/\r\n/g, '\n').trim()}`;
24
+ return createHash('sha256').update(text).digest('hex').slice(0, 16);
25
+ }
26
+ // A verification still counts when it has no hash (written by hand) or its
27
+ // hash matches the current content.
28
+ export function validVerifications(data, body) {
29
+ const current = contentHash(data, body);
30
+ return verifiedList(data).filter((e) => !e.hash || String(e.hash) === current);
31
+ }
32
+ // tier: unverified | machine-confirmed | human-reviewed
33
+ // outdated: the page was verified, but its content changed afterwards.
34
+ export function trustState(data, body) {
35
+ const all = verifiedList(data);
36
+ const valid = validVerifications(data, body);
37
+ if (!valid.length)
38
+ return { tier: 'unverified', outdated: all.length > 0, last: all[all.length - 1] };
39
+ const human = valid.some((e) => String(e.by ?? '').startsWith('human:'));
40
+ return { tier: human ? 'human-reviewed' : 'machine-confirmed', outdated: false, last: valid[valid.length - 1] };
41
+ }
42
+ export const countTodos = (body) => (body.match(/<!--\s*TODO/g) ?? []).length;
@@ -0,0 +1,61 @@
1
+ import type { WriteStatus } from './util.js';
2
+ export type Severity = 'error' | 'warning' | 'off';
3
+ /** A folder of the wiki: the base profile ones plus `extra_folders` from the profile. */
4
+ export interface Folder {
5
+ name: string;
6
+ type: string;
7
+ title: string;
8
+ description: string;
9
+ sections: string[];
10
+ core: boolean;
11
+ extra?: boolean;
12
+ }
13
+ export interface Profile {
14
+ config: Record<string, unknown>;
15
+ severity: Record<string, Severity>;
16
+ folders: Folder[];
17
+ }
18
+ /** Parsed YAML frontmatter. Its values come from user files, so they are `unknown`. */
19
+ export type Frontmatter = Record<string, unknown>;
20
+ export interface Page {
21
+ /** Path relative to the wiki root, with "/" separators. */
22
+ rel: string;
23
+ text: string;
24
+ /** null when the page has no parseable frontmatter. */
25
+ data: Frontmatter | null;
26
+ body: string;
27
+ /** First path segment, or null for a page at the wiki root. */
28
+ folder: string | null;
29
+ }
30
+ /** One entry of the `verified` list. Hand-written entries may omit `hash`. */
31
+ export interface Verification {
32
+ by?: unknown;
33
+ at?: unknown;
34
+ hash?: unknown;
35
+ }
36
+ export type TrustTier = 'unverified' | 'machine-confirmed' | 'human-reviewed';
37
+ export interface Issue {
38
+ rule: string;
39
+ severity: 'error' | 'warning';
40
+ file: string;
41
+ message: string;
42
+ fix: string;
43
+ }
44
+ export interface ReviewSummary {
45
+ pages: number;
46
+ deprecated: number;
47
+ 'human-reviewed': number;
48
+ 'machine-confirmed': number;
49
+ unverified: number;
50
+ outdated: number;
51
+ }
52
+ /** What every command returns: `text` for people, the rest for `--json`. */
53
+ export interface CommandResult {
54
+ text: string;
55
+ ok?: boolean;
56
+ [key: string]: unknown;
57
+ }
58
+ export interface AgentFileResult {
59
+ path: string;
60
+ status: WriteStatus;
61
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ /** Expected error: the message is printed and the process exits with code 1, no stack trace. */
2
+ export declare class UserError extends Error {
3
+ }
4
+ export declare const RESERVED: Set<string>;
5
+ export declare const KEBAB: RegExp;
6
+ export declare const toPosix: (p: string) => string;
7
+ export declare function writeText(file: string, text: string): void;
8
+ export declare function createText(file: string, text: string): void;
9
+ export declare function readIfExists(file: string): string | null;
10
+ export type WriteStatus = 'created' | 'updated' | 'unchanged';
11
+ export declare function writeWithStatus(file: string, text: string): WriteStatus;
12
+ export declare function humanize(slug: string): string;
13
+ export declare function isoNow(now?: Date): string;
14
+ export declare const yamlScalar: (value: unknown) => string;
15
+ export declare function assertKebabPath(rel: string): void;
16
+ /** Reads the `code` of a Node error without assuming the thrown value is an Error. */
17
+ export declare const errorCode: (e: unknown) => string | undefined;
@@ -0,0 +1,67 @@
1
+ import { mkdirSync, writeFileSync, readFileSync, renameSync, rmSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { stringify } from 'yaml';
4
+ /** Expected error: the message is printed and the process exits with code 1, no stack trace. */
5
+ export class UserError extends Error {
6
+ }
7
+ // Files the CLI owns inside every wiki folder; they are never pages.
8
+ export const RESERVED = new Set(['index.md', 'log.md']);
9
+ export const KEBAB = /^[a-z0-9]+(-[a-z0-9]+)*$/;
10
+ // Wiki paths always use "/", on Windows too.
11
+ export const toPosix = (p) => p.split('\\').join('/');
12
+ // Writes through a temp file and a rename, so a crash never leaves a truncated page.
13
+ export function writeText(file, text) {
14
+ mkdirSync(dirname(file), { recursive: true });
15
+ const tmp = `${file}.${process.pid}.tmp`;
16
+ try {
17
+ writeFileSync(tmp, text);
18
+ renameSync(tmp, file);
19
+ }
20
+ catch (err) {
21
+ rmSync(tmp, { force: true });
22
+ throw err;
23
+ }
24
+ }
25
+ // Creates a new file; fails with EEXIST instead of overwriting (no check-then-write race).
26
+ export function createText(file, text) {
27
+ mkdirSync(dirname(file), { recursive: true });
28
+ writeFileSync(file, text, { flag: 'wx' });
29
+ }
30
+ // Reads a text file, or null when it does not exist (or is a directory).
31
+ export function readIfExists(file) {
32
+ try {
33
+ return readFileSync(file, 'utf8');
34
+ }
35
+ catch (e) {
36
+ const code = errorCode(e);
37
+ if (code === 'ENOENT' || code === 'EISDIR')
38
+ return null;
39
+ throw e;
40
+ }
41
+ }
42
+ // Writes the file only when its content changes, and says what happened.
43
+ export function writeWithStatus(file, text) {
44
+ const current = readIfExists(file);
45
+ if (current === text)
46
+ return 'unchanged';
47
+ writeText(file, text);
48
+ return current === null ? 'created' : 'updated';
49
+ }
50
+ export function humanize(slug) {
51
+ const words = slug.replace(/-/g, ' ');
52
+ return words.charAt(0).toUpperCase() + words.slice(1);
53
+ }
54
+ export function isoNow(now = new Date()) {
55
+ return now.toISOString().replace(/\.\d{3}Z$/, 'Z');
56
+ }
57
+ // Single-line YAML scalar (quoted only when needed).
58
+ export const yamlScalar = (value) => stringify(value, { lineWidth: 0 }).trim();
59
+ export function assertKebabPath(rel) {
60
+ for (const s of rel.split('/')) {
61
+ if (!KEBAB.test(s)) {
62
+ throw new UserError(`"${s}" is not kebab-case (lowercase letters, digits and hyphens). Example: "state-management".`);
63
+ }
64
+ }
65
+ }
66
+ /** Reads the `code` of a Node error without assuming the thrown value is an Error. */
67
+ export const errorCode = (e) => typeof e === 'object' && e !== null && 'code' in e && typeof e.code === 'string' ? e.code : undefined;
@@ -0,0 +1,24 @@
1
+ import { type AgentName } from './targets.js';
2
+ import type { AgentFileResult, Profile } from '../core/types.js';
3
+ export declare const BLOCK_START = "<!-- okfcli:start -->";
4
+ export declare const BLOCK_END = "<!-- okfcli:end -->";
5
+ export declare function pointerBlock(wiki: string): string;
6
+ export declare function skillFiles({ wiki, profile, version }: {
7
+ wiki: string;
8
+ profile: Profile;
9
+ version: string;
10
+ }): {
11
+ path: string;
12
+ text: string;
13
+ }[];
14
+ export declare function upsertBlock(current: string | null, block: string, file: string): string;
15
+ export declare function pointerFile(projectRoot: string, candidates: readonly [string, ...string[]]): string;
16
+ interface WriteAgentFilesOptions {
17
+ projectRoot: string;
18
+ wiki: string;
19
+ profile: Profile;
20
+ version: string;
21
+ agents: AgentName[];
22
+ }
23
+ export declare function writeAgentFiles({ projectRoot, wiki, profile, version, agents }: WriteAgentFilesOptions): AgentFileResult[];
24
+ export {};