@erclx/aitk 0.51.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/docs/agents/context-audit-checks.md +3 -1
- package/docs/agents/context-audit.md +35 -2
- package/docs/agents/index.md +1 -1
- package/docs/agents/install-and-sync.md +6 -0
- package/docs/ai-workflow.md +1 -1
- package/docs/operating-model.md +4 -4
- package/docs/visual-design-workflow.md +16 -16
- package/docs/zshrc-aliases.md +1 -1
- package/package.json +1 -1
- package/scripts/core/verify.sh +83 -0
- package/src/claude/seeds.ts +17 -3
- package/src/commands/context.ts +34 -7
- package/src/context/audit.ts +14 -0
- package/src/context/gate.ts +43 -0
- package/src/seed-marker.ts +74 -0
- package/src/sync/seeds-report.ts +16 -1
- package/src/tooling/inject.ts +17 -1
|
@@ -13,7 +13,9 @@ The required-section check reports what does not declare `## Overview` and `## L
|
|
|
13
13
|
|
|
14
14
|
Which unit answers depends on the folder. Entries of the folder named under `.claude/` are one domain each, so each answers for itself and a finding names the entry. Entries of a folder a domain split into describe that one domain between them, so any sibling answers and a finding names the folder. Holding a split folder to the rule per file would report every child beside its `overview.md`, and rolling the named folder up would let one conforming entry stand in for every other domain sitting next to it.
|
|
15
15
|
|
|
16
|
-
It reports rather than gates, the closer call because a missing section reads more like a fact than the other judgments. What settles it is that the standard sanctions omitting `## Layout` from a domain owning no paths in the repo, and no measure separates that from an entry that forgot it. A domain covering only external tools is that case, and it reports on every run. The JSON record carries the findings as `missingSections` and the list as `checkpoints.requiredSections`.
|
|
16
|
+
It reports rather than gates by default, the closer call because a missing section reads more like a fact than the other judgments. What settles it is that the standard sanctions omitting `## Layout` from a domain owning no paths in the repo, and no measure separates that from an entry that forgot it. A domain covering only external tools is that case, and it reports on every run. The JSON record carries the findings as `missingSections` and the list as `checkpoints.requiredSections`.
|
|
17
|
+
|
|
18
|
+
`--gate` promotes the finding to a failing exit code, which the seed stage runs and no other caller does. That mode needs an answer to the sanctioned omission above, so a file declaring `stub: true` in its frontmatter is dropped before the check and reported nowhere. Both are described in `context-audit.md`.
|
|
17
19
|
|
|
18
20
|
## Length and depth
|
|
19
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Context audit
|
|
3
|
-
description: Running the audit, its flags and folder scope, the exit codes,
|
|
3
|
+
description: Running the audit, its flags and folder scope, the exit codes, the citation gate, and the widened gate the seed stage runs
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Context audit
|
|
@@ -13,6 +13,7 @@ aitk context audit --json
|
|
|
13
13
|
aitk context audit --citations-only
|
|
14
14
|
aitk context audit --folder context,diagrams
|
|
15
15
|
aitk context audit --folder docs
|
|
16
|
+
aitk context audit tooling/base/seeds --gate
|
|
16
17
|
```
|
|
17
18
|
|
|
18
19
|
| Option | Behavior |
|
|
@@ -20,6 +21,9 @@ aitk context audit --folder docs
|
|
|
20
21
|
| `--json` | Add a machine-readable record on stdout, keeping the frame |
|
|
21
22
|
| `--folder <list>` | Comma-separated folder names (default: the three below) |
|
|
22
23
|
| `--citations-only` | Run the gating citation check alone, printing nothing when it passes |
|
|
24
|
+
| `--gate` | Fail on a missing required section or index drift as well |
|
|
25
|
+
|
|
26
|
+
`--citations-only` and `--gate` refuse together. The first runs the citation check alone, so the two findings the second adds are never measured, and a run honoring both would exit clean on a file short a required section.
|
|
23
27
|
|
|
24
28
|
## Folder scope
|
|
25
29
|
|
|
@@ -31,7 +35,36 @@ A run where no requested name resolves refuses, whichever list it read. Naming t
|
|
|
31
35
|
|
|
32
36
|
## Exit codes
|
|
33
37
|
|
|
34
|
-
Exit codes are `0` for a clean run, `1` for a refusal, and `2` for
|
|
38
|
+
Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a gating finding. An unresolved citation gates under every mode. Length, depth, bullet weight, table, and provenance findings print and return `0` under every mode, because each is a judgment and failing a push on one would make the check something to route around.
|
|
39
|
+
|
|
40
|
+
Required-section and index findings sit between the two. Both are answerable from the file rather than weighed, so `--gate` promotes them to failing codes while a bare run leaves them advisory. The toolkit runs the bare form against itself and the widened form against the seed tree, described below.
|
|
41
|
+
|
|
42
|
+
## The seed gate
|
|
43
|
+
|
|
44
|
+
`bun run check` runs `--gate` against every `tooling/<stack>/seeds/` directory carrying a `.claude/` folder, discovered per run so a new stack is covered without a script edit. The seed tree installs into every scaffolded project, so a seed breaking the standard it seeds teaches the wrong shape to each one, and no rule path reaches the tree to report it.
|
|
45
|
+
|
|
46
|
+
The widened gate is correct here and wrong at the project root. A seed is authored once and read by every target, while a context entry in a live project is edited under time pressure by the people who own it. A missing section in the first is a defect shipping outward, and in the second it is a threshold worth reporting and not worth blocking a push over.
|
|
47
|
+
|
|
48
|
+
Coverage follows the index-plus-entry contract, so it reaches seeded entries and the indexes beside them. Seed files sitting directly under `.claude/`, currently `ARCHITECTURE.md`, `DESIGN.md`, and `REQUIREMENTS.md`, belong to no audited folder and stay outside it.
|
|
49
|
+
|
|
50
|
+
The stage prints the entries it measured per root and warns on a root that measured none. A root can resolve an audited folder and hold no entry in it, which `tooling/claude/seeds` does today, so a single pass line over the set would report coverage of a tree nothing opened.
|
|
51
|
+
|
|
52
|
+
It reads the exit code rather than pass against fail, since the two failing codes mean opposite things. A root carrying a `.claude/` that resolves no audited folder refuses at 1 and warns, while a seed short a required section gates at 2 and fails the push. A new stack seeding `.claude/` alone therefore reports what it is rather than a violation it does not have.
|
|
53
|
+
|
|
54
|
+
### Exempting a skeleton
|
|
55
|
+
|
|
56
|
+
A seed that deliberately omits a required section sets `stub: true` in its frontmatter, and the section check then skips it. Every other measure still reads the file, since a stub is exempt from owing sections rather than from being well formed.
|
|
57
|
+
|
|
58
|
+
```markdown
|
|
59
|
+
---
|
|
60
|
+
title: Architecture
|
|
61
|
+
stub: true
|
|
62
|
+
---
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Only `true` counts. A field holding anything else reads as a seed that meant to turn the exemption off, so a typo cannot silence the gate. Both install paths strip the field on the way into a target, which keeps toolkit bookkeeping out of a project whose own tooling would never read it. A block holding nothing else is dropped whole.
|
|
66
|
+
|
|
67
|
+
The exemption exists because the section check has a false-positive class. A standard may sanction omitting a section, and no measure separates that from a file that forgot it, which is why the finding stays advisory everywhere the widened gate is not running. No seed sets the field today.
|
|
35
68
|
|
|
36
69
|
## The citation gate
|
|
37
70
|
|
package/docs/agents/index.md
CHANGED
|
@@ -12,7 +12,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
12
12
|
- [Command catalog](commands.md): Every project-level command and every domain subcommand, plus the shape each domain exposes
|
|
13
13
|
- [Comments](comments.md): Comment density by language and kind, the two structural exclusions, and how the degradation sweep finds its vocabulary
|
|
14
14
|
- [Context audit checks](context-audit-checks.md): What each non-gating check reports, the unit each checkpoint is measured in, and which folders each check reaches
|
|
15
|
-
- [Context audit](context-audit.md): Running the audit, its flags and folder scope, the exit codes,
|
|
15
|
+
- [Context audit](context-audit.md): Running the audit, its flags and folder scope, the exit codes, the citation gate, and the widened gate the seed stage runs
|
|
16
16
|
- [Docs](docs.md): How aitk docs resolves the toolkit's own reference surface from an install root, and how a split domain is named
|
|
17
17
|
- [Indexes](indexes.md): Flags, exit codes, and JSON shape for aitk indexes regen, plus when it auto-stages what it rewrote
|
|
18
18
|
- [Install and sync](install-and-sync.md): What each install and sync verb writes, refuses, or leaves alone, and how drift is attributed in a target project
|
|
@@ -144,6 +144,12 @@ needs a stamp and seeds carry none, so a file history cannot attribute stays
|
|
|
144
144
|
`drifted`. Reconcile the section with `claude-seed-sync`, which merges one
|
|
145
145
|
section at a time rather than replacing a file the project edits.
|
|
146
146
|
|
|
147
|
+
A markdown seed installs rewritten rather than copied, since the `stub: true`
|
|
148
|
+
marker the toolkit's own seed gate reads is stripped on the way in. The
|
|
149
|
+
comparison above runs against what the install would write, so a marked seed a
|
|
150
|
+
target never touched still reports `matching`. Every other seed copies byte for
|
|
151
|
+
byte.
|
|
152
|
+
|
|
147
153
|
`superseded` names a file a newer seed folder replaced, such as `.claude/TASKS.md`
|
|
148
154
|
against the `.claude/tasks/` that now ships. The entry carries `replacedBy` and
|
|
149
155
|
nothing else, and the file is never deleted, since the content belongs to the
|
package/docs/ai-workflow.md
CHANGED
|
@@ -76,7 +76,7 @@ Run one orchestrator at a time. The board is gitignored, so a second session rea
|
|
|
76
76
|
|
|
77
77
|
Before a handoff, the orchestrator checks the plan against the tree rather than reading it: grep each construct it names and count the sites, confirm every phase label it cites is still open, and open each file it describes. A plan goes stale from whatever merged after it was written, and reading cannot catch that.
|
|
78
78
|
|
|
79
|
-
`.claude/plans/`, `.claude/review/`, and `.claude/memory/` all resolve at the main worktree root, so artifacts created in any session are visible from any sibling worktree. See [Claude Code and git worktrees](../wiki/claude-worktrees.md) for the full rule and the domain-level fan-out guidance.
|
|
79
|
+
`.claude/plans/`, `.claude/review/`, and `.claude/memory/` all resolve at the main worktree root, so artifacts created in any session are visible from any sibling worktree. See [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for the full rule and the domain-level fan-out guidance.
|
|
80
80
|
|
|
81
81
|
A plan that ships is archived, never deleted. `aitk:claude-docs` moves it to `.claude/plans-archive/` and retargets the task file's `Plan:` line at the new location, so a completed task still leads to the reasoning behind it. Both folders are gitignored, which is why a deleted plan had no recovery path. A plan cited by more than one task stays put until the last of them closes, since moving it early would strand every other pointer.
|
|
82
82
|
|
package/docs/operating-model.md
CHANGED
|
@@ -10,7 +10,7 @@ A way to build fast and reliably across parallel Claude Code sessions without a
|
|
|
10
10
|
loop and without losing the human review gate. One warm session plans and
|
|
11
11
|
reviews. Cold worker sessions build. The human launches workers and merges. This
|
|
12
12
|
page covers the roles and the loop. For the worktree mechanism (isolation, merge
|
|
13
|
-
order, port collisions), see [Claude Code and git worktrees](../wiki/claude-worktrees.md).
|
|
13
|
+
order, port collisions), see [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md).
|
|
14
14
|
|
|
15
15
|
## Two roles
|
|
16
16
|
|
|
@@ -105,11 +105,11 @@ at two or three worker tracks and split them across the stack so they do not
|
|
|
105
105
|
collide on the same files. Unit checks run freely in many worktrees at once.
|
|
106
106
|
Only fixed-port work (a dev server, an end-to-end run, a screenshot) and
|
|
107
107
|
singleton resources (one local model server, one GPU) serialize. See
|
|
108
|
-
[Claude Code and git worktrees](../wiki/claude-worktrees.md) for merge order and the
|
|
108
|
+
[Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for merge order and the
|
|
109
109
|
port-collision detail.
|
|
110
110
|
|
|
111
111
|
## Related
|
|
112
112
|
|
|
113
|
-
- [Claude Code and git worktrees](../wiki/claude-worktrees.md) for the isolation and fan-out mechanics
|
|
114
|
-
- [Claude Code subagents](../wiki/claude-subagents.md) for in-session parallelism without worktrees
|
|
113
|
+
- [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for the isolation and fan-out mechanics
|
|
114
|
+
- [Claude Code subagents](../wiki/claude/claude-subagents.md) for in-session parallelism without worktrees
|
|
115
115
|
- `.claude/context/claude-plugin/skill-strategy.md` for how the skills in the loop are categorized
|
|
@@ -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-design.md), [visual wireframes](../wiki/visual-wireframes.md), [community skills and plugins](../wiki/community-skills.md), and [community MCP servers](../wiki/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), [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.
|
|
12
12
|
|
|
13
|
-
Two tools anchor tier 1 and tier 2. [Stitch](../wiki/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-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](../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.
|
|
14
14
|
|
|
15
15
|
## Tier 0: prose only
|
|
16
16
|
|
|
@@ -51,16 +51,16 @@ Impeccable, if installed, keeps its own root `DESIGN.md` and `PRODUCT.md` in the
|
|
|
51
51
|
|
|
52
52
|
### Tools
|
|
53
53
|
|
|
54
|
-
- Stitch via MCP at `stitch.googleapis.com/mcp`. Default pick for agent-driven visual generation. Free tier of 400 daily credits. See [Stitch](../wiki/stitch.md).
|
|
55
|
-
- 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/visual-wireframes.md) for setup and footguns.
|
|
56
|
-
- Playwright MCP for browser-side verification. See [Playwright](../wiki/community-mcp-servers.md#playwright-microsoft).
|
|
57
|
-
- Chrome DevTools MCP for live frontend debugging. See [Chrome DevTools](../wiki/community-mcp-servers.md#chrome-devtools-google).
|
|
58
|
-
- Claude Design as the ceiling option for codebase extraction or polished handoff bundles. See [Claude Design](../wiki/claude-design.md).
|
|
54
|
+
- 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).
|
|
55
|
+
- 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.
|
|
56
|
+
- Playwright MCP for browser-side verification. See [Playwright](../wiki/tools/community-mcp-servers.md#playwright-microsoft).
|
|
57
|
+
- Chrome DevTools MCP for live frontend debugging. See [Chrome DevTools](../wiki/tools/community-mcp-servers.md#chrome-devtools-google).
|
|
58
|
+
- Claude Design as the ceiling option for codebase extraction or polished handoff bundles. See [Claude Design](../wiki/claude/claude-design.md).
|
|
59
59
|
|
|
60
60
|
### Skills
|
|
61
61
|
|
|
62
62
|
- Everything from tier 0
|
|
63
|
-
- A frontend design skill to steer visual quality. Pick one of [Impeccable](../wiki/community-skills.md#pbakausimpeccable), [UI/UX Pro Max](../wiki/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.
|
|
63
|
+
- 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.
|
|
64
64
|
|
|
65
65
|
### When to pick
|
|
66
66
|
|
|
@@ -91,9 +91,9 @@ Design happens in a graphical tool. `.claude/DESIGN.md` either regenerates from
|
|
|
91
91
|
|
|
92
92
|
### Tools
|
|
93
93
|
|
|
94
|
-
- Figma desktop app with the [Figma Dev Mode MCP](../wiki/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.
|
|
95
|
-
- 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-design.md).
|
|
96
|
-
- Stitch via MCP as a low-cost complement to either, used for bulk screen generation driven by Claude Code. See [Stitch](../wiki/stitch.md).
|
|
94
|
+
- 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.
|
|
95
|
+
- 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).
|
|
96
|
+
- 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).
|
|
97
97
|
- Playwright and Chrome DevTools MCPs as in tier 1
|
|
98
98
|
|
|
99
99
|
### Skills
|
|
@@ -128,9 +128,9 @@ Zero or one yes: tier 0. Two or three: tier 1. Four: tier 2. Resist over-tiering
|
|
|
128
128
|
|
|
129
129
|
## References
|
|
130
130
|
|
|
131
|
-
- [Stitch](../wiki/stitch.md): Gemini-powered design product with the MCP server that anchors tier 1
|
|
132
|
-
- [Claude Design](../wiki/claude-design.md): first-party hosted design product and handoff bundle
|
|
131
|
+
- [Stitch](../wiki/tools/stitch.md): Gemini-powered design product with the MCP server that anchors tier 1
|
|
132
|
+
- [Claude Design](../wiki/claude/claude-design.md): first-party hosted design product and handoff bundle
|
|
133
133
|
- `.claude/context/claude-plugin/skill-strategy.md`: how to decide between workflow and domain-knowledge skills
|
|
134
|
-
- [Visual wireframes](../wiki/visual-wireframes.md): Excalidraw research and setup for the tier 1 wireframe companion
|
|
135
|
-
- [Community skills and plugins](../wiki/community-skills.md): catalog of frontend design skills and integrations
|
|
136
|
-
- [Community MCP servers](../wiki/community-mcp-servers.md): catalog of MCPs referenced across all tiers
|
|
134
|
+
- [Visual wireframes](../wiki/tools/visual-wireframes.md): Excalidraw research and setup for the tier 1 wireframe companion
|
|
135
|
+
- [Community skills and plugins](../wiki/tools/community-skills.md): catalog of frontend design skills and integrations
|
|
136
|
+
- [Community MCP servers](../wiki/tools/community-mcp-servers.md): catalog of MCPs referenced across all tiers
|
package/docs/zshrc-aliases.md
CHANGED
|
@@ -56,7 +56,7 @@ Use `clp` in any other repository where you want the toolkit skills available. W
|
|
|
56
56
|
|
|
57
57
|
Use `cls` or `clps` to save Opus usage on routine sessions. Switch mid-session with `/model` to avoid restarting.
|
|
58
58
|
|
|
59
|
-
Use `clw <name>` for features that will take more than one session. A worktree isolates the branch, the transcripts, and the `/resume` history. See [Claude Code and git worktrees](../wiki/claude-worktrees.md) for fan-out rules.
|
|
59
|
+
Use `clw <name>` for features that will take more than one session. A worktree isolates the branch, the transcripts, and the `/resume` history. See [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for fan-out rules.
|
|
60
60
|
|
|
61
61
|
Use `clc` to resume the last session without a picker. Use `clr` when you have several sessions and need to pick by name or recency. Outside the toolkit repo, `clpc` is the same shortcut as `clc` with the plugin loaded.
|
|
62
62
|
|
package/package.json
CHANGED
package/scripts/core/verify.sh
CHANGED
|
@@ -116,6 +116,28 @@ assert_hero_pair() {
|
|
|
116
116
|
[ "$html_commit" = "$png_commit" ]
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
# Whatever stacks the repo currently carries, so a new one is covered without an
|
|
120
|
+
# edit here. A seed root holding no `.claude/` seeds nothing a standard governs.
|
|
121
|
+
collect_seed_roots() {
|
|
122
|
+
local dir
|
|
123
|
+
for dir in "$PROJECT_ROOT"/tooling/*/seeds; do
|
|
124
|
+
[ -d "$dir/.claude" ] || continue
|
|
125
|
+
printf '%s\n' "${dir#"$PROJECT_ROOT"/}"
|
|
126
|
+
done
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Entries the audit actually measured, summed across the folders it resolved.
|
|
130
|
+
# `--json` carries one `"entries":<n>` per folder object, and the top-level key
|
|
131
|
+
# of that name holds an array, so the numeric match reaches folders alone.
|
|
132
|
+
#
|
|
133
|
+
# A root can resolve a folder and measure nothing in it, which is a passing gate
|
|
134
|
+
# over an empty set. The stage prints this per root rather than reporting one
|
|
135
|
+
# verdict for every root, or a tree nobody measured reads as a tree that passed.
|
|
136
|
+
seed_entry_count() {
|
|
137
|
+
printf '%s' "$1" | grep -o '"entries":[0-9]\+' | grep -o '[0-9]\+' |
|
|
138
|
+
awk '{ total += $1 } END { print total + 0 }'
|
|
139
|
+
}
|
|
140
|
+
|
|
119
141
|
assert_no_drift() {
|
|
120
142
|
local paths=$1
|
|
121
143
|
local err_msg=$2
|
|
@@ -183,6 +205,67 @@ main() {
|
|
|
183
205
|
run_check "cd $PROJECT_ROOT && bun src/cli.ts context audit --citations-only" "A cited context path does not resolve. Run bun src/cli.ts context audit."
|
|
184
206
|
log_info "Context citations resolve"
|
|
185
207
|
|
|
208
|
+
# The stage above audits this repository. Its seed tree ships into every
|
|
209
|
+
# scaffolded project, so a seed breaking the standard it seeds propagates
|
|
210
|
+
# instead of sitting still, and no rule path reaches the tree to report it.
|
|
211
|
+
# `--gate` fails on the two findings beside citations that are facts, a
|
|
212
|
+
# missing required section and index drift, and leaves the thresholds
|
|
213
|
+
# advisory for the reason the stage above leaves them so. A passing run stays
|
|
214
|
+
# silent because the audit prints a frame that would nest inside this one.
|
|
215
|
+
log_step "Seed standards"
|
|
216
|
+
local seed_roots seed_root seed_output seed_frame seed_entries seed_measured seed_status
|
|
217
|
+
seed_roots=$(collect_seed_roots)
|
|
218
|
+
if [ -z "$seed_roots" ]; then
|
|
219
|
+
log_info "Skipped, no seed root carries .claude/"
|
|
220
|
+
else
|
|
221
|
+
seed_measured=0
|
|
222
|
+
while IFS= read -r seed_root; do
|
|
223
|
+
# `--json` puts the record on stdout and the frame on stderr, so the
|
|
224
|
+
# passing run stays silent and the failing one is re-run for its frame
|
|
225
|
+
# rather than parsed out of a stream this script would have to strip.
|
|
226
|
+
seed_status=0
|
|
227
|
+
seed_output=$(cd "$PROJECT_ROOT" && bun src/cli.ts context audit "$seed_root" --gate --json 2>/dev/null) || seed_status=$?
|
|
228
|
+
|
|
229
|
+
# The audit separates 1 from 2 and they mean opposite things. 2 is a seed
|
|
230
|
+
# breaking the standard it seeds. 1 is the audit refusing, which a seed
|
|
231
|
+
# root carrying no audited folder produces, and reporting that as a
|
|
232
|
+
# violation sends a reader hunting one that does not exist. Discovery is
|
|
233
|
+
# what puts this in reach, since a new stack seeding `.claude/` alone
|
|
234
|
+
# arrives here with no edit to this script.
|
|
235
|
+
case $seed_status in
|
|
236
|
+
0) ;;
|
|
237
|
+
1)
|
|
238
|
+
log_warn "$seed_root: no audited folder under .claude/, nothing measured"
|
|
239
|
+
continue
|
|
240
|
+
;;
|
|
241
|
+
*)
|
|
242
|
+
# `|| true` because the re-run exits non-zero by construction, and
|
|
243
|
+
# `set -e` would take the script down before log_error names the root.
|
|
244
|
+
seed_frame=$(cd "$PROJECT_ROOT" && bun src/cli.ts context audit "$seed_root" --gate 2>&1 || true)
|
|
245
|
+
echo "$seed_frame" | pipe_output
|
|
246
|
+
if [ "$seed_status" -eq 2 ]; then
|
|
247
|
+
log_error "A seed breaks the standard governing the folder it seeds: $seed_root"
|
|
248
|
+
else
|
|
249
|
+
log_error "The seed audit exited $seed_status against $seed_root, which is neither a pass nor a finding."
|
|
250
|
+
fi
|
|
251
|
+
;;
|
|
252
|
+
esac
|
|
253
|
+
|
|
254
|
+
seed_entries=$(seed_entry_count "$seed_output")
|
|
255
|
+
seed_measured=$((seed_measured + seed_entries))
|
|
256
|
+
|
|
257
|
+
if [ "$seed_entries" -eq 0 ]; then
|
|
258
|
+
log_warn "$seed_root: no entry under an audited folder, nothing measured"
|
|
259
|
+
else
|
|
260
|
+
log_info "$seed_root: $seed_entries entries measured"
|
|
261
|
+
fi
|
|
262
|
+
done <<<"$seed_roots"
|
|
263
|
+
|
|
264
|
+
if [ "$seed_measured" -eq 0 ]; then
|
|
265
|
+
log_warn "No seed entry was measured. The stage covered nothing."
|
|
266
|
+
fi
|
|
267
|
+
fi
|
|
268
|
+
|
|
186
269
|
# Presence of a required file is a fact, so it gates. The name, description,
|
|
187
270
|
# folder, and requirement-section measures beside it report and are read from a
|
|
188
271
|
# bare run. `bun src/cli.ts` for the reason the stage above uses it, and the
|
package/src/claude/seeds.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
|
-
import { chmod } from 'node:fs/promises'
|
|
3
|
-
import { join } from 'node:path'
|
|
2
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
4
|
import { copyPreservingMode } from '@/copy'
|
|
5
|
+
import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
|
|
5
6
|
|
|
6
7
|
const SEEDS_DIR = join('tooling', 'claude', 'seeds')
|
|
7
8
|
const CLAUDE_DIR = '.claude'
|
|
@@ -122,12 +123,25 @@ export function countByScope(seeds: readonly Seed[]): SeedCounts {
|
|
|
122
123
|
/**
|
|
123
124
|
* Copies each pending seed. Hooks get the executable bit the way `chmod +x`
|
|
124
125
|
* granted it, added on top of whatever mode the destination already carried.
|
|
126
|
+
*
|
|
127
|
+
* A markdown seed is rewritten rather than copied, so the stub marker the seed
|
|
128
|
+
* gate reads does not reach the target. Every other seed copies byte for byte,
|
|
129
|
+
* which is what keeps the hook scripts and `settings.json` untouched.
|
|
125
130
|
*/
|
|
126
131
|
export async function applySeeds(seeds: readonly Seed[]): Promise<string[]> {
|
|
127
132
|
const applied: string[] = []
|
|
128
133
|
|
|
129
134
|
for (const seed of seeds) {
|
|
130
|
-
|
|
135
|
+
if (rewritesOnInstall(seed.src)) {
|
|
136
|
+
await mkdir(dirname(seed.dest), { recursive: true })
|
|
137
|
+
await writeFile(
|
|
138
|
+
seed.dest,
|
|
139
|
+
stripSeedMarker(await readFile(seed.src, 'utf8')),
|
|
140
|
+
)
|
|
141
|
+
} else {
|
|
142
|
+
await copyPreservingMode(seed.src, seed.dest)
|
|
143
|
+
}
|
|
144
|
+
|
|
131
145
|
if (seed.executable) await chmod(seed.dest, 0o755)
|
|
132
146
|
applied.push(seed.applyLabel)
|
|
133
147
|
}
|
package/src/commands/context.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
presentNames,
|
|
22
22
|
resolveFolders,
|
|
23
23
|
} from '@/context/folders'
|
|
24
|
+
import { isGating } from '@/context/gate'
|
|
24
25
|
import { auditIndexes, type FolderDrift } from '@/context/index-drift'
|
|
25
26
|
import {
|
|
26
27
|
frameError,
|
|
@@ -34,8 +35,8 @@ import {
|
|
|
34
35
|
plural,
|
|
35
36
|
} from '@/ui'
|
|
36
37
|
|
|
37
|
-
/** Returned when
|
|
38
|
-
const
|
|
38
|
+
/** Returned when a gating finding is present. */
|
|
39
|
+
const EXIT_GATE = 2
|
|
39
40
|
|
|
40
41
|
/** A name of dots alone is `.` or `..`, both of which escape the audit root. */
|
|
41
42
|
const FOLDER_NAME = /^(?!\.+$)[A-Za-z0-9._-]+$/
|
|
@@ -44,6 +45,7 @@ interface AuditCommandOptions {
|
|
|
44
45
|
readonly json?: boolean
|
|
45
46
|
readonly folder?: string
|
|
46
47
|
readonly citationsOnly?: boolean
|
|
48
|
+
readonly gate?: boolean
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export function register(program: Command): void {
|
|
@@ -65,17 +67,23 @@ export function register(program: Command): void {
|
|
|
65
67
|
'Comma-separated folder names, resolved under .claude/ then the project root',
|
|
66
68
|
)
|
|
67
69
|
.option('--citations-only', 'Run the gating citation check alone')
|
|
70
|
+
.option(
|
|
71
|
+
'--gate',
|
|
72
|
+
'Also fail on a missing required section or index drift, the findings that are facts',
|
|
73
|
+
)
|
|
68
74
|
.addHelpText(
|
|
69
75
|
'after',
|
|
70
76
|
[
|
|
71
77
|
'',
|
|
72
78
|
'Exit codes:',
|
|
73
|
-
' 0 the audit completed with
|
|
79
|
+
' 0 the audit completed with no gating finding',
|
|
74
80
|
' 1 refused, with the reason on stderr',
|
|
75
|
-
' 2 a
|
|
81
|
+
' 2 a gating finding is present',
|
|
76
82
|
'',
|
|
77
|
-
'
|
|
78
|
-
'
|
|
83
|
+
'An unresolved citation always gates. --gate widens the gate to the',
|
|
84
|
+
'other two findings that are facts rather than judgments: a missing',
|
|
85
|
+
'required section and index drift. Length, depth, bullet, table, and',
|
|
86
|
+
'provenance findings are thresholds and stay advisory under both.',
|
|
79
87
|
'',
|
|
80
88
|
'Examples:',
|
|
81
89
|
' aitk context audit',
|
|
@@ -83,6 +91,7 @@ export function register(program: Command): void {
|
|
|
83
91
|
' aitk context audit --citations-only',
|
|
84
92
|
' aitk context audit --folder context,diagrams',
|
|
85
93
|
' aitk context audit --folder docs',
|
|
94
|
+
' aitk context audit tooling/base/seeds --gate',
|
|
86
95
|
'',
|
|
87
96
|
].join('\n'),
|
|
88
97
|
)
|
|
@@ -118,6 +127,17 @@ async function runAudit(
|
|
|
118
127
|
const root = resolve(path ?? process.cwd())
|
|
119
128
|
const names = parseFolders(opts.folder)
|
|
120
129
|
const gateOnly = opts.citationsOnly ?? false
|
|
130
|
+
const widened = opts.gate ?? false
|
|
131
|
+
|
|
132
|
+
// `--citations-only` runs the citation check alone, so the two findings
|
|
133
|
+
// `--gate` adds are never measured. Honouring both would exit 0 on a seed
|
|
134
|
+
// short a required section, which is the pass a gate exists to prevent.
|
|
135
|
+
if (gateOnly && widened) {
|
|
136
|
+
return refuse(
|
|
137
|
+
'--citations-only runs the citation check alone, so --gate would widen the gate to findings the run never measures. Pass one.',
|
|
138
|
+
gateOnly,
|
|
139
|
+
)
|
|
140
|
+
}
|
|
121
141
|
|
|
122
142
|
if (typeof names === 'string') return refuse(names, gateOnly)
|
|
123
143
|
|
|
@@ -213,7 +233,14 @@ async function runAudit(
|
|
|
213
233
|
)
|
|
214
234
|
}
|
|
215
235
|
|
|
216
|
-
|
|
236
|
+
const gating = isGating({
|
|
237
|
+
unresolvedCitations: citations.unresolved.length,
|
|
238
|
+
sections,
|
|
239
|
+
drift,
|
|
240
|
+
widened,
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
return gating ? EXIT_GATE : 0
|
|
217
244
|
}
|
|
218
245
|
|
|
219
246
|
function refuse(message: string, gateOnly: boolean): number {
|
package/src/context/audit.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
2
|
import { relative } from 'node:path'
|
|
3
3
|
import type { AuditedFolder } from '@/context/folders'
|
|
4
|
+
import { isStubSeed } from '@/seed-marker'
|
|
4
5
|
|
|
5
6
|
/** Checkpoints quoted from `standards/context.md`. Neither is a cap. */
|
|
6
7
|
export const LENGTH_CHECKPOINT = 150
|
|
@@ -166,6 +167,12 @@ export interface EntryReport {
|
|
|
166
167
|
* is `missingSections`, since one entry answers for its siblings.
|
|
167
168
|
*/
|
|
168
169
|
readonly sections: readonly string[]
|
|
170
|
+
/**
|
|
171
|
+
* Whether the file declares itself a skeleton, which excludes it from the
|
|
172
|
+
* section check alone. Every other measure still reads it, since a stub is
|
|
173
|
+
* exempt from owing sections rather than from being well formed.
|
|
174
|
+
*/
|
|
175
|
+
readonly stub: boolean
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
export interface SectionFinding {
|
|
@@ -544,6 +551,7 @@ export function measureEntry(
|
|
|
544
551
|
provenance: governsContent ? provenance(lines) : [],
|
|
545
552
|
heavyBullets: governsContent ? heavyBullets(lines) : [],
|
|
546
553
|
sections: governsContent ? declaredSections(lines) : [],
|
|
554
|
+
stub: isStubSeed(source),
|
|
547
555
|
}
|
|
548
556
|
}
|
|
549
557
|
|
|
@@ -615,9 +623,15 @@ export function missingSections(
|
|
|
615
623
|
for (const folder of folders) {
|
|
616
624
|
if (!governsContent(folder) || folder.entries.length === 0) continue
|
|
617
625
|
|
|
626
|
+
// A stub owes no sections, so it is dropped before either branch rather
|
|
627
|
+
// than inside them. Leaving one in the split-folder aggregate would let a
|
|
628
|
+
// skeleton answer for the siblings that do owe the sections.
|
|
618
629
|
const reports = folder.entries
|
|
619
630
|
.map((path) => byRel.get(relative(root, path)))
|
|
620
631
|
.filter((entry) => entry !== undefined)
|
|
632
|
+
.filter((entry) => !entry.stub)
|
|
633
|
+
|
|
634
|
+
if (reports.length === 0) continue
|
|
621
635
|
|
|
622
636
|
if (folder.nested) {
|
|
623
637
|
const missing = shortOf(reports.flatMap((entry) => entry.sections))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { SectionFinding } from '@/context/audit'
|
|
2
|
+
import type { FolderDrift } from '@/context/index-drift'
|
|
3
|
+
|
|
4
|
+
export interface GateInput {
|
|
5
|
+
/** Cited paths that resolved to nothing, which gate under either mode. */
|
|
6
|
+
readonly unresolvedCitations: number
|
|
7
|
+
readonly sections: readonly SectionFinding[]
|
|
8
|
+
readonly drift: readonly FolderDrift[]
|
|
9
|
+
/**
|
|
10
|
+
* Whether the caller asked for the widened gate. False leaves a missing
|
|
11
|
+
* section and a drifted index advisory, which is what the project-root stage
|
|
12
|
+
* runs so a judgment threshold never fails a push.
|
|
13
|
+
*/
|
|
14
|
+
readonly widened: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Whether any folder disagrees with its own index. */
|
|
18
|
+
export function hasDrift(drift: readonly FolderDrift[]): boolean {
|
|
19
|
+
return drift.some(
|
|
20
|
+
(folder) => folder.unlisted.length > 0 || folder.missing.length > 0,
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether the audit found something that should fail the caller.
|
|
26
|
+
*
|
|
27
|
+
* An unresolved citation is a broken pointer and gates unconditionally. The two
|
|
28
|
+
* findings `--gate` adds are the ones answerable from the file itself: a
|
|
29
|
+
* required section it does not declare, and an index disagreeing with its
|
|
30
|
+
* folder. Length, depth, bullet, table, and provenance findings are thresholds
|
|
31
|
+
* a reader weighs, so they stay out under both modes.
|
|
32
|
+
*/
|
|
33
|
+
export function isGating({
|
|
34
|
+
unresolvedCitations,
|
|
35
|
+
sections,
|
|
36
|
+
drift,
|
|
37
|
+
widened,
|
|
38
|
+
}: GateInput): boolean {
|
|
39
|
+
if (unresolvedCitations > 0) return true
|
|
40
|
+
if (!widened) return false
|
|
41
|
+
|
|
42
|
+
return sections.length > 0 || hasDrift(drift)
|
|
43
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frontmatter field exempting a seed from the section check the seed gate
|
|
3
|
+
* runs.
|
|
4
|
+
*
|
|
5
|
+
* The check has a false-positive class its own comment records: a standard may
|
|
6
|
+
* sanction omitting a section, and no measure separates that from a file that
|
|
7
|
+
* forgot it. Reporting is the right response in a live project, where the
|
|
8
|
+
* finding is advisory. The seed gate promotes the same finding to a failing
|
|
9
|
+
* exit code, so the seed tree needs a way to say the omission is deliberate.
|
|
10
|
+
*/
|
|
11
|
+
export const SEED_STUB_FIELD = 'stub'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether an install rewrites this seed rather than copying it byte for byte.
|
|
15
|
+
*
|
|
16
|
+
* This answers what the caller branches on and not whether the file holds a
|
|
17
|
+
* marker, which is `isStubSeed` reading content. Only markdown carries
|
|
18
|
+
* frontmatter, so only markdown can hold one, and the extension decides the
|
|
19
|
+
* copy path without opening the file. Every install path and the drift report
|
|
20
|
+
* ask this one question, or the report compares a marked source against a
|
|
21
|
+
* stripped target and reads a seed nobody touched as drifted.
|
|
22
|
+
*/
|
|
23
|
+
export function rewritesOnInstall(src: string): boolean {
|
|
24
|
+
return src.endsWith('.md')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FRONTMATTER = /^---\n([\s\S]*?)\n---(\n|$)/
|
|
28
|
+
const STUB_LINE = new RegExp(`^${SEED_STUB_FIELD}:[ \\t]*true[ \\t]*$`, 'm')
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether the source declares itself a skeleton for the target to fill.
|
|
32
|
+
*
|
|
33
|
+
* Only `true` counts. A field set to anything else reads as a seed that meant
|
|
34
|
+
* to turn the exemption off, and treating an unparsed value as exempt would
|
|
35
|
+
* make a typo silence the gate.
|
|
36
|
+
*/
|
|
37
|
+
export function isStubSeed(source: string): boolean {
|
|
38
|
+
const block = source.match(FRONTMATTER)
|
|
39
|
+
if (!block) return false
|
|
40
|
+
|
|
41
|
+
return STUB_LINE.test(block[1])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Removes the marker so it reaches no target.
|
|
46
|
+
*
|
|
47
|
+
* The field is toolkit bookkeeping about the seed tree, and a project that
|
|
48
|
+
* received it would carry a field its own tooling never reads. A file whose
|
|
49
|
+
* frontmatter holds nothing else loses the block entirely rather than keeping
|
|
50
|
+
* an empty one.
|
|
51
|
+
*/
|
|
52
|
+
export function stripSeedMarker(source: string): string {
|
|
53
|
+
const block = source.match(FRONTMATTER)
|
|
54
|
+
if (!block) return source
|
|
55
|
+
|
|
56
|
+
const kept = block[1]
|
|
57
|
+
.split('\n')
|
|
58
|
+
.filter((line) => !STUB_LINE.test(line))
|
|
59
|
+
.join('\n')
|
|
60
|
+
|
|
61
|
+
// A function replacement, because a string one reads `$1` and `$&` in the
|
|
62
|
+
// kept fields as references to this match. A `description` naming a dollar
|
|
63
|
+
// amount would otherwise substitute the whole frontmatter into itself and
|
|
64
|
+
// carry the marker along with it.
|
|
65
|
+
if (kept.trim() !== '') {
|
|
66
|
+
const replacement = `---\n${kept}\n---${block[2] ?? ''}`
|
|
67
|
+
return source.replace(FRONTMATTER, () => replacement)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Dropping the block takes the blank line that separated it from the body
|
|
71
|
+
// with it. Leaving that behind opens the installed file on whitespace, which
|
|
72
|
+
// is a diff every target would carry against its own formatter.
|
|
73
|
+
return source.slice(block[0].length).replace(/^\n+/, '')
|
|
74
|
+
}
|
package/src/sync/seeds-report.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
import { relative } from 'node:path'
|
|
3
3
|
import { planSeeds, type Seed } from '@/claude/seeds'
|
|
4
|
+
import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
|
|
4
5
|
import { findInstalledOrigin, readHistoryIndex } from '@/sync/history'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -106,6 +107,20 @@ function attribute(
|
|
|
106
107
|
return false
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Compares the target against what an install would write, not against the seed
|
|
112
|
+
* source. The two differ for a markdown seed carrying the stub marker, which the
|
|
113
|
+
* install strips, so comparing sources would report a file the target never
|
|
114
|
+
* touched as drifted for as long as the marker stays set.
|
|
115
|
+
*/
|
|
109
116
|
function sameContent(source: string, dest: string): boolean {
|
|
110
|
-
|
|
117
|
+
const installed = readFileSync(dest)
|
|
118
|
+
|
|
119
|
+
if (!rewritesOnInstall(source)) {
|
|
120
|
+
return readFileSync(source).equals(installed)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
stripSeedMarker(readFileSync(source, 'utf8')) === installed.toString('utf8')
|
|
125
|
+
)
|
|
111
126
|
}
|
package/src/tooling/inject.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { dirname, join } from 'node:path'
|
|
11
11
|
import { $ } from 'bun'
|
|
12
12
|
import { copyPreservingMode } from '@/copy'
|
|
13
|
+
import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
|
|
13
14
|
import { mergeSections, pruneSections } from '@/tooling/gitignore'
|
|
14
15
|
import { ancestorsFirst, listFiles, type Manifest } from '@/tooling/manifest'
|
|
15
16
|
import {
|
|
@@ -44,6 +45,21 @@ export async function injectConfigs(
|
|
|
44
45
|
return applied
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Writes a seed to a target that does not have it, dropping the stub marker on
|
|
50
|
+
* the way. The marker is toolkit bookkeeping read by the seed gate, so a target
|
|
51
|
+
* receiving it would carry a field its own tooling never reads. Only markdown
|
|
52
|
+
* carries frontmatter, and every other seed copies byte for byte.
|
|
53
|
+
*/
|
|
54
|
+
async function writeSeed(src: string, dest: string): Promise<void> {
|
|
55
|
+
if (!rewritesOnInstall(src)) {
|
|
56
|
+
await copyFile(src, dest)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await writeFile(dest, stripSeedMarker(await readFile(src, 'utf8')))
|
|
61
|
+
}
|
|
62
|
+
|
|
47
63
|
/**
|
|
48
64
|
* Copies a seed when the target lacks it. When the target already has one and
|
|
49
65
|
* the seed is a `.txt` word list, missing lines are appended and the file is
|
|
@@ -53,7 +69,7 @@ async function mergeSeedFile(src: string, dest: string): Promise<void> {
|
|
|
53
69
|
await mkdir(dirname(dest), { recursive: true })
|
|
54
70
|
|
|
55
71
|
if (!existsSync(dest)) {
|
|
56
|
-
await
|
|
72
|
+
await writeSeed(src, dest)
|
|
57
73
|
return
|
|
58
74
|
}
|
|
59
75
|
|