@hongmaple0820/scale-engine 0.9.0 → 0.10.1

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 (103) hide show
  1. package/README.en.md +127 -179
  2. package/README.md +168 -1094
  3. package/dist/agents/AgentSourceLoader.js +1 -1
  4. package/dist/agents/AgentSourceLoader.js.map +1 -1
  5. package/dist/agents/types.d.ts +1 -1
  6. package/dist/api/cli.js +222 -6
  7. package/dist/api/cli.js.map +1 -1
  8. package/dist/api/quickstart.d.ts +23 -0
  9. package/dist/api/quickstart.js +57 -0
  10. package/dist/api/quickstart.js.map +1 -0
  11. package/dist/artifact/types.d.ts +5 -1
  12. package/dist/artifact/types.js.map +1 -1
  13. package/dist/capabilities/BrowserCapability.d.ts +30 -0
  14. package/dist/capabilities/BrowserCapability.js +73 -0
  15. package/dist/capabilities/BrowserCapability.js.map +1 -0
  16. package/dist/capabilities/CapabilityRegistry.d.ts +17 -0
  17. package/dist/capabilities/CapabilityRegistry.js +65 -0
  18. package/dist/capabilities/CapabilityRegistry.js.map +1 -0
  19. package/dist/capabilities/ComputerCapability.d.ts +28 -0
  20. package/dist/capabilities/ComputerCapability.js +40 -0
  21. package/dist/capabilities/ComputerCapability.js.map +1 -0
  22. package/dist/capabilities/InstalledSkillsIntegration.d.ts +66 -0
  23. package/dist/capabilities/InstalledSkillsIntegration.js +188 -0
  24. package/dist/capabilities/InstalledSkillsIntegration.js.map +1 -0
  25. package/dist/capabilities/SearchCapability.d.ts +46 -0
  26. package/dist/capabilities/SearchCapability.js +88 -0
  27. package/dist/capabilities/SearchCapability.js.map +1 -0
  28. package/dist/capabilities/index.d.ts +6 -0
  29. package/dist/capabilities/index.js +9 -0
  30. package/dist/capabilities/index.js.map +1 -0
  31. package/dist/capabilities/types.d.ts +92 -0
  32. package/dist/capabilities/types.js +7 -0
  33. package/dist/capabilities/types.js.map +1 -0
  34. package/dist/cli/liteCommands.js +1 -1
  35. package/dist/cli/liteCommands.js.map +1 -1
  36. package/dist/cli/phaseCommands.d.ts +42 -3
  37. package/dist/cli/phaseCommands.js +490 -149
  38. package/dist/cli/phaseCommands.js.map +1 -1
  39. package/dist/core/logger.js +9 -2
  40. package/dist/core/logger.js.map +1 -1
  41. package/dist/hooks/HookGeneratorEnhanced.js +84 -5
  42. package/dist/hooks/HookGeneratorEnhanced.js.map +1 -1
  43. package/dist/hooks/WorkflowHooksManager.d.ts +30 -0
  44. package/dist/hooks/WorkflowHooksManager.js +117 -0
  45. package/dist/hooks/WorkflowHooksManager.js.map +1 -0
  46. package/dist/hooks/index.d.ts +2 -0
  47. package/dist/hooks/index.js +2 -1
  48. package/dist/hooks/index.js.map +1 -1
  49. package/dist/skills/SkillExecutor.d.ts +11 -1
  50. package/dist/skills/SkillExecutor.js +160 -5
  51. package/dist/skills/SkillExecutor.js.map +1 -1
  52. package/dist/workflow/EvidenceStore.d.ts +20 -0
  53. package/dist/workflow/EvidenceStore.js +48 -0
  54. package/dist/workflow/EvidenceStore.js.map +1 -0
  55. package/dist/workflow/ReviewAnalyzer.d.ts +33 -0
  56. package/dist/workflow/ReviewAnalyzer.js +264 -0
  57. package/dist/workflow/ReviewAnalyzer.js.map +1 -0
  58. package/dist/workflow/ReviewStore.d.ts +32 -0
  59. package/dist/workflow/ReviewStore.js +42 -0
  60. package/dist/workflow/ReviewStore.js.map +1 -0
  61. package/dist/workflow/VerificationCommands.d.ts +23 -0
  62. package/dist/workflow/VerificationCommands.js +125 -0
  63. package/dist/workflow/VerificationCommands.js.map +1 -0
  64. package/dist/workflow/WorkflowEngine.d.ts +62 -0
  65. package/dist/workflow/WorkflowEngine.js +151 -0
  66. package/dist/workflow/WorkflowEngine.js.map +1 -0
  67. package/dist/workflow/cognitive/AmbiguityScorer.d.ts +17 -0
  68. package/dist/workflow/cognitive/AmbiguityScorer.js +107 -0
  69. package/dist/workflow/cognitive/AmbiguityScorer.js.map +1 -0
  70. package/dist/workflow/cognitive/ConsensusPlanner.d.ts +26 -0
  71. package/dist/workflow/cognitive/ConsensusPlanner.js +141 -0
  72. package/dist/workflow/cognitive/ConsensusPlanner.js.map +1 -0
  73. package/dist/workflow/cognitive/SocraticQuestioner.d.ts +33 -0
  74. package/dist/workflow/cognitive/SocraticQuestioner.js +276 -0
  75. package/dist/workflow/cognitive/SocraticQuestioner.js.map +1 -0
  76. package/dist/workflow/execution/RalphEngine.d.ts +36 -0
  77. package/dist/workflow/execution/RalphEngine.js +123 -0
  78. package/dist/workflow/execution/RalphEngine.js.map +1 -0
  79. package/dist/workflow/execution/UltraworkEngine.d.ts +43 -0
  80. package/dist/workflow/execution/UltraworkEngine.js +135 -0
  81. package/dist/workflow/execution/UltraworkEngine.js.map +1 -0
  82. package/dist/workflow/gates/GateSystem.d.ts +130 -0
  83. package/dist/workflow/gates/GateSystem.js +788 -0
  84. package/dist/workflow/gates/GateSystem.js.map +1 -0
  85. package/dist/workflow/index.d.ts +12 -0
  86. package/dist/workflow/index.js +14 -0
  87. package/dist/workflow/index.js.map +1 -0
  88. package/dist/workflow/quality/HonestDelivery.d.ts +19 -0
  89. package/dist/workflow/quality/HonestDelivery.js +77 -0
  90. package/dist/workflow/quality/HonestDelivery.js.map +1 -0
  91. package/dist/workflow/quality/KarpathyEvaluator.d.ts +18 -0
  92. package/dist/workflow/quality/KarpathyEvaluator.js +76 -0
  93. package/dist/workflow/quality/KarpathyEvaluator.js.map +1 -0
  94. package/dist/workflow/types.d.ts +146 -0
  95. package/dist/workflow/types.js +4 -0
  96. package/dist/workflow/types.js.map +1 -0
  97. package/dist/workflows/DAGBuilder.js +1 -1
  98. package/dist/workflows/DAGBuilder.js.map +1 -1
  99. package/dist/workflows/WorkflowOrchestrator.js +1 -1
  100. package/dist/workflows/WorkflowOrchestrator.js.map +1 -1
  101. package/dist/workflows/index.js +1 -1
  102. package/dist/workflows/index.js.map +1 -1
  103. package/package.json +3 -3
