@aperant/framework 0.6.7 → 0.7.3
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/CHANGELOG.md +240 -0
- package/agents/apt-planner.md +12 -0
- package/agents/apt-pr-review-fixer.md +13 -9
- package/bin/apt-tools.mjs +7 -0
- package/dist/cli/ci-watch/stop-matrix.d.mts.map +1 -1
- package/dist/cli/ci-watch/stop-matrix.mjs +16 -0
- package/dist/cli/ci-watch/stop-matrix.mjs.map +1 -1
- package/dist/cli/commands/ci-watch.d.mts +11 -0
- package/dist/cli/commands/ci-watch.d.mts.map +1 -1
- package/dist/cli/commands/ci-watch.mjs +147 -3
- package/dist/cli/commands/ci-watch.mjs.map +1 -1
- package/dist/cli/commands/features-audit.d.mts +24 -0
- package/dist/cli/commands/features-audit.d.mts.map +1 -1
- package/dist/cli/commands/features-audit.mjs +159 -5
- package/dist/cli/commands/features-audit.mjs.map +1 -1
- package/dist/cli/commands/health-check.d.mts +16 -0
- package/dist/cli/commands/health-check.d.mts.map +1 -1
- package/dist/cli/commands/health-check.mjs +119 -3
- package/dist/cli/commands/health-check.mjs.map +1 -1
- package/dist/cli/commands/init.d.mts +19 -1
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +143 -8
- package/dist/cli/commands/init.mjs.map +1 -1
- package/dist/cli/commands/modes.d.mts.map +1 -1
- package/dist/cli/commands/modes.mjs +11 -0
- package/dist/cli/commands/modes.mjs.map +1 -1
- package/dist/cli/commands/pr-review-audit-fixer.d.mts +13 -0
- package/dist/cli/commands/pr-review-audit-fixer.d.mts.map +1 -1
- package/dist/cli/commands/pr-review-audit-fixer.mjs +18 -5
- package/dist/cli/commands/pr-review-audit-fixer.mjs.map +1 -1
- package/dist/cli/commands/route.d.mts.map +1 -1
- package/dist/cli/commands/route.mjs +37 -2
- package/dist/cli/commands/route.mjs.map +1 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +132 -5
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/validate-evidence.d.mts +24 -2
- package/dist/cli/commands/validate-evidence.d.mts.map +1 -1
- package/dist/cli/commands/validate-evidence.mjs +154 -17
- package/dist/cli/commands/validate-evidence.mjs.map +1 -1
- package/dist/cli/commands/vitest-doctor.d.mts +2 -0
- package/dist/cli/commands/vitest-doctor.d.mts.map +1 -0
- package/dist/cli/commands/vitest-doctor.mjs +168 -0
- package/dist/cli/commands/vitest-doctor.mjs.map +1 -0
- package/dist/cli/config/gitignore-drift.d.mts +23 -1
- package/dist/cli/config/gitignore-drift.d.mts.map +1 -1
- package/dist/cli/config/gitignore-drift.mjs +81 -3
- package/dist/cli/config/gitignore-drift.mjs.map +1 -1
- package/dist/cli/config/load.d.mts +56 -2
- package/dist/cli/config/load.d.mts.map +1 -1
- package/dist/cli/config/load.mjs +192 -2
- package/dist/cli/config/load.mjs.map +1 -1
- package/dist/cli/consistency/parse-review.mjs +7 -0
- package/dist/cli/consistency/parse-review.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +24 -2
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/gate/gates/gitignore-in-sync.d.mts +1 -1
- package/dist/cli/gate/gates/gitignore-in-sync.d.mts.map +1 -1
- package/dist/cli/gate/gates/gitignore-in-sync.mjs +5 -2
- package/dist/cli/gate/gates/gitignore-in-sync.mjs.map +1 -1
- package/dist/cli/gate/gates/review-clean.d.mts +5 -1
- package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
- package/dist/cli/gate/gates/review-clean.mjs +23 -18
- package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
- package/dist/cli/gate/gates/verify-approved.d.mts +49 -1
- package/dist/cli/gate/gates/verify-approved.d.mts.map +1 -1
- package/dist/cli/gate/gates/verify-approved.mjs +93 -14
- package/dist/cli/gate/gates/verify-approved.mjs.map +1 -1
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +8 -2
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/host/detect.d.mts +122 -9
- package/dist/cli/host/detect.d.mts.map +1 -1
- package/dist/cli/host/detect.mjs +132 -0
- package/dist/cli/host/detect.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts +38 -0
- package/dist/cli/install/legacy-paths.d.mts.map +1 -0
- package/dist/cli/install/legacy-paths.mjs +69 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -0
- package/dist/cli/install/runtime-detect.d.mts +13 -0
- package/dist/cli/install/runtime-detect.d.mts.map +1 -1
- package/dist/cli/install/runtime-detect.mjs +9 -0
- package/dist/cli/install/runtime-detect.mjs.map +1 -1
- package/dist/cli/install/runtime-migrate.d.mts +84 -0
- package/dist/cli/install/runtime-migrate.d.mts.map +1 -0
- package/dist/cli/install/runtime-migrate.mjs +244 -0
- package/dist/cli/install/runtime-migrate.mjs.map +1 -0
- package/dist/cli/route/drift-detect.d.mts +20 -0
- package/dist/cli/route/drift-detect.d.mts.map +1 -0
- package/dist/cli/route/drift-detect.mjs +107 -0
- package/dist/cli/route/drift-detect.mjs.map +1 -0
- package/dist/cli/task/index-md.d.mts.map +1 -1
- package/dist/cli/task/index-md.mjs +14 -2
- package/dist/cli/task/index-md.mjs.map +1 -1
- package/dist/cli/util/aperant-section.d.mts +34 -0
- package/dist/cli/util/aperant-section.d.mts.map +1 -0
- package/dist/cli/util/aperant-section.mjs +127 -0
- package/dist/cli/util/aperant-section.mjs.map +1 -0
- package/dist/cli/util/copy.d.mts +28 -1
- package/dist/cli/util/copy.d.mts.map +1 -1
- package/dist/cli/util/copy.mjs +43 -55
- package/dist/cli/util/copy.mjs.map +1 -1
- package/dist/cli/util/semver.d.mts +17 -0
- package/dist/cli/util/semver.d.mts.map +1 -0
- package/dist/cli/util/semver.mjs +29 -0
- package/dist/cli/util/semver.mjs.map +1 -0
- package/dist/cli/util/skill-installs.d.mts +65 -9
- package/dist/cli/util/skill-installs.d.mts.map +1 -1
- package/dist/cli/util/skill-installs.mjs +130 -21
- package/dist/cli/util/skill-installs.mjs.map +1 -1
- package/dist/cli/util/version-preflight.d.mts +44 -0
- package/dist/cli/util/version-preflight.d.mts.map +1 -0
- package/dist/cli/util/version-preflight.mjs +66 -0
- package/dist/cli/util/version-preflight.mjs.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +11 -2
- package/dist/plugin/agents/apt-improver.md +99 -0
- package/dist/plugin/agents/apt-planner.md +127 -10
- package/dist/plugin/agents/apt-pr-review-fixer.md +13 -9
- package/dist/plugin/skills/apt/SKILL.md +1 -0
- package/dist/plugin/skills/apt-close-task/SKILL.md +63 -1
- package/dist/plugin/skills/apt-debug/SKILL.md +39 -6
- package/dist/plugin/skills/apt-debug/appendices/diagnose-discipline.md +119 -0
- package/dist/plugin/skills/apt-diagram/SKILL.md +378 -0
- package/dist/plugin/skills/apt-diagram/appendices/design-discipline.md +97 -0
- package/dist/plugin/skills/apt-discuss/SKILL.md +72 -5
- package/dist/plugin/skills/apt-discuss/appendices/grill-discipline.md +104 -0
- package/dist/plugin/skills/apt-discuss/appendices/zoom-out-helper.md +79 -0
- package/dist/plugin/skills/apt-execute/SKILL.md +57 -5
- package/dist/plugin/skills/apt-execute/appendices/tdd-mode.md +107 -0
- package/dist/plugin/skills/apt-improve/DEEPENING.md +84 -0
- package/dist/plugin/skills/apt-improve/INTERFACE-DESIGN.md +97 -0
- package/dist/plugin/skills/apt-improve/LANGUAGE.md +104 -0
- package/dist/plugin/skills/apt-improve/SKILL.md +141 -0
- package/dist/plugin/skills/apt-plan/SKILL.md +171 -4
- package/dist/plugin/skills/apt-plan/adapters/conductor.md +98 -0
- package/dist/plugin/skills/apt-pr-review/SKILL.md +57 -18
- package/dist/plugin/skills/apt-prototype/LOGIC.md +109 -0
- package/dist/plugin/skills/apt-prototype/SKILL.md +143 -0
- package/dist/plugin/skills/apt-prototype/UI.md +90 -0
- package/dist/plugin/skills/apt-quick/SKILL.md +49 -8
- package/dist/plugin/skills/apt-release-notes/SKILL.md +193 -0
- package/dist/plugin/skills/apt-release-notes/appendices/persona-voice.md +59 -0
- package/dist/plugin/skills/apt-review/SKILL.md +2 -0
- package/dist/plugin/skills/apt-run/SKILL.md +32 -4
- package/dist/plugin/skills/apt-setup/SKILL.md +308 -6
- package/dist/plugin/skills/apt-ship/SKILL.md +122 -1
- package/dist/plugin/skills/apt-spar/SKILL.md +315 -0
- package/dist/plugin/skills/apt-triage/AGENT-BRIEF.md +84 -0
- package/dist/plugin/skills/apt-triage/OUT-OF-SCOPE.md +75 -0
- package/dist/plugin/skills/apt-triage/SKILL.md +169 -0
- package/dist/plugin/skills/apt-update/SKILL.md +77 -10
- package/dist/plugin/skills/apt-verify/SKILL.md +3 -0
- package/dist/plugin/skills/apt-verify-proof/SKILL.md +10 -5
- package/dist/plugin/skills/apt-watch-ci/SKILL.md +166 -0
- package/dist/plugin/skills/apt-zoom-out/SKILL.md +130 -0
- package/package.json +133 -133
- package/prompts/conductor-framework-context.md +63 -0
- package/prompts/conductor-system.md +11 -0
- package/skills/apt-close-task/SKILL.md +6 -0
- package/skills/apt-discuss/SKILL.md +47 -5
- package/skills/apt-execute/SKILL.md +9 -0
- package/skills/apt-plan/SKILL.md +12 -0
- package/skills/apt-pr-review/SKILL.md +11 -2
- package/skills/apt-quick/SKILL.md +19 -8
- package/skills/apt-researcher.md +1 -0
- package/skills/apt-setup/SKILL.md +33 -2
- package/skills/apt-ship/SKILL.md +16 -4
- package/skills/apt-spar/SKILL.md +36 -11
- package/skills/apt-update/SKILL.md +26 -1
- package/skills/apt-verify-proof/SKILL.md +7 -5
- package/skills/apt-watch-ci/SKILL.md +4 -1
- package/src/cli/ci-watch/stop-matrix.mjs +17 -0
- package/src/cli/commands/ci-watch.mjs +152 -3
- package/src/cli/commands/features-audit.mjs +164 -5
- package/src/cli/commands/health-check.mjs +116 -3
- package/src/cli/commands/init.mjs +154 -6
- package/src/cli/commands/modes.mjs +11 -0
- package/src/cli/commands/pr-review-audit-fixer.mjs +18 -5
- package/src/cli/commands/route.mjs +38 -2
- package/src/cli/commands/task.mjs +132 -5
- package/src/cli/commands/validate-evidence.mjs +158 -17
- package/src/cli/commands/vitest-doctor.mjs +173 -0
- package/src/cli/config/gitignore-drift.mjs +74 -3
- package/src/cli/config/load.mjs +188 -2
- package/src/cli/consistency/parse-review.mjs +6 -0
- package/src/cli/dispatch.mjs +23 -2
- package/src/cli/gate/gates/gitignore-in-sync.mjs +5 -2
- package/src/cli/gate/gates/review-clean.mjs +24 -19
- package/src/cli/gate/gates/verify-approved.mjs +97 -14
- package/src/cli/help.mjs +8 -2
- package/src/cli/host/detect.mjs +135 -0
- package/src/cli/install/legacy-paths.mjs +69 -0
- package/src/cli/install/runtime-detect.mjs +9 -0
- package/src/cli/install/runtime-migrate.mjs +252 -0
- package/src/cli/route/drift-detect.mjs +107 -0
- package/src/cli/task/index-md.mjs +15 -2
- package/src/cli/util/aperant-section.mjs +136 -0
- package/src/cli/util/copy.mjs +43 -56
- package/src/cli/util/semver.mjs +28 -0
- package/src/cli/util/skill-installs.mjs +134 -21
- package/src/cli/util/version-preflight.mjs +65 -0
- package/templates/aperant-claude-md-appendix.md +37 -0
- package/templates/config.json +2 -7
- package/workflows/verify-proof.md +8 -3
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/vitest-doctor.mjs — FRAMEWORK-BUG-026 diagnostic subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Globs every `vitest.config.{ts,js,mjs}` under the project root (skipping
|
|
5
|
+
* node_modules), parses each for the two shim signals the framework
|
|
6
|
+
* centralizes in `packages/framework/test-utils/vitest.shared.mjs`:
|
|
7
|
+
*
|
|
8
|
+
* - `NODE_ENV='test'` (process.env.NODE_ENV set OR defineVitestShim active)
|
|
9
|
+
* - `oxc.jsx.runtime='automatic'` (jsxAutomaticShim active)
|
|
10
|
+
*
|
|
11
|
+
* Reports a matrix and warns when a workspace has JSX-rendering tests
|
|
12
|
+
* (any `*.test.tsx` under `src/`) but no jsxAutomaticShim coverage —
|
|
13
|
+
* that's the BUG-026 first-test paper cut.
|
|
14
|
+
*
|
|
15
|
+
* Exit codes:
|
|
16
|
+
* 0 — no warnings (or only informational matrix output)
|
|
17
|
+
* 1 — at least one workspace has JSX-rendering tests without
|
|
18
|
+
* jsxAutomaticShim coverage
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
22
|
+
import { join, relative, resolve } from 'node:path'
|
|
23
|
+
import { parseFlags } from '../util/args.mjs'
|
|
24
|
+
import { err, exitWith, ok } from '../util/result.mjs'
|
|
25
|
+
|
|
26
|
+
const VITEST_CONFIG_NAMES = new Set(['vitest.config.ts', 'vitest.config.js', 'vitest.config.mjs'])
|
|
27
|
+
|
|
28
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'out', '.next', '.aperant'])
|
|
29
|
+
|
|
30
|
+
export function cmdVitestDoctor(projectDir, extraArgs) {
|
|
31
|
+
if (!projectDir) return err('Usage: apt-tools vitest doctor <project-dir> [--format json|md]')
|
|
32
|
+
const root = resolve(projectDir)
|
|
33
|
+
if (!existsSync(root)) return err(`project-dir not found: ${root}`)
|
|
34
|
+
const flags = parseFlags(extraArgs ?? [])
|
|
35
|
+
const format = (flags.get('format') || 'json').toLowerCase()
|
|
36
|
+
|
|
37
|
+
const configs = findVitestConfigs(root)
|
|
38
|
+
const matrix = configs.map((configPath) => inspectConfig(root, configPath))
|
|
39
|
+
const warnings = matrix
|
|
40
|
+
.filter((row) => row.has_jsx_rendering_tests && !row.jsx_automatic_shim)
|
|
41
|
+
.map((row) => ({
|
|
42
|
+
workspace: row.workspace,
|
|
43
|
+
reason:
|
|
44
|
+
'has JSX-rendering tests under src/ but vitest.config does not enable jsxAutomaticShim',
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
const envelope = {
|
|
48
|
+
status: 'ok',
|
|
49
|
+
command: 'vitest-doctor',
|
|
50
|
+
project_dir: root,
|
|
51
|
+
configs_found: matrix.length,
|
|
52
|
+
matrix,
|
|
53
|
+
warnings,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (format === 'md') {
|
|
57
|
+
envelope.markdown = renderMarkdown(matrix, warnings)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (warnings.length > 0) {
|
|
61
|
+
// Exit code 1 — surface drift so the user knows to wire the shim.
|
|
62
|
+
return exitWith(envelope, 1)
|
|
63
|
+
}
|
|
64
|
+
return ok(envelope)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function findVitestConfigs(root) {
|
|
68
|
+
const out = []
|
|
69
|
+
walk(root, root, out)
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function walk(rootAbs, dirAbs, out) {
|
|
74
|
+
let names
|
|
75
|
+
try {
|
|
76
|
+
names = readdirSync(dirAbs)
|
|
77
|
+
} catch {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
for (const name of names) {
|
|
81
|
+
if (SKIP_DIRS.has(name)) continue
|
|
82
|
+
const abs = join(dirAbs, name)
|
|
83
|
+
let st
|
|
84
|
+
try {
|
|
85
|
+
st = statSync(abs)
|
|
86
|
+
} catch {
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
if (st.isDirectory()) {
|
|
90
|
+
walk(rootAbs, abs, out)
|
|
91
|
+
} else if (st.isFile() && VITEST_CONFIG_NAMES.has(name)) {
|
|
92
|
+
out.push(abs)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function inspectConfig(rootAbs, configPath) {
|
|
98
|
+
const workspace = relative(rootAbs, configPath)
|
|
99
|
+
const dir = join(configPath, '..')
|
|
100
|
+
let body = ''
|
|
101
|
+
try {
|
|
102
|
+
body = readFileSync(configPath, 'utf-8')
|
|
103
|
+
} catch {
|
|
104
|
+
return {
|
|
105
|
+
workspace,
|
|
106
|
+
node_env_shim: false,
|
|
107
|
+
jsx_automatic_shim: false,
|
|
108
|
+
has_jsx_rendering_tests: false,
|
|
109
|
+
error: 'read-failed',
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// NODE_ENV shim: either the literal `process.env.NODE_ENV = 'test'`
|
|
113
|
+
// pattern OR `defineVitestShim(` (any non-explicit-false arg).
|
|
114
|
+
const node_env_shim =
|
|
115
|
+
/process\.env\.NODE_ENV\s*=\s*['"`]test['"`]/.test(body) ||
|
|
116
|
+
/defineVitestShim\([^)]*\)/.test(body)
|
|
117
|
+
// jsxAutomaticShim: literal `oxc.jsx.runtime` = 'automatic' OR
|
|
118
|
+
// `jsxAutomaticShim: true` inside a defineVitestShim() arg.
|
|
119
|
+
const jsx_automatic_shim =
|
|
120
|
+
/oxc\s*:\s*\{[^}]*jsx[^}]*runtime\s*:\s*['"`]automatic['"`]/s.test(body) ||
|
|
121
|
+
/jsxAutomaticShim\s*:\s*true/.test(body)
|
|
122
|
+
const has_jsx_rendering_tests = workspaceHasJsxRenderingTests(dir)
|
|
123
|
+
return { workspace, node_env_shim, jsx_automatic_shim, has_jsx_rendering_tests }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function workspaceHasJsxRenderingTests(workspaceDir) {
|
|
127
|
+
const srcDir = join(workspaceDir, 'src')
|
|
128
|
+
if (!existsSync(srcDir)) return false
|
|
129
|
+
return scanForTsxTest(srcDir)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function scanForTsxTest(dirAbs) {
|
|
133
|
+
let names
|
|
134
|
+
try {
|
|
135
|
+
names = readdirSync(dirAbs)
|
|
136
|
+
} catch {
|
|
137
|
+
return false
|
|
138
|
+
}
|
|
139
|
+
for (const name of names) {
|
|
140
|
+
if (SKIP_DIRS.has(name)) continue
|
|
141
|
+
const abs = join(dirAbs, name)
|
|
142
|
+
let st
|
|
143
|
+
try {
|
|
144
|
+
st = statSync(abs)
|
|
145
|
+
} catch {
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
if (st.isDirectory()) {
|
|
149
|
+
if (scanForTsxTest(abs)) return true
|
|
150
|
+
} else if (st.isFile() && /\.test\.tsx$/.test(name)) {
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderMarkdown(matrix, warnings) {
|
|
158
|
+
const lines = ['## vitest-doctor matrix', '']
|
|
159
|
+
lines.push('| workspace | NODE_ENV shim | jsxAutomaticShim | has JSX tests |')
|
|
160
|
+
lines.push('|-----------|---------------|------------------|---------------|')
|
|
161
|
+
for (const r of matrix) {
|
|
162
|
+
lines.push(
|
|
163
|
+
`| \`${r.workspace}\` | ${r.node_env_shim ? '✓' : '✗'} | ${r.jsx_automatic_shim ? '✓' : '✗'} | ${r.has_jsx_rendering_tests ? '✓' : '—'} |`,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
if (warnings.length > 0) {
|
|
167
|
+
lines.push('', '## Warnings', '')
|
|
168
|
+
for (const w of warnings) {
|
|
169
|
+
lines.push(`- \`${w.workspace}\` — ${w.reason}`)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return lines.join('\n')
|
|
173
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { existsSync, readFileSync } from 'node:fs'
|
|
16
16
|
import { join } from 'node:path'
|
|
17
|
+
import { detectWorktree } from '../git/worktree-detect.mjs'
|
|
17
18
|
import { resolveSharePolicy } from './share-policy.mjs'
|
|
18
19
|
import { buildAperantGitignoreTemplate } from './upgrade-gitignore.mjs'
|
|
19
20
|
|
|
@@ -35,11 +36,66 @@ const SKIP_COMMANDS = new Set([
|
|
|
35
36
|
'-h',
|
|
36
37
|
])
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* BUG-028 — resolve the shared (main-repo) root for the dispatch hook.
|
|
41
|
+
*
|
|
42
|
+
* Mirrors `coordination/agent-identity.mjs::resolveSharedRoot` but does NOT
|
|
43
|
+
* depend on that module. The CoordinationStore seam is `@internal` (R8) —
|
|
44
|
+
* `bin/apt-tools.mjs` and its dispatch hooks must reach git through
|
|
45
|
+
* `git/worktree-detect.mjs` directly, not through `coordination/*`.
|
|
46
|
+
*
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
function resolveSharedRootForDrift(projectDir) {
|
|
50
|
+
try {
|
|
51
|
+
const wt = detectWorktree(projectDir)
|
|
52
|
+
return wt.is_worktree && wt.main_repo_root ? wt.main_repo_root : projectDir
|
|
53
|
+
} catch {
|
|
54
|
+
return projectDir
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* BUG-028 — pretty-print the first few missing / extra lines as a
|
|
60
|
+
* unified-diff snippet for the gate's `reason` field. Capped at
|
|
61
|
+
* `maxLines` per side so the reason stays short enough for terminal
|
|
62
|
+
* output. Returns the empty string when both sides match.
|
|
63
|
+
*/
|
|
64
|
+
export function buildGitignoreDiffSummary(expected, actual, maxLines = 3) {
|
|
65
|
+
const expSet = new Set(String(expected).split('\n'))
|
|
66
|
+
const actSet = new Set(String(actual).split('\n'))
|
|
67
|
+
const missing = []
|
|
68
|
+
for (const line of expSet) {
|
|
69
|
+
if (line === '') continue
|
|
70
|
+
if (!actSet.has(line)) {
|
|
71
|
+
missing.push(line)
|
|
72
|
+
if (missing.length >= maxLines) break
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const extra = []
|
|
76
|
+
for (const line of actSet) {
|
|
77
|
+
if (line === '') continue
|
|
78
|
+
if (!expSet.has(line)) {
|
|
79
|
+
extra.push(line)
|
|
80
|
+
if (extra.length >= maxLines) break
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (missing.length === 0 && extra.length === 0) return ''
|
|
84
|
+
const parts = []
|
|
85
|
+
for (const line of missing) parts.push(`+ ${line}`)
|
|
86
|
+
for (const line of extra) parts.push(`- ${line}`)
|
|
87
|
+
return parts.join('\n')
|
|
88
|
+
}
|
|
89
|
+
|
|
38
90
|
/**
|
|
39
91
|
* Pure comparison primitive — returns whether the on-disk `.aperant/.gitignore`
|
|
40
92
|
* differs from the policy-expected template. Returns `{ drifted: false }` when
|
|
41
93
|
* the `.aperant/` dir or file is absent (nothing to compare).
|
|
42
94
|
*
|
|
95
|
+
* BUG-028: when drifted, embeds a unified-diff summary (≤3 missing/extra
|
|
96
|
+
* lines per side) in `reason` so the user sees exactly what drifted
|
|
97
|
+
* without re-running `apt-tools config upgrade-gitignore --dry-run`.
|
|
98
|
+
*
|
|
43
99
|
* @internal
|
|
44
100
|
*/
|
|
45
101
|
export function checkGitignoreDrift(projectRoot) {
|
|
@@ -65,7 +121,16 @@ export function checkGitignoreDrift(projectRoot) {
|
|
|
65
121
|
} catch {
|
|
66
122
|
return { drifted: false, reason: 'read-failed' }
|
|
67
123
|
}
|
|
68
|
-
|
|
124
|
+
const drifted = actual !== expected
|
|
125
|
+
if (!drifted) return { drifted: false, reason: null }
|
|
126
|
+
const diff = buildGitignoreDiffSummary(expected, actual)
|
|
127
|
+
return {
|
|
128
|
+
drifted: true,
|
|
129
|
+
reason: diff
|
|
130
|
+
? `.aperant/.gitignore drift detected:\n${diff}`
|
|
131
|
+
: '.aperant/.gitignore drift detected (whitespace-only)',
|
|
132
|
+
diff,
|
|
133
|
+
}
|
|
69
134
|
}
|
|
70
135
|
|
|
71
136
|
/**
|
|
@@ -74,15 +139,21 @@ export function checkGitignoreDrift(projectRoot) {
|
|
|
74
139
|
* - `command` is not in the hot-path allowlist
|
|
75
140
|
* - `.aperant/.gitignore` exists AND does not match the expected template
|
|
76
141
|
*
|
|
142
|
+
* BUG-028: resolves the shared (main-repo) root internally so the same drift
|
|
143
|
+
* case produces the same verdict regardless of cwd context. Pre-fix this
|
|
144
|
+
* inspected `cwd` directly, producing a warning from one cwd context and a
|
|
145
|
+
* clean envelope from another for the same on-disk drift.
|
|
146
|
+
*
|
|
77
147
|
* Never throws.
|
|
78
148
|
*
|
|
79
149
|
* @internal
|
|
80
150
|
*/
|
|
81
|
-
export function emitDriftWarningIfAny(
|
|
151
|
+
export function emitDriftWarningIfAny(cwd, command) {
|
|
82
152
|
try {
|
|
83
153
|
if (process.env.APT_SUPPRESS_GITIGNORE_DRIFT === '1') return
|
|
84
154
|
if (!command || SKIP_COMMANDS.has(command)) return
|
|
85
|
-
const
|
|
155
|
+
const sharedRoot = resolveSharedRootForDrift(cwd)
|
|
156
|
+
const { drifted } = checkGitignoreDrift(sharedRoot)
|
|
86
157
|
if (drifted) {
|
|
87
158
|
process.stderr.write(
|
|
88
159
|
'[apt-tools] warning: .aperant/.gitignore is out of sync with share policy — run `apt-tools config upgrade-gitignore . --dry-run` to see the diff.\n',
|
package/src/cli/config/load.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* config/load.mjs — project config loader + task-isolation normalizer (C29).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
6
6
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
8
|
import { cliToInstallRoot } from '../install/cli-to-install-root.mjs'
|
|
@@ -31,7 +31,7 @@ export function loadProjectConfig(targetDir) {
|
|
|
31
31
|
* fallback would mask the user's broken override. See FRAMEWORK-AUDIT-001.
|
|
32
32
|
*
|
|
33
33
|
* @param {string} targetDir
|
|
34
|
-
* @returns {
|
|
34
|
+
* @returns {Record<string, any> | null}
|
|
35
35
|
*/
|
|
36
36
|
export function loadMergedProjectConfig(targetDir) {
|
|
37
37
|
const shared = loadProjectConfig(targetDir)
|
|
@@ -57,6 +57,192 @@ function deepMerge(a, b) {
|
|
|
57
57
|
return out
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* AUDIT-001 — read+write helpers for `.aperant/config.local.json`.
|
|
62
|
+
* Used by the 5 deferred fields migrated out of `config.json`:
|
|
63
|
+
* - verification.open_report
|
|
64
|
+
* - multi_model.cross_verification.tools
|
|
65
|
+
* - preferences.date_format
|
|
66
|
+
* - preferences.time_format
|
|
67
|
+
* - preferences.quick_task_post_verify
|
|
68
|
+
*
|
|
69
|
+
* Each field has its own read/write site. The shared helpers below give
|
|
70
|
+
* those sites a single primitive instead of hand-rolled JSON I/O.
|
|
71
|
+
*/
|
|
72
|
+
function setDottedPath(obj, dotPath, value) {
|
|
73
|
+
const parts = dotPath.split('.')
|
|
74
|
+
let cursor = obj
|
|
75
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
76
|
+
const key = parts[i]
|
|
77
|
+
if (!cursor[key] || typeof cursor[key] !== 'object' || Array.isArray(cursor[key])) {
|
|
78
|
+
cursor[key] = {}
|
|
79
|
+
}
|
|
80
|
+
cursor = cursor[key]
|
|
81
|
+
}
|
|
82
|
+
cursor[parts[parts.length - 1]] = value
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getDottedPath(obj, dotPath) {
|
|
86
|
+
const parts = dotPath.split('.')
|
|
87
|
+
let cursor = obj
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
if (cursor === null || cursor === undefined || typeof cursor !== 'object') return undefined
|
|
90
|
+
cursor = cursor[part]
|
|
91
|
+
}
|
|
92
|
+
return cursor
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function deleteDottedPath(obj, dotPath) {
|
|
96
|
+
const parts = dotPath.split('.')
|
|
97
|
+
let cursor = obj
|
|
98
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
99
|
+
const key = parts[i]
|
|
100
|
+
if (!cursor[key] || typeof cursor[key] !== 'object') return false
|
|
101
|
+
cursor = cursor[key]
|
|
102
|
+
}
|
|
103
|
+
const leaf = parts[parts.length - 1]
|
|
104
|
+
if (leaf in cursor) {
|
|
105
|
+
delete cursor[leaf]
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Read a single dotted-path field from the merged config (local overrides
|
|
113
|
+
* shared). Returns `undefined` when neither file carries the path.
|
|
114
|
+
*
|
|
115
|
+
* @internal
|
|
116
|
+
*/
|
|
117
|
+
export function readLocalOrSharedField(targetDir, dotPath) {
|
|
118
|
+
const merged = loadMergedProjectConfig(targetDir)
|
|
119
|
+
if (!merged) return undefined
|
|
120
|
+
return getDottedPath(merged, dotPath)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Write a single dotted-path field into `.aperant/config.local.json`,
|
|
125
|
+
* creating the file (and intermediate path) when it doesn't exist.
|
|
126
|
+
*
|
|
127
|
+
* The shared `config.json` is NOT touched by this helper — that's the
|
|
128
|
+
* load-bearing AUDIT-001 contract. Callers that need to remove a stale
|
|
129
|
+
* entry from `config.json` (e.g. the init.mjs migrator) must do so
|
|
130
|
+
* explicitly.
|
|
131
|
+
*
|
|
132
|
+
* @internal
|
|
133
|
+
*/
|
|
134
|
+
export function writeLocalConfigField(targetDir, dotPath, value) {
|
|
135
|
+
const aperantDir = join(targetDir, '.aperant')
|
|
136
|
+
mkdirSync(aperantDir, { recursive: true })
|
|
137
|
+
const localPath = join(aperantDir, 'config.local.json')
|
|
138
|
+
let local = {}
|
|
139
|
+
if (existsSync(localPath)) {
|
|
140
|
+
try {
|
|
141
|
+
local = JSON.parse(readFileSync(localPath, 'utf-8'))
|
|
142
|
+
if (local === null || typeof local !== 'object' || Array.isArray(local)) local = {}
|
|
143
|
+
} catch (e) {
|
|
144
|
+
throw new Error(`config.local.json parse error: ${e.message}`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
setDottedPath(local, dotPath, value)
|
|
148
|
+
writeFileSync(localPath, `${JSON.stringify(local, null, 2)}\n`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* AUDIT-001 — first-run migrator. For each dotted-path entry present in
|
|
153
|
+
* `.aperant/config.json`, copy the value into `.aperant/config.local.json`
|
|
154
|
+
* (idempotent — won't overwrite an existing local value) and delete the
|
|
155
|
+
* entry from `config.json`. Each field's migration is independent —
|
|
156
|
+
* one failure does NOT block the others (a per-field try/catch wraps
|
|
157
|
+
* each move).
|
|
158
|
+
*
|
|
159
|
+
* Returns an array `[{ field, migrated_value, status }]` for caller
|
|
160
|
+
* telemetry; `status` is `'migrated' | 'already-local' | 'absent' | 'error'`.
|
|
161
|
+
*
|
|
162
|
+
* @internal
|
|
163
|
+
*/
|
|
164
|
+
export function migrateDeferredFieldsToLocalConfig(targetDir, dotPaths) {
|
|
165
|
+
const results = []
|
|
166
|
+
const sharedPath = join(targetDir, '.aperant', 'config.json')
|
|
167
|
+
if (!existsSync(sharedPath)) {
|
|
168
|
+
for (const dotPath of dotPaths) results.push({ field: dotPath, status: 'absent' })
|
|
169
|
+
return results
|
|
170
|
+
}
|
|
171
|
+
let shared
|
|
172
|
+
try {
|
|
173
|
+
shared = JSON.parse(readFileSync(sharedPath, 'utf-8'))
|
|
174
|
+
} catch {
|
|
175
|
+
for (const dotPath of dotPaths) results.push({ field: dotPath, status: 'error' })
|
|
176
|
+
return results
|
|
177
|
+
}
|
|
178
|
+
// Pre-load local config ONCE so the already-local check reads from
|
|
179
|
+
// local-only state (NOT from the merged loader — that would conflate
|
|
180
|
+
// the shared entry we're about to delete with a real local override).
|
|
181
|
+
const localPath = join(targetDir, '.aperant', 'config.local.json')
|
|
182
|
+
let localAtStart = {}
|
|
183
|
+
if (existsSync(localPath)) {
|
|
184
|
+
try {
|
|
185
|
+
localAtStart = JSON.parse(readFileSync(localPath, 'utf-8')) ?? {}
|
|
186
|
+
} catch {
|
|
187
|
+
localAtStart = {}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
let sharedMutated = false
|
|
191
|
+
for (const dotPath of dotPaths) {
|
|
192
|
+
try {
|
|
193
|
+
const sharedValue = getDottedPath(shared, dotPath)
|
|
194
|
+
if (sharedValue === undefined) {
|
|
195
|
+
results.push({ field: dotPath, status: 'absent' })
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
const existingLocal = getDottedPath(localAtStart, dotPath)
|
|
199
|
+
// Already-local: the local override already exists. Strip the
|
|
200
|
+
// stale shared entry but keep the local value as-is (don't
|
|
201
|
+
// overwrite a user-tuned local with the stale shared default).
|
|
202
|
+
if (existingLocal !== undefined) {
|
|
203
|
+
deleteDottedPath(shared, dotPath)
|
|
204
|
+
sharedMutated = true
|
|
205
|
+
results.push({ field: dotPath, status: 'already-local', migrated_value: sharedValue })
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
writeLocalConfigField(targetDir, dotPath, sharedValue)
|
|
209
|
+
deleteDottedPath(shared, dotPath)
|
|
210
|
+
sharedMutated = true
|
|
211
|
+
results.push({ field: dotPath, status: 'migrated', migrated_value: sharedValue })
|
|
212
|
+
} catch (e) {
|
|
213
|
+
results.push({ field: dotPath, status: 'error', error: e?.message ?? String(e) })
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (sharedMutated) {
|
|
217
|
+
try {
|
|
218
|
+
writeFileSync(sharedPath, `${JSON.stringify(shared, null, 2)}\n`)
|
|
219
|
+
} catch (e) {
|
|
220
|
+
// Disk write failed after per-field local writes succeeded.
|
|
221
|
+
// Log a structured warning so callers can surface the partial state
|
|
222
|
+
// to the user (config.local.json updated, config.json NOT cleaned up).
|
|
223
|
+
results.push({
|
|
224
|
+
field: '__write_shared__',
|
|
225
|
+
status: 'error',
|
|
226
|
+
error: `migration_partial_rollback: config.local.json was updated but config.json could not be written (${e?.message ?? String(e)}). Re-run init to retry.`,
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return results
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* AUDIT-001 — the 5 deferred fields that move to config.local.json.
|
|
235
|
+
* Single source of truth used by both the init migrator and the
|
|
236
|
+
* per-field regression tests.
|
|
237
|
+
*/
|
|
238
|
+
export const AUDIT_001_LOCAL_FIELDS = Object.freeze([
|
|
239
|
+
'verification.open_report',
|
|
240
|
+
'multi_model.cross_verification.tools',
|
|
241
|
+
'preferences.date_format',
|
|
242
|
+
'preferences.time_format',
|
|
243
|
+
'preferences.quick_task_post_verify',
|
|
244
|
+
])
|
|
245
|
+
|
|
60
246
|
/**
|
|
61
247
|
* Return a normalized task_isolation config object with safe fallbacks.
|
|
62
248
|
* Always returns a concrete object even when the project has no config at all,
|
|
@@ -95,6 +95,12 @@ function normalizeStatus(f) {
|
|
|
95
95
|
if (typeof f.status === 'string') {
|
|
96
96
|
const s = f.status.toLowerCase().trim()
|
|
97
97
|
if (STATUS_MAP[s]) return STATUS_MAP[s]
|
|
98
|
+
// BUG-009: reviewer-skill writes `deferred-per-spec`, `deferred-followup`,
|
|
99
|
+
// etc. for findings that are intentionally not being fixed in the
|
|
100
|
+
// current scope. Collapse the variant prefix back to canonical
|
|
101
|
+
// `deferred` so downstream gates (review-clean, ship) treat them
|
|
102
|
+
// the same as a bare `status: "deferred"`.
|
|
103
|
+
if (s.startsWith('deferred-')) return 'deferred'
|
|
98
104
|
}
|
|
99
105
|
return 'open'
|
|
100
106
|
}
|
package/src/cli/dispatch.mjs
CHANGED
|
@@ -41,7 +41,7 @@ import { cmdAuthorSkill } from './commands/author-skill.mjs'
|
|
|
41
41
|
import { cmdBootstrap } from './commands/bootstrap.mjs'
|
|
42
42
|
import { cmdCheck } from './commands/check.mjs'
|
|
43
43
|
import { cmdCheckVersion } from './commands/check-version.mjs'
|
|
44
|
-
import { cmdCiWatch } from './commands/ci-watch.mjs'
|
|
44
|
+
import { cmdCiWatch, cmdCiWatchSweep } from './commands/ci-watch.mjs'
|
|
45
45
|
import { cmdCommit } from './commands/commit.mjs'
|
|
46
46
|
import { cmdConsistency } from './commands/consistency.mjs'
|
|
47
47
|
import { cmdConstitution } from './commands/constitution.mjs'
|
|
@@ -93,6 +93,7 @@ import { cmdValidateEvidence } from './commands/validate-evidence.mjs'
|
|
|
93
93
|
import { cmdVariantGallery } from './commands/variant-gallery.mjs'
|
|
94
94
|
import { cmdVariantPick } from './commands/variant-pick.mjs'
|
|
95
95
|
import { cmdVariantPool } from './commands/variant-pool.mjs'
|
|
96
|
+
import { cmdVitestDoctor } from './commands/vitest-doctor.mjs'
|
|
96
97
|
import { cmdWave } from './commands/wave.mjs'
|
|
97
98
|
import { cmdWfrun } from './commands/wfrun.mjs'
|
|
98
99
|
import { cmdWorkflow } from './commands/workflow.mjs'
|
|
@@ -241,6 +242,21 @@ export const DISPATCH = {
|
|
|
241
242
|
'precompact-hook': (args) => wrap(cmdPrecompactHook)(args[1]),
|
|
242
243
|
'migrate-pr-reviews': (args) => wrap(cmdMigratePrReviews)(args[1]),
|
|
243
244
|
'validate-evidence': (args) => wrap(cmdValidateEvidence)(args[1], args.slice(2)),
|
|
245
|
+
vitest: (args) => {
|
|
246
|
+
// `vitest doctor <project-dir>` — currently the only `vitest`
|
|
247
|
+
// subcommand. Reserved namespace for future vitest diagnostics.
|
|
248
|
+
const subcommand = args[1]
|
|
249
|
+
if (subcommand === 'doctor') {
|
|
250
|
+
return wrap(cmdVitestDoctor)(args[2], args.slice(3))
|
|
251
|
+
}
|
|
252
|
+
return wrap(() => ({
|
|
253
|
+
envelope: {
|
|
254
|
+
status: 'error',
|
|
255
|
+
error: `Usage: apt-tools vitest doctor <project-dir>. Unknown subcommand: ${subcommand ?? '(none)'}`,
|
|
256
|
+
},
|
|
257
|
+
exitCode: 1,
|
|
258
|
+
}))()
|
|
259
|
+
},
|
|
244
260
|
|
|
245
261
|
// ---- (positional dir, args[2] subcommand, slice(3)) shape ----
|
|
246
262
|
tokens: (args) => wrap(cmdTokens)(args[1], args[2], args.slice(3)),
|
|
@@ -261,7 +277,12 @@ export const DISPATCH = {
|
|
|
261
277
|
seed: (args) => wrap(cmdSeed)(args[1], args[2], args.slice(3)),
|
|
262
278
|
workflow: (args) => wrap(cmdWorkflow)(args[1], args[2], args.slice(3)),
|
|
263
279
|
'pr-review': (args) => wrap(cmdPrReview)(args[1], args[2], args.slice(3)),
|
|
264
|
-
'ci-watch': (args) =>
|
|
280
|
+
'ci-watch': (args) => {
|
|
281
|
+
// `sweep` is async; route it to the async sibling so the sync dispatcher
|
|
282
|
+
// path doesn't return a CommandResult | Promise<CommandResult> union.
|
|
283
|
+
if (args[1] === 'sweep') return wrap(cmdCiWatchSweep)(args[2], args.slice(3))
|
|
284
|
+
return wrap(cmdCiWatch)(args[1], args[2], args.slice(3))
|
|
285
|
+
},
|
|
265
286
|
'plan-approval': (args) => wrap(cmdPlanApproval)(args[1], args[2], args.slice(3)),
|
|
266
287
|
|
|
267
288
|
// ---- Pocock adoption (ST-5, ST-6, ST-10, ST-13) artifact writers + discoverability ----
|
|
@@ -22,10 +22,13 @@ const gate = {
|
|
|
22
22
|
]
|
|
23
23
|
const result = checkGitignoreDrift(ctx.sharedRoot)
|
|
24
24
|
if (result.drifted) {
|
|
25
|
+
// BUG-028: surface the unified-diff summary in the reason so
|
|
26
|
+
// users see exactly which template lines drifted without
|
|
27
|
+
// re-running `apt-tools config upgrade-gitignore --dry-run`.
|
|
25
28
|
return {
|
|
26
29
|
status: 'fail',
|
|
27
|
-
reason: '.aperant/.gitignore has drifted from policy-expected template',
|
|
28
|
-
evidence: {
|
|
30
|
+
reason: result.reason ?? '.aperant/.gitignore has drifted from policy-expected template',
|
|
31
|
+
evidence: { diff: result.diff ?? null },
|
|
29
32
|
inputs,
|
|
30
33
|
}
|
|
31
34
|
}
|
|
@@ -3,24 +3,27 @@
|
|
|
3
3
|
* G5 review-clean — blocking gate for `apt:ship`.
|
|
4
4
|
*
|
|
5
5
|
* Skipped when {task_dir}/review.json is absent (review is optional for some
|
|
6
|
-
* tracks). When it is present,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* reviewer format — we accept either.
|
|
6
|
+
* tracks). When it is present, delegates to `parseReview` (DRY with the
|
|
7
|
+
* consistency rule) which normalizes severities + statuses across all the
|
|
8
|
+
* shapes the reviewer skill writes: `fixed: true`, `resolved: true`,
|
|
9
|
+
* `status: "fixed"` (string), and `status: "deferred-*"` variants
|
|
10
|
+
* (BUG-009). Fails if any post-normalization finding is severity
|
|
11
|
+
* critical|major AND status is neither `fixed` nor `deferred`.
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
14
|
import { existsSync, readFileSync } from 'node:fs'
|
|
16
15
|
import { join } from 'node:path'
|
|
16
|
+
import { parseReview } from '../../consistency/parse-review.mjs'
|
|
17
17
|
import { registerGate } from '../registry.mjs'
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Post-normalization severities. `parseReview` maps `warning → major`,
|
|
20
|
+
// so the gate keys off `major` here even though the input may carry
|
|
21
|
+
// `warning`.
|
|
22
|
+
const POST_NORM_BLOCKING = new Set(['critical', 'major'])
|
|
20
23
|
|
|
21
24
|
const gate = {
|
|
22
25
|
id: 'review-clean',
|
|
23
|
-
description: 'review.json has no unresolved critical/
|
|
26
|
+
description: 'review.json has no unresolved critical/major findings (skipped if absent)',
|
|
24
27
|
severity: 'blocking',
|
|
25
28
|
skills: ['ship'],
|
|
26
29
|
async check(ctx) {
|
|
@@ -43,23 +46,25 @@ const gate = {
|
|
|
43
46
|
inputs,
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
// BUG-009: delegate to parseReview so this gate sees the SAME
|
|
50
|
+
// normalized findings the consistency rule does. The pre-fix
|
|
51
|
+
// inline filter only inspected boolean `fixed`/`resolved`, missing
|
|
52
|
+
// `status: "fixed"` strings + `summary.findings_remaining: 0`
|
|
53
|
+
// shapes the reviewer skill writes; this delegation closes that
|
|
54
|
+
// hole and picks up `deferred-*` variants for free.
|
|
55
|
+
const { findings: normalized } = parseReview(doc)
|
|
56
|
+
const unresolved = normalized.filter(
|
|
57
|
+
(f) => POST_NORM_BLOCKING.has(f.severity) && f.status !== 'fixed' && f.status !== 'deferred',
|
|
53
58
|
)
|
|
54
59
|
if (unresolved.length > 0) {
|
|
55
60
|
return {
|
|
56
61
|
status: 'fail',
|
|
57
|
-
reason: `${unresolved.length} unresolved critical/
|
|
62
|
+
reason: `${unresolved.length} unresolved critical/major finding(s) in review.json`,
|
|
58
63
|
evidence: {
|
|
59
64
|
unresolved: unresolved.map((f) => ({
|
|
60
65
|
severity: f.severity,
|
|
61
66
|
id: f.id ?? null,
|
|
62
|
-
title: f.title ??
|
|
67
|
+
title: f.title ?? null,
|
|
63
68
|
})),
|
|
64
69
|
},
|
|
65
70
|
inputs,
|
|
@@ -67,7 +72,7 @@ const gate = {
|
|
|
67
72
|
}
|
|
68
73
|
return {
|
|
69
74
|
status: 'pass',
|
|
70
|
-
reason: `review.json has no unresolved critical/
|
|
75
|
+
reason: `review.json has no unresolved critical/major findings (${normalized.length} total)`,
|
|
71
76
|
inputs,
|
|
72
77
|
}
|
|
73
78
|
},
|