@freetesh/canon 0.17.0 → 0.18.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 +31 -27
- package/cli/dist/api.d.ts +1 -2
- package/cli/dist/api.js +1 -2
- package/cli/dist/check.d.ts +7 -3
- package/cli/dist/check.js +18 -7
- package/cli/dist/checks/commands.d.ts +1 -1
- package/cli/dist/checks/commands.js +1 -1
- package/cli/dist/checks/designs.d.ts +2 -0
- package/cli/dist/checks/designs.js +13 -0
- package/cli/dist/checks/index.js +2 -1
- package/cli/dist/checks/refs.js +8 -4
- package/cli/dist/commands/add.d.ts +1 -1
- package/cli/dist/commands/add.js +1 -4
- package/cli/dist/commands/check.js +4 -4
- package/cli/dist/commands/copies.js +1 -1
- package/cli/dist/commands/hooks.js +4 -4
- package/cli/dist/commands/learn.js +7 -5
- package/cli/dist/commands/pipeline.js +2 -2
- package/cli/dist/commands/scripts.d.ts +3 -0
- package/cli/dist/commands/scripts.js +32 -0
- package/cli/dist/commands/standards.js +1 -1
- package/cli/dist/commands/sync.js +1 -1
- package/cli/dist/commands/ui.js +3 -3
- package/cli/dist/config/load.d.ts +5 -11
- package/cli/dist/config/load.js +19 -23
- package/cli/dist/config/schema.d.ts +4 -2
- package/cli/dist/config/schema.js +4 -4
- package/cli/dist/doctor.js +2 -1
- package/cli/dist/index.js +4 -2
- package/cli/dist/learn.d.ts +2 -2
- package/cli/dist/learn.js +6 -6
- package/cli/dist/lib/env.d.ts +3 -3
- package/cli/dist/lib/env.js +4 -4
- package/cli/dist/packs.d.ts +1 -3
- package/cli/dist/packs.js +4 -7
- package/cli/dist/status.js +1 -1
- package/cli/dist/sync.d.ts +0 -1
- package/cli/dist/sync.js +1 -1
- package/package.json +3 -1
- package/scripts/arch-set.py +140 -0
- package/scripts/flow.py +313 -0
- package/skills/ingest/SKILL.md +1 -1
- package/standards/context-layout.md +1 -1
- package/standards/library/design-sets.md +1 -1
- package/standards/library/secrets.md +3 -3
- package/standards/library/store-listing.md +1 -1
package/README.md
CHANGED
|
@@ -74,57 +74,61 @@ argument captures what a session taught.
|
|
|
74
74
|
```bash
|
|
75
75
|
canon init # write .claude/canon.yaml and the missing .context files
|
|
76
76
|
canon add # list packs; `canon add expo` adds one and indexes its docs
|
|
77
|
-
canon check # this repo against the standards (exit 1 on any error); --all for
|
|
77
|
+
canon check # this repo against the standards (exit 1 on any error); --all for every repo beside it
|
|
78
78
|
canon copies --sync # repair drifted shared files from copies.source (never adds one)
|
|
79
79
|
canon hooks install # a pre-push hook that runs `canon check` (--all: every product)
|
|
80
80
|
canon standards # which standards apply here, and where each comes from
|
|
81
81
|
canon sync # refresh pack docs and the search index, move the pinned version
|
|
82
82
|
canon status # version, kind, modules, config layers, check summary
|
|
83
|
-
canon doctor # node, git, qmd — plus any tools your config lists
|
|
83
|
+
canon doctor # node, git, python3, qmd — plus any tools your config lists
|
|
84
84
|
canon learn # proposals from overridden verdicts
|
|
85
85
|
canon approve <stage> # a person records a pipeline gate approval
|
|
86
86
|
canon pipeline # the stages this repo resolves to, and where each stands
|
|
87
87
|
canon weigh "<ask>" # the decision gate from the terminal
|
|
88
88
|
canon ui # every wiki and design gallery on localhost
|
|
89
89
|
canon search "<q>" # search the knowledge index
|
|
90
|
+
canon flow <product> # how far each local Claude conversation got against the pipeline (--write: metrics/flow.json)
|
|
91
|
+
canon arch-set <spec> # give a design set its architecture screens
|
|
90
92
|
canon mcp # the search MCP server, for any MCP client
|
|
91
93
|
```
|
|
92
94
|
|
|
93
95
|
## Config
|
|
94
96
|
|
|
95
|
-
`.claude/canon.yaml
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
```yaml
|
|
100
|
-
# <estate>/.claude/canon.yaml — shared by every repo below it
|
|
101
|
-
estate: true
|
|
102
|
-
commands: # your own checkers, run by `canon check`
|
|
103
|
-
- { id: lint-copy, run: ./scripts/lint-copy.sh, if: .context/designs, module: design }
|
|
104
|
-
- { id: designs-first, run: test -d .context/designs, kinds: [mobile, web] } # only repos that ship screens
|
|
105
|
-
copies: # files products copy from one source repo, in named sets
|
|
106
|
-
source: design-system
|
|
107
|
-
sets:
|
|
108
|
-
upload: [scripts/upload.mjs]
|
|
109
|
-
shell-web:
|
|
110
|
-
- { from: designs/mobile/index.html, to: designs/web/index.html, named: true }
|
|
111
|
-
```
|
|
97
|
+
Every repo carries its own rules in `.claude/canon.yaml`; nothing above the repo
|
|
98
|
+
is read. An uncommitted `canon.local.yaml` beside it overrides it on one machine.
|
|
99
|
+
Commands that span products (`--all`, `ui`, `learn`, `flow`) use the folder that
|
|
100
|
+
holds the repo you are in, or `CANON_WORKSPACE`.
|
|
112
101
|
|
|
113
102
|
```yaml
|
|
114
103
|
# <repo>/.claude/canon.yaml — written by `canon init`
|
|
115
|
-
canon: 0.
|
|
104
|
+
canon: 0.18.0
|
|
116
105
|
kind: mobile # mobile | web | mobile-and-web | package | content
|
|
117
106
|
standards: [design-sets, pipeline, expo] # canon's library standards this repo follows; ./rules adds a folder of its own
|
|
118
107
|
modules: [docs, knowledge, code, listing, refs]
|
|
119
108
|
packs: [expo]
|
|
120
109
|
name: Acme # how the product is written; `named` copies swap it in
|
|
110
|
+
formerly: [acme-old] # older folder names, so `canon flow` counts their conversations
|
|
111
|
+
commands: # your own checkers, run by `canon check`
|
|
112
|
+
- { id: lint-copy, run: ./scripts/lint-copy.sh, if: .context/designs, module: design }
|
|
113
|
+
copies: # files copied from one source repo, kept equal to it
|
|
114
|
+
source: ../design-system
|
|
115
|
+
use: [upload, shell-web] # sets the source names under its own copies.sets
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
# ../design-system/.claude/canon.yaml — the source names its sets
|
|
121
120
|
copies:
|
|
122
|
-
|
|
121
|
+
sets:
|
|
122
|
+
upload: [scripts/upload.mjs]
|
|
123
|
+
shell-web:
|
|
124
|
+
- { from: designs/mobile/index.html, to: designs/web/index.html, named: true }
|
|
123
125
|
```
|
|
124
126
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
Built in, for every repo: a mobile or web repo keeps `.context/designs/` (designs
|
|
128
|
+
first), and a repo that draws its screens always gets the design checks.
|
|
129
|
+
|
|
130
|
+
Lists of names (`packs`, `standards`, `copies.use`) add up across the two files;
|
|
131
|
+
`commands`, `doctor.tools`, `wiki.types` and `context.folders` merge by name.
|
|
128
132
|
|
|
129
133
|
## Layout
|
|
130
134
|
|
|
@@ -136,8 +140,8 @@ hooks/ session start: constitution titles, qmd refresh
|
|
|
136
140
|
standards/ default standards
|
|
137
141
|
packs/ vendor doc packs
|
|
138
142
|
templates/ files init writes (dependency-cruiser base and starters)
|
|
139
|
-
scripts/ log-verdict.mjs
|
|
140
|
-
evals/ plugin evals
|
|
143
|
+
scripts/ log-verdict.mjs (the verdict log), check-designs.py, flow.py, arch-set.py
|
|
144
|
+
evals/ plugin evals: the fixture product, and cases over real product repos (_scaffold/product.sh)
|
|
141
145
|
cli/ the canon CLI (TypeScript, vitest); `@freetesh/canon/api` for tools built on it
|
|
142
146
|
```
|
|
143
147
|
|
|
@@ -148,7 +152,7 @@ index.ts the program: registers every command
|
|
|
148
152
|
api.ts what other tools import (`@freetesh/canon/api`)
|
|
149
153
|
commands/ one file per command — options and output only
|
|
150
154
|
checks/ one file per check, named by its id; index.ts lists them
|
|
151
|
-
config/ .claude/canon.yaml: schema
|
|
155
|
+
config/ .claude/canon.yaml: schema and loading
|
|
152
156
|
lib/ shared helpers: files, frontmatter, env, qmd, system calls
|
|
153
157
|
<name>.ts the logic the commands call: check (+ format for its output), init, packs (add), sync, …
|
|
154
158
|
```
|
package/cli/dist/api.d.ts
CHANGED
package/cli/dist/api.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
// What other tools build on: the canon knowledge site reads repos the way `canon check --all` finds them.
|
|
2
|
-
export {
|
|
3
|
-
export { findProducts } from "./check.js";
|
|
2
|
+
export { findProducts, findWorkspace } from "./check.js";
|
|
4
3
|
export { qmdQuery, qmdRefresh } from "./lib/qmd.js";
|
package/cli/dist/check.d.ts
CHANGED
|
@@ -11,9 +11,13 @@ export interface Report {
|
|
|
11
11
|
export interface RunOptions {
|
|
12
12
|
checks?: Check[];
|
|
13
13
|
canonRoot: string;
|
|
14
|
-
estateRoot?: string | null;
|
|
15
14
|
today?: Date;
|
|
16
15
|
}
|
|
17
16
|
export declare function runChecks(repo: string, options: RunOptions): Report;
|
|
18
|
-
/** Product repos in
|
|
19
|
-
export declare function findProducts(
|
|
17
|
+
/** Product repos in a workspace: every direct child folder holding a .context/. */
|
|
18
|
+
export declare function findProducts(workspace: string): string[];
|
|
19
|
+
/**
|
|
20
|
+
* The folder holding the product repos: the parent of the git repo `start` sits in, or `start`
|
|
21
|
+
* itself outside any repo. Null when that folder holds no product.
|
|
22
|
+
*/
|
|
23
|
+
export declare function findWorkspace(start: string): string | null;
|
package/cli/dist/check.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
-
import { basename, join } from "node:path";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { ALL_CHECKS } from "./checks/index.js";
|
|
4
4
|
import { loadConfig } from "./config/load.js";
|
|
5
5
|
import { isDirectory } from "./lib/files.js";
|
|
@@ -11,7 +11,7 @@ export function runChecks(repo, options) {
|
|
|
11
11
|
}
|
|
12
12
|
let config;
|
|
13
13
|
try {
|
|
14
|
-
config = loadConfig(repo
|
|
14
|
+
config = loadConfig(repo).config;
|
|
15
15
|
}
|
|
16
16
|
catch (error) {
|
|
17
17
|
report.findings.push({ check: "config", level: "error", message: error.message });
|
|
@@ -31,10 +31,21 @@ export function runChecks(repo, options) {
|
|
|
31
31
|
}
|
|
32
32
|
return report;
|
|
33
33
|
}
|
|
34
|
-
/** Product repos in
|
|
35
|
-
export function findProducts(
|
|
36
|
-
return readdirSync(
|
|
37
|
-
.filter((entry) => entry.isDirectory() && existsSync(join(
|
|
38
|
-
.map((entry) => join(
|
|
34
|
+
/** Product repos in a workspace: every direct child folder holding a .context/. */
|
|
35
|
+
export function findProducts(workspace) {
|
|
36
|
+
return readdirSync(workspace, { withFileTypes: true })
|
|
37
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(workspace, entry.name, ".context")))
|
|
38
|
+
.map((entry) => join(workspace, entry.name))
|
|
39
39
|
.sort();
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The folder holding the product repos: the parent of the git repo `start` sits in, or `start`
|
|
43
|
+
* itself outside any repo. Null when that folder holds no product.
|
|
44
|
+
*/
|
|
45
|
+
export function findWorkspace(start) {
|
|
46
|
+
let folder = resolve(start);
|
|
47
|
+
while (!existsSync(join(folder, ".git")) && dirname(folder) !== folder)
|
|
48
|
+
folder = dirname(folder);
|
|
49
|
+
const workspace = existsSync(join(folder, ".git")) ? dirname(folder) : resolve(start);
|
|
50
|
+
return findProducts(workspace).length > 0 ? workspace : null;
|
|
51
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Check } from "./types.ts";
|
|
2
2
|
/**
|
|
3
|
-
* Runs each checker a repo
|
|
3
|
+
* Runs each checker a repo declares under `commands:` in
|
|
4
4
|
* canon.yaml. canon does not know what they check; a non-zero exit is an error
|
|
5
5
|
* reported under the command's own id.
|
|
6
6
|
*/
|
|
@@ -7,7 +7,7 @@ function tail(text) {
|
|
|
7
7
|
return text.trim().split("\n").slice(-MAX_OUTPUT_LINES).join("\n");
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Runs each checker a repo
|
|
10
|
+
* Runs each checker a repo declares under `commands:` in
|
|
11
11
|
* canon.yaml. canon does not know what they check; a non-zero exit is an error
|
|
12
12
|
* reported under the command's own id.
|
|
13
13
|
*/
|
|
@@ -3,3 +3,5 @@ import type { Check } from "./types.ts";
|
|
|
3
3
|
export declare function designSurfaces(repo: string): string[];
|
|
4
4
|
/** Runs canon's design-set checker (scripts/check-designs.py) once per drawn surface. */
|
|
5
5
|
export declare const designs: Check;
|
|
6
|
+
/** A repo with screens draws them first (pipeline stage 1): a mobile or web repo keeps .context/designs/. */
|
|
7
|
+
export declare const designsFirst: Check;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { kindCovers } from "../config/schema.js";
|
|
4
5
|
const DESIGNS = ".context/designs";
|
|
5
6
|
const MAX_OUTPUT_LINES = 15;
|
|
6
7
|
/** Every drawn surface: a folder under .context/designs/ with a routes.js manifest. */
|
|
@@ -31,3 +32,15 @@ export const designs = {
|
|
|
31
32
|
});
|
|
32
33
|
},
|
|
33
34
|
};
|
|
35
|
+
/** A repo with screens draws them first (pipeline stage 1): a mobile or web repo keeps .context/designs/. */
|
|
36
|
+
export const designsFirst = {
|
|
37
|
+
id: "designs-first",
|
|
38
|
+
module: null,
|
|
39
|
+
description: "a mobile or web repo draws its screens in .context/designs/ before any code",
|
|
40
|
+
run({ repo, config }) {
|
|
41
|
+
const hasScreens = kindCovers(config.kind, "mobile") || kindCovers(config.kind, "web");
|
|
42
|
+
if (!hasScreens || existsSync(join(repo, DESIGNS)))
|
|
43
|
+
return [];
|
|
44
|
+
return [{ check: "designs-first", level: "error", file: DESIGNS, message: "no .context/designs yet — stage 1 (HTML designs) comes before any code" }];
|
|
45
|
+
},
|
|
46
|
+
};
|
package/cli/dist/checks/index.js
CHANGED
|
@@ -5,7 +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
|
+
import { designs, designsFirst } from "./designs.js";
|
|
9
9
|
import { features } from "./features.js";
|
|
10
10
|
import { listing } from "./listing.js";
|
|
11
11
|
import { metricsDefinitions } from "./metrics-definitions.js";
|
|
@@ -31,6 +31,7 @@ export const ALL_CHECKS = [
|
|
|
31
31
|
metricsDefinitions,
|
|
32
32
|
sources,
|
|
33
33
|
pipeline,
|
|
34
|
+
designsFirst,
|
|
34
35
|
designs,
|
|
35
36
|
commandChecks,
|
|
36
37
|
];
|
package/cli/dist/checks/refs.js
CHANGED
|
@@ -15,6 +15,8 @@ const BINARY = /\.(png|jpe?g|gif|webp|ico|icns|mp4|mov|pdf|zip|aab|apk|jks|keyst
|
|
|
15
15
|
const FEATURE_FILE = /^\.context\/features\/(\d{4})-[^/]+\.md$/;
|
|
16
16
|
// A pipeline stage's `done:` paths name what will exist once that stage starts.
|
|
17
17
|
const STAGE_DONE = /^\s*-\s*\{[^}]*\bdone:\s*\[/;
|
|
18
|
+
// A copy set's `to:` path is where the file lands in the products that copy it, not in this repo.
|
|
19
|
+
const COPY_TO = /,\s*to:.*$/;
|
|
18
20
|
function git(repo, args) {
|
|
19
21
|
const result = spawnSync("git", ["-C", repo, ...args], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
20
22
|
return { status: result.status ?? 1, stdout: result.stdout ?? "" };
|
|
@@ -66,10 +68,12 @@ export const refs = {
|
|
|
66
68
|
const links = file.startsWith(".context/") && file.endsWith(".md");
|
|
67
69
|
text.split("\n").forEach((line, index) => {
|
|
68
70
|
const at = `${file}:${index + 1}`;
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const inConfig = file === ".claude/canon.yaml";
|
|
72
|
+
const future = inConfig && STAGE_DONE.test(line);
|
|
73
|
+
const own = inConfig ? line.replace(COPY_TO, "") : line;
|
|
74
|
+
if (!dated && !future && own.includes(".context/") && !FROM_HISTORY.test(own)) {
|
|
75
|
+
for (const match of own.matchAll(PATH)) {
|
|
76
|
+
let path = literalPart(match[2], own.slice(match.index + match[0].length));
|
|
73
77
|
if (path === null)
|
|
74
78
|
continue;
|
|
75
79
|
if (match[1])
|
package/cli/dist/commands/add.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { CACHE_DIR, fetchText, runCommand } from "../lib/system.js";
|
|
3
3
|
import { addPack, fetchPackDocs, findPack, listPacks } from "../packs.js";
|
|
4
|
-
import { requireEstate } from "../lib/env.js";
|
|
5
4
|
export function registerAdd(program, env) {
|
|
6
5
|
program
|
|
7
6
|
.command("add")
|
|
8
7
|
.description("add a knowledge pack (rules + vendor docs in search), e.g. canon add expo")
|
|
9
8
|
.argument("[pack]", "pack name; omit to list packs")
|
|
10
|
-
.option("-g, --global", "add for every product (estate canon.yaml)")
|
|
11
9
|
.option("--repo <repo>", "product repo", ".")
|
|
12
10
|
.action(async (name, options) => {
|
|
13
11
|
if (!name) {
|
|
@@ -15,8 +13,7 @@ export function registerAdd(program, env) {
|
|
|
15
13
|
console.log(`${pack.name.padEnd(11)} ${pack.description}`);
|
|
16
14
|
return;
|
|
17
15
|
}
|
|
18
|
-
const
|
|
19
|
-
const { file, added } = addPack(resolve(options.repo), name, { canonRoot: env.canonRoot, global: options.global, estateRoot });
|
|
16
|
+
const { file, added } = addPack(resolve(options.repo), name, { canonRoot: env.canonRoot });
|
|
20
17
|
console.log(added ? `added ${name} to ${file}` : `${name} already in ${file}`);
|
|
21
18
|
const dir = await fetchPackDocs(findPack(env.canonRoot, name), { cacheDir: CACHE_DIR, fetchText, run: runCommand });
|
|
22
19
|
console.log(`indexed ${dir} as qmd://pack-${name}`);
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { exitCode, formatReports } from "../format.js";
|
|
3
3
|
import { findProducts, runChecks } from "../check.js";
|
|
4
|
-
import {
|
|
4
|
+
import { requireWorkspace } from "../lib/env.js";
|
|
5
5
|
export function registerCheck(program, env) {
|
|
6
6
|
program
|
|
7
7
|
.command("check")
|
|
8
8
|
.description("check that a product follows the standards (read-only)")
|
|
9
9
|
.argument("[repo]", "product repo", ".")
|
|
10
|
-
.option("--all", "check every product in the
|
|
10
|
+
.option("--all", "check every product in the workspace")
|
|
11
11
|
.option("--json", "print machine-readable JSON")
|
|
12
12
|
.option("-q, --quiet", "hide products with no findings")
|
|
13
13
|
.action((repo, options) => {
|
|
14
|
-
const repos = options.all ? findProducts(
|
|
15
|
-
const reports = repos.map((path) => runChecks(path, { canonRoot: env.canonRoot
|
|
14
|
+
const repos = options.all ? findProducts(requireWorkspace(env)) : [resolve(repo)];
|
|
15
|
+
const reports = repos.map((path) => runChecks(path, { canonRoot: env.canonRoot }));
|
|
16
16
|
console.log(options.json ? JSON.stringify(reports, null, 2) : formatReports(reports, { quiet: options.quiet }));
|
|
17
17
|
process.exitCode = exitCode(reports);
|
|
18
18
|
});
|
|
@@ -9,7 +9,7 @@ export function registerCopies(program, env) {
|
|
|
9
9
|
.option("--sync", "overwrite drifted copies from the source (never adds a missing file)")
|
|
10
10
|
.action((repo, options) => {
|
|
11
11
|
const path = resolve(repo);
|
|
12
|
-
const { config } = loadConfig(path
|
|
12
|
+
const { config } = loadConfig(path);
|
|
13
13
|
if (options.sync) {
|
|
14
14
|
const written = syncCopies(path, config);
|
|
15
15
|
console.log(written.length ? written.map((file) => ` synced: ${file}`).join("\n") : " nothing to sync");
|
|
@@ -2,9 +2,9 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { chmodSync, existsSync, lstatSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { findProducts } from "../check.js";
|
|
5
|
-
import {
|
|
5
|
+
import { requireWorkspace } from "../lib/env.js";
|
|
6
6
|
const MARK = "installed by `canon hooks install`";
|
|
7
|
-
// Hooks written by the tools this command replaced (the
|
|
7
|
+
// Hooks written by the tools this command replaced (the old ~/git/bin script, then zeno); they are ours to overwrite.
|
|
8
8
|
const OLD_MARKS = [MARK, "installed by ~/git/bin/install-hooks", "installed by `zeno hooks install`"];
|
|
9
9
|
export const HOOK = `#!/bin/sh
|
|
10
10
|
# ${MARK} — run it again rather than editing this file.
|
|
@@ -44,11 +44,11 @@ export function registerHooks(program, env) {
|
|
|
44
44
|
.description("install a pre-push hook that runs `canon check`")
|
|
45
45
|
.argument("<action>", "install")
|
|
46
46
|
.argument("[repo]", "product repo", ".")
|
|
47
|
-
.option("--all", "every product in the
|
|
47
|
+
.option("--all", "every product in the workspace")
|
|
48
48
|
.action((action, repo, options) => {
|
|
49
49
|
if (action !== "install")
|
|
50
50
|
throw new Error(`unknown action ${action} — use: canon hooks install`);
|
|
51
|
-
const repos = options.all ? findProducts(
|
|
51
|
+
const repos = options.all ? findProducts(requireWorkspace(env)) : [resolve(repo)];
|
|
52
52
|
for (const path of repos)
|
|
53
53
|
console.log(` ${installHook(path)}`);
|
|
54
54
|
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
1
2
|
import { proposals, readDecisions, readVerdicts, recordDecision, summarise } from "../learn.js";
|
|
2
3
|
import { findProducts } from "../check.js";
|
|
3
|
-
import {
|
|
4
|
+
import { requireWorkspace } from "../lib/env.js";
|
|
4
5
|
const DEFAULT_THRESHOLD = 3;
|
|
6
|
+
// The decisions log lives in the user's home: it spans every product and must survive canon upgrades.
|
|
7
|
+
const DECISIONS_HOME = homedir();
|
|
5
8
|
export function registerLearn(program, env) {
|
|
6
9
|
program
|
|
7
10
|
.command("learn")
|
|
@@ -11,15 +14,14 @@ export function registerLearn(program, env) {
|
|
|
11
14
|
.option("--reject <id>", "mark a proposal rejected; it returns only with new evidence")
|
|
12
15
|
.option("--note <text>", "why, for --approve / --reject", "")
|
|
13
16
|
.action((options) => {
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const found = proposals(entries, { threshold: Number(options.threshold), decisions: readDecisions(estateRoot) });
|
|
17
|
+
const entries = readVerdicts(findProducts(requireWorkspace(env)));
|
|
18
|
+
const found = proposals(entries, { threshold: Number(options.threshold), decisions: readDecisions(DECISIONS_HOME) });
|
|
17
19
|
const target = options.approve ?? options.reject;
|
|
18
20
|
if (target) {
|
|
19
21
|
const proposal = found.find((p) => p.id === target);
|
|
20
22
|
if (!proposal)
|
|
21
23
|
throw new Error(`no open proposal "${target}" — run \`canon learn\` to list them`);
|
|
22
|
-
recordDecision(
|
|
24
|
+
recordDecision(DECISIONS_HOME, { id: target, decision: options.approve ? "approved" : "rejected", count: proposal.count, note: options.note });
|
|
23
25
|
console.log(`${options.approve ? "approved" : "rejected"} ${target}`);
|
|
24
26
|
return;
|
|
25
27
|
}
|
|
@@ -4,12 +4,12 @@ import { stagesOf } from "../pipeline.js";
|
|
|
4
4
|
export function registerPipeline(program, env) {
|
|
5
5
|
program
|
|
6
6
|
.command("pipeline")
|
|
7
|
-
.description("the repo's stages in order —
|
|
7
|
+
.description("the repo's stages in order — and where each stands")
|
|
8
8
|
.argument("[repo]", "product repo", ".")
|
|
9
9
|
.option("--json", "print JSON, for scripts")
|
|
10
10
|
.action((repo, options) => {
|
|
11
11
|
const root = resolve(repo);
|
|
12
|
-
const stages = stagesOf(root, loadConfig(root
|
|
12
|
+
const stages = stagesOf(root, loadConfig(root).config.pipeline);
|
|
13
13
|
if (options.json) {
|
|
14
14
|
console.log(JSON.stringify(stages, null, 2));
|
|
15
15
|
return;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { findProducts } from "../check.js";
|
|
4
|
+
import { loadConfig } from "../config/load.js";
|
|
5
|
+
import { requireWorkspace } from "../lib/env.js";
|
|
6
|
+
/** Runs one of canon's Python scripts with the terminal attached, and passes its exit code on. */
|
|
7
|
+
function runScript(env, script, args, extraEnv = {}) {
|
|
8
|
+
const result = spawnSync("python3", [join(env.canonRoot, "scripts", script), ...args], { stdio: "inherit", env: { ...process.env, ...extraEnv } });
|
|
9
|
+
if (result.error)
|
|
10
|
+
throw new Error(`could not run python3: ${result.error.message}`);
|
|
11
|
+
process.exitCode = result.status ?? 1;
|
|
12
|
+
}
|
|
13
|
+
export function registerScripts(program, env) {
|
|
14
|
+
program
|
|
15
|
+
.command("flow")
|
|
16
|
+
.description("how far each local Claude conversation about a product got against its pipeline, and how long each step took")
|
|
17
|
+
.argument("<product...>", "product folder names in the workspace")
|
|
18
|
+
.option("--write", "also write <product>/.context/metrics/flow.json")
|
|
19
|
+
.action((products, options) => {
|
|
20
|
+
const workspace = requireWorkspace(env);
|
|
21
|
+
const formerly = Object.fromEntries(findProducts(workspace).map((repo) => [resolve(repo).split("/").pop(), loadConfig(repo).config.formerly]));
|
|
22
|
+
runScript(env, "flow.py", [...products, ...(options.write ? ["--write"] : [])], {
|
|
23
|
+
CANON_WORKSPACE: workspace,
|
|
24
|
+
CANON_FLOW_FORMERLY: JSON.stringify(formerly),
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
program
|
|
28
|
+
.command("arch-set")
|
|
29
|
+
.description("give a design set its architecture screens from a spec (see scripts/arch-set.py)")
|
|
30
|
+
.argument("<spec>", "spec.json naming the set and what to draw")
|
|
31
|
+
.action((spec) => runScript(env, "arch-set.py", [resolve(spec)]));
|
|
32
|
+
}
|
|
@@ -4,7 +4,7 @@ import { constitutionTitles, resolveStandards } from "../standards.js";
|
|
|
4
4
|
export function registerStandards(program, env) {
|
|
5
5
|
program
|
|
6
6
|
.command("standards")
|
|
7
|
-
.description("list the standards that apply to a repo: canon's defaults, then the
|
|
7
|
+
.description("list the standards that apply to a repo: canon's defaults, then the ones the repo imports")
|
|
8
8
|
.argument("[repo]", "product repo", ".")
|
|
9
9
|
.option("--json", "print machine-readable JSON")
|
|
10
10
|
.option("--constitution", "print only the principle titles, canon's then any added ones")
|
|
@@ -10,7 +10,7 @@ export function registerSync(program, env) {
|
|
|
10
10
|
.argument("[repo]", "product repo", ".")
|
|
11
11
|
.action(async (repo) => {
|
|
12
12
|
const path = resolve(repo);
|
|
13
|
-
const result = await syncRepo(path, { canonRoot: env.canonRoot,
|
|
13
|
+
const result = await syncRepo(path, { canonRoot: env.canonRoot, cacheDir: CACHE_DIR, fetchText, run: runCommand });
|
|
14
14
|
console.log(`packs refreshed: ${result.packs.join(", ") || "none"}`);
|
|
15
15
|
if (result.base)
|
|
16
16
|
console.log("architecture base updated from canon's template");
|
package/cli/dist/commands/ui.js
CHANGED
|
@@ -6,7 +6,7 @@ import { formatReports } from "../format.js";
|
|
|
6
6
|
import { CACHE_DIR } from "../lib/system.js";
|
|
7
7
|
import { findProducts, runChecks } from "../check.js";
|
|
8
8
|
import { buildSite, quartzConfig } from "../ui.js";
|
|
9
|
-
import {
|
|
9
|
+
import { requireWorkspace } from "../lib/env.js";
|
|
10
10
|
// Quartz v5 has no tagged release yet, so pin the exact commit: an unpinned
|
|
11
11
|
// clone could change the site under us on the next fresh install.
|
|
12
12
|
const QUARTZ_REPO = "https://github.com/jackyzha0/quartz";
|
|
@@ -37,11 +37,11 @@ export function registerUi(program, env) {
|
|
|
37
37
|
.option("--ws-port <port>", "live-reload socket port", "4612")
|
|
38
38
|
.action((options) => {
|
|
39
39
|
ensureQuartz();
|
|
40
|
-
const products = findProducts(
|
|
40
|
+
const products = findProducts(requireWorkspace(env));
|
|
41
41
|
const siteDir = join(CACHE_DIR, "site");
|
|
42
42
|
const designsDir = join(CACHE_DIR, "designs");
|
|
43
43
|
const designsUrl = `http://127.0.0.1:${options.designsPort}`;
|
|
44
|
-
const report = formatReports(products.map((repo) => runChecks(repo, { canonRoot: env.canonRoot
|
|
44
|
+
const report = formatReports(products.map((repo) => runChecks(repo, { canonRoot: env.canonRoot })));
|
|
45
45
|
buildSite(products, { siteDir, designsDir, designsUrl, report, graph: architectureGraph });
|
|
46
46
|
const designs = spawn("python3", ["-m", "http.server", options.designsPort, "--bind", "127.0.0.1", "--directory", designsDir], {
|
|
47
47
|
stdio: "ignore",
|
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
import { type Config, type Kind, type Module } from "./schema.ts";
|
|
2
2
|
export interface ConfigSource {
|
|
3
|
-
layer: "
|
|
3
|
+
layer: "product" | "local";
|
|
4
4
|
path: string;
|
|
5
5
|
}
|
|
6
|
-
export interface LoadOptions {
|
|
7
|
-
/** The estate folder; found by walking up from the repo when not given. */
|
|
8
|
-
estateRoot?: string | null;
|
|
9
|
-
}
|
|
10
|
-
/** The nearest folder at or above `start` whose canon.yaml says `estate: true`, or null. */
|
|
11
|
-
export declare function findEstate(start: string): string | null;
|
|
12
6
|
export declare function inferKind(repo: string): Kind;
|
|
13
7
|
export declare function inferModules(repo: string, kind: Kind): Module[];
|
|
14
8
|
/**
|
|
15
|
-
* Reads the
|
|
16
|
-
* from the repo. Scalars: the
|
|
9
|
+
* Reads the repo's canon.yaml and its uncommitted canon.local.yaml over defaults
|
|
10
|
+
* inferred from the repo. Scalars: the local layer wins. Lists of names add up
|
|
17
11
|
* (packs, standards); lists of records merge by their key (commands, tools,
|
|
18
|
-
* wiki types, context folders).
|
|
12
|
+
* wiki types, context folders). Copy sets come from the source repo's canon.yaml.
|
|
19
13
|
*/
|
|
20
|
-
export declare function loadConfig(repo: string
|
|
14
|
+
export declare function loadConfig(repo: string): {
|
|
21
15
|
config: Config;
|
|
22
16
|
sources: ConfigSource[];
|
|
23
17
|
};
|