@@ -0,0 +1,788 @@
1
+ // SCALE Engine - Gate System
2
+ // Quality gate system G0-G7.
3
+ import { EvidenceStore } from '../EvidenceStore.js';
4
+ import { detectVerificationCommands } from '../VerificationCommands.js';
5
+ import { execa } from 'execa';
6
+ import { createHash } from 'node:crypto';
7
+ function tail(value, maxLength = 1000) {
8
+ return value.length > maxLength ? value.slice(-maxLength) : value;
9
+ }
10
+ function sha256(value) {
11
+ return createHash('sha256').update(value).digest('hex');
12
+ }
13
+ export async function runShellCommand(command, timeout) {
14
+ const start = Date.now();
15
+ const cwd = process.cwd();
16
+ try {
17
+ const result = await execa(command, {
18
+ shell: true,
19
+ timeout,
20
+ reject: false,
21
+ all: false,
22
+ });
23
+ return {
24
+ code: result.exitCode ?? 1,
25
+ stdout: result.stdout ?? '',
26
+ stderr: result.stderr ?? '',
27
+ durationMs: Date.now() - start,
28
+ startedAt: start,
29
+ endedAt: Date.now(),
30
+ cwd,
31
+ };
32
+ }
33
+ catch (error) {
34
+ return {
35
+ code: 1,
36
+ stdout: '',
37
+ stderr: error instanceof Error ? error.message : String(error),
38
+ durationMs: Date.now() - start,
39
+ startedAt: start,
40
+ endedAt: Date.now(),
41
+ cwd,
42
+ };
43
+ }
44
+ }
45
+ function createEvidence(input) {
46
+ return {
47
+ id: `EVID-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
48
+ ...input,
49
+ };
50
+ }
51
+ function textEvidence(items) {
52
+ return items.map(item => `${item.label}: ${item.detail}`).join('\n');
53
+ }
54
+ export class GateSystem {
55
+ constructor(eventBus, commandConfig = {}) {
56
+ this.gates = new Map();
57
+ this.results = new Map();
58
+ this.eventBus = eventBus;
59
+ this.evidenceStore = new EvidenceStore();
60
+ this.commands = detectVerificationCommands(process.cwd(), commandConfig);
61
+ this.registerDefaultGates();
62
+ }
63
+ registerGate(gate) {
64
+ this.gates.set(gate.stage, gate);
65
+ }
66
+ async executeGate(stage) {
67
+ const gate = this.gates.get(stage);
68
+ if (!gate) {
69
+ const evidenceItems = [
70
+ createEvidence({
71
+ kind: 'manual',
72
+ label: 'Gate registry',
73
+ passed: false,
74
+ detail: `Gate ${stage} is not registered`,
75
+ }),
76
+ ];
77
+ return {
78
+ gate: stage,
79
+ status: 'FAILED',
80
+ passed: false,
81
+ evidence: textEvidence(evidenceItems),
82
+ evidenceItems,
83
+ blockers: [],
84
+ durationMs: 0,
85
+ };
86
+ }
87
+ const start = Date.now();
88
+ try {
89
+ const result = await gate.execute();
90
+ result.durationMs = Date.now() - start;
91
+ this.results.set(stage, result);
92
+ this.persistEvidence(result);
93
+ this.eventBus.emit('gate.executed', { stage, passed: result.passed });
94
+ return result;
95
+ }
96
+ catch (e) {
97
+ const result = {
98
+ gate: stage,
99
+ status: 'FAILED',
100
+ passed: false,
101
+ evidence: `Gate execution failed: ${e}`,
102
+ evidenceItems: [
103
+ createEvidence({
104
+ kind: 'manual',
105
+ label: 'Gate execution',
106
+ passed: false,
107
+ detail: String(e),
108
+ }),
109
+ ],
110
+ blockers: [String(e)],
111
+ durationMs: Date.now() - start
112
+ };
113
+ this.results.set(stage, result);
114
+ this.persistEvidence(result);
115
+ return result;
116
+ }
117
+ }
118
+ persistEvidence(result) {
119
+ try {
120
+ const record = this.evidenceStore.saveGateResult(result);
121
+ result.evidenceRecordId = record.id;
122
+ }
123
+ catch {
124
+ // Evidence persistence must not mask the gate decision itself.
125
+ }
126
+ }
127
+ async executeAll(order = ['G0', 'G1', 'G2', 'G3', 'G4', 'G5', 'G6', 'G7']) {
128
+ const results = [];
129
+ for (const stage of order) {
130
+ const result = await this.executeGate(stage);
131
+ results.push(result);
132
+ if (!result.passed && stage !== 'G1' && stage !== 'G2') {
133
+ this.eventBus.emit('gate.blocked', { stage, blockers: result.blockers });
134
+ break;
135
+ }
136
+ }
137
+ return results;
138
+ }
139
+ getResult(stage) {
140
+ return this.results.get(stage);
141
+ }
142
+ getAllResults() {
143
+ return this.results;
144
+ }
145
+ registerDefaultGates() {
146
+ this.registerGate(new ExplorationGate());
147
+ this.registerGate(new PlanningGate());
148
+ this.registerGate(new TDDGate(this.commands.tddEvidence, this.commands.tddStrict));
149
+ this.registerGate(new BuildGate(this.commands.build));
150
+ this.registerGate(new LintGate(this.commands.lint));
151
+ this.registerGate(new TestGate(this.commands.test));
152
+ this.registerGate(new CoverageGate(this.commands.coverage));
153
+ this.registerGate(new SecurityGate());
154
+ }
155
+ }
156
+ function missingCommandResult(stage, label, command) {
157
+ const evidenceItems = [
158
+ createEvidence({
159
+ kind: 'command',
160
+ label,
161
+ passed: false,
162
+ detail: command.reason,
163
+ }),
164
+ ];
165
+ return {
166
+ gate: stage,
167
+ status: 'BLOCKED',
168
+ passed: false,
169
+ evidence: textEvidence(evidenceItems),
170
+ evidenceItems,
171
+ blockers: [command.reason],
172
+ durationMs: 0,
173
+ };
174
+ }
175
+ function commandEvidence(label, command, passed, commandResult, fallbackDetail = 'command did not complete') {
176
+ const output = commandResult ? `${commandResult.stdout}\n${commandResult.stderr}` : '';
177
+ return createEvidence({
178
+ kind: 'command',
179
+ label,
180
+ passed,
181
+ command: command.command,
182
+ exitCode: commandResult?.code,
183
+ durationMs: commandResult?.durationMs,
184
+ cwd: commandResult?.cwd,
185
+ startedAt: commandResult?.startedAt,
186
+ endedAt: commandResult?.endedAt,
187
+ stdoutTail: commandResult ? tail(commandResult.stdout) : undefined,
188
+ stderrTail: commandResult ? tail(commandResult.stderr) : undefined,
189
+ outputHash: output ? sha256(output) : undefined,
190
+ source: command.source,
191
+ detail: commandResult
192
+ ? `${command.reason}\n${tail(commandResult.stdout || commandResult.stderr || `exit code ${commandResult.code}`, 500)}`
193
+ : fallbackDetail,
194
+ });
195
+ }
196
+ export class ExplorationGate {
197
+ constructor() {
198
+ this.stage = 'G1';
199
+ this.name = 'Exploration';
200
+ this.description = 'Project knowledge file, knowledge graph, and contradiction analysis checks';
201
+ this.requiredLevel = 'M';
202
+ }
203
+ async execute() {
204
+ const blockers = [];
205
+ const knowledgeFile = await this.findKnowledgeFile();
206
+ if (!knowledgeFile) {
207
+ blockers.push('No project knowledge file found');
208
+ }
209
+ const hasKnowledgeGraph = await this.checkKnowledgeGraph();
210
+ const evidenceItems = [
211
+ createEvidence({
212
+ kind: 'file',
213
+ label: 'Project knowledge file',
214
+ passed: Boolean(knowledgeFile),
215
+ path: knowledgeFile ?? undefined,
216
+ detail: knowledgeFile ? `found ${knowledgeFile}` : 'missing AGENTS.md, CLAUDE.md, .cursorrules, and GEMINI.md',
217
+ }),
218
+ createEvidence({
219
+ kind: 'file',
220
+ label: 'Knowledge graph',
221
+ passed: hasKnowledgeGraph,
222
+ path: 'graphify-out/GRAPH_REPORT.md',
223
+ detail: hasKnowledgeGraph ? 'available' : 'not available',
224
+ }),
225
+ ];
226
+ const passed = blockers.length === 0;
227
+ return {
228
+ gate: this.stage,
229
+ status: passed ? 'PASSED' : 'BLOCKED',
230
+ passed,
231
+ evidence: textEvidence(evidenceItems),
232
+ evidenceItems,
233
+ blockers
234
+ };
235
+ }
236
+ async findKnowledgeFile() {
237
+ const fs = await import('fs/promises');
238
+ const candidates = ['AGENTS.md', 'CLAUDE.md', '.cursorrules', 'GEMINI.md'];
239
+ for (const candidate of candidates) {
240
+ try {
241
+ await fs.access(candidate);
242
+ return candidate;
243
+ }
244
+ catch {
245
+ // Try the next platform-specific knowledge file.
246
+ }
247
+ }
248
+ return null;
249
+ }
250
+ async checkKnowledgeGraph() {
251
+ try {
252
+ const fs = await import('fs/promises');
253
+ await fs.access('graphify-out/GRAPH_REPORT.md');
254
+ return true;
255
+ }
256
+ catch {
257
+ return false;
258
+ }
259
+ }
260
+ }
261
+ export class PlanningGate {
262
+ constructor() {
263
+ this.stage = 'G2';
264
+ this.name = 'Planning';
265
+ this.description = 'Mini-Spec or SDD planning artifact checks';
266
+ this.requiredLevel = 'L';
267
+ }
268
+ async execute() {
269
+ const blockers = [];
270
+ const hasSpec = await this.checkSpecDocument();
271
+ if (!hasSpec) {
272
+ blockers.push('Spec document not found');
273
+ }
274
+ const evidenceItems = [
275
+ createEvidence({
276
+ kind: 'file',
277
+ label: 'Spec document',
278
+ passed: hasSpec,
279
+ path: '.scale/specs',
280
+ detail: hasSpec ? 'spec directory contains at least one markdown spec' : 'missing spec directory or markdown spec',
281
+ }),
282
+ ];
283
+ const passed = blockers.length === 0;
284
+ return {
285
+ gate: this.stage,
286
+ status: passed ? 'PASSED' : 'BLOCKED',
287
+ passed,
288
+ evidence: textEvidence(evidenceItems),
289
+ evidenceItems,
290
+ blockers
291
+ };
292
+ }
293
+ async checkSpecDocument() {
294
+ try {
295
+ const fs = await import('fs/promises');
296
+ const specDir = '.scale/specs';
297
+ const entries = await fs.readdir(specDir);
298
+ return entries.some(entry => entry.endsWith('.md'));
299
+ }
300
+ catch {
301
+ return false;
302
+ }
303
+ }
304
+ }
305
+ export class TDDGate {
306
+ constructor(evidencePath, strict = false) {
307
+ this.evidencePath = evidencePath;
308
+ this.strict = strict;
309
+ this.stage = 'G3';
310
+ this.name = 'TDD';
311
+ this.description = 'RED -> GREEN -> REFACTOR evidence check';
312
+ this.requiredLevel = 'CRITICAL';
313
+ }
314
+ async execute() {
315
+ if (this.evidencePath) {
316
+ return this.verifyEvidenceFile(this.evidencePath);
317
+ }
318
+ const detail = this.strict
319
+ ? 'TDD evidence file is required in strict mode'
320
+ : 'TDD cycle not strictly verified; provide --tdd-evidence or use --tdd-strict to enforce';
321
+ const evidenceItems = [
322
+ createEvidence({
323
+ kind: 'manual',
324
+ label: 'TDD cycle',
325
+ passed: !this.strict,
326
+ detail,
327
+ source: 'tdd-gate',
328
+ }),
329
+ ];
330
+ return {
331
+ gate: this.stage,
332
+ status: this.strict ? 'BLOCKED' : 'PASSED',
333
+ passed: !this.strict,
334
+ evidence: textEvidence(evidenceItems),
335
+ evidenceItems,
336
+ blockers: this.strict ? [detail] : [],
337
+ durationMs: 0
338
+ };
339
+ }
340
+ async verifyEvidenceFile(path) {
341
+ const fs = await import('fs/promises');
342
+ const blockers = [];
343
+ let parsed;
344
+ let content = '';
345
+ try {
346
+ content = await fs.readFile(path, 'utf-8');
347
+ parsed = JSON.parse(content);
348
+ }
349
+ catch (error) {
350
+ blockers.push(`TDD evidence could not be read: ${error instanceof Error ? error.message : String(error)}`);
351
+ }
352
+ const evidence = parsed;
353
+ if (!blockers.length) {
354
+ if (evidence.red !== true)
355
+ blockers.push('TDD evidence missing red=true');
356
+ if (evidence.green !== true)
357
+ blockers.push('TDD evidence missing green=true');
358
+ if (evidence.refactor !== true)
359
+ blockers.push('TDD evidence missing refactor=true');
360
+ if (evidence.testFirst !== true)
361
+ blockers.push('TDD evidence missing testFirst=true');
362
+ }
363
+ const passed = blockers.length === 0;
364
+ const evidenceItems = [
365
+ createEvidence({
366
+ kind: 'file',
367
+ label: 'TDD evidence',
368
+ passed,
369
+ path,
370
+ detail: passed ? 'TDD evidence contains red/green/refactor/testFirst=true' : blockers.join('; '),
371
+ outputHash: content ? sha256(content) : undefined,
372
+ source: 'tdd-evidence',
373
+ }),
374
+ ];
375
+ return {
376
+ gate: this.stage,
377
+ status: passed ? 'PASSED' : 'BLOCKED',
378
+ passed,
379
+ evidence: textEvidence(evidenceItems),
380
+ evidenceItems,
381
+ blockers,
382
+ durationMs: 0,
383
+ };
384
+ }
385
+ }
386
+ export class BuildGate {
387
+ constructor(command) {
388
+ this.command = command;
389
+ this.stage = 'G0';
390
+ this.name = 'Build';
391
+ this.description = 'Run configured build or typecheck command';
392
+ this.requiredLevel = 'ALWAYS';
393
+ }
394
+ async execute() {
395
+ if (!this.command.command) {
396
+ return missingCommandResult(this.stage, 'Build command', this.command);
397
+ }
398
+ const blockers = [];
399
+ let commandResult = null;
400
+ try {
401
+ commandResult = await runShellCommand(this.command.command, 120000);
402
+ if (commandResult.code !== 0) {
403
+ blockers.push(`Build failed: ${commandResult.stderr}`);
404
+ }
405
+ }
406
+ catch (e) {
407
+ blockers.push(`Build execution failed: ${e}`);
408
+ }
409
+ const passed = blockers.length === 0;
410
+ const evidenceItems = [
411
+ commandEvidence('Build command', this.command, passed, commandResult),
412
+ ];
413
+ return {
414
+ gate: this.stage,
415
+ status: passed ? 'PASSED' : 'FAILED',
416
+ passed,
417
+ evidence: textEvidence(evidenceItems),
418
+ evidenceItems,
419
+ blockers
420
+ };
421
+ }
422
+ }
423
+ export class LintGate {
424
+ constructor(command) {
425
+ this.command = command;
426
+ this.stage = 'G4';
427
+ this.name = 'Lint';
428
+ this.description = 'Run configured lint command';
429
+ this.requiredLevel = 'ALWAYS';
430
+ }
431
+ async execute() {
432
+ if (!this.command.command) {
433
+ return missingCommandResult(this.stage, 'Lint command', this.command);
434
+ }
435
+ const blockers = [];
436
+ let commandResult = null;
437
+ try {
438
+ commandResult = await runShellCommand(this.command.command, 60000);
439
+ if (commandResult.code !== 0) {
440
+ blockers.push(`Lint failed: ${commandResult.stderr}`);
441
+ }
442
+ }
443
+ catch (e) {
444
+ blockers.push(`Lint execution failed: ${e}`);
445
+ }
446
+ const passed = blockers.length === 0;
447
+ const evidenceItems = [
448
+ commandEvidence('Lint command', this.command, passed, commandResult),
449
+ ];
450
+ return {
451
+ gate: this.stage,
452
+ status: passed ? 'PASSED' : 'FAILED',
453
+ passed,
454
+ evidence: textEvidence(evidenceItems),
455
+ evidenceItems,
456
+ blockers
457
+ };
458
+ }
459
+ }
460
+ export class TestGate {
461
+ constructor(command) {
462
+ this.command = command;
463
+ this.stage = 'G5';
464
+ this.name = 'Test';
465
+ this.description = 'Run configured test command';
466
+ this.requiredLevel = 'ALWAYS';
467
+ }
468
+ async execute() {
469
+ if (!this.command.command) {
470
+ return missingCommandResult(this.stage, 'Test command', this.command);
471
+ }
472
+ const blockers = [];
473
+ let commandResult = null;
474
+ try {
475
+ commandResult = await runShellCommand(this.command.command, 120000);
476
+ if (commandResult.code !== 0) {
477
+ blockers.push(`Tests failed: ${commandResult.stderr}`);
478
+ }
479
+ }
480
+ catch (e) {
481
+ blockers.push(`Test execution failed: ${e}`);
482
+ }
483
+ const passed = blockers.length === 0;
484
+ const evidenceItems = [
485
+ commandEvidence('Test command', this.command, passed, commandResult),
486
+ ];
487
+ return {
488
+ gate: this.stage,
489
+ status: passed ? 'PASSED' : 'FAILED',
490
+ passed,
491
+ evidence: textEvidence(evidenceItems),
492
+ evidenceItems,
493
+ blockers
494
+ };
495
+ }
496
+ }
497
+ export class CoverageGate {
498
+ constructor(command) {
499
+ this.command = command;
500
+ this.stage = 'G6';
501
+ this.name = 'Coverage';
502
+ this.description = 'Run configured coverage command';
503
+ this.requiredLevel = 'ALWAYS';
504
+ }
505
+ async execute() {
506
+ if (!this.command.command) {
507
+ return missingCommandResult(this.stage, 'Coverage command', this.command);
508
+ }
509
+ const blockers = [];
510
+ let detail = '';
511
+ let commandResult = null;
512
+ try {
513
+ commandResult = await runShellCommand(this.command.command, 120000);
514
+ if (commandResult.code !== 0) {
515
+ blockers.push(`Coverage command failed: ${commandResult.stderr}`);
516
+ }
517
+ const coverageMatch = commandResult.stdout.match(/All files[^|]*\|[^|]*\|[^|]*\|[^|]*\|[^|]*\|\s*(\d+\.?\d*)/);
518
+ if (coverageMatch) {
519
+ const coverage = parseFloat(coverageMatch[1]);
520
+ detail = `Coverage: ${coverage}%`;
521
+ if (coverage < 80) {
522
+ blockers.push(`Coverage ${coverage}% below 80% threshold`);
523
+ }
524
+ }
525
+ else {
526
+ detail = (commandResult.stdout || commandResult.stderr || `exit code ${commandResult.code}`).slice(-500);
527
+ blockers.push('Coverage percentage could not be parsed');
528
+ }
529
+ }
530
+ catch (e) {
531
+ blockers.push(`Coverage check failed: ${e}`);
532
+ }
533
+ const passed = blockers.length === 0;
534
+ const evidenceItems = [
535
+ {
536
+ ...commandEvidence('Coverage command', this.command, passed, commandResult),
537
+ detail: detail ? `${this.command.reason}\n${detail}` : 'command did not complete',
538
+ },
539
+ ];
540
+ return {
541
+ gate: this.stage,
542
+ status: passed ? 'PASSED' : 'FAILED',
543
+ passed,
544
+ evidence: textEvidence(evidenceItems),
545
+ evidenceItems,
546
+ blockers
547
+ };
548
+ }
549
+ }
550
+ export class SecurityGate {
551
+ constructor(options = {}) {
552
+ this.stage = 'G7';
553
+ this.name = 'Security';
554
+ this.description = 'Built-in OWASP-oriented security scan';
555
+ this.requiredLevel = 'ALWAYS';
556
+ this.rootDir = options.rootDir ?? process.cwd();
557
+ this.scanDirs = options.scanDirs ?? ['src'];
558
+ this.maxFileBytes = options.maxFileBytes ?? 300_000;
559
+ this.maxFindings = options.maxFindings ?? 50;
560
+ this.strict = options.strict ?? false;
561
+ }
562
+ async execute() {
563
+ const findings = await this.scan();
564
+ const blockers = findings
565
+ .filter(finding => finding.severity === 'CRITICAL' || (this.strict && finding.severity === 'HIGH'))
566
+ .map(finding => `${finding.severity} ${finding.ruleId} in ${finding.file}:${finding.line} - ${finding.description}`);
567
+ const passed = blockers.length === 0;
568
+ const summary = this.summarize(findings);
569
+ const evidenceItems = [
570
+ createEvidence({
571
+ kind: 'scan',
572
+ label: 'Security scan',
573
+ passed,
574
+ path: this.scanDirs.join(','),
575
+ detail: findings.length > 0
576
+ ? `${findings.length} finding(s): critical=${summary.CRITICAL}, high=${summary.HIGH}, medium=${summary.MEDIUM}, low=${summary.LOW}, strict=${this.strict}`
577
+ : 'no built-in security findings detected',
578
+ source: 'built-in-security-scan',
579
+ }),
580
+ ...findings.slice(0, this.maxFindings).map(finding => createEvidence({
581
+ kind: 'scan',
582
+ label: `Security finding ${finding.ruleId}`,
583
+ passed: finding.severity !== 'CRITICAL' && finding.severity !== 'HIGH',
584
+ path: finding.file,
585
+ detail: `${finding.severity} line ${finding.line}: ${finding.description}; ${finding.evidence}`,
586
+ source: 'built-in-security-scan',
587
+ })),
588
+ ];
589
+ return {
590
+ gate: this.stage,
591
+ status: passed ? 'PASSED' : 'FAILED',
592
+ passed,
593
+ evidence: textEvidence(evidenceItems),
594
+ evidenceItems,
595
+ blockers
596
+ };
597
+ }
598
+ async scan() {
599
+ const findings = [];
600
+ try {
601
+ const fs = await import('fs/promises');
602
+ const { join, relative } = await import('path');
603
+ const files = [];
604
+ for (const dir of this.scanDirs) {
605
+ files.push(...await this.walkDir(join(this.rootDir, dir)));
606
+ }
607
+ for (const file of files) {
608
+ if (findings.length >= this.maxFindings)
609
+ break;
610
+ const stat = await fs.stat(file);
611
+ if (!stat.isFile() || stat.size > this.maxFileBytes)
612
+ continue;
613
+ const content = await fs.readFile(file, 'utf-8');
614
+ if (content.includes('\u0000'))
615
+ continue;
616
+ const displayPath = relative(this.rootDir, file).replace(/\\/g, '/');
617
+ findings.push(...this.scanFile(displayPath, content).slice(0, this.maxFindings - findings.length));
618
+ }
619
+ }
620
+ catch {
621
+ // A missing scan directory should not mask the rest of the verification run.
622
+ }
623
+ return findings;
624
+ }
625
+ scanFile(file, content) {
626
+ const findings = [];
627
+ const lines = content.split('\n');
628
+ for (const rule of this.rulesForFile(file)) {
629
+ for (let index = 0; index < lines.length; index += 1) {
630
+ const line = lines[index];
631
+ if (this.isRuleDefinition(file, line) || this.isSecurityTestFixture(file, line))
632
+ continue;
633
+ rule.pattern.lastIndex = 0;
634
+ if (rule.pattern.test(line)) {
635
+ findings.push({
636
+ ruleId: rule.id,
637
+ severity: rule.severity,
638
+ description: rule.description,
639
+ file,
640
+ line: index + 1,
641
+ evidence: line.trim().slice(0, 180),
642
+ });
643
+ }
644
+ }
645
+ }
646
+ findings.push(...this.findEmptyCatchBlocks(file, lines));
647
+ return findings;
648
+ }
649
+ async walkDir(dir) {
650
+ const fs = await import('fs/promises');
651
+ const { join } = await import('path');
652
+ const results = [];
653
+ try {
654
+ const entries = await fs.readdir(dir, { withFileTypes: true });
655
+ for (const entry of entries) {
656
+ const fullPath = join(dir, entry.name);
657
+ if (entry.isDirectory()) {
658
+ if (['node_modules', 'dist', '.git', '.scale', 'coverage'].includes(entry.name))
659
+ continue;
660
+ results.push(...await this.walkDir(fullPath));
661
+ }
662
+ else if (/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(entry.name)) {
663
+ results.push(fullPath);
664
+ }
665
+ }
666
+ }
667
+ catch {
668
+ // Ignore unreadable directories.
669
+ }
670
+ return results;
671
+ }
672
+ rulesForFile(file) {
673
+ const rules = [
674
+ {
675
+ id: 'secret.assignment',
676
+ severity: 'CRITICAL',
677
+ description: 'Hardcoded credential or token assignment',
678
+ pattern: /\b(password|passwd|api[_-]?key|secret|token|auth[_-]?token|access[_-]?token|refresh[_-]?token|private[_-]?key)\b\s*[:=]\s*['"`][^'"`]{6,}['"`]/i,
679
+ },
680
+ {
681
+ id: 'secret.private-key',
682
+ severity: 'CRITICAL',
683
+ description: 'Private key material appears in source',
684
+ pattern: /-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/,
685
+ },
686
+ {
687
+ id: 'security.tls-disabled',
688
+ severity: 'HIGH',
689
+ description: 'TLS certificate verification is disabled',
690
+ pattern: /NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"`]0['"`]|rejectUnauthorized\s*:\s*false|strictSSL\s*:\s*false/i,
691
+ },
692
+ {
693
+ id: 'injection.eval',
694
+ severity: 'HIGH',
695
+ description: 'Dynamic code execution can enable injection',
696
+ pattern: /\beval\s*\(|new\s+Function\s*\(/,
697
+ },
698
+ {
699
+ id: 'xss.raw-html',
700
+ severity: 'HIGH',
701
+ description: 'Raw HTML rendering can enable XSS',
702
+ pattern: /dangerouslySetInnerHTML|\.innerHTML\s*=/,
703
+ },
704
+ {
705
+ id: 'command.dangerous',
706
+ severity: 'HIGH',
707
+ description: 'Dangerous shell or Git command pattern',
708
+ pattern: /\bgit\s+add\s+\.(?=$|[\s'"`),;])|rm\s+-rf\s+(?:\/|~|\*|\.)|curl\b.*\|.*\b(?:bash|sh|pwsh|powershell|cmd)\b|Invoke-WebRequest\b.*\|\s*iex\b/i,
709
+ },
710
+ {
711
+ id: 'command.shell-exec',
712
+ severity: 'MEDIUM',
713
+ description: 'Shell execution requires argument control review',
714
+ pattern: /\bshell\s*:\s*true\b|\bexecSync\s*\(|\bchild_process\.exec\s*\(/,
715
+ },
716
+ {
717
+ id: 'types.ts-ignore',
718
+ severity: 'MEDIUM',
719
+ description: 'TypeScript error suppression can hide unsafe code',
720
+ pattern: /^\s*(?:\/\/|\/\*)\s*@ts-ignore\b/,
721
+ },
722
+ ];
723
+ return this.isTestPath(file)
724
+ ? rules.filter(rule => rule.severity === 'CRITICAL' || rule.id === 'command.dangerous')
725
+ : rules;
726
+ }
727
+ findEmptyCatchBlocks(file, lines) {
728
+ if (this.isTestPath(file))
729
+ return [];
730
+ const findings = [];
731
+ for (let index = 0; index < lines.length; index += 1) {
732
+ const line = lines[index];
733
+ if (/catch\s*(?:\([^)]*\))?\s*\{\s*(?:\/\*.*?\*\/|\/\/.*)?\s*\}/.test(line)) {
734
+ findings.push({
735
+ ruleId: 'logic.empty-catch',
736
+ severity: 'HIGH',
737
+ description: 'Empty or comment-only catch block suppresses failures',
738
+ file,
739
+ line: index + 1,
740
+ evidence: line.trim().slice(0, 180),
741
+ });
742
+ continue;
743
+ }
744
+ if (!/catch\s*(?:\([^)]*\))?\s*\{\s*$/.test(line))
745
+ continue;
746
+ for (let probe = index + 1; probe < Math.min(lines.length, index + 8); probe += 1) {
747
+ const trimmed = lines[probe].trim();
748
+ if (trimmed === '' || trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('*/')) {
749
+ continue;
750
+ }
751
+ if (/^}\s*[),;]?$/.test(trimmed)) {
752
+ findings.push({
753
+ ruleId: 'logic.empty-catch',
754
+ severity: 'HIGH',
755
+ description: 'Empty or comment-only catch block suppresses failures',
756
+ file,
757
+ line: index + 1,
758
+ evidence: line.trim().slice(0, 180),
759
+ });
760
+ }
761
+ break;
762
+ }
763
+ }
764
+ return findings;
765
+ }
766
+ summarize(findings) {
767
+ return {
768
+ CRITICAL: findings.filter(f => f.severity === 'CRITICAL').length,
769
+ HIGH: findings.filter(f => f.severity === 'HIGH').length,
770
+ MEDIUM: findings.filter(f => f.severity === 'MEDIUM').length,
771
+ LOW: findings.filter(f => f.severity === 'LOW').length,
772
+ };
773
+ }
774
+ isTestPath(file) {
775
+ return /(^|\/)(tests?|__tests__)\//i.test(file) || /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs)$/i.test(file);
776
+ }
777
+ isRuleDefinition(file, line) {
778
+ const trimmed = line.trim();
779
+ return file.endsWith('GateSystem.ts') && (/^pattern:\s*\/.*\/[dgimsuy]*,?$/.test(trimmed) || /^id:\s*['"`][^'"`]+['"`],?$/.test(trimmed));
780
+ }
781
+ isSecurityTestFixture(file, line) {
782
+ if (!this.isTestPath(file))
783
+ return false;
784
+ return /\b(?:text|content|diff|source)\b\s*[:=]/.test(line) &&
785
+ /['"`].*(?:password|api[_-]?key|secret|token|auth|credential|private[_-]?key|git add|shell: true|@ts-ignore|catch)/i.test(line);
786
+ }
787
+ }
788
+ //# sourceMappingURL=GateSystem.js.map