gemstack-ai 1.0.1 → 1.2.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 (58) hide show
  1. package/.agents/rules/01-gemstack-core.md +15 -0
  2. package/.agents/rules/02-gemstack-constitution.md +12 -1
  3. package/.agents/skills/gemstack-handoff/SKILL.md +2 -1
  4. package/.agents/skills/gemstack-plan/SKILL.md +3 -1
  5. package/.agents/skills/gemstack-qa/SKILL.md +3 -0
  6. package/.agents/skills/gemstack-review/SKILL.md +7 -5
  7. package/.agents/skills/gemstack-ship/SKILL.md +10 -1
  8. package/.agents/skills/gemstack-spec/SKILL.md +4 -2
  9. package/.agents/skills/gemstack-tasks/SKILL.md +5 -3
  10. package/.gemstack/state.json +18 -2
  11. package/CHANGELOG.md +84 -0
  12. package/MANUAL.md +4 -4
  13. package/README.md +49 -8
  14. package/RELEASE_NOTES.md +129 -0
  15. package/assets/logo.jpg +0 -0
  16. package/docs/architecture-consistency.md +156 -0
  17. package/docs/spec-driven-development.md +26 -0
  18. package/gemstack-ai-1.2.0.tgz +0 -0
  19. package/handoff.md +40 -40
  20. package/package.json +3 -2
  21. package/scripts/ci/smoke-cli.js +1 -0
  22. package/specs/006-architecture-consistency-engine/.gemstack.json +9 -0
  23. package/specs/006-architecture-consistency-engine/plan.md +319 -0
  24. package/specs/006-architecture-consistency-engine/spec.md +179 -0
  25. package/specs/006-architecture-consistency-engine/tasks.md +532 -0
  26. package/specs/007-mechanical-test-matrix-closure-evidence/.gemstack.json +5 -0
  27. package/specs/007-mechanical-test-matrix-closure-evidence/closure.json +59 -0
  28. package/specs/007-mechanical-test-matrix-closure-evidence/plan.md +484 -0
  29. package/specs/007-mechanical-test-matrix-closure-evidence/spec.md +597 -0
  30. package/specs/007-mechanical-test-matrix-closure-evidence/tasks.md +536 -0
  31. package/specs/templates/plan.md +41 -0
  32. package/specs/templates/spec.md +35 -0
  33. package/specs/templates/tasks.md +10 -0
  34. package/src/cli.js +15 -4
  35. package/src/commands/collect.js +340 -0
  36. package/src/commands/ship.js +79 -0
  37. package/src/commands/verify.js +433 -0
  38. package/src/lib/closure-context.js +444 -0
  39. package/src/lib/contracts.js +388 -0
  40. package/src/lib/findings.js +227 -0
  41. package/src/lib/hasher.js +103 -0
  42. package/src/lib/runner-adapters.js +347 -0
  43. package/src/lib/state.js +143 -0
  44. package/src/lib/test-matrix.js +187 -0
  45. package/src/mcp-server.js +1 -1
  46. package/template/.agents/rules/01-gemstack-core.md +15 -0
  47. package/template/.agents/rules/02-gemstack-constitution.md +12 -1
  48. package/template/.agents/skills/gemstack-handoff/SKILL.md +2 -1
  49. package/template/.agents/skills/gemstack-plan/SKILL.md +2 -1
  50. package/template/.agents/skills/gemstack-review/SKILL.md +7 -5
  51. package/template/.agents/skills/gemstack-ship/SKILL.md +5 -0
  52. package/template/.agents/skills/gemstack-spec/SKILL.md +3 -2
  53. package/template/.agents/skills/gemstack-tasks/SKILL.md +4 -3
  54. package/template/docs/architecture-consistency.md +144 -0
  55. package/template/specs/templates/plan.md +11 -0
  56. package/template/specs/templates/spec.md +17 -0
  57. package/template/specs/templates/tasks.md +1 -0
  58. package/gemstack-ai-1.0.1.tgz +0 -0
