tewip-ai 0.70.6 → 0.70.8

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.
@@ -62,19 +62,52 @@ ${noModelHelp()}`);
62
62
  const fileName = framework.fileFor(scenario.id);
63
63
  const file = join(framework.testDir, fileName);
64
64
  if (existsSync(file) || (cmd.moveTo && existsSync(join(cmd.moveTo, fileName)))) {
65
- results.push({ id: scenario.id, title: scenario.title, file, outcome: { kind: 'rejected', reason: `${fileName} already exists; left untouched`, repairs: 0, passedReliably: false } });
65
+ // Every file this command writes carries "tewip" in its name, so one already sitting here is
66
+ // one of its own - an unverified draft from a run that did not finish, not a test the team
67
+ // wrote. Calling that "left untouched" sounds like a decision and hides that the file is in
68
+ // the test folder, where the suite is running it.
69
+ //
70
+ // Matched case-insensitively on the whole name rather than as a prefix, because only the
71
+ // JavaScript frameworks produce `tewip-ls-002.spec.ts`: pytest gives `test_tewip_ls_002.py`
72
+ // and JUnit `TewipLs002Test.java`, and a prefix check would have handed Python and Java teams
73
+ // the misleading message this fix exists to remove.
74
+ const mine = /tewip/i.test(fileName);
75
+ results.push({
76
+ id: scenario.id,
77
+ title: scenario.title,
78
+ file,
79
+ outcome: {
80
+ kind: 'rejected',
81
+ reason: mine
82
+ ? `${fileName} is already here from a run that did not finish, and was never verified. It is in your test folder, so your suite is running it: delete it, or move it to .tewip/drafts/, then ask again.`
83
+ : `${fileName} already exists and was written by somebody else; left untouched`,
84
+ repairs: 0,
85
+ passedReliably: false,
86
+ },
87
+ });
66
88
  continue;
67
89
  }
68
90
  console.error(` ${scenario.id} ${scenario.title}`);
69
- // Stopped part-way (Ctrl+C), a draft that was never verified must not stay in the project.
91
+ // A draft that was never verified must not stay in the project, however the run ended.
92
+ //
93
+ // SIGINT and SIGTERM were covered; nothing else was. A run of this interrupted another way -
94
+ // a closed pipe, an uncaught throw, a kill - left the draft in the **test folder**, where the
95
+ // project's own suite then runs it. Seen for real: a two-scenario run was cut short by a broken
96
+ // shell command, the suite went from green to red, and the next `write` reported the leftover as
97
+ // "already exists; left untouched", which reads like a decision rather than a casualty.
98
+ //
99
+ // `renameSync` is synchronous, so an `exit` handler can still do it: that covers every way out.
100
+ const rescue = () => {
101
+ if (!existsSync(file))
102
+ return;
103
+ mkdirSync(drafts, { recursive: true });
104
+ renameSync(file, join(drafts, fileName));
105
+ };
70
106
  const leaveNothing = () => {
71
- if (existsSync(file)) {
72
- mkdirSync(drafts, { recursive: true });
73
- renameSync(file, join(drafts, fileName));
74
- }
107
+ rescue();
75
108
  process.exit(130);
76
109
  };
77
- process.once('SIGINT', leaveNothing).once('SIGTERM', leaveNothing);
110
+ process.once('SIGINT', leaveNothing).once('SIGTERM', leaveNothing).once('exit', rescue);
78
111
  let outcome;
79
112
  try {
80
113
  ({ 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) }));
@@ -83,7 +116,7 @@ ${noModelHelp()}`);
83
116
  outcome = { kind: 'rejected', reason: `stopped: ${error.message}`, repairs: 0, passedReliably: false };
84
117
  }
85
118
  finally {
86
- process.off('SIGINT', leaveNothing).off('SIGTERM', leaveNothing);
119
+ process.off('SIGINT', leaveNothing).off('SIGTERM', leaveNothing).off('exit', rescue);
87
120
  }
88
121
  // Some runners take minutes per run (Angular rebuilds the app for every `ng test`): say how long
89
122
  // the rest will take, so a slow run is not mistaken for a stuck one.
@@ -1,14 +1,14 @@
1
1
  export const SPEC_PROMPT_VERSION = 'spec-1';
