@erclx/canon 4.77.0 → 4.78.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.77.0",
4
+ "version": "4.78.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -14,16 +14,14 @@ A reference page that reads like a lesson is a lesson wearing the wrong extensio
14
14
 
15
15
  Sort by who owns the subject, which is the test the wiki already runs. The reader's activity decided the workspace and decides nothing here.
16
16
 
17
- | The page's subject | Destination |
18
- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
19
- | Owned by someone outside the project, such as a tool or vendor | the project's wiki |
20
- | How this project itself works, for the people who build it | `.claude/context/<domain>.md`, or the domain's own nested `index.md` when it is already split into a folder |
21
- | Written for someone consuming what this project ships | the project's public docs |
17
+ | The page's subject | Destination |
18
+ | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
19
+ | Owned by Anthropic | the project's wiki |
20
+ | Owned by another tool or vendor, owned by no single vendor, or written for someone consuming what this project ships | the project's public docs |
21
+ | How this project itself works, for the people who build it | `.claude/context/<domain>.md`, or the domain's own nested `index.md` when it is already split into a folder |
22
22
 
23
23
  A page a reader cannot sort by that question is a page whose subject has not been settled. Say which two destinations it sits between and let the operator choose, rather than picking the nearer one.
24
24
 
25
- Within the wiki, place the page by the same owner test that decides the folder: a vendor's own subject, another tool's subject, and a subject no single vendor owns each take the folder the project's wiki index names for them.
26
-
27
25
  ## Where the wiki sits
28
26
 
29
27
  The folder has two spellings and a project carries whichever its scaffold wrote:
@@ -11,6 +11,7 @@ description: Rendering HTML sources to PNG, what the command asserts about fonts
11
11
  canon capture --selector .window
12
12
  canon capture assets/captures/install.html --selector .window --out assets
13
13
  canon capture assets/captures --selector .window --out .canon/review/captures
14
+ canon capture https://example.com --selector .window --out preview.png
14
15
  ```
15
16
 
16
17
  `--selector` is required and every example above passes it. The element a capture crops to belongs to the page, not to the command, so there is no value that could be right for an arbitrary project's markup. `.window` is what this repository's own five sources declare, and a project renders its own pages by naming whatever theirs declare.
@@ -29,10 +30,10 @@ Every render writes a stamp beside its PNG, `hero.png` next to `hero.stamp`, hol
29
30
 
30
31
  Neither digest is ever written by hand. A digest is what the gate compares, so a hand-set one asserts agreement the tool never checked, and the way to move it is a capture. The `source:` line above them is the one field a rename may correct in place, since nothing reads it and the alternative is a stamp naming a file that no longer exists. Renaming a frame is the case that comes up, and the correction is the same basename the next capture would have written anyway.
31
32
 
32
- | Option | Behavior |
33
- | ------------------ | ------------------------------------------------- |
34
- | `--out <dir>` | Write every PNG here instead of beside its source |
35
- | `--selector <sel>` | Element to capture, required and never defaulted |
33
+ | Option | Behavior |
34
+ | ------------------ | ------------------------------------------------------------------------------------------------------ |
35
+ | `--out <dir>` | Write every PNG here instead of beside its source. For a URL source, names the destination PNG itself. |
36
+ | `--selector <sel>` | Element to capture, required and never defaulted |
36
37
 
37
38
  ## What the command asserts
38
39
 
@@ -48,4 +49,4 @@ The command ships to targets, alongside `demo`, `inventory`, and `drive`. It was
48
49
 
49
50
  Shipping it also fixed what the exclusion was hiding. The render module imported the `@playwright/test` development dependency, which no published tarball carries. Every browser reference still sits behind a dynamic import, so a browser loads for this command rather than in front of every other one.
50
51
 
51
- `demo.md` and `driver.md` cover two of the other three browser commands. What separates this one is that a capture renders a single state from a file on disk, where the rest drive a running application.
52
+ `demo.md` and `driver.md` cover two of the other three browser commands. What separates this one is that a capture renders a single state, from a file on disk or a named `http(s)://` URL, where the rest drive a running application.
@@ -94,7 +94,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
94
94
  | `canon gate run` | Run every stage that guards a branch here, scoping shell, types, and tests to the changed set (`--all`, `--no-write`, `--nested`, `--json`) |
