explorbot 0.4.7 → 0.4.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/boat/api-tester/src/ai/chief.ts +3 -1
- package/boat/api-tester/src/ai/curler.ts +4 -0
- package/boat/api-tester/src/cli.ts +1 -1
- package/boat/api-tester/src/config.ts +34 -26
- package/dist/boat/api-tester/src/ai/chief.js +3 -1
- package/dist/boat/api-tester/src/ai/curler.js +4 -0
- package/dist/boat/api-tester/src/cli.js +1 -1
- package/dist/boat/api-tester/src/config.js +32 -26
- package/dist/package.json +1 -1
- package/dist/rules/chief/general.md +2 -0
- package/dist/rules/researcher/pagination.md +1 -0
- package/dist/src/action.js +3 -2
- package/dist/src/ai/fisherman/tools.js +10 -4
- package/dist/src/ai/navigator.js +1 -4
- package/dist/src/ai/pilot.js +19 -18
- package/dist/src/ai/planner/session-dedup.d.ts +2 -1
- package/dist/src/ai/planner/session-dedup.js +18 -1
- package/dist/src/ai/planner.js +8 -4
- package/dist/src/ai/provider.js +18 -4
- package/dist/src/ai/rules.js +1 -0
- package/dist/src/ai/tester.js +1 -1
- package/dist/src/ai/tools.js +10 -2
- package/dist/src/commands/config-command.d.ts +2 -0
- package/dist/src/commands/config-command.js +8 -2
- package/dist/src/commands/init-command.js +6 -26
- package/dist/src/commands/sites-command.js +6 -1
- package/dist/src/config.d.ts +5 -4
- package/dist/src/config.js +25 -23
- package/dist/src/global-config.d.ts +6 -0
- package/dist/src/global-config.js +82 -7
- package/dist/src/utils/code-extractor.js +6 -2
- package/dist/src/utils/html.js +6 -0
- package/dist/src/utils/merge.d.ts +1 -0
- package/dist/src/utils/merge.js +11 -0
- package/docs/reference/commands.md +10 -2
- package/docs/reference/configuration.md +37 -4
- package/docs/superpowers/plans/2026-09-15-mdq-package.md +2029 -0
- package/docs/superpowers/specs/2026-09-14-mdq-package-design.md +397 -0
- package/package.json +1 -1
- package/rules/chief/general.md +2 -0
- package/rules/researcher/pagination.md +1 -0
- package/src/action.ts +3 -2
- package/src/ai/fisherman/tools.ts +10 -4
- package/src/ai/navigator.ts +1 -4
- package/src/ai/pilot.ts +19 -18
- package/src/ai/planner/session-dedup.ts +16 -2
- package/src/ai/planner.ts +8 -4
- package/src/ai/provider.ts +18 -3
- package/src/ai/rules.ts +1 -0
- package/src/ai/tester.ts +1 -1
- package/src/ai/tools.ts +9 -2
- package/src/commands/config-command.ts +9 -2
- package/src/commands/init-command.ts +6 -27
- package/src/commands/sites-command.ts +6 -1
- package/src/config.ts +28 -25
- package/src/global-config.ts +81 -6
- package/src/utils/code-extractor.ts +6 -2
- package/src/utils/html.ts +6 -0
- package/src/utils/merge.ts +13 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# mdq — Markdown Query & Edit Package
|
|
2
|
+
|
|
3
|
+
Date: 2026-09-14
|
|
4
|
+
Status: Approved design, pending implementation
|
|
5
|
+
|
|
6
|
+
## Goal
|
|
7
|
+
|
|
8
|
+
Extract `src/utils/markdown-query.ts` into `src/utils/mdq/`, designed as a publishable
|
|
9
|
+
standalone package: query markdown *and* update it, with a jq-like CLI planned as a
|
|
10
|
+
second phase.
|
|
11
|
+
|
|
12
|
+
The name `mdq` is unclaimed on npm (verified 404). Publishing is deferred; this change
|
|
13
|
+
makes the package publish-ready but adds no `package.json` or build script.
|
|
14
|
+
|
|
15
|
+
## Constraints
|
|
16
|
+
|
|
17
|
+
- **Zero explorbot imports.** Two dependencies only: `marked` for markdown, `yaml` for
|
|
18
|
+
frontmatter. Both are already repo deps (`marked` ^16.2.0, `yaml` ^2.8.3).
|
|
19
|
+
- **Two files**, per the module split below.
|
|
20
|
+
- 54 in-repo call sites must keep working; a re-export shim carries them.
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
src/utils/mdq/
|
|
26
|
+
query.ts selector grammar - token index - MarkdownDoc - Selection (reads)
|
|
27
|
+
edit.ts pure edits over (source, ranges): splicing - whitespace - renderers
|
|
28
|
+
README.md public documentation
|
|
29
|
+
src/utils/markdown-query.ts re-export shim
|
|
30
|
+
tests/unit/mdq/*.test.ts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`edit.ts` exports pure functions taking source text plus ranges or tokens, and returning
|
|
34
|
+
new source text. It imports types from `query.ts` type-only and never references a class
|
|
35
|
+
value. `query.ts` owns both classes and delegates each write verb to exactly one `edit.ts`
|
|
36
|
+
call. This keeps the split acyclic by construction.
|
|
37
|
+
|
|
38
|
+
Two types:
|
|
39
|
+
|
|
40
|
+
- **`MarkdownDoc`** — a whole document. Returned by `mdq()` and by every write.
|
|
41
|
+
- **`Selection`** — a set of matched ranges. Returned by `query()` and the sugar methods.
|
|
42
|
+
|
|
43
|
+
One rule, stated in the README: **reads narrow, writes return the document.**
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
### `mdq(source)`
|
|
48
|
+
|
|
49
|
+
`mdq(source: string | MarkdownDoc): MarkdownDoc`
|
|
50
|
+
|
|
51
|
+
Accepting a `MarkdownDoc` makes re-wrapping free.
|
|
52
|
+
|
|
53
|
+
### `MarkdownDoc`
|
|
54
|
+
|
|
55
|
+
| Method | Returns | Notes |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `query(selector, matcher?)` | `Selection` | |
|
|
58
|
+
| `frontmatter()` | `Record<string, unknown>` | `{}` when absent |
|
|
59
|
+
| `setFrontmatter(key, value)` | `MarkdownDoc` | `null` value deletes the key |
|
|
60
|
+
| `append(md)` | `MarkdownDoc` | add a block at end of document |
|
|
61
|
+
| `prepend(md)` | `MarkdownDoc` | add a block at start of body, after frontmatter |
|
|
62
|
+
| `toString()` / `valueOf()` | `string` | full document, frontmatter included |
|
|
63
|
+
|
|
64
|
+
Plus the shared sugar layer.
|
|
65
|
+
|
|
66
|
+
`append`/`prepend` exist because "add a section to the end of the document" otherwise has
|
|
67
|
+
no clean path — only the `section().last().insertAfter(...)` workaround. There is a real
|
|
68
|
+
call site: `deep-analysis.ts:131` builds it by hand today as
|
|
69
|
+
`` `${cached.trimEnd()}\n\n# Extended Research\n\n...` ``.
|
|
70
|
+
|
|
71
|
+
### `Selection` — reads
|
|
72
|
+
|
|
73
|
+
Narrow or extract; never mutate.
|
|
74
|
+
|
|
75
|
+
| Method | Returns | Replaces |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `query(selector, matcher?)` | `Selection` | sub-query, unchanged |
|
|
78
|
+
| `text()` | `string` | — (`get()` deprecated) |
|
|
79
|
+
| `count()` | `number` | |
|
|
80
|
+
| `exists()` | `boolean` | the `.count() > 0` idiom, 3 in-repo uses |
|
|
81
|
+
| `first()` / `last()` | `Selection` | |
|
|
82
|
+
| `at(n)` | `Selection` | new; sugar-path equivalent of DSL `[n]` |
|
|
83
|
+
| `slice(from?, to?)` | `Selection` | new; sugar-path equivalent of DSL `[a:b]` |
|
|
84
|
+
| `each()` | `Selection[]` | |
|
|
85
|
+
| `nodes()` | `NodeInfo[]` | `meta()` |
|
|
86
|
+
| `rows()` | `Record<string,string>[]` | `toJson()` — it only ever handled tables |
|
|
87
|
+
| `entries()` | `Record<string,string>` | `keyValue()` |
|
|
88
|
+
| `preceding()` / `following()` | `Selection` | `before()` / `after()` |
|
|
89
|
+
|
|
90
|
+
`before`/`after` are renamed specifically to free those names from colliding with
|
|
91
|
+
`insertBefore`/`insertAfter`.
|
|
92
|
+
|
|
93
|
+
### `Selection` — writes
|
|
94
|
+
|
|
95
|
+
Every write returns `MarkdownDoc`, so edits chain in one expression.
|
|
96
|
+
|
|
97
|
+
| Method | Signature | Notes |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `replace(md)` | `(Markdown) => MarkdownDoc` | |
|
|
100
|
+
| `replaceEach(fn)` | `((Selection, number) => Markdown) => MarkdownDoc` | |
|
|
101
|
+
| `remove()` | `() => MarkdownDoc` | node **plus its adjacent `space` token** |
|
|
102
|
+
| `insertBefore(md)` | `(Markdown) => MarkdownDoc` | sibling |
|
|
103
|
+
| `insertAfter(md)` | `(Markdown) => MarkdownDoc` | sibling |
|
|
104
|
+
| `prepend(md)` | `(Markdown) => MarkdownDoc` | inside a section or list; `MdqOperationError` on a leaf node |
|
|
105
|
+
| `append(md)` | `(Markdown) => MarkdownDoc` | inside a section or list; `MdqOperationError` on a leaf node |
|
|
106
|
+
| `addRow(row)` | `(Record<string,string>) => MarkdownDoc` | table only; re-aligns columns |
|
|
107
|
+
| `addItem(text)` | `(string) => MarkdownDoc` | list only; matches marker + indent |
|
|
108
|
+
| `setEntry(key, value)` | `(string, string \| null) => MarkdownDoc` | `null` deletes |
|
|
109
|
+
|
|
110
|
+
Naming now pairs: `rows()`/`addRow()`, `entries()`/`setEntry()`, `nodes()`.
|
|
111
|
+
|
|
112
|
+
Every verb that *takes* markdown accepts `Markdown = string | MarkdownDoc`, mirroring
|
|
113
|
+
`mdq()` itself. A `replaceEach` callback may therefore return a `MarkdownDoc` built by a
|
|
114
|
+
nested edit, without a `.toString()` hop.
|
|
115
|
+
|
|
116
|
+
### Sugar layer
|
|
117
|
+
|
|
118
|
+
Eleven methods on both classes: `section` `heading` `paragraph` `table` `list` `item`
|
|
119
|
+
`code` `blockquote` `comment` `html` `hr`.
|
|
120
|
+
|
|
121
|
+
Each is `(matcher?, opts?) => Selection` and is *defined as* `query(sel, matcher)` —
|
|
122
|
+
documented as sugar, not a parallel implementation. Defined once on a shared base that
|
|
123
|
+
implements them in terms of an abstract `query()`, so the two classes do not duplicate it.
|
|
124
|
+
|
|
125
|
+
Depth is an option rather than 12 near-duplicate methods:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
mdq(src).section('API', { depth: 2 }) // DSL: query('section2("API")')
|
|
129
|
+
mdq(src).heading(/^f/i).at(0) // DSL: query('heading(/^f/i)[0]')
|
|
130
|
+
mdq(src).comment(/^test/) // DSL: query('comment(/^test/)')
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Exported types
|
|
134
|
+
|
|
135
|
+
Declared at the end of their file, per repo convention.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
type Markdown = string | MarkdownDoc;
|
|
139
|
+
type Matcher = string | RegExp | ((text: string) => boolean);
|
|
140
|
+
|
|
141
|
+
interface NodeInfo {
|
|
142
|
+
type: string; // 'heading' | 'paragraph' | 'table' | 'comment' | ...
|
|
143
|
+
depth: number | null; // heading level, else null
|
|
144
|
+
text: string; // unwrapped text; comment bodies without <!-- -->
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface SelectorOptions {
|
|
148
|
+
depth?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`Matcher` semantics:
|
|
153
|
+
|
|
154
|
+
- `string` — exact match (mirrors DSL `"x"`)
|
|
155
|
+
- `RegExp` — pattern, honoring its own flags
|
|
156
|
+
- function — predicate; needs no escaping at all
|
|
157
|
+
|
|
158
|
+
Note the consequence for `comment`: a `string` matcher is **exact**, and this repo's own
|
|
159
|
+
test-plan comments are multi-line (`<!-- test\n priority=critical\n-->`). So
|
|
160
|
+
`comment('test')` matches only a bare `<!-- test -->`; reaching the multi-line ones needs
|
|
161
|
+
`comment(/^test/)` or a predicate. Exactness is the consistent rule and is kept, but it is
|
|
162
|
+
the one place the sugar is likely to surprise.
|
|
163
|
+
|
|
164
|
+
This removes an existing wart. Today the repo hand-escapes to build selector strings:
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
const escaped = section.name.replace(/"/g, '\\"'); // researcher/focus.ts:77
|
|
168
|
+
mdq(result.text).query(`section2(~"${escaped}")`);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Deprecated aliases
|
|
172
|
+
|
|
173
|
+
`get` `toJson` `keyValue` `setKeyValue` `meta` `before` `after` are kept, marked
|
|
174
|
+
`@deprecated`, and omitted from the README so the published surface reads clean.
|
|
175
|
+
|
|
176
|
+
Aliases cover the read renames completely. They cannot shield the write return-type
|
|
177
|
+
change — see Migration.
|
|
178
|
+
|
|
179
|
+
## Selector grammar
|
|
180
|
+
|
|
181
|
+
Unchanged, plus one addition and three fixes.
|
|
182
|
+
|
|
183
|
+
### `comment` (new)
|
|
184
|
+
|
|
185
|
+
`html` tokens filtered to those that are comments. Not an alias for `html`: `<div>x</div>`
|
|
186
|
+
lexes as `html` too.
|
|
187
|
+
|
|
188
|
+
- `comment` matches on the **inner** body, trimmed. `html` matches on raw.
|
|
189
|
+
This is required for anchored patterns — `/^test/` against `<!-- test id=1 -->` only
|
|
190
|
+
works if the text is `test id=1`.
|
|
191
|
+
- Multi-line comments are a single token and keep their newlines in the matched text.
|
|
192
|
+
- **Inline comments are out of scope for 1.0.** `para with <!-- x --> comment` lexes the
|
|
193
|
+
comment inside the paragraph token; it is not reachable as a block. Documented, not faked.
|
|
194
|
+
|
|
195
|
+
`comment` and `html` together finish the `test-plan-markdown.ts` story: its hand-rolled
|
|
196
|
+
line parser (`src/utils/test-plan-markdown.ts:122+`) exists only because mdq could not
|
|
197
|
+
see `<!-- suite -->` and `<!-- test ... -->`.
|
|
198
|
+
|
|
199
|
+
### Fixes
|
|
200
|
+
|
|
201
|
+
1. **Regex flags are honored.** Today flags are parsed then discarded
|
|
202
|
+
(`markdown-query.ts:90`) and `'i'` is hardcoded (`markdown-query.ts:156`), so `/x/` is
|
|
203
|
+
case-insensitive while `"x"` and `~"x"` are case-sensitive. After the fix `/x/i` is
|
|
204
|
+
insensitive and `/x/` is not. One production call site relies on the old behavior:
|
|
205
|
+
`researcher.ts:316` `section2(/^summary/)` becomes `/^summary/i`. Tests already write
|
|
206
|
+
flags explicitly.
|
|
207
|
+
2. **Unknown selectors throw.** Today `query('secton("A")')` silently matches nothing
|
|
208
|
+
(`markdown-query.ts:103-106`, `:356`). Unacceptable for a CLI.
|
|
209
|
+
3. **Table text-match widens to headers plus cells.** Today `getTokenText` returns headers
|
|
210
|
+
only (`markdown-query.ts:181`), so `table(~"GET")` can never match a cell. No call site
|
|
211
|
+
uses table text-matching, so this is safe.
|
|
212
|
+
|
|
213
|
+
## Update semantics
|
|
214
|
+
|
|
215
|
+
`marked` separators are uneven, and every write rule follows from this:
|
|
216
|
+
|
|
217
|
+
| Token | `raw` |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `heading` | `"# A\n\n"` — separators baked in |
|
|
220
|
+
| `paragraph` | `"para"` — no trailing newline |
|
|
221
|
+
| `space` | `"\n\n"` — a separate token |
|
|
222
|
+
|
|
223
|
+
The token index therefore records each node's range **and its adjacent `space` range**.
|
|
224
|
+
|
|
225
|
+
> **Invariant: mdq never leaves zero blank lines between blocks, and never more than one.**
|
|
226
|
+
|
|
227
|
+
- `remove()` takes the node plus its trailing `space` — or its leading `space` when it is
|
|
228
|
+
the last block. Without this, removing a paragraph leaves a four-newline crater. This is
|
|
229
|
+
the most likely bug in the feature and gets dedicated tests.
|
|
230
|
+
- `insertAfter` / `append` normalize inserted markdown to one trailing `\n` and splice at
|
|
231
|
+
the boundary, never inside a space token.
|
|
232
|
+
- `append` on a section inserts before the next same-or-shallower heading, reusing the
|
|
233
|
+
existing `computeSections` end boundary.
|
|
234
|
+
- `addRow` re-renders the whole table so column pipes stay aligned.
|
|
235
|
+
- `addItem` copies the list's existing marker (`-`, `*`, `1.`) and indent.
|
|
236
|
+
|
|
237
|
+
## Frontmatter
|
|
238
|
+
|
|
239
|
+
Every `knowledge/` and `experience/` file opens with `---\nurl: /login\n---`, which
|
|
240
|
+
`marked` lexes as a setext h2 titled `url: /login`.
|
|
241
|
+
|
|
242
|
+
mdq detects leading frontmatter, excludes it from the token index with offsets preserved
|
|
243
|
+
so edits splice correctly, and exposes it as data.
|
|
244
|
+
|
|
245
|
+
Reading and writing both go through `yaml`'s **Document API** (`YAML.parseDocument`), not
|
|
246
|
+
`parse`/`stringify`. That buys two things a hand-rolled parser cannot: correctness on
|
|
247
|
+
nested maps, lists and block scalars — the Jekyll/Astro/Obsidian files that justify the
|
|
248
|
+
feature — and **comment preservation through a write**, verified:
|
|
249
|
+
|
|
250
|
+
```yaml
|
|
251
|
+
# a leading comment <- survives setFrontmatter('wait', 2000)
|
|
252
|
+
url: /login
|
|
253
|
+
wait: 2000
|
|
254
|
+
tags:
|
|
255
|
+
- auth
|
|
256
|
+
- smoke
|
|
257
|
+
nested:
|
|
258
|
+
key: value # trailing note <- also survives
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`gray-matter` is deliberately not used: `knowledge-tracker.ts` keeps it for its own
|
|
262
|
+
purposes, but a published package should not carry it to do what `yaml` already does.
|
|
263
|
+
|
|
264
|
+
```js
|
|
265
|
+
const doc = mdq(knowledgeFile);
|
|
266
|
+
doc.frontmatter(); // { url: '/login', wait: 1000, tags: ['auth'] }
|
|
267
|
+
doc.query('h2').count(); // 0 — the --- block is not a heading
|
|
268
|
+
doc.setFrontmatter('wait', 2000).toString();
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Errors
|
|
272
|
+
|
|
273
|
+
`MdqError` base, with:
|
|
274
|
+
|
|
275
|
+
- `MdqSelectorError` — malformed or unknown selector, carrying the offending index.
|
|
276
|
+
- `MdqOperationError` — a verb applied to the wrong node type, e.g. `addRow` on a paragraph.
|
|
277
|
+
|
|
278
|
+
An **empty selection is a safe no-op**: reads return `''` / `[]`, writes return the
|
|
279
|
+
document unchanged. This preserves the existing "returns source unchanged when no matches"
|
|
280
|
+
test.
|
|
281
|
+
|
|
282
|
+
## CLI (phase 2)
|
|
283
|
+
|
|
284
|
+
The selector is the program, the file or stdin is the input, markdown is the default output.
|
|
285
|
+
|
|
286
|
+
A leading `.` is accepted and ignored, so muscle memory from jq (`mdq '.h2'`) works. It is
|
|
287
|
+
sugar in the grammar, not a separate syntax — without it the new "unknown selectors throw"
|
|
288
|
+
rule would reject the most natural thing a jq user types first.
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
mdq 'h2' README.md # raw markdown of matches
|
|
292
|
+
cat plan.md | mdq 'section("API") table' -j # rows() as JSON
|
|
293
|
+
mdq 'comment(~"test")' plan.md --count
|
|
294
|
+
mdq 'section("FAQ")' doc.md --remove -i # edit in place
|
|
295
|
+
mdq 'table[0]' api.md --add-row '{"Method":"GET","Path":"/users"}' -i
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Flags mirror library verbs exactly: `--remove` `--replace` `--insert-before`
|
|
299
|
+
`--insert-after` `--prepend` `--append` `--add-row` `--add-item` `--set k=v`, plus
|
|
300
|
+
`-i/--in-place`, `-j/--json`, `-c/--count`, `-t/--text`, `--frontmatter`.
|
|
301
|
+
|
|
302
|
+
Built with Commander, per repo convention. Exit codes compose like grep: **0** match,
|
|
303
|
+
**1** no match, **2** usage or selector error.
|
|
304
|
+
|
|
305
|
+
1.0 reads one file or stdin. Multi-file input is out of scope.
|
|
306
|
+
|
|
307
|
+
## Testing
|
|
308
|
+
|
|
309
|
+
Port the existing 801-line suite first — it is the regression net for all 54 call sites.
|
|
310
|
+
Then add coverage for what is new or newly specified:
|
|
311
|
+
|
|
312
|
+
- whitespace craters on `remove` (the invariant above)
|
|
313
|
+
- chained multi-edits through `MarkdownDoc`
|
|
314
|
+
- `addRow` column alignment; `addItem` marker and indent matching
|
|
315
|
+
- frontmatter round-trip, including a file whose body has its own `---`
|
|
316
|
+
- `comment` inner-text matching, multi-line comments, and `html` versus `comment`
|
|
317
|
+
- `MdqSelectorError` on unknown selectors and malformed input
|
|
318
|
+
- sugar equivalence: every sugar call equals its `query()` form
|
|
319
|
+
|
|
320
|
+
## Migration
|
|
321
|
+
|
|
322
|
+
Two steps. Only the second carries risk.
|
|
323
|
+
|
|
324
|
+
1. `src/utils/markdown-query.ts` becomes a re-export shim. **All 54 call sites keep
|
|
325
|
+
working untouched.**
|
|
326
|
+
2. A sweep updates imports, then fixes the call sites the return-type change breaks.
|
|
327
|
+
|
|
328
|
+
Deprecated read aliases mean **no read call site changes**. Writes are not shielded: a
|
|
329
|
+
verb that returned `string` now returns `MarkdownDoc`. That breaks four classes of site,
|
|
330
|
+
at least eleven in total.
|
|
331
|
+
|
|
332
|
+
**(a) Assignment into a `string`-typed target** — 7 sites, each needs `.toString()`:
|
|
333
|
+
|
|
334
|
+
| Site | Target |
|
|
335
|
+
|---|---|
|
|
336
|
+
| `experience-tracker.ts:265` | `content` (inferred `string`) |
|
|
337
|
+
| `experience-tracker.ts:289` | `combined` (inferred `string`) |
|
|
338
|
+
| `researcher/deep-analysis.ts:129` | `let updated: string` |
|
|
339
|
+
| `researcher/locators.ts:307` | `result.text` |
|
|
340
|
+
| `researcher/locators.ts:309` | `result.text` |
|
|
341
|
+
| `researcher/pagination.ts:61` | `result.text` |
|
|
342
|
+
| `researcher/research-result.ts:57` | `section.rawMarkdown` |
|
|
343
|
+
| `researcher/research-result.ts:58` | `this.text` |
|
|
344
|
+
|
|
345
|
+
**(b) A string method called on the result** — 2 sites:
|
|
346
|
+
|
|
347
|
+
- `planner.ts:304` — `.replace('').trim()`
|
|
348
|
+
- `planner.ts:322` — `const kept = ...replace('')`, then `kept.trimEnd()` on line 324
|
|
349
|
+
|
|
350
|
+
**(c) Returned from a `replaceEach` callback** — `deep-analysis.ts:542`. **Resolved by
|
|
351
|
+
design**, not by migration: callbacks accept `Markdown`, so returning a `MarkdownDoc` is
|
|
352
|
+
valid. No edit needed.
|
|
353
|
+
|
|
354
|
+
**(d) Compared against a string — the dangerous one.** `research-result.ts:56`:
|
|
355
|
+
|
|
356
|
+
```js
|
|
357
|
+
const updated = sectionQuery.query('table').replace(`${newTable.trimEnd()}\n`);
|
|
358
|
+
if (updated === this.text) return; // MarkdownDoc === string is always false
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
This does not crash. The guard silently stops firing and the method starts doing work it
|
|
362
|
+
used to skip. `tsc` does flag it — comparing types with no overlap is an error — which is
|
|
363
|
+
precisely why the manual type-check below is not optional. Every `replace`/`setEntry`
|
|
364
|
+
result used in an equality or truthiness test must be audited, not just the ones that fail
|
|
365
|
+
to compile.
|
|
366
|
+
|
|
367
|
+
Sites that flow the result straight back into `mdq()` — `planner.ts:303`, `planner.ts:405`
|
|
368
|
+
— keep working unchanged, because `mdq()` accepts a `MarkdownDoc`.
|
|
369
|
+
|
|
370
|
+
`tsc` is the complete detector for these breaks: every one surfaces as a type error. The migration step is therefore *run `tsc` over the changed files and fix
|
|
371
|
+
what it reports*, with the table above as the expected result rather than the whole story.
|
|
372
|
+
|
|
373
|
+
Also in the sweep: `researcher.ts:316` gains its `i` flag (`section2(/^summary/i)`).
|
|
374
|
+
|
|
375
|
+
### Hazards
|
|
376
|
+
|
|
377
|
+
- **`.claude/worktrees/**` holds four stale copies** of `markdown-query.ts` and its
|
|
378
|
+
consumers. Every grep or sed sweep must exclude that path.
|
|
379
|
+
- **`tsc` runs with `--noCheck` in CI.** The nine breaks above are *exactly* the errors a
|
|
380
|
+
type-check would raise, and CI raises none of them — a fully green build proves nothing
|
|
381
|
+
here. Run `tsc` manually over the changed files before considering the sweep done.
|
|
382
|
+
|
|
383
|
+
## Style compliance
|
|
384
|
+
|
|
385
|
+
The current file violates several repo rules that the rewrite fixes: types belong at the
|
|
386
|
+
end of the file, ternaries are banned (`markdown-query.ts:30`, `:34`, `:129-130`, `:295`),
|
|
387
|
+
and the `switch` in `matchText` should be early returns.
|
|
388
|
+
|
|
389
|
+
## Deliberately out of scope
|
|
390
|
+
|
|
391
|
+
- Publishing: no `package.json`, no build script, no npm release in this change.
|
|
392
|
+
- Frontmatter formats other than YAML (TOML `+++`, JSON) — detected and skipped from the
|
|
393
|
+
token index, but not parsed.
|
|
394
|
+
- Row-level and item-level *selectors* (`addRow` has no `removeRow` partner). A future
|
|
395
|
+
`row(...)` selector is the right shape for that; guessing at it now is premature.
|
|
396
|
+
- Inline HTML comments.
|
|
397
|
+
- Multi-file CLI input.
|
package/package.json
CHANGED
package/rules/chief/general.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
- Steps should specify exact HTTP methods, paths, and key payload details
|
|
3
3
|
- Expected outcomes should be specific and verifiable (status codes, response fields, error messages)
|
|
4
4
|
- For CRUD operations, each test should handle its own setup and teardown
|
|
5
|
+
- Treat existing records and IDs discovered from the API, knowledge, or sample data as read-only
|
|
6
|
+
- A scenario that updates, patches, deletes, archives, or otherwise mutates a record must create that target inside the same scenario first; omit the scenario if safe setup is impossible
|
|
5
7
|
- Expect standard REST conventions: 200 OK, 201 Created, 204 No Content, 400 Bad Request, 404 Not Found, 422 Unprocessable Entity
|
|
6
8
|
- NEVER propose scenarios that test the same thing. "Create a basic suite" and "Successful creation of a simple suite" are DUPLICATES. Each scenario must test a DISTINCT behavior or aspect.
|
|
7
9
|
- Before finalizing, review all scenarios and remove any that overlap in what they actually verify.
|
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
When a section is a list that continues beyond what is shown, add one line under its `> Container:` line:
|
|
3
3
|
`> Pagination: controls` — it has page numbers (1, 2, 3), prev/next arrows, or a "load more" button.
|
|
4
4
|
`> Pagination: infinite` — it has none of those and loads more as it is scrolled.
|
|
5
|
+
Omit the line when the items already shown are the whole collection.
|
|
5
6
|
Sorting, filtering and switching tabs are not pagination — omit the line then.
|
|
6
7
|
</pagination>
|
package/src/action.ts
CHANGED
|
@@ -592,10 +592,11 @@ export const attachStepLogger = (target: ExecutedStep[], assertionsTarget?: Arra
|
|
|
592
592
|
}
|
|
593
593
|
tag('step').log(step);
|
|
594
594
|
};
|
|
595
|
-
|
|
595
|
+
const onPassed: StepListener = (step) => listener(step);
|
|
596
|
+
codeceptjs.event.dispatcher.on(codeceptjs.event.step.passed, onPassed);
|
|
596
597
|
codeceptjs.event.dispatcher.on(codeceptjs.event.step.failed, listener);
|
|
597
598
|
return () => {
|
|
598
|
-
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed,
|
|
599
|
+
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, onPassed);
|
|
599
600
|
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
|
|
600
601
|
};
|
|
601
602
|
};
|
|
@@ -221,13 +221,19 @@ export function createAskApiTool(fisherman: Fisherman | null, task: Test) {
|
|
|
221
221
|
return {
|
|
222
222
|
askApi: tool({
|
|
223
223
|
description: dedent`
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
Read the app's data over the API, changing nothing.
|
|
225
|
+
Answers questions about records: which ones are there, what they are called, whether a particular one exists.
|
|
226
|
+
|
|
227
|
+
Use it to:
|
|
228
|
+
- check whether suitable data already exists, before precondition() creates any
|
|
229
|
+
- get the exact name or id of a record a step must act on
|
|
230
|
+
- find out whether an action was stored, when the app reported success but the page does not show the result
|
|
231
|
+
- find out whether data exists at all, when a list or dropdown is empty
|
|
232
|
+
|
|
227
233
|
It never creates, edits or deletes anything — precondition() does that.
|
|
228
234
|
`,
|
|
229
235
|
inputSchema: z.object({
|
|
230
|
-
question: z.string().describe('What to find out about data
|
|
236
|
+
question: z.string().describe('What to find out about the data'),
|
|
231
237
|
}),
|
|
232
238
|
execute: async ({ question }) => {
|
|
233
239
|
tag('info').log(`Ask API: ${question}`);
|
package/src/ai/navigator.ts
CHANGED
|
@@ -703,7 +703,7 @@ class Navigator implements Agent {
|
|
|
703
703
|
const cachedVerification = actionResult.getVerification(message);
|
|
704
704
|
if (cachedVerification !== null) {
|
|
705
705
|
tag('operation').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
|
|
706
|
-
return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
706
|
+
return { verified: cachedVerification, inexpressible: false, results: [], successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
707
707
|
}
|
|
708
708
|
|
|
709
709
|
const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
|
|
@@ -832,9 +832,6 @@ class Navigator implements Agent {
|
|
|
832
832
|
observability: {
|
|
833
833
|
agent: 'navigator',
|
|
834
834
|
},
|
|
835
|
-
catch: async (error) => {
|
|
836
|
-
debugLog(error);
|
|
837
|
-
},
|
|
838
835
|
}
|
|
839
836
|
);
|
|
840
837
|
} finally {
|
package/src/ai/pilot.ts
CHANGED
|
@@ -152,14 +152,6 @@ export class Pilot implements Agent {
|
|
|
152
152
|
${sessionLog || 'No actions recorded'}
|
|
153
153
|
</session_log>
|
|
154
154
|
|
|
155
|
-
Decide and commit. "continue" extends the loop and burns iterations — choose it only when
|
|
156
|
-
evidence is genuinely insufficient to call pass/fail, not as a safety hedge.
|
|
157
|
-
- "pass" if final state proves the SCENARIO GOAL is accomplished. Set requestVerification.
|
|
158
|
-
- "fail" if scenario was attempted but goal not achieved.
|
|
159
|
-
- "skipped" if scenario is irrelevant/inapplicable, OR systematic infrastructure failures.
|
|
160
|
-
- "continue" only when a concrete missing piece of evidence (a verify/see) would change your verdict.
|
|
161
|
-
- Mixed evidence + final state shows success → pass. Mixed + final state unclear → continue with guidance.
|
|
162
|
-
|
|
163
155
|
When deciding "pass", you MUST also set requestVerification to a one-sentence natural-language
|
|
164
156
|
claim about the current page (e.g., "New item Foo is visible in the items list"). NOT
|
|
165
157
|
code — do not write I.*, expect(), .then(), or any JavaScript. Choose the strongest single
|
|
@@ -401,7 +393,7 @@ export class Pilot implements Agent {
|
|
|
401
393
|
private buildVerdictSystemPrompt(task: Test): string {
|
|
402
394
|
return dedent`
|
|
403
395
|
You are Pilot — final decision maker for test pass/fail. Review the evidence and commit to a
|
|
404
|
-
verdict
|
|
396
|
+
verdict.
|
|
405
397
|
|
|
406
398
|
${capabilityGroundingRule}
|
|
407
399
|
|
|
@@ -415,10 +407,11 @@ export class Pilot implements Agent {
|
|
|
415
407
|
DOM assertion can't be made.
|
|
416
408
|
Do not pass when Tester achieved only a related navigation/filter/tab/status outcome instead of the
|
|
417
409
|
requested action, workflow, or entity detail goal.
|
|
418
|
-
- "fail":
|
|
410
|
+
- "fail": goal not achieved and no further step toward it is available on the current page.
|
|
419
411
|
- "skipped": scenario is irrelevant to the app, OR systematic infrastructure failures (LLM errors,
|
|
420
412
|
crashes) prevented testing. NOT for "test failed to interact" — that's "fail" or "continue".
|
|
421
|
-
- "continue":
|
|
413
|
+
- "continue": goal incomplete but the control for the NEXT step is present on the current page, or a
|
|
414
|
+
concrete missing check would change your verdict. Guidance must name that step.
|
|
422
415
|
If a verify() asserted a state that was ALREADY TRUE before the test, it proves nothing — reject.
|
|
423
416
|
|
|
424
417
|
reason field: one short sentence, maximum 120 characters. Do NOT restate the decision
|
|
@@ -1023,14 +1016,14 @@ export class Pilot implements Agent {
|
|
|
1023
1016
|
private formatSuccessfulAssertions(currentState: ActionResult, testerConversation: Conversation): string {
|
|
1024
1017
|
const lines: string[] = [];
|
|
1025
1018
|
for (const [assertion, passed] of Object.entries(currentState.verifications ?? {})) {
|
|
1026
|
-
if (passed) lines.push(`
|
|
1019
|
+
if (passed) lines.push(`verify: ${assertion}`);
|
|
1027
1020
|
}
|
|
1028
1021
|
|
|
1029
1022
|
for (const exec of testerConversation.getToolExecutions()) {
|
|
1030
1023
|
if (!EVIDENCE_TOOLS.includes(exec.toolName) || !exec.wasSuccessful) continue;
|
|
1031
1024
|
const description = exec.input?.assertion || exec.input?.request || truncateJson(exec.input);
|
|
1032
|
-
const
|
|
1033
|
-
lines.push(
|
|
1025
|
+
const analysis = exec.output?.analysis;
|
|
1026
|
+
lines.push(`${exec.toolName}: ${description}${analysis ? ` -> ${analysis}` : ''}`);
|
|
1034
1027
|
}
|
|
1035
1028
|
|
|
1036
1029
|
return [...new Set(lines)].join('\n');
|
|
@@ -1134,7 +1127,9 @@ export class Pilot implements Agent {
|
|
|
1134
1127
|
${interactive ? '- Use askUser() only as last resort.' : ''}
|
|
1135
1128
|
|
|
1136
1129
|
Diagnostic patterns (use <state>, executed/element/skipped fields, ariaDiff):
|
|
1137
|
-
-
|
|
1130
|
+
- Scenario's target control in "disabled buttons" → a precondition is unmet; identify which before acting.
|
|
1131
|
+
Other disabled controls often name the unsatisfied constraint; "active form" marks [required] fields.
|
|
1132
|
+
Aim Tester at the constraint the page names, not the one the scenario assumed — note the difference in PROGRESS.
|
|
1138
1133
|
- "overlay: none" but Tester targets an overlay → overlay closed; re-trigger.
|
|
1139
1134
|
- "region:" in <state> → a large area appeared in place without navigation (subview, wizard step, panel). Direct Tester to act inside it; the rest of the page is still usable.
|
|
1140
1135
|
- Action SUCCESS but ariaDiff empty → may have worked without visible DOM change; check result message.
|
|
@@ -1156,14 +1151,20 @@ export class Pilot implements Agent {
|
|
|
1156
1151
|
Tester tools: click, pressKey, form, see, verify, interact, context, research, xpathCheck,
|
|
1157
1152
|
visualClick, back, getVisitedStates, reset, stop, finish, record.
|
|
1158
1153
|
Use tool names exactly as listed. Do not invent combined names or aliases.
|
|
1154
|
+
Reloading is not a tool: to re-read a page from the server, instruct Tester to run I.reloadPage() through form.
|
|
1159
1155
|
|
|
1160
1156
|
${capabilityGroundingRule}
|
|
1161
1157
|
|
|
1162
1158
|
YOUR Pilot-only tools, both over the API:
|
|
1163
1159
|
|
|
1164
|
-
askApi(question) —
|
|
1165
|
-
|
|
1166
|
-
|
|
1160
|
+
askApi(question) — read the app's data over the API. It changes nothing. Use when:
|
|
1161
|
+
|
|
1162
|
+
- Before precondition() — check whether suitable data already exists.
|
|
1163
|
+
- A step needs the exact name or id of an existing record.
|
|
1164
|
+
- The app reported success but the page does not show the result — ask whether it was stored.
|
|
1165
|
+
- A list or dropdown is empty — ask whether the data exists at all.
|
|
1166
|
+
|
|
1167
|
+
The page is not the only witness. A record missing from the page may still exist.
|
|
1167
1168
|
|
|
1168
1169
|
precondition(description) — create FRESH disposable test data. Never request users. Use when:
|
|
1169
1170
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Plan, type Test, TestResult } from '../../test-plan.ts';
|
|
2
2
|
import type { Constructor } from '../researcher/mixin.ts';
|
|
3
3
|
|
|
4
4
|
const previousPlans: Plan[] = [];
|
|
@@ -18,7 +18,7 @@ export function WithSessionDedup<T extends Constructor>(Base: T) {
|
|
|
18
18
|
for (const plan of previousPlans) {
|
|
19
19
|
if (plan === this.currentPlan) continue;
|
|
20
20
|
for (const test of plan.tests) {
|
|
21
|
-
lines.push(
|
|
21
|
+
lines.push(formatSessionTest(plan, test));
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
return lines.join('\n');
|
|
@@ -34,6 +34,20 @@ export function WithSessionDedup<T extends Constructor>(Base: T) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function formatSessionTest(plan: Plan, test: Test): string {
|
|
38
|
+
const lastNote = Object.values(test.notes)
|
|
39
|
+
.filter((note) => note.message)
|
|
40
|
+
.pop();
|
|
41
|
+
let outcome: string | null = test.result;
|
|
42
|
+
if (!outcome) outcome = 'pending';
|
|
43
|
+
if (!test.result && lastNote) outcome = 'unfinished';
|
|
44
|
+
|
|
45
|
+
const line = `${plan.url || '/'} | ${test.style || 'default'} | ${outcome} | ${test.scenario}`;
|
|
46
|
+
if (!lastNote) return line;
|
|
47
|
+
if (outcome !== TestResult.FAILED && outcome !== 'unfinished') return line;
|
|
48
|
+
return `${line} — ${lastNote.message.slice(0, 140)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
export function clearSessionDedup(): void {
|
|
38
52
|
previousPlans.length = 0;
|
|
39
53
|
}
|
package/src/ai/planner.ts
CHANGED
|
@@ -358,8 +358,10 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
358
358
|
You can suggest scenarios that can be tested only through web interface.
|
|
359
359
|
You can't test emails, database, SMS, or any external services.
|
|
360
360
|
Suggest scenarios that can be potentially verified by UI.
|
|
361
|
-
|
|
362
|
-
|
|
361
|
+
Prefer outcomes grounded in observed interface behavior.
|
|
362
|
+
Every expected outcome must be verifiable through the web interface.
|
|
363
|
+
If a page or subpage has not been observed, describe the expected visible result generically instead of inventing interface details.
|
|
364
|
+
Persistency after a reload counts only when the persisted state can be confirmed through the interface.
|
|
363
365
|
If there are subpages (pages with same URL path) plan testing of those subpages as well
|
|
364
366
|
Plan CRUD operations in order: create, read, update, delete.
|
|
365
367
|
Do not invent specific route names, success messages, validation texts, badge counts, or welcome messages unless they are visible in research, visited pages, or prior observed flows.
|
|
@@ -370,7 +372,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
370
372
|
If a scenario needs existing records, recipients, results, notifications, or other target data, propose it only when that data is visible, API preconditions can create it, or the scenario itself creates the record as its setup.
|
|
371
373
|
If the page appears read-only, degraded, demo-limited, maintenance-like, or lacks write controls, prefer read-only scenarios such as opening panels, inspecting visible lists, filtering, searching, or verifying current state.
|
|
372
374
|
Do not assume hidden data exists just because a control is present.
|
|
373
|
-
|
|
375
|
+
Do not put record IDs or unique record names in test plans. Describe which record is needed and let Pilot choose it during execution; name a specific record only when research shows a small, complete list of available records.
|
|
374
376
|
If the list is empty or no concrete item names are visible, do not invent "known" or "existing" items. Prefer empty-state, no-match search, clear-search, or read-only list behavior scenarios.
|
|
375
377
|
Search, filter, sorting, tab, and list scenarios must start from a stable page where those controls are visible; avoid transient create/edit/new URLs unless the scenario tests that form.
|
|
376
378
|
For option values and list items, use only visible or previously observed data; do not add create/update/delete setup unless the user explicitly requests that workflow.
|
|
@@ -595,7 +597,9 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
595
597
|
const sessionTests = this.getSessionTestsSummary();
|
|
596
598
|
if (sessionTests) {
|
|
597
599
|
conversation.addUserText(dedent`
|
|
598
|
-
Tests already planned in this session across all pages. DO NOT duplicate any of these
|
|
600
|
+
Tests already planned in this session across all pages, with how each one ended. DO NOT duplicate any of these.
|
|
601
|
+
A failed test means the app or the harness could not do what it tried: do not re-propose the same behavior on another page unless you can name what makes it work this time.
|
|
602
|
+
A failed or unfinished test carries the last thing it observed after the dash — read it before deciding that the behavior is worth trying again.
|
|
599
603
|
|
|
600
604
|
<session_tests>
|
|
601
605
|
${sessionTests}
|