2
- const SYSTEM = `You are Tewip's specification writer, a senior product engineer who works from the code.
3
-
4
- Write an epic, its user stories, and acceptance criteria for the feature described. Use only the facts given: the feature description, the change, the pages of the running app, the tests that already exist, the source files read, and this project's failure history.
5
-
6
- Rules:
7
- - Every statement about what the system ALREADY does must be backed by a quote: evidence is a file path from the facts, then ": ", then a piece of that file copied character for character (a line of code, a test name, a comment). If you cannot quote it, do not claim it - put the uncertainty in open_questions.
8
- - Acceptance criteria are Given / When / Then, each one checkable by a person or a test. No criterion may say "works correctly", "is fast" or "is user-friendly": say the observable thing.
9
- - A story is one user-visible outcome, written as "As a <role>, I want <outcome> so that <benefit>". Split anything larger.
10
- - Prefer what the failure history and the existing tests show is risky or already broken.
11
- - Never write estimates, story points, dates, budgets, market claims or business cases. You cannot check those, so they do not belong here.
2
+ const SYSTEM = `You are Tewip's specification writer, a senior product engineer who works from the code.
3
+
4
+ Write an epic, its user stories, and acceptance criteria for the feature described. Use only the facts given: the feature description, the change, the pages of the running app, the tests that already exist, the source files read, and this project's failure history.
5
+
6
+ Rules:
7
+ - Every statement about what the system ALREADY does must be backed by a quote: evidence is a file path from the facts, then ": ", then a piece of that file copied character for character (a line of code, a test name, a comment). If you cannot quote it, do not claim it - put the uncertainty in open_questions.
8
+ - Acceptance criteria are Given / When / Then, each one checkable by a person or a test. No criterion may say "works correctly", "is fast" or "is user-friendly": say the observable thing.
9
+ - A story is one user-visible outcome, written as "As a <role>, I want <outcome> so that <benefit>". Split anything larger.
10
+ - Prefer what the failure history and the existing tests show is risky or already broken.
11
+ - Never write estimates, story points, dates, budgets, market claims or business cases. You cannot check those, so they do not belong here.
12
12
  - out_of_scope is what this epic deliberately does not do. open_questions are the decisions a person must make.`;
13
13
  const CRITERION = {
14
14
  type: 'object',
@@ -64,9 +64,29 @@ function readable(facts) {
64
64
  return [...(facts.testSources ?? []), ...(facts.codeSources ?? [])].map((s) => ({ file: s.file, content: s.content }));
65
65
  }
66
66
  /**
67
- * A quote is real when the file is one Tewip read and the distinctive parts of the quote are in it.
68
- * Whitespace and quote style differ between a model's copy and the file, so the comparison is on
69
- * the words and symbols, not on the exact spacing.
67
+ * A quote is real when the file is one Tewip read and the quote appears in it **as one piece**.
68
+ *
69
+ * This used to collect the quote's "distinctive parts" - runs of `[A-Za-z0-9_$.]{3,}` - and require
70
+ * each to appear somewhere in the file, in any order. Three fabrications got through a real check
71
+ * against a real file, and the worst was this, offered as evidence that a 20% discount cap is
72
+ * enforced:
73
+ *
74
+ * const rate = Math.min(percent, 20) / 100; // the file says 100, not 20
75
+ *
76
+ * Every part of that - `const`, `rate`, `Math.min`, `percent`, `100` - is in the file. **`20` is
77
+ * not a part at all**, because `{3,}` cannot see a two-character number. So the one token that
78
+ * carried the entire meaning was the one token not checked, and a reader of "What the code already
79
+ * does - each line is quoted from a file Tewip read" would have concluded the bug was fixed.
80
+ *
81
+ * The comment that used to sit here saw the risk and named it exactly - *"a quote that is mostly
82
+ * real but wrong where it matters... 'slice(0, 50)' and 'slice(0, 10000)' differ by one word"* -
83
+ * and `50` was never a word it could compare. A line assembled from real words in an order the file
84
+ * has never contained got through for the same reason.
85
+ *
86
+ * So the quote has to be in the file contiguously. Whitespace is squashed and quote characters are
87
+ * normalised, because those are the two differences between a model's copy and the file that carry
88
+ * no meaning; nothing else is forgiven. A claim that fails this becomes an open question, which is
89
+ * the right place for it.
70
90
  */
71
91
  export function quoteHolds(evidence, sources) {
72
92
  const at = evidence.indexOf(': ');
@@ -77,13 +97,8 @@ export function quoteHolds(evidence, sources) {
77
97
  const source = sources.find((s) => s.file === file || s.file.endsWith(`/${file}`) || file.endsWith(s.file));
78
98
  if (!source || quote.length < 3)
79
99
  return false;
80
- const parts = quote.match(/[A-Za-z0-9_$.]{3,}/g) ?? [];
81
- if (parts.length === 0)
82
- return false;
83
- const body = source.content;
84
- // Every distinctive part must be in the file. Anything less lets a quote that is mostly real but
85
- // wrong where it matters through - "slice(0, 50)" and "slice(0, 10000)" differ by one word.
86
- return parts.every((part) => body.includes(part));
100
+ const squash = (text) => text.replace(/["'`]/g, '"').replace(/\s+/g, ' ').trim();
101
+ return squash(source.content).includes(squash(quote));
87
102
  }
88
103
  /**
89
104
  * Checks every claim about what already exists, and demotes the ones Tewip cannot quote into open
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tewip-ai",
3
- "version": "0.70.6",
3
+ "version": "0.70.8",
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": [