gemstack-ai 1.1.2 → 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.
Files changed (45) hide show
  1. package/.agents/skills/gemstack-plan/SKILL.md +2 -1
  2. package/.agents/skills/gemstack-qa/SKILL.md +3 -0
  3. package/.agents/skills/gemstack-ship/SKILL.md +5 -1
  4. package/.agents/skills/gemstack-spec/SKILL.md +3 -2
  5. package/.agents/skills/gemstack-tasks/SKILL.md +4 -3
  6. package/.gemstack/state.json +7 -8
  7. package/CHANGELOG.md +75 -0
  8. package/README.md +36 -0
  9. package/RELEASE_NOTES.md +61 -0
  10. package/docs/architecture-consistency.md +14 -2
  11. package/docs/spec-driven-development.md +26 -0
  12. package/{gemstack-ai-1.1.2.tgz → gemstack-ai-1.3.0.tgz} +0 -0
  13. package/handoff.md +30 -15
  14. package/package.json +2 -2
  15. package/specs/007-mechanical-test-matrix-closure-evidence/.gemstack.json +5 -0
  16. package/specs/007-mechanical-test-matrix-closure-evidence/closure.json +59 -0
  17. package/specs/007-mechanical-test-matrix-closure-evidence/plan.md +484 -0
  18. package/specs/007-mechanical-test-matrix-closure-evidence/spec.md +597 -0
  19. package/specs/007-mechanical-test-matrix-closure-evidence/tasks.md +536 -0
  20. package/specs/008-cost-provider-safety-gates/.gemstack.json +5 -0
  21. package/specs/008-cost-provider-safety-gates/closure.json +59 -0
  22. package/specs/008-cost-provider-safety-gates/plan.md +456 -0
  23. package/specs/008-cost-provider-safety-gates/spec.md +633 -0
  24. package/specs/008-cost-provider-safety-gates/tasks.md +635 -0
  25. package/specs/009-context-capsule/closure.json +59 -0
  26. package/specs/009-context-capsule/context-capsule.json +428 -0
  27. package/specs/009-context-capsule/plan.md +663 -0
  28. package/specs/009-context-capsule/spec.md +913 -0
  29. package/specs/009-context-capsule/tasks.md +720 -0
  30. package/specs/templates/plan.md +30 -0
  31. package/specs/templates/spec.md +18 -0
  32. package/specs/templates/tasks.md +9 -0
  33. package/src/cli.js +8 -0
  34. package/src/commands/collect.js +340 -0
  35. package/src/commands/context.js +95 -0
  36. package/src/commands/ship.js +79 -0
  37. package/src/commands/verify.js +182 -6
  38. package/src/lib/closure-context.js +453 -0
  39. package/src/lib/context-capsule.js +594 -0
  40. package/src/lib/cost-ledger.js +355 -0
  41. package/src/lib/provider-boundary.js +186 -0
  42. package/src/lib/provider-registry.js +265 -0
  43. package/src/lib/runner-adapters.js +347 -0
  44. package/src/lib/safety-gates.js +277 -0
  45. package/src/lib/test-matrix.js +187 -0
@@ -47,3 +47,33 @@ ruta/archivo: [Razón]
47
47
  [
48
48
  ]
49
49
  ```
50
+
51
+ ## 7. Vinculación Física de Pruebas (Upgrade B)
52
+ <!--
53
+ Vincula cada test_id canónico de spec.md a un archivo físico ejecutable por el runner correspondiente.
54
+ -->
55
+ ```gemstack-test-bindings
56
+ [
57
+ {
58
+ "test_id": "TEST-FEATURE-A01",
59
+ "runner": "node:test",
60
+ "file": "tests/feature.test.js"
61
+ }
62
+ ]
63
+ ```
64
+
65
+ ## 8. Gates de Cierre de Proyecto (Upgrade B)
66
+ <!--
67
+ Declara scripts requeridos del package.json como compuertas obligatorias de cierre.
68
+ -->
69
+ ```gemstack-closure-gates
70
+ [
71
+ {
72
+ "id": "project-tests",
73
+ "type": "PACKAGE_SCRIPT",
74
+ "script": "test",
75
+ "requirement": "REQUIRED",
76
+ "waivable": false
77
+ }
78
+ ]
79
+ ```
@@ -50,3 +50,21 @@
50
50
  }
51
51
  ]
52
52
  ```