95
95
  | `canon inventory [subject]` | Walk every route a project declares and group its elements by the property each computes, as a listing rather than a gate (`--json`) |
96
96
  | `canon drive <url> <run>` | Walk a page through named interactions and measure each state it reaches, reporting findings rather than gating (`--json`) |
97
- | `canon capture [source]` | Render HTML sources to PNG and prove the font each one declares resolved (`--selector` required, `--out`) |
97
+ | `canon capture [source]` | Render an HTML source or an `http(s)://` URL to PNG and prove the font each one declares resolved (`--selector` required, `--out`) |
98
98
  | `canon serve [dir]` | Serve a directory on the loopback interface and print the link that opens it, running until interrupted (`--port`, `--entry`, `--json`) |
99
99
  | `canon upgrade` | Reinstall the CLI globally with the package manager the install path names (`--json`) |
100
100
 
@@ -49,7 +49,7 @@ Every step names what it did, and that name is carried onto each finding it prod
49
49
 
50
50
  Probes run after a step and never on arrival, so a run reaches the load state by opening with a `wait` step of its own, as the example above does. Nothing probes before the first step runs, which makes that leading step the only way to measure the page as it first painted, and naming it is what puts the load state on its own findings rather than under whatever ran next.
51
51
 
52
- Write one wherever no capture runs against the page. `canon capture` renders a single state from a source on disk, so where it runs it already answers about arrival and a leading `wait` duplicates it. Where it does not, this command is the only thing measuring the page at all, and a run without that step reports every driven state and nothing about the one a visitor sees first.
52
+ Write one wherever no capture runs against the page. `canon capture` renders a single state from a source on disk or a URL, so where it runs it already answers about arrival and a leading `wait` duplicates it. Where it does not, this command is the only thing measuring the page at all, and a run without that step reports every driven state and nothing about the one a visitor sees first.
53
53
 
54
54
  Each height is driven in a context of its own from a fresh navigation, rather than by resizing the page the previous height already drove, since a sweep asks the same question of each height rather than a later question of an already-driven page.
55
55
 
