@jaimevalasek/aioson 1.29.1 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +7 -5
  3. package/docs/en/5-reference/cli-reference.md +40 -10
  4. package/docs/pt/4-agentes/pm.md +1 -1
  5. package/docs/pt/5-referencia/autopilot-handoff.md +4 -4
  6. package/docs/pt/5-referencia/comandos-cli.md +5 -3
  7. package/docs/pt/5-referencia/fluxo-artefatos.md +1 -1
  8. package/docs/pt/5-referencia/memoria-e-contexto.md +2 -2
  9. package/docs/pt/_arquivo/monitor-de-contexto.md +2 -2
  10. package/package.json +4 -2
  11. package/src/cli.js +67 -24
  12. package/src/commands/ac-test-audit.js +45 -0
  13. package/src/commands/artifact-validate.js +62 -50
  14. package/src/commands/classify.js +73 -2
  15. package/src/commands/context-brief.js +59 -0
  16. package/src/commands/context-guard.js +88 -0
  17. package/src/commands/context-monitor.js +1 -1
  18. package/src/commands/context-search.js +101 -52
  19. package/src/commands/context-select.js +11 -2
  20. package/src/commands/feature-archive.js +21 -12
  21. package/src/commands/feature-current.js +82 -0
  22. package/src/commands/gate-check.js +32 -15
  23. package/src/commands/harness-check.js +17 -1
  24. package/src/commands/hooks-install.js +169 -26
  25. package/src/commands/hygiene-scan.js +423 -0
  26. package/src/commands/rules-lint.js +11 -3
  27. package/src/commands/sdd-benchmark.js +134 -0
  28. package/src/commands/spec-analyze.js +6 -4
  29. package/src/commands/store-system.js +329 -49
  30. package/src/constants.js +8 -3
  31. package/src/context-brief.js +585 -0
  32. package/src/context-guard.js +209 -0
  33. package/src/context-search.js +796 -96
  34. package/src/context-selector.js +802 -444
  35. package/src/handoff-contract.js +14 -6
  36. package/src/harness/contract-schema.js +1 -1
  37. package/src/i18n/messages/en.js +12 -5
  38. package/src/i18n/messages/es.js +11 -4
  39. package/src/i18n/messages/fr.js +11 -4
  40. package/src/i18n/messages/pt-BR.js +12 -5
  41. package/src/lib/ac-test-audit.js +194 -0
  42. package/src/preflight-engine.js +10 -6
  43. package/src/squad/state-manager.js +1 -1
  44. package/template/.aioson/agents/analyst.md +41 -17
  45. package/template/.aioson/agents/architect.md +4 -2
  46. package/template/.aioson/agents/briefing-refiner.md +15 -2
  47. package/template/.aioson/agents/briefing.md +12 -8
  48. package/template/.aioson/agents/committer.md +1 -1
  49. package/template/.aioson/agents/copywriter.md +20 -9
  50. package/template/.aioson/agents/design-hybrid-forge.md +9 -5
  51. package/template/.aioson/agents/dev.md +22 -25
  52. package/template/.aioson/agents/deyvin.md +126 -124
  53. package/template/.aioson/agents/discover.md +3 -1
  54. package/template/.aioson/agents/discovery-design-doc.md +11 -2
  55. package/template/.aioson/agents/forge-run.md +3 -0
  56. package/template/.aioson/agents/genome.md +9 -5
  57. package/template/.aioson/agents/neo.md +30 -24
  58. package/template/.aioson/agents/orache.md +10 -6
  59. package/template/.aioson/agents/orchestrator.md +4 -2
  60. package/template/.aioson/agents/pentester.md +22 -12
  61. package/template/.aioson/agents/pm.md +5 -3
  62. package/template/.aioson/agents/product.md +25 -18
  63. package/template/.aioson/agents/profiler-enricher.md +10 -6
  64. package/template/.aioson/agents/profiler-forge.md +10 -6
  65. package/template/.aioson/agents/profiler-researcher.md +10 -6
  66. package/template/.aioson/agents/qa.md +21 -19
  67. package/template/.aioson/agents/scope-check.md +9 -3
  68. package/template/.aioson/agents/sheldon.md +22 -8
  69. package/template/.aioson/agents/site-forge.md +2 -0
  70. package/template/.aioson/agents/squad.md +4 -2
  71. package/template/.aioson/agents/tester.md +19 -15
  72. package/template/.aioson/agents/ux-ui.md +16 -8
  73. package/template/.aioson/config.md +4 -3
  74. package/template/.aioson/design-docs/agent-loading-contract.md +3 -3
  75. package/template/.aioson/docs/autopilot-handoff.md +3 -3
  76. package/template/.aioson/docs/dev/simple-plan-lane.md +73 -27
  77. package/template/.aioson/docs/dev/stack-conventions.md +1 -1
  78. package/template/.aioson/docs/deyvin/continuity-recovery.md +1 -1
  79. package/template/.aioson/docs/deyvin/runtime-handoffs.md +3 -3
  80. package/template/.aioson/docs/feature-expansion-taxonomy.md +53 -0
  81. package/template/.aioson/docs/handoff-persistence.md +14 -12
  82. package/template/.aioson/docs/product/conversation-playbook.md +1 -1
  83. package/template/.aioson/docs/sheldon/enrichment-paths.md +44 -1
  84. package/template/.aioson/docs/sheldon/harness-contract.md +23 -21
  85. package/template/.aioson/docs/tester/coverage-quality.md +1 -1
  86. package/template/.aioson/docs/ux-ui/design-execution.md +9 -7
  87. package/template/.aioson/rules/README.md +35 -17
  88. package/template/.aioson/rules/agent-structural-contract.md +165 -160
  89. package/template/.aioson/rules/aioson-context-boundary.md +5 -4
  90. package/template/.aioson/rules/canonical-path-contract.md +5 -4
  91. package/template/.aioson/rules/data-format-convention.md +5 -4
  92. package/template/.aioson/rules/disk-first-artifacts.md +2 -2
  93. package/template/.aioson/rules/implementation-structure-and-data-access.md +50 -0
  94. package/template/.aioson/rules/security-baseline.md +4 -3
  95. package/template/.aioson/rules/simple-plan-lane.md +18 -6
  96. package/template/.aioson/rules/source-code-language-convention.md +34 -0
  97. package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +24 -23
  98. package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -0
  99. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +2 -2
  100. package/template/.aioson/skills/process/aioson-spec-driven/references/qa.md +1 -1
  101. package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +72 -0
  102. package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +74 -0
  103. package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +67 -0
  104. package/template/.aioson/skills/static/context-budget-guide.md +1 -1
  105. package/template/.aioson/skills/static/multi-agent-patterns.md +5 -4
  106. package/template/AGENTS.md +36 -19
  107. package/template/CLAUDE.md +9 -5
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('node:fs/promises');
4
4
  const path = require('node:path');
