tewip-ai 0.69.7 → 0.69.9

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/dist/cli/help.js CHANGED
@@ -152,7 +152,9 @@ Usage:
152
152
  report and it reads that too - Lighthouse runs axe rules, so
153
153
  either works. Contrast moves out of the unchecked list, rules
154
154
  that map to no WCAG criterion are reported as advice rather than
155
- barriers (51 of 174 findings on nine real pages), and what the
155
+ barriers (51 of 174 findings on nine real pages run through axe;
156
+ a Lighthouse report runs a WCAG-aligned subset and produced 2 of
157
+ 363 on six real pages, so expect little advice from one), and what the
156
158
  tool could not determine becomes a question. Lighthouse's ten
157
159
  manual checks are questions too, never passes. No model, no cost.
158
160
  tewip standards [--diff main...HEAD] [--strict] [--out standards.md]
@@ -12,6 +12,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
12
  import { normalise } from '../quality/source-text.js';
13
13
  import { dirname, join, relative, resolve } from 'node:path';
14
14
  import { sourceFiles } from '../secure/secrets.js';
15
+ import { readHandWritten } from '../../reporters/store.js';
15
16
  import { JS_LIKE, MARKUP, tags } from './markup.js';
16
17
  import { checkFile } from './checks.js';
17
18
  import { readAxeReport } from './reports.js';
