tewip-ai 0.47.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/cli/help.js +5 -0
- package/dist/cli/tewip.js +9 -0
- package/dist/engine/start/start-command.js +216 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,36 @@ never goes to the model again.
|
|
|
38
38
|
Tewip prints what every command costs, caps its own spend, and works with no key at all - the rules
|
|
39
39
|
run on their own and everything they cannot settle is left as UNKNOWN rather than guessed.
|
|
40
40
|
|
|
41
|
+
## Start here
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm i -g tewip-ai
|
|
45
|
+
cd your-project
|
|
46
|
+
tewip
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`tewip` on its own looks at **your** project and names the two or three commands that will tell you
|
|
50
|
+
something, with your own paths in them — and says plainly what it cannot answer here. It reads the
|
|
51
|
+
directory and stops: nothing is run, nothing is written, about a second.
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Here is what Tewip can tell you about **this** project, in the order worth doing.
|
|
55
|
+
|
|
56
|
+
**1. `tewip vouch`**
|
|
57
|
+
282 test files here (282 JavaScript or TypeScript). This reads them and names the ones
|
|
58
|
+
that cannot fail - no model, a second or two.
|
|
59
|
+
|
|
60
|
+
**2. `tewip schema --diff main...HEAD`**
|
|
61
|
+
165 migrations here (165 SQL). This says what a change would do to a live database
|
|
62
|
+
before it runs.
|
|
63
|
+
|
|
64
|
+
## What it will not do here
|
|
65
|
+
- No k6, JMeter or Lighthouse report was found. Tewip judges one you produce; it does
|
|
66
|
+
not run load tests itself.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Everything below is the full list, for when you want it.
|
|
70
|
+
|
|
41
71
|
## Use it in your project
|
|
42
72
|
|
|
43
73
|
**Giving it to your team (or another company): [TEAM-GUIDE.md](TEAM-GUIDE.md)** — what to install, what each role uses it for, and what it never does.
|
package/dist/cli/help.js
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
export const HELP = `Tewip - finds why each test failed, for any stack.
|
|
5
5
|
|
|
6
6
|
Usage:
|
|
7
|
+
tewip Look at this project and name the two or three commands that
|
|
8
|
+
tewip start will tell you something about it, with your own paths in them,
|
|
9
|
+
and say plainly what Tewip cannot answer here. Reads the
|
|
10
|
+
directory and stops: nothing is run and nothing is written.
|
|
11
|
+
Start here. Everything below is the full list.
|
|
7
12
|
tewip explain <report> Explain a test report: a JUnit XML or .trx file, a folder of
|
|
8
13
|
them, or a Playwright blob-report folder. Keeps history per
|
|
9
14
|
project, so later runs are judged against earlier ones.
|
package/dist/cli/tewip.js
CHANGED
|
@@ -237,7 +237,16 @@ async function main() {
|
|
|
237
237
|
case '-v':
|
|
238
238
|
console.log(version());
|
|
239
239
|
return;
|
|
240
|
+
// `tewip` on its own used to print 233 lines of help, which is a reference for somebody who
|
|
241
|
+
// already knows what they want and a wall for everybody else. It now looks at the project and
|
|
242
|
+
// names the two or three commands that will tell *this* team something. --help is unchanged
|
|
243
|
+
// and still has all of it.
|
|
240
244
|
case undefined:
|
|
245
|
+
case 'start': {
|
|
246
|
+
const { runStart } = await import('../engine/start/start-command.js');
|
|
247
|
+
runStart(process.cwd());
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
241
250
|
case '--help':
|
|
242
251
|
case '-h':
|
|
243
252
|
case 'help':
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// `tewip` with nothing after it: look at this project and say what Tewip can actually do for it.
|
|
2
|
+
//
|
|
3
|
+
// Until now the answer to "I installed it, now what" was 233 lines of help listing eleven commands,
|
|
4
|
+
// which is a reference for somebody who already knows what they want and a wall for everybody else.
|
|
5
|
+
// Eleven commands is already more than most teams will run; the problem was never the number of
|
|
6
|
+
// things Tewip can do, it was that none of them were connected to *this* repository.
|
|
7
|
+
//
|
|
8
|
+
// So this looks. It counts what is actually here - test files and which framework wrote them,
|
|
9
|
+
// migrations and which tool, markup and which template language, a k6 or JMeter or Lighthouse
|
|
10
|
+
// report somebody has already produced - and then names the two or three commands that will tell
|
|
11
|
+
// this team something, with their own paths in them. And it says what Tewip cannot do here, because
|
|
12
|
+
// a front door that implies it can do everything is the same lie as a report with no limits on it.
|
|
13
|
+
//
|
|
14
|
+
// Nothing is run and nothing is written. It reads the directory and stops.
|
|
15
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
16
|
+
import { join, relative, resolve } from 'node:path';
|
|
17
|
+
import { languageOf, looksLikeTests } from '../vouch/tests-in.js';
|
|
18
|
+
import { MARKUP } from '../access/markup.js';
|
|
19
|
+
import { dialectOf } from '../schema/frameworks.js';
|
|
20
|
+
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'out', 'coverage', 'target', 'vendor', '__pycache__', '.venv', 'venv', '.next', '.cache', '.tewip', '.triage-agent']);
|
|
21
|
+
/**
|
|
22
|
+
* A bounded look: enough to recognise a project, not enough to be slow on a large one.
|
|
23
|
+
*
|
|
24
|
+
* Breadth-first, and that is the whole correctness of it. Depth-first with a budget spends the
|
|
25
|
+
* budget wherever the alphabet happens to lead: pointed at Tewip itself it descended into `data/`,
|
|
26
|
+
* exhausted 4000 files on stored datasets, and reported **7 test files in a repository with 69**.
|
|
27
|
+
* A number that wrong is worse than no number, because the front door is the one place a person
|
|
28
|
+
* has no way to tell. Breadth-first sees every top-level directory before descending into any of
|
|
29
|
+
* them, so the budget is spent on breadth, which is what recognising a project needs.
|
|
30
|
+
*/
|
|
31
|
+
export function lookAround(root, limit = 4000) {
|
|
32
|
+
const found = {
|
|
33
|
+
tests: new Map(),
|
|
34
|
+
migrations: new Map(),
|
|
35
|
+
markup: 0,
|
|
36
|
+
perfReports: [],
|
|
37
|
+
hasWorkflows: existsSync(join(root, '.github', 'workflows')),
|
|
38
|
+
hasStandards: existsSync(join(root, '.tewip', 'standards')),
|
|
39
|
+
filesSeen: false,
|
|
40
|
+
};
|
|
41
|
+
let seen = 0;
|
|
42
|
+
// A queue rather than recursion: every directory at one depth is read before any at the next.
|
|
43
|
+
const queue = [{ dir: root, depth: 0 }];
|
|
44
|
+
while (queue.length > 0 && seen < limit) {
|
|
45
|
+
const { dir, depth } = queue.shift();
|
|
46
|
+
if (depth > 8)
|
|
47
|
+
continue;
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = readdirSync(dir);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (seen >= limit)
|
|
57
|
+
break;
|
|
58
|
+
if (SKIP.has(entry) || (entry.startsWith('.') && entry !== '.github'))
|
|
59
|
+
continue;
|
|
60
|
+
const full = join(dir, entry);
|
|
61
|
+
let info;
|
|
62
|
+
try {
|
|
63
|
+
info = statSync(full);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (info.isDirectory()) {
|
|
69
|
+
queue.push({ dir: full, depth: depth + 1 });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
seen += 1;
|
|
73
|
+
found.filesSeen = true;
|
|
74
|
+
const rel = relative(root, full).replaceAll('\\', '/');
|
|
75
|
+
if (MARKUP.test(entry))
|
|
76
|
+
found.markup += 1;
|
|
77
|
+
if (/\.jtl$/i.test(entry))
|
|
78
|
+
found.perfReports.push(rel);
|
|
79
|
+
else if (/(k6|lighthouse|summary|lhr)[\w.-]*\.json$/i.test(entry) && info.size < 8_000_000)
|
|
80
|
+
found.perfReports.push(rel);
|
|
81
|
+
const inMigrations = /(^|\/)(migrations?|db\/migrate|changelog)\//i.test(rel);
|
|
82
|
+
// Reading a file is only worth it for the two questions a name cannot answer: is this a
|
|
83
|
+
// test, and is this a migration. Both are cheap, and both are often wrong from the path.
|
|
84
|
+
const language = languageOf(entry);
|
|
85
|
+
const worthReading = (language !== null || /\.(sql|xml)$/i.test(entry)) && info.size < 1_000_000;
|
|
86
|
+
if (!worthReading)
|
|
87
|
+
continue;
|
|
88
|
+
let text = '';
|
|
89
|
+
try {
|
|
90
|
+
text = readFileSync(full, 'utf8');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (language && looksLikeTests(rel, text))
|
|
96
|
+
found.tests.set(language, (found.tests.get(language) ?? 0) + 1);
|
|
97
|
+
if (inMigrations) {
|
|
98
|
+
const dialect = dialectOf(rel, text);
|
|
99
|
+
if (dialect !== 'unknown')
|
|
100
|
+
found.migrations.set(dialect, (found.migrations.get(dialect) ?? 0) + 1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return found;
|
|
105
|
+
}
|
|
106
|
+
const total = (counts) => [...counts.values()].reduce((a, b) => a + b, 0);
|
|
107
|
+
/** What a person calls these, rather than what the code calls them. */
|
|
108
|
+
const PLAIN = {
|
|
109
|
+
js: 'JavaScript or TypeScript',
|
|
110
|
+
python: 'Python',
|
|
111
|
+
jvm: 'Java, Kotlin or Scala',
|
|
112
|
+
go: 'Go',
|
|
113
|
+
csharp: 'C#',
|
|
114
|
+
ruby: 'Ruby',
|
|
115
|
+
php: 'PHP',
|
|
116
|
+
rust: 'Rust',
|
|
117
|
+
sql: 'SQL',
|
|
118
|
+
django: 'Django',
|
|
119
|
+
rails: 'Rails',
|
|
120
|
+
alembic: 'Alembic',
|
|
121
|
+
typeorm: 'TypeORM',
|
|
122
|
+
sequelize: 'Sequelize',
|
|
123
|
+
knex: 'Knex',
|
|
124
|
+
efcore: 'EF Core',
|
|
125
|
+
liquibase: 'Liquibase',
|
|
126
|
+
};
|
|
127
|
+
const named = (counts) => [...counts.entries()]
|
|
128
|
+
.sort((a, b) => b[1] - a[1])
|
|
129
|
+
// Only the ones worth naming: a repository with one stray file in a ninth language does not
|
|
130
|
+
// need all nine listed at the front door.
|
|
131
|
+
.slice(0, 3)
|
|
132
|
+
.map(([name, n]) => `${n} ${PLAIN[name] ?? name}`)
|
|
133
|
+
.join(', ');
|
|
134
|
+
/** The two or three things worth running here, most useful first. */
|
|
135
|
+
export function suggest(found) {
|
|
136
|
+
const out = [];
|
|
137
|
+
const tests = total(found.tests);
|
|
138
|
+
const migrations = total(found.migrations);
|
|
139
|
+
if (tests > 0) {
|
|
140
|
+
out.push({
|
|
141
|
+
command: 'tewip vouch',
|
|
142
|
+
because: `${tests} test ${tests === 1 ? 'file' : 'files'} here (${named(found.tests)}). This reads them and names the ones that cannot fail - no model, a second or two.`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (migrations > 0) {
|
|
146
|
+
out.push({
|
|
147
|
+
command: 'tewip schema --diff main...HEAD',
|
|
148
|
+
because: `${migrations} ${migrations === 1 ? 'migration' : 'migrations'} here (${named(found.migrations)}). This says what a change would do to a live database before it runs.`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (found.markup > 0) {
|
|
152
|
+
out.push({
|
|
153
|
+
command: 'tewip access',
|
|
154
|
+
because: `${found.markup} markup ${found.markup === 1 ? 'file' : 'files'} here. This names who cannot use the interface, against the WCAG criterion a regulator would cite.`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
for (const report of found.perfReports.slice(0, 1)) {
|
|
158
|
+
out.push({
|
|
159
|
+
command: `tewip perf ${report}`,
|
|
160
|
+
because: 'there is already a performance report here, and Tewip will judge it rather than restate it.',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (tests > 0) {
|
|
164
|
+
out.push({
|
|
165
|
+
command: 'tewip done',
|
|
166
|
+
because: 'the one verdict over your current change, once you have made one. It answers ready, not ready, or cannot say.',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
export function renderStart(found, root) {
|
|
172
|
+
const lines = ['# Tewip', ''];
|
|
173
|
+
if (!found.filesSeen) {
|
|
174
|
+
lines.push(`There is nothing to read in \`${root}\`. Run Tewip from the root of a project.`, '');
|
|
175
|
+
return lines.join('\n');
|
|
176
|
+
}
|
|
177
|
+
const suggestions = suggest(found);
|
|
178
|
+
if (suggestions.length === 0) {
|
|
179
|
+
lines.push(`Tewip looked around \`${root}\` and found nothing it can judge: no test files, no migrations, no markup, ` +
|
|
180
|
+
'and no performance report. That is an honest answer rather than a failure - point it at a project with ' +
|
|
181
|
+
'tests in it, or run `tewip --help` for the full list.', '');
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
lines.push('Here is what Tewip can tell you about **this** project, in the order worth doing.', '');
|
|
185
|
+
for (const [i, s] of suggestions.entries()) {
|
|
186
|
+
lines.push(`**${i + 1}. \`${s.command}\`**`, '', s.because, '');
|
|
187
|
+
}
|
|
188
|
+
lines.push('## What it will not do here', '');
|
|
189
|
+
const cannot = [];
|
|
190
|
+
if (total(found.tests) === 0)
|
|
191
|
+
cannot.push('No test files were found, so nothing can be said about whether your tests could fail or would catch a bug.');
|
|
192
|
+
if (total(found.migrations) === 0)
|
|
193
|
+
cannot.push('No migrations were found, so nothing is said about your database.');
|
|
194
|
+
if (found.markup === 0)
|
|
195
|
+
cannot.push('No markup was found, so nothing is said about accessibility.');
|
|
196
|
+
if (found.perfReports.length === 0)
|
|
197
|
+
cannot.push('No k6, JMeter or Lighthouse report was found. Tewip judges one you produce; it does not run load tests itself.');
|
|
198
|
+
if (!found.hasStandards)
|
|
199
|
+
cannot.push('This project has not taught Tewip any house rules yet. Put a markdown file in `.tewip/standards/` and `tewip standards` will enforce it.');
|
|
200
|
+
if (!found.hasWorkflows)
|
|
201
|
+
cannot.push('There is no GitHub Actions workflow here. `uses: hmamut39/tewip@main` with `mode: check` runs these same checks on every pull request.');
|
|
202
|
+
else
|
|
203
|
+
cannot.push('You have GitHub Actions. `mode: check` runs these same checks on every pull request.');
|
|
204
|
+
for (const line of cannot)
|
|
205
|
+
lines.push(`- ${line}`);
|
|
206
|
+
lines.push('');
|
|
207
|
+
lines.push('Everything Tewip answers carries its evidence and says what it did **not** check. It answers ' +
|
|
208
|
+
'"cannot say" rather than guessing. `tewip --help` has all eleven commands when you want them.', '');
|
|
209
|
+
return lines.join('\n');
|
|
210
|
+
}
|
|
211
|
+
export function runStart(root) {
|
|
212
|
+
const where = resolve(root);
|
|
213
|
+
const report = renderStart(lookAround(where), where);
|
|
214
|
+
console.log(report);
|
|
215
|
+
return report;
|
|
216
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"mcpName": "io.github.hmamut39/tewip",
|
|
5
5
|
"description": "Tewip finds why each failing test failed - real bug, flaky test, environment, test data or changed selector - for any stack, from the command line, CI, or your editor.",
|
|
6
6
|
"keywords": [
|