@nurix/etna 0.19.39 → 0.19.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,12 @@ The reference knowledge for **Discovery** (`discover` harness, Mode 1). It carri
14
14
 
15
15
  Before Part A, decide **where this run writes** — the harness picks intelligently, it does not hardcode `research/`:
16
16
 
17
+ > **EXCEPTION — a calibration run defers this step.** If this run is a calibration (`calibration.md`), do
18
+ > **not** restore or read the prior corpus yet: re-derive the sampled claims from the live product and write
19
+ > your observations down first, then diff. Reading the prior claim first anchors the re-derivation and turns
20
+ > the exercise into a consistency check. Everything below applies **after** step 3 of the blind protocol.
21
+
22
+ 0. **Check git history BEFORE declaring any target fresh.** The filesystem is not the whole record — run `git log --oneline -- <corpus>/<target>`, `git ls-tree -r --name-only HEAD -- <corpus>/<target>`, and `git stash list`. **A prior corpus may exist in HEAD (or a stash) with no working-tree copy.** If one is found, restore it (`git checkout HEAD -- …` / `git stash pop`) and **RECONCILE against it** — a prior run is an *independent corroboration lane* and may refute the current one. The user's own wording is a signal too: "**re**run", "again", "update" all mean a prior run is expected. (Origin: Emergent — a run checked `ls research/` only, found the folder thin, and spent its opening declaring a re-run to be a first run; 110 files of prior corpus were sitting in HEAD.)
17
23
  1. **Reuse an existing discovery corpus** if the project has one — search for any folder containing a prior run's `*/00-scope-verdict.md` (commonly `research/`, but also `discovery/`, `analysis/`, or a folder the user names).
18
24
  2. If several exist, prefer the one the user points at, else the most recently written.
19
25
  3. If none exists and the user hasn't named one, default to `research/` at the repo root.
@@ -136,6 +142,33 @@ The three named **cases** are presets over this vector — a target can light se
136
142
 
137
143
  Discovery records the resolved vector in the `access_grade` frontmatter of `00-recon-plan.md`.
138
144
 