@@ -27,7 +28,7 @@ export function readAccepted(root) {
27
28
  const file = join(root, '.tewip', 'access-accepted.txt');
28
29
  if (!existsSync(file))
29
30
  return new Set();
30
- return new Set(readFileSync(file, 'utf8')
31
+ return new Set(readHandWritten(file)
31
32
  .split('\n')
32
33
  .map((l) => l.split(' - ')[0].trim())
33
34
  .filter((l) => l !== '' && !l.startsWith('#')));
@@ -61,17 +61,36 @@ export function renderAccess(result, scanned) {
61
61
  }
62
62
  else {
63
63
  const stopping = barriers.filter((f) => blocks(f.rule)).length;
64
- const counts = [
65
- `${barriers.length} ${barriers.length === 1 ? 'barrier' : 'barriers'} (${stopping} that stop somebody outright)`,
66
- `${questions.length} for a person to settle`,
67
- ];
68
- if (accepted.length > 0)
69
- counts.push(`${accepted.length} already accepted`);
64
+ const counts = [];
70
65
  if (browser) {
71
- counts.unshift(`${browser.barriers.length} barriers the browser run proves`);
72
- if (browser.questions.length > 0)
73
- counts.push(`${browser.questions.length} the browser could not determine`);
66
+ // How many **rules**, then how many places. On Hacker News the browser count was "244
67
+ // barriers", of which **238 were one rule** - colour contrast - on one page. The number was
68
+ // arithmetically right and told the reader nothing they could act on: five things are wrong
69
+ // here, one of them in 238 places. A count of occurrences presented as a count of problems
70
+ // makes a page look hopeless and hides which five things to fix.
71
+ const rules = new Set(browser.barriers.map((f) => f.rule)).size;
72
+ counts.push(browser.barriers.length === 0
73
+ ? 'no barrier in the browser run'
74
+ : `${rules} ${rules === 1 ? 'barrier' : 'barriers'} the browser run proves, in ${browser.barriers.length} ${browser.barriers.length === 1 ? 'place' : 'places'}`);
75
+ }
76
+ // And this count is from reading markup, which the sentence never said. Beside a browser count
77
+ // also called "barriers", a reader saw "244 barriers..., 0 barriers" and could not tell whether
78
+ // the second number corrected the first. With no markup read at all there is no count to give:
79
+ // zero files examined is not a finding of zero.
80
+ if (filesRead > 0) {
81
+ counts.push(`${barriers.length} ${barriers.length === 1 ? 'barrier' : 'barriers'} in the markup (${stopping} that stop somebody outright)`);
82
+ counts.push(`${questions.length} for a person to settle`);
83
+ }
84
+ else if (!browser) {
85
+ counts.push('no markup file was read, so nothing was checked');
74
86
  }
87
+ else if (questions.length > 0) {
88
+ counts.push(`${questions.length} for a person to settle`);
89
+ }
90
+ if (accepted.length > 0)
91
+ counts.push(`${accepted.length} already accepted`);
92
+ if (browser && browser.questions.length > 0)
93
+ counts.push(`${browser.questions.length} the browser could not determine`);
75
94
  lines.push(`**${counts.join(', ')}.**`, '');
76
95
  }
77
96
  if (browser) {
@@ -82,8 +101,15 @@ export function renderAccess(result, scanned) {
82
101
  section('Only a person can settle these', [...questions].sort(worstFirst), lines);
83
102
  if (browser && browser.advice.length > 0) {
84
103
  // Kept out of the counts above on purpose. These are axe's own best-practice rules, which test
85
- // no standard: on nine real pages they were 51 of 174 findings, outnumbering every real barrier.
86
- // Reporting them as equals is how a team learns to ignore the whole report.
104
+ // no standard: on nine real pages **run through axe** they were 51 of 174 findings, outnumbering
105
+ // every real barrier. Reporting them as equals is how a team learns to ignore the whole report.
106
+ //
107
+ // That 51-of-174 is an axe number and only an axe number. Measured again on six real pages
108
+ // through **Lighthouse** - gov.uk, MDN, Hacker News, python.org, w3.org/WAI, Wikipedia - advice
109
+ // was 2 of 363 findings, 0.6%, because Lighthouse's accessibility category runs a WCAG-aligned
110
+ // subset of axe and leaves nearly all the best-practice rules out. Both numbers are real; a
111
+ // published one without the condition it was measured under is the kind of claim this project
112
+ // exists to stop making.
87
113
  const rules = [...new Set(browser.advice.map((f) => f.rule))].sort();
88
114
  lines.push('## Advice, not barriers', '');
89
115
  lines.push(`${browser.advice.length} ${browser.advice.length === 1 ? 'finding comes' : 'findings come'} from axe rules that map to no WCAG criterion - ` +
@@ -15,12 +15,13 @@
15
15
  import { existsSync, readFileSync } from 'node:fs';
16
16
  import { join } from 'node:path';
17
17
  import { spawnSync } from 'node:child_process';
18
+ import { readHandWritten } from '../../reporters/store.js';
18
19
  /** Findings a person has already judged, one per line: "<kind> <where> - reason". */
19
20
  export function readAccepted(root) {
20
21
  const file = join(root, '.tewip', 'security-accepted.txt');
21
22
  if (!existsSync(file))
22
23
  return new Set();
23
- return new Set(readFileSync(file, 'utf8')
24
+ return new Set(readHandWritten(file)
24
25
  .split('\n')
25
26
  .map((l) => l.split(' - ')[0].trim())
26
27
  .filter(Boolean));
@@ -78,11 +78,21 @@ export function renderStandards(standards, violations, checkedFiles) {
78
78
  ].join(String.fromCharCode(10));
79
79
  }
80
80
  const checkable = standards.filter((s) => s.rules.length > 0);
81
- const guidanceOnly = standards.filter((s) => s.rules.length === 0 && s.guidance.trim());
81
+ // Every standard that carries prose, not only the ones carrying nothing else.
82
+ //
83
+ // This was `s.rules.length === 0 && s.guidance.trim()`, so a standard written in the shape the
84
+ // feature is documented with - front matter rules, then prose below them - had its prose
85
+ // **invisible in this report**. Found by writing this project's own house rules in exactly that
86
+ // shape: four rules were listed as enforced and three paragraphs of guidance were never mentioned,
87
+ // under a closing line promising "everything else is prose Tewip carries for you". The other
88
+ // renderer, `renderViolations`, gets this right but only prints when something is broken - so a
89
+ // team doing it properly and keeping to its own rules is never told its guidance was read at all.
90
+ const carrying = standards.filter((s) => s.guidance.trim().length > 0);
91
+ const guidanceOnly = carrying.filter((s) => s.rules.length === 0);
82
92
  const lines = [
83
93
  '# What this project has taught Tewip',
84
94
  '',
85
- `${standards.length} standard(s) loaded: ${checkable.length} with rules Tewip can check, ${guidanceOnly.length} guidance only.`,
95
+ `${standards.length} standard(s) loaded: ${checkable.length} with rules Tewip can check, ${carrying.length} carrying guidance it cannot${guidanceOnly.length > 0 ? ` (${guidanceOnly.length} guidance only)` : ''}.`,
86
96
  ...(checkedFiles > 0 ? ['', `${checkedFiles} changed file(s) checked. **${violations.length}** broke a rule.`] : []),
87
97
  '',
88
98
  '## Checked and enforced',
@@ -97,8 +107,12 @@ export function renderStandards(standards, violations, checkedFiles) {
97
107
  }
98
108
  }
99
109
  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}` : ''}`), '');
110
+ if (carrying.length > 0) {
111
+ 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.', '', ...carrying.map((s) => {
112
+ const words = s.guidance.trim().split(/\s+/).length;
113
+ const also = s.rules.length > 0 ? ` (alongside its ${s.rules.length} enforced rule${s.rules.length === 1 ? '' : 's'})` : '';
114
+ return `- **${s.name}**${s.description ? ` — ${s.description}` : ''}: ${words} word(s) of guidance${also}`;
115
+ }), '');
102
116
  }
103
117
  const unusable = standards.filter((s) => s.unusable);
104
118
  if (unusable.length > 0) {
@@ -6,8 +6,16 @@
6
6
  * unusable rather than silently ignored - a rule nobody is enforcing is worse than no rule, because
7
7
  * somebody believes in it.
8
8
  */
9
- export function readStandard(file, text) {
9
+ export function readStandard(file, rawText) {
10
10
  const name = file.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? file;
11
+ // A leading byte-order mark, which Notepad, Visual Studio and PowerShell's `Set-Content -Encoding
12
+ // utf8` all write by default on Windows. Three invisible bytes before `---` stop the front matter
13
+ // matching, so **every enforced rule in the file silently becomes prose** and the report says
14
+ // "0 with rules Tewip can check" without once mentioning why. Found by accident: this project's own
15
+ // standards file was rewritten by a PowerShell command and four working rules stopped being
16
+ // enforced with no error anywhere. For a team on Windows that is total, silent rule loss - the
17
+ // exact thing `unusable` exists to prevent, happening in the one place it could not report.
18
+ const text = rawText.replace(/^/, '');
11
19
  const front = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
12
20
  const body = front ? text.slice(front[0].length).trim() : text.trim();
13
21
  const standard = { name, description: '', appliesTo: [], rules: [], guidance: body, unusable: '' };
@@ -40,7 +48,12 @@ export function readStandard(file, text) {
40
48
  standard.description = firstLine(body);
41
49
  return standard;
42
50
  }
43
- /** `forbid: /waitForTimeout/ in tests because a fixed wait hides a race` */
51
+ /**
52
+ * `forbid: /waitForTimeout/ in tests because a fixed wait hides a race`
53
+ *
54
+ * The `in` clause may carry an exception: `in engine/ not -command.ts`. Everything before `not` is
55
+ * where the rule applies, everything after it is where it does not.
56
+ */
44
57
  function parseRule(kind, value, scope) {
45
58
  const shape = /^\/(.+?)\/([a-z]*)\s*(?:\bin\s+([^]*?))?\s*(?:\bbecause\s+([^]*))?$/i.exec(value.trim());
46
59
  if (!shape)
@@ -51,10 +64,12 @@ function parseRule(kind, value, scope) {
51
64
  catch {
52
65
  return null;
53
66
  }
67
+ const [inside, outside] = (shape[3] ?? '').split(/\bnot\b/);
54
68
  return {
55
69
  kind,
56
70
  pattern: shape[2] ? `/${shape[1]}/${shape[2]}` : shape[1],
57
- where: shape[3] ? list(shape[3]) : scope,
71
+ where: inside && inside.trim() ? list(inside) : scope,
72
+ except: outside ? list(outside) : [],
58
73
  because: (shape[4] ?? '').trim(),
59
74
  };
60
75
  }
@@ -92,7 +107,11 @@ export function check(standards, file, text) {
92
107
  if (!covers(standard.appliesTo, file))
93
108
  continue;
94
109
  for (const rule of standard.rules) {
95
- if (!covers(rule.where, file))
110
+ // `covers([])` is deliberately true - an empty scope means everything - so an exception has to
111
+ // be checked for existence first. Written the short way, `|| covers(rule.except, file)`, every
112
+ // rule with no exception is skipped and the report says nothing broke: a clean answer produced
113
+ // by a bug, which is the exact shape this project keeps finding.
114
+ if (!covers(rule.where, file) || (rule.except.length > 0 && covers(rule.except, file)))
96
115
  continue;
97
116
  let pattern;
98
117
  try {
@@ -7,6 +7,19 @@ export const DATA_DIR = join(PROJECT_ROOT, 'data');
7
7
  const COLLECT_DIR = process.env.TRIAGE_DATA_DIR ? resolve(PROJECT_ROOT, process.env.TRIAGE_DATA_DIR) : DATA_DIR;
8
8
  export const TRIAGE_FILE = join(COLLECT_DIR, 'triage.jsonl');
9
9
  export const RUNS_FILE = join(COLLECT_DIR, 'runs.jsonl');
10
+ /**
11
+ * A file a person wrote, read as text, without the byte-order mark Windows editors put in front.
12
+ *
13
+ * Notepad, Visual Studio and PowerShell's `Set-Content -Encoding utf8` all write three invisible
14
+ * bytes before the first character. Nothing downstream expects them: a standards file stops being
15
+ * enforced because `/^---/` no longer matches, and the first line of an accepted-findings list stops
16
+ * matching the finding it was written to accept - so a judged finding comes back and keeps coming
17
+ * back, with nothing anywhere saying why. Every file this project reads **because a person wrote it
18
+ * by hand** goes through here.
19
+ */
20
+ export function readHandWritten(file) {
21
+ return readFileSync(file, 'utf8').replace(/^\uFEFF/, '');
22
+ }
10
23
  export function appendJsonl(file, rows) {
11
24
  if (rows.length === 0)
12
25
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tewip-ai",
3
- "version": "0.69.7",
3
+ "version": "0.69.9",
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": [