-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaudit.html
More file actions
458 lines (423 loc) · 115 KB
/
Copy pathaudit.html
File metadata and controls
458 lines (423 loc) · 115 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Projekt-Audit · @spearwolf/shadow-objects (Monorepo) · 2026-08-30</title>
<style>
*,*::before,*::after{box-sizing:border-box}
:root{
--bg:#fafaf9; --surface:#ffffff; --line:#e7e5e4; --line-strong:#d6d3d1;
--ink:#1c1917; --ink-2:#44403c; --muted:#57534e;
--accent:#4338ca; --accent-soft:#eef2ff;
--c-critical:#dc2626; --c-high:#ea580c; --c-medium:#d97706; --c-low:#2563eb; --c-info:#6b7280;
--bg-critical:#fee2e2; --fg-critical:#991b1b;
--bg-high:#ffedd5; --fg-high:#9a3412;
--bg-medium:#fef3c7; --fg-medium:#92400e;
--bg-low:#dbeafe; --fg-low:#1e40af;
--bg-info:#f1f5f9; --fg-info:#475569;
--gutter:clamp(16px,4vw,64px);
--radius:10px;
}
html{-webkit-text-size-adjust:100%}
body{
margin:0; background:var(--bg); color:var(--ink);
font-family:ui-sans-serif,-apple-system,"Segoe UI",Inter,Roboto,"Helvetica Neue",sans-serif;
font-size:clamp(16px,0.35vw + 15px,17px); line-height:1.65;
overflow-x:hidden;
}
.wrap{padding:0 var(--gutter) 96px}
.prose{max-width:72ch}
h1,h2,h3{font-weight:600; line-height:1.25; margin:0}
h2{font-size:clamp(1.25rem,1.4vw + 1rem,1.6rem); margin:0 0 .35em}
h3{font-size:1.05rem}
p{margin:0 0 .9em}
a{color:var(--accent)}
code,.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:.88em}
section{margin-top:clamp(40px,5vw,72px)}
.eyebrow{font-size:.78rem; letter-spacing:.09em; text-transform:uppercase; color:var(--muted); font-weight:600; margin:0 0 1.1em}
hr.sep{border:0; border-top:1px solid var(--line); margin:0}
/* ---------- header ---------- */
header{padding:clamp(32px,5vw,64px) var(--gutter) clamp(28px,3vw,40px); border-bottom:1px solid var(--line)}
.title{font-size:clamp(1.7rem,2.6vw + 1rem,2.6rem); letter-spacing:-.02em}
.subtitle{color:var(--muted); margin:.4em 0 0}
.head-grid{display:flex; flex-wrap:wrap; gap:clamp(24px,4vw,56px); align-items:flex-start; justify-content:space-between}
.head-left{min-width:min(100%,340px); flex:1 1 380px}
.badges{display:flex; flex-wrap:wrap; gap:6px; margin-top:1.1em}
.badge{font-size:.76rem; padding:3px 9px; border:1px solid var(--line-strong); border-radius:999px; color:var(--ink-2); background:var(--surface)}
.scorebox{flex:0 0 auto; text-align:left}
.score{font-size:clamp(3rem,6vw,4.6rem); font-weight:600; line-height:1; letter-spacing:-.03em}
.score small{font-size:.3em; font-weight:500; color:var(--muted); letter-spacing:0}
.subscores{margin-top:.5em; color:var(--muted); font-size:.9rem}
.subscores b{color:var(--ink-2); font-weight:600}
.delta{margin-top:1.2em; padding:12px 14px; background:var(--surface); border:1px solid var(--line); border-left:3px solid var(--accent); border-radius:var(--radius); font-size:.92rem; max-width:62ch}
.delta .trend{font-weight:600}
.down{color:var(--c-high)} .up{color:#15803d}
.delta .note{color:var(--muted)}
/* ---------- cards ---------- */
.card{background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:clamp(16px,2vw,24px)}
.grid2{display:grid; gap:20px; grid-template-columns:1fr}
@media (min-width:900px){ .grid2{grid-template-columns:1fr 1fr; gap:24px} }
.domain-head{display:flex; align-items:baseline; justify-content:space-between; gap:12px; margin-bottom:.7em}
.domain-score{font-size:1.6rem; font-weight:600; letter-spacing:-.02em}
.domain-score span{font-size:.62rem; color:var(--muted); font-weight:500; letter-spacing:.05em; text-transform:uppercase}
/* ---------- severity bars ---------- */
.bars{margin:1.4em 0 0; display:grid; gap:7px}
.bar-row{display:grid; grid-template-columns:5.6em 1fr 2.2em; align-items:center; gap:10px; font-size:.85rem}
.bar-label{color:var(--muted)}
.bar-track{background:#f0efee; border-radius:999px; height:9px; overflow:hidden}
.bar-fill{display:block; height:100%; border-radius:999px}
.bar-count{text-align:right; font-variant-numeric:tabular-nums; color:var(--ink-2)}
.f-critical{background:var(--c-critical)} .f-high{background:var(--c-high)} .f-medium{background:var(--c-medium)}
.f-low{background:var(--c-low)} .f-info{background:var(--c-info)}
@media (max-width:719px){
.bar-row{grid-template-columns:1fr auto; grid-template-areas:"l c" "b b"; row-gap:3px}
.bar-label{grid-area:l} .bar-count{grid-area:c} .bar-track{grid-area:b}
}
.cats{margin:1.4em 0 0; display:grid; gap:4px; font-size:.88rem}
.cat-row{display:flex; justify-content:space-between; gap:12px; border-bottom:1px dotted var(--line); padding:3px 0}
.cat-row span:last-child{color:var(--muted); font-variant-numeric:tabular-nums}
/* ---------- portrait ---------- */
.domains{display:grid; gap:14px; grid-template-columns:1fr; margin-top:1.4em}
@media (min-width:760px){ .domains{grid-template-columns:repeat(2,1fr)} }
@media (min-width:1200px){ .domains{grid-template-columns:repeat(3,1fr)} }
.dom h3{margin-bottom:.3em}
.dom p{font-size:.92rem; color:var(--ink-2); margin-bottom:.6em}
.chips{display:flex; flex-wrap:wrap; gap:5px}
.chip{font-size:.74rem; padding:2px 7px; background:var(--accent-soft); color:#3730a3; border-radius:5px; overflow-wrap:anywhere}
figure{margin:1.8em 0 0}
.svg-scroll{overflow-x:auto}
figcaption{margin-top:.9em; color:var(--muted); font-size:.88rem; max-width:72ch}
/* ---------- filters ---------- */
.filters{display:flex; flex-wrap:wrap; gap:18px; margin:1.2em 0 1.4em}
.fgroup{display:flex; flex-wrap:wrap; gap:6px; align-items:center}
.fgroup > span{font-size:.78rem; color:var(--muted); margin-right:2px}
.chipbtn{font:inherit; font-size:.82rem; padding:7px 12px; min-height:40px; display:inline-flex; align-items:center;
border:1px solid var(--line-strong); background:var(--surface); color:var(--ink-2); border-radius:999px; cursor:pointer}
.chipbtn[aria-pressed="true"]{background:var(--ink); color:#fff; border-color:var(--ink)}
select.chipbtn{min-height:40px; padding-right:10px}
.count{color:var(--muted); font-size:.88rem; margin-bottom:.8em}
/* ---------- backlog ---------- */
.tablehead{display:grid; grid-template-columns:4.8rem 5.6rem 1fr 8.4rem 4.6rem 7.2rem; gap:12px; padding:0 14px 8px;
font-size:.74rem; letter-spacing:.07em; text-transform:uppercase; color:var(--muted); font-weight:600}
.rows{display:grid; gap:8px}
.row{background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); overflow:hidden}
.row.sev-high{border-left:3px solid var(--c-high)}
.row.sev-critical{border-left:3px solid var(--c-critical)}
.row.sev-medium{border-left:3px solid var(--c-medium)}
.rowhead{display:grid; grid-template-columns:4.8rem 5.6rem 1fr 8.4rem 4.6rem 7.2rem; gap:12px; align-items:start;
width:100%; text-align:left; font:inherit; background:none; border:0; padding:13px 14px; cursor:pointer; min-height:44px; color:inherit}
.rowhead:hover{background:#fbfaf9}
.rowhead:focus-visible{outline:2px solid var(--accent); outline-offset:-2px}
.sev{font-size:.74rem; font-weight:600; padding:3px 8px; border-radius:5px; display:inline-block; white-space:nowrap}
.sev-b-critical{background:var(--bg-critical); color:var(--fg-critical)}
.sev-b-high{background:var(--bg-high); color:var(--fg-high)}
.sev-b-medium{background:var(--bg-medium); color:var(--fg-medium)}
.sev-b-low{background:var(--bg-low); color:var(--fg-low)}
.sev-b-info{background:var(--bg-info); color:var(--fg-info)}
.rid{font-weight:600; font-size:.82rem; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.rtitle{font-weight:500}
.rmeta{font-size:.8rem; color:var(--muted); overflow-wrap:anywhere}
.dom-badge{font-size:.72rem; padding:2px 7px; border-radius:5px; background:#f5f5f4; color:var(--muted); border:1px solid var(--line); white-space:nowrap}
.st{font-size:.72rem; padding:2px 7px; border-radius:5px; white-space:nowrap; border:1px solid transparent}
.st-new{background:var(--accent-soft); color:#3730a3; border-color:#c7d2fe}
.st-carried-over{background:#f5f5f4; color:var(--muted); border-color:var(--line)}
.st-unchanged{background:#f5f5f4; color:var(--ink-2); border-color:var(--line-strong)}
.st-improved{background:#dcfce7; color:#166534; border-color:#bbf7d0}
.rowbody{display:none; padding:0 14px 18px; border-top:1px solid var(--line)}
.row.open .rowbody{display:block}
.row.open .caret{transform:rotate(90deg)}
.caret{display:inline-block; transition:transform .12s; color:var(--muted)}
.rowbody h4{margin:1.1em 0 .3em; font-size:.76rem; letter-spacing:.07em; text-transform:uppercase; color:var(--muted); font-weight:600}
.rowbody p{margin:0; max-width:72ch; color:var(--ink-2)}
.rowbody .loc{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:.84rem; overflow-wrap:anywhere; color:var(--ink-2)}
.empty{padding:28px 14px; color:var(--muted)}
@media (max-width:719px){
.tablehead{display:none}
.rowhead{grid-template-columns:1fr; gap:8px; padding:14px}
.rowhead .line1{display:flex; flex-wrap:wrap; gap:6px; align-items:center}
.rowhead .line3{display:flex; flex-wrap:wrap; gap:4px 12px}
}
@media (min-width:720px){
.rowhead .line1{display:contents}
.rowhead .line3{display:contents}
}
.eff{font-size:.8rem; color:var(--muted)}
.rloc{display:none; font-size:.76rem; color:var(--muted); overflow-wrap:anywhere; flex-basis:100%}
@media (max-width:719px){ .rloc{display:block} }
/* ---------- chart ---------- */
.chart{margin-top:1.6em; max-width:660px}
.chart svg{width:100%; height:auto; display:block}
/* ---------- details sections ---------- */
details.block{background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:2px 18px}
details.block > summary{cursor:pointer; padding:16px 0; font-weight:600; list-style:none; min-height:44px; display:flex; align-items:center; gap:8px}
details.block > summary::-webkit-details-marker{display:none}
details.block > summary::before{content:"›"; color:var(--muted); font-size:1.2em; line-height:1; transition:transform .12s}
details.block[open] > summary::before{transform:rotate(90deg)}
details.block .inner{padding:0 0 18px}
details.block ul{padding-left:1.2em; margin:.2em 0 1.2em}
details.block li{margin-bottom:.5em; max-width:72ch}
.ack{border-left:3px solid var(--line-strong); padding-left:14px; margin-bottom:1.4em; opacity:.92}
.ack h4{margin:0 0 .2em; font-size:.98rem; font-weight:600}
.opt{margin-bottom:1.4em}
.opt h3{margin-bottom:.25em}
.opt p{color:var(--ink-2); max-width:72ch}
</style>
</head>
<body>
<header>
<div class="head-grid">
<div class="head-left">
<h1 class="title">Projekt-Audit</h1>
<p class="subtitle">@spearwolf/shadow-objects (Monorepo) · 2026-08-30</p>
<div class="badges"><span class="badge">TypeScript 7</span><span class="badge">pnpm 11</span><span class="badge">turborepo 2.10</span><span class="badge">esbuild 0.28</span><span class="badge">vitest 4</span><span class="badge">Playwright 1.62</span><span class="badge">Biome 2.5</span><span class="badge">Node 24</span><span class="badge">Web Components</span><span class="badge">Web Worker</span></div>
</div>
<div class="scorebox">
<div class="score">73,5<small> / 100</small></div>
<div class="subscores">
<b>Code & Laufzeit</b> 85 ·
<b>Projekt-Harness</b> 88,5
</div>
</div>
</div>
<div class="delta">
<span class="trend down">▼ −20,5</span> gegenüber 94 vom 2026-08-30
<span class="note">(Prüftiefe, nicht Codeverfall)</span><br>
<span class="note">3 Findings behoben, 0 verbessert, 17 neu seit 2026-08-30. 27 übernommen, jedes davor an seiner Fundstelle nachgeprüft.</span>
</div>
</header>
<div class="wrap">
<section>
<p class="eyebrow">Projektportrait</p>
<p class="prose">Shadow Objects ist ein Entity-Component-System für die Browser-Plattform: die Anwendungslogik verlässt den UI-Thread und läuft in einer Shadow Environment, die entweder auf dem Haupt-Thread oder in einem Web Worker steht. Die View — DOM, Canvas, ein Framework — spannt den Entity-Baum über Custom Elements auf und rendert; was passiert, entscheiden Shadow Objects, die der Kernel anhand von Tokens an die Entities hängt. Zwischen beiden Seiten liegt ein asynchrones Protokoll aus serialisierten Change Trails, und derselbe Shadow-Object-Code läuft unverändert in beiden Umgebungen. Das Repository ist ein pnpm-Monorepo mit zwei veröffentlichten Paketen — dem Framework und einem Offscreen-Canvas-Element als Referenzanwendung — und zwei Prüfständen.</p>
<div class="domains"><div class="dom card"><h3>Entity-Kern</h3><p>Kernel, Entities, Registry und der Creation Scope eines Shadow Objects: Lebenszyklus, Token-Routing, Entity-Kontexte und der Abbau in der richtigen Reihenfolge.</p><div class="chips"><span class='chip'>packages/shadow-objects/src/in-the-dark/</span></div></div><div class="dom card"><h3>View-Brücke</h3><p>Die Buchführung der View-Seite: welche Komponenten es gibt, wie sie hängen, was sich seit dem letzten Change Trail geändert hat — und die Fassade, die daraus einen Synchronisationszyklus macht.</p><div class="chips"><span class='chip'>packages/shadow-objects/src/view/ComponentContext.ts</span><span class='chip'>ViewComponent.ts</span><span class='chip'>ComponentChanges.ts</span><span class='chip'>ShadowEnv.ts</span></div></div><div class="dom card"><h3>Umgebungs-Proxys</h3><p>Die zwei austauschbaren Laufzeiten hinter derselben Schnittstelle — in-process oder über einen Worker — samt Nachrichtenprotokoll, Handshake, Zeitfenstern und Fehlerpfaden.</p><div class="chips"><span class='chip'>packages/shadow-objects/src/view/LocalShadowObjectEnv.ts</span><span class='chip'>view/RemoteWorkerEnv.ts</span><span class='chip'>src/worker/</span></div></div><div class="dom card"><h3>DOM-Elemente</h3><p>Die drei Custom Elements, über die eine Seite den Entity-Baum aufspannt: shae-ent, shae-prop und shae-worker — mit Slot-Projektion, Shadow-Root-Grenzen und Namensräumen.</p><div class="chips"><span class='chip'>packages/shadow-objects/src/elements/</span></div></div><div class="dom card"><h3>Offscreen-Canvas</h3><p>Das zweite veröffentlichte Paket: ein Custom Element, das sein Canvas an die Shadow Environment übergibt, plus fünf Shadow Objects vom 2D-Kontext bis zum Three-Mehrfachrenderer.</p><div class="chips"><span class='chip'>packages/shae-offscreen-canvas/</span></div></div><div class="dom card"><h3>Prüfstände</h3><p>Zwei nicht veröffentlichte Pakete: die Integrationssuite in echtem Chromium und die Playwright-Strecke gegen eine Vite-gehostete Seite.</p><div class="chips"><span class='chip'>packages/shadow-objects-testing/</span><span class='chip'>packages/shadow-objects-e2e/</span></div></div></div>
<figure><div class="svg-scroll"><svg viewBox="0 0 860 260" role="img"
aria-label="Datenfluss von der View über die Shadow Environment in den Kernel und zurück"
style="max-width:860px">
<defs>
<marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#4338ca"/>
</marker>
<marker id="ar2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#78716c"/>
</marker>
</defs>
<rect x="8" y="8" width="392" height="244" rx="10" fill="#ffffff" stroke="#e7e5e4"/>
<rect x="460" y="8" width="392" height="244" rx="10" fill="#ffffff" stroke="#e7e5e4"/>
<text x="24" y="32" font-size="12" font-weight="600" fill="#57534e">VIEW · Hauptthread</text>
<text x="476" y="32" font-size="12" font-weight="600" fill="#57534e">SHADOW ENVIRONMENT · Thread oder Worker</text>
<rect x="28" y="52" width="352" height="46" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
<text x="204" y="73" text-anchor="middle" font-size="13" font-weight="600" fill="#3730a3"><shae-ent> · <shae-prop> · <shae-worker></text>
<text x="204" y="89" text-anchor="middle" font-size="11" fill="#4c46a8">DOM spannt den Entity-Baum auf</text>
<rect x="28" y="118" width="168" height="46" rx="8" fill="#fafaf9" stroke="#d6d3d1"/>
<text x="112" y="140" text-anchor="middle" font-size="12.5" font-weight="600" fill="#1c1917">ComponentContext</text>
<text x="112" y="155" text-anchor="middle" font-size="10.5" fill="#57534e">Ist-Zustand + Änderungen</text>
<rect x="212" y="118" width="168" height="46" rx="8" fill="#fafaf9" stroke="#d6d3d1"/>
<text x="296" y="140" text-anchor="middle" font-size="12.5" font-weight="600" fill="#1c1917">ShadowEnv</text>
<text x="296" y="155" text-anchor="middle" font-size="10.5" fill="#57534e">Sync-Zyklus</text>
<rect x="28" y="184" width="352" height="44" rx="8" fill="#fafaf9" stroke="#d6d3d1"/>
<text x="204" y="205" text-anchor="middle" font-size="12.5" font-weight="600" fill="#1c1917">LocalShadowObjectEnv · RemoteWorkerEnv</text>
<text x="204" y="220" text-anchor="middle" font-size="10.5" fill="#57534e">austauschbarer Proxy hinter IShadowObjectEnvProxy</text>
<rect x="480" y="52" width="352" height="46" rx="8" fill="#fafaf9" stroke="#d6d3d1"/>
<text x="656" y="73" text-anchor="middle" font-size="12.5" font-weight="600" fill="#1c1917">MessageRouter · WorkerRuntime</text>
<text x="656" y="89" text-anchor="middle" font-size="10.5" fill="#57534e">nur im Worker-Fall</text>
<rect x="480" y="118" width="168" height="46" rx="8" fill="#fafaf9" stroke="#d6d3d1"/>
<text x="564" y="140" text-anchor="middle" font-size="12.5" font-weight="600" fill="#1c1917">Kernel</text>
<text x="564" y="155" text-anchor="middle" font-size="10.5" fill="#57534e">Lebenszyklus + Registry</text>
<rect x="664" y="118" width="168" height="46" rx="8" fill="#fafaf9" stroke="#d6d3d1"/>
<text x="748" y="140" text-anchor="middle" font-size="12.5" font-weight="600" fill="#1c1917">Entities</text>
<text x="748" y="155" text-anchor="middle" font-size="10.5" fill="#57534e">Baum, Props, Kontexte</text>
<rect x="480" y="184" width="352" height="44" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
<text x="656" y="205" text-anchor="middle" font-size="13" font-weight="600" fill="#3730a3">Shadow Objects</text>
<text x="656" y="220" text-anchor="middle" font-size="10.5" fill="#4c46a8">die Anwendungslogik, per Token angehängt</text>
<line x1="204" y1="98" x2="204" y2="116" stroke="#78716c" stroke-width="1.4" marker-end="url(#ar2)"/>
<line x1="196" y1="141" x2="210" y2="141" stroke="#78716c" stroke-width="1.4" marker-end="url(#ar2)"/>
<line x1="296" y1="164" x2="296" y2="182" stroke="#78716c" stroke-width="1.4" marker-end="url(#ar2)"/>
<line x1="564" y1="98" x2="564" y2="116" stroke="#78716c" stroke-width="1.4" marker-end="url(#ar2)"/>
<line x1="648" y1="141" x2="662" y2="141" stroke="#78716c" stroke-width="1.4" marker-end="url(#ar2)"/>
<line x1="748" y1="164" x2="748" y2="182" stroke="#78716c" stroke-width="1.4" marker-end="url(#ar2)"/>
<text x="430" y="112" text-anchor="middle" font-size="9.5" font-weight="600" fill="#4338ca">Change Trail</text>
<line x1="402" y1="122" x2="458" y2="122" stroke="#4338ca" stroke-width="2" marker-end="url(#ar)"/>
<text x="430" y="170" text-anchor="middle" font-size="9.5" fill="#57534e">Messages</text>
<line x1="458" y1="180" x2="402" y2="180" stroke="#78716c" stroke-width="1.6" stroke-dasharray="4 3" marker-end="url(#ar2)"/>
</svg></div><figcaption>Der Datenfluss in einer Richtung pro Pfeil. Die View spannt über die drei Custom Elements den Entity-Baum auf und schreibt ihn in den ComponentContext; ShadowEnv bündelt die aufgelaufenen Änderungen zu einem Change Trail und reicht ihn über einen austauschbaren Proxy weiter, entweder in-process oder über einen Worker. Auf der anderen Seite wendet der Kernel den Trail auf Entities an und hängt daran die Shadow Objects. Zurück läuft nur Ereignisverkehr: Nachrichten eines Shadow Objects an seine View-Komponente.</figcaption></figure>
</section>
<section>
<p class="eyebrow">Zusammenfassung nach Bereich</p>
<div class="grid2">
<div class="card">
<div class="domain-head"><h3>Code & Laufzeit</h3>
<div class="domain-score">85<span> Score</span></div></div>
<p class="prose">Der Kern hält, was seine Kommentare versprechen: gescheiterte Aufbauten werden zurückgenommen, jeder Abbauschritt steht hinter seinem eigenen Wächter, und 94,89 % der Anweisungen des Kernpakets sind geprüft. Genau ein Befund fällt aus diesem Bild, und er fällt weit: die geteilte FrameLoop ist die einzige Fan-out-Zustellung des Projekts ohne runGuarded(), und ein einziger werfender Listener legt damit »auto-sync« und Canvas-Rendering der ganzen Seite still, ohne ein Wort. Daneben stehen vier mittlere Punkte, von denen keiner ein Fehler ist: eine unbehandelte Rejection im Render-Pfad des Canvas-Pakets, zwei Klassen, die zu viel auf einmal tun, und die quadratischen Einfügekosten der geordneten Listen auf der View-Seite. Die vier kleinen Punkte sind durchweg Asymmetrien zu Regeln, die das Projekt sonst überall einhält.</p>
<div class="bars"><div class="bar-row"><span class="bar-label">kritisch</span><span class="bar-track"></span><span class="bar-count">0</span></div><div class="bar-row"><span class="bar-label">hoch</span><span class="bar-track"><span class="bar-fill f-high" style="width:25.0%"></span></span><span class="bar-count">1</span></div><div class="bar-row"><span class="bar-label">mittel</span><span class="bar-track"><span class="bar-fill f-medium" style="width:100.0%"></span></span><span class="bar-count">4</span></div><div class="bar-row"><span class="bar-label">gering</span><span class="bar-track"><span class="bar-fill f-low" style="width:100.0%"></span></span><span class="bar-count">4</span></div><div class="bar-row"><span class="bar-label">Hinweis</span><span class="bar-track"><span class="bar-fill f-info" style="width:75.0%"></span></span><span class="bar-count">3</span></div></div>
<div class="cats"><div class="cat-row"><span>Architektur & Struktur</span><span>2</span></div><div class="cat-row"><span>Bugs & Korrektheitsrisiken</span><span>2</span></div><div class="cat-row"><span>Lesbarkeit & Clean Code</span><span>2</span></div><div class="cat-row"><span>Performance</span><span>2</span></div><div class="cat-row"><span>Async & Concurrency</span><span>1</span></div><div class="cat-row"><span>Konsistenz</span><span>1</span></div><div class="cat-row"><span>Memory Leaks & Ressourcen</span><span>1</span></div><div class="cat-row"><span>Öffentliche API</span><span>1</span></div></div>
</div>
<div class="card">
<div class="domain-head"><h3>Projekt-Harness</h3>
<div class="domain-score">88,5<span> Score</span></div></div>
<p class="prose">Das Gerüst ist der ruhigere der beiden Bereiche und war es schon vorher: Lint und Typecheck laufen ohne Diagnose, 1 420 Vitest-Fälle und 654 Playwright-Fälle sind grün, pnpm audit meldet null Advisories, und beide veröffentlichten Pakete halten ihren Auslieferungsumfang gegen eine aufgezeichnete Erwartung. Zwei mittlere Punkte treffen genau die Stelle, an der diese Absicherung endet: das Skript, das die Versionsangaben für die Veröffentlichung auflöst, parst YAML mit Zeilen-Regex und schreibt bei einem Fehlschlag ein »catalog:« wörtlich ins publizierte Manifest, wo keine Zusicherung es abfängt; und die Reaktivitäts-Grundlage steht als versionsgenaues Beta in den Laufzeitabhängigkeiten beider Pakete. Die fünfzehn kleinen Punkte sind zum größten Teil Testlücken an Zweigen, die nur außerhalb des Regelfalls laufen, dazu ein zweites publiziertes Paket ohne Typen und ohne Typprüfung.</p>
<div class="bars"><div class="bar-row"><span class="bar-label">kritisch</span><span class="bar-track"></span><span class="bar-count">0</span></div><div class="bar-row"><span class="bar-label">hoch</span><span class="bar-track"></span><span class="bar-count">0</span></div><div class="bar-row"><span class="bar-label">mittel</span><span class="bar-track"><span class="bar-fill f-medium" style="width:13.3%"></span></span><span class="bar-count">2</span></div><div class="bar-row"><span class="bar-label">gering</span><span class="bar-track"><span class="bar-fill f-low" style="width:100.0%"></span></span><span class="bar-count">15</span></div><div class="bar-row"><span class="bar-label">Hinweis</span><span class="bar-track"><span class="bar-fill f-info" style="width:100.0%"></span></span><span class="bar-count">15</span></div></div>
<div class="cats"><div class="cat-row"><span>Testabdeckung & Teststrategie</span><span>13</span></div><div class="cat-row"><span>Developer Experience</span><span>9</span></div><div class="cat-row"><span>Typsicherheit</span><span>6</span></div><div class="cat-row"><span>Projektaufbau & Build</span><span>3</span></div><div class="cat-row"><span>Dependencies</span><span>1</span></div></div>
</div>
</div>
</section>
<section>
<p class="eyebrow">Backlog</p>
<div class="filters">
<div class="fgroup"><span>Bereich</span>
<button class="chipbtn" type="button" data-filter="domain" data-value="all" aria-pressed="true">alle</button>
<button class="chipbtn" type="button" data-filter="domain" data-value="code">Code & Laufzeit</button>
<button class="chipbtn" type="button" data-filter="domain" data-value="harness">Projekt-Harness</button>
</div>
<div class="fgroup"><span>Schweregrad</span>
<button class="chipbtn" type="button" data-filter="severity" data-value="all" aria-pressed="true">alle</button>
<button class="chipbtn" type="button" data-filter="severity" data-value="high" aria-pressed="false">hoch (1)</button><button class="chipbtn" type="button" data-filter="severity" data-value="medium" aria-pressed="false">mittel (6)</button><button class="chipbtn" type="button" data-filter="severity" data-value="low" aria-pressed="false">gering (19)</button><button class="chipbtn" type="button" data-filter="severity" data-value="info" aria-pressed="false">Hinweis (18)</button>
</div>
<div class="fgroup"><span>Status</span>
<button class="chipbtn" type="button" data-filter="status" data-value="all" aria-pressed="true">alle</button>
<button class="chipbtn" type="button" data-filter="status" data-value="carried-over" aria-pressed="false">übernommen</button><button class="chipbtn" type="button" data-filter="status" data-value="new" aria-pressed="false">neu</button>
</div>
<div class="fgroup"><span>Kategorie</span>
<select class="chipbtn" id="f-category" data-filter="category"><option value="all">Alle Kategorien</option></select>
</div>
</div>
<p class="count" id="count"></p>
<div class="tablehead">
<span>Grad</span><span>ID</span><span>Titel</span><span>Kategorie</span><span>Aufwand</span><span>Bereich · Status</span>
</div>
<div class="rows" id="rows"></div>
</section>
<section>
<p class="eyebrow">Optimierungspotenzial</p>
<div class="card"><div class="opt"><h3>Die Wächter-Regel maschinell durchsetzen statt sie zu wiederholen</h3><p>Das Projekt hat eine klare Regel: eine Zustellung an mehrere Empfänger läuft unter runGuarded(), damit ein Ausfall nur sich selbst kostet. Sie steht in runGuarded.ts, in der API-Referenz und in einem Dutzend Kommentaren. BUG-001 ist die eine Stelle, an der sie fehlt, und sie ist niemandem aufgefallen, weil nichts sie prüft. Eine Biome-Regel wird das nicht leisten. Was es leistet, ist eine Konvention mit Zähnen: jeder emit() über eine Sammlung bekommt einen Spec-Fall mit einem werfenden Empfänger. Fünf oder sechs Fälle, und die Regel prüft sich selbst.</p></div><div class="opt"><h3>Eine gemeinsame Zusicherung für die Form beider dist-Manifeste</h3><p>Beide veröffentlichten Pakete haben eine distContract-Spec, und beide prüfen dasselbe Muster in zwei Fassungen. Die Prüfungen, die nicht paketspezifisch sind — kein catalog:/workspace: in den Abhängigkeiten (siehe BUILD-004), jeder Einstiegspunkt zeigt auf eine existierende Datei, keine .d.ts importiert über eine Quellendung —, ließen sich als eine Handvoll exportierter Helfer in scripts/ ablegen, die beide Specs aufrufen. Dann bekommt ein drittes Paket sie am Tag seiner Veröffentlichung mit, statt sie abzuschreiben.</p></div><div class="opt"><h3>Die Messzahlen aus den Kommentaren in eine Datei ziehen</h3><p>An collectPeerReRequest() steht eine ganze Messreihe im Doc-Kommentar, mit Datum, Browser und Playwright-Version. Das ist die einzige Performance-Grundlinie des Projekts, und sie liegt in einem Kommentar in der Mitte einer 1 009-Zeilen-Datei. PERF-001 und PERF-002 brauchen genau solche Zahlen, um überhaupt entschieden werden zu können. Eine docs/performance.md mit Datum, Messaufbau und Tabelle macht die vorhandene Reihe auffindbar und gibt der nächsten einen Platz.</p></div></div>
</section>
<section>
<details class="block">
<summary>Methodik, Prüfumfang und Score-Berechnung</summary>
<div class="inner">
<h3>Score</h3>
<p class="prose">Start bei 100, Abzug je Finding: critical −10, high −5, medium −2, low −0,5, info 0, Untergrenze 0. Dieselbe Formel läuft dreimal: einmal über alle Findings und je einmal über die einer Domain. Die beiden Teilscores sind keine Summanden des Gesamtscores und ergeben zusammen nicht 100 — sie stehen auf derselben Skala und bewerten je einen Bereich für sich.</p>
<p class="prose"><b>Gesamt 73,5</b> = 100 − 1 × 5 (hoch) − 6 × 2 (mittel) − 19 × 0,5 (gering) − 18 × 0 (Hinweis).
<b>Code & Laufzeit 85</b> aus 1 hoch, 4 mittel, 4 gering, 3 Hinweisen.
<b>Projekt-Harness 88,5</b> aus 2 mittel, 15 gering, 15 Hinweisen.</p>
<div class="chart"><svg viewBox="0 0 640 210" role="img"
aria-label="Verlauf der Health-Scores seit 2026-05-14, aktuell 73,5">
<line x1="34" y1="176.0" x2="628" y2="176.0" stroke="#e7e5e4" stroke-width="1"/><text x="27" y="179.5" text-anchor="end" font-size="9" fill="#57534e">0</text><line x1="34" y1="97.0" x2="628" y2="97.0" stroke="#e7e5e4" stroke-width="1"/><text x="27" y="100.5" text-anchor="end" font-size="9" fill="#57534e">50</text><line x1="34" y1="18.0" x2="628" y2="18.0" stroke="#e7e5e4" stroke-width="1"/><text x="27" y="21.5" text-anchor="end" font-size="9" fill="#57534e">100</text>
<polyline points="34.0,94.6 65.3,142.0 96.5,114.4 127.8,48.0 159.1,59.1 190.3,59.9 221.6,42.5 252.8,73.3 284.1,55.9 315.4,51.2 346.6,97.0 377.9,77.2 409.2,50.4 440.4,47.2 471.7,39.3 502.9,52.8 534.2,37.0 565.5,30.6 596.7,27.5 628.0,59.9" fill="none" stroke="#4338ca" stroke-width="1.8" stroke-linejoin="round"/>
<circle cx="34.0" cy="94.6" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="65.3" cy="142.0" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="96.5" cy="114.4" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="127.8" cy="48.0" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="159.1" cy="59.1" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="190.3" cy="59.9" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="221.6" cy="42.5" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="252.8" cy="73.3" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="284.1" cy="55.9" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="315.4" cy="51.2" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="346.6" cy="97.0" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="377.9" cy="77.2" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="409.2" cy="50.4" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="440.4" cy="47.2" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="471.7" cy="39.3" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="502.9" cy="52.8" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="534.2" cy="37.0" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="565.5" cy="30.6" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="596.7" cy="27.5" r="2.4" fill="#fafaf9" stroke="#4338ca" stroke-width="1.6"/><circle cx="628.0" cy="59.9" r="4.2" fill="#4338ca" stroke="#4338ca" stroke-width="1.6"/><text x="628.0" y="48.9" text-anchor="end" font-size="11" font-weight="600" fill="#4338ca">73,5</text><text x="34" y="198" font-size="9.5" fill="#57534e">2026-05-14</text><text x="628" y="198" text-anchor="end" font-size="9.5" fill="#57534e">2026-08-30</text>
</svg>
<p class="rmeta" style="margin-top:.6em">Verlauf der Health-Scores seit 2026-05-14. 20 Läufe, Audits und Remediation-Buchungen gemischt.</p></div>
<h3 style="margin-top:1.6em">Einordnung des Score-Sprungs</h3>
<p class="prose">Der Vorwert von 94,0 ist keine Messung, und der Vorgänger-Report sagt das in seiner eigenen Methodik-Sektion: für ihn wurde kein Code neu gelesen, die Zahl ist die Buchführung eines Remediation-Laufs vom 2026-08-29/30 auf dem Prüfumfang des Audits vom 2026-08-27, und der Bereich »Projekt-Harness« war dabei ausdrücklich nicht im Scope. Die letzte gemessene Bewertung war 78,0 am 2026-08-27; gegen die beträgt das Delta −4,5. Seit dem Vorgänger-Report hat sich keine Zeile Quelltext bewegt: git diff zwischen dem Report-Commit und HEAD zeigt genau eine gelöschte remediation-plan.md. Der Sprung kommt also vollständig aus der Prüftiefe, nicht aus Codeverfall. Dieser Lauf hat drei Bereiche bewertet, die im Befundsatz des Vorgängers gar nicht vorkommen: die Fehlerpfade von FrameLoop.ts und ThreeRenderView.js, die Dimensionen Architektur und Performance, und die Build-Skripte samt Abhängigkeitsgraph. Der Hochwert-Befund BUG-001 wurde dabei nicht gelesen, sondern mit einer Wegwerf-Spec reproduziert.</p>
<h3>Gelesen</h3><ul><li>Alle 74 Nicht-Spec-Quelldateien unter packages/shadow-objects/src/ und packages/shae-offscreen-canvas/src/ vollständig gelesen, 11 793 Zeilen, einschließlich Kernel.ts, Entity.ts, ComponentContext.ts, ComponentChanges.ts, ShaeEntElement.ts, ShaePropElement.ts, ShaeWorkerElement.ts, RemoteWorkerEnv.ts, ShadowObjectCreationScope.ts, FrameLoop.ts und ConsoleLogger.ts in ganzer Länge.</li><li>packages/shae-offscreen-canvas/ vollständig: das Custom Element, alle sieben Shadow Objects, die geteilten Konstanten, build.mjs, distContract.files.txt und beide Manifeste.</li><li>Harness vollständig: package.json aller vier Pakete, pnpm-workspace.yaml, turbo.json, tsconfig.json, biome.json, .editorconfig, .nvmrc, mise.toml, .gitignore, beide GitHub-Workflows, scripts/makePackageJson.mjs und scripts/publishNpmPkg.mjs, alle drei vitest.config.ts, vitest.setup.ts und playwright.config.ts.</li><li>Dokumentation: AGENTS.md, CLAUDE.md, README.md und die drei Paket-READMEs, TODO.md, KNOWN-DEFECTS.md, TEST-PLAN.md, die Verzeichnisse beider docs/-Ordner nach Umfang, distContract.spec.ts vollständig.</li><li>Ausgeführt: pnpm lint:ci (229 Dateien, 0 Diagnosen), pnpm typecheck (3 Tasks grün), pnpm test:ci und ein erzwungener Testlauf ohne Cache (30 + 28 + 6 Dateien, 902 + 383 + 135 Fälle, alle grün), pnpm outdated -r, pnpm audit (0 Advisories), der zusammengeführte Coverage-Bericht pro Datei, eine Zeilenklassifikation über Code gegen Kommentar, und eine Wegwerf-Spec zur Reproduktion von BUG-001, die danach wieder entfernt wurde.</li></ul>
<h3>Nicht gelesen</h3><ul><li>Die Spec-Dateien wurden nur dort gelesen, wo ein Befund sie betrifft. 26 867 der 42 241 Zeilen JavaScript und TypeScript im Repository stehen in 75 Spec- und Test-Dateien; die Bewertung der Testlage stützt sich auf den Coverage-Bericht, den TEST-PLAN und Stichproben.</li><li>packages/shadow-objects-e2e/src/ und tests/ nur an den Fundstellen der Altbefunde, für den TEST-PLAN-Abgleich und für die Suche nach Risikomustern.</li><li>docs/api-reference.md (3 220 Zeilen) wurde nicht Zeile für Zeile gelesen, sondern an den Stellen, die Altbefunde nennen, sowie über eine Zeilenlängen-Auswertung.</li><li>Die E2E-Strecke wurde nicht ausgeführt: sie braucht drei installierte Playwright-Browser und läuft in CI in einem eigenen Job. Ihre Grünmeldung ist aus dem Workflow und dem TEST-PLAN übernommen, nicht gemessen.</li><li>Die vorhandene ./audit.html blieb bis zum Merge geschlossen; sie ist in keinem Befund dieses Laufs eine Quelle.</li></ul>
<h3>Abgleich mit dem Vorlauf vom 2026-08-30</h3>
<p class="prose">Primär über Kategorie plus überlappende Location, sekundär über Titelähnlichkeit. Jeder der 30 Altbefunde wurde vor der Übernahme an seiner Fundstelle nachgelesen; drei ließen sich nicht mehr belegen und sind entfallen, drei weitere hatten sich verschoben und tragen die korrigierte Fundstelle samt Vermerk. Die verbleibenden 27 sind carried-over. Kontextbedingt entfernt wurde keiner.</p>
<p class="prose">Entfallen sind:</p>
<ul><li><b>DX-024</b> · generateUUID.ts steht heute bei 76 Zeilen, die Hex-Tabelle ist durch toString(16).padStart(2,'0') ersetzt, und im Repository gibt es keinen einzigen prettier-Treffer mehr.</li><li><b>TEST-015</b> · generateUUID.spec.ts hat einen dritten Fall bekommen ('says once that a realm without Web Crypto falls to Math.random'); der Coverage-Bericht weist die Datei mit 100 % in allen vier Maßen aus.</li><li><b>DX-029</b> · Die genannte Stelle (api-reference.md:1822) ist heute eine 20 Zeichen lange Tabellenzeile. Ein Zählen über die Datei zeigt Dutzende Prosazeilen jenseits von 120 Spalten, also beschreibt der Befund als Einzelstelle nichts mehr; Biome formatiert Markdown-Prosa nicht.</li></ul>
<h3>Theme</h3><p class="prose">Light, übernommen aus summary.theme des Vorgänger-Audits; in dieser Sitzung wurde keine andere Vorgabe gemacht.</p>
</div>
</details>
</section>
<section>
<details class="block">
<summary>Anhang · Akzeptierte und zurückgestellte Punkte (1)</summary>
<div class="inner">
<p class="prose">Diese Punkte ruhen auf ausdrücklichen Wunsch. Sie zählen nicht in den Health-Score und stehen in keinem Backlog; jeder von ihnen lässt sich jederzeit wieder aktivieren.</p>
<div class="ack"><h4>Der Demo-Dev-Server schaltet die Host-Prüfung ab</h4><p class="rmeta">Sicherheit · <span class="mono">packages/shae-offscreen-canvas/vite.config.js:1-5</span></p><p>Vom Nutzer am 2026-08-26 so akzeptiert, wie es ist: der Schalter betrifft ausschließlich pnpm dev und nicht das publizierte Paket.</p><p class="rmeta">Zurückgestellt: 2026-08-26</p></div>
</div>
</details>
</section>
</div>
<script id="audit-data" type="application/json">{"summary": {"project": "@spearwolf/shadow-objects (Monorepo)", "stack": ["TypeScript 7", "pnpm 11", "turborepo 2.10", "esbuild 0.28", "vitest 4", "Playwright 1.62", "Biome 2.5", "Node 24", "Web Components", "Web Worker"], "date": "2026-08-30", "previousDate": "2026-08-30", "theme": "light", "score": 73.5, "previousScore": 94.0, "scoreDelta": -20.5, "deltaCause": "coverage", "deltaExplanation": "Der Vorwert von 94,0 ist keine Messung, und der Vorgänger-Report sagt das in seiner eigenen Methodik-Sektion: für ihn wurde kein Code neu gelesen, die Zahl ist die Buchführung eines Remediation-Laufs vom 2026-08-29/30 auf dem Prüfumfang des Audits vom 2026-08-27, und der Bereich »Projekt-Harness« war dabei ausdrücklich nicht im Scope. Die letzte gemessene Bewertung war 78,0 am 2026-08-27; gegen die beträgt das Delta −4,5. Seit dem Vorgänger-Report hat sich keine Zeile Quelltext bewegt: git diff zwischen dem Report-Commit und HEAD zeigt genau eine gelöschte remediation-plan.md. Der Sprung kommt also vollständig aus der Prüftiefe, nicht aus Codeverfall. Dieser Lauf hat drei Bereiche bewertet, die im Befundsatz des Vorgängers gar nicht vorkommen: die Fehlerpfade von FrameLoop.ts und ThreeRenderView.js, die Dimensionen Architektur und Performance, und die Build-Skripte samt Abhängigkeitsgraph. Der Hochwert-Befund BUG-001 wurde dabei nicht gelesen, sondern mit einer Wegwerf-Spec reproduziert.", "bySeverity": {"critical": 0, "high": 1, "medium": 6, "low": 19, "info": 18}, "byCategory": {"Testabdeckung & Teststrategie": 13, "Developer Experience": 9, "Typsicherheit": 6, "Projektaufbau & Build": 3, "Architektur & Struktur": 2, "Bugs & Korrektheitsrisiken": 2, "Lesbarkeit & Clean Code": 2, "Performance": 2, "Async & Concurrency": 1, "Dependencies": 1, "Konsistenz": 1, "Memory Leaks & Ressourcen": 1, "Öffentliche API": 1}, "findingCount": 44, "resolvedCount": 3, "improvedCount": 0, "newCount": 17, "carriedOverCount": 27, "scoreHistory": [{"date": "2026-05-14", "score": 51.5}, {"date": "2026-08-13", "score": 21.5}, {"date": "2026-08-14", "score": 39, "source": "remediation"}, {"date": "2026-08-19", "score": 81, "source": "remediation"}, {"date": "2026-08-19", "score": 74, "source": "audit"}, {"date": "2026-08-20", "score": 73.5, "source": "remediation"}, {"date": "2026-08-20", "score": 84.5, "source": "remediation"}, {"date": "2026-08-21", "score": 65, "source": "audit"}, {"date": "2026-08-21", "score": 76, "source": "remediation"}, {"date": "2026-08-22", "score": 79, "source": "remediation"}, {"date": "2026-08-23", "score": 50, "source": "audit"}, {"date": "2026-08-23", "score": 62.5, "source": "remediation"}, {"date": "2026-08-24", "score": 79.5, "source": "remediation"}, {"date": "2026-08-26", "score": 81.5, "source": "remediation"}, {"date": "2026-08-26", "score": 86.5, "source": "remediation"}, {"date": "2026-08-27", "score": 78, "source": "audit"}, {"date": "2026-08-28", "score": 88, "source": "remediation"}, {"date": "2026-08-28", "score": 92.0, "source": "remediation"}, {"date": "2026-08-30", "score": 94.0, "source": "remediation"}, {"date": "2026-08-30", "score": 73.5, "source": "audit"}], "domains": {"code": {"label": "Code & Laufzeit", "score": 85.0, "bySeverity": {"critical": 0, "high": 1, "medium": 4, "low": 4, "info": 3}, "byCategory": {"Architektur & Struktur": 2, "Bugs & Korrektheitsrisiken": 2, "Lesbarkeit & Clean Code": 2, "Performance": 2, "Async & Concurrency": 1, "Konsistenz": 1, "Memory Leaks & Ressourcen": 1, "Öffentliche API": 1}, "executiveSummary": "Der Kern hält, was seine Kommentare versprechen: gescheiterte Aufbauten werden zurückgenommen, jeder Abbauschritt steht hinter seinem eigenen Wächter, und 94,89 % der Anweisungen des Kernpakets sind geprüft. Genau ein Befund fällt aus diesem Bild, und er fällt weit: die geteilte FrameLoop ist die einzige Fan-out-Zustellung des Projekts ohne runGuarded(), und ein einziger werfender Listener legt damit »auto-sync« und Canvas-Rendering der ganzen Seite still, ohne ein Wort. Daneben stehen vier mittlere Punkte, von denen keiner ein Fehler ist: eine unbehandelte Rejection im Render-Pfad des Canvas-Pakets, zwei Klassen, die zu viel auf einmal tun, und die quadratischen Einfügekosten der geordneten Listen auf der View-Seite. Die vier kleinen Punkte sind durchweg Asymmetrien zu Regeln, die das Projekt sonst überall einhält."}, "harness": {"label": "Projekt-Harness", "score": 88.5, "bySeverity": {"critical": 0, "high": 0, "medium": 2, "low": 15, "info": 15}, "byCategory": {"Testabdeckung & Teststrategie": 13, "Developer Experience": 9, "Typsicherheit": 6, "Projektaufbau & Build": 3, "Dependencies": 1}, "executiveSummary": "Das Gerüst ist der ruhigere der beiden Bereiche und war es schon vorher: Lint und Typecheck laufen ohne Diagnose, 1 420 Vitest-Fälle und 654 Playwright-Fälle sind grün, pnpm audit meldet null Advisories, und beide veröffentlichten Pakete halten ihren Auslieferungsumfang gegen eine aufgezeichnete Erwartung. Zwei mittlere Punkte treffen genau die Stelle, an der diese Absicherung endet: das Skript, das die Versionsangaben für die Veröffentlichung auflöst, parst YAML mit Zeilen-Regex und schreibt bei einem Fehlschlag ein »catalog:« wörtlich ins publizierte Manifest, wo keine Zusicherung es abfängt; und die Reaktivitäts-Grundlage steht als versionsgenaues Beta in den Laufzeitabhängigkeiten beider Pakete. Die fünfzehn kleinen Punkte sind zum größten Teil Testlücken an Zweigen, die nur außerhalb des Regelfalls laufen, dazu ein zweites publiziertes Paket ohne Typen und ohne Typprüfung."}}}, "portrait": {"description": "Shadow Objects ist ein Entity-Component-System für die Browser-Plattform: die Anwendungslogik verlässt den UI-Thread und läuft in einer Shadow Environment, die entweder auf dem Haupt-Thread oder in einem Web Worker steht. Die View — DOM, Canvas, ein Framework — spannt den Entity-Baum über Custom Elements auf und rendert; was passiert, entscheiden Shadow Objects, die der Kernel anhand von Tokens an die Entities hängt. Zwischen beiden Seiten liegt ein asynchrones Protokoll aus serialisierten Change Trails, und derselbe Shadow-Object-Code läuft unverändert in beiden Umgebungen. Das Repository ist ein pnpm-Monorepo mit zwei veröffentlichten Paketen — dem Framework und einem Offscreen-Canvas-Element als Referenzanwendung — und zwei Prüfständen.", "domains": [{"name": "Entity-Kern", "text": "Kernel, Entities, Registry und der Creation Scope eines Shadow Objects: Lebenszyklus, Token-Routing, Entity-Kontexte und der Abbau in der richtigen Reihenfolge.", "paths": ["packages/shadow-objects/src/in-the-dark/"]}, {"name": "View-Brücke", "text": "Die Buchführung der View-Seite: welche Komponenten es gibt, wie sie hängen, was sich seit dem letzten Change Trail geändert hat — und die Fassade, die daraus einen Synchronisationszyklus macht.", "paths": ["packages/shadow-objects/src/view/ComponentContext.ts", "ViewComponent.ts", "ComponentChanges.ts", "ShadowEnv.ts"]}, {"name": "Umgebungs-Proxys", "text": "Die zwei austauschbaren Laufzeiten hinter derselben Schnittstelle — in-process oder über einen Worker — samt Nachrichtenprotokoll, Handshake, Zeitfenstern und Fehlerpfaden.", "paths": ["packages/shadow-objects/src/view/LocalShadowObjectEnv.ts", "view/RemoteWorkerEnv.ts", "src/worker/"]}, {"name": "DOM-Elemente", "text": "Die drei Custom Elements, über die eine Seite den Entity-Baum aufspannt: shae-ent, shae-prop und shae-worker — mit Slot-Projektion, Shadow-Root-Grenzen und Namensräumen.", "paths": ["packages/shadow-objects/src/elements/"]}, {"name": "Offscreen-Canvas", "text": "Das zweite veröffentlichte Paket: ein Custom Element, das sein Canvas an die Shadow Environment übergibt, plus fünf Shadow Objects vom 2D-Kontext bis zum Three-Mehrfachrenderer.", "paths": ["packages/shae-offscreen-canvas/"]}, {"name": "Prüfstände", "text": "Zwei nicht veröffentlichte Pakete: die Integrationssuite in echtem Chromium und die Playwright-Strecke gegen eine Vite-gehostete Seite.", "paths": ["packages/shadow-objects-testing/", "packages/shadow-objects-e2e/"]}], "diagramCaption": "Der Datenfluss in einer Richtung pro Pfeil. Die View spannt über die drei Custom Elements den Entity-Baum auf und schreibt ihn in den ComponentContext; ShadowEnv bündelt die aufgelaufenen Änderungen zu einem Change Trail und reicht ihn über einen austauschbaren Proxy weiter, entweder in-process oder über einen Worker. Auf der anderen Seite wendet der Kernel den Trail auf Entities an und hängt daran die Shadow Objects. Zurück läuft nur Ereignisverkehr: Nachrichten eines Shadow Objects an seine View-Komponente."}, "findings": [{"id": "BUG-001", "category": "Bugs & Korrektheitsrisiken", "domain": "code", "severity": "high", "status": "new", "title": "Ein werfender Frame-Listener legt die geteilte FrameLoop still", "location": "packages/shadow-objects/src/utils/FrameLoop.ts:536-562", "description": "#onFrame verteilt den Frame über ein synchrones emit(). Eventize bricht eine Zustellung beim ersten Listener ab, der wirft, und reicht den Fehler an den Aufrufer zurück, hier also an den requestAnimationFrame-Callback. Die Zeile, die den nächsten Frame anfordert, steht darunter und wird nie erreicht: #rafID ist zu diesem Zeitpunkt schon auf 0 zurückgesetzt, subscriptionCount meldet seine Ziele weiterhin, und niemand fordert je wieder einen Frame an. Die Schleife ist tot, und nichts sagt es. FrameLoop.get() ist eine Singleton-Instanz pro Realm, an der <shae-worker> sein auto-sync und <shae-offscreen-canvas> sein Rendering hängen: ein einziges fehlerhaftes Ziel friert beide für die ganze Seite ein. Überall sonst in dieser Codebasis läuft eine Fan-out-Zustellung unter runGuarded(), genau damit ein Ausfall nur sich selbst kostet.", "recommendation": "Den emit in runGuarded() legen, so wie Kernel.destroyEntity() und ComponentContext.#deliverPeerReRequests() es tun, oder das erneute Anfordern in ein finally ziehen. Dazu ein Spec-Fall mit einem werfenden Ziel: FrameLoop.spec.ts hat 20 Fälle, und keiner davon lässt einen Listener werfen.", "effort": "S", "evidence": "Mit einer Wegwerf-Spec im Paket verifiziert und danach wieder entfernt (2026-08-30): zwei Ziele, das erste wirft, requestAnimationFrame gestubbt. Nach dem ersten Frame steht die Warteschlange bei 0 angeforderten Frames, während subscriptionCount 2 meldet, und das zweite Ziel hat nie einen Frame gesehen."}, {"id": "ARCH-001", "category": "Architektur & Struktur", "domain": "code", "severity": "medium", "status": "new", "title": "ShaeEntElement trägt sechs Zuständigkeiten in einer Klasse", "location": "packages/shadow-objects/src/elements/ShaeEntElement.ts (1 060 Zeilen); daneben view/ComponentContext.ts (1 009) und in-the-dark/Kernel.ts (993)", "description": "In einer Datei liegen: der Custom-Element-Lebenszyklus, der Namensraumwechsel samt Umzug in einen anderen ComponentContext, das Auflösungsprotokoll für den Entity-Elternteil über drei CustomEvents, ein Register der beantworteten <slot>s mit WeakRef-Buchführung und modulweiter WeakMap, ein Monkey-Patch auf ViewComponent.dispatchEvent für forward-custom-events, und ein MutationObserver auf den Elternknoten. Jedes Stück ist für sich begründet; zusammen sind es 539 Zeilen Code und 368 Zeilen Kommentar, die niemand mehr am Stück im Kopf hält. Bezahlt wird das im Review: eine Änderung am Slot-Register muss gegen das Elternprotokoll geprüft werden, weil beide über dieselben Ereignisse laufen und beide in denselben Callback zurückführen.", "recommendation": "Zwei Nähte liegen frei. Das Slot-Register (entHostOfSlot, reRequestedForSlotChange, #hostedSlots, #watchHostedSlot, #releaseHostedSlot(s), #collectHostedSlots, #onHostedSlotChange, askEveryoneToReRequest) ist heute schon halb modulweit und trägt außer der Zugehörigkeit keinen Elementzustand: es passt in ein eigenes Modul mit einer Handvoll Funktionen und einem Register. Der dispatchEvent-Patch ist das zweite geschlossene Stück, rund 55 Zeilen mitsamt Cleanup. Was bleibt, ist ein Element mit Lebenszyklus und Elternauflösung, und das ist die Klasse, die der Name verspricht.", "effort": "L", "evidence": "Zeilenzählung über die Nicht-Spec-Quellen (2026-08-30) und vollständige Lektüre der Datei; die sechs Zuständigkeiten sind an ihren jeweiligen Feldern und Methoden ausgezählt."}, {"id": "ARCH-002", "category": "Architektur & Struktur", "domain": "code", "severity": "medium", "status": "new", "title": "Der Element-Lebenszyklus steht zweimal im Repository", "location": "packages/shadow-objects/src/elements/ShaeElement.ts:83-282 gegen elements/ShaePropElement.ts:98-437", "description": "ShaePropElement erbt nicht von ShaeElement, und der Grund dafür steht ausführlich im Kopf der Datei: eine Property wählt keine Umgebung und hätte an einem ns-Attribut nichts. Die Folge ist, dass sie den ganzen Lebenszyklus-Vertrag nachbaut: das Feldpaar #destroyed/#subscribed samt der Begründung, warum es zwei sein müssen, den DeferredTeardown, das Paar restore()/teardown(), ein destroy(), das die Flagge vor die Arbeit setzt, den ensureDisplayContentsRule-Aufruf im connectedCallback und die hibernate()-Klammer darum. Die Doc-Kommentare stehen an beiden Stellen nahezu wortgleich, an einer Stelle mit dem ausdrücklichen Verweis auf die andere. Wer den Vertrag ändert, ändert ihn zweimal, und ein Auseinanderlaufen bleibt still: kein Test hält die beiden gegeneinander.", "recommendation": "Den Vertrag in eine gemeinsame Basis ohne Namensraum ziehen, aus der ShaeElement die ns-Behandlung ergänzt und von der ShaePropElement direkt erbt. Wo eine Klassenhierarchie nicht passt, tut es ein Mixin oder eine ElementLifecycle-Hilfsklasse, an die beide delegieren. Entscheidend ist nur, dass die Regel 'Flagge vor der Arbeit' an einer Stelle steht und an einer geprüft wird.", "effort": "M", "evidence": "Beide Dateien vollständig gelesen (2026-08-30); die sechs Bestandteile des Vertrags sind in beiden Klassen benannt und einzeln gegenübergestellt."}, {"id": "BUG-002", "category": "Async & Concurrency", "domain": "code", "severity": "medium", "status": "new", "title": "Ein fehlgeschlagener Render endet als unbehandelte Rejection, Frame für Frame", "location": "packages/shae-offscreen-canvas/src/shadow-objects/ThreeRenderView.js:387-411", "description": "Der OnFrame-Listener ist async und wartet auf multiViewRenderer.renderView(view), umschlossen von try/finally ohne catch. #renderViewNow() wirft bei einer fremden View ausdrücklich ('not my view'), und WebGLRenderer.render() wie createImageBitmap() können es ebenfalls. Eventize erwartet das zurückgegebene Promise nicht, also verlässt die Rejection den Listener und landet als unhandled rejection im Realm. Das finally setzt frameInFlight zurück, der nächste Frame versucht dasselbe, und die Meldung wiederholt sich mit der Frame-Rate. In der E2E-Strecke fiele das auf, denn zwei Seiten prüfen ausdrücklich auf unbehandelte Fehler; der Renderer wird dort nur nicht ausgeführt.", "recommendation": "Ein catch neben das finally, das den Grund über den ConsoleLogger meldet. Die eigentliche Entscheidung liegt dahinter: ein 'not my view' wiederholt sich bei jedem Frame und ist ein Programmierfehler, der einmal gemeldet und dann stillgelegt gehört; ein verlorener WebGL-Kontext ist etwas anderes und darf es erneut versuchen.", "effort": "S", "evidence": "An der Fundstelle nachgelesen (2026-08-30): try { … await … } finally { frameInFlight = false } ohne catch, und der Listener ist über on(entity, OnFrame, Priority.Low, async () => …) registriert."}, {"id": "PERF-001", "category": "Performance", "domain": "code", "severity": "medium", "status": "new", "title": "Die geordneten Listen der View-Seite kosten pro Operation eine lineare Suche", "location": "packages/shadow-objects/src/view/ComponentContext.ts:950-966, :329-380, :480-498", "description": "#appendToOrdered() prüft zuerst mit includes(), ob die uuid schon dasteht, und sucht dann linear die Einfügestelle, wobei jeder Schritt einen Map-Lookup auf #components macht. addToChildren(), removeFromParent() und changeOrder() legen mit removeFrom() je eine weitere lineare Suche darüber. n Geschwister unter einem Elternteil aufzubauen ist damit O(n²), und für #rootComponents gilt dasselbe. Die Größenordnung steht im Repository selbst: der Kommentar an collectPeerReRequest() beziffert einen Build über 600 Wurzeln in einem Namensraum mit 42 ms bei abgeschaltetem Peer-Kanal, und das ist genau diese Rechnung.", "recommendation": "Der Preis liegt in der Datenstruktur, nicht im Code. Eine Map<uuid, index> neben jeder geordneten Liste macht includes und removeFrom konstant; die Einfügestelle bleibt linear, wird aber billiger, weil die order der Geschwister nicht mehr über #components geholt werden muss. Vorher messen und die Zahl neben die vorhandene aus collectPeerReRequest() schreiben: unterhalb einiger Dutzend Geschwister ist das Array die schnellere Struktur, und dann bleibt es besser, wie es ist.", "effort": "M", "evidence": "Die drei Fundstellen gelesen (2026-08-30); die 42-ms-Zahl stammt aus dem Doc-Kommentar an ComponentContext.collectPeerReRequest(), gemessen laut Kommentar am 2026-08-22 in Chromium."}, {"id": "BUILD-004", "category": "Projektaufbau & Build", "domain": "harness", "severity": "medium", "status": "new", "title": "Ein nicht aufgelöstes catalog: wird als wörtliche Versionsangabe veröffentlicht", "location": "scripts/makePackageJson.mjs:50-71 und :73-120; packages/shadow-objects/src/distContract.spec.ts:88-97", "description": "makePackageJson.mjs liest pnpm-workspace.yaml mit einem Zeilen-Regex und übersetzt daraus jedes \"<dep>\": \"catalog:\" in die echte Version. Findet der Parser den Eintrag nicht, schreibt er ein console.warn und lässt die Angabe stehen. Im veröffentlichten package.json steht dann \"@spearwolf/eventize\": \"catalog:\", und das Paket ist für jeden Konsumenten uninstallierbar. Der Weg dorthin ist kurz, weil der Parser keine YAML-Bibliothek ist: ein mehrzeiliger Wert, ein Kommentar an einer neuen Stelle, ein Schlüssel mit Doppelpunkt im Namen. Aufgefangen wird es von nichts. distContract.spec.ts prüft ausdrücklich nur die Namen der Abhängigkeiten und nicht ihre Ranges, mit der guten Begründung, dass Ranges sich bei jedem Release bewegen; und der Publish-Schritt in der CI liest keine Warnung, er liest einen Exit-Code.", "recommendation": "Eine Zusicherung in distContract.spec.ts, die keinen konkreten Range festschreibt, sondern nur die Form: kein Wert unter dependencies und peerDependencies beginnt mit catalog: oder workspace:. Das ist genau der Fehler, um den es geht, und er ändert sich bei keinem Release. Beide Pakete haben eine solche Spec, also gilt die Zeile zweimal. Stärker und billiger noch: makePackageJson.mjs bricht mit Exit-Code 1 ab, statt zu warnen.", "effort": "S", "evidence": "Skript und Spec gelesen (2026-08-30); das aktuell gebaute dist/package.json löst beide Abhängigkeiten korrekt auf, also ist der Befund ein fehlender Wächter und kein aktueller Defekt."}, {"id": "DEP-001", "category": "Dependencies", "domain": "harness", "severity": "medium", "status": "new", "title": "Zwei veröffentlichte Pakete hängen versionsgenau an einem Beta", "location": "pnpm-workspace.yaml (catalog: '@spearwolf/signalize': 1.0.0-beta.0); packages/shadow-objects/package.json (dependencies), packages/shae-offscreen-canvas/package.json (dependencies)", "description": "@spearwolf/signalize ist die Reaktivitäts-Grundlage und steht als Laufzeit-Abhängigkeit in beiden publizierten Paketen, versionsgenau auf 1.0.0-beta.0. Wer @spearwolf/shadow-objects@0.33.0 installiert, bekommt damit ein Prerelease ins Projekt, und ohne Ausweg: ein exakter Pin lässt keine spätere 1.0.0 zu, auch keine Patch-Fassung des Betas. Zusammen mit der Realm-Symbol-Regel, die dieses Repository selbst in pnpm-workspace.yaml und AGENTS.md dokumentiert, wird das scharf: ein Konsument, der signalize auch direkt benutzt und dabei auf ^1.0.0 steht, hat zwei Kopien im Baum, die sich denselben Symbol.for-Slot teilen und an der Grenze werfen statt nur Code zu verdoppeln.", "recommendation": "Der Pin ist richtig, solange es ein Beta ist, und seine Begründung steht im Manifest. Was fehlt, ist der Weg heraus und der Hinweis nach außen. Ein Eintrag unter ## [Unreleased] in beiden CHANGELOGs, der sagt, dass die nächste Minor auf die finale 1.0.0 geht, und ein Satz im Installationsabschnitt beider READMEs, der Konsumenten sagt, dass sie signalize nicht selbst danebenlegen dürfen. Die Regel steht heute nur im Agenten-Leitfaden, und der wird nicht mit veröffentlicht.", "effort": "S", "evidence": "Katalog und beide Manifeste gelesen, dist/package.json und .npm-pkg/package.json geprüft (2026-08-30): beide tragen \"@spearwolf/signalize\": \"1.0.0-beta.0\" in dependencies."}, {"id": "API-001", "category": "Öffentliche API", "domain": "code", "severity": "low", "status": "new", "title": "debug(), info() und warn() des ConsoleLogger fragen ihre eigenen Schalter nicht", "location": "packages/shadow-objects/src/utils/ConsoleLogger.ts:291-325", "description": "Die Klasse bietet isDebug, isInfo und isWarn, und #print() fragt keinen davon: jeder Aufruf landet auf der Konsole, gleichgültig wie die Schalter stehen. Die Gattung ist damit reine Aufruferpflicht. Im Repository wird sie fast überall erfüllt, jeweils als if (this.logger.isWarn) { … }, und die API-Referenz führt die Regel als Tabelle von Aufruf gegen Getter. Drei Aufrufe stehen ohne Klammer da: zwei mit einer Begründung im Kommentar, einer ohne (RemoteWorkerEnv.ts:437, die Meldung über einen Worker, der den Teardown nicht quittiert). Von außen ist Absicht nicht von Vergessen zu unterscheiden. ConsoleLogger ist als eigener Einstiegspunkt exportiert, also trifft die Falle auch Konsumenten: ein logger.debug() ohne Klammer protokolliert für immer in Produktion.", "recommendation": "Den Schalter in die Methode ziehen: debug() prüft isDebug, info() prüft isInfo, warn() prüft isWarn, error() bleibt ungefiltert und behält damit seine dokumentierte Rolle. Die Klammern an den Aufrufstellen dürfen bleiben, wo das Zusammenstellen der Argumente selbst teuer ist, und verschwinden überall sonst. Bleibt es beim heutigen Vertrag, gehört wenigstens die eine unbegründete Stelle in RemoteWorkerEnv.destroy() entweder hinter einen isWarn-Test oder hinter einen Kommentar, der sagt warum nicht.", "effort": "S", "evidence": "ConsoleLogger.ts vollständig gelesen und alle 37 Aufrufe von logger.debug/info/warn in den Nicht-Spec-Quellen auf ihre Klammer geprüft (2026-08-30): 34 gegated, 3 nicht, alle drei in RemoteWorkerEnv.ts."}, {"id": "BUG-003", "category": "Bugs & Korrektheitsrisiken", "domain": "code", "severity": "low", "status": "new", "title": "Entity.removeChild() schneidet das letzte Kind heraus, wenn es das gemeinte nicht findet", "location": "packages/shadow-objects/src/in-the-dark/Entity.ts:360-365", "description": "Die Zugehörigkeit wird über die uuid geprüft (#childrenUuids.has(child.uuid)), das Entfernen über die Identität (#children.splice(#children.indexOf(child), 1)). Fallen die beiden auseinander, liefert indexOf −1, und splice(-1, 1) entfernt das letzte Element der Liste statt keines. Auseinanderfallen können sie, sobald zwei Entity-Instanzen dieselbe uuid tragen. Der Kernel lässt das nicht zu, und Entity steht in keinem Export, also ist der Fall über die öffentliche Oberfläche heute nicht erreichbar. Der Wächter davor ist trotzdem einer, und er prüft etwas anderes, als er schützt.", "recommendation": "Den Index einmal lesen und beide Zweige daran hängen: const idx = this.#children.indexOf(child); if (idx !== -1) { this.#childrenUuids.delete(child.uuid); this.#children.splice(idx, 1); }. ComponentContext.removeFromParent() macht es auf der View-Seite bereits genau so, und diese Zeile brächte die beiden Seiten wieder zur Deckung.", "effort": "S", "evidence": "An der Fundstelle nachgelesen (2026-08-30); die View-seitige Entsprechung steht in ComponentContext.ts:338-342 und liest den Index vor der Prüfung."}, {"id": "MEM-001", "category": "Memory Leaks & Ressourcen", "domain": "code", "severity": "low", "status": "new", "title": "Kernel.destroy() löst als einziger Teardown seine eigenen Abonnements nicht", "location": "packages/shadow-objects/src/in-the-dark/Kernel.ts:961-992", "description": "Der Kernel wird im Konstruktor eventized und sendet MessageToView. Sein destroy() räumt Entities, Wurzelkontexte und die Traversierungs-Caches, ruft aber kein off(this). ViewComponent.destroy(), ShadowEnv.destroy(), Entity[onDestroy]() und SignalsPath.dispose() tun das alle vier. Heute fällt es nicht auf, weil MessageRouter sich selbst mit off(this.kernel, this) abmeldet und der LocalShadowObjectEnv mitsamt seinem Kernel fallengelassen wird. Der Kernel ist aber öffentliche API und über shadow-objects.js exportiert: wer selbst on(kernel, MessageToView, …) schreibt, bleibt nach dem Teardown abonniert, hält den Kernel darüber am Leben und kann von einer Nachricht erreicht werden, die während des Abbaus über dispatchMessageToView() in eine Mikrotask gelegt wurde.", "recommendation": "off(this) an das Ende von destroy(), hinter das Aufräumen der Wurzelkontexte. Die Reihenfolge ist die von ShadowEnv.destroy(): erst zustellen, was noch zuzustellen ist, dann die Leitungen kappen. Ein Fall in der Kernel-Spec, der nach destroy() eine Nachricht auslöst und prüft, dass kein Listener sie sieht, hält die Regel danach fest.", "effort": "S", "evidence": "grep über beide Pakete (2026-08-30): off(this) steht in ViewComponent.ts, ShadowEnv.ts, Entity.ts und SignalsPath.ts, in Kernel.ts stehen nur die beiden off(entity, shadowObject)."}, {"id": "PERF-002", "category": "Performance", "domain": "code", "severity": "low", "status": "new", "title": "changeProperty() durchsucht dieselbe Liste dreimal, einmal pro Eigenschaft und Frame", "location": "packages/shadow-objects/src/view/ComponentChanges.ts:183-221", "description": "#propsChangeOrder ist ein Array, weil die Reihenfolge der Änderungen zählt, und der Kommentar sagt das auch. Jeder Aufruf macht darauf ein includes(), und dann je nach Zweig ein removeFrom() oder ein appendToEnd(), die beide selbst indexOf plus splice sind. Das sind bis zu drei lineare Durchläufe für eine einzelne Eigenschaft. Der Pfad ist heiß: <shae-prop> schreibt darüber bei jeder Attributänderung, und ShaeOffscreenCanvasElement setzt in seinem OnFrame-Handler vier Eigenschaften pro Frame.", "recommendation": "Ein Set neben dem Array, das nur die Zugehörigkeit beantwortet, macht das includes konstant und kostet eine Zeile in jedem der drei Schreibpfade. Bei den heutigen Größenordnungen, eine Handvoll Eigenschaften pro Komponente, ist das Aufräumen und keine Rettung; es lohnt sich, sobald eine Komponente zweistellig viele Eigenschaften pro Frame bewegt. Erst messen, dann ändern.", "effort": "S", "evidence": "Die Methode gelesen (2026-08-30) und die drei Aufrufe von includes/removeFrom/appendToEnd ausgezählt; array-utils.ts zeigt, dass beide Helfer über indexOf laufen."}, {"id": "BUILD-002", "category": "Projektaufbau & Build", "domain": "harness", "severity": "low", "status": "carried-over", "title": "Das zweite publizierte Paket liefert keine Typen und wird nie typgeprüft", "location": "packages/shae-offscreen-canvas/package.json:19-31, :52-62", "description": "@spearwolf/shae-offscreen-canvas wird als Quelldistribution aus reinem JavaScript publiziert. Weder package.json noch die exports-Bedingungen führen einen types-Eintrag, und es entsteht keine .d.ts: ein TypeScript-Consumer bekommt any für das gesamte Paket, einschließlich des Custom Elements und der fünf registrierten Shadow Objects. Dazu fehlt dem Paket — wie auch shadow-objects-testing — ein typecheck-Skript, weshalb pnpm typecheck drei statt vier Packages prüft und pnpm run ci die Hälfte des Workspace nie durch den Typprüfer schickt. Das Kernpaket macht beides vor: tsconfig.lib.json emittiert Deklarationen, tsconfig.json prüft den ganzen Baum.", "recommendation": "checkJs mit einer eigenen tsconfig einschalten und Deklarationen aus den JSDoc-Kommentaren emittieren, oder das Paket nach TypeScript ziehen. Ersteres ist der kleinere Schritt und bringt beides auf einmal: Typen für den Consumer und ein typecheck-Skript, das turbo aufnehmen kann.", "effort": "M", "evidence": "Gemessen (2026-08-19): pnpm typecheck meldet »Packages in scope: 4« und »Tasks: 3 successful, 3 total«. Weder types noch eine .d.ts sind im Manifest oder unter .npm-pkg/ zu finden. Beim Re-Check am 2026-08-27 an der genannten Stelle erneut nachgelesen und bestätigt."}, {"id": "BUILD-003", "category": "Projektaufbau & Build", "domain": "harness", "severity": "low", "status": "carried-over", "title": "Das Canvas-Paket veröffentlicht vier Beispielmodule, die niemand importieren kann", "location": "packages/shae-offscreen-canvas/src/distContract.files.txt:12-14,17; src/worker-sample.js; package.json:19-31", "description": "Der aufgezeichnete Paketinhalt führt src/worker-sample.js und die drei Shadow Objects unter src/shadow-objects/sample/ auf — sie gehen mit jedem Release an die Registry. Erreichbar sind sie dort nicht: die exports-Map des Pakets nennt drei Einstiegspunkte, und eine exports-Map ist abschließend, also läuft jeder Import auf diese Dateien ins Leere. Zwei Eigenschaften machen die Fracht unschön statt bloß überflüssig. CubeScene.js importiert three, das für dieses Paket nur eine Peer-Abhängigkeit ist — ein Konsument ohne three trägt eine Datei mit einem unauflösbaren Import in seinem node_modules. Und worker-sample.js schreibt beim Laden eine console.debug-Zeile, ungefiltert, an dem ConsoleLogger vorbei, den das Paket sonst überall benutzt. Im Coverage-Bericht steht das ganze sample-Verzeichnis bei 0 %.", "recommendation": "Den filter in build.mjs um das sample-Verzeichnis und worker-sample.js erweitern, so wie er die Specs und die distContract-Fixtures schon aussortiert, und distContract.files.txt im selben Zug nachziehen. Sollen die Beispiele Konsumenten erreichen, ist der andere Weg der richtige: ein Eintrag in exports, ein Abschnitt in der README und ein Testfall — dann sind es Beispiele und keine Reste.", "effort": "S", "evidence": "distContract.files.txt gegen package.json#exports gelesen (2026-08-27); CubeScene.js:2 importiert aus three, worker-sample.js:22 ruft console.debug. Im zusammengeführten Bericht steht src/shadow-objects/sample bei 0 %."}, {"id": "DX-026", "category": "Developer Experience", "domain": "harness", "severity": "low", "status": "carried-over", "title": "Zwei Stellen sprechen über eine .npmrc, die es nicht mehr gibt", "location": "scripts/publishNpmPkg.mjs:80; CLAUDE.md, Abschnitt »pnpm 11 specifics«", "description": "publishNpmPkg.mjs kopiert vor dem Veröffentlichen die .npmrc der Wurzel in das Paketverzeichnis, und CLAUDE.md hält als Regel fest, dass .npmrc ausschließlich Auth und Registry trägt, während jede pnpm-Einstellung in pnpm-workspace.yaml gehört. Die Datei ist seit Commit d8d83fe nicht mehr im Repository. Der Kopiervorgang läuft ins Leere — copyFile prüft auf Existenz und tut sonst nichts —, also fällt niemandem etwas auf; die Regel in CLAUDE.md dagegen beschreibt eine Aufteilung, deren eine Hälfte fehlt, und wer sie befolgt, legt die Datei womöglich neu an.", "recommendation": "Beides an die Lage anpassen: den Kopierschritt in preparePackageRoot() streichen oder mit einem Satz begründen, warum er für den Fall stehenbleibt, dass wieder eine .npmrc gebraucht wird. In CLAUDE.md den Halbsatz so umschreiben, dass er sagt, was gilt — jede pnpm-Einstellung steht in pnpm-workspace.yaml, eine .npmrc führt das Projekt nicht.", "effort": "S", "evidence": "ls -a in der Wurzel (2026-08-27): keine .npmrc. git log -- .npmrc endet bei d8d83fe »remove ignore-workspace-root-check from .npmrc«."}, {"id": "DX-030", "category": "Developer Experience", "domain": "harness", "severity": "low", "status": "new", "title": "pnpm clean lässt das Coverage-Verzeichnis der Integrationssuite stehen", "location": "package.json (Skript clean); packages/shadow-objects-testing/package.json (scripts)", "description": "pnpm clean ist turbo run clean plus ein rimraf auf dist coverage node_modules/.cache .turbo in der Wurzel. shadow-objects-testing hat als einziges Paket kein clean-Skript, also überspringt turbo es, und das rimraf nennt nur die Wurzel. packages/shadow-objects-testing/coverage/ überlebt damit jeden Aufräumlauf, pnpm cbt eingeschlossen, das CLAUDE.md als »full local cycle« führt. Der nächste Coverage-Merge liest dann ein Verzeichnis, dessen Inhalt aus einem älteren Lauf stammen kann, und der zusammengeführte Bericht sagt nicht, aus welchem.", "recommendation": "Ein \"clean\": \"rimraf coverage\" in packages/shadow-objects-testing/package.json, wie es die drei anderen Pakete haben. Eine Zeile, und die Zusage von pnpm cbt stimmt wieder.", "effort": "S", "evidence": "Die scripts aller vier Pakete aufgelistet (2026-08-30): shadow-objects-testing hat test, watch und update, kein clean. Das Verzeichnis coverage/ liegt dort und ist gitignoriert."}, {"id": "DX-031", "category": "Developer Experience", "domain": "harness", "severity": "low", "status": "new", "title": ".nvmrc und engines.node nennen nicht dieselbe Untergrenze", "location": ".nvmrc (24); mise.toml ([tools] node = \"24\"); package.json (engines.node >=24.13.0)", "description": "Drei Dateien sagen, welches Node gilt, und zwei davon sagen 24, während engines >=24.13.0 verlangt. Wer nvm use oder mise install folgt und dabei auf einer 24.0 landet, erfüllt beide Versionsdateien und verletzt die Engine-Angabe; pnpm meldet das je nach engine-strict-Einstellung gar nicht. In CI fällt es nicht auf, weil actions/setup-node mit node-version-file: .nvmrc die neueste 24.x zieht, also gerade den Fall, den die Datei nicht ausschließt.", "recommendation": ".nvmrc auf 24.13.0 setzen und mise.toml mit. Die Engine-Angabe ist die Aussage, die zählt; die beiden anderen Dateien sollten sie wiederholen statt sie zu unterbieten.", "effort": "S", "evidence": "Die drei Dateien gelesen (2026-08-30) und gegen den Aufruf in .github/workflows/ci.yml gehalten."}, {"id": "DX-032", "category": "Developer Experience", "domain": "harness", "severity": "low", "status": "new", "title": "Die API-Referenz ist über ihre Struktur hinausgewachsen, die Doku des zweiten Pakets folgt keiner", "location": "packages/shadow-objects/docs/api-reference.md (3 220 Zeilen, 223 KB); packages/shae-offscreen-canvas/docs/01-shadow-objects-api.md", "description": "AGENTS.md beschreibt für das Kernpaket eine flache Sieben-Datei-Struktur, und sechs der sieben halten sich daran. Die siebte trägt inzwischen 3 220 Zeilen, ein Fünftel mehr als beim Audit vom 2026-08-27 (3 062), und ist die Datei, in der niemand mehr etwas findet, ohne zu suchen; ein Inhaltsverzeichnis hat sie nicht. Das zweite veröffentlichte Paket hat daneben ein docs/-Verzeichnis mit genau einer Datei, die als einzige im Repository ein Nummernpräfix trägt und auf die kein README.md im selben Verzeichnis zeigt. Für ein Paket, das sich laut AGENTS.md §4 auf dieselbe Weise dokumentieren soll wie das Kernpaket, ist das der Anfang einer Struktur und noch keine.", "recommendation": "Für die API-Referenz reicht ein Inhaltsverzeichnis am Kopf mit Ankern auf die Hauptabschnitte; eine Aufteilung würde die Sieben-Datei-Regel brechen, die AGENTS.md ausdrücklich setzt. Für das Canvas-Paket entweder das Präfix streichen und ein docs/README.md daneben, das die Datei einordnet, oder die eine Datei nach README.md ziehen und das Verzeichnis auflösen, solange es bei einer bleibt.", "effort": "S", "evidence": "Zeilen und Bytes beider Verzeichnisse gezählt (2026-08-30); die 3 062 stammen aus der Methodik-Sektion des Vorgänger-Audits."}, {"id": "DX-033", "category": "Developer Experience", "domain": "harness", "severity": "low", "status": "new", "title": "Das README-Bild wiegt 2,4 MB und ist damit ein Fünftel jedes Clones", "location": "docs/what-is-shadow-objects.webp (2 423 516 Bytes), referenziert in README.md:31", "description": "Die Datei ist das größte Objekt im Repository, größer als die Kernel-Spec und die API-Referenz zusammen, und macht rund ein Fünftel der 12 MB aus, die ein git clone heute holt. Sie ist ein einziges Erklärbild über der README. WebP ist bereits das richtige Format; die Größe kommt aus Auflösung und Qualitätsstufe, nicht aus dem Container. Anders als die drei Architekturbilder, die DX-027 nennt, wird dieses Bild tatsächlich referenziert, es ist nur zu schwer für seinen Zweck.", "recommendation": "Auf die Breite bringen, in der GitHub sie überhaupt darstellt (rund 900 px), und die Qualität auf 80 stellen. Das landet erfahrungsgemäß im niedrigen dreistelligen KB-Bereich. Die alte Fassung bleibt in der Historie, der Austausch kostet also nichts an Nachvollziehbarkeit und auch nichts an bereits geklonter Größe.", "effort": "S", "evidence": "git ls-files nach Größe sortiert (2026-08-30): 2 423 516 Bytes, nächstgrößte Datei 223 957 Bytes. .git misst 12 MB."}, {"id": "TEST-005", "category": "Testabdeckung & Teststrategie", "domain": "harness", "severity": "low", "status": "carried-over", "title": "Die Trennung der drei Cache-Schlüsselräume hält weder ein Typ noch ein Fall", "location": "packages/shadow-objects/src/in-the-dark/ShadowObjectCreationScope.ts:75-88", "description": "Der gemeinsame Reader-Helfer bekommt Reader-Map und Compare-Map als Argumente; drei Aufrufstellen verdrahten je einen der drei Schlüsselräume — Property, Kontext, Elternkontext. Der Typparameter, der sie zusammenhalten soll, sichert nichts: Methodenparameter von Map sind bivariant, der Typprüfer nimmt eine Verdrahtung des Elternkontexts auf die Property-Maps klaglos an. Der Testbestand nimmt sie ebenso an. Beobachtbar würde eine gekreuzte Verdrahtung erst, wenn ein Fall denselben Namen über zwei der drei Wege liest und die Werte gegeneinander hält; kein Fall im Repository tut das.", "recommendation": "Ein Fall, der denselben Namen über useContext und useParentContext liest und zwei verschiedene Werte erwartet. Er beantwortet zugleich die Frage, wodurch die Trennung gesichert ist — durch einen Typ oder durch einen Fall.", "effort": "S", "evidence": "Gemessen (2026-08-20): useParentContext auf die Kontext-Reader-Map umgebogen, beide Spec-Dateien bleiben mit 112 Fällen grün. Beim Re-Check am 2026-08-27 an der genannten Stelle erneut nachgelesen und bestätigt."}, {"id": "TEST-006", "category": "Testabdeckung & Teststrategie", "domain": "harness", "severity": "low", "status": "carried-over", "title": "allowConsoleErrors nimmt zwei E2E-Seiten die Prüfung auf unbehandelte Fehler", "location": "packages/shadow-objects-e2e/tests/sync-failure.spec.ts:22, tests/worker-failure.spec.ts:23", "description": "Das Flag streicht den Fall »no uncaught or logged errors« samt seiner pageerror-Prüfung. Gerade auf einer Seite, deren Gegenstand abgelehnte Promises sind, wird damit die unbehandelte Rejection unsichtbar — dieselbe Klasse Fehler, die die Seite belegen soll. Es betrifft ebenso die Seiten worker-failure und create-element.", "recommendation": "Das Harness so erweitern, dass erwartete Konsolenfehler benannt statt pauschal erlaubt werden; alles andere bleibt dann ein Fehlschlag. Der Zug berührt drei Seiten und die Semantik des Harness und will für sich geplant sein.", "effort": "M", "evidence": "Am 2026-08-27 nachgezählt: der Schalter steht noch in zwei Spec-Dateien, die dritte des Vorlaufs ist ohne ihn ausgekommen."}, {"id": "TEST-012", "category": "Testabdeckung & Teststrategie", "domain": "harness", "severity": "low", "effort": "M", "title": "Die Kernel-Spec fasst 181 Fälle in einer Datei, ein describe davon über dreitausend Zeilen", "location": "packages/shadow-objects/src/in-the-dark/Kernel.spec.ts (6 018 Zeilen, 194 Fälle; describe('Shadow Object Creation API') ab Zeile 267)", "description": "Die Datei ist mit Abstand die größte des Repositories und mehr als sechsmal so lang wie die Klasse, die sie prüft. Dreißig describe-Blöcke stehen nebeneinander, einer davon nimmt gut die Hälfte der Datei ein. Wer einen Fall sucht, scrollt; wer einen hinzufügt, muss raten, wohin er gehört; und ein Lauf, der einen einzelnen Bereich prüfen soll, lädt immer alles. Die übrigen Specs des Pakets liegen zwischen 168 und 1221 Zeilen und zeigen, dass es auch anders geht.", "recommendation": "Entlang der vorhandenen describe-Grenzen aufteilen, etwa in Kernel.creation-api.spec.ts, Kernel.entity-tree.spec.ts, Kernel.teardown.spec.ts und Kernel.change-trail.spec.ts. Die Blöcke sind bereits thematisch sortiert, der Schnitt ist mechanisch, und der gemeinsame Aufbau wandert in eine Hilfsdatei daneben.", "status": "carried-over", "evidence": "Beim Re-Check am 2026-08-27 an der genannten Stelle erneut nachgelesen und bestätigt.", "recheckNote": "Beim Re-Check gewachsen: 6 018 statt 5 838 Zeilen, 194 statt 184 Fälle."}, {"id": "TEST-014", "category": "Testabdeckung & Teststrategie", "domain": "harness", "severity": "low", "effort": "S", "status": "carried-over", "title": "Der Ablehnungsweg gegen eine echte Worker-Umgebung ist von keiner Suite geprüft", "location": "packages/shadow-objects/src/elements/ShaeWorkerElement.ts:449-469; packages/shadow-objects-testing/test/worker-element-attributes.test.js:336", "description": "#refuseLocalChange() schreibt den abgelehnten Wert über zwei Zweige zurück: einen für die lokale Umgebung und einen, der das Attribut entfernt, wenn die Umgebung keine lokale ist. Die drei Fälle in worker-element-attributes.test.js laufen alle gegen eine lokale Umgebung und erreichen damit nur den ersten Zweig. Der zweite trägt die Richtung, die ein Anwender in der Produktion fährt, und niemand prüft ihn.", "recommendation": "Einen Fall nach shadow-objects-e2e legen, wo ein echter Worker läuft: local an einem laufenden <shae-worker> setzen und prüfen, dass das Attribut wieder verschwindet und die Umgebung ihren Modus behält.", "evidence": "Beim Re-Check am 2026-08-27 an der genannten Stelle erneut nachgelesen und bestätigt."}, {"id": "TEST-016", "category": "Testabdeckung & Teststrategie", "domain": "harness", "severity": "low", "status": "carried-over", "title": "Der Transferables-Zweig der lokalen Umgebung ist von keiner Suite berührt", "location": "packages/shadow-objects/src/view/cloneChangeTrail.ts:5-7", "description": "cloneChangeTrail() steht bei 60 %; ungeprüft sind genau die zwei Zeilen, die einen Change-Trail-Eintrag mit Transferables behandeln — structuredClone mit transfer-Liste. Das ist der Weg, auf dem ein ArrayBuffer oder ein OffscreenCanvas in einer LocalShadowObjectEnv an die Shadow Objects geht, und er hat eine Eigenschaft, die keine andere Zeile des Moduls hat: nach dem Klon ist der Puffer auf der View-Seite abgetrennt. Was ein AfterSync-Hörer danach in dem Trail vorfindet, den er als Argument bekommt, ist von keinem Test festgehalten. Das zweite veröffentlichte Paket reicht sein Canvas über genau diesen Mechanismus weiter.", "recommendation": "Ein Fall in LocalShadowObjectEnv.spec.ts, der einen Trail mit einem ArrayBuffer durch applyChangeTrail() schickt: drüben kommt der Inhalt an, hüben ist byteLength null. Ein zweiter mit disableStructuredClone hält die Gegenprobe fest.", "effort": "S", "evidence": "Zusammengeführter Bericht unter coverage/ (2026-08-27): cloneChangeTrail.ts 60 %, nicht abgedeckt die Zeilen 6-7."}, {"id": "TEST-017", "category": "Testabdeckung & Teststrategie", "domain": "harness", "severity": "low", "status": "carried-over", "title": "Ein P1-Fall des E2E-Testplans ist abgedeckt und nirgends als erledigt geführt", "location": "packages/shadow-objects-e2e/TEST-PLAN.md:254", "description": "Der Fall ASYNC-2 — dispatchShadowObjectsEvent von der View zum Shadow Object über einen echten Worker — trägt Priorität P1, ist aber weder als Implemented markiert wie die Zeilen in §3.3, noch in der Offen-Liste im Kopf des Dokuments, noch in der ID-Klammer seiner §2-Zeile. Abgedeckt ist er tatsächlich, und zwar zweimal: src/async-events.js:158 und :177 schicken beide über dispatchShadowObjectsEvent, laufen dort aber unter anderen Fallnummern. Es fehlt die Zuordnung, nicht der Test — und wer den Plan liest, hält eine P1-Lücke für offen, die keine ist.", "recommendation": "Den Fall als Implemented markieren und in seiner ID-Klammer die beiden Stellen nennen, die ihn tatsächlich abdecken. Wenn zwei Fälle denselben Weg prüfen, gehört das in den Plan, damit die nächste Lücken-Suche nicht dieselbe Runde dreht.", "effort": "S", "evidence": "Beim Abgleich von Paket 1 eines Remediation-Laufs am 2026-08-27 gegen den Testcode geprüft; beide Fundstellen gelesen."}, {"id": "TEST-021", "category": "Testabdeckung & Teststrategie", "domain": "harness", "severity": "low", "status": "new", "title": "ShaeWorkerElement ist das am schwächsten geprüfte Modul des Kernpakets", "location": "packages/shadow-objects/src/elements/ShaeWorkerElement.ts (81,98 % Anweisungen, 77,65 % Zweige)", "description": "Das Kernpaket steht bei 94,89 % Anweisungen und 89,38 % Zweigen; dieses Modul liegt dreizehn Punkte darunter und ist damit der einzige echte Ausreißer unter den Kernmodulen. Es trägt start(), den Auto-Sync-Effekt mit seinen vier Schreibweisen (frame, <n>fps, eine Millisekundenzahl, die Aus-Formen), das Lesen der vier Timeout-Attribute und die Ablehnung einer nachträglichen local-Änderung. Der Auto-Sync-Effekt ist der Teil, an dem eine Lücke wehtut: er startet Timer und Frame-Abonnements und muss sie beim Wechsel wieder abbauen, und genau diese Abbauzweige sind es, die die 77,65 % übrig lassen.", "recommendation": "Die vier Schreibweisen von auto-sync gegen ihre Wirkung prüfen, jeweils mit dem Übergang: frame → 30fps → 250 → off, und nach jedem Schritt, dass das vorherige Abonnement wirklich abgebaut wurde. Der Frame-Zweig lässt sich über FrameLoop.get().subscriptionCount beobachten, der Intervall-Zweig über eine gestellte Zeit.", "effort": "M", "evidence": "Aus dem zusammengeführten Coverage-Bericht (Lauf vom 2026-08-30): ShaeWorkerElement.ts 81,98 / 77,65 / 86,48 / 83,66 gegen 94,89 / 89,38 / 96,34 / 96,76 für packages/shadow-objects/src."}, {"id": "TYPE-005", "category": "Typsicherheit", "domain": "harness", "severity": "low", "status": "carried-over", "title": "Ein any in einem öffentlichen Setter, dessen Getter typisiert ist", "location": "packages/shadow-objects/src/elements/ShaeWorkerElement.ts:209", "description": "set autoSync(val: any) nimmt alles an, während der zugehörige Getter typisiert ist. Der Aufrufer bekommt damit an der Schreibstelle keine Hilfe und an der Lesestelle einen genauen Typ — die beiden Hälften derselben Eigenschaft sagen Verschiedenes. Was tatsächlich erlaubt ist, sagt die Doku bereits: string | boolean | number.", "recommendation": "Den Setter auf string | boolean | number bringen. Das deckt sich mit der Zusage der Doku und mit dem, was der Setter ohnehin verarbeitet.", "effort": "S", "evidence": "Beim Re-Check am 2026-08-27 an der genannten Stelle erneut nachgelesen und bestätigt."}, {"id": "CLEAN-018", "category": "Lesbarkeit & Clean Code", "domain": "code", "severity": "info", "effort": "S", "title": "Ein Kommentar im Teardown erklärt sich aus dem Vorzustand", "location": "packages/shadow-objects/src/in-the-dark/ShadowObjectCreationScope.ts:326", "description": "Der Kommentarblock über den Abbauschleifen endet auf »…does not terminate, and never did«. Der Halbsatz hinter dem Komma sagt nichts über den geltenden Zustand, sondern über den vorherigen — und er trägt auch sachlich nicht durch: er stimmt innerhalb einer Menge, über die Mengengrenze hinweg wird die Nichtterminierung erst mit den nachziehenden Runden erreichbar.", "recommendation": "Den Satz nach »does not terminate« enden lassen. Damit fällt der Rückblick weg, und was stehen bleibt, stimmt auch über die Mengengrenze hinweg.", "status": "carried-over", "evidence": "Im Remediation-Lauf vom 2026-08-30 vom Reviewer des Teardown-Pakets als kleiner Befund gemeldet und nicht behoben; an der Fundstelle belegt."}, {"id": "CLEAN-019", "category": "Lesbarkeit & Clean Code", "domain": "code", "severity": "info", "status": "new", "title": "Jede dritte Zeile Quelltext ist ein Kommentar", "location": "packages/shadow-objects/src/ und packages/shae-offscreen-canvas/src/ (6 663 Zeilen Code, 3 445 Zeilen Kommentar, ohne Specs)", "description": "34,1 % aller nicht leeren Zeilen in den Nicht-Spec-Quellen sind Kommentar; ShaeElement.ts steht bei 56,3 %, ComponentContext.ts bei 44,2 %, ShadowEnv.ts bei 42,8 %, Kernel.ts bei 38,6 %. Das ist kein Vorwurf, im Gegenteil: die Kommentare erklären durchweg das Warum statt das Was, und sie tragen Invarianten, die man dem Code nicht ansieht. Es ist eine Feststellung über die Pflegelast. Ein Kommentar, der eine Reihenfolge begründet, altert still, wenn die Reihenfolge sich ändert, und weder Linter noch Test schlagen dabei an. Bei 3 445 Zeilen ist das eine zweite Codebasis ohne eigene Suite.", "recommendation": "Nichts kürzen. Aber dort, wo ein Kommentar eine Invariante behauptet, gehört ein Fall in die Spec, der genau diese Invariante festhält, und zwar mit einem Namen, der den Satz des Kommentars aufnimmt. Die Suite tut das an vielen Stellen bereits vorbildlich; TEST-005 in diesem Backlog nennt eine Stelle, an der sie es nicht tut, und BUG-001 ist der Fall, in dem ein solcher Test den Fehler gefunden hätte.", "effort": "L", "evidence": "Zeilenklassifikation über alle 74 Nicht-Spec-Quelldateien beider Pakete (2026-08-30), Block- und Zeilenkommentare getrennt gezählt, Leerzeilen ausgenommen."}, {"id": "CONS-021", "category": "Konsistenz", "domain": "code", "severity": "info", "effort": "M", "title": "reCreateChanges() verliert eine Eigenschaft, die nur ihren Schlüssel nennt", "location": "packages/shadow-objects/src/view/ComponentContext.ts:824; src/view/ComponentChanges.ts:357", "description": "ComponentPropertiesType führt zwei Formen, und types.ts:23-31 hält sie ausdrücklich auseinander: [name, value] heißt »Wert ist da«, das einträgrige [name] heißt »gesetzt, ohne Wert«. ComponentMemory bewahrt die einträgrige Form treu. reCreateChanges() destrukturiert dagegen const [key, value] und reicht einen solchen Eintrag als changeProperty(key, undefined, …) weiter — aus »gesetzt, ohne Wert« wird damit »Wert ist weg«. ComponentChanges führt seine Eigenschaften als Map<string, unknown> und nimmt beim Bauen des Create-Eintrags jeden undefined-Wert wieder heraus: der Schlüssel fällt ganz weg. Eine so gesetzte Eigenschaft überlebt den Neuaufbau eines Context also nicht.", "recommendation": "Zuerst die dritte Form darstellbar machen. Solange ComponentChanges seine Eigenschaften als Map<string, unknown> führt, kann sie »gesetzt, ohne Wert« von »Wert ist weg« nicht unterscheiden, und jede Korrektur allein an reCreateChanges() läuft eine Grenze weiter wieder auf. Das ist eine Entscheidung über die Darstellung im Change Trail und berührt beide Seiten des Protokolls — sie gehört in einen eigenen Durchgang, nicht in ein Aufräumpaket.", "status": "carried-over", "evidence": "Im Remediation-Lauf vom 2026-08-30 in Zug 0 von Paket 6 beim Nachsehen der Aufrufer von filterUndefinedProps() aufgefallen und an der Fundstelle belegt; vorbestehend, unverändert seit df9f9b9. Der Nutzer hat den Punkt in der Drain-Runde ausdrücklich einem eigenen Lauf zugewiesen."}, {"id": "DX-009", "category": "Developer Experience", "domain": "harness", "severity": "info", "status": "carried-over", "effort": "S", "title": "Kein Fenster in das Component Memory", "location": "packages/shadow-objects/src/view/ComponentContext.ts:109", "description": "Das Component Memory ist privat und hat keinen lesenden Zugang. Wer prüfen will, ob ein Zustand gespeichert wurde — im Test wie in der Diagnose —, muss den Umweg über `reCreateChanges()` nehmen und damit den ganzen Namespace neu aufbauen.", "recommendation": "Ein lesender Durchreicher, etwa `hasComponentState(uuid)`, kostet wenige Zeilen und ersetzt einen Umweg mit Nebenwirkungen.", "evidence": "Beim Re-Check am 2026-08-27 an der genannten Stelle erneut nachgelesen und bestätigt."}, {"id": "DX-023", "category": "Developer Experience", "domain": "harness", "severity": "info", "status": "carried-over", "title": "Die Wurzel-README trägt Werbesprache in ihren Strukturlisten", "location": "README.md:95-96", "description": "Die Liste unter »Examples & Testing« bewertet, was sie beschreiben soll: »A reference implementation demonstrating heavy lifting!«, »proving the power of Transferables and Namespaces«, »Massive test suite spanning unit tests (vitest) …«. Die Information steht in beiden Fällen im jeweils zweiten Halbsatz — drei Suiten, drei Werkzeuge, benannt; der Rest ist Ton. Wertadjektive ohne Bezugsgröße können nicht falsch werden und werden deshalb nie korrigiert, anders als die Zeilenzahl in derselben Liste, die der Remediation-Lauf vom 2026-08-26 um den Faktor 2,5 danebenliegend vorfand.", "recommendation": "Einen Durchgang über die Strukturlisten der Datei, nicht Satz für Satz. Der Lauf vom 2026-08-26 hat drei Befunde dieser Sorte in derselben Datei gefunden, jeder beim Anfassen des vorigen; das ist ein Redigat und kein Einzelfix.", "effort": "S", "evidence": "Nebenbefunde aus dem Remediation-Lauf vom 2026-08-26, zwei Fundstellen zu einem Eintrag gebündelt; vorbestehend, git show bfcc54b:README.md trägt beide Sätze wörtlich. Beim Re-Check am 2026-08-27 an der genannten Stelle erneut nachgelesen und bestätigt."}, {"id": "DX-027", "category": "Developer Experience", "domain": "harness", "severity": "info", "status": "carried-over", "title": "Drei Architekturbilder im Doku-Verzeichnis, auf die nichts zeigt", "location": "packages/shadow-objects/docs/architecture.svg, architecture@2x.png, architecture.afdesign", "description": "Die drei Dateien belegen zusammen rund 308 KB im Doku-Verzeichnis des Kernpakets, und keine Markdown-Datei, kein Quelltext und keine README verweist auf sie. Eine davon ist eine .afdesign — das Binärformat von Affinity Designer, das ohne dieses Programm niemand öffnet. Ein Lauf im August hat bereits zwei unreferenzierte Illustrationen entfernt (Commit 1cff638); diese drei sind übriggeblieben, ohne dass irgendwo stünde, ob absichtlich.", "recommendation": "Entweder einbinden — das Diagramm gehört in docs/concepts.md, wo die Architektur beschrieben wird — oder entfernen. Bleibt die .afdesign als Quelle des SVG stehen, gehört ein Satz in docs/README.md, der sie als solche benennt; sonst ist sie beim nächsten Aufräumen wieder eine Datei ohne Grund.", "effort": "S", "evidence": "grep nach den Dateinamen über alle .md, .ts, .js und .html des Repositories (2026-08-27): kein Treffer außerhalb der Dateien selbst."}, {"id": "DX-028", "category": "Developer Experience", "domain": "harness", "severity": "info", "effort": "S", "title": "Ein Abschnitt des Cheat-Sheets steht ohne den Trenner der übrigen", "location": "packages/shadow-objects/docs/cheat-sheet.md:501 (## FrameLoop)", "description": "## FrameLoop beginnt ohne die ---, die jeder andere Abschnitt der Datei vor sich führt. Beim Überfliegen verschwimmt die Grenze zum Abschnitt darüber.", "recommendation": "Den Trenner ergänzen.", "status": "carried-over", "evidence": "Im Remediation-Lauf vom 2026-08-28 an der Fundstelle aufgefallen und dort belegt; der Code wurde für diesen Stand nicht neu geprüft.", "recheckNote": "Beim Re-Check verschoben: 14 Abschnitte, 12 Trenner; ohne Trenner steht heute ## FrameLoop auf Zeile 501."}, {"id": "TEST-004", "category": "Testabdeckung & Teststrategie", "domain": "harness", "severity": "info", "status": "carried-over", "title": "Das Pending-Gate der Re-Subscription ist von außen unbeobachtbar", "location": "packages/shadow-objects/src/elements/ShaeEntElement.ts:210-224", "description": "Das Gate bündelt mehrere Abbauten desselben Tasks zu einem Abonnier-Durchlauf. Mit entschärftem Gate bleibt die gesamte Suite grün: jeder Effekt-Neulauf räumt vor dem Abonnieren auf, zwei Microtasks liefern dasselbe Ergebnis wie einer. Es ist eine Sparmaßnahme, keine Korrektheitswache — wer es entfernt, merkt es an keinem Test.", "recommendation": "Entweder einen Fall bauen, der die Ersparnis misst statt das Ergebnis, oder im Code festhalten, dass das Gate bewusst nicht testbar ist. Ein Wächter, der nur das Ergebnis prüft, hält es nie.", "effort": "S", "evidence": "Gemessen (2026-08-18): mit auf void this.#reSubscribePending; entschärftem Gate bleibt das Integrationspaket vollständig grün. Beim Re-Check am 2026-08-27 an der genannten Stelle erneut nachgelesen und bestätigt."}, {"id": "TEST-007", "category": "Testabdeckung & Teststrategie", "domain": "harness", "severity": "info", "status": "carried-over", "title": "Drei Behauptungen in einem it, von denen die erste die übrigen verdeckt", "location": "packages/shadow-objects/src/in-the-dark/ShadowObjectCreationScope.spec.ts:61-73", "description": "Jeder der fünf Fälle prüft Warnung, Einmaligkeit und Wirkung in einem einzigen it-Block; fällt die erste Behauptung, nennt der Lauf die beiden anderen nicht. Aufteilen ist versperrt, solange die Deprecation-Warnung pro Realm und Methodenname nur einmal fällt — der Kopfkommentar der Datei hält das fest. Kein Deckungsverlust: zu jeder der drei Hälften existiert eine Mutation, die sie allein rot macht. Was fehlt, ist die Fehlermeldung, die den Grund beim ersten Blick nennt.", "recommendation": "Die drei Hälften so prüfen, dass ein Fehlschlag alle drei Gründe nennt. Ein zusätzlicher Fall löst es nicht — die Modulflagge lässt nur einen zu.", "effort": "S", "evidence": "An der Fundstelle nachgelesen (2026-08-20). Beim Re-Check am 2026-08-27 an der genannten Stelle erneut nachgelesen und bestätigt.", "recheckNote": "Beim Re-Check verschoben und abgeschwächt: aus drei Behauptungen sind zwei geworden, das Muster steht aber unverändert da (ein toBeDefined() vor der eigentlichen Zusicherung)."}, {"id": "TEST-011", "category": "Testabdeckung & Teststrategie", "domain": "harness", "severity": "info", "status": "carried-over", "title": "Eine Zusicherung in der Entity-Spec zählt Aufrufe, die eine Implementierungszufälligkeit bestimmt", "location": "packages/shadow-objects/src/in-the-dark/Entity.spec.ts:978", "description": "Der Fall prüft mit toHaveBeenCalledTimes(4), dass der Abbau jeden Schritt einzeln absichert. Die Vier steht dort aber nicht, weil vier Schritte vorgesehen wären, sondern weil SignalsPath.dispose() an seinem eigenen value$.destroy() abbricht. Ändert sich dort etwas, wird der Test rot, ohne dass die geprüfte Zusage verletzt wäre. Eine Zahl als Zusicherung ist nur so haltbar wie das, was sie zufällig zählt.", "recommendation": "Auf die vier Schritt-Labels prüfen statt auf die Aufrufzahl — die Meldungen sagen, welcher Schritt gefangen wurde, und genau das ist die Zusage. Der Fall bleibt dann grün, solange die Absicherung trägt, und wird rot, wenn sie fällt.", "effort": "S", "evidence": "Beim Re-Check am 2026-08-27 an der genannten Stelle erneut nachgelesen und bestätigt."}, {"id": "TEST-018", "category": "Testabdeckung & Teststrategie", "domain": "harness", "severity": "info", "effort": "S", "title": "Zwei Testnamen behaupten das Gegenteil ihrer eigenen Zusicherung", "location": "packages/shadow-objects/src/view/ViewComponent.spec.ts:430,438", "description": "Die Fälle heißen »ignores a token change« und »ignores an order change«, prüfen aber beide, dass der Wert lokal ankommt: expect(c.token).toBe('other') und expect(c.order).toBe(5). Ignoriert wird allein die Meldung an den ComponentContext. Wer die Namen liest, um den Vertrag zu verstehen, liest ihn falsch herum.", "recommendation": "Die Namen auf das bringen, was sie prüfen — etwa »keeps a token change to itself«. Der Testname ist die Zusage, die ein Leser zuerst sieht.", "status": "carried-over", "evidence": "Im Remediation-Lauf vom 2026-08-28 an der Fundstelle aufgefallen und dort belegt; der Code wurde für diesen Stand nicht neu geprüft."}, {"id": "TEST-019", "category": "Testabdeckung & Teststrategie", "domain": "harness", "severity": "info", "effort": "S", "title": "new Set('x') in einer Spec hält nur, solange der Name ein Zeichen lang ist", "location": "packages/shadow-objects/src/in-the-dark/Registry.spec.ts:31", "description": "Die Zeile übergibt einen String statt eines Arrays. Das ergibt hier zufällig Set {'x'}, weil der Property-Name aus einem Zeichen besteht; bei new Set('debug') stünden fünf Einzelbuchstaben in der Menge. Der Test prüft, was er meint, aber nur bis jemand den Namen verlängert. Die vier Nachbarn schreiben new Set([...]).", "recommendation": "new Set(['x']) schreiben, wie die Nachbarn.", "status": "carried-over", "evidence": "Im Remediation-Lauf vom 2026-08-28 an der Fundstelle aufgefallen und dort belegt; der Code wurde für diesen Stand nicht neu geprüft."}, {"id": "TEST-020", "category": "Testabdeckung & Teststrategie", "domain": "harness", "severity": "info", "effort": "S", "title": "Ein Testname der props-utils-Spec verspricht mehr, als der Fall prüft", "location": "packages/shadow-objects/src/utils/props-utils.spec.ts", "description": "Der Fall heißt »reads a bare key the same way with and without curProps« und prüft dabei [] gegen undefined. Er ist damit enger als sein Name: die Gleichheit, die er zusagt, deckt er nur für diesen einen Wertvergleich ab.", "recommendation": "Den Namen auf den geprüften Vergleich verengen oder den Fall auf die Zusage erweitern.", "status": "carried-over", "evidence": "Im Remediation-Lauf vom 2026-08-28 an der Fundstelle aufgefallen und dort belegt; der Code wurde für diesen Stand nicht neu geprüft."}, {"id": "TYPE-003", "category": "Typsicherheit", "domain": "harness", "severity": "info", "status": "carried-over", "title": "useProperty steht auf <T = any>, sein Vertrag auf <T = unknown>", "location": "packages/shadow-objects/src/in-the-dark/ShadowObjectCreationScope.ts:391 gegen packages/shadow-objects/src/types.ts:153", "description": "Die Implementierung deklariert den Typparameter mit any als Voreinstellung, der Typ ShadowObjectCreationAPI mit unknown. Für Konsumenten unsichtbar, weil sie gegen den Vertrag typisieren und die Klasse ihr Modul nicht verlässt. Innerhalb des Moduls fällt damit die Prüfung weg, die unknown erzwingen würde.", "recommendation": "Auf unknown angleichen und dabei useProperties und jeden internen Aufrufer mitziehen, den der Typcheck danach anspricht. Der Wechsel ist kein Beifang einer anderen Arbeit.", "effort": "S", "evidence": "An der Fundstelle nachgelesen (2026-08-20). Beim Re-Check am 2026-08-27 an der genannten Stelle erneut nachgelesen und bestätigt."}, {"id": "TYPE-006", "category": "Typsicherheit", "domain": "harness", "severity": "info", "effort": "S", "status": "carried-over", "title": "Ein untypisierter Payload zwischen zwei typisierten Nachbarn", "location": "packages/shadow-objects/src/worker/MessageRouter.ts:189", "description": "#onDestroy(data: any) nimmt einen untypisierten Payload, während die beiden Nachbarmethoden #configure(data: ConfigurePayloadData) und #onChangeTrail(data: SyncEvent) ihre Form benennen. Die Nachricht hat eine feste Gestalt, und wer sie ändert, bekommt an dieser Stelle keinen Widerspruch vom Compiler.", "recommendation": "Den Payload-Typ benennen wie bei den Nachbarn. Trägt die Nachricht keine Nutzdaten, ist das ebenfalls ein Typ und sagt mehr als any.", "evidence": "Beim Re-Check am 2026-08-27 an der genannten Stelle erneut nachgelesen und bestätigt."}, {"id": "TYPE-007", "category": "Typsicherheit", "domain": "harness", "severity": "info", "status": "carried-over", "title": "Ein Cast nach any schaltet die Prüfung auf der Transfer-Zeile ab", "location": "packages/shadow-objects/src/view/cloneChangeTrail.ts:7", "description": "structuredClone(data, {transfer: transferables as any}) castet einen Wert nach any, der an dieser Stelle bereits auf TransferablesType verengt ist — also auf genau den Typ, den StructuredSerializeOptions.transfer erwartet. Der Cast trägt nichts und schaltet die Prüfung auf einer Zeile ab, die Objekte an einen ablösenden Aufruf reicht: eine künftige Änderung an IComponentChange.transferables liefe hier ohne Warnung durch. Gemessen am 2026-08-26 an einer Kopie außerhalb des Arbeitsbaums: ohne das as any compiliert die Datei unverändert, mit und ohne --exactOptionalPropertyTypes, und die Fehlerzahl bewegt sich in keiner Richtung.", "recommendation": "Den Cast entfernen. Er kostet nichts und nimmt der Zeile ihre einzige Prüfung.", "effort": "S", "evidence": "Nebenbefund aus dem Remediation-Lauf vom 2026-08-26, an der Fundstelle nachgesehen; vorbestehend, git show bfcc54b:… trägt denselben Cast. Beim Re-Check am 2026-08-27 an der genannten Stelle erneut nachgelesen und bestätigt."}, {"id": "TYPE-008", "category": "Typsicherheit", "domain": "harness", "severity": "info", "effort": "S", "title": "WorkerTimeoutError.messageType steht auf string statt auf der Union der vier Konstanten", "location": "packages/shadow-objects/src/utils/waitForMessageOfType.ts", "description": "Das Feld nennt die Nachricht, die ausgeblieben ist, und kann nur einen von vier Werten tragen. Deklariert ist string, weil der Wert aus dem type: string-Parameter von waitForMessageOfType() stammt. Wer den Fehler im catch auswertet, bekommt vom Typ keine Hilfe dabei, welche Fälle es zu behandeln gibt.", "recommendation": "Die Union führen und die Signatur von waitForMessageOfType() mit umstellen. Beides gehört in einen Zug, sonst wandert der breite Typ nur eine Stelle weiter.", "status": "carried-over", "evidence": "Im Remediation-Lauf vom 2026-08-28 an der Fundstelle aufgefallen und dort belegt; der Code wurde für diesen Stand nicht neu geprüft."}, {"id": "TYPE-009", "category": "Typsicherheit", "domain": "harness", "severity": "info", "effort": "S", "title": "Die Template-Literal-Form der uuid hängt allein an der Inferenz", "location": "packages/shadow-objects/src/utils/generateUUID.ts", "description": "Der Rückgabetyp trägt die Template-Literal-Form, weil TypeScript sie aus der Implementierung ableitet. Weder eine Typ-Assertion noch ein Contract-Check hält dist/src/utils/generateUUID.d.ts darauf fest. Eine spätere Annotation oder ein Umbau der Implementierung nimmt die Zusage zurück, ohne dass ein Lauf rot wird.", "recommendation": "Den Rückgabetyp ausschreiben, statt ihn ableiten zu lassen. Dann steht die Zusage da, wo sie gelesen wird, und ein Umbau muss sie ausdrücklich aufgeben.", "status": "carried-over", "evidence": "Im Remediation-Lauf vom 2026-08-28 an der Fundstelle aufgefallen und dort belegt; der Code wurde für diesen Stand nicht neu geprüft."}], "acknowledged": [{"id": "SEC-002", "category": "Sicherheit", "domain": "harness", "severity": "info", "status": "acknowledged", "title": "Der Demo-Dev-Server schaltet die Host-Prüfung ab", "location": "packages/shae-offscreen-canvas/vite.config.js:1-5", "description": "server.allowedHosts: true nimmt Vites Host-Header-Prüfung vollständig heraus: der Dev-Server antwortet danach unter jedem Hostnamen. Genau das soll die Prüfung verhindern — eine Seite im Netz kann einen Namen auf 127.0.0.1 auflösen lassen und den Dev-Server des Entwicklers ansprechen, als wäre sie sein Origin. Der Schalter betrifft ausschließlich pnpm dev, nicht das publizierte Paket, und er hat vermutlich einen guten Grund (Zugriff von einem zweiten Gerät im LAN). Nur steht der nirgends.", "recommendation": "Auf die Namen einschränken, die wirklich gebraucht werden — allowedHosts nimmt eine Liste. Bleibt es bei true, gehört ein Kommentar daneben, der sagt wofür; so wie die beiden zurückgehaltenen Majors in pnpm-workspace.yaml ihre Begründung tragen.", "effort": "S", "evidence": "An der Fundstelle nachgelesen (2026-08-19): die Konfiguration besteht aus genau diesem einen Schalter.", "acknowledgedNote": "Vom Nutzer am 2026-08-26 so akzeptiert, wie es ist: der Schalter betrifft ausschließlich pnpm dev und nicht das publizierte Paket."}], "openQuestions": [], "optimizations": [{"title": "Die Wächter-Regel maschinell durchsetzen statt sie zu wiederholen", "text": "Das Projekt hat eine klare Regel: eine Zustellung an mehrere Empfänger läuft unter runGuarded(), damit ein Ausfall nur sich selbst kostet. Sie steht in runGuarded.ts, in der API-Referenz und in einem Dutzend Kommentaren. BUG-001 ist die eine Stelle, an der sie fehlt, und sie ist niemandem aufgefallen, weil nichts sie prüft. Eine Biome-Regel wird das nicht leisten. Was es leistet, ist eine Konvention mit Zähnen: jeder emit() über eine Sammlung bekommt einen Spec-Fall mit einem werfenden Empfänger. Fünf oder sechs Fälle, und die Regel prüft sich selbst."}, {"title": "Eine gemeinsame Zusicherung für die Form beider dist-Manifeste", "text": "Beide veröffentlichten Pakete haben eine distContract-Spec, und beide prüfen dasselbe Muster in zwei Fassungen. Die Prüfungen, die nicht paketspezifisch sind — kein catalog:/workspace: in den Abhängigkeiten (siehe BUILD-004), jeder Einstiegspunkt zeigt auf eine existierende Datei, keine .d.ts importiert über eine Quellendung —, ließen sich als eine Handvoll exportierter Helfer in scripts/ ablegen, die beide Specs aufrufen. Dann bekommt ein drittes Paket sie am Tag seiner Veröffentlichung mit, statt sie abzuschreiben."}, {"title": "Die Messzahlen aus den Kommentaren in eine Datei ziehen", "text": "An collectPeerReRequest() steht eine ganze Messreihe im Doc-Kommentar, mit Datum, Browser und Playwright-Version. Das ist die einzige Performance-Grundlinie des Projekts, und sie liegt in einem Kommentar in der Mitte einer 1 009-Zeilen-Datei. PERF-001 und PERF-002 brauchen genau solche Zahlen, um überhaupt entschieden werden zu können. Eine docs/performance.md mit Datum, Messaufbau und Tabelle macht die vorhandene Reihe auffindbar und gibt der nächsten einen Platz."}], "methodology": {"read": ["Alle 74 Nicht-Spec-Quelldateien unter packages/shadow-objects/src/ und packages/shae-offscreen-canvas/src/ vollständig gelesen, 11 793 Zeilen, einschließlich Kernel.ts, Entity.ts, ComponentContext.ts, ComponentChanges.ts, ShaeEntElement.ts, ShaePropElement.ts, ShaeWorkerElement.ts, RemoteWorkerEnv.ts, ShadowObjectCreationScope.ts, FrameLoop.ts und ConsoleLogger.ts in ganzer Länge.", "packages/shae-offscreen-canvas/ vollständig: das Custom Element, alle sieben Shadow Objects, die geteilten Konstanten, build.mjs, distContract.files.txt und beide Manifeste.", "Harness vollständig: package.json aller vier Pakete, pnpm-workspace.yaml, turbo.json, tsconfig.json, biome.json, .editorconfig, .nvmrc, mise.toml, .gitignore, beide GitHub-Workflows, scripts/makePackageJson.mjs und scripts/publishNpmPkg.mjs, alle drei vitest.config.ts, vitest.setup.ts und playwright.config.ts.", "Dokumentation: AGENTS.md, CLAUDE.md, README.md und die drei Paket-READMEs, TODO.md, KNOWN-DEFECTS.md, TEST-PLAN.md, die Verzeichnisse beider docs/-Ordner nach Umfang, distContract.spec.ts vollständig.", "Ausgeführt: pnpm lint:ci (229 Dateien, 0 Diagnosen), pnpm typecheck (3 Tasks grün), pnpm test:ci und ein erzwungener Testlauf ohne Cache (30 + 28 + 6 Dateien, 902 + 383 + 135 Fälle, alle grün), pnpm outdated -r, pnpm audit (0 Advisories), der zusammengeführte Coverage-Bericht pro Datei, eine Zeilenklassifikation über Code gegen Kommentar, und eine Wegwerf-Spec zur Reproduktion von BUG-001, die danach wieder entfernt wurde."], "notRead": ["Die Spec-Dateien wurden nur dort gelesen, wo ein Befund sie betrifft. 26 867 der 42 241 Zeilen JavaScript und TypeScript im Repository stehen in 75 Spec- und Test-Dateien; die Bewertung der Testlage stützt sich auf den Coverage-Bericht, den TEST-PLAN und Stichproben.", "packages/shadow-objects-e2e/src/ und tests/ nur an den Fundstellen der Altbefunde, für den TEST-PLAN-Abgleich und für die Suche nach Risikomustern.", "docs/api-reference.md (3 220 Zeilen) wurde nicht Zeile für Zeile gelesen, sondern an den Stellen, die Altbefunde nennen, sowie über eine Zeilenlängen-Auswertung.", "Die E2E-Strecke wurde nicht ausgeführt: sie braucht drei installierte Playwright-Browser und läuft in CI in einem eigenen Job. Ihre Grünmeldung ist aus dem Workflow und dem TEST-PLAN übernommen, nicht gemessen.", "Die vorhandene ./audit.html blieb bis zum Merge geschlossen; sie ist in keinem Befund dieses Laufs eine Quelle."], "matching": "Primär über Kategorie plus überlappende Location, sekundär über Titelähnlichkeit. Jeder der 30 Altbefunde wurde vor der Übernahme an seiner Fundstelle nachgelesen; drei ließen sich nicht mehr belegen und sind entfallen, drei weitere hatten sich verschoben und tragen die korrigierte Fundstelle samt Vermerk. Die verbleibenden 27 sind carried-over. Kontextbedingt entfernt wurde keiner.", "resolvedNotes": {"DX-024": "generateUUID.ts steht heute bei 76 Zeilen, die Hex-Tabelle ist durch toString(16).padStart(2,'0') ersetzt, und im Repository gibt es keinen einzigen prettier-Treffer mehr.", "TEST-015": "generateUUID.spec.ts hat einen dritten Fall bekommen ('says once that a realm without Web Crypto falls to Math.random'); der Coverage-Bericht weist die Datei mit 100 % in allen vier Maßen aus.", "DX-029": "Die genannte Stelle (api-reference.md:1822) ist heute eine 20 Zeichen lange Tabellenzeile. Ein Zählen über die Datei zeigt Dutzende Prosazeilen jenseits von 120 Spalten, also beschreibt der Befund als Einzelstelle nichts mehr; Biome formatiert Markdown-Prosa nicht."}, "scoreFormula": "Start bei 100, Abzug je Finding: critical −10, high −5, medium −2, low −0,5, info 0, Untergrenze 0. Dieselbe Formel läuft dreimal: einmal über alle Findings und je einmal über die einer Domain. Die beiden Teilscores sind keine Summanden des Gesamtscores und ergeben zusammen nicht 100 — sie stehen auf derselben Skala und bewerten je einen Bereich für sich.", "theme": "Light, übernommen aus summary.theme des Vorgänger-Audits; in dieser Sitzung wurde keine andere Vorgabe gemacht."}}</script>
<script>
(function(){
var D = JSON.parse(document.getElementById('audit-data').textContent);
var F = D.findings;
var state = {domain:'all', severity:'all', category:'all', status:'all'};
var rows = document.getElementById('rows');
var counter = document.getElementById('count');
var catSel = document.getElementById('f-category');
function esc(s){ var d=document.createElement('div'); d.textContent=s==null?'':String(s); return d.innerHTML; }
var SEVL={critical:'kritisch',high:'hoch',medium:'mittel',low:'gering',info:'Hinweis'};
var STL={'new':'neu','unchanged':'unverändert','improved':'verbessert','carried-over':'übernommen'};
var DOML={code:'Code', harness:'Harness'};
function matches(f){
return (state.domain==='all'||f.domain===state.domain)
&& (state.severity==='all'||f.severity===state.severity)
&& (state.category==='all'||f.category===state.category)
&& (state.status==='all'||f.status===state.status);
}
function syncCategories(){
var pool = F.filter(function(f){ return state.domain==='all'||f.domain===state.domain; });
var cats = []; pool.forEach(function(f){ if(cats.indexOf(f.category)<0) cats.push(f.category); });
cats.sort();
if(state.category!=='all' && cats.indexOf(state.category)<0) state.category='all';
catSel.innerHTML = '<option value="all">Alle Kategorien</option>' +
cats.map(function(c){ return '<option value="'+esc(c)+'">'+esc(c)+'</option>'; }).join('');
catSel.value = state.category;
}
function render(){
var list = F.filter(matches);
counter.textContent = list.length===F.length
? list.length + ' Befunde'
: list.length + ' von ' + F.length + ' Befunden';
if(!list.length){ rows.innerHTML = '<p class="empty">Kein Befund passt auf diese Auswahl.</p>'; return; }
rows.innerHTML = list.map(function(f){
var gh = f.github ? '<span> · <a href="'+esc(f.github.url)+'">#'+esc(f.github.number)+'</a>'+(f.github.state==='closed'?' closed':'')+'</span>' : '';
return ''
+ '<article class="row sev-'+f.severity+'" data-id="'+esc(f.id)+'">'
+ '<button class="rowhead" type="button" aria-expanded="false">'
+ '<span class="line1">'
+ '<span class="sev sev-b-'+f.severity+'">'+SEVL[f.severity]+'</span>'
+ '<span class="rid"><span class="caret">›</span> '+esc(f.id)+'</span>'
+ '</span>'
+ '<span class="rtitle">'+esc(f.title)+'</span>'
+ '<span class="line3">'
+ '<span class="rloc mono">'+esc(f.location.split(';')[0])+'</span>'
+ '<span class="rmeta">'+esc(f.category)+'</span>'
+ '<span class="eff">'+esc(f.effort)+'</span>'
+ '<span><span class="dom-badge">'+DOML[f.domain]+'</span> <span class="st st-'+f.status+'">'+STL[f.status]+'</span></span>'
+ '</span>'
+ '</button>'
+ '<div class="rowbody">'
+ '<h4>Fundstelle</h4><p class="loc">'+esc(f.location)+gh+'</p>'
+ '<h4>Problem</h4><p>'+esc(f.description)+'</p>'
+ '<h4>Empfehlung</h4><p>'+esc(f.recommendation)+'</p>'
+ (f.evidence?'<h4>Beleg</h4><p>'+esc(f.evidence)+'</p>':'')
+ (f.recheckNote?'<h4>Re-Check</h4><p>'+esc(f.recheckNote)+'</p>':'')
+ (f.github&&f.github.note?'<h4>Issue</h4><p>'+esc(f.github.note)+'</p>':'')
+ (f.github&&f.github.assignee?'<p class="rmeta">'+esc(f.github.assignee)+'</p>':'')
+ '</div>'
+ '</article>';
}).join('');
}
rows.addEventListener('click', function(ev){
var btn = ev.target.closest('.rowhead'); if(!btn) return;
var row = btn.parentNode, open = row.classList.toggle('open');
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
});
document.querySelectorAll('[data-filter]').forEach(function(el){
if(el.tagName==='SELECT'){
el.addEventListener('change', function(){
state[el.dataset.filter] = el.value;
if(el.dataset.filter==='domain') syncCategories();
render();
});
} else {
el.addEventListener('click', function(){
var key = el.dataset.filter;
document.querySelectorAll('[data-filter="'+key+'"]').forEach(function(b){ b.setAttribute('aria-pressed','false'); });
el.setAttribute('aria-pressed','true');
state[key] = el.dataset.value;
if(key==='domain') syncCategories();
render();
});
}
});
syncCategories();
render();
})();
</script>
</body>
</html>