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/.gemstack/state.json +8 -7
- package/CHANGELOG.md +43 -0
- package/README.md +23 -0
- package/RELEASE_NOTES.md +37 -0
- package/{gemstack-ai-1.2.0.tgz → gemstack-ai-1.3.0.tgz} +0 -0
- package/handoff.md +14 -12
- package/package.json +2 -2
- package/specs/008-cost-provider-safety-gates/.gemstack.json +5 -0
- package/specs/008-cost-provider-safety-gates/closure.json +59 -0
- package/specs/008-cost-provider-safety-gates/plan.md +456 -0
- package/specs/008-cost-provider-safety-gates/spec.md +633 -0
- package/specs/008-cost-provider-safety-gates/tasks.md +635 -0
- package/specs/009-context-capsule/closure.json +59 -0
- package/specs/009-context-capsule/context-capsule.json +428 -0
- package/specs/009-context-capsule/plan.md +663 -0
- package/specs/009-context-capsule/spec.md +913 -0
- package/specs/009-context-capsule/tasks.md +720 -0
- package/src/cli.js +2 -0
- package/src/commands/context.js +95 -0
- package/src/commands/verify.js +54 -0
- package/src/lib/closure-context.js +9 -0
- package/src/lib/context-capsule.js +594 -0
- package/src/lib/cost-ledger.js +355 -0
- package/src/lib/provider-boundary.js +186 -0
- package/src/lib/provider-registry.js +265 -0
- package/src/lib/safety-gates.js +277 -0
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;
|
package/src/commands/verify.js
CHANGED
|
@@ -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);
|