tewip-ai 0.31.0 → 0.33.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
@@ -114,6 +114,22 @@ The report reads the history cache of the branch it runs on, usually the default
114
114
 
115
115
  The repository needs "Allow GitHub Actions to create and approve pull requests" enabled for fix PRs. Pull requests opened by the workflow token don't trigger CI themselves; the agent verified each change by re-running the test before opening the PR. See [triage-agent-sandbox](https://github.com/hmamut39/triage-agent-sandbox) for a working example. If this repository is private, other repositories can only use the action when Settings → Actions → Access allows it.
116
116
 
117
+ ### As a Claude Code plugin (no account, no marketplace approval)
118
+
119
+ ```bash
120
+ claude plugin marketplace add hmamut39/tewip
121
+ claude plugin install tewip@tewip
122
+ ```
123
+
124
+ That is the whole publication process. A marketplace is a `.claude-plugin/marketplace.json` file in a git
125
+ repository, so Tewip ships one in its own repo - nobody has to approve it, and there is nothing to pay
126
+ for. The plugin brings Tewip's MCP server and three skills that tell Claude Code *when* to use it:
127
+ before claiming a change is done, when a test fails, and when a change touches a migration. It costs
128
+ about 410 tokens per session until a skill is actually needed.
129
+
130
+ The MCP server runs from the published package (`npx -y tewip-ai@latest mcp`), so the plugin stays small
131
+ and you always get the current version.
132
+
117
133
  ### The editor extension (VS Code, Cursor, Windsurf, VSCodium)
118
134
 
