@nan0web/release 3.2.0 → 3.4.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/bin/nan0release.js +33 -2
- package/bin/serve.js +73 -0
- package/package.json +45 -17
- package/src/Release/Document.js +1 -0
- package/src/Release/Scanner.js +3 -2
- package/src/Release.js +7 -7
- package/src/domain/App.js +31 -2
- package/src/domain/BumpCommand.js +10 -3
- package/src/domain/CheckCommand.js +504 -146
- package/src/domain/CheckCommand.story.js +92 -0
- package/src/domain/CloseCommand.js +13 -9
- package/src/domain/Dashboard/Dashboard.story.js +155 -0
- package/src/domain/Dashboard/DashboardModel.js +308 -0
- package/src/domain/Dashboard/ReleaseDiscovery.js +132 -0
- package/src/domain/Dashboard/ReleaseParser.js +226 -0
- package/src/domain/Dashboard/index.js +19 -0
- package/src/domain/DepsCommand.js +5 -2
- package/src/domain/InitCommand.js +18 -16
- package/src/domain/ListCommand.js +45 -0
- package/src/domain/PreviewCommand.js +39 -0
- package/src/domain/PublishCommand.js +7 -3
- package/src/domain/Registry/Registry.story.js +82 -0
- package/src/domain/Registry/RegistryModel.js +124 -0
- package/src/domain/ServeCommand.js +165 -0
- package/src/domain/ServeCommand.story.js +88 -0
- package/src/domain/SpecCommand.js +3 -1
- package/src/domain/StatusCommand.js +249 -3
- package/src/domain/ViewCommand.js +99 -0
- package/src/domain/WebCommand.js +140 -0
- package/src/domain/inspect/ReleaseAuditor.js +508 -0
- package/src/domain/inspect/ReleaseAuditor.story.js +136 -0
- package/src/domain/templates/release.html +34 -0
- package/src/domain/templates/release.html.template.js +34 -0
- package/src/index.js +6 -0
- package/src/inspectors/inspect-js.js +32 -0
- package/src/releases/1/0/v1.0.3/task.test.js +1 -1
- package/src/utils/nodejs/GitTelemetry.js +154 -0
- package/types/Release/Document.d.ts +4 -0
- package/types/Release/Scanner.d.ts +2 -2
- package/types/domain/App.d.ts +19 -3
- package/types/domain/BumpCommand.d.ts +9 -3
- package/types/domain/CheckCommand.d.ts +127 -15
- package/types/domain/CheckCommand.story.d.ts +1 -0
- package/types/domain/CloseCommand.d.ts +2 -1
- package/types/domain/Dashboard/Dashboard.story.d.ts +1 -0
- package/types/domain/Dashboard/DashboardModel.d.ts +273 -0
- package/types/domain/Dashboard/GitTelemetry.d.ts +23 -0
- package/types/domain/Dashboard/ReleaseDiscovery.d.ts +47 -0
- package/types/domain/Dashboard/ReleaseParser.d.ts +79 -0
- package/types/domain/Dashboard/index.d.ts +3 -0
- package/types/domain/DepsCommand.d.ts +4 -2
- package/types/domain/InitCommand.d.ts +2 -1
- package/types/domain/ListCommand.d.ts +18 -0
- package/types/domain/PreviewCommand.d.ts +6 -0
- package/types/domain/PublishCommand.d.ts +6 -3
- package/types/domain/Registry/Registry.story.d.ts +1 -0
- package/types/domain/Registry/RegistryModel.d.ts +61 -0
- package/types/domain/ServeCommand.d.ts +59 -0
- package/types/domain/ServeCommand.story.d.ts +1 -0
- package/types/domain/SpecCommand.d.ts +2 -1
- package/types/domain/StatusCommand.d.ts +39 -0
- package/types/domain/ViewCommand.d.ts +27 -0
- package/types/domain/WebCommand.d.ts +47 -0
- package/types/domain/inspect/ReleaseAuditor.d.ts +134 -0
- package/types/domain/inspect/ReleaseAuditor.story.d.ts +1 -0
- package/types/domain/templates/release.html.template.d.ts +2 -0
- package/types/index.d.ts +5 -0
- package/types/inspectors/inspect-js.d.ts +2 -0
- package/types/utils/nodejs/GitTelemetry.d.ts +23 -0
|
@@ -1,193 +1,551 @@
|
|
|
1
|
-
import { ModelAsApp, result,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { ModelAsApp, show, result, progress } from '@nan0web/ui'
|
|
2
|
+
import { RegistryModel } from './Registry/RegistryModel.js'
|
|
3
|
+
import { DashboardModel } from './Dashboard/DashboardModel.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} DiagnosticItem
|
|
7
|
+
* @property {'error' | 'warning' | 'info'} level
|
|
8
|
+
* @property {string} code
|
|
9
|
+
* @property {string} project
|
|
10
|
+
* @property {string} message
|
|
11
|
+
* @property {string} file
|
|
12
|
+
* @property {string} fix
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* CheckCommand - PM-as-Code Release Structure & Compliance Validator.
|
|
17
|
+
*/
|
|
18
|
+
export class CheckCommand extends ModelAsApp {
|
|
19
|
+
static alias = 'check'
|
|
5
20
|
|
|
6
|
-
export default class CheckCommand extends ModelAsApp {
|
|
7
21
|
static UI = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
22
|
+
errorDbRequired: 'Database instance ({ db }) is required to execute CheckCommand',
|
|
23
|
+
title: 'PM-as-Code Structure & Compliance Validator',
|
|
24
|
+
header: '🔍 PM-as-Code Release Structure Validator (nan0release check)',
|
|
25
|
+
scanningCount: 'Scanning {$count} project(s)...',
|
|
26
|
+
projectHeader: '\n📦 Project: {$name} ({$path})',
|
|
27
|
+
missingPackageJson: ' ❌ Missing package.json',
|
|
28
|
+
packageJsonValid: ' ✅ package.json (v{$version})',
|
|
29
|
+
noReleasesDir: ' ❌ No release directories found in {$path}',
|
|
30
|
+
releasesDirFound: ' ✅ releases/ directory located ({$count} release(s) found)',
|
|
31
|
+
releaseHeader: ' 📂 Release [v{$version}] at {$path}:',
|
|
32
|
+
releaseDocCompleted: ' ✅ {$file} ({$done}/{$total} tasks completed)',
|
|
33
|
+
missingReleaseDoc: ' ❌ Missing release.md or task.md',
|
|
34
|
+
missingUserDoc: ' ⚠️ Missing user.md metrics document',
|
|
35
|
+
incompleteUserMetrics:
|
|
36
|
+
' ⚠️ user.md present but metrics are incomplete (Hours: 0h, RRS: 0)',
|
|
37
|
+
userMetricsValid: ' ✅ user.md (Hours: {$hours}h, Iterations: {$iterations}, RRS: {$rrs})',
|
|
38
|
+
ambiguousContracts: ' ❌ Both task.spec.js and task.test.js exist simultaneously',
|
|
39
|
+
missingContract: ' ❌ Missing contract test (task.spec.js or task.test.js)',
|
|
40
|
+
activeContractWip: ' ✅ Active contract: task.spec.js [WIP]',
|
|
41
|
+
sealedRegressionClosed: ' ✅ Sealed regression: task.test.js [CLOSED]',
|
|
42
|
+
orphanFile: ' ⚠️ Orphan file in releases/: {$file}',
|
|
43
|
+
recommendationsHeader: '\n📋 Actionable Recommendations & Agent Instructions ({$count}):',
|
|
44
|
+
recommendationItem:
|
|
45
|
+
'\n[{$idx}] {$badge} [{$code}] {$message}\n File: {$file}\n 👉 Fix Instruction (Human / AI Agent):\n {$fix}',
|
|
46
|
+
summary:
|
|
47
|
+
'\n📊 Summary: {$projects} project(s) checked | Passed: {$passed} | Errors: {$errors} | Warnings: {$warnings}',
|
|
48
|
+
Diagnostics: {
|
|
49
|
+
MISSING_PACKAGE_JSON: {
|
|
50
|
+
level: 'error',
|
|
51
|
+
code: 'MISSING_PACKAGE_JSON',
|
|
52
|
+
message: 'Missing package.json in {$path}',
|
|
53
|
+
fix: 'Ensure {$projectRoot}package.json exists with valid "name" and "version" fields.',
|
|
54
|
+
},
|
|
55
|
+
MISSING_RELEASES_DIR: {
|
|
56
|
+
level: 'error',
|
|
57
|
+
code: 'MISSING_RELEASES_DIR',
|
|
58
|
+
message: 'No release directories found under {$path}',
|
|
59
|
+
fix: 'Initialize release directory: `pnpm exec release init v{$version}`',
|
|
60
|
+
},
|
|
61
|
+
MISSING_RELEASE_MD: {
|
|
62
|
+
level: 'error',
|
|
63
|
+
code: 'MISSING_RELEASE_MD',
|
|
64
|
+
message: 'Missing release.md or task.md in {$path}',
|
|
65
|
+
fix: 'Create {$path}/release.md with release notes, tasks checklist, and acceptance criteria.',
|
|
66
|
+
},
|
|
67
|
+
MISSING_USER_MD: {
|
|
68
|
+
level: 'warning',
|
|
69
|
+
code: 'MISSING_USER_MD',
|
|
70
|
+
message: 'Missing user.md metrics document in {$path}',
|
|
71
|
+
fix: 'Create {$path}/user.md containing Execution Metrics (Hours, Iterations, and RRS score).',
|
|
72
|
+
},
|
|
73
|
+
INCOMPLETE_USER_METRICS: {
|
|
74
|
+
level: 'warning',
|
|
75
|
+
code: 'INCOMPLETE_USER_METRICS',
|
|
76
|
+
message: 'user.md in {$path} is missing hours, iterations or RRS score',
|
|
77
|
+
fix: 'Update {$path}/user.md with sections: "Витрачений час розробки (Годин):", "Кількість ітерацій:", "Фінальний бал RRS:".',
|
|
78
|
+
},
|
|
79
|
+
AMBIGUOUS_CONTRACTS: {
|
|
80
|
+
level: 'error',
|
|
81
|
+
code: 'AMBIGUOUS_CONTRACTS',
|
|
82
|
+
message:
|
|
83
|
+
'Both task.spec.js (WIP) and task.test.js (Closed) exist simultaneously in {$path}',
|
|
84
|
+
fix: 'If release is still in progress, delete task.test.js. If closed, delete task.spec.js.',
|
|
85
|
+
},
|
|
86
|
+
MISSING_CONTRACT: {
|
|
87
|
+
level: 'error',
|
|
88
|
+
code: 'MISSING_CONTRACT',
|
|
89
|
+
message: 'No contract test found in {$path} (missing task.spec.js or task.test.js)',
|
|
90
|
+
fix: 'Create contract test {$path}/task.spec.js for active development, or rename to task.test.js upon release sealing.',
|
|
91
|
+
},
|
|
92
|
+
ORPHAN_RELEASE_FILE: {
|
|
93
|
+
level: 'warning',
|
|
94
|
+
code: 'ORPHAN_RELEASE_FILE',
|
|
95
|
+
message:
|
|
96
|
+
'Orphan file "{$file}" located directly in releases/ directory outside of version folders',
|
|
97
|
+
fix: 'Move "{$file}" into the appropriate version directory (e.g. releases/vX.Y.Z/) or remove it.',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
11
100
|
}
|
|
12
101
|
|
|
13
|
-
static
|
|
102
|
+
static target = {
|
|
103
|
+
help: 'Project path, package name, or "." to validate',
|
|
104
|
+
default: '',
|
|
105
|
+
type: 'string',
|
|
106
|
+
positional: true,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static registryFile = {
|
|
110
|
+
help: 'Path to releases.txt registry file',
|
|
111
|
+
default: 'releases.txt',
|
|
112
|
+
type: 'string',
|
|
113
|
+
alias: 'f',
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static all = {
|
|
117
|
+
help: 'Validate all projects registered in releases.txt',
|
|
118
|
+
default: false,
|
|
119
|
+
type: 'boolean',
|
|
120
|
+
alias: 'a',
|
|
121
|
+
}
|
|
14
122
|
|
|
15
123
|
/**
|
|
16
|
-
* @
|
|
124
|
+
* @param {Partial<CheckCommand>} [data]
|
|
125
|
+
* @param {Partial<import('@nan0web/ui').ModelAsAppOptions>} [options]
|
|
17
126
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
127
|
+
constructor(data = {}, options = {}) {
|
|
128
|
+
super(data, options)
|
|
129
|
+
/** @type {string} */ this.target
|
|
130
|
+
/** @type {string} */ this.registryFile
|
|
131
|
+
/** @type {boolean} */ this.all
|
|
132
|
+
}
|
|
22
133
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Execute release structure check
|
|
136
|
+
* @returns {AsyncGenerator<import('@nan0web/ui').Intent, { success: boolean, diagnostics: DiagnosticItem[], checkedProjects: any[] }, any>}
|
|
137
|
+
*/
|
|
138
|
+
async *run() {
|
|
139
|
+
const rawT = this._.t
|
|
140
|
+
const db = this._.db
|
|
141
|
+
const t = (template, params = {}) => {
|
|
142
|
+
let str = (rawT ? rawT(template, params) : template) || template || ''
|
|
143
|
+
if (params && typeof params === 'object') {
|
|
144
|
+
for (const [k, v] of Object.entries(params)) {
|
|
145
|
+
str = str.replaceAll(`{$${k}}`, String(v)).replaceAll(`{${k}}`, String(v))
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return str
|
|
149
|
+
}
|
|
150
|
+
if (!db) {
|
|
151
|
+
throw new Error(t(CheckCommand.UI.errorDbRequired))
|
|
26
152
|
}
|
|
27
153
|
|
|
28
|
-
|
|
29
|
-
const version = pkg.version
|
|
30
|
-
yield show(t('Auditing {name}@{version} vs NPM...', { name, version }), 'info')
|
|
154
|
+
yield show(t(CheckCommand.UI.header))
|
|
31
155
|
|
|
32
|
-
|
|
33
|
-
|
|
156
|
+
// 1. Resolve list of projects to check
|
|
157
|
+
const registry = new RegistryModel({ filePath: this.registryFile || 'releases.txt' }, { db })
|
|
158
|
+
let registeredProjects = []
|
|
159
|
+
try {
|
|
160
|
+
registeredProjects = await registry.loadProjects()
|
|
161
|
+
} catch {
|
|
162
|
+
registeredProjects = []
|
|
163
|
+
}
|
|
34
164
|
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
165
|
+
let targetsToCheck = []
|
|
166
|
+
const targetParam = (this.target || '').trim()
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
this.all ||
|
|
170
|
+
targetParam === '--all' ||
|
|
171
|
+
targetParam === '-a' ||
|
|
172
|
+
(!targetParam && registeredProjects.length > 0)
|
|
173
|
+
) {
|
|
174
|
+
targetsToCheck = registeredProjects
|
|
175
|
+
} else if (targetParam) {
|
|
176
|
+
const targetNum = parseInt(targetParam, 10)
|
|
177
|
+
if (!isNaN(targetNum) && targetNum >= 1 && targetNum <= registeredProjects.length) {
|
|
178
|
+
targetsToCheck = [registeredProjects[targetNum - 1]]
|
|
179
|
+
} else {
|
|
180
|
+
const matched = registeredProjects.find(
|
|
181
|
+
(p) =>
|
|
182
|
+
p.name === targetParam ||
|
|
183
|
+
p.path === targetParam ||
|
|
184
|
+
p.path === targetParam.replace(/\/+$/, '')
|
|
185
|
+
)
|
|
186
|
+
if (matched) {
|
|
187
|
+
targetsToCheck = [matched]
|
|
188
|
+
} else {
|
|
189
|
+
targetsToCheck = [{ path: targetParam, name: targetParam, version: '0.0.0' }]
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
targetsToCheck = [{ path: '.', name: 'current-project', version: '0.0.0' }]
|
|
194
|
+
}
|
|
39
195
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
196
|
+
yield progress(t(CheckCommand.UI.scanningCount, { count: targetsToCheck.length }))
|
|
197
|
+
|
|
198
|
+
/** @type {DiagnosticItem[]} */
|
|
199
|
+
const diagnostics = []
|
|
200
|
+
const checkedProjects = []
|
|
201
|
+
|
|
202
|
+
for (const proj of targetsToCheck) {
|
|
203
|
+
const projectRoot = proj.path === '.' ? '' : `${proj.path.replace(/\/+$/, '')}/`
|
|
204
|
+
|
|
205
|
+
yield show(t(CheckCommand.UI.projectHeader, { name: proj.name, path: proj.path }))
|
|
206
|
+
|
|
207
|
+
// A. Validate package.json via db
|
|
208
|
+
let pkg = null
|
|
209
|
+
try {
|
|
210
|
+
pkg = await db.loadDocument(`${projectRoot}package.json`, null)
|
|
211
|
+
} catch {}
|
|
212
|
+
|
|
213
|
+
if (!pkg) {
|
|
214
|
+
const diagDef = CheckCommand.UI.Diagnostics.MISSING_PACKAGE_JSON
|
|
215
|
+
diagnostics.push({
|
|
216
|
+
level: diagDef.level,
|
|
217
|
+
code: diagDef.code,
|
|
218
|
+
project: proj.name,
|
|
219
|
+
file: `${projectRoot}package.json`,
|
|
220
|
+
message: t(diagDef.message, { path: proj.path }),
|
|
221
|
+
fix: t(diagDef.fix, { projectRoot }),
|
|
222
|
+
})
|
|
223
|
+
yield show(t(CheckCommand.UI.missingPackageJson), 'error')
|
|
224
|
+
} else {
|
|
225
|
+
yield show(
|
|
226
|
+
t(CheckCommand.UI.packageJsonValid, { version: pkg.version || '0.0.0' }),
|
|
227
|
+
'success'
|
|
228
|
+
)
|
|
70
229
|
}
|
|
71
230
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
231
|
+
// B. Discover release directories via db
|
|
232
|
+
const releaseDirs = await this.#discoverReleases(db, projectRoot)
|
|
233
|
+
|
|
234
|
+
if (releaseDirs.length === 0) {
|
|
235
|
+
const diagDef = CheckCommand.UI.Diagnostics.MISSING_RELEASES_DIR
|
|
236
|
+
diagnostics.push({
|
|
237
|
+
level: diagDef.level,
|
|
238
|
+
code: diagDef.code,
|
|
239
|
+
project: proj.name,
|
|
240
|
+
file: `${projectRoot}releases/`,
|
|
241
|
+
message: t(diagDef.message, { path: `${projectRoot}releases/` }),
|
|
242
|
+
fix: t(diagDef.fix, { version: pkg?.version || '0.1.0' }),
|
|
243
|
+
})
|
|
244
|
+
yield show(
|
|
245
|
+
t(CheckCommand.UI.noReleasesDir, { path: `${projectRoot}releases/` }),
|
|
246
|
+
'error'
|
|
247
|
+
)
|
|
248
|
+
checkedProjects.push({ name: proj.name, path: proj.path, valid: false })
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
75
251
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
252
|
+
yield show(
|
|
253
|
+
t(CheckCommand.UI.releasesDirFound, { count: releaseDirs.length }),
|
|
254
|
+
'success'
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
// C. Check each release directory
|
|
258
|
+
for (const rel of releaseDirs) {
|
|
259
|
+
const relPath = rel.path
|
|
260
|
+
yield show(t(CheckCommand.UI.releaseHeader, { version: rel.version, path: relPath }))
|
|
261
|
+
|
|
262
|
+
// 1. Check release.md / task.md
|
|
263
|
+
let releaseContent = null
|
|
264
|
+
let releaseDocFile = ''
|
|
265
|
+
for (const docFile of ['release.md', 'task.md', 'plan.md']) {
|
|
266
|
+
try {
|
|
267
|
+
const doc = await db.loadDocument(`${relPath}/${docFile}`, null)
|
|
268
|
+
if (doc) {
|
|
269
|
+
releaseContent = String(doc)
|
|
270
|
+
releaseDocFile = `${relPath}/${docFile}`
|
|
271
|
+
break
|
|
272
|
+
}
|
|
273
|
+
} catch {}
|
|
79
274
|
}
|
|
80
275
|
|
|
81
|
-
|
|
82
|
-
|
|
276
|
+
if (!releaseContent) {
|
|
277
|
+
const diagDef = CheckCommand.UI.Diagnostics.MISSING_RELEASE_MD
|
|
278
|
+
diagnostics.push({
|
|
279
|
+
level: diagDef.level,
|
|
280
|
+
code: diagDef.code,
|
|
281
|
+
project: proj.name,
|
|
282
|
+
file: `${relPath}/release.md`,
|
|
283
|
+
message: t(diagDef.message, { path: relPath }),
|
|
284
|
+
fix: t(diagDef.fix, { path: relPath }),
|
|
285
|
+
})
|
|
286
|
+
yield show(t(CheckCommand.UI.missingReleaseDoc), 'error')
|
|
287
|
+
} else {
|
|
288
|
+
const tasks = this.#countTasks(releaseContent)
|
|
289
|
+
yield show(
|
|
290
|
+
t(CheckCommand.UI.releaseDocCompleted, {
|
|
291
|
+
file: releaseDocFile.split('/').pop(),
|
|
292
|
+
done: tasks.done,
|
|
293
|
+
total: tasks.total,
|
|
294
|
+
}),
|
|
295
|
+
'success'
|
|
296
|
+
)
|
|
297
|
+
}
|
|
83
298
|
|
|
84
|
-
|
|
85
|
-
|
|
299
|
+
// 2. Check user.md
|
|
300
|
+
let userContent = null
|
|
301
|
+
try {
|
|
302
|
+
const u = await db.loadDocument(`${relPath}/user.md`, null)
|
|
303
|
+
if (u) userContent = String(u)
|
|
304
|
+
} catch {}
|
|
305
|
+
|
|
306
|
+
if (!userContent) {
|
|
307
|
+
const diagDef = CheckCommand.UI.Diagnostics.MISSING_USER_MD
|
|
308
|
+
diagnostics.push({
|
|
309
|
+
level: diagDef.level,
|
|
310
|
+
code: diagDef.code,
|
|
311
|
+
project: proj.name,
|
|
312
|
+
file: `${relPath}/user.md`,
|
|
313
|
+
message: t(diagDef.message, { path: relPath }),
|
|
314
|
+
fix: t(diagDef.fix, { path: relPath }),
|
|
315
|
+
})
|
|
316
|
+
yield show(t(CheckCommand.UI.missingUserDoc), 'warn')
|
|
86
317
|
} else {
|
|
87
|
-
|
|
318
|
+
const metrics = DashboardModel.parseUserMetrics(userContent)
|
|
319
|
+
if (metrics.hours === 0 && metrics.rrs === 0) {
|
|
320
|
+
const diagDef = CheckCommand.UI.Diagnostics.INCOMPLETE_USER_METRICS
|
|
321
|
+
diagnostics.push({
|
|
322
|
+
level: diagDef.level,
|
|
323
|
+
code: diagDef.code,
|
|
324
|
+
project: proj.name,
|
|
325
|
+
file: `${relPath}/user.md`,
|
|
326
|
+
message: t(diagDef.message, { path: relPath }),
|
|
327
|
+
fix: t(diagDef.fix, { path: relPath }),
|
|
328
|
+
})
|
|
329
|
+
yield show(t(CheckCommand.UI.incompleteUserMetrics), 'warn')
|
|
330
|
+
} else {
|
|
331
|
+
yield show(
|
|
332
|
+
t(CheckCommand.UI.userMetricsValid, {
|
|
333
|
+
hours: metrics.hours,
|
|
334
|
+
iterations: metrics.iterations,
|
|
335
|
+
rrs: metrics.rrs,
|
|
336
|
+
}),
|
|
337
|
+
'success'
|
|
338
|
+
)
|
|
339
|
+
}
|
|
88
340
|
}
|
|
89
|
-
}
|
|
90
341
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
342
|
+
// 3. Check contract tests (task.spec.js / task.test.js)
|
|
343
|
+
let hasSpec = false
|
|
344
|
+
let hasTest = false
|
|
345
|
+
try {
|
|
346
|
+
hasSpec = (await db.loadDocument(`${relPath}/task.spec.js`, null)) !== null
|
|
347
|
+
} catch {}
|
|
348
|
+
try {
|
|
349
|
+
hasTest = (await db.loadDocument(`${relPath}/task.test.js`, null)) !== null
|
|
350
|
+
} catch {}
|
|
351
|
+
|
|
352
|
+
if (hasSpec && hasTest) {
|
|
353
|
+
const diagDef = CheckCommand.UI.Diagnostics.AMBIGUOUS_CONTRACTS
|
|
354
|
+
diagnostics.push({
|
|
355
|
+
level: diagDef.level,
|
|
356
|
+
code: diagDef.code,
|
|
357
|
+
project: proj.name,
|
|
358
|
+
file: relPath,
|
|
359
|
+
message: t(diagDef.message, { path: relPath }),
|
|
360
|
+
fix: t(diagDef.fix),
|
|
361
|
+
})
|
|
362
|
+
yield show(t(CheckCommand.UI.ambiguousContracts), 'error')
|
|
363
|
+
} else if (!hasSpec && !hasTest) {
|
|
364
|
+
const diagDef = CheckCommand.UI.Diagnostics.MISSING_CONTRACT
|
|
365
|
+
diagnostics.push({
|
|
366
|
+
level: diagDef.level,
|
|
367
|
+
code: diagDef.code,
|
|
368
|
+
project: proj.name,
|
|
369
|
+
file: `${relPath}/task.spec.js`,
|
|
370
|
+
message: t(diagDef.message, { path: relPath }),
|
|
371
|
+
fix: t(diagDef.fix, { path: relPath }),
|
|
372
|
+
})
|
|
373
|
+
yield show(t(CheckCommand.UI.missingContract), 'error')
|
|
374
|
+
} else if (hasSpec) {
|
|
375
|
+
yield show(t(CheckCommand.UI.activeContractWip), 'success')
|
|
376
|
+
} else if (hasTest) {
|
|
377
|
+
yield show(t(CheckCommand.UI.sealedRegressionClosed), 'success')
|
|
96
378
|
}
|
|
97
379
|
}
|
|
98
380
|
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
381
|
+
// D. Check for orphan files inside releases/ via db
|
|
382
|
+
const orphanFiles = await this.#findOrphanReleaseFiles(db, projectRoot)
|
|
383
|
+
if (orphanFiles.length > 0) {
|
|
384
|
+
const diagDef = CheckCommand.UI.Diagnostics.ORPHAN_RELEASE_FILE
|
|
385
|
+
for (const orphan of orphanFiles) {
|
|
386
|
+
diagnostics.push({
|
|
387
|
+
level: diagDef.level,
|
|
388
|
+
code: diagDef.code,
|
|
389
|
+
project: proj.name,
|
|
390
|
+
file: `${projectRoot}releases/${orphan}`,
|
|
391
|
+
message: t(diagDef.message, { file: orphan }),
|
|
392
|
+
fix: t(diagDef.fix, { file: orphan }),
|
|
393
|
+
})
|
|
394
|
+
yield show(t(CheckCommand.UI.orphanFile, { file: orphan }), 'warn')
|
|
395
|
+
}
|
|
103
396
|
}
|
|
104
397
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
398
|
+
checkedProjects.push({
|
|
399
|
+
name: proj.name,
|
|
400
|
+
path: proj.path,
|
|
401
|
+
releases: releaseDirs.map((r) => r.version),
|
|
402
|
+
})
|
|
403
|
+
}
|
|
109
404
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
405
|
+
// Clear active progress spinner before rendering diagnostic summary
|
|
406
|
+
yield progress('')
|
|
407
|
+
|
|
408
|
+
// 3. Print Actionable Diagnostic Fix Summary
|
|
409
|
+
if (diagnostics.length > 0) {
|
|
410
|
+
yield show(t(CheckCommand.UI.recommendationsHeader, { count: diagnostics.length }))
|
|
411
|
+
let idx = 1
|
|
412
|
+
for (const diag of diagnostics) {
|
|
413
|
+
const badge = diag.level === 'error' ? '❌ ERROR' : '⚠️ WARN'
|
|
414
|
+
yield show(
|
|
415
|
+
t(CheckCommand.UI.recommendationItem, {
|
|
416
|
+
idx: idx++,
|
|
417
|
+
badge,
|
|
418
|
+
code: diag.code,
|
|
419
|
+
message: diag.message,
|
|
420
|
+
file: diag.file,
|
|
421
|
+
fix: diag.fix,
|
|
422
|
+
})
|
|
423
|
+
)
|
|
113
424
|
}
|
|
425
|
+
}
|
|
114
426
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
427
|
+
const errorCount = diagnostics.filter((d) => d.level === 'error').length
|
|
428
|
+
const warnCount = diagnostics.filter((d) => d.level === 'warning').length
|
|
429
|
+
const isSuccess = errorCount === 0
|
|
430
|
+
|
|
431
|
+
yield show(
|
|
432
|
+
t(CheckCommand.UI.summary, {
|
|
433
|
+
projects: checkedProjects.length,
|
|
434
|
+
passed: isSuccess ? 'YES' : 'NO',
|
|
435
|
+
errors: errorCount,
|
|
436
|
+
warnings: warnCount,
|
|
437
|
+
}),
|
|
438
|
+
isSuccess ? 'success' : 'warn'
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
return result({
|
|
442
|
+
success: isSuccess,
|
|
443
|
+
diagnostics,
|
|
444
|
+
checkedProjects,
|
|
445
|
+
})
|
|
446
|
+
}
|
|
125
447
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
448
|
+
/**
|
|
449
|
+
* Count tasks in markdown content
|
|
450
|
+
* @param {string} content
|
|
451
|
+
* @returns {{ done: number, total: number }}
|
|
452
|
+
*/
|
|
453
|
+
#countTasks(content = '') {
|
|
454
|
+
let total = 0
|
|
455
|
+
let done = 0
|
|
456
|
+
for (const line of content.split('\n')) {
|
|
457
|
+
const m = line.match(/^[-*+]?\s*(?:\[([ xX])\]|\d+\.\s*\[([ xX])\])\s*(.+)$/)
|
|
458
|
+
if (m) {
|
|
459
|
+
total++
|
|
460
|
+
if ((m[1] || m[2] || '').toLowerCase() === 'x') done++
|
|
461
|
+
}
|
|
131
462
|
}
|
|
132
|
-
|
|
133
|
-
return result({ success: true })
|
|
463
|
+
return { done, total }
|
|
134
464
|
}
|
|
135
465
|
|
|
136
466
|
/**
|
|
137
|
-
*
|
|
138
|
-
* @
|
|
467
|
+
* Discover release version directories using database
|
|
468
|
+
* @param {any} db
|
|
469
|
+
* @param {string} projectRoot
|
|
470
|
+
* @returns {Promise<Array<{ version: string, path: string }>>}
|
|
139
471
|
*/
|
|
140
|
-
async
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
472
|
+
async #discoverReleases(db, projectRoot) {
|
|
473
|
+
const results = []
|
|
474
|
+
const releasesUri = `${projectRoot}releases`.replace(/^\/+/, '') || 'releases'
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
if (typeof db.listDir === 'function') {
|
|
478
|
+
const queue = [{ uri: releasesUri, depth: 0 }]
|
|
479
|
+
while (queue.length > 0) {
|
|
480
|
+
const item = queue.shift()
|
|
481
|
+
if (!item || item.depth > 3) continue
|
|
482
|
+
let entries = []
|
|
483
|
+
try {
|
|
484
|
+
entries = await db.listDir(item.uri)
|
|
485
|
+
} catch {
|
|
486
|
+
continue
|
|
487
|
+
}
|
|
488
|
+
if (!Array.isArray(entries)) continue
|
|
489
|
+
|
|
490
|
+
for (const entry of entries) {
|
|
491
|
+
const rawName = entry.name || (entry.path || '').split('/').pop() || ''
|
|
492
|
+
const name = rawName.replace(/\/$/, '')
|
|
493
|
+
const isDir = Boolean(
|
|
494
|
+
entry.stat?.isDirectory ||
|
|
495
|
+
entry.isDirectory ||
|
|
496
|
+
entry.isDir ||
|
|
497
|
+
rawName.endsWith('/')
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
const dirMatch = name.match(/^v(\d+\.\d+\.\d+[a-zA-Z0-9._-]*)/)
|
|
501
|
+
if (dirMatch) {
|
|
502
|
+
const version = dirMatch[1]
|
|
503
|
+
const entryPath = entry.path ? entry.path.replace(/\/$/, '') : `${item.uri}/${name}`
|
|
504
|
+
const normalizedPath = entryPath.replace(/^\.\//, '')
|
|
505
|
+
if (!results.find((r) => r.path === normalizedPath)) {
|
|
506
|
+
results.push({ version, path: normalizedPath })
|
|
507
|
+
}
|
|
508
|
+
} else if (isDir && !name.startsWith('.') && name !== 'node_modules' && name !== '_') {
|
|
509
|
+
const nextUri = entry.path ? entry.path.replace(/\/$/, '') : `${item.uri}/${name}`
|
|
510
|
+
queue.push({ uri: nextUri, depth: item.depth + 1 })
|
|
511
|
+
}
|
|
512
|
+
}
|
|
159
513
|
}
|
|
160
514
|
}
|
|
161
|
-
}
|
|
515
|
+
} catch {}
|
|
162
516
|
|
|
163
|
-
|
|
164
|
-
return files
|
|
517
|
+
return results
|
|
165
518
|
}
|
|
166
519
|
|
|
167
|
-
|
|
168
520
|
/**
|
|
169
|
-
*
|
|
170
|
-
* @param {
|
|
521
|
+
* Find orphan files inside releases/ directory using db
|
|
522
|
+
* @param {any} db
|
|
523
|
+
* @param {string} projectRoot
|
|
171
524
|
* @returns {Promise<string[]>}
|
|
172
525
|
*/
|
|
173
|
-
async
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
526
|
+
async #findOrphanReleaseFiles(db, projectRoot) {
|
|
527
|
+
const orphans = []
|
|
528
|
+
const releasesUri = `${projectRoot}releases`.replace(/^\/+/, '') || 'releases'
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
if (typeof db.listDir === 'function') {
|
|
532
|
+
const entries = await db.listDir(releasesUri, { depth: 0 })
|
|
533
|
+
for (const entry of entries) {
|
|
534
|
+
const isFile = !entry.isDirectory && (entry.isFile || !entry.isDir)
|
|
535
|
+
const name = entry.name || (entry.path || '').split('/').pop() || ''
|
|
536
|
+
if (
|
|
537
|
+
isFile &&
|
|
538
|
+
!['index.js', 'current.js', 'README.md', 'backlog.md'].includes(name) &&
|
|
539
|
+
!name.startsWith('.')
|
|
540
|
+
) {
|
|
541
|
+
orphans.push(name)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
189
544
|
}
|
|
190
|
-
}
|
|
191
|
-
|
|
545
|
+
} catch {}
|
|
546
|
+
|
|
547
|
+
return orphans
|
|
192
548
|
}
|
|
193
549
|
}
|
|
550
|
+
|
|
551
|
+
export default CheckCommand
|