@autor3search/javascript 0.2.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/LICENSE +21 -0
- package/README.md +500 -0
- package/bin/autor3search-javascript.js +4 -0
- package/package.json +50 -0
- package/src/adapters/bench/index.js +31 -0
- package/src/adapters/bench/vitest.js +128 -0
- package/src/adapters/driver-child.js +72 -0
- package/src/adapters/driver-hooks.js +18 -0
- package/src/adapters/driver.js +192 -0
- package/src/adapters/gates/index.js +64 -0
- package/src/adapters/gates/lint.js +46 -0
- package/src/adapters/gates/test.js +27 -0
- package/src/adapters/gates/typecheck.js +98 -0
- package/src/adapters/gates/util.js +45 -0
- package/src/adapters/vitest-shim.js +51 -0
- package/src/bench/parse.js +120 -0
- package/src/bench/set.js +101 -0
- package/src/bench/stats.js +443 -0
- package/src/cli/cmd-baseline.js +136 -0
- package/src/cli/cmd-doctor.js +38 -0
- package/src/cli/cmd-eval.js +231 -0
- package/src/cli/cmd-init.js +136 -0
- package/src/cli/cmd-profile.js +38 -0
- package/src/cli/cmd-report.js +96 -0
- package/src/cli/cmd-status.js +68 -0
- package/src/cli/cmd-stop.js +98 -0
- package/src/cli/cmd-version.js +31 -0
- package/src/cli/context.js +98 -0
- package/src/cli/main.js +62 -0
- package/src/config.js +209 -0
- package/src/discover.js +239 -0
- package/src/doctor.js +282 -0
- package/src/duration.js +75 -0
- package/src/freeze.js +234 -0
- package/src/gitx.js +115 -0
- package/src/measure.js +127 -0
- package/src/pipeline.js +391 -0
- package/src/profile.js +100 -0
- package/src/results.js +124 -0
- package/src/runner.js +198 -0
- package/src/scope.js +92 -0
- package/src/state/index.js +312 -0
- package/src/state/lock.js +189 -0
- package/src/state/stop.js +56 -0
- package/src/verdict.js +214 -0
- package/templates/program.md +264 -0
package/src/verdict.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns gate outcomes and measurements into a single decision.
|
|
3
|
+
*
|
|
4
|
+
* This module is the contract with the agent: program.md documents these
|
|
5
|
+
* status strings, these reason codes and these exit codes, and the agent's
|
|
6
|
+
* loop branches on them. They are frozen — renaming one is a breaking change
|
|
7
|
+
* to templates/program.md and must happen in the same commit.
|
|
8
|
+
*/
|
|
9
|
+
import { countForAlpha, minAchievableP } from './bench/stats.js'
|
|
10
|
+
|
|
11
|
+
/** The terminal outcomes of one experiment. */
|
|
12
|
+
export const STATUS = {
|
|
13
|
+
KEEP: 'KEEP',
|
|
14
|
+
DISCARD: 'DISCARD',
|
|
15
|
+
FAIL: 'FAIL',
|
|
16
|
+
CRASH: 'CRASH',
|
|
17
|
+
/** Not a verdict: an experiment interrupted before it was measured. */
|
|
18
|
+
ABORTED: 'ABORTED',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Machine-readable explanations, grouped by the stage that produces them. */
|
|
22
|
+
export const REASON = {
|
|
23
|
+
IMPROVED: 'improved',
|
|
24
|
+
NO_IMPROVEMENT: 'no_significant_improvement',
|
|
25
|
+
BELOW_MIN_EFFECT: 'improvement_below_min_effect',
|
|
26
|
+
GUARD_REGRESSION: 'guard_regression',
|
|
27
|
+
SCOPE: 'scope_violation',
|
|
28
|
+
CONFIG_CHANGED: 'config_changed',
|
|
29
|
+
NEW_TEST_FILE: 'new_test_file',
|
|
30
|
+
MISSING_TEST_FILE: 'missing_test_file',
|
|
31
|
+
SYMLINK_SWAP: 'symlink_swap',
|
|
32
|
+
HARDLINK_SWAP: 'hardlink_swap',
|
|
33
|
+
FROZEN_TAMPERED: 'frozen_store_tampered',
|
|
34
|
+
BASELINE_TAMPERED: 'baseline_tampered',
|
|
35
|
+
TYPECHECK: 'typecheck_failed',
|
|
36
|
+
LINT: 'lint_failed',
|
|
37
|
+
/** The benchmark run itself failed, so there is nothing to score. */
|
|
38
|
+
MEASUREMENT: 'measurement_failed',
|
|
39
|
+
TESTS: 'tests_failed',
|
|
40
|
+
TIMEOUT: 'timeout',
|
|
41
|
+
STOP_FORCED: 'stop_forced',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Builds a Result for a failed correctness stage, before measurement.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} status
|
|
48
|
+
* @param {string} reason
|
|
49
|
+
* @param {string} message
|
|
50
|
+
*/
|
|
51
|
+
export function gate(status, reason, message) {
|
|
52
|
+
return { status, reason, score: 0, message, regressions: [], warnings: [] }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Applies the scoring rules.
|
|
57
|
+
*
|
|
58
|
+
* 1. Any regression significant at the RAW, uncorrected alpha and larger than
|
|
59
|
+
* maxRegressPct rejects the change, however good the overall score. This
|
|
60
|
+
* check deliberately does not apply the Bonferroni correction from rule 2:
|
|
61
|
+
* Bonferroni only ever makes it harder to call a result significant, and
|
|
62
|
+
* applying it here would make the guard LESS sensitive to harm — backwards
|
|
63
|
+
* from what a guard is for. The asymmetry is intentional: be conservative
|
|
64
|
+
* about accepting a win, be liberal about catching a regression.
|
|
65
|
+
*
|
|
66
|
+
* 2. Otherwise keep only when BOTH:
|
|
67
|
+
* a. the score is a real speedup by at least minEffectPct — below
|
|
68
|
+
* 1 - minEffectPct/100, not merely below 1. A result that is
|
|
69
|
+
* technically significant but trivially small is not worth a commit in
|
|
70
|
+
* an unattended loop.
|
|
71
|
+
* b. at least one benchmark improved at the Bonferroni-corrected
|
|
72
|
+
* threshold alpha/k, where k is the number of benchmarks compared.
|
|
73
|
+
* Comparing k benchmarks against the same uncorrected alpha inflates
|
|
74
|
+
* the family-wise false-positive rate — with k = 4, roughly an 18%
|
|
75
|
+
* chance at least one shows a spurious "significant" improvement even
|
|
76
|
+
* when nothing changed.
|
|
77
|
+
*
|
|
78
|
+
* A change clearing 2b but missing 2a discards with BELOW_MIN_EFFECT rather
|
|
79
|
+
* than NO_IMPROVEMENT: it measurably worked, it was just too small to bank,
|
|
80
|
+
* and those call for different next moves.
|
|
81
|
+
*
|
|
82
|
+
* @param {{deltas: object[], score: number, maxRegressPct: number, minEffectPct: number}} input
|
|
83
|
+
*/
|
|
84
|
+
export function decide(input) {
|
|
85
|
+
const deltas = input.deltas ?? []
|
|
86
|
+
// k is the family size for the Bonferroni correction. compareAll never
|
|
87
|
+
// returns an empty list, but guard against division by zero anyway.
|
|
88
|
+
const k = Math.max(1, deltas.length)
|
|
89
|
+
const warnings = measurementWarnings(deltas, k)
|
|
90
|
+
const score = input.score
|
|
91
|
+
|
|
92
|
+
const regressions = deltas.filter((d) => d.significant && d.pctChange > input.maxRegressPct)
|
|
93
|
+
if (regressions.length > 0) {
|
|
94
|
+
const detail = regressions.map((d) => `${d.name} ${signed(d.pctChange, 1)}%`).join(', ')
|
|
95
|
+
return {
|
|
96
|
+
status: STATUS.DISCARD,
|
|
97
|
+
reason: REASON.GUARD_REGRESSION,
|
|
98
|
+
score,
|
|
99
|
+
message: `regression guard tripped (limit ${signed(input.maxRegressPct, 1)}%): ${detail}`,
|
|
100
|
+
regressions,
|
|
101
|
+
warnings,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const improved = deltas.some((d) => d.pctChange < 0 && d.p < d.alpha / k)
|
|
106
|
+
const minEffectThreshold = 1 - input.minEffectPct / 100
|
|
107
|
+
|
|
108
|
+
if (score < minEffectThreshold && improved) {
|
|
109
|
+
return {
|
|
110
|
+
status: STATUS.KEEP,
|
|
111
|
+
reason: REASON.IMPROVED,
|
|
112
|
+
score,
|
|
113
|
+
message: `score ${score.toFixed(4)} (${signed((score - 1) * 100, 2)}%)`,
|
|
114
|
+
regressions: [],
|
|
115
|
+
warnings,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (improved && score < 1) {
|
|
120
|
+
return {
|
|
121
|
+
status: STATUS.DISCARD,
|
|
122
|
+
reason: REASON.BELOW_MIN_EFFECT,
|
|
123
|
+
score,
|
|
124
|
+
message:
|
|
125
|
+
`score ${score.toFixed(4)} (${signed((score - 1) * 100, 2)}%), a real improvement but below ` +
|
|
126
|
+
`the ${input.minEffectPct.toFixed(1)}% minimum effect size`,
|
|
127
|
+
regressions: [],
|
|
128
|
+
warnings,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
status: STATUS.DISCARD,
|
|
134
|
+
reason: REASON.NO_IMPROVEMENT,
|
|
135
|
+
score,
|
|
136
|
+
message: `score ${score.toFixed(4)} (${signed((score - 1) * 100, 2)}%), no significant improvement`,
|
|
137
|
+
regressions: [],
|
|
138
|
+
warnings,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Gathers everything that qualifies how far the numbers in a Result can be
|
|
144
|
+
* trusted: each comparison's own warnings, then the check that a KEEP was
|
|
145
|
+
* statistically reachable at all. These never change the decision — they say
|
|
146
|
+
* what it can and cannot mean.
|
|
147
|
+
*/
|
|
148
|
+
function measurementWarnings(deltas, k) {
|
|
149
|
+
const out = [...new Set(deltas.flatMap((d) => d.warnings ?? []))]
|
|
150
|
+
const unreachable = unreachableAlphaWarning(deltas, k)
|
|
151
|
+
if (unreachable) out.push(unreachable)
|
|
152
|
+
return out
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Reports when rule 2b cannot be satisfied by any result whatsoever, so the
|
|
157
|
+
* run is incapable of a KEEP before it starts.
|
|
158
|
+
*
|
|
159
|
+
* config.validate enforces a count floor for a single benchmark; this is the
|
|
160
|
+
* same footgun at k benchmarks, which the validator cannot see because it does
|
|
161
|
+
* not know how many benchmarks a run will compare. A KEEP needs only ONE
|
|
162
|
+
* benchmark to clear the threshold, so this warns only when none of them can.
|
|
163
|
+
*/
|
|
164
|
+
function unreachableAlphaWarning(deltas, k) {
|
|
165
|
+
if (deltas.length === 0) return null
|
|
166
|
+
let worstN = 0
|
|
167
|
+
let alpha = 0
|
|
168
|
+
for (const d of deltas) {
|
|
169
|
+
const corrected = d.alpha / k
|
|
170
|
+
// This one can clear it; that is enough for a KEEP to be possible.
|
|
171
|
+
if (minAchievableP(d.nBase, d.nCand) < corrected) return null
|
|
172
|
+
const n = Math.min(d.nBase, d.nCand)
|
|
173
|
+
if (n > worstN) {
|
|
174
|
+
worstN = n
|
|
175
|
+
alpha = d.alpha
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const corrected = alpha / k
|
|
179
|
+
const need = countForAlpha(corrected)
|
|
180
|
+
const tail = need > 0 ? ` — raise count to at least ${need}` : ' — raise count, or measure fewer benchmarks'
|
|
181
|
+
return (
|
|
182
|
+
`no KEEP was reachable: comparing ${k} benchmark(s) corrects the significance threshold to ` +
|
|
183
|
+
`${corrected.toFixed(5)}, but with ${worstN} rounds per side the test cannot produce a p-value ` +
|
|
184
|
+
`below ${minAchievableP(worstN, worstN).toFixed(5)} however large the improvement is${tail}`
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Maps a Result to the process exit code program.md documents.
|
|
190
|
+
*
|
|
191
|
+
* Anything unrecognised — including ABORTED, which is an interrupted
|
|
192
|
+
* experiment rather than a verdict — maps to FAIL. Never to 0: a status the
|
|
193
|
+
* harness cannot classify must not read to the agent as a successful KEEP.
|
|
194
|
+
*
|
|
195
|
+
* @param {{status: string}} result
|
|
196
|
+
* @returns {number}
|
|
197
|
+
*/
|
|
198
|
+
export function exitCode(result) {
|
|
199
|
+
switch (result.status) {
|
|
200
|
+
case STATUS.KEEP:
|
|
201
|
+
return 0
|
|
202
|
+
case STATUS.DISCARD:
|
|
203
|
+
return 1
|
|
204
|
+
case STATUS.CRASH:
|
|
205
|
+
return 3
|
|
206
|
+
default:
|
|
207
|
+
return 2
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Formats a number with an explicit sign, e.g. "+12.0", "-8.77". */
|
|
212
|
+
function signed(n, digits) {
|
|
213
|
+
return `${n >= 0 ? '+' : ''}${n.toFixed(digits)}`
|
|
214
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# program.md
|
|
2
|
+
|
|
3
|
+
This file is your instructions, coding agent. Read it fully before doing
|
|
4
|
+
anything. It is the only thing a human edits to steer this run — everything
|
|
5
|
+
else (the metric, the gates, the verdict) belongs to the harness.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
Every command below is written as `autor3search-javascript <cmd>`. If it is
|
|
10
|
+
not on your PATH, prefix each one with `npx` — `npx autor3search-javascript
|
|
11
|
+
status` — and keep using the same form for the whole run, so the version that
|
|
12
|
+
recorded the baseline is the version that scores against it.
|
|
13
|
+
|
|
14
|
+
Before starting the loop, do this once:
|
|
15
|
+
|
|
16
|
+
1. Agree a run tag with the human if one was not already given (a short slug
|
|
17
|
+
like `sep7` — today's date or similar is fine).
|
|
18
|
+
2. Confirm `autor3search-javascript baseline -tag <tag>` has already been run
|
|
19
|
+
for this tag. If it has not, stop and ask the human to run it, or run it
|
|
20
|
+
yourself if you have been told you may. `baseline` creates the run branch,
|
|
21
|
+
freezes the current test AND benchmark files as golden copies, and records
|
|
22
|
+
the commit this run measures against. Everything downstream depends on this
|
|
23
|
+
having happened exactly once.
|
|
24
|
+
3. Read the repository. Skim the files matched by `scope` in
|
|
25
|
+
`.autor3search/config.yaml`. Run `autor3search-javascript profile` once so
|
|
26
|
+
you know what you are starting from before changing anything.
|
|
27
|
+
|
|
28
|
+
## Experimentation
|
|
29
|
+
|
|
30
|
+
What you MAY do:
|
|
31
|
+
|
|
32
|
+
- Edit any source file matched by a `scope` pattern in
|
|
33
|
+
`.autor3search/config.yaml`.
|
|
34
|
+
- Add new files inside `scope`, as long as they are not test or benchmark
|
|
35
|
+
files.
|
|
36
|
+
- Run any read-only diagnostic (`autor3search-javascript profile`,
|
|
37
|
+
`node --cpu-prof`, reading `run.log`) as often as you like between
|
|
38
|
+
experiments.
|
|
39
|
+
|
|
40
|
+
What you MUST NOT do:
|
|
41
|
+
|
|
42
|
+
- **Edit any `*.test.*`, `*.spec.*` or `*.bench.*` file.** All of them are
|
|
43
|
+
restored from the frozen baseline copies before every `eval`, so an edit
|
|
44
|
+
there is silently discarded and wastes an experiment slot. The benchmark
|
|
45
|
+
files matter as much as the tests: they define the metric. If a test looks
|
|
46
|
+
wrong, say so in your `-desc` and move on; do not try to route around it.
|
|
47
|
+
- **Edit `package.json` or any lockfile, ever.** These are rejected outright
|
|
48
|
+
regardless of `scope`. Changing a dependency is a supply-chain decision a
|
|
49
|
+
human makes, and a swapped dependency changes *what* is measured, not just
|
|
50
|
+
how fast it runs.
|
|
51
|
+
- **Edit `.autor3search/config.yaml`.** Its hash is recorded at `baseline`
|
|
52
|
+
time; if it has changed by `eval` time the run fails with reason
|
|
53
|
+
`config_changed`.
|
|
54
|
+
- Edit an ordinary source file outside `scope`. That is what the scope gate
|
|
55
|
+
rejects, failing the experiment before it is even measured.
|
|
56
|
+
- Try to weaken, disable or reinterpret the verdict. `eval`'s exit code and
|
|
57
|
+
`--json` output are the only truth.
|
|
58
|
+
- Batch multiple unrelated changes into one experiment. One idea per
|
|
59
|
+
experiment keeps every result attributable and every discard cheap.
|
|
60
|
+
|
|
61
|
+
## Output format
|
|
62
|
+
|
|
63
|
+
`autor3search-javascript eval` is the only command whose result decides
|
|
64
|
+
anything. It exits with one of four codes:
|
|
65
|
+
|
|
66
|
+
| Exit code | Meaning | Verdict status |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `0` | KEEP — a real, safe improvement | `KEEP` |
|
|
69
|
+
| `1` | DISCARD — no significant improvement, too small to bank, or a regression | `DISCARD` |
|
|
70
|
+
| `2` | FAIL — a gate rejected the change: scope, config, tampering with a frozen file, a new or missing test/bench file, lint, or a failing test | `FAIL` |
|
|
71
|
+
| `3` | CRASH — the typecheck failed outright, a phase timed out, or the benchmark measurement itself failed | `CRASH` |
|
|
72
|
+
|
|
73
|
+
Any status the harness cannot classify arrives as exit code `2`. `ABORTED`
|
|
74
|
+
(see "Stopping") does too, for exactly that reason: it is not a verdict, and
|
|
75
|
+
treating it like FAIL is the right thing to do with it.
|
|
76
|
+
|
|
77
|
+
**A `KEEP` requires a real, not just a technically significant, improvement.**
|
|
78
|
+
`eval` returns `KEEP` only when `score` clears `1 - min_effect_pct/100`
|
|
79
|
+
(default 1%, i.e. score < 0.99) AND at least one benchmark improved past a
|
|
80
|
+
Bonferroni-corrected significance bar. A change that shaves off a fraction of
|
|
81
|
+
a percent is `DISCARD`ed by design — do not spend a night chasing sub-1% wins.
|
|
82
|
+
And even a `KEEP` is evidence, not proof: any significance threshold admits
|
|
83
|
+
some false positives.
|
|
84
|
+
|
|
85
|
+
With `--json`, `eval` prints one JSON object to stdout and nothing else:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"status": "KEEP",
|
|
90
|
+
"reason": "improved",
|
|
91
|
+
"score": 0.9123,
|
|
92
|
+
"message": "score 0.9123 (-8.77%)",
|
|
93
|
+
"regressions": [],
|
|
94
|
+
"warnings": [],
|
|
95
|
+
"stop_requested": false,
|
|
96
|
+
"run": {
|
|
97
|
+
"tag": "sep7",
|
|
98
|
+
"branch": "autor3search-javascript/sep7",
|
|
99
|
+
"baseline_commit": "a3f1c2d",
|
|
100
|
+
"measure_commit": "9b7e410",
|
|
101
|
+
"worktree": "/Users/you/Library/Caches/autor3search-javascript/1a2b3c4d/sep7/baseline-worktree",
|
|
102
|
+
"experiment": 5
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`reason` is a stable machine-readable code: `improved`,
|
|
108
|
+
`no_significant_improvement`, `improvement_below_min_effect`,
|
|
109
|
+
`guard_regression`, `scope_violation`, `config_changed`, `new_test_file`,
|
|
110
|
+
`missing_test_file`, `symlink_swap`, `hardlink_swap`, `frozen_store_tampered`,
|
|
111
|
+
`baseline_tampered`, `typecheck_failed`, `lint_failed`, `tests_failed`,
|
|
112
|
+
`measurement_failed`, `timeout`, `stop_forced`.
|
|
113
|
+
|
|
114
|
+
Two discard reasons mean genuinely different things.
|
|
115
|
+
`no_significant_improvement` means nothing measurably moved — the idea did not
|
|
116
|
+
work, drop it. `improvement_below_min_effect` means it DID work and the
|
|
117
|
+
harness measured a real speedup, just smaller than `min_effect_pct` will bank.
|
|
118
|
+
That says the direction is right: a variation with a larger effect, or the
|
|
119
|
+
same idea applied to a hotter path, may well clear the bar. Do not read it as
|
|
120
|
+
failure.
|
|
121
|
+
|
|
122
|
+
`score` is the geometric mean of `candidate_time / baseline_time` across the
|
|
123
|
+
declared benchmarks — below 1 is faster.
|
|
124
|
+
|
|
125
|
+
`warnings`, when present, says the measurement is too weak to carry the
|
|
126
|
+
verdict printed beside it. They never change the decision; they tell you not
|
|
127
|
+
to over-read it. Two you may see:
|
|
128
|
+
|
|
129
|
+
- **too few observations for a confidence interval** — the medians are real
|
|
130
|
+
but the interval around them is unbounded. Raise `count`.
|
|
131
|
+
- **no KEEP was reachable** — the significance threshold is corrected for the
|
|
132
|
+
number of benchmarks compared (`alpha/k`), and with the configured `count`
|
|
133
|
+
the test cannot produce a p-value that small however large the improvement
|
|
134
|
+
is. Every experiment will `DISCARD` until `count` is raised. Treat this as a
|
|
135
|
+
broken configuration and stop rather than burning the night on experiments
|
|
136
|
+
that cannot be banked.
|
|
137
|
+
|
|
138
|
+
**What the baseline means changes as the run progresses.** It is NOT always
|
|
139
|
+
the commit `baseline` recorded — it is whatever the measurement baseline
|
|
140
|
+
currently points to, and a KEEP moves that pointer to the commit you just
|
|
141
|
+
kept. So `score` always answers "did THIS experiment help, compared to the
|
|
142
|
+
last thing that was kept" — never "is the tree better than when the run
|
|
143
|
+
started." Two consequences: after a KEEP, running `eval` again with nothing
|
|
144
|
+
new committed measures your last commit against itself and correctly
|
|
145
|
+
`DISCARD`s — that is not a bug; and a long run's total progress is the
|
|
146
|
+
*product* of every kept `score`, which is what `report` computes.
|
|
147
|
+
|
|
148
|
+
`stop_requested` is the one field that changes what you do. See below.
|
|
149
|
+
|
|
150
|
+
## Stopping
|
|
151
|
+
|
|
152
|
+
The loop does not end on its own. It ends in one of two ways.
|
|
153
|
+
|
|
154
|
+
**A graceful stop.** The human runs `autor3search-javascript stop`. That
|
|
155
|
+
writes a request `eval` reports back as `"stop_requested": true`, alongside a
|
|
156
|
+
verdict that is still fully valid. When you see it:
|
|
157
|
+
|
|
158
|
+
1. Apply the verdict exactly as you would have anyway — KEEP leaves the
|
|
159
|
+
commit, anything else is `git reset --hard HEAD~1`. A stop must never leave
|
|
160
|
+
a commit on the branch that nothing decided on.
|
|
161
|
+
2. Do NOT start another experiment.
|
|
162
|
+
3. Run `autor3search-javascript report` and summarize in a few lines: what you
|
|
163
|
+
tried, what was kept, what you would try next.
|
|
164
|
+
4. Exit the loop and say you stopped because the human asked.
|
|
165
|
+
|
|
166
|
+
**An interrupt.** Ctrl+C, or `autor3search-javascript stop --force`. Either
|
|
167
|
+
cancels `eval` mid-experiment. You will see `"status": "ABORTED"` with
|
|
168
|
+
`"reason": "stop_forced"`, exit code `2`, and no `results.tsv` row — nothing
|
|
169
|
+
was measured, so nothing was recorded. Treat the commit as you would any FAIL
|
|
170
|
+
(`git reset --hard HEAD~1`), then stop as above.
|
|
171
|
+
|
|
172
|
+
If the human wants the run to continue after all, they clear the request with
|
|
173
|
+
`autor3search-javascript stop --clear`. That is their decision, not something
|
|
174
|
+
to wait for or ask about.
|
|
175
|
+
|
|
176
|
+
## Logging
|
|
177
|
+
|
|
178
|
+
`results.tsv` is HARNESS-OWNED. `eval` appends exactly one row on every
|
|
179
|
+
invocation, KEEP or not. Never create, append to, or edit it yourself — a
|
|
180
|
+
manual write is either redundant or corrupts a file the harness parses
|
|
181
|
+
strictly, breaking the human's morning `report` on the whole file.
|
|
182
|
+
|
|
183
|
+
The one column that is yours is `description`, set with `-desc`:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
autor3search-javascript eval --json -desc "preallocate the map"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Always pass `-desc`, on every invocation, KEEP or not — it is the only record
|
|
190
|
+
of what you were trying. Keep it terse and never dishonest: describe what you
|
|
191
|
+
actually tried, including for a DISCARD, FAIL or CRASH. A long trail of honest
|
|
192
|
+
discards is more useful to the human than a short trail that hides them.
|
|
193
|
+
|
|
194
|
+
## JavaScript optimization idea bank
|
|
195
|
+
|
|
196
|
+
Measure first, then reach for these:
|
|
197
|
+
|
|
198
|
+
- **Ask whether the work is needed at all.** An algorithmic change, or
|
|
199
|
+
skipping work entirely, usually beats every micro-optimization below.
|
|
200
|
+
- **Allocation in hot loops.** Every intermediate array, object literal and
|
|
201
|
+
closure allocates. `autor3search-javascript profile` reports an approximate
|
|
202
|
+
`bytes/op` alongside the timings.
|
|
203
|
+
- **Avoid intermediate arrays.** `arr.map(f).filter(g).reduce(h)` walks the
|
|
204
|
+
data three times and allocates twice. One loop does not.
|
|
205
|
+
- **Keep object shapes stable.** Assigning properties in a consistent order,
|
|
206
|
+
and never deleting them, keeps V8 on a monomorphic hidden class. A
|
|
207
|
+
polymorphic call site is far slower than a monomorphic one.
|
|
208
|
+
- **Preallocate.** `new Array(n)` when n is known, and typed arrays
|
|
209
|
+
(`Float64Array`, `Uint8Array`) for numeric data — they avoid boxing entirely.
|
|
210
|
+
- **Hoist closures out of loops.** A function expression created inside a loop
|
|
211
|
+
allocates on every iteration.
|
|
212
|
+
- **String building.** Repeated `+=` in a long loop can be quadratic; collect
|
|
213
|
+
into an array and `join('')`, or build with a fixed-size buffer.
|
|
214
|
+
- **`Map` vs plain object vs array.** For small fixed key sets a linear scan
|
|
215
|
+
over an array beats both. `Map` beats an object for frequent insertion and
|
|
216
|
+
deletion of non-identifier keys. Measure at your real size.
|
|
217
|
+
- **Avoid `try`/`catch` and `arguments` in the hottest function** if it stops
|
|
218
|
+
V8 optimizing it — check with `--trace-deopt` before assuming.
|
|
219
|
+
- **Regular expressions.** Compile once outside the loop; a `RegExp` literal
|
|
220
|
+
inside a loop with the `g` flag also carries mutable `lastIndex` state.
|
|
221
|
+
- **Character codes over string methods.** `charCodeAt` and numeric comparison
|
|
222
|
+
avoid the allocation `toLowerCase()` on a single character causes.
|
|
223
|
+
|
|
224
|
+
## The experiment loop
|
|
225
|
+
|
|
226
|
+
`eval` splits its own output for you: the verdict — one compact JSON object —
|
|
227
|
+
goes to stdout, and the full build/lint/test/benchmark transcript goes to
|
|
228
|
+
`run.log` in the repository root. `run.log` is the harness's own file, opened
|
|
229
|
+
by `eval` before it does anything else. **Never redirect `eval`'s stdout into
|
|
230
|
+
it** (`eval --json > run.log 2>&1` or similar): that opens a second descriptor
|
|
231
|
+
on a path the harness already holds open, and whichever writes second
|
|
232
|
+
overwrites the other from byte 0 — destroying exactly the transcript you need
|
|
233
|
+
when something FAILs or CRASHes. Run `eval --json` bare and read the verdict
|
|
234
|
+
from its stdout; open `run.log` only to read.
|
|
235
|
+
|
|
236
|
+
LOOP FOREVER:
|
|
237
|
+
|
|
238
|
+
0. Print one context line so the human watching knows where the run is:
|
|
239
|
+
`[exp <n> | <branch> | vs <measure_commit> | stop: autor3search-javascript stop]`
|
|
240
|
+
Take the numbers from the previous experiment's `run` object; on the first
|
|
241
|
+
pass, from `autor3search-javascript status`.
|
|
242
|
+
1. Confirm you are on the run branch.
|
|
243
|
+
2. If you have no strong hypothesis, run `autor3search-javascript profile` and
|
|
244
|
+
read the hot spots.
|
|
245
|
+
3. Change ONE thing in the in-scope source. One idea per experiment.
|
|
246
|
+
4. `git add -A && git commit -m "<idea>"`
|
|
247
|
+
5. `autor3search-javascript eval --json -desc "<idea>"` — no redirect.
|
|
248
|
+
6. Read the verdict from stdout: one compact JSON object.
|
|
249
|
+
7. KEEP → leave the commit; the branch advances and the measurement baseline
|
|
250
|
+
advances to this commit, so your next experiment is measured against what
|
|
251
|
+
you just kept.
|
|
252
|
+
Anything else → `git reset --hard HEAD~1`
|
|
253
|
+
8. If the verdict carried `"stop_requested": true`, or its status was
|
|
254
|
+
`ABORTED`, follow "Stopping" and leave the loop.
|
|
255
|
+
9. Go to 0.
|
|
256
|
+
|
|
257
|
+
**NEVER STOP ON YOUR OWN.** Do not pause to ask whether to continue. The human
|
|
258
|
+
may be asleep. You are autonomous. If you run out of ideas, re-read the
|
|
259
|
+
profile output, re-read the idea bank, combine previous near-misses, or try a
|
|
260
|
+
more radical change.
|
|
261
|
+
|
|
262
|
+
Only the human ends this loop, and only in the two ways "Stopping" describes.
|
|
263
|
+
Running out of ideas is not one of them, and neither is a long string of
|
|
264
|
+
DISCARDs.
|