@freetesh/canon 0.15.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.
Files changed (72) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +24 -7
  4. package/cli/dist/api.d.ts +2 -2
  5. package/cli/dist/api.js +2 -2
  6. package/cli/dist/checks/{external.js → commands.js} +2 -0
  7. package/cli/dist/checks/designs.d.ts +5 -0
  8. package/cli/dist/checks/designs.js +33 -0
  9. package/cli/dist/checks/index.js +6 -3
  10. package/cli/dist/checks/stale-copies.d.ts +2 -0
  11. package/cli/dist/checks/stale-copies.js +18 -0
  12. package/cli/dist/checks/{copies.d.ts → standards-copies.d.ts} +0 -1
  13. package/cli/dist/checks/{copies.js → standards-copies.js} +0 -16
  14. package/cli/dist/commands/add.d.ts +1 -1
  15. package/cli/dist/commands/add.js +1 -1
  16. package/cli/dist/commands/check.d.ts +1 -1
  17. package/cli/dist/commands/check.js +2 -2
  18. package/cli/dist/commands/copies.d.ts +1 -1
  19. package/cli/dist/commands/hooks.d.ts +1 -1
  20. package/cli/dist/commands/hooks.js +2 -2
  21. package/cli/dist/commands/init.d.ts +1 -1
  22. package/cli/dist/commands/init.js +2 -2
  23. package/cli/dist/commands/learn.d.ts +1 -1
  24. package/cli/dist/commands/learn.js +2 -2
  25. package/cli/dist/commands/pipeline.d.ts +1 -1
  26. package/cli/dist/{search → commands}/search.js +1 -1
  27. package/cli/dist/commands/standards.d.ts +1 -1
  28. package/cli/dist/commands/status.d.ts +1 -1
  29. package/cli/dist/commands/sync.d.ts +1 -1
  30. package/cli/dist/commands/sync.js +1 -1
  31. package/cli/dist/commands/ui.d.ts +1 -1
  32. package/cli/dist/commands/ui.js +2 -2
  33. package/cli/dist/config/load.js +18 -6
  34. package/cli/dist/config/schema.d.ts +24 -3
  35. package/cli/dist/config/schema.js +17 -3
  36. package/cli/dist/format.d.ts +1 -1
  37. package/cli/dist/index.js +2 -2
  38. package/cli/dist/init.js +2 -2
  39. package/cli/dist/packs.d.ts +3 -1
  40. package/cli/dist/standards.d.ts +11 -6
  41. package/cli/dist/standards.js +37 -17
  42. package/cli/dist/status.d.ts +1 -1
  43. package/cli/dist/status.js +1 -1
  44. package/package.json +3 -3
  45. package/packs/expo/pack.yaml +1 -1
  46. package/packs/firebase/pack.yaml +1 -1
  47. package/packs/react/pack.yaml +1 -1
  48. package/packs/revenuecat/pack.yaml +1 -1
  49. package/packs/uniwind/pack.yaml +1 -1
  50. package/scripts/check-designs.py +709 -0
  51. package/standards/README.md +23 -6
  52. package/standards/agent-contract.md +21 -6
  53. package/standards/context-layout.md +41 -6
  54. package/standards/library/design-sets.md +857 -0
  55. package/standards/library/expo.md +54 -0
  56. package/standards/library/frontend.md +44 -0
  57. package/standards/library/performance.md +45 -0
  58. package/standards/library/pipeline.md +42 -0
  59. package/standards/library/secrets.md +37 -0
  60. package/standards/library/store-listing.md +122 -0
  61. /package/cli/dist/{run.d.ts → check.d.ts} +0 -0
  62. /package/cli/dist/{run.js → check.js} +0 -0
  63. /package/cli/dist/checks/{external.d.ts → commands.d.ts} +0 -0
  64. /package/cli/dist/checks/{metrics.d.ts → metrics-definitions.d.ts} +0 -0
  65. /package/cli/dist/checks/{metrics.js → metrics-definitions.js} +0 -0
  66. /package/cli/dist/{search → commands}/mcp.d.ts +0 -0
  67. /package/cli/dist/{search → commands}/mcp.js +0 -0
  68. /package/cli/dist/{search → commands}/search.d.ts +0 -0
  69. /package/cli/dist/{commands → lib}/env.d.ts +0 -0
  70. /package/cli/dist/{commands → lib}/env.js +0 -0
  71. /package/cli/dist/{search → lib}/qmd.d.ts +0 -0
  72. /package/cli/dist/{search → lib}/qmd.js +0 -0
