kryptheon 0.1.19 → 0.1.21
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/README.md +48 -10
- package/bin/kryptheon.js +466 -6
- package/kryptheon-changes.js +222 -0
- package/kryptheon-code.js +564 -0
- package/kryptheon-dashboard-page.js +1003 -0
- package/kryptheon-dashboard.html +446 -0
- package/kryptheon-dashboard.js +532 -0
- package/kryptheon-fixture.js +7 -3
- package/kryptheon-logo.png +0 -0
- package/kryptheon-look.js +259 -0
- package/kryptheon-reporter.js +18 -6
- package/kryptheon-secrets.js +5 -3
- package/kryptheon-store.js +332 -0
- package/package.json +93 -78
- package/playwright.config.js +5 -2
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// What changed in a project since Kryptheon last looked, and what that means
|
|
2
|
+
// for which checks have to run.
|
|
3
|
+
//
|
|
4
|
+
// A snapshot is a content hash of every file that is the project's own - its
|
|
5
|
+
// source, its database migrations, its dependency list - kept in the store,
|
|
6
|
+
// never in the project. Comparing two snapshots answers the first question a
|
|
7
|
+
// person has after an AI tool says "done": what did it actually touch?
|
|
8
|
+
//
|
|
9
|
+
// Content, not timestamps and not git: an editor that saves a file unchanged
|
|
10
|
+
// is not a change, and a landing page with no git repository still has one.
|
|
11
|
+
//
|
|
12
|
+
// Each changed file is put in a part of the app (frontend, backend, database,
|
|
13
|
+
// tests, dependencies, config, docs), and each part names the checks that
|
|
14
|
+
// can speak to it. The mapping is written here, once, and deliberately
|
|
15
|
+
// plain: a path-based guess that is right for how most projects are laid
|
|
16
|
+
// out, and said as a guess ("likely affected") rather than a proof.
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
|
|
22
|
+
const SKIP_DIRS = new Set([
|
|
23
|
+
'node_modules', '.git', 'dist', 'build', 'out', '.next', '.nuxt', '.svelte-kit', 'coverage',
|
|
24
|
+
'.vercel', '.netlify', '.turbo', '.cache', 'test-results', 'playwright-report', '.kryptheon',
|
|
25
|
+
]);
|
|
26
|
+
const MAX_BYTES = 5 * 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
// Where a file sits in the app. The first rule that matches wins, so the
|
|
29
|
+
// specific ones come first: a .sql file inside backend/ is a database change.
|
|
30
|
+
const PARTS = [
|
|
31
|
+
{ part: 'dependencies', test: (p) => /(^|\/)(package\.json|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|requirements\.txt|poetry\.lock|Gemfile\.lock|go\.sum)$/i.test(p) },
|
|
32
|
+
{ part: 'database', test: (p) => /\.sql$/i.test(p) || /(^|\/)(migrations|supabase|prisma|drizzle|db\/schema)(\/|$)/i.test(p) || /(^|\/)schema\.prisma$/i.test(p) },
|
|
33
|
+
{ part: 'tests', test: (p) => /(^|\/)(tests?|__tests__|e2e|cypress)\//i.test(p) || /\.(spec|test)\.[cm]?[jt]sx?$/i.test(p) },
|
|
34
|
+
{ part: 'docs', test: (p) => /\.(md|mdx|txt|rst)$/i.test(p) || /(^|\/)docs\//i.test(p) },
|
|
35
|
+
{ part: 'config', test: (p) => /(^|\/)(\.env\.example|\.env\.sample|tsconfig[^/]*\.json|jsconfig\.json|vercel\.json|netlify\.toml|dockerfile|docker-compose[^/]*\.ya?ml)$/i.test(p) || /\.config\.[cm]?[jt]s$/i.test(p) || /(^|\/)\.github\//i.test(p) },
|
|
36
|
+
{ part: 'backend', test: (p) => /(^|\/)(server|backend|api|routes|controllers|services|middleware|functions|workers|jobs|lib\/server)(\/|\.[cm]?[jt]s$)/i.test(p) || /(^|\/)(server|app|index)\.[cm]?[jt]s$/i.test(p) && !/(^|\/)(src|public|frontend|client|pages|components)\//i.test(p) },
|
|
37
|
+
{ part: 'frontend', test: (p) => /\.(html?|css|scss|sass|less|jsx|tsx|vue|svelte|astro)$/i.test(p) || /(^|\/)(frontend|client|public|src|pages|components|app|views|static)\//i.test(p) && /\.[cm]?[jt]s$/i.test(p) },
|
|
38
|
+
{ part: 'assets', test: (p) => /\.(png|jpe?g|gif|svg|webp|ico|woff2?|ttf|otf|mp4|webm|mp3|pdf)$/i.test(p) },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Which checks can speak to a change in each part. Ids are the dashboard's.
|
|
42
|
+
const CHECKS_FOR = {
|
|
43
|
+
frontend: ['frontend', 'regression'],
|
|
44
|
+
backend: ['backend', 'api', 'regression'],
|
|
45
|
+
database: ['data', 'regression'],
|
|
46
|
+
dependencies: ['dependency', 'build', 'regression'],
|
|
47
|
+
tests: ['regression'],
|
|
48
|
+
config: ['build', 'regression'],
|
|
49
|
+
code: ['regression'],
|
|
50
|
+
docs: [],
|
|
51
|
+
assets: [],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function partOf(rel) {
|
|
55
|
+
const p = rel.split(path.sep).join('/');
|
|
56
|
+
const hit = PARTS.find((r) => r.test(p));
|
|
57
|
+
if (hit) return hit.part;
|
|
58
|
+
return /\.[cm]?[jt]sx?$/i.test(p) ? 'code' : 'other';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Every file that is the project's own, as relative paths with forward slashes. */
|
|
62
|
+
function listFiles(root) {
|
|
63
|
+
const out = [];
|
|
64
|
+
(function walk(dir) {
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const full = path.join(dir, entry.name);
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
if (!SKIP_DIRS.has(entry.name)) walk(full);
|
|
75
|
+
} else if (entry.isFile()) {
|
|
76
|
+
// A real .env holds secrets; its content is never hashed or kept.
|
|
77
|
+
if (/^\.env(\..+)?$/i.test(entry.name) && !/\.(example|sample)$/i.test(entry.name)) continue;
|
|
78
|
+
out.push(path.relative(root, full).split(path.sep).join('/'));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
})(root);
|
|
82
|
+
return out.sort();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The dependency lists of package.json, kept so a change can say which package moved. */
|
|
86
|
+
function dependenciesOf(root) {
|
|
87
|
+
try {
|
|
88
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
89
|
+
return Object.assign({}, pkg.dependencies || {}, pkg.devDependencies || {});
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The project as it is now. Files whose size and modified time match the
|
|
97
|
+
* previous snapshot keep its hash without being read again - on a large
|
|
98
|
+
* project that is the difference between milliseconds and seconds, and an
|
|
99
|
+
* unchanged size and time is the same bytes for every editor that exists.
|
|
100
|
+
*/
|
|
101
|
+
function takeSnapshot(root, previous) {
|
|
102
|
+
const before = (previous && previous.files) || {};
|
|
103
|
+
const files = {};
|
|
104
|
+
for (const rel of listFiles(root)) {
|
|
105
|
+
let stat;
|
|
106
|
+
try {
|
|
107
|
+
stat = fs.statSync(path.join(root, rel));
|
|
108
|
+
} catch (err) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const old = before[rel];
|
|
112
|
+
if (old && old.size === stat.size && old.mtimeMs === stat.mtimeMs) {
|
|
113
|
+
files[rel] = old;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
let hash;
|
|
117
|
+
if (stat.size > MAX_BYTES) {
|
|
118
|
+
hash = 'size:' + stat.size + ':' + stat.mtimeMs;
|
|
119
|
+
} else {
|
|
120
|
+
try {
|
|
121
|
+
hash = crypto.createHash('sha1').update(fs.readFileSync(path.join(root, rel))).digest('hex');
|
|
122
|
+
} catch (err) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
files[rel] = { hash: hash, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
127
|
+
}
|
|
128
|
+
return { takenAt: new Date().toISOString(), files: files, dependencies: dependenciesOf(root) };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Packages added, removed or moved to another version between two snapshots. */
|
|
132
|
+
function dependencyChanges(before, after) {
|
|
133
|
+
const a = (before && before.dependencies) || {};
|
|
134
|
+
const b = (after && after.dependencies) || {};
|
|
135
|
+
const out = [];
|
|
136
|
+
for (const name of new Set(Object.keys(a).concat(Object.keys(b)))) {
|
|
137
|
+
if (!(name in a)) out.push({ name: name, change: 'added', to: b[name] });
|
|
138
|
+
else if (!(name in b)) out.push({ name: name, change: 'removed', from: a[name] });
|
|
139
|
+
else if (a[name] !== b[name]) out.push({ name: name, change: 'changed', from: a[name], to: b[name] });
|
|
140
|
+
}
|
|
141
|
+
return out.sort((x, y) => x.name.localeCompare(y.name));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* What changed between two snapshots, by part of the app, and which checks
|
|
146
|
+
* that calls for. With no earlier snapshot there is nothing to compare, and
|
|
147
|
+
* that is said - it is not reported as "everything changed".
|
|
148
|
+
*/
|
|
149
|
+
function compare(before, after) {
|
|
150
|
+
if (!before) return { first: true, files: [], parts: [], checks: [], dependencies: [] };
|
|
151
|
+
const files = [];
|
|
152
|
+
const a = before.files || {};
|
|
153
|
+
const b = after.files || {};
|
|
154
|
+
for (const rel of Object.keys(b)) {
|
|
155
|
+
if (!a[rel]) files.push({ path: rel, state: 'added' });
|
|
156
|
+
else if (a[rel].hash !== b[rel].hash) files.push({ path: rel, state: 'modified' });
|
|
157
|
+
}
|
|
158
|
+
for (const rel of Object.keys(a)) if (!b[rel]) files.push({ path: rel, state: 'deleted' });
|
|
159
|
+
files.sort((x, y) => x.path.localeCompare(y.path));
|
|
160
|
+
for (const f of files) f.part = partOf(f.path);
|
|
161
|
+
|
|
162
|
+
const parts = [];
|
|
163
|
+
for (const f of files) if (!parts.includes(f.part)) parts.push(f.part);
|
|
164
|
+
const checks = [];
|
|
165
|
+
for (const part of parts) for (const id of CHECKS_FOR[part] || []) if (!checks.includes(id)) checks.push(id);
|
|
166
|
+
return {
|
|
167
|
+
first: false,
|
|
168
|
+
files: files,
|
|
169
|
+
parts: parts,
|
|
170
|
+
checks: checks,
|
|
171
|
+
dependencies: dependencyChanges(before, after),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Findings that were not there last time. Keyed by what they are about rather
|
|
177
|
+
* than by line number: a line added above a known finding moves it, and a
|
|
178
|
+
* moved finding reported as new would teach a person to ignore this list.
|
|
179
|
+
*/
|
|
180
|
+
//
|
|
181
|
+
// The detail is part of what a finding is only for the code read, where it is
|
|
182
|
+
// the expression itself. For a recorded flow or a database finding it is the
|
|
183
|
+
// account of how it broke this time - "ended up somewhere different", "could
|
|
184
|
+
// not be opened" - and a flow that is still broken in a new way is the same
|
|
185
|
+
// finding, not a fixed one and a new one.
|
|
186
|
+
function findingKey(f) {
|
|
187
|
+
return [
|
|
188
|
+
f.check || '',
|
|
189
|
+
f.where ? String(f.where).replace(/:\d+$/, '') : '',
|
|
190
|
+
f.headline ? String(f.headline).replace(/:\d+\b/g, '') : '',
|
|
191
|
+
f.check === 'frontend' ? f.detail || '' : '',
|
|
192
|
+
].join('|');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** A short, stable name for a finding: what a person types to re-check it. */
|
|
196
|
+
function findingId(f) {
|
|
197
|
+
return crypto.createHash('sha1').update(findingKey(f)).digest('hex').slice(0, 8);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function newFindings(before, after) {
|
|
201
|
+
const seen = new Set((before || []).map(findingKey));
|
|
202
|
+
return (after || []).filter((f) => !seen.has(findingKey(f)));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function goneFindings(before, after) {
|
|
206
|
+
const now = new Set((after || []).map(findingKey));
|
|
207
|
+
return (before || []).filter((f) => !now.has(findingKey(f)));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
PARTS: PARTS,
|
|
212
|
+
CHECKS_FOR: CHECKS_FOR,
|
|
213
|
+
partOf: partOf,
|
|
214
|
+
listFiles: listFiles,
|
|
215
|
+
takeSnapshot: takeSnapshot,
|
|
216
|
+
compare: compare,
|
|
217
|
+
dependencyChanges: dependencyChanges,
|
|
218
|
+
findingKey: findingKey,
|
|
219
|
+
findingId: findingId,
|
|
220
|
+
newFindings: newFindings,
|
|
221
|
+
goneFindings: goneFindings,
|
|
222
|
+
};
|