verikun 0.4.1
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/LICENSE +21 -0
- package/README.md +435 -0
- package/dist/agent/cache.js +128 -0
- package/dist/agent/claude.js +144 -0
- package/dist/agent/cost.js +100 -0
- package/dist/agent/engine.js +205 -0
- package/dist/agent/grammar.js +80 -0
- package/dist/agent/ir.js +212 -0
- package/dist/agent/provider.js +2 -0
- package/dist/args.js +102 -0
- package/dist/bin/verikun.js +8 -0
- package/dist/cli.js +1298 -0
- package/dist/drivers/adb.js +300 -0
- package/dist/drivers/index.js +13 -0
- package/dist/drivers/simctl.js +156 -0
- package/dist/errors.js +51 -0
- package/dist/exec.js +42 -0
- package/dist/image.js +212 -0
- package/dist/output.js +43 -0
- package/dist/report.js +223 -0
- package/dist/run.js +434 -0
- package/dist/types.js +5 -0
- package/dist/ui/android-parse.js +149 -0
- package/dist/ui/format.js +71 -0
- package/dist/ui/selector.js +117 -0
- package/dist/version.js +6 -0
- package/package.json +53 -0
package/dist/agent/ir.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The plan IR: the deterministic, replayable program the LLM compiles a natural-
|
|
3
|
+
// language test down to. The LLM is a COMPILER here, not a runtime — once an IR
|
|
4
|
+
// exists it is re-run with zero model calls on the happy path. The model is woken
|
|
5
|
+
// only to repair a step that genuinely fails (see ../agent/engine.ts).
|
|
6
|
+
//
|
|
7
|
+
// Shape rules (load-bearing, decided in plan-eng-review):
|
|
8
|
+
// - Nodes are a UNIFORM typed union — a leaf command, or a control node. (Mixing
|
|
9
|
+
// command-strings with structured control nodes would be the ugly split we
|
|
10
|
+
// avoided.) A leaf carries the verikun command triple {command, positionals,
|
|
11
|
+
// flags} directly, so the engine feeds it straight to executeOutcome.
|
|
12
|
+
// - SHALLOW: control-node bodies hold LEAF steps only — no loop-inside-loop / if-
|
|
13
|
+
// inside-loop in v1. That keeps the JSON schema NON-RECURSIVE, which is what lets
|
|
14
|
+
// Anthropic structured output (output_config.format) guarantee a valid IR.
|
|
15
|
+
// (A real flow that needs deeper nesting is the trigger to revisit — bounded-depth
|
|
16
|
+
// schema or prompt-and-parse; see the design doc's validation gate.)
|
|
17
|
+
// - flags are an array of {name,value} pairs (string values), NOT an open map —
|
|
18
|
+
// structured-output schemas can't express an arbitrary-key object. A pure boolean
|
|
19
|
+
// flag is value:"true" (flagBool reads 'true' as true; flagStr reads the string).
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.InvalidPlanError = exports.REPAIR_DECISION_JSON_SCHEMA = exports.PLAN_JSON_SCHEMA = exports.DEFAULT_LOOP_CAP = exports.KNOWN_COMMANDS = void 0;
|
|
22
|
+
exports.validateNode = validateNode;
|
|
23
|
+
exports.parsePlan = parsePlan;
|
|
24
|
+
exports.leafToFlags = leafToFlags;
|
|
25
|
+
/** Commands a leaf step is allowed to carry — the agent-emittable ACTION/assertion verbs
|
|
26
|
+
* the grammar offers, a SUBSET of cli.ts's dispatch (inspection/diagnostic commands are
|
|
27
|
+
* excluded; see the note in the set). The engine validates every step — including a
|
|
28
|
+
* model-generated REPAIR — against this set before executing it, so a hallucinated
|
|
29
|
+
* command is rejected instead of hitting executeCommand's `default` (exit 2 + abort). */
|
|
30
|
+
exports.KNOWN_COMMANDS = new Set([
|
|
31
|
+
'tap', 'click',
|
|
32
|
+
'text', 'type',
|
|
33
|
+
'key', 'back', 'home', 'enter',
|
|
34
|
+
'swipe', 'scroll',
|
|
35
|
+
'screenshot', 'shot',
|
|
36
|
+
'wait', 'assert',
|
|
37
|
+
'launch', 'open', 'stop', 'clear',
|
|
38
|
+
// Inspection/diagnostic commands (`current`, `ui`, `find`, `log`, `logs`) are
|
|
39
|
+
// deliberately NOT here: they are not test actions (the grammar never offers them), so
|
|
40
|
+
// a plan or repair must never emit them — and `log`'s flags reach a device shell
|
|
41
|
+
// (`--since`) and a host write (`--out`). Restricting the allowlist to the grammar's
|
|
42
|
+
// action set also preserves a load-bearing invariant: every command that can raise a
|
|
43
|
+
// heal trigger (a selector miss/ambiguity) is RECORDABLE, so markLastStepHealed ("heal
|
|
44
|
+
// the last recorded step") always targets the step that actually failed — a
|
|
45
|
+
// non-recordable selector-resolver here (e.g. `find`) would corrupt an unrelated step.
|
|
46
|
+
]);
|
|
47
|
+
exports.DEFAULT_LOOP_CAP = 25;
|
|
48
|
+
/**
|
|
49
|
+
* JSON Schema for `output_config.format` so the model returns a guaranteed-valid
|
|
50
|
+
* Plan. Deliberately NON-RECURSIVE: control-node `body` arrays reference only the
|
|
51
|
+
* leaf schema, so there is no `Plan -> node -> Plan` cycle (structured output
|
|
52
|
+
* rejects recursive schemas). One nesting level, by design.
|
|
53
|
+
*/
|
|
54
|
+
exports.PLAN_JSON_SCHEMA = {
|
|
55
|
+
type: 'object',
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
required: ['version', 'steps'],
|
|
58
|
+
properties: {
|
|
59
|
+
version: { type: 'integer', enum: [1] },
|
|
60
|
+
package: { type: 'string' },
|
|
61
|
+
platform: { type: 'string', enum: ['android', 'ios'] },
|
|
62
|
+
steps: {
|
|
63
|
+
type: 'array',
|
|
64
|
+
items: {
|
|
65
|
+
anyOf: [
|
|
66
|
+
leafSchema(),
|
|
67
|
+
{
|
|
68
|
+
type: 'object',
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
required: ['type', 'selector', 'body'],
|
|
71
|
+
properties: {
|
|
72
|
+
type: { type: 'string', enum: ['if-present'] },
|
|
73
|
+
selector: { type: 'string' },
|
|
74
|
+
body: { type: 'array', items: leafSchema() },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: 'object',
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
required: ['type', 'selector', 'cap', 'body'],
|
|
81
|
+
properties: {
|
|
82
|
+
type: { type: 'string', enum: ['repeat'] },
|
|
83
|
+
selector: { type: 'string' },
|
|
84
|
+
cap: { type: 'integer' },
|
|
85
|
+
body: { type: 'array', items: leafSchema() },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Schema for a repair RESPONSE. A repair is a DECISION, not a forced substitution:
|
|
95
|
+
* - "repair" + `step`: the screen has an element serving the failed step's intent.
|
|
96
|
+
* - "give_up" + `reason`: it does NOT (the flow drifted to an unexpected screen),
|
|
97
|
+
* so the test must FAIL rather than tap a loosely-related element and pass falsely.
|
|
98
|
+
* Non-recursive (the `step` is a flat leaf), so structured output can guarantee it.
|
|
99
|
+
*/
|
|
100
|
+
exports.REPAIR_DECISION_JSON_SCHEMA = {
|
|
101
|
+
type: 'object',
|
|
102
|
+
additionalProperties: false,
|
|
103
|
+
required: ['decision'],
|
|
104
|
+
properties: {
|
|
105
|
+
decision: { type: 'string', enum: ['repair', 'give_up'] },
|
|
106
|
+
step: leafSchema(),
|
|
107
|
+
reason: { type: 'string' },
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
function leafSchema() {
|
|
111
|
+
return {
|
|
112
|
+
type: 'object',
|
|
113
|
+
additionalProperties: false,
|
|
114
|
+
required: ['type', 'command', 'positionals', 'flags'],
|
|
115
|
+
properties: {
|
|
116
|
+
type: { type: 'string', enum: ['command'] },
|
|
117
|
+
command: { type: 'string' },
|
|
118
|
+
positionals: { type: 'array', items: { type: 'string' } },
|
|
119
|
+
flags: {
|
|
120
|
+
type: 'array',
|
|
121
|
+
items: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
additionalProperties: false,
|
|
124
|
+
required: ['name', 'value'],
|
|
125
|
+
properties: { name: { type: 'string' }, value: { type: 'string' } },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Raised when a model-produced plan (or repair) is structurally invalid. Kept
|
|
132
|
+
* out of errors.ts because it is an agent-layer concern, not a CLI exit code. */
|
|
133
|
+
class InvalidPlanError extends Error {
|
|
134
|
+
constructor(message) {
|
|
135
|
+
super(message);
|
|
136
|
+
this.name = 'InvalidPlanError';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
exports.InvalidPlanError = InvalidPlanError;
|
|
140
|
+
function isFlagSpecArray(v) {
|
|
141
|
+
return (Array.isArray(v) &&
|
|
142
|
+
v.every((f) => f && typeof f === 'object' && typeof f.name === 'string' && typeof f.value === 'string'));
|
|
143
|
+
}
|
|
144
|
+
/** Validate a single node (used for both compile output and a spliced repair). */
|
|
145
|
+
function validateNode(node, where) {
|
|
146
|
+
if (!node || typeof node !== 'object')
|
|
147
|
+
throw new InvalidPlanError(`${where}: not an object`);
|
|
148
|
+
const n = node;
|
|
149
|
+
switch (n.type) {
|
|
150
|
+
case 'command': {
|
|
151
|
+
if (typeof n.command !== 'string' || !exports.KNOWN_COMMANDS.has(n.command)) {
|
|
152
|
+
throw new InvalidPlanError(`${where}: unknown command ${JSON.stringify(n.command)}`);
|
|
153
|
+
}
|
|
154
|
+
if (!Array.isArray(n.positionals) || !n.positionals.every((p) => typeof p === 'string')) {
|
|
155
|
+
throw new InvalidPlanError(`${where}: positionals must be a string[]`);
|
|
156
|
+
}
|
|
157
|
+
if (!isFlagSpecArray(n.flags))
|
|
158
|
+
throw new InvalidPlanError(`${where}: flags must be {name,value}[]`);
|
|
159
|
+
return { type: 'command', command: n.command, positionals: n.positionals, flags: n.flags };
|
|
160
|
+
}
|
|
161
|
+
case 'if-present':
|
|
162
|
+
case 'repeat': {
|
|
163
|
+
if (typeof n.selector !== 'string' || !n.selector)
|
|
164
|
+
throw new InvalidPlanError(`${where}: ${n.type} needs a selector`);
|
|
165
|
+
if (!Array.isArray(n.body))
|
|
166
|
+
throw new InvalidPlanError(`${where}: ${n.type} body must be an array`);
|
|
167
|
+
const body = n.body.map((b, i) => {
|
|
168
|
+
const leaf = validateNode(b, `${where}.body[${i}]`);
|
|
169
|
+
if (leaf.type !== 'command')
|
|
170
|
+
throw new InvalidPlanError(`${where}.body[${i}]: only leaf commands allowed (no nesting in v1)`);
|
|
171
|
+
return leaf;
|
|
172
|
+
});
|
|
173
|
+
if (n.type === 'if-present')
|
|
174
|
+
return { type: 'if-present', selector: n.selector, body };
|
|
175
|
+
const cap = typeof n.cap === 'number' && n.cap > 0 ? Math.floor(n.cap) : exports.DEFAULT_LOOP_CAP;
|
|
176
|
+
return { type: 'repeat', selector: n.selector, cap, body };
|
|
177
|
+
}
|
|
178
|
+
default:
|
|
179
|
+
throw new InvalidPlanError(`${where}: unknown node type ${JSON.stringify(n.type)}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Validate + normalize a parsed plan object (belt-and-suspenders even with
|
|
183
|
+
* structured output, and the parse path when structured output is unavailable). */
|
|
184
|
+
function parsePlan(raw) {
|
|
185
|
+
if (!raw || typeof raw !== 'object')
|
|
186
|
+
throw new InvalidPlanError('plan: not an object');
|
|
187
|
+
const p = raw;
|
|
188
|
+
if (p.version !== 1)
|
|
189
|
+
throw new InvalidPlanError(`plan: unsupported version ${JSON.stringify(p.version)} (expected 1)`);
|
|
190
|
+
if (!Array.isArray(p.steps))
|
|
191
|
+
throw new InvalidPlanError('plan: steps must be an array');
|
|
192
|
+
const steps = p.steps.map((s, i) => validateNode(s, `steps[${i}]`));
|
|
193
|
+
// A plan with zero steps would "pass" green having done nothing — reject it so a
|
|
194
|
+
// compiler misfire, prompt injection, or truncated cache entry can't be a false green.
|
|
195
|
+
if (steps.length === 0)
|
|
196
|
+
throw new InvalidPlanError('plan: has no steps (a test must have at least one step)');
|
|
197
|
+
return {
|
|
198
|
+
version: 1,
|
|
199
|
+
package: typeof p.package === 'string' ? p.package : undefined,
|
|
200
|
+
platform: typeof p.platform === 'string' ? p.platform : undefined,
|
|
201
|
+
steps,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/** Convert a leaf's {name,value}[] flags into the args-parser Flags record that
|
|
205
|
+
* executeOutcome consumes. A boolean flag is carried as value "true" (flagBool
|
|
206
|
+
* reads 'true' as true); a valued flag keeps its string. */
|
|
207
|
+
function leafToFlags(step) {
|
|
208
|
+
const out = {};
|
|
209
|
+
for (const { name, value } of step.flags)
|
|
210
|
+
out[name] = value;
|
|
211
|
+
return out;
|
|
212
|
+
}
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseArgs = parseArgs;
|
|
4
|
+
exports.flagStr = flagStr;
|
|
5
|
+
exports.flagBool = flagBool;
|
|
6
|
+
exports.flagNum = flagNum;
|
|
7
|
+
const errors_1 = require("./errors");
|
|
8
|
+
const ALIASES = {
|
|
9
|
+
d: 'device',
|
|
10
|
+
i: 'index',
|
|
11
|
+
o: 'out',
|
|
12
|
+
p: 'platform',
|
|
13
|
+
c: 'contains',
|
|
14
|
+
j: 'json',
|
|
15
|
+
q: 'quiet',
|
|
16
|
+
h: 'help',
|
|
17
|
+
v: 'version',
|
|
18
|
+
t: 'timeout',
|
|
19
|
+
w: 'wait',
|
|
20
|
+
n: 'lines',
|
|
21
|
+
};
|
|
22
|
+
// Flags that never consume a following value.
|
|
23
|
+
const BOOLEAN = new Set([
|
|
24
|
+
'json',
|
|
25
|
+
'contains',
|
|
26
|
+
'all',
|
|
27
|
+
'gone',
|
|
28
|
+
'enter',
|
|
29
|
+
'clear',
|
|
30
|
+
'tree',
|
|
31
|
+
'help',
|
|
32
|
+
'quiet',
|
|
33
|
+
'version',
|
|
34
|
+
'ios',
|
|
35
|
+
'android',
|
|
36
|
+
'fix',
|
|
37
|
+
'no-wait',
|
|
38
|
+
'full',
|
|
39
|
+
'more',
|
|
40
|
+
'show-plan',
|
|
41
|
+
'recompile',
|
|
42
|
+
'no-cache',
|
|
43
|
+
'no-restart',
|
|
44
|
+
]);
|
|
45
|
+
function parseArgs(argv) {
|
|
46
|
+
const positionals = [];
|
|
47
|
+
const flags = {};
|
|
48
|
+
for (let i = 0; i < argv.length; i++) {
|
|
49
|
+
const tok = argv[i];
|
|
50
|
+
if (tok === '--') {
|
|
51
|
+
positionals.push(...argv.slice(i + 1));
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if (tok.startsWith('-') && tok !== '-') {
|
|
55
|
+
let name = tok.replace(/^-+/, '');
|
|
56
|
+
let inlineValue;
|
|
57
|
+
const eq = name.indexOf('=');
|
|
58
|
+
if (eq >= 0) {
|
|
59
|
+
inlineValue = name.slice(eq + 1);
|
|
60
|
+
name = name.slice(0, eq);
|
|
61
|
+
}
|
|
62
|
+
name = ALIASES[name] ?? name;
|
|
63
|
+
if (inlineValue !== undefined) {
|
|
64
|
+
flags[name] = inlineValue;
|
|
65
|
+
}
|
|
66
|
+
else if (BOOLEAN.has(name)) {
|
|
67
|
+
flags[name] = true;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const next = argv[i + 1];
|
|
71
|
+
if (next !== undefined && !(next.startsWith('-') && next !== '-')) {
|
|
72
|
+
flags[name] = next;
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
flags[name] = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
positionals.push(tok);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const command = positionals.shift();
|
|
85
|
+
return { command, positionals, flags };
|
|
86
|
+
}
|
|
87
|
+
function flagStr(flags, name) {
|
|
88
|
+
const v = flags[name];
|
|
89
|
+
return typeof v === 'string' ? v : undefined;
|
|
90
|
+
}
|
|
91
|
+
function flagBool(flags, name) {
|
|
92
|
+
return flags[name] === true || flags[name] === 'true';
|
|
93
|
+
}
|
|
94
|
+
function flagNum(flags, name) {
|
|
95
|
+
const v = flagStr(flags, name);
|
|
96
|
+
if (v === undefined)
|
|
97
|
+
return undefined;
|
|
98
|
+
const n = Number(v);
|
|
99
|
+
if (!Number.isFinite(n))
|
|
100
|
+
throw new errors_1.CliError(`--${name} must be a number, got '${v}'`, 2);
|
|
101
|
+
return n;
|
|
102
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const cli_1 = require("../cli");
|
|
5
|
+
(0, cli_1.run)(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
|
|
6
|
+
process.stderr.write('Fatal: ' + (e instanceof Error ? e.message : String(e)) + '\n');
|
|
7
|
+
process.exit(3);
|
|
8
|
+
});
|