@nan0web/release 3.3.0 → 3.5.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.
Files changed (94) hide show
  1. package/README.md +0 -35
  2. package/bin/nan0release.js +33 -2
  3. package/bin/serve.js +73 -0
  4. package/package.json +46 -17
  5. package/src/Release/Document.js +1 -0
  6. package/src/Release/Scanner.js +3 -2
  7. package/src/Release.js +7 -7
  8. package/src/domain/App.js +33 -2
  9. package/src/domain/BumpCommand.js +10 -3
  10. package/src/domain/CheckCommand.js +504 -146
  11. package/src/domain/CheckCommand.story.js +92 -0
  12. package/src/domain/CloseCommand.js +13 -9
  13. package/src/domain/Dashboard/Dashboard.story.js +155 -0
  14. package/src/domain/Dashboard/DashboardModel.js +308 -0
  15. package/src/domain/Dashboard/ReleaseDiscovery.js +132 -0
  16. package/src/domain/Dashboard/ReleaseParser.js +226 -0
  17. package/src/domain/Dashboard/index.js +19 -0
  18. package/src/domain/DepsCommand.js +5 -2
  19. package/src/domain/InitCommand.js +18 -16
  20. package/src/domain/ListCommand.js +45 -0
  21. package/src/domain/PMaCParser.js +418 -0
  22. package/src/domain/PMaCParser.story.js +152 -0
  23. package/src/domain/PreviewCommand.js +39 -0
  24. package/src/domain/PublishCommand.js +7 -3
  25. package/src/domain/Registry/Registry.story.js +82 -0
  26. package/src/domain/Registry/RegistryModel.js +124 -0
  27. package/src/domain/ServeCommand.js +165 -0
  28. package/src/domain/ServeCommand.story.js +88 -0
  29. package/src/domain/SpecCommand.js +3 -1
  30. package/src/domain/StatusCommand.js +249 -3
  31. package/src/domain/ValidateCommand.js +426 -0
  32. package/src/domain/ValidateCommand.story.js +92 -0
  33. package/src/domain/ViewCommand.js +99 -0
  34. package/src/domain/WebCommand.js +140 -0
  35. package/src/domain/inspect/ReleaseAuditor.js +508 -0
  36. package/src/domain/inspect/ReleaseAuditor.story.js +136 -0
  37. package/src/domain/models/ReleaseIssueModel.js +27 -0
  38. package/src/domain/models/ReleaseMemberModel.js +21 -0
  39. package/src/domain/models/ReleaseModel.js +191 -0
  40. package/src/domain/models/ReleaseParityModel.js +27 -0
  41. package/src/domain/models/ReleaseTaskModel.js +33 -0
  42. package/src/domain/models/index.js +6 -0
  43. package/src/domain/templates/release.html +34 -0
  44. package/src/domain/templates/release.html.template.js +34 -0
  45. package/src/index.js +15 -0
  46. package/src/inspectors/inspect-js.js +32 -0
  47. package/src/releases/1/0/v1.0.3/task.test.js +1 -1
  48. package/src/utils/nodejs/GitTelemetry.js +154 -0
  49. package/types/Release/Document.d.ts +24 -27
  50. package/types/Release/MarkdownToTest.d.ts +1 -1
  51. package/types/Release/Person.d.ts +9 -7
  52. package/types/Release/Scanner.d.ts +41 -41
  53. package/types/Release.d.ts +42 -42
  54. package/types/architecture/ProjectManagementAsCode.d.ts +59 -64
  55. package/types/domain/App.d.ts +25 -7
  56. package/types/domain/BumpCommand.d.ts +11 -6
  57. package/types/domain/CheckCommand.d.ts +128 -16
  58. package/types/domain/CheckCommand.story.d.ts +1 -0
  59. package/types/domain/CloseCommand.d.ts +4 -6
  60. package/types/domain/Dashboard/Dashboard.story.d.ts +1 -0
  61. package/types/domain/Dashboard/DashboardModel.d.ts +273 -0
  62. package/types/domain/Dashboard/GitTelemetry.d.ts +23 -0
  63. package/types/domain/Dashboard/ReleaseDiscovery.d.ts +47 -0
  64. package/types/domain/Dashboard/ReleaseParser.d.ts +79 -0
  65. package/types/domain/Dashboard/index.d.ts +3 -0
  66. package/types/domain/DepsCommand.d.ts +6 -6
  67. package/types/domain/InitCommand.d.ts +4 -6
  68. package/types/domain/ListCommand.d.ts +18 -0
  69. package/types/domain/PMaCParser.d.ts +64 -0
  70. package/types/domain/PMaCParser.story.d.ts +1 -0
  71. package/types/domain/PreviewCommand.d.ts +6 -0
  72. package/types/domain/PublishCommand.d.ts +8 -6
  73. package/types/domain/Registry/Registry.story.d.ts +1 -0
  74. package/types/domain/Registry/RegistryModel.d.ts +61 -0
  75. package/types/domain/ServeCommand.d.ts +59 -0
  76. package/types/domain/ServeCommand.story.d.ts +1 -0
  77. package/types/domain/SpecCommand.d.ts +4 -6
  78. package/types/domain/StatusCommand.d.ts +38 -3
  79. package/types/domain/ValidateCommand.d.ts +69 -0
  80. package/types/domain/ValidateCommand.story.d.ts +1 -0
  81. package/types/domain/ViewCommand.d.ts +27 -0
  82. package/types/domain/WebCommand.d.ts +47 -0
  83. package/types/domain/inspect/ReleaseAuditor.d.ts +134 -0
  84. package/types/domain/inspect/ReleaseAuditor.story.d.ts +1 -0
  85. package/types/domain/models/ReleaseIssueModel.d.ts +35 -0
  86. package/types/domain/models/ReleaseMemberModel.d.ts +25 -0
  87. package/types/domain/models/ReleaseModel.d.ts +122 -0
  88. package/types/domain/models/ReleaseParityModel.d.ts +37 -0
  89. package/types/domain/models/ReleaseTaskModel.d.ts +46 -0
  90. package/types/domain/models/index.d.ts +5 -0
  91. package/types/domain/templates/release.html.template.d.ts +2 -0
  92. package/types/index.d.ts +21 -10
  93. package/types/inspectors/inspect-js.d.ts +2 -0
  94. package/types/utils/nodejs/GitTelemetry.d.ts +23 -0