145
+ > **A wide CSP / allow-list is a hypothesis GENERATOR, never evidence of use.** A `content-security-policy`
146
+ > (especially `connect-src` / `script-src` / `form-action`), a `permissions-policy`, or a bundled vendor SDK
147
+ > tells you what the app **may** contact — not what it does, and never what **role** that vendor plays.
148
+ > Record the **declared origin as fact** and the **role as hypothesis**, tagged `verify:`. A vendor's mere
149
+ > presence in a bundle or allow-list is not proof it is on the critical path. (Origin: Emergent — one CSP
150
+ > header yielded ~25 vendor leads in a single request, an outstanding lead generator; the `verify:` tags are
151
+ > what stopped three collectors echoing those leads as established fact.)
152
+ >
153
+ > **But the INVERSE error is just as costly — and it is the one that actually bit.** A `verify:` tag
154
+ > resolves to **`confirmed`** or **`not-yet-observed`** — **NEVER to `refuted`** on the strength of an
155
+ > *absence* in an instrument you have not shown is capable of seeing the thing. (Origin: Emergent — the run
156
+ > hypothesised from the CSP that **Pusher carried streaming**, failed to observe a single Pusher frame, and
157
+ > published "Pusher does not carry build streaming" as a refuted hypothesis and a methodology win. **Pusher
158
+ > was live the whole time** — `state: connected`, `cluster: us2`, channel `user-{uuid}`, carrying
159
+ > `traj-update`/`terminal-state`/`streak-update` pings. The `window.WebSocket` wrapper had been installed
160
+ > *after* app boot and was **structurally blind** to the socket the Pusher library opens at boot
161
+ > (`tradecraft.md` gotcha #13/§2.12), whose prescribed remedy — the `bind_global` library tap — was never
162
+ > installed, even though `pusherTransportTLS` sat in the storage census as a live signal. A CSP lead that you
163
+ > then fail to observe is **unconfirmed, not disproved**.)
164
+
165
+ > **Never infer target geography from `cf-ray` / edge-PoP codes.** Anycast CDNs route to the PoP nearest the
166
+ > **capture host**, so a `BOM`/`BLR`/`FRA` suffix describes *the observer*, not the target. Geography needs a
167
+ > real lane: the legal entity on the site or in whois, job-post locations, billing currency, or a
168
+ > data-residency statement. (Origin: Emergent — BOM/BLR PoPs were read as evidence of an India-based
169
+ > operation; the conclusion happened to be right, but on **other** evidence — an Indian legal entity, 28/43
170
+ > engineering roles in Bangalore, and INR billing — while the PoP reasoning was simply invalid.)
171
+
139
172
  > **Hypotheses are not facts (seed them tagged, never echo them).** Any _derived inference_ Discovery makes beyond the four graded axes — e.g. "this `localStorage` key implies a GraphQL backend", "this host is the realtime endpoint" — is recorded in `00-recon-plan.md` **as a labelled hypothesis**, never as a plan fact, and is tagged `verify:may-be-stale` when seeded to a collector (CLAUDE.md Mode 2) so Ingestion **re-derives** rather than echoes it. A wrong inference seeded as framing propagates: a real run's `apolloAnonId ⇒ GraphQL` guess was repeated (hedged) by two captures before the `session` dimension refuted it (the backend was RSC + Server Actions; the key was an Apollo.io tracker).
140
173
 
141
174
  ---
@@ -156,7 +189,32 @@ Discovery records the resolved vector in the `access_grade` frontmatter of `00-r
156
189
  | 10 | **wire-capture** | `wire-capture` | runtime (source:none) | the 3-rung ladder for non-browser/no-source clients (gated) |
157
190
  | 11 | **distribution-artifacts** | `distribution-artifacts` | runtime (non-web client) | extension CRX / Electron `.asar` / mobile binary / CLI |
158
191
 
159
- > **Deferred (available on request, not built):** `external-reputation` (G2/Reddit/HN — the only "what others say" surface) and `hiring-intel` (job posts leak the backend stack). Strong candidates, declined for this iteration. Re-add via self-correction if a run needs them. **Conditional trigger:** when `community` returns **no first-party feedback loop** (no public issues/roadmap/changelog/forum — feedback funnelled to a gated channel), flag `external-reputation` as **recommended-for-this-run** in `00-recon-plan.md`, so the user-sentiment blind spot is a surfaced choice rather than a silent gap (the `community` user-pain claims are then capped at tentative — evaluation closed-feedback cap).
192
+ > **Deferred (available on request, not built):** `external-reputation` (G2/Reddit/HN — the only "what others say" surface) and `hiring-intel` (job posts leak the backend stack). Strong candidates, declined for this iteration. Re-add via self-correction if a run needs them. **Each now carries its own conditional trigger — see below.**
193
+ >
194
+ > **Trigger 1 — `hiring-intel` on any `source:none` target.** When the access vector grades **`source:none`**,
195
+ > flag `hiring-intel` **recommended-for-this-run** in `00-recon-plan.md`. Rationale: every other
196
+ > non-marketing lane on a closed target (`deployed-client-bundle`, `session`, `wire-capture`,
197
+ > `distribution-artifacts`) ultimately reads **an artifact the company ships to a client** — so they all
198
+ > share one blind spot, **any backend component that never reaches a browser**. Job posts are the only
199
+ > cheap lane that is independent of both marketing *and* the client. (Origin: Emergent — the corpus asserted
200
+ > a "Python/FastAPI backend" from bundle+wire mining; the company's own declared stack names **Go co-equal
201
+ > with Python** and `FastAPI` appears **zero times** in 45 postings. A Go service ships no JS and answers in
202
+ > plain JSON, so it is invisible to those instruments *by construction*. The same lane also surfaced Kafka,
203
+ > PostgreSQL/Redis as first-tier stores, AWS as a second cloud, and an in-house **model post-training**
204
+ > function that refuted a pure-inference-broker read.) Grade a posting-derived claim by §3 `ats-api` (high)
205
+ > when it comes from the official ATS API, `crawl-clip` (medium) when from a careers page or aggregator.
206
+ >
207
+ > **Trigger 2 — `external-reputation` on a closed feedback loop.** When `community` returns **no first-party feedback loop** (no public issues/roadmap/changelog/forum — feedback funnelled to a gated channel), flag `external-reputation` as **recommended-for-this-run** in `00-recon-plan.md`, so the user-sentiment blind spot is a surfaced choice rather than a silent gap (the `community` user-pain claims are then capped at tentative — evaluation closed-feedback cap).
208
+ >
209
+ > **When you DO run `external-reputation`, split the sample by review SOURCE before reporting any average.**
210
+ > Review platforms commonly label whether a review was user-initiated or **vendor-solicited** (Trustpilot
211
+ > tags `AFSv2` / `FileUploadInvitation` and exposes `hasRecentlyInvitedUsers`). A blended average over a
212
+ > vendor-controlled sample is not a satisfaction measure. Report the **organic** figure, and look for a
213
+ > **natural control group** — an *unclaimed* or duplicate profile for the same product, where solicitation
214
+ > is impossible, is the strongest corroboration available. State the *effect* from the platform's own
215
+ > labels and **never allege fake reviews**: soliciting is legitimate, and the finding is about **sampling**,
216
+ > not fraud. (Origin: Emergent — published 3.0 decomposed to organic **2.43** (n=141) vs solicited **4.58**
217
+ > (n=59); the unclaimed sibling profile sat at **2.2**.)
160
218
 
161
219
  An **absent** dimension is a finding — record it with its evidence; never silently drop it.
162
220
 
@@ -18,6 +18,19 @@
18
18
 
19
19
  **Write-side-unobserved cap.** A `session` dimension carrying `write_side_observed: false` (a Pass-1-only run) has observed feature _existence/gating_ but **not** feature _execution behavior_. Any marketed generate/submit/launch _behavior_ claim therefore stays **tentative and is never promoted to fact** from marketing/docs alone — render the write-side gap **once** from the frontmatter flag, not re-hedged per rollup.
20
20
 
21
+ > **Drift between a target's own sources is a FINDING about the target, not a defect in the capture.**
22
+ > When the docs say one thing and the runtime does another — a documented field the API doesn't return, a
23
+ > published limit the wire contradicts, a spec route that 404s — **do not treat this as an error to
24
+ > reconcile away, and do not go hunting for which source is "really" right.** Both captures are faithful;
25
+ > the product is incoherent. Record both verbatim (each anchored to its own `raw/` artifact), state the
26
+ > divergence plainly, and **name it as a product finding** — documentation drift, an aspirational spec, a
27
+ > deprecated-but-published surface, or a versioning gap between the docs and the deploy.
28
+ >
29
+ > This is often among the most valuable things a run produces: it is exactly what an integrator would hit
30
+ > on day one and what no vendor advertises. The harness's job is **faithful capture with provenance**, not
31
+ > adjudicating which of a company's own statements is true. A run that flattens a real inconsistency into a
32
+ > single tidy claim has destroyed a finding and manufactured a false coherence.
33
+
21
34
  **Presentation mapping:** high → stated as fact; low/single-source → hedged as tentative; conflicted → flagged. (This is the "lighter" form of the provenance-weighting — done inline in the prose, not via a separate `claims.yaml`.)
22
35
 
23
36
  ---
@@ -78,11 +78,22 @@ Keep `status` and `confidence` honest — they drive the entire evaluation weigh
78
78
 
79
79
  ## 3. Method & confidence vocabulary (drives evaluation weighting)
80
80
 
81
- `method` is one of: `clone-and-map`, `source-map-reassembly`, `registry-metadata`, `openapi-verbatim`, `graphql-introspection`, `crawl-clip`, `docs-reconstructed`, `bundle-string-mine`, `dns-ct-fingerprint`, `wire-tap-browser`, `wire-har`, `wire-mitmproxy`, `binary-extract` (asar/apk/crx), `session-playbook`, `sdk-harness`, `inferred`.
81
+ `method` is one of: `clone-and-map`, `source-map-reassembly`, `registry-metadata`, `ats-api`, `openapi-verbatim`, `graphql-introspection`, `crawl-clip`, `docs-reconstructed`, `bundle-string-mine`, `dns-ct-fingerprint`, `wire-tap-browser`, `wire-har`, `wire-mitmproxy`, `binary-extract` (asar/apk/crx), `session-playbook`, `sdk-harness`, `inferred`.
82
82
 
83
83
  **Confidence is a function of method** (evaluation relies on this mapping):
84
84
 
85
- - **high** — verbatim machine artifacts: `openapi-verbatim`, `graphql-introspection`, `registry-metadata`, `clone-and-map`, `binary-extract`.
85
+ - **high** — verbatim machine artifacts: `openapi-verbatim`, `graphql-introspection`, `registry-metadata`, `ats-api`, `clone-and-map`, `binary-extract`.
86
+
87
+ > **`ats-api` — a first-party job-board / ATS JSON API is NOT a crawl.** A response from an official
88
+ > applicant-tracking endpoint (`boards-api.greenhouse.io/v1/boards/<slug>/jobs?content=true`,
89
+ > `api.ashbyhq.com/posting-api/job-board/<slug>`, `api.lever.co/v0/postings/<slug>?mode=json`) is a
90
+ > **verbatim machine artifact published by the company itself** — the same evidence class as
91
+ > `registry-metadata`, and it maps to **high**. Grading it `crawl-clip` (medium) forces the strict reader
92
+ > to *downgrade a high-quality first-party artifact*, which is the wrong direction. The `careers` marketing
93
+ > page, an aggregator listing, or a search-result snippet **is** `crawl-clip`; the ATS API is not.
94
+ > (Origin: Emergent — a 433 KB verbatim Greenhouse JSON body carrying a byte-identical declared tech stack
95
+ > across two postings was labelled `crawl-clip` with `confidence: high`, a mismatch against this very
96
+ > table. The confidence was right and the vocabulary had no word for the artifact.)
86
97
  - **medium** — `crawl-clip`, `docs-reconstructed`, `bundle-string-mine`, `dns-ct-fingerprint`, `wire-har`.
87
98
  - **low** — `inferred`, `session-playbook` Pass-1-only, `wire-tap-browser` shapes-only, any wire-drift observation without a spec.
88
99
 
@@ -115,9 +126,157 @@ Prose captures (doc/marketing pages, structure maps, catalogs) → markdown with
115
126
 
116
127
  ---
117
128
 
118
- ## 5. Size discipline — reduce, don't dump
129
+ ## 5. Capture discipline — DUMP VERBATIM, digest alongside
119
130
 
120
- Large artifacts (a 3 MB bundle, a 5 MB response, a 700-page sitemap) are **digested**, not pasted: the schema/shape, enum distributions, one redacted representative sample, counts. A `raw/` file is readable, not a memory dump. Set `digested: true` context in the artifact's `_meta`.
131
+ > **This rule was inverted until 2026-07-28.** It used to read *"Size discipline — reduce, don't dump… a
132
+ > `raw/` file is readable, not a memory dump,"* and collectors obeyed it: across 21 corpora `raw/` came out
133
+ > **640 `.md` to 115 `.json` — 84.8% model-written prose** — and **6 of 12 sessions marked `status: complete`
134
+ > held ZERO verbatim envelopes.** The corpus became unreproducible by following its own contract.
135
+ >
136
+ > The old rule conflated two different problems with one answer:
137
+ > **(a) don't put 6 MB through your context** — correct, always.
138
+ > **(b) don't put 6 MB on disk** — wrong, and the cause of the damage.
139
+ > They have different solutions. **Write to disk without routing through context** (§5.2). Context budget is
140
+ > never a reason to discard evidence.
141
+
142
+ ### 5.1 The two layers — never let one substitute for the other
143
+
144
+ | Layer | Files | Audience | Rule |
145
+ | --- | --- | --- | --- |
146
+ | **Evidence** | `raw/*`, `captures/*.json` | a *future run* re-deriving the claim | **verbatim**, redacted, complete. Unreadable is fine. |
147
+ | **Narrative** | `_summary.md`, `captures/*.md`, `evaluation/*` | a human, and Evaluation | digested, decoded, opinionated. |
148
+
149
+ **A digest is written IN ADDITION TO the artifact, never INSTEAD OF it.** If you find yourself writing "the
150
+ response contained 47 fields including…", stop: dump the response, *then* write that sentence. The sentence
151
+ is worth little without the file; the file is worth a lot without the sentence.
152
+
153
+ ### 5.2 Size is a routing problem, not a reason to discard
154
+
155
+ Never read a large artifact into context in order to save it. Route it to disk directly:
156
+
157
+ ```bash
158
+ curl -sS "$URL" -o raw/spec.json # never touches context
159
+ curl -sS "$URL" | gzip > raw/bundle.js.gz # compress in transit
160
+ ```
161
+ - **In-page (browser) captures** → the clipboard channel (`tradecraft.md` §1b): scrub → `__CP(obj)` →
162
+ `pbpaste > raw/capture.json`. Unlimited size, never passes through the display layer.
163
+ - **Genuinely unbounded streams** (an infinite log, a 700-page crawl): bound it explicitly — first N,
164
+ every Nth, or a time window — and **record the bound in `_meta.sampling`**. A recorded sample is evidence;
165
+ an unrecorded summary is not.
166
+ - **Too big to keep** (>50 MB): keep a verbatim head/tail slice + the full `sha256` + the retrieval command
167
+ in `_meta.refetch`, so a later run can reproduce it exactly.
168
+
169
+ `_meta` gains: `verbatim: true|false`, `sampling: <bound, if any>`, `refetch: <exact command>`.
170
+ Set `digested: true` **only** when the verbatim form is also on disk beside it.
171
+
172
+ ### 5.3 THE TRACEABILITY RULE — if you cite it, dump it
173
+
174
+ **Every value that appears in a `_summary.md`, a rollup, a README, or a provenance anchor MUST be traceable
175
+ to a file on disk in the same corpus.** No exceptions.
176
+
177
+ **The session is not a storage medium.** A collector that reads a response, extracts a number, writes the
178
+ number into prose, and never saves the response has produced a claim that **cannot be checked by anyone,
179
+ ever** — including the next run, including the same model an hour later. The claim outlives its evidence,
180
+ and the corpus silently degrades from research into assertion.
181
+
182
+ This is the single highest-value rule in this file, because it is the root cause of an entire class of
183
+ apparent defects. When a later reader finds a citation pointing at a file that does not contain the claim,
184
+ the overwhelmingly likely explanation is **not** fabrication — it is that the evidence existed in the
185
+ session, shaped the claim honestly, and was never written down. The citation outlived the data.
186
+
187
+ **Practical form:** before writing any Finding, ask *"which file on disk would let someone else check
188
+ this?"* If the answer is "none," write the file first. If the value came from a tool result you did not
189
+ save, save it now — a `raw/` file with `_meta.source` and the raw body is thirty seconds of work and is the
190
+ difference between evidence and hearsay.
191
+
192
+ > (Origin: layer-ai — a rollup promoted a finding on *"five independent dimensions"*, citing
193
+ > `infra-backend-fingerprint`; the string it relied on appears **zero** times in that dimension's files. The
194
+ > lane was real in-session and never dumped. Also emergent — 26 session envelopes, the only corpus of 12
195
+ > where the session lane is genuinely reproducible.)
196
+
197
+ ### 5.4 Dump manifest — the floor, per dimension
198
+
199
+ Each dimension is `status: complete` only when these exist as **verbatim** files. Anything else is `partial`.
200
+
201
+ | Dimension | MUST land in `raw/` (verbatim, redacted) |
202
+ | --- | --- |
203
+ | `api` | the served spec **as served** (`openapi.json` / SDL / introspection result), not a re-typed table |
204
+ | `packages` | the registry metadata JSON per package (`npm view --json`, PyPI JSON), + the `.d.ts` / exports surface |
205
+ | `deployed-client-bundle` | the entry bundle (gzipped is fine) + any `.map`; the route/flag extracts *in addition* |
206
+ | `session` | ≥3 envelopes (identity read · one canonical list read · the primary write) — §8 |
207
+ | `wire-capture` | the flow log (HAR/JSON), not only the narrative |
208
+ | `infra-backend-fingerprint` | raw `dig`/`crt.sh`/header responses, not only the decoded table |
209
+ | `docs` | the source form of each captured page (md/html), not only the distilled section |
210
+ | `website` | the raw HTML or extracted text per captured page, + the pricing table as data |
211
+ | `community` | the changelog/issue payloads as fetched (JSON where an API exists) |
212
+ | `distribution-artifacts` | the manifest(s) verbatim (`manifest.json`, `Info.plist`, `AndroidManifest`) + the extracted file list |
213
+ | `codebase` | (the clone **is** the artifact) the structure map + resolved manifests |
214
+ | `external-reputation` | the platform's own structured payload (`__NEXT_DATA__`, schema.org block, API JSON) |
215
+ | `hiring-intel` | the ATS API response verbatim |
216
+
217
+ **When a MUST is unobtainable, that is fine — record it in `gaps:` with the reason and mark the dimension
218
+ `partial`.** An honest `partial` is worth more than a `complete` resting on prose.
219
+
220
+ ---
221
+
222
+ ### 5.5 Visual capture — screenshot GENEROUSLY (for the human, not the score)
223
+
224
+ **Any dimension driving a real browser captures screenshots continuously, not one-per-surface.** A capture
225
+ session that walks fifteen screens should leave far more than fifteen images.
226
+
227
+ > **This rule is deliberately NOT tied to a score.** `screenshot_coverage` (`cartography-coverage.md`)
228
+ > measures something narrow — did each *primary surface* get one image — and it is a Mode-5 rubric input.
229
+ > **This is a different obligation with a different beneficiary: the human who reads the corpus weeks
230
+ > later, for a purpose nobody anticipated during the run.** Do not capture to satisfy the metric and stop.
231
+ > Satisfying the metric is a floor so low it is nearly always already met; the rule here is *keep going*.
232
+ > Extra images cost a few hundred KB and buy a reader something no prose reconstructs — what the product
233
+ > **actually looked like**, at that version, in that plan tier, on that date. That is a genuinely
234
+ > irreproducible asset: the run can be repeated, the product's past state cannot.
235
+
236
+ **Capture every surface, AND every materially different state within a surface.** The states worth having,
237
+ roughly in order of how often a later reader wants them:
238
+
239
+ | State | Why a reader wants it |
240
+ | --- | --- |
241
+ | **Empty** (no data yet, zero-state, onboarding) | the new-user experience — usually the most-designed and least-documented screen in the product |
242
+ | **Populated** (real data present) | the actual working UI |
243
+ | **A write in progress** (form filled *before* submit, mid-action) | the input schema as the product presents it — often richer than the API's |
244
+ | **After the write** (success state, the created entity) | what the product considers a result |
245
+ | **Modal / drawer / panel open** | usually a whole surface the nav never reveals |
246
+ | **Dropdown / picker expanded** | enumerates the option set — engine lists, model lists, tier lists, permission sets |
247
+ | **Error and validation states** | constraints the docs never state |
248
+ | **Permission / plan walls** | exactly what is gated, at which tier |
249
+ | **Loading / async / in-flight** | reveals the job model and whether work is sync or queued |
250
+ | **Hover, tooltip, help text** | inline documentation that exists nowhere else |
251
+ | **Settings and config surfaces, fully scrolled** | the configuration surface is where products differ most |
252
+
253
+ Also capture the **before/after pair** around any state-changing action you were authorized to take — the
254
+ delta is the finding, and one image of the result cannot show it.
255
+
256
+ **Naming and index.** Volume only helps if it stays navigable:
257
+
258
+ - `captures/screens/<nn>-<surface>[-<state>].png` — e.g. `07-agents-modal-open.png`,
259
+ `12-composer-model-picker-expanded.png`, `18-deploy-confirm-before.png` / `19-deploy-confirm-after.png`.
260
+ - Keep `screens/_index.md` current: **file → surface → route → depth → state → one line on what it shows.**
261
+ An unindexed directory of 60 images is an archive nobody opens.
262
+ - Sequential numbering encodes the walk order, which is itself information — it preserves the path taken.
263
+
264
+ **Redaction happens BEFORE the shutter, and it is not optional.** More images means more leak surface, and
265
+ a leaked pixel is undetectable afterwards — `grep` cannot audit a PNG, so the §7.0 4c literal sweep has **no
266
+ image analogue**. Apply DOM-level redaction (`tradecraft.md` §1c) and re-check before each capture, because
267
+ the elements needing redaction change as you navigate.
268
+
269
+ > **The third-party trap — check the CORNERS of the frame, not just the content.** It is easy to redact the
270
+ > account email in the header and ship a screenshot whose bottom-right corner holds a support-chat widget
271
+ > displaying **another human's name and message**. Redact any: support/chat widget (Intercom, Crisp,
272
+ > Drift, Zendesk), notification toast, presence avatar or "who's online" strip, comment or activity feed,
273
+ > shared-workspace member list, and any other tenant's data visible in a list.
274
+ >
275
+ > (Origin: Emergent — a run redacted the account holder's email and display name at the DOM level, wrote
276
+ > *"no PII exists in the pixels — verified by reading each capture"* into `screens/_index.md`, and shipped
277
+ > ~20 images each carrying an Intercom widget with a named support agent and her message text. The
278
+ > redaction was real; the *frame* was never examined. **Never write a zero-PII assertion you did not verify
279
+ > by looking at the image** — and prefer "redacted: <what>" over a blanket claim.)
121
280
 
122
281
  ---
123
282
 
@@ -260,6 +419,52 @@ You lose JSON braces and special characters, but you keep the **structural infor
260
419
  - **Inline image data URIs** — `data:image/…`.
261
420
  - **Provider key prefixes** — Stripe (`sk_`/`rk_`/`pk_`), OpenAI (`sk-`/`sk-proj-`), Slack (`xox*`), AWS (`AKIA`/`ASIA`), GitHub (`gh[pousr]_`), Google (`AIza`).
262
421
 
422
+ #### 4b. Field-level redaction does NOT protect prose — scrub the VALUE document-wide
423
+
424
+ `redactSecrets` walks **field names**, so it reliably turns `{"password": "abc123"}` into
425
+ `<REDACTED:SECRET>`. It does **nothing** for the same value written into surrounding narrative —
426
+ a capture that correctly redacts the field and then explains *"the password is returned in the `abc123`
427
+ form"* has **leaked the secret anyway**, and the field-name walker will never catch it.
428
+
429
+ **Rule: once a value is identified as secret, scrub that literal from EVERY file in the capture, not just
430
+ from the JSON field it arrived in.** After redacting, grep the corpus for the literal and confirm zero hits.
431
+
432
+ **And do not trust `SCRUB` alone to find it afterwards.** `SCRUB`'s generic hex rule is `[a-f0-9]{32,}`,
433
+ which is **structurally incapable** of matching a short secret — an 8-char hex password, a 6-digit OTP, a
434
+ short numeric PIN, a 4-char confirmation code. A "sweep clean" result from a pattern that cannot match the
435
+ shape you are looking for is **not evidence of cleanliness**. Grep for the **specific literal**.
436
+
437
+ (Origin: Emergent — a per-job code-server password `<8-hex>` was correctly redacted in the `password` JSON
438
+ field of three artifacts and then quoted verbatim in the adjacent prose of all three. A later corpus-wide
439
+ sweep declared all 175 files clean; the sweep's own regex could not match an 8-char hex value, so the
440
+ "clean" result was meaningless. The leak was found only by a cross-run reader that read the prose.)
441
+
442
+ #### 4c. The literal pre-commit sweep (MANDATORY — a regex sweep is not evidence)
443
+
444
+ **Before committing a corpus, grep for the LITERAL of every value ever identified as secret for this
445
+ target — including values named in PRIOR runs' own defect notes — and assert zero hits.** Record the
446
+ literal-grep result in the run's Mode-5 output.
447
+
448
+ Rule 4b tells you *why* short secrets evade a value-shape regex. This rule is the *verification step*,
449
+ and it exists because documenting the lesson turned out not to be enough:
450
+
451
+ > **(Origin: Emergent, second run.)** The 4b rule above was written FROM the Emergent password leak — and
452
+ > the three leaked files were still in the corpus a month later, unremediated. The next run's opening
453
+ > sweep found them only because it grepped the **literal 8-char value** rather than a shape. A rule that
454
+ > records a lesson without a verification step lets the same leak ship again.
455
+
456
+ ```bash
457
+ # For every value ever flagged secret for this target (incl. ones quoted in prior defect notes):
458
+ for lit in "$KNOWN_SECRET_LITERAL" ...; do
459
+ grep -rIl -- "$lit" . && echo "LEAK: $lit" || echo "clean: $lit"
460
+ done
461
+ # Plus the field-shape pass, which catches NEW secrets the literal list doesn't know about:
462
+ grep -rIoE '"(password|passwd|secret|access_token|refresh_token|api_key)"[[:space:]]*:[[:space:]]*"[^"<][^"]{2,}"' . | grep -v REDACTED
463
+ ```
464
+
465
+ A sweep that reports "clean" using a pattern structurally incapable of matching the target shape is
466
+ **not a clean result** — it is an absence of evidence. Say which patterns you ran.
467
+
263
468
  #### 5. Maintenance loop
264
469
 
265
470
  When a run uncovers a new key or value shape (a regional BSP / telephony token, a niche analytics SDK key, an internal-tool API, an opaquely-named credential field), **add the regex to the `SCRUB` table here** (and the field name to `SECRET_KEY_RE` if it's name-based) — the layer only stays a real guardrail if every new shape is folded back in.
@@ -286,12 +491,46 @@ Downloading a public binary (CRX/`.asar`/`.ipa`/`.apk`) and reading its manifest
286
491
 
287
492
  ## 8. Completeness check
288
493
 
289
- A capture is complete when: `_summary.md` exists with full provenance frontmatter + honest `status`; every Finding is backed by a `raw/` artifact or inline value; `raw/` artifacts have `_meta` + decoding (large ones digested); redaction pass run; `gaps` recorded (never fabricated).
494
+ A capture is complete when: `_summary.md` exists with full provenance frontmatter + honest `status`; **every Finding is backed by a `raw/` artifact — an inline value is NOT sufficient (§5.3)**; the dimension's **§5.4 dump manifest is satisfied verbatim**; `raw/` artifacts carry `_meta` (with `verbatim:` and, if bounded, `sampling:`); redaction pass run; `gaps` recorded (never fabricated).
495
+
496
+ > **Self-check before writing `status: complete`:** count the verbatim artifacts in `raw/`. If that count is **zero**, the dimension is `partial` no matter how good the prose is — you have written an essay, not a capture. If a Finding names a number, a path, a version, or a field, open the file that contains it; if no such file exists, write it now (§5.3).
290
497
 
291
498
  **Summary-first + bounded, visible fan-out (never go silent long enough to look dead).** Write a `status: partial` `_summary.md` with whatever is mined so far **before** any long-tail fetch loop (downloading many chunks, crawling many pages, deep pagination), then enrich it in place and flip to `complete`. **Bound** every fan-out — cap it by count / total bytes / wall-time (e.g. ≤40 items or ≤90 s) and record the remainder as a `gap` rather than fetching exhaustively; emit a progress note per batch. A collector that fetches everything before writing anything can run many times longer than its siblings and read as hung to the orchestrator, forcing a needless salvage. Partial-but-finalized beats complete-but-silent.
292
499
 
500
+ **A single reading of a mutable numeric field is a sample, not a schema.** Before describing any observed
501
+ limit / quota / cap / budget / ceiling as **fixed**, either re-read it at **≥2 points in the entity's
502
+ lifecycle** (e.g. at creation and again mid-run) or record it as `observed-at-T` rather than as a structural
503
+ property. A first reading that merely *looks* bounded is an observation, not a contract — promoting it to a
504
+ structural fact is an inference-stated-as-fact defect, and it is one the same run usually refutes on its own.
505
+ (Origin: Emergent — `GET /budget/{job_id}` returned `max_budget: 25.0` on job acceptance and was written up
506
+ as "a hard per-job credit ceiling"; the *same field on the same job* later read `100.87`, because it tracks
507
+ available account credit. The cap was never real.)
508
+
509
+ **And when a re-read moves, look for the SIBLING FIELD that is the real cap.** Do not merely re-label a
510
+ moving value `observed-at-T` — **search the write payload for a client-declared field that states the
511
+ limit outright.** A moving server-side figure and a static request field are usually two different
512
+ things, and the request field is the contract. (Origin: Emergent, second run — `budget.max_budget` read
513
+ `25.0`, then `100.89` on the same job, while the submit payload carried `per_instance_cost_limit: 25`
514
+ all along. The moving number was account credit; the static one was the actual per-job cap.)
515
+
293
516
  **Capability gap = a checkpoint, not a silent fallback.** When a _preferred_ capture capability is found **unavailable at runtime** — a CDP/remote-debugging port is closed, Chrome / the browser MCP is unreachable, a gated proxy or tool can't be stood up — such that you would proceed on a **materially weaker** method (e.g. source-only or a digest instead of a live wire tap), **pause and confirm with the user before continuing.** State plainly: what is unavailable, the degraded method you would fall back to, and the coverage that would be lost. This is distinct from a _planned_ method pre-grade (e.g. `openapi-verbatim` → `docs-reconstructed` when no spec is served), which is **recorded, not confirmed** — the checkpoint is for an _infrastructure/capability_ gap discovered mid-run, where silently degrading hides a real coverage loss behind a confident-looking capture. (A real run found the CDP port closed and silently fell back to a browser-MCP tap; it worked, but the degradation should have been surfaced first.)
294
517
 
518
+
519
+ **A `session` capture is not `complete` until the verbatim envelopes exist, not just the decoded prose.**
520
+ The §9.5 contract requires `raw/*.json` (with `_meta` + redacted `body` + `_decoded_fields`) so a later run
521
+ can *reproduce* the capture, not merely read about it. A session lane may therefore only be marked
522
+ `status: complete` once **at least** these three exist as redacted `raw/*.json`:
523
+
524
+ 1. the **identity/account** read (`/me`, `/account`, `/credits`, or the tenant equivalent),
525
+ 2. one **canonical list** read (the entity taxonomy), and
526
+ 3. the **primary write** request+response envelope.
527
+
528
+ Rich `captures/*.md` narrative does **not** substitute — prose can record a *schema* while losing the
529
+ *envelope* (status codes, headers-shape, error shape, field ordering) that the reproducer needs. If an
530
+ envelope genuinely could not be captured, mark the lane `partial` and record it in `gaps:` rather than
531
+ calling it complete. (Origin: Emergent — the session lane produced excellent decoded captures and a full
532
+ shared path catalog, but zero `raw/*.json`, so the endpoint envelopes are unreproducible.)
533
+
295
534
  ---
296
535
 
297
536
  ## 9. The session dimension — logged-in browser-session playbook
@@ -316,6 +555,8 @@ A single linear sweep wastes context on things you can read for free and wastes
316
555
 
317
556
  **Pass 1 — read-only mining (no money, no state change).** If the user already has data (a project, conversation, account, order), mine it first. Hit every list/state/history endpoint with the existing token; capture verbatim bodies. This yields ~70% of the spec for free. Mine, by role: identity (`/me`, `/account`, `/workspaces`), list (the canonical taxonomy), single-resource state (server-injected vs client-sent fields), history/event-log (a whole trajectory in one call), runtime/preview state, per-resource billing, account billing, feature flags/cohort, **the account's `entitlements`/`permissions` arrays** (the single richest data-model artifact — `entitlements`/`features` enumerate every gated feature _including ones your plan hides_; `permissions` are CRUD-verbs-per-entity, so the verb set enumerates the domain entity set — `X_CREATE/VIEW/EDIT/DELETE` ⇒ entity `X`; mine these first), integration catalog (`/mcp/`, `/integrations/`), and schema/capabilities (`/openapi.json`, `/.well-known/*`, `/page-metadata`).
318
557
 
558
+ > **Mine the cohort / entitlement / flags endpoint FIRST — it is routinely the single richest artifact of a run.** Modern SaaS SPAs resolve the whole tenant's feature surface in **one authenticated call** (`/user/cohort`, `/entitlements`, `/flags`, `/bootstrap`, `/me/features`, `/config`). One request returns the **complete flag key-set with this tenant's resolved values** — richer than an exhaustive bundle string-mine, and it names capabilities that have **no public marketing footprint at all**. Read the **key set** as the durable finding and the **values as tenant-scoped**: a `false` means *not enabled for this tenant*, **never** "the feature does not exist". (Origin: Emergent — a single `POST /user/cohort` returned **72 flags**, including whole unshipped surfaces and a sibling product's feature family, in one call.)
559
+
319
560
  **Pass 2 — fresh-run validation (with explicit cost/state-change confirmation).** Only after Pass 1, and only with explicit user OK stating the exact action + cheapest mode + estimated cost. Catches what Pass 1 can't: the submit/write request body, live state-machine transitions, the completion event, server-side resolution of shorthand, ID/slug-generation, the real iteration endpoint, and the full realtime event catalog. **Legitimate to skip** if the user opts out — leave write-side shapes as open questions; never fabricate them.
320
561
 
321
562
  > **Record the write-side decision as a machine flag, not re-derived prose.** A Pass-1-only run leaves the _entire_ write/generate/launch surface unobserved — feature _existence/gating_ is solid, but feature _execution behavior_ rests on marketing/docs and must never be promoted to fact (this caveat otherwise gets re-hedged across 3–4 rollups every cost-gated run). So the session `_summary.md` frontmatter carries `write_side_observed: <true|false>` and `pass2: <not-applicable | offered-pending | declined-by-user | deferred | done>` (this flag is the load-bearing part — it lets Evaluation render the write-side gap _once_). When it would help a later run resume, optionally add `captures/pass2-followup.md` listing the **exact cheapest-mode write actions + est. cost** that would close the gap. Discovery seeds the `pass2:` decision in `00-recon-plan.md` for any `auth:have-login` target.
@@ -344,10 +585,10 @@ The same phases apply whether the target is an AI builder, analytics dashboard,
344
585
 
345
586
  - **WebSocket / EventSource / fetch+XHR interceptors** — wrap the constructors/methods to log URLs, frames, and bodies (shapes, not secrets — redact in the wrapper before anything reaches disk). Add a `SCRUB(value)` step inside the fetch wrapper so credentials never reach the harness. **Never log a binary frame as opaque `<binary>`.** Any realtime/streaming/collab layer (token streams, CRDT/OT edits, presence, telemetry, game or market state) may ship `ArrayBuffer`/`Blob` frames; for each _distinct_ frame capture its **byte-length + a hex dump of the first ~16 bytes** — enough to fingerprint the wire format (protobuf, MessagePack, CBOR, Yjs/CRDT, gRPC-web, or a text/topic-prefixed channel protocol) without recording content. **Decode the signature as ASCII first** — many "binary" protocols are framed text (STOMP, Phoenix, Centrifugo, a `+topic` channel prefix), so a legible prefix names the protocol in one step before a binary-codec guess. The frame _signature_ is the finding; the payload is not.
346
587
  - **Realtime-on-load capture (the connection that opens before you can wrap it).** The "install the interceptor FIRST" rule assumes you are _already on_ the target surface; it does **not** cover a realtime connection that fires during the **initial load of a page you `navigate` to** (or a lazy-loaded/`iframe`d live demo). A post-load interceptor **cannot** wrap a `WebSocket`/`EventSource` constructed before injection, and an `offline`→`online`/`focus` reconnect-trigger may spawn no new connection on such a demo — so frame capture comes back empty (a real run got 0 frames on a collab-demo this way). When that happens: (a) capture the **CDP-visible auth/token-mint fetch** that precedes the socket (it names the realtime host/handshake); (b) **the technique that works — client-side-navigate within the SPA** (a router `<Link>` click, not a full reload): the in-page wrapper **survives client-side nav**, and the next route **mounts a fresh room → a new socket through your wrapper** (this captures the handshake + frames where `offline`/`online` reconnect-triggers don't — a real run decoded Liveblocks' v8 JSON protocol this way after a 0-frame first pass); (c) if a raw **CDP `Network.webSocketFrameReceived`** tap is reachable, it captures frames protocol-level with no injection-timing problem at all (the browser MCP's HTTP-only `read_network_requests` does **not** surface WS frames — that gap is what forces (b)); and (d) fall back to the `codebase`/`distribution` lanes for the wire format — record it as a _folded_ gap, not a silent miss, since the protocol is then sourced independently rather than live-observed. **Decode captured frames against any in-repo protocol enums** (message-code / op-code tables) so the live wire _validates_ the source read.
347
- - **Storage shape inspector** — enumerate localStorage / sessionStorage / cookies / IndexedDB **key names + lengths**, never raw values.
588
+ - **Storage shape inspector** — enumerate localStorage / sessionStorage / cookies / IndexedDB **key names + lengths**, never raw values. **Treat this census as an independent vendor-corroboration lane, not just an auth probe.** Key *names* alone fingerprint the embedded vendor stack (see the `tradecraft.md` §2.1 prefix table) and are derived from what the app **actually stores** — so they corroborate (or refute) a CSP/bundle-declared vendor list from a genuinely different source. Run it early and diff it against the CSP. (Origin: Emergent — a 40-key census confirmed Supabase, Pusher, PostHog, New Relic, VWO and Razorpay *and* surfaced Intercom, Criteo, Reddit Pixel, Bing UET and Commission Junction, none of which the CSP named.)
348
589
  - **JWT decoder** — decode the token _structure_ (alg, claim names, exp) with identity claims (`sub`, `email`, `phone`, `user_id`, `uid`, `sid`) redacted.
349
590
  - **Auth-channel disambiguation.** Token _location_ is itself a tell: not in localStorage ⇒ an httpOnly cookie **or** an SDK store (IndexedDB — Firebase/Auth0/Supabase/Cognito keep tokens there, which is why a "no JWT in localStorage" result is a finding, not a dead end). Determine the _required_ transport by **omission**: replay one harmless authed read — using a field that needs the authed context (`viewer`/ `me`), **never** a schema-only field (GraphQL `__typename`, a health route) that resolves unauthenticated — with each candidate credential dropped (cookie-only, then header-only); whichever response comes back unauthorized names the channel the app actually requires.
350
- - If a realtime SDK is loaded (Pusher, Ably, Socket.IO, Centrifugo, Phoenix, …), bind a global listener to capture the event/channel catalog.
591
+ - **MANDATORY realtime library tap — a `window.WebSocket` wrapper is NOT sufficient evidence of absence.** If a realtime SDK global is present (`window.Pusher` / `Ably` / `io` / `Centrifuge` / `Phoenix` / `Socket`) **or** a `pusher*` / `ably-*` / `*Transport*` / `socket*` key appears in the storage census, the **library-specific event-bus tap is REQUIRED before ANY realtime claim — positive or negative.** These libraries open their socket through `window.WebSocket` **at app boot**, before any wrapper you install post-load; a constructor wrapper is therefore *structurally blind* to them (gotcha #13/§2.12), and "my tap saw no frames" is **not** evidence the channel is idle. Install the tap (`p.bind_global(...)` for Pusher; the same pattern via `Ably.Realtime.instances` / `io.managers` / `Centrifuge` handles) and read the live handshake (`state`, `cluster`, `channels`) — see `tradecraft.md` Tap toolkit item 4. **Reporting "vendor X carries no traffic" without library-tap evidence is a named Ingestion defect.** (Origin: Emergent — a post-boot `window.WebSocket` wrapper saw zero Pusher frames and the run published "Pusher does not carry build streaming" as a refuted hypothesis; Pusher was live the entire time — `connected`, `cluster: us2`, channel `user-{uuid}`, carrying `traj-update`/`terminal-state`/`streak-update`.)
351
592
  - **Third-party-SDK fingerprint** (cross-checks `infra-backend-fingerprint`). Storage key names + the network host list name the embedded vendor stack — auth (Firebase/Auth0), flags (LaunchDarkly/Statsig), support (Intercom/Zendesk/Ada), analytics (Segment/Amplitude/Datadog), payments (Stripe). Record them; they corroborate the sub-processor list _independently_ (a different dimension → promotes confidence).
352
593
  - **Drive op coverage from any catalog a sibling declared.** If `deployed-client-bundle` (or `api`/`docs`) recovered a declared **route / endpoint / operation catalog** — a REST path table, a GraphQL operation set, an RPC/tool method list — consume it as a **coverage checklist**: walk the routes/actions that trigger the un-observed entries so the declared-vs-observed diff (§6) reflects a real walk, not just the landing page.
353
594
  - **Server-side-read (RSC) check — don't mis-record "no read API."** In a modern Next.js App-Router / React-Server-Components app (and similar SSR/streaming stacks), the initial **read** data is fetched _server-side_ with the session and streamed — so the client wire shows only mutations, asset/media fetches, telemetry, and token-mints, and **no client-side data-read XHRs fire on load even though the UI is fully populated.** Before recording "read API not observed," check for the server-side path: a document/`?_rsc=` response with `text/x-component`, embedded `__next_f`/flight payloads, `_next/data` JSON, or server actions. When reads are server-resolved, say so — and note that a sibling bundle's app-own _path count_ then **overstates the client read surface** (fold this into the published-vs-app-own diff, §6, rather than reporting a phantom "unused routes" gap). (Writes can be off-main-thread too — a generate POST issued from a **Web Worker** won't appear in a main-thread fetch tap; corroborate via CDP / Performance resource timing.)
@@ -37,6 +37,7 @@ Inspect the run and emit **every** defect that fired, each as: `{mode, defect, s
37
37
  - **Silent absence** — a dimension dropped with no `status: absent/folded` record. _(med)_
38
38
  - **Went silent / looked hung** — a long fan-out with no summary-first partial (ingestion §8). _(low)_
39
39
  - **An absence asserted that contradicts a sibling**, without a second-method re-check (ingestion §7 rule 10). _(med)_
40
+ - **A negative realtime finding with no library-tap evidence** — "vendor X carries no traffic" / "the dashboard has no realtime layer" asserted from a `window.WebSocket` wrapper alone, when a realtime SDK global or a `pusher*`/`ably-*`/`*Transport*` storage key was present and the mandatory `bind_global`-class tap (ingestion §9.4) was never installed. _(HIGH — a constructor wrapper is structurally blind to a boot-time socket, so this converts instrument blindness into a published refutation)_
40
41
 
41
42
  ### Evaluation defects
42
43
 
@@ -51,6 +52,7 @@ Inspect the run and emit **every** defect that fired, each as: `{mode, defect, s
51
52
  ### Cartography defects (Mode 4 — the experiential / coverage layer)
52
53
 
53
54
  - **Marketed feature not located and not dispositioned** — a website/docs/changelog feature with no in-product route AND no `edition-gated | roadmap-not-shipped | over-claim | deeper-than-looked` disposition in `feature-coverage.md`. _(med — an unfilled coverage hole masquerading as "covered")_
55
+ - **`not-yet-reachable` surface mis-filed as `deeper-than-looked`** — a shipped surface that does not render until a state change (deploy / connect / upgrade / first-run) filed as a navigation shortfall, sending the read-only auto-loop after something no amount of walking can reveal. Equally: an entitlement flag reading `true` treated as proof the surface is reachable, when the gate is two-factor (flag **and** state). _(med — it mis-routes the within-run loop and mis-reads an authorization boundary as sloppiness)_
54
56
  - **Located-but-not-walked not queued** — a feature found in the nav but never navigated, absent from the within-run re-walk queue (so Mode 5 never closes it). _(med)_
55
57
  - **Nav destination unmapped** — a top-level nav destination with no surface card in `information-architecture.md` (`ia_nav_complete:false` with no recorded reason). _(med)_
56
58
  - **Primary flow not diagrammed** — an identified core journey (a captured write/auth/realtime/AI wire) with no `ux-flows.md` sequence diagram. _(med)_
@@ -76,10 +78,10 @@ Inspect the run and emit **every** defect that fired, each as: `{mode, defect, s
76
78
  | --- | --- | --- |
77
79
  | 1 | **Dimensional coverage** (15) | `15 × (dimensions captured 'complete' ÷ dimensions reachable per the access vector)`; **−3** per dimension blocked where Discovery predicted available. |
78
80
  | 2 | **Experiential / IA coverage** (20) | the Cartography scorecard, fed in: `8 × feature_location_rate/100` + `6 × flow_coverage/100` + `4 × screenshot_coverage/100` + `2 ×` (`ia_nav_complete` ? 1 : surface-cards ÷ top-level-destinations); **−2** per _claimed-but-not-located_ feature left **un-dispositioned**. Floored at 0. **When the session/experiential lane is deliberately deferred — gated by policy or no-login, NOT a tool failure — set `experiential_gated:true`:** keep the honest 0 on observed `flow_coverage`/`screenshot_coverage` (the run truly lacks live coverage), but **record the cause** beside the axis so cross-run comparison can tell a policy-deferral from a sloppy coverage miss — the annotation documents, it does **not** inflate the score. _(the heavily-weighted axis: a run that collects feature pages but never locates / walks / diagrams / screenshots them scores low here.)_ |
79
- | 3 | **Provenance integrity** (15) | `7.5 × (captures with complete honest frontmatter ÷ all captures)` + `7.5 × (evaluation claims anchored+provenance-tagged ÷ all claims)`. |
81
+ | 3 | **Provenance integrity** (15) | `7.5 × (captures whose §5.4 dump manifest is satisfied ÷ all captures)` + `7.5 × (evaluation claims **backed by a resolvable `raw/` artifact** ÷ all claims)`. **Score the artifact, not the syntax.** An anchor is satisfied only if the cited file exists AND contains the claim's key token — a well-formatted citation pointing at a file that does not contain the value scores **zero**, not full marks. (Origin: layer-ai — a finding promoted on *"five independent dimensions"* cited a dimension in which the relevant string appears zero times; the evidence was real in-session and never dumped. Scoring anchor *presence* rewarded exactly the behaviour that lost it — see ingestion §5.3.) |
80
82
  | 4 | **Reconciliation correctness** (20) | start 20; **−3** per weighting violation (over-promotion, tiebreaker skipped, single-source-as-fact, count-divergence-as-conflict, published-vs-app diff missing where a public API exists, a UX-flow drawn observed on a `write_side_observed:false` run). |
81
83
  | 5 | **Calibration & safety** (15) | gaps/absences recorded honestly + confidence bands honest, scaled to 15. **A redaction miss hard-zeros this axis** and flags the run `unsafe` regardless of the others. |
82
- | 6 | **Defect load** (15) | `15 − Σ(severity weight)` over stage-A defects (high=3, med=2, low=1), floored at 0. |
84
+ | 6 | **Defect load** (15) | `15 − Σ(severity weight)` over **defects still OUTSTANDING at the end of the run** (high=3, med=2, low=1), floored at 0. **A defect found AND fixed within the run is charged on its home axis only, not here** — it is already paid for by the axis it damaged, and charging it twice makes auditing strictly dominated by not looking. Record every defect found in the ledger regardless (the list stays monotonic and honest); the *score* charges only what remains broken. |
83
85
 
84
86
  **Rollup:** sum the six → `score/100` (max 15+20+15+20+15+15). **Vibe band:** ≥85 strong · 70–84 solid · 50–69 thin · <50 weak. Plus a one-line gestalt (e.g. "solid wire capture, but cartography located only 9 of 17 marketed features and screenshotted none").
85
87
 
@@ -122,6 +124,8 @@ present(harness) as 05-self-correction-proposal.md + the iteration ledger # a
122
124
 
123
125
  1. **Converged** — no within-run defects remain.
124
126
  2. **Regression** — `score[iter] < score[iter-1] − TOL` → **revert to the best prior run and stop** (never let the loop make the run worse chasing a defect). `TOL` is a small tolerance (≈2 points) so a within-noise wobble doesn't false-stop — stop only on a _clear_ drop; always keep the highest-scoring run as `best`.
127
+
128
+ > **Distinguish a _quality_ regression from a _measurement_ correction — they look identical in the ledger and must be handled oppositely.** If a score drop is caused by **discovering that a PRIOR iteration's score rested on an error** (a claim it scored well for turns out false; a redaction sweep that "passed" is found to have been incapable of matching the target shape; a coverage number counted an unverified surface), that is **NOT a regression — do NOT revert.** Reverting would restore the *false* artifacts and discard the correction, i.e. the loop would actively select for undetected error. Keep the corrected iteration, record the earlier score as **superseded (inflated)** rather than as the `best` to beat, and note the correction in the ledger. **The revert rule applies only when a fix made the artifacts themselves worse.** _(Corollary — defect load is monotonic: a defect, once found, stays on the ledger permanently, so a run that verifies harder scores its axis-6 lower than one that never looked. Read a falling score alongside the defect list; "score fell because we found real errors" is a run getting **better**, and the ledger must say so.)_
125
129
  3. **Cap** — `iter` hits the cap. **The cap is ≥ 5**: the loop must be _willing_ to run at least five iterations on a still-improving run before giving up; raise it while the score keeps climbing.
126
130
 
127
131
  ---