5
+ const ignore = require('ignore');
5
6
  const { exists, ensureDir } = require('../utils');
6
7
  const { readConfig } = require('./config');
7
8
  const { readWorkspace, findProjectRoot } = require('./workspace');
@@ -12,7 +13,55 @@ function getTerser() {
12
13
  return _terser;
13
14
  }
14
15
 
16
+ let _obfuscator = null;
17
+ function getObfuscator() {
18
+ if (!_obfuscator) _obfuscator = require('javascript-obfuscator');
19
+ return _obfuscator;
20
+ }
21
+
22
+ // Ofusca JS compilado no publish --build (minify + string-array encoding +
23
+ // mangling). Funcionalidade do framework — o app não configura nada. Conservador
24
+ // (renameGlobals/controlFlowFlattening/selfDefending OFF) pra não quebrar runtime
25
+ // Node (require/exports, prisma) nem bundles de frontend. Falha num arquivo →
26
+ // devolve o compilado original (não derruba o publish).
27
+ // Detecta JS já minificado (bundle de frontend tipo vite/webpack): linhas muito
28
+ // longas e poucas quebras. Não vale re-ofuscar — incha o pacote, pode quebrar o
29
+ // React e o ganho é baixo (já está minificado). O alvo de valor é o backend (tsc,
30
+ // código legível), esse sim é ofuscado.
31
+ function looksMinified(code) {
32
+ const newlines = (code.match(/\n/g) || []).length;
33
+ const avgLineLen = code.length / (newlines + 1);
34
+ return code.length > 30000 && avgLineLen > 200;
35
+ }
36
+
37
+ function obfuscateJs(code) {
38
+ if (looksMinified(code)) return code; // já minificado → mantém como está
39
+ try {
40
+ return getObfuscator()
41
+ .obfuscate(code, {
42
+ compact: true,
43
+ controlFlowFlattening: false,
44
+ deadCodeInjection: false,
45
+ stringArray: true,
46
+ stringArrayEncoding: ['base64'],
47
+ stringArrayThreshold: 0.75,
48
+ identifierNamesGenerator: 'hexadecimal',
49
+ renameGlobals: false,
50
+ selfDefending: false,
51
+ debugProtection: false,
52
+ disableConsoleOutput: false,
53
+ sourceMap: false,
54
+ })
55
+ .getObfuscatedCode();
56
+ } catch {
57
+ return code;
58
+ }
59
+ }
60
+
15
61
  async function createZipBuffer(files) {
62
+ // archiver fica fixado em ^7 (CJS, API chamável `archiver('zip', opts)`). A v8
63
+ // virou ESM e trocou a API por classes nomeadas (sem função default) — o que
64
+ // quebrava com "archiver is not a function" no Node 23. Ver package.json.
16
65
  const archiver = require('archiver');
17
66
  const { PassThrough } = require('stream');
18
67
  return new Promise((resolve, reject) => {
@@ -46,6 +95,7 @@ const SYSTEM_ALLOWED_EXTS = new Set([
46
95
  '.svg', '.ico',
47
96
  '.md', '.txt',
48
97
  '.sql',
98
+ '.prisma',
49
99
  '.env', '.env.example', '.env.template',
50
100
  '.yaml', '.yml',
51
101
  '.toml',
@@ -68,21 +118,80 @@ const SKIP_DIRS = new Set([
68
118
  'node_modules', '.git', 'dist', 'build', '.turbo', '.next',
69
119
  '.cache', 'coverage', '.nyc_output', 'out',
70
120
  // AIOSON tooling — não faz parte do código-fonte do sistema
71
- '.aioson', '.claude', '.codex', 'researchs',
121
+ '.aioson', '.claude', '.codex', 'researchs',
72
122
  ]);
73
123
 
74
124
  const SKIP_DIRS_BUILD = new Set([
75
125
  'node_modules', '.git', '.turbo', '.next',
76
126
  '.cache', 'coverage', '.nyc_output',
77
127
  'src', 'dashboard/src',
78
- '.aioson', '.claude', '.codex', 'researchs',
128
+ '.aioson', '.claude', '.codex', 'researchs',
79
129
  ]);
80
130
 
81
131
  const SKIP_FILES = new Set([
82
132
  'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
83
133
  'bun.lockb',
134
+ // Arquivo de credenciais LLM local (convenção AIOSON) — NUNCA publicar, mesmo
135
+ // que tenha sido commitado por engano. Defense-in-depth: o filtro de
136
+ // .gitignore abaixo também pega, mas isto cobre apps sem .gitignore.
137
+ 'aioson-models.json',
84
138
  ]);
85
139
 
140
+ // `.aioson` é tooling/dev e fica de fora do pacote (SKIP_DIRS), MAS algumas
141
+ // coisas dali os squads leem EM RUNTIME — sem elas o app quebra (ex.:
142
+ // SQUAD_MANIFEST_INVALID procurando `.aioson/squads/<slug>/squad.manifest.json`).
143
+ //
144
+ // Modelo: `.aioson/squads` é SEMPRE incluído (obrigatório). O resto é OPT-IN —
145
+ // o dev declara só o que o squad realmente precisa num `.aioson/build-options.json`
146
+ // (não viaja peso à toa). Cada entrada é um caminho relativo a `.aioson/` e pode
147
+ // ser pasta (`docs`), subpasta (`skills/skill-x`) ou arquivo (`docs/guia.md`).
148
+ // { "include": ["docs", "skills/atendimento", "rules/foo.md"] }
149
+ const AIOSON_MANDATORY_INCLUDES = ['squads'];
150
+
151
+ /** Resolve o que incluir do `.aioson` de um app: `squads` (sempre) + o que o
152
+ * `build-options.json` declarar. Normaliza e descarta entradas inseguras. */
153
+ async function readAiosonIncludes(aiosonDir) {
154
+ const includes = new Set(AIOSON_MANDATORY_INCLUDES);
155
+ try {
156
+ const optsPath = path.join(aiosonDir, 'build-options.json');
157
+ if (await exists(optsPath)) {
158
+ const opts = JSON.parse(await fs.readFile(optsPath, 'utf8'));
159
+ const list = Array.isArray(opts.include) ? opts.include : [];
160
+ for (let entry of list) {
161
+ if (typeof entry !== 'string') continue;
162
+ entry = entry.replace(/^\.aioson[\\/]/i, '').replace(/^[\\/]+/, '').replace(/[\\/]+$/, '').trim();
163
+ if (!entry || entry.split(/[\\/]/).includes('..')) continue; // anti path-traversal
164
+ includes.add(entry);
165
+ }
166
+ }
167
+ } catch { /* build-options.json inválido → só os obrigatórios */ }
168
+ return [...includes];
169
+ }
170
+
171
+ // No modo --build, estas pastas são a SAÍDA do build (compilado/minificado) e
172
+ // DEVEM viajar no pacote — mesmo estando no .gitignore (build output costuma ser
173
+ // gitignored). Sem isso, o filtro de .gitignore mataria o `dist/` e o app
174
+ // instalado não teria o que rodar.
175
+ const BUILD_OUTPUT_DIRS = new Set(['dist', 'build', 'out', '.next']);
176
+
177
+ // Testes / mocks NUNCA vão no pacote — são peso morto em runtime (e ainda
178
+ // inflavam o pacote ao serem ofuscados). O check é INCONDICIONAL: pega também os
179
+ // testes COMPILADOS dentro do `dist/` (que viajam mesmo com `src/` excluído).
180
+ const TEST_DIRS = new Set(['__tests__', '__mocks__', '__snapshots__']);
181
+ const TEST_FILE_RE = /\.(test|spec)\.[cm]?[jt]sx?$/i;
182
+
183
+ // Config de runtime que PRECISA viajar mesmo no --build (mesmo sendo .ts, que
184
+ // normalmente é excluído): o `vite preview` (frontend em produção) lê o
185
+ // `vite.config.*` pra porta + proxy do /api. Sem ele, instalação limpa quebra o
186
+ // frontend. NÃO é ofuscado (é config lida pelo vite, não lógica a proteger).
187
+ const RUNTIME_CONFIG_RE = /^vite\.config\.[cm]?[jt]s$/i;
188
+
189
+ // Dentro das pastas de runtime do `.aioson`, os arquivos são majoritariamente
190
+ // markdown (definições de agentes/skills, docs) além de json/yaml — então
191
+ // ampliamos as extensões permitidas pra esse subconjunto, senão os `.md` seriam
192
+ // filtrados e o squad subiria sem os agentes.
193
+ const AIOSON_RUNTIME_EXTS = new Set(['.md', '.mdx', '.txt']);
194
+
86
195
  const MAX_FILE_BYTES = 512 * 1024; // 512 KB per file (source)
87
196
  const MAX_FILE_BYTES_BUILD = 2 * 1024 * 1024; // 2 MB per file (compiled bundles)
88
197
  const MAX_PACKAGE_BYTES = 20 * 1024 * 1024; // 20 MB total
@@ -167,62 +276,115 @@ async function collectSystemFiles(dir, { buildMode = false } = {}) {
167
276
  const skipDirs = buildMode ? SKIP_DIRS_BUILD : SKIP_DIRS;
168
277
  const allowedExts = buildMode ? SYSTEM_BUILD_ALLOWED_EXTS : SYSTEM_ALLOWED_EXTS;
169
278
 
170
- async function walk(current, rel) {
279
+ // Respeita o .gitignore do app: arquivos/pastas locais ou gerados em runtime
280
+ // NÃO devem viajar no pacote. Sem isso vazavam coisas como `aioson-models.json`
281
+ // (chave LLM do dev!), `.env` e `atendimento-config.json` (config por-instalação
282
+ // que gateia o onboarding). NÃO se aplica às pastas de runtime do `.aioson`
283
+ // (forceInclude) — essas viajam por design, mesmo que gitignored.
284
+ const ig = ignore();
285
+ try {
286
+ const gitignorePath = path.join(dir, '.gitignore');
287
+ if (await exists(gitignorePath)) {
288
+ ig.add(await fs.readFile(gitignorePath, 'utf8'));
289
+ }
290
+ } catch { /* sem .gitignore → não filtra por ignore */ }
291
+
292
+ let limitHit = false;
293
+
294
+ // Processa UM arquivo (checa skip/ignore/extensão/tamanho, lê, ofusca se build,
295
+ // grava). Usado pelo walk e pelos includes pontuais do `.aioson` (que podem ser
296
+ // arquivo único). `forceInclude` = bypassa skip/ignore/extensão (pastas runtime).
297
+ async function addFile(fullPath, relPath, forceInclude, entryName) {
298
+ if (limitHit) return;
299
+ if (!forceInclude && SKIP_FILES.has(entryName)) return;
300
+ if (!forceInclude && ig.ignores(relPath)) return;
301
+
302
+ const ext = entryName.includes('.')
303
+ ? `.${entryName.split('.').pop().toLowerCase()}`
304
+ : '';
305
+ const extAllowed =
306
+ allowedExts.has(ext) ||
307
+ (forceInclude && AIOSON_RUNTIME_EXTS.has(ext)) ||
308
+ RUNTIME_CONFIG_RE.test(entryName); // vite.config.* viaja mesmo no --build
309
+ if (!extAllowed && ext !== '') return;
310
+
311
+ try {
312
+ const stat = await fs.stat(fullPath);
313
+ const maxBytes = buildMode ? MAX_FILE_BYTES_BUILD : MAX_FILE_BYTES;
314
+ if (stat.size > maxBytes) {
315
+ errors.push(`File too large (skipped): "${relPath}" (${(stat.size / 1024).toFixed(0)} KB)`);
316
+ return;
317
+ }
318
+ totalBytes += stat.size;
319
+ if (totalBytes > MAX_PACKAGE_BYTES) {
320
+ errors.push(`Package exceeds ${MAX_PACKAGE_BYTES / 1024 / 1024} MB limit — stop collecting.`);
321
+ limitHit = true;
322
+ return;
323
+ }
324
+ let content = await fs.readFile(fullPath, 'utf8');
325
+ if (
326
+ buildMode &&
327
+ (ext === '.js' || ext === '.mjs' || ext === '.cjs') &&
328
+ !RUNTIME_CONFIG_RE.test(entryName) // não ofuscar config lida pelo vite
329
+ ) {
330
+ content = obfuscateJs(content);
331
+ }
332
+ files[relPath] = content;
333
+ } catch {
334
+ // binary or unreadable — skip silently
335
+ }
336
+ }
337
+
338
+ async function walk(current, rel, forceInclude = false) {
339
+ if (limitHit) return;
171
340
  const entries = await fs.readdir(current, { withFileTypes: true });
172
341
  for (const entry of entries) {
173
- if (skipDirs.has(entry.name)) continue;
174
- if (rel && skipDirs.has(`${rel}/${entry.name}`)) continue;
175
- if (SKIP_FILES.has(entry.name)) continue;
176
-
177
342
  const fullPath = path.join(current, entry.name);
178
343
  const relPath = rel ? `${rel}/${entry.name}` : entry.name;
179
344
 
180
- if (entry.isDirectory()) {
181
- await walk(fullPath, relPath);
182
- continue;
183
- }
184
-
185
- const ext = entry.name.includes('.')
186
- ? `.${entry.name.split('.').pop().toLowerCase()}`
187
- : '';
345
+ // Testes/mocks fora do pacote — incondicional (pega até dentro do dist).
346
+ if (entry.isDirectory() && TEST_DIRS.has(entry.name)) continue;
347
+ if (!entry.isDirectory() && TEST_FILE_RE.test(entry.name)) continue;
188
348
 
189
- if (!allowedExts.has(ext) && ext !== '') continue;
190
-
191
- try {
192
- const stat = await fs.stat(fullPath);
193
- const maxBytes = buildMode ? MAX_FILE_BYTES_BUILD : MAX_FILE_BYTES;
194
- if (stat.size > maxBytes) {
195
- errors.push(`File too large (skipped): "${relPath}" (${(stat.size / 1024).toFixed(0)} KB)`);
349
+ if (entry.isDirectory()) {
350
+ // `.aioson`: normalmente fica fora, mas descemos só nas subpastas de
351
+ // runtime (squads/docs/skills/rules/genomes/agents) pra o app não
352
+ // quebrar. As subpastas entram em modo forceInclude (mantém estrutura
353
+ // e arquivos originais). Vale pra qualquer nível onde apareça `.aioson`.
354
+ if (entry.name === '.aioson' && !forceInclude) {
355
+ // `squads` (sempre) + o que o build-options.json declarar. Cada include
356
+ // pode ser pasta/subpasta (→ walk) ou arquivo único (→ addFile).
357
+ const includes = await readAiosonIncludes(fullPath);
358
+ for (const inc of includes) {
359
+ const incPath = path.join(fullPath, inc);
360
+ if (!(await exists(incPath))) continue;
361
+ const st = await fs.stat(incPath);
362
+ if (st.isDirectory()) {
363
+ await walk(incPath, `${relPath}/${inc}`, true);
364
+ } else {
365
+ await addFile(incPath, `${relPath}/${inc}`, true, path.basename(inc));
366
+ }
367
+ }
196
368
  continue;
197
369
  }
198
- totalBytes += stat.size;
199
- if (totalBytes > MAX_PACKAGE_BYTES) {
200
- errors.push(`Package exceeds ${MAX_PACKAGE_BYTES / 1024 / 1024} MB limit stop collecting.`);
201
- return;
370
+ // Saída do build (--build): viaja mesmo gitignored. forceInclude bypassa
371
+ // o filtro de .gitignore; o filtro de extensão + minify continuam (sourcemaps
372
+ // `.map` ficam de fora por não estarem nas extensões permitidas não vaza fonte).
373
+ if (buildMode && !forceInclude && BUILD_OUTPUT_DIRS.has(entry.name)) {
374
+ await walk(fullPath, relPath, true);
375
+ continue;
202
376
  }
203
- let content = await fs.readFile(fullPath, 'utf8');
204
-
205
- if (buildMode && (ext === '.js' || ext === '.mjs' || ext === '.cjs')) {
206
- try {
207
- const terser = getTerser();
208
- const result = await terser.minify(content, {
209
- compress: { passes: 2, drop_console: false },
210
- mangle: {
211
- toplevel: true,
212
- properties: { regex: /^_/ },
213
- },
214
- format: { comments: false },
215
- });
216
- if (result.code) content = result.code;
217
- } catch {
218
- // terser failed on this file — keep original compiled JS
219
- }
377
+ if (!forceInclude) {
378
+ if (skipDirs.has(entry.name)) continue;
379
+ if (rel && skipDirs.has(`${rel}/${entry.name}`)) continue;
380
+ if (ig.ignores(relPath)) continue; // gitignored → não viaja
220
381
  }
221
-
222
- files[relPath] = content;
223
- } catch {
224
- // binary or unreadable — skip silently
382
+ await walk(fullPath, relPath, forceInclude);
383
+ continue;
225
384
  }
385
+
386
+ // Arquivo — processado pelo addFile (skip/ignore/ext/tamanho/ofuscação).
387
+ await addFile(fullPath, relPath, forceInclude, entry.name);
226
388
  }
227
389
  }
228
390
 
@@ -248,9 +410,91 @@ async function readSystemJson(dir, t) {
248
410
  if (!manifest.slug) throw new Error(t('system.error_manifest_missing_slug'));
249
411
  if (!manifest.version) throw new Error(t('system.error_manifest_missing_version'));
250
412
  if (!manifest.name) throw new Error(t('system.error_manifest_missing_name'));
413
+ validateListingFields(manifest);
251
414
  return manifest;
252
415
  }
253
416
 
417
+ /**
418
+ * Valida os campos opcionais de listing da loja (modelo Chrome Web Store).
419
+ * Todos são opcionais — apps antigos sem eles publicam normalmente. Falha cedo,
420
+ * com mensagem clara, quando um campo presente está num formato inválido.
421
+ * Imagens (icon/screenshots) são URLs http(s) externas (Opção A — sem hosting).
422
+ */
423
+ function isHttpUrl(value) {
424
+ return typeof value === 'string' && /^https?:\/\/.+/i.test(value.trim());
425
+ }
426
+
427
+ function validateListingFields(m) {
428
+ const fail = (msg) => { throw new Error(`system.json: ${msg}`); };
429
+
430
+ if (m.summary != null) {
431
+ if (typeof m.summary !== 'string') fail('"summary" deve ser texto.');
432
+ if (m.summary.length > 132) fail(`"summary" deve ter no máximo 132 caracteres (tem ${m.summary.length}).`);
433
+ }
434
+ if (m.purpose != null) {
435
+ if (typeof m.purpose !== 'string') fail('"purpose" deve ser texto.');
436
+ if (m.purpose.length > 280) fail(`"purpose" deve ter no máximo 280 caracteres (tem ${m.purpose.length}).`);
437
+ }
438
+ if (m.category != null && typeof m.category !== 'string') fail('"category" deve ser texto.');
439
+ if (m.permissions_note != null && typeof m.permissions_note !== 'string') fail('"permissions_note" deve ser texto.');
440
+
441
+ if (m.tags != null) {
442
+ if (!Array.isArray(m.tags) || m.tags.some((x) => typeof x !== 'string')) fail('"tags" deve ser uma lista de textos.');
443
+ if (m.tags.length > 10) fail('"tags" aceita no máximo 10 itens.');
444
+ }
445
+
446
+ if (m.icon != null && !isHttpUrl(m.icon)) fail('"icon" deve ser uma URL http(s) (Opção A — imagem hospedada externamente).');
447
+
448
+ if (m.screenshots != null) {
449
+ if (!Array.isArray(m.screenshots)) fail('"screenshots" deve ser uma lista de URLs.');
450
+ if (m.screenshots.length > 5) fail('"screenshots" aceita no máximo 5 itens.');
451
+ for (const s of m.screenshots) {
452
+ if (!isHttpUrl(s)) fail('cada item de "screenshots" deve ser uma URL http(s).');
453
+ }
454
+ }
455
+
456
+ for (const key of ['homepage_url', 'support_url', 'privacy_url']) {
457
+ if (m[key] != null && !isHttpUrl(m[key])) fail(`"${key}" deve ser uma URL http(s).`);
458
+ }
459
+ if (m.support_email != null) {
460
+ if (typeof m.support_email !== 'string' || !m.support_email.includes('@')) fail('"support_email" deve ser um e-mail válido.');
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Sincroniza a versão dos package.json do app com o system.json (fonte da
466
+ * verdade do publish). Sem isso o package.json fica preso (ex.: 1.0.0) e os logs
467
+ * mostram `app@1.0.0` enquanto a loja publica 1.2.13 — divergência que só
468
+ * confunde. O Play usa a versão do system.json/manifest; aqui só alinhamos os
469
+ * package.json (raiz + `dashboard/` em apps split-stack) pra não divergir. Roda
470
+ * ANTES do build/coleta, então a versão sincronizada já entra no pacote.
471
+ * Best-effort: package.json ausente/ilegível não bloqueia o publish.
472
+ */
473
+ async function syncPackageVersions(dir, version, logger) {
474
+ const candidates = [
475
+ path.join(dir, 'package.json'),
476
+ path.join(dir, 'dashboard', 'package.json'),
477
+ ];
478
+ for (const pkgPath of candidates) {
479
+ if (!(await exists(pkgPath))) continue;
480
+ let pkg;
481
+ try {
482
+ pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
483
+ } catch {
484
+ continue;
485
+ }
486
+ if (pkg.version === version) continue;
487
+ const prev = pkg.version || '(ausente)';
488
+ pkg.version = version;
489
+ try {
490
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
491
+ logger.log(`package.json sincronizado: ${path.relative(dir, pkgPath) || 'package.json'} ${prev} → ${version}`);
492
+ } catch {
493
+ // best-effort — não bloqueia o publish
494
+ }
495
+ }
496
+ }
497
+
254
498
  // ── system:package ──────────────────────────────────────────────────────────
255
499
 
256
500
  async function runSystemPackage({ args, options, logger, t }) {
@@ -260,6 +504,11 @@ async function runSystemPackage({ args, options, logger, t }) {
260
504
  const manifest = await readSystemJson(dir, t);
261
505
  logger.log(t('system.package_manifest_ok', { slug: manifest.slug, version: manifest.version, name: manifest.name }));
262
506
 
507
+ // Alinha os package.json à versão do system.json antes de coletar.
508
+ if (!options['dry-run']) {
509
+ await syncPackageVersions(dir, manifest.version, logger);
510
+ }
511
+
263
512
  logger.log(t('system.package_collecting_files'));
264
513
  const { files, totalBytes, errors } = await collectSystemFiles(dir);
265
514
 
@@ -304,6 +553,11 @@ async function runSystemPublish({ args, options, logger, t }) {
304
553
  const manifest = await readSystemJson(dir, t);
305
554
  logger.log(t('system.package_manifest_ok', { slug: manifest.slug, version: manifest.version, name: manifest.name }));
306
555
 
556
+ // Alinha os package.json à versão do system.json antes de buildar/coletar.
557
+ if (!options['dry-run']) {
558
+ await syncPackageVersions(dir, manifest.version, logger);
559
+ }
560
+
307
561
  if (buildMode) {
308
562
  const buildCmd = manifest.build_command || 'npm run build';
309
563
  logger.log(`Building: ${buildCmd}`);
@@ -337,6 +591,28 @@ async function runSystemPublish({ args, options, logger, t }) {
337
591
 
338
592
  const visibility = options.private ? 'private' : 'public';
339
593
  const paid = Boolean(options.paid);
594
+
595
+ // app-licensing-revenue-share (Fase 5 / BR-01): app PAID exige preço na fonte
596
+ // única (system.json). Falha cedo aqui, antes do upload — o servidor também
597
+ // recusa na criação. Aceita priceInCents | price_in_cents | price (em unidades).
598
+ if (paid) {
599
+ const priceCents =
600
+ Number(manifest.priceInCents) ||
601
+ Number(manifest.price_in_cents) ||
602
+ (Number(manifest.price) > 0 ? Math.round(Number(manifest.price) * 100) : 0);
603
+ if (!priceCents || priceCents <= 0) {
604
+ throw new Error(
605
+ 'App PAID exige preço: defina "priceInCents" (centavos) ou "price" no system.json antes de publicar com --paid.'
606
+ );
607
+ }
608
+ // SF-alrs-03: visibility=PAID e preço são geridos no banco do aioson.com (dashboard
609
+ // da loja) — NÃO via este flag. O --paid só valida o preço localmente; não publica
610
+ // o app como pago por si só.
611
+ logger.log(
612
+ 'Nota: visibility=PAID e preço são definidos no aioson.com (dashboard da loja). O flag --paid valida o preço localmente, mas não publica o app como pago sozinho.'
613
+ );
614
+ }
615
+
340
616
  const ws = await readWorkspace(dir);
341
617
 
342
618
  // Lista de emails autorizados a instalar quando visibility=private.
@@ -351,9 +627,13 @@ async function runSystemPublish({ args, options, logger, t }) {
351
627
 
352
628
  logger.log('Creating ZIP package...');
353
629
  const zipBuffer = await createZipBuffer(files);
354
- const MAX_ZIP_BYTES = 2 * 1024 * 1024;
630
+ // 10 MB: a ofuscação (string-array/base64 + alta entropia) incha e comprime
631
+ // pior que a fonte, então 2 MB era apertado demais pra `--build`. O servidor
632
+ // (aioson-com) não impõe limite na rota (só `request.json()`).
633
+ const MAX_ZIP_BYTES = 10 * 1024 * 1024;
355
634
  if (zipBuffer.length > MAX_ZIP_BYTES) {
356
- throw new Error(`ZIP exceeds 2 MB limit (${(zipBuffer.length / 1024 / 1024).toFixed(2)} MB). Reduce the number of files or bundle size.`);
635
+ const mb = (MAX_ZIP_BYTES / 1024 / 1024).toFixed(0);
636
+ throw new Error(`ZIP exceeds ${mb} MB limit (${(zipBuffer.length / 1024 / 1024).toFixed(2)} MB). Reduce the number of files or bundle size.`);
357
637
  }
358
638
  const zipBase64 = zipBuffer.toString('base64');
359
639
  const zipKb = (zipBuffer.length / 1024).toFixed(1);
package/src/constants.js CHANGED
@@ -45,6 +45,7 @@ const MANAGED_FILES = [
45
45
  '.aioson/docs/squad/content-output.md',
46
46
  '.aioson/docs/squad/session-operations.md',
47
47
  '.aioson/docs/squad/genome-bindings.md',
48
+ '.aioson/docs/feature-expansion-taxonomy.md',
48
49
  '.aioson/docs/product/conversation-playbook.md',
49
50
  '.aioson/docs/product/research-loop.md',
50
51
  '.aioson/docs/product/quality-lens.md',
@@ -61,6 +62,7 @@ const MANAGED_FILES = [
61
62
  '.aioson/docs/sheldon/harness-contract.md',
62
63
  '.aioson/docs/dev/stack-conventions.md',
63
64
  '.aioson/docs/dev/execution-discipline.md',
65
+ '.aioson/docs/dev/simple-plan-lane.md',
64
66
  '.aioson/docs/quality/code-health-analysis.md',
65
67
  '.aioson/skills/process/decision-presentation/SKILL.md',
66
68
  '.aioson/skills/process/decision-presentation/references/jargon-map.en.yaml',
@@ -68,6 +70,9 @@ const MANAGED_FILES = [
68
70
  '.aioson/skills/process/prompt-sharpener/SKILL.md',
69
71
  '.aioson/skills/process/prompt-sharpener/references/prompt-diagnostics.md',
70
72
  '.aioson/skills/process/prompt-sharpener/agents/openai.yaml',
73
+ '.aioson/skills/process/briefing-expansion-scout/SKILL.md',
74
+ '.aioson/skills/process/product-scope-expansion/SKILL.md',
75
+ '.aioson/skills/process/sheldon-expansion-audit/SKILL.md',
71
76
  '.aioson/skills/static/laravel-conventions.md',
72
77
  '.aioson/skills/static/tall-stack-patterns.md',
73
78
  '.aioson/skills/static/jetstream-setup.md',
@@ -265,7 +270,7 @@ const AGENT_DEFINITIONS = [
265
270
  '.aioson/context/discovery.md',
266
271
  '.aioson/context/architecture.md'
267
272
  ],
268
- output: '.aioson/context/ui-spec.md + Visual identity enrichment in prd.md or prd-{slug}.md'
273
+ output: '.aioson/context/ui-spec-{slug}.md (project mode: .aioson/context/ui-spec.md) + Visual identity enrichment in prd.md or prd-{slug}.md'
269
274
  },
270
275
  {
271
276
  id: 'pm',
@@ -342,7 +347,7 @@ const AGENT_DEFINITIONS = [
342
347
  command: '@tester',
343
348
  path: '.aioson/agents/tester.md',
344
349
  dependsOn: ['.aioson/context/project.context.md'],
345
- output: '.aioson/context/test-inventory.md + .aioson/context/test-plan.md'
350
+ output: '.aioson/context/test-inventory-{slug}.md + .aioson/context/test-plan-{slug}.md (project mode: bare names)'
346
351
  },
347
352
  {
348
353
  id: 'orchestrator',
@@ -431,7 +436,7 @@ const AGENT_DEFINITIONS = [
431
436
  command: '@sheldon',
432
437
  path: '.aioson/agents/sheldon.md',
433
438
  dependsOn: ['.aioson/context/project.context.md'],
434
- output: 'enriched PRD or phased execution plan'
439
+ output: 'enriched PRD or phased execution plan (+ sheldon-validation-{slug}.md readiness gate on MEDIUM)'
435
440
  },
436
441
  {
437
442
  id: 'committer',