decision-guardian 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +792 -0
- package/dist/adapters/github/actions-logger.js +88 -0
- package/dist/adapters/github/comment.js +601 -0
- package/dist/adapters/github/github-provider.js +260 -0
- package/dist/adapters/github/health.js +56 -0
- package/dist/adapters/local/console-logger.js +46 -0
- package/dist/adapters/local/local-git-provider.js +247 -0
- package/dist/cli/commands/check.js +134 -0
- package/dist/cli/commands/init.js +58 -0
- package/dist/cli/commands/template.js +70 -0
- package/dist/cli/formatter.js +68 -0
- package/dist/cli/index.js +12458 -0
- package/dist/cli/licenses.txt +143 -0
- package/dist/cli/paths.js +40 -0
- package/dist/core/content-matchers.js +333 -0
- package/dist/core/health.js +52 -0
- package/dist/core/interfaces/index.js +2 -0
- package/dist/core/interfaces/logger.js +2 -0
- package/dist/core/interfaces/scm-provider.js +5 -0
- package/dist/core/logger.js +20 -0
- package/dist/core/matcher.js +184 -0
- package/dist/core/metrics.js +87 -0
- package/dist/core/parser.js +338 -0
- package/dist/core/rule-evaluator.js +186 -0
- package/dist/core/rule-parser.js +211 -0
- package/dist/core/rule-types.js +22 -0
- package/dist/core/trie.js +83 -0
- package/dist/core/types.js +2 -0
- package/dist/index.js +61142 -0
- package/dist/licenses.txt +758 -0
- package/dist/main.js +290 -0
- package/dist/telemetry/payload.js +25 -0
- package/dist/telemetry/privacy.js +37 -0
- package/dist/telemetry/sender.js +40 -0
- package/dist/version.js +7 -0
- package/package.json +60 -0
- package/templates/advanced-rules.md +94 -0
- package/templates/api.md +70 -0
- package/templates/basic.md +38 -0
- package/templates/database.md +81 -0
- package/templates/security.md +89 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const github = __importStar(require("@actions/github"));
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const parser_1 = require("./core/parser");
|
|
40
|
+
const matcher_1 = require("./core/matcher");
|
|
41
|
+
const metrics_1 = require("./core/metrics");
|
|
42
|
+
const logger_1 = require("./core/logger");
|
|
43
|
+
const health_1 = require("./core/health");
|
|
44
|
+
const actions_logger_1 = require("./adapters/github/actions-logger");
|
|
45
|
+
const github_provider_1 = require("./adapters/github/github-provider");
|
|
46
|
+
const health_2 = require("./adapters/github/health");
|
|
47
|
+
const zod_1 = require("zod");
|
|
48
|
+
const sender_1 = require("./telemetry/sender");
|
|
49
|
+
const version_1 = require("./version");
|
|
50
|
+
// Create the logger for the entire action lifetime
|
|
51
|
+
const logger = new actions_logger_1.ActionsLogger();
|
|
52
|
+
/**
|
|
53
|
+
* Main entry point for the GitHub Action
|
|
54
|
+
*/
|
|
55
|
+
async function run() {
|
|
56
|
+
const startTime = Date.now();
|
|
57
|
+
const errors = [];
|
|
58
|
+
let config;
|
|
59
|
+
try {
|
|
60
|
+
// 1. Load configuration
|
|
61
|
+
config = loadConfig();
|
|
62
|
+
// 2. Health checks
|
|
63
|
+
const decisionFileOk = await (0, health_1.checkDecisionFileExists)(config.decisionFile);
|
|
64
|
+
const tokenOk = await (0, health_2.validateToken)(config.token, logger);
|
|
65
|
+
if (!decisionFileOk || !tokenOk) {
|
|
66
|
+
logger.setFailed('System health check failed');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
logger.info(`Decision file: ${config.decisionFile}`);
|
|
70
|
+
// 3. Parse decisions
|
|
71
|
+
logger.startGroup('Parsing decisions...');
|
|
72
|
+
const parser = new parser_1.DecisionParser();
|
|
73
|
+
// Check if path is a directory and handle accordingly
|
|
74
|
+
const isDir = fs.existsSync(config.decisionFile) && fs.statSync(config.decisionFile).isDirectory();
|
|
75
|
+
const parseResult = isDir
|
|
76
|
+
? await parser.parseDirectory(config.decisionFile)
|
|
77
|
+
: await parser.parseFile(config.decisionFile);
|
|
78
|
+
if (parseResult.warnings.length > 0) {
|
|
79
|
+
parseResult.warnings.forEach((warn) => {
|
|
80
|
+
logger.warning(warn);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (parseResult.errors.length > 0) {
|
|
84
|
+
logger.warning(`Found ${parseResult.errors.length} parse errors`);
|
|
85
|
+
parseResult.errors.forEach((err) => {
|
|
86
|
+
logger.warning(`Line ${err.line}: ${err.message}`);
|
|
87
|
+
});
|
|
88
|
+
if (config.failOnError) {
|
|
89
|
+
logger.setFailed(`Decision file has ${parseResult.errors.length} parse errors`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const hasRules = parseResult.decisions.some((d) => d.rules);
|
|
94
|
+
logger.info(`Loaded ${parseResult.decisions.length} decisions (${hasRules ? 'with advanced rules' : 'file-based only'})`);
|
|
95
|
+
logger.endGroup();
|
|
96
|
+
// 4. Create SCM provider and fetch diffs
|
|
97
|
+
logger.startGroup('Fetching file diffs...');
|
|
98
|
+
const provider = new github_provider_1.GitHubProvider(config.token, logger);
|
|
99
|
+
const changedFiles = await provider.getChangedFiles();
|
|
100
|
+
const useStreaming = changedFiles.length > 1000;
|
|
101
|
+
// Create FileMatcher once for all code paths
|
|
102
|
+
const matcher = new matcher_1.FileMatcher(parseResult.decisions, logger);
|
|
103
|
+
let matches = [];
|
|
104
|
+
let processedFileCount = 0;
|
|
105
|
+
if (useStreaming) {
|
|
106
|
+
logger.info(`Large PR detected (${changedFiles.length} files), using streaming mode`);
|
|
107
|
+
logger.endGroup();
|
|
108
|
+
logger.startGroup('Processing with streaming...');
|
|
109
|
+
matches = await processWithStreaming(parseResult.decisions, provider);
|
|
110
|
+
processedFileCount = changedFiles.length;
|
|
111
|
+
logger.endGroup();
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const fileDiffs = await provider.getFileDiffs();
|
|
115
|
+
processedFileCount = fileDiffs.length;
|
|
116
|
+
metrics_1.metrics.addFilesProcessed(fileDiffs.length);
|
|
117
|
+
logger.info(`PR modifies ${fileDiffs.length} files`);
|
|
118
|
+
if (fileDiffs.length === 0) {
|
|
119
|
+
logger.info('No file diffs found - PR is clear!');
|
|
120
|
+
logger.setOutput('matches_found', '0');
|
|
121
|
+
logger.setOutput('critical_count', '0');
|
|
122
|
+
(0, logger_1.logStructured)(logger, 'info', 'Decision Guardian completed successfully (no files)', {
|
|
123
|
+
duration_ms: Date.now() - startTime,
|
|
124
|
+
});
|
|
125
|
+
metrics_1.metrics.setDuration(Date.now() - startTime);
|
|
126
|
+
reportMetrics(config);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
logger.endGroup();
|
|
130
|
+
// 5. Match files to decisions
|
|
131
|
+
logger.startGroup('Matching decisions...');
|
|
132
|
+
try {
|
|
133
|
+
matches = await matcher.findMatchesWithDiffs(fileDiffs);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
logger.warning(`Advanced matching failed, falling back to simple mode: ${error}`);
|
|
137
|
+
const fileNames = fileDiffs.map((f) => f.filename);
|
|
138
|
+
matches = await matcher.findMatches(fileNames);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const grouped = matcher.groupBySeverity(matches);
|
|
142
|
+
metrics_1.metrics.addMatchesFound(matches.length);
|
|
143
|
+
metrics_1.metrics.addCriticalMatches(grouped.critical.length);
|
|
144
|
+
metrics_1.metrics.addWarningMatches(grouped.warning.length);
|
|
145
|
+
metrics_1.metrics.addInfoMatches(grouped.info.length);
|
|
146
|
+
logger.info(`Found ${matches.length} matches:`);
|
|
147
|
+
logger.info(` - Critical: ${grouped.critical.length}`);
|
|
148
|
+
logger.info(` - Warning: ${grouped.warning.length}`);
|
|
149
|
+
logger.info(` - Info: ${grouped.info.length}`);
|
|
150
|
+
logger.endGroup();
|
|
151
|
+
// 6. Post comment if matches found
|
|
152
|
+
if (matches.length > 0) {
|
|
153
|
+
logger.startGroup('Posting PR comment...');
|
|
154
|
+
await provider.postComment(matches);
|
|
155
|
+
logger.endGroup();
|
|
156
|
+
logger.setOutput('matches_found', String(matches.length));
|
|
157
|
+
logger.setOutput('critical_count', String(grouped.critical.length));
|
|
158
|
+
// 7. Fail check if critical decisions violated
|
|
159
|
+
if (config.failOnCritical && grouped.critical.length > 0) {
|
|
160
|
+
const failureMessage = `PR modifies ${grouped.critical.length} files protected by critical decisions`;
|
|
161
|
+
(0, logger_1.logStructured)(logger, 'error', 'Decision Guardian failed checks', {
|
|
162
|
+
match_count: matches.length,
|
|
163
|
+
critical_count: grouped.critical.length,
|
|
164
|
+
duration_ms: Date.now() - startTime,
|
|
165
|
+
});
|
|
166
|
+
logger.setFailed(failureMessage);
|
|
167
|
+
metrics_1.metrics.setDuration(Date.now() - startTime);
|
|
168
|
+
reportMetrics(config);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
logger.info('No decision matches found - PR is clear!');
|
|
174
|
+
logger.setOutput('matches_found', '0');
|
|
175
|
+
logger.setOutput('critical_count', '0');
|
|
176
|
+
if (provider.postAllClear) {
|
|
177
|
+
logger.startGroup('Updating status to All Clear...');
|
|
178
|
+
try {
|
|
179
|
+
await provider.postAllClear();
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
logger.warning(`Failed to post all-clear status: ${error}`);
|
|
183
|
+
}
|
|
184
|
+
logger.endGroup();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const duration = Date.now() - startTime;
|
|
188
|
+
(0, logger_1.logStructured)(logger, 'info', 'Decision Guardian completed successfully', {
|
|
189
|
+
pr_number: github.context.payload.pull_request?.number,
|
|
190
|
+
file_count: processedFileCount,
|
|
191
|
+
decision_count: parseResult.decisions.length,
|
|
192
|
+
match_count: matches.length,
|
|
193
|
+
duration_ms: duration,
|
|
194
|
+
});
|
|
195
|
+
metrics_1.metrics.setDuration(duration);
|
|
196
|
+
reportMetrics(config);
|
|
197
|
+
logger.info('✅ Decision Guardian completed successfully');
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
201
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
202
|
+
errors.push(message);
|
|
203
|
+
(0, logger_1.logStructured)(logger, 'error', 'Decision Guardian failed', {
|
|
204
|
+
duration_ms: Date.now() - startTime,
|
|
205
|
+
errors,
|
|
206
|
+
});
|
|
207
|
+
logger.setFailed(`Action failed: ${message}`);
|
|
208
|
+
if (stack) {
|
|
209
|
+
logger.debug(stack);
|
|
210
|
+
}
|
|
211
|
+
metrics_1.metrics.setDuration(Date.now() - startTime);
|
|
212
|
+
// Send telemetry only if config was loaded successfully
|
|
213
|
+
if (config) {
|
|
214
|
+
reportMetrics(config);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const ConfigSchema = zod_1.z.object({
|
|
219
|
+
decisionFile: zod_1.z
|
|
220
|
+
.string()
|
|
221
|
+
.regex(/^[a-zA-Z0-9._/-]+$/, 'Invalid characters in decision file path')
|
|
222
|
+
.refine((val) => !val.includes('..'), 'Path traversal not allowed'),
|
|
223
|
+
failOnCritical: zod_1.z.boolean(),
|
|
224
|
+
failOnError: zod_1.z.boolean(),
|
|
225
|
+
token: zod_1.z.string().min(1, 'Token cannot be empty'),
|
|
226
|
+
});
|
|
227
|
+
/**
|
|
228
|
+
* Load action configuration from inputs
|
|
229
|
+
*/
|
|
230
|
+
function loadConfig() {
|
|
231
|
+
const rawConfig = {
|
|
232
|
+
decisionFile: logger.getInput('decision_file') || '.decispher/decisions.md',
|
|
233
|
+
failOnCritical: logger.getBooleanInput('fail_on_critical'),
|
|
234
|
+
failOnError: logger.getBooleanInput('fail_on_error'),
|
|
235
|
+
token: logger.getInput('token', true),
|
|
236
|
+
};
|
|
237
|
+
const result = ConfigSchema.safeParse(rawConfig);
|
|
238
|
+
if (!result.success) {
|
|
239
|
+
const errorMessage = result.error.issues
|
|
240
|
+
.map((e) => `${e.path.join('.')}: ${e.message}`)
|
|
241
|
+
.join(', ');
|
|
242
|
+
throw new Error(`Invalid configuration: ${errorMessage}`);
|
|
243
|
+
}
|
|
244
|
+
if (path.isAbsolute(result.data.decisionFile)) {
|
|
245
|
+
throw new Error('decision_file must be a relative path');
|
|
246
|
+
}
|
|
247
|
+
const config = result.data;
|
|
248
|
+
logger.debug(`Configuration loaded: ${JSON.stringify({
|
|
249
|
+
...config,
|
|
250
|
+
token: '***',
|
|
251
|
+
})}`);
|
|
252
|
+
return config;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Report metrics using the decoupled snapshot approach
|
|
256
|
+
*/
|
|
257
|
+
function reportMetrics(_config) {
|
|
258
|
+
const snapshot = metrics_1.metrics.getSnapshot();
|
|
259
|
+
logger.info('=== Performance Metrics ===');
|
|
260
|
+
logger.info(`API Calls: ${snapshot.api_calls}`);
|
|
261
|
+
logger.info(`API Errors: ${snapshot.api_errors}`);
|
|
262
|
+
logger.info(`Rate Limit Hits: ${snapshot.rate_limit_hits}`);
|
|
263
|
+
logger.info(`Files Processed: ${snapshot.files_processed}`);
|
|
264
|
+
logger.info(`Decisions Evaluated: ${snapshot.decisions_evaluated}`);
|
|
265
|
+
logger.info(`Matches Found: ${snapshot.matches_found}`);
|
|
266
|
+
logger.info(`Duration: ${snapshot.duration_ms}ms`);
|
|
267
|
+
logger.setOutput('metrics', JSON.stringify(snapshot));
|
|
268
|
+
// Send telemetry (controlled by DG_TELEMETRY env variable)
|
|
269
|
+
(0, sender_1.sendTelemetry)('action', snapshot, version_1.VERSION).catch(() => { });
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Process large PRs using streaming
|
|
273
|
+
*/
|
|
274
|
+
async function processWithStreaming(decisions, provider) {
|
|
275
|
+
const matcher = new matcher_1.FileMatcher(decisions, logger);
|
|
276
|
+
const allMatches = [];
|
|
277
|
+
let processedCount = 0;
|
|
278
|
+
if (!provider.streamFileDiffs) {
|
|
279
|
+
throw new Error('Provider does not support streaming');
|
|
280
|
+
}
|
|
281
|
+
for await (const batch of provider.streamFileDiffs()) {
|
|
282
|
+
const batchMatches = await matcher.findMatchesWithDiffs(batch);
|
|
283
|
+
allMatches.push(...batchMatches);
|
|
284
|
+
processedCount += batch.length;
|
|
285
|
+
logger.info(`Processed ${processedCount} files, found ${allMatches.length} matches so far`);
|
|
286
|
+
}
|
|
287
|
+
return allMatches;
|
|
288
|
+
}
|
|
289
|
+
// Run the action
|
|
290
|
+
run();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildPayload = buildPayload;
|
|
4
|
+
function buildPayload(source, snapshot, version) {
|
|
5
|
+
return {
|
|
6
|
+
event: 'run_complete',
|
|
7
|
+
version,
|
|
8
|
+
source,
|
|
9
|
+
timestamp: new Date().toISOString(),
|
|
10
|
+
metrics: {
|
|
11
|
+
files_processed: snapshot.files_processed,
|
|
12
|
+
decisions_evaluated: snapshot.decisions_evaluated,
|
|
13
|
+
matches_found: snapshot.matches_found,
|
|
14
|
+
critical_matches: snapshot.critical_matches,
|
|
15
|
+
warning_matches: snapshot.warning_matches,
|
|
16
|
+
info_matches: snapshot.info_matches,
|
|
17
|
+
duration_ms: snapshot.duration_ms,
|
|
18
|
+
},
|
|
19
|
+
environment: {
|
|
20
|
+
node_version: process.version,
|
|
21
|
+
os_platform: process.platform,
|
|
22
|
+
ci: !!process.env.CI,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validatePrivacy = validatePrivacy;
|
|
4
|
+
const BLOCKED_FIELDS = new Set([
|
|
5
|
+
'repo_name',
|
|
6
|
+
'org_name',
|
|
7
|
+
'file_names',
|
|
8
|
+
'file_paths',
|
|
9
|
+
'pr_title',
|
|
10
|
+
'pr_body',
|
|
11
|
+
'decision_content',
|
|
12
|
+
'user_names',
|
|
13
|
+
'github_token',
|
|
14
|
+
'commit_message',
|
|
15
|
+
'branch_name',
|
|
16
|
+
'author',
|
|
17
|
+
'email',
|
|
18
|
+
]);
|
|
19
|
+
function validatePrivacy(payload) {
|
|
20
|
+
const violations = findBlockedKeys(payload);
|
|
21
|
+
if (violations.length > 0) {
|
|
22
|
+
throw new Error(`Telemetry privacy violation: blocked fields found: ${violations.join(', ')}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function findBlockedKeys(obj, prefix = '') {
|
|
26
|
+
const violations = [];
|
|
27
|
+
for (const key of Object.keys(obj)) {
|
|
28
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
29
|
+
if (BLOCKED_FIELDS.has(key.toLowerCase())) {
|
|
30
|
+
violations.push(fullKey);
|
|
31
|
+
}
|
|
32
|
+
if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
|
33
|
+
violations.push(...findBlockedKeys(obj[key], fullKey));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return violations;
|
|
37
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sendTelemetry = sendTelemetry;
|
|
4
|
+
const payload_1 = require("./payload");
|
|
5
|
+
const privacy_1 = require("./privacy");
|
|
6
|
+
const DEFAULT_ENDPOINT = 'https://decision-guardian-telemetry.decision-guardian-telemetry.workers.dev/collect';
|
|
7
|
+
const TIMEOUT_MS = 5000;
|
|
8
|
+
function isOptedIn(_source) {
|
|
9
|
+
// Unified telemetry control for both GitHub Actions and CLI via DG_TELEMETRY env
|
|
10
|
+
// Opt-out model: telemetry is enabled by default
|
|
11
|
+
// Users must explicitly set DG_TELEMETRY to '0' or 'false' to disable
|
|
12
|
+
if (process.env.DG_TELEMETRY === '0' || process.env.DG_TELEMETRY === 'false') {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
// Enabled by default if not set, or if set to '1' or 'true'
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
function getEndpoint() {
|
|
19
|
+
return process.env.DG_TELEMETRY_URL || DEFAULT_ENDPOINT;
|
|
20
|
+
}
|
|
21
|
+
async function sendTelemetry(source, snapshot, version) {
|
|
22
|
+
if (!isOptedIn(source))
|
|
23
|
+
return;
|
|
24
|
+
try {
|
|
25
|
+
const payload = (0, payload_1.buildPayload)(source, snapshot, version);
|
|
26
|
+
(0, privacy_1.validatePrivacy)(payload);
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
29
|
+
await fetch(getEndpoint(), {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify(payload),
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
});
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Silently fail — never break the tool
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/version.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "decision-guardian",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Surface architectural decision context on Pull Requests",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"decision-guardian": "./dist/cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"templates/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"bundle": "ncc build src/main.ts -o dist --license licenses.txt",
|
|
18
|
+
"build:cli": "ncc build src/cli/index.ts -o dist/cli --license licenses.txt",
|
|
19
|
+
"test": "jest",
|
|
20
|
+
"test:e2e": "npx ts-node scripts/e2e-test.ts",
|
|
21
|
+
"lint": "eslint src/**/*.ts",
|
|
22
|
+
"format": "prettier --write src/**/*.ts",
|
|
23
|
+
"all": "npm run format && npm run lint && npm run test && npm run build && npm run bundle"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"actions",
|
|
27
|
+
"github",
|
|
28
|
+
"documentation",
|
|
29
|
+
"architecture",
|
|
30
|
+
"decisions"
|
|
31
|
+
],
|
|
32
|
+
"author": "Decispher",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/DecispherHQ/decision-guardian.git"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@actions/core": "^1.10.1",
|
|
40
|
+
"@actions/github": "^6.0.0",
|
|
41
|
+
"minimatch": "^9.0.3",
|
|
42
|
+
"parse-diff": "^0.11.1",
|
|
43
|
+
"safe-regex": "^2.1.1",
|
|
44
|
+
"zod": "^4.3.5"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/jest": "^29.5.11",
|
|
48
|
+
"@types/node": "^20.0.0",
|
|
49
|
+
"@types/safe-regex": "^1.1.6",
|
|
50
|
+
"@typescript-eslint/eslint-plugin": "^6.18.0",
|
|
51
|
+
"@typescript-eslint/parser": "^6.18.0",
|
|
52
|
+
"@vercel/ncc": "^0.38.1",
|
|
53
|
+
"dotenv": "^17.2.3",
|
|
54
|
+
"eslint": "^8.56.0",
|
|
55
|
+
"jest": "^29.7.0",
|
|
56
|
+
"prettier": "^3.1.1",
|
|
57
|
+
"ts-jest": "^29.1.1",
|
|
58
|
+
"typescript": "^5.3.3"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<!-- DECISION-ADV-001 -->
|
|
2
|
+
## Decision: Config File Validation
|
|
3
|
+
**Status**: Active
|
|
4
|
+
**Date**: 2024-06-01
|
|
5
|
+
**Severity**: Critical
|
|
6
|
+
**Files**:
|
|
7
|
+
- `config/**/*.json`
|
|
8
|
+
- `config/**/*.yml`
|
|
9
|
+
|
|
10
|
+
**Rules**:
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"match": "any",
|
|
14
|
+
"conditions": [
|
|
15
|
+
{
|
|
16
|
+
"files": ["config/**/*.json"],
|
|
17
|
+
"content": {
|
|
18
|
+
"mode": "json_path",
|
|
19
|
+
"paths": ["database.host", "database.port", "database.password"]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"files": ["config/**/*.yml"],
|
|
24
|
+
"content": {
|
|
25
|
+
"mode": "regex",
|
|
26
|
+
"pattern": "(password|secret|api_key)\\s*[:=]",
|
|
27
|
+
"flags": "i"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Context
|
|
35
|
+
All config changes affecting database credentials must be reviewed.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
<!-- DECISION-ADV-002 -->
|
|
40
|
+
## Decision: License Header Enforcement
|
|
41
|
+
**Status**: Active
|
|
42
|
+
**Date**: 2024-06-15
|
|
43
|
+
**Severity**: Warning
|
|
44
|
+
**Files**:
|
|
45
|
+
- `src/**/*.ts`
|
|
46
|
+
|
|
47
|
+
**Rules**:
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"match": "all",
|
|
51
|
+
"conditions": [
|
|
52
|
+
{
|
|
53
|
+
"files": ["src/**/*.ts"],
|
|
54
|
+
"content": {
|
|
55
|
+
"mode": "line_range",
|
|
56
|
+
"start": 1,
|
|
57
|
+
"end": 5
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Context
|
|
65
|
+
Source files must contain license headers in the first 5 lines.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
<!-- DECISION-ADV-003 -->
|
|
70
|
+
## Decision: Deprecated API Detection
|
|
71
|
+
**Status**: Active
|
|
72
|
+
**Date**: 2024-07-01
|
|
73
|
+
**Severity**: Info
|
|
74
|
+
**Files**:
|
|
75
|
+
- `src/**/*.ts`
|
|
76
|
+
|
|
77
|
+
**Rules**:
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"match": "any",
|
|
81
|
+
"conditions": [
|
|
82
|
+
{
|
|
83
|
+
"files": ["src/**/*.ts"],
|
|
84
|
+
"content": {
|
|
85
|
+
"mode": "string",
|
|
86
|
+
"patterns": ["@deprecated", "TODO: remove"]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Context
|
|
94
|
+
Track usage of deprecated APIs and items marked for removal.
|
package/templates/api.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!-- DECISION-API-001 -->
|
|
2
|
+
## Decision: API Versioning
|
|
3
|
+
**Status**: Active
|
|
4
|
+
**Date**: 2024-05-01
|
|
5
|
+
**Severity**: Critical
|
|
6
|
+
**Files**:
|
|
7
|
+
- `src/api/v1/**/*`
|
|
8
|
+
- `src/routes/v1/**/*`
|
|
9
|
+
|
|
10
|
+
### Context
|
|
11
|
+
V1 API endpoints are frozen. New features go to V2. Modifying V1 routes breaks existing integrations.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<!-- DECISION-API-002 -->
|
|
16
|
+
## Decision: Rate Limiting Required
|
|
17
|
+
**Status**: Active
|
|
18
|
+
**Date**: 2024-05-15
|
|
19
|
+
**Severity**: Warning
|
|
20
|
+
**Files**:
|
|
21
|
+
- `src/api/**/*.ts`
|
|
22
|
+
- `src/routes/**/*.ts`
|
|
23
|
+
|
|
24
|
+
**Rules**:
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"match": "any",
|
|
28
|
+
"conditions": [
|
|
29
|
+
{
|
|
30
|
+
"files": ["src/api/**/*.ts", "src/routes/**/*.ts"],
|
|
31
|
+
"content": {
|
|
32
|
+
"mode": "string",
|
|
33
|
+
"patterns": ["router.get(", "router.post(", "app.get(", "app.post("]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Context
|
|
41
|
+
All public endpoints must include rate limiting. New endpoints require load testing results.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
<!-- DECISION-API-003 -->
|
|
46
|
+
## Decision: Response Schema Validation
|
|
47
|
+
**Status**: Active
|
|
48
|
+
**Date**: 2024-06-01
|
|
49
|
+
**Severity**: Info
|
|
50
|
+
**Files**:
|
|
51
|
+
- `src/api/**/*.ts`
|
|
52
|
+
|
|
53
|
+
**Rules**:
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"match": "any",
|
|
57
|
+
"conditions": [
|
|
58
|
+
{
|
|
59
|
+
"files": ["src/api/**/*.ts"],
|
|
60
|
+
"content": {
|
|
61
|
+
"mode": "string",
|
|
62
|
+
"patterns": ["res.json(", "res.send(", "response.json("]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Context
|
|
70
|
+
API responses should use validated schemas. Check that response DTOs are defined.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!-- DECISION-001 -->
|
|
2
|
+
## Decision: Database Connection Pool
|
|
3
|
+
**Status**: Active
|
|
4
|
+
**Date**: 2024-01-15
|
|
5
|
+
**Severity**: Critical
|
|
6
|
+
**Files**:
|
|
7
|
+
- `src/db/pool.ts`
|
|
8
|
+
- `config/database.yml`
|
|
9
|
+
|
|
10
|
+
### Context
|
|
11
|
+
Connection pool size must stay at 20 to prevent exhaustion.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<!-- DECISION-002 -->
|
|
16
|
+
## Decision: API Rate Limiting
|
|
17
|
+
**Status**: Active
|
|
18
|
+
**Date**: 2024-02-01
|
|
19
|
+
**Severity**: Warning
|
|
20
|
+
**Files**:
|
|
21
|
+
- `src/api/**/*.ts`
|
|
22
|
+
|
|
23
|
+
### Context
|
|
24
|
+
All new API endpoints must implement the standard rate limiter middleware.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<!-- DECISION-003 -->
|
|
29
|
+
## Decision: Authentication Module
|
|
30
|
+
**Status**: Active
|
|
31
|
+
**Date**: 2024-03-10
|
|
32
|
+
**Severity**: Critical
|
|
33
|
+
**Files**:
|
|
34
|
+
- `src/auth/**/*`
|
|
35
|
+
- `config/auth.json`
|
|
36
|
+
|
|
37
|
+
### Context
|
|
38
|
+
Auth module changes require security team review.
|