53
+
54
+ ## 7. Matriz Mecánica de Pruebas de Aceptación (Upgrade B)
55
+ <!--
56
+ Declara la matriz canónica de pruebas para cierre mecánico en el bloque gemstack-test-matrix.
57
+ Campos requeridos: id, category, layer, description, pass_criteria, gate (REQUIRED | SUPPLEMENTAL).
58
+ -->
59
+ ```gemstack-test-matrix
60
+ [
61
+ {
62
+ "id": "TEST-FEATURE-A01",
63
+ "category": "CORE",
64
+ "layer": "UNIT",
65
+ "description": "Verifica el comportamiento principal de la funcionalidad",
66
+ "pass_criteria": "Retorna resultado esperado bajo condiciones normales",
67
+ "gate": "REQUIRED"
68
+ }
69
+ ]
70
+ ```
@@ -1,3 +1,12 @@
1
+
2
+ <!--
3
+ Sintaxis de Metadatos de Tareas (Upgrade B):
4
+ Para cada tarea ejecutable, añade comentarios HTML que declaren:
5
+ <!-- gemstack:validation_required=true|false -->
6
+ <!-- gemstack:tests=TEST-ID-1,TEST-ID-2 -->
7
+ <!-- gemstack:files=src/file.js,tests/file.test.js -->
8
+ <!-- gemstack:depends=T001,T002 -->
9
+ -->
1
10
  # Tareas de Implementación
2
11
 
3
12
  <!--
package/src/cli.js CHANGED
@@ -9,6 +9,9 @@ const showCommand = require('./commands/show');
9
9
  const hooksCommand = require('./commands/hooks');
10
10
  const installCommand = require('./commands/install');
11
11
  const verifyCommand = require('./commands/verify');
12
+ const collectCommand = require('./commands/collect');
13
+ const shipCommand = require('./commands/ship');
14
+ const contextCommand = require('./commands/context');
12
15
 
