@freetesh/canon 0.0.0-stage → 0.16.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.
- package/.claude-plugin/marketplace.json +15 -0
- package/.claude-plugin/plugin.json +5 -0
- package/LICENSE +21 -0
- package/README.md +161 -2
- package/cli/dist/api.d.ts +3 -0
- package/cli/dist/api.js +4 -0
- package/cli/dist/check.d.ts +19 -0
- package/cli/dist/check.js +40 -0
- package/cli/dist/checks/architecture.d.ts +23 -0
- package/cli/dist/checks/architecture.js +135 -0
- package/cli/dist/checks/canonical-values.d.ts +2 -0
- package/cli/dist/checks/canonical-values.js +48 -0
- package/cli/dist/checks/commands.d.ts +7 -0
- package/cli/dist/checks/commands.js +33 -0
- package/cli/dist/checks/context-layout.d.ts +5 -0
- package/cli/dist/checks/context-layout.js +83 -0
- package/cli/dist/checks/context-size.d.ts +2 -0
- package/cli/dist/checks/context-size.js +46 -0
- package/cli/dist/checks/features.d.ts +2 -0
- package/cli/dist/checks/features.js +38 -0
- package/cli/dist/checks/index.d.ts +2 -0
- package/cli/dist/checks/index.js +34 -0
- package/cli/dist/checks/listing.d.ts +2 -0
- package/cli/dist/checks/listing.js +51 -0
- package/cli/dist/checks/metrics-definitions.d.ts +2 -0
- package/cli/dist/checks/metrics-definitions.js +14 -0
- package/cli/dist/checks/pipeline.d.ts +8 -0
- package/cli/dist/checks/pipeline.js +34 -0
- package/cli/dist/checks/refs.d.ts +2 -0
- package/cli/dist/checks/refs.js +92 -0
- package/cli/dist/checks/shared-files.d.ts +22 -0
- package/cli/dist/checks/shared-files.js +84 -0
- package/cli/dist/checks/sources.d.ts +2 -0
- package/cli/dist/checks/sources.js +40 -0
- package/cli/dist/checks/stale-copies.d.ts +2 -0
- package/cli/dist/checks/stale-copies.js +18 -0
- package/cli/dist/checks/standards-copies.d.ts +2 -0
- package/cli/dist/checks/standards-copies.js +57 -0
- package/cli/dist/checks/tasks.d.ts +2 -0
- package/cli/dist/checks/tasks.js +66 -0
- package/cli/dist/checks/types.d.ts +23 -0
- package/cli/dist/checks/types.js +1 -0
- package/cli/dist/checks/wiki-frontmatter.d.ts +3 -0
- package/cli/dist/checks/wiki-frontmatter.js +84 -0
- package/cli/dist/commands/add.d.ts +3 -0
- package/cli/dist/commands/add.js +24 -0
- package/cli/dist/commands/approve.d.ts +2 -0
- package/cli/dist/commands/approve.js +20 -0
- package/cli/dist/commands/check.d.ts +3 -0
- package/cli/dist/commands/check.js +19 -0
- package/cli/dist/commands/copies.d.ts +3 -0
- package/cli/dist/commands/copies.js +23 -0
- package/cli/dist/commands/doctor.d.ts +2 -0
- package/cli/dist/commands/doctor.js +16 -0
- package/cli/dist/commands/hooks.d.ts +6 -0
- package/cli/dist/commands/hooks.js +55 -0
- package/cli/dist/commands/init.d.ts +3 -0
- package/cli/dist/commands/init.js +78 -0
- package/cli/dist/commands/learn.d.ts +3 -0
- package/cli/dist/commands/learn.js +36 -0
- package/cli/dist/commands/mcp.d.ts +2 -0
- package/cli/dist/commands/mcp.js +11 -0
- package/cli/dist/commands/pipeline.d.ts +3 -0
- package/cli/dist/commands/pipeline.js +26 -0
- package/cli/dist/commands/search.d.ts +2 -0
- package/cli/dist/commands/search.js +13 -0
- package/cli/dist/commands/standards.d.ts +3 -0
- package/cli/dist/commands/standards.js +26 -0
- package/cli/dist/commands/status.d.ts +3 -0
- package/cli/dist/commands/status.js +11 -0
- package/cli/dist/commands/sync.d.ts +3 -0
- package/cli/dist/commands/sync.js +23 -0
- package/cli/dist/commands/ui.d.ts +3 -0
- package/cli/dist/commands/ui.js +64 -0
- package/cli/dist/commands/weigh.d.ts +2 -0
- package/cli/dist/commands/weigh.js +16 -0
- package/cli/dist/config/load.d.ts +23 -0
- package/cli/dist/config/load.js +116 -0
- package/cli/dist/config/schema.d.ts +200 -0
- package/cli/dist/config/schema.js +116 -0
- package/cli/dist/doctor.d.ts +12 -0
- package/cli/dist/doctor.js +31 -0
- package/cli/dist/format.d.ts +6 -0
- package/cli/dist/format.js +35 -0
- package/cli/dist/index.d.ts +2 -0
- package/cli/dist/index.js +48 -0
- package/cli/dist/init.d.ts +46 -0
- package/cli/dist/init.js +119 -0
- package/cli/dist/learn.d.ts +44 -0
- package/cli/dist/learn.js +80 -0
- package/cli/dist/lib/env.d.ts +7 -0
- package/cli/dist/lib/env.js +6 -0
- package/cli/dist/lib/files.d.ts +3 -0
- package/cli/dist/lib/files.js +23 -0
- package/cli/dist/lib/frontmatter.d.ts +6 -0
- package/cli/dist/lib/frontmatter.js +16 -0
- package/cli/dist/lib/qmd.d.ts +9 -0
- package/cli/dist/lib/qmd.js +13 -0
- package/cli/dist/lib/system.d.ts +4 -0
- package/cli/dist/lib/system.js +19 -0
- package/cli/dist/packs.d.ts +40 -0
- package/cli/dist/packs.js +85 -0
- package/cli/dist/pipeline.d.ts +20 -0
- package/cli/dist/pipeline.js +30 -0
- package/cli/dist/standards.d.ts +14 -0
- package/cli/dist/standards.js +42 -0
- package/cli/dist/status.d.ts +3 -0
- package/cli/dist/status.js +26 -0
- package/cli/dist/sync.d.ts +19 -0
- package/cli/dist/sync.js +30 -0
- package/cli/dist/ui.d.ts +15 -0
- package/cli/dist/ui.js +69 -0
- package/package.json +54 -4
- package/packs/expo/pack.yaml +5 -0
- package/packs/firebase/pack.yaml +5 -0
- package/packs/nextjs/pack.yaml +5 -0
- package/packs/react/pack.yaml +5 -0
- package/packs/revenuecat/pack.yaml +5 -0
- package/packs/uniwind/pack.yaml +5 -0
- package/scripts/log-verdict.mjs +56 -0
- package/skills/ask/SKILL.md +40 -0
- package/skills/decision/SKILL.md +36 -0
- package/skills/ingest/SKILL.md +45 -0
- package/skills/standards/SKILL.md +16 -0
- package/skills/weigh/SKILL.md +160 -0
- package/skills/wiki-lint/SKILL.md +67 -0
- package/standards/README.md +14 -0
- package/standards/agent-contract.md +47 -0
- package/standards/constitution.md +104 -0
- package/standards/context-layout.md +129 -0
- package/standards/dev-methods.md +29 -0
- package/standards/wiki-schema.md +71 -0
- package/templates/dependency-cruiser/app.cjs +23 -0
- package/templates/dependency-cruiser/base.cjs +52 -0
- package/templates/dependency-cruiser/package.cjs +31 -0
- package/templates/dependency-cruiser/root.cjs +15 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const NOT_STANDARDS = new Set(["README.md"]);
|
|
4
|
+
const PRINCIPLE = /^### (C-\d{2}) (.+)$/;
|
|
5
|
+
function markdownIn(dir) {
|
|
6
|
+
return readdirSync(dir).filter((file) => file.endsWith(".md") && !NOT_STANDARDS.has(file));
|
|
7
|
+
}
|
|
8
|
+
/** canon's default standards, then each configured folder in order; a later file of the same name replaces an earlier one. */
|
|
9
|
+
export function resolveStandards(canonRoot, folders) {
|
|
10
|
+
const byName = new Map();
|
|
11
|
+
const sources = [{ dir: join(canonRoot, "standards"), from: "canon" }, ...folders.map((dir) => ({ dir, from: dir }))];
|
|
12
|
+
for (const { dir, from } of sources) {
|
|
13
|
+
if (!existsSync(dir))
|
|
14
|
+
throw new Error(`standards folder not found: ${dir}`);
|
|
15
|
+
for (const file of markdownIn(dir)) {
|
|
16
|
+
const name = file.replace(/\.md$/, "");
|
|
17
|
+
// Constitutions add up (see constitutionTitles), so each one keeps its own entry.
|
|
18
|
+
const key = name === "constitution" ? `${name}:${from}` : name;
|
|
19
|
+
byName.set(key, { name, path: join(dir, file), from });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name) || Number(a.from !== "canon") - Number(b.from !== "canon"));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Principle headings from canon's constitution and every configured one. A
|
|
26
|
+
* constitution in a folder extends rather than replaces: it adds principles,
|
|
27
|
+
* and restating an ID rewords it.
|
|
28
|
+
*/
|
|
29
|
+
export function constitutionTitles(canonRoot, folders) {
|
|
30
|
+
const titles = new Map();
|
|
31
|
+
for (const dir of [join(canonRoot, "standards"), ...folders]) {
|
|
32
|
+
const file = join(dir, "constitution.md");
|
|
33
|
+
if (!existsSync(file))
|
|
34
|
+
continue;
|
|
35
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
36
|
+
const match = PRINCIPLE.exec(line);
|
|
37
|
+
if (match)
|
|
38
|
+
titles.set(match[1], `${match[1]} ${match[2]}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [...titles.values()];
|
|
42
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { loadConfig } from "./config/load.js";
|
|
4
|
+
import { runChecks } from "./check.js";
|
|
5
|
+
export function canonVersion(canonRoot) {
|
|
6
|
+
const manifest = join(canonRoot, ".claude-plugin", "plugin.json");
|
|
7
|
+
if (!existsSync(manifest))
|
|
8
|
+
return "unknown";
|
|
9
|
+
return JSON.parse(readFileSync(manifest, "utf8")).version ?? "unknown";
|
|
10
|
+
}
|
|
11
|
+
export function buildStatus(repo, options) {
|
|
12
|
+
const { config, sources } = loadConfig(repo, { estateRoot: options.estateRoot });
|
|
13
|
+
const report = runChecks(repo, options);
|
|
14
|
+
const errors = report.findings.filter((f) => f.level === "error").length;
|
|
15
|
+
const warnings = report.findings.length - errors;
|
|
16
|
+
const configLine = sources.length === 0
|
|
17
|
+
? "none — inferred; run `canon init`"
|
|
18
|
+
: sources.map((source) => `${source.layer} ${source.path}`).join("\n ");
|
|
19
|
+
return [
|
|
20
|
+
`canon ${canonVersion(options.canonRoot)} · ${report.name}`,
|
|
21
|
+
`kind ${config.kind}`,
|
|
22
|
+
`modules ${config.modules.join(", ")}`,
|
|
23
|
+
`config ${configLine}`,
|
|
24
|
+
`checks ${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"}`,
|
|
25
|
+
].join("\n");
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CommandRunner } from "./packs.ts";
|
|
2
|
+
export interface SyncOptions {
|
|
3
|
+
canonRoot: string;
|
|
4
|
+
estateRoot?: string;
|
|
5
|
+
cacheDir: string;
|
|
6
|
+
run: CommandRunner;
|
|
7
|
+
fetchText: (url: string) => Promise<string>;
|
|
8
|
+
}
|
|
9
|
+
export interface SyncResult {
|
|
10
|
+
packs: string[];
|
|
11
|
+
/** True when the managed architecture base was rewritten from canon's template. */
|
|
12
|
+
base: boolean;
|
|
13
|
+
version: {
|
|
14
|
+
from: string | undefined;
|
|
15
|
+
to: string;
|
|
16
|
+
} | null;
|
|
17
|
+
}
|
|
18
|
+
/** Refreshes a product's pack docs and search index, and moves its pinned canon version to the current one. */
|
|
19
|
+
export declare function syncRepo(repo: string, options: SyncOptions): Promise<SyncResult>;
|
package/cli/dist/sync.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { parseDocument } from "yaml";
|
|
4
|
+
import { loadConfig } from "./config/load.js";
|
|
5
|
+
import { indexKnowledge, refreshArchitectureBase } from "./init.js";
|
|
6
|
+
import { fetchPackDocs, findPack } from "./packs.js";
|
|
7
|
+
import { canonVersion } from "./status.js";
|
|
8
|
+
/** Refreshes a product's pack docs and search index, and moves its pinned canon version to the current one. */
|
|
9
|
+
export async function syncRepo(repo, options) {
|
|
10
|
+
const file = join(repo, ".claude", "canon.yaml");
|
|
11
|
+
if (!existsSync(file))
|
|
12
|
+
throw new Error(`${repo} has no .claude/canon.yaml — run \`canon init\` first`);
|
|
13
|
+
const { config } = loadConfig(repo, { estateRoot: options.estateRoot });
|
|
14
|
+
const packs = config.packs;
|
|
15
|
+
for (const name of packs) {
|
|
16
|
+
await fetchPackDocs(findPack(options.canonRoot, name), options);
|
|
17
|
+
}
|
|
18
|
+
if (config.modules.includes("knowledge"))
|
|
19
|
+
indexKnowledge(repo, basename(repo), options.run);
|
|
20
|
+
options.run("qmd", ["update"]);
|
|
21
|
+
options.run("qmd", ["embed"]);
|
|
22
|
+
const base = refreshArchitectureBase(repo, options.canonRoot);
|
|
23
|
+
const current = canonVersion(options.canonRoot);
|
|
24
|
+
if (config.canon === current)
|
|
25
|
+
return { packs, base, version: null };
|
|
26
|
+
const document = parseDocument(readFileSync(file, "utf8"));
|
|
27
|
+
document.set("canon", current);
|
|
28
|
+
writeFileSync(file, document.toString());
|
|
29
|
+
return { packs, base, version: { from: config.canon, to: current } };
|
|
30
|
+
}
|
package/cli/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface SiteOptions {
|
|
2
|
+
/** Quartz content folder: one symlink per product wiki, plus the home page. */
|
|
3
|
+
siteDir: string;
|
|
4
|
+
/** Static folder for the design galleries: one symlink per product designs/. */
|
|
5
|
+
designsDir: string;
|
|
6
|
+
designsUrl: string;
|
|
7
|
+
/** `canon check --all` output, shown as its own page. */
|
|
8
|
+
report: string;
|
|
9
|
+
/** A product's architecture as markdown with mermaid graphs, or undefined when it has none. */
|
|
10
|
+
graph?: (repo: string) => string | undefined;
|
|
11
|
+
}
|
|
12
|
+
/** Lays out the content Quartz renders and the folder the design server serves. */
|
|
13
|
+
export declare function buildSite(products: string[], options: SiteOptions): void;
|
|
14
|
+
/** Quartz's default config, adjusted for a private local site: no analytics, no schema files as pages. */
|
|
15
|
+
export declare function quartzConfig(defaultsYaml: string): string;
|
package/cli/dist/ui.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { parseDocument } from "yaml";
|
|
4
|
+
const SURFACES = ["mobile", "web"];
|
|
5
|
+
function relink(target, link) {
|
|
6
|
+
rmSync(link, { force: true, recursive: false });
|
|
7
|
+
symlinkSync(target, link);
|
|
8
|
+
}
|
|
9
|
+
// routes.js comes in two styles (`architecture: "…"` and `"architecture": "…"`);
|
|
10
|
+
// a regex reads both without executing the manifest.
|
|
11
|
+
const ARCHITECTURE_KEY = /["']?architecture["']?\s*:\s*["']([^"']+)["']/;
|
|
12
|
+
/** The views a drawn surface offers — flow chart, screenshots, architecture — as relative paths that exist. */
|
|
13
|
+
function surfaceViews(designs, surface) {
|
|
14
|
+
const dir = join(designs, surface);
|
|
15
|
+
if (!existsSync(join(dir, "index.html")))
|
|
16
|
+
return [];
|
|
17
|
+
const views = [["flow chart", "index.html"]];
|
|
18
|
+
if (existsSync(join(dir, "screenshots.html")))
|
|
19
|
+
views.push(["screenshots", "screenshots.html"]);
|
|
20
|
+
const routes = join(dir, "routes.js");
|
|
21
|
+
const architecture = existsSync(routes) ? ARCHITECTURE_KEY.exec(readFileSync(routes, "utf8"))?.[1] : undefined;
|
|
22
|
+
if (architecture && existsSync(join(dir, architecture)))
|
|
23
|
+
views.push(["architecture", architecture]);
|
|
24
|
+
return views.map(([label, path]) => [label, `${surface}/${path}`]);
|
|
25
|
+
}
|
|
26
|
+
/** Lays out the content Quartz renders and the folder the design server serves. */
|
|
27
|
+
export function buildSite(products, options) {
|
|
28
|
+
mkdirSync(options.siteDir, { recursive: true });
|
|
29
|
+
mkdirSync(options.designsDir, { recursive: true });
|
|
30
|
+
const rows = [];
|
|
31
|
+
for (const product of products) {
|
|
32
|
+
const name = basename(product);
|
|
33
|
+
const wiki = join(product, ".context", "wiki");
|
|
34
|
+
const designs = join(product, ".context", "designs");
|
|
35
|
+
const links = [];
|
|
36
|
+
if (existsSync(wiki)) {
|
|
37
|
+
relink(wiki, join(options.siteDir, name));
|
|
38
|
+
links.push(`[[${name}/index|${name}]]`);
|
|
39
|
+
}
|
|
40
|
+
const graph = options.graph?.(product);
|
|
41
|
+
if (graph) {
|
|
42
|
+
mkdirSync(join(options.siteDir, "architecture"), { recursive: true });
|
|
43
|
+
writeFileSync(join(options.siteDir, "architecture", `${name}.md`), `---\ntitle: ${name} architecture\n---\n\nImports between parts of ${name}, from its \`.dependency-cruiser.cjs\`. Broken rules fail \`canon check\`.\n\n${graph}\n`);
|
|
44
|
+
links.push(`[[architecture/${name}|architecture graph]]`);
|
|
45
|
+
}
|
|
46
|
+
const surfaces = SURFACES.map((surface) => [surface, surfaceViews(designs, surface)]).filter(([, views]) => views.length > 0);
|
|
47
|
+
if (surfaces.length > 0)
|
|
48
|
+
relink(designs, join(options.designsDir, name));
|
|
49
|
+
if (links.length > 0 || surfaces.length > 0)
|
|
50
|
+
rows.push(`- ${links.join(" · ") || name}`);
|
|
51
|
+
for (const [surface, views] of surfaces) {
|
|
52
|
+
const linked = views.map(([label, path]) => `[${label}](${options.designsUrl}/${name}/${path})`);
|
|
53
|
+
rows.push(` - ${surface}: ${linked.join(" · ")}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
writeFileSync(join(options.siteDir, "index.md"), `---\ntitle: canon\n---\n\nEvery product's wiki, searchable and linked. Each surface opens its flow chart, screenshots and architecture overview; products with architecture rules also get an import graph.\n\n${rows.join("\n")}\n\n[[canon-check|Standards check]]\n`);
|
|
57
|
+
writeFileSync(join(options.siteDir, "canon-check.md"), `---\ntitle: Standards check\n---\n\nOutput of \`canon check --all\` when this site was built.\n\n\`\`\`\n${options.report}\n\`\`\`\n`);
|
|
58
|
+
}
|
|
59
|
+
const IGNORED = ["**/CLAUDE.md", "_designs"];
|
|
60
|
+
/** Quartz's default config, adjusted for a private local site: no analytics, no schema files as pages. */
|
|
61
|
+
export function quartzConfig(defaultsYaml) {
|
|
62
|
+
const document = parseDocument(defaultsYaml);
|
|
63
|
+
document.setIn(["configuration", "pageTitle"], "canon");
|
|
64
|
+
document.setIn(["configuration", "analytics"], null);
|
|
65
|
+
document.setIn(["configuration", "baseUrl"], "localhost");
|
|
66
|
+
const current = document.getIn(["configuration", "ignorePatterns"])?.toJSON() ?? [];
|
|
67
|
+
document.setIn(["configuration", "ignorePatterns"], [...current, ...IGNORED.filter((p) => !current.includes(p))]);
|
|
68
|
+
return document.toString();
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@freetesh/canon",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.16.0",
|
|
4
|
+
"description": "canon CLI: keep your domain knowledge and AI rules in your repo, and enforce them.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Ritesh Firodiya",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ritesh-firodiya/canon-cli.git"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
"./api": "./cli/dist/api.js"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"canon": "cli/dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"cli/dist",
|
|
20
|
+
"standards",
|
|
21
|
+
"skills",
|
|
22
|
+
"packs",
|
|
23
|
+
"templates",
|
|
24
|
+
"scripts/log-verdict.mjs",
|
|
25
|
+
".claude-plugin"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22"
|
|
29
|
+
},
|
|
30
|
+
"packageManager": "pnpm@12.3.4",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"canon": "node cli/src/index.ts",
|
|
33
|
+
"check": "node cli/src/index.ts check . && pnpm typecheck && pnpm test",
|
|
34
|
+
"build": "rm -rf cli/dist && tsc -p cli/tsconfig.build.json",
|
|
35
|
+
"test": "vitest run --root cli",
|
|
36
|
+
"coverage": "vitest run --root cli --coverage",
|
|
37
|
+
"typecheck": "tsc -p cli",
|
|
38
|
+
"prepack": "pnpm build"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@clack/prompts": "^1.8.1",
|
|
42
|
+
"commander": "^15.0.0",
|
|
43
|
+
"yaml": "^2.9.1",
|
|
44
|
+
"zod": "^4.6.7"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^26.6.5",
|
|
48
|
+
"@vitest/coverage-v8": "^5.0.5",
|
|
49
|
+
"dependency-cruiser": "18.7.0",
|
|
50
|
+
"typescript": "^7.0.2",
|
|
51
|
+
"vitest": "^5.0.5"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Appends one verdict, and what happened next, to .context/raw/canon/verdicts.jsonl
|
|
3
|
+
// in the current repo. Append-only: raw/ is immutable, and `canon learn` reads
|
|
4
|
+
// the history to show how often the gate stops work and to propose rule
|
|
5
|
+
// changes from overrides, so earlier lines must never be rewritten.
|
|
6
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { parseArgs } from "node:util";
|
|
9
|
+
|
|
10
|
+
const VERDICTS = ["do-it", "do-it-differently", "push-back"];
|
|
11
|
+
const OUTCOMES = ["proceeded", "stopped", "overridden", "dropped"];
|
|
12
|
+
const PRINCIPLE_ID = /^C-\d{2}$/;
|
|
13
|
+
|
|
14
|
+
const { values } = parseArgs({
|
|
15
|
+
options: {
|
|
16
|
+
verdict: { type: "string" },
|
|
17
|
+
principles: { type: "string", default: "" },
|
|
18
|
+
request: { type: "string" },
|
|
19
|
+
reason: { type: "string", default: "" },
|
|
20
|
+
outcome: { type: "string", default: "overridden" },
|
|
21
|
+
// One per term the user agreed to, verbatim. Repeat the flag for each.
|
|
22
|
+
term: { type: "string", multiple: true, default: [] },
|
|
23
|
+
repo: { type: "string", default: process.cwd() },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function fail(message) {
|
|
28
|
+
console.error(`log-verdict: ${message}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!VERDICTS.includes(values.verdict)) fail(`--verdict must be one of ${VERDICTS.join(", ")}`);
|
|
33
|
+
if (!values.request?.trim()) fail("--request is required");
|
|
34
|
+
if (!OUTCOMES.includes(values.outcome)) fail(`--outcome must be one of ${OUTCOMES.join(", ")}`);
|
|
35
|
+
if (values.outcome === "overridden" && !values.reason.trim()) fail("--reason is required: an override without a reason teaches nothing");
|
|
36
|
+
|
|
37
|
+
const principles = values.principles.split(",").map((id) => id.trim()).filter(Boolean);
|
|
38
|
+
const invalid = principles.filter((id) => !PRINCIPLE_ID.test(id));
|
|
39
|
+
if (invalid.length > 0) fail(`not principle IDs: ${invalid.join(", ")}`);
|
|
40
|
+
|
|
41
|
+
const terms = values.term.map((term) => term.trim()).filter(Boolean);
|
|
42
|
+
|
|
43
|
+
const entry = {
|
|
44
|
+
at: new Date().toISOString(),
|
|
45
|
+
verdict: values.verdict,
|
|
46
|
+
principles,
|
|
47
|
+
request: values.request.trim(),
|
|
48
|
+
reason: values.reason.trim(),
|
|
49
|
+
outcome: values.outcome,
|
|
50
|
+
...(terms.length > 0 && { terms }),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const dir = join(values.repo, ".context", "raw", "canon");
|
|
54
|
+
mkdirSync(dir, { recursive: true });
|
|
55
|
+
appendFileSync(join(dir, "verdicts.jsonl"), `${JSON.stringify(entry)}\n`);
|
|
56
|
+
console.log(`logged to ${join(dir, "verdicts.jsonl")}`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ask
|
|
3
|
+
description: Answer a question from the wiki only. Searches .context/wiki/ pages and returns a cited answer with [[wikilinks]] to every page used. Never reads .context/raw/ directly — synthesis must be written down. Usage `/ask <question>`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are answering a question using ONLY `.context/wiki/` as the knowledge source.
|
|
7
|
+
|
|
8
|
+
Question: `$ARGUMENTS`
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. **Search first.** If the `qmd` MCP tool is available, run a `query` for the question against this product's wiki collection (`<product>-wiki`). Its hits are the first candidates.
|
|
13
|
+
|
|
14
|
+
2. **Read `.context/wiki/index.md`** and **identify candidate pages** by matching the question against page titles, IDs, and the index entries. Prefer:
|
|
15
|
+
- `entity` pages for "what is the value / price / name of X"
|
|
16
|
+
- `synthesis` pages for cross-cutting summaries (use these before chasing individual entities)
|
|
17
|
+
- `concept` pages for "why" or "what does X mean" questions
|
|
18
|
+
- `decision` pages for "why did we choose X over Y", and its `Rejected:` line for "did we consider Y"
|
|
19
|
+
- for "what does this screen do", the HTML in `.context/designs/` is the spec; its `surfaces/` note, if any, says why
|
|
20
|
+
|
|
21
|
+
3. **Read the candidate pages.** Follow `[[wikilinks]]` one hop if the answer is incomplete. Stop at two hops — if you need three, the wiki is missing a synthesis page; say so.
|
|
22
|
+
|
|
23
|
+
4. **Answer the question** in the product's own voice:
|
|
24
|
+
- Follow the copy rules in the root `CLAUDE.md` — plain words, short sentences, active voice.
|
|
25
|
+
- Write numbers, prices and dates the way the product writes them to a user.
|
|
26
|
+
- Cite every claim with a `[[wikilink]]` to the source page.
|
|
27
|
+
- If two pages disagree, surface the contradiction explicitly — don't pick a winner. `/wiki-lint` will flag it; the user resolves it via `/decision`.
|
|
28
|
+
|
|
29
|
+
5. **If the wiki cannot answer:**
|
|
30
|
+
- Say so plainly.
|
|
31
|
+
- List the pages you checked.
|
|
32
|
+
- Suggest what's missing: "no entity page exists for `<thing>`", "no synthesis page combines `<X>` and `<Y>`", etc.
|
|
33
|
+
- Recommend the user run `/ingest <raw-source>` to fill the gap.
|
|
34
|
+
|
|
35
|
+
## Rules
|
|
36
|
+
|
|
37
|
+
- **Never read `.context/raw/` from this skill.** If the answer requires raw inspection, recommend `/ingest`. Designs may be read for screen questions.
|
|
38
|
+
- **Never invent facts.** If a canonical value isn't in an `entity` page, say "not in the wiki".
|
|
39
|
+
- **Every cited page must actually exist.** Don't fabricate a `[[wikilink]]` to make the answer look authoritative.
|
|
40
|
+
- A page with `last-verified` > 90 days is stale — flag it in the answer ("last verified 2025-11-04 — may be out of date").
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: decision
|
|
3
|
+
description: Record or change a product or technical decision in .context/wiki/decisions/<topic>.md — one file per topic, overwritten when the choice changes. Usage `/decision <topic> — <choice and why>`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are recording a decision in `.context/wiki/decisions/<topic>.md`.
|
|
7
|
+
|
|
8
|
+
Decision: `$ARGUMENTS`
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. Split `$ARGUMENTS` on the first ` — `: left = topic, right = the choice and why. With no ` — `, ask for the choice.
|
|
13
|
+
2. Slug = the topic in kebab-case, no date (`pricing`, `auth-provider`). Look for an existing file on the same topic first, under any name. If there is one, it is the file to change.
|
|
14
|
+
3. Write the file, at most 30 lines:
|
|
15
|
+
|
|
16
|
+
```markdown
|
|
17
|
+
---
|
|
18
|
+
last-verified: <today>
|
|
19
|
+
---
|
|
20
|
+
# <Topic>
|
|
21
|
+
**Choice:** <one or two lines; link values to their entity page>
|
|
22
|
+
**Why:** <the trigger: data, incident, user feedback, cost>
|
|
23
|
+
**Rejected:** <option (why)> · <option (why)>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
When changing an existing decision, replace the choice and why, and move the
|
|
27
|
+
old choice into `Rejected:` with the reason it was dropped. Keep the rest of `Rejected:`.
|
|
28
|
+
4. Put a price, SKU or limit on its entity page, not here (`canonical-values` checks this).
|
|
29
|
+
5. Add or update the one line in `.context/wiki/index.md`.
|
|
30
|
+
6. Confirm: `decision recorded: <topic> → <path>`.
|
|
31
|
+
|
|
32
|
+
## Rules
|
|
33
|
+
|
|
34
|
+
- One file per topic, overwritten. Git keeps the old text; never add a dated or `-2` file.
|
|
35
|
+
- No fix lists, audit results or step-by-step history. Those go in the commit message or `CHANGELOG.md`.
|
|
36
|
+
- If the user's text is long, keep the choice, the reason and what was rejected, and drop the rest. Show what you dropped.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ingest
|
|
3
|
+
description: Turn a raw source (wireframe, meeting note, research doc) — or, with no argument, what this session taught — into wiki pages that pass the admission test, and update .context/wiki/index.md. Usage `/ingest [path-to-raw-file]`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are ingesting a raw source into the wiki at `.context/wiki/`.
|
|
7
|
+
|
|
8
|
+
Source to ingest: `$ARGUMENTS`
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. **Read the wiki schema (the `wiki-schema.md` that `canon standards` lists — a team's own replaces canon's; without the CLI, `../../standards/wiki-schema.md` relative to this skill) first.** It defines the folders, the one test for what belongs, and the caps. Every page you write must conform.
|
|
13
|
+
|
|
14
|
+
2. **Read the raw source** at the path in `$ARGUMENTS`. Accepted: anything under `.context/raw/` or `.context/designs/`, and application source in this repository. Refuse paths outside the repo.
|
|
15
|
+
With no argument, the source is this session: list what it established that a later agent would get wrong without (a rule, a value, a choice and what was rejected). Search the wiki first and update a page rather than add one. Show the proposed changes and stop; write only when the user says so.
|
|
16
|
+
|
|
17
|
+
3. **Classify** the source into one or more wiki page types:
|
|
18
|
+
- HTML wireframe → nothing; the HTML is the spec. Only a constraint it can't show becomes that screen's `surfaces/<slug>.md` note.
|
|
19
|
+
- Decision note → the `decisions/<topic>.md` file, via the `decision` skill's format
|
|
20
|
+
- Research doc / interview → typically `concept` pages (split if it covers multiple ideas), or one `synthesis` page for a brief that draws a conclusion
|
|
21
|
+
- A new tier / product / canonical value → `entity` page
|
|
22
|
+
- A user journey description → a concept, only if it holds a rule the designs don't show
|
|
23
|
+
- Write nothing an agent could read from the code, the designs or git.
|
|
24
|
+
- Don't force one source into one page — a meeting note can produce multiple pages.
|
|
25
|
+
- Folders are the four in the schema plus any listed under `wiki.types` in the repo's or the estate's `.claude/canon.yaml`, each with its folder and its required `sections` (use its `sections` headings). Use an added type only when the source fits it.
|
|
26
|
+
|
|
27
|
+
4. **For each page you will create:**
|
|
28
|
+
- Filename: the topic, lowercase kebab-case, no date.
|
|
29
|
+
- Check whether a page on the topic already exists. If yes, edit it in place and refresh `last-verified` to today. Keep any `id`.
|
|
30
|
+
- Write only sections with something to say. No placeholders. Stay under the caps (page 60 lines).
|
|
31
|
+
- Link related pages with `[[slug]]` where it helps a reader. List the source path under `Sources`.
|
|
32
|
+
|
|
33
|
+
5. **Update `.context/wiki/index.md`:** one line for the new page, under its folder heading, alphabetical. Don't reformat existing entries.
|
|
34
|
+
|
|
35
|
+
6. **Leave the source where it is.** `.context/raw/` is immutable: a source is "processed" when a wiki page lists it under `Sources`, not when it moves.
|
|
36
|
+
|
|
37
|
+
7. **Report**: list each page created or updated with its ID and path. Flag any `[[wikilinks]]` to non-existent pages as "TODO targets".
|
|
38
|
+
|
|
39
|
+
## Rules
|
|
40
|
+
|
|
41
|
+
- Never edit anything under `.context/raw/` or `.context/designs/`.
|
|
42
|
+
- Never delete or rename an existing wiki page from this skill.
|
|
43
|
+
- Never invent canonical values. If the source has a price/string/enum, copy it verbatim; if it's ambiguous, ask the user before writing.
|
|
44
|
+
- Follow the copy rules from the root `CLAUDE.md` for any user-facing strings you transcribe.
|
|
45
|
+
- Today's date is in the session context — use it for `last-verified`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: standards
|
|
3
|
+
description: Load the standards that cover the work before doing it — the repo's design rules, code standards, .context layout, wiki schema, agent contract, and any team-specific rules. Use before creating or changing designs, code, .context/ files, wiki pages, store listings, or anything a team rule might govern, and when asked "what are the rules for…".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Load the standards that apply
|
|
7
|
+
|
|
8
|
+
1. List them: run `canon standards` in the repo. It prints every standard that
|
|
9
|
+
applies — canon's defaults, then the team's and the repo's own (a team file
|
|
10
|
+
with the same name replaces canon's). If the `canon` CLI is not installed, use
|
|
11
|
+
the files in `../../standards/` (relative to this skill).
|
|
12
|
+
2. Read only the ones that cover this work: designs, code, `.context/`, wiki,
|
|
13
|
+
listing, secrets — judge from the file names, then their first lines.
|
|
14
|
+
3. Follow them. Where two disagree, the more specific one (the repo's, then the
|
|
15
|
+
team's, then canon's) wins; say which one you followed.
|
|
16
|
+
4. Before calling the work done, run `canon check` and report its result.
|