@@ -323,7 +323,7 @@ This section is the corpus the coverage claim is measured against: every name `c
323
323
 
324
324
  Every row answers a question rather than marking a point in a project's life, so a phase above would send a reader to the wrong group.
325
325
 
326
- A learning workspace produces two halves and only one of them leaves. A lesson is worked through once and stays in the workspace, and a reference page or a glossary carries no learner, so it belongs wherever the project already keeps prose on that subject. Asking `canon:teach-workspace` to promote sorts each durable page by who owns its subject, sending an outside subject to the wiki, an internal one to the matching context entry, and consumer-facing material to the public docs. It proposes and waits, because a promoted page is public prose that needs a line naming who owns the subject, and it writes nothing to a destination: each page the operator confirms goes to a handoff file that `canon:docs-fold` folds in from a branch. A project with no wiki folder gets a refusal naming `canon wiki init` rather than a folder it never asked for.
326
+ A learning workspace produces two halves and only one of them leaves. A lesson is worked through once and stays in the workspace, and a reference page or a glossary carries no learner, so it belongs wherever the project already keeps prose on that subject. Asking `canon:teach-workspace` to promote sorts each durable page by who owns its subject, sending an Anthropic-owned subject to the wiki, an internal one to the matching context entry, and everything else, subject-neutral material included, to the public docs. It proposes and waits, because a promoted page is public prose that needs a line naming who owns the subject, and it writes nothing to a destination: each page the operator confirms goes to a handoff file that `canon:docs-fold` folds in from a branch. A project with no wiki folder gets a refusal naming `canon wiki init` rather than a folder it never asked for.
327
327
 
328
328
  ## Feedback routing
329
329
 
@@ -8,9 +8,9 @@ category: Workflow
8
8
 
9
9
  Three tiers cover the range from prose-only design docs to a fully graphical design source of truth. Pick one per project based on how UI-heavy the work is, whether stakeholders review visuals, and whether a designer is involved. Tiers stack, so moving up does not invalidate work done at a lower tier.
10
10
 
11
- The tier framework sits alongside [Claude Design](../../wiki/claude/claude-design.md), [visual wireframes](../../wiki/tools/visual-wireframes.md), [community skills and plugins](../../wiki/tools/community-skills.md), and [community MCP servers](../../wiki/tools/community-mcp-servers.md). Those pages catalog the tooling. This page decides when to reach for what.
11
+ The tier framework sits alongside [Claude Design](../../wiki/claude/claude-design.md), the one catalog page in this range. Stitch, Excalidraw, and the surrounding MCP and skill ecosystem are named inline below rather than catalogued separately. This page decides when to reach for what.
12
12
 
13
- Two tools anchor tier 1 and tier 2. [Stitch](../../wiki/tools/stitch.md) is the agent-addressable default through its MCP server at `stitch.googleapis.com/mcp`, with a free tier of 400 daily credits that covers daily iteration. [Claude Design](../../wiki/claude/claude-design.md), released 2026-04-17 and priced inside Claude subscriptions, is the ceiling tool reserved for codebase extraction and the richly annotated handoff bundle. Each covers a different job, they are not swappable.
13
+ Two tools anchor tier 1 and tier 2. Stitch is the agent-addressable default through its MCP server at `stitch.googleapis.com/mcp`, with a free tier of 400 daily credits that covers daily iteration. [Claude Design](../../wiki/claude/claude-design.md), released 2026-04-17 and priced inside Claude subscriptions, is the ceiling tool reserved for codebase extraction and the richly annotated handoff bundle. Each covers a different job, they are not swappable.
14
14
 
15
15
  ## Tier 0: prose only
16
16
 
@@ -68,16 +68,16 @@ Impeccable, if installed, keeps its own root `DESIGN.md` and `PRODUCT.md` in the
68
68
 
69
69
  ### Tools
70
70
 
71
- - Stitch via MCP at `stitch.googleapis.com/mcp`. Default pick for agent-driven visual generation. Free tier of 400 daily credits. See [Stitch](../../wiki/tools/stitch.md).
72
- - Excalidraw canvas server on localhost plus the `yctimlin/mcp_excalidraw` MCP shim, for projects that need an agent to draw, read back, and revise a canvas. See [visual wireframes](../../wiki/tools/visual-wireframes.md) for setup and footguns.
73
- - Playwright MCP for browser-side verification. See [Playwright](../../wiki/tools/community-mcp-servers.md#playwright-microsoft).
74
- - Chrome DevTools MCP for live frontend debugging. See [Chrome DevTools](../../wiki/tools/community-mcp-servers.md#chrome-devtools-google).
71
+ - Stitch via MCP at `stitch.googleapis.com/mcp`, Google's Gemini-powered design product. Agent-addressable through `generate_screen_from_text`, `edit_screens`, and `generate_variants`. Default pick for agent-driven visual generation. Free tier of 400 daily credits.
72
+ - Excalidraw canvas server on localhost plus the community [`yctimlin/mcp_excalidraw`](https://github.com/yctimlin/mcp_excalidraw) MCP shim, for projects that need an agent to draw, read back, and revise a canvas. MIT-licensed, plaintext JSON scene format. `describe_scene` and `get_canvas_screenshot` let the agent verify its own layout before claiming it is correct.
73
+ - Playwright MCP ([`microsoft/playwright-mcp`](https://github.com/microsoft/playwright-mcp)) for browser-side verification, driven off the accessibility tree rather than screenshots.
74
+ - Chrome DevTools MCP ([`ChromeDevTools/chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp)) for live frontend debugging: performance traces, network inspection, and DOM and CSS inspection.
75
75
  - Claude Design as the ceiling option for codebase extraction or polished handoff bundles. See [Claude Design](../../wiki/claude/claude-design.md).
76
76
 
77
77
  ### Skills
78
78
 
79
79
  - Everything from tier 0
