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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +792 -0
  3. package/dist/adapters/github/actions-logger.js +88 -0
  4. package/dist/adapters/github/comment.js +601 -0
  5. package/dist/adapters/github/github-provider.js +260 -0
  6. package/dist/adapters/github/health.js +56 -0
  7. package/dist/adapters/local/console-logger.js +46 -0
  8. package/dist/adapters/local/local-git-provider.js +247 -0
  9. package/dist/cli/commands/check.js +134 -0
  10. package/dist/cli/commands/init.js +58 -0
  11. package/dist/cli/commands/template.js +70 -0
  12. package/dist/cli/formatter.js +68 -0
  13. package/dist/cli/index.js +12458 -0
  14. package/dist/cli/licenses.txt +143 -0
  15. package/dist/cli/paths.js +40 -0
  16. package/dist/core/content-matchers.js +333 -0
  17. package/dist/core/health.js +52 -0
  18. package/dist/core/interfaces/index.js +2 -0
  19. package/dist/core/interfaces/logger.js +2 -0
  20. package/dist/core/interfaces/scm-provider.js +5 -0
  21. package/dist/core/logger.js +20 -0
  22. package/dist/core/matcher.js +184 -0
  23. package/dist/core/metrics.js +87 -0
  24. package/dist/core/parser.js +338 -0
  25. package/dist/core/rule-evaluator.js +186 -0
  26. package/dist/core/rule-parser.js +211 -0
  27. package/dist/core/rule-types.js +22 -0
  28. package/dist/core/trie.js +83 -0
  29. package/dist/core/types.js +2 -0
  30. package/dist/index.js +61142 -0
  31. package/dist/licenses.txt +758 -0
  32. package/dist/main.js +290 -0
  33. package/dist/telemetry/payload.js +25 -0
  34. package/dist/telemetry/privacy.js +37 -0
  35. package/dist/telemetry/sender.js +40 -0
  36. package/dist/version.js +7 -0
  37. package/package.json +60 -0
  38. package/templates/advanced-rules.md +94 -0
  39. package/templates/api.md +70 -0
  40. package/templates/basic.md +38 -0
  41. package/templates/database.md +81 -0
  42. 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
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VERSION = void 0;
4
+ /**
5
+ * Version information for Decision Guardian
6
+ */
7
+ exports.VERSION = '1.1.0';
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.
@@ -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.