@joshuafolkken/kit 1.978.0 → 1.998.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/skills/epic-commands/SKILL.md +6 -5
- package/.claude/skills/workflow-commands/SKILL.md +9 -9
- package/.claude/skills/workflow-commands/background-commands.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-child.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-steps.md +1 -1
- package/.claude/skills/workflow-commands/fullrun-steps.md +8 -8
- package/.claude/skills/workflow-commands/halfrun.md +4 -5
- package/.claude/skills/workflow-commands/issue-comments.md +1 -1
- package/.claude/skills/workflow-commands/issue-scout.md +21 -22
- package/.claude/skills/workflow-commands/kickoff.md +8 -9
- package/.claude/skills/workflow-commands/observation-filing.md +9 -10
- package/.claude/skills/workflow-commands/prerequisite.md +7 -6
- package/.claude/skills/workflow-commands/retrospective.md +5 -5
- package/.claude/skills/workflow-commands/rule-residency.md +2 -2
- package/.claude/skills/workflow-commands/split-assessment.md +4 -3
- package/.claude/skills/workflow-commands/target-repository.md +7 -6
- package/.github/workflows/sonar-qube.yml +12 -2
- package/CLAUDE.md +2 -2
- package/README.md +30 -21
- package/dist/CLAUDE.md +2 -2
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-ZYYSBNR6.js → chunk-4GDR2YTV.js} +4 -4
- package/dist/hooks/{chunk-2K52BRWV.js → chunk-56LGMJJV.js} +2 -2
- package/dist/hooks/{chunk-BNGGEY3Z.js → chunk-762BDXDU.js} +266 -327
- package/dist/hooks/{chunk-Q7LE77CO.js → chunk-7WAOP42A.js} +34 -30
- package/dist/hooks/{chunk-NXL3SBXY.js → chunk-A2H5I2VD.js} +2 -2
- package/dist/hooks/{chunk-CRGJTD6E.js → chunk-AWTFKKFW.js} +1 -1
- package/dist/hooks/{chunk-KQG4776P.js → chunk-GBJUERYK.js} +1 -1
- package/dist/hooks/{chunk-BVMSPG3M.js → chunk-KPZN42RT.js} +3 -3
- package/dist/hooks/{chunk-ZPLXLJ7O.js → chunk-PAXMLGSS.js} +42 -8
- package/dist/hooks/{chunk-F6OM6SIG.js → chunk-WMIR5SNX.js} +12 -12
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +9 -9
- package/dist/hooks/stop-guard.js +99 -27
- package/dist/josh.js +21 -6
- package/package.json +5 -1
- package/prettier/basic.d.ts +3 -0
- package/prettier/basic.js +10 -0
- package/prettier/index.js +2 -2
- package/prettier/static.js +3 -10
- package/prompts/collaboration-workflow/epic-bundle.md +8 -8
- package/prompts/collaboration-workflow/issue-template.md +5 -5
- package/prompts/collaboration-workflow/rule-delivery.md +43 -30
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/prompts/collaboration-workflow/wip-cap.md +3 -3
- package/prompts/review.md +11 -9
- package/prompts/sonar-hotspot-handling.md +6 -0
- package/prompts/testing-guide.md +1 -1
- package/scripts/agent/agent-diagnostics.ts +1 -0
- package/scripts/agent/agent-role-profile.ts +1 -1
- package/scripts/behavior/behavior-rules.ts +8 -1
- package/scripts/build/build-claude-md.ts +13 -4
- package/scripts/delegation/delegation-policy.ts +9 -9
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/gate/gate-steps.ts +1 -1
- package/scripts/gate/project-checks.ts +12 -11
- package/scripts/gate/type-check-command.ts +1 -1
- package/scripts/git/git-branch.ts +14 -3
- package/scripts/git/git-issue.ts +13 -5
- package/scripts/git/git-pr.ts +16 -10
- package/scripts/git/git-preflight.ts +138 -0
- package/scripts/git/telegram-notify.ts +21 -0
- package/scripts/hooks/stop-guard.ts +2 -0
- package/scripts/init/basic-path-migration.ts +24 -0
- package/scripts/init/distributed-paths.ts +22 -0
- package/scripts/init/init-actions.ts +27 -19
- package/scripts/init/init-ai-copy.ts +86 -28
- package/scripts/init/{init-static.ts → init-basic.ts} +6 -6
- package/scripts/init/init-bootstrap.ts +4 -3
- package/scripts/init/init-install.ts +1 -1
- package/scripts/init/init-logic-templates.ts +1 -1
- package/scripts/init/init-logic.ts +1 -1
- package/scripts/init/init.ts +41 -19
- package/scripts/init/kit-setup-state.ts +30 -0
- package/scripts/init/kit-written-paths.ts +33 -0
- package/scripts/init/project-profile-cli.ts +15 -1
- package/scripts/init/project-profile.ts +41 -12
- package/scripts/init/session-language-environment.ts +83 -0
- package/scripts/init/start-exec.ts +28 -0
- package/scripts/init/start-plan.ts +35 -13
- package/scripts/init/start-prompt.ts +22 -8
- package/scripts/init/start-setup-pr.ts +138 -0
- package/scripts/init/start-steps.ts +30 -30
- package/scripts/init/start.ts +7 -3
- package/scripts/issue/issue-file-cli.ts +188 -0
- package/scripts/issue/issue-file.ts +192 -0
- package/scripts/issue/issue-lint-cli.ts +1 -1
- package/scripts/issue/issue-scout-cli.ts +41 -97
- package/scripts/issue/issue-scout-closed.ts +102 -0
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +12 -4
- package/scripts/josh/josh-command-map.ts +1 -0
- package/scripts/josh/josh-commands-development.ts +3 -3
- package/scripts/josh/josh-commands-issue.ts +11 -0
- package/scripts/josh/josh-commands-project.ts +7 -3
- package/scripts/lint/cspell-cached.ts +3 -3
- package/scripts/lint/format.ts +1 -1
- package/scripts/lint/lint-parallel.ts +2 -2
- package/scripts/propagate/propagate-run.ts +1 -0
- package/scripts/propagate/propagate-select.ts +72 -0
- package/scripts/propagate/propagate-targets.ts +5 -3
- package/scripts/propagate/propagate.ts +86 -24
- package/scripts/rules/bash-triggers.ts +19 -6
- package/scripts/rules/delivered-rules.ts +28 -32
- package/scripts/rules/direct-filing.ts +38 -0
- package/scripts/rules/filing-cap.ts +12 -8
- package/scripts/rules/issue-citation.ts +30 -1
- package/scripts/rules/issue-fold.ts +4 -4
- package/scripts/rules/last-prompt.ts +79 -0
- package/scripts/rules/oracle-consulted.ts +4 -4
- package/scripts/rules/oracle-firing.ts +13 -16
- package/scripts/rules/rule-body-guard.ts +6 -9
- package/scripts/rules/stop-rules.ts +8 -5
- package/scripts/safe-chain/ci-installer-pin.ts +19 -12
- package/scripts/safe-chain/project-config.ts +1 -1
- package/scripts/sync/sync-ai-files.ts +204 -0
- package/scripts/sync/sync.ts +48 -184
- package/scripts/test/josh-harness-environment.ts +1 -1
- package/scripts/test/pilot-files.ts +0 -2
- package/scripts/test/test-declared-changed.ts +1 -1
- package/scripts/test/test-declared-logic.ts +55 -14
- package/scripts/test/test-declared.ts +23 -11
- package/scripts/test/test-unit-guard.ts +6 -6
- package/scripts/time-runtime/time-shell.ts +36 -2
- package/scripts/version/fresh-metadata-cache.ts +36 -0
- package/scripts/version/latest-update.ts +2 -1
- package/scripts-ai/git-workflow.ts +10 -0
- package/templates/CLAUDE.basic.md +8 -0
- package/templates/workflows/ci.yml +27 -0
- package/scripts/rules/issue-bug-label.ts +0 -85
- package/scripts/rules/issue-filing-args.ts +0 -50
- package/scripts/rules/issue-scout.ts +0 -64
- /package/{templates/CLAUDE.static.md → dist/CLAUDE.basic.md} +0 -0
- /package/templates/{cursorrules.static → cursorrules.basic} +0 -0
- /package/templates/{pnpm-workspace.static.yaml → pnpm-workspace.basic.yaml} +0 -0
- /package/templates/{prettierignore.static → prettierignore.basic} +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { init_ai_copy } from '#scripts/init/init-ai-copy'
|
|
4
|
+
import { transform_copied_content } from '#scripts/init/init-copy-content'
|
|
5
|
+
import { init_logic } from '#scripts/init/init-logic'
|
|
6
|
+
import { PACKAGE_DIR, PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
7
|
+
import type { ProjectShape } from '#scripts/init/project-profile'
|
|
8
|
+
import { copy_directory_failure, directory_copy_blocker } from './directory-copy-guard'
|
|
9
|
+
import { file_content } from './file-content'
|
|
10
|
+
import { REMOVED_SKILL_MANIFEST } from './removed-skill-manifest'
|
|
11
|
+
import { skill_migration, type MigrationResult } from './skill-migration'
|
|
12
|
+
import { sync_hook_safety } from './sync-hook-safety'
|
|
13
|
+
|
|
14
|
+
const WORKSPACE_YAML = 'pnpm-workspace.yaml'
|
|
15
|
+
const PACKAGE_JSON = 'package.json'
|
|
16
|
+
const CLAUDE_MD_FILENAME = 'CLAUDE.md'
|
|
17
|
+
const CLAUDE_SETTINGS_FILE = '.claude/settings.json'
|
|
18
|
+
const CODEX_HOOKS_FILE = '.codex/hooks.json'
|
|
19
|
+
|
|
20
|
+
function sync_ai_file(source_path: string, destination_path: string): boolean {
|
|
21
|
+
const content = readFileSync(source_path, 'utf8')
|
|
22
|
+
|
|
23
|
+
return file_content.write_text_if_changed(
|
|
24
|
+
destination_path,
|
|
25
|
+
transform_copied_content(destination_path, content),
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Both copied hook files must run against the consumer's installed bundle. When sync is run from a
|
|
30
|
+
// newer source than that install, writing either file would leave hooks
|
|
31
|
+
// that fail every prompt (joshuafolkken/kit#1930); skip it and print how to update instead.
|
|
32
|
+
function should_skip_hook_file(filename: string): boolean {
|
|
33
|
+
if (filename !== CLAUDE_SETTINGS_FILE && filename !== CODEX_HOOKS_FILE) return false
|
|
34
|
+
const warning = sync_hook_safety.hook_write_warning(
|
|
35
|
+
PROJECT_ROOT,
|
|
36
|
+
sync_hook_safety.read_version_at(path.join(PACKAGE_DIR, PACKAGE_JSON)),
|
|
37
|
+
filename,
|
|
38
|
+
)
|
|
39
|
+
if (warning === undefined) return false
|
|
40
|
+
console.warn(warning)
|
|
41
|
+
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sync_file(source: string, filename: string): void {
|
|
46
|
+
if (should_skip_hook_file(filename)) return
|
|
47
|
+
const did_change = sync_ai_file(path.join(PACKAGE_DIR, source), path.join(PROJECT_ROOT, filename))
|
|
48
|
+
|
|
49
|
+
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${filename}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sync_file_mapping(source_path: string, destination_path: string): void {
|
|
53
|
+
if (!existsSync(source_path)) {
|
|
54
|
+
console.warn(` ⚠ skipped ${path.basename(destination_path)} (not found in package)`)
|
|
55
|
+
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Routed through sync_ai_file rather than cpSync: templates/workflows/ci.yml is a mapped
|
|
60
|
+
// file, so a byte copy would hand the consumer the template's own action pins. The pins
|
|
61
|
+
// have to be resolved from .github/workflows at write time (joshuafolkken/kit#747).
|
|
62
|
+
const did_change = sync_ai_file(source_path, destination_path)
|
|
63
|
+
|
|
64
|
+
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${path.basename(destination_path)}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sync_workspace_yaml(
|
|
68
|
+
template_path: string,
|
|
69
|
+
destination_path: string,
|
|
70
|
+
is_force = false,
|
|
71
|
+
): boolean {
|
|
72
|
+
const template = readFileSync(template_path, 'utf8')
|
|
73
|
+
const existing =
|
|
74
|
+
!is_force && existsSync(destination_path) ? readFileSync(destination_path, 'utf8') : ''
|
|
75
|
+
const merged = init_logic.merge_workspace_yaml(existing, template)
|
|
76
|
+
|
|
77
|
+
return file_content.write_text_if_changed(destination_path, merged)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The source is the profile's: a basic project syncs from its own templates, as `josh init` wrote it.
|
|
81
|
+
function sync_ai_copy_file(filename: string, is_force: boolean, shape?: ProjectShape): void {
|
|
82
|
+
const source = init_ai_copy.ai_file_source(filename, shape)
|
|
83
|
+
|
|
84
|
+
if (filename === WORKSPACE_YAML) {
|
|
85
|
+
const did_change = sync_workspace_yaml(
|
|
86
|
+
path.join(PACKAGE_DIR, source),
|
|
87
|
+
path.join(PROJECT_ROOT, WORKSPACE_YAML),
|
|
88
|
+
is_force,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${WORKSPACE_YAML}`)
|
|
92
|
+
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
sync_file(source, filename)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The reason this directory was not synced, or nothing when it was. Both halves answer the same
|
|
100
|
+
// question — what stops the copy — so the caller has one line to print either way.
|
|
101
|
+
function directory_sync_failure(
|
|
102
|
+
directory_name: string,
|
|
103
|
+
on_copy: (did_change: boolean) => void,
|
|
104
|
+
): string | undefined {
|
|
105
|
+
const source_path = path.join(PACKAGE_DIR, directory_name)
|
|
106
|
+
const destination_path = path.join(PROJECT_ROOT, directory_name)
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
directory_copy_blocker(source_path, destination_path) ??
|
|
110
|
+
copy_directory_failure(source_path, destination_path, on_copy)
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function sync_directory(directory_name: string): void {
|
|
115
|
+
const failure = directory_sync_failure(directory_name, (did_change) => {
|
|
116
|
+
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${directory_name}/`)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
if (failure !== undefined) console.warn(` ⚠ skipped ${directory_name}/ (${failure})`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// A consumer's stale copy of a distributed skill is removed once it still matches the shipment and
|
|
123
|
+
// kept with a warning once it does not (joshuafolkken/kit#1879). Plugin skills compare against the
|
|
124
|
+
// package source; a retired skill compares against the frozen manifest (joshuafolkken/kit#1990). The
|
|
125
|
+
// note distinguishes the two, and `absent` — the consumer never had the copy — prints nothing.
|
|
126
|
+
function report_migration_result(result: MigrationResult): void {
|
|
127
|
+
if (result.action === 'absent') return
|
|
128
|
+
|
|
129
|
+
if (result.action === 'removed') {
|
|
130
|
+
console.info(
|
|
131
|
+
` ✔ removed ${result.directory}/ (${skill_migration.removed_note(result.source)})`,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.warn(` ⚠ kept ${result.directory}/ (${skill_migration.kept_note(result.source)})`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function migrate_removed_skills(): void {
|
|
141
|
+
const plugin = skill_migration.migrate_removed_skill_directories(PACKAGE_DIR, PROJECT_ROOT)
|
|
142
|
+
const retired = skill_migration.migrate_manifest_skills(PROJECT_ROOT, REMOVED_SKILL_MANIFEST)
|
|
143
|
+
|
|
144
|
+
for (const result of [...plugin, ...retired]) report_migration_result(result)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ensured_claude_md(existing: string | undefined, shape?: ProjectShape): string {
|
|
148
|
+
return shape?.profile === 'basic'
|
|
149
|
+
? init_ai_copy.ensure_basic_claude_md(existing)
|
|
150
|
+
: init_logic.ensure_claude_md_import(existing)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// CLAUDE.md is not byte-copied (joshuafolkken/kit#1878): a consumer's file is one @import of kit's
|
|
154
|
+
// published rules plus the project's own additions below it. Ensure the import line is present
|
|
155
|
+
// without ever disturbing those additions — so this ignores --force, which would otherwise mean
|
|
156
|
+
// discarding a consumer's content.
|
|
157
|
+
function sync_claude_md(destination_path: string, shape?: ProjectShape): void {
|
|
158
|
+
const existing = existsSync(destination_path) ? readFileSync(destination_path, 'utf8') : undefined
|
|
159
|
+
const ensured = ensured_claude_md(existing, shape)
|
|
160
|
+
|
|
161
|
+
if (ensured === existing) {
|
|
162
|
+
console.info(` ✔ unchanged ${CLAUDE_MD_FILENAME}`)
|
|
163
|
+
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
mkdirSync(path.dirname(destination_path), { recursive: true })
|
|
168
|
+
writeFileSync(destination_path, ensured)
|
|
169
|
+
console.info(` ✔ synced ${CLAUDE_MD_FILENAME}`)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// A basic shape syncs the file set `josh init` writes for that profile; no shape is the full set,
|
|
173
|
+
// unfiltered, as sync has always written it (joshuafolkken/kit#2827).
|
|
174
|
+
function sync_ai_copy_all(is_force: boolean, shape?: ProjectShape): void {
|
|
175
|
+
console.info('AI files:')
|
|
176
|
+
|
|
177
|
+
sync_claude_md(path.join(PROJECT_ROOT, CLAUDE_MD_FILENAME), shape)
|
|
178
|
+
|
|
179
|
+
for (const filename of init_ai_copy.ai_files(shape)) {
|
|
180
|
+
sync_ai_copy_file(filename, is_force, shape)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const { src, dest } of init_ai_copy.ai_file_mappings(shape)) {
|
|
184
|
+
sync_file_mapping(path.join(PACKAGE_DIR, src), path.join(PROJECT_ROOT, dest))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const directory_name of init_logic.get_ai_copy_directories()) {
|
|
188
|
+
sync_directory(directory_name)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
migrate_removed_skills()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const sync_ai_files = {
|
|
195
|
+
should_skip_hook_file,
|
|
196
|
+
sync_file_mapping,
|
|
197
|
+
sync_ai_file,
|
|
198
|
+
sync_workspace_yaml,
|
|
199
|
+
sync_claude_md,
|
|
200
|
+
sync_directory,
|
|
201
|
+
sync_ai_copy_all,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export { sync_ai_files }
|
package/scripts/sync/sync.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { project_checks } from '#scripts/gate/project-checks'
|
|
5
6
|
import { gh_spawn } from '#scripts/gh/gh-spawn'
|
|
6
|
-
import {
|
|
7
|
+
import { basic_path_migration } from '#scripts/init/basic-path-migration'
|
|
7
8
|
import { init_logic } from '#scripts/init/init-logic'
|
|
8
9
|
import { PACKAGE_DIR, PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
9
10
|
import { plugin_install_hint_module } from '#scripts/init/plugin-install-hint'
|
|
11
|
+
import { project_profile } from '#scripts/init/project-profile'
|
|
10
12
|
import { auto_merge_setting } from '#scripts/repo/auto-merge-setting'
|
|
11
13
|
import { repository_labels } from '#scripts/repo/repository-labels'
|
|
12
14
|
import { project_config } from '#scripts/safe-chain/project-config'
|
|
@@ -14,121 +16,13 @@ import { security_updates } from '#scripts/security/security-updates'
|
|
|
14
16
|
import { sonar_file } from '#scripts/security/sonar-file'
|
|
15
17
|
import { did_refuse_self_run } from '#scripts/self-sync-guard/self-sync-refusal'
|
|
16
18
|
import { package_manager_version } from '#scripts/version/package-manager-version'
|
|
17
|
-
import { copy_directory_failure, directory_copy_blocker } from './directory-copy-guard'
|
|
18
19
|
import { file_content } from './file-content'
|
|
19
|
-
import {
|
|
20
|
-
import { skill_migration, type MigrationResult } from './skill-migration'
|
|
20
|
+
import { sync_ai_files } from './sync-ai-files'
|
|
21
21
|
import { sync_configs } from './sync-configs'
|
|
22
|
-
import { sync_hook_safety } from './sync-hook-safety'
|
|
23
22
|
|
|
24
|
-
const WORKSPACE_YAML = 'pnpm-workspace.yaml'
|
|
25
23
|
const PACKAGE_JSON = 'package.json'
|
|
26
24
|
const PACKAGE_JSON_UNCHANGED_MSG = ' ✔ unchanged package.json'
|
|
27
|
-
const
|
|
28
|
-
const CLAUDE_SETTINGS_FILE = '.claude/settings.json'
|
|
29
|
-
const CODEX_HOOKS_FILE = '.codex/hooks.json'
|
|
30
|
-
|
|
31
|
-
function sync_ai_file(source_path: string, destination_path: string): boolean {
|
|
32
|
-
const content = readFileSync(source_path, 'utf8')
|
|
33
|
-
|
|
34
|
-
return file_content.write_text_if_changed(
|
|
35
|
-
destination_path,
|
|
36
|
-
transform_copied_content(destination_path, content),
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Both copied hook files must run against the consumer's installed bundle. When sync is run from a
|
|
41
|
-
// newer source than that install, writing either file would leave hooks
|
|
42
|
-
// that fail every prompt (joshuafolkken/kit#1930); skip it and print how to update instead.
|
|
43
|
-
function should_skip_hook_file(filename: string): boolean {
|
|
44
|
-
if (filename !== CLAUDE_SETTINGS_FILE && filename !== CODEX_HOOKS_FILE) return false
|
|
45
|
-
const warning = sync_hook_safety.hook_write_warning(
|
|
46
|
-
PROJECT_ROOT,
|
|
47
|
-
sync_hook_safety.read_version_at(path.join(PACKAGE_DIR, PACKAGE_JSON)),
|
|
48
|
-
filename,
|
|
49
|
-
)
|
|
50
|
-
if (warning === undefined) return false
|
|
51
|
-
console.warn(warning)
|
|
52
|
-
|
|
53
|
-
return true
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function sync_file(filename: string): void {
|
|
57
|
-
if (should_skip_hook_file(filename)) return
|
|
58
|
-
const did_change = sync_ai_file(
|
|
59
|
-
path.join(PACKAGE_DIR, filename),
|
|
60
|
-
path.join(PROJECT_ROOT, filename),
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${filename}`)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function sync_file_mapping(source_path: string, destination_path: string): void {
|
|
67
|
-
if (!existsSync(source_path)) {
|
|
68
|
-
console.warn(` ⚠ skipped ${path.basename(destination_path)} (not found in package)`)
|
|
69
|
-
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Routed through sync_ai_file rather than cpSync: templates/workflows/ci.yml is a mapped
|
|
74
|
-
// file, so a byte copy would hand the consumer the template's own action pins. The pins
|
|
75
|
-
// have to be resolved from .github/workflows at write time (joshuafolkken/kit#747).
|
|
76
|
-
const did_change = sync_ai_file(source_path, destination_path)
|
|
77
|
-
|
|
78
|
-
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${path.basename(destination_path)}`)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function sync_workspace_yaml(
|
|
82
|
-
template_path: string,
|
|
83
|
-
destination_path: string,
|
|
84
|
-
is_force = false,
|
|
85
|
-
): boolean {
|
|
86
|
-
const template = readFileSync(template_path, 'utf8')
|
|
87
|
-
const existing =
|
|
88
|
-
!is_force && existsSync(destination_path) ? readFileSync(destination_path, 'utf8') : ''
|
|
89
|
-
const merged = init_logic.merge_workspace_yaml(existing, template)
|
|
90
|
-
|
|
91
|
-
return file_content.write_text_if_changed(destination_path, merged)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function sync_ai_copy_file(filename: string, is_force: boolean): void {
|
|
95
|
-
if (filename === WORKSPACE_YAML) {
|
|
96
|
-
const did_change = sync_workspace_yaml(
|
|
97
|
-
path.join(PACKAGE_DIR, WORKSPACE_YAML),
|
|
98
|
-
path.join(PROJECT_ROOT, WORKSPACE_YAML),
|
|
99
|
-
is_force,
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${WORKSPACE_YAML}`)
|
|
103
|
-
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
sync_file(filename)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// The reason this directory was not synced, or nothing when it was. Both halves answer the same
|
|
111
|
-
// question — what stops the copy — so the caller has one line to print either way.
|
|
112
|
-
function directory_sync_failure(
|
|
113
|
-
directory_name: string,
|
|
114
|
-
on_copy: (did_change: boolean) => void,
|
|
115
|
-
): string | undefined {
|
|
116
|
-
const source_path = path.join(PACKAGE_DIR, directory_name)
|
|
117
|
-
const destination_path = path.join(PROJECT_ROOT, directory_name)
|
|
118
|
-
|
|
119
|
-
return (
|
|
120
|
-
directory_copy_blocker(source_path, destination_path) ??
|
|
121
|
-
copy_directory_failure(source_path, destination_path, on_copy)
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function sync_directory(directory_name: string): void {
|
|
126
|
-
const failure = directory_sync_failure(directory_name, (did_change) => {
|
|
127
|
-
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${directory_name}/`)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
if (failure !== undefined) console.warn(` ⚠ skipped ${directory_name}/ (${failure})`)
|
|
131
|
-
}
|
|
25
|
+
const BASIC_PRETTIER_CONFIG = 'prettier.config.mjs'
|
|
132
26
|
|
|
133
27
|
function did_migrate_prettierrc(destination_path: string): boolean {
|
|
134
28
|
const legacy_path = path.join(path.dirname(destination_path), '.prettierrc')
|
|
@@ -171,6 +65,20 @@ function sync_prettier_config(destination_path: string): void {
|
|
|
171
65
|
write_merged_prettier_config(destination_path)
|
|
172
66
|
}
|
|
173
67
|
|
|
68
|
+
// A basic project's prettier config written before joshuafolkken/kit#2829 still imports the static
|
|
69
|
+
// preset path; it is moved onto the basic one with the rule `josh init` applies.
|
|
70
|
+
function sync_basic_prettier_config(destination_path: string): void {
|
|
71
|
+
if (!existsSync(destination_path)) return
|
|
72
|
+
|
|
73
|
+
const existing = readFileSync(destination_path, 'utf8')
|
|
74
|
+
const did_change = file_content.write_text_if_changed(
|
|
75
|
+
destination_path,
|
|
76
|
+
basic_path_migration.migrate_basic_paths(existing),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${BASIC_PRETTIER_CONFIG}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
174
82
|
function sync_playwright_config(destination_path: string): void {
|
|
175
83
|
if (!existsSync(destination_path)) return
|
|
176
84
|
|
|
@@ -220,70 +128,6 @@ function sync_sonar_with_template(name_with_owner: string | undefined, is_force
|
|
|
220
128
|
console.info(` ✔ synced ${destination}`)
|
|
221
129
|
}
|
|
222
130
|
|
|
223
|
-
// A consumer's stale copy of a distributed skill is removed once it still matches the shipment and
|
|
224
|
-
// kept with a warning once it does not (joshuafolkken/kit#1879). Plugin skills compare against the
|
|
225
|
-
// package source; a retired skill compares against the frozen manifest (joshuafolkken/kit#1990). The
|
|
226
|
-
// note distinguishes the two, and `absent` — the consumer never had the copy — prints nothing.
|
|
227
|
-
function report_migration_result(result: MigrationResult): void {
|
|
228
|
-
if (result.action === 'absent') return
|
|
229
|
-
|
|
230
|
-
if (result.action === 'removed') {
|
|
231
|
-
console.info(
|
|
232
|
-
` ✔ removed ${result.directory}/ (${skill_migration.removed_note(result.source)})`,
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
return
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
console.warn(` ⚠ kept ${result.directory}/ (${skill_migration.kept_note(result.source)})`)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function migrate_removed_skills(): void {
|
|
242
|
-
const plugin = skill_migration.migrate_removed_skill_directories(PACKAGE_DIR, PROJECT_ROOT)
|
|
243
|
-
const retired = skill_migration.migrate_manifest_skills(PROJECT_ROOT, REMOVED_SKILL_MANIFEST)
|
|
244
|
-
|
|
245
|
-
for (const result of [...plugin, ...retired]) report_migration_result(result)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// CLAUDE.md is not byte-copied (joshuafolkken/kit#1878): a consumer's file is one @import of kit's
|
|
249
|
-
// published rules plus the project's own additions below it. Ensure the import line is present
|
|
250
|
-
// without ever disturbing those additions — so this ignores --force, which would otherwise mean
|
|
251
|
-
// discarding a consumer's content.
|
|
252
|
-
function sync_claude_md(destination_path: string): void {
|
|
253
|
-
const existing = existsSync(destination_path) ? readFileSync(destination_path, 'utf8') : undefined
|
|
254
|
-
const ensured = init_logic.ensure_claude_md_import(existing)
|
|
255
|
-
|
|
256
|
-
if (ensured === existing) {
|
|
257
|
-
console.info(` ✔ unchanged ${CLAUDE_MD_FILENAME}`)
|
|
258
|
-
|
|
259
|
-
return
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
mkdirSync(path.dirname(destination_path), { recursive: true })
|
|
263
|
-
writeFileSync(destination_path, ensured)
|
|
264
|
-
console.info(` ✔ synced ${CLAUDE_MD_FILENAME}`)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function sync_ai_copy_all(is_force: boolean): void {
|
|
268
|
-
console.info('AI files:')
|
|
269
|
-
|
|
270
|
-
sync_claude_md(path.join(PROJECT_ROOT, CLAUDE_MD_FILENAME))
|
|
271
|
-
|
|
272
|
-
for (const filename of init_logic.get_ai_copy_files()) {
|
|
273
|
-
sync_ai_copy_file(filename, is_force)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
for (const { src, dest } of init_logic.get_ai_copy_file_mappings()) {
|
|
277
|
-
sync_file_mapping(path.join(PACKAGE_DIR, src), path.join(PROJECT_ROOT, dest))
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
for (const directory_name of init_logic.get_ai_copy_directories()) {
|
|
281
|
-
sync_directory(directory_name)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
migrate_removed_skills()
|
|
285
|
-
}
|
|
286
|
-
|
|
287
131
|
function sync_config_files(): void {
|
|
288
132
|
sync_configs.sync_npmrc(path.join(PROJECT_ROOT, '.npmrc'))
|
|
289
133
|
sync_configs.sync_gitignore(path.join(PROJECT_ROOT, '.gitignore'))
|
|
@@ -376,7 +220,7 @@ function report_repository_settings(name_with_owner: string | undefined): void {
|
|
|
376
220
|
}
|
|
377
221
|
|
|
378
222
|
function sync_project_artifacts(is_force: boolean): void {
|
|
379
|
-
sync_ai_copy_all(is_force)
|
|
223
|
+
sync_ai_files.sync_ai_copy_all(is_force)
|
|
380
224
|
sync_prettier_config(path.join(PROJECT_ROOT, 'prettier.config.js'))
|
|
381
225
|
sync_playwright_config(path.join(PROJECT_ROOT, 'playwright.config.ts'))
|
|
382
226
|
sync_deploy_vps(path.join(PROJECT_ROOT, '.github/workflows/deploy-vps.yml'))
|
|
@@ -394,29 +238,47 @@ function sync_project_artifacts(is_force: boolean): void {
|
|
|
394
238
|
report_repository_settings(name_with_owner)
|
|
395
239
|
}
|
|
396
240
|
|
|
241
|
+
// A basic project gets what `josh init` gives that profile and nothing more: its AI file set and the
|
|
242
|
+
// move of its prettier preset path. The full toolchain's configs, Sonar, package.json migrations and
|
|
243
|
+
// repository settings are never written into it (joshuafolkken/kit#2827).
|
|
244
|
+
function sync_basic_artifacts(is_force: boolean): void {
|
|
245
|
+
console.info('profile: basic (package.json josh.profile)\n')
|
|
246
|
+
sync_ai_files.sync_ai_copy_all(is_force, project_profile.inspect_project(PROJECT_ROOT, 'basic'))
|
|
247
|
+
sync_basic_prettier_config(path.join(PROJECT_ROOT, BASIC_PRETTIER_CONFIG))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function sync_full_artifacts(is_force: boolean): void {
|
|
251
|
+
sync_project_artifacts(is_force)
|
|
252
|
+
plugin_install_hint_module.report_plugin_install_hint()
|
|
253
|
+
}
|
|
254
|
+
|
|
397
255
|
// Checked before anything is written, never per file: the damage is the whole run, and a partial
|
|
398
256
|
// sync that stopped halfway would leave the source repository in a state neither `git checkout` nor
|
|
399
257
|
// a re-run describes (joshuafolkken/kit#868).
|
|
258
|
+
//
|
|
259
|
+
// Only a recorded basic profile switches the file set; an unrecorded one stays full, as every
|
|
260
|
+
// project initialized before the profile was recorded was set up.
|
|
400
261
|
function main(): void {
|
|
401
262
|
if (did_refuse_self_run(PACKAGE_DIR, PROJECT_ROOT)) return
|
|
402
263
|
|
|
403
264
|
const is_force = process.argv.includes('--force')
|
|
404
265
|
|
|
405
266
|
console.info('\n🔄 Syncing @joshuafolkken/kit AI files\n')
|
|
406
|
-
|
|
407
|
-
|
|
267
|
+
if (project_checks.is_basic(PROJECT_ROOT)) sync_basic_artifacts(is_force)
|
|
268
|
+
else sync_full_artifacts(is_force)
|
|
408
269
|
console.info('\n✅ Done.\n')
|
|
409
270
|
}
|
|
410
271
|
|
|
411
272
|
if (process.argv[1] === fileURLToPath(import.meta.url)) main()
|
|
412
273
|
|
|
413
274
|
const sync = {
|
|
414
|
-
should_skip_hook_file,
|
|
415
|
-
sync_file_mapping,
|
|
416
|
-
sync_ai_file,
|
|
417
|
-
sync_workspace_yaml,
|
|
418
|
-
sync_claude_md,
|
|
275
|
+
should_skip_hook_file: sync_ai_files.should_skip_hook_file,
|
|
276
|
+
sync_file_mapping: sync_ai_files.sync_file_mapping,
|
|
277
|
+
sync_ai_file: sync_ai_files.sync_ai_file,
|
|
278
|
+
sync_workspace_yaml: sync_ai_files.sync_workspace_yaml,
|
|
279
|
+
sync_claude_md: sync_ai_files.sync_claude_md,
|
|
419
280
|
sync_prettier_config,
|
|
281
|
+
sync_basic_prettier_config,
|
|
420
282
|
sync_playwright_config,
|
|
421
283
|
sync_deploy_vps,
|
|
422
284
|
sync_package_manager_version,
|
|
@@ -427,4 +289,6 @@ const sync = {
|
|
|
427
289
|
|
|
428
290
|
// `sync` is the entry point; `sync_directory` and `main` are exported for their own unit tests, the
|
|
429
291
|
// way `git-pr-checks.ts` exports the helpers beside its namespace.
|
|
292
|
+
const { sync_directory } = sync_ai_files
|
|
293
|
+
|
|
430
294
|
export { main, sync, sync_directory }
|
|
@@ -85,7 +85,7 @@ type FixtureFile = readonly [string, string]
|
|
|
85
85
|
|
|
86
86
|
const TOOLCHAIN_FILES: ReadonlyArray<FixtureFile> = [
|
|
87
87
|
['.gitignore', 'node_modules\n.*cache\n.tsbuildinfo\npnpm-lock.yaml\n'],
|
|
88
|
-
['.prettierignore', '*.json\npnpm-lock.yaml\n'],
|
|
88
|
+
['.prettierignore', '*.json\npnpm-lock.yaml\n_tmp_*\n'],
|
|
89
89
|
['.prettierrc', json_file({ semi: false, singleQuote: true, useTabs: true })],
|
|
90
90
|
[
|
|
91
91
|
'cspell.config.yaml',
|
|
@@ -199,7 +199,6 @@ const PILOT_FILES: ReadonlyArray<string> = [
|
|
|
199
199
|
'scripts/hooks/prompt-hooks.test.ts',
|
|
200
200
|
'scripts/hooks/reserved-run.test.ts',
|
|
201
201
|
'scripts/init/claude-plugin-config.test.ts',
|
|
202
|
-
'scripts/init/init-actions.test.ts',
|
|
203
202
|
'scripts/init/init-copy-content.test.ts',
|
|
204
203
|
'scripts/init/init-logic-deploy-vps.test.ts',
|
|
205
204
|
'scripts/init/init-logic-development-engines.test.ts',
|
|
@@ -320,7 +319,6 @@ const PILOT_FILES: ReadonlyArray<string> = [
|
|
|
320
319
|
'scripts/rules/issue-comments-rule.test.ts',
|
|
321
320
|
'scripts/rules/issue-depth-label.test.ts',
|
|
322
321
|
'scripts/rules/issue-fold.test.ts',
|
|
323
|
-
'scripts/rules/issue-scout.test.ts',
|
|
324
322
|
'scripts/rules/josh-git-bare.test.ts',
|
|
325
323
|
'scripts/rules/lane-interactive-ask-rule.test.ts',
|
|
326
324
|
'scripts/rules/lane-park-rule.test.ts',
|
|
@@ -62,7 +62,7 @@ function source_paths(): ReadonlyArray<string> {
|
|
|
62
62
|
// The verdict over the current working tree. `paths` stays injectable for a direct caller; when it is
|
|
63
63
|
// omitted the injected set is read if one is in force, else the real tree.
|
|
64
64
|
function current_verdict(paths: ReadonlyArray<string> = source_paths()): Verdict {
|
|
65
|
-
return test_declared_logic.verdict_for(paths, project_checks.
|
|
65
|
+
return test_declared_logic.verdict_for(paths, project_checks.is_basic(process.cwd()))
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// Put the injection back to what it was before a `with_paths` span — cleared to the live reader when
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
// a clone avoided. Here only paths that are mechanically certain to touch no runtime code are exempt —
|
|
15
15
|
// the non-executable config and cosmetic-asset arms of `CLAUDE.md`'s "Non-runtime updates" exception
|
|
16
16
|
// stay `required`, and a person declares that exception in the Step 0 work summary. Under-exempting
|
|
17
|
-
// costs one declared exception; over-exempting ships an untested runtime change.
|
|
17
|
+
// costs one declared exception; over-exempting ships an untested runtime change. The one widening is
|
|
18
|
+
// the basic profile's manual check below, for files no runner kit has could test.
|
|
18
19
|
|
|
19
20
|
type Verdict = 'required' | 'exempt' | 'satisfied'
|
|
20
21
|
|
|
@@ -27,7 +28,30 @@ const TEST_SUFFIXES: ReadonlyArray<string> = ['.e2e.ts', '.test.ts']
|
|
|
27
28
|
const EXEMPT_PATHS: ReadonlyArray<string> = ['.editorconfig']
|
|
28
29
|
const EXEMPT_PREFIXES: ReadonlyArray<string> = ['.idea/', '.vscode/', 'prompts/']
|
|
29
30
|
const EXEMPT_SUFFIXES: ReadonlyArray<string> = ['.md']
|
|
30
|
-
const
|
|
31
|
+
const BASIC_VISUAL_SUFFIXES: ReadonlyArray<string> = ['.html', '.css']
|
|
32
|
+
|
|
33
|
+
// What stays `required` under the basic profile (joshuafolkken/kit#2820): the languages kit's own
|
|
34
|
+
// runner can test, and the data and config formats a JS/TS test can load. Any other file — HTML/CSS, or
|
|
35
|
+
// the source of a project kit does not run, such as Lua — has no test kit could execute, so `required`
|
|
36
|
+
// there would be unsatisfiable; it is confirmed by hand instead. The kept side is enumerated rather than
|
|
37
|
+
// the manual one, because that side is closed.
|
|
38
|
+
const BASIC_REQUIRED_SUFFIXES: ReadonlyArray<string> = [
|
|
39
|
+
'.ts',
|
|
40
|
+
'.tsx',
|
|
41
|
+
'.mts',
|
|
42
|
+
'.cts',
|
|
43
|
+
'.js',
|
|
44
|
+
'.jsx',
|
|
45
|
+
'.mjs',
|
|
46
|
+
'.cjs',
|
|
47
|
+
'.svelte',
|
|
48
|
+
'.json',
|
|
49
|
+
'.jsonc',
|
|
50
|
+
'.json5',
|
|
51
|
+
'.yaml',
|
|
52
|
+
'.yml',
|
|
53
|
+
'.toml',
|
|
54
|
+
]
|
|
31
55
|
|
|
32
56
|
function normalize(paths: ReadonlyArray<string>): Array<string> {
|
|
33
57
|
return paths.map((path) => path.trim()).filter((path) => path !== '')
|
|
@@ -37,12 +61,21 @@ function is_test_file(path: string): boolean {
|
|
|
37
61
|
return TEST_SUFFIXES.some((suffix) => path.endsWith(suffix))
|
|
38
62
|
}
|
|
39
63
|
|
|
40
|
-
|
|
41
|
-
|
|
64
|
+
// Matched case-insensitively, so `Main.TS` is not mistaken for a language kit cannot test.
|
|
65
|
+
function is_basic_manual(path: string, is_basic: boolean): boolean {
|
|
66
|
+
const lower = path.toLowerCase()
|
|
67
|
+
|
|
68
|
+
return is_basic && BASIC_REQUIRED_SUFFIXES.every((suffix) => !lower.endsWith(suffix))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function is_basic_visual(path: string): boolean {
|
|
72
|
+
const lower = path.toLowerCase()
|
|
73
|
+
|
|
74
|
+
return BASIC_VISUAL_SUFFIXES.some((suffix) => lower.endsWith(suffix))
|
|
42
75
|
}
|
|
43
76
|
|
|
44
|
-
function is_exempt(path: string,
|
|
45
|
-
if (EXEMPT_PATHS.includes(path) ||
|
|
77
|
+
function is_exempt(path: string, is_basic = false): boolean {
|
|
78
|
+
if (EXEMPT_PATHS.includes(path) || is_basic_manual(path, is_basic)) return true
|
|
46
79
|
|
|
47
80
|
return (
|
|
48
81
|
EXEMPT_SUFFIXES.some((suffix) => path.endsWith(suffix)) ||
|
|
@@ -52,31 +85,37 @@ function is_exempt(path: string, is_static = false): boolean {
|
|
|
52
85
|
|
|
53
86
|
// A runtime file is one that is neither a test nor exempt. Its presence with no test file beside it is
|
|
54
87
|
// the whole of what `required` reports.
|
|
55
|
-
function is_runtime(path: string,
|
|
56
|
-
return !is_test_file(path) && !is_exempt(path,
|
|
88
|
+
function is_runtime(path: string, is_basic = false): boolean {
|
|
89
|
+
return !is_test_file(path) && !is_exempt(path, is_basic)
|
|
57
90
|
}
|
|
58
91
|
|
|
59
92
|
// **A test file changed → `satisfied`, whatever else did.** A runtime file with no test beside it →
|
|
60
93
|
// `required`. Everything left — an all-exempt change, and the empty diff — is `exempt`, because there
|
|
61
94
|
// is no runtime file the change failed to test.
|
|
62
|
-
function verdict_for(paths: ReadonlyArray<string>,
|
|
95
|
+
function verdict_for(paths: ReadonlyArray<string>, is_basic = false): Verdict {
|
|
63
96
|
const changed = normalize(paths)
|
|
64
97
|
|
|
65
98
|
if (changed.some((path) => is_test_file(path))) return 'satisfied'
|
|
66
|
-
if (changed.some((path) => is_runtime(path,
|
|
99
|
+
if (changed.some((path) => is_runtime(path, is_basic))) return 'required'
|
|
67
100
|
|
|
68
101
|
return 'exempt'
|
|
69
102
|
}
|
|
70
103
|
|
|
71
104
|
// The runtime files with no test beside them — the `required` detail line, so the answer says which
|
|
72
105
|
// files it is about rather than only that it refused.
|
|
73
|
-
function runtime_files(paths: ReadonlyArray<string>,
|
|
74
|
-
return normalize(paths).filter((path) => is_runtime(path,
|
|
106
|
+
function runtime_files(paths: ReadonlyArray<string>, is_basic = false): Array<string> {
|
|
107
|
+
return normalize(paths).filter((path) => is_runtime(path, is_basic))
|
|
75
108
|
}
|
|
76
109
|
|
|
77
110
|
// The paths that made the change exempt — the `exempt` detail line.
|
|
78
|
-
function exempt_files(paths: ReadonlyArray<string>,
|
|
79
|
-
return normalize(paths).filter((path) => is_exempt(path,
|
|
111
|
+
function exempt_files(paths: ReadonlyArray<string>, is_basic = false): Array<string> {
|
|
112
|
+
return normalize(paths).filter((path) => is_exempt(path, is_basic))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The paths exempt only because the basic profile has no runner for them — the ones a person still has
|
|
116
|
+
// to confirm by hand, as opposed to docs and editor settings, which nobody checks.
|
|
117
|
+
function manual_check_files(paths: ReadonlyArray<string>, is_basic: boolean): Array<string> {
|
|
118
|
+
return normalize(paths).filter((path) => is_exempt(path, is_basic) && !is_exempt(path))
|
|
80
119
|
}
|
|
81
120
|
|
|
82
121
|
const test_declared_logic = {
|
|
@@ -85,9 +124,11 @@ const test_declared_logic = {
|
|
|
85
124
|
EXEMPT_SUFFIXES,
|
|
86
125
|
TEST_SUFFIXES,
|
|
87
126
|
exempt_files,
|
|
127
|
+
is_basic_visual,
|
|
88
128
|
is_exempt,
|
|
89
129
|
is_runtime,
|
|
90
130
|
is_test_file,
|
|
131
|
+
manual_check_files,
|
|
91
132
|
runtime_files,
|
|
92
133
|
verdict_for,
|
|
93
134
|
}
|