@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,191 @@
1
+ import { Model } from '@nan0web/types'
2
+ import { ReleaseMemberModel } from './ReleaseMemberModel.js'
3
+ import { ReleaseTaskModel } from './ReleaseTaskModel.js'
4
+ import { ReleaseParityModel } from './ReleaseParityModel.js'
5
+ import { ReleaseIssueModel } from './ReleaseIssueModel.js'
6
+
7
+ export class ReleaseModel extends Model {
8
+ static version = { type: 'string', required: true }
9
+ static title = { type: 'string', required: true }
10
+ static status = { type: 'string', default: 'todo' } // 'todo' | 'work' | 'done' | 'fail'
11
+ static type = { type: 'string', default: 'minor' } // 'major' | 'minor' | 'patch' | 'hotfix' | 'chore'
12
+ static summary = { type: 'string' }
13
+ static epic = { type: 'string' }
14
+ static sprint = { type: 'string' }
15
+ static tracker = { type: 'string' }
16
+
17
+ // Sub-collections
18
+ static team = { type: 'array', itemType: ReleaseMemberModel, default: [] }
19
+ static tasks = { type: 'array', itemType: ReleaseTaskModel, default: [] }
20
+ static criteria = { type: 'array', itemType: ReleaseTaskModel, default: [] }
21
+ static parity = { type: 'array', itemType: ReleaseParityModel, default: [] }
22
+ static issues = { type: 'array', itemType: ReleaseIssueModel, default: [] }
23
+ static files = { type: 'array', default: [] }
24
+ static meta = { type: 'object', default: {} }
25
+
26
+ /**
27
+ * @param {Partial<ReleaseModel>} [data={}]
28
+ * @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
29
+ */
30
+ constructor(data = {}, options = {}) {
31
+ super(data, options)
32
+ /** @type {string} Release version */
33
+ this.version
34
+ /** @type {string} Release title */
35
+ this.title
36
+ /** @type {'todo' | 'work' | 'done' | 'fail'} Release status */
37
+ this.status
38
+ /** @type {'major' | 'minor' | 'patch' | 'hotfix' | 'chore'} Release type */
39
+ this.type
40
+ /** @type {string} Release summary / description */
41
+ this.summary
42
+ /** @type {string} Epic identifier */
43
+ this.epic
44
+ /** @type {string} Sprint name */
45
+ this.sprint
46
+ /** @type {string} External tracker URL template */
47
+ this.tracker
48
+ /** @type {ReleaseMemberModel[]} Team members (ролі, імена, логіни) */
49
+ this.team
50
+ /** @type {ReleaseTaskModel[]} Tasks in scope */
51
+ this.tasks
52
+ /** @type {ReleaseTaskModel[]} Acceptance criteria (DoD) */
53
+ this.criteria
54
+ /** @type {ReleaseParityModel[]} Data parity verification items */
55
+ this.parity
56
+ /** @type {ReleaseIssueModel[]} Issues and risks */
57
+ this.issues
58
+ /** @type {Array<{file: string, size?: string, hash?: string, type?: string}>} Artifact files */
59
+ this.files
60
+ /** @type {Record<string, any>} Frontmatter / metadata */
61
+ this.meta
62
+ }
63
+
64
+ static SECTIONS = {
65
+ summary: [
66
+ // EN (English)
67
+ 'summary', 'objective', 'description', 'overview',
68
+ // UK (Українська)
69
+ 'опис', 'мета', 'ціль', 'огляд',
70
+ // ES (Español)
71
+ 'resumen', 'objetivo', 'descripcion',
72
+ // FR (Français)
73
+ 'resume', 'objectif',
74
+ // DE (Deutsch)
75
+ 'zusammenfassung', 'ziel', 'beschreibung',
76
+ // ZH (中文 - Chinese)
77
+ '摘要', '目标', '描述',
78
+ // RU (Російська - для сумісності з двомовними українцями)
79
+ 'описание', 'цель', 'обзор',
80
+ ],
81
+ team: [
82
+ // EN (English)
83
+ 'team', 'members', 'contributors', 'roles',
84
+ // UK (Українська)
85
+ 'команда', 'учасники', 'ролі',
86
+ // ES (Español)
87
+ 'equipo', 'miembros',
88
+ // FR (Français)
89
+ 'equipe', 'membres',
90
+ // DE (Deutsch)
91
+ 'mitglieder',
92
+ // ZH (中文 - Chinese)
93
+ '团队', '成员',
94
+ // RU (Російська)
95
+ 'участники', 'роли',
96
+ ],
97
+ tasks: [
98
+ // EN (English)
99
+ 'tasks', 'scope', 'work plan', 'wbs', 'backlog', 'plan',
100
+ // UK (Українська)
101
+ 'план', 'задачі', 'робочий план', 'обсяг',
102
+ // ES (Español)
103
+ 'tareas', 'plan de trabajo',
104
+ // FR (Français)
105
+ 'taches', 'plan de travail',
106
+ // DE (Deutsch)
107
+ 'aufgaben', 'arbeitsplan',
108
+ // ZH (中文 - Chinese)
109
+ '任务', '工作计划',
110
+ // RU (Російська)
111
+ 'задачи', 'рабочий план',
112
+ ],
113
+ criteria: [
114
+ // EN (English)
115
+ 'acceptance', 'acceptance criteria', 'dod', 'definition of done', 'requirements',
116
+ // UK (Українська)
117
+ 'критерії', 'критерії прийнятності', 'вимоги',
118
+ // ES (Español)
119
+ 'criterios', 'criterios de aceptacion', 'requisitos',
120
+ // FR (Français)
121
+ 'criteres', 'criteres d acceptation', 'exigences',
122
+ // DE (Deutsch)
123
+ 'akzeptanzkriterien', 'anforderungen',
124
+ // ZH (中文 - Chinese)
125
+ '验收标准', '准入条件', '要求',
126
+ // RU (Російська)
127
+ 'критерии', 'критерии приемки', 'требования',
128
+ ],
129
+ parity: [
130
+ // EN (English)
131
+ 'parity', 'data parity', 'audit', 'verification',
132
+ // UK (Українська)
133
+ 'паритет', 'аудит', 'паритет даних', 'верифікація',
134
+ // ES (Español)
135
+ 'paridad', 'auditoria',
136
+ // FR (Français)
137
+ 'parite',
138
+ // DE (Deutsch)
139
+ 'datenparitaet',
140
+ // ZH (中文 - Chinese)
141
+ '数据对齐', '数据一致性', '审计',
142
+ // RU (Російська)
143
+ 'паритет данных', 'верификация',
144
+ ],
145
+ issues: [
146
+ // EN (English)
147
+ 'issues', 'risks', 'blockers', 'problems', 'notes',
148
+ // UK (Українська)
149
+ 'ризики', 'зауваження', 'блокери', 'проблеми',
150
+ // ES (Español)
151
+ 'riesgos', 'problemas',
152
+ // FR (Français)
153
+ 'risques',
154
+ // DE (Deutsch)
155
+ 'risiken', 'hinweise',
156
+ // ZH (中文 - Chinese)
157
+ '风险', '问题', '注意事项',
158
+ // RU (Російська)
159
+ 'риски', 'замечания', 'блокеры',
160
+ ],
161
+ artifacts: [
162
+ // EN (English)
163
+ 'artifacts', 'files', 'provenance', 'deliverables',
164
+ // UK (Українська)
165
+ 'артефакти', 'файли',
166
+ // ES (Español)
167
+ 'artefactos', 'archivos',
168
+ // FR (Français)
169
+ 'artefacts', 'fichiers',
170
+ // DE (Deutsch)
171
+ 'artefakte', 'dateien',
172
+ // ZH (中文 - Chinese)
173
+ '产物', '文件',
174
+ // RU (Російська)
175
+ 'артефакты',
176
+ ],
177
+ }
178
+
179
+ /**
180
+ * Extend section synonyms dictionary with custom locale mappings.
181
+ * @param {Record<string, string[]>} localeMap
182
+ */
183
+ static addLocaleSections(localeMap) {
184
+ for (const [section, words] of Object.entries(localeMap)) {
185
+ if (this.SECTIONS[section]) {
186
+ this.SECTIONS[section].push(...words)
187
+ }
188
+ }
189
+ }
190
+ }
191
+
@@ -0,0 +1,27 @@
1
+ import { Model } from '@nan0web/types'
2
+
3
+ export class ReleaseParityModel extends Model {
4
+ static collection = { type: 'string', required: true }
5
+ static expected = { type: 'number', required: true }
6
+ static actual = { type: 'number', required: true }
7
+ static synced = { type: 'boolean', default: true }
8
+ static comment = { type: 'string' }
9
+
10
+ /**
11
+ * @param {Partial<ReleaseParityModel>} [data={}]
12
+ * @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
13
+ */
14
+ constructor(data = {}, options = {}) {
15
+ super(data, options)
16
+ /** @type {string} Collection name */
17
+ this.collection
18
+ /** @type {number} Expected count */
19
+ this.expected
20
+ /** @type {number} Actual count */
21
+ this.actual
22
+ /** @type {boolean} Synced flag */
23
+ this.synced
24
+ /** @type {string} Audit comment */
25
+ this.comment
26
+ }
27
+ }
@@ -0,0 +1,33 @@
1
+ import { Model } from '@nan0web/types'
2
+
3
+ export class ReleaseTaskModel extends Model {
4
+ static id = { type: 'string', required: true }
5
+ static title = { type: 'string', required: true }
6
+ static status = { type: 'string', default: 'todo' } // 'todo' | 'work' | 'done' | 'fail'
7
+ static assignee = { type: 'string' }
8
+ static estimateHours = { type: 'number' }
9
+ static spentHours = { type: 'number' }
10
+ static notes = { type: 'string' }
11
+
12
+ /**
13
+ * @param {Partial<ReleaseTaskModel>} [data={}]
14
+ * @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
15
+ */
16
+ constructor(data = {}, options = {}) {
17
+ super(data, options)
18
+ /** @type {string} Task ID */
19
+ this.id
20
+ /** @type {string} Task Title */
21
+ this.title
22
+ /** @type {'todo' | 'work' | 'done' | 'fail'} Task Status */
23
+ this.status
24
+ /** @type {string} Assignee */
25
+ this.assignee
26
+ /** @type {number} Estimated Hours */
27
+ this.estimateHours
28
+ /** @type {number} Spent Hours */
29
+ this.spentHours
30
+ /** @type {string} Notes */
31
+ this.notes
32
+ }
33
+ }
@@ -0,0 +1,6 @@
1
+ export { ReleaseMemberModel } from './ReleaseMemberModel.js'
2
+ export { ReleaseTaskModel } from './ReleaseTaskModel.js'
3
+ export { ReleaseParityModel } from './ReleaseParityModel.js'
4
+ export { ReleaseIssueModel } from './ReleaseIssueModel.js'
5
+ export { ReleaseModel } from './ReleaseModel.js'
6
+
@@ -0,0 +1,34 @@
1
+ <!DOCTYPE html>
2
+ <html lang="uk" data-bs-theme="dark">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Releases Catalog — NaN•Web</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0f172a;
10
+ --surface: #1e293b;
11
+ --accent: #38bdf8;
12
+ --text: #f8fafc;
13
+ --text-dim: #94a3b8;
14
+ --border: #334155;
15
+ }
16
+ body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 2.5rem 1.5rem; display: flex; justify-content: center; line-height: 1.6; }
17
+ .container { max-width: 880px; width: 100%; }
18
+ h1, h2, h3 { color: #fff; font-weight: 700; margin-top: 1.5rem; }
19
+ h1 { font-size: 2rem; border-bottom: 1px solid var(--border); padding-bottom: 0.8rem; }
20
+ a { color: var(--accent); text-decoration: none; }
21
+ a:hover { text-decoration: underline; }
22
+ table { width: 100%; border-collapse: collapse; margin: 1.5rem 0; background: var(--surface); border-radius: 8px; overflow: hidden; }
23
+ th, td { padding: 0.85rem 1rem; text-align: left; border-bottom: 1px solid var(--border); }
24
+ th { background: rgba(0,0,0,0.2); font-weight: 600; color: var(--accent); }
25
+ pre { background: var(--surface); padding: 1rem; border-radius: 8px; border: 1px solid var(--border); overflow-x: auto; color: #a5b4fc; font-family: monospace; }
26
+ blockquote { border-left: 4px solid var(--accent); margin: 1rem 0; padding: 0.5rem 1rem; background: var(--surface); border-radius: 0 8px 8px 0; }
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <div class="container">
31
+ {{CONTENT}}
32
+ </div>
33
+ </body>
34
+ </html>
@@ -0,0 +1,34 @@
1
+ export default `<!DOCTYPE html>
2
+ <html lang="uk" data-bs-theme="dark">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Releases Catalog — NaN•Web</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0f172a;
10
+ --surface: #1e293b;
11
+ --accent: #38bdf8;
12
+ --text: #f8fafc;
13
+ --text-dim: #94a3b8;
14
+ --border: #334155;
15
+ }
16
+ body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 2.5rem 1.5rem; display: flex; justify-content: center; line-height: 1.6; }
17
+ .container { max-width: 880px; width: 100%; }
18
+ h1, h2, h3 { color: #fff; font-weight: 700; margin-top: 1.5rem; }
19
+ h1 { font-size: 2rem; border-bottom: 1px solid var(--border); padding-bottom: 0.8rem; }
20
+ a { color: var(--accent); text-decoration: none; }
21
+ a:hover { text-decoration: underline; }
22
+ table { width: 100%; border-collapse: collapse; margin: 1.5rem 0; background: var(--surface); border-radius: 8px; overflow: hidden; }
23
+ th, td { padding: 0.85rem 1rem; text-align: left; border-bottom: 1px solid var(--border); }
24
+ th { background: rgba(0,0,0,0.2); font-weight: 600; color: var(--accent); }
25
+ pre { background: var(--surface); padding: 1rem; border-radius: 8px; border: 1px solid var(--border); overflow-x: auto; color: #a5b4fc; font-family: monospace; }
26
+ blockquote { border-left: 4px solid var(--accent); margin: 1rem 0; padding: 0.5rem 1rem; background: var(--surface); border-radius: 0 8px 8px 0; }
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <div class="container">
31
+ {{CONTENT}}
32
+ </div>
33
+ </body>
34
+ </html>`
package/src/index.js CHANGED
@@ -30,3 +30,18 @@ export { default as DepsCommand } from './domain/DepsCommand.js'
30
30
  export { default as PublishCommand } from './domain/PublishCommand.js'
31
31
  export { default as SpecCommand } from './domain/SpecCommand.js'
32
32
  export { default as StatusCommand } from './domain/StatusCommand.js'
33
+ export { default as WebCommand } from './domain/WebCommand.js'
34
+ export { default as PreviewCommand } from './domain/PreviewCommand.js'
35
+ export { RegistryModel } from './domain/Registry/RegistryModel.js'
36
+ export { DashboardModel } from './domain/Dashboard/DashboardModel.js'
37
+ export { ReleaseAuditor } from './domain/inspect/ReleaseAuditor.js'
38
+ export {
39
+ ReleaseModel,
40
+ ReleaseMemberModel,
41
+ ReleaseTaskModel,
42
+ ReleaseParityModel,
43
+ ReleaseIssueModel,
44
+ } from './domain/models/index.js'
45
+ export { PMaCParser, resolveTrackerUrl } from './domain/PMaCParser.js'
46
+ export { ValidateCommand } from './domain/ValidateCommand.js'
47
+
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // Inspector: checks JavaScript/TypeScript files for syntax errors using `node --check`.
3
+ // Uses git to obtain list of changed files in the current HEAD.
4
+ const { execSync } = require('child_process');
5
+
6
+ function getChangedFiles() {
7
+ try {
8
+ const output = execSync('git diff --name-only --diff-filter=ACM HEAD', { encoding: 'utf8' });
9
+ return output.split(/\n/).filter(f => f.match(/\.(js|ts)$/));
10
+ } catch (e) {
11
+ console.error('Failed to obtain changed files:', e.message);
12
+ process.exit(1);
13
+ }
14
+ }
15
+
16
+ function checkFile(file) {
17
+ console.log(`🔎 Checking ${file}`);
18
+ try {
19
+ execSync(`node --check ${file}`, { stdio: 'inherit' });
20
+ } catch (e) {
21
+ console.error(`❌ Syntax error in ${file}`);
22
+ process.exit(1);
23
+ }
24
+ }
25
+
26
+ const files = getChangedFiles();
27
+ if (files.length === 0) {
28
+ console.log('✅ No JavaScript/TypeScript files changed.');
29
+ process.exit(0);
30
+ }
31
+ files.forEach(checkFile);
32
+ process.exit(0);
@@ -9,7 +9,7 @@ describe('Release v3.0.0 - Dependency Alignment', () => {
9
9
  const pkgPath = join(process.cwd(), 'package.json')
10
10
  const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
11
11
 
12
- assert.ok(/^3\.1\.\d+$/.test(pkg.version), 'Version should start with 3.1')
12
+ assert.ok(/^3\.\d+\.\d+$/.test(pkg.version), 'Version should start with 3.x')
13
13
  assert.ok(/^(\^1\.8\.0|workspace:\*)$/.test(pkg.dependencies['@nan0web/ui']), '@nan0web/ui should be ^1.8.0 or workspace:*')
14
14
  assert.ok(/^(\^2\.9\.0|workspace:\*)$/.test(pkg.dependencies['@nan0web/ui-cli']), '@nan0web/ui-cli should be ^2.9.0 or workspace:*')
15
15
  })
@@ -0,0 +1,154 @@
1
+ import fs from 'node:fs'
2
+ import { execSync } from 'node:child_process'
3
+
4
+ let rootGitStatusCache = null
5
+ let rootGitBranchCache = null
6
+ const gitInfoCache = new Map()
7
+ const gitTelemetryCache = new Map()
8
+
9
+ /**
10
+ * Reset internal Git caches (useful for testing or cache invalidation)
11
+ */
12
+ export function resetGitCache() {
13
+ rootGitStatusCache = null
14
+ rootGitBranchCache = null
15
+ gitInfoCache.clear()
16
+ gitTelemetryCache.clear()
17
+ }
18
+
19
+ /**
20
+ * Retrieve git branch and workspace status (clean / dirty)
21
+ * @param {string} [projectDir]
22
+ * @returns {{ branch: string, gitStatus: 'clean' | 'dirty' }}
23
+ */
24
+ export function getGitInfo(projectDir) {
25
+ const key = projectDir || process.cwd()
26
+ const isMonorepoInternal =
27
+ !projectDir ||
28
+ projectDir === '.' ||
29
+ (!projectDir.startsWith('../') && !projectDir.startsWith('/'))
30
+
31
+ if (isMonorepoInternal) {
32
+ if (rootGitStatusCache === null) {
33
+ try {
34
+ rootGitBranchCache =
35
+ execSync('git rev-parse --abbrev-ref HEAD', {
36
+ cwd: process.cwd(),
37
+ stdio: ['ignore', 'pipe', 'ignore'],
38
+ encoding: 'utf8',
39
+ }).trim() || 'main'
40
+ rootGitStatusCache = execSync('git status --porcelain', {
41
+ cwd: process.cwd(),
42
+ stdio: ['ignore', 'pipe', 'ignore'],
43
+ encoding: 'utf8',
44
+ }).trim()
45
+ } catch {
46
+ rootGitBranchCache = 'main'
47
+ rootGitStatusCache = ''
48
+ }
49
+ }
50
+ const normPrefix =
51
+ projectDir && projectDir !== '.'
52
+ ? `${projectDir.replace(/\/+$/, '')}/`
53
+ : ''
54
+ const isDirty = normPrefix
55
+ ? rootGitStatusCache
56
+ .split('\n')
57
+ .some((line) => line.slice(3).trim().startsWith(normPrefix))
58
+ : rootGitStatusCache.length > 0
59
+
60
+ return {
61
+ branch: rootGitBranchCache,
62
+ gitStatus: isDirty ? 'dirty' : 'clean',
63
+ }
64
+ }
65
+
66
+ if (gitInfoCache.has(key)) return gitInfoCache.get(key)
67
+ if (fs.existsSync && !fs.existsSync(key)) {
68
+ const res = { branch: 'main', gitStatus: 'clean' }
69
+ gitInfoCache.set(key, res)
70
+ return res
71
+ }
72
+
73
+ try {
74
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', {
75
+ cwd: key,
76
+ stdio: ['ignore', 'pipe', 'ignore'],
77
+ encoding: 'utf8',
78
+ }).trim()
79
+ const statusOutput = execSync('git status --porcelain', {
80
+ cwd: key,
81
+ stdio: ['ignore', 'pipe', 'ignore'],
82
+ encoding: 'utf8',
83
+ }).trim()
84
+ const res = {
85
+ branch: branch || 'main',
86
+ gitStatus: statusOutput.length > 0 ? 'dirty' : 'clean',
87
+ }
88
+ gitInfoCache.set(key, res)
89
+ return res
90
+ } catch {
91
+ const res = { branch: 'main', gitStatus: 'clean' }
92
+ gitInfoCache.set(key, res)
93
+ return res
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Retrieve development hours and iterations from git commit log
99
+ * @param {string} [projectDir]
100
+ * @param {string} [releasePath]
101
+ * @returns {{ hours: number, iterations: number }}
102
+ */
103
+ export function getGitCommitTelemetry(projectDir, releasePath) {
104
+ const targetDir = releasePath || projectDir
105
+ if (!targetDir) return { hours: 0, iterations: 0 }
106
+ if (gitTelemetryCache.has(targetDir)) return gitTelemetryCache.get(targetDir)
107
+ if (fs.existsSync && !fs.existsSync(targetDir)) {
108
+ const res = { hours: 0, iterations: 0 }
109
+ gitTelemetryCache.set(targetDir, res)
110
+ return res
111
+ }
112
+
113
+ try {
114
+ const logOutput = execSync(
115
+ `git log --format="%at" -n 50 -- "${targetDir}"`,
116
+ {
117
+ cwd: process.cwd(),
118
+ stdio: ['ignore', 'pipe', 'ignore'],
119
+ encoding: 'utf8',
120
+ }
121
+ ).trim()
122
+
123
+ if (!logOutput) {
124
+ const res = { hours: 0, iterations: 0 }
125
+ gitTelemetryCache.set(targetDir, res)
126
+ return res
127
+ }
128
+
129
+ const timestamps = logOutput
130
+ .split('\n')
131
+ .map((t) => parseInt(t.trim(), 10))
132
+ .filter((t) => !isNaN(t))
133
+
134
+ if (timestamps.length < 2) {
135
+ const res = { hours: 0.5, iterations: timestamps.length }
136
+ gitTelemetryCache.set(targetDir, res)
137
+ return res
138
+ }
139
+
140
+ const minT = Math.min(...timestamps)
141
+ const maxT = Math.max(...timestamps)
142
+ const diffHours = Math.round(((maxT - minT) / 3600) * 10) / 10
143
+ const hours = diffHours > 0 ? diffHours : 0.5
144
+ const iterations = timestamps.length
145
+
146
+ const res = { hours, iterations }
147
+ gitTelemetryCache.set(targetDir, res)
148
+ return res
149
+ } catch {
150
+ const res = { hours: 0, iterations: 0 }
151
+ gitTelemetryCache.set(targetDir, res)
152
+ return res
153
+ }
154
+ }
@@ -1,14 +1,10 @@
1
- import Markdown, { MDElement } from '@nan0web/markdown';
2
- import Person from './Person.js';
1
+ export default ReleaseDocument;
3
2
  declare class ReleaseDocument extends Markdown {
4
- /** @type {Person[]} */
5
- team: Person[];
6
- /** @type {Map<string, Person[]>} */
7
- roles: Map<string, Person[]>;
8
- /** @type {string} */
9
- version: string;
10
- /** @type {Date | undefined} */
11
- date: Date | undefined;
3
+ /**
4
+ * @param {*} input
5
+ * @returns {ReleaseDocument}
6
+ */
7
+ static from(input: any): ReleaseDocument;
12
8
  /**
13
9
  * @param {Object} options
14
10
  * @param {Array} [options.team] - Release team members
@@ -17,22 +13,23 @@ declare class ReleaseDocument extends Markdown {
17
13
  * @param {string} [options.date] - Release date
18
14
  */
19
15
  constructor(options?: {
20
- team?: any[];
21
- roles?: Map<any, any> | any[];
22
- version?: string;
23
- date?: string;
16
+ team?: any[] | undefined;
17
+ roles?: any[] | Map<any, any> | undefined;
18
+ version?: string | undefined;
19
+ date?: string | undefined;
24
20
  });
25
- /**
26
- * Parse release document content
27
- * Extracts version and date from H1 heading
28
- * @param {string} input - Markdown content
29
- * @returns {MDElement[]}
30
- */
31
- parse(input: string): MDElement[];
32
- /**
33
- * @param {*} input
34
- * @returns {ReleaseDocument}
35
- */
36
- static from(input: any): ReleaseDocument;
21
+ /** @type {Person[]} */
22
+ team: Person[];
23
+ /** @type {Map<string, Person[]>} */
24
+ roles: Map<string, Person[]>;
25
+ /** @type {string} */
26
+ version: string;
27
+ /** @type {Date | undefined} */
28
+ date: Date | undefined;
29
+ sections: {
30
+ title: string;
31
+ tasks: any[];
32
+ }[] | undefined;
37
33
  }
38
- export default ReleaseDocument;
34
+ import Markdown from '@nan0web/markdown';
35
+ import Person from './Person.js';
@@ -1,3 +1,4 @@
1
+ export default MarkdownToTest;
1
2
  /**
2
3
  * MarkdownToTest - generates node:test code from markdown release notes
3
4
  * Enables writing release notes first, then generating executable tests
@@ -44,4 +45,3 @@ declare class MarkdownToTest {
44
45
  tasks: any[];
45
46
  }>): string;
46
47
  }
47
- export default MarkdownToTest;
@@ -1,15 +1,17 @@
1
- import { HumanName, HumanContact, HumanGender } from '@nan0web/logos';
2
1
  export default class Person {
2
+ /**
3
+ * @param {*} input
4
+ * @returns {Person}
5
+ */
6
+ static from(input: any): Person;
7
+ constructor(input?: {});
3
8
  /** @type {HumanName} */
4
9
  name: HumanName;
5
10
  /** @type {HumanGender} */
6
11
  gender: HumanGender;
7
12
  /** @type {HumanContact[]} */
8
13
  contacts: HumanContact[];
9
- constructor(input?: {});
10
- /**
11
- * @param {*} input
12
- * @returns {Person}
13
- */
14
- static from(input: any): Person;
15
14
  }
15
+ import { HumanName } from '@nan0web/logos';
16
+ import { HumanGender } from '@nan0web/logos';
17
+ import { HumanContact } from '@nan0web/logos';