gemstack-ai 1.2.0 → 1.3.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.
package/src/cli.js CHANGED
@@ -11,6 +11,7 @@ const installCommand = require('./commands/install');
11
11
  const verifyCommand = require('./commands/verify');
12
12
  const collectCommand = require('./commands/collect');
13
13
  const shipCommand = require('./commands/ship');
14
+ const contextCommand = require('./commands/context');
14
15
 
15
16
  async function main() {
16
17
  const { command, args, flags } = parser.parse(process.argv);
@@ -48,6 +49,7 @@ Options:
48
49
  case 'audit': await verifyCommand(flags); break;
49
50
  case 'collect': await collectCommand(flags); break;
50
51
  case 'ship': await shipCommand(flags); break;
52
+ case 'context': await contextCommand(args, flags); break;
51
53
  case 'list': await listCommand(flags); break;
52
54
  case 'show': await showCommand(args[0], flags); break;
53
55
  case 'hooks': hooksCommand.installHooks(flags.target); break;
@@ -0,0 +1,95 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const logger = require('../lib/logger');
4
+ const fssafe = require('../lib/filesystem-safe');
5
+ const {
6
+ generateContextCapsule,
7
+ validateContextCapsule
8
+ } = require('../lib/context-capsule');
9
+
10
+ async function contextCommand(args = [], flags = {}) {
11
+ const subcommand = args[0] || 'show';
12
+ const targetDir = flags.target ? path.resolve(flags.target) : process.cwd();
13
+
14
+ let activeSpec = flags.feature;
15
+ if (!activeSpec) {
16
+ const stateFile = path.join(targetDir, '.gemstack/state.json');
17
+ if (fs.existsSync(stateFile)) {
18
+ try {
19
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
20
+ activeSpec = state.active_spec;
21
+ } catch (_) {}
22
+ }
23
+ }
24
+
25
+ if (!activeSpec) {
26
+ // Check if specs/009-context-capsule or specs/current exists
27
+ if (fs.existsSync(path.join(targetDir, 'specs/009-context-capsule'))) {
28
+ activeSpec = 'specs/009-context-capsule';
29
+ } else if (fs.existsSync(path.join(targetDir, 'specs/current'))) {
30
+ activeSpec = 'specs/current';
31
+ } else {
32
+ logger.error('No active feature found or specified via --feature.');
33
+ process.exit(1);
34
+ }
35
+ }
36
+
37
+ switch (subcommand) {
38
+ case 'generate': {
39
+ logger.info(`Generating context capsule for "${activeSpec}" in: ${targetDir}`);
40
+ try {
41
+ const res = generateContextCapsule(targetDir, activeSpec);
42
+ logger.ok(`Context capsule generated successfully: ${res.path} (${res.byteLength} bytes, ${res.invariantsCount} invariants).`);
43
+ } catch (err) {
44
+ logger.error(`Generation failed: ${err.message}`);
45
+ process.exit(1);
46
+ }
47
+ break;
48
+ }
49
+
50
+ case 'show': {
51
+ const capsuleFile = path.join(targetDir, activeSpec, 'context-capsule.json');
52
+ if (!fs.existsSync(capsuleFile)) {
53
+ logger.error(`Context capsule not found at: ${capsuleFile}`);
54
+ process.exit(1);
55
+ }
56
+ const raw = fs.readFileSync(capsuleFile, 'utf8');
57
+ if (flags.json) {
58
+ console.log(raw);
59
+ } else {
60
+ const parsed = JSON.parse(raw);
61
+ console.log('=== Gemstack Context Capsule ===');
62
+ console.log(`Schema Version: ${parsed.schema_version}`);
63
+ console.log(`Project: ${parsed.project ? parsed.project.name : 'unknown'}`);
64
+ console.log(`Feature: ${parsed.project ? parsed.project.active_feature : 'unknown'}`);
65
+ console.log(`Lifecycle: ${parsed.project ? parsed.project.lifecycle_status : 'unknown'}`);
66
+ console.log(`Sources: ${parsed.provenance ? parsed.provenance.sources.length : 0} files`);
67
+ console.log(`Invariants: ${parsed.canonical_invariants ? parsed.canonical_invariants.length : 0} rules`);
68
+ console.log(`Contracts: ${parsed.frozen_contracts ? parsed.frozen_contracts.length : 0} contracts`);
69
+ console.log(`Acceptance: ${parsed.acceptance_matrix ? parsed.acceptance_matrix.total_required : 0} tests`);
70
+ }
71
+ break;
72
+ }
73
+
74
+ case 'verify': {
75
+ logger.info(`Auditing context capsule for "${activeSpec}" in: ${targetDir}`);
76
+ const res = validateContextCapsule(targetDir, activeSpec);
77
+ if (res.valid) {
78
+ logger.ok(`[OK] Context capsule is VALID and FRESH for ${activeSpec}.`);
79
+ } else {
80
+ logger.error(`[${res.state}] Context capsule validation failed.`);
81
+ for (const f of res.findings) {
82
+ logger.error(` - [${f.code}] ${f.message}`);
83
+ }
84
+ process.exit(1);
85
+ }
86
+ break;
87
+ }
88
+
89
+ default:
90
+ logger.error(`Unknown context subcommand: ${subcommand}. Valid subcommands: generate, show, verify.`);
91
+ process.exit(1);
92
+ }
93
+ }
94
+
95
+ module.exports = contextCommand;
@@ -376,6 +376,60 @@ module.exports = async (flags) => {
376
376
  logger.ok('Sin spec activa configurada para validación de evidencia de cierre.');
377
377
  }
378
378
 
379
+ // 5.1 Verificación de Políticas de Costos y Proveedores (Upgrade C - Read-Only)
380
+ const { loadCostLedger } = require('../lib/cost-ledger');
381
+ let costLedgerFound = false;
382
+ const ledgerCandidates = [];
383
+ if (loadedState && loadedState.active_spec) {
384
+ ledgerCandidates.push(fssafe.resolveSafe(targetDir, path.join(loadedState.active_spec, 'cost-ledger.json')));
385
+ }
386
+ ledgerCandidates.push(fssafe.resolveSafe(targetDir, 'cost-ledger.json'));
387
+ ledgerCandidates.push(fssafe.resolveSafe(targetDir, '.gemstack/cost-ledger.json'));
388
+
389
+ for (const lPath of ledgerCandidates) {
390
+ if (fs.existsSync(lPath)) {
391
+ costLedgerFound = true;
392
+ const res = loadCostLedger(lPath);
393
+ if (res.findings.length > 0) {
394
+ for (const f of res.findings) {
395
+ if (f.is_blocking) {
396
+ logger.error(`[COST_SAFETY_BLOCKER] ${f.code}: ${f.details}`);
397
+ totalErrors++;
398
+ } else {
399
+ logger.warn(`[COST_SAFETY_WARNING] ${f.code}: ${f.details}`);
400
+ totalWarnings++;
401
+ }
402
+ }
403
+ } else {
404
+ logger.ok(`Cost ledger verificado (${path.basename(lPath)}): íntegro y sin secretos.`);
405
+ }
406
+ break;
407
+ }
408
+ }
409
+
410
+ if (!costLedgerFound) {
411
+ logger.info('[LEGACY] [LEGACY_NO_PROVIDERS_DECLARED] No se detectaron declaraciones de costos o proveedores (Modo Legacy Provider-Free).');
412
+ }
413
+
414
+ // 5.2 Verificación de Context Capsule (Upgrade D - Read-Only)
415
+ logger.info('--- 5.2 Verificación de Context Capsule (Read-Only) ---');
416
+ if (loadedState && loadedState.active_spec) {
417
+ const { validateContextCapsule } = require('../lib/context-capsule');
418
+ const capResult = validateContextCapsule(targetDir, loadedState.active_spec);
419
+ if (capResult.valid) {
420
+ logger.ok(`Context capsule verificado y fresco (${loadedState.active_spec}/context-capsule.json).`);
421
+ } else if (capResult.state === 'MISSING') {
422
+ logger.info(`[LEGACY] No se detectó context-capsule.json en "${loadedState.active_spec}" (Modo Legacy Context-Free).`);
423
+ } else {
424
+ for (const f of capResult.findings) {
425
+ logger.error(`[${f.code}] ${f.message}`);
426
+ totalErrors++;
427
+ }
428
+ }
429
+ } else {
430
+ logger.ok('Sin spec activa configurada para verificación de context capsule.');
431
+ }
432
+
379
433
  // 6. Seguridad Local y Anti-Silent Failures en Tests
380
434
  logger.info('--- 6/6 Verificación de Seguridad y Test Runners ---');
381
435
  const envPath = fssafe.resolveSafe(targetDir, '.env');
@@ -334,6 +334,15 @@ function resolveRelevantFiles(rootPath, featureDir, planBindings, taskList, plan
334
334
  if (fs.existsSync(path.join(rootPath, planPath))) filesSet.add(planPath);
335
335
  if (fs.existsSync(path.join(rootPath, tasksPath))) filesSet.add(tasksPath);
336
336
 
337
+ const featureLedger = (relFeature + '/cost-ledger.json').replace(/^\.\//, '');
338
+ if (fs.existsSync(path.join(rootPath, featureLedger))) filesSet.add(featureLedger);
339
+ if (fs.existsSync(path.join(rootPath, 'cost-ledger.json'))) filesSet.add('cost-ledger.json');
340
+ if (fs.existsSync(path.join(rootPath, '.gemstack/cost-ledger.json'))) filesSet.add('.gemstack/cost-ledger.json');
341
+
342
+ const featureCapsule = (relFeature + '/context-capsule.json').replace(/^\.\//, '');
343
+ if (fs.existsSync(path.join(rootPath, featureCapsule))) filesSet.add(featureCapsule);
344
+ if (fs.existsSync(path.join(rootPath, '.gemstack/context-capsule.json'))) filesSet.add('.gemstack/context-capsule.json');
345
+
337
346
  for (const b of (planBindings || [])) {
338
347
  if (b.file && fs.existsSync(path.join(rootPath, b.file))) {
339
348
  filesSet.add(b.file);