@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +19 -6
  3. package/docs/en/5-reference/cli-reference.md +1 -1
  4. package/docs/pt/5-referencia/comandos-cli.md +1 -1
  5. package/docs/pt/5-referencia/harness-retro.md +2 -1
  6. package/package.json +1 -1
  7. package/src/cli.js +38 -21
  8. package/src/commands/classify.js +389 -327
  9. package/src/commands/harness-retro-promote.js +387 -0
  10. package/src/commands/prototype-check.js +163 -0
  11. package/src/commands/verify-implementation.js +428 -0
  12. package/src/commands/workflow-next.js +222 -9
  13. package/src/constants.js +2 -0
  14. package/src/lib/retro/retro-render.js +10 -1
  15. package/src/lib/retro/retro-sources.js +45 -27
  16. package/src/lib/retro/verification-reports.js +230 -0
  17. package/src/runtime-store.js +13 -9
  18. package/src/verification/evidence-bundle.js +251 -0
  19. package/src/verification/ledger-store.js +221 -0
  20. package/src/verification/path-policy.js +74 -0
  21. package/src/verification/policy-engine.js +95 -0
  22. package/src/verification/prompt-package.js +314 -0
  23. package/src/verification/redaction.js +77 -0
  24. package/src/verification/report-parser.js +132 -0
  25. package/src/verification/report-store.js +97 -0
  26. package/src/verification/result.js +16 -0
  27. package/src/verification/runners/index.js +319 -0
  28. package/src/verification/runtime-telemetry.js +144 -0
  29. package/src/verification/schema.js +276 -0
  30. package/src/verification/source-discovery.js +153 -0
  31. package/template/.aioson/agents/analyst.md +9 -6
  32. package/template/.aioson/agents/briefing-refiner.md +22 -0
  33. package/template/.aioson/agents/briefing.md +69 -12
  34. package/template/.aioson/agents/design-hybrid-forge.md +18 -14
  35. package/template/.aioson/agents/dev.md +23 -10
  36. package/template/.aioson/agents/deyvin.md +3 -2
  37. package/template/.aioson/agents/product.md +15 -11
  38. package/template/.aioson/agents/qa.md +13 -4
  39. package/template/.aioson/agents/scope-check.md +19 -5
  40. package/template/.aioson/agents/sheldon.md +4 -3
  41. package/template/.aioson/agents/ux-ui.md +3 -2
  42. package/template/.aioson/docs/feature-expansion-taxonomy.md +31 -3
  43. package/template/.aioson/docs/prototype-contract.md +81 -0
  44. package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +25 -3
  45. package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +5 -3
  46. package/template/.aioson/skills/process/design-hybrid-forge/references/external-source-ingestion.md +89 -0
  47. package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +29 -2
  48. package/template/.aioson/skills/process/prototype-forge/SKILL.md +92 -0
  49. package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +23 -2