@@ -0,0 +1,433 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+ const fssafe = require('../lib/filesystem-safe');
5
+ const logger = require('../lib/logger');
6
+ const manifestLib = require('../lib/manifest');
7
+
8
+ module.exports = async (flags) => {
9
+ const targetDir = flags.target || process.cwd();
10
+ logger.info(`Ejecutando verificación integral de Gemstack en: ${targetDir}`);
11
+
12
+ let totalErrors = 0;
13
+ let totalWarnings = 0;
14
+
15
+ // 1. Verificación Estructural y Manifest
16
+ logger.info('--- 1/6 Verificación Estructural (Archivos Base) ---');
17
+ const manifestPath = fssafe.resolveSafe(targetDir, '.gemstack/manifest.json');
18
+ if (!fs.existsSync(manifestPath)) {
19
+ logger.warn('Manifest no encontrado (.gemstack/manifest.json). Es posible que Gemstack no esté inicializado en este directorio.');
20
+ totalWarnings++;
21
+ } else {
22
+ try {
23
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
24
+ let missing = 0;
25
+ let modified = 0;
26
+ (manifest.files || []).forEach(f => {
27
+ const p = fssafe.resolveSafe(targetDir, f.path);
28
+ if (!fs.existsSync(p)) {
29
+ logger.error(`Archivo requerido faltante: ${f.path}`);
30
+ missing++;
31
+ } else {
32
+ const destContent = fs.readFileSync(p);
33
+ if (manifestLib.getChecksum(destContent) !== f.checksum) {
34
+ modified++;
35
+ }
36
+ }
37
+ });
38
+ if (missing === 0 && modified === 0) {
39
+ logger.ok('Todos los archivos base de Gemstack están presentes e íntegros.');
40
+ } else {
41
+ if (missing > 0) totalErrors += missing;
42
+ if (modified > 0) {
43
+ logger.info(`${modified} archivos base modificados localmente (personalización permitida).`);
44
+ }
45
+ }
46
+ } catch (e) {
47
+ logger.error(`Error leyendo manifest.json: ${e.message}`);
48
+ totalErrors++;
49
+ }
50
+ }
51
+
52
+ const giPath = fssafe.resolveSafe(targetDir, '.gitignore');
53
+ if (fs.existsSync(giPath) && fs.readFileSync(giPath, 'utf8').includes('# Gemstack')) {
54
+ logger.ok('.gitignore contiene el bloque de Gemstack.');
55
+ } else {
56
+ logger.warn('.gitignore no está configurado con las exclusiones de Gemstack.');
57
+ totalWarnings++;
58
+ }
59
+
60
+ // 2. Verificación de Memoria (handoff.md)
61
+ logger.info('--- 2/6 Verificación de Memoria e Integridad de Handoff ---');
62
+ const handoffPath = fssafe.resolveSafe(targetDir, 'handoff.md');
63
+ if (!fs.existsSync(handoffPath)) {
64
+ logger.error('handoff.md no existe en la raíz. La memoria de sesión es obligatoria.');
65
+ totalErrors++;
66
+ } else {
67
+ const handoffContent = fs.readFileSync(handoffPath, 'utf8');
68
+ const requiredSections = [
69
+ '1. Objetivo',
70
+ '2. Estado actual',
71
+ '3. Archivos y cambios',
72
+ '4. Intentos fallidos',
73
+ '5. Próximos pasos'
74
+ ];
75
+ const missingSections = requiredSections.filter(s => !handoffContent.includes(s));
76
+ if (missingSections.length > 0) {
77
+ logger.error(`handoff.md está incompleto. Faltan secciones obligatorias: ${missingSections.join(', ')}`);
78
+ totalErrors++;
79
+ } else {
80
+ logger.ok('handoff.md contiene las 5 secciones obligatorias.');
81
+ }
82
+
83
+ // Regla inmutable: '4. Intentos fallidos' no debe estar eliminada
84
+ if (!handoffContent.includes('4. Intentos fallidos')) {
85
+ logger.error('Violación de la Constitución: La sección "4. Intentos fallidos" fue eliminada.');
86
+ totalErrors++;
87
+ } else {
88
+ logger.ok('Sección inmutable "4. Intentos fallidos" preservada.');
89
+ }
90
+ }
91
+
92
+ // 3. Consistencia de Estado Local (.gemstack/state.json)
93
+ logger.info('--- 3/6 Verificación de Estado Local (.gemstack/state.json) ---');
94
+ const statePath = fssafe.resolveSafe(targetDir, '.gemstack/state.json');
95
+ let loadedState = null;
96
+ if (!fs.existsSync(statePath)) {
97
+ logger.warn('.gemstack/state.json no encontrado.');
98
+ totalWarnings++;
99
+ } else {
100
+ try {
101
+ const { readState } = require('../lib/state');
102
+ loadedState = readState(targetDir);
103
+ logger.ok(`Estado local cargado. Fase actual: ${loadedState.current_phase || 'no definida'}`);
104
+ if (loadedState.active_spec) {
105
+ const specFile = fssafe.resolveSafe(targetDir, path.join(loadedState.active_spec, 'spec.md'));
106
+ if (!fs.existsSync(specFile)) {
107
+ logger.warn(`Desfase de estado: active_spec apunta a "${loadedState.active_spec}", pero "${specFile}" no existe.`);
108
+ totalWarnings++;
109
+ } else {
110
+ logger.ok(`active_spec confirmado: ${loadedState.active_spec}`);
111
+ }
112
+ } else {
113
+ logger.ok('Sin spec activa pendiente (estado limpio o cerrado).');
114
+ }
115
+ } catch (e) {
116
+ logger.error(`.gemstack/state.json tiene formato JSON inválido: ${e.message}`);
117
+ totalErrors++;
118
+ }
119
+ }
120
+
121
+ // 4. Consistencia de Arquitectura y Hashes de Fase (Upgrade A)
122
+ logger.info('--- 4/6 Verificación de Consistencia de Arquitectura y Hashes de Fase ---');
123
+ if (loadedState && loadedState.active_spec) {
124
+ try {
125
+ const { hashFile } = require('../lib/hasher');
126
+ const {
127
+ extractContractsBlock,
128
+ validateContractSchemas,
129
+ comparePhaseContracts,
130
+ resolvePhaseInheritance
131
+ } = require('../lib/contracts');
132
+ const {
133
+ reconcileFindings,
134
+ evaluateAcceptedExceptions,
135
+ formatDisplayFingerprint
136
+ } = require('../lib/findings');
137
+
138
+ const specFile = fssafe.resolveSafe(targetDir, path.join(loadedState.active_spec, 'spec.md'));
139
+ const planFile = fssafe.resolveSafe(targetDir, path.join(loadedState.active_spec, 'plan.md'));
140
+ const tasksFile = fssafe.resolveSafe(targetDir, path.join(loadedState.active_spec, 'tasks.md'));
141
+
142
+ if (fs.existsSync(specFile)) {
143
+ const specRaw = fs.readFileSync(specFile, 'utf8');
144
+ const specBlock = extractContractsBlock(specRaw);
145
+
146
+ if (specBlock.isLegacy) {
147
+ logger.ok(`[LEGACY] Feature "${loadedState.active_spec}" opera en modo legacy (sin bloques de contratos).`);
148
+ } else {
149
+ const specContracts = validateContractSchemas(specBlock.contracts);
150
+ logger.ok(`[STRUCTURED] ${specContracts.length} contrato(s) base declarados en spec.md.`);
151
+
152
+ // Detección de mutación de spec congelada (VERIFY != FREEZE)
153
+ if (loadedState.phase_hashes && loadedState.phase_hashes.spec) {
154
+ const currentSpecHash = hashFile(specFile);
155
+ if (currentSpecHash !== loadedState.phase_hashes.spec) {
156
+ logger.error(`[FROZEN_ARTIFACT_CHANGED] El artefacto congelado spec.md fue mutado sin autorización. Hash esperado: ${loadedState.phase_hashes.spec}, actual: ${currentSpecHash}`);
157
+ totalErrors++;
158
+ } else {
159
+ logger.ok(`Hash congelado de spec.md verificado: ${loadedState.phase_hashes.spec.slice(0, 12)}...`);
160
+ }
161
+ }
162
+
163
+ // Validación de PLAN si existe
164
+ let effectiveUpstream = specContracts;
165
+ let planContracts = [];
166
+ let violations = [];
167
+
168
+ if (fs.existsSync(planFile)) {
169
+ const planRaw = fs.readFileSync(planFile, 'utf8');
170
+ const planBlock = extractContractsBlock(planRaw);
171
+ if (!planBlock.isLegacy) {
172
+ planContracts = validateContractSchemas(planBlock.contracts);
173
+ const planViolations = comparePhaseContracts(effectiveUpstream, planContracts, 'plan');
174
+ violations.push(...planViolations.map(v => ({ ...v, location: path.join(loadedState.active_spec, 'plan.md') })));
175
+ effectiveUpstream = resolvePhaseInheritance(effectiveUpstream, planContracts);
176
+ }
177
+
178
+ if (loadedState.phase_hashes && loadedState.phase_hashes.plan) {
179
+ const currentPlanHash = hashFile(planFile);
180
+ if (currentPlanHash !== loadedState.phase_hashes.plan) {
181
+ logger.error(`[FROZEN_ARTIFACT_CHANGED] El artefacto congelado plan.md fue mutado sin autorización. Hash esperado: ${loadedState.phase_hashes.plan}, actual: ${currentPlanHash}`);
182
+ totalErrors++;
183
+ } else {
184
+ logger.ok(`Hash congelado de plan.md verificado: ${loadedState.phase_hashes.plan.slice(0, 12)}...`);
185
+ }
186
+ }
187
+ }
188
+
189
+ // Validación de TASKS si existe
190
+ if (fs.existsSync(tasksFile)) {
191
+ const tasksRaw = fs.readFileSync(tasksFile, 'utf8');
192
+ const tasksBlock = extractContractsBlock(tasksRaw);
193
+ if (!tasksBlock.isLegacy) {
194
+ const tasksContracts = validateContractSchemas(tasksBlock.contracts);
195
+ const tasksViolations = comparePhaseContracts(effectiveUpstream, tasksContracts, 'tasks');
196
+ violations.push(...tasksViolations.map(v => ({ ...v, location: path.join(loadedState.active_spec, 'tasks.md') })));
197
+ }
198
+
199
+ if (loadedState.phase_hashes && loadedState.phase_hashes.tasks) {
200
+ const currentTasksHash = hashFile(tasksFile);
201
+ if (currentTasksHash !== loadedState.phase_hashes.tasks) {
202
+ logger.error(`[FROZEN_ARTIFACT_CHANGED] El artefacto congelado tasks.md fue mutado sin autorización. Hash esperado: ${loadedState.phase_hashes.tasks}, actual: ${currentTasksHash}`);
203
+ totalErrors++;
204
+ } else {
205
+ logger.ok(`Hash congelado de tasks.md verificado: ${loadedState.phase_hashes.tasks.slice(0, 12)}...`);
206
+ }
207
+ }
208
+ }
209
+
210
+ // Reconciliación de hallazgos y evaluación de excepciones aceptadas vía sidecar de feature
211
+ const featureDir = fssafe.resolveSafe(targetDir, loadedState.active_spec);
212
+ const { readSidecar, writeSidecarAtomic } = require('../lib/state');
213
+ const sidecar = readSidecar(featureDir);
214
+
215
+ // Migración retrocompatible: si state tenía findings o accepted_exceptions, migrarlos al sidecar
216
+ const existingFindings = sidecar.historical_findings && sidecar.historical_findings.length > 0
217
+ ? sidecar.historical_findings
218
+ : (loadedState.findings || []);
219
+ const acceptedExceptions = sidecar.accepted_exceptions && sidecar.accepted_exceptions.length > 0
220
+ ? sidecar.accepted_exceptions
221
+ : (loadedState.accepted_exceptions || []);
222
+
223
+ const reconciled = reconcileFindings(existingFindings, violations);
224
+ const currentContext = {
225
+ upstreamAcceptedPhaseHash: (loadedState.phase_hashes && loadedState.phase_hashes.spec) || '',
226
+ currentComparedPhaseHash: (loadedState.phase_hashes && loadedState.phase_hashes.plan) || '',
227
+ normalizedContractRepresentation: JSON.stringify(effectiveUpstream)
228
+ };
229
+ const evaluated = evaluateAcceptedExceptions(reconciled, acceptedExceptions, currentContext);
230
+
231
+ // Persistir el historial detallado de hallazgos exclusivamente en el sidecar
232
+ sidecar.historical_findings = evaluated;
233
+ sidecar.accepted_exceptions = acceptedExceptions;
234
+ writeSidecarAtomic(featureDir, sidecar);
235
+
236
+ const blockers = evaluated.filter(f => f.is_blocking);
237
+ if (blockers.length > 0) {
238
+ for (const b of blockers) {
239
+ logger.error(`[CONSISTENCY_BLOCKER] Contrato "${b.contractId}" en fase "${b.phase}" (${formatDisplayFingerprint(b.fingerprint)}): ${JSON.stringify(b.delta)}`);
240
+ }
241
+ totalErrors += blockers.length;
242
+ } else {
243
+ logger.ok('Verificación de consistencia arquitectónica aprobada (0 bloqueadores).');
244
+ }
245
+ }
246
+ } else {
247
+ logger.ok(`Modo legacy: no existe spec.md en ${loadedState.active_spec}`);
248
+ }
249
+ } catch (cErr) {
250
+ logger.error(`Error en verificación de consistencia: ${cErr.message}`);
251
+ totalErrors++;
252
+ }
253
+ } else {
254
+ logger.ok('Sin spec activa configurada para verificación de contratos.');
255
+ }
256
+
257
+ // 5. Verificación de Evidencia de Cierre Mecánico (Upgrade B - Read-Only)
258
+ logger.info('--- 5/6 Verificación de Evidencia de Cierre Mecánico (Read-Only) ---');
259
+ if (loadedState && loadedState.active_spec) {
260
+ try {
261
+ const { extractTestMatrixBlock } = require('../lib/test-matrix');
262
+ const specFile = fssafe.resolveSafe(targetDir, path.join(loadedState.active_spec, 'spec.md'));
263
+
264
+ if (!fs.existsSync(specFile)) {
265
+ logger.ok(`Modo legacy: no existe spec.md en ${loadedState.active_spec}`);
266
+ } else {
267
+ const specContent = fs.readFileSync(specFile, 'utf8');
268
+ const { isLegacy } = extractTestMatrixBlock(specContent);
269
+
270
+ if (isLegacy) {
271
+ logger.info(`[LEGACY] Spec "${loadedState.active_spec}" opera en modo legacy sin matriz de pruebas.`);
272
+ } else {
273
+ // Feature structured: read closure.json in strictly read-only mode
274
+ const specDir = fssafe.resolveSafe(targetDir, loadedState.active_spec);
275
+ const closurePath = path.join(specDir, 'closure.json');
276
+
277
+ if (!fs.existsSync(closurePath)) {
278
+ logger.error(`[CLOSURE_MANIFEST_MISSING] closure.json no existe en ${loadedState.active_spec}. Ejecuta "gemstack collect" para generar la evidencia mecánica.`);
279
+ totalErrors++;
280
+ } else {
281
+ let manifest;
282
+ try {
283
+ manifest = JSON.parse(fs.readFileSync(closurePath, 'utf8'));
284
+ } catch (e) {
285
+ logger.error(`[CLOSURE_MANIFEST_INVALID] closure.json tiene formato JSON inválido: ${e.message}`);
286
+ totalErrors++;
287
+ manifest = null;
288
+ }
289
+
290
+ if (manifest) {
291
+ // Recompute closureContextHash in-memory without modifying any file
292
+ const {
293
+ validateTestMatrix,
294
+ computeAcceptanceSignature
295
+ } = require('../lib/test-matrix');
296
+ const {
297
+ parsePlanBindings,
298
+ parsePlanGates,
299
+ parseTaskMetadata,
300
+ computeContentAggregateHash,
301
+ resolveRepositoryContext,
302
+ computeClosureContextHash
303
+ } = require('../lib/closure-context');
304
+ const { hashFile } = require('../lib/hasher');
305
+
306
+ const planFile = path.join(specDir, 'plan.md');
307
+ const tasksFile = path.join(specDir, 'tasks.md');
308
+
309
+ let planBindings = [];
310
+ let planGates = [];
311
+ if (fs.existsSync(planFile)) {
312
+ const planContent = fs.readFileSync(planFile, 'utf8');
313
+ planBindings = parsePlanBindings(planContent);
314
+ planGates = parsePlanGates(planContent);
315
+ }
316
+
317
+ let tasks = [];
318
+ if (fs.existsSync(tasksFile)) {
319
+ const tasksContent = fs.readFileSync(tasksFile, 'utf8');
320
+ tasks = parseTaskMetadata(tasksContent);
321
+ }
322
+
323
+ const { matrix } = extractTestMatrixBlock(specContent);
324
+ const canonicalMatrix = validateTestMatrix(matrix);
325
+ const acceptanceSignature = computeAcceptanceSignature(canonicalMatrix);
326
+
327
+ const repoContext = resolveRepositoryContext(targetDir);
328
+ const phaseHashes = {
329
+ spec: hashFile(specFile),
330
+ plan: fs.existsSync(planFile) ? hashFile(planFile) : null,
331
+ tasks: fs.existsSync(tasksFile) ? hashFile(tasksFile) : null
332
+ };
333
+
334
+ const boundTestFiles = Array.from(new Set(planBindings.map(b => b.file)));
335
+ const testFilesHash = computeContentAggregateHash(targetDir, boundTestFiles);
336
+ const implementationFiles = Array.from(new Set(tasks.flatMap(t => t.files || [])))
337
+ .filter(f => !f.endsWith('closure.json'));
338
+ const implementationContextHash = computeContentAggregateHash(targetDir, implementationFiles);
339
+ const requiredGateDefinitionHash = computeContentAggregateHash(targetDir, ['package.json']);
340
+
341
+ const freshContextObj = {
342
+ version: 1,
343
+ repository: repoContext,
344
+ phase_hashes: phaseHashes,
345
+ acceptance_signature: acceptanceSignature,
346
+ test_files_hash: testFilesHash,
347
+ implementation_context_hash: implementationContextHash,
348
+ required_gate_definition_hash: requiredGateDefinitionHash
349
+ };
350
+
351
+ const freshContextHash = computeClosureContextHash(freshContextObj);
352
+ const recordedContextHash = manifest.closure_context ? manifest.closure_context.closure_context_hash : null;
353
+
354
+ if (recordedContextHash !== freshContextHash) {
355
+ logger.error(`[CLOSURE_EVIDENCE_STALE] La evidencia de cierre está desactualizada respecto al estado actual del proyecto. Re-ejecuta "gemstack collect". (Registrado: ${recordedContextHash ? recordedContextHash.slice(0, 12) : 'none'}..., Actual: ${freshContextHash.slice(0, 12)}...)`);
356
+ totalErrors++;
357
+ } else {
358
+ logger.ok(`Frescura de evidencia de cierre verificada (${freshContextHash.slice(0, 12)}...).`);
359
+
360
+ if (manifest.status !== 'VERIFIED' && manifest.status !== 'VERIFIED_WITH_EXCEPTIONS') {
361
+ logger.error(`[CLOSURE_NOT_VERIFIED] Estado del manifiesto es "${manifest.status}". Bloqueadores: ${JSON.stringify(manifest.blockers || [])}`);
362
+ totalErrors++;
363
+ } else {
364
+ logger.ok(`Evidencia de cierre aprobada: status="${manifest.status}", ${manifest.canonical_summary ? manifest.canonical_summary.required_passed : 0}/${manifest.canonical_summary ? manifest.canonical_summary.required_total : 0} pruebas canónicas pasadas.`);
365
+ }
366
+ }
367
+ }
368
+ }
369
+ }
370
+ }
371
+ } catch (mErr) {
372
+ logger.error(`Error en verificación de evidencia de cierre: ${mErr.message}`);
373
+ totalErrors++;
374
+ }
375
+ } else {
376
+ logger.ok('Sin spec activa configurada para validación de evidencia de cierre.');
377
+ }
378
+
379
+ // 6. Seguridad Local y Anti-Silent Failures en Tests
380
+ logger.info('--- 6/6 Verificación de Seguridad y Test Runners ---');
381
+ const envPath = fssafe.resolveSafe(targetDir, '.env');
382
+ if (fs.existsSync(envPath)) {
383
+ logger.warn('Archivo .env detectado en el directorio de trabajo. Verifica que esté en .gitignore.');
384
+ totalWarnings++;
385
+ }
386
+
387
+ const pkgPath = fssafe.resolveSafe(targetDir, 'package.json');
388
+ if (fs.existsSync(pkgPath)) {
389
+ try {
390
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
391
+ const scripts = pkg.scripts || {};
392
+ for (const [scriptName, scriptCmd] of Object.entries(scripts)) {
393
+ if (typeof scriptCmd === 'string') {
394
+ // Detección de supresores de error peligrosos en scripts
395
+ if (scriptCmd.includes('2>nul') || scriptCmd.includes('2> nul')) {
396
+ logger.error(`Falso positivo silencioso detectado en script "${scriptName}": contiene "2>nul", incompatible con PowerShell/Bash y enmascara fallos.`);
397
+ logger.info(`Solución: Estandariza el runner multiplataforma usando: "tsx --test <paths>" o "node --test <paths>"`);
398
+ totalErrors++;
399
+ } else if (scriptCmd.includes('|| true') && scriptName.includes('test')) {
400
+ logger.warn(`Script de prueba "${scriptName}" contiene "|| true", lo que ignora fallos de testing.`);
401
+ totalWarnings++;
402
+ }
403
+ }
404
+ }
405
+ logger.ok('Revisión de scripts de package.json completada.');
406
+ } catch (e) {
407
+ logger.warn(`No se pudo parsear package.json: ${e.message}`);
408
+ }
409
+ }
410
+
411
+ // Ejecución de pruebas si se solicita explícitamente (--run-tests)
412
+ if (flags.runTests) {
413
+ logger.info('Ejecutando suite de pruebas (--run-tests activado)...');
414
+ try {
415
+ execSync('npm test', { cwd: targetDir, stdio: 'inherit' });
416
+ logger.ok('Suite de pruebas ejecutada con éxito.');
417
+ } catch (err) {
418
+ logger.error('La suite de pruebas falló.');
419
+ totalErrors++;
420
+ }
421
+ }
422
+
423
+ // Resumen Final
424
+ console.log('\n--- Resumen de Auditoría Gemstack ---');
425
+ if (totalErrors === 0 && totalWarnings === 0) {
426
+ logger.ok('Auditoría completada sin errores ni advertencias. Todo el sistema está saludable.');
427
+ } else if (totalErrors === 0) {
428
+ logger.ok(`Auditoría completada con éxito (${totalWarnings} advertencia(s) menores).`);
429
+ } else {
430
+ logger.error(`Auditoría finalizada con ${totalErrors} error(es) y ${totalWarnings} advertencia(s).`);
431
+ process.exit(1);
432
+ }
433
+ };