119
135
  Download **[tewip-0.5.0.vsix](https://github.com/hmamut39/tewip/releases/tag/vscode-0.5.0)**, then in the
@@ -552,6 +568,43 @@ With `--json` the verdict is **sealed**: editing `not-ready` to `ready` in the f
552
568
  fail, so a verdict that travels into a ticket or a release note can be checked rather than trusted. It is
553
569
  also an MCP tool, so an agent can ask whether its own work is done before saying that it is.
554
570
 
571
+ ## Teach it your house rules (Phase 27)
572
+
573
+ ```bash
574
+ tewip standards # what this project has taught Tewip
575
+ tewip standards --diff main...HEAD --strict
576
+ ```
577
+
578
+ Tewip knows a lot that is true of software in general and nothing that is true of *your company*. Every
579
+ IT department has its own rules — "never drop a column in the release that stops writing to it",
580
+ "locators use data-testid", "no test may sleep" — and a tool that cannot be taught them is a tool that
581
+ argues with the house.
582
+
583
+ A standard is a markdown file in `.tewip/standards/`:
584
+
585
+ ```markdown
586
+ ---
587
+ description: A fixed wait hides a race instead of fixing it.
588
+ applies-to: tests/, e2e/
589
+ forbid: /waitForTimeout|sleep\(/ because a fixed wait passes by luck and fails in CI
590
+ ---
591
+
592
+ Wait for the thing you are actually waiting for — an element, a response, a state —
593
+ never for a number of milliseconds.
594
+ ```
595
+
596
+ **The split is the point.** The `forbid` and `require` lines are checked and enforced, with the offending
597
+ line quoted and your own reason printed beside it. The prose below is **guidance**: Tewip carries it into
598
+ whatever it writes or fixes, and quotes it in reports — but it is never counted as checked and never
599
+ reported as kept. A team that believes an unenforceable rule is being enforced is worse off than a team
600
+ with no tool at all, because they have stopped watching for it themselves. A rule Tewip cannot parse is
601
+ reported as unusable rather than silently dropped.
602
+
603
+ Once written, the rules appear everywhere the work is judged: in `tewip review`, in the pull request
604
+ comment, and as a question in `tewip done` — *"Does it keep this project's own standards?"* — which
605
+ answers **no** and names the rule. The guidance goes into the prompt when `tewip write` drafts a test, so
606
+ what comes back is in your conventions rather than Tewip's.
607
+
555
608
  ## What will this migration do to a live database? (Phase 26)
556
609
 
557
610
  ```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
- const report = [renderReview(review, runCommand), ...(vouched.length > 0 ? ['', ...vouched] : [])].join(String.fromCharCode(10));
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. Is what was asked for covered? Only where the project keeps plans to compare against.
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
  }
@@ -8,11 +8,25 @@
8
8
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
9
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
10
  import { existsSync } from 'node:fs';
11
- import { isAbsolute } from 'node:path';
11
+ import { readFileSync } from 'node:fs';
12
+ import { isAbsolute, join } from 'node:path';
12
13
  import { z } from 'zod';
13
14
  import { githubCiReport } from './github-report.js';
14
15
  import { explainFailure, listDatasets, listFailedRuns, runView, windowView } from './tools.js';
15
- const server = new McpServer({ name: 'tewip', version: '0.5.0' });
16
+ /**
17
+ * The version a client is told, read from the package rather than written here. It had drifted to 0.5.0
18
+ * while the package was at 0.32.0 - a small lie, but this server's whole job is to be the thing that
19
+ * does not say unverified numbers.
20
+ */
21
+ const version = (() => {
22
+ try {
23
+ return JSON.parse(readFileSync(join(import.meta.dirname, '..', 'package.json'), 'utf8')).version ?? '0.0.0';
24
+ }
25
+ catch {
26
+ return '0.0.0';
27
+ }
28
+ })();
29
+ const server = new McpServer({ name: 'tewip', version });
16
30
  const readOnly = { readOnlyHint: true, destructiveHint: false, idempotentHint: true };
17
31
  const dataset = z.string().optional().describe('Dataset directory relative to the Tewip repo, e.g. "data/real/gutenberg-holdout". Default: data/ (what the reporter collected). See list_datasets.');
18
32
  const run = z.string().optional().describe('Run id or a unique part of it. Default: the latest run with failures.');
@@ -216,4 +230,22 @@ server.registerTool('what_will_this_migration_do', {
216
230
  return 'No migration or schema file in that change.';
217
231
  return renderSchema(migrations, stillUsed(root, migrations.flatMap((m) => m.findings)));
218
232
  }));
233
+ server.registerTool('project_standards', {
234
+ title: "What are this project's own rules?",
235
+ 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.",
236
+ inputSchema: {
237
+ project,
238
+ files: z.array(z.string()).optional().describe('Files to check against the rules. Omit to list what the project has written.'),
239
+ diff: z.string().optional().describe('A git range whose changed files should be checked instead'),
240
+ },
241
+ annotations: readOnly,
242
+ }, ({ project: root, files, diff }) => answer(async () => {
243
+ if (!isAbsolute(root) || !existsSync(root))
244
+ throw new Error(`project must be an existing absolute path, got "${root}"`);
245
+ const { checkChanged, loadStandards, renderStandards } = await import('../engine/standards/standards-command.js');
246
+ const { changeSummary } = await import('../engine/quality/grounding.js');
247
+ const standards = loadStandards(root);
248
+ const changed = files && files.length > 0 ? files : diff ? (changeSummary(root, diff)?.files ?? []) : [];
249
+ return renderStandards(standards, changed.length > 0 ? checkChanged(root, standards, changed) : [], changed.length);
250
+ }));
219
251
  await server.connect(new StdioServerTransport());
package/package.json CHANGED
@@ -1,81 +1,82 @@
1
- {
2
- "name": "tewip-ai",
3
- "version": "0.31.0",
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
- "keywords": [
6
- "test-failures",
7
- "flaky-tests",
8
- "triage",
9
- "root-cause",
10
- "ci",
11
- "github-action",
12
- "playwright",
13
- "pytest",
14
- "jest",
15
- "vitest",
16
- "junit",
17
- "mcp",
18
- "ai-agent",
19
- "test-automation",
20
- "qa",
21
- "sdet",
22
- "devops"
23
- ],
24
- "homepage": "https://github.com/hmamut39/tewip#readme",
25
- "repository": {
26
- "type": "git",
27
- "url": "https://github.com/hmamut39/tewip.git"
28
- },
29
- "type": "module",
30
- "engines": {
31
- "node": ">=22.18"
32
- },
33
- "bin": {
34
- "tewip": "dist/cli/tewip.js"
35
- },
36
- "files": [
37
- "LICENSE",
38
- "QUICKSTART.md",
39
- "README.md",
40
- "TEAM-GUIDE.md",
41
- "dist/"
42
- ],
43
- "scripts": {
44
- "test": "playwright test",
45
- "test:loop": "node scripts/test-loop.ts 5",
46
- "history": "node scripts/history.ts",
47
- "diagnose": "node scripts/diagnose.ts",
48
- "triage": "node scripts/triage.ts",
49
- "explain": "node scripts/explain.ts",
50
- "lab": "node scripts/lab.ts",
51
- "test:engine": "node --test engine/test/*.test.ts outlets/test/*.test.ts mcp/test/*.test.ts",
52
- "mcp": "node mcp/server.ts",
53
- "eval": "node eval/evaluate.ts eval/ground-truth.jsonl --data data/real/gutenberg",
54
- "eval:holdout": "node eval/evaluate.ts eval/ground-truth.holdout.jsonl --data data/real/gutenberg-holdout",
55
- "eval:fresh": "node eval/evaluate.ts eval/ground-truth.fresh.jsonl --data data/real/gutenberg-fresh",
56
- "eval:aug": "node eval/evaluate.ts eval/ground-truth.aug.jsonl --data data/real/gutenberg-aug",
57
- "eval:aug2": "node eval/evaluate.ts eval/ground-truth.aug2.jsonl --data data/real/gutenberg-aug2",
58
- "eval:supabase": "node eval/evaluate.ts eval/ground-truth.supabase.jsonl --data data/real/supabase",
59
- "shadow": "node scripts/shadow.ts",
60
- "harvest": "node eval/harvest/harvest.ts",
61
- "eval:synthetic": "node eval/evaluate.ts eval/ground-truth.synthetic.jsonl",
62
- "typecheck": "tsc --noEmit",
63
- "build": "tsc -p tsconfig.build.json",
64
- "prepack": "npm run build",
65
- "tewip": "node cli/tewip.ts"
66
- },
67
- "license": "Apache-2.0",
68
- "devDependencies": {
69
- "@playwright/test": "^1.63.0",
70
- "@types/node": "^26.5.1",
71
- "typescript": "^7.0.2"
72
- },
73
- "dependencies": {
74
- "@modelcontextprotocol/sdk": "^1.30.0",
75
- "zod": "^4.6.5"
76
- },
77
- "bugs": {
78
- "url": "https://github.com/hmamut39/tewip/issues"
79
- },
80
- "author": "Hasan Mamut"
81
- }
1
+ {
2
+ "name": "tewip-ai",
3
+ "version": "0.33.0",
4
+ "mcpName": "io.github.hmamut39/tewip",
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
+ "keywords": [
7
+ "test-failures",
8
+ "flaky-tests",
9
+ "triage",
10
+ "root-cause",
11
+ "ci",
12
+ "github-action",
13
+ "playwright",
14
+ "pytest",
15
+ "jest",
16
+ "vitest",
17
+ "junit",
18
+ "mcp",
19
+ "ai-agent",
20
+ "test-automation",
21
+ "qa",
22
+ "sdet",
23
+ "devops"
24
+ ],
25
+ "homepage": "https://github.com/hmamut39/tewip#readme",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/hmamut39/tewip.git"
29
+ },
30
+ "type": "module",
31
+ "engines": {
32
+ "node": ">=22.18"
33
+ },
34
+ "bin": {
35
+ "tewip": "dist/cli/tewip.js"
36
+ },
37
+ "files": [
38
+ "LICENSE",
39
+ "QUICKSTART.md",
40
+ "README.md",
41
+ "TEAM-GUIDE.md",
42
+ "dist/"
43
+ ],
44
+ "scripts": {
45
+ "test": "playwright test",
46
+ "test:loop": "node scripts/test-loop.ts 5",
47
+ "history": "node scripts/history.ts",
48
+ "diagnose": "node scripts/diagnose.ts",
49
+ "triage": "node scripts/triage.ts",
50
+ "explain": "node scripts/explain.ts",
51
+ "lab": "node scripts/lab.ts",
52
+ "test:engine": "node --test engine/test/*.test.ts outlets/test/*.test.ts mcp/test/*.test.ts",
53
+ "mcp": "node mcp/server.ts",
54
+ "eval": "node eval/evaluate.ts eval/ground-truth.jsonl --data data/real/gutenberg",
55
+ "eval:holdout": "node eval/evaluate.ts eval/ground-truth.holdout.jsonl --data data/real/gutenberg-holdout",
56
+ "eval:fresh": "node eval/evaluate.ts eval/ground-truth.fresh.jsonl --data data/real/gutenberg-fresh",
57
+ "eval:aug": "node eval/evaluate.ts eval/ground-truth.aug.jsonl --data data/real/gutenberg-aug",
58
+ "eval:aug2": "node eval/evaluate.ts eval/ground-truth.aug2.jsonl --data data/real/gutenberg-aug2",
59
+ "eval:supabase": "node eval/evaluate.ts eval/ground-truth.supabase.jsonl --data data/real/supabase",
60
+ "shadow": "node scripts/shadow.ts",
61
+ "harvest": "node eval/harvest/harvest.ts",
62
+ "eval:synthetic": "node eval/evaluate.ts eval/ground-truth.synthetic.jsonl",
63
+ "typecheck": "tsc --noEmit",
64
+ "build": "tsc -p tsconfig.build.json",
65
+ "prepack": "npm run build",
66
+ "tewip": "node cli/tewip.ts"
67
+ },
68
+ "license": "Apache-2.0",
69
+ "devDependencies": {
70
+ "@playwright/test": "^1.63.0",
71
+ "@types/node": "^26.5.1",
72
+ "typescript": "^7.0.2"
73
+ },
74
+ "dependencies": {
75
+ "@modelcontextprotocol/sdk": "^1.30.0",
76
+ "zod": "^4.6.5"
77
+ },
78
+ "bugs": {
79
+ "url": "https://github.com/hmamut39/tewip/issues"
80
+ },
81
+ "author": "Hasan Mamut"
82
+ }