tewip-ai 0.13.0 → 0.14.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 +16 -0
- package/TEAM-GUIDE.md +1 -0
- package/dist/cli/tewip.js +19 -0
- package/dist/engine/design/design-command.js +17 -2
- package/dist/engine/flaky/flaky-command.js +9 -1
- package/dist/engine/learn/correct-command.js +88 -0
- package/dist/engine/learn/verdicts.js +71 -0
- package/dist/engine/secure/secure-command.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -181,6 +181,22 @@ Each reader was checked against real reports published in public repositories, w
|
|
|
181
181
|
defects were found before release - a Robot timestamp that is not a date, k6's numbers living under
|
|
182
182
|
`values`, and a JMeter line that read "mostly 200" when an assertion had failed.
|
|
183
183
|
|
|
184
|
+
## Tell it when it is wrong (new)
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
tewip correct diagnosis "checkout charges twice" TEST_DATA "the fixture has stale prices"
|
|
188
|
+
tewip correct flaky "adds an item" fix-now "it is a real race, not flakiness"
|
|
189
|
+
tewip correct security "src/config.ts:12" noise "that is a sample key in a fixture"
|
|
190
|
+
tewip correct --list
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Tewip answers with **your** decision from then on, and says it was yours: *"TEST_DATA, confidence
|
|
194
|
+
1.00, source: corrected by a person"*. Corrections live in your project, so they belong to the team;
|
|
195
|
+
the newest one wins, so changing your mind is just saying so again.
|
|
196
|
+
|
|
197
|
+
**A correction changes what Tewip says, never what it does on its own** — it will not change code,
|
|
198
|
+
quarantine a test or file a ticket because you corrected it.
|
|
199
|
+
|
|
184
200
|
## Build a feature, test first (new)
|
|
185
201
|
|
|
186
202
|
```bash
|
package/TEAM-GUIDE.md
CHANGED
|
@@ -127,6 +127,7 @@ collects environment failures in one issue, and (optionally) opens a fix pull re
|
|
|
127
127
|
| `tewip spec "<feature>"` | An epic, its user stories and acceptance criteria, grounded in your code; files them in Jira with `--project PROJ --create` | Before the work is broken down |
|
|
128
128
|
| `tewip report report.xml --gate` | The CI command for GitLab, Jenkins, CircleCI, Azure, Bitbucket and the rest: diagnoses the run, comments on the GitLab merge request, and fails the build only on failures that need a person | Your CI is not GitHub Actions |
|
|
129
129
|
| `tewip watch --cmd "npm test"` | Runs your tests when you save a file and explains each failure straight away | While you are writing code |
|
|
130
|
+
| `tewip correct <kind> "<what>" <verdict> "why"` | Tell Tewip it was wrong about anything it decides; it answers with your decision from then on | Whenever you disagree with it |
|
|
130
131
|
| `tewip build "<what it should do>"` | Writes a feature test first, then the code, and keeps it only if the test failed without it and passes with it | You want a small feature built and proved |
|
|
131
132
|
| `tewip review --diff main...HEAD` | Which tests cover your change, which have flaked before, what nothing tests, and what to run first | Before you open a pull request |
|
|
132
133
|
| `tewip flaky --days 30` | Which tests to quarantine, which to fix, what they cost in CI minutes - and when a quarantined one can come back | Monday morning |
|
package/dist/cli/tewip.js
CHANGED
|
@@ -102,6 +102,11 @@ Usage:
|
|
|
102
102
|
Compare a Figma frame against the running page: the words the
|
|
103
103
|
design specifies and the controls its layer names imply. Says
|
|
104
104
|
"the design says X, the page says Y". No model, no cost.
|
|
105
|
+
tewip correct <kind> "<what>" <verdict> ["why"] | --list
|
|
106
|
+
Tell Tewip it was wrong, about anything it decides: a diagnosis,
|
|
107
|
+
a flaky verdict, a security finding, a design difference, a
|
|
108
|
+
refused build. It answers with your decision from then on, and
|
|
109
|
+
says it was yours. It never acts on a correction by itself.
|
|
105
110
|
tewip mcp Serve Tewip over MCP (stdio) for an editor or AI assistant.
|
|
106
111
|
tewip --version Print the version.
|
|
107
112
|
|
|
@@ -397,6 +402,20 @@ ${proposed.status === 'opened' ? 'Opened a draft pull request.' : proposed.statu
|
|
|
397
402
|
// it in CI without the build going red over a design decision nobody has made yet.
|
|
398
403
|
return;
|
|
399
404
|
}
|
|
405
|
+
case 'correct': {
|
|
406
|
+
const { runCorrect } = await import('../engine/learn/correct-command.js');
|
|
407
|
+
const words = args.filter((a) => !a.startsWith('--'));
|
|
408
|
+
console.log(runCorrect({
|
|
409
|
+
root: process.cwd(),
|
|
410
|
+
kind: words[0] ?? '',
|
|
411
|
+
key: words[1],
|
|
412
|
+
verdict: words[2],
|
|
413
|
+
reason: words.slice(3).join(' ') || undefined,
|
|
414
|
+
list: args.includes('--list'),
|
|
415
|
+
by: flag(args, 'by'),
|
|
416
|
+
}));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
400
419
|
case 'mcp':
|
|
401
420
|
// An editor starts this from any folder, and an installed tool's own folder may be read-only:
|
|
402
421
|
// the AI answer cache goes to the user's home folder, which is always writable.
|
|
@@ -8,6 +8,7 @@ import { readPages } from '../quality/grounding.js';
|
|
|
8
8
|
import { Figma, figmaHelp, figmaTokenFromEnv, parseFigmaUrl, topFrames } from './figma.js';
|
|
9
9
|
import { frameChecks } from './frame.js';
|
|
10
10
|
import { compareDesign, renderDrift } from './drift.js';
|
|
11
|
+
import { loadVerdicts, saidByAPerson, verdictFor } from '../learn/verdicts.js';
|
|
11
12
|
/** A saved Figma response, in any of the shapes Figma's API returns. */
|
|
12
13
|
function fromFile(path) {
|
|
13
14
|
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
@@ -70,8 +71,22 @@ export async function runDesign(cmd) {
|
|
|
70
71
|
console.log(`\nThe page was not read, so nothing was compared: ${pageNote ?? 'unknown reason'}`);
|
|
71
72
|
return [];
|
|
72
73
|
}
|
|
73
|
-
const
|
|
74
|
-
|
|
74
|
+
const found = compareDesign(checks, pages[0].aria);
|
|
75
|
+
// A difference a person has already looked at and accepted is not reported again as a surprise.
|
|
76
|
+
const corrections = loadVerdicts(cmd.root);
|
|
77
|
+
const settled = [];
|
|
78
|
+
const drifts = found.map((d) => {
|
|
79
|
+
const key = `${d.check.role ?? d.check.kind} ${d.check.name}`.trim();
|
|
80
|
+
const said = verdictFor('design', key, corrections) ?? verdictFor('design', d.check.name, corrections);
|
|
81
|
+
if (!said || said.verdict !== 'not-a-difference')
|
|
82
|
+
return d;
|
|
83
|
+
settled.push(`${key}: ${saidByAPerson(said)}`);
|
|
84
|
+
return { ...d, verdict: 'met' };
|
|
85
|
+
});
|
|
86
|
+
const alreadySettled = settled.length
|
|
87
|
+
? ['', `${settled.length} difference(s) your team has already settled, so they are not reported:`, ...settled.map((s) => ` - ${s}`)].join('\n')
|
|
88
|
+
: '';
|
|
89
|
+
const report = [renderDrift(drifts, design, cmd.url), alreadySettled].filter(Boolean).join('\n');
|
|
75
90
|
console.log(report);
|
|
76
91
|
if (note)
|
|
77
92
|
console.log(`\n${note}`);
|
|
@@ -6,6 +6,7 @@ import { join, resolve } from 'node:path';
|
|
|
6
6
|
import { datasetFiles, readRunSummaries, readTriageRecords } from '../../reporters/store.js';
|
|
7
7
|
import { flakyTests, readyToRelease, weeklyCost } from './flaky.js';
|
|
8
8
|
import { applyQuarantine, frameworkOf, quarantineFor } from './quarantine.js';
|
|
9
|
+
import { loadVerdicts, saidByAPerson, verdictFor } from '../learn/verdicts.js';
|
|
9
10
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
10
11
|
const minutes = (ms) => `${(ms / 60_000).toFixed(1)} CI min`;
|
|
11
12
|
function projects(root, only) {
|
|
@@ -31,7 +32,14 @@ export async function runFlaky(cmd) {
|
|
|
31
32
|
const history = datasetFiles(join(cmd.root, '.tewip', 'history', name));
|
|
32
33
|
const records = readTriageRecords(history.triage);
|
|
33
34
|
const runs = readRunSummaries(history.runs);
|
|
34
|
-
const
|
|
35
|
+
const computed = flakyTests(records, runs, { since });
|
|
36
|
+
// What a person already decided about a test wins over what Tewip works out, and is shown as
|
|
37
|
+
// theirs: the argument is had once, not every Monday.
|
|
38
|
+
const corrections = loadVerdicts(cmd.root);
|
|
39
|
+
const tests = computed.map((test) => {
|
|
40
|
+
const said = verdictFor('flaky', test.title, corrections) ?? verdictFor('flaky', test.testId, corrections);
|
|
41
|
+
return said ? { ...test, verdict: said.verdict, why: `${saidByAPerson(said)} (Tewip had said ${test.verdict})` } : test;
|
|
42
|
+
});
|
|
35
43
|
if (tests.length === 0)
|
|
36
44
|
continue;
|
|
37
45
|
all.push(...tests);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// `tewip correct`: the one place a person tells Tewip it was wrong, about anything.
|
|
2
|
+
//
|
|
3
|
+
// A diagnosis can already be corrected where Tewip posts it in CI. This is the same thing for
|
|
4
|
+
// everything else it decides, and on a person's own machine, where most of the disagreeing happens.
|
|
5
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { ROOT_CAUSES } from '../../reporters/types.js';
|
|
8
|
+
import { datasetFiles, readTriageRecords } from '../../reporters/store.js';
|
|
9
|
+
import { loadCorrections, saveCorrections } from '../corrections.js';
|
|
10
|
+
import { errorShape } from '../signature.js';
|
|
11
|
+
import { isVerdictWord, loadVerdicts, saidByAPerson, saveVerdict, verdictHelp, VERDICT_WORDS } from './verdicts.js';
|
|
12
|
+
const KINDS = Object.keys(VERDICT_WORDS);
|
|
13
|
+
/**
|
|
14
|
+
* The failure a title names, and the history folder it lives in. The folder matters: corrections are
|
|
15
|
+
* kept beside the runs of that project, which is where the engine looks for them - an earlier
|
|
16
|
+
* version wrote them one directory up, where nothing ever read them.
|
|
17
|
+
*/
|
|
18
|
+
function findRecord(root, key) {
|
|
19
|
+
const dir = join(root, '.tewip', 'history');
|
|
20
|
+
if (!existsSync(dir))
|
|
21
|
+
return null;
|
|
22
|
+
for (const project of readdirSync(dir)) {
|
|
23
|
+
const historyDir = join(dir, project);
|
|
24
|
+
const records = readTriageRecords(datasetFiles(historyDir).triage);
|
|
25
|
+
const found = [...records].reverse().find((r) => r.testId.toLowerCase().includes(key.trim().toLowerCase()));
|
|
26
|
+
if (found)
|
|
27
|
+
return { record: found, historyDir };
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
export function runCorrect(cmd) {
|
|
32
|
+
const who = cmd.by ?? process.env.USER ?? process.env.USERNAME ?? 'someone on the team';
|
|
33
|
+
if (cmd.list) {
|
|
34
|
+
const verdicts = loadVerdicts(cmd.root);
|
|
35
|
+
const historyRoot = join(cmd.root, '.tewip', 'history');
|
|
36
|
+
const diagnoses = (existsSync(historyRoot) ? readdirSync(historyRoot) : []).flatMap((project) => loadCorrections(join(historyRoot, project)));
|
|
37
|
+
if (verdicts.length === 0 && diagnoses.length === 0)
|
|
38
|
+
return 'Nothing has been corrected yet.\n\n' + verdictHelp();
|
|
39
|
+
const lines = [`${verdicts.length + diagnoses.length} correction(s), newest last:`, ''];
|
|
40
|
+
for (const v of verdicts)
|
|
41
|
+
lines.push(` ${v.kind.padEnd(9)} ${v.key} -> ${v.verdict} (${saidByAPerson(v)})`);
|
|
42
|
+
for (const d of diagnoses)
|
|
43
|
+
lines.push(` diagnosis ${d.testId} -> ${d.category} (${d.by}${d.reason ? `: ${d.reason}` : ''})`);
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|
|
46
|
+
const kind = cmd.kind?.trim().toLowerCase() ?? '';
|
|
47
|
+
if (!cmd.key || !cmd.verdict)
|
|
48
|
+
throw new Error(`usage: tewip correct <${[...KINDS, 'diagnosis'].join('|')}> "<what it is about>" <verdict> ["why"]\n\n${verdictHelp()}`);
|
|
49
|
+
// A diagnosis is corrected into the same store the CI path writes, so both ends agree.
|
|
50
|
+
if (kind === 'diagnosis') {
|
|
51
|
+
const category = cmd.verdict.trim().toUpperCase();
|
|
52
|
+
if (!ROOT_CAUSES.includes(category))
|
|
53
|
+
throw new Error(`"${cmd.verdict}" is not a category. They are: ${ROOT_CAUSES.join(', ')}`);
|
|
54
|
+
const found = findRecord(cmd.root, cmd.key);
|
|
55
|
+
if (!found)
|
|
56
|
+
throw new Error(`no failure of a test called "${cmd.key}" is in the history Tewip has kept here. Run \`tewip explain\` on the report first, so there is something to correct.`);
|
|
57
|
+
const { record, historyDir } = found;
|
|
58
|
+
const correction = {
|
|
59
|
+
schemaVersion: 1,
|
|
60
|
+
testId: record.testId,
|
|
61
|
+
// Stored exactly as the matcher compares it: a truncated shape can never equal a real one.
|
|
62
|
+
errorShape: errorShape(record.errorMessage),
|
|
63
|
+
category,
|
|
64
|
+
by: who,
|
|
65
|
+
at: new Date().toISOString(),
|
|
66
|
+
reason: cmd.reason,
|
|
67
|
+
source: 'tewip correct',
|
|
68
|
+
wasCategory: record.diagnosis?.category,
|
|
69
|
+
};
|
|
70
|
+
saveCorrections(historyDir, [correction]);
|
|
71
|
+
return [
|
|
72
|
+
`Noted: "${record.testId}" is ${category}${record.diagnosis?.category ? `, not ${record.diagnosis.category}` : ''}.`,
|
|
73
|
+
'Tewip will answer with your decision the next time that test fails the same way, and say it was yours.',
|
|
74
|
+
].join('\n');
|
|
75
|
+
}
|
|
76
|
+
if (!KINDS.includes(kind))
|
|
77
|
+
throw new Error(`"${cmd.kind}" is not something Tewip decides. It decides: ${[...KINDS, 'diagnosis'].join(', ')}.\n\n${verdictHelp()}`);
|
|
78
|
+
const verdictKind = kind;
|
|
79
|
+
if (!isVerdictWord(verdictKind, cmd.verdict)) {
|
|
80
|
+
throw new Error(`"${cmd.verdict}" is not one of the answers a ${verdictKind} verdict takes. They are: ${VERDICT_WORDS[verdictKind].join(', ')}.`);
|
|
81
|
+
}
|
|
82
|
+
const saved = saveVerdict(cmd.root, { kind: verdictKind, key: cmd.key, verdict: cmd.verdict.trim().toLowerCase(), reason: cmd.reason ?? '', by: who });
|
|
83
|
+
return [
|
|
84
|
+
`Noted: ${verdictKind} "${cmd.key}" is ${saved.verdict}.`,
|
|
85
|
+
'Tewip will say that from now on, and say it was your decision rather than its own.',
|
|
86
|
+
'It will not act on it by itself: a correction changes what it says, never what it does.',
|
|
87
|
+
].join('\n');
|
|
88
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// The LEARN step, everywhere.
|
|
2
|
+
//
|
|
3
|
+
// A person could already correct a diagnosis by replying under Tewip's own comment in CI, and Tewip
|
|
4
|
+
// answers with their decision from then on. Every other verdict it gives - this test should be
|
|
5
|
+
// quarantined, this secret is a real risk, the page has drifted from the design, this feature
|
|
6
|
+
// cannot be built here - had no such path, so the same argument had to be had again every week.
|
|
7
|
+
//
|
|
8
|
+
// This is that path, for all of them, and it keeps the properties that made the first one worth
|
|
9
|
+
// having: a correction is written by a person, it is stored with the project rather than inside
|
|
10
|
+
// Tewip, it says who and why, and **it never authorises an action on its own**. Deciding that a
|
|
11
|
+
// failure is a real bug does not make Tewip change code; deciding a flaky test is really broken
|
|
12
|
+
// does not make it un-quarantine anything. It changes what Tewip *says*, which is what a person
|
|
13
|
+
// was disagreeing with.
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { appendJsonl, readJsonl } from '../../reporters/store.js';
|
|
17
|
+
/** The words each kind accepts, so a typo is caught rather than stored and silently ignored. */
|
|
18
|
+
export const VERDICT_WORDS = {
|
|
19
|
+
flaky: ['quarantine', 'fix-now', 'watch', 'release'],
|
|
20
|
+
security: ['real-risk', 'not-reachable', 'noise', 'accepted'],
|
|
21
|
+
design: ['real', 'not-a-difference'],
|
|
22
|
+
build: ['belongs-here', 'not-this-project'],
|
|
23
|
+
review: ['covers', 'does-not-cover'],
|
|
24
|
+
};
|
|
25
|
+
export const VERDICTS_FILE = 'verdicts.jsonl';
|
|
26
|
+
const fileIn = (root) => join(root, '.tewip', VERDICTS_FILE);
|
|
27
|
+
export function loadVerdicts(root) {
|
|
28
|
+
const path = fileIn(root);
|
|
29
|
+
return existsSync(path) ? readJsonl(path) : [];
|
|
30
|
+
}
|
|
31
|
+
/** Adds one correction, replacing an earlier one about the same thing. */
|
|
32
|
+
export function saveVerdict(root, correction) {
|
|
33
|
+
const entry = { schemaVersion: 1, at: correction.at ?? new Date().toISOString(), ...correction };
|
|
34
|
+
appendJsonl(fileIn(root), [entry]);
|
|
35
|
+
return entry;
|
|
36
|
+
}
|
|
37
|
+
const same = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
38
|
+
/**
|
|
39
|
+
* The person's answer about this thing, or null. The newest wins, so a team that changes its mind
|
|
40
|
+
* simply says so again rather than editing a file.
|
|
41
|
+
*/
|
|
42
|
+
export function verdictFor(kind, key, corrections) {
|
|
43
|
+
const matching = corrections.filter((c) => c.kind === kind && (same(c.key, key) || key.toLowerCase().endsWith(c.key.trim().toLowerCase())));
|
|
44
|
+
return matching.length > 0 ? matching[matching.length - 1] : null;
|
|
45
|
+
}
|
|
46
|
+
/** What a corrected verdict is shown as, so nobody mistakes a person's decision for Tewip's. */
|
|
47
|
+
export function saidByAPerson(correction) {
|
|
48
|
+
const when = correction.at.slice(0, 10);
|
|
49
|
+
return `${correction.by} decided this on ${when}${correction.reason ? `: ${correction.reason}` : ''}`;
|
|
50
|
+
}
|
|
51
|
+
/** Whether a word is one this kind of verdict uses. */
|
|
52
|
+
export function isVerdictWord(kind, word) {
|
|
53
|
+
return VERDICT_WORDS[kind].includes(word.trim().toLowerCase());
|
|
54
|
+
}
|
|
55
|
+
export function verdictHelp() {
|
|
56
|
+
return [
|
|
57
|
+
'Correct something Tewip said, and it will answer with your decision from now on:',
|
|
58
|
+
'',
|
|
59
|
+
' tewip correct flaky "adds an item to the cart" fix-now "it is a real race, not flakiness"',
|
|
60
|
+
' tewip correct security "src/config.ts:12" noise "that is a sample key in a fixture"',
|
|
61
|
+
' tewip correct design "button Sign in" not-a-difference "we renamed it on purpose"',
|
|
62
|
+
' tewip correct build "an HTTP sink" belongs-here "this really is the right repository"',
|
|
63
|
+
'',
|
|
64
|
+
'The words each kind takes:',
|
|
65
|
+
...Object.entries(VERDICT_WORDS).map(([kind, words]) => ` ${kind.padEnd(9)} ${words.join(', ')}`),
|
|
66
|
+
'',
|
|
67
|
+
'A correction changes what Tewip says, never what it does on its own: it will not change code,',
|
|
68
|
+
'quarantine a test or file a ticket because you corrected it.',
|
|
69
|
+
'They are kept in .tewip/verdicts.jsonl, with the project, so the whole team shares them.',
|
|
70
|
+
].join('\n');
|
|
71
|
+
}
|
|
@@ -9,6 +9,7 @@ import { auditDependencies } from './deps.js';
|
|
|
9
9
|
import { scanSecrets, sourceFiles } from './secrets.js';
|
|
10
10
|
import { readAccepted, triageDependency, triageSecret, triageWeb, worstFirst } from './triage.js';
|
|
11
11
|
import { isLocal, scanWeb } from './web.js';
|
|
12
|
+
import { loadVerdicts, saidByAPerson, verdictFor } from '../learn/verdicts.js';
|
|
12
13
|
import { readZapReport, zapAvailable, zapInstructions } from './zap.js';
|
|
13
14
|
const CODE = /\.(?:[cm]?[jt]sx?|py|rb|go|java|kt|cs|php|rs|vue|svelte)$/;
|
|
14
15
|
export function renderSecurity(items, checked, notChecked, root) {
|
|
@@ -97,6 +98,14 @@ export async function runSecure(cmd) {
|
|
|
97
98
|
else if (cmd.url) {
|
|
98
99
|
notChecked.push(`a full web scan. Tewip's own check is passive; for the deep one: ${zapInstructions(cmd.url, zapAvailable(root))}`);
|
|
99
100
|
}
|
|
101
|
+
// A person's decision about a finding wins over Tewip's, and says whose it was.
|
|
102
|
+
const corrections = loadVerdicts(root);
|
|
103
|
+
const decided = items.map((item) => {
|
|
104
|
+
const said = verdictFor('security', item.where, corrections) ?? verdictFor('security', item.title, corrections);
|
|
105
|
+
return said ? { ...item, verdict: said.verdict, why: `${saidByAPerson(said)} (Tewip had said ${item.verdict})` } : item;
|
|
106
|
+
});
|
|
107
|
+
items.length = 0;
|
|
108
|
+
items.push(...decided);
|
|
100
109
|
items.sort(worstFirst);
|
|
101
110
|
const report = renderSecurity(items, checked, notChecked, root);
|
|
102
111
|
console.log(report);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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",
|