tewip-ai 0.70.16 → 0.70.17

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.
@@ -95,6 +95,21 @@ export function decide(test, { minRuns, alwaysFailsAbove }) {
95
95
  //
96
96
  // Phase 56 is where this comes from: of the signals available when a test fails, non-determinism
97
97
  // is the one that is genuinely decisive, and it costs nothing to read.
98
+ // This is also what makes quarantine safe, and not the rule above it.
99
+ //
100
+ // The rail that refuses to quarantine a test whose failures looked like the application being
101
+ // wrong reads the *diagnosed* category, and on real stored history there is none: across
102
+ // gutenberg, gutenberg-aug2 and supabase, all 3,249 harvested failures have no diagnosis at all,
103
+ // because nobody ran `tewip explain` over them. So the rail never fires.
104
+ //
105
+ // Measured anyway, against 2,773 hand labels (`node eval/flaky-safety.ts`): of 241 tests
106
+ // recommended for quarantine, 218 had a labelled failure and **none of them was ever labelled a
107
+ // real bug**. All 68 tests that were catching real bugs came back `watch`, never `quarantine`.
108
+ //
109
+ // The safety is real and its source is this condition, not the rail: a test that failed and then
110
+ // passed on a retry of the same commit did both on the same code. A test catching a real bug does
111
+ // not pass on a retry. That is an observation, which is why it still works when every diagnosis
112
+ // is missing.
98
113
  if (test.recoveredOnRetry > 0 && test.failedRuns >= minRuns) {
99
114
  const times = test.recoveredOnRetry === 1 ? 'once' : `${test.recoveredOnRetry} times`;
100
115
  const spread = test.branches.length > 1 ? ` It failed on ${test.branches.length} different branches, so it is the test rather than one person's change.` : '';
@@ -5,7 +5,7 @@ import { changeSummary } from '../quality/grounding.js';
5
5
  import { chooseFramework, detectFrameworks } from '../quality/frameworks.js';
6
6
  import { readTriageRecords, datasetFiles } from '../../reporters/store.js';
7
7
  import { coverageFor, uncovered } from './coverage.js';
8
- import { renderReview, reviewChange } from './review.js';
8
+ import { renderReview, reviewChange, runFrom } 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';
@@ -35,7 +35,13 @@ export async function runReview(cmd) {
35
35
  // Printing a Playwright command for a file Playwright never runs is worse than printing nothing.
36
36
  const framework = chooseFramework(detectFrameworks(cmd.root), undefined, false) ?? detectFrameworks(cmd.root)[0] ?? null;
37
37
  const ownsThem = framework ? review.runFirst.some((f) => resolve(cmd.root, f).startsWith(resolve(framework.testDir))) : false;
38
- const runCommand = framework && ownsThem ? (chosen) => chosen.map((f) => framework.command(resolve(cmd.root, f))).join(' && ') : undefined;
38
+ // And from the directory the runner actually runs in. `framework.command` builds paths relative
39
+ // to the framework's own cwd, which in a layered project is not where the report is read: this
40
+ // printed `node --test "test/flaky.test.ts"` for a file at engine/test/flaky.test.ts, and copying
41
+ // it gave "Could not find 'test/flaky.test.ts'". The one actionable line in a review has to work
42
+ // where somebody pastes it.
43
+ const prefix = framework ? runFrom(cmd.root, framework.cwd) : '';
44
+ const runCommand = framework && ownsThem ? (chosen) => prefix + chosen.map((f) => framework.command(resolve(cmd.root, f))).join(' && ') : undefined;
39
45
  // Whether the tests in this change could ever fail. Costs nothing, calls no model, and is the one
40
46
  // question an agent-written diff most needs asked of it.
41
47
  const vouched = changedTestLines(vouchChanged(cmd.root, files));
@@ -1,3 +1,4 @@
1
+ import { relative, sep } from 'node:path';
1
2
  // The risk review of a change, from what Tewip already knows about this project.
2
3
  //
3
4
  // A general coding agent can read a diff. What it cannot do is tell you that the two tests covering
@@ -98,6 +99,19 @@ const when = (at) => {
98
99
  return days <= 1 ? 'yesterday' : days < 14 ? `${days} days ago` : days < 60 ? `${Math.round(days / 7)} weeks ago` : `${Math.round(days / 30)} months ago`;
99
100
  };
100
101
  /** The review as a person reads it, before they read the diff. */
102
+ /**
103
+ * The `cd` a suggested command needs, when the runner does not run from the repository root.
104
+ *
105
+ * A framework builds its commands relative to its own working directory, which in a layered
106
+ * project is not where the report is read. `tewip review` printed
107
+ * `node --test "test/flaky.test.ts"` for a file at engine/test/flaky.test.ts, and copying it gave
108
+ * "Could not find 'test/flaky.test.ts'". The command is the one line in a review somebody acts on;
109
+ * it has to work where they paste it.
110
+ */
111
+ export function runFrom(root, cwd) {
112
+ const where = relative(root, cwd).split(sep).join('/');
113
+ return where && where !== '.' ? `cd ${where} && ` : '';
114
+ }
101
115
  export function renderReview(review, runCommand) {
102
116
  const lines = [`# Risk review: ${review.range}`, '', `${review.files.length} changed file(s).`, ''];
103
117
  if (review.notes.length > 0) {
@@ -51,7 +51,7 @@ export function traceReport(cmd) {
51
51
  const found = plans(cmd.root, cmd.only);
52
52
  const { runs, records } = history(cmd.root, cmd.name);
53
53
  const traced = found.map((plan) => tracePlan(plan, runs, records));
54
- return { traced, report: renderTrace(traced), plans: found.length, runs: runs.length };
54
+ return { traced, report: renderTrace(traced, runs.length), plans: found.length, runs: runs.length };
55
55
  }
56
56
  export function runTrace(cmd) {
57
57
  const { traced, report, plans: found, runs } = traceReport(cmd);
@@ -78,7 +78,16 @@ export function tracePlan(plan, runs, records) {
78
78
  }
79
79
  const MARK = { passing: 'passing', failing: 'FAILING', 'never-run': 'never run', 'not-covered': 'NOT COVERED' };
80
80
  /** The answer to "is this covered, and is it passing", written so it can be disagreed with. */
81
- export function renderTrace(traced) {
81
+ /**
82
+ * The report.
83
+ *
84
+ * `runsKept` is passed separately because the scenarios cannot carry it. With no run history at
85
+ * all, every scenario comes back `never run` - true of each one and misleading as a whole: it reads
86
+ * as a measurement of a suite nobody has exercised, when the truth is that nothing has been
87
+ * recorded here to measure. The footer made it worse, claiming the lines came from "a run it
88
+ * recorded" when there was no run.
89
+ */
90
+ export function renderTrace(traced, runsKept = 1) {
82
91
  const all = traced.flatMap((t) => t.scenarios);
83
92
  const count = (s) => all.filter((x) => x.standing === s).length;
84
93
  const lines = ['# From what was asked for, to what is passing', ''];
@@ -90,7 +99,13 @@ export function renderTrace(traced) {
90
99
  'writes one, and `tewip write` turns its scenarios into tests.',
91
100
  ].join('\n');
92
101
  }
93
- lines.push(`${all.length} scenario(s) across ${traced.length} requirement(s): ${count('passing')} passing, ${count('failing')} failing, ${count('never-run')} never run, ${count('not-covered')} not covered.`, '', 'A scenario counts as covered only when a real test can be named, and as passing only when a real', 'run attempted it. Everything else says what is missing instead.', '');
102
+ lines.push(`${all.length} scenario(s) across ${traced.length} requirement(s): ${count('passing')} passing, ${count('failing')} failing, ${count('never-run')} never run, ${count('not-covered')} not covered.`, '');
103
+ if (runsKept === 0) {
104
+ lines.push('**No run has been kept here, so nothing below says whether anything passes.** Every scenario', 'reads "never run" because there is no run to have attempted it - not because the tests were', 'written and then left alone. Run `tewip explain <report>` on a real run first, and this becomes', 'an answer rather than a list.', '');
105
+ }
106
+ else {
107
+ lines.push('A scenario counts as covered only when a real test can be named, and as passing only when a real', 'run attempted it. Everything else says what is missing instead.', '');
108
+ }
94
109
  for (const requirement of traced) {
95
110
  lines.push(`## ${requirement.requirement}${requirement.ticket ? ` (${requirement.ticket})` : ''}`, '');
96
111
  for (const s of requirement.scenarios) {
@@ -112,6 +127,8 @@ export function renderTrace(traced) {
112
127
  lines.push(`- ${s.standing === 'failing' ? `fix or triage ${s.test}` : `write a test for ${s.id}: ${s.title}`}`);
113
128
  lines.push('');
114
129
  }
115
- lines.push('Every line above comes from a plan Tewip wrote and a run it recorded. Nothing is inferred.');
130
+ lines.push(runsKept === 0
131
+ ? 'Every line above comes from a plan Tewip wrote. No run was recorded here, so nothing above was checked against one.'
132
+ : 'Every line above comes from a plan Tewip wrote and a run it recorded. Nothing is inferred.');
116
133
  return lines.join('\n');
117
134
  }
@@ -0,0 +1,12 @@
1
+ // What a verdict is made of: the questions, the standings, and everything `decide` is allowed to
2
+ // read. Split from verdict.ts when it passed this project's 300-line rule.
3
+ //
4
+ // Worth keeping together, because the shape of this file is the argument. Every field that can be
5
+ // absent is nullable or counted, and the comments say which absence means what - an empty list and
6
+ // a question nobody answered look identical otherwise, and four separate phases found the gate
7
+ // reading one as the other.
8
+ export {};
9
+ /**
10
+ * The verdict, in a fixed order: what is broken first, then what cannot be believed, then what is
11
+ * missing, and only then "ready".
12
+ */
@@ -0,0 +1,22 @@
1
+ export function renderDecision(range, decision) {
2
+ const mark = { met: 'yes', 'not-met': '**no**', 'not-applicable': 'n/a', 'not-checked': 'not checked' };
3
+ const title = decision.verdict === 'ready' ? 'Ready, as far as this can be shown' : decision.verdict === 'not-ready' ? 'Not ready' : 'Cannot say';
4
+ const lines = [
5
+ `# Is this work done? ${title}`,
6
+ '',
7
+ `For \`${range}\`. ${decision.headline}.`,
8
+ '',
9
+ '| Question | Answer |',
10
+ '| --- | --- |',
11
+ ...decision.checks.map((c) => `| ${c.question} | ${mark[c.standing]} |`),
12
+ '',
13
+ ];
14
+ for (const check of decision.checks) {
15
+ if (check.standing === 'not-applicable')
16
+ continue;
17
+ const said = `${mark[check.standing] === '**no**' ? 'No' : mark[check.standing] === 'yes' ? 'Yes' : 'Not checked'}: ${check.answer}`;
18
+ lines.push(`### ${check.question}`, '', /[.!?]$/.test(said) ? said : `${said}.`, '', `_Decided by ${check.decidedBy}._`, '');
19
+ }
20
+ lines.push('## What this says nothing about', '', ...decision.notChecked.map((n) => `- ${n}`), '', '---', '', 'Nothing above is judged here: each answer comes from a layer with its own measured accuracy, and', 'this only puts them in a fixed order - what is broken, then what cannot be believed, then what is', 'missing. "Cannot say" is a real answer and is never rounded up to ready.');
21
+ return lines.join(String.fromCharCode(10));
22
+ }
@@ -37,13 +37,23 @@ function history(root, only) {
37
37
  return { runs, records };
38
38
  }
39
39
  /** Scenarios of kept plans that this change relates to and nothing covers. Null when no plans exist. */
40
+ /**
41
+ * Scenarios this change touches that no test covers, and what stopped the question being answered.
42
+ *
43
+ * The gaps alone cannot carry it. An empty list means one of three different things: every scenario
44
+ * is covered, no plan here is about this change, or every plan failed to parse and was silently
45
+ * skipped - and the gate read all three as "every scenario this change relates to has a test".
46
+ * Measured on a project with one plan file containing the word "refunds" and no valid JSON.
47
+ */
40
48
  function uncoveredFor(root, changed, attempted) {
41
49
  const dir = join(root, '.tewip', 'plans');
42
50
  if (!existsSync(dir))
43
- return null;
51
+ return { gaps: null, unreadable: 0, considered: 0 };
44
52
  const words = new Set(changed.flatMap((f) => (f.toLowerCase().match(/[a-z][a-z0-9]{3,}/g) ?? [])));
45
53
  const clean = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '');
46
54
  const out = [];
55
+ let unreadable = 0;
56
+ let considered = 0;
47
57
  for (const file of readdirSync(dir).filter((f) => f.endsWith('.json'))) {
48
58
  try {
49
59
  const saved = JSON.parse(readFileSync(join(dir, file), 'utf8'));
@@ -55,6 +65,7 @@ function uncoveredFor(root, changed, attempted) {
55
65
  const about = (`${plan.title ?? ''} ${saved.feature ?? ''}`.toLowerCase().match(/[a-z][a-z0-9]{3,}/g) ?? []).some((w) => words.has(w));
56
66
  if (!about)
57
67
  continue;
68
+ considered += 1;
58
69
  for (const scenario of plan.scenarios) {
59
70
  const covered = (scenario.covered_by ?? '').trim() || attempted.find((t) => clean(t).includes(clean(scenario.id)) && clean(scenario.id).length >= 4);
60
71
  if (!covered)
@@ -62,10 +73,12 @@ function uncoveredFor(root, changed, attempted) {
62
73
  }
63
74
  }
64
75
  catch {
65
- // Not a readable plan.
76
+ // Skipped in silence until Phase 112: an unparseable plan left the gap list empty,
77
+ // which the gate read as full coverage.
78
+ unreadable += 1;
66
79
  }
67
80
  }
68
- return out;
81
+ return { gaps: out, unreadable, considered };
69
82
  }
70
83
  export function gather(cmd) {
71
84
  const change = changeSummary(cmd.root, cmd.diff);
@@ -112,6 +125,7 @@ export function gather(cmd) {
112
125
  })
113
126
  .map((r) => ({ testId: r.testId, category: String(r.diagnosis?.category ?? 'UNKNOWN') }));
114
127
  const standards = loadStandards(cmd.root);
128
+ const plans = uncoveredFor(cmd.root, changed, Object.keys(newest?.attempts ?? {}));
115
129
  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 }));
116
130
  const proved = cmd.prove ? proveChanged(cmd.root, changedSource, { budgetSeconds: cmd.proveBudgetSeconds ?? 300, onStep: (m) => console.error(` ${m}`) }) : null;
117
131
  return {
@@ -123,10 +137,15 @@ export function gather(cmd) {
123
137
  coverage,
124
138
  cannotFail: vouched.cannotFail.map((j) => ({ name: j.name, file: j.file, line: j.line, why: j.why })),
125
139
  crashOnly: vouched.crashOnly.length,
140
+ couldNotJudge: vouched.couldNotJudge.length,
141
+ proofsTried: proved ? proved.proofs.reduce((n, pr) => n + pr.tried.length, 0) : 0,
142
+ proofSkipped: proved?.skipped ?? '',
126
143
  unnoticed: proved ? proved.proofs.flatMap((p) => p.tried.filter((t) => t.fate === 'unnoticed').map((t) => ({ file: p.file, line: t.mutant.line, kind: t.mutant.kind }))) : null,
127
144
  failingNow,
128
145
  noHistory: runs.length === 0,
129
- uncovered: uncoveredFor(cmd.root, changed, Object.keys(newest?.attempts ?? {})),
146
+ uncovered: plans.gaps,
147
+ plansUnreadable: plans.unreadable,
148
+ plansConsidered: plans.considered,
130
149
  brokenRules,
131
150
  },
132
151
  };
@@ -1,25 +1,27 @@
1
1
  /** The questions that cannot be skipped on the way to "ready". */
2
- const ESSENTIAL = ['Is anything failing?', 'Is the changed code tested?'];
3
- /**
4
- * The verdict, in a fixed order: what is broken first, then what cannot be believed, then what is
5
- * missing, and only then "ready".
6
- */
2
+ const ESSENTIAL = ['Is everything passing?', 'Is the changed code tested?'];
7
3
  export function decide(evidence) {
8
4
  const checks = [];
9
5
  // 1. Is anything failing right now? Nothing else matters while it is.
6
+ //
7
+ // Asked as "Is everything passing?" so that a yes is good news, like every other row. Phrased
8
+ // the other way round, the table said **no** to "Is anything failing?" when a test really was
9
+ // failing - true of the standing, backwards as an answer, and the table is the part a reviewer
10
+ // scans before anything else. The headline said Not ready and named the test either way; the
11
+ // row beside it still read like reassurance.
10
12
  if (evidence.noHistory) {
11
- checks.push({ question: 'Is anything failing?', standing: 'not-checked', answer: 'no run has been kept for this project, so Tewip has not seen a test result here. Run `tewip explain <report>` in CI and it will know.', decidedBy: 'kept history' });
13
+ checks.push({ question: 'Is everything passing?', standing: 'not-checked', answer: 'no run has been kept for this project, so Tewip has not seen a test result here. Run `tewip explain <report>` in CI and it will know.', decidedBy: 'kept history' });
12
14
  }
13
15
  else if (evidence.failingNow.length > 0) {
14
16
  checks.push({
15
- question: 'Is anything failing?',
17
+ question: 'Is everything passing?',
16
18
  standing: 'not-met',
17
19
  answer: `${evidence.failingNow.length} test(s) touching this change failed in the last run: ${evidence.failingNow.slice(0, 3).map((f) => `${f.testId} (${f.category})`).join(', ')}`,
18
20
  decidedBy: 'the triage rules and model, measured on real CI data',
19
21
  });
20
22
  }
21
23
  else {
22
- checks.push({ question: 'Is anything failing?', standing: 'met', answer: 'nothing touching this change failed in the last kept run', decidedBy: 'kept history' });
24
+ checks.push({ question: 'Is everything passing?', standing: 'met', answer: 'nothing touching this change failed in the last kept run', decidedBy: 'kept history' });
23
25
  }
24
26
  // 2. Does anything test the change at all?
25
27
  if (evidence.changedSource.length === 0) {
@@ -96,6 +98,19 @@ export function decide(evidence) {
96
98
  decidedBy: 'tewip vouch, measured on 41,523 real tests in the six public projects in corpus/expected.json',
97
99
  });
98
100
  }
101
+ else if (evidence.couldNotJudge > 0 && evidence.couldNotJudge >= evidence.changedTests.length) {
102
+ // Nothing was established. `cannotFail` is empty because vouch declined every test in the
103
+ // change, not because it found them sound - three tests that call the function under test and
104
+ // assert nothing come back exactly like three good ones. Answering "yes" there is a clean
105
+ // answer produced by looking at nothing, in the question that matters most on a change somebody
106
+ // did not write by hand.
107
+ checks.push({
108
+ question: 'Can the tests in this change fail?',
109
+ standing: 'not-checked',
110
+ answer: `vouch would not judge any of the ${evidence.couldNotJudge} test(s) here: each body is one call to something it cannot follow, so whether they assert anything is unknown`,
111
+ decidedBy: 'tewip vouch',
112
+ });
113
+ }
99
114
  else {
100
115
  checks.push({
101
116
  question: 'Can the tests in this change fail?',
@@ -116,6 +131,19 @@ export function decide(evidence) {
116
131
  decidedBy: 'tewip prove, by running the tests against a broken copy of the code',
117
132
  });
118
133
  }
134
+ else if (evidence.proofsTried === 0) {
135
+ // Nothing was tried, so nothing was established. The reason is carried through rather than
136
+ // flattened into a shrug: "no unit test framework was found here" is a different thing for a
137
+ // reader to act on than "this costs minutes, ask for it".
138
+ checks.push({
139
+ question: 'Would the tests catch a bug in this code?',
140
+ standing: 'not-checked',
141
+ answer: evidence.proofSkipped
142
+ ? `nothing could be proved here: ${evidence.proofSkipped}`
143
+ : 'nothing was proved here, so whether the tests would catch a bug is unknown',
144
+ decidedBy: 'tewip prove',
145
+ });
146
+ }
119
147
  else {
120
148
  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' });
121
149
  }
@@ -147,6 +175,19 @@ export function decide(evidence) {
147
175
  decidedBy: 'tewip trace, which counts a scenario covered only when a real test can be named',
148
176
  });
149
177
  }
178
+ else if (evidence.plansUnreadable > 0 || evidence.plansConsidered === 0) {
179
+ // An empty gap list means one of three things and only one of them is good news. A plan that
180
+ // would not parse was skipped in silence, and a project whose plans are all about other
181
+ // features has not had this question answered at all.
182
+ checks.push({
183
+ question: 'Is what was asked for covered?',
184
+ standing: 'not-checked',
185
+ answer: evidence.plansUnreadable > 0
186
+ ? `${evidence.plansUnreadable} plan file(s) here could not be read, so their scenarios were never compared with this change`
187
+ : 'no plan this project keeps is about this change, so there was nothing to compare it with',
188
+ decidedBy: 'tewip trace',
189
+ });
190
+ }
150
191
  else {
151
192
  checks.push({ question: 'Is what was asked for covered?', standing: 'met', answer: 'every scenario this change relates to has a test that can be named', decidedBy: 'tewip trace' });
152
193
  }
@@ -179,25 +220,6 @@ export function decide(evidence) {
179
220
  ],
180
221
  };
181
222
  }
182
- export function renderDecision(range, decision) {
183
- const mark = { met: 'yes', 'not-met': '**no**', 'not-applicable': 'n/a', 'not-checked': 'not checked' };
184
- const title = decision.verdict === 'ready' ? 'Ready, as far as this can be shown' : decision.verdict === 'not-ready' ? 'Not ready' : 'Cannot say';
185
- const lines = [
186
- `# Is this work done? ${title}`,
187
- '',
188
- `For \`${range}\`. ${decision.headline}.`,
189
- '',
190
- '| Question | Answer |',
191
- '| --- | --- |',
192
- ...decision.checks.map((c) => `| ${c.question} | ${mark[c.standing]} |`),
193
- '',
194
- ];
195
- for (const check of decision.checks) {
196
- if (check.standing === 'not-applicable')
197
- continue;
198
- const said = `${mark[check.standing] === '**no**' ? 'No' : mark[check.standing] === 'yes' ? 'Yes' : 'Not checked'}: ${check.answer}`;
199
- lines.push(`### ${check.question}`, '', /[.!?]$/.test(said) ? said : `${said}.`, '', `_Decided by ${check.decidedBy}._`, '');
200
- }
201
- lines.push('## What this says nothing about', '', ...decision.notChecked.map((n) => `- ${n}`), '', '---', '', 'Nothing above is judged here: each answer comes from a layer with its own measured accuracy, and', 'this only puts them in a fixed order - what is broken, then what cannot be believed, then what is', 'missing. "Cannot say" is a real answer and is never rounded up to ready.');
202
- return lines.join(String.fromCharCode(10));
203
- }
223
+ // The report lives in render.ts; re-exported here so nothing that reads a verdict has to know
224
+ // where it is drawn.
225
+ export { renderDecision } from './render.js';
@@ -9,7 +9,7 @@ import { existsSync, readFileSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
10
  import { CANNOT_FAIL, vouchFile } from './vouch.js';
11
11
  export function vouchChanged(root, changed) {
12
- const out = { files: 0, read: 0, cannotFail: [], crashOnly: [] };
12
+ const out = { files: 0, read: 0, cannotFail: [], crashOnly: [], couldNotJudge: [] };
13
13
  for (const file of changed) {
14
14
  const full = join(root, file);
15
15
  if (!existsSync(full))
@@ -28,6 +28,7 @@ export function vouchChanged(root, changed) {
28
28
  out.read += vouched.judgements.length;
29
29
  out.cannotFail.push(...vouched.judgements.filter((j) => CANNOT_FAIL.includes(j.verdict)));
30
30
  out.crashOnly.push(...vouched.judgements.filter((j) => j.verdict === 'crash-only'));
31
+ out.couldNotJudge.push(...vouched.judgements.filter((j) => j.verdict === 'cannot-tell'));
31
32
  }
32
33
  return out;
33
34
  }
@@ -37,9 +37,22 @@ export function withoutComments(body) {
37
37
  }
38
38
  if (ch === '/' && next === '*') {
39
39
  const close = body.indexOf('*/', i + 2);
40
- // An unclosed block comment runs to the end, which is what a compiler would do too.
40
+ // Only when it closes. An unclosed one was treated as running to the end of the body, on the
41
+ // reasoning that a compiler would do the same - but a compiler is reading a language that has
42
+ // block comments, and this is handed every language. Ruby has none, and discourse writes
43
+ //
44
+ // %r{https://matrix.org/_matrix/client/r0/rooms/!blah/send/m.room.message/*}
45
+ //
46
+ // where the `/*` is the end of a regex literal. Running to the end of the body deleted the
47
+ // three assertions below it and reported two thoroughly checked tests as crash-only. A file
48
+ // that really ends inside a block comment does not compile; a regex containing `/*` is
49
+ // ordinary. The old regex required the closing `*/` and was right to.
50
+ if (close === -1) {
51
+ out += ch;
52
+ continue;
53
+ }
41
54
  out += ' ';
42
- i = close === -1 ? body.length : close + 1;
55
+ i = close + 1;
43
56
  continue;
44
57
  }
45
58
  const atLineStart = i === 0 || /\s/.test(body[i - 1] ?? '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tewip-ai",
3
- "version": "0.70.16",
3
+ "version": "0.70.17",
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": [