tewip-ai 0.47.0 → 0.49.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 +31 -1
- package/dist/cli/help.js +6 -1
- package/dist/cli/tewip.js +9 -0
- package/dist/engine/access/markup.js +17 -1
- package/dist/engine/schema/dialect-rules.js +47 -0
- package/dist/engine/schema/frameworks.js +8 -1
- package/dist/engine/schema/schema-command.js +1 -1
- 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.
|
|
@@ -897,7 +927,7 @@ That last section is the part no SQL linter does: it searches your own code for
|
|
|
897
927
|
away. "This drops `users.nickname`, and four files still read it" is a different sentence from "dropping a
|
|
898
928
|
column is risky", and it is the one that stops the outage.
|
|
899
929
|
|
|
900
|
-
It reads **SQL, Django, Rails, Alembic, TypeORM, Sequelize, Knex, EF Core and Liquibase** migrations — which covers Prisma, Drizzle, Flyway
|
|
930
|
+
It reads **SQL, Django, Rails, Alembic, TypeORM, Sequelize, Knex, Laravel, EF Core and Liquibase** migrations — which covers Prisma, Drizzle, Flyway
|
|
901
931
|
and golang-migrate too, since those all emit plain `.sql`. It judges **only the forward path** (a rollback step
|
|
902
932
|
contains the mirror image of the change, so reading it says a migration that creates a table destroys
|
|
903
933
|
one), and stays quiet when the team already did the safe thing — `atomic = False`, `algorithm:
|
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.
|
|
@@ -162,7 +167,7 @@ Usage:
|
|
|
162
167
|
that break the version still running during a deploy, destroy data,
|
|
163
168
|
block writes while they run, or cannot be undone - and searches your
|
|
164
169
|
code for what still uses the column it takes away. Reads SQL,
|
|
165
|
-
Django, Rails, Alembic, TypeORM, Sequelize, Knex, EF Core and
|
|
170
|
+
Django, Rails, Alembic, TypeORM, Sequelize, Knex, Laravel, EF Core and
|
|
166
171
|
Liquibase - so Prisma, Drizzle, Flyway and golang-migrate too,
|
|
167
172
|
since those emit plain SQL - and says so when it cannot read one.
|
|
168
173
|
It judges only the forward path, never the rollback. No cost.
|
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':
|
|
@@ -50,7 +50,23 @@ function withoutNonMarkup(source, jsLike) {
|
|
|
50
50
|
.replace(/\{\/\*[\s\S]*?\*\/\}/g, blank)
|
|
51
51
|
.replace(/<script\b[\s\S]*?<\/script\s*>/gi, blank)
|
|
52
52
|
.replace(/<style\b[\s\S]*?<\/style\s*>/gi, blank);
|
|
53
|
-
|
|
53
|
+
// A template expression is code, not markup, and real templates put markup *inside* it: GOV.UK's
|
|
54
|
+
// own examples gallery passes `{{ govukLabel({ "text": "<h1> govuk-label--xl" }) }}`, and reading
|
|
55
|
+
// that `<h1>` as a heading poisoned the outline so badly that twelve heading skips were reported
|
|
56
|
+
// on a page whose headings are perfectly ordered - on the design system built by the UK
|
|
57
|
+
// government's accessibility specialists, which is as wrong as this check can be.
|
|
58
|
+
//
|
|
59
|
+
// The delimiters are kept and only the contents blanked, which matters: `<button>{{ label }}</button>`
|
|
60
|
+
// must still read as content this file cannot see, so the button is asked about rather than
|
|
61
|
+
// called empty, and `alt="{{ alt }}"` must still count as an alt attribute that is present.
|
|
62
|
+
const withoutExpressions = stripped
|
|
63
|
+
// A template comment is a comment: Jinja, Nunjucks and Twig write `{# ... #}`, and Django and
|
|
64
|
+
// Jinja also have a `{% comment %}` block. Neither renders, so neither is markup.
|
|
65
|
+
.replace(/\{#[\s\S]*?#\}/g, blank)
|
|
66
|
+
.replace(/\{%-?\s*comment\s*-?%\}[\s\S]*?\{%-?\s*endcomment\s*-?%\}/gi, blank)
|
|
67
|
+
.replace(/\{\{[\s\S]*?\}\}/g, (m) => `{{${m.slice(2, -2).replace(/[^\n]/g, ' ')}}}`)
|
|
68
|
+
.replace(/\{%[\s\S]*?%\}/g, (m) => `{%${m.slice(2, -2).replace(/[^\n]/g, ' ')}%}`);
|
|
69
|
+
return jsLike ? withoutExpressions.replace(/`(?:[^`\\]|\\[\s\S])*`/g, blank) : withoutExpressions;
|
|
54
70
|
}
|
|
55
71
|
/** Files whose markup is embedded in JavaScript, where a backtick string is not page markup. */
|
|
56
72
|
export const JS_LIKE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro)$/i;
|
|
@@ -144,4 +144,51 @@ const ALEMBIC = [
|
|
|
144
144
|
// empty on purpose and every finding comes from reading that SQL, which is the same reader the
|
|
145
145
|
// `.sql` dialect uses. The exhaustive Record is what made this a compile error rather than a
|
|
146
146
|
// TypeORM migration silently producing no findings at all.
|
|
147
|
+
/**
|
|
148
|
+
* Laravel. Found missing by running Tewip over firefly-iii, a real finance application with **61
|
|
149
|
+
* migrations Tewip could not read a word of** - and Laravel is one of the most used web frameworks
|
|
150
|
+
* there is, while `schema` advertised nine dialects.
|
|
151
|
+
*
|
|
152
|
+
* Structurally it is Knex's shape: the table is named by `Schema::table('accounts', ...)` and the
|
|
153
|
+
* operation happens inside the closure, so the two are matched together across the lines between.
|
|
154
|
+
*/
|
|
155
|
+
export const LARAVEL = [
|
|
156
|
+
{
|
|
157
|
+
find: new RegExp(String.raw `Schema::(?:table|connection\(.*?\)->table)\(\s*${STR}[\s\S]{0,600}?->dropColumn\(\s*${STR}`, 'i'),
|
|
158
|
+
risk: 'breaks-old-code',
|
|
159
|
+
what: (m) => `drops ${m[1]}.${m[2]}`,
|
|
160
|
+
instead: 'deploy code that no longer selects or writes the column, then drop it in a later migration. Eloquent caches the column list, so the running version keeps selecting it',
|
|
161
|
+
table: (m) => m[1],
|
|
162
|
+
column: (m) => m[2],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
find: new RegExp(String.raw `Schema::(?:table|connection\(.*?\)->table)\(\s*${STR}[\s\S]{0,600}?->renameColumn\(\s*${STR}`, 'i'),
|
|
166
|
+
risk: 'breaks-old-code',
|
|
167
|
+
what: (m) => `renames ${m[1]}.${m[2]}`,
|
|
168
|
+
instead: 'add the new column, write to both, move the readers, then remove the old one',
|
|
169
|
+
table: (m) => m[1],
|
|
170
|
+
column: (m) => m[2],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
find: new RegExp(String.raw `Schema::drop(?:IfExists)?\(\s*${STR}`, 'i'),
|
|
174
|
+
risk: 'destroys-data',
|
|
175
|
+
what: (m) => `drops the table ${m[1]}`,
|
|
176
|
+
instead: 'stop reading the table, deploy that, then drop it once nothing refers to it',
|
|
177
|
+
table: (m) => m[1],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
find: new RegExp(String.raw `Schema::(?:table|connection\(.*?\)->table)\(\s*${STR}[\s\S]{0,600}?->index\(`, 'i'),
|
|
181
|
+
risk: 'locks-writes',
|
|
182
|
+
what: (m) => `builds an index on ${m[1]} while holding a lock that blocks writes to it`,
|
|
183
|
+
instead: 'build it with a raw CREATE INDEX CONCURRENTLY through DB::statement, or accept the lock if the table is small',
|
|
184
|
+
table: (m) => m[1],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
find: new RegExp(String.raw `Schema::(?:table|connection\(.*?\)->table)\(\s*${STR}[\s\S]{0,600}?->foreign\(`, 'i'),
|
|
188
|
+
risk: 'locks-writes',
|
|
189
|
+
what: (m) => `adds a foreign key to ${m[1]}, which checks every existing row under a lock on both tables`,
|
|
190
|
+
instead: 'add it NOT VALID and validate it afterwards, which takes a weaker lock',
|
|
191
|
+
table: (m) => m[1],
|
|
192
|
+
},
|
|
193
|
+
];
|
|
147
194
|
export { DJANGO, RAILS, ALEMBIC };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ALEMBIC, DJANGO, RAILS } from './dialect-rules.js';
|
|
1
|
+
import { ALEMBIC, DJANGO, LARAVEL, RAILS } from './dialect-rules.js';
|
|
2
2
|
import { EFCORE, KNEX, LIQUIBASE, SEQUELIZE } from './dialect-rules-node.js';
|
|
3
3
|
export function dialectOf(file, text) {
|
|
4
4
|
const path = file.split('\\').join('/').toLowerCase();
|
|
@@ -17,6 +17,10 @@ export function dialectOf(file, text) {
|
|
|
17
17
|
// strings passed to queryRunner.query(). The SQL is right there and perfectly readable; only the
|
|
18
18
|
// wrapper was unfamiliar, so every TypeORM project was told Tewip could not read its migrations
|
|
19
19
|
// at all - and on a Node backend that is most of them.
|
|
20
|
+
// Laravel, before the generic checks: its migrations are PHP and would otherwise fall through
|
|
21
|
+
// to 'unknown'. firefly-iii has 61 of them and Tewip could not read a word.
|
|
22
|
+
if (/Illuminate.Database.Migrations.Migration|\bSchema::(?:table|create|drop|dropIfExists|connection)\s*\(/.test(text))
|
|
23
|
+
return 'laravel';
|
|
20
24
|
if (/\bMigrationInterface\b|\bqueryRunner\.query\s*\(/.test(text))
|
|
21
25
|
return 'typeorm';
|
|
22
26
|
if (/\bqueryInterface\.\w+\s*\(/.test(text))
|
|
@@ -40,6 +44,7 @@ const BY_DIALECT = {
|
|
|
40
44
|
knex: KNEX,
|
|
41
45
|
efcore: EFCORE,
|
|
42
46
|
liquibase: LIQUIBASE,
|
|
47
|
+
laravel: LARAVEL,
|
|
43
48
|
};
|
|
44
49
|
/** Findings from a framework migration, with the line each one sits on. */
|
|
45
50
|
export function readFramework(dialect, text) {
|
|
@@ -111,6 +116,8 @@ export function forwardOnly(dialect, text) {
|
|
|
111
116
|
(dialect === 'sequelize' && /^\s*down\s*:/.test(line)) ||
|
|
112
117
|
(dialect === 'knex' && /^\s*(?:exports\.down|module\.exports\.down)\s*=/.test(line)) ||
|
|
113
118
|
(dialect === 'efcore' && /^\s*protected\s+override\s+void\s+Down\s*\(/.test(line)) ||
|
|
119
|
+
// Laravel's rollback, which in real files often comes *before* up().
|
|
120
|
+
(dialect === 'laravel' && /^\s*(?:public\s+)?function\s+down\s*\(/.test(line)) ||
|
|
114
121
|
// Liquibase puts the reverse inside the change itself rather than in a second method.
|
|
115
122
|
(dialect === 'liquibase' && /^\s*<rollback\b/.test(line));
|
|
116
123
|
if (down) {
|
|
@@ -70,7 +70,7 @@ export function readMigration(root, file) {
|
|
|
70
70
|
file,
|
|
71
71
|
dialect,
|
|
72
72
|
findings: [],
|
|
73
|
-
unread: 'Tewip does not recognise how this migration is written, so it says nothing about it rather than passing it. It reads SQL, Django, Rails, Alembic, TypeORM, Sequelize, Knex, EF Core and Liquibase migrations',
|
|
73
|
+
unread: 'Tewip does not recognise how this migration is written, so it says nothing about it rather than passing it. It reads SQL, Django, Rails, Alembic, TypeORM, Sequelize, Knex, Laravel, EF Core and Liquibase migrations',
|
|
74
74
|
irreversible: false,
|
|
75
75
|
};
|
|
76
76
|
}
|
|
@@ -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.49.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": [
|