@@ -0,0 +1,92 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import DB from '@nan0web/db'
4
+ import { ValidateCommand } from './ValidateCommand.js'
5
+
6
+ describe('Scenario: ValidateCommand Multi-Format Inspection with In-Memory DB', () => {
7
+ it('executes full scan and renders formatted report', async () => {
8
+ const releaseMd = `---
9
+ status: work
10
+ type: minor
11
+ epic: EPIC-CORE
12
+ sprint: Sprint 1
13
+ tracker: https://github.com/nan0web/nan.web/issues/{id}
14
+ ---
15
+
16
+ # v1.0.0 — Core Release Inspector
17
+
18
+ ## Опис та Мета
19
+ Створення CLI інструменту інспекції релізів.
20
+
21
+ ## Команда
22
+ - **Lead / Architect:** Олександр Снігірьов (@snigirev)
23
+ - **AI Lead Developer:** Agent Antigravity (@agy)
24
+
25
+ ## Робочий План та Задачі
26
+ - [x] [#101] Core inspection logic
27
+ - [ ] [#102] Multi-format reporter
28
+
29
+ ## Паритет Даних
30
+ | Колекція | Очікувано | Фактично | Статус | Коментар |
31
+ | :--- | :---: | :---: | :---: | :--- |
32
+ | releases | 1 | 1 | ✅ OK | Synced |
33
+ `
34
+ const userMd = `---
35
+ hours: 1.5
36
+ iterations: 4
37
+ rrs: 330
38
+ ---
39
+ # Metrics
40
+ Hours: 1.5h
41
+ Iterations: 4
42
+ RRS: 330
43
+ `
44
+
45
+ const db = new DB({
46
+ predefined: [
47
+ ['package.json', { name: '@nan0web/sample', version: '1.0.0' }],
48
+ ['releases/1/0/v1.0.0/release.md', releaseMd],
49
+ ['releases/1/0/v1.0.0/user.md', userMd],
50
+ ['releases/1/0/v1.0.0/task.spec.js', '// spec'],
51
+ ],
52
+ })
53
+ await db.connect()
54
+
55
+ // 1. Text format
56
+ const cmdText = new ValidateCommand({ target: '.' }, { db })
57
+ const genText = cmdText.run()
58
+ let step = await genText.next()
59
+ while (!step.done) {
60
+ step = await genText.next()
61
+ }
62
+ const resText = step.value?.data || step.value
63
+ assert.ok(resText.success)
64
+ assert.equal(resText.releases.length, 1)
65
+ assert.match(resText.output, /Release \[v1\.0\.0\] ✅ VALID/)
66
+ assert.match(resText.output, /Core inspection logic/)
67
+
68
+ // 2. JSON format
69
+ const cmdJson = new ValidateCommand({ target: '.', format: 'json' }, { db })
70
+ const genJson = cmdJson.run()
71
+ step = await genJson.next()
72
+ while (!step.done) {
73
+ step = await genJson.next()
74
+ }
75
+ const resJson = step.value?.data || step.value
76
+ assert.ok(resJson.success)
77
+ const parsed = JSON.parse(resJson.output)
78
+ assert.equal(parsed.releases[0].tasks.length, 2)
79
+ assert.equal(parsed.releases[0].parity.length, 1)
80
+
81
+ // 3. CSV format with file export
82
+ const cmdCsv = new ValidateCommand({ target: '.', format: 'csv', output: 'export.csv' }, { db })
83
+ const genCsv = cmdCsv.run()
84
+ step = await genCsv.next()
85
+ while (!step.done) {
86
+ step = await genCsv.next()
87
+ }
88
+ const savedCsv = await db.loadDocument('export.csv', null)
89
+ assert.ok(savedCsv)
90
+ assert.match(savedCsv, /v1\.0\.0,101,Core inspection logic,done/)
91
+ })
92
+ })
@@ -0,0 +1,99 @@
1
+ import { ModelAsApp, show, result, ask } from '@nan0web/ui'
2
+ import { select } from '@nan0web/ui-cli'
3
+ import Scanner from '../Release/Scanner.js'
4
+ import { readFile } from 'node:fs/promises'
5
+ import { join } from 'node:path'
6
+
7
+ /**
8
+ * ViewCommand - ModelAsApp Subcommand to inspect and view release details across multimodal interfaces.
9
+ */
10
+ export default class ViewCommand extends ModelAsApp {
11
+ static alias = 'view'
12
+
13
+ static UI = {
14
+ title: 'view',
15
+ help: 'View release summary and launch multimodal playgrounds (CLI, TUI, WEB, CHAT, VOICE)',
16
+ noReleases: 'No releases found in current directory.',
17
+ selectPrompt: 'Select release version to view:',
18
+ }
19
+
20
+ static version = {
21
+ help: 'Target release version (e.g. v3.4.0). If omitted, latest version is used.',
22
+ positional: true,
23
+ type: 'string',
24
+ default: '',
25
+ }
26
+
27
+ /**
28
+ * @param {Partial<ViewCommand>} [data]
29
+ * @param {import('@nan0web/types').ModelOptions} [options]
30
+ */
31
+ constructor(data = {}, options = {}) {
32
+ super(data, options)
33
+ /** @type {string} */
34
+ this.version = String(data.version || (options && 'version' in options ? /** @type {any} */ (options).version : ''))
35
+ }
36
+
37
+ async *run() {
38
+ const targetDir = process.env.RELEASE_CWD || process.env.INIT_CWD || process.cwd()
39
+ const scanner = new Scanner(targetDir)
40
+ const statuses = scanner.status()
41
+
42
+ if (!statuses.length) {
43
+ yield show(ViewCommand.UI.noReleases, 'warn')
44
+ return result({ success: false })
45
+ }
46
+
47
+ // Sort SemVer descending (newest first)
48
+ const sorted = [...statuses].sort((a, b) => b.version.localeCompare(a.version, undefined, { numeric: true }))
49
+
50
+ let targetVer = this.version
51
+ if (!targetVer) {
52
+ // If not specified via CLI arguments, pick the newest
53
+ targetVer = sorted[0].version
54
+ }
55
+
56
+ const current = sorted.find((s) => s.version === targetVer) || sorted[0]
57
+ targetVer = current.version
58
+
59
+ // Find release dir
60
+ const allSpecs = scanner.findSpecs(targetVer)
61
+ const allTests = scanner.findTests(targetVer)
62
+ const entry = allSpecs[0] || allTests[0]
63
+ const releaseDir = entry ? entry.dir : ''
64
+
65
+ let progressStr = '100%'
66
+ let taskCount = '8/8'
67
+
68
+ if (releaseDir) {
69
+ try {
70
+ const taskRaw = await readFile(join(releaseDir, 'task.md'), 'utf8')
71
+ const completed = (taskRaw.match(/- \[[xX]\]/g) || []).length
72
+ const pending = (taskRaw.match(/- \[ \]/g) || []).length
73
+ const total = completed + pending
74
+ if (total > 0) {
75
+ const pct = Math.round((completed / total) * 100)
76
+ progressStr = `${pct}%`
77
+ taskCount = `${completed}/${total}`
78
+ }
79
+ } catch {}
80
+ }
81
+
82
+ yield show(`Release ${targetVer}`)
83
+ yield show(`Progress ${progressStr} (${taskCount})`)
84
+ yield show('Playground available for CLI, TUI, WEB, CHAT, VOICE to see releases run:')
85
+ yield show(' pnpm exec serve releases # WEB version (http://localhost:3131)')
86
+ yield show(' pnpm exec release # CLI version')
87
+ yield show(' pnpm exec release --tui # TUI version')
88
+ yield show(` pnpm exec release --chat "Show me a release where I have added ${targetVer} features" # CHAT version`)
89
+
90
+ return result({
91
+ version: targetVer,
92
+ progress: progressStr,
93
+ tasks: taskCount,
94
+ dir: releaseDir,
95
+ })
96
+ }
97
+ }
98
+
99
+ export { ViewCommand }
@@ -0,0 +1,140 @@
1
+ import { ModelAsApp, result, show, progress } from '@nan0web/ui'
2
+ import { DashboardModel } from './Dashboard/DashboardModel.js'
3
+
4
+ /**
5
+ * WebCommand - ModelAsApp Subcommand to generate and launch PM-as-Code Web Dashboard.
6
+ */
7
+ export default class WebCommand extends ModelAsApp {
8
+ static alias = 'web'
9
+
10
+ static UI = {
11
+ title: 'web',
12
+ help: 'Generate and launch PM-as-Code Web Dashboard',
13
+ loading: 'Aggregating release metrics for Web Dashboard...',
14
+ generated: '✨ Web Dashboard успішно згенеровано: {$path}',
15
+ serving: '🌐 Web Dashboard запущено: http://localhost:{$port} (Натисніть Ctrl+C для зупинки)',
16
+ }
17
+
18
+ static registryFile = {
19
+ help: 'Path to releases.txt registry file',
20
+ default: 'releases.txt',
21
+ type: 'string',
22
+ alias: 'f',
23
+ }
24
+
25
+ static out = {
26
+ help: 'Destination path for HTML dashboard',
27
+ default: 'apps/release.app/play/index.html',
28
+ type: 'string',
29
+ alias: 'o',
30
+ }
31
+
32
+ static port = {
33
+ help: 'Port to serve HTML dashboard over HTTP (e.g. 3333)',
34
+ default: 0,
35
+ type: 'number',
36
+ alias: 'p',
37
+ }
38
+
39
+ static open = {
40
+ help: 'Automatically open in browser',
41
+ default: false,
42
+ type: 'boolean',
43
+ }
44
+
45
+ /**
46
+ * @param {Partial<WebCommand>} [data]
47
+ * @param {import('@nan0web/types').ModelOptions} [options]
48
+ */
49
+ constructor(data = {}, options = {}) {
50
+ super(data, options)
51
+ /** @type {string} */ this.registryFile = String(data.registryFile || 'releases.txt')
52
+ /** @type {string} */ this.out = String(data.out || 'apps/release.app/play/index.html')
53
+ /** @type {number} */ this.port = Number(data.port ?? options.port ?? 0)
54
+ /** @type {boolean} */ this.open = Boolean(data.open ?? options.open ?? false)
55
+ }
56
+
57
+ async *run() {
58
+ const db = this._?.db || this.db
59
+ let gitTelemetry = null
60
+ try {
61
+ gitTelemetry = await import('../utils/nodejs/GitTelemetry.js')
62
+ } catch {}
63
+ const dashboardModel = new DashboardModel(
64
+ { registryFile: this.registryFile || 'releases.txt' },
65
+ { db, gitTelemetry }
66
+ )
67
+ for await (const p of dashboardModel.aggregateStream()) {
68
+ yield progress(`[${p.current}/${p.total}] Scanning ${p.project}...`, p.current, p.total, {
69
+ id: 'web-dashboard',
70
+ })
71
+ }
72
+ const summary = dashboardModel.summary
73
+ const projects = dashboardModel.projects
74
+
75
+ // Dynamically load ReleaseDashboard from release.app if available or render inline
76
+ let html = ''
77
+ try {
78
+ const { ReleaseDashboard } =
79
+ await import('../../../../apps/release.app/src/ui/web/ReleaseDashboard.js')
80
+ const dashboard = new ReleaseDashboard({ summary, projects })
81
+ html = dashboard.render()
82
+ } catch (err) {
83
+ html = `<!DOCTYPE html><html><body><h1>Dashboard: ${summary.totalProjects} projects</h1><p>${err.message}</p></body></html>`
84
+ }
85
+
86
+ const outPath = this.out || 'apps/release.app/play/index.html'
87
+ await db.saveDocument(outPath, html)
88
+
89
+ yield progress('', 100, { id: 'web-dashboard', stop: 'success' })
90
+ yield show(WebCommand.UI.generated.replace('{$path}', outPath), 'info')
91
+
92
+ if (this.port > 0) {
93
+ try {
94
+ const { createServer } = await import('node:http')
95
+ const server = createServer((req, res) => {
96
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
97
+ res.end(html)
98
+ })
99
+ server.listen(this.port)
100
+ yield show(WebCommand.UI.serving.replace('{$port}', String(this.port)), 'success')
101
+
102
+ if (this.open) {
103
+ try {
104
+ const { exec } = await import('node:child_process')
105
+ exec(`open http://localhost:${this.port}`)
106
+ } catch {}
107
+ }
108
+
109
+ if (typeof process !== 'undefined' && process.stdin) {
110
+ await new Promise((resolve) => {
111
+ process.on('SIGINT', () => {
112
+ server.close()
113
+ resolve(null)
114
+ })
115
+ })
116
+ }
117
+ } catch (err) {
118
+ yield show(
119
+ `⚠️ Не вдалося запустити HTTP сервер на порті ${this.port}: ${err.message}`,
120
+ 'error'
121
+ )
122
+ }
123
+ } else if (this.open) {
124
+ try {
125
+ const { exec } = await import('node:child_process')
126
+ exec(`open "${outPath}"`)
127
+ } catch {}
128
+ }
129
+
130
+ return result({
131
+ status: 'ok',
132
+ outPath,
133
+ summary,
134
+ projects,
135
+ port: this.port,
136
+ })
137
+ }
138
+ }
139
+
140
+ export { WebCommand }