contextos-agents 2.0.0 → 2.1.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.
- package/.agents/AGENTS.md +53 -33
- package/.agents/adapters/aider/export.js +41 -14
- package/.agents/adapters/claude/export.js +54 -3
- package/.agents/adapters/copilot/export.js +1 -1
- package/.agents/adapters/cursor/export.js +1 -1
- package/.agents/adapters/drift-detector.js +86 -10
- package/.agents/adapters/gemini/export.js +1 -1
- package/.agents/adapters/pure-compiler.js +28 -6
- package/.agents/adapters/shared.js +13 -4
- package/.agents/adapters/zed/export.js +1 -1
- package/.agents/compiled/registry.v2.json +29 -25
- package/.agents/compiled/registry.v2.sha256 +1 -1
- package/.agents/core/skills/context-os/SKILL.md +34 -37
- package/.agents/core/skills/engineering-workflow/SKILL.md +24 -24
- package/.agents/core/skills/gemini-precision/EXAMPLES.md +72 -0
- package/.agents/core/skills/gemini-precision/SKILL.md +2 -1
- package/.agents/core/skills/gemini-precision/TROUBLESHOOTING.md +25 -0
- package/.agents/core/skills/gemini-precision/skill.yaml +2 -0
- package/.agents/core/skills/gstack-roles/SKILL.md +7 -6
- package/.agents/core/skills/security/SKILL.md +44 -16
- package/.agents/core/skills/security/skill.yaml +0 -1
- package/.agents/ctx.js +20 -14
- package/.agents/generated/claude/skills/context-os/SKILL.md +34 -37
- package/.agents/generated/claude/skills/engineering-workflow/SKILL.md +24 -24
- package/.agents/generated/claude/skills/gemini-precision/SKILL.md +102 -1
- package/.agents/generated/claude/skills/gstack-roles/SKILL.md +7 -6
- package/.agents/generated/claude/skills/security/SKILL.md +44 -16
- package/.agents/generated/gemini/skills/context-os/SKILL.md +34 -37
- package/.agents/generated/gemini/skills/engineering-workflow/SKILL.md +24 -24
- package/.agents/generated/gemini/skills/gemini-precision/SKILL.md +105 -1
- package/.agents/generated/gemini/skills/gstack-roles/SKILL.md +7 -6
- package/.agents/generated/gemini/skills/security/SKILL.md +44 -125
- package/.agents/plugins.js +105 -8
- package/.agents/profiles.js +32 -11
- package/.agents/resolver/canonical-resolver.js +7 -7
- package/.agents/validate.js +69 -1
- package/README.md +81 -24
- package/bin/commands/hook.js +167 -0
- package/bin/commands/scan.js +77 -0
- package/bin/commands.js +39 -1
- package/bin/index.js +151 -34
- package/bin/lib/gate.js +171 -0
- package/bin/lib/git-snapshot.js +214 -0
- package/bin/lib/scan.js +461 -0
- package/catalog/skills/adapters/EXAMPLES.md +19 -0
- package/catalog/skills/adapters/SKILL.md +101 -0
- package/catalog/skills/adapters/TROUBLESHOOTING.md +7 -0
- package/catalog/skills/adapters/VALIDATION.json +12 -0
- package/catalog/skills/adapters/skill.yaml +13 -0
- package/catalog/skills/api-design/EXAMPLES.md +91 -0
- package/catalog/skills/api-design/SKILL.md +63 -0
- package/catalog/skills/api-design/TROUBLESHOOTING.md +54 -0
- package/catalog/skills/api-design/VALIDATION.json +11 -0
- package/catalog/skills/api-design/skill.yaml +14 -0
- package/catalog/skills/architecture-diagrams/SKILL.md +108 -0
- package/catalog/skills/architecture-diagrams/VALIDATION.json +12 -0
- package/catalog/skills/architecture-diagrams/skill.yaml +9 -0
- package/catalog/skills/brutalist-design/EXAMPLES.md +59 -0
- package/catalog/skills/brutalist-design/SKILL.md +150 -0
- package/catalog/skills/brutalist-design/VALIDATION.json +12 -0
- package/catalog/skills/brutalist-design/skill.yaml +10 -0
- package/catalog/skills/ci-cd/EXAMPLES.md +79 -0
- package/catalog/skills/ci-cd/SKILL.md +69 -0
- package/catalog/skills/ci-cd/TROUBLESHOOTING.md +52 -0
- package/catalog/skills/ci-cd/VALIDATION.json +11 -0
- package/catalog/skills/ci-cd/skill.yaml +13 -0
- package/catalog/skills/database/EXAMPLES.md +74 -0
- package/catalog/skills/database/SKILL.md +101 -0
- package/catalog/skills/database/TROUBLESHOOTING.md +18 -0
- package/catalog/skills/database/VALIDATION.json +11 -0
- package/catalog/skills/database/skill.yaml +14 -0
- package/catalog/skills/ddd/EXAMPLES.md +42 -0
- package/catalog/skills/ddd/SKILL.md +247 -0
- package/catalog/skills/ddd/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/ddd/VALIDATION.json +12 -0
- package/catalog/skills/ddd/skill.yaml +14 -0
- package/catalog/skills/decisions/EXAMPLES.md +35 -0
- package/catalog/skills/decisions/SKILL.md +90 -0
- package/catalog/skills/decisions/TROUBLESHOOTING.md +13 -0
- package/catalog/skills/decisions/VALIDATION.json +12 -0
- package/catalog/skills/decisions/skill.yaml +13 -0
- package/catalog/skills/docker/EXAMPLES.md +56 -0
- package/catalog/skills/docker/SKILL.md +169 -0
- package/catalog/skills/docker/TROUBLESHOOTING.md +18 -0
- package/catalog/skills/docker/VALIDATION.json +11 -0
- package/catalog/skills/docker/skill.yaml +13 -0
- package/catalog/skills/fastapi/EXAMPLES.md +36 -0
- package/catalog/skills/fastapi/SKILL.md +171 -0
- package/catalog/skills/fastapi/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/fastapi/VALIDATION.json +12 -0
- package/catalog/skills/fastapi/skill.yaml +14 -0
- package/catalog/skills/generators/EXAMPLES.md +19 -0
- package/catalog/skills/generators/SKILL.md +110 -0
- package/catalog/skills/generators/TROUBLESHOOTING.md +7 -0
- package/catalog/skills/generators/VALIDATION.json +12 -0
- package/catalog/skills/generators/skill.yaml +22 -0
- package/catalog/skills/generators/templates/API.md +77 -0
- package/catalog/skills/generators/templates/ARCHITECTURE.md +70 -0
- package/catalog/skills/generators/templates/DATABASE.md +42 -0
- package/catalog/skills/generators/templates/DECISION.md +46 -0
- package/catalog/skills/generators/templates/PRD.md +67 -0
- package/catalog/skills/generators/templates/PROJECT_GRAPH.md +56 -0
- package/catalog/skills/generators/templates/ROADMAP.md +51 -0
- package/catalog/skills/generators/templates/TASKS.md +43 -0
- package/catalog/skills/generators/templates/UI.md +73 -0
- package/catalog/skills/graphify/EXAMPLES.md +73 -0
- package/catalog/skills/graphify/SKILL.md +130 -0
- package/catalog/skills/graphify/VALIDATION.json +12 -0
- package/catalog/skills/graphify/skill.yaml +13 -0
- package/catalog/skills/impeccable-design/EXAMPLES.md +26 -0
- package/catalog/skills/impeccable-design/SKILL.md +201 -0
- package/catalog/skills/impeccable-design/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/impeccable-design/VALIDATION.json +12 -0
- package/catalog/skills/impeccable-design/skill.yaml +15 -0
- package/catalog/skills/interview-me/SKILL.md +97 -0
- package/catalog/skills/interview-me/VALIDATION.json +12 -0
- package/catalog/skills/interview-me/skill.yaml +9 -0
- package/catalog/skills/microservices/EXAMPLES.md +38 -0
- package/catalog/skills/microservices/SKILL.md +164 -0
- package/catalog/skills/microservices/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/microservices/VALIDATION.json +12 -0
- package/catalog/skills/microservices/skill.yaml +14 -0
- package/catalog/skills/minimalist-design/EXAMPLES.md +58 -0
- package/catalog/skills/minimalist-design/SKILL.md +113 -0
- package/catalog/skills/minimalist-design/VALIDATION.json +12 -0
- package/catalog/skills/minimalist-design/skill.yaml +10 -0
- package/catalog/skills/nestjs/EXAMPLES.md +40 -0
- package/catalog/skills/nestjs/SKILL.md +139 -0
- package/catalog/skills/nestjs/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/nestjs/VALIDATION.json +12 -0
- package/catalog/skills/nestjs/skill.yaml +14 -0
- package/catalog/skills/nextjs/EXAMPLES.md +40 -0
- package/catalog/skills/nextjs/SKILL.md +163 -0
- package/catalog/skills/nextjs/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/nextjs/VALIDATION.json +12 -0
- package/catalog/skills/nextjs/skill.yaml +14 -0
- package/catalog/skills/node/EXAMPLES.md +80 -0
- package/catalog/skills/node/SKILL.md +128 -0
- package/catalog/skills/node/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/node/VALIDATION.json +12 -0
- package/catalog/skills/node/skill.yaml +14 -0
- package/catalog/skills/performance/EXAMPLES.md +30 -0
- package/catalog/skills/performance/SKILL.md +75 -0
- package/catalog/skills/performance/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/performance/VALIDATION.json +12 -0
- package/catalog/skills/performance/skill.yaml +14 -0
- package/catalog/skills/react/EXAMPLES.md +79 -0
- package/catalog/skills/react/SKILL.md +132 -0
- package/catalog/skills/react/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/react/VALIDATION.json +12 -0
- package/catalog/skills/react/skill.yaml +14 -0
- package/catalog/skills/react-best-practices/SKILL.md +158 -0
- package/catalog/skills/react-best-practices/VALIDATION.json +12 -0
- package/catalog/skills/react-best-practices/skill.yaml +13 -0
- package/catalog/skills/redesign-audit/SKILL.md +117 -0
- package/catalog/skills/redesign-audit/VALIDATION.json +12 -0
- package/catalog/skills/redesign-audit/skill.yaml +9 -0
- package/catalog/skills/security-audit/EXAMPLES.md +79 -0
- package/catalog/skills/security-audit/SKILL.md +91 -0
- package/catalog/skills/security-audit/TROUBLESHOOTING.md +46 -0
- package/catalog/skills/security-audit/VALIDATION.json +11 -0
- package/catalog/skills/security-audit/skill.yaml +14 -0
- package/catalog/skills/soft-design/EXAMPLES.md +51 -0
- package/catalog/skills/soft-design/SKILL.md +108 -0
- package/catalog/skills/soft-design/VALIDATION.json +12 -0
- package/catalog/skills/soft-design/skill.yaml +10 -0
- package/catalog/skills/state-management/EXAMPLES.md +56 -0
- package/catalog/skills/state-management/SKILL.md +168 -0
- package/catalog/skills/state-management/TROUBLESHOOTING.md +18 -0
- package/catalog/skills/state-management/VALIDATION.json +11 -0
- package/catalog/skills/state-management/skill.yaml +14 -0
- package/catalog/skills/subagent-orchestrator/SKILL.md +117 -0
- package/catalog/skills/subagent-orchestrator/VALIDATION.json +12 -0
- package/catalog/skills/subagent-orchestrator/skill.yaml +9 -0
- package/catalog/skills/system-design/EXAMPLES.md +75 -0
- package/catalog/skills/system-design/SKILL.md +419 -0
- package/catalog/skills/system-design/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/system-design/VALIDATION.json +12 -0
- package/catalog/skills/system-design/skill.yaml +14 -0
- package/catalog/skills/terraform/EXAMPLES.md +74 -0
- package/catalog/skills/terraform/SKILL.md +55 -0
- package/catalog/skills/terraform/TROUBLESHOOTING.md +53 -0
- package/catalog/skills/terraform/VALIDATION.json +11 -0
- package/catalog/skills/terraform/skill.yaml +14 -0
- package/catalog/skills/testing/EXAMPLES.md +122 -0
- package/catalog/skills/testing/SKILL.md +70 -0
- package/catalog/skills/testing/TROUBLESHOOTING.md +18 -0
- package/catalog/skills/testing/VALIDATION.json +11 -0
- package/catalog/skills/testing/skill.yaml +14 -0
- package/catalog/skills/typescript/EXAMPLES.md +64 -0
- package/catalog/skills/typescript/SKILL.md +112 -0
- package/catalog/skills/typescript/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/typescript/VALIDATION.json +12 -0
- package/catalog/skills/typescript/skill.yaml +14 -0
- package/catalog/skills/ui-design/EXAMPLES.md +21 -0
- package/catalog/skills/ui-design/SKILL.md +124 -0
- package/catalog/skills/ui-design/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/ui-design/VALIDATION.json +12 -0
- package/catalog/skills/ui-design/skill.yaml +16 -0
- package/catalog/skills/ui-ux-pro/EXAMPLES.md +62 -0
- package/catalog/skills/ui-ux-pro/SKILL.md +418 -0
- package/catalog/skills/ui-ux-pro/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/ui-ux-pro/VALIDATION.json +12 -0
- package/catalog/skills/ui-ux-pro/skill.yaml +14 -0
- package/catalog/skills/ux-design/EXAMPLES.md +36 -0
- package/catalog/skills/ux-design/SKILL.md +116 -0
- package/catalog/skills/ux-design/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/ux-design/VALIDATION.json +12 -0
- package/catalog/skills/ux-design/skill.yaml +16 -0
- package/catalog/skills/vercel-optimize/SKILL.md +83 -0
- package/catalog/skills/vercel-optimize/VALIDATION.json +12 -0
- package/catalog/skills/vercel-optimize/scripts/collect-signals.mjs +131 -0
- package/catalog/skills/vercel-optimize/scripts/gate-investigations.mjs +142 -0
- package/catalog/skills/vercel-optimize/scripts/merge-signals.mjs +143 -0
- package/catalog/skills/vercel-optimize/scripts/scan-codebase.mjs +174 -0
- package/catalog/skills/vercel-optimize/skill.yaml +15 -0
- package/catalog/skills/web-accessibility/EXAMPLES.md +39 -0
- package/catalog/skills/web-accessibility/SKILL.md +151 -0
- package/catalog/skills/web-accessibility/TROUBLESHOOTING.md +19 -0
- package/catalog/skills/web-accessibility/VALIDATION.json +12 -0
- package/catalog/skills/web-accessibility/skill.yaml +14 -0
- package/package.json +5 -2
- package/.agents/core/skills/security/security.md +0 -106
package/bin/lib/scan.js
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bin/lib/scan.js
|
|
3
|
+
* ContextOS Staged Index Scanner & Code Governance Engine
|
|
4
|
+
*
|
|
5
|
+
* Inspects Git staged changes for:
|
|
6
|
+
* 1. Hardcoded secrets, API tokens, private keys, and blocked files
|
|
7
|
+
* 2. Unfinished lazy placeholder stubs in newly added code
|
|
8
|
+
* 3. Scope containment violations against declared task scopes
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const {
|
|
16
|
+
findGitRoot,
|
|
17
|
+
getStagedFiles,
|
|
18
|
+
getStagedBlob,
|
|
19
|
+
getStagedAddedLines,
|
|
20
|
+
} = require('./git-snapshot.js');
|
|
21
|
+
|
|
22
|
+
// ── Blocked File Names and Extensions ───────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const BLOCKED_EXACT_NAMES = new Set([
|
|
25
|
+
'.env',
|
|
26
|
+
'.env.local',
|
|
27
|
+
'.env.production',
|
|
28
|
+
'.env.development',
|
|
29
|
+
'.env.staging',
|
|
30
|
+
'.env.test',
|
|
31
|
+
'.netrc',
|
|
32
|
+
'.git-credentials',
|
|
33
|
+
'.npmrc',
|
|
34
|
+
'credentials.json',
|
|
35
|
+
'service-account.json',
|
|
36
|
+
'serviceaccountkey.json',
|
|
37
|
+
'id_rsa',
|
|
38
|
+
'id_ed25519',
|
|
39
|
+
'mcp_config.json',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const BLOCKED_EXTENSIONS = new Set([
|
|
43
|
+
'.key',
|
|
44
|
+
'.pem',
|
|
45
|
+
'.pfx',
|
|
46
|
+
'.p12',
|
|
47
|
+
'.pkcs12',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
// ── Secret Content Patterns ──────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
const SECRET_PATTERNS = [
|
|
53
|
+
{
|
|
54
|
+
ruleId: 'SEC-001',
|
|
55
|
+
name: 'Private Key Header',
|
|
56
|
+
pattern: /-----BEGIN\s+(?:RSA|OPENSSH|EC|DSA|PGP)?\s*PRIVATE\s+KEY-----/,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
ruleId: 'SEC-002',
|
|
60
|
+
name: 'Hardcoded User Home Path Leak',
|
|
61
|
+
pattern: /(?:[a-zA-Z]:[/\\]Users[/\\]|\/(?:home|Users)\/)[a-zA-Z0-9_-]+[/\\](?:Desktop|Documents|Downloads|code|projects|repos)\b/i,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
ruleId: 'SEC-003',
|
|
65
|
+
name: 'GitHub Personal Access Token',
|
|
66
|
+
pattern: /\b(?:ghp|gho)_[A-Za-z0-9]{36}\b/,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
ruleId: 'SEC-004',
|
|
70
|
+
name: 'GitHub Fine-Grained PAT',
|
|
71
|
+
pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
ruleId: 'SEC-005',
|
|
75
|
+
name: 'Google API Key',
|
|
76
|
+
pattern: /\bAIza[A-Za-z0-9_-]{35}\b/,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
ruleId: 'SEC-006',
|
|
80
|
+
name: 'OpenAI API Key',
|
|
81
|
+
pattern: /\bsk-[A-Za-z0-9]{32,}\b/,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
ruleId: 'SEC-007',
|
|
85
|
+
name: 'Anthropic API Key',
|
|
86
|
+
pattern: /\bsk-ant-[A-Za-z0-9-]{90,}\b/,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
ruleId: 'SEC-008',
|
|
90
|
+
name: 'OpenRouter API Key',
|
|
91
|
+
pattern: /\bsk-or-v1-[a-f0-9]{64}\b/,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
ruleId: 'SEC-009',
|
|
95
|
+
name: 'Slack Token',
|
|
96
|
+
pattern: /\bxox[baprs]-[A-Za-z0-9_-]{10,48}\b/,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
ruleId: 'SEC-010',
|
|
100
|
+
name: 'npm Access Token',
|
|
101
|
+
pattern: /\bnpm_[A-Za-z0-9]{32,36}\b/,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
ruleId: 'SEC-011',
|
|
105
|
+
name: 'AWS Access Key ID',
|
|
106
|
+
pattern: /\bAKIA[0-9A-Z]{16}\b/,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
// ── Lazy Placeholder Patterns ───────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
const PLACEHOLDER_PATTERNS = [
|
|
113
|
+
{
|
|
114
|
+
ruleId: 'CODE-001',
|
|
115
|
+
name: 'Lazy Stub Comment',
|
|
116
|
+
pattern: /(?:\/\/|\/\*|#)\s*(?:TODO:\s*implement later|\.\.\.\s*rest of code stays here\s*\.\.\.|TODO:\s*implement\b)/i,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
ruleId: 'CODE-002',
|
|
120
|
+
name: 'NotImplementedError Stub',
|
|
121
|
+
pattern: /raise\s+NotImplementedError\s*\(\s*["'].*TODO.*["']\s*\)/i,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
ruleId: 'CODE-003',
|
|
125
|
+
name: 'Python Pass Stub',
|
|
126
|
+
pattern: /^\s*pass\s*#\s*TODO\b/i,
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
// Exempt path segments and exact files (e.g. test fixtures, test files)
|
|
131
|
+
const EXEMPT_SEGMENTS = new Set([
|
|
132
|
+
'tests',
|
|
133
|
+
'.git',
|
|
134
|
+
'node_modules',
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
const EXEMPT_EXACT_PATHS = new Set([
|
|
138
|
+
'scripts/check-secrets.js',
|
|
139
|
+
'bin/lib/scan.js',
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
function isExempt(filePath) {
|
|
143
|
+
const norm = filePath.replace(/\\/g, '/');
|
|
144
|
+
if (EXEMPT_EXACT_PATHS.has(norm)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
const segments = norm.split('/');
|
|
148
|
+
return segments.some(seg => EXEMPT_SEGMENTS.has(seg));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function redact(str) {
|
|
152
|
+
if (str.length <= 8) return '***';
|
|
153
|
+
return str.slice(0, 4) + '...' + str.slice(-4);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Checks if a relative path matches a glob pattern or prefix with strict segment boundaries.
|
|
158
|
+
*/
|
|
159
|
+
function matchesScope(filePath, pattern) {
|
|
160
|
+
const normFile = filePath.replace(/\\/g, '/').toLowerCase();
|
|
161
|
+
const normPattern = pattern.replace(/\\/g, '/').toLowerCase();
|
|
162
|
+
|
|
163
|
+
if (normPattern.endsWith('/**')) {
|
|
164
|
+
const prefix = normPattern.slice(0, -3);
|
|
165
|
+
return normFile === prefix || normFile.startsWith(prefix + '/');
|
|
166
|
+
}
|
|
167
|
+
if (normPattern.endsWith('/*')) {
|
|
168
|
+
const prefix = normPattern.slice(0, -2);
|
|
169
|
+
if (!normFile.startsWith(prefix + '/')) return false;
|
|
170
|
+
const rest = normFile.slice(prefix.length + 1);
|
|
171
|
+
return !rest.includes('/');
|
|
172
|
+
}
|
|
173
|
+
if (normPattern.includes('*')) {
|
|
174
|
+
const reg = new RegExp('^' + normPattern.replace(/\./g, '\\.').replace(/\*/g, '[^/]*') + '$');
|
|
175
|
+
return reg.test(normFile);
|
|
176
|
+
}
|
|
177
|
+
return normFile === normPattern || normFile.startsWith(normPattern + '/');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Runs the staged scanner on the given repository root.
|
|
182
|
+
*
|
|
183
|
+
* @param {Object} options - Scan configuration options
|
|
184
|
+
* @returns {Object} Structured scan results with findings and status code
|
|
185
|
+
*/
|
|
186
|
+
function runScan(options = {}) {
|
|
187
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
188
|
+
const gitRoot = findGitRoot(cwd);
|
|
189
|
+
|
|
190
|
+
if (!gitRoot) {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
code: 2,
|
|
194
|
+
error: 'Not a Git repository. ContextOS scanner requires Git.',
|
|
195
|
+
findings: [],
|
|
196
|
+
stats: { filesScanned: 0, violations: 0 },
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const checkSecrets = options.secrets !== false;
|
|
201
|
+
const checkPlaceholders = Boolean(options.placeholders);
|
|
202
|
+
const enforce = Boolean(options.enforce);
|
|
203
|
+
|
|
204
|
+
let targetScopeFile = options.scope || null;
|
|
205
|
+
let scopeStatus = 'not_configured';
|
|
206
|
+
|
|
207
|
+
if (!targetScopeFile) {
|
|
208
|
+
const defaultScope = path.join(gitRoot, '.agents', 'task-scope.json');
|
|
209
|
+
if (fs.existsSync(defaultScope)) {
|
|
210
|
+
targetScopeFile = '.agents/task-scope.json';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let allowedScopePatterns = null;
|
|
215
|
+
if (targetScopeFile) {
|
|
216
|
+
const resolvedScopePath = path.resolve(gitRoot, targetScopeFile);
|
|
217
|
+
if (!fs.existsSync(resolvedScopePath)) {
|
|
218
|
+
return {
|
|
219
|
+
ok: false,
|
|
220
|
+
code: 2,
|
|
221
|
+
error: `Scope file not found: ${targetScopeFile}`,
|
|
222
|
+
findings: [],
|
|
223
|
+
stats: { filesScanned: 0, violations: 0 },
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const scopeData = JSON.parse(fs.readFileSync(resolvedScopePath, 'utf8'));
|
|
228
|
+
const list = Array.isArray(scopeData) ? scopeData : scopeData.allowedPaths || scopeData.files;
|
|
229
|
+
if (!Array.isArray(list)) {
|
|
230
|
+
throw new Error('Scope declaration must be an array of paths or have an allowedPaths array');
|
|
231
|
+
}
|
|
232
|
+
for (const p of list) {
|
|
233
|
+
if (path.isAbsolute(p) || p.includes('..')) {
|
|
234
|
+
throw new Error(`Scope entry "${p}" is invalid (must be relative and cannot traverse outside repo)`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
allowedScopePatterns = list;
|
|
238
|
+
scopeStatus = `active (${targetScopeFile})`;
|
|
239
|
+
} catch (err) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
code: 2,
|
|
243
|
+
error: `Invalid task scope file: ${err.message}`,
|
|
244
|
+
findings: [],
|
|
245
|
+
stats: { filesScanned: 0, violations: 0 },
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let stagedEntries = [];
|
|
251
|
+
try {
|
|
252
|
+
stagedEntries = getStagedFiles(gitRoot);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return {
|
|
255
|
+
ok: false,
|
|
256
|
+
code: 2,
|
|
257
|
+
error: `Failed to inspect Git index: ${err.message}`,
|
|
258
|
+
findings: [],
|
|
259
|
+
stats: { filesScanned: 0, violations: 0 },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const findings = [];
|
|
264
|
+
let addedLinesMap = new Map();
|
|
265
|
+
if (checkPlaceholders) {
|
|
266
|
+
try {
|
|
267
|
+
const activePaths = stagedEntries.filter(e => e.status !== 'D').map(e => e.path);
|
|
268
|
+
addedLinesMap = getStagedAddedLines(gitRoot, activePaths);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
code: 2,
|
|
273
|
+
error: `Failed to inspect staged added lines: ${err.message}`,
|
|
274
|
+
findings: [],
|
|
275
|
+
stats: { filesScanned: stagedEntries.length, violations: 0 },
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const entry of stagedEntries) {
|
|
281
|
+
const relPath = entry.path;
|
|
282
|
+
const baseName = path.basename(relPath).toLowerCase();
|
|
283
|
+
const extName = path.extname(relPath).toLowerCase();
|
|
284
|
+
|
|
285
|
+
// 1. Scope Containment Check (checked for ALL staged actions: added, modified, deleted, renamed)
|
|
286
|
+
if (allowedScopePatterns) {
|
|
287
|
+
const inScope = allowedScopePatterns.some(p => matchesScope(relPath, p));
|
|
288
|
+
if (!inScope) {
|
|
289
|
+
findings.push({
|
|
290
|
+
ruleId: 'SCOPE-001',
|
|
291
|
+
file: relPath,
|
|
292
|
+
type: 'Scope Violation',
|
|
293
|
+
severity: 'error',
|
|
294
|
+
details: `Staged file "${relPath}" is outside allowed task scope`,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (entry.oldPath) {
|
|
298
|
+
const oldInScope = allowedScopePatterns.some(p => matchesScope(entry.oldPath, p));
|
|
299
|
+
if (!oldInScope) {
|
|
300
|
+
findings.push({
|
|
301
|
+
ruleId: 'SCOPE-001',
|
|
302
|
+
file: entry.oldPath,
|
|
303
|
+
type: 'Scope Violation',
|
|
304
|
+
severity: 'error',
|
|
305
|
+
details: `Source file of rename "${entry.oldPath}" is outside allowed task scope`,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Skip deleted files from content and blocked filename checks
|
|
312
|
+
if (entry.status === 'D') continue;
|
|
313
|
+
|
|
314
|
+
// 2. Blocked Exact Names Check
|
|
315
|
+
if (checkSecrets && BLOCKED_EXACT_NAMES.has(baseName)) {
|
|
316
|
+
findings.push({
|
|
317
|
+
ruleId: 'SEC-000',
|
|
318
|
+
file: relPath,
|
|
319
|
+
type: 'Blocked Filename',
|
|
320
|
+
severity: 'error',
|
|
321
|
+
details: `Filename "${baseName}" is forbidden from being committed into Git.`,
|
|
322
|
+
});
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 3. Blocked Extensions Check
|
|
327
|
+
if (checkSecrets && BLOCKED_EXTENSIONS.has(extName)) {
|
|
328
|
+
findings.push({
|
|
329
|
+
ruleId: 'SEC-000',
|
|
330
|
+
file: relPath,
|
|
331
|
+
type: 'Blocked Extension',
|
|
332
|
+
severity: 'error',
|
|
333
|
+
details: `Extension "${extName}" indicates private cryptographic keys or certificates.`,
|
|
334
|
+
});
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Skip content scan for exempt paths
|
|
339
|
+
if (isExempt(relPath)) continue;
|
|
340
|
+
|
|
341
|
+
// Read blob from index (fails closed with code 2 on read errors in any mode)
|
|
342
|
+
let blob;
|
|
343
|
+
try {
|
|
344
|
+
blob = getStagedBlob(relPath, gitRoot);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
return {
|
|
347
|
+
ok: false,
|
|
348
|
+
code: 2,
|
|
349
|
+
error: `Failed to read staged blob for "${relPath}": ${err.message}`,
|
|
350
|
+
findings,
|
|
351
|
+
stats: { filesScanned: stagedEntries.length, violations: findings.length },
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!blob) {
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
code: 2,
|
|
359
|
+
error: `Failed to read staged blob for "${relPath}": file unreadable in git index`,
|
|
360
|
+
findings,
|
|
361
|
+
stats: { filesScanned: stagedEntries.length, violations: findings.length },
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Large files (> 2MB): fail-closed in enforce mode
|
|
366
|
+
if (blob.length > 2 * 1024 * 1024) {
|
|
367
|
+
if (enforce) {
|
|
368
|
+
findings.push({
|
|
369
|
+
ruleId: 'SCAN-SKIP-001',
|
|
370
|
+
file: relPath,
|
|
371
|
+
type: 'Skipped Large File',
|
|
372
|
+
severity: 'error',
|
|
373
|
+
details: `Staged file "${relPath}" exceeds 2MB limit (${(blob.length / (1024 * 1024)).toFixed(2)}MB) and cannot be verified for secrets in enforce mode.`,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const content = blob.toString('utf8');
|
|
380
|
+
const lines = content.split(/\r?\n/);
|
|
381
|
+
|
|
382
|
+
// 4. Staged Content Secrets Check
|
|
383
|
+
if (checkSecrets) {
|
|
384
|
+
for (let lineNum = 1; lineNum <= lines.length; lineNum++) {
|
|
385
|
+
const line = lines[lineNum - 1];
|
|
386
|
+
for (const { ruleId, name, pattern } of SECRET_PATTERNS) {
|
|
387
|
+
const match = line.match(pattern);
|
|
388
|
+
if (match) {
|
|
389
|
+
findings.push({
|
|
390
|
+
ruleId,
|
|
391
|
+
file: relPath,
|
|
392
|
+
line: lineNum,
|
|
393
|
+
type: name,
|
|
394
|
+
severity: 'error',
|
|
395
|
+
details: `Detected pattern "${name}": ${redact(match[0])}`,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 5. Newly Added Code Placeholders Check
|
|
403
|
+
if (checkPlaceholders && addedLinesMap.has(relPath)) {
|
|
404
|
+
const addedLines = addedLinesMap.get(relPath);
|
|
405
|
+
// Skip markdown and doc files for placeholder checks
|
|
406
|
+
if (extName !== '.md' && extName !== '.txt') {
|
|
407
|
+
for (const { line, content: addedLine } of addedLines) {
|
|
408
|
+
if (addedLine.includes('contextos:allow-placeholder')) continue;
|
|
409
|
+
|
|
410
|
+
for (const { ruleId, name, pattern } of PLACEHOLDER_PATTERNS) {
|
|
411
|
+
const match = addedLine.match(pattern);
|
|
412
|
+
if (match) {
|
|
413
|
+
findings.push({
|
|
414
|
+
ruleId,
|
|
415
|
+
file: relPath,
|
|
416
|
+
line,
|
|
417
|
+
type: name,
|
|
418
|
+
severity: 'warning',
|
|
419
|
+
details: `Detected un-implemented placeholder stub: "${match[0].trim()}"`,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const hasViolations = findings.length > 0;
|
|
429
|
+
// If enforce is true, violations return code 1. If enforce is false, warnings return code 0.
|
|
430
|
+
// Incomplete/failed inspection returns code 2 in all modes.
|
|
431
|
+
const exitCode = hasViolations && enforce ? 1 : 0;
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
ok: exitCode === 0,
|
|
435
|
+
code: exitCode,
|
|
436
|
+
message: exitCode === 0 ? 'Verification completed cleanly, no blocking violations' : 'Blocking violations detected in staged index',
|
|
437
|
+
gitRoot,
|
|
438
|
+
enforce,
|
|
439
|
+
scope: {
|
|
440
|
+
file: targetScopeFile,
|
|
441
|
+
status: scopeStatus,
|
|
442
|
+
},
|
|
443
|
+
findings,
|
|
444
|
+
stats: {
|
|
445
|
+
stagedFilesCount: stagedEntries.length,
|
|
446
|
+
violationsCount: findings.length,
|
|
447
|
+
errorsCount: findings.filter(f => f.severity === 'error').length,
|
|
448
|
+
warningsCount: findings.filter(f => f.severity === 'warning').length,
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
module.exports = {
|
|
454
|
+
runScan,
|
|
455
|
+
BLOCKED_EXACT_NAMES,
|
|
456
|
+
BLOCKED_EXTENSIONS,
|
|
457
|
+
SECRET_PATTERNS,
|
|
458
|
+
PLACEHOLDER_PATTERNS,
|
|
459
|
+
matchesScope,
|
|
460
|
+
redact,
|
|
461
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# adapters Examples - Anti-patterns vs ContextOS Standard
|
|
2
|
+
|
|
3
|
+
## Example 1: Multi-Agent Configuration
|
|
4
|
+
|
|
5
|
+
### Anti-pattern: Manually Syncing 6 Different Rule Files
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
Editing .cursorrules, then forgetting to update CLAUDE.md, then editing copilot-instructions.md.
|
|
9
|
+
Rules diverge across teammates using different IDEs.
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### Best practice: ContextOS Standard (Single Source of Truth)
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Edit skills once in .agents/core/skills/
|
|
16
|
+
# Compile to all agents with one command:
|
|
17
|
+
node .agents/ctx.js export all
|
|
18
|
+
# Automatically updates .cursorrules, CLAUDE.md, copilot-instructions.md, .aider, .zed
|
|
19
|
+
```
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: adapters
|
|
3
|
+
description: >
|
|
4
|
+
Generates agent-specific configuration files from the unified ContextOS format.
|
|
5
|
+
One internal system, multiple output formats for Claude, Gemini, Copilot,
|
|
6
|
+
Cursor, Aider, Continue, OpenHands, and others.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# agent-adapters
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Unified cross-agent configuration engine. Translates single ContextOS source rules into optimized native formats for Claude Code (CLAUDE.md), Gemini (.agents/skills), Cursor (.cursorrules, .cursor/rules/*.mdc), GitHub Copilot, Zed, Aider, and Continue.
|
|
14
|
+
|
|
15
|
+
## When to Use
|
|
16
|
+
|
|
17
|
+
Activate when configuring, synchronizing, or exporting agent rules and skills across multiple IDEs and AI programming assistants.
|
|
18
|
+
|
|
19
|
+
## Rules & Patterns
|
|
20
|
+
|
|
21
|
+
ContextOS is agent-agnostic. This skill generates the right config format for any AI agent.
|
|
22
|
+
|
|
23
|
+
## Supported Agents
|
|
24
|
+
|
|
25
|
+
| Agent | Config Target | Format |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| **Gemini / Antigravity** | `.agents/generated/gemini/skills/` | Native modular skills |
|
|
28
|
+
| **Claude Code** | `.agents/generated/claude/skills/` | Aggregated markdown skills |
|
|
29
|
+
| **GitHub Copilot** | `.github/copilot-instructions.md` | Single instructions file with on-demand skill references |
|
|
30
|
+
| **Cursor** | `.cursor/rules/*.mdc` + `.cursorrules` | MDC modular glob rules + fallback |
|
|
31
|
+
| **Aider** | `.aider.conf.yml` + `CONVENTIONS.md` | YAML config + conventions markdown |
|
|
32
|
+
| **Zed IDE** | `.zed/rules.md` + `.zed/prompts/*.md` | Native rules and prompt templates |
|
|
33
|
+
|
|
34
|
+
## Generation Commands
|
|
35
|
+
|
|
36
|
+
- `contextos export <agent>` (or `node .agents/ctx.js export <agent>`) - Generate config for a specific agent (`gemini`, `claude`, `cursor`, `copilot`, `aider`, `zed`).
|
|
37
|
+
- `contextos export all` - Generate configs for all supported agents in a single transaction.
|
|
38
|
+
|
|
39
|
+
## Adapter Logic
|
|
40
|
+
|
|
41
|
+
### For Claude Code (`CLAUDE.md`)
|
|
42
|
+
|
|
43
|
+
Compile into a single markdown file:
|
|
44
|
+
|
|
45
|
+
1. Project overview from `docs/PRD.md` (summary only)
|
|
46
|
+
2. Architecture summary from `docs/ARCHITECTURE.md`
|
|
47
|
+
3. Coding rules from loaded skills
|
|
48
|
+
4. Active Decision Records
|
|
49
|
+
5. Current tasks from `docs/TASKS.md`
|
|
50
|
+
|
|
51
|
+
### For Gemini (`.agents/AGENTS.md`)
|
|
52
|
+
|
|
53
|
+
Already native format. Just ensure:
|
|
54
|
+
|
|
55
|
+
1. `AGENTS.md` references the skill directory
|
|
56
|
+
2. Skills have proper SKILL.md with frontmatter
|
|
57
|
+
3. Context Manager rules are in AGENTS.md
|
|
58
|
+
|
|
59
|
+
### For Cursor (`.cursorrules`)
|
|
60
|
+
|
|
61
|
+
Compile into a flat text file:
|
|
62
|
+
|
|
63
|
+
1. Project context (condensed)
|
|
64
|
+
2. Coding style rules
|
|
65
|
+
3. Framework-specific instructions
|
|
66
|
+
4. Anti-patterns to avoid
|
|
67
|
+
|
|
68
|
+
### For Aider (`.aider.conf.yml`)
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
read:
|
|
72
|
+
- docs/ARCHITECTURE.md
|
|
73
|
+
- docs/API.md
|
|
74
|
+
- docs/PROJECT_GRAPH.md
|
|
75
|
+
conventions:
|
|
76
|
+
- {{coding rules from skills}}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Sync Rules
|
|
80
|
+
|
|
81
|
+
- Adapters read from the canonical ContextOS documents
|
|
82
|
+
- Never edit adapter output files directly
|
|
83
|
+
- Re-run `contextos export all` after any skill or rule change
|
|
84
|
+
- Each adapter file includes a header: `# Generated by ContextOS - do not edit directly`
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
## Code Examples
|
|
88
|
+
|
|
89
|
+
See `EXAMPLES.md` for detailed code examples.
|
|
90
|
+
|
|
91
|
+
## Validation Checklist
|
|
92
|
+
|
|
93
|
+
What to verify during the review phase before completing the task.
|
|
94
|
+
|
|
95
|
+
## Common Mistakes
|
|
96
|
+
|
|
97
|
+
Anti-patterns and things to explicitly avoid. See `TROUBLESHOOTING.md`.
|
|
98
|
+
|
|
99
|
+
## Integration Notes
|
|
100
|
+
|
|
101
|
+
How this skill interacts with other skills.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# adapters Troubleshooting & Common Mistakes
|
|
2
|
+
|
|
3
|
+
## 1. Overwriting Custom Configs
|
|
4
|
+
|
|
5
|
+
- **Symptom**: Custom non-ContextOS rules wiped out during export.
|
|
6
|
+
- **Root Cause**: Running export with force flags over unmanaged files.
|
|
7
|
+
- **Fix**: Keep custom project overrides in dedicated config files or use plugin skills.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
schemaVersion: 2
|
|
2
|
+
name: adapters
|
|
3
|
+
category: engineering
|
|
4
|
+
type: compiler
|
|
5
|
+
description: >
|
|
6
|
+
Generates agent-specific configuration files from unified ContextOS format.
|
|
7
|
+
Produces configs for Claude, Gemini, Copilot, Cursor, Aider, Continue, and Zed.
|
|
8
|
+
version: 1.0.0
|
|
9
|
+
resources:
|
|
10
|
+
- EXAMPLES.md
|
|
11
|
+
- SKILL.md
|
|
12
|
+
- TROUBLESHOOTING.md
|
|
13
|
+
- VALIDATION.json
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# API Design - Examples & Best Practices
|
|
2
|
+
|
|
3
|
+
## Example 1: Contract-First Interface with Zod & TypeScript
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
// 1. Unified Contract Definition
|
|
9
|
+
export const CreateTaskSchema = z.object({
|
|
10
|
+
title: z.string().min(1, 'Title is required').max(120, 'Title exceeds max length'),
|
|
11
|
+
description: z.string().max(1000).optional(),
|
|
12
|
+
priority: z.enum(['low', 'medium', 'high']).default('medium')
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type CreateTaskInput = z.infer<typeof CreateTaskSchema>;
|
|
16
|
+
|
|
17
|
+
export interface TaskDTO {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
description: string | null;
|
|
21
|
+
priority: 'low' | 'medium' | 'high';
|
|
22
|
+
createdAt: string; // ISO 8601 string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface APIErrorResponse {
|
|
26
|
+
error: {
|
|
27
|
+
code: string;
|
|
28
|
+
message: string;
|
|
29
|
+
details?: Array<{ field: string; message: string }>;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. Safe Edge Controller
|
|
34
|
+
export async function handleCreateTask(
|
|
35
|
+
body: unknown,
|
|
36
|
+
createTaskService: (input: CreateTaskInput) => Promise<TaskDTO>
|
|
37
|
+
): Promise<{ status: number; data: TaskDTO | APIErrorResponse }> {
|
|
38
|
+
const result = CreateTaskSchema.safeParse(body);
|
|
39
|
+
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
return {
|
|
42
|
+
status: 422,
|
|
43
|
+
data: {
|
|
44
|
+
error: {
|
|
45
|
+
code: 'VALIDATION_FAILED',
|
|
46
|
+
message: 'Request payload failed schema validation',
|
|
47
|
+
details: result.error.errors.map(err => ({
|
|
48
|
+
field: err.path.join('.'),
|
|
49
|
+
message: err.message
|
|
50
|
+
}))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const task = await createTaskService(result.data);
|
|
57
|
+
return { status: 201, data: task };
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Example 2: Idempotent Mutation Handler
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import crypto from 'node:crypto';
|
|
67
|
+
|
|
68
|
+
interface IdempotencyStore {
|
|
69
|
+
get(key: string): Promise<{ status: number; body: unknown } | null>;
|
|
70
|
+
set(key: string, result: { status: number; body: unknown }, ttlSeconds: number): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function withIdempotency<T>(
|
|
74
|
+
key: string | undefined,
|
|
75
|
+
store: IdempotencyStore,
|
|
76
|
+
handler: () => Promise<{ status: number; body: T }>
|
|
77
|
+
): Promise<{ status: number; body: T }> {
|
|
78
|
+
if (!key) {
|
|
79
|
+
return handler();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const cached = await store.get(key);
|
|
83
|
+
if (cached) {
|
|
84
|
+
return cached as { status: number; body: T };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const response = await handler();
|
|
88
|
+
await store.set(key, response, 86400); // 24-hour cache
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
```
|