@erclx/canon 5.25.0 → 5.27.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/README.md +4 -3
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +2 -0
- package/claude/skills/auto-ship/SKILL.md +3 -11
- package/claude/skills/auto-ship/references/review-findings.md +20 -0
- package/claude/skills/backlog-triage/SKILL.md +2 -0
- package/claude/skills/bash-cli-script/SKILL.md +2 -0
- package/claude/skills/build-in-slices/SKILL.md +2 -0
- package/claude/skills/canon-cli/SKILL.md +2 -0
- package/claude/skills/canon-feedback/SKILL.md +2 -0
- package/claude/skills/canon-feedback-triage/SKILL.md +2 -0
- package/claude/skills/canon-operator/SKILL.md +2 -0
- package/claude/skills/canon-rollout/SKILL.md +3 -11
- package/claude/skills/canon-rollout/references/failure-recovery.md +20 -0
- package/claude/skills/canvas/SKILL.md +14 -1
- package/claude/skills/ci-workflow/SKILL.md +2 -0
- package/claude/skills/code-craft/SKILL.md +2 -0
- package/claude/skills/codebase-layout/SKILL.md +2 -0
- package/claude/skills/context-fold/REQUIREMENT.md +6 -2
- package/claude/skills/context-fold/SKILL.md +9 -21
- package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
- package/claude/skills/context-fold/references/completion-report.md +26 -0
- package/claude/skills/create-rule/SKILL.md +2 -0
- package/claude/skills/create-skill/SKILL.md +2 -0
- package/claude/skills/create-standard/SKILL.md +2 -0
- package/claude/skills/decision-escalate/SKILL.md +2 -0
- package/claude/skills/deploy-app/SKILL.md +2 -0
- package/claude/skills/deprecation-migration/SKILL.md +2 -0
- package/claude/skills/design-extract/SKILL.md +2 -0
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/design-taste/SKILL.md +6 -2
- package/claude/skills/design-taste/references/adopted.md +40 -0
- package/claude/skills/design-taste/references/kinds.md +1 -1
- package/claude/skills/design-taste/references/systems.md +1 -7
- package/claude/skills/design-taste/references/tells.md +2 -24
- package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
- package/claude/skills/docs-sync/SKILL.md +2 -0
- package/claude/skills/document-health/REQUIREMENT.md +2 -0
- package/claude/skills/document-health/SKILL.md +23 -0
- package/claude/skills/document-health/references/axes.md +12 -0
- package/claude/skills/draft-and-pick/SKILL.md +3 -1
- package/claude/skills/draft-doc/SKILL.md +2 -0
- package/claude/skills/draft-doc/references/wiki.md +1 -1
- package/claude/skills/draft-figure/SKILL.md +2 -0
- package/claude/skills/draft-identity/SKILL.md +3 -1
- package/claude/skills/draft-ready/SKILL.md +2 -0
- package/claude/skills/draft-screencast/SKILL.md +11 -7
- package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
- package/claude/skills/draft-slides/SKILL.md +33 -20
- package/claude/skills/git-branch/SKILL.md +2 -0
- package/claude/skills/git-commit/SKILL.md +2 -0
- package/claude/skills/git-followup/SKILL.md +2 -0
- package/claude/skills/git-issue/SKILL.md +2 -0
- package/claude/skills/git-issue/references/issue.md +10 -0
- package/claude/skills/git-pr/SKILL.md +2 -0
- package/claude/skills/git-pr/references/evidence.md +14 -6
- package/claude/skills/git-pr/references/labels.md +12 -0
- package/claude/skills/git-ship/SKILL.md +2 -0
- package/claude/skills/git-split/SKILL.md +21 -8
- package/claude/skills/git-stage/SKILL.md +2 -0
- package/claude/skills/git-worktree/SKILL.md +2 -0
- package/claude/skills/index-lookup/SKILL.md +2 -0
- package/claude/skills/markdown-propose/SKILL.md +2 -0
- package/claude/skills/markdown-propose/references/format.md +9 -0
- package/claude/skills/memory-capture/SKILL.md +2 -0
- package/claude/skills/memory-review/SKILL.md +2 -0
- package/claude/skills/memory-review/references/later-phases.md +7 -0
- package/claude/skills/plan-feature/SKILL.md +2 -0
- package/claude/skills/plan-groundwork/SKILL.md +2 -0
- package/claude/skills/plan-intake/SKILL.md +2 -0
- package/claude/skills/plan-intake-answer/SKILL.md +2 -0
- package/claude/skills/project-commands/SKILL.md +2 -0
- package/claude/skills/read-frames/SKILL.md +2 -0
- package/claude/skills/record-screencast/SKILL.md +2 -0
- package/claude/skills/repo-metadata/SKILL.md +2 -0
- package/claude/skills/restate-plainly/SKILL.md +2 -0
- package/claude/skills/review-address/SKILL.md +3 -7
- package/claude/skills/review-address/references/post-review-findings.md +16 -0
- package/claude/skills/review-branch/SKILL.md +2 -0
- package/claude/skills/review-craft/SKILL.md +2 -0
- package/claude/skills/review-craft/references/ui.md +2 -2
- package/claude/skills/review-pr/SKILL.md +4 -8
- package/claude/skills/review-pr/references/body-variants.md +52 -0
- package/claude/skills/review-pr/references/key-changes.md +16 -0
- package/claude/skills/review-pr/references/post.md +15 -43
- package/claude/skills/review-ui/SKILL.md +3 -1
- package/claude/skills/review-ui/references/post.md +4 -2
- package/claude/skills/role-orchestrator/SKILL.md +5 -14
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +8 -0
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +23 -0
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +5 -33
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -21
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +38 -0
- package/claude/skills/role-orchestrator/references/orchestrator-watch.md +26 -0
- package/claude/skills/role-planner/SKILL.md +2 -0
- package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
- package/claude/skills/role-reviewer/SKILL.md +2 -0
- package/claude/skills/role-worker/SKILL.md +2 -0
- package/claude/skills/search-craft/REQUIREMENT.md +1 -1
- package/claude/skills/search-craft/SKILL.md +5 -1
- package/claude/skills/search-craft/references/adopted.md +30 -0
- package/claude/skills/seed-sync/SKILL.md +2 -0
- package/claude/skills/session-compact/SKILL.md +2 -0
- package/claude/skills/session-map/SKILL.md +2 -0
- package/claude/skills/session-relay/SKILL.md +2 -0
- package/claude/skills/session-resume/SKILL.md +2 -0
- package/claude/skills/session-worktree/SKILL.md +3 -11
- package/claude/skills/session-worktree/references/install-command.md +20 -0
- package/claude/skills/sketch-design/SKILL.md +2 -0
- package/claude/skills/systematic-debugging/SKILL.md +2 -0
- package/claude/skills/target-check/SKILL.md +2 -0
- package/claude/skills/target-setup/SKILL.md +2 -0
- package/claude/skills/target-setup/references/detect.md +8 -0
- package/claude/skills/target-setup/references/indexes.md +13 -0
- package/claude/skills/target-setup/references/verify.md +8 -0
- package/claude/skills/task-board/SKILL.md +2 -0
- package/claude/skills/teach-workspace/SKILL.md +5 -15
- package/claude/skills/teach-workspace/references/lesson-build.md +75 -0
- package/claude/skills/teach-workspace/references/lesson-craft.md +13 -69
- package/claude/skills/teach-workspace/references/revisit.md +22 -0
- package/claude/skills/test-craft/SKILL.md +2 -0
- package/claude/skills/test-first/SKILL.md +4 -0
- package/claude/skills/test-first/references/adopted.md +36 -0
- package/claude/skills/ui-checklist/SKILL.md +2 -0
- package/claude/skills/ux-audit/SKILL.md +2 -0
- package/claude/skills/ux-measure/SKILL.md +2 -0
- package/claude/skills/ux-walkthrough/SKILL.md +2 -0
- package/claude/skills/video-craft/SKILL.md +2 -0
- package/claude/skills/write-human/SKILL.md +3 -1
- package/claude/skills/write-human/references/machine-tells.md +13 -0
- package/claude/skills/youtube-transcripts/SKILL.md +2 -0
- package/docs/agents/canvas.md +24 -6
- package/docs/agents/capture.md +6 -6
- package/docs/agents/commands.md +5 -5
- package/docs/agents/index.md +5 -5
- package/docs/agents/install-and-sync.md +7 -0
- package/docs/agents/key-changes.md +1 -1
- package/docs/agents/pr-evidence.md +38 -15
- package/docs/agents/pr-local.md +19 -13
- package/docs/agents/pr-preview.md +44 -21
- package/docs/agents/records-stale.md +22 -9
- package/docs/agents/records.md +1 -1
- package/docs/agents/sandbox.md +29 -5
- package/docs/agents/scripting.md +8 -1
- package/docs/agents/skills-audit.md +11 -3
- package/docs/agents/slides.md +15 -3
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/index.md +1 -0
- package/docs/workflow/operating-model.md +5 -6
- package/docs/workflow/parallel-features.md +23 -2
- package/docs/workflow/skill-map-on-request.md +42 -0
- package/docs/workflow/skill-map.md +3 -34
- package/docs/workflow/zshrc-aliases.md +1 -1
- package/package.json +1 -1
- package/scripts/core/regen-hero.ts +1 -1
- package/scripts/lib/sandbox-git.sh +1 -9
- package/scripts/lib/sandbox-path.sh +6 -162
- package/scripts/sandbox-hook.sh +99 -0
- package/src/audits/run.ts +1 -1
- package/src/canvas/address.ts +12 -3
- package/src/canvas/capture.ts +1 -9
- package/src/canvas/client/inspector/align-grid.tsx +162 -0
- package/src/canvas/client/inspector/color-field.tsx +398 -0
- package/src/canvas/client/inspector/color-inputs.tsx +139 -0
- package/src/canvas/client/inspector/color-picker.tsx +668 -0
- package/src/canvas/client/inspector/color-space.ts +240 -0
- package/src/canvas/client/inspector/field.tsx +5 -1
- package/src/canvas/client/inspector/fields.css +547 -17
- package/src/canvas/client/inspector/size-field.tsx +69 -0
- package/src/canvas/client/inspector/values.ts +166 -1
- package/src/canvas/client/inspector.tsx +236 -73
- package/src/canvas/client/selection.tsx +197 -0
- package/src/canvas/client/shell.css +104 -12
- package/src/canvas/client/state.ts +69 -9
- package/src/canvas/client/surface.tsx +147 -22
- package/src/canvas/content.ts +50 -1
- package/src/canvas/edit.ts +37 -4
- package/src/canvas/editing.ts +203 -0
- package/src/canvas/global.d.ts +15 -0
- package/src/canvas/server.ts +149 -12
- package/src/canvas/tokens.ts +59 -17
- package/src/capture/render.ts +45 -7
- package/src/capture/sources.ts +43 -0
- package/src/census/count.ts +2 -2
- package/src/claude/cases/workflow.ts +5 -0
- package/src/claude/seeds.ts +2 -2
- package/src/claude/skills-audit.ts +96 -14
- package/src/claude/skills-drift.ts +1 -1
- package/src/claude/skills-families.ts +36 -0
- package/src/claude/skills-list.ts +76 -15
- package/src/commands/audits.ts +2 -2
- package/src/commands/canvas.ts +179 -2
- package/src/commands/claude.ts +94 -11
- package/src/commands/design.ts +142 -2
- package/src/commands/feedback.ts +3 -3
- package/src/commands/gov.ts +1 -1
- package/src/commands/hooks.ts +1 -1
- package/src/commands/indexes.ts +1 -1
- package/src/commands/init.ts +1 -1
- package/src/commands/intake.ts +1 -1
- package/src/commands/migrate.ts +1 -1
- package/src/commands/pr.ts +105 -58
- package/src/commands/records.ts +89 -7
- package/src/commands/repo.ts +1 -1
- package/src/commands/sandbox.ts +103 -11
- package/src/commands/sessions.ts +1 -1
- package/src/commands/slides.ts +31 -77
- package/src/commands/sync.ts +1 -1
- package/src/commands/tasks.ts +2 -2
- package/src/commands/teach.ts +1 -1
- package/src/commands/transcripts.ts +1 -1
- package/src/commands/worktrees.ts +1 -1
- package/src/comments/scan.ts +1 -1
- package/src/comments/trend.ts +1 -1
- package/src/context/audit.ts +1 -1
- package/src/context/citations.ts +1 -1
- package/src/context/classify/extract.ts +2 -2
- package/src/design/project-fonts.ts +392 -0
- package/src/design/tokens.ts +2 -18
- package/src/docs/read.ts +1 -1
- package/src/{github.ts → feedback/github.ts} +2 -2
- package/src/{client-commands.ts → gate/client-commands.ts} +1 -1
- package/src/gate/measures.ts +53 -13
- package/src/gate/sequencer.ts +1 -1
- package/src/gate/stages.ts +9 -0
- package/src/{git-files.ts → git/files.ts} +2 -2
- package/src/{git-ignore.ts → git/ignore.ts} +1 -1
- package/src/{worktree.ts → git/worktree.ts} +1 -1
- package/src/gov/citations.ts +2 -2
- package/src/gov/install.ts +1 -1
- package/src/gov/payload.ts +1 -1
- package/src/gov/superseded.ts +3 -3
- package/src/gov/test-order.ts +2 -2
- package/src/help.ts +1 -1
- package/src/hooks/post-merge.ts +1 -1
- package/src/indexes/walk.ts +1 -1
- package/src/labels/audit.ts +1 -1
- package/src/markdown/ceiling.ts +1 -1
- package/src/markdown/files.ts +1 -1
- package/src/migrate/apply.ts +1 -1
- package/src/pr/bijection.ts +1 -1
- package/src/pr/evidence.ts +96 -43
- package/src/pr/local.ts +16 -10
- package/src/pr/preview.ts +78 -2
- package/src/record-root.ts +1 -0
- package/src/records/backup.ts +2 -1
- package/src/records/push-guard.ts +1 -1
- package/src/records/stale-canonical.ts +146 -0
- package/src/records/stale.ts +1 -1
- package/src/sandbox/headless/dispatch.ts +264 -0
- package/src/sandbox/headless/run.ts +653 -0
- package/src/sandbox/headless/watch.ts +145 -0
- package/src/sandbox/hooks.ts +128 -0
- package/src/sandbox/provision.ts +607 -0
- package/src/sandbox/tree.ts +99 -4
- package/src/secrets/marker.ts +2 -2
- package/src/secrets/scan.ts +2 -2
- package/src/sessions/claim.ts +1 -1
- package/src/sessions/resolve.ts +1 -1
- package/src/sessions/transfer/bundle.ts +1 -1
- package/src/shipped/references.ts +2 -2
- package/src/slides/convert/export.ts +2 -16
- package/src/slides/decks.ts +110 -0
- package/src/slides/package/fonts.ts +6 -2
- package/src/standards/audit.ts +2 -2
- package/src/standards/read.ts +1 -1
- package/src/sync/check.ts +2 -2
- package/src/sync/engine.ts +2 -2
- package/src/sync/git.ts +1 -1
- package/src/sync/history.ts +1 -1
- package/src/sync/reverse.ts +1 -1
- package/src/sync/seeds-report.ts +1 -1
- package/src/sync/target.ts +1 -1
- package/src/sync/workflow.ts +1 -1
- package/src/targets/pulls.ts +2 -2
- package/src/targets/sweep.ts +2 -2
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/tasks/reach.ts +1 -1
- package/src/tasks/trunk.ts +1 -1
- package/src/teach/nav.ts +1 -1
- package/src/tooling/inject.ts +2 -2
- package/src/tooling/subfolder.ts +1 -1
- package/src/web/readme-citations.ts +2 -2
- package/src/worktrees/bare-flag.ts +1 -1
- package/src/worktrees/reclaim.ts +2 -2
- package/src/worktrees/remove.ts +1 -1
- package/standards/architecture.md +1 -0
- package/standards/canvas.md +3 -2
- package/standards/requirements.md +2 -0
- package/standards/skill-practice.md +2 -1
- package/standards/skill.md +2 -0
- package/standards/wiki.md +12 -3
- package/tooling/base/configs/.github/workflows/pr-local-preview.yml +2 -2
- package/scripts/lib/sandbox-dispatch.sh +0 -190
- package/scripts/manage-sandbox.sh +0 -472
- package/src/slides/layouts.ts +0 -478
- package/src/slides/parse.ts +0 -84
- package/src/slides/render.ts +0 -110
- package/src/slides/styles.ts +0 -96
- /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
- /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
- /package/src/{github-format.ts → feedback/github-format.ts} +0 -0
- /package/src/{git-base.ts → git/base.ts} +0 -0
- /package/src/{git-env.ts → git/env.ts} +0 -0
- /package/src/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
- /package/src/{binary.ts → sweep/binary.ts} +0 -0
- /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
- /package/src/{copy.ts → sync/copy.ts} +0 -0
- /package/src/{target.ts → targets/validate.ts} +0 -0
- /package/src/{paths.ts → tasks/paths.ts} +0 -0
- /package/src/{seed-marker.ts → tooling/seed-marker.ts} +0 -0
package/src/capture/render.ts
CHANGED
|
@@ -2,9 +2,13 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
|
2
2
|
import { basename, dirname } from 'node:path'
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
4
|
import { chromium } from 'playwright-core'
|
|
5
|
-
import type { Browser, Page } from 'playwright-core'
|
|
5
|
+
import type { Browser, Locator, Page } from 'playwright-core'
|
|
6
6
|
import type { CaptureSource } from '@/capture/sources'
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
isGenericFamily,
|
|
9
|
+
resolveCaptureSources,
|
|
10
|
+
textFamilies,
|
|
11
|
+
} from '@/capture/sources'
|
|
8
12
|
import { formatStamp, hashSource, stampPath } from '@/capture/stamp'
|
|
9
13
|
|
|
10
14
|
/**
|
|
@@ -97,15 +101,22 @@ async function captureOne(
|
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
await page.evaluate(() => document.fonts.ready.then(() => undefined))
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
)
|
|
103
|
-
if (family && !(await resolvesFont(page, family))) {
|
|
104
|
+
const declarations = await renderedTextFamilies(element)
|
|
105
|
+
const generic = declarations.find(isGenericFamily)
|
|
106
|
+
if (generic !== undefined) {
|
|
104
107
|
return failed(
|
|
105
108
|
source,
|
|
106
|
-
`${
|
|
109
|
+
`${generic.split(',')[0]?.trim()} is a generic family that resolves differently per machine, so name a real font`,
|
|
107
110
|
)
|
|
108
111
|
}
|
|
112
|
+
for (const family of textFamilies(declarations)) {
|
|
113
|
+
if (!(await resolvesFont(page, family))) {
|
|
114
|
+
return failed(
|
|
115
|
+
source,
|
|
116
|
+
`${family} is not installed, so the capture would rewrap against a fallback`,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
109
120
|
|
|
110
121
|
mkdirSync(dirname(source.pngPath), { recursive: true })
|
|
111
122
|
const png = await element.screenshot({ omitBackground: true })
|
|
@@ -170,6 +181,33 @@ function failed(source: CaptureSource, reason: string): CaptureResult {
|
|
|
170
181
|
return { status: 'failed', source: sourceIdentifier(source), reason }
|
|
171
182
|
}
|
|
172
183
|
|
|
184
|
+
/**
|
|
185
|
+
* The computed families of the captured element and of each element under it
|
|
186
|
+
* holding a non-blank text node of its own that draws a box, which is the text
|
|
187
|
+
* the screenshot shows. Testing the text node's box rather than the element's
|
|
188
|
+
* keeps out `<style>`, `<script>`, `<title>`, and anything hidden, whose
|
|
189
|
+
* computed family is the browser default and would refuse every source that
|
|
190
|
+
* styles a descendant rather than the root, and keeps in text directly inside a
|
|
191
|
+
* `display: contents` element, which draws no box of its own.
|
|
192
|
+
*
|
|
193
|
+
* Returns nothing when no element holds text, since a font cannot rewrap text
|
|
194
|
+
* that is not there, so a container of iframes skips the probe.
|
|
195
|
+
*/
|
|
196
|
+
async function renderedTextFamilies(element: Locator): Promise<string[]> {
|
|
197
|
+
return element.evaluate((root) => {
|
|
198
|
+
const range = document.createRange()
|
|
199
|
+
const holdsText = (node: Element): boolean =>
|
|
200
|
+
[...node.childNodes].some((child) => {
|
|
201
|
+
if (child.nodeType !== Node.TEXT_NODE) return false
|
|
202
|
+
if ((child.textContent ?? '').trim() === '') return false
|
|
203
|
+
range.selectNodeContents(child)
|
|
204
|
+
return range.getClientRects().length > 0
|
|
205
|
+
})
|
|
206
|
+
const texts = [root, ...root.querySelectorAll('*')].filter(holdsText)
|
|
207
|
+
return texts.map((node) => getComputedStyle(node).fontFamily)
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
173
211
|
/**
|
|
174
212
|
* Compares the family's text metrics against a family that cannot exist. Equal
|
|
175
213
|
* widths mean the browser fell through to its default for both, which is the
|
package/src/capture/sources.ts
CHANGED
|
@@ -66,3 +66,46 @@ export function primaryFontFamily(declaration: string): string {
|
|
|
66
66
|
const first = declaration.split(',')[0]?.trim() ?? ''
|
|
67
67
|
return first.replace(/^['"]|['"]$/g, '')
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The CSS generic family keywords. A generic names no font, so each machine
|
|
72
|
+
* maps it to whatever it has, and a capture measured against one cannot
|
|
73
|
+
* reproduce on another.
|
|
74
|
+
*/
|
|
75
|
+
export const GENERIC_FAMILIES: ReadonlySet<string> = new Set([
|
|
76
|
+
'serif',
|
|
77
|
+
'sans-serif',
|
|
78
|
+
'monospace',
|
|
79
|
+
'cursive',
|
|
80
|
+
'fantasy',
|
|
81
|
+
'system-ui',
|
|
82
|
+
'ui-serif',
|
|
83
|
+
'ui-sans-serif',
|
|
84
|
+
'ui-monospace',
|
|
85
|
+
'ui-rounded',
|
|
86
|
+
'math',
|
|
87
|
+
'emoji',
|
|
88
|
+
'fangsong',
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether the first family of a computed `font-family` is a generic keyword.
|
|
93
|
+
* Takes the raw declaration rather than `primaryFontFamily`'s result, since
|
|
94
|
+
* quoting is what separates the keyword from a family that happens to share its
|
|
95
|
+
* name, and the stripped form has lost it. Keywords match case-insensitively.
|
|
96
|
+
*/
|
|
97
|
+
export function isGenericFamily(declaration: string): boolean {
|
|
98
|
+
const first = declaration.split(',')[0]?.trim() ?? ''
|
|
99
|
+
return GENERIC_FAMILIES.has(first.toLowerCase())
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reduces the computed `font-family` of each rendered text element to the
|
|
104
|
+
* distinct families a capture has to prove resolved, in document order, so a
|
|
105
|
+
* frame that sets its font on any ancestor of its text is checked against the
|
|
106
|
+
* face the text actually draws in.
|
|
107
|
+
*/
|
|
108
|
+
export function textFamilies(declarations: readonly string[]): string[] {
|
|
109
|
+
const families = declarations.map(primaryFontFamily).filter(Boolean)
|
|
110
|
+
return [...new Set(families)]
|
|
111
|
+
}
|
package/src/census/count.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
2
|
import { extname, join } from 'node:path'
|
|
3
|
-
import { isBinary } from '@/binary'
|
|
4
|
-
import { listRepositoryFiles } from '@/git
|
|
3
|
+
import { isBinary } from '@/sweep/binary'
|
|
4
|
+
import { listRepositoryFiles } from '@/git/files'
|
|
5
5
|
|
|
6
6
|
export type CensusRefusal = 'no-git'
|
|
7
7
|
|
|
@@ -146,6 +146,11 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
146
146
|
'Which of our docs have gone stale, grown too long, or ended up in the wrong file?',
|
|
147
147
|
expect: 'document-health',
|
|
148
148
|
},
|
|
149
|
+
{
|
|
150
|
+
prompt:
|
|
151
|
+
'Are our requirements and architecture docs still true, and when were they last reviewed?',
|
|
152
|
+
expect: 'document-health',
|
|
153
|
+
},
|
|
149
154
|
{
|
|
150
155
|
prompt: 'Open a new entry on the task board for this piece of work.',
|
|
151
156
|
expect: 'task-board',
|
package/src/claude/seeds.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { dirname, join, relative } from 'node:path'
|
|
4
|
-
import { copyPreservingMode } from '@/copy'
|
|
4
|
+
import { copyPreservingMode } from '@/sync/copy'
|
|
5
5
|
import { creationRel, isRecordEntry } from '@/record-root'
|
|
6
|
-
import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
|
|
6
|
+
import { rewritesOnInstall, stripSeedMarker } from '@/tooling/seed-marker'
|
|
7
7
|
import { SURFACE_ENTRIES, surfaceDir } from '@/surface-root'
|
|
8
8
|
|
|
9
9
|
const SEEDS_DIR = join('tooling', 'claude', 'seeds')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
|
+
import { isFamilyKey } from '@/claude/skills-families'
|
|
3
4
|
import { parseFrontmatter, readField } from '@/indexes/frontmatter'
|
|
4
5
|
import { bodyLines, maskDisplayed } from '@/markdown/scan'
|
|
5
6
|
|
|
@@ -40,6 +41,21 @@ export const PRACTICE_SKILLS: readonly string[] = [
|
|
|
40
41
|
join(SHIPPED_CORPUS, 'code-craft'),
|
|
41
42
|
join(SHIPPED_CORPUS, 'build-in-slices'),
|
|
42
43
|
join(SHIPPED_CORPUS, 'deprecation-migration'),
|
|
44
|
+
join(SHIPPED_CORPUS, 'search-craft'),
|
|
45
|
+
join(SHIPPED_CORPUS, 'video-craft'),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Every skill carrying the source ledger, being the practice skills and the
|
|
50
|
+
* skills that teach a craft without the practice closing sections. Kept apart
|
|
51
|
+
* from `PRACTICE_SKILLS` so citing sources never forces those sections onto a
|
|
52
|
+
* skill whose body is not shaped around a practice a session cuts short.
|
|
53
|
+
*/
|
|
54
|
+
export const LEDGER_SKILLS: readonly string[] = [
|
|
55
|
+
...PRACTICE_SKILLS,
|
|
56
|
+
join(SHIPPED_CORPUS, 'design-taste'),
|
|
57
|
+
join(SHIPPED_CORPUS, 'write-human'),
|
|
58
|
+
join(SHIPPED_CORPUS, 'test-first'),
|
|
43
59
|
]
|
|
44
60
|
|
|
45
61
|
/** The closing H2s a practice skill carries, matched exactly outside fences. */
|
|
@@ -52,6 +68,16 @@ export const PRACTICE_SECTIONS: readonly string[] = [
|
|
|
52
68
|
/** The source ledger every practice skill carries beside its body. */
|
|
53
69
|
export const PRACTICE_LEDGER = join('references', 'adopted.md')
|
|
54
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The line count past which a reference opens with a contents list, from
|
|
73
|
+
* Anthropic's skill authoring guidance. Counted over the whole file the way
|
|
74
|
+
* `wc -l` counts it, so "over 100" holds at 101 and not at 100.
|
|
75
|
+
*/
|
|
76
|
+
export const CONTENTS_THRESHOLD = 100
|
|
77
|
+
|
|
78
|
+
/** The H2 a long reference opens with, matched exactly outside fences. */
|
|
79
|
+
export const CONTENTS_SECTION = 'Contents'
|
|
80
|
+
|
|
55
81
|
/**
|
|
56
82
|
* The one reason this audit refuses. A project carrying neither corpus is the
|
|
57
83
|
* ordinary state of a target that has not adopted either skills convention,
|
|
@@ -102,6 +128,8 @@ export interface SkillsAudit {
|
|
|
102
128
|
readonly requirementSections: readonly SkillFinding[]
|
|
103
129
|
readonly datedProvenance: readonly SkillFinding[]
|
|
104
130
|
readonly practiceShape: readonly SkillFinding[]
|
|
131
|
+
readonly referenceContents: readonly SkillFinding[]
|
|
132
|
+
readonly family: readonly SkillFinding[]
|
|
105
133
|
}
|
|
106
134
|
|
|
107
135
|
interface SkillSource {
|
|
@@ -110,6 +138,8 @@ interface SkillSource {
|
|
|
110
138
|
readonly hasReadme: boolean
|
|
111
139
|
readonly name: string | undefined
|
|
112
140
|
readonly description: string | undefined
|
|
141
|
+
/** `metadata.family`, undefined when absent, blank, or not a string. */
|
|
142
|
+
readonly family: string | undefined
|
|
113
143
|
/** Undefined when the folder carries no `REQUIREMENT.md` at all. */
|
|
114
144
|
readonly requirementHeadings: readonly string[] | undefined
|
|
115
145
|
/** Every H2 in `SKILL.md` outside a fence, for the practice shape. */
|
|
@@ -126,6 +156,7 @@ interface SkillSource {
|
|
|
126
156
|
interface SkillText {
|
|
127
157
|
readonly rel: string
|
|
128
158
|
readonly text: string
|
|
159
|
+
readonly isReference: boolean
|
|
129
160
|
}
|
|
130
161
|
|
|
131
162
|
/**
|
|
@@ -177,6 +208,10 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
|
|
|
177
208
|
),
|
|
178
209
|
...sources.flatMap(practiceFindings),
|
|
179
210
|
],
|
|
211
|
+
referenceContents: sources.flatMap((source) =>
|
|
212
|
+
source.bodies.flatMap(contentsFindings),
|
|
213
|
+
),
|
|
214
|
+
family: sources.flatMap(familyFindings),
|
|
180
215
|
}
|
|
181
216
|
}
|
|
182
217
|
|
|
@@ -231,18 +266,17 @@ async function readSkill(
|
|
|
231
266
|
hasReadme: existsSync(join(skillDir, 'README.md')),
|
|
232
267
|
name: declared(readField(fields, 'name')),
|
|
233
268
|
description: declared(readField(fields, 'description')),
|
|
269
|
+
family: declaredFamily(fields?.fields.metadata),
|
|
234
270
|
requirementHeadings:
|
|
235
271
|
requirement === undefined ? undefined : headings(requirement),
|
|
236
|
-
sections:
|
|
237
|
-
.filter((line) => !line.fenced)
|
|
238
|
-
.map((line) => SECTION.exec(line.text)?.[1])
|
|
239
|
-
.filter((text): text is string => text !== undefined),
|
|
272
|
+
sections: sectionsOf(body),
|
|
240
273
|
hasLedger: existsSync(join(skillDir, PRACTICE_LEDGER)),
|
|
241
274
|
bodies: [
|
|
242
|
-
{ rel: join(skillRel, 'SKILL.md'), text: body },
|
|
275
|
+
{ rel: join(skillRel, 'SKILL.md'), text: body, isReference: false },
|
|
243
276
|
...references.map((path, index) => ({
|
|
244
277
|
rel: join(skillRel, path),
|
|
245
278
|
text: referenceTexts[index] ?? '',
|
|
279
|
+
isReference: true,
|
|
246
280
|
})),
|
|
247
281
|
],
|
|
248
282
|
}
|
|
@@ -257,6 +291,21 @@ function declared(value: string | undefined): string | undefined {
|
|
|
257
291
|
return trimmed === undefined || trimmed === '' ? undefined : trimmed
|
|
258
292
|
}
|
|
259
293
|
|
|
294
|
+
function declaredFamily(metadata: unknown): string | undefined {
|
|
295
|
+
if (typeof metadata !== 'object' || metadata === null) return undefined
|
|
296
|
+
if (Array.isArray(metadata)) return undefined
|
|
297
|
+
const { family } = metadata as Record<string, unknown>
|
|
298
|
+
return typeof family === 'string' ? declared(family) : undefined
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Every H2 outside a fence, the one level both exact-section checks read. */
|
|
302
|
+
function sectionsOf(source: string): string[] {
|
|
303
|
+
return bodyLines(source)
|
|
304
|
+
.filter((line) => !line.fenced)
|
|
305
|
+
.map((line) => SECTION.exec(line.text)?.[1])
|
|
306
|
+
.filter((text): text is string => text !== undefined)
|
|
307
|
+
}
|
|
308
|
+
|
|
260
309
|
function headings(source: string): string[] {
|
|
261
310
|
return source
|
|
262
311
|
.split('\n')
|
|
@@ -311,7 +360,7 @@ function missingPracticeSkills(
|
|
|
311
360
|
): SkillFinding[] {
|
|
312
361
|
if (!hasShippedCorpus) return []
|
|
313
362
|
const found = new Set(sources.map((source) => source.rel))
|
|
314
|
-
return
|
|
363
|
+
return LEDGER_SKILLS.filter((rel) => !found.has(rel)).map((rel) => ({
|
|
315
364
|
rel,
|
|
316
365
|
detail: 'missing skill: no folder under the shipped corpus',
|
|
317
366
|
}))
|
|
@@ -323,14 +372,15 @@ function missingPracticeSkills(
|
|
|
323
372
|
* standard never names.
|
|
324
373
|
*/
|
|
325
374
|
function practiceFindings(source: SkillSource): SkillFinding[] {
|
|
326
|
-
if (!
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
375
|
+
if (!LEDGER_SKILLS.includes(source.rel)) return []
|
|
376
|
+
|
|
377
|
+
const isPractice = PRACTICE_SKILLS.includes(source.rel)
|
|
378
|
+
const sections = (isPractice ? PRACTICE_SECTIONS : [])
|
|
379
|
+
.filter((section) => !source.sections.includes(section))
|
|
380
|
+
.map((section) => ({
|
|
381
|
+
rel: source.rel,
|
|
382
|
+
detail: `missing section: ${section}`,
|
|
383
|
+
}))
|
|
334
384
|
const ledger = source.hasLedger
|
|
335
385
|
? []
|
|
336
386
|
: [{ rel: source.rel, detail: `missing ledger: ${PRACTICE_LEDGER}` }]
|
|
@@ -338,6 +388,38 @@ function practiceFindings(source: SkillSource): SkillFinding[] {
|
|
|
338
388
|
return [...sections, ...ledger]
|
|
339
389
|
}
|
|
340
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Asks only the shipped corpus for a family, since the family names the skill
|
|
393
|
+
* map group a skill's row sits under and an internal skill takes no row.
|
|
394
|
+
*/
|
|
395
|
+
function familyFindings(source: SkillSource): SkillFinding[] {
|
|
396
|
+
if (dirname(source.rel) !== SHIPPED_CORPUS) return []
|
|
397
|
+
if (source.family === undefined) {
|
|
398
|
+
return [
|
|
399
|
+
{ rel: source.rel, detail: 'frontmatter declares no metadata.family' },
|
|
400
|
+
]
|
|
401
|
+
}
|
|
402
|
+
if (isFamilyKey(source.family)) return []
|
|
403
|
+
return [{ rel: source.rel, detail: `unknown family: ${source.family}` }]
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Reads references alone, since `SKILL.md` carries its own length checkpoint
|
|
408
|
+
* and no contents rule. Frontmatter counts toward the length, so the count a
|
|
409
|
+
* finding names is the one `wc -l` prints beside it.
|
|
410
|
+
*/
|
|
411
|
+
function contentsFindings(source: SkillText): SkillFinding[] {
|
|
412
|
+
if (!source.isReference) return []
|
|
413
|
+
|
|
414
|
+
const lines = source.text.replace(/\n$/, '').split('\n').length
|
|
415
|
+
if (lines <= CONTENTS_THRESHOLD) return []
|
|
416
|
+
if (sectionsOf(source.text).includes(CONTENTS_SECTION)) return []
|
|
417
|
+
|
|
418
|
+
return [
|
|
419
|
+
{ rel: source.rel, detail: `${lines} lines, no ## ${CONTENTS_SECTION}` },
|
|
420
|
+
]
|
|
421
|
+
}
|
|
422
|
+
|
|
341
423
|
/**
|
|
342
424
|
* A date inside a fence or a code span is example data a reader is shown, such
|
|
343
425
|
* as a sample frontmatter block, so only prose outside both is read.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { execaSync } from 'execa'
|
|
4
|
-
import { gitEnv } from '@/git
|
|
4
|
+
import { gitEnv } from '@/git/env'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The shipped corpus alone. A session loads a body from the plugin root, which
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface SkillFamily {
|
|
2
|
+
/** The value a shipped `SKILL.md` declares as `metadata.family`. */
|
|
3
|
+
readonly key: string
|
|
4
|
+
/** The skill map heading the family's rows sit under, verbatim. */
|
|
5
|
+
readonly group: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The one vocabulary a shipped skill's family is drawn from, in the order the
|
|
10
|
+
* skill map reads across its two pages. A heading is the map's prose and a key
|
|
11
|
+
* is what frontmatter carries, so renaming a heading touches this constant and
|
|
12
|
+
* no skill.
|
|
13
|
+
*
|
|
14
|
+
* Import-free on purpose: `skills-list.ts` and `skills-audit.ts` both read it,
|
|
15
|
+
* and the first already imports the second.
|
|
16
|
+
*/
|
|
17
|
+
export const SKILL_FAMILIES: readonly SkillFamily[] = [
|
|
18
|
+
{ key: 'setup', group: 'Set up a project' },
|
|
19
|
+
{ key: 'decide', group: 'Decide what to build' },
|
|
20
|
+
{ key: 'build', group: 'Build the feature' },
|
|
21
|
+
{ key: 'check', group: 'Check the work before it leaves the branch' },
|
|
22
|
+
{ key: 'ship', group: 'Ship it' },
|
|
23
|
+
{ key: 'after-pr', group: 'After the pull request opens' },
|
|
24
|
+
{ key: 'parallel', group: 'Run several tracks at once' },
|
|
25
|
+
{ key: 'upkeep', group: 'Keep the project current with the toolkit' },
|
|
26
|
+
{ key: 'generate', group: 'Generate an artifact on demand' },
|
|
27
|
+
{ key: 'answer', group: 'Answer a question at any point' },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
export function familyKeys(): string[] {
|
|
31
|
+
return SKILL_FAMILIES.map((family) => family.key)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isFamilyKey(value: string): boolean {
|
|
35
|
+
return SKILL_FAMILIES.some((family) => family.key === value)
|
|
36
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
3
|
import { CORPORA } from '@/claude/skills-audit'
|
|
4
|
+
import { isFamilyKey, SKILL_FAMILIES } from '@/claude/skills-families'
|
|
4
5
|
|
|
5
6
|
const FRONTMATTER = /^---\n([\s\S]*?)\n---/
|
|
6
7
|
|
|
@@ -8,6 +9,11 @@ export interface SkillListing {
|
|
|
8
9
|
readonly name: string
|
|
9
10
|
readonly description: string
|
|
10
11
|
readonly requirement: boolean
|
|
12
|
+
/**
|
|
13
|
+
* The `metadata.family` the frontmatter declares, read as written. Whether it
|
|
14
|
+
* names a vocabulary key is the audit's finding, not the listing's.
|
|
15
|
+
*/
|
|
16
|
+
readonly family: string | null
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
export interface SkillsCorpus {
|
|
@@ -72,28 +78,83 @@ export function listSkillsAt(skillsRoot: string): SkillListing[] {
|
|
|
72
78
|
}),
|
|
73
79
|
].sort()
|
|
74
80
|
|
|
75
|
-
return paths.map((path) =>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
return paths.map((path) => {
|
|
82
|
+
const { description, family } = readListedFields(join(skillsRoot, path))
|
|
83
|
+
return {
|
|
84
|
+
name: dirname(path),
|
|
85
|
+
description,
|
|
86
|
+
requirement: existsSync(
|
|
87
|
+
join(skillsRoot, dirname(path), 'REQUIREMENT.md'),
|
|
88
|
+
),
|
|
89
|
+
family,
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface FamilyGroup {
|
|
95
|
+
readonly key: string
|
|
96
|
+
readonly group: string
|
|
97
|
+
readonly skills: readonly string[]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface FamilyGrouping {
|
|
101
|
+
/** Vocabulary order, holding only the families some listing declares. */
|
|
102
|
+
readonly groups: readonly FamilyGroup[]
|
|
103
|
+
/** Listings whose family is missing or names no vocabulary key. */
|
|
104
|
+
readonly unassigned: readonly string[]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function groupByFamily(
|
|
108
|
+
listings: readonly SkillListing[],
|
|
109
|
+
): FamilyGrouping {
|
|
110
|
+
const groups = SKILL_FAMILIES.map((family) => ({
|
|
111
|
+
...family,
|
|
112
|
+
skills: listings
|
|
113
|
+
.filter((listing) => listing.family === family.key)
|
|
114
|
+
.map((listing) => listing.name),
|
|
115
|
+
})).filter((group) => group.skills.length > 0)
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
groups,
|
|
119
|
+
unassigned: listings
|
|
120
|
+
.filter(
|
|
121
|
+
(listing) => listing.family === null || !isFamilyKey(listing.family),
|
|
122
|
+
)
|
|
123
|
+
.map((listing) => listing.name),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface ListedFields {
|
|
128
|
+
readonly description: string
|
|
129
|
+
readonly family: string | null
|
|
80
130
|
}
|
|
81
131
|
|
|
82
132
|
/**
|
|
83
|
-
* Returns an empty description rather than throwing on a
|
|
84
|
-
* frontmatter is missing or unparseable, so one malformed file
|
|
85
|
-
* the rest of the catalog from a caller counting it.
|
|
133
|
+
* Returns an empty description and a null family rather than throwing on a
|
|
134
|
+
* skill whose frontmatter is missing or unparseable, so one malformed file
|
|
135
|
+
* does not hide the rest of the catalog from a caller counting it.
|
|
86
136
|
*/
|
|
87
|
-
function
|
|
137
|
+
function readListedFields(path: string): ListedFields {
|
|
138
|
+
const empty = { description: '', family: null }
|
|
88
139
|
const match = FRONTMATTER.exec(readFileSync(path, 'utf8'))
|
|
89
|
-
if (!match) return
|
|
140
|
+
if (!match) return empty
|
|
90
141
|
|
|
142
|
+
let parsed: unknown
|
|
91
143
|
try {
|
|
92
|
-
|
|
93
|
-
if (typeof parsed !== 'object' || parsed === null) return ''
|
|
94
|
-
const { description } = parsed as Record<string, unknown>
|
|
95
|
-
return typeof description === 'string' ? description : ''
|
|
144
|
+
parsed = Bun.YAML.parse(match[1])
|
|
96
145
|
} catch {
|
|
97
|
-
return
|
|
146
|
+
return empty
|
|
98
147
|
}
|
|
148
|
+
if (!isRecord(parsed)) return empty
|
|
149
|
+
|
|
150
|
+
const { description, metadata } = parsed
|
|
151
|
+
const family = isRecord(metadata) ? metadata.family : undefined
|
|
152
|
+
return {
|
|
153
|
+
description: typeof description === 'string' ? description : '',
|
|
154
|
+
family: typeof family === 'string' ? family : null,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
159
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
99
160
|
}
|
package/src/commands/audits.ts
CHANGED
|
@@ -19,9 +19,9 @@ import {
|
|
|
19
19
|
type Summary,
|
|
20
20
|
summarize,
|
|
21
21
|
} from '@/audits/run'
|
|
22
|
-
import { gitEnv } from '@/git
|
|
22
|
+
import { gitEnv } from '@/git/env'
|
|
23
23
|
import { intro, logError, logInfo, logStep, logWarn, outro, plural } from '@/ui'
|
|
24
|
-
import { currentWorktreeRoot } from '@/worktree'
|
|
24
|
+
import { currentWorktreeRoot } from '@/git/worktree'
|
|
25
25
|
|
|
26
26
|
interface RunCommandOptions {
|
|
27
27
|
readonly json?: boolean
|