@freetesh/canon 0.16.0 → 0.17.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +8 -6
- package/cli/dist/checks/designs.d.ts +5 -0
- package/cli/dist/checks/designs.js +33 -0
- package/cli/dist/checks/index.js +2 -0
- package/cli/dist/config/load.js +2 -2
- package/cli/dist/config/schema.d.ts +3 -1
- package/cli/dist/config/schema.js +5 -1
- package/cli/dist/standards.d.ts +11 -6
- package/cli/dist/standards.js +37 -17
- package/package.json +2 -1
- package/scripts/check-designs.py +709 -0
- package/standards/README.md +23 -6
- package/standards/agent-contract.md +21 -6
- package/standards/context-layout.md +41 -6
- package/standards/library/design-sets.md +857 -0
- package/standards/library/expo.md +54 -0
- package/standards/library/frontend.md +44 -0
- package/standards/library/performance.md +45 -0
- package/standards/library/pipeline.md +42 -0
- package/standards/library/secrets.md +37 -0
- package/standards/library/store-listing.md +122 -0
package/README.md
CHANGED
|
@@ -47,9 +47,11 @@ is a separate paid product built on this CLI.
|
|
|
47
47
|
listing tree, dead `.context/` paths, prices with no canonical home, shared
|
|
48
48
|
files, the import architecture, pipeline gates, and every checker your config
|
|
49
49
|
declares. `canon hooks install` makes every push run it.
|
|
50
|
-
- **Standards with one home.** canon ships defaults
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
- **Standards with one home.** canon ships defaults every repo follows, and a
|
|
51
|
+
library a repo imports by name (`standards: [design-sets, pipeline]`): design
|
|
52
|
+
sets, the build pipeline, Expo, frontend, performance, secrets, store listings.
|
|
53
|
+
A repo's own rules can sit in a folder it adds. `canon standards` shows what
|
|
54
|
+
applies.
|
|
53
55
|
- **A repo names only what it uses.** Modules, packs and shared-file sets are
|
|
54
56
|
opt-in; there is no skip list and no setting that turns a check off.
|
|
55
57
|
|
|
@@ -97,9 +99,8 @@ repo whose `canon.yaml` says `estate: true`, or `CANON_ESTATE`.
|
|
|
97
99
|
```yaml
|
|
98
100
|
# <estate>/.claude/canon.yaml — shared by every repo below it
|
|
99
101
|
estate: true
|
|
100
|
-
standards: [.canon/standards] # estate rules; same name replaces a canon default
|
|
101
102
|
commands: # your own checkers, run by `canon check`
|
|
102
|
-
- { id:
|
|
103
|
+
- { id: lint-copy, run: ./scripts/lint-copy.sh, if: .context/designs, module: design }
|
|
103
104
|
- { id: designs-first, run: test -d .context/designs, kinds: [mobile, web] } # only repos that ship screens
|
|
104
105
|
copies: # files products copy from one source repo, in named sets
|
|
105
106
|
source: design-system
|
|
@@ -111,8 +112,9 @@ copies: # files products copy from one source repo,
|
|
|
111
112
|
|
|
112
113
|
```yaml
|
|
113
114
|
# <repo>/.claude/canon.yaml — written by `canon init`
|
|
114
|
-
canon: 0.
|
|
115
|
+
canon: 0.17.0
|
|
115
116
|
kind: mobile # mobile | web | mobile-and-web | package | content
|
|
117
|
+
standards: [design-sets, pipeline, expo] # canon's library standards this repo follows; ./rules adds a folder of its own
|
|
116
118
|
modules: [docs, knowledge, code, listing, refs]
|
|
117
119
|
packs: [expo]
|
|
118
120
|
name: Acme # how the product is written; `named` copies swap it in
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Check } from "./types.ts";
|
|
2
|
+
/** Every drawn surface: a folder under .context/designs/ with a routes.js manifest. */
|
|
3
|
+
export declare function designSurfaces(repo: string): string[];
|
|
4
|
+
/** Runs canon's design-set checker (scripts/check-designs.py) once per drawn surface. */
|
|
5
|
+
export declare const designs: Check;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const DESIGNS = ".context/designs";
|
|
5
|
+
const MAX_OUTPUT_LINES = 15;
|
|
6
|
+
/** Every drawn surface: a folder under .context/designs/ with a routes.js manifest. */
|
|
7
|
+
export function designSurfaces(repo) {
|
|
8
|
+
const root = join(repo, DESIGNS);
|
|
9
|
+
if (!existsSync(root))
|
|
10
|
+
return [];
|
|
11
|
+
return readdirSync(root, { withFileTypes: true })
|
|
12
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(root, entry.name, "routes.js")))
|
|
13
|
+
.map((entry) => `${DESIGNS}/${entry.name}`)
|
|
14
|
+
.sort();
|
|
15
|
+
}
|
|
16
|
+
/** Runs canon's design-set checker (scripts/check-designs.py) once per drawn surface. */
|
|
17
|
+
export const designs = {
|
|
18
|
+
id: "designs",
|
|
19
|
+
module: "design",
|
|
20
|
+
description: "every drawn surface follows the design-sets standard (scripts/check-designs.py)",
|
|
21
|
+
run({ repo, canonRoot }) {
|
|
22
|
+
const script = join(canonRoot, "scripts", "check-designs.py");
|
|
23
|
+
return designSurfaces(repo).flatMap((surface) => {
|
|
24
|
+
const result = spawnSync("python3", [script, surface], { cwd: repo, encoding: "utf8" });
|
|
25
|
+
if (result.error)
|
|
26
|
+
return [{ check: "designs", level: "error", file: surface, message: `could not run python3: ${result.error.message}` }];
|
|
27
|
+
if (result.status === 0)
|
|
28
|
+
return [];
|
|
29
|
+
const output = `${result.stdout}\n${result.stderr}`.trim().split("\n").slice(-MAX_OUTPUT_LINES).join("\n");
|
|
30
|
+
return [{ check: "designs", level: "error", file: surface, message: output }];
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
};
|
package/cli/dist/checks/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { contextSize } from "./context-size.js";
|
|
|
5
5
|
import { staleCopies } from "./stale-copies.js";
|
|
6
6
|
import { standardsCopies } from "./standards-copies.js";
|
|
7
7
|
import { commandChecks } from "./commands.js";
|
|
8
|
+
import { designs } from "./designs.js";
|
|
8
9
|
import { features } from "./features.js";
|
|
9
10
|
import { listing } from "./listing.js";
|
|
10
11
|
import { metricsDefinitions } from "./metrics-definitions.js";
|
|
@@ -30,5 +31,6 @@ export const ALL_CHECKS = [
|
|
|
30
31
|
metricsDefinitions,
|
|
31
32
|
sources,
|
|
32
33
|
pipeline,
|
|
34
|
+
designs,
|
|
33
35
|
commandChecks,
|
|
34
36
|
];
|
package/cli/dist/config/load.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { parse } from "yaml";
|
|
4
|
-
import { MODULES_BY_KIND, layerSchema } from "./schema.js";
|
|
4
|
+
import { isStandardName, MODULES_BY_KIND, layerSchema } from "./schema.js";
|
|
5
5
|
const EXPO_MARKERS = ["app.config.ts", "app.config.js", "app.json"];
|
|
6
6
|
const WEB_MARKERS = ["next.config.ts", "next.config.js", "next.config.mjs", "vite.config.ts"];
|
|
7
7
|
function configPath(folder) {
|
|
@@ -94,7 +94,7 @@ export function loadConfig(repo, options = {}) {
|
|
|
94
94
|
modules: lastDefined(pick("modules")) ?? inferModules(repo, kind),
|
|
95
95
|
expo: Object.assign({}, ...pick("expo")),
|
|
96
96
|
packs: [...new Set(pick("packs").flatMap((packs) => packs ?? []))].sort(),
|
|
97
|
-
standards: [...new Set(layers.flatMap((layer) => (layer.data.standards ?? []).map((
|
|
97
|
+
standards: [...new Set(layers.flatMap((layer) => (layer.data.standards ?? []).map((entry) => (isStandardName(entry) ? entry : resolve(layer.base, entry)))))],
|
|
98
98
|
commands: mergeBy(pick("commands").map((list) => list ?? []), (command) => command.id),
|
|
99
99
|
doctor: { tools: mergeBy(pick("doctor").map((doctor) => doctor?.tools ?? []), (tool) => tool.tool) },
|
|
100
100
|
weigh: { weights: Object.assign({}, ...pick("weigh").map((weigh) => weigh?.weights)) },
|
|
@@ -11,6 +11,8 @@ export declare const WIKI_FOLDER_BY_TYPE: Record<string, string>;
|
|
|
11
11
|
export declare const REMOVED_WIKI_FOLDERS: Record<string, string>;
|
|
12
12
|
/** The `.context/` folders every repo may have. A repo adds more under `context.folders`. */
|
|
13
13
|
export declare const CONTEXT_FOLDERS: string[];
|
|
14
|
+
/** A `standards:` entry naming a library standard rather than a folder path. */
|
|
15
|
+
export declare function isStandardName(entry: string): boolean;
|
|
14
16
|
export type Kind = (typeof KINDS)[number];
|
|
15
17
|
export type Module = (typeof MODULES)[number];
|
|
16
18
|
declare const command: z.ZodObject<{
|
|
@@ -167,7 +169,7 @@ export interface Config {
|
|
|
167
169
|
sdk?: number;
|
|
168
170
|
};
|
|
169
171
|
packs: string[];
|
|
170
|
-
/**
|
|
172
|
+
/** Library standard names and absolute folders, estate first; a later file of the same name wins. */
|
|
171
173
|
standards: string[];
|
|
172
174
|
commands: CommandSpec[];
|
|
173
175
|
doctor: {
|
|
@@ -19,6 +19,10 @@ export const REMOVED_WIKI_FOLDERS = {
|
|
|
19
19
|
};
|
|
20
20
|
/** The `.context/` folders every repo may have. A repo adds more under `context.folders`. */
|
|
21
21
|
export const CONTEXT_FOLDERS = ["raw", "wiki", "features", "designs", "listing", "marketing", "metrics", "support", "legal", "ops"];
|
|
22
|
+
/** A `standards:` entry naming a library standard rather than a folder path. */
|
|
23
|
+
export function isStandardName(entry) {
|
|
24
|
+
return /^[a-z0-9-]+$/.test(entry);
|
|
25
|
+
}
|
|
22
26
|
const principleId = z.string().regex(/^C-\d{2}$/, "principle IDs look like C-07");
|
|
23
27
|
const command = z
|
|
24
28
|
.object({
|
|
@@ -83,7 +87,7 @@ export const layerSchema = z
|
|
|
83
87
|
modules: z.array(z.enum(MODULES)),
|
|
84
88
|
expo: z.object({ sdk: z.number().int().positive() }).partial(),
|
|
85
89
|
packs: z.array(z.string().regex(/^[a-z0-9-]+$/, "pack names are lowercase-kebab")),
|
|
86
|
-
/**
|
|
90
|
+
/** Standards to add: a bare name imports canon's library standard (`design-sets`); a path (`./rules`) is a folder of the repo's own. */
|
|
87
91
|
standards: z.array(z.string()),
|
|
88
92
|
commands: z.array(command),
|
|
89
93
|
doctor: z.object({ tools: z.array(tool) }).partial(),
|
package/cli/dist/standards.d.ts
CHANGED
|
@@ -4,11 +4,16 @@ export interface Standard {
|
|
|
4
4
|
/** "canon" for a built-in default, else the configured folder it came from. */
|
|
5
5
|
from: string;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
8
|
-
export declare function
|
|
7
|
+
/** The opt-in standards canon ships; a repo imports one by name under `standards:`. */
|
|
8
|
+
export declare function libraryStandards(canonRoot: string): string[];
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* and restating an ID rewords it.
|
|
10
|
+
* canon's defaults, then each `standards:` entry in order: a name imports canon's library
|
|
11
|
+
* standard, a folder adds its files. A later file of the same name replaces an earlier one.
|
|
13
12
|
*/
|
|
14
|
-
export declare function
|
|
13
|
+
export declare function resolveStandards(canonRoot: string, entries: string[]): Standard[];
|
|
14
|
+
/**
|
|
15
|
+
* Principle headings from canon's constitution, every imported library standard
|
|
16
|
+
* and every configured folder's constitution. They extend rather than replace:
|
|
17
|
+
* each adds principles, and restating an ID rewords it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function constitutionTitles(canonRoot: string, entries: string[]): string[];
|
package/cli/dist/standards.js
CHANGED
|
@@ -1,35 +1,55 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { isStandardName } from "./config/schema.js";
|
|
3
4
|
const NOT_STANDARDS = new Set(["README.md"]);
|
|
4
5
|
const PRINCIPLE = /^### (C-\d{2}) (.+)$/;
|
|
5
6
|
function markdownIn(dir) {
|
|
6
7
|
return readdirSync(dir).filter((file) => file.endsWith(".md") && !NOT_STANDARDS.has(file));
|
|
7
8
|
}
|
|
8
|
-
/**
|
|
9
|
-
export function
|
|
9
|
+
/** The opt-in standards canon ships; a repo imports one by name under `standards:`. */
|
|
10
|
+
export function libraryStandards(canonRoot) {
|
|
11
|
+
const dir = join(canonRoot, "standards", "library");
|
|
12
|
+
return existsSync(dir) ? markdownIn(dir).map((file) => file.replace(/\.md$/, "")).sort() : [];
|
|
13
|
+
}
|
|
14
|
+
function libraryFile(canonRoot, name) {
|
|
15
|
+
const path = join(canonRoot, "standards", "library", `${name}.md`);
|
|
16
|
+
if (!existsSync(path))
|
|
17
|
+
throw new Error(`no canon standard named ${name} — canon has: ${libraryStandards(canonRoot).join(", ")}`);
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* canon's defaults, then each `standards:` entry in order: a name imports canon's library
|
|
22
|
+
* standard, a folder adds its files. A later file of the same name replaces an earlier one.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveStandards(canonRoot, entries) {
|
|
10
25
|
const byName = new Map();
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
const add = (name, path, from) => {
|
|
27
|
+
// Constitutions add up (see constitutionTitles), so each one keeps its own entry.
|
|
28
|
+
byName.set(name === "constitution" ? `${name}:${from}` : name, { name, path, from });
|
|
29
|
+
};
|
|
30
|
+
const defaults = join(canonRoot, "standards");
|
|
31
|
+
for (const entry of [defaults, ...entries]) {
|
|
32
|
+
if (isStandardName(entry)) {
|
|
33
|
+
add(entry, libraryFile(canonRoot, entry), "canon library");
|
|
34
|
+
continue;
|
|
20
35
|
}
|
|
36
|
+
if (!existsSync(entry))
|
|
37
|
+
throw new Error(`standards folder not found: ${entry}`);
|
|
38
|
+
const from = entry === defaults ? "canon" : entry;
|
|
39
|
+
for (const file of markdownIn(entry))
|
|
40
|
+
add(file.replace(/\.md$/, ""), join(entry, file), from);
|
|
21
41
|
}
|
|
22
42
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name) || Number(a.from !== "canon") - Number(b.from !== "canon"));
|
|
23
43
|
}
|
|
24
44
|
/**
|
|
25
|
-
* Principle headings from canon's constitution
|
|
26
|
-
*
|
|
27
|
-
* and restating an ID rewords it.
|
|
45
|
+
* Principle headings from canon's constitution, every imported library standard
|
|
46
|
+
* and every configured folder's constitution. They extend rather than replace:
|
|
47
|
+
* each adds principles, and restating an ID rewords it.
|
|
28
48
|
*/
|
|
29
|
-
export function constitutionTitles(canonRoot,
|
|
49
|
+
export function constitutionTitles(canonRoot, entries) {
|
|
30
50
|
const titles = new Map();
|
|
31
|
-
|
|
32
|
-
|
|
51
|
+
const files = [join(canonRoot, "standards"), ...entries].map((entry) => isStandardName(entry) ? libraryFile(canonRoot, entry) : join(entry, "constitution.md"));
|
|
52
|
+
for (const file of files) {
|
|
33
53
|
if (!existsSync(file))
|
|
34
54
|
continue;
|
|
35
55
|
for (const line of readFileSync(file, "utf8").split("\n")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@freetesh/canon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "canon CLI: keep your domain knowledge and AI rules in your repo, and enforce them.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ritesh Firodiya",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"packs",
|
|
23
23
|
"templates",
|
|
24
24
|
"scripts/log-verdict.mjs",
|
|
25
|
+
"scripts/check-designs.py",
|
|
25
26
|
".claude-plugin"
|
|
26
27
|
],
|
|
27
28
|
"engines": {
|