@jaimevalasek/aioson 1.30.2 → 1.33.1
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/CHANGELOG.md +27 -0
- package/README.md +19 -6
- package/docs/en/5-reference/cli-reference.md +1 -1
- package/docs/pt/5-referencia/comandos-cli.md +1 -1
- package/docs/pt/5-referencia/harness-retro.md +2 -1
- package/package.json +1 -1
- package/src/cli.js +38 -21
- package/src/commands/classify.js +389 -327
- package/src/commands/harness-retro-promote.js +387 -0
- package/src/commands/prototype-check.js +163 -0
- package/src/commands/verify-implementation.js +428 -0
- package/src/commands/workflow-next.js +222 -9
- package/src/constants.js +2 -0
- package/src/lib/retro/retro-render.js +10 -1
- package/src/lib/retro/retro-sources.js +45 -27
- package/src/lib/retro/verification-reports.js +230 -0
- package/src/runtime-store.js +13 -9
- package/src/verification/evidence-bundle.js +251 -0
- package/src/verification/ledger-store.js +221 -0
- package/src/verification/path-policy.js +74 -0
- package/src/verification/policy-engine.js +95 -0
- package/src/verification/prompt-package.js +314 -0
- package/src/verification/redaction.js +77 -0
- package/src/verification/report-parser.js +132 -0
- package/src/verification/report-store.js +97 -0
- package/src/verification/result.js +16 -0
- package/src/verification/runners/index.js +319 -0
- package/src/verification/runtime-telemetry.js +144 -0
- package/src/verification/schema.js +276 -0
- package/src/verification/source-discovery.js +153 -0
- package/template/.aioson/agents/analyst.md +9 -6
- package/template/.aioson/agents/briefing-refiner.md +22 -0
- package/template/.aioson/agents/briefing.md +69 -12
- package/template/.aioson/agents/design-hybrid-forge.md +18 -14
- package/template/.aioson/agents/dev.md +23 -10
- package/template/.aioson/agents/deyvin.md +3 -2
- package/template/.aioson/agents/product.md +15 -11
- package/template/.aioson/agents/qa.md +13 -4
- package/template/.aioson/agents/scope-check.md +19 -5
- package/template/.aioson/agents/sheldon.md +4 -3
- package/template/.aioson/agents/ux-ui.md +3 -2
- package/template/.aioson/docs/feature-expansion-taxonomy.md +31 -3
- package/template/.aioson/docs/prototype-contract.md +81 -0
- package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +25 -3
- package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +5 -3
- package/template/.aioson/skills/process/design-hybrid-forge/references/external-source-ingestion.md +89 -0
- package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +29 -2
- package/template/.aioson/skills/process/prototype-forge/SKILL.md +92 -0
- package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +23 -2
package/src/commands/classify.js
CHANGED
|
@@ -1,327 +1,389 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* aioson classify — deterministic classification scoring (MICRO/SMALL/MEDIUM).
|
|
5
|
-
*
|
|
6
|
-
* Reads prd-{slug}.md or requirements-{slug}.md and counts complexity indicators.
|
|
7
|
-
* Falls back to --interactive mode if insufficient data.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* aioson classify . --feature=checkout
|
|
11
|
-
* aioson classify . --feature=checkout --json
|
|
12
|
-
* aioson classify . --interactive
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const path = require('node:path');
|
|
16
|
-
const readline = require('node:readline');
|
|
17
|
-
const { readFileSafe, contextDir } = require('../preflight-engine');
|
|
18
|
-
|
|
19
|
-
const BAR = '━'.repeat(30);
|
|
20
|
-
|
|
21
|
-
// Scoring thresholds
|
|
22
|
-
// user_types: 1→0, 2→1, 3+→2
|
|
23
|
-
// external_integrations: 0→0, 1-2→1, 3+→2
|
|
24
|
-
// rule_complexity: none→0, some→1, complex→2
|
|
25
|
-
// Score: 0-1=MICRO, 2-3=SMALL, 4-6=MEDIUM
|
|
26
|
-
|
|
27
|
-
function scoreUserTypes(count) {
|
|
28
|
-
if (count >= 3) return 2;
|
|
29
|
-
if (count >= 2) return 1;
|
|
30
|
-
return 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function scoreIntegrations(count) {
|
|
34
|
-
if (count >= 3) return 2;
|
|
35
|
-
if (count >= 1) return 1;
|
|
36
|
-
return 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function scoreComplexity(level) {
|
|
40
|
-
if (level === 'complex') return 2;
|
|
41
|
-
if (level === 'some') return 1;
|
|
42
|
-
return 0;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function scoreToClassification(score) {
|
|
46
|
-
if (score <= 1) return 'MICRO';
|
|
47
|
-
if (score <= 3) return 'SMALL';
|
|
48
|
-
return 'MEDIUM';
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function classificationToPhaseDepth(classification) {
|
|
52
|
-
if (classification === 'MICRO') {
|
|
53
|
-
return {
|
|
54
|
-
specify: 'brief note or inline',
|
|
55
|
-
research: 'not needed',
|
|
56
|
-
requirements: 'optional',
|
|
57
|
-
design: 'skip',
|
|
58
|
-
plan: 'optional',
|
|
59
|
-
execute: 'direct from task description'
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
if (classification === 'SMALL') {
|
|
63
|
-
return {
|
|
64
|
-
specify: 'full PRD',
|
|
65
|
-
research: 'recommended (@sheldon)',
|
|
66
|
-
requirements: 'required (requirements-{slug}.md)',
|
|
67
|
-
design: 'selective (only if new pattern)',
|
|
68
|
-
plan: 'recommended',
|
|
69
|
-
execute: 'from requirements + spec'
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
return {
|
|
73
|
-
specify: 'full PRD + stakeholder review',
|
|
74
|
-
research: 'required (@sheldon)',
|
|
75
|
-
requirements: 'required + conformance YAML',
|
|
76
|
-
design: 'required (@ux-ui + @architect)',
|
|
77
|
-
plan: 'required + @pm backlog',
|
|
78
|
-
execute: 'from approved plan, phased delivery'
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Pattern-based auto-detection from markdown content
|
|
83
|
-
|
|
84
|
-
const USER_TYPE_PATTERNS = [
|
|
85
|
-
/\b(admin|administrator)\b/gi,
|
|
86
|
-
/\b(user|customer|client|buyer|seller|vendor|manager|operator|guest|visitor|member|owner|reviewer|moderator)\b/gi,
|
|
87
|
-
/\bAs an? ([a-z]+)/gi,
|
|
88
|
-
/\brole[s]?\b.*?:\s*([^,\n]+)/gi
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
const INTEGRATION_PATTERNS = [
|
|
92
|
-
/\b(stripe|paypal|braintree|square)\b/gi,
|
|
93
|
-
/\b(sendgrid|mailchimp|ses|postmark|smtp)\b/gi,
|
|
94
|
-
/\b(twilio|vonage|nexmo)\b/gi,
|
|
95
|
-
/\b(s3|cloudinary|gcs|azure blob)\b/gi,
|
|
96
|
-
/\b(oauth|jwt|saml|sso|auth0|firebase auth)\b/gi,
|
|
97
|
-
/\b(redis|memcached|elasticsearch|algolia)\b/gi,
|
|
98
|
-
/\bAPI\s+(integration|endpoint|call)\b/gi,
|
|
99
|
-
/\bthird.party\b/gi,
|
|
100
|
-
/\bwebhook[s]?\b/gi,
|
|
101
|
-
/\bexternal\s+service\b/gi
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
const COMPLEXITY_HIGH_PATTERNS = [
|
|
105
|
-
/\b(multi.step|multi-phase|pipeline|workflow)\b/gi,
|
|
106
|
-
/\b(state machine|finite state|transition)\b/gi,
|
|
107
|
-
/\b(calculation|formula|algorithm|score|pricing engine)\b/gi,
|
|
108
|
-
/\b(complex|intricate|elaborate)\s+(logic|rule|condition)/gi,
|
|
109
|
-
/\b(concurrent|parallel|async|queue)\b/gi,
|
|
110
|
-
/\b(if.+then.+else|conditional|depends on)\b/gi
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
const COMPLEXITY_SOME_PATTERNS = [
|
|
114
|
-
/\b(validation|constraint|rule)\b/gi,
|
|
115
|
-
/\b(permission|role.based|access control)\b/gi,
|
|
116
|
-
/\b(notification|trigger|event)\b/gi
|
|
117
|
-
];
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
function
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson classify — deterministic classification scoring (MICRO/SMALL/MEDIUM).
|
|
5
|
+
*
|
|
6
|
+
* Reads prd-{slug}.md or requirements-{slug}.md and counts complexity indicators.
|
|
7
|
+
* Falls back to --interactive mode if insufficient data.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* aioson classify . --feature=checkout
|
|
11
|
+
* aioson classify . --feature=checkout --json
|
|
12
|
+
* aioson classify . --interactive
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const readline = require('node:readline');
|
|
17
|
+
const { readFileSafe, contextDir } = require('../preflight-engine');
|
|
18
|
+
|
|
19
|
+
const BAR = '━'.repeat(30);
|
|
20
|
+
|
|
21
|
+
// Scoring thresholds
|
|
22
|
+
// user_types: 1→0, 2→1, 3+→2
|
|
23
|
+
// external_integrations: 0→0, 1-2→1, 3+→2
|
|
24
|
+
// rule_complexity: none→0, some→1, complex→2
|
|
25
|
+
// Score: 0-1=MICRO, 2-3=SMALL, 4-6=MEDIUM
|
|
26
|
+
|
|
27
|
+
function scoreUserTypes(count) {
|
|
28
|
+
if (count >= 3) return 2;
|
|
29
|
+
if (count >= 2) return 1;
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function scoreIntegrations(count) {
|
|
34
|
+
if (count >= 3) return 2;
|
|
35
|
+
if (count >= 1) return 1;
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function scoreComplexity(level) {
|
|
40
|
+
if (level === 'complex') return 2;
|
|
41
|
+
if (level === 'some') return 1;
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function scoreToClassification(score) {
|
|
46
|
+
if (score <= 1) return 'MICRO';
|
|
47
|
+
if (score <= 3) return 'SMALL';
|
|
48
|
+
return 'MEDIUM';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function classificationToPhaseDepth(classification) {
|
|
52
|
+
if (classification === 'MICRO') {
|
|
53
|
+
return {
|
|
54
|
+
specify: 'brief note or inline',
|
|
55
|
+
research: 'not needed',
|
|
56
|
+
requirements: 'optional',
|
|
57
|
+
design: 'skip',
|
|
58
|
+
plan: 'optional',
|
|
59
|
+
execute: 'direct from task description'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (classification === 'SMALL') {
|
|
63
|
+
return {
|
|
64
|
+
specify: 'full PRD',
|
|
65
|
+
research: 'recommended (@sheldon)',
|
|
66
|
+
requirements: 'required (requirements-{slug}.md)',
|
|
67
|
+
design: 'selective (only if new pattern)',
|
|
68
|
+
plan: 'recommended',
|
|
69
|
+
execute: 'from requirements + spec'
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
specify: 'full PRD + stakeholder review',
|
|
74
|
+
research: 'required (@sheldon)',
|
|
75
|
+
requirements: 'required + conformance YAML',
|
|
76
|
+
design: 'required (@ux-ui + @architect)',
|
|
77
|
+
plan: 'required + @pm backlog',
|
|
78
|
+
execute: 'from approved plan, phased delivery'
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Pattern-based auto-detection from markdown content
|
|
83
|
+
|
|
84
|
+
const USER_TYPE_PATTERNS = [
|
|
85
|
+
/\b(admin|administrator)\b/gi,
|
|
86
|
+
/\b(user|customer|client|buyer|seller|vendor|manager|operator|guest|visitor|member|owner|reviewer|moderator)\b/gi,
|
|
87
|
+
/\bAs an? ([a-z]+)/gi,
|
|
88
|
+
/\brole[s]?\b.*?:\s*([^,\n]+)/gi
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const INTEGRATION_PATTERNS = [
|
|
92
|
+
/\b(stripe|paypal|braintree|square)\b/gi,
|
|
93
|
+
/\b(sendgrid|mailchimp|ses|postmark|smtp)\b/gi,
|
|
94
|
+
/\b(twilio|vonage|nexmo)\b/gi,
|
|
95
|
+
/\b(s3|cloudinary|gcs|azure blob)\b/gi,
|
|
96
|
+
/\b(oauth|jwt|saml|sso|auth0|firebase auth)\b/gi,
|
|
97
|
+
/\b(redis|memcached|elasticsearch|algolia)\b/gi,
|
|
98
|
+
/\bAPI\s+(integration|endpoint|call)\b/gi,
|
|
99
|
+
/\bthird.party\b/gi,
|
|
100
|
+
/\bwebhook[s]?\b/gi,
|
|
101
|
+
/\bexternal\s+service\b/gi
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const COMPLEXITY_HIGH_PATTERNS = [
|
|
105
|
+
/\b(multi.step|multi-phase|pipeline|workflow)\b/gi,
|
|
106
|
+
/\b(state machine|finite state|transition)\b/gi,
|
|
107
|
+
/\b(calculation|formula|algorithm|score|pricing engine)\b/gi,
|
|
108
|
+
/\b(complex|intricate|elaborate)\s+(logic|rule|condition)/gi,
|
|
109
|
+
/\b(concurrent|parallel|async|queue)\b/gi,
|
|
110
|
+
/\b(if.+then.+else|conditional|depends on)\b/gi
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const COMPLEXITY_SOME_PATTERNS = [
|
|
114
|
+
/\b(validation|constraint|rule)\b/gi,
|
|
115
|
+
/\b(permission|role.based|access control)\b/gi,
|
|
116
|
+
/\b(notification|trigger|event)\b/gi
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
// Fold diacritics so the surface detectors match a localized PRD the same way in
|
|
120
|
+
// any supported language: "cartões"->"cartoes", "gestão"->"gestao",
|
|
121
|
+
// "autenticação"->"autenticacao". Patterns below stay ASCII and run against the
|
|
122
|
+
// folded text, so EN and pt-BR phrasings of the same surface are detected
|
|
123
|
+
// identically. The detector recognizes the surface from the words present in the
|
|
124
|
+
// text — it does NOT depend on the configured interaction_language.
|
|
125
|
+
function foldDiacritics(content) {
|
|
126
|
+
return String(content || '').normalize('NFD').replace(/\p{Diacritic}/gu, '');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Sensitive-surface floor (Gap 3B): a feature touching any of these surfaces is
|
|
130
|
+
// never MICRO. Mirrors the secure-tdd sensitive list in @dev. The floor can only
|
|
131
|
+
// RAISE the tier (MICRO -> SMALL); it never lowers it. Keep patterns tight — a
|
|
132
|
+
// false positive needlessly costs the SMALL chain. Patterns are bilingual
|
|
133
|
+
// (EN + pt-BR) and matched against diacritic-folded text. Tune as the project learns.
|
|
134
|
+
const SENSITIVE_SURFACE_PATTERNS = [
|
|
135
|
+
{ surface: 'money', re: /\b(money|stripe|paypal|braintree|square|payments?|payouts?|refunds?|subscriptions?|billing|invoices?|credit card|pagamentos?|pagar|cobran[cç]as?|faturas?|assinaturas?|reembolsos?|cartao de credito|boleto|pix)\b/i },
|
|
136
|
+
{ surface: 'auth', re: /\b(oauth|jwt|saml|sso|auth0|firebase auth|log[- ]?in|sign[- ]?in|sign[- ]?up|passwords?|authenticat\w*|2fa|mfa|autentica\w*|senhas?|cadastr(o|os|ar))\b/i },
|
|
137
|
+
{ surface: 'authz', re: /\b(authoriz\w*|access control|role[- ]based|rbac|ownership|owner[- ]only|only the owner|autoriza(cao|r)|controle de acesso|apenas o dono|somente o dono)\b/i },
|
|
138
|
+
{ surface: 'uploads', re: /\b(file uploads?|uploads?|attachments?|anexos?|envio de arquivos?|upload de arquivos?)\b/i },
|
|
139
|
+
{ surface: 'external_url', re: /\b(webhooks?|callback urls?|ssrf|user[- ]?supplied urls?|url de retorno|urls? fornecidas?)\b/i },
|
|
140
|
+
{ surface: 'secrets', re: /\b(secrets?|api keys?|credentials?|private key|access tokens?|segredos?|chaves? de api|credenciais|chave privada|tokens? de acesso)\b/i },
|
|
141
|
+
{ surface: 'sensitive_storage', re: /\b(pii|personal data|ssn|sensitive (data|storage|information)|dados (pessoais|sensiveis)|cpf|informac\w* sensivel)\b/i }
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
function detectSensitiveSurfaces(content) {
|
|
145
|
+
const c = foldDiacritics(content);
|
|
146
|
+
const found = [];
|
|
147
|
+
for (const { surface, re } of SENSITIVE_SURFACE_PATTERNS) {
|
|
148
|
+
if (re.test(c)) found.push(surface);
|
|
149
|
+
}
|
|
150
|
+
return found;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Operational-surface floor: a rich operational surface (workspaces, boards +
|
|
154
|
+
// cards, Kanban/CRM pipelines, explicit CRUD/admin management) is never MICRO —
|
|
155
|
+
// it needs management screens, so it must get at least the SMALL chain so
|
|
156
|
+
// @analyst/@architect/the prototype are not skipped. Patterns are bilingual
|
|
157
|
+
// (EN + pt-BR), matched against diacritic-folded text, and kept tight to avoid
|
|
158
|
+
// flooring genuinely simple features. Bare "dashboard"/"quadro" is intentionally
|
|
159
|
+
// NOT a signal (too common); only admin/management surfaces and board+card pairs count.
|
|
160
|
+
function detectRichSurfaces(content) {
|
|
161
|
+
const c = foldDiacritics(content);
|
|
162
|
+
const found = [];
|
|
163
|
+
if (/\b(kanban|trello|scrum board|task board|quadro kanban|quadro de tarefas)\b/i.test(c)) found.push('kanban');
|
|
164
|
+
if (/\bworkspaces?\b/i.test(c) || /\bespacos? de trabalho\b/i.test(c)) found.push('workspace');
|
|
165
|
+
if ((/\bboards?\b/i.test(c) && /\bcards?\b/i.test(c))
|
|
166
|
+
|| (/\bquadros?\b/i.test(c) && /\bcart(ao|oes)\b/i.test(c))) found.push('board_cards');
|
|
167
|
+
if (/\b(crm|sales pipeline|deals? pipeline|leads? pipeline|funil de vendas|pipeline de (vendas|negocios|leads))\b/i.test(c)) found.push('crm_pipeline');
|
|
168
|
+
if (/\bcrud\b/i.test(c)
|
|
169
|
+
|| /\badmin (panel|dashboard|area|console)\b/i.test(c)
|
|
170
|
+
|| /\bmanagement (screen|page|panel|dashboard|interface|surface)\b/i.test(c)
|
|
171
|
+
|| /\barea administrativa\b/i.test(c)
|
|
172
|
+
|| /\bpainel (de )?admin(istracao)?\b/i.test(c)
|
|
173
|
+
|| /\b(painel|tela|pagina|area|console) de (administracao|gestao|gerenciamento)\b/i.test(c)) found.push('crud_admin');
|
|
174
|
+
return [...new Set(found)];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Explicit `sensitive_surfaces:` frontmatter override — additive, can only force
|
|
178
|
+
// the floor when content detection misses. Supports inline (`[a, b]` / `a, b`)
|
|
179
|
+
// and YAML block list forms.
|
|
180
|
+
function parseSurfacesOverride(content, key) {
|
|
181
|
+
const fm = String(content || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
182
|
+
if (!fm) return [];
|
|
183
|
+
const body = fm[1];
|
|
184
|
+
const items = [];
|
|
185
|
+
const inline = body.match(new RegExp(`^${key}:[ \\t]*(.+)$`, 'm'));
|
|
186
|
+
if (inline) {
|
|
187
|
+
inline[1].trim().replace(/^\[|\]$/g, '').split(',').forEach((s) => {
|
|
188
|
+
const v = s.trim().replace(/^["']|["']$/g, '');
|
|
189
|
+
if (v) items.push(v);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const block = body.match(new RegExp(`^${key}:[ \\t]*\\r?\\n((?:[ \\t]*-[ \\t]*.+\\r?\\n?)+)`, 'm'));
|
|
193
|
+
if (block) {
|
|
194
|
+
block[1].split(/\r?\n/).forEach((line) => {
|
|
195
|
+
const m = line.match(/^[ \t]*-[ \t]*(.+)$/);
|
|
196
|
+
if (m) {
|
|
197
|
+
const v = m[1].trim().replace(/^["']|["']$/g, '');
|
|
198
|
+
if (v) items.push(v);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return items;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function applySensitiveFloor(classification) {
|
|
206
|
+
return classification === 'MICRO' ? 'SMALL' : classification;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function analyzeContent(content) {
|
|
210
|
+
// Count unique user types
|
|
211
|
+
const userTypeSet = new Set();
|
|
212
|
+
for (const pattern of USER_TYPE_PATTERNS) {
|
|
213
|
+
let m;
|
|
214
|
+
while ((m = pattern.exec(content)) !== null) {
|
|
215
|
+
userTypeSet.add(m[1] ? m[1].toLowerCase() : m[0].toLowerCase());
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const userTypeCount = Math.min(userTypeSet.size, 5);
|
|
219
|
+
|
|
220
|
+
// Count integrations
|
|
221
|
+
const integrationSet = new Set();
|
|
222
|
+
for (const pattern of INTEGRATION_PATTERNS) {
|
|
223
|
+
let m;
|
|
224
|
+
while ((m = pattern.exec(content)) !== null) {
|
|
225
|
+
integrationSet.add(m[0].toLowerCase());
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const integrationCount = integrationSet.size;
|
|
229
|
+
|
|
230
|
+
// Complexity level
|
|
231
|
+
let complexityLevel = 'none';
|
|
232
|
+
const highMatches = COMPLEXITY_HIGH_PATTERNS.some((p) => p.test(content));
|
|
233
|
+
if (highMatches) {
|
|
234
|
+
complexityLevel = 'complex';
|
|
235
|
+
} else {
|
|
236
|
+
const someMatches = COMPLEXITY_SOME_PATTERNS.some((p) => p.test(content));
|
|
237
|
+
if (someMatches) complexityLevel = 'some';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { userTypeCount, integrationCount, complexityLevel };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function runInteractive(logger) {
|
|
244
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
245
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
logger.log('');
|
|
249
|
+
logger.log('Classification — Interactive Mode');
|
|
250
|
+
logger.log(BAR);
|
|
251
|
+
|
|
252
|
+
const utRaw = await ask('User types (1 / 2 / 3+): ');
|
|
253
|
+
const userTypeCount = parseInt(utRaw) || 1;
|
|
254
|
+
|
|
255
|
+
const intRaw = await ask('External integrations (0 / 1-2 / 3+): ');
|
|
256
|
+
const integrationCount = parseInt(intRaw) || 0;
|
|
257
|
+
|
|
258
|
+
const cxRaw = await ask('Business rule complexity (none / some / complex): ');
|
|
259
|
+
const complexityLevel = ['none', 'some', 'complex'].includes(cxRaw.trim().toLowerCase())
|
|
260
|
+
? cxRaw.trim().toLowerCase()
|
|
261
|
+
: 'none';
|
|
262
|
+
|
|
263
|
+
rl.close();
|
|
264
|
+
return { userTypeCount, integrationCount, complexityLevel };
|
|
265
|
+
} catch {
|
|
266
|
+
rl.close();
|
|
267
|
+
return { userTypeCount: 1, integrationCount: 0, complexityLevel: 'none' };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function runClassify({ args, options = {}, logger }) {
|
|
272
|
+
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
273
|
+
const slug = options.feature ? String(options.feature) : null;
|
|
274
|
+
const interactive = Boolean(options.interactive);
|
|
275
|
+
|
|
276
|
+
let userTypeCount, integrationCount, complexityLevel;
|
|
277
|
+
let sourceFile = null;
|
|
278
|
+
let content = null;
|
|
279
|
+
|
|
280
|
+
if (interactive) {
|
|
281
|
+
({ userTypeCount, integrationCount, complexityLevel } = await runInteractive(logger));
|
|
282
|
+
} else {
|
|
283
|
+
// Auto-detect from PRD or requirements file
|
|
284
|
+
const dir = contextDir(targetDir);
|
|
285
|
+
const candidates = slug
|
|
286
|
+
? [
|
|
287
|
+
path.join(dir, `requirements-${slug}.md`),
|
|
288
|
+
path.join(dir, `prd-${slug}.md`),
|
|
289
|
+
path.join(dir, `sheldon-enrichment-${slug}.md`)
|
|
290
|
+
]
|
|
291
|
+
: [path.join(dir, 'requirements.md'), path.join(dir, 'prd.md')];
|
|
292
|
+
|
|
293
|
+
for (const candidate of candidates) {
|
|
294
|
+
content = await readFileSafe(candidate);
|
|
295
|
+
if (content) { sourceFile = path.relative(targetDir, candidate); break; }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!content) {
|
|
299
|
+
if (options.json) return { ok: false, reason: 'no_source', message: 'No prd or requirements file found. Use --interactive or --feature=<slug>.' };
|
|
300
|
+
logger.log('No source file found. Use --interactive or provide --feature=<slug>.');
|
|
301
|
+
return { ok: false };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
({ userTypeCount, integrationCount, complexityLevel } = analyzeContent(content));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const utScore = scoreUserTypes(userTypeCount);
|
|
308
|
+
const intScore = scoreIntegrations(integrationCount);
|
|
309
|
+
const cxScore = scoreComplexity(complexityLevel);
|
|
310
|
+
const totalScore = utScore + intScore + cxScore;
|
|
311
|
+
let classification = scoreToClassification(totalScore);
|
|
312
|
+
|
|
313
|
+
// Gap 3B — sensitive-surface floor (deterministic; raises MICRO -> SMALL only).
|
|
314
|
+
const detectedSurfaces = content ? detectSensitiveSurfaces(content) : [];
|
|
315
|
+
const declaredSurfaces = content ? parseSurfacesOverride(content, 'sensitive_surfaces') : [];
|
|
316
|
+
const sensitiveSurfaces = [...new Set([...detectedSurfaces, ...declaredSurfaces])];
|
|
317
|
+
let floored = false;
|
|
318
|
+
if (sensitiveSurfaces.length > 0) {
|
|
319
|
+
const scored = classification;
|
|
320
|
+
classification = applySensitiveFloor(classification);
|
|
321
|
+
floored = classification !== scored;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Operational-surface floor (deterministic; raises MICRO -> SMALL only). A rich
|
|
325
|
+
// operational surface needs management screens, so a Trello/Kanban/CRM/workspace
|
|
326
|
+
// feature can't take the MICRO shortcut that skips @analyst/@architect/prototype.
|
|
327
|
+
const detectedOps = content ? detectRichSurfaces(content) : [];
|
|
328
|
+
const declaredOps = content ? parseSurfacesOverride(content, 'operational_surfaces') : [];
|
|
329
|
+
const operationalSurfaces = [...new Set([...detectedOps, ...declaredOps])];
|
|
330
|
+
if (operationalSurfaces.length > 0) {
|
|
331
|
+
const scored = classification;
|
|
332
|
+
classification = applySensitiveFloor(classification);
|
|
333
|
+
floored = floored || classification !== scored;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const phaseDepth = classificationToPhaseDepth(classification);
|
|
337
|
+
|
|
338
|
+
// A rich operational surface is exactly the case a clickable prototype is meant
|
|
339
|
+
// to de-risk (management screens + interactions before the PRD). Emit the
|
|
340
|
+
// recommendation from the deterministic tool so it does not rely on agent prose
|
|
341
|
+
// alone — @product/@briefing-refiner key off this flag.
|
|
342
|
+
const recommendPrototype = operationalSurfaces.length > 0;
|
|
343
|
+
|
|
344
|
+
const result = {
|
|
345
|
+
ok: true,
|
|
346
|
+
feature_slug: slug,
|
|
347
|
+
source_file: sourceFile,
|
|
348
|
+
inputs: { user_types: userTypeCount, external_integrations: integrationCount, rule_complexity: complexityLevel },
|
|
349
|
+
scores: { user_types: utScore, integrations: intScore, complexity: cxScore, total: totalScore },
|
|
350
|
+
classification,
|
|
351
|
+
sensitive_surfaces: sensitiveSurfaces,
|
|
352
|
+
operational_surfaces: operationalSurfaces,
|
|
353
|
+
floored,
|
|
354
|
+
recommend_prototype: recommendPrototype,
|
|
355
|
+
phase_depth: phaseDepth
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
if (options.json) return result;
|
|
359
|
+
|
|
360
|
+
const header = slug ? `Classification — ${slug}` : 'Classification';
|
|
361
|
+
logger.log('');
|
|
362
|
+
logger.log(header);
|
|
363
|
+
logger.log(BAR);
|
|
364
|
+
if (sourceFile) logger.log(`Source: ${sourceFile}`);
|
|
365
|
+
logger.log(`User types: ${userTypeCount} → +${utScore}`);
|
|
366
|
+
logger.log(`External integrations: ${integrationCount} → +${intScore}`);
|
|
367
|
+
logger.log(`Business rule complexity: ${complexityLevel} → +${cxScore}`);
|
|
368
|
+
logger.log(BAR);
|
|
369
|
+
logger.log(`Score: ${totalScore} → ${classification}`);
|
|
370
|
+
if (sensitiveSurfaces.length > 0) {
|
|
371
|
+
logger.log(`Sensitive surfaces: ${sensitiveSurfaces.join(', ')}${floored ? ' → floored to SMALL' : ''}`);
|
|
372
|
+
}
|
|
373
|
+
if (operationalSurfaces.length > 0) {
|
|
374
|
+
logger.log(`Operational surfaces: ${operationalSurfaces.join(', ')}${floored ? ' → floored to at least SMALL' : ''}`);
|
|
375
|
+
}
|
|
376
|
+
if (recommendPrototype) {
|
|
377
|
+
logger.log('Recommendation: generate a clickable prototype in @briefing-refiner before @product (surfaces management screens + interactions early).');
|
|
378
|
+
}
|
|
379
|
+
logger.log('');
|
|
380
|
+
logger.log('Phase depth:');
|
|
381
|
+
for (const [phase, desc] of Object.entries(phaseDepth)) {
|
|
382
|
+
logger.log(` ${phase.padEnd(14)}: ${desc}`);
|
|
383
|
+
}
|
|
384
|
+
logger.log('');
|
|
385
|
+
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
module.exports = { runClassify };
|