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 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.47.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": [