@@ -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
- // Sensitive-surface floor (Gap 3B): a feature touching any of these surfaces is
120
- // never MICRO. Mirrors the secure-tdd sensitive list in @dev. The floor can only
121
- // RAISE the tier (MICRO -> SMALL); it never lowers it. Keep patterns tight — a
122
- // false positive needlessly costs the SMALL chain. Tune as the project learns.
123
- const SENSITIVE_SURFACE_PATTERNS = [
124
- { surface: 'money', re: /\b(money|stripe|paypal|braintree|square|payments?|payouts?|refunds?|subscriptions?|billing|invoices?|credit card)\b/i },
125
- { surface: 'auth', re: /\b(oauth|jwt|saml|sso|auth0|firebase auth|log[- ]?in|sign[- ]?in|sign[- ]?up|passwords?|authenticat\w*|2fa|mfa)\b/i },
126
- { surface: 'authz', re: /\b(authoriz\w*|access control|role[- ]based|rbac|ownership|owner[- ]only|only the owner)\b/i },
127
- { surface: 'uploads', re: /\b(file uploads?|uploads?|attachments?)\b/i },
128
- { surface: 'external_url', re: /\b(webhooks?|callback urls?|ssrf|user[- ]?supplied urls?)\b/i },
129
- { surface: 'secrets', re: /\b(secrets?|api keys?|credentials?|private key|access tokens?)\b/i },
130
- { surface: 'sensitive_storage', re: /\b(pii|personal data|ssn|sensitive (data|storage|information))\b/i }
131
- ];
132
-
133
- function detectSensitiveSurfaces(content) {
134
- const found = [];
135
- for (const { surface, re } of SENSITIVE_SURFACE_PATTERNS) {
136
- if (re.test(content)) found.push(surface);
137
- }
138
- return found;
139
- }
140
-
141
- // Explicit `sensitive_surfaces:` frontmatter override additive, can only force
142
- // the floor when content detection misses. Supports inline (`[a, b]` / `a, b`)
143
- // and YAML block list forms.
144
- function parseSensitiveSurfacesOverride(content) {
145
- const fm = String(content || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
146
- if (!fm) return [];
147
- const body = fm[1];
148
- const items = [];
149
- const inline = body.match(/^sensitive_surfaces:[ \t]*(.+)$/m);
150
- if (inline) {
151
- inline[1].trim().replace(/^\[|\]$/g, '').split(',').forEach((s) => {
152
- const v = s.trim().replace(/^["']|["']$/g, '');
153
- if (v) items.push(v);
154
- });
155
- }
156
- const block = body.match(/^sensitive_surfaces:[ \t]*\r?\n((?:[ \t]*-[ \t]*.+\r?\n?)+)/m);
157
- if (block) {
158
- block[1].split(/\r?\n/).forEach((line) => {
159
- const m = line.match(/^[ \t]*-[ \t]*(.+)$/);
160
- if (m) {
161
- const v = m[1].trim().replace(/^["']|["']$/g, '');
162
- if (v) items.push(v);
163
- }
164
- });
165
- }
166
- return items;
167
- }
168
-
169
- function applySensitiveFloor(classification) {
170
- return classification === 'MICRO' ? 'SMALL' : classification;
171
- }
172
-
173
- function analyzeContent(content) {
174
- // Count unique user types
175
- const userTypeSet = new Set();
176
- for (const pattern of USER_TYPE_PATTERNS) {
177
- let m;
178
- while ((m = pattern.exec(content)) !== null) {
179
- userTypeSet.add(m[1] ? m[1].toLowerCase() : m[0].toLowerCase());
180
- }
181
- }
182
- const userTypeCount = Math.min(userTypeSet.size, 5);
183
-
184
- // Count integrations
185
- const integrationSet = new Set();
186
- for (const pattern of INTEGRATION_PATTERNS) {
187
- let m;
188
- while ((m = pattern.exec(content)) !== null) {
189
- integrationSet.add(m[0].toLowerCase());
190
- }
191
- }
192
- const integrationCount = integrationSet.size;
193
-
194
- // Complexity level
195
- let complexityLevel = 'none';
196
- const highMatches = COMPLEXITY_HIGH_PATTERNS.some((p) => p.test(content));
197
- if (highMatches) {
198
- complexityLevel = 'complex';
199
- } else {
200
- const someMatches = COMPLEXITY_SOME_PATTERNS.some((p) => p.test(content));
201
- if (someMatches) complexityLevel = 'some';
202
- }
203
-
204
- return { userTypeCount, integrationCount, complexityLevel };
205
- }
206
-
207
- async function runInteractive(logger) {
208
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
209
- const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
210
-
211
- try {
212
- logger.log('');
213
- logger.log('Classification — Interactive Mode');
214
- logger.log(BAR);
215
-
216
- const utRaw = await ask('User types (1 / 2 / 3+): ');
217
- const userTypeCount = parseInt(utRaw) || 1;
218
-
219
- const intRaw = await ask('External integrations (0 / 1-2 / 3+): ');
220
- const integrationCount = parseInt(intRaw) || 0;
221
-
222
- const cxRaw = await ask('Business rule complexity (none / some / complex): ');
223
- const complexityLevel = ['none', 'some', 'complex'].includes(cxRaw.trim().toLowerCase())
224
- ? cxRaw.trim().toLowerCase()
225
- : 'none';
226
-
227
- rl.close();
228
- return { userTypeCount, integrationCount, complexityLevel };
229
- } catch {
230
- rl.close();
231
- return { userTypeCount: 1, integrationCount: 0, complexityLevel: 'none' };
232
- }
233
- }
234
-
235
- async function runClassify({ args, options = {}, logger }) {
236
- const targetDir = path.resolve(process.cwd(), args[0] || '.');
237
- const slug = options.feature ? String(options.feature) : null;
238
- const interactive = Boolean(options.interactive);
239
-
240
- let userTypeCount, integrationCount, complexityLevel;
241
- let sourceFile = null;
242
- let content = null;
243
-
244
- if (interactive) {
245
- ({ userTypeCount, integrationCount, complexityLevel } = await runInteractive(logger));
246
- } else {
247
- // Auto-detect from PRD or requirements file
248
- const dir = contextDir(targetDir);
249
- const candidates = slug
250
- ? [
251
- path.join(dir, `requirements-${slug}.md`),
252
- path.join(dir, `prd-${slug}.md`),
253
- path.join(dir, `sheldon-enrichment-${slug}.md`)
254
- ]
255
- : [path.join(dir, 'requirements.md'), path.join(dir, 'prd.md')];
256
-
257
- for (const candidate of candidates) {
258
- content = await readFileSafe(candidate);
259
- if (content) { sourceFile = path.relative(targetDir, candidate); break; }
260
- }
261
-
262
- if (!content) {
263
- if (options.json) return { ok: false, reason: 'no_source', message: 'No prd or requirements file found. Use --interactive or --feature=<slug>.' };
264
- logger.log('No source file found. Use --interactive or provide --feature=<slug>.');
265
- return { ok: false };
266
- }
267
-
268
- ({ userTypeCount, integrationCount, complexityLevel } = analyzeContent(content));
269
- }
270
-
271
- const utScore = scoreUserTypes(userTypeCount);
272
- const intScore = scoreIntegrations(integrationCount);
273
- const cxScore = scoreComplexity(complexityLevel);
274
- const totalScore = utScore + intScore + cxScore;
275
- let classification = scoreToClassification(totalScore);
276
-
277
- // Gap 3B — sensitive-surface floor (deterministic; raises MICRO -> SMALL only).
278
- const detectedSurfaces = content ? detectSensitiveSurfaces(content) : [];
279
- const declaredSurfaces = content ? parseSensitiveSurfacesOverride(content) : [];
280
- const sensitiveSurfaces = [...new Set([...detectedSurfaces, ...declaredSurfaces])];
281
- let floored = false;
282
- if (sensitiveSurfaces.length > 0) {
283
- const scored = classification;
284
- classification = applySensitiveFloor(classification);
285
- floored = classification !== scored;
286
- }
287
-
288
- const phaseDepth = classificationToPhaseDepth(classification);
289
-
290
- const result = {
291
- ok: true,
292
- feature_slug: slug,
293
- source_file: sourceFile,
294
- inputs: { user_types: userTypeCount, external_integrations: integrationCount, rule_complexity: complexityLevel },
295
- scores: { user_types: utScore, integrations: intScore, complexity: cxScore, total: totalScore },
296
- classification,
297
- sensitive_surfaces: sensitiveSurfaces,
298
- floored,
299
- phase_depth: phaseDepth
300
- };
301
-
302
- if (options.json) return result;
303
-
304
- const header = slug ? `Classification — ${slug}` : 'Classification';
305
- logger.log('');
306
- logger.log(header);
307
- logger.log(BAR);
308
- if (sourceFile) logger.log(`Source: ${sourceFile}`);
309
- logger.log(`User types: ${userTypeCount} → +${utScore}`);
310
- logger.log(`External integrations: ${integrationCount} → +${intScore}`);
311
- logger.log(`Business rule complexity: ${complexityLevel} → +${cxScore}`);
312
- logger.log(BAR);
313
- logger.log(`Score: ${totalScore} ${classification}`);
314
- if (sensitiveSurfaces.length > 0) {
315
- logger.log(`Sensitive surfaces: ${sensitiveSurfaces.join(', ')}${floored ? ' → floored to SMALL' : ''}`);
316
- }
317
- logger.log('');
318
- logger.log('Phase depth:');
319
- for (const [phase, desc] of Object.entries(phaseDepth)) {
320
- logger.log(` ${phase.padEnd(14)}: ${desc}`);
321
- }
322
- logger.log('');
323
-
324
- return result;
325
- }
326
-
327
- module.exports = { runClassify };
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 };