@nan0web/release 3.4.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.
- package/README.md +0 -35
- package/bin/nan0release.js +2 -2
- package/package.json +9 -8
- package/src/domain/App.js +2 -0
- package/src/domain/PMaCParser.js +418 -0
- package/src/domain/PMaCParser.story.js +152 -0
- package/src/domain/ValidateCommand.js +426 -0
- package/src/domain/ValidateCommand.story.js +92 -0
- package/src/domain/models/ReleaseIssueModel.js +27 -0
- package/src/domain/models/ReleaseMemberModel.js +21 -0
- package/src/domain/models/ReleaseModel.js +191 -0
- package/src/domain/models/ReleaseParityModel.js +27 -0
- package/src/domain/models/ReleaseTaskModel.js +33 -0
- package/src/domain/models/index.js +6 -0
- package/src/index.js +9 -0
- package/types/domain/App.d.ts +2 -1
- package/types/domain/PMaCParser.d.ts +64 -0
- package/types/domain/PMaCParser.story.d.ts +1 -0
- package/types/domain/ValidateCommand.d.ts +69 -0
- package/types/domain/ValidateCommand.story.d.ts +1 -0
- package/types/domain/models/ReleaseIssueModel.d.ts +35 -0
- package/types/domain/models/ReleaseMemberModel.d.ts +25 -0
- package/types/domain/models/ReleaseModel.d.ts +122 -0
- package/types/domain/models/ReleaseParityModel.d.ts +37 -0
- package/types/domain/models/ReleaseTaskModel.d.ts +46 -0
- package/types/domain/models/index.d.ts +5 -0
- package/types/index.d.ts +3 -0
package/README.md
CHANGED
|
@@ -6,36 +6,29 @@ Project management as code, Git-native, GPG-signed, and test-driven.
|
|
|
6
6
|
|
|
7
7
|
Unlike traditional project tools that require constant syncing or manual updates,
|
|
8
8
|
`@nan0web/release` enforces project structure where:
|
|
9
|
-
|
|
10
9
|
- Tasks are represented as tests in `.test.js` files.
|
|
11
10
|
- Status is tracked via test outcomes.
|
|
12
11
|
- Team roles and approvals are stored in structured files.
|
|
13
12
|
- All changes are signed, versioned, and composable.
|
|
14
13
|
|
|
15
|
-
|
|
16
14
|
This ensures:
|
|
17
|
-
|
|
18
15
|
- Clear, automatable project tracking.
|
|
19
16
|
- Immutable release notes and retro reflections.
|
|
20
17
|
- Zero ambiguity – tasks don't exist unless tested.
|
|
21
18
|
|
|
22
|
-
|
|
23
19
|
## Installation
|
|
24
20
|
|
|
25
21
|
How to install with pnpm?
|
|
26
|
-
|
|
27
22
|
```bash
|
|
28
23
|
pnpm add @nan0web/release
|
|
29
24
|
```
|
|
30
25
|
|
|
31
26
|
How to install with npm?
|
|
32
|
-
|
|
33
27
|
```bash
|
|
34
28
|
npm install @nan0web/release
|
|
35
29
|
```
|
|
36
30
|
|
|
37
31
|
How to install with yarn?
|
|
38
|
-
|
|
39
32
|
```bash
|
|
40
33
|
yarn add @nan0web/release
|
|
41
34
|
```
|
|
@@ -45,7 +38,6 @@ yarn add @nan0web/release
|
|
|
45
38
|
Start by initializing a new release:
|
|
46
39
|
|
|
47
40
|
How to initialize a new release version?
|
|
48
|
-
|
|
49
41
|
```bash
|
|
50
42
|
nan0release init v1.0.0
|
|
51
43
|
```
|
|
@@ -53,7 +45,6 @@ nan0release init v1.0.0
|
|
|
53
45
|
Show release details:
|
|
54
46
|
|
|
55
47
|
How to show release information?
|
|
56
|
-
|
|
57
48
|
```bash
|
|
58
49
|
release show [--full]
|
|
59
50
|
```
|
|
@@ -66,7 +57,6 @@ console.info(release.version) // v1.0.0
|
|
|
66
57
|
List all releases in the project:
|
|
67
58
|
|
|
68
59
|
How to list all releases?
|
|
69
|
-
|
|
70
60
|
```bash
|
|
71
61
|
nan0release list [--json]
|
|
72
62
|
```
|
|
@@ -74,7 +64,6 @@ nan0release list [--json]
|
|
|
74
64
|
Add a chat message to the current release:
|
|
75
65
|
|
|
76
66
|
How to write a release chat message?
|
|
77
|
-
|
|
78
67
|
```bash
|
|
79
68
|
nan0release chat write --user alice "Issue with the build pipeline"
|
|
80
69
|
```
|
|
@@ -82,7 +71,6 @@ nan0release chat write --user alice "Issue with the build pipeline"
|
|
|
82
71
|
Host a static server for viewing releases:
|
|
83
72
|
|
|
84
73
|
How to host release UI?
|
|
85
|
-
|
|
86
74
|
```bash
|
|
87
75
|
nan0release host [--webui] [--port 3000]
|
|
88
76
|
```
|
|
@@ -90,7 +78,6 @@ nan0release host [--webui] [--port 3000]
|
|
|
90
78
|
Serve all release files for local inspection:
|
|
91
79
|
|
|
92
80
|
How to serve release static assets?
|
|
93
|
-
|
|
94
81
|
```bash
|
|
95
82
|
nan0release serve [--port 8080]
|
|
96
83
|
```
|
|
@@ -98,7 +85,6 @@ nan0release serve [--port 8080]
|
|
|
98
85
|
Validate release tasks and integrity:
|
|
99
86
|
|
|
100
87
|
How to validate release tasks?
|
|
101
|
-
|
|
102
88
|
```bash
|
|
103
89
|
nan0release validate [--ignore-fail]
|
|
104
90
|
```
|
|
@@ -106,7 +92,6 @@ nan0release validate [--ignore-fail]
|
|
|
106
92
|
Seal the release with retro reflection:
|
|
107
93
|
|
|
108
94
|
How to seal a release?
|
|
109
|
-
|
|
110
95
|
```bash
|
|
111
96
|
nan0release seal [--message "All core APIs are stable and tested"]
|
|
112
97
|
```
|
|
@@ -116,16 +101,13 @@ nan0release seal [--message "All core APIs are stable and tested"]
|
|
|
116
101
|
### 1. Release as Object
|
|
117
102
|
|
|
118
103
|
A `Release` is a structured class containing:
|
|
119
|
-
|
|
120
104
|
- `version`: release identifier (vX.Y.Z).
|
|
121
105
|
- `createdAt`, `startAt`, `planAt`, `completeAt`: datetime milestones.
|
|
122
106
|
- `document`: parsed markdown document with structure-aware parsing.
|
|
123
107
|
|
|
124
|
-
|
|
125
108
|
Each property is typed and validated automatically via JSDoc and runtime parsing.
|
|
126
109
|
|
|
127
110
|
How to instantiate a Release?
|
|
128
|
-
|
|
129
111
|
```js
|
|
130
112
|
import { Release } from '@nan0web/release'
|
|
131
113
|
const release = new Release({
|
|
@@ -138,14 +120,11 @@ console.info(release.createdAt instanceof Date) // ← true
|
|
|
138
120
|
### 2. Release Document Parsing
|
|
139
121
|
|
|
140
122
|
A `ReleaseDocument` extends `@nan0web/markdown` with:
|
|
141
|
-
|
|
142
123
|
- Structured parsing of markdown release notes
|
|
143
124
|
- Extraction of sections and tasks
|
|
144
125
|
- Team and role parsing from markdown frontmatter or code
|
|
145
126
|
|
|
146
|
-
|
|
147
127
|
How to parse release notes markdown into structured data?
|
|
148
|
-
|
|
149
128
|
```js
|
|
150
129
|
import { ReleaseDocument } from '@nan0web/release'
|
|
151
130
|
const md = `# v1.0.0 - 2025-08-20
|
|
@@ -162,14 +141,11 @@ console.info(doc.document.children instanceof Array) // ← true
|
|
|
162
141
|
### 3. Person & Team Structures
|
|
163
142
|
|
|
164
143
|
A `Person` instance includes:
|
|
165
|
-
|
|
166
144
|
- `name`: HumanName
|
|
167
145
|
- `gender`: HumanGender
|
|
168
146
|
- `contacts`: array of HumanContact
|
|
169
147
|
|
|
170
|
-
|
|
171
148
|
How to create a Person with typed properties?
|
|
172
|
-
|
|
173
149
|
```js
|
|
174
150
|
import { Person } from '@nan0web/release'
|
|
175
151
|
const person = new Person({
|
|
@@ -190,7 +166,6 @@ Tasks are registered as `taskId` → `testFilePath` in `ProjectManagement`.
|
|
|
190
166
|
Their status is derived by running `node:test` suites.
|
|
191
167
|
|
|
192
168
|
How to register and validate tasks as tests?
|
|
193
|
-
|
|
194
169
|
```js
|
|
195
170
|
import { ProjectManagement } from '@nan0web/release'
|
|
196
171
|
const pm = new ProjectManagement()
|
|
@@ -210,14 +185,11 @@ console.info(results.pending.includes('task-2')) // ← true
|
|
|
210
185
|
|
|
211
186
|
The `ReleaseManager` coordinates releases and validates readiness.
|
|
212
187
|
It ensures:
|
|
213
|
-
|
|
214
188
|
- All registered tests pass
|
|
215
189
|
- Version is correctly incremented
|
|
216
190
|
- Git tag is applied if all validations succeed
|
|
217
191
|
|
|
218
|
-
|
|
219
192
|
How to execute a release after validation?
|
|
220
|
-
|
|
221
193
|
```js
|
|
222
194
|
import { ReleaseManager, ProjectManagement } from '@nan0web/release'
|
|
223
195
|
const pm = new ProjectManagement()
|
|
@@ -231,14 +203,11 @@ console.info(result.published) // ← true
|
|
|
231
203
|
### Changelog Integration
|
|
232
204
|
|
|
233
205
|
Using `ChangelogTaskManager`, you can:
|
|
234
|
-
|
|
235
206
|
- Extract task definitions from changelog entries
|
|
236
207
|
- Generate corresponding test files
|
|
237
208
|
- Automate task progression in CI
|
|
238
209
|
|
|
239
|
-
|
|
240
210
|
How to parse changelog and extract tasks?
|
|
241
|
-
|
|
242
211
|
```js
|
|
243
212
|
import { ChangelogTaskManager } from '@nan0web/release'
|
|
244
213
|
const ctm = new ChangelogTaskManager()
|
|
@@ -260,7 +229,6 @@ Uses JSDoc and .d.ts files for autocompletion
|
|
|
260
229
|
Try the package in the terminal:
|
|
261
230
|
|
|
262
231
|
How to run playground demo?
|
|
263
|
-
|
|
264
232
|
```bash
|
|
265
233
|
git clone https://github.com/nan0web/release.git
|
|
266
234
|
cd release
|
|
@@ -273,7 +241,6 @@ pnpm run playground
|
|
|
273
241
|
To verify the project is ready, run:
|
|
274
242
|
|
|
275
243
|
How to check project status before publishing?
|
|
276
|
-
|
|
277
244
|
```bash
|
|
278
245
|
npm test
|
|
279
246
|
npm run test:coverage
|
|
@@ -287,5 +254,3 @@ How to contribute? - [check here](./CONTRIBUTING.md)
|
|
|
287
254
|
## License
|
|
288
255
|
|
|
289
256
|
How to license ISC? - [check here](./LICENSE)
|
|
290
|
-
|
|
291
|
-
|
package/bin/nan0release.js
CHANGED
|
@@ -28,10 +28,10 @@ if (process.cwd() !== root && existsSync(resolve(root, 'releases.txt'))) {
|
|
|
28
28
|
process.chdir(root)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const db = new DBFS()
|
|
31
|
+
const db = new DBFS({ root: '.' })
|
|
32
32
|
await db.connect()
|
|
33
33
|
|
|
34
|
-
bootstrapApp(ReleaseApp, { db }).catch((err) => {
|
|
34
|
+
bootstrapApp(ReleaseApp, { db, root: '.' }).catch((err) => {
|
|
35
35
|
console.error(err)
|
|
36
36
|
process.exit(1)
|
|
37
37
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nan0web/release",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Project Management as Code, Git-Native, GPG-Signed, Test-Driven",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -37,16 +37,17 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@nan0web/changelog": "3.4.0",
|
|
40
|
-
"@nan0web/
|
|
40
|
+
"@nan0web/csv0": "3.6.0",
|
|
41
|
+
"@nan0web/co": "3.5.0",
|
|
42
|
+
"@nan0web/db-fs": "3.6.0",
|
|
43
|
+
"@nan0web/db": "3.5.0",
|
|
44
|
+
"@nan0web/inspect": "3.7.0",
|
|
41
45
|
"@nan0web/log": "3.4.0",
|
|
42
46
|
"@nan0web/logos": "3.4.0",
|
|
43
|
-
"@nan0web/
|
|
44
|
-
"@nan0web/
|
|
47
|
+
"@nan0web/markdown": "3.4.0",
|
|
48
|
+
"@nan0web/ui": "3.5.0",
|
|
45
49
|
"@nan0web/types": "3.4.0",
|
|
46
|
-
"@nan0web/ui": "3.
|
|
47
|
-
"@nan0web/ui-cli": "3.4.0",
|
|
48
|
-
"@nan0web/db-fs": "3.4.0",
|
|
49
|
-
"@nan0web/markdown": "3.4.0"
|
|
50
|
+
"@nan0web/ui-cli": "3.5.0"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"@types/node": "^25.6.0",
|
package/src/domain/App.js
CHANGED
|
@@ -12,6 +12,7 @@ import PreviewCommand from './PreviewCommand.js'
|
|
|
12
12
|
import ViewCommand from './ViewCommand.js'
|
|
13
13
|
import ListCommand from './ListCommand.js'
|
|
14
14
|
import ServeCommand from './ServeCommand.js'
|
|
15
|
+
import ValidateCommand from './ValidateCommand.js'
|
|
15
16
|
|
|
16
17
|
export class ReleaseApp extends ModelAsApp {
|
|
17
18
|
static alias = 'release'
|
|
@@ -25,6 +26,7 @@ export class ReleaseApp extends ModelAsApp {
|
|
|
25
26
|
options: [
|
|
26
27
|
InitCommand,
|
|
27
28
|
CheckCommand,
|
|
29
|
+
ValidateCommand,
|
|
28
30
|
CloseCommand,
|
|
29
31
|
DepsCommand,
|
|
30
32
|
PublishCommand,
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import Markdown, {
|
|
2
|
+
MDHeading1,
|
|
3
|
+
MDHeading2,
|
|
4
|
+
MDHeading3,
|
|
5
|
+
MDTaskList,
|
|
6
|
+
MDTable,
|
|
7
|
+
MDTableRow,
|
|
8
|
+
MDList,
|
|
9
|
+
MDParagraph,
|
|
10
|
+
} from '@nan0web/markdown'
|
|
11
|
+
import {
|
|
12
|
+
ReleaseModel,
|
|
13
|
+
ReleaseMemberModel,
|
|
14
|
+
ReleaseTaskModel,
|
|
15
|
+
ReleaseParityModel,
|
|
16
|
+
ReleaseIssueModel,
|
|
17
|
+
} from './models/index.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Clean text from emoji, symbols, markdown formatting and trim.
|
|
21
|
+
* @param {string} text
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
export function cleanSectionTitle(text) {
|
|
25
|
+
return String(text || '')
|
|
26
|
+
.replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/gu, '')
|
|
27
|
+
.replace(/^[#\s*_-]+|[#\s*_-]+$/g, '')
|
|
28
|
+
.trim()
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Match section title against known synonyms in ReleaseModel.SECTIONS.
|
|
34
|
+
* @param {string} headingText
|
|
35
|
+
* @param {typeof ReleaseModel} ModelClass
|
|
36
|
+
* @returns {string | null} Recognized section key or null
|
|
37
|
+
*/
|
|
38
|
+
export function identifySection(headingText, ModelClass = ReleaseModel) {
|
|
39
|
+
const cleaned = cleanSectionTitle(headingText)
|
|
40
|
+
if (!cleaned) return null
|
|
41
|
+
|
|
42
|
+
for (const [sectionKey, synonyms] of Object.entries(ModelClass.SECTIONS)) {
|
|
43
|
+
for (const synonym of synonyms) {
|
|
44
|
+
const synCleaned = synonym.trim().toLowerCase()
|
|
45
|
+
if (cleaned === synCleaned || cleaned.startsWith(synCleaned) || cleaned.endsWith(synCleaned)) {
|
|
46
|
+
return sectionKey
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve external tracker URL for a task or issue ID using template URL.
|
|
55
|
+
* Example: trackerUrl = "https://github.com/org/repo/issues/{id}", rawId = "[#142]" -> "https://github.com/org/repo/issues/142"
|
|
56
|
+
* @param {string} id
|
|
57
|
+
* @param {string} trackerTemplate
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function resolveTrackerUrl(id, trackerTemplate) {
|
|
61
|
+
if (!trackerTemplate || !id) return ''
|
|
62
|
+
const cleanId = String(id).replace(/^\[#?|\]$/g, '').trim()
|
|
63
|
+
return trackerTemplate.replace(/\{id\}/g, cleanId)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* PMaCParser - Parses PMaC Release/Task specification documents into ReleaseModel instances.
|
|
68
|
+
*/
|
|
69
|
+
export class PMaCParser {
|
|
70
|
+
/**
|
|
71
|
+
* @param {typeof ReleaseModel} [modelClass]
|
|
72
|
+
*/
|
|
73
|
+
constructor(modelClass = ReleaseModel) {
|
|
74
|
+
this.ModelClass = modelClass
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse markdown content into a ReleaseModel instance.
|
|
79
|
+
* @param {string} markdownContent
|
|
80
|
+
* @returns {ReleaseModel}
|
|
81
|
+
*/
|
|
82
|
+
parse(markdownContent) {
|
|
83
|
+
const md = new Markdown(markdownContent)
|
|
84
|
+
const frontmatter = md.vars || {}
|
|
85
|
+
const elements = md.document?.children || []
|
|
86
|
+
|
|
87
|
+
// 1. Identify H1 for version and title
|
|
88
|
+
let version = ''
|
|
89
|
+
let title = ''
|
|
90
|
+
|
|
91
|
+
const h1 = elements.find((el) => el instanceof MDHeading1)
|
|
92
|
+
if (h1) {
|
|
93
|
+
const rawTitle = h1.content?.trim() || ''
|
|
94
|
+
// Matches formats like: "# v0.1.0 — Core Release Engine" or "# v0.1.0 - Core Release" or "# v1.0.0"
|
|
95
|
+
const h1Match = rawTitle.match(/^(v\d+\.\d+\.\d+(?:-[\w.]+)?)\s*(?:[—–-]\s*(.+))?$/i)
|
|
96
|
+
if (h1Match) {
|
|
97
|
+
version = h1Match[1]
|
|
98
|
+
title = (h1Match[2] || '').trim()
|
|
99
|
+
} else {
|
|
100
|
+
// Fallback: check if starts with version
|
|
101
|
+
const vMatch = rawTitle.match(/^(v\d+\.\d+\.\d+(?:-[\w.]+)?)/i)
|
|
102
|
+
if (vMatch) {
|
|
103
|
+
version = vMatch[1]
|
|
104
|
+
title = rawTitle.slice(vMatch[0].length).replace(/^[—–-\s]+/, '').trim()
|
|
105
|
+
} else {
|
|
106
|
+
title = rawTitle
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const trackerTemplate = frontmatter.tracker || ''
|
|
112
|
+
|
|
113
|
+
// Build release data
|
|
114
|
+
const releaseData = {
|
|
115
|
+
version: frontmatter.version || version,
|
|
116
|
+
title: frontmatter.title || title,
|
|
117
|
+
status: frontmatter.status || 'todo',
|
|
118
|
+
type: frontmatter.type || 'minor',
|
|
119
|
+
epic: frontmatter.epic,
|
|
120
|
+
sprint: frontmatter.sprint,
|
|
121
|
+
tracker: trackerTemplate,
|
|
122
|
+
summary: '',
|
|
123
|
+
team: [],
|
|
124
|
+
tasks: [],
|
|
125
|
+
criteria: [],
|
|
126
|
+
parity: [],
|
|
127
|
+
issues: [],
|
|
128
|
+
files: [],
|
|
129
|
+
meta: { ...frontmatter },
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 2. Iterate through sections delimited by MDHeading2
|
|
133
|
+
let currentSection = null
|
|
134
|
+
let currentTaskGroup = ''
|
|
135
|
+
|
|
136
|
+
for (const el of elements) {
|
|
137
|
+
if (el instanceof MDHeading2) {
|
|
138
|
+
currentSection = identifySection(el.content, this.ModelClass)
|
|
139
|
+
currentTaskGroup = ''
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (el instanceof MDHeading3) {
|
|
144
|
+
currentTaskGroup = el.content?.trim() || ''
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!currentSection) continue
|
|
149
|
+
|
|
150
|
+
if (currentSection === 'summary') {
|
|
151
|
+
if (el instanceof MDParagraph) {
|
|
152
|
+
const text = el.content?.trim()
|
|
153
|
+
if (text) {
|
|
154
|
+
releaseData.summary = releaseData.summary
|
|
155
|
+
? `${releaseData.summary}\n\n${text}`
|
|
156
|
+
: text
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} else if (currentSection === 'team') {
|
|
160
|
+
this._parseTeamSection(el, releaseData.team)
|
|
161
|
+
} else if (currentSection === 'tasks') {
|
|
162
|
+
this._parseTasksSection(el, releaseData.tasks, currentTaskGroup, trackerTemplate)
|
|
163
|
+
} else if (currentSection === 'criteria') {
|
|
164
|
+
this._parseTasksSection(el, releaseData.criteria, currentTaskGroup, trackerTemplate)
|
|
165
|
+
} else if (currentSection === 'parity') {
|
|
166
|
+
this._parseParitySection(el, releaseData.parity)
|
|
167
|
+
} else if (currentSection === 'issues') {
|
|
168
|
+
this._parseIssuesSection(el, releaseData.issues)
|
|
169
|
+
} else if (currentSection === 'artifacts') {
|
|
170
|
+
this._parseArtifactsSection(el, releaseData.files)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return new this.ModelClass(releaseData)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @private
|
|
179
|
+
*/
|
|
180
|
+
_parseTeamSection(el, teamList) {
|
|
181
|
+
const items = []
|
|
182
|
+
if (el instanceof MDList) {
|
|
183
|
+
for (const child of el.children) {
|
|
184
|
+
items.push(child.content?.trim() || '')
|
|
185
|
+
}
|
|
186
|
+
} else if (el.content) {
|
|
187
|
+
const lines = el.content.split('\n')
|
|
188
|
+
for (const line of lines) {
|
|
189
|
+
const trimmed = line.trim()
|
|
190
|
+
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
|
191
|
+
items.push(trimmed.slice(2).trim())
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const item of items) {
|
|
197
|
+
// Matches: "**Lead / Architect:** Олександр Снігірьов (@snigirev)"
|
|
198
|
+
// or "**Посада:** Ім'я (@handle)"
|
|
199
|
+
const match = item.match(/^\*\*([^*]+)\*\*\s*(.*)$/)
|
|
200
|
+
if (match) {
|
|
201
|
+
const role = match[1].replace(/:$/, '').trim()
|
|
202
|
+
const rest = match[2].trim()
|
|
203
|
+
const handleMatch = rest.match(/\(@([^)]+)\)/)
|
|
204
|
+
const handle = handleMatch ? handleMatch[1].trim() : ''
|
|
205
|
+
const name = rest.replace(/\(@[^)]+\)/, '').trim()
|
|
206
|
+
teamList.push(new ReleaseMemberModel({ role, name, handle }))
|
|
207
|
+
} else {
|
|
208
|
+
teamList.push(new ReleaseMemberModel({ role: '', name: item, handle: '' }))
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @private
|
|
215
|
+
*/
|
|
216
|
+
_parseTasksSection(el, tasksList, taskGroup, trackerTemplate) {
|
|
217
|
+
const items = []
|
|
218
|
+
if (el instanceof MDTaskList || el instanceof MDList) {
|
|
219
|
+
for (const child of el.children) {
|
|
220
|
+
items.push(child.content?.trim() || '')
|
|
221
|
+
}
|
|
222
|
+
} else if (el.content && el.content.includes('- [')) {
|
|
223
|
+
const lines = el.content.split('\n')
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
const trimmed = line.trim()
|
|
226
|
+
if (trimmed.startsWith('- [')) {
|
|
227
|
+
items.push(trimmed.slice(2).trim())
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const rawItem of items) {
|
|
233
|
+
// e.g. "[x] [#101-1] Винести базову модель ReleaseModel у пакет @nan0web/release"
|
|
234
|
+
// or "- [x] [#101-1] ..." or "[ ] [#101-3] ..."
|
|
235
|
+
let line = rawItem
|
|
236
|
+
if (line.startsWith('- ')) line = line.slice(2).trim()
|
|
237
|
+
|
|
238
|
+
const boxMatch = line.match(/^\[([ xX])\]\s*(.*)$/)
|
|
239
|
+
let isDone = false
|
|
240
|
+
let text = line
|
|
241
|
+
|
|
242
|
+
if (boxMatch) {
|
|
243
|
+
isDone = boxMatch[1].toLowerCase() === 'x'
|
|
244
|
+
text = boxMatch[2].trim()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Extract task ID like [#101-1] or [101-1] or [CORE-101]
|
|
248
|
+
let id = ''
|
|
249
|
+
const idMatch = text.match(/^\[#?([A-Za-z0-9_-]+)\]\s*(.*)$/)
|
|
250
|
+
if (idMatch) {
|
|
251
|
+
id = idMatch[1]
|
|
252
|
+
text = idMatch[2].trim()
|
|
253
|
+
} else {
|
|
254
|
+
// Fallback: derive ID from group or random
|
|
255
|
+
id = `task-${tasksList.length + 1}`
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check for notes or assignee or hours in parens/brackets
|
|
259
|
+
let notes = ''
|
|
260
|
+
if (taskGroup) {
|
|
261
|
+
notes = taskGroup
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
tasksList.push(
|
|
265
|
+
new ReleaseTaskModel({
|
|
266
|
+
id,
|
|
267
|
+
title: text,
|
|
268
|
+
status: isDone ? 'done' : 'todo',
|
|
269
|
+
notes,
|
|
270
|
+
})
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
_parseParitySection(el, parityList) {
|
|
279
|
+
let rows = []
|
|
280
|
+
if (el instanceof MDTable) {
|
|
281
|
+
rows = el.children || []
|
|
282
|
+
} else if (el instanceof MDTableRow) {
|
|
283
|
+
rows = [el]
|
|
284
|
+
}
|
|
285
|
+
if (rows.length === 0) return
|
|
286
|
+
|
|
287
|
+
for (const row of rows) {
|
|
288
|
+
const rowContent = row.content?.trim() || ''
|
|
289
|
+
// Skip separator rows like | :--- | :---: |
|
|
290
|
+
if (/^\|[\s|:-]+\|$/.test(rowContent)) continue
|
|
291
|
+
|
|
292
|
+
const cells = rowContent
|
|
293
|
+
.split('|')
|
|
294
|
+
.map((c) => c.trim())
|
|
295
|
+
.filter((c, idx, arr) => idx > 0 && idx < arr.length - 1)
|
|
296
|
+
|
|
297
|
+
// Skip header row
|
|
298
|
+
if (cells.length >= 3) {
|
|
299
|
+
if (cells[0].toLowerCase() === 'колекція' || cells[0].toLowerCase() === 'collection') {
|
|
300
|
+
continue
|
|
301
|
+
}
|
|
302
|
+
const collection = cells[0].replace(/`/g, '')
|
|
303
|
+
const expected = parseInt(cells[1], 10) || 0
|
|
304
|
+
const actual = parseInt(cells[2], 10) || 0
|
|
305
|
+
const statusText = cells[3] || ''
|
|
306
|
+
const comment = cells[4] || ''
|
|
307
|
+
const synced = statusText.includes('OK') || statusText.includes('✅') || expected === actual
|
|
308
|
+
|
|
309
|
+
parityList.push(
|
|
310
|
+
new ReleaseParityModel({
|
|
311
|
+
collection,
|
|
312
|
+
expected,
|
|
313
|
+
actual,
|
|
314
|
+
synced,
|
|
315
|
+
comment,
|
|
316
|
+
})
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* @private
|
|
324
|
+
*/
|
|
325
|
+
_parseIssuesSection(el, issuesList) {
|
|
326
|
+
const items = []
|
|
327
|
+
if (el instanceof MDList) {
|
|
328
|
+
for (const child of el.children) {
|
|
329
|
+
items.push(child.content?.trim() || '')
|
|
330
|
+
}
|
|
331
|
+
} else if (el.content) {
|
|
332
|
+
const lines = el.content.split('\n')
|
|
333
|
+
for (const line of lines) {
|
|
334
|
+
const trimmed = line.trim()
|
|
335
|
+
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
|
336
|
+
items.push(trimmed.slice(2).trim())
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
for (const item of items) {
|
|
342
|
+
// Check if this is a sub-detail line (e.g. "Джерело: `@nan0web/payload-release/admin.jsx:L85`" or "Source: ...")
|
|
343
|
+
const sourceMatch = item.match(/^(?:Джерело|Source|File):\s*`?([^`]+)`?/i)
|
|
344
|
+
if (sourceMatch && issuesList.length > 0) {
|
|
345
|
+
const lastIssue = issuesList[issuesList.length - 1]
|
|
346
|
+
lastIssue.source = sourceMatch[1].trim()
|
|
347
|
+
lastIssue.file = sourceMatch[1].split(':')[0].trim()
|
|
348
|
+
continue
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Example: "**[WARN-001]** (Performance) Файли розміром > 2MB не повинні відкриватися"
|
|
352
|
+
const codeMatch = item.match(/^\*\*\[?([A-Za-z0-9_-]+)\]?\*\*\s*(.*)$/)
|
|
353
|
+
let code = ''
|
|
354
|
+
let rest = item
|
|
355
|
+
if (codeMatch) {
|
|
356
|
+
code = codeMatch[1]
|
|
357
|
+
rest = codeMatch[2].trim()
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let level = 'warning'
|
|
361
|
+
const upperCode = code.toUpperCase()
|
|
362
|
+
if (upperCode.includes('ERR') || upperCode.includes('BLOCK')) level = 'error'
|
|
363
|
+
else if (upperCode.includes('INFO')) level = 'info'
|
|
364
|
+
|
|
365
|
+
issuesList.push(
|
|
366
|
+
new ReleaseIssueModel({
|
|
367
|
+
code,
|
|
368
|
+
message: rest,
|
|
369
|
+
level,
|
|
370
|
+
})
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @private
|
|
377
|
+
*/
|
|
378
|
+
_parseArtifactsSection(el, filesList) {
|
|
379
|
+
let rows = []
|
|
380
|
+
if (el instanceof MDTable) {
|
|
381
|
+
rows = el.children || []
|
|
382
|
+
} else if (el instanceof MDTableRow) {
|
|
383
|
+
rows = [el]
|
|
384
|
+
}
|
|
385
|
+
if (rows.length === 0) return
|
|
386
|
+
|
|
387
|
+
for (const row of rows) {
|
|
388
|
+
const rowContent = row.content?.trim() || ''
|
|
389
|
+
if (/^\|[\s|:-]+\|$/.test(rowContent)) continue
|
|
390
|
+
|
|
391
|
+
const cells = rowContent
|
|
392
|
+
.split('|')
|
|
393
|
+
.map((c) => c.trim())
|
|
394
|
+
.filter((c, idx, arr) => idx > 0 && idx < arr.length - 1)
|
|
395
|
+
|
|
396
|
+
if (cells.length >= 1) {
|
|
397
|
+
if (cells[0].toLowerCase() === 'файл' || cells[0].toLowerCase() === 'file') {
|
|
398
|
+
continue
|
|
399
|
+
}
|
|
400
|
+
const file = cells[0].replace(/`/g, '')
|
|
401
|
+
const size = cells[1] || ''
|
|
402
|
+
const hash = cells[2] ? cells[2].replace(/`/g, '') : ''
|
|
403
|
+
const type = cells[3] || ''
|
|
404
|
+
filesList.push({ file, size, hash, type })
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Static shortcut to parse specification
|
|
411
|
+
* @param {string} markdownContent
|
|
412
|
+
* @returns {ReleaseModel}
|
|
413
|
+
*/
|
|
414
|
+
static parse(markdownContent) {
|
|
415
|
+
return new PMaCParser().parse(markdownContent)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|