@paths.design/caws-cli 7.0.1 → 7.0.3
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/dist/budget-derivation.js +5 -4
- package/dist/commands/diagnose.js +26 -20
- package/dist/commands/init.js +72 -5
- package/dist/commands/specs.js +40 -1
- package/dist/commands/status.js +2 -2
- package/dist/commands/templates.js +10 -0
- package/dist/commands/tool.js +2 -3
- package/dist/commands/validate.js +12 -0
- package/dist/config/index.js +17 -8
- package/dist/generators/working-spec.js +42 -9
- package/dist/index.js +3 -1
- package/dist/scaffold/cursor-hooks.js +10 -2
- package/dist/scaffold/git-hooks.js +189 -32
- package/dist/scaffold/index.js +105 -17
- package/dist/templates/.caws/tools/README.md +20 -0
- package/dist/templates/.cursor/README.md +311 -0
- package/dist/templates/.cursor/hooks/audit.sh +55 -0
- package/dist/templates/.cursor/hooks/block-dangerous.sh +83 -0
- package/dist/templates/.cursor/hooks/caws-quality-check.sh +52 -0
- package/dist/templates/.cursor/hooks/caws-scope-guard.sh +130 -0
- package/dist/templates/.cursor/hooks/caws-tool-validation.sh +121 -0
- package/dist/templates/.cursor/hooks/format.sh +38 -0
- package/dist/templates/.cursor/hooks/naming-check.sh +64 -0
- package/dist/templates/.cursor/hooks/scan-secrets.sh +46 -0
- package/dist/templates/.cursor/hooks/scope-guard.sh +52 -0
- package/dist/templates/.cursor/hooks/validate-spec.sh +83 -0
- package/dist/templates/.cursor/hooks.json +59 -0
- package/dist/templates/.cursor/rules/00-claims-verification.mdc +144 -0
- package/dist/templates/.cursor/rules/01-working-style.mdc +50 -0
- package/dist/templates/.cursor/rules/02-quality-gates.mdc +370 -0
- package/dist/templates/.cursor/rules/03-naming-and-refactor.mdc +33 -0
- package/dist/templates/.cursor/rules/04-logging-language-style.mdc +23 -0
- package/dist/templates/.cursor/rules/05-safe-defaults-guards.mdc +23 -0
- package/dist/templates/.cursor/rules/06-typescript-conventions.mdc +36 -0
- package/dist/templates/.cursor/rules/07-process-ops.mdc +20 -0
- package/dist/templates/.cursor/rules/08-solid-and-architecture.mdc +16 -0
- package/dist/templates/.cursor/rules/09-docstrings.mdc +89 -0
- package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +390 -0
- package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +385 -0
- package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +516 -0
- package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +588 -0
- package/dist/templates/.cursor/rules/README.md +148 -0
- package/dist/templates/.github/copilot/instructions.md +311 -0
- package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +5 -0
- package/dist/templates/.idea/runConfigurations/CAWS_Validate.xml +5 -0
- package/dist/templates/.vscode/launch.json +56 -0
- package/dist/templates/.vscode/settings.json +93 -0
- package/dist/templates/.windsurf/workflows/caws-guided-development.md +92 -0
- package/dist/templates/COMMIT_CONVENTIONS.md +86 -0
- package/dist/templates/OIDC_SETUP.md +300 -0
- package/dist/templates/agents.md +1047 -0
- package/dist/templates/codemod/README.md +1 -0
- package/dist/templates/codemod/test.js +93 -0
- package/dist/templates/docs/README.md +150 -0
- package/dist/templates/scripts/quality-gates/check-god-objects.js +146 -0
- package/dist/templates/scripts/quality-gates/run-quality-gates.js +50 -0
- package/dist/templates/scripts/v3/analysis/todo_analyzer.py +1997 -0
- package/dist/tool-loader.js +6 -1
- package/dist/tool-validator.js +8 -2
- package/dist/utils/detection.js +34 -6
- package/dist/utils/git-lock.js +118 -0
- package/dist/utils/gitignore-updater.js +148 -0
- package/dist/utils/quality-gates.js +47 -7
- package/dist/utils/spec-resolver.js +23 -3
- package/dist/utils/yaml-validation.js +155 -0
- package/dist/validation/spec-validation.js +105 -2
- package/package.json +2 -2
- package/templates/.caws/schemas/waivers.schema.json +30 -0
- package/templates/.caws/schemas/working-spec.schema.json +133 -0
- package/templates/.caws/templates/working-spec.template.yml +74 -0
- package/templates/.caws/tools/README.md +20 -0
- package/templates/.caws/tools/scope-guard.js +208 -0
- package/templates/.caws/tools-allow.json +331 -0
- package/templates/.caws/waivers.yml +19 -0
- package/templates/.cursor/hooks/scope-guard.sh +2 -2
- package/templates/.cursor/hooks/validate-spec.sh +42 -7
- package/templates/apps/tools/caws/COMPLETION_REPORT.md +0 -331
- package/templates/apps/tools/caws/MIGRATION_SUMMARY.md +0 -360
- package/templates/apps/tools/caws/README.md +0 -463
- package/templates/apps/tools/caws/TEST_STATUS.md +0 -365
- package/templates/apps/tools/caws/attest.js +0 -357
- package/templates/apps/tools/caws/ci-optimizer.js +0 -642
- package/templates/apps/tools/caws/config.ts +0 -245
- package/templates/apps/tools/caws/cross-functional.js +0 -876
- package/templates/apps/tools/caws/dashboard.js +0 -1112
- package/templates/apps/tools/caws/flake-detector.ts +0 -362
- package/templates/apps/tools/caws/gates.js +0 -198
- package/templates/apps/tools/caws/gates.ts +0 -271
- package/templates/apps/tools/caws/language-adapters.ts +0 -381
- package/templates/apps/tools/caws/language-support.d.ts +0 -367
- package/templates/apps/tools/caws/language-support.d.ts.map +0 -1
- package/templates/apps/tools/caws/language-support.js +0 -585
- package/templates/apps/tools/caws/legacy-assessment.ts +0 -408
- package/templates/apps/tools/caws/legacy-assessor.js +0 -764
- package/templates/apps/tools/caws/mutant-analyzer.js +0 -734
- package/templates/apps/tools/caws/perf-budgets.ts +0 -349
- package/templates/apps/tools/caws/prompt-lint.js.backup +0 -274
- package/templates/apps/tools/caws/property-testing.js +0 -707
- package/templates/apps/tools/caws/provenance.d.ts +0 -14
- package/templates/apps/tools/caws/provenance.d.ts.map +0 -1
- package/templates/apps/tools/caws/provenance.js +0 -132
- package/templates/apps/tools/caws/provenance.js.backup +0 -73
- package/templates/apps/tools/caws/provenance.ts +0 -211
- package/templates/apps/tools/caws/security-provenance.ts +0 -483
- package/templates/apps/tools/caws/shared/base-tool.ts +0 -281
- package/templates/apps/tools/caws/shared/config-manager.ts +0 -366
- package/templates/apps/tools/caws/shared/gate-checker.ts +0 -849
- package/templates/apps/tools/caws/shared/types.ts +0 -444
- package/templates/apps/tools/caws/shared/validator.ts +0 -305
- package/templates/apps/tools/caws/shared/waivers-manager.ts +0 -174
- package/templates/apps/tools/caws/spec-test-mapper.ts +0 -391
- package/templates/apps/tools/caws/test-quality.js +0 -578
- package/templates/apps/tools/caws/validate.js +0 -76
- package/templates/apps/tools/caws/validate.ts +0 -228
- package/templates/apps/tools/caws/waivers.js +0 -344
- /package/{templates/apps/tools/caws → dist/templates/.caws}/schemas/waivers.schema.json +0 -0
- /package/{templates/apps/tools/caws → dist/templates/.caws}/schemas/working-spec.schema.json +0 -0
- /package/{templates/apps/tools/caws → dist/templates/.caws}/templates/working-spec.template.yml +0 -0
- /package/{templates/apps/tools/caws → dist/templates/.caws/tools}/scope-guard.js +0 -0
- /package/{templates/apps/tools/caws → dist/templates/.caws}/tools-allow.json +0 -0
- /package/{templates/apps/tools/caws → dist/templates/.caws}/waivers.yml +0 -0
|
@@ -1,849 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CAWS Gate Checker
|
|
3
|
-
* Consolidated gate checking logic for coverage, mutation, contracts, and trust score
|
|
4
|
-
*
|
|
5
|
-
* @author @darianrosebrook
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as path from 'path';
|
|
9
|
-
import { CawsBaseTool } from './base-tool.js';
|
|
10
|
-
import {
|
|
11
|
-
GateResult,
|
|
12
|
-
GateCheckOptions,
|
|
13
|
-
MutationData,
|
|
14
|
-
ContractTestResults,
|
|
15
|
-
TierPolicy,
|
|
16
|
-
WaiverConfig,
|
|
17
|
-
HumanOverride,
|
|
18
|
-
AIAssessment,
|
|
19
|
-
} from './types.js';
|
|
20
|
-
import { WaiversManager } from './waivers-manager.js';
|
|
21
|
-
|
|
22
|
-
export class CawsGateChecker extends CawsBaseTool {
|
|
23
|
-
private tierPolicies: Record<number, TierPolicy> = {
|
|
24
|
-
1: {
|
|
25
|
-
min_branch: 0.9,
|
|
26
|
-
min_mutation: 0.7,
|
|
27
|
-
min_coverage: 0.9,
|
|
28
|
-
requires_contracts: true,
|
|
29
|
-
requires_manual_review: true,
|
|
30
|
-
},
|
|
31
|
-
2: {
|
|
32
|
-
min_branch: 0.8,
|
|
33
|
-
min_mutation: 0.5,
|
|
34
|
-
min_coverage: 0.8,
|
|
35
|
-
requires_contracts: true,
|
|
36
|
-
},
|
|
37
|
-
3: {
|
|
38
|
-
min_branch: 0.7,
|
|
39
|
-
min_mutation: 0.3,
|
|
40
|
-
min_coverage: 0.7,
|
|
41
|
-
requires_contracts: false,
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
private waiversManager: WaiversManager;
|
|
46
|
-
|
|
47
|
-
constructor() {
|
|
48
|
-
super();
|
|
49
|
-
this.loadTierPolicies();
|
|
50
|
-
this.waiversManager = new WaiversManager();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Load tier policies from configuration
|
|
55
|
-
*/
|
|
56
|
-
private loadTierPolicies(): void {
|
|
57
|
-
const policy = this.loadTierPolicy();
|
|
58
|
-
if (policy) {
|
|
59
|
-
this.tierPolicies = { ...this.tierPolicies, ...policy };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Auto-detect the correct working directory for coverage/mutation reports in monorepos
|
|
65
|
-
*/
|
|
66
|
-
private findReportDirectory(startPath: string = this.getWorkingDirectory()): string {
|
|
67
|
-
// Priority 1: Check if the current directory has the reports or test results
|
|
68
|
-
if (
|
|
69
|
-
this.hasCoverageReports(startPath) ||
|
|
70
|
-
this.hasMutationReports(startPath) ||
|
|
71
|
-
this.hasTestResults(startPath)
|
|
72
|
-
) {
|
|
73
|
-
return startPath;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Priority 2: Check for npm workspaces configuration
|
|
77
|
-
const packageJsonPath = path.join(startPath, 'package.json');
|
|
78
|
-
if (this.pathExists(packageJsonPath)) {
|
|
79
|
-
try {
|
|
80
|
-
const packageJson = this.readJsonFile<any>(packageJsonPath);
|
|
81
|
-
if (packageJson?.workspaces) {
|
|
82
|
-
const workspaces = packageJson.workspaces;
|
|
83
|
-
|
|
84
|
-
// Handle workspace patterns (e.g., ["packages/*", "iterations/*"])
|
|
85
|
-
for (const wsPattern of workspaces) {
|
|
86
|
-
if (wsPattern.includes('*')) {
|
|
87
|
-
const baseDir = wsPattern.split('*')[0];
|
|
88
|
-
const fullBaseDir = path.join(startPath, baseDir);
|
|
89
|
-
|
|
90
|
-
if (this.pathExists(fullBaseDir)) {
|
|
91
|
-
const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
|
|
92
|
-
for (const entry of entries) {
|
|
93
|
-
if (entry.isDirectory()) {
|
|
94
|
-
const wsPath = path.join(fullBaseDir, entry.name);
|
|
95
|
-
if (
|
|
96
|
-
this.hasCoverageReports(wsPath) ||
|
|
97
|
-
this.hasMutationReports(wsPath) ||
|
|
98
|
-
this.hasTestResults(wsPath)
|
|
99
|
-
) {
|
|
100
|
-
return wsPath;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
} else {
|
|
106
|
-
// Direct workspace path
|
|
107
|
-
const wsPath = path.join(startPath, wsPattern);
|
|
108
|
-
if (
|
|
109
|
-
this.hasCoverageReports(wsPath) ||
|
|
110
|
-
this.hasMutationReports(wsPath) ||
|
|
111
|
-
this.hasTestResults(wsPath)
|
|
112
|
-
) {
|
|
113
|
-
return wsPath;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Priority 3: If no reports found in workspaces, look for workspaces with test scripts
|
|
120
|
-
if (packageJson?.workspaces) {
|
|
121
|
-
for (const wsPattern of workspaces) {
|
|
122
|
-
if (wsPattern.includes('*')) {
|
|
123
|
-
const baseDir = wsPattern.split('*')[0];
|
|
124
|
-
const fullBaseDir = path.join(startPath, baseDir);
|
|
125
|
-
|
|
126
|
-
if (this.pathExists(fullBaseDir)) {
|
|
127
|
-
const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
|
|
128
|
-
for (const entry of entries) {
|
|
129
|
-
if (entry.isDirectory()) {
|
|
130
|
-
const wsPath = path.join(fullBaseDir, entry.name);
|
|
131
|
-
if (this.hasTestScript(wsPath)) {
|
|
132
|
-
// Found a workspace with tests, prefer this even without reports
|
|
133
|
-
return wsPath;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
} else {
|
|
139
|
-
const wsPath = path.join(startPath, wsPattern);
|
|
140
|
-
if (this.hasTestScript(wsPath)) {
|
|
141
|
-
return wsPath;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
} catch (error) {
|
|
147
|
-
// Ignore workspace parsing errors
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Fall back to original working directory
|
|
152
|
-
return startPath;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Check if a directory has coverage reports
|
|
157
|
-
*/
|
|
158
|
-
private hasCoverageReports(dirPath: string): boolean {
|
|
159
|
-
const coveragePath = path.join(dirPath, 'coverage', 'coverage-final.json');
|
|
160
|
-
return this.pathExists(coveragePath);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Check if a directory has mutation reports
|
|
165
|
-
*/
|
|
166
|
-
private hasMutationReports(dirPath: string): boolean {
|
|
167
|
-
const mutationPath = path.join(dirPath, 'reports', 'mutation', 'mutation.json');
|
|
168
|
-
return this.pathExists(mutationPath);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Check if a directory has test results
|
|
173
|
-
*/
|
|
174
|
-
private hasTestResults(dirPath: string): boolean {
|
|
175
|
-
const testResultsPath = path.join(dirPath, 'test-results');
|
|
176
|
-
if (this.pathExists(testResultsPath)) {
|
|
177
|
-
try {
|
|
178
|
-
const entries = fs.readdirSync(testResultsPath);
|
|
179
|
-
return entries.some((entry) => entry.endsWith('.json') || entry.endsWith('.xml'));
|
|
180
|
-
} catch (error) {
|
|
181
|
-
// Ignore read errors
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Check if a directory has a package.json with test scripts
|
|
189
|
-
*/
|
|
190
|
-
private hasTestScript(dirPath: string): boolean {
|
|
191
|
-
const packageJsonPath = path.join(dirPath, 'package.json');
|
|
192
|
-
if (this.pathExists(packageJsonPath)) {
|
|
193
|
-
try {
|
|
194
|
-
const packageJson = this.readJsonFile<any>(packageJsonPath);
|
|
195
|
-
return !!packageJson?.scripts?.test;
|
|
196
|
-
} catch (error) {
|
|
197
|
-
// Ignore parse errors
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Check if a waiver applies to the given gate
|
|
205
|
-
*/
|
|
206
|
-
private async checkWaiver(
|
|
207
|
-
gate: string,
|
|
208
|
-
workingDirectory?: string
|
|
209
|
-
): Promise<{
|
|
210
|
-
waived: boolean;
|
|
211
|
-
waiver?: WaiverConfig;
|
|
212
|
-
reason?: string;
|
|
213
|
-
}> {
|
|
214
|
-
try {
|
|
215
|
-
const waivers = await this.waiversManager.getWaiversByGate(gate);
|
|
216
|
-
if (waivers.length === 0) {
|
|
217
|
-
return { waived: false };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Check if any waiver applies (for now, return the first active one)
|
|
221
|
-
for (const waiver of waivers) {
|
|
222
|
-
const status = await this.waiversManager.checkWaiverStatus(waiver.created_at);
|
|
223
|
-
if (status.active) {
|
|
224
|
-
return { waived: true, waiver };
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return { waived: false };
|
|
229
|
-
} catch (error) {
|
|
230
|
-
return { waived: false, reason: `Waiver check failed: ${error}` };
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Load and validate working spec from project
|
|
236
|
-
*/
|
|
237
|
-
private async loadWorkingSpec(workingDirectory?: string): Promise<{
|
|
238
|
-
spec?: any;
|
|
239
|
-
experiment_mode?: boolean;
|
|
240
|
-
human_override?: HumanOverride;
|
|
241
|
-
ai_assessment?: AIAssessment;
|
|
242
|
-
errors?: string[];
|
|
243
|
-
}> {
|
|
244
|
-
try {
|
|
245
|
-
const specPath = path.join(
|
|
246
|
-
workingDirectory || this.getWorkingDirectory(),
|
|
247
|
-
'.caws/working-spec.yml'
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
if (!this.pathExists(specPath)) {
|
|
251
|
-
return { errors: ['Working spec not found at .caws/working-spec.yml'] };
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const spec = await this.readYamlFile(specPath);
|
|
255
|
-
if (!spec) {
|
|
256
|
-
return { errors: ['Failed to parse working spec'] };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return {
|
|
260
|
-
spec,
|
|
261
|
-
experiment_mode: spec.experiment_mode,
|
|
262
|
-
human_override: spec.human_override,
|
|
263
|
-
ai_assessment: spec.ai_assessment,
|
|
264
|
-
};
|
|
265
|
-
} catch (error) {
|
|
266
|
-
return { errors: [`Failed to load working spec: ${error}`] };
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Check if human override applies to waive requirements
|
|
272
|
-
*/
|
|
273
|
-
private checkHumanOverride(
|
|
274
|
-
humanOverride: HumanOverride | undefined,
|
|
275
|
-
requirement: string
|
|
276
|
-
): { waived: boolean; reason?: string } {
|
|
277
|
-
if (!humanOverride) {
|
|
278
|
-
return { waived: false };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (humanOverride.waived_requirements?.includes(requirement)) {
|
|
282
|
-
return {
|
|
283
|
-
waived: true,
|
|
284
|
-
reason: `Human override by ${humanOverride.approved_by}: ${humanOverride.reason}`,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return { waived: false };
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Check if experiment mode applies reduced requirements
|
|
293
|
-
*/
|
|
294
|
-
private checkExperimentMode(experimentMode: boolean | undefined): {
|
|
295
|
-
reduced: boolean;
|
|
296
|
-
adjustments?: Record<string, any>;
|
|
297
|
-
} {
|
|
298
|
-
if (!experimentMode) {
|
|
299
|
-
return { reduced: false };
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
reduced: true,
|
|
304
|
-
adjustments: {
|
|
305
|
-
skip_mutation: true,
|
|
306
|
-
skip_contracts: true,
|
|
307
|
-
reduced_coverage: 0.5, // Minimum coverage for experiments
|
|
308
|
-
skip_manual_review: true,
|
|
309
|
-
},
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Check branch coverage against tier requirements
|
|
315
|
-
*/
|
|
316
|
-
async checkCoverage(options: GateCheckOptions): Promise<GateResult> {
|
|
317
|
-
try {
|
|
318
|
-
// Check waivers and overrides first
|
|
319
|
-
const waiverCheck = await this.checkWaiver('coverage', options.workingDirectory);
|
|
320
|
-
if (waiverCheck.waived) {
|
|
321
|
-
return {
|
|
322
|
-
passed: true,
|
|
323
|
-
score: 1.0, // Waived checks pass with perfect score
|
|
324
|
-
details: {
|
|
325
|
-
waived: true,
|
|
326
|
-
waiver_reason: waiverCheck.waiver?.reason,
|
|
327
|
-
waiver_owner: waiverCheck.waiver?.owner,
|
|
328
|
-
},
|
|
329
|
-
tier: options.tier,
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Load working spec for overrides and experiment mode
|
|
334
|
-
const specData = await this.loadWorkingSpec(options.workingDirectory);
|
|
335
|
-
|
|
336
|
-
// Check human override
|
|
337
|
-
const overrideCheck = this.checkHumanOverride(specData.human_override, 'coverage');
|
|
338
|
-
if (overrideCheck.waived) {
|
|
339
|
-
return {
|
|
340
|
-
passed: true,
|
|
341
|
-
score: 1.0,
|
|
342
|
-
details: {
|
|
343
|
-
overridden: true,
|
|
344
|
-
override_reason: overrideCheck.reason,
|
|
345
|
-
},
|
|
346
|
-
tier: options.tier,
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Check experiment mode
|
|
351
|
-
const experimentCheck = this.checkExperimentMode(specData.experiment_mode);
|
|
352
|
-
|
|
353
|
-
let effectiveTier = options.tier;
|
|
354
|
-
if (experimentCheck.reduced && experimentCheck.adjustments?.reduced_coverage) {
|
|
355
|
-
// For experiments, use reduced coverage requirement
|
|
356
|
-
effectiveTier = 4; // Special experiment tier
|
|
357
|
-
this.tierPolicies[4] = {
|
|
358
|
-
min_branch: experimentCheck.adjustments.reduced_coverage,
|
|
359
|
-
min_mutation: 0,
|
|
360
|
-
min_coverage: experimentCheck.adjustments.reduced_coverage,
|
|
361
|
-
requires_contracts: false,
|
|
362
|
-
requires_manual_review: false,
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Auto-detect the correct directory for coverage reports
|
|
367
|
-
const reportDir = this.findReportDirectory(
|
|
368
|
-
options.workingDirectory || this.getWorkingDirectory()
|
|
369
|
-
);
|
|
370
|
-
const coveragePath = path.join(reportDir, 'coverage', 'coverage-final.json');
|
|
371
|
-
|
|
372
|
-
if (!this.pathExists(coveragePath)) {
|
|
373
|
-
return {
|
|
374
|
-
passed: false,
|
|
375
|
-
score: 0,
|
|
376
|
-
details: {
|
|
377
|
-
error: 'Coverage report not found. Run tests with coverage first.',
|
|
378
|
-
searched_paths: [
|
|
379
|
-
path.join(reportDir, 'coverage', 'coverage-final.json'),
|
|
380
|
-
path.join(this.getWorkingDirectory(), 'coverage', 'coverage-final.json'),
|
|
381
|
-
],
|
|
382
|
-
expected_format: 'Istanbul coverage format (coverage-final.json)',
|
|
383
|
-
expected_schema: {
|
|
384
|
-
description: 'JSON object with coverage data by file',
|
|
385
|
-
example: {
|
|
386
|
-
'/path/to/file.js': {
|
|
387
|
-
statementMap: {
|
|
388
|
-
/* ... */
|
|
389
|
-
},
|
|
390
|
-
fnMap: {
|
|
391
|
-
/* ... */
|
|
392
|
-
},
|
|
393
|
-
branchMap: {
|
|
394
|
-
/* ... */
|
|
395
|
-
},
|
|
396
|
-
s: {
|
|
397
|
-
/* hit counts */
|
|
398
|
-
},
|
|
399
|
-
f: {
|
|
400
|
-
/* function hits */
|
|
401
|
-
},
|
|
402
|
-
b: {
|
|
403
|
-
/* branch hits */
|
|
404
|
-
},
|
|
405
|
-
},
|
|
406
|
-
},
|
|
407
|
-
},
|
|
408
|
-
run_command: 'npm test -- --coverage --coverageReporters=json',
|
|
409
|
-
alternative_commands: [
|
|
410
|
-
'npm run test:coverage',
|
|
411
|
-
'jest --coverage --coverageReporters=json',
|
|
412
|
-
'vitest run --coverage',
|
|
413
|
-
],
|
|
414
|
-
workspace_hint:
|
|
415
|
-
reportDir !== this.getWorkingDirectory()
|
|
416
|
-
? `Auto-detected workspace: ${path.relative(this.getWorkingDirectory(), reportDir)}`
|
|
417
|
-
: 'Run from workspace directory if using monorepo',
|
|
418
|
-
waiver_available: true,
|
|
419
|
-
waiver_suggestion:
|
|
420
|
-
'If this is an exceptional case, consider creating a coverage waiver',
|
|
421
|
-
waiver_command:
|
|
422
|
-
'caws waivers create --title="Coverage waiver" --reason=emergency_hotfix --gates=coverage',
|
|
423
|
-
},
|
|
424
|
-
errors: [
|
|
425
|
-
`Coverage report not found at ${path.relative(this.getWorkingDirectory(), coveragePath)}`,
|
|
426
|
-
],
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const coverageData = this.readJsonFile<any>(coveragePath);
|
|
431
|
-
if (!coverageData) {
|
|
432
|
-
return {
|
|
433
|
-
passed: false,
|
|
434
|
-
score: 0,
|
|
435
|
-
details: { error: 'Failed to parse coverage data' },
|
|
436
|
-
errors: ['Failed to parse coverage data'],
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Calculate coverage from detailed data
|
|
441
|
-
let totalStatements = 0;
|
|
442
|
-
let coveredStatements = 0;
|
|
443
|
-
let totalBranches = 0;
|
|
444
|
-
let coveredBranches = 0;
|
|
445
|
-
let totalFunctions = 0;
|
|
446
|
-
let coveredFunctions = 0;
|
|
447
|
-
|
|
448
|
-
for (const file of Object.values(coverageData)) {
|
|
449
|
-
const fileData = file as any;
|
|
450
|
-
if (fileData.s) {
|
|
451
|
-
totalStatements += Object.keys(fileData.s).length;
|
|
452
|
-
coveredStatements += Object.values(fileData.s).filter((s: any) => s > 0).length;
|
|
453
|
-
}
|
|
454
|
-
if (fileData.b) {
|
|
455
|
-
for (const branches of Object.values(fileData.b) as number[][]) {
|
|
456
|
-
totalBranches += branches.length;
|
|
457
|
-
coveredBranches += branches.filter((b: number) => b > 0).length;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
if (fileData.f) {
|
|
461
|
-
totalFunctions += Object.keys(fileData.f).length;
|
|
462
|
-
coveredFunctions += Object.values(fileData.f).filter((f: any) => f > 0).length;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Calculate percentages
|
|
467
|
-
const statementsPct = totalStatements > 0 ? (coveredStatements / totalStatements) * 100 : 0;
|
|
468
|
-
const branchesPct = totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0;
|
|
469
|
-
const functionsPct = totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0;
|
|
470
|
-
|
|
471
|
-
const branchCoverage = branchesPct / 100;
|
|
472
|
-
const policy = this.tierPolicies[effectiveTier];
|
|
473
|
-
const passed = branchCoverage >= policy.min_branch;
|
|
474
|
-
|
|
475
|
-
return {
|
|
476
|
-
passed,
|
|
477
|
-
score: branchCoverage,
|
|
478
|
-
details: {
|
|
479
|
-
branch_coverage: branchCoverage,
|
|
480
|
-
required_branch: policy.min_branch,
|
|
481
|
-
functions_coverage: functionsPct / 100,
|
|
482
|
-
lines_coverage: statementsPct / 100,
|
|
483
|
-
statements_coverage: statementsPct / 100,
|
|
484
|
-
},
|
|
485
|
-
};
|
|
486
|
-
} catch (error) {
|
|
487
|
-
return {
|
|
488
|
-
passed: false,
|
|
489
|
-
score: 0,
|
|
490
|
-
details: { error: `Coverage check failed: ${error}` },
|
|
491
|
-
errors: [`Coverage check failed: ${error}`],
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Check mutation testing score
|
|
498
|
-
*/
|
|
499
|
-
async checkMutation(options: GateCheckOptions): Promise<GateResult> {
|
|
500
|
-
try {
|
|
501
|
-
// Check waivers and overrides first
|
|
502
|
-
const waiverCheck = await this.checkWaiver('mutation', options.workingDirectory);
|
|
503
|
-
if (waiverCheck.waived) {
|
|
504
|
-
return {
|
|
505
|
-
passed: true,
|
|
506
|
-
score: 1.0,
|
|
507
|
-
details: {
|
|
508
|
-
waived: true,
|
|
509
|
-
waiver_reason: waiverCheck.waiver?.reason,
|
|
510
|
-
waiver_owner: waiverCheck.waiver?.owner,
|
|
511
|
-
},
|
|
512
|
-
tier: options.tier,
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Load working spec for overrides and experiment mode
|
|
517
|
-
const specData = await this.loadWorkingSpec(options.workingDirectory);
|
|
518
|
-
|
|
519
|
-
// Check human override
|
|
520
|
-
const overrideCheck = this.checkHumanOverride(specData.human_override, 'mutation_testing');
|
|
521
|
-
if (overrideCheck.waived) {
|
|
522
|
-
return {
|
|
523
|
-
passed: true,
|
|
524
|
-
score: 1.0,
|
|
525
|
-
details: {
|
|
526
|
-
overridden: true,
|
|
527
|
-
override_reason: overrideCheck.reason,
|
|
528
|
-
},
|
|
529
|
-
tier: options.tier,
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Check experiment mode
|
|
534
|
-
const experimentCheck = this.checkExperimentMode(specData.experiment_mode);
|
|
535
|
-
if (experimentCheck.reduced && experimentCheck.adjustments?.skip_mutation) {
|
|
536
|
-
return {
|
|
537
|
-
passed: true,
|
|
538
|
-
score: 1.0,
|
|
539
|
-
details: {
|
|
540
|
-
experiment_mode: true,
|
|
541
|
-
mutation_skipped: true,
|
|
542
|
-
},
|
|
543
|
-
tier: options.tier,
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// Auto-detect the correct directory for mutation reports
|
|
548
|
-
const reportDir = this.findReportDirectory(
|
|
549
|
-
options.workingDirectory || this.getWorkingDirectory()
|
|
550
|
-
);
|
|
551
|
-
const mutationPath = path.join(reportDir, 'reports', 'mutation', 'mutation.json');
|
|
552
|
-
|
|
553
|
-
if (!this.pathExists(mutationPath)) {
|
|
554
|
-
return {
|
|
555
|
-
passed: false,
|
|
556
|
-
score: 0,
|
|
557
|
-
details: {
|
|
558
|
-
error: 'Mutation report not found. Run mutation tests first.',
|
|
559
|
-
searched_paths: [
|
|
560
|
-
path.join(reportDir, 'reports', 'mutation', 'mutation.json'),
|
|
561
|
-
path.join(this.getWorkingDirectory(), 'reports', 'mutation', 'mutation.json'),
|
|
562
|
-
],
|
|
563
|
-
expected_format: 'Stryker mutation testing JSON report',
|
|
564
|
-
expected_schema: {
|
|
565
|
-
description: 'JSON object with mutation testing results',
|
|
566
|
-
example: {
|
|
567
|
-
files: {
|
|
568
|
-
/* file-specific results */
|
|
569
|
-
},
|
|
570
|
-
testFiles: {
|
|
571
|
-
/* test file results */
|
|
572
|
-
},
|
|
573
|
-
mutants: [
|
|
574
|
-
{
|
|
575
|
-
/* mutant details */
|
|
576
|
-
},
|
|
577
|
-
],
|
|
578
|
-
metrics: {
|
|
579
|
-
killed: 85,
|
|
580
|
-
survived: 5,
|
|
581
|
-
timeout: 2,
|
|
582
|
-
totalDetected: 92,
|
|
583
|
-
totalUndetected: 0,
|
|
584
|
-
totalValid: 92,
|
|
585
|
-
},
|
|
586
|
-
},
|
|
587
|
-
},
|
|
588
|
-
run_command: 'npx stryker run',
|
|
589
|
-
alternative_commands: [
|
|
590
|
-
'npm run test:mutation',
|
|
591
|
-
'npx stryker run --configFile stryker.conf.json',
|
|
592
|
-
'yarn mutation:test',
|
|
593
|
-
],
|
|
594
|
-
workspace_hint:
|
|
595
|
-
reportDir !== this.getWorkingDirectory()
|
|
596
|
-
? `Auto-detected workspace: ${path.relative(this.getWorkingDirectory(), reportDir)}`
|
|
597
|
-
: 'Run from workspace directory if using monorepo',
|
|
598
|
-
},
|
|
599
|
-
errors: [
|
|
600
|
-
`Mutation report not found at ${path.relative(this.getWorkingDirectory(), mutationPath)}`,
|
|
601
|
-
],
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
const mutationData = this.readJsonFile<MutationData>(mutationPath);
|
|
606
|
-
if (!mutationData) {
|
|
607
|
-
return {
|
|
608
|
-
passed: false,
|
|
609
|
-
score: 0,
|
|
610
|
-
details: { error: 'Failed to parse mutation data' },
|
|
611
|
-
errors: ['Failed to parse mutation data'],
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const killed = mutationData.metrics.killed || 0;
|
|
616
|
-
const total = mutationData.metrics.totalDetected || 1;
|
|
617
|
-
const mutationScore = killed / total;
|
|
618
|
-
const policy = this.tierPolicies[options.tier];
|
|
619
|
-
const passed = mutationScore >= policy.min_mutation;
|
|
620
|
-
|
|
621
|
-
return {
|
|
622
|
-
passed,
|
|
623
|
-
score: mutationScore,
|
|
624
|
-
details: {
|
|
625
|
-
mutation_score: mutationScore,
|
|
626
|
-
required_mutation: policy.min_mutation,
|
|
627
|
-
killed,
|
|
628
|
-
total,
|
|
629
|
-
survived: mutationData.metrics.survived || 0,
|
|
630
|
-
},
|
|
631
|
-
};
|
|
632
|
-
} catch (error) {
|
|
633
|
-
return {
|
|
634
|
-
passed: false,
|
|
635
|
-
score: 0,
|
|
636
|
-
details: { error: `Mutation check failed: ${error}` },
|
|
637
|
-
errors: [`Mutation check failed: ${error}`],
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* Check contract test compliance
|
|
644
|
-
*/
|
|
645
|
-
async checkContracts(options: GateCheckOptions): Promise<GateResult> {
|
|
646
|
-
try {
|
|
647
|
-
// Check waivers and overrides first
|
|
648
|
-
const waiverCheck = await this.checkWaiver('contracts', options.workingDirectory);
|
|
649
|
-
if (waiverCheck.waived) {
|
|
650
|
-
return {
|
|
651
|
-
passed: true,
|
|
652
|
-
score: 1.0,
|
|
653
|
-
details: {
|
|
654
|
-
waived: true,
|
|
655
|
-
waiver_reason: waiverCheck.waiver?.reason,
|
|
656
|
-
waiver_owner: waiverCheck.waiver?.owner,
|
|
657
|
-
},
|
|
658
|
-
tier: options.tier,
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const policy = this.tierPolicies[options.tier];
|
|
663
|
-
|
|
664
|
-
if (!policy.requires_contracts) {
|
|
665
|
-
return {
|
|
666
|
-
passed: true,
|
|
667
|
-
score: 1.0,
|
|
668
|
-
details: { contracts_required: false, tier: options.tier },
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Auto-detect the correct directory for contract test results
|
|
673
|
-
const reportDir = this.findReportDirectory(
|
|
674
|
-
options.workingDirectory || this.getWorkingDirectory()
|
|
675
|
-
);
|
|
676
|
-
const contractResultsPath = path.join(reportDir, 'test-results', 'contract-results.json');
|
|
677
|
-
|
|
678
|
-
if (!this.pathExists(contractResultsPath)) {
|
|
679
|
-
return {
|
|
680
|
-
passed: false,
|
|
681
|
-
score: 0,
|
|
682
|
-
details: {
|
|
683
|
-
error: 'Contract test results not found',
|
|
684
|
-
searched_paths: [
|
|
685
|
-
path.join(reportDir, 'test-results', 'contract-results.json'),
|
|
686
|
-
path.join(this.getWorkingDirectory(), 'test-results', 'contract-results.json'),
|
|
687
|
-
path.join(reportDir, '.caws', 'contract-results.json'),
|
|
688
|
-
path.join(this.getWorkingDirectory(), '.caws', 'contract-results.json'),
|
|
689
|
-
],
|
|
690
|
-
expected_format:
|
|
691
|
-
'JSON with { tests: [], passed: boolean, numPassed: number, numTotal: number }',
|
|
692
|
-
example_command:
|
|
693
|
-
'npm run test:contract -- --json --outputFile=test-results/contract-results.json',
|
|
694
|
-
},
|
|
695
|
-
errors: [
|
|
696
|
-
`Contract test results not found. Searched in: ${[
|
|
697
|
-
path.relative(
|
|
698
|
-
this.getWorkingDirectory(),
|
|
699
|
-
path.join(reportDir, 'test-results', 'contract-results.json')
|
|
700
|
-
),
|
|
701
|
-
'test-results/contract-results.json',
|
|
702
|
-
'.caws/contract-results.json',
|
|
703
|
-
].join(', ')}`,
|
|
704
|
-
],
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
const results = this.readJsonFile<ContractTestResults>(contractResultsPath);
|
|
709
|
-
if (!results) {
|
|
710
|
-
return {
|
|
711
|
-
passed: false,
|
|
712
|
-
score: 0,
|
|
713
|
-
details: { error: 'Failed to parse contract test results' },
|
|
714
|
-
errors: ['Failed to parse contract test results'],
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
const passed = results.numPassed === results.numTotal && results.numTotal > 0;
|
|
719
|
-
|
|
720
|
-
return {
|
|
721
|
-
passed,
|
|
722
|
-
score: passed ? 1.0 : 0,
|
|
723
|
-
details: {
|
|
724
|
-
tests_passed: results.numPassed,
|
|
725
|
-
tests_total: results.numTotal,
|
|
726
|
-
consumer_tests: results.consumer || false,
|
|
727
|
-
provider_tests: results.provider || false,
|
|
728
|
-
},
|
|
729
|
-
};
|
|
730
|
-
} catch (error) {
|
|
731
|
-
return {
|
|
732
|
-
passed: false,
|
|
733
|
-
score: 0,
|
|
734
|
-
details: { error: `Contract check failed: ${error}` },
|
|
735
|
-
errors: [`Contract check failed: ${error}`],
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
/**
|
|
741
|
-
* Calculate overall trust score
|
|
742
|
-
*/
|
|
743
|
-
async calculateTrustScore(options: GateCheckOptions): Promise<GateResult> {
|
|
744
|
-
try {
|
|
745
|
-
// Run all gate checks
|
|
746
|
-
const [coverageResult, mutationResult, contractResult] = await Promise.all([
|
|
747
|
-
this.checkCoverage(options),
|
|
748
|
-
this.checkMutation(options),
|
|
749
|
-
this.checkContracts(options),
|
|
750
|
-
]);
|
|
751
|
-
|
|
752
|
-
// Load provenance if available
|
|
753
|
-
let provenance = null;
|
|
754
|
-
try {
|
|
755
|
-
const provenancePath = path.join(
|
|
756
|
-
options.workingDirectory || this.getWorkingDirectory(),
|
|
757
|
-
'.agent/provenance.json'
|
|
758
|
-
);
|
|
759
|
-
if (this.pathExists(provenancePath)) {
|
|
760
|
-
provenance = this.readJsonFile(provenancePath);
|
|
761
|
-
}
|
|
762
|
-
} catch {
|
|
763
|
-
// Provenance not available
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// CAWS trust score weights
|
|
767
|
-
const weights = {
|
|
768
|
-
coverage: 0.3,
|
|
769
|
-
mutation: 0.3,
|
|
770
|
-
contracts: 0.2,
|
|
771
|
-
a11y: 0.1,
|
|
772
|
-
perf: 0.1,
|
|
773
|
-
};
|
|
774
|
-
|
|
775
|
-
// Calculate weighted score
|
|
776
|
-
let totalScore = 0;
|
|
777
|
-
let totalWeight = 0;
|
|
778
|
-
|
|
779
|
-
// Coverage component
|
|
780
|
-
totalScore += coverageResult.score * weights.coverage;
|
|
781
|
-
totalWeight += weights.coverage;
|
|
782
|
-
|
|
783
|
-
// Mutation component
|
|
784
|
-
totalScore += mutationResult.score * weights.mutation;
|
|
785
|
-
totalWeight += weights.mutation;
|
|
786
|
-
|
|
787
|
-
// Contracts component
|
|
788
|
-
totalScore += contractResult.score * weights.contracts;
|
|
789
|
-
totalWeight += weights.contracts;
|
|
790
|
-
|
|
791
|
-
// A11y component (placeholder - would check axe results)
|
|
792
|
-
const a11yScore = provenance?.results?.a11y === 'pass' ? 1.0 : 0.5;
|
|
793
|
-
totalScore += a11yScore * weights.a11y;
|
|
794
|
-
totalWeight += weights.a11y;
|
|
795
|
-
|
|
796
|
-
// Performance component (placeholder - would check perf budgets)
|
|
797
|
-
const perfScore = provenance?.results?.perf ? 0.8 : 0.5;
|
|
798
|
-
totalScore += perfScore * weights.perf;
|
|
799
|
-
totalWeight += weights.perf;
|
|
800
|
-
|
|
801
|
-
const trustScore = totalScore / totalWeight;
|
|
802
|
-
const tierPolicy = this.tierPolicies[options.tier];
|
|
803
|
-
const passed = trustScore >= 0.8;
|
|
804
|
-
|
|
805
|
-
// Apply tier-specific penalties
|
|
806
|
-
let adjustedScore = trustScore;
|
|
807
|
-
if (options.tier <= 2 && !contractResult.passed) {
|
|
808
|
-
adjustedScore *= 0.8; // 20% penalty for missing contracts on high tiers
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
return {
|
|
812
|
-
passed,
|
|
813
|
-
score: adjustedScore,
|
|
814
|
-
details: {
|
|
815
|
-
tier: options.tier,
|
|
816
|
-
tier_policy: tierPolicy,
|
|
817
|
-
coverage: coverageResult,
|
|
818
|
-
mutation: mutationResult,
|
|
819
|
-
contracts: contractResult,
|
|
820
|
-
a11y: { score: a11yScore, details: provenance?.results?.a11y },
|
|
821
|
-
perf: { score: perfScore, details: provenance?.results?.perf },
|
|
822
|
-
raw_score: trustScore,
|
|
823
|
-
weights,
|
|
824
|
-
},
|
|
825
|
-
};
|
|
826
|
-
} catch (error) {
|
|
827
|
-
return {
|
|
828
|
-
passed: false,
|
|
829
|
-
score: 0,
|
|
830
|
-
details: { error: `Trust score calculation failed: ${error}` },
|
|
831
|
-
errors: [`Trust score calculation failed: ${error}`],
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
/**
|
|
837
|
-
* Get tier policy for a specific tier
|
|
838
|
-
*/
|
|
839
|
-
getTierPolicy(tier: number): TierPolicy | null {
|
|
840
|
-
return this.tierPolicies[tier] || null;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
/**
|
|
844
|
-
* Get all available tiers
|
|
845
|
-
*/
|
|
846
|
-
getAvailableTiers(): number[] {
|
|
847
|
-
return Object.keys(this.tierPolicies).map(Number);
|
|
848
|
-
}
|
|
849
|
-
}
|