@ekkos/cli 1.3.9 → 1.4.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/dist/commands/dashboard.js +520 -42
- package/dist/commands/gemini.d.ts +1 -0
- package/dist/commands/gemini.js +170 -10
- package/dist/commands/init-living-docs.d.ts +6 -0
- package/dist/commands/init-living-docs.js +57 -0
- package/dist/commands/living-docs.js +3 -3
- package/dist/commands/run.js +85 -21
- package/dist/commands/setup-ci.d.ts +3 -0
- package/dist/commands/setup-ci.js +107 -0
- package/dist/commands/validate-living-docs.d.ts +27 -0
- package/dist/commands/validate-living-docs.js +489 -0
- package/dist/index.js +109 -82
- package/dist/utils/state.d.ts +16 -3
- package/dist/utils/state.js +10 -3
- package/package.json +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.setupCiCommand = setupCiCommand;
|
|
40
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const WORKFLOW_CONTENT = `name: ekkOS Cortex Validation
|
|
44
|
+
|
|
45
|
+
on:
|
|
46
|
+
push:
|
|
47
|
+
branches: [main, master]
|
|
48
|
+
paths:
|
|
49
|
+
- '**/ekkOS_CONTEXT.md'
|
|
50
|
+
pull_request:
|
|
51
|
+
branches: [main, master]
|
|
52
|
+
paths:
|
|
53
|
+
- '**/ekkOS_CONTEXT.md'
|
|
54
|
+
|
|
55
|
+
jobs:
|
|
56
|
+
validate-cortex:
|
|
57
|
+
name: Validate Cortex Docs
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
timeout-minutes: 10
|
|
60
|
+
|
|
61
|
+
steps:
|
|
62
|
+
- name: Checkout repository
|
|
63
|
+
uses: actions/checkout@v4
|
|
64
|
+
|
|
65
|
+
- name: Setup Node.js
|
|
66
|
+
uses: actions/setup-node@v4
|
|
67
|
+
with:
|
|
68
|
+
node-version: '20'
|
|
69
|
+
|
|
70
|
+
- name: Validate Cortex docs
|
|
71
|
+
run: npx @google/gemini-cli docs validate
|
|
72
|
+
|
|
73
|
+
- name: Add validation summary
|
|
74
|
+
if: always()
|
|
75
|
+
run: |
|
|
76
|
+
echo "### ekkOS Cortex Validation" >> "$GITHUB_STEP_SUMMARY"
|
|
77
|
+
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
78
|
+
echo "Validated \`ekkOS_CONTEXT.md\` files across the repository." >> "$GITHUB_STEP_SUMMARY"
|
|
79
|
+
if [ "\${{ job.status }}" = "success" ]; then
|
|
80
|
+
echo "**Result:** All Cortex checks passed." >> "$GITHUB_STEP_SUMMARY"
|
|
81
|
+
else
|
|
82
|
+
echo "**Result:** Some Cortex checks failed. See logs for details." >> "$GITHUB_STEP_SUMMARY"
|
|
83
|
+
fi
|
|
84
|
+
`;
|
|
85
|
+
async function setupCiCommand(options) {
|
|
86
|
+
const repoRoot = options.repoRoot || process.cwd();
|
|
87
|
+
const workflowsDir = path.join(repoRoot, '.github', 'workflows');
|
|
88
|
+
const workflowFile = path.join(workflowsDir, 'ekkos-cortex-validation.yml');
|
|
89
|
+
console.log(chalk_1.default.cyan.bold('\n ekkOS Cortex CI/CD Setup'));
|
|
90
|
+
console.log(chalk_1.default.gray(' ─────────────────────────'));
|
|
91
|
+
if (!fs.existsSync(workflowsDir)) {
|
|
92
|
+
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
93
|
+
console.log(chalk_1.default.gray(` Created directory: .github/workflows`));
|
|
94
|
+
}
|
|
95
|
+
if (fs.existsSync(workflowFile)) {
|
|
96
|
+
console.log(chalk_1.default.yellow(` ⚠️ Workflow file already exists at: .github/workflows/ekkos-cortex-validation.yml`));
|
|
97
|
+
console.log(chalk_1.default.gray(` Skipping creation to avoid overwriting.`));
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
fs.writeFileSync(workflowFile, WORKFLOW_CONTENT, 'utf-8');
|
|
101
|
+
console.log(chalk_1.default.green(` ✓ Created GitHub Actions workflow: .github/workflows/ekkos-cortex-validation.yml`));
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log(chalk_1.default.gray(' This workflow will automatically run `ekkos docs validate` on pull requests'));
|
|
104
|
+
console.log(chalk_1.default.gray(' and pushes that modify any ekkOS_CONTEXT.md (Cortex) file in your repository.'));
|
|
105
|
+
console.log('');
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ekkOS Cortex — Living Docs Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates all ekkOS_CONTEXT.md files in the repository against 12 checks:
|
|
5
|
+
* 1. Frontmatter exists
|
|
6
|
+
* 2. Required fields present
|
|
7
|
+
* 3. Content hash matches body
|
|
8
|
+
* 4. AUTOGENERATED markers present
|
|
9
|
+
* 5. Confidence value valid
|
|
10
|
+
* 6. No secrets leaked
|
|
11
|
+
* 7. Token budget respected
|
|
12
|
+
* 8. Dream quarantine enforced
|
|
13
|
+
* 9. No invented file paths
|
|
14
|
+
* 10. Staleness check
|
|
15
|
+
* 11. Connected system_ids valid
|
|
16
|
+
* 12. No PII
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* npx tsx validate.ts # validate all
|
|
20
|
+
* npx tsx validate.ts --fix # auto-fix what's possible
|
|
21
|
+
* npx tsx validate.ts --system=apps-proxy # validate one system
|
|
22
|
+
*/
|
|
23
|
+
export declare function validateLivingDocsCommand(options: {
|
|
24
|
+
repoRoot: string;
|
|
25
|
+
fix: boolean;
|
|
26
|
+
systemFilter?: string;
|
|
27
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ekkOS Cortex — Living Docs Validator
|
|
4
|
+
*
|
|
5
|
+
* Validates all ekkOS_CONTEXT.md files in the repository against 12 checks:
|
|
6
|
+
* 1. Frontmatter exists
|
|
7
|
+
* 2. Required fields present
|
|
8
|
+
* 3. Content hash matches body
|
|
9
|
+
* 4. AUTOGENERATED markers present
|
|
10
|
+
* 5. Confidence value valid
|
|
11
|
+
* 6. No secrets leaked
|
|
12
|
+
* 7. Token budget respected
|
|
13
|
+
* 8. Dream quarantine enforced
|
|
14
|
+
* 9. No invented file paths
|
|
15
|
+
* 10. Staleness check
|
|
16
|
+
* 11. Connected system_ids valid
|
|
17
|
+
* 12. No PII
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* npx tsx validate.ts # validate all
|
|
21
|
+
* npx tsx validate.ts --fix # auto-fix what's possible
|
|
22
|
+
* npx tsx validate.ts --system=apps-proxy # validate one system
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.validateLivingDocsCommand = validateLivingDocsCommand;
|
|
26
|
+
const supabase_js_1 = require("@supabase/supabase-js");
|
|
27
|
+
const crypto_1 = require("crypto");
|
|
28
|
+
const promises_1 = require("fs/promises");
|
|
29
|
+
const fs_1 = require("fs");
|
|
30
|
+
const path_1 = require("path");
|
|
31
|
+
const child_process_1 = require("child_process");
|
|
32
|
+
// ── Constants ────────────────────────────────────────────────────────────
|
|
33
|
+
const REQUIRED_FIELDS = [
|
|
34
|
+
'system_id',
|
|
35
|
+
'domain',
|
|
36
|
+
'status',
|
|
37
|
+
'last_compiled_at',
|
|
38
|
+
'content_hash',
|
|
39
|
+
'confidence',
|
|
40
|
+
];
|
|
41
|
+
const VALID_CONFIDENCE = ['high', 'medium', 'low'];
|
|
42
|
+
const SECRET_PATTERNS = [
|
|
43
|
+
/sk-[a-zA-Z0-9]{20,}/, // OpenAI keys
|
|
44
|
+
/ghp_[a-zA-Z0-9]{36,}/, // GitHub PATs
|
|
45
|
+
/Bearer\s+[a-zA-Z0-9._\-]{20,}/, // Bearer tokens
|
|
46
|
+
/postgres:\/\/[^\s]+/, // Postgres connection strings
|
|
47
|
+
/mongodb(\+srv)?:\/\/[^\s]+/, // MongoDB connection strings
|
|
48
|
+
/AKIA[0-9A-Z]{16}/, // AWS access keys
|
|
49
|
+
/[a-zA-Z0-9\/+]{40}(?=\s|$)/, // AWS secret keys (40-char base64)
|
|
50
|
+
/eyJ[a-zA-Z0-9_-]{30,}\.[a-zA-Z0-9_-]{30,}/, // JWT tokens
|
|
51
|
+
/xoxb-[0-9]{10,}-[a-zA-Z0-9]{20,}/, // Slack bot tokens
|
|
52
|
+
/sk_live_[a-zA-Z0-9]{20,}/, // Stripe keys
|
|
53
|
+
/redis:\/\/[^\s]+/, // Redis connection strings
|
|
54
|
+
];
|
|
55
|
+
const PII_PATTERNS = [
|
|
56
|
+
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/, // Email addresses
|
|
57
|
+
/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, // IP addresses
|
|
58
|
+
/\/Users\/[a-zA-Z0-9._-]+\//, // macOS absolute user paths
|
|
59
|
+
/\/home\/[a-zA-Z0-9._-]+\//, // Linux absolute user paths
|
|
60
|
+
/C:\\Users\\[a-zA-Z0-9._-]+\\/, // Windows absolute user paths
|
|
61
|
+
];
|
|
62
|
+
const AUTOGENERATED_START = 'EKKOS_AUTOGENERATED_START';
|
|
63
|
+
const AUTOGENERATED_END = 'EKKOS_AUTOGENERATED_END';
|
|
64
|
+
const DREAM_QUARANTINE_START = 'EKKOS_DREAM_QUARANTINE_START';
|
|
65
|
+
const DREAM_QUARANTINE_END = 'EKKOS_DREAM_QUARANTINE_END';
|
|
66
|
+
const TOKEN_BUDGET_CHARS = 12000; // ~3000 tokens
|
|
67
|
+
const STALENESS_DAYS = 30;
|
|
68
|
+
// ── Frontmatter Parser ──────────────────────────────────────────────────
|
|
69
|
+
function parseFrontmatter(content) {
|
|
70
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
71
|
+
if (!match)
|
|
72
|
+
return { frontmatter: null, body: content };
|
|
73
|
+
const rawYaml = match[1];
|
|
74
|
+
const body = match[2];
|
|
75
|
+
// Simple YAML parser (key: value lines)
|
|
76
|
+
const frontmatter = {};
|
|
77
|
+
for (const line of rawYaml.split('\n')) {
|
|
78
|
+
const kv = line.match(/^([a-z_]+):\s*(.*)$/);
|
|
79
|
+
if (kv) {
|
|
80
|
+
const [, key, value] = kv;
|
|
81
|
+
// Handle arrays like source_event_ids: []
|
|
82
|
+
if (value === '[]') {
|
|
83
|
+
frontmatter[key] = [];
|
|
84
|
+
}
|
|
85
|
+
else if (value.startsWith('[')) {
|
|
86
|
+
try {
|
|
87
|
+
frontmatter[key] = JSON.parse(value);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
frontmatter[key] = value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
frontmatter[key] = value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { frontmatter, body };
|
|
99
|
+
}
|
|
100
|
+
// ── Body Extraction ─────────────────────────────────────────────────────
|
|
101
|
+
function extractAutogeneratedBody(content) {
|
|
102
|
+
const startPattern = new RegExp(`<!--\\s*${AUTOGENERATED_START}\\s+[a-f0-9]+\\s*-->`);
|
|
103
|
+
const endPattern = new RegExp(`<!--\\s*${AUTOGENERATED_END}\\s*-->`);
|
|
104
|
+
const startMatch = content.match(startPattern);
|
|
105
|
+
const endMatch = content.match(endPattern);
|
|
106
|
+
if (!startMatch || !endMatch)
|
|
107
|
+
return null;
|
|
108
|
+
const startIdx = content.indexOf(startMatch[0]) + startMatch[0].length;
|
|
109
|
+
const endIdx = content.indexOf(endMatch[0]);
|
|
110
|
+
if (endIdx <= startIdx)
|
|
111
|
+
return null;
|
|
112
|
+
return content.substring(startIdx, endIdx);
|
|
113
|
+
}
|
|
114
|
+
// ── Hash Computation ────────────────────────────────────────────────────
|
|
115
|
+
function computeHash(body) {
|
|
116
|
+
return (0, crypto_1.createHash)('sha256').update(body.trim()).digest('hex');
|
|
117
|
+
}
|
|
118
|
+
// ── File Discovery ──────────────────────────────────────────────────────
|
|
119
|
+
function findContextFiles(repoRoot, systemFilter) {
|
|
120
|
+
try {
|
|
121
|
+
const cmd = `find "${repoRoot}" -name "ekkOS_CONTEXT.md" -not -path "*/node_modules/*" -not -path "*/.next/*" -not -path "*/dist/*" -not -path "*/out/*" -not -path "*/.turbo/*" -not -path "*/.cache/*"`;
|
|
122
|
+
const output = (0, child_process_1.execSync)(cmd, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
123
|
+
let files = output.trim().split('\n').filter(Boolean);
|
|
124
|
+
if (systemFilter) {
|
|
125
|
+
// Filter to files whose frontmatter system_id matches
|
|
126
|
+
files = files.filter(f => {
|
|
127
|
+
try {
|
|
128
|
+
const content = (0, fs_1.readFileSync)(f, 'utf-8');
|
|
129
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
130
|
+
return frontmatter?.system_id === systemFilter;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return files;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ── Individual Checks ───────────────────────────────────────────────────
|
|
144
|
+
function checkFrontmatterExists(file, content) {
|
|
145
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
146
|
+
return {
|
|
147
|
+
check: 'frontmatter-exists',
|
|
148
|
+
file,
|
|
149
|
+
passed: frontmatter !== null,
|
|
150
|
+
reason: frontmatter === null ? 'No YAML frontmatter found between --- markers' : undefined,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function checkRequiredFields(file, content) {
|
|
154
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
155
|
+
if (!frontmatter) {
|
|
156
|
+
return { check: 'required-fields', file, passed: false, reason: 'No frontmatter to check' };
|
|
157
|
+
}
|
|
158
|
+
const missing = REQUIRED_FIELDS.filter(f => frontmatter[f] === undefined || frontmatter[f] === '');
|
|
159
|
+
return {
|
|
160
|
+
check: 'required-fields',
|
|
161
|
+
file,
|
|
162
|
+
passed: missing.length === 0,
|
|
163
|
+
reason: missing.length > 0 ? `Missing fields: ${missing.join(', ')}` : undefined,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function checkContentHash(file, content, fix) {
|
|
167
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
168
|
+
if (!frontmatter?.content_hash) {
|
|
169
|
+
return { check: 'content-hash', file, passed: false, reason: 'No content_hash in frontmatter' };
|
|
170
|
+
}
|
|
171
|
+
const body = extractAutogeneratedBody(content);
|
|
172
|
+
if (body === null) {
|
|
173
|
+
return { check: 'content-hash', file, passed: false, reason: 'Cannot extract autogenerated body' };
|
|
174
|
+
}
|
|
175
|
+
const computed = computeHash(body);
|
|
176
|
+
const matches = computed === frontmatter.content_hash;
|
|
177
|
+
if (!matches && fix) {
|
|
178
|
+
// Auto-fix: replace the content_hash in frontmatter and in the START marker
|
|
179
|
+
const fixed = content
|
|
180
|
+
.replace(`content_hash: ${frontmatter.content_hash}`, `content_hash: ${computed}`)
|
|
181
|
+
.replace(new RegExp(`${AUTOGENERATED_START}\\s+${frontmatter.content_hash}`), `${AUTOGENERATED_START} ${computed}`);
|
|
182
|
+
(0, fs_1.writeFileSync)(file, fixed, 'utf-8');
|
|
183
|
+
return { check: 'content-hash', file, passed: true, fixApplied: true };
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
check: 'content-hash',
|
|
187
|
+
file,
|
|
188
|
+
passed: matches,
|
|
189
|
+
reason: !matches ? `Hash mismatch: expected ${frontmatter.content_hash}, computed ${computed}` : undefined,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function checkAutogeneratedMarkers(file, content) {
|
|
193
|
+
const hasStart = content.includes(AUTOGENERATED_START);
|
|
194
|
+
const hasEnd = content.includes(AUTOGENERATED_END);
|
|
195
|
+
const both = hasStart && hasEnd;
|
|
196
|
+
let reason;
|
|
197
|
+
if (!hasStart && !hasEnd)
|
|
198
|
+
reason = 'Both START and END markers missing';
|
|
199
|
+
else if (!hasStart)
|
|
200
|
+
reason = 'START marker missing';
|
|
201
|
+
else if (!hasEnd)
|
|
202
|
+
reason = 'END marker missing';
|
|
203
|
+
return { check: 'autogenerated-markers', file, passed: both, reason };
|
|
204
|
+
}
|
|
205
|
+
function checkConfidenceValid(file, content) {
|
|
206
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
207
|
+
if (!frontmatter?.confidence) {
|
|
208
|
+
return { check: 'confidence-valid', file, passed: false, reason: 'No confidence field' };
|
|
209
|
+
}
|
|
210
|
+
const valid = VALID_CONFIDENCE.includes(frontmatter.confidence);
|
|
211
|
+
return {
|
|
212
|
+
check: 'confidence-valid',
|
|
213
|
+
file,
|
|
214
|
+
passed: valid,
|
|
215
|
+
reason: !valid ? `Invalid confidence "${frontmatter.confidence}"; must be one of: ${VALID_CONFIDENCE.join(', ')}` : undefined,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function checkNoSecrets(file, content) {
|
|
219
|
+
const body = extractAutogeneratedBody(content) || content;
|
|
220
|
+
const found = [];
|
|
221
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
222
|
+
const match = body.match(pattern);
|
|
223
|
+
if (match) {
|
|
224
|
+
// Redact the match for the error message
|
|
225
|
+
const redacted = match[0].substring(0, 8) + '...' + match[0].substring(match[0].length - 4);
|
|
226
|
+
found.push(`${pattern.source.substring(0, 20)}... → "${redacted}"`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
check: 'no-secrets',
|
|
231
|
+
file,
|
|
232
|
+
passed: found.length === 0,
|
|
233
|
+
reason: found.length > 0 ? `Potential secrets found: ${found.join('; ')}` : undefined,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function checkTokenBudget(file, content) {
|
|
237
|
+
const body = extractAutogeneratedBody(content);
|
|
238
|
+
if (body === null) {
|
|
239
|
+
return { check: 'token-budget', file, passed: true }; // Can't check without markers
|
|
240
|
+
}
|
|
241
|
+
const charCount = body.trim().length;
|
|
242
|
+
const passed = charCount <= TOKEN_BUDGET_CHARS;
|
|
243
|
+
return {
|
|
244
|
+
check: 'token-budget',
|
|
245
|
+
file,
|
|
246
|
+
passed,
|
|
247
|
+
reason: !passed ? `Body is ${charCount} chars (~${Math.round(charCount / 4)} tokens), exceeds budget of ${TOKEN_BUDGET_CHARS} chars (~2000 tokens)` : undefined,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function checkDreamQuarantine(file, content) {
|
|
251
|
+
const body = extractAutogeneratedBody(content) || content;
|
|
252
|
+
// Check if Dream Forge section exists
|
|
253
|
+
const hasDreamSection = body.includes('## Dream Forge Hypotheses');
|
|
254
|
+
if (!hasDreamSection) {
|
|
255
|
+
return { check: 'dream-quarantine', file, passed: true }; // No dreams, no problem
|
|
256
|
+
}
|
|
257
|
+
// Check quarantine markers
|
|
258
|
+
const hasStart = body.includes(DREAM_QUARANTINE_START);
|
|
259
|
+
const hasEnd = body.includes(DREAM_QUARANTINE_END);
|
|
260
|
+
if (!hasStart || !hasEnd) {
|
|
261
|
+
return {
|
|
262
|
+
check: 'dream-quarantine',
|
|
263
|
+
file,
|
|
264
|
+
passed: false,
|
|
265
|
+
reason: `Dream Forge section exists but missing quarantine markers (${DREAM_QUARANTINE_START}/${DREAM_QUARANTINE_END})`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// Check that dream-related keywords don't appear outside the quarantine zone
|
|
269
|
+
const quarantineStartIdx = body.indexOf(DREAM_QUARANTINE_START);
|
|
270
|
+
const quarantineEndIdx = body.indexOf(DREAM_QUARANTINE_END);
|
|
271
|
+
const beforeQuarantine = body.substring(0, quarantineStartIdx);
|
|
272
|
+
// Look for speculative language that should only be in Dream section
|
|
273
|
+
const speculativePatterns = [
|
|
274
|
+
/\bovernight consolidation\b/i,
|
|
275
|
+
/\bnot yet validated\b/i,
|
|
276
|
+
/\bhypothes[ie]s?\b/i,
|
|
277
|
+
];
|
|
278
|
+
const leaks = [];
|
|
279
|
+
for (const pattern of speculativePatterns) {
|
|
280
|
+
if (pattern.test(beforeQuarantine)) {
|
|
281
|
+
leaks.push(pattern.source);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
check: 'dream-quarantine',
|
|
286
|
+
file,
|
|
287
|
+
passed: leaks.length === 0,
|
|
288
|
+
reason: leaks.length > 0 ? `Speculative content found outside Dream quarantine zone: ${leaks.join(', ')}` : undefined,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function checkNoInventedPaths(file, content, repoRoot) {
|
|
292
|
+
const body = extractAutogeneratedBody(content) || content;
|
|
293
|
+
// Extract the system directory from the file path
|
|
294
|
+
const systemDir = (0, path_1.dirname)(file);
|
|
295
|
+
const relSystemDir = (0, path_1.relative)(repoRoot, systemDir);
|
|
296
|
+
// Find "Key Files" section and extract file references
|
|
297
|
+
const keyFilesMatch = body.match(/## Key Files\n([\s\S]*?)(?=\n## |\n<!-- |$)/);
|
|
298
|
+
if (!keyFilesMatch) {
|
|
299
|
+
return { check: 'no-invented-paths', file, passed: true }; // No Key Files section
|
|
300
|
+
}
|
|
301
|
+
const keyFilesSection = keyFilesMatch[1];
|
|
302
|
+
// Match backtick-wrapped file paths or numbered list items with file paths
|
|
303
|
+
const pathPattern = /`([a-zA-Z0-9._\-/]+\.[a-zA-Z0-9]+)`/g;
|
|
304
|
+
const invented = [];
|
|
305
|
+
let pathMatch;
|
|
306
|
+
while ((pathMatch = pathPattern.exec(keyFilesSection)) !== null) {
|
|
307
|
+
const refPath = pathMatch[1];
|
|
308
|
+
// Check relative to the system directory
|
|
309
|
+
const absPath = (0, path_1.join)(systemDir, refPath);
|
|
310
|
+
// Also check relative to repo root
|
|
311
|
+
const absPathFromRoot = (0, path_1.join)(repoRoot, relSystemDir, refPath);
|
|
312
|
+
if (!(0, fs_1.existsSync)(absPath) && !(0, fs_1.existsSync)(absPathFromRoot)) {
|
|
313
|
+
// Also try just the filename in the system dir
|
|
314
|
+
const justFilename = (0, path_1.join)(systemDir, refPath.split('/').pop() || '');
|
|
315
|
+
if (!(0, fs_1.existsSync)(justFilename)) {
|
|
316
|
+
invented.push(refPath);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
check: 'no-invented-paths',
|
|
322
|
+
file,
|
|
323
|
+
passed: invented.length === 0,
|
|
324
|
+
reason: invented.length > 0 ? `File paths not found: ${invented.join(', ')}` : undefined,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function checkStaleness(file, content) {
|
|
328
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
329
|
+
if (!frontmatter?.last_compiled_at) {
|
|
330
|
+
return { check: 'staleness', file, passed: false, reason: 'No last_compiled_at field' };
|
|
331
|
+
}
|
|
332
|
+
const compiledAt = new Date(frontmatter.last_compiled_at);
|
|
333
|
+
const now = new Date();
|
|
334
|
+
const daysDiff = (now.getTime() - compiledAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
335
|
+
return {
|
|
336
|
+
check: 'staleness',
|
|
337
|
+
file,
|
|
338
|
+
passed: daysDiff <= STALENESS_DAYS,
|
|
339
|
+
reason: daysDiff > STALENESS_DAYS
|
|
340
|
+
? `Last compiled ${Math.round(daysDiff)} days ago (threshold: ${STALENESS_DAYS} days)`
|
|
341
|
+
: undefined,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
async function checkConnectedSystems(file, content, knownSystemIds) {
|
|
345
|
+
const body = extractAutogeneratedBody(content) || content;
|
|
346
|
+
// Find Connected Systems section
|
|
347
|
+
const connectedMatch = body.match(/## Connected Systems\n([\s\S]*?)(?=\n## |\n<!-- |$)/);
|
|
348
|
+
if (!connectedMatch) {
|
|
349
|
+
return { check: 'connected-systems', file, passed: true }; // No section
|
|
350
|
+
}
|
|
351
|
+
if (!knownSystemIds) {
|
|
352
|
+
return { check: 'connected-systems', file, passed: true, reason: 'Skipped — no DB connection' };
|
|
353
|
+
}
|
|
354
|
+
// Extract system_id references (backtick-wrapped kebab-case ids)
|
|
355
|
+
const idPattern = /`([a-z0-9][a-z0-9-]{0,62}[a-z0-9])`/g;
|
|
356
|
+
const invalid = [];
|
|
357
|
+
let idMatch;
|
|
358
|
+
while ((idMatch = idPattern.exec(connectedMatch[1])) !== null) {
|
|
359
|
+
const refId = idMatch[1];
|
|
360
|
+
// Skip common non-system-id strings
|
|
361
|
+
if (refId.includes('.') || refId.length < 3)
|
|
362
|
+
continue;
|
|
363
|
+
if (!knownSystemIds.has(refId)) {
|
|
364
|
+
invalid.push(refId);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
check: 'connected-systems',
|
|
369
|
+
file,
|
|
370
|
+
passed: invalid.length === 0,
|
|
371
|
+
reason: invalid.length > 0 ? `Unknown system_ids: ${invalid.join(', ')}` : undefined,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function checkNoPII(file, content) {
|
|
375
|
+
const body = extractAutogeneratedBody(content) || content;
|
|
376
|
+
const found = [];
|
|
377
|
+
for (const pattern of PII_PATTERNS) {
|
|
378
|
+
const match = body.match(pattern);
|
|
379
|
+
if (match) {
|
|
380
|
+
// Allow common false positives
|
|
381
|
+
const val = match[0];
|
|
382
|
+
// Skip email-like patterns that are clearly example/placeholder
|
|
383
|
+
if (val.includes('@example.') || val.includes('@ci.') || val.includes('@test.'))
|
|
384
|
+
continue;
|
|
385
|
+
// Skip IPs that are localhost or common internal
|
|
386
|
+
if (val === '127.0.0.1' || val === '0.0.0.0' || val.startsWith('192.168.') || val.startsWith('10.'))
|
|
387
|
+
continue;
|
|
388
|
+
found.push(`${pattern.source.substring(0, 30)}... → "${val}"`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
check: 'no-pii',
|
|
393
|
+
file,
|
|
394
|
+
passed: found.length === 0,
|
|
395
|
+
reason: found.length > 0 ? `Potential PII found: ${found.join('; ')}` : undefined,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
// ── Supabase Setup (Optional) ───────────────────────────────────────────
|
|
399
|
+
async function loadKnownSystemIds() {
|
|
400
|
+
const supabaseUrl = process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
401
|
+
const supabaseKey = process.env.SUPABASE_SECRET_KEY;
|
|
402
|
+
const userId = process.env.EKKOS_ADMIN_USER_ID;
|
|
403
|
+
if (!supabaseUrl || !supabaseKey || !userId) {
|
|
404
|
+
console.log('[Validator] No Supabase credentials — skipping DB-dependent checks');
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
const supabase = (0, supabase_js_1.createClient)(supabaseUrl, supabaseKey);
|
|
409
|
+
const { data } = await supabase
|
|
410
|
+
.from('system_registry')
|
|
411
|
+
.select('system_id')
|
|
412
|
+
.eq('user_id', userId);
|
|
413
|
+
if (!data)
|
|
414
|
+
return null;
|
|
415
|
+
return new Set(data.map((r) => r.system_id));
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
console.log('[Validator] Supabase query failed — skipping DB-dependent checks');
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// ── Main Validator ──────────────────────────────────────────────────────
|
|
423
|
+
async function validateLivingDocsCommand(options) {
|
|
424
|
+
const { repoRoot, fix, systemFilter } = options;
|
|
425
|
+
console.log(`[Cortex] Scanning ${repoRoot} for ekkOS_CONTEXT.md files...`);
|
|
426
|
+
const files = findContextFiles(repoRoot, systemFilter);
|
|
427
|
+
if (files.length === 0) {
|
|
428
|
+
console.log('[Cortex] No ekkOS_CONTEXT.md files found');
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
console.log(`[Cortex] Found ${files.length} files to validate\n`);
|
|
432
|
+
// Load known system_ids from Supabase (optional)
|
|
433
|
+
const knownSystemIds = await loadKnownSystemIds();
|
|
434
|
+
const results = [];
|
|
435
|
+
let fixCount = 0;
|
|
436
|
+
for (const file of files) {
|
|
437
|
+
const relPath = (0, path_1.relative)(repoRoot, file);
|
|
438
|
+
let content;
|
|
439
|
+
try {
|
|
440
|
+
content = await (0, promises_1.readFile)(file, 'utf-8');
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
results.push({ check: 'file-readable', file: relPath, passed: false, reason: `Cannot read file: ${err}` });
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
// Run all 12 checks
|
|
447
|
+
results.push(checkFrontmatterExists(relPath, content));
|
|
448
|
+
results.push(checkRequiredFields(relPath, content));
|
|
449
|
+
const hashResult = checkContentHash(file, content, fix);
|
|
450
|
+
if (hashResult.fixApplied)
|
|
451
|
+
fixCount++;
|
|
452
|
+
results.push({ check: hashResult.check, file: relPath, passed: hashResult.passed, reason: hashResult.reason });
|
|
453
|
+
results.push(checkAutogeneratedMarkers(relPath, content));
|
|
454
|
+
results.push(checkConfidenceValid(relPath, content));
|
|
455
|
+
results.push(checkNoSecrets(relPath, content));
|
|
456
|
+
results.push(checkTokenBudget(relPath, content));
|
|
457
|
+
results.push(checkDreamQuarantine(relPath, content));
|
|
458
|
+
const inventedResult = checkNoInventedPaths(repoRoot, file, content);
|
|
459
|
+
results.push({ ...inventedResult, file: relPath });
|
|
460
|
+
results.push(checkStaleness(relPath, content));
|
|
461
|
+
results.push(await checkConnectedSystems(relPath, content, knownSystemIds));
|
|
462
|
+
results.push(checkNoPII(relPath, content));
|
|
463
|
+
}
|
|
464
|
+
// Output results
|
|
465
|
+
const passed = results.filter(r => r.passed);
|
|
466
|
+
const failed = results.filter(r => !r.passed);
|
|
467
|
+
console.log('─'.repeat(72));
|
|
468
|
+
console.log('ekkOS CORTEX VALIDATION RESULTS');
|
|
469
|
+
console.log('─'.repeat(72));
|
|
470
|
+
// Print passed checks (grouped by file)
|
|
471
|
+
for (const r of passed) {
|
|
472
|
+
console.log(` \u2713 ${r.check}: ${r.file}`);
|
|
473
|
+
}
|
|
474
|
+
// Print failed checks
|
|
475
|
+
if (failed.length > 0) {
|
|
476
|
+
console.log('');
|
|
477
|
+
for (const r of failed) {
|
|
478
|
+
console.log(` \u2717 ${r.check}: ${r.file} \u2014 ${r.reason}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
console.log('');
|
|
482
|
+
console.log('─'.repeat(72));
|
|
483
|
+
console.log(`SUMMARY: ${passed.length} passed, ${failed.length} failed across ${files.length} files`);
|
|
484
|
+
if (fixCount > 0) {
|
|
485
|
+
console.log(`AUTO-FIXED: ${fixCount} content hashes regenerated`);
|
|
486
|
+
}
|
|
487
|
+
console.log('─'.repeat(72));
|
|
488
|
+
return failed.length > 0 ? 1 : 0;
|
|
489
|
+
}
|