80
- - A frontend design skill to steer visual quality. Pick one of [Impeccable](../../wiki/tools/community-skills.md#pbakausimpeccable), [UI/UX Pro Max](../../wiki/tools/community-skills.md#nextlevelbuilderui-ux-pro-max-skill), or Anthropic's `frontend-design` plugin. Impeccable is the strongest default because of its curated anti-patterns.
80
+ - A frontend design skill to steer visual quality. Pick one of [pbakaus/impeccable](https://github.com/pbakaus/impeccable), which bundles curated anti-pattern references and is the strongest default, [nextlevelbuilder/ui-ux-pro-max-skill](https://github.com/nextlevelbuilder/ui-ux-pro-max-skill), which maps product type to UI rules through a reasoning engine, or Anthropic's `frontend-design` plugin.
81
81
 
82
82
  ### When to pick
83
83
 
@@ -108,9 +108,9 @@ Design happens in a graphical tool. `.claude/DESIGN.md` either regenerates from
108
108
 
109
109
  ### Tools
110
110
 
111
- - Figma desktop app with the [Figma Dev Mode MCP](../../wiki/tools/community-skills.md#figma-mcp-and-code-to-canvas) for teams with a dedicated designer already on Figma. Bidirectional sync and Code to Canvas capture.
111
+ - Figma desktop app with the official [Figma Dev Mode MCP](https://www.figma.com/blog/introducing-claude-code-to-figma/) for teams with a dedicated designer already on Figma. Bidirectional sync and Code to Canvas capture of a running Claude Code UI into editable Figma frames.
112
112
  - Claude Design with its Claude Code handoff bundle for teams without an existing Figma investment and for solo founders or PMs driving design themselves. One-way handoff, no bidirectional sync. See [Claude Design](../../wiki/claude/claude-design.md).
113
- - Stitch via MCP as a low-cost complement to either, used for bulk screen generation driven by Claude Code. See [Stitch](../../wiki/tools/stitch.md).
113
+ - Stitch via MCP as a low-cost complement to either, used for bulk screen generation driven by Claude Code.
114
114
  - Playwright and Chrome DevTools MCPs as in tier 1
115
115
 
116
116
  ### Skills
@@ -147,9 +147,8 @@ Resist over-tiering early. Moving up is cheap because tiers stack. Moving down m
147
147
 
148
148
  ## References
149
149
 
150
- - [Stitch](../../wiki/tools/stitch.md): Gemini-powered design product with the MCP server that anchors tier 1
151
150
  - [Claude Design](../../wiki/claude/claude-design.md): first-party hosted design product and handoff bundle
152
151
  - `.claude/context/claude-plugin/skill-strategy.md`: how to decide between workflow and domain-knowledge skills
153
- - [Visual wireframes](../../wiki/tools/visual-wireframes.md): Excalidraw research and setup for the tier 1 wireframe companion
154
- - [Community skills and plugins](../../wiki/tools/community-skills.md): catalog of frontend design skills and integrations
155
- - [Community MCP servers](../../wiki/tools/community-mcp-servers.md): catalog of MCPs referenced across all tiers
152
+ - [`microsoft/playwright-mcp`](https://github.com/microsoft/playwright-mcp): browser automation MCP used in tier 1 and tier 2
153
+ - [`ChromeDevTools/chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp): live frontend debugging MCP used in tier 1 and tier 2
154
+ - [`yctimlin/mcp_excalidraw`](https://github.com/yctimlin/mcp_excalidraw): community MCP server behind the tier 1 wireframe companion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.77.0",
4
+ "version": "4.78.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -37,12 +37,12 @@ export interface CaptureOptions {
37
37
  export type CaptureResult =
38
38
  | {
39
39
  status: 'rendered'
40
- htmlPath: string
40
+ source: string
41
41
  pngPath: string
42
42
  width: number
43
43
  height: number
44
44
  }
45
- | { status: 'failed'; htmlPath: string; reason: string }
45
+ | { status: 'failed'; source: string; reason: string }
46
46
 
47
47
  export async function captureSources(
48
48
  sourcePath: string,
@@ -74,7 +74,9 @@ async function captureOne(
74
74
  deviceScaleFactor: DEVICE_SCALE_FACTOR,
75
75
  })
76
76
  try {
77
- await page.goto(pathToFileURL(source.htmlPath).href)
77
+ await page.goto(
78
+ source.kind === 'url' ? source.url : pathToFileURL(source.htmlPath).href,
79
+ )
78
80
  const element = page.locator(selector).first()
79
81
  if ((await element.count()) === 0) {
80
82
  return failed(source, `no element matched ${selector}`)
@@ -97,7 +99,7 @@ async function captureOne(
97
99
  writeStamp(source, png)
98
100
  return {
99
101
  status: 'rendered',
100
- htmlPath: source.htmlPath,
102
+ source: sourceIdentifier(source),
101
103
  pngPath: source.pngPath,
102
104
  width: png.readUInt32BE(16),
103
105
  height: png.readUInt32BE(20),
@@ -118,8 +120,11 @@ async function captureOne(
118
120
  * caller's catch and reports the source as failed, which is correct: a PNG
119
121
  * whose stamp never landed is the state the verify stage exists to reject.
120
122
  *
121
- * The source is stored as a bare filename. An absolute path would record the
122
- * machine that ran the capture into a tracked file and differ per checkout.
123
+ * A file source is stored as a bare filename. An absolute path would record
124
+ * the machine that ran the capture into a tracked file and differ per
125
+ * checkout. A URL source is stored as the URL itself, and its digest hashes
126
+ * the URL string's UTF-8 bytes rather than any local file, since there is
127
+ * none to hash.
123
128
  *
124
129
  * The image digest is taken over the buffer the screenshot returned rather than
125
130
  * by reading the file back, so the stamp describes the bytes this run wrote.
@@ -128,15 +133,27 @@ function writeStamp(source: CaptureSource, png: Uint8Array): void {
128
133
  writeFileSync(
129
134
  stampPath(source.pngPath),
130
135
  formatStamp({
131
- source: basename(source.htmlPath),
132
- sourceSha256: hashSource(readFileSync(source.htmlPath)),
136
+ source: source.kind === 'url' ? source.url : basename(source.htmlPath),
137
+ sourceSha256:
138
+ source.kind === 'url'
139
+ ? hashSource(Buffer.from(source.url, 'utf8'))
140
+ : hashSource(readFileSync(source.htmlPath)),
133
141
  imageSha256: hashSource(png),
134
142
  }),
135
143
  )
136
144
  }
137
145
 
146
+ /**
147
+ * The identifier a `CaptureResult` reports, kept as the full path for a file
148
+ * source so `displayPath` in `src/commands/capture.ts` can still show it
149
+ * relative to the working directory.
150
+ */
151
+ function sourceIdentifier(source: CaptureSource): string {
152
+ return source.kind === 'url' ? source.url : source.htmlPath
153
+ }
154
+
138
155
  function failed(source: CaptureSource, reason: string): CaptureResult {
139
- return { status: 'failed', htmlPath: source.htmlPath, reason }
156
+ return { status: 'failed', source: sourceIdentifier(source), reason }
140
157
  }
141
158
 
142
159
  /**
@@ -1,20 +1,45 @@
1
1
  import { readdirSync, statSync } from 'node:fs'
2
2
  import { basename, dirname, extname, join } from 'node:path'
3
3
 
4
- export interface CaptureSource {
5
- htmlPath: string
6
- pngPath: string
4
+ const URL_SOURCE = /^https?:\/\//
5
+
6
+ /**
7
+ * The one test for whether a source string names a URL rather than a
8
+ * filesystem path, shared with `src/commands/capture.ts` so the two never
9
+ * drift apart on what counts as a URL source.
10
+ */
11
+ export function isUrlSource(source: string): boolean {
12
+ return URL_SOURCE.test(source)
7
13
  }
8
14
 
15
+ export type CaptureSource =
16
+ | {
17
+ readonly kind: 'file'
18
+ readonly htmlPath: string
19
+ readonly pngPath: string
20
+ }
21
+ | { readonly kind: 'url'; readonly url: string; readonly pngPath: string }
22
+
9
23
  /**
10
24
  * Pairs each HTML source with the PNG it renders to. A directory expands to
11
25
  * every `.html` directly inside it, so adding a capture is a file drop rather
12
26
  * than a flag. Omitting `outDir` writes the PNG beside its source.
27
+ *
28
+ * A `http(s)://` source has no directory to walk and no basename on disk to
29
+ * name the PNG from, so `outDir` there is read as the destination file
30
+ * itself rather than a directory, and the caller is what requires it.
13
31
  */
14
32
  export function resolveCaptureSources(
15
33
  sourcePath: string,
16
34
  outDir?: string,
17
35
  ): CaptureSource[] {
36
+ if (isUrlSource(sourcePath)) {
37
+ if (!outDir) {
38
+ throw new Error('a URL source needs --out naming the destination PNG')
39
+ }
40
+ return [{ kind: 'url', url: sourcePath, pngPath: outDir }]
41
+ }
42
+
18
43
  const htmlPaths = statSync(sourcePath).isDirectory()
19
44
  ? readdirSync(sourcePath)
20
45
  .filter((name) => extname(name) === '.html')
@@ -23,6 +48,7 @@ export function resolveCaptureSources(
23
48
  : [sourcePath]
24
49
 
25
50
  return htmlPaths.map((htmlPath) => ({
51
+ kind: 'file',
26
52
  htmlPath,
27
53
  pngPath: join(
28
54
  outDir ?? dirname(htmlPath),
@@ -6,6 +6,7 @@ import {
6
6
  isBrowserMissing,
7
7
  isEngineMissing,
8
8
  } from '@/browser/engine'
9
+ import { isUrlSource } from '@/capture/sources'
9
10
  import {
10
11
  frameError,
11
12
  intro,
@@ -42,8 +43,15 @@ export function register(program: Command): void {
42
43
  program
43
44
  .command('capture')
44
45
  .description('Render HTML capture sources to PNG')
45
- .argument('[source]', 'HTML file or a directory of them', DEFAULT_SOURCE)
46
- .option('-o, --out <dir>', 'Output directory, defaults beside the source')
46
+ .argument(
47
+ '[source]',
48
+ 'HTML file, a directory of them, or an http(s):// URL',
49
+ DEFAULT_SOURCE,
50
+ )
51
+ .option(
52
+ '-o, --out <path>',
53
+ 'Output directory for a file source, or the destination PNG for a URL source; defaults beside the source',
54
+ )
47
55
  .option('-s, --selector <selector>', 'Element to capture')
48
56
  .action(
49
57
  async (
@@ -66,8 +74,9 @@ export function register(program: Command): void {
66
74
  }
67
75
  const selector = opts.selector
68
76
 
69
- const sourcePath = resolve(process.cwd(), source)
70
- if (!existsSync(sourcePath)) {
77
+ const isUrl = isUrlSource(source)
78
+ const sourcePath = isUrl ? source : resolve(process.cwd(), source)
79
+ if (!isUrl && !existsSync(sourcePath)) {
71
80
  frameError(`${source} not found`)
72
81
  process.exitCode = 1
73
82
  return
@@ -100,7 +109,7 @@ export function register(program: Command): void {
100
109
  `${displayPath(result.pngPath)} ${result.width}x${result.height}`,
101
110
  )
102
111
  } else {
103
- logError(`${displayPath(result.htmlPath)}: ${result.reason}`)
112
+ logError(`${displayPath(result.source)}: ${result.reason}`)
104
113
  }
105
114
  }
106
115
  outro()
@@ -141,9 +150,11 @@ function reportInFrame(error: unknown): void {
141
150
  /**
142
151
  * Keeps a path clickable in the operator's terminal. A source outside the
143
152
  * project reports absolute, since a relative path to it is a run of `..`
144
- * segments no editor resolves.
153
+ * segments no editor resolves. A URL is not a filesystem path at all, so it
154
+ * reports as given.
145
155
  */
146
156
  function displayPath(path: string): string {
157
+ if (isUrlSource(path)) return path
147
158
  const fromCwd = relative(process.cwd(), path)
148
159
  return fromCwd.startsWith('..') ? path : fromCwd
149
160
  }
package/standards/wiki.md CHANGED
@@ -28,8 +28,7 @@ A page failing either is non-conforming even when it satisfies every shape rule
28
28
  ## Placement
29
29
 
30
30
  - Write a page here only when its subject is owned outside this repository. Route anything about how this repository works to `docs/`, `.claude/context/`, or a skill body instead.
31
- - File the page by owner: `wiki/claude/` for Anthropic, `wiki/tools/` for another vendor, `wiki/concepts/` where no single vendor owns the subject.
32
- - Treat `wiki/concepts/rule-writing-vocabulary.md` as the one recorded exception to the owner test, stated in the page itself.
31
+ - File the page under `wiki/claude/`. A subject Anthropic does not own, whether a third-party tool or a vendor-neutral concept, is out of scope for this folder split. Route it to `docs/` or a skill body instead of adding a second wiki folder for it.
33
32
 
34
33
  ## Frontmatter
35
34
 
@@ -38,8 +37,7 @@ A page failing either is non-conforming even when it satisfies every shape rule
38
37
 
39
38
  ## Naming
40
39
 
41
- - Name a `wiki/claude/` page with a `claude-` prefix before the kebab subject.
42
- - Name a `wiki/tools/` or `wiki/concepts/` page as a bare kebab subject, with no prefix.
40
+ - Name a page with a `claude-` prefix before the kebab subject.
43
41
 
44
42
  ## Sourcing
45
43