tewip-ai 0.31.0 → 0.32.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 +37 -0
- package/dist/cli/help.js +7 -0
- package/dist/cli/tewip.js +11 -0
- package/dist/engine/quality/write-command.js +2 -1
- package/dist/engine/quality/write.js +2 -0
- package/dist/engine/review/review-command.js +6 -1
- package/dist/engine/standards/standards-command.js +126 -0
- package/dist/engine/standards/standards.js +140 -0
- package/dist/engine/verdict/verdict-command.js +4 -0
- package/dist/engine/verdict/verdict.js +17 -1
- package/dist/mcp/server.js +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -552,6 +552,43 @@ With `--json` the verdict is **sealed**: editing `not-ready` to `ready` in the f
|
|
|
552
552
|
fail, so a verdict that travels into a ticket or a release note can be checked rather than trusted. It is
|
|
553
553
|
also an MCP tool, so an agent can ask whether its own work is done before saying that it is.
|
|
554
554
|
|
|
555
|
+
## Teach it your house rules (Phase 27)
|
|
556
|
+
|
|
557
|
+
```bash
|
|
558
|
+
tewip standards # what this project has taught Tewip
|
|
559
|
+
tewip standards --diff main...HEAD --strict
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Tewip knows a lot that is true of software in general and nothing that is true of *your company*. Every
|
|
563
|
+
IT department has its own rules — "never drop a column in the release that stops writing to it",
|
|
564
|
+
"locators use data-testid", "no test may sleep" — and a tool that cannot be taught them is a tool that
|
|
565
|
+
argues with the house.
|
|
566
|
+
|
|
567
|
+
A standard is a markdown file in `.tewip/standards/`:
|
|
568
|
+
|
|
569
|
+
```markdown
|
|
570
|
+
---
|
|
571
|
+
description: A fixed wait hides a race instead of fixing it.
|
|
572
|
+
applies-to: tests/, e2e/
|
|
573
|
+
forbid: /waitForTimeout|sleep\(/ because a fixed wait passes by luck and fails in CI
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
Wait for the thing you are actually waiting for — an element, a response, a state —
|
|
577
|
+
never for a number of milliseconds.
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
**The split is the point.** The `forbid` and `require` lines are checked and enforced, with the offending
|
|
581
|
+
line quoted and your own reason printed beside it. The prose below is **guidance**: Tewip carries it into
|
|
582
|
+
whatever it writes or fixes, and quotes it in reports — but it is never counted as checked and never
|
|
583
|
+
reported as kept. A team that believes an unenforceable rule is being enforced is worse off than a team
|
|
584
|
+
with no tool at all, because they have stopped watching for it themselves. A rule Tewip cannot parse is
|
|
585
|
+
reported as unusable rather than silently dropped.
|
|
586
|
+
|
|
587
|
+
Once written, the rules appear everywhere the work is judged: in `tewip review`, in the pull request
|
|
588
|
+
comment, and as a question in `tewip done` — *"Does it keep this project's own standards?"* — which
|
|
589
|
+
answers **no** and names the rule. The guidance goes into the prompt when `tewip write` drafts a test, so
|
|
590
|
+
what comes back is in your conventions rather than Tewip's.
|
|
591
|
+
|
|
555
592
|
## What will this migration do to a live database? (Phase 26)
|
|
556
593
|
|
|
557
594
|
```bash
|
package/dist/cli/help.js
CHANGED
|
@@ -118,6 +118,13 @@ Usage:
|
|
|
118
118
|
is what was asked for covered. Answers ready, not ready, or cannot
|
|
119
119
|
say - and cannot say is never rounded up to ready. --json writes a
|
|
120
120
|
sealed verdict that can be checked later. No model, no cost.
|
|
121
|
+
tewip standards [--diff main...HEAD] [--strict] [--out standards.md]
|
|
122
|
+
Teach Tewip your house rules. Markdown files in
|
|
123
|
+
.tewip/standards/ - a forbid or require line is checked and
|
|
124
|
+
enforced on the files a change touches; the prose below it is
|
|
125
|
+
guidance Tewip carries into whatever it writes or fixes. It never
|
|
126
|
+
reports guidance as verified, because a rule nobody enforces is
|
|
127
|
+
worse than no rule. No model, no cost.
|
|
121
128
|
tewip schema [<files>] [--diff main...HEAD] [--strict] [--out schema.md]
|
|
122
129
|
What will this migration do to a live database? Names the changes
|
|
123
130
|
that break the version still running during a deploy, destroy data,
|
package/dist/cli/tewip.js
CHANGED
|
@@ -199,6 +199,17 @@ async function main() {
|
|
|
199
199
|
});
|
|
200
200
|
return;
|
|
201
201
|
}
|
|
202
|
+
case 'standards': {
|
|
203
|
+
const { runStandards } = await import('../engine/standards/standards-command.js');
|
|
204
|
+
runStandards({
|
|
205
|
+
root: process.cwd(),
|
|
206
|
+
diff: flag(args, 'diff'),
|
|
207
|
+
files: args.filter((a, i) => !a.startsWith('--') && args[i - 1] !== '--diff' && args[i - 1] !== '--out'),
|
|
208
|
+
out: flag(args, 'out'),
|
|
209
|
+
strict: args.includes('--strict'),
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
202
213
|
case 'schema': {
|
|
203
214
|
const { runSchema } = await import('../engine/schema/schema-command.js');
|
|
204
215
|
runSchema({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// `tewip write`: turn a plan's "to automate next" scenarios into tests in the project's own
|
|
2
2
|
// framework. Kept tests stay beside the project's tests for the team to review and commit; every
|
|
3
3
|
// other draft is moved to .tewip/drafts with the reason, and nothing is committed or pushed.
|
|
4
|
+
import { guidanceFor, loadStandards } from '../standards/standards-command.js';
|
|
4
5
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
5
6
|
import { dirname, extname, join, relative } from 'node:path';
|
|
6
7
|
import { noModelHelp } from '../llm/providers.js';
|
|
@@ -76,7 +77,7 @@ ${noModelHelp()}`);
|
|
|
76
77
|
process.once('SIGINT', leaveNothing).once('SIGTERM', leaveNothing);
|
|
77
78
|
let outcome;
|
|
78
79
|
try {
|
|
79
|
-
({ outcome } = await writeAndVerify(model, { framework, fileName, pages, feature: saved.feature, code }, scenario, file, { runs: cmd.runs, repairs: cmd.repairs, mutateSource: cmd.mutateSource, log: (l) => console.error(l) }));
|
|
80
|
+
({ outcome } = await writeAndVerify(model, { framework, fileName, pages, feature: saved.feature, code, houseRules: guidanceFor(loadStandards(cmd.root), fileName) }, scenario, file, { runs: cmd.runs, repairs: cmd.repairs, mutateSource: cmd.mutateSource, log: (l) => console.error(l) }));
|
|
80
81
|
}
|
|
81
82
|
catch (error) {
|
|
82
83
|
outcome = { kind: 'rejected', reason: `stopped: ${error.message}`, repairs: 0, passedReliably: false };
|
|
@@ -7,6 +7,7 @@ import { readFileSync } from 'node:fs';
|
|
|
7
7
|
export const WRITE_PROMPT_VERSION = 'write-6';
|
|
8
8
|
const RULES = `Rules:
|
|
9
9
|
- Write in the given framework and language, in the style of the example test: the same imports, helpers, and way of opening pages (if it uses a base URL with relative paths, do the same).
|
|
10
|
+
- When houseRules is given, it is this team's own written guidance. Follow it as you would the example test; where it conflicts with your habits, the team wins.
|
|
10
11
|
- Find elements only by what the accessibility snapshots show: role and accessible name, label, placeholder or visible text. Never invent ids, classes or test ids that no snapshot shows.
|
|
11
12
|
- Check the scenario's expected result specifically. A check that is always true (a page merely loads, a list merely exists) is not a test.
|
|
12
13
|
- Wait for conditions, never for fixed times.
|
|
@@ -42,6 +43,7 @@ function contextFor(ctx, scenario) {
|
|
|
42
43
|
exampleTest: example,
|
|
43
44
|
pages: ctx.pages.map((p) => ({ readAt: p.url, title: p.title, accessibilitySnapshot: p.aria })),
|
|
44
45
|
codeUnderTest: ctx.code?.length ? ctx.code : undefined,
|
|
46
|
+
houseRules: ctx.houseRules || undefined,
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
export async function draftTest(model, ctx, scenario) {
|
|
@@ -9,6 +9,8 @@ import { renderReview, reviewChange } from './review.js';
|
|
|
9
9
|
import { changedExpectations } from './expectations.js';
|
|
10
10
|
import { changedAgreements } from './agreements.js';
|
|
11
11
|
import { changedTestLines, vouchChanged } from '../vouch/changed.js';
|
|
12
|
+
import { checkChanged, loadStandards } from '../standards/standards-command.js';
|
|
13
|
+
import { renderViolations } from '../standards/standards.js';
|
|
12
14
|
/** Every failure Tewip has diagnosed in this project, from the history it keeps. */
|
|
13
15
|
function history(root, only) {
|
|
14
16
|
const dir = join(root, '.tewip', 'history');
|
|
@@ -37,7 +39,10 @@ export async function runReview(cmd) {
|
|
|
37
39
|
// Whether the tests in this change could ever fail. Costs nothing, calls no model, and is the one
|
|
38
40
|
// question an agent-written diff most needs asked of it.
|
|
39
41
|
const vouched = changedTestLines(vouchChanged(cmd.root, files));
|
|
40
|
-
|
|
42
|
+
// The project's own rules, which Tewip only knows because this team wrote them down.
|
|
43
|
+
const standards = loadStandards(cmd.root);
|
|
44
|
+
const broken = renderViolations(checkChanged(cmd.root, standards, files), standards);
|
|
45
|
+
const report = [renderReview(review, runCommand), ...(vouched.length > 0 ? ['', ...vouched] : []), ...(broken.length > 0 ? ['', ...broken] : [])].join(String.fromCharCode(10));
|
|
41
46
|
console.log(report);
|
|
42
47
|
if (past.length === 0) {
|
|
43
48
|
console.log('\nNo failure history yet, so the "what has gone wrong here before" part is empty. It fills itself: run `tewip explain` or `tewip report` on your CI reports and every later review knows more.');
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// `tewip standards`: what this project has taught Tewip, and what of it can actually be checked.
|
|
2
|
+
//
|
|
3
|
+
// Loading is on demand and cheap: only the name and description of each standard are read until
|
|
4
|
+
// something needs the rest, which is the same discipline a skill file uses. The report leads with the
|
|
5
|
+
// split that matters - rules Tewip enforces, and guidance it can only carry - because a team that
|
|
6
|
+
// believes an unenforceable rule is being enforced is worse off than a team with no tool at all.
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { dirname, join, resolve } from 'node:path';
|
|
9
|
+
import { changeSummary } from '../quality/grounding.js';
|
|
10
|
+
import { check, readStandard, renderViolations } from './standards.js';
|
|
11
|
+
export const STANDARDS_DIR = join('.tewip', 'standards');
|
|
12
|
+
/** Every standard this project has written, newest name order, with unreadable ones reported. */
|
|
13
|
+
export function loadStandards(root) {
|
|
14
|
+
const dir = join(root, STANDARDS_DIR);
|
|
15
|
+
if (!existsSync(dir))
|
|
16
|
+
return [];
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const entry of readdirSync(dir).filter((f) => /\.(md|markdown)$/i.test(f)).sort()) {
|
|
19
|
+
try {
|
|
20
|
+
out.push(readStandard(entry, readFileSync(join(dir, entry), 'utf8')));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
out.push({ name: entry, description: '', appliesTo: [], rules: [], guidance: '', unusable: 'the file could not be read' });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The guidance to hand a model when writing or fixing something in this file.
|
|
30
|
+
*
|
|
31
|
+
* Only the standards that apply, and only their prose: the checkable rules are enforced afterwards by
|
|
32
|
+
* `check`, so repeating them in a prompt would be asking politely for something that is about to be
|
|
33
|
+
* verified anyway.
|
|
34
|
+
*/
|
|
35
|
+
export function guidanceFor(standards, file) {
|
|
36
|
+
const relevant = standards.filter((s) => s.guidance.trim() && covers(s.appliesTo, file));
|
|
37
|
+
if (relevant.length === 0)
|
|
38
|
+
return '';
|
|
39
|
+
return relevant.map((s) => `## ${s.name}${s.description ? ` - ${s.description}` : ''}${String.fromCharCode(10)}${s.guidance.trim()}`).join(String.fromCharCode(10) + String.fromCharCode(10)).slice(0, 6000);
|
|
40
|
+
}
|
|
41
|
+
const covers = (where, file) => where.length === 0 || where.some((p) => file.split('\\').join('/').includes(p.replace(/^\*\*?\//, '').replace(/\*/g, '')));
|
|
42
|
+
/** Violations in the files a change touched. */
|
|
43
|
+
export function checkChanged(root, standards, files) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const full = resolve(root, file);
|
|
47
|
+
if (!existsSync(full))
|
|
48
|
+
continue;
|
|
49
|
+
try {
|
|
50
|
+
out.push(...check(standards, file.split('\\').join('/'), readFileSync(full, 'utf8')));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Unreadable: nothing can be said about it.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
export function renderStandards(standards, violations, checkedFiles) {
|
|
59
|
+
if (standards.length === 0) {
|
|
60
|
+
return [
|
|
61
|
+
'# This project has taught Tewip nothing yet',
|
|
62
|
+
'',
|
|
63
|
+
`Put a markdown file in \`${STANDARDS_DIR.split('\\').join('/')}/\` and Tewip will read it. For example, \`no-fixed-waits.md\`:`,
|
|
64
|
+
'',
|
|
65
|
+
'```markdown',
|
|
66
|
+
'---',
|
|
67
|
+
'description: A fixed wait hides a race instead of fixing it.',
|
|
68
|
+
'applies-to: tests/, e2e/',
|
|
69
|
+
'forbid: /waitForTimeout|sleep\\(/ because a fixed wait passes by luck and fails in CI',
|
|
70
|
+
'---',
|
|
71
|
+
'',
|
|
72
|
+
'Wait for the thing you are actually waiting for - an element, a response, a state -',
|
|
73
|
+
'never for a number of milliseconds.',
|
|
74
|
+
'```',
|
|
75
|
+
'',
|
|
76
|
+
'The `forbid` and `require` lines are checked and enforced. The prose below them is guidance: Tewip',
|
|
77
|
+
'passes it on when it writes or fixes something, and never claims to have verified it.',
|
|
78
|
+
].join(String.fromCharCode(10));
|
|
79
|
+
}
|
|
80
|
+
const checkable = standards.filter((s) => s.rules.length > 0);
|
|
81
|
+
const guidanceOnly = standards.filter((s) => s.rules.length === 0 && s.guidance.trim());
|
|
82
|
+
const lines = [
|
|
83
|
+
'# What this project has taught Tewip',
|
|
84
|
+
'',
|
|
85
|
+
`${standards.length} standard(s) loaded: ${checkable.length} with rules Tewip can check, ${guidanceOnly.length} guidance only.`,
|
|
86
|
+
...(checkedFiles > 0 ? ['', `${checkedFiles} changed file(s) checked. **${violations.length}** broke a rule.`] : []),
|
|
87
|
+
'',
|
|
88
|
+
'## Checked and enforced',
|
|
89
|
+
'',
|
|
90
|
+
];
|
|
91
|
+
if (checkable.length === 0)
|
|
92
|
+
lines.push('None. No standard here carries a `forbid:` or `require:` rule, so nothing is enforced.', '');
|
|
93
|
+
for (const standard of checkable) {
|
|
94
|
+
lines.push(`- **${standard.name}**${standard.description ? ` — ${standard.description}` : ''}`);
|
|
95
|
+
for (const rule of standard.rules) {
|
|
96
|
+
lines.push(` - ${rule.kind} \`${rule.pattern}\`${rule.where.length > 0 ? ` in ${rule.where.join(', ')}` : ''}${rule.because ? ` — ${rule.because}` : ''}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
lines.push('');
|
|
100
|
+
if (guidanceOnly.length > 0) {
|
|
101
|
+
lines.push('## Carried, but not checked', '', 'Tewip passes these to the model when it writes or fixes something, and quotes them in reports. It', 'cannot verify them, and it will never report them as kept.', '', ...guidanceOnly.map((s) => `- **${s.name}**${s.description ? ` — ${s.description}` : ''}`), '');
|
|
102
|
+
}
|
|
103
|
+
const unusable = standards.filter((s) => s.unusable);
|
|
104
|
+
if (unusable.length > 0) {
|
|
105
|
+
lines.push(`## Could not be used (${unusable.length})`, '', ...unusable.map((s) => `- **${s.name}**: ${s.unusable}`), '');
|
|
106
|
+
}
|
|
107
|
+
lines.push(...renderViolations(violations, standards));
|
|
108
|
+
lines.push('---', '', 'A rule is enforced only if it appears above under "checked and enforced". Everything else is prose', 'Tewip carries for you - useful, and not the same thing as a check.');
|
|
109
|
+
return lines.join(String.fromCharCode(10));
|
|
110
|
+
}
|
|
111
|
+
export function runStandards(cmd) {
|
|
112
|
+
const standards = loadStandards(cmd.root);
|
|
113
|
+
const files = cmd.files && cmd.files.length > 0 ? cmd.files : cmd.diff ? (changeSummary(cmd.root, cmd.diff)?.files ?? []) : [];
|
|
114
|
+
const violations = files.length > 0 ? checkChanged(cmd.root, standards, files.map((f) => f.split('\\').join('/'))) : [];
|
|
115
|
+
const report = renderStandards(standards, violations, files.length);
|
|
116
|
+
console.log(report);
|
|
117
|
+
if (cmd.out) {
|
|
118
|
+
mkdirSync(dirname(resolve(cmd.out)), { recursive: true });
|
|
119
|
+
writeFileSync(resolve(cmd.out), report + String.fromCharCode(10), 'utf8');
|
|
120
|
+
}
|
|
121
|
+
if (cmd.strict && violations.length > 0) {
|
|
122
|
+
console.error(String.fromCharCode(10) + `${violations.length} violation(s) of this project's own standards.`);
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
}
|
|
125
|
+
return { violations: violations.length };
|
|
126
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads one standard.
|
|
3
|
+
*
|
|
4
|
+
* The format is deliberately plain: a markdown file with a small front matter block. A team should be
|
|
5
|
+
* able to write one without reading a manual, and anything Tewip cannot understand is reported as
|
|
6
|
+
* unusable rather than silently ignored - a rule nobody is enforcing is worse than no rule, because
|
|
7
|
+
* somebody believes in it.
|
|
8
|
+
*/
|
|
9
|
+
export function readStandard(file, text) {
|
|
10
|
+
const name = file.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? file;
|
|
11
|
+
const front = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
12
|
+
const body = front ? text.slice(front[0].length).trim() : text.trim();
|
|
13
|
+
const standard = { name, description: '', appliesTo: [], rules: [], guidance: body, unusable: '' };
|
|
14
|
+
if (!front) {
|
|
15
|
+
// Prose only. That is allowed, and it is guidance - but say so, so nobody thinks it is enforced.
|
|
16
|
+
standard.description = firstLine(body);
|
|
17
|
+
return standard;
|
|
18
|
+
}
|
|
19
|
+
for (const line of front[1].split(String.fromCharCode(10))) {
|
|
20
|
+
const entry = /^\s*([a-z-]+)\s*:\s*(.*)$/i.exec(line);
|
|
21
|
+
if (!entry)
|
|
22
|
+
continue;
|
|
23
|
+
const [, key, rawValue] = entry;
|
|
24
|
+
const value = rawValue.trim();
|
|
25
|
+
if (key === 'name' && value)
|
|
26
|
+
standard.name = value;
|
|
27
|
+
else if (key === 'description')
|
|
28
|
+
standard.description = value;
|
|
29
|
+
else if (key === 'applies-to')
|
|
30
|
+
standard.appliesTo = list(value);
|
|
31
|
+
else if (key === 'forbid' || key === 'require') {
|
|
32
|
+
const rule = parseRule(key, value, standard.appliesTo);
|
|
33
|
+
if (rule)
|
|
34
|
+
standard.rules.push(rule);
|
|
35
|
+
else
|
|
36
|
+
standard.unusable = `the ${key} on this line could not be read: ${line.trim().slice(0, 120)}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!standard.description)
|
|
40
|
+
standard.description = firstLine(body);
|
|
41
|
+
return standard;
|
|
42
|
+
}
|
|
43
|
+
/** `forbid: /waitForTimeout/ in tests because a fixed wait hides a race` */
|
|
44
|
+
function parseRule(kind, value, scope) {
|
|
45
|
+
const shape = /^\/(.+?)\/([a-z]*)\s*(?:\bin\s+([^]*?))?\s*(?:\bbecause\s+([^]*))?$/i.exec(value.trim());
|
|
46
|
+
if (!shape)
|
|
47
|
+
return null;
|
|
48
|
+
try {
|
|
49
|
+
new RegExp(shape[1], shape[2] || undefined); // it has to be a pattern before it is a rule
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
kind,
|
|
56
|
+
pattern: shape[2] ? `/${shape[1]}/${shape[2]}` : shape[1],
|
|
57
|
+
where: shape[3] ? list(shape[3]) : scope,
|
|
58
|
+
because: (shape[4] ?? '').trim(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const list = (value) => value
|
|
62
|
+
.replace(/^\[|\]$/g, '')
|
|
63
|
+
.split(/[,\s]+/)
|
|
64
|
+
.map((v) => v.trim().replace(/^["']|["']$/g, ''))
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
const firstLine = (text) => text.split(String.fromCharCode(10)).map((l) => l.replace(/^#+\s*/, '').trim()).find(Boolean)?.slice(0, 160) ?? '';
|
|
67
|
+
/** Does this file fall under that scope? A suffix, an extension, or a path fragment. */
|
|
68
|
+
export function covers(where, file) {
|
|
69
|
+
if (where.length === 0)
|
|
70
|
+
return true;
|
|
71
|
+
const path = file.split('\\').join('/');
|
|
72
|
+
return where.some((pattern) => {
|
|
73
|
+
const clean = pattern.replace(/^\*\*?\//, '').replace(/\*/g, '');
|
|
74
|
+
return clean !== '' && path.includes(clean);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const regexOf = (rule) => {
|
|
78
|
+
const slashed = /^\/(.+)\/([a-z]*)$/.exec(rule.pattern);
|
|
79
|
+
return slashed ? new RegExp(slashed[1], slashed[2]) : new RegExp(rule.pattern);
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The rules this file breaks.
|
|
83
|
+
*
|
|
84
|
+
* `forbid` is checked line by line, so the violation can be quoted. `require` is checked over the whole
|
|
85
|
+
* file, because "this file must mention X" is a statement about the file and pointing at a line would be
|
|
86
|
+
* inventing a location.
|
|
87
|
+
*/
|
|
88
|
+
export function check(standards, file, text) {
|
|
89
|
+
const out = [];
|
|
90
|
+
const lines = text.split(String.fromCharCode(10));
|
|
91
|
+
for (const standard of standards) {
|
|
92
|
+
if (!covers(standard.appliesTo, file))
|
|
93
|
+
continue;
|
|
94
|
+
for (const rule of standard.rules) {
|
|
95
|
+
if (!covers(rule.where, file))
|
|
96
|
+
continue;
|
|
97
|
+
let pattern;
|
|
98
|
+
try {
|
|
99
|
+
pattern = regexOf(rule);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (rule.kind === 'forbid') {
|
|
105
|
+
lines.forEach((line, index) => {
|
|
106
|
+
if (pattern.test(line))
|
|
107
|
+
out.push({ standard: standard.name, file, line: index + 1, text: line.trim().slice(0, 160), rule });
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else if (!pattern.test(text)) {
|
|
111
|
+
out.push({ standard: standard.name, file, line: 0, text: '', rule });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
/** The section for a review, a pull request comment or a verdict. Empty when nothing is broken. */
|
|
118
|
+
export function renderViolations(violations, loaded) {
|
|
119
|
+
if (violations.length === 0)
|
|
120
|
+
return [];
|
|
121
|
+
const lines = [`## Your own standards (${violations.length})`, ''];
|
|
122
|
+
for (const v of violations.slice(0, 20)) {
|
|
123
|
+
const where = v.line > 0 ? `${v.file}:${v.line}` : v.file;
|
|
124
|
+
lines.push(v.rule.kind === 'forbid'
|
|
125
|
+
? `- **${v.standard}** — \`${where}\` has \`${v.rule.pattern}\`, which this project forbids`
|
|
126
|
+
: `- **${v.standard}** — \`${where}\` does not have \`${v.rule.pattern}\`, which this project requires`);
|
|
127
|
+
if (v.text)
|
|
128
|
+
lines.push(` - \`${v.text.replace(/`/g, "'")}\``);
|
|
129
|
+
if (v.rule.because)
|
|
130
|
+
lines.push(` - ${v.rule.because}`);
|
|
131
|
+
}
|
|
132
|
+
if (violations.length > 20)
|
|
133
|
+
lines.push(`- …and ${violations.length - 20} more`);
|
|
134
|
+
const guidance = loaded.filter((s) => s.guidance.trim().length > 0);
|
|
135
|
+
if (guidance.length > 0) {
|
|
136
|
+
lines.push('', `${guidance.length} standard(s) also carry guidance Tewip cannot check: ${guidance.map((s) => s.name).join(', ')}. It is passed on when Tewip writes or fixes something, and it is not counted here as either kept or broken.`);
|
|
137
|
+
}
|
|
138
|
+
lines.push('');
|
|
139
|
+
return lines;
|
|
140
|
+
}
|
|
@@ -13,6 +13,7 @@ import { coverageFor, testFiles } from '../review/coverage.js';
|
|
|
13
13
|
import { importingTests } from '../prove/prove.js';
|
|
14
14
|
import { testsReaching } from '../prove/reaches.js';
|
|
15
15
|
import { vouchChanged } from '../vouch/changed.js';
|
|
16
|
+
import { checkChanged, loadStandards } from '../standards/standards-command.js';
|
|
16
17
|
import { datasetFiles, readJsonl, readTriageRecords } from '../../reporters/store.js';
|
|
17
18
|
import { decide, renderDecision } from './verdict.js';
|
|
18
19
|
const SOURCE = /\.(js|jsx|mjs|cjs|ts|tsx|mts|cts|py|java|kt|go|cs|rb|php)$/i;
|
|
@@ -97,6 +98,8 @@ export function gather(cmd) {
|
|
|
97
98
|
return touched.has(where) || [...touched].some((t) => r.testId.includes(t.replace(/\.[a-z]+$/, '')));
|
|
98
99
|
})
|
|
99
100
|
.map((r) => ({ testId: r.testId, category: String(r.diagnosis?.category ?? 'UNKNOWN') }));
|
|
101
|
+
const standards = loadStandards(cmd.root);
|
|
102
|
+
const brokenRules = standards.length === 0 ? null : checkChanged(cmd.root, standards, changed).map((v) => ({ standard: v.standard, file: v.file, line: v.line, why: v.rule.because }));
|
|
100
103
|
const proved = cmd.prove ? proveChanged(cmd.root, changedSource, { budgetSeconds: cmd.proveBudgetSeconds ?? 300, onStep: (m) => console.error(` ${m}`) }) : null;
|
|
101
104
|
return {
|
|
102
105
|
changed,
|
|
@@ -110,6 +113,7 @@ export function gather(cmd) {
|
|
|
110
113
|
failingNow,
|
|
111
114
|
noHistory: runs.length === 0,
|
|
112
115
|
uncovered: uncoveredFor(cmd.root, changed, Object.keys(newest?.attempts ?? {})),
|
|
116
|
+
brokenRules,
|
|
113
117
|
},
|
|
114
118
|
};
|
|
115
119
|
}
|
|
@@ -76,7 +76,23 @@ export function decide(evidence) {
|
|
|
76
76
|
else {
|
|
77
77
|
checks.push({ question: 'Would the tests catch a bug in this code?', standing: 'met', answer: 'every deliberate change made to this code was caught by a covering test', decidedBy: 'tewip prove' });
|
|
78
78
|
}
|
|
79
|
-
// 5.
|
|
79
|
+
// 5. Does it keep the rules this team wrote down? Only a project that has written some can break them.
|
|
80
|
+
if (evidence.brokenRules === null) {
|
|
81
|
+
checks.push({ question: "Does it keep this project's own standards?", standing: 'not-checked', answer: 'this project has written none for Tewip to read, so there is nothing to keep or break', decidedBy: 'tewip standards' });
|
|
82
|
+
}
|
|
83
|
+
else if (evidence.brokenRules.length > 0) {
|
|
84
|
+
const first = evidence.brokenRules[0];
|
|
85
|
+
checks.push({
|
|
86
|
+
question: "Does it keep this project's own standards?",
|
|
87
|
+
standing: 'not-met',
|
|
88
|
+
answer: `${evidence.brokenRules.length} rule(s) this team wrote are broken, starting with ${first.standard} at ${first.file}${first.line > 0 ? `:${first.line}` : ''}${first.why ? ` - ${first.why}` : ''}`,
|
|
89
|
+
decidedBy: 'tewip standards, which enforces only the rules a pattern can check',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
checks.push({ question: "Does it keep this project's own standards?", standing: 'met', answer: 'every rule this team wrote and Tewip can check is kept', decidedBy: 'tewip standards' });
|
|
94
|
+
}
|
|
95
|
+
// 6. Is what was asked for covered? Only where the project keeps plans to compare against.
|
|
80
96
|
if (evidence.uncovered === null) {
|
|
81
97
|
checks.push({ question: 'Is what was asked for covered?', standing: 'not-checked', answer: 'this project keeps no test plan Tewip can read, so there is nothing to compare the change against', decidedBy: 'tewip trace' });
|
|
82
98
|
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -216,4 +216,22 @@ server.registerTool('what_will_this_migration_do', {
|
|
|
216
216
|
return 'No migration or schema file in that change.';
|
|
217
217
|
return renderSchema(migrations, stillUsed(root, migrations.flatMap((m) => m.findings)));
|
|
218
218
|
}));
|
|
219
|
+
server.registerTool('project_standards', {
|
|
220
|
+
title: "What are this project's own rules?",
|
|
221
|
+
description: "The house rules this team has written down in .tewip/standards/, and whether a change keeps them. Returns two things kept carefully apart: rules Tewip checks and enforces (a forbid or require pattern over a set of files), and guidance it can only carry - prose for the parts no pattern captures, which is passed on when something is written but never reported as verified. Use it before writing or changing code in somebody's project, so what you produce matches their conventions rather than yours.",
|
|
222
|
+
inputSchema: {
|
|
223
|
+
project,
|
|
224
|
+
files: z.array(z.string()).optional().describe('Files to check against the rules. Omit to list what the project has written.'),
|
|
225
|
+
diff: z.string().optional().describe('A git range whose changed files should be checked instead'),
|
|
226
|
+
},
|
|
227
|
+
annotations: readOnly,
|
|
228
|
+
}, ({ project: root, files, diff }) => answer(async () => {
|
|
229
|
+
if (!isAbsolute(root) || !existsSync(root))
|
|
230
|
+
throw new Error(`project must be an existing absolute path, got "${root}"`);
|
|
231
|
+
const { checkChanged, loadStandards, renderStandards } = await import('../engine/standards/standards-command.js');
|
|
232
|
+
const { changeSummary } = await import('../engine/quality/grounding.js');
|
|
233
|
+
const standards = loadStandards(root);
|
|
234
|
+
const changed = files && files.length > 0 ? files : diff ? (changeSummary(root, diff)?.files ?? []) : [];
|
|
235
|
+
return renderStandards(standards, changed.length > 0 ? checkChanged(root, standards, changed) : [], changed.length);
|
|
236
|
+
}));
|
|
219
237
|
await server.connect(new StdioServerTransport());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"test-failures",
|