sloptimize 0.3.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/.claude-plugin/marketplace.json +11 -0
- package/.claude-plugin/plugin.json +8 -0
- package/.mcp.json +9 -0
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/bin/sloptimize.mjs +346 -0
- package/docs/DESIGN-mecharoyale-v0.md +32 -0
- package/docs/INTEGRATION.md +230 -0
- package/docs/JITTER-AND-FOOTPRINTS.md +236 -0
- package/docs/SPEC-attach.md +176 -0
- package/docs/SPEC.md +845 -0
- package/docs/USAGE.md +227 -0
- package/hooks/hooks.json +16 -0
- package/mcp/server.mjs +127 -0
- package/package.json +64 -0
- package/skills/install/SKILL.md +143 -0
- package/skills/sloptimize/SKILL.md +74 -0
- package/src/attach.mjs +197 -0
- package/src/census.js +193 -0
- package/src/classify.js +70 -0
- package/src/footprint.js +170 -0
- package/src/history.js +273 -0
- package/src/index.js +8 -0
- package/src/inject-body.js +152 -0
- package/src/motion.js +345 -0
- package/src/panel.js +530 -0
- package/src/proposals.mjs +268 -0
- package/src/recorder.js +235 -0
- package/src/watch.mjs +242 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Coordinate jitter and the issue catalogue — the integrator's cookbook
|
|
2
|
+
|
|
3
|
+
This is the shortest complete path from "my game has a recorder feed"
|
|
4
|
+
(INTEGRATION.md §1) to: **every snap of the player's unit or camera lands as
|
|
5
|
+
a classified record, every incident of every kind carries a footprint naming
|
|
6
|
+
its cause and the game's situation, and the debugger's Issues tab shows what
|
|
7
|
+
keeps happening and what was done about it.** Everything here is framework-
|
|
8
|
+
agnostic; the reference implementation is mecharoyale (three.js/WebGPU), and
|
|
9
|
+
each step names the file to copy from.
|
|
10
|
+
|
|
11
|
+
Two things in the package do the work; the game supplies four small answers.
|
|
12
|
+
|
|
13
|
+
| The package gives you | You tell it |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `createMotionMonitor` — the jitter detector (SPEC §3.6) | two points per rendered frame, which frames are not its to judge, when the view cut on purpose, your sim's dt clamp |
|
|
16
|
+
| `footprintOf` + `canonicalContext` + `buildIssues` — footprints and the catalogue (SPEC §3.7) | a few facets of the player's situation, refreshed once a second |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. What a jitter is, and why the naive detector fails
|
|
21
|
+
|
|
22
|
+
The recorder's hitches are about **time**: a frame that took too long. The
|
|
23
|
+
other thing a player feels is about **space**: the unit or the camera landing
|
|
24
|
+
somewhere its own motion did not predict. The detector is a one-step
|
|
25
|
+
constant-velocity prediction from the last two samples; the residual
|
|
26
|
+
`r = p − p̂` is how far the point landed off its trajectory (½·a·dt² for smooth
|
|
27
|
+
motion — millimetres). A residual is only a **jump** when the next frame
|
|
28
|
+
**reverses** it; an un-reversed residual is a change of motion (a dash, a
|
|
29
|
+
boom easing) and is never reported.
|
|
30
|
+
|
|
31
|
+
Three things will make a naive position track lie to you, and the design
|
|
32
|
+
answers each:
|
|
33
|
+
|
|
34
|
+
1. **Rotation.** A camera on a boom swings metres in one frame when the
|
|
35
|
+
mouse flicks: 16° per frame on a 10 m boom is 2.9 m of travel that is
|
|
36
|
+
entirely intended. Do not feed a camera's raw world position and expect
|
|
37
|
+
silence. Feed the **pivot** the view is arranged around (the unit) — it
|
|
38
|
+
is rotation-invariant by construction — and feed the eye **held** on any
|
|
39
|
+
frame that consumed look or zoom input.
|
|
40
|
+
2. **Transient offsets.** A camera shake applied around the render and
|
|
41
|
+
restored after it is a per-frame random offset by design. Sample **after
|
|
42
|
+
the render**, once the transient is gone, so what you feed is what was
|
|
43
|
+
drawn.
|
|
44
|
+
3. **The dt clamp.** Every game clamps the delta its sim integrates with
|
|
45
|
+
(mecharoyale: 50 ms). Past the clamp the point moves by the clamp while
|
|
46
|
+
the wall clock moved further, so the prediction is wrong for the frame's
|
|
47
|
+
whole length — the detector must know the clamp (`longFrameMs`) to file
|
|
48
|
+
those as `long-frame-catch-up` (the stall is the incident) instead of
|
|
49
|
+
`snap`. Measured on a software rasteriser drawing a frame every 1.5 s:
|
|
50
|
+
without it every frame was a "snap".
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 2. Feed the detector
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
import { createMotionMonitor } from 'sloptimize';
|
|
58
|
+
|
|
59
|
+
const motion = createMotionMonitor({
|
|
60
|
+
unit: 'm', // your world unit, for evidence strings
|
|
61
|
+
longFrameMs: MAX_SIM_STEP_S * 1000, // YOUR sim's dt clamp — never a guess
|
|
62
|
+
tracks: {
|
|
63
|
+
unit: { floor: 0.1 }, // the view pivot
|
|
64
|
+
camera: { floor: 0.1, reach: 'boom', follows: 'unit' }, // the eye
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `floor` is the smallest off-trajectory displacement worth a record, in
|
|
70
|
+
your units. Pick the smallest pop a player can see at your camera
|
|
71
|
+
distances and comfortably above ½·a·dt² for your fastest acceleration
|
|
72
|
+
(120 m/s² at 60 fps is 1.7 cm; mecharoyale uses 10 cm).
|
|
73
|
+
- `reach` names the scalar you sample beside the camera — its distance to
|
|
74
|
+
the pivot — so a jump that is a boom clamp or a zoom is explained as
|
|
75
|
+
`reach-change`, not a teleport.
|
|
76
|
+
- `follows` declares the hierarchy: when the camera jumps in the same frame
|
|
77
|
+
as the unit it follows, its record is filed `follows-track` (the
|
|
78
|
+
passenger) and does not wake anyone twice.
|
|
79
|
+
|
|
80
|
+
Once per **rendered** frame, after the render, with the same clock every
|
|
81
|
+
frame:
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
const source = viewPivot(camera.position, pivot); // who published the pivot this frame
|
|
85
|
+
const key = `${source}|${thirdPerson ? 'tps' : 'fp'}|${spectateTargetId}`;
|
|
86
|
+
if (key !== lastKey) { lastKey = key; motion.cut(); } // an intentional discontinuity
|
|
87
|
+
|
|
88
|
+
const off = paused || !continuityExpected(phase); // covered tab, boot, a cinematic that cuts
|
|
89
|
+
motion.sample('unit', pivot.x, pivot.y, pivot.z, now, { held: off || source === FALLBACK, phase, ctx });
|
|
90
|
+
motion.sample('camera', cam.x, cam.y, cam.z, now, { held: off || lookInputSinceLastSample, reach: dist(cam, pivot), phase, ctx });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Rules the reference keeps, each learned the hard way:
|
|
94
|
+
|
|
95
|
+
- **Cuts are derived, not declared.** Compose a view-configuration key from
|
|
96
|
+
everything about the camera that is not motion — who publishes the pivot
|
|
97
|
+
(foot/hull/spectate), first vs third person, the spectated target — and
|
|
98
|
+
`cut()` whenever it changes. Mode flips, boarding, dismount, death, respawn
|
|
99
|
+
and spectate cycling all change it; no call site has to remember.
|
|
100
|
+
- **Held ≠ cut.** `held` is a frame the track must not judge (input-driven,
|
|
101
|
+
paused, a phase with no continuous view); the track re-seeds after it.
|
|
102
|
+
Both are counted in `stats()`.
|
|
103
|
+
- **The pivot fallback is not the unit.** When no camera published a pivot
|
|
104
|
+
and the camera stood in, hold the unit track — judging it would judge the
|
|
105
|
+
camera twice.
|
|
106
|
+
- **Drain beside the recorder**, same pipe, same ledger:
|
|
107
|
+
`post('records', [...rec.drainRecords(), ...motion.drainRecords()])`.
|
|
108
|
+
- **Rate limit is per track, 1/s, 200/session**, drops counted onto the next
|
|
109
|
+
record. A unit that teleports takes its camera with it in the same frame;
|
|
110
|
+
a shared gap would drop exactly the camera's explanatory record.
|
|
111
|
+
|
|
112
|
+
Reference: `packages/client/src/dev/sloptimize-motion-feed.ts` (policy +
|
|
113
|
+
plumbing, ~150 lines, unit-tested with a fake camera and pivot), called from
|
|
114
|
+
the runtime's counter site.
|
|
115
|
+
|
|
116
|
+
### The record
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{ "type": "jitter", "at": "…", "track": "unit", "kind": "snap",
|
|
120
|
+
"jump": [-16.25, 5.55, 3.58], "units": 17.54, "travelUnits": 1.36, "speed": 33.25,
|
|
121
|
+
"dtMs": 41.0, "medianDtMs": 16.7, "from": [x,y,z], "to": [x,y,z],
|
|
122
|
+
"coincident": ["camera"], "reach": { "name": "boom", "before": 15.7, "after": 15.7 },
|
|
123
|
+
"classification": [{ "guess": "snap", "confidence": "high", "evidence": "…" }],
|
|
124
|
+
"phase": "play", "ctx": "combat=yes,hull=droyd-g,squad=duo,stance=helm,view=tps",
|
|
125
|
+
"footprint": { "v": 1, "id": "b5b203ba", "key": "jitter|unit|snap|play|snap|horizontal|ctx:…" } }
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Verdicts (closed): `snap` · `oscillation` (≥2 reversals in one burst — a
|
|
129
|
+
fixed-step sim drawn without interpolation, or two writers on one transform)
|
|
130
|
+
· `long-frame-catch-up` · `follows-track` · `reach-change`. The watcher wakes
|
|
131
|
+
on the first two only.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 3. Declare the situation
|
|
136
|
+
|
|
137
|
+
Time is not part of an issue's identity; the game's **state** is. Declare a
|
|
138
|
+
handful of **low-cardinality** facets — categories, never positions or
|
|
139
|
+
counters — and hand the canonical string along:
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
import { canonicalContext } from 'sloptimize';
|
|
143
|
+
|
|
144
|
+
function situation() { // read ONCE A SECOND, never per frame
|
|
145
|
+
return {
|
|
146
|
+
stance: dead ? 'dead' : aboard ? (atHelm ? 'helm' : 'crew') : 'foot',
|
|
147
|
+
hull: aboard ? frameName(aboard) : 'none',
|
|
148
|
+
squad: squadSize >= 3 ? 'trio' : squadSize === 2 ? 'duo' : 'solo',
|
|
149
|
+
view: thirdPerson ? 'tps' : 'fp',
|
|
150
|
+
combat: now - lastHitAt <= 10_000 ? 'yes' : 'no',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
let ctx = '';
|
|
154
|
+
setInterval(() => { ctx = canonicalContext(situation()); }, 1000); // "combat=no,hull=none,squad=solo,stance=foot,view=fp"
|
|
155
|
+
|
|
156
|
+
rec.frame({ …, phase, ctx }); // hitches carry it from mint
|
|
157
|
+
rec.usermark({ …, phase, ctx });
|
|
158
|
+
motion.sample(track, x, y, z, now, { …, phase, ctx });
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
A facet that varies per frame makes every incident its own issue and turns
|
|
162
|
+
the catalogue back into a log. Values with `|`, `,`, `=` or whitespace are
|
|
163
|
+
scrubbed; keys are sorted; the string is what gets hashed.
|
|
164
|
+
|
|
165
|
+
Reference: `packages/client/src/dev/sloptimize-situation.ts` (pure rules,
|
|
166
|
+
every fact injected, unit-tested) wired as `context` on the runtime.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 4. Stamp every record at post
|
|
171
|
+
|
|
172
|
+
The writer stamps the identity; every reader then agrees without
|
|
173
|
+
re-deriving — the watcher's `×N`, the Issues tab, a service deduping many
|
|
174
|
+
clients:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
import { footprintOf } from 'sloptimize';
|
|
178
|
+
|
|
179
|
+
for (const r of records) {
|
|
180
|
+
if (r.build === undefined) r.build = buildStamp();
|
|
181
|
+
if (r.phase === undefined) r.phase = currentPhase();
|
|
182
|
+
if (navigator.webdriver && r.automated === undefined) r.automated = true; // robots never wake anyone
|
|
183
|
+
if (r.ctx === undefined && ctx) r.ctx = ctx; // host-built records take the current situation
|
|
184
|
+
const fp = footprintOf(r); if (fp) r.footprint = fp;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
What goes into a footprint key, per type, and what never does, is SPEC §3.7.
|
|
189
|
+
Records written before footprints existed are derived at read time, so the
|
|
190
|
+
catalogue reaches back over the whole ledger. Changing what enters a key is a
|
|
191
|
+
new `FOOTPRINT_VERSION`, never a silent reshuffle.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 5. Read it back
|
|
196
|
+
|
|
197
|
+
- **`sloptimize issues [--fp <id>] [--from … --to …] [--all]`** — every
|
|
198
|
+
incident type grouped by footprint: `×N`, first/last, `last 3h ago`,
|
|
199
|
+
builds, worst, the last verdict, the fixes applied. `report` shows the
|
|
200
|
+
top five.
|
|
201
|
+
- **`sloptimize watch`** — every wake line ends `fp=<id> ×N`, the count
|
|
202
|
+
being this ledger's own (seeded from the file at arm). ×1 is new; ×40 is
|
|
203
|
+
the same issue again.
|
|
204
|
+
- **The debugger's Issues tab** — `createPanel` renders it from the same
|
|
205
|
+
`history()` callback the Timeline uses (`{ records, fixes }` from your
|
|
206
|
+
dev-gated ledger read-back); no extra host work. Rows by frequency with
|
|
207
|
+
`×N` and last-seen and the situation as chips; a row opens its history and
|
|
208
|
+
its fixes. Session rows show their `fp` when the host's incident rows
|
|
209
|
+
carry one (`{ …, fp, label, glyph }`).
|
|
210
|
+
- **Linking fixes** — `sloptimize fix propose --footprints <id>,<id> …` (and
|
|
211
|
+
`sloptimize fix`). The catalogue answers "which fixes were applied to this
|
|
212
|
+
issue" by that join; the fix's measured before/after says whether it
|
|
213
|
+
landed.
|
|
214
|
+
|
|
215
|
+
Host-side ranges: the in-page tab folds only the ledger tail the server
|
|
216
|
+
hands back (the reference serves 2 MB, a few thousand records); the CLI folds
|
|
217
|
+
the whole file.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## 6. Checklist
|
|
222
|
+
|
|
223
|
+
- [ ] `createMotionMonitor` with `longFrameMs` = your sim's dt clamp and a
|
|
224
|
+
`floor` you can defend
|
|
225
|
+
- [ ] `unit` = the view pivot (rotation-invariant), `camera` = the eye held
|
|
226
|
+
on look/zoom input, `reach` = distance to the pivot, `follows: 'unit'`
|
|
227
|
+
- [ ] sample after the render, same clock every frame; hold paused frames
|
|
228
|
+
and phases without a continuous view; cut on a derived view key
|
|
229
|
+
- [ ] drain beside the recorder, same post
|
|
230
|
+
- [ ] `context()` with ≤ 6 categorical facets; `canonicalContext` once a
|
|
231
|
+
second; `ctx` on `frame`, `usermark`, `sample`
|
|
232
|
+
- [ ] at post: `build`, `phase`, `automated`, `ctx`, `footprint`
|
|
233
|
+
- [ ] incident rows for the panel carry `fp` (and `label`/`glyph` for
|
|
234
|
+
non-millisecond rows)
|
|
235
|
+
- [ ] watcher armed; `sloptimize issues` in the agent's playbook; fixes
|
|
236
|
+
recorded with `--footprints`
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# sloptimize v2 — the incident pipeline (supersedes the first attach draft)
|
|
2
|
+
|
|
3
|
+
Status: draft for approval. Amends SPEC.md. Rewritten after the operator
|
|
4
|
+
corrected the frame twice, and both corrections are load-bearing:
|
|
5
|
+
|
|
6
|
+
1. *"Your job is to report data to Claude Code so it can proceed with
|
|
7
|
+
optimization — remember which gaps you're filling."*
|
|
8
|
+
2. *"It logs incidents automatically in the background; the debugger is
|
|
9
|
+
optional and shows the list; all of it is fed to Claude Code."*
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 0. Mission, restated as the contract
|
|
14
|
+
|
|
15
|
+
The agent cannot watch, cannot localize, cannot verify. sloptimize is the
|
|
16
|
+
agent's senses and ruler — nothing more. Every feature below must resolve
|
|
17
|
+
to one of the three gaps, or it is scope creep:
|
|
18
|
+
|
|
19
|
+
| gap | what fills it |
|
|
20
|
+
|---|---|
|
|
21
|
+
| MEASURE — the agent will never feel a hitch | incidents recorded automatically, before anyone asks |
|
|
22
|
+
| ATTRIBUTE — "it stutters" is not a work item | every incident carries its classification, evidence, and (by tier) stacks/entities |
|
|
23
|
+
| VERIFY — an unmeasured fix is a hypothesis | exact counters, before/after windows, budgets with exit codes |
|
|
24
|
+
|
|
25
|
+
The tool decides what is true; the agent decides what to try; the human
|
|
26
|
+
plays. Any design that asks the human to operate instruments, or asks the
|
|
27
|
+
agent to trust prose, violates the contract.
|
|
28
|
+
|
|
29
|
+
## 1. First principles: the incident
|
|
30
|
+
|
|
31
|
+
An **incident** is a frame (or run of frames) where time went somewhere
|
|
32
|
+
the frame needed. There are only four somewheres, each with the one
|
|
33
|
+
instrument that names it:
|
|
34
|
+
|
|
35
|
+
| where | field example | naming instrument | tier |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| main-thread JS | warm sweep, 165–305ms | sampling JS profile over the window → function names | 0 |
|
|
38
|
+
| GPU process | 8.4s page-load freeze (no rAF, no long task, no counter) | queue latency + creation ledger with call stacks | 0 |
|
|
39
|
+
| scene structure | uninstanced 200-mesh group | census / measured bisection → entity names | 1–2 |
|
|
40
|
+
| game logic | launch playing under the battleground | NOT an incident — a correctness bug; out of scope, said out loud | — |
|
|
41
|
+
|
|
42
|
+
Corollary: **detection is threshold math; attribution is per-somewhere.**
|
|
43
|
+
A pipeline that detects everything but attributes nothing (our first
|
|
44
|
+
field build: `long-script`, 14ms inside render, full stop) makes the
|
|
45
|
+
agent guess — the exact failure this product exists to end.
|
|
46
|
+
|
|
47
|
+
## 2. The pipeline
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
detect (always on) → classify+attribute → deliver ┬→ agent (push: wake with evidence; pull: files/CLI)
|
|
51
|
+
└→ human (OPTIONAL debugger: the incident list + one-line annotation)
|
|
52
|
+
→ agent changes ONE thing → verify (counters/bench) → ledger
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- **Detect**: relative + absolute thresholds (2× median, 1.5× budget),
|
|
56
|
+
rate-limited with loud drop counts. No keypress anywhere in this stage.
|
|
57
|
+
- **Deliver to the agent** is the primary edge. Push: incident → agent
|
|
58
|
+
wakeup with the classification attached (MCP notification when
|
|
59
|
+
packaged; a session Monitor until then). Pull: `.sloptimize/` files +
|
|
60
|
+
CLI with exit codes — works headless, in CI, and after the fact.
|
|
61
|
+
- **Deliver to the human** is a MIRROR, not a transport: opening the
|
|
62
|
+
debugger shows what already shipped (list: when, how long, why;
|
|
63
|
+
manual keyframes starred) and offers one text line — semantics only a
|
|
64
|
+
human has. The mirror must never claim more than the pipe did: rows
|
|
65
|
+
read "sent", meaning *landed in the sink*; whether an agent session is
|
|
66
|
+
currently consuming the sink is not the page's claim to make.
|
|
67
|
+
- **Verify** closes the loop: counters compare exactly on any renderer;
|
|
68
|
+
timing only within its regime; verdicts land in the append-only ledger
|
|
69
|
+
so a reverted strategy is never retried.
|
|
70
|
+
|
|
71
|
+
## 3. Tiers of sensing (progressive precision, none required to start)
|
|
72
|
+
|
|
73
|
+
- **Tier 0 — attach** (`sloptimize attach [--launch <url>]`): CDP;
|
|
74
|
+
injected recorder; rAF timing; graphics-API wraps (draws, triangles,
|
|
75
|
+
pipeline creations WITH `Error().stack`, uploads, queue latency);
|
|
76
|
+
rolling sampling profiler (~1–3% dev overhead) so every `long-script`
|
|
77
|
+
incident carries `topFrames`. Zero game code. Chromium-only, dev-only.
|
|
78
|
+
- **Tier 1 — in-page feed**: the game hands engine-true numbers
|
|
79
|
+
(`rec.frame(...)` at its stats site — one line) and ships the recorder
|
|
80
|
+
ambient with every dev session, no attach needed. Exact
|
|
81
|
+
insideRenderMs, spawn deltas, engine tags.
|
|
82
|
+
- **Tier 2 — engine/slopjs**: census, entity attribution, measured
|
|
83
|
+
bisection, snapshot repro of a keyframe's workload.
|
|
84
|
+
|
|
85
|
+
Every number carries its tier and regime; a tier-0 approximation never
|
|
86
|
+
poses as a tier-1 measurement.
|
|
87
|
+
|
|
88
|
+
## 4. Critical review (of this spec, including against its own drafts)
|
|
89
|
+
|
|
90
|
+
**Fixed since draft 1:**
|
|
91
|
+
- Draw-call counting was the wrong headline; the rolling profiler is the
|
|
92
|
+
prize — it attributes the class of freeze (`long-script`,
|
|
93
|
+
unexplained) that the field deployment recorded a dozen times and
|
|
94
|
+
could never name.
|
|
95
|
+
- The human's role was over-weighted (Ctrl+F11 as a pillar). Corrected:
|
|
96
|
+
auto-first; the debugger is an optional mirror + annotation channel.
|
|
97
|
+
- "Sent to Claude Code" is now specified as *sink-landed*, because the
|
|
98
|
+
UI must not assert a live consumer it cannot see.
|
|
99
|
+
|
|
100
|
+
**Standing weaknesses, stated:**
|
|
101
|
+
- **Operator's everyday browser**: attach needs a debug port; ambient
|
|
102
|
+
always-on coverage of the human's normal play is tier 1's job, which
|
|
103
|
+
costs one line of game code. Zero-code AND ambient-for-the-human is
|
|
104
|
+
not achievable simultaneously; the spec stops pretending otherwise.
|
|
105
|
+
- **Correctness bugs** (most of what the field ticket actually fixed)
|
|
106
|
+
are invisible to every tier. The doctrine must route "it looks/behaves
|
|
107
|
+
wrong" reports away from the profiler before anyone burns a loop on it.
|
|
108
|
+
- **Incident flooding / identity**: a recurring root cause fires
|
|
109
|
+
incidents forever. Rate limits bound volume but not repetition;
|
|
110
|
+
clustering (same classification + same top frame ⇒ same incident id,
|
|
111
|
+
count incremented) is REQUIRED in v2 so the agent investigates a cause
|
|
112
|
+
once, not per occurrence. New in this draft; unimplemented.
|
|
113
|
+
- **Profiler observer effect**: 1–3% steady overhead plus GC from stack
|
|
114
|
+
sampling. Bounded and labeled (`profiled: true` on the window) so a
|
|
115
|
+
profiled p95 is never compared against an unprofiled one.
|
|
116
|
+
- **Attribution ceiling at tier 0**: draw calls cannot be attributed to
|
|
117
|
+
entities from the API (in three, every draw shares one internal call
|
|
118
|
+
site). Entity work items require tier ≥1. The table in §1 is honest
|
|
119
|
+
about which somewhere needs which tier.
|
|
120
|
+
- **Trust**: an injected recorder and a writable sink are spoofable by
|
|
121
|
+
anything local. Unchanged posture from SPEC §9 — price and expose,
|
|
122
|
+
don't pretend to prevent.
|
|
123
|
+
|
|
124
|
+
**Will it resolve the issues this ticket actually dealt with?**
|
|
125
|
+
|
|
126
|
+
| issue | verdict |
|
|
127
|
+
|---|---|
|
|
128
|
+
| random unattributed freezes | YES — this spec's center of mass; profile stacks name the function |
|
|
129
|
+
| 8.4s page-load freeze | YES (diagnosis) — queue latency + creation stacks discriminate; the fix follows what they name |
|
|
130
|
+
| descent-entry compile stutters | YES — creation stacks replace the bespoke attribution probe |
|
|
131
|
+
| warm-sweep stalls | YES — stacks ≥ hand tags |
|
|
132
|
+
| launch logic bugs | NO — out of scope by principle, forever |
|
|
133
|
+
|
|
134
|
+
## 4.1 Dogfood verdict (tier 0 against the real game, zero integration)
|
|
135
|
+
|
|
136
|
+
`sloptimize attach --launch http://127.0.0.1:4477/?play&launch --headless`
|
|
137
|
+
against mecharoyale (1.4MB minified bundle, WebGL2 fallback on the QA
|
|
138
|
+
box): 35 hitches recorded, every one attributed and clustered — the
|
|
139
|
+
software rasterizer's real costs by name (`bufferSubData` ×7,
|
|
140
|
+
`getUniformBlockIndex` ×6), three.js internals (`_setupBindings`,
|
|
141
|
+
`update`), and one game function. Two limits surfaced and kept:
|
|
142
|
+
|
|
143
|
+
- **Minified names**: the game frame arrived as `O_e@game.min.js` — the
|
|
144
|
+
pipeline works, but human-readable attribution in bundled games needs
|
|
145
|
+
sourcemaps served with the dev build. Doctrine: turn sourcemaps on in
|
|
146
|
+
dev, or accept minified names as cluster keys (they are stable per
|
|
147
|
+
build, so clustering still holds).
|
|
148
|
+
- **API coverage follows the backend**: `gpu-create` records require
|
|
149
|
+
WebGPU; on a WebGL2-fallback page the WebGL draw wraps carry the
|
|
150
|
+
counters and creation stacks are absent (honestly, not silently).
|
|
151
|
+
|
|
152
|
+
## 5. Milestones
|
|
153
|
+
|
|
154
|
+
- **M-A0 attach MVP** — **SHIPPED, exit criterion measured**: on the
|
|
155
|
+
zero-integration fixture the seeded freeze is attributed
|
|
156
|
+
`seededFreezeWork@seeded-freeze.html:5` from the written record alone
|
|
157
|
+
(test/attach-e2e.mjs; unit tier in test/attach.test.js).
|
|
158
|
+
- **M-A1 incident identity** — **SHIPPED with a measured limit**: repeats
|
|
159
|
+
merge into an existing cause when its identifying frame appears in the
|
|
160
|
+
new hitch's top-3 (fixture: 3 occurrences → 2 clusters). The measured
|
|
161
|
+
limit, recorded rather than papered over: V8 INLINING can erase the
|
|
162
|
+
leaf frame between occurrences (freeze #1 named the function, #2
|
|
163
|
+
arrived as its caller), so a cause can still split across an
|
|
164
|
+
optimization boundary. Deopt-aware matching is deferred until a real
|
|
165
|
+
case demands it.
|
|
166
|
+
- **M-A2 plugin packaging** — **SHIPPED (structure + live tier)**: the
|
|
167
|
+
repo IS the plugin (`.claude-plugin/plugin.json` at root, so `bin/` and
|
|
168
|
+
`src/` travel with any install), carrying the doctrine skill, the
|
|
169
|
+
silent-by-default UserPromptSubmit hook via `${CLAUDE_PLUGIN_ROOT}`,
|
|
170
|
+
and a dependency-free stdio MCP server (`get_report`, `check_budgets`,
|
|
171
|
+
`attach_start`, `attach_stop`) protocol-smoked over real JSON-RPC.
|
|
172
|
+
Install: `claude --plugin-dir <path-to-sloptimize>` in dev; marketplace
|
|
173
|
+
add once published. The exit criterion's PUSH half (agent woken by
|
|
174
|
+
incident with zero project files) still rides the session Monitor —
|
|
175
|
+
server-initiated MCP wakeups are not yet a documented host contract,
|
|
176
|
+
recorded here as the remaining gap rather than claimed.*
|