@@ -7,7 +7,7 @@
7
7
  {
8
8
  "name": "canon",
9
9
  "source": "./",
10
- "version": "0.15.0",
10
+ "version": "0.17.0",
11
11
  "description": "Keeps your domain knowledge and AI rules in your repos and enforces them, so Claude hallucinates less."
12
12
  }
13
13
  ],
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "canon",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Keeps your domain knowledge and AI rules in your repos and enforces them, so Claude hallucinates less."
5
5
  }
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; an estate adds or replaces
51
- them from its own folder; a repo can override again. `canon standards` shows
52
- what applies.
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,9 @@ 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: designs, run: ./scripts/check-designs.sh, if: .context/designs, module: design }
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
103
105
  copies: # files products copy from one source repo, in named sets
104
106
  source: design-system
105
107
  sets:
@@ -110,8 +112,9 @@ copies: # files products copy from one source repo,
110
112
 
111
113
  ```yaml
112
114
  # <repo>/.claude/canon.yaml — written by `canon init`
113
- canon: 0.14.0
114
- kind: expo-app # expo-app | web | content
115
+ canon: 0.17.0
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
115
118
  modules: [docs, knowledge, code, listing, refs]
116
119
  packs: [expo]
117
120
  name: Acme # how the product is written; `named` copies swap it in
@@ -138,6 +141,20 @@ evals/ plugin evals and the fixture product
138
141
  cli/ the canon CLI (TypeScript, vitest); `@freetesh/canon/api` for tools built on it
139
142
  ```
140
143
 
144
+ Inside `cli/src/`:
145
+
146
+ ```
147
+ index.ts the program: registers every command
148
+ api.ts what other tools import (`@freetesh/canon/api`)
149
+ commands/ one file per command — options and output only
150
+ checks/ one file per check, named by its id; index.ts lists them
151
+ config/ .claude/canon.yaml: schema, layered loading, estate discovery
152
+ lib/ shared helpers: files, frontmatter, env, qmd, system calls
153
+ <name>.ts the logic the commands call: check (+ format for its output), init, packs (add), sync, …
154
+ ```
155
+
156
+ Tests mirror it in `cli/test/`.
157
+
141
158
  ## Develop
142
159
 
143
160
  `pnpm check` runs `canon check` on this repo, the typecheck and every test.
package/cli/dist/api.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { findEstate } from "./config/load.ts";
2
- export { findProducts } from "./run.ts";
3
- export { qmdQuery, qmdRefresh } from "./search/qmd.ts";
2
+ export { findProducts } from "./check.ts";
3
+ export { qmdQuery, qmdRefresh } from "./lib/qmd.ts";
package/cli/dist/api.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // What other tools build on: the canon knowledge site reads repos the way `canon check --all` finds them.
2
2
  export { findEstate } from "./config/load.js";
3
- export { findProducts } from "./run.js";
4
- export { qmdQuery, qmdRefresh } from "./search/qmd.js";
3
+ export { findProducts } from "./check.js";
4
+ export { qmdQuery, qmdRefresh } from "./lib/qmd.js";
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { kindCovers } from "../config/schema.js";
4
5
  const MAX_OUTPUT_LINES = 15;