13
16
  async function main() {
14
17
  const { command, args, flags } = parser.parse(process.argv);
@@ -20,6 +23,8 @@ Commands:
20
23
  update Update Gemstack-owned files
21
24
  doctor Check health of the installation
22
25
  verify Run complete integrity, state, memory and security audit (alias: audit)
26
+ collect Collect mechanical test matrix and closure evidence
27
+ ship Verify closure gates and transition feature to shipped
23
28
  list List available skills
24
29
  show Show content of a skill
25
30
  handoff Show content of handoff.md
@@ -42,6 +47,9 @@ Options:
42
47
  case 'doctor': await doctorCommand(flags); break;
43
48
  case 'verify':
44
49
  case 'audit': await verifyCommand(flags); break;
50
+ case 'collect': await collectCommand(flags); break;
51
+ case 'ship': await shipCommand(flags); break;
52
+ case 'context': await contextCommand(args, flags); break;
45
53
  case 'list': await listCommand(flags); break;
46
54
  case 'show': await showCommand(args[0], flags); break;
47
55
  case 'hooks': hooksCommand.installHooks(flags.target); break;
@@ -0,0 +1,340 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const fssafe = require('../lib/filesystem-safe');
4
+ const logger = require('../lib/logger');
5
+ const { hashFile } = require('../lib/hasher');
6
+ const { readState } = require('../lib/state');
7
+ const {
8
+ extractTestMatrixBlock,
9
+ validateTestMatrix,
10
+ computeAcceptanceSignature
11
+ } = require('../lib/test-matrix');
12
+ const {
13
+ parsePlanBindings,
14
+ parsePlanGates,
15
+ parseTaskMetadata,
16
+ reconcileTaskTraceability,
17
+ resolveRelevantFiles,
18
+ computeContentAggregateHash,
19
+ resolveRepositoryContext,
20
+ computeClosureContextHash
21
+ } = require('../lib/closure-context');
22
+ const {
23
+ executeNodeTestRunner,
24
+ parseNodeTestTap,
25
+ reconcileTestRun,
26
+ executePackageScriptGate,
27
+ generateClosureManifest
28
+ } = require('../lib/runner-adapters');
29
+
30
+ module.exports = async (flags = {}) => {
31
+ const targetDir = flags.target || process.cwd();
32
+ const state = readState(targetDir);
33
+
34
+ if (!state || !state.active_spec) {
35
+ logger.error('No active spec found in state.json.');
36
+ process.exit(1);
37
+ }
38
+
39
+ const activeSpec = state.active_spec;
40
+ const specDir = fssafe.resolveSafe(targetDir, activeSpec);
41
+ const specFile = path.join(specDir, 'spec.md');
42
+ const planFile = path.join(specDir, 'plan.md');
43
+ const tasksFile = path.join(specDir, 'tasks.md');
44
+
45
+ if (!fs.existsSync(specFile)) {
46
+ logger.error(`spec.md not found at ${specFile}`);
47
+ process.exit(1);
48
+ }
49
+
50
+ logger.info(`Iniciando recolección mecánica de evidencias para: ${activeSpec}`);
51
+
52
+ // 1. Parse SPEC test matrix
53
+ const specContent = fs.readFileSync(specFile, 'utf8');
54
+ const { matrix, isLegacy } = extractTestMatrixBlock(specContent);
55
+
56
+ if (isLegacy) {
57
+ logger.info(`[LEGACY] Spec "${activeSpec}" opera en modo legacy sin matriz de pruebas.`);
58
+ return;
59
+ }
60
+
61
+ let canonicalMatrix;
62
+ let acceptanceSignature;
63
+ try {
64
+ canonicalMatrix = validateTestMatrix(matrix);
65
+ acceptanceSignature = computeAcceptanceSignature(canonicalMatrix);
66
+ logger.ok(`Matriz de pruebas validada (${canonicalMatrix.length} pruebas canónicas, signature: ${acceptanceSignature.slice(0, 12)}...)`);
67
+ } catch (e) {
68
+ logger.error(`Error validando matriz de pruebas: ${e.message}`);
69
+ process.exit(1);
70
+ }
71
+
72
+ // 2. Parse PLAN bindings and gates
73
+ let planBindings = [];
74
+ let planGates = [];
75
+ if (fs.existsSync(planFile)) {
76
+ const planContent = fs.readFileSync(planFile, 'utf8');
77
+ try {
78
+ planBindings = parsePlanBindings(planContent);
79
+ planGates = parsePlanGates(planContent);
80
+ logger.ok(`Bindings de plan parseados (${planBindings.length} bindings, ${planGates.length} gates).`);
81
+ } catch (e) {
82
+ logger.error(`Error parseando plan.md: ${e.message}`);
83
+ process.exit(1);
84
+ }
85
+ } else {
86
+ logger.error(`plan.md no encontrado en ${planFile}`);
87
+ process.exit(1);
88
+ }
89
+
90
+ // 3. Parse TASKS metadata & traceability
91
+ let tasks = [];
92
+ let traceability = {
93
+ summary: {
94
+ tasks_total: 0,
95
+ tasks_with_validation: 0,
96
+ tasks_documentation_only: 0,
97
+ unmapped_canonical_tests: []
98
+ },
99
+ unmappedCanonical: [],
100
+ reverseMap: {}
101
+ };
102
+ if (fs.existsSync(tasksFile)) {
103
+ const tasksContent = fs.readFileSync(tasksFile, 'utf8');
104
+ try {
105
+ tasks = parseTaskMetadata(tasksContent);
106
+ traceability = reconcileTaskTraceability(canonicalMatrix, tasks);
107
+ if (traceability.unmappedCanonical.length === 0) {
108
+ logger.ok(`Trazabilidad TASK <-> TEST confirmada (${tasks.length} tareas totales).`);
109
+ } else {
110
+ logger.warn(`Pruebas canónicas no mapeadas a tareas: ${traceability.unmappedCanonical.join(', ')}`);
111
+ }
112
+ } catch (e) {
113
+ logger.error(`Error parseando tasks.md: ${e.message}`);
114
+ process.exit(1);
115
+ }
116
+ }
117
+
118
+ // 4. Execute Test Runner on bound files
119
+ const boundTestFiles = Array.from(new Set(planBindings.map(b => b.file)));
120
+ logger.info(`Ejecutando runner sobre ${boundTestFiles.length} archivo(s) de prueba vinculados...`);
121
+
122
+ const startTime = Date.now();
123
+ let runnerResult;
124
+ try {
125
+ runnerResult = await executeNodeTestRunner(targetDir, boundTestFiles);
126
+ } catch (e) {
127
+ logger.error(`Fallo crítico ejecutando test runner: ${e.message}`);
128
+ runnerResult = { exitCode: 1, stdout: '', stderr: e.message };
129
+ }
130
+ const runnerDuration = Date.now() - startTime;
131
+
132
+ const tapSummary = parseNodeTestTap(runnerResult.stdout);
133
+ const reconciliation = reconcileTestRun(canonicalMatrix, planBindings, tapSummary.tests);
134
+
135
+ logger.ok(`Runner completado (exitCode: ${runnerResult.exitCode}, physical: ${tapSummary.physicalTotal}, passed: ${tapSummary.passed}, failed: ${tapSummary.failed})`);
136
+
137
+ // 5. Execute Required & Supplemental Gates
138
+ const requiredGateResults = {};
139
+ const supplementalGateResults = {};
140
+ const blockers = [];
141
+ const warnings = [];
142
+
143
+ for (const gate of planGates) {
144
+ logger.info(`Ejecutando gate "${gate.id}" (${gate.type}: ${gate.script})...`);
145
+ if (gate.type === 'PACKAGE_SCRIPT') {
146
+ const gateRes = await executePackageScriptGate(targetDir, gate);
147
+ const passed = gateRes.exitCode === 0;
148
+ const status = passed ? 'PASS' : 'FAIL';
149
+
150
+ if (gate.requirement === 'REQUIRED') {
151
+ requiredGateResults[gate.id] = status;
152
+ if (!passed) {
153
+ blockers.push({
154
+ code: 'REQUIRED_GATE_FAILED',
155
+ gate: gate.id,
156
+ message: `Required gate "${gate.id}" failed with exit code ${gateRes.exitCode}`,
157
+ waivable: gate.waivable
158
+ });
159
+ }
160
+ } else {
161
+ supplementalGateResults[gate.id] = status;
162
+ if (!passed) {
163
+ warnings.push({
164
+ code: 'SUPPLEMENTAL_GATE_FAILED',
165
+ gate: gate.id,
166
+ message: `Supplemental gate "${gate.id}" failed with exit code ${gateRes.exitCode}`
167
+ });
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ // Check reconciliation anomalies
174
+ if (!reconciliation.mathValid) {
175
+ blockers.push({
176
+ code: 'CLOSURE_RECONCILIATION_FAILURE',
177
+ message: 'Arithmetic count mismatch between executed canonical/supporting events and total physical events'
178
+ });
179
+ }
180
+
181
+ for (const m of reconciliation.missing) {
182
+ blockers.push({
183
+ code: 'REQUIRED_TEST_MISSING',
184
+ canonicalId: m,
185
+ message: `Required canonical test "${m}" is missing a physical binding in plan.md`
186
+ });
187
+ }
188
+
189
+ for (const ne of reconciliation.notExecuted) {
190
+ blockers.push({
191
+ code: 'REQUIRED_TEST_NOT_EXECUTED',
192
+ canonicalId: ne,
193
+ message: `Required canonical test "${ne}" was bound in plan.md but not executed in test runner`
194
+ });
195
+ }
196
+
197
+ for (const ph of reconciliation.phantoms) {
198
+ blockers.push({
199
+ code: 'PHANTOM_TEST',
200
+ canonicalId: ph,
201
+ message: `Claimed test "${ph}" was absent from runner execution traces`
202
+ });
203
+ }
204
+
205
+ for (const orp of reconciliation.orphans) {
206
+ blockers.push({
207
+ code: 'ORPHAN_TEST',
208
+ canonicalId: orp,
209
+ message: `Physical test claimed canonical ID "${orp}" which is absent from spec.md matrix`
210
+ });
211
+ }
212
+
213
+ for (const dup of reconciliation.duplicates) {
214
+ blockers.push({
215
+ code: 'DUPLICATE_TEST_BINDING',
216
+ canonicalId: dup,
217
+ message: `Multiple physical tests claimed the same canonical ID "${dup}"`
218
+ });
219
+ }
220
+
221
+ // Check physical failures
222
+ const executedCanonicalPassed = tapSummary.tests.filter(t => t.id && t.rawOutcome === 'PASS');
223
+ const executedCanonicalFailed = tapSummary.tests.filter(t => t.id && t.rawOutcome === 'FAIL');
224
+ for (const f of executedCanonicalFailed) {
225
+ blockers.push({
226
+ code: 'REQUIRED_TEST_FAILED',
227
+ canonicalId: f.id,
228
+ message: `Required canonical test "${f.id}" failed during execution`
229
+ });
230
+ }
231
+
232
+ // Transfer traceability unmapped errors if any
233
+ for (const unmappedId of traceability.unmappedCanonical) {
234
+ blockers.push({
235
+ code: 'UNMAPPED_CANONICAL_TEST',
236
+ canonicalId: unmappedId,
237
+ message: `Required canonical test "${unmappedId}" is not bound to any implementation task`
238
+ });
239
+ }
240
+
241
+ // 6. Compute Closure Context Hash & Manifest
242
+ const repoContext = resolveRepositoryContext(targetDir);
243
+
244
+ const phaseHashes = {
245
+ spec: hashFile(specFile),
246
+ plan: hashFile(planFile),
247
+ tasks: fs.existsSync(tasksFile) ? hashFile(tasksFile) : null
248
+ };
249
+
250
+ const relevantFiles = resolveRelevantFiles(targetDir, activeSpec, planBindings, tasks, planGates);
251
+ const relevantFilesDigest = computeContentAggregateHash(targetDir, relevantFiles);
252
+
253
+ const testFilesHash = computeContentAggregateHash(targetDir, boundTestFiles);
254
+ const implementationFiles = Array.from(new Set(tasks.flatMap(t => t.files || [])))
255
+ .filter(f => !f.endsWith('closure.json'));
256
+ const implementationContextHash = computeContentAggregateHash(targetDir, implementationFiles);
257
+ const requiredGateDefinitionHash = computeContentAggregateHash(targetDir, ['package.json']);
258
+
259
+ const contextObj = {
260
+ version: 1,
261
+ repository: repoContext,
262
+ phase_hashes: phaseHashes,
263
+ acceptance_signature: acceptanceSignature,
264
+ test_files_hash: testFilesHash,
265
+ implementation_context_hash: implementationContextHash,
266
+ required_gate_definition_hash: requiredGateDefinitionHash
267
+ };
268
+
269
+ const closureContextHash = computeClosureContextHash(contextObj);
270
+
271
+ // Status determination
272
+ let status = 'VERIFIED';
273
+ if (blockers.length > 0) {
274
+ status = 'BLOCKED';
275
+ }
276
+
277
+ const manifestData = {
278
+ feature: activeSpec,
279
+ generated_at: new Date().toISOString(),
280
+ status,
281
+ closure_context: {
282
+ closure_context_hash: closureContextHash,
283
+ repository_type: repoContext.type,
284
+ git_commit: repoContext.commit,
285
+ working_tree_clean: repoContext.working_tree_clean,
286
+ relevant_files_digest: relevantFilesDigest
287
+ },
288
+ acceptance_signature: acceptanceSignature,
289
+ canonical_summary: {
290
+ required_total: canonicalMatrix.filter(c => c.gate === 'REQUIRED').length,
291
+ required_passed: executedCanonicalPassed.length,
292
+ supplemental_total: canonicalMatrix.filter(c => c.gate === 'SUPPLEMENTAL').length,
293
+ supplemental_passed: 0
294
+ },
295
+ physical_summary: {
296
+ supporting_total: reconciliation.supportingCount,
297
+ supporting_passed: reconciliation.supportingCount - (tapSummary.failed - executedCanonicalFailed.length),
298
+ total_executed: tapSummary.physicalTotal,
299
+ total_passed: tapSummary.passed,
300
+ total_failed: tapSummary.failed,
301
+ total_skipped: tapSummary.skipped + tapSummary.todo + tapSummary.cancelled
302
+ },
303
+ reconciliation: {
304
+ math_valid: reconciliation.mathValid,
305
+ phantoms_detected: reconciliation.phantoms.length,
306
+ orphans_detected: reconciliation.orphans.length,
307
+ missing_canonical_ids: reconciliation.missing
308
+ },
309
+ task_traceability_summary: {
310
+ tasks_total: tasks.length,
311
+ tasks_with_validation: traceability.summary.tasks_with_validation,
312
+ tasks_documentation_only: traceability.summary.tasks_documentation_only,
313
+ unmapped_canonical_tests: traceability.unmappedCanonical
314
+ },
315
+ required_gates: requiredGateResults,
316
+ supplemental_gates: supplementalGateResults,
317
+ exceptions: [],
318
+ evidence_sources: [
319
+ {
320
+ type: 'PACKAGE_SCRIPT',
321
+ script: 'test',
322
+ runner: 'node:test',
323
+ exit_code: runnerResult.exitCode,
324
+ duration_ms: runnerDuration
325
+ }
326
+ ],
327
+ blockers,
328
+ warnings
329
+ };
330
+
331
+ if (flags.dryRun) {
332
+ logger.info('[DRY RUN] Manifest generado en memoria (no persistido en disco):');
333
+ console.log(JSON.stringify(manifestData, null, 2));
334
+ return manifestData;
335
+ }
336
+
337
+ const writtenManifest = generateClosureManifest(specDir, manifestData);
338
+ logger.ok(`Manifest de cierre persistido en: ${path.join(specDir, 'closure.json')} (status: ${status})`);
339
+ return writtenManifest;
340
+ };
@@ -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;
@@ -0,0 +1,79 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const fssafe = require('../lib/filesystem-safe');
4
+ const logger = require('../lib/logger');
5
+ const { readState, writeStateAtomic } = require('../lib/state');
6
+ const { extractTestMatrixBlock } = require('../lib/test-matrix');
7
+
8
+ module.exports = async (flags = {}) => {
9
+ const targetDir = flags.target || process.cwd();
10
+ const state = readState(targetDir);
11
+
12
+ if (!state || !state.active_spec) {
13
+ const err = new Error('No active spec found to ship');
14
+ err.code = 'NO_ACTIVE_SPEC';
15
+ throw err;
16
+ }
17
+
18
+ const activeSpec = state.active_spec;
19
+ const specDir = fssafe.resolveSafe(targetDir, activeSpec);
20
+ const specFile = path.join(specDir, 'spec.md');
21
+
22
+ if (!fs.existsSync(specFile)) {
23
+ const err = new Error(`spec.md not found at ${specFile}`);
24
+ err.code = 'SPEC_NOT_FOUND';
25
+ throw err;
26
+ }
27
+
28
+ const specContent = fs.readFileSync(specFile, 'utf8');
29
+ const { matrix, isLegacy } = extractTestMatrixBlock(specContent);
30
+
31
+ if (isLegacy) {
32
+ logger.info(`[LEGACY] Spec "${activeSpec}" opera en modo legacy sin matriz de pruebas.`);
33
+ // Transition lifecycle
34
+ state.last_completed_feature = activeSpec;
35
+ state.active_spec = null;
36
+ state.current_phase = 'shipped';
37
+ state.status = 'SHIPPED';
38
+ state.last_update = new Date().toISOString();
39
+ writeStateAtomic(targetDir, state);
40
+ logger.ok(`Feature "${activeSpec}" enviada exitosamente en modo legacy.`);
41
+ return { shipped: true, legacy: true, feature: activeSpec };
42
+ }
43
+
44
+ // Structured Upgrade B closure verification
45
+ const closurePath = path.join(specDir, 'closure.json');
46
+ if (!fs.existsSync(closurePath)) {
47
+ const err = new Error(`Cannot ship "${activeSpec}": closure.json not found. Run "gemstack collect" first.`);
48
+ err.code = 'CLOSURE_MANIFEST_MISSING';
49
+ throw err;
50
+ }
51
+
52
+ let manifest;
53
+ try {
54
+ manifest = JSON.parse(fs.readFileSync(closurePath, 'utf8'));
55
+ } catch (e) {
56
+ const err = new Error(`Failed to parse closure.json: ${e.message}`);
57
+ err.code = 'CLOSURE_MANIFEST_INVALID';
58
+ throw err;
59
+ }
60
+
61
+ const allowedStatuses = ['VERIFIED', 'VERIFIED_WITH_EXCEPTIONS'];
62
+ if (!allowedStatuses.includes(manifest.status)) {
63
+ const blockersSummary = (manifest.blockers || []).map(b => b.code || b.message).join(', ');
64
+ const err = new Error(`Cannot ship "${activeSpec}": closure status is "${manifest.status}". Blockers: ${blockersSummary || 'none'}`);
65
+ err.code = 'CLOSURE_NOT_VERIFIED';
66
+ throw err;
67
+ }
68
+
69
+ // Transition lifecycle to SHIPPED
70
+ state.last_completed_feature = activeSpec;
71
+ state.active_spec = null;
72
+ state.current_phase = 'shipped';
73
+ state.status = 'SHIPPED';
74
+ state.last_update = new Date().toISOString();
75
+ writeStateAtomic(targetDir, state);
76
+
77
+ logger.ok(`Feature "${activeSpec}" verificada mecánicamente y enviada exitosamente (status: ${manifest.status}).`);
78
+ return { shipped: true, legacy: false, feature: activeSpec, status: manifest.status };
79
+ };