deepflow 0.1.59 → 0.1.60
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/hooks/df-spec-lint.js +53 -34
- package/package.json +1 -1
package/hooks/df-spec-lint.js
CHANGED
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
12
|
+
// Each entry: [canonical name, ...aliases that also satisfy the requirement]
|
|
12
13
|
const REQUIRED_SECTIONS = [
|
|
13
|
-
'Objective',
|
|
14
|
-
'Requirements',
|
|
15
|
-
'Constraints',
|
|
16
|
-
'Out of Scope',
|
|
17
|
-
'Acceptance Criteria',
|
|
18
|
-
'Technical Notes',
|
|
14
|
+
['Objective', 'overview', 'goal', 'goals', 'summary'],
|
|
15
|
+
['Requirements', 'functional requirements'],
|
|
16
|
+
['Constraints', 'tech constraints', 'technical constraints'],
|
|
17
|
+
['Out of Scope', 'out of scope (mvp)', 'non-goals', 'exclusions'],
|
|
18
|
+
['Acceptance Criteria'],
|
|
19
|
+
['Technical Notes', 'architecture notes', 'architecture', 'tech notes', 'implementation notes'],
|
|
19
20
|
];
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -34,36 +35,42 @@ function validateSpec(content, { mode = 'interactive' } = {}) {
|
|
|
34
35
|
const headersFound = [];
|
|
35
36
|
for (const line of content.split('\n')) {
|
|
36
37
|
const m = line.match(/^##\s+(.+)/i);
|
|
37
|
-
if (m)
|
|
38
|
+
if (m) {
|
|
39
|
+
// Strip leading numbering like "1.", "2.", "3." from headers
|
|
40
|
+
const raw = m[1].trim().replace(/^\d+\.\s*/, '');
|
|
41
|
+
headersFound.push(raw);
|
|
42
|
+
}
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
for (const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
);
|
|
45
|
+
for (const [canonical, ...aliases] of REQUIRED_SECTIONS) {
|
|
46
|
+
const allNames = [canonical, ...aliases].map((n) => n.toLowerCase());
|
|
47
|
+
const found = headersFound.some((h) => allNames.includes(h.toLowerCase()));
|
|
44
48
|
if (!found) {
|
|
45
|
-
|
|
49
|
+
// Inline *AC: lines satisfy the Acceptance Criteria requirement
|
|
50
|
+
if (canonical === 'Acceptance Criteria' && /\*AC[:.]/.test(content)) continue;
|
|
51
|
+
hard.push(`Missing required section: "## ${canonical}"`);
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
// ── (b)
|
|
55
|
+
// ── (b) Check that requirements have REQ-N identifiers ──────────────
|
|
56
|
+
// Requirements can be formatted as:
|
|
57
|
+
// - List items: "- REQ-1: ..." or "- **REQ-1** — ..."
|
|
58
|
+
// - Paragraphs: "**REQ-1 — Title**"
|
|
59
|
+
// We verify that at least one REQ-N identifier exists in the section.
|
|
60
|
+
// Sub-bullets (detail items) are not flagged.
|
|
50
61
|
const reqSection = extractSection(content, 'Requirements');
|
|
51
62
|
if (reqSection !== null) {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!/^\s*[-*]\s+/.test(line)) continue;
|
|
56
|
-
// Must match REQ-\d+: with optional bold markers
|
|
57
|
-
if (!/^\s*[-*]\s*\*{0,2}(REQ-\d+)\*{0,2}\s*:/.test(line)) {
|
|
58
|
-
hard.push(
|
|
59
|
-
`Requirement line missing REQ-N: prefix: "${line.trim()}"`
|
|
60
|
-
);
|
|
61
|
-
}
|
|
63
|
+
const hasReqIds = /REQ-\d+/.test(reqSection);
|
|
64
|
+
if (!hasReqIds) {
|
|
65
|
+
hard.push('Requirements section has no REQ-N identifiers');
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
// ── (c) Acceptance Criteria
|
|
69
|
+
// ── (c) Acceptance Criteria ────────────────────────────────────────
|
|
70
|
+
// Accept either a dedicated ## Acceptance Criteria section with checkboxes,
|
|
71
|
+
// or inline *AC: lines within the requirements section.
|
|
66
72
|
const acSection = extractSection(content, 'Acceptance Criteria');
|
|
73
|
+
const hasInlineAC = /\*AC[:.]/.test(content);
|
|
67
74
|
if (acSection !== null) {
|
|
68
75
|
const lines = acSection.split('\n');
|
|
69
76
|
for (const line of lines) {
|
|
@@ -74,10 +81,12 @@ function validateSpec(content, { mode = 'interactive' } = {}) {
|
|
|
74
81
|
);
|
|
75
82
|
}
|
|
76
83
|
}
|
|
84
|
+
} else if (!hasInlineAC) {
|
|
85
|
+
// No dedicated section and no inline ACs — already flagged by missing section check
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
// ── (d) No duplicate REQ-N IDs ──────────────────────────────────────
|
|
80
|
-
const reqIdPattern = /\*{0,2}(REQ-\d+)\*{0,2}\s
|
|
89
|
+
const reqIdPattern = /\*{0,2}(REQ-\d+[a-z]?)\*{0,2}\s*(?:[:\u2014]|—)/g;
|
|
81
90
|
const seenIds = new Map();
|
|
82
91
|
let match;
|
|
83
92
|
while ((match = reqIdPattern.exec(content)) !== null) {
|
|
@@ -90,16 +99,17 @@ function validateSpec(content, { mode = 'interactive' } = {}) {
|
|
|
90
99
|
|
|
91
100
|
// ── Advisory checks ──────────────────────────────────────────────────
|
|
92
101
|
|
|
93
|
-
// (adv-a) Line count >
|
|
102
|
+
// (adv-a) Line count > 200
|
|
94
103
|
const lineCount = content.split('\n').length;
|
|
95
|
-
if (lineCount >
|
|
96
|
-
advisory.push(`Spec exceeds
|
|
104
|
+
if (lineCount > 200) {
|
|
105
|
+
advisory.push(`Spec exceeds 200 lines (${lineCount} lines)`);
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
// (adv-b) Orphaned REQ-N IDs not referenced in Acceptance Criteria
|
|
109
|
+
// Skip this check when ACs are inline within requirements
|
|
100
110
|
if (reqSection !== null && acSection !== null) {
|
|
101
111
|
const reqIds = [];
|
|
102
|
-
const reqLinePattern = /\*{0,2}(REQ-\d+)\*{0,2}\s
|
|
112
|
+
const reqLinePattern = /\*{0,2}(REQ-\d+[a-z]?)\*{0,2}\s*[:\u2014-]/g;
|
|
103
113
|
let reqMatch;
|
|
104
114
|
while ((reqMatch = reqLinePattern.exec(reqSection)) !== null) {
|
|
105
115
|
reqIds.push(reqMatch[1]);
|
|
@@ -120,9 +130,9 @@ function validateSpec(content, { mode = 'interactive' } = {}) {
|
|
|
120
130
|
}
|
|
121
131
|
}
|
|
122
132
|
|
|
123
|
-
// (adv-d) More than
|
|
124
|
-
if (seenIds.size >
|
|
125
|
-
advisory.push(`Too many requirements (${seenIds.size}, limit
|
|
133
|
+
// (adv-d) More than 20 requirements
|
|
134
|
+
if (seenIds.size > 20) {
|
|
135
|
+
advisory.push(`Too many requirements (${seenIds.size}, limit 20)`);
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
// ── Auto-mode escalation ─────────────────────────────────────────────
|
|
@@ -138,15 +148,24 @@ function validateSpec(content, { mode = 'interactive' } = {}) {
|
|
|
138
148
|
* Returns null if the section is not found.
|
|
139
149
|
*/
|
|
140
150
|
function extractSection(content, sectionName) {
|
|
151
|
+
// Find the matching aliases for this section name
|
|
152
|
+
const entry = REQUIRED_SECTIONS.find(
|
|
153
|
+
([canonical]) => canonical.toLowerCase() === sectionName.toLowerCase()
|
|
154
|
+
);
|
|
155
|
+
const allNames = entry
|
|
156
|
+
? [entry[0], ...entry.slice(1)].map((n) => n.toLowerCase())
|
|
157
|
+
: [sectionName.toLowerCase()];
|
|
158
|
+
|
|
141
159
|
const lines = content.split('\n');
|
|
142
160
|
let capturing = false;
|
|
143
161
|
const captured = [];
|
|
144
162
|
|
|
145
163
|
for (const line of lines) {
|
|
146
|
-
const headerMatch = line.match(
|
|
164
|
+
const headerMatch = line.match(/^##\s+(.+)/);
|
|
147
165
|
if (headerMatch) {
|
|
148
166
|
if (capturing) break; // hit the next section
|
|
149
|
-
|
|
167
|
+
const normalized = headerMatch[1].trim().replace(/^\d+\.\s*/, '').toLowerCase();
|
|
168
|
+
if (allNames.includes(normalized)) {
|
|
150
169
|
capturing = true;
|
|
151
170
|
}
|
|
152
171
|
continue;
|