5
6
  function tail(text) {
6
7
  return text.trim().split("\n").slice(-MAX_OUTPUT_LINES).join("\n");
@@ -18,6 +19,7 @@ export const commandChecks = {
18
19
  return config.commands
19
20
  .filter((command) => !command.if || existsSync(join(repo, command.if)))
20
21
  .filter((command) => !command.module || config.modules.includes(command.module))
22
+ .filter((command) => !command.kinds || command.kinds.some((kind) => kindCovers(config.kind, kind)))
21
23
  .flatMap((command) => {
22
24
  const result = spawnSync("sh", ["-c", command.run], { cwd: repo, encoding: "utf8" });
23
25
  if (result.error) {
@@ -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
+ };
@@ -2,11 +2,13 @@ import { architecture } from "./architecture.js";
2
2
  import { canonicalValues } from "./canonical-values.js";
3
3
  import { contextLayout } from "./context-layout.js";
4
4
  import { contextSize } from "./context-size.js";
5
- import { staleCopies, standardsCopies } from "./copies.js";
6
- import { commandChecks } from "./external.js";
5
+ import { staleCopies } from "./stale-copies.js";
6
+ import { standardsCopies } from "./standards-copies.js";
7
+ import { commandChecks } from "./commands.js";
8
+ import { designs } from "./designs.js";
7
9
  import { features } from "./features.js";
8
10
  import { listing } from "./listing.js";
9
- import { metricsDefinitions } from "./metrics.js";
11
+ import { metricsDefinitions } from "./metrics-definitions.js";
10
12
  import { pipeline } from "./pipeline.js";
11
13
  import { refs } from "./refs.js";
12
14
  import { sharedFiles } from "./shared-files.js";
@@ -29,5 +31,6 @@ export const ALL_CHECKS = [
29
31
  metricsDefinitions,
30
32
  sources,
31
33
  pipeline,
34
+ designs,
32
35
  commandChecks,
33
36
  ];
@@ -0,0 +1,2 @@
1
+ import type { Check } from "./types.ts";
2
+ export declare const staleCopies: Check;
@@ -0,0 +1,18 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ // Instruction files for agents other than Claude Code. They are copies of
4
+ // AGENTS.md that nobody reads, so they only drift.
5
+ const OTHER_AGENT_FILES = [".github/copilot-instructions.md", ".cursorrules", ".windsurfrules"];
6
+ export const staleCopies = {
7
+ id: "stale-copies",
8
+ module: null,
9
+ description: "no instruction files for agents that are not used (Copilot, Cursor, Windsurf)",
10
+ run({ repo }) {
11
+ return OTHER_AGENT_FILES.filter((file) => existsSync(join(repo, file))).map((file) => ({
12
+ check: "stale-copies",
13
+ level: "warn",
14
+ file,
15
+ message: "instructions for an agent not in use — delete, Claude reads AGENTS.md",
16
+ }));
17
+ },
18
+ };
@@ -1,3 +1,2 @@
1
1
  import type { Check } from "./types.ts";
2
- export declare const staleCopies: Check;
3
2
  export declare const standardsCopies: Check;
@@ -1,25 +1,9 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- // Instruction files for agents other than Claude Code. They are copies of
4
- // AGENTS.md that nobody reads, so they only drift.
5
- const OTHER_AGENT_FILES = [".github/copilot-instructions.md", ".cursorrules", ".windsurfrules"];
6
3
  // Fraction of a shared standard's lines that must appear in a product file
7
4
  // before it counts as a copy rather than a reference.
8
5
  const COPY_THRESHOLD = 0.6;
9
6
  const STANDARD_COPIES = [{ file: ".context/wiki/CLAUDE.md", standard: "standards/wiki-schema.md" }];
10
- export const staleCopies = {
11
- id: "stale-copies",
12
- module: null,
13
- description: "no instruction files for agents that are not used (Copilot, Cursor, Windsurf)",
14
- run({ repo }) {
15
- return OTHER_AGENT_FILES.filter((file) => existsSync(join(repo, file))).map((file) => ({
16
- check: "stale-copies",
17
- level: "warn",
18
- file,
19
- message: "instructions for an agent not in use — delete, Claude reads AGENTS.md",
20
- }));
21
- },
22
- };
23
7
  function meaningfulLines(text) {
24
8
  return new Set(text
25
9
  .split("\n")
@@ -1,3 +1,3 @@
1
1
  import type { Command } from "commander";
2
- import { type Env } from "./env.ts";
2
+ import { type Env } from "../lib/env.ts";
3
3
  export declare function registerAdd(program: Command, env: Env): void;
@@ -1,7 +1,7 @@
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 "./env.js";
4
+ import { requireEstate } from "../lib/env.js";
5
5
  export function registerAdd(program, env) {
6
6
  program
7
7
  .command("add")
@@ -1,3 +1,3 @@
1
1
  import type { Command } from "commander";
2
- import { type Env } from "./env.ts";
2
+ import { type Env } from "../lib/env.ts";
3
3
  export declare function registerCheck(program: Command, env: Env): void;
@@ -1,7 +1,7 @@
1
1
  import { resolve } from "node:path";
2
2
  import { exitCode, formatReports } from "../format.js";
3
- import { findProducts, runChecks } from "../run.js";
4
- import { requireEstate } from "./env.js";
3
+ import { findProducts, runChecks } from "../check.js";
4
+ import { requireEstate } from "../lib/env.js";
5
5
  export function registerCheck(program, env) {
6
6
  program
7
7
  .command("check")
@@ -1,3 +1,3 @@
1
1
  import type { Command } from "commander";
2
- import type { Env } from "./env.ts";
2
+ import type { Env } from "../lib/env.ts";
3
3
  export declare function registerCopies(program: Command, env: Env): void;
@@ -1,5 +1,5 @@
1
1
  import type { Command } from "commander";
2
- import { type Env } from "./env.ts";
2
+ import { type Env } from "../lib/env.ts";
3
3
  export declare const HOOK = "#!/bin/sh\n# installed by `canon hooks install` \u2014 run it again rather than editing this file.\n# A machine without canon pushes unchecked, as it would without any hook.\ncommand -v canon >/dev/null 2>&1 || exit 0\n# The product is the folder that owns .git, not the folder being pushed from: a worktree shares these hooks.\nroot=$(cd \"$(git rev-parse --git-common-dir)/..\" && pwd) || exit 0\nexec canon check \"$root\"\n";
4
4
  /** Writes the pre-push hook into one repo. Returns a one-line outcome. */
5
5
  export declare function installHook(repo: string): string;
@@ -1,8 +1,8 @@
1
1
  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
- import { findProducts } from "../run.js";
5
- import { requireEstate } from "./env.js";
4
+ import { findProducts } from "../check.js";
5
+ import { requireEstate } from "../lib/env.js";
6
6
  const MARK = "installed by `canon hooks install`";
7
7
  // Hooks written by the tools this command replaced (the estate 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`"];
@@ -1,3 +1,3 @@
1
1
  import { type Command } from "commander";
2
- import type { Env } from "./env.ts";
2
+ import type { Env } from "../lib/env.ts";
3
3
  export declare function registerInit(program: Command, env: Env): void;
@@ -1,7 +1,7 @@
1
1
  import { basename, resolve } from "node:path";
2
2
  import * as prompts from "@clack/prompts";
3
3
  import { Option } from "commander";
4
- import { KINDS, MODULES } from "../config/schema.js";
4
+ import { KINDS, kindCovers, MODULES } from "../config/schema.js";
5
5
  import { RULES_FILES } from "../checks/architecture.js";
6
6
  import { applyInit, parseChoices, planInit } from "../init.js";
7
7
  import { listPacks } from "../packs.js";
@@ -29,7 +29,7 @@ async function askAnswers(inferred, packs) {
29
29
  });
30
30
  if (cancelled(modules))
31
31
  return null;
32
- const forKind = packs.filter((pack) => pack.kinds.includes(kind));
32
+ const forKind = packs.filter((pack) => pack.kinds.some((wanted) => kindCovers(kind, wanted)));
33
33
  if (forKind.length === 0)
34
34
  return { kind, modules, packs: [] };
35
35
  const chosen = await prompts.multiselect({
@@ -1,3 +1,3 @@
1
1
  import type { Command } from "commander";
2
- import { type Env } from "./env.ts";
2
+ import { type Env } from "../lib/env.ts";
3
3
  export declare function registerLearn(program: Command, env: Env): void;
@@ -1,6 +1,6 @@
1
1
  import { proposals, readDecisions, readVerdicts, recordDecision, summarise } from "../learn.js";
2
- import { findProducts } from "../run.js";
3
- import { requireEstate } from "./env.js";
2
+ import { findProducts } from "../check.js";
3
+ import { requireEstate } from "../lib/env.js";
4
4
  const DEFAULT_THRESHOLD = 3;
5
5
  export function registerLearn(program, env) {
6
6
  program
@@ -1,3 +1,3 @@
1
1
  import type { Command } from "commander";
2
- import type { Env } from "./env.ts";
2
+ import type { Env } from "../lib/env.ts";
3
3
  export declare function registerPipeline(program: Command, env: Env): void;
@@ -1,4 +1,4 @@
1
- import { qmdQuery } from './qmd.js';
1
+ import { qmdQuery } from '../lib/qmd.js';
2
2
  export function registerSearch(program) {
3
3
  program
4
4
  .command('search')
@@ -1,3 +1,3 @@
1
1
  import type { Command } from "commander";
2
- import type { Env } from "./env.ts";
2
+ import type { Env } from "../lib/env.ts";
3
3
  export declare function registerStandards(program: Command, env: Env): void;
@@ -1,3 +1,3 @@
1
1
  import type { Command } from "commander";
2
- import type { Env } from "./env.ts";
2
+ import type { Env } from "../lib/env.ts";
3
3
  export declare function registerStatus(program: Command, env: Env): void;
@@ -1,3 +1,3 @@
1
1
  import type { Command } from "commander";
2
- import type { Env } from "./env.ts";
2
+ import type { Env } from "../lib/env.ts";
3
3
  export declare function registerSync(program: Command, env: Env): void;
@@ -1,7 +1,7 @@
1
1
  import { resolve } from "node:path";
2
2
  import { exitCode, formatReports } from "../format.js";
3
3
  import { CACHE_DIR, fetchText, runCommand } from "../lib/system.js";
4
- import { runChecks } from "../run.js";
4
+ import { runChecks } from "../check.js";
5
5
  import { syncRepo } from "../sync.js";
6
6
  export function registerSync(program, env) {
7
7
  program
@@ -1,3 +1,3 @@
1
1
  import type { Command } from "commander";
2
- import { type Env } from "./env.ts";
2
+ import { type Env } from "../lib/env.ts";
3
3
  export declare function registerUi(program: Command, env: Env): void;
@@ -4,9 +4,9 @@ import { join } from "node:path";
4
4
  import { architectureGraph } from "../checks/architecture.js";
5
5
  import { formatReports } from "../format.js";
6
6
  import { CACHE_DIR } from "../lib/system.js";
7
- import { findProducts, runChecks } from "../run.js";
7
+ import { findProducts, runChecks } from "../check.js";
8
8
  import { buildSite, quartzConfig } from "../ui.js";
9
- import { requireEstate } from "./env.js";
9
+ import { requireEstate } 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";
@@ -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) {
@@ -36,14 +36,26 @@ function appRoots(repo) {
36
36
  const children = readdirSync(apps, { withFileTypes: true }).filter((entry) => entry.isDirectory());
37
37
  return [repo, ...children.map((entry) => join(apps, entry.name))];
38
38
  }
39
+ /** A root package.json that ships a CLI or exports a library. */
40
+ function isPackage(repo) {
41
+ const file = join(repo, "package.json");
42
+ if (!existsSync(file))
43
+ return false;
44
+ const pkg = JSON.parse(readFileSync(file, "utf8"));
45
+ return Boolean(pkg.bin || pkg.exports || pkg.main) && !pkg.private;
46
+ }
39
47
  export function inferKind(repo) {
40
48
  const roots = appRoots(repo);
41
49
  const anyHas = (markers) => roots.some((root) => markers.some((file) => existsSync(join(root, file))));
42
- if (anyHas(EXPO_MARKERS))
43
- return "expo-app";
44
- if (anyHas(WEB_MARKERS))
50
+ const mobile = anyHas(EXPO_MARKERS);
51
+ const web = anyHas(WEB_MARKERS);
52
+ if (mobile && web)
53
+ return "mobile-and-web";
54
+ if (mobile)
55
+ return "mobile";
56
+ if (web)
45
57
  return "web";
46
- return "content";
58
+ return isPackage(repo) ? "package" : "content";
47
59
  }
48
60
  export function inferModules(repo, kind) {
49
61
  const modules = MODULES_BY_KIND[kind];
@@ -82,7 +94,7 @@ export function loadConfig(repo, options = {}) {
82
94
  modules: lastDefined(pick("modules")) ?? inferModules(repo, kind),
83
95
  expo: Object.assign({}, ...pick("expo")),
84
96
  packs: [...new Set(pick("packs").flatMap((packs) => packs ?? []))].sort(),
85
- standards: [...new Set(layers.flatMap((layer) => (layer.data.standards ?? []).map((dir) => resolve(layer.base, dir))))],
97
+ standards: [...new Set(layers.flatMap((layer) => (layer.data.standards ?? []).map((entry) => (isStandardName(entry) ? entry : resolve(layer.base, entry)))))],
86
98
  commands: mergeBy(pick("commands").map((list) => list ?? []), (command) => command.id),
87
99
  doctor: { tools: mergeBy(pick("doctor").map((doctor) => doctor?.tools ?? []), (tool) => tool.tool) },
88
100
  weigh: { weights: Object.assign({}, ...pick("weigh").map((weigh) => weigh?.weights)) },
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
- export declare const KINDS: readonly ["expo-app", "web", "content"];
2
+ /** What a repo ships: an app to the stores, a site to the web, both, a package to a registry, or no code at all. */
3
+ export declare const KINDS: readonly ["mobile", "web", "mobile-and-web", "package", "content"];
3
4
  export declare const MODULES: readonly ["docs", "design", "knowledge", "metrics", "code", "architecture", "listing", "refs"];
4
5
  /**
5
6
  * The wiki page types every repo has, and the folder each lives in. A repo adds more under `wiki.types`.
@@ -10,6 +11,8 @@ export declare const WIKI_FOLDER_BY_TYPE: Record<string, string>;
10
11
  export declare const REMOVED_WIKI_FOLDERS: Record<string, string>;
11
12
  /** The `.context/` folders every repo may have. A repo adds more under `context.folders`. */
12
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;
13
16
  export type Kind = (typeof KINDS)[number];
14
17
  export type Module = (typeof MODULES)[number];
15
18
  declare const command: z.ZodObject<{
@@ -26,6 +29,13 @@ declare const command: z.ZodObject<{
26
29
  metrics: "metrics";
27
30
  refs: "refs";
28
31
  }>>;
32
+ kinds: z.ZodOptional<z.ZodArray<z.ZodEnum<{
33
+ content: "content";
34
+ mobile: "mobile";
35
+ "mobile-and-web": "mobile-and-web";
36
+ package: "package";
37
+ web: "web";
38
+ }>>>;
29
39
  }, z.core.$strict>;
30
40
  declare const wikiType: z.ZodObject<{
31
41
  type: z.ZodString;
@@ -60,7 +70,9 @@ export declare const layerSchema: z.ZodObject<{
60
70
  canon: z.ZodOptional<z.ZodString>;
61
71
  kind: z.ZodOptional<z.ZodEnum<{
62
72
  content: "content";
63
- "expo-app": "expo-app";
73
+ mobile: "mobile";
74
+ "mobile-and-web": "mobile-and-web";
75
+ package: "package";
64
76
  web: "web";
65
77
  }>>;
66
78
  modules: z.ZodOptional<z.ZodArray<z.ZodEnum<{
@@ -92,6 +104,13 @@ export declare const layerSchema: z.ZodObject<{
92
104
  metrics: "metrics";
93
105
  refs: "refs";
94
106
  }>>;
107
+ kinds: z.ZodOptional<z.ZodArray<z.ZodEnum<{
108
+ content: "content";
109
+ mobile: "mobile";
110
+ "mobile-and-web": "mobile-and-web";
111
+ package: "package";
112
+ web: "web";
113
+ }>>>;
95
114
  }, z.core.$strict>>>;
96
115
  doctor: z.ZodOptional<z.ZodObject<{
97
116
  tools: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -150,7 +169,7 @@ export interface Config {
150
169
  sdk?: number;
151
170
  };
152
171
  packs: string[];
153
- /** Absolute standards folders, estate first; a later file of the same name wins. */
172
+ /** Library standard names and absolute folders, estate first; a later file of the same name wins. */
154
173
  standards: string[];
155
174
  commands: CommandSpec[];
156
175
  doctor: {
@@ -178,4 +197,6 @@ export interface Config {
178
197
  };
179
198
  }
180
199
  export declare const MODULES_BY_KIND: Record<Kind, Module[]>;
200
+ /** A repo that ships both counts as mobile and as web: it gets both kinds' packs. */
201
+ export declare function kindCovers(kind: Kind, wanted: Kind): boolean;
181
202
  export {};
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
- export const KINDS = ["expo-app", "web", "content"];
2
+ /** What a repo ships: an app to the stores, a site to the web, both, a package to a registry, or no code at all. */
3
+ export const KINDS = ["mobile", "web", "mobile-and-web", "package", "content"];
3
4
  export const MODULES = ["docs", "design", "knowledge", "metrics", "code", "architecture", "listing", "refs"];
4
5
  /**
5
6
  * The wiki page types every repo has, and the folder each lives in. A repo adds more under `wiki.types`.
@@ -18,6 +19,10 @@ export const REMOVED_WIKI_FOLDERS = {
18
19
  };
19
20
  /** The `.context/` folders every repo may have. A repo adds more under `context.folders`. */
20
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
+ }
21
26
  const principleId = z.string().regex(/^C-\d{2}$/, "principle IDs look like C-07");
22
27
  const command = z
23
28
  .object({
@@ -28,6 +33,8 @@ const command = z
28
33
  if: z.string().optional(),
29
34
  /** Only run in a repo that uses this module. */
30
35
  module: z.enum(MODULES).optional(),
36
+ /** Only run in a repo of one of these kinds. */
37
+ kinds: z.array(z.enum(KINDS)).optional(),
31
38
  })
32
39
  .strict();
33
40
  const kebab = z.string().regex(/^[a-z0-9-]+$/, "use lowercase-kebab");
@@ -80,7 +87,7 @@ export const layerSchema = z
80
87
  modules: z.array(z.enum(MODULES)),
81
88
  expo: z.object({ sdk: z.number().int().positive() }).partial(),
82
89
  packs: z.array(z.string().regex(/^[a-z0-9-]+$/, "pack names are lowercase-kebab")),
83
- /** Folders of extra standards, relative to the folder that holds `.claude/`. */
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. */
84
91
  standards: z.array(z.string()),
85
92
  commands: z.array(command),
86
93
  doctor: z.object({ tools: z.array(tool) }).partial(),
@@ -100,7 +107,14 @@ export const layerSchema = z
100
107
  .partial()
101
108
  .strict();
102
109
  export const MODULES_BY_KIND = {
103
- "expo-app": ["docs", "knowledge", "code", "listing", "refs"],
110
+ mobile: ["docs", "knowledge", "code", "listing", "refs"],
104
111
  web: ["docs", "knowledge", "code", "refs"],
112
+ "mobile-and-web": ["docs", "knowledge", "code", "listing", "refs"],
113
+ // A package's docs describe other repos' .context/ paths, so refs would flag them.
114
+ package: ["docs", "knowledge", "code"],
105
115
  content: ["docs", "knowledge", "refs"],
106
116
  };
117
+ /** A repo that ships both counts as mobile and as web: it gets both kinds' packs. */
118
+ export function kindCovers(kind, wanted) {
119
+ return kind === wanted || (kind === "mobile-and-web" && (wanted === "mobile" || wanted === "web"));
120
+ }
@@ -1,4 +1,4 @@
1
- import type { Report } from "./run.ts";
1
+ import type { Report } from "./check.ts";
2
2
  /** Compact, rtk-style text: one header per product, one line per finding, one total line. */
3
3
  export declare function formatReports(reports: Report[], options?: {
4
4
  quiet?: boolean;
package/cli/dist/index.js CHANGED
@@ -16,8 +16,8 @@ import { registerSync } from "./commands/sync.js";
16
16
  import { registerUi } from "./commands/ui.js";
17
17
  import { registerWeigh } from "./commands/weigh.js";
18
18
  import { findEstate } from "./config/load.js";
19
- import { registerMcp } from "./search/mcp.js";
20
- import { registerSearch } from "./search/search.js";
19
+ import { registerMcp } from "./commands/mcp.js";
20
+ import { registerSearch } from "./commands/search.js";
21
21
  import { canonVersion } from "./status.js";
22
22
  const env = {
23
23
  canonRoot: resolve(import.meta.dirname, "..", ".."),
package/cli/dist/init.js CHANGED
@@ -4,7 +4,7 @@ import { stringify } from "yaml";
4
4
  import { RULES_FILES } from "./checks/architecture.js";
5
5
  import { requiredContext } from "./checks/context-layout.js";
6
6
  import { inferKind, inferModules } from "./config/load.js";
7
- import { MODULES_BY_KIND } from "./config/schema.js";
7
+ import { kindCovers, MODULES_BY_KIND } from "./config/schema.js";
8
8
  export const ARCHITECTURE_BASE = ".dependency-cruiser.base.cjs";
9
9
  const ARCHITECTURE_TEMPLATES = join("templates", "dependency-cruiser");
10
10
  const WORKSPACE_PARENTS = ["apps", "packages"];
@@ -86,7 +86,7 @@ export function planInit(repo, answers) {
86
86
  }
87
87
  const kind = answers.kind ?? inferKind(repo);
88
88
  const modules = answers.modules ?? (answers.kind ? MODULES_BY_KIND[kind] : inferModules(repo, kind));
89
- const sdk = kind === "expo-app" ? detectExpoSdk(repo) : undefined;
89
+ const sdk = kindCovers(kind, "mobile") ? detectExpoSdk(repo) : undefined;
90
90
  const packs = answers.packs?.length ? { packs: answers.packs } : {};
91
91
  const config = { canon: answers.canonVersion, kind, modules, ...(sdk ? { expo: { sdk } } : {}), ...packs };
92
92
  const create = requiredContext(modules).filter((name) => !existsSync(join(repo, ".context", name))).map((name) => FIRST_FILE[name]);
@@ -4,7 +4,9 @@ declare const packSchema: z.ZodObject<{
4
4
  description: z.ZodString;
5
5
  kinds: z.ZodArray<z.ZodEnum<{
6
6
  content: "content";
7
- "expo-app": "expo-app";
7
+ mobile: "mobile";
8
+ "mobile-and-web": "mobile-and-web";
9
+ package: "package";
8
10
  web: "web";
9
11
  }>>;
10
12
  skill: z.ZodOptional<z.ZodString>;