1 parent
ce4d994598
commit
c0ce1bd026
5 files changed
+377
-3
No files matched your search
@@ -0,0 +1,221 @@
|
|||||||
|
# ADR 0005: Web browser qualification and reference performance passport
|
||||||
|
|
||||||
|
- Status: proposed; awaits independent security review of S00-006.
|
||||||
|
- Task / requirements: S00-006 / NFR-003, NFR-005; specification §27.
|
||||||
|
- Date: 2026-10-15.
|
||||||
|
- Decision owner: auto-orchestrator-710c6ba5d8ea (orchestrator).
|
||||||
|
- Reviewer: assigned security reviewer, not yet recorded.
|
||||||
|
|
||||||
|
## Context and decision
|
||||||
|
|
||||||
|
Input: accepted ADR 0001, exact requirements, architecture and contract registry,
|
||||||
|
S00-002 executor/review evidence. Output: one qualification passport with concrete
|
||||||
|
hardware, browser policy, measurable budgets and permission/fallback procedures.
|
||||||
|
This decision adds no shared interface, application implementation or dependency.
|
||||||
|
Budgets are proposed engineering acceptance targets, not observed performance or
|
||||||
|
a universal SLA. Approval selects reference devices; actual product qualification
|
||||||
|
requires later browser runs on these devices and the production app.
|
||||||
|
|
||||||
|
Select six targets below. The machine-readable table and budget values are in
|
||||||
|
`experiments/0005-browser-matrix/matrix.json`; this ADR defines their meaning.
|
||||||
|
|
||||||
|
| ID | Reference hardware | OS family | Browser / syntax lower bound | CSS viewport / DPR | Modes |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| win-chrome | ThinkPad T14 Gen 2 Intel, i5-1135G7, 16 GiB | Windows 11 24H2 x64 | Chrome stable / 111 | 1366×768 / 1 | tab, installed |
|
||||||
|
| win-edge | Same ThinkPad | Same Windows | Edge stable / 111 | 1366×768 / 1 | tab, installed |
|
||||||
|
| linux-firefox | Same ThinkPad | Ubuntu 24.04 LTS x64, GNOME Wayland | Firefox stable / 114 | 1366×768 / 1 | tab |
|
||||||
|
| mac-safari | MacBook Air M1 2020, 8 GiB, 7-core GPU | macOS 15 | Safari stable / 18 | 1280×800 / 2 | tab, installed web app |
|
||||||
|
| android-chrome | Pixel 6a 128 GB, Tensor, 6 GiB | Android 15 | Chrome stable / 111 | 393×851 / 2.75 | tab, installed |
|
||||||
|
| ios-safari | iPhone 13 128 GB, A15, 4 GiB | iOS 18 | Safari stable / 18 | 390×844 / 3 | tab, Home Screen |
|
||||||
|
|
||||||
|
All measurements use 60 Hz. Desktop SSD, mobile internal storage; leave ≥20%
|
||||||
|
free. Mobile portrait, standard display scaling/font size, browser UI expanded
|
||||||
|
at navigation; record actual inner viewport after browser chrome. Desktop set
|
||||||
|
window inner viewport explicitly. Also test mobile landscape, 320 CSS px width,
|
||||||
|
wide 1440 CSS px, keyboard/IME, dark mode, 200% text/zoom and reduced motion as
|
||||||
|
functional variants; they are not substitutes for physical performance devices.
|
||||||
|
|
||||||
|
Support policy: qualify the vendor-supported stable build available for each OS
|
||||||
|
at release cutoff, then freeze its full build in the run record. Floors above
|
||||||
|
come from ADR 0001, with Safari 18 selected for the named OS families; they do
|
||||||
|
not authorize using obsolete vulnerable builds online or promise support for all
|
||||||
|
versions since the floor. If an OS/browser is no longer vendor supported at the
|
||||||
|
cutoff, revise this passport through review before qualification. Beta/dev,
|
||||||
|
embedded webviews, other iOS browsers, private profiles and extensions are not
|
||||||
|
qualified targets. Private/blocked storage must still fail honestly.
|
||||||
|
|
||||||
|
Exact patch versions are intentionally resolved at qualification, rather than
|
||||||
|
inventing future release pins now. Every run MUST supply all `freeze_fields` in
|
||||||
|
the JSON: full OS/browser build, acquisition source and installer hash (or Apple
|
||||||
|
OS build), app/asset/fixture hashes, profile reset recipe, tools, time, actual
|
||||||
|
viewport/DPR/refresh rate and network settings. Archive synthetic fixtures and
|
||||||
|
build assets with the record. A record saying only “latest” or “stable” is
|
||||||
|
invalid. Retest the frozen builds for comparison; qualify a new stable version
|
||||||
|
with a new record after security updates. No automatic acceptance carries over.
|
||||||
|
Physical devices may be rented/lab-owned; no claim of their availability here.
|
||||||
|
Emulation/Playwright WebKit alone cannot qualify Safari or mobile hardware.
|
||||||
|
|
||||||
|
## PWA and capability policy
|
||||||
|
|
||||||
|
Tab mode is primary. Installation is optional progressive enhancement, not a
|
||||||
|
native Desktop/Android client. No automatic install prompt is assumed. Firefox
|
||||||
|
uses tab mode in this passport. iOS ordinary Safari tab has no promised Web Push;
|
||||||
|
Home Screen mode with granted permission, valid subscription and configured
|
||||||
|
provider is a separate required push scenario. No installation mode promises
|
||||||
|
continuous sockets/media after hiding, locking, closing or terminating browser.
|
||||||
|
Resume requires reconciliation; password re-entry/unlock follows its accepted
|
||||||
|
policy. No offline send/delivery guarantee is introduced.
|
||||||
|
|
||||||
|
The JSON `fallbacks` table is normative for all six targets. Probe capabilities
|
||||||
|
and actual initialization outcomes, not user agent names. Missing required
|
||||||
|
crypto/secure transport blocks the protected operation. Missing optional media,
|
||||||
|
location, push, install or biometric capability disables only the dependent
|
||||||
|
feature and gives a reason. Synthetic fixtures never replace production crypto,
|
||||||
|
IndexedDB or transports. Storage failure and eviction follow accepted ADR 0004;
|
||||||
|
this cache budget does not revise persistence schemas or unlock policy.
|
||||||
|
|
||||||
|
## Budgets and measurement protocol
|
||||||
|
|
||||||
|
| Metric | Desktop | Mobile | Gate |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Cold startup p95 | 2500 ms | 4000 ms | navigation start → usable local shell |
|
||||||
|
| Warm startup p95 | 1200 ms | 2000 ms | same end point, cached assets/history |
|
||||||
|
| Action → painted result p95 | 100 ms | 150 ms | open chat, select, type, scroll |
|
||||||
|
| rAF gap p95 / maximum | 20 / 100 ms | 25 / 150 ms | foreground scripted scroll, 60 Hz |
|
||||||
|
| Peak app process RAM | 350 MiB | 250 MiB | 10-minute non-call session |
|
||||||
|
| Retained RAM growth | 30 MiB | 25 MiB | final settled minute minus first |
|
||||||
|
| Disposable cache ceiling | 128 MiB | 64 MiB | all accounts combined per origin |
|
||||||
|
|
||||||
|
Protocol for a later production qualification (not executed in this task):
|
||||||
|
|
||||||
|
1. Build production assets with accepted ADR 0001 pins and lockfile. Serve via
|
||||||
|
valid HTTPS with ordinary certificate validation; no dev server timings. Use
|
||||||
|
no extensions, other tabs or competing foreground apps; power connected,
|
||||||
|
power saver off, thermal nominal, ≥50% battery. Record OS/browser builds and
|
||||||
|
tool versions before runs. Disable diagnostics during timing except required
|
||||||
|
trace collection. Keep measurements with tracing separately labeled.
|
||||||
|
2. Deterministic synthetic dataset: one active account, 100 conversations × 100
|
||||||
|
messages, text `message-00000` through `message-09999`, each padded with ASCII
|
||||||
|
`x` to 512 characters; 50 visible messages in selected chat. No real JIDs,
|
||||||
|
credentials or conversations. Seed through future test fixture adapters, not
|
||||||
|
production authentication bypass. Hash the exact serialized fixture and seed
|
||||||
|
script; check local row count before each run. Cache workload: 1 MiB synthetic
|
||||||
|
byte blobs until the ceiling plus two blobs. No audio/video running in the RAM
|
||||||
|
workload; call/media owners must later define separate active-call budgets.
|
||||||
|
3. Controlled network: 10 Mbit/s download, 2 Mbit/s upload, 80 ms round-trip,
|
||||||
|
zero intentional loss, no CPU throttle. Use a recorded external shaper for
|
||||||
|
physical devices; record tool version/config and measured throughput/RTT to
|
||||||
|
fixture host. WSS/MAM/server latency is measured separately and not part of
|
||||||
|
usable-local-shell time. “Ready” means conversation list and 50 local messages
|
||||||
|
painted, composer focusable and input accepted. Login/unlock/password human
|
||||||
|
time is excluded and reported separately. Never mark connected/crypto ready
|
||||||
|
merely to satisfy this local timing gate.
|
||||||
|
4. Thirty cold runs: retain seeded IndexedDB, clear HTTP/disposable asset cache
|
||||||
|
and unregister prior service worker, terminate browser, reopen at target URL.
|
||||||
|
“Cold” here means application asset cold, not OS disk cold. Thirty warm runs:
|
||||||
|
first prime assets/worker and fixture, close tab and reopen with same profile.
|
||||||
|
Installed/Home Screen mode repeats both sequences and reports separately.
|
||||||
|
Record navigation start and app-ready mark using Performance API; confirm
|
||||||
|
visible ready UI in trace/screenshot. Report raw runs, failures and p95 via
|
||||||
|
sorted nearest-rank ceil(0.95*n). Do not discard slow runs or mix modes.
|
||||||
|
5. For each mode collect 100 actions each (open chat, select message, type one
|
||||||
|
character, scroll one viewport), paced 500 ms apart; 60 seconds continuous
|
||||||
|
deterministic scroll. Measure input timestamp → frame containing changed UI
|
||||||
|
with browser performance trace. A double-rAF marker is only a proxy and must
|
||||||
|
be labeled. Collect rAF timestamps in foreground; compute consecutive gaps,
|
||||||
|
p95 and maximum. Record hidden/throttled runs separately, not as foreground
|
||||||
|
passes. Any action failure fails function regardless of percentile.
|
||||||
|
6. RAM: record baseline blank profile browser process tree, then sample app
|
||||||
|
renderer/WebContent plus dedicated workers every second during 10 minutes of
|
||||||
|
repeated chat switching/scrolling. Chromium task manager/process trace,
|
||||||
|
Firefox about:processes plus OS RSS, Safari WebContent/worker OS RSS (macOS
|
||||||
|
Activity Monitor; iOS Instruments on physical device). Use isolated browser
|
||||||
|
profile so shared workers are attributable. Report sum RSS and baseline
|
||||||
|
separately, not JS heap as total RAM. Peak must fit ceiling; last-minute
|
||||||
|
median minus first-minute median must fit growth budget, with no forced GC.
|
||||||
|
If attribution/tool access is unavailable, RAM gate is unverified, never 0.
|
||||||
|
7. Cache: sum owned disposable asset/media payload bytes with adapter inventory;
|
||||||
|
compare browser origin storage estimate as advisory, not exact cache size.
|
||||||
|
Limit is min(reference ceiling, 10% reported quota) when estimate available;
|
||||||
|
if unavailable, use reference ceiling and stop on actual quota error. Before
|
||||||
|
admitting another blob evict disposable LRU entries or reject the write;
|
||||||
|
never exceed budget deliberately. History, crypto/trust and pending unsynced
|
||||||
|
work are excluded from disposable cache and must not be evicted by this gate.
|
||||||
|
Confirm counts after reopen; inject QuotaExceededError and test actual device
|
||||||
|
low-disk failure separately. No checkpoint advance on save failure. Browser
|
||||||
|
may evict the entire origin: verify explicit recovery state after user clear.
|
||||||
|
|
||||||
|
Pass each target/mode independently; a fast desktop cannot offset mobile failure.
|
||||||
|
These numbers are initial product targets chosen for responsiveness and bounded
|
||||||
|
resources, not a benchmark derived from the small S00-002 counter. Later measured
|
||||||
|
infeasibility requires reviewed budget/device change, not silently relaxing gates.
|
||||||
|
|
||||||
|
## Contextual permissions and negative scenarios
|
||||||
|
|
||||||
|
For each target AND mode, run the following against a trusted HTTPS test origin
|
||||||
|
with synthetic identities. Fresh profile has no prompts at startup. Trigger only
|
||||||
|
via explicit relevant action: Start call/record (mic/camera), Send location
|
||||||
|
(geolocation), Enable notifications (push), Enable unlock (only S00-007 policy).
|
||||||
|
Storage persistence request must explain context rather than masquerade as login.
|
||||||
|
|
||||||
|
| State | Reproduction | Required observation |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| prompt / granted | Reset site and OS permissions; invoke each action and grant | Only requested capability activates; no unrelated permission prompt |
|
||||||
|
| denied | Deny browser prompt, then deny at OS level separately | Reason/actionable settings path; text still usable; no retry prompt loop |
|
||||||
|
| hardware missing | Disconnect USB camera/mic on desktop; use OS-disabled device on mobile | Capture fails honestly; camera-only failure may offer working audio |
|
||||||
|
| revoked during session | Start capture then revoke site/OS permission; unplug device; separately revoke location/notification permission | End/reconcile capture, stop watches; clear usable/push state on observed failure or resume; no stale success |
|
||||||
|
| unsupported | Disable API via test-only fixture boundary or use naturally absent mode | Each JSON fallback observed, protected operations remain blocked |
|
||||||
|
| ignored prompt / busy | Leave prompt unanswered then cancel action; separately hold camera in another app | Pending is cancellable; late grant is stopped/ignored; busy error not labeled denial |
|
||||||
|
| lifecycle / loss | Hide, lock, sleep, offline, close, reopen; remove subscription and clear origin | No continuous session promise; resync/unlock and explain lost local state |
|
||||||
|
|
||||||
|
A Permissions API query may be unsupported for a descriptor: catch failure and
|
||||||
|
rely on real API result and track/error events; recheck on focus/resume. Revocation
|
||||||
|
may arrive through track ended or subsequent failed operation, not a uniform
|
||||||
|
permission event. Include denied→settings grant→explicit retry and account switch
|
||||||
|
while prompt pending. Do not log coordinates, device labels, endpoints or secrets.
|
||||||
|
Screenshots contain synthetic UI only. Browser/OS policy injection is labeled as
|
||||||
|
fixture evidence; it does not prove physical device revoke or hardware absence.
|
||||||
|
|
||||||
|
## Candidates, sources and constraints
|
||||||
|
|
||||||
|
Primary sources inspected 2026-10-15; these describe mechanisms, not results on
|
||||||
|
our selected devices:
|
||||||
|
|
||||||
|
- [Vite build compatibility](https://vite.dev/guide/build.html): syntax floors and
|
||||||
|
lack of automatic API polyfills. The matrix does not change ADR 0001 targets.
|
||||||
|
- [Chrome release channels](https://developer.chrome.com/docs/web-platform/chrome-release-channels):
|
||||||
|
stable qualification is chosen over beta/dev; exact run builds still required.
|
||||||
|
- [WebKit iOS Web Push](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/):
|
||||||
|
Home Screen eligibility and direct user interaction for permission requests.
|
||||||
|
- [Media Capture and Streams](https://www.w3.org/TR/mediacapture-streams/):
|
||||||
|
permission/device errors and track lifecycle underpin media negative scenarios.
|
||||||
|
- [Permissions](https://www.w3.org/TR/permissions/): permission state and change
|
||||||
|
handling; implementation support for individual descriptors remains conditional.
|
||||||
|
- [Storage Standard](https://storage.spec.whatwg.org/): persistence/estimates are
|
||||||
|
browser controlled; ceilings are application policy, not guaranteed disk quota.
|
||||||
|
- [Web Authentication Level 3](https://www.w3.org/TR/webauthn-3/): authenticator
|
||||||
|
operations do not establish a universal browser hardware-backed storage vault.
|
||||||
|
|
||||||
|
Rejected: Chromium-only coverage (misses independent engines/iOS behavior),
|
||||||
|
“all modern browsers” without reference hardware, emulator-only performance,
|
||||||
|
unconditional PWA push/background claims and frozen obsolete online binaries.
|
||||||
|
A broad rolling channel without recorded full builds is also insufficient.
|
||||||
|
Selected: bounded three-engine/six-target qualification with immutable run records.
|
||||||
|
|
||||||
|
No added libraries: dependency license review is not needed for this document and
|
||||||
|
stdlib checker. Existing stack/library notices remain with their respective ADRs.
|
||||||
|
No secrets, crypto implementation, network proxy, CORS/TLS exception or application
|
||||||
|
module is introduced. S01 contracts and downstream performance/browser/provider
|
||||||
|
implementation tasks retain ownership of exact adapter and telemetry interfaces.
|
||||||
|
|
||||||
|
## Focused experiment and actual result
|
||||||
|
|
||||||
|
`python3 experiments/0005-browser-matrix/check.py` validates the passport's target
|
||||||
|
coverage, reproduction fields, budget consistency and fallback coverage; negative
|
||||||
|
mutations prove incomplete targets, missing fallback and relaxed guards fail.
|
||||||
|
This is document/fixture validation only. No physical reference device, browser
|
||||||
|
permission behavior, production startup/RAM/render/cache measurement or provider
|
||||||
|
integration was executed. Their procedures above are external qualification
|
||||||
|
checks, not passed results. S00-006 selects a passport; it does not certify release.
|
||||||
|
Acceptance/qualification failure, OS vendor support ending, new engine/API need,
|
||||||
|
or changed resource workload requires a separately reviewed passport revision.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Browser passport validation
|
||||||
|
|
||||||
|
From project root, Python 3.10+ stdlib only:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 experiments/0005-browser-matrix/check.py
|
||||||
|
```
|
||||||
|
|
||||||
|
`matrix.json` contains target recipes, resource ceilings, required run-record
|
||||||
|
fields and capability fallback rules. Normative measurement and permission
|
||||||
|
procedures: `docs/decisions/0005-browser-matrix.md`. Target OS families/stable
|
||||||
|
channels must be resolved to supported full builds before a real qualification;
|
||||||
|
archive full build identifiers, installer hash or Apple OS build, app and fixture
|
||||||
|
hashes, actual viewport and tool configuration. No browser binaries are supplied.
|
||||||
|
|
||||||
|
The checker rejects missing targets/reproduction fields/fallbacks, compiler floor
|
||||||
|
regressions, impossible RAM ceilings and missing iOS Home Screen mode. Negative
|
||||||
|
fixtures exercise those refusals; percentile fixtures verify nearest-rank p95.
|
||||||
|
It does not execute a browser, simulate a successful permission request, measure
|
||||||
|
resources, qualify hardware or prove a real fallback implementation. A passing
|
||||||
|
passport check must not be reported as passing production browser acceptance.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""Validate a qualification passport, not browser capability or app performance."""
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
REQUIRED_FALLBACKS = {
|
||||||
|
"secure-context", "crypto-worker-wasm", "indexeddb", "persistence", "push",
|
||||||
|
"install", "media", "recording-codec", "geolocation", "webauthn",
|
||||||
|
"permissions-query", "lifecycle",
|
||||||
|
}
|
||||||
|
REPRODUCTION = {
|
||||||
|
"target_id", "run_utc", "device_model", "cpu", "ram_gib", "os_full_build",
|
||||||
|
"browser_full_build", "browser_channel", "installer_source",
|
||||||
|
"installer_sha256_or_os_build", "viewport_css", "actual_inner_size_css",
|
||||||
|
"dpr", "refresh_hz", "display_mode", "app_commit", "asset_sha256",
|
||||||
|
"fixture_sha256", "profile_reset_recipe", "network_profile",
|
||||||
|
"measurement_tool_version",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate(data):
|
||||||
|
targets = data["targets"]
|
||||||
|
ids = [t["id"] for t in targets]
|
||||||
|
assert len(ids) == len(set(ids)), "Duplicate target"
|
||||||
|
assert set(ids) == {"win-chrome", "win-edge", "linux-firefox", "mac-safari",
|
||||||
|
"android-chrome", "ios-safari"}, "Missing engine/device target"
|
||||||
|
assert set(data["freeze_fields"]) >= REPRODUCTION, "Incomplete reproduction record"
|
||||||
|
for t in targets:
|
||||||
|
for key in ("device", "cpu", "os", "browser", "channel", "budget"):
|
||||||
|
assert isinstance(t[key], str) and t[key].strip(), f"Missing {key}"
|
||||||
|
assert t["channel"] == "stable", "Unqualified channel"
|
||||||
|
assert t["ram_gib"] > 0 and t["dpr"] > 0 and t["hz"] == 60
|
||||||
|
assert len(t["viewport_css"]) == 2 and min(t["viewport_css"]) >= 320
|
||||||
|
assert "tab" in t["modes"] and len(t["modes"]) == len(set(t["modes"]))
|
||||||
|
floor = {"Chrome":111, "Edge":111, "Firefox":114, "Safari":18}[t["browser"]]
|
||||||
|
assert t["minimum_major"] >= floor, "Target below accepted syntax/OS floor"
|
||||||
|
b = data["budgets"][t["budget"]]
|
||||||
|
assert all(isinstance(v, (int, float)) and v > 0 for v in b.values())
|
||||||
|
assert b["warm_start_p95_ms"] <= b["cold_start_p95_ms"]
|
||||||
|
assert b["raf_gap_p95_ms"] <= b["raf_gap_max_ms"]
|
||||||
|
assert b["ram_growth_mib"] < b["ram_peak_mib"] < t["ram_gib"] * 1024
|
||||||
|
assert set(data["fallbacks"]) >= REQUIRED_FALLBACKS, "Missing capability fallback"
|
||||||
|
for rule in data["fallbacks"].values():
|
||||||
|
assert len(rule["detection"].strip()) >= 20, "No actionable detection"
|
||||||
|
assert len(rule["fallback"].strip()) >= 50, "No actionable fallback"
|
||||||
|
ios = next(t for t in targets if t["id"] == "ios-safari")
|
||||||
|
assert "home-screen" in ios["modes"], "iOS push mode missing"
|
||||||
|
|
||||||
|
|
||||||
|
def p95(values):
|
||||||
|
assert values and all(math.isfinite(v) and v >= 0 for v in values)
|
||||||
|
return sorted(values)[math.ceil(0.95 * len(values)) - 1]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
data = json.loads((HERE / "matrix.json").read_text())
|
||||||
|
validate(data)
|
||||||
|
# Reject plausible omissions and unsafe qualification drift.
|
||||||
|
mutants = []
|
||||||
|
m = copy.deepcopy(data); m["targets"].pop(); mutants.append(m)
|
||||||
|
m = copy.deepcopy(data); m["targets"][0]["cpu"] = ""; mutants.append(m)
|
||||||
|
m = copy.deepcopy(data); m["freeze_fields"].remove("browser_full_build"); mutants.append(m)
|
||||||
|
m = copy.deepcopy(data); del m["fallbacks"]["crypto-worker-wasm"]; mutants.append(m)
|
||||||
|
m = copy.deepcopy(data); m["fallbacks"]["media"]["fallback"] = ""; mutants.append(m)
|
||||||
|
m = copy.deepcopy(data); m["targets"][0]["minimum_major"] = 80; mutants.append(m)
|
||||||
|
m = copy.deepcopy(data); m["targets"][-1]["modes"] = ["tab"]; mutants.append(m)
|
||||||
|
m = copy.deepcopy(data); m["budgets"]["mobile"]["ram_peak_mib"] = 10000; mutants.append(m)
|
||||||
|
for i, m in enumerate(mutants, 1):
|
||||||
|
try:
|
||||||
|
validate(m)
|
||||||
|
except AssertionError:
|
||||||
|
continue
|
||||||
|
raise AssertionError(f"Invalid passport accepted: mutation {i}")
|
||||||
|
# Thirty-run nearest-rank semantics: two slow runs fail a 2500 ms p95 gate;
|
||||||
|
# a single tail still remains in raw evidence, not silently discarded.
|
||||||
|
assert p95([1000] * 28 + [3000, 4000]) == 3000
|
||||||
|
assert p95([2500] * 30) == 2500
|
||||||
|
assert p95([1000] * 29 + [4000]) == 1000
|
||||||
|
print(f"PASS: {len(data['targets'])} target recipes, {len(REPRODUCTION)} freeze fields, "
|
||||||
|
f"{len(data['fallbacks'])} capability fallbacks; {len(mutants)} negative mutations rejected; "
|
||||||
|
"nearest-rank percentile fixtures passed.")
|
||||||
|
print("LIMIT: document validation only; no browser/device execution or app budget pass.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"targets": [
|
||||||
|
{"id":"win-chrome","device":"Lenovo ThinkPad T14 Gen 2 Intel","cpu":"Intel Core i5-1135G7","ram_gib":16,"os":"Windows 11 24H2 x64","browser":"Chrome","minimum_major":111,"channel":"stable","viewport_css":[1366,768],"dpr":1,"hz":60,"modes":["tab","installed"],"budget":"desktop"},
|
||||||
|
{"id":"win-edge","device":"Lenovo ThinkPad T14 Gen 2 Intel","cpu":"Intel Core i5-1135G7","ram_gib":16,"os":"Windows 11 24H2 x64","browser":"Edge","minimum_major":111,"channel":"stable","viewport_css":[1366,768],"dpr":1,"hz":60,"modes":["tab","installed"],"budget":"desktop"},
|
||||||
|
{"id":"linux-firefox","device":"Lenovo ThinkPad T14 Gen 2 Intel","cpu":"Intel Core i5-1135G7","ram_gib":16,"os":"Ubuntu 24.04 LTS x64 GNOME Wayland","browser":"Firefox","minimum_major":114,"channel":"stable","viewport_css":[1366,768],"dpr":1,"hz":60,"modes":["tab"],"budget":"desktop"},
|
||||||
|
{"id":"mac-safari","device":"MacBook Air 13-inch M1 2020","cpu":"Apple M1 8-core CPU / 7-core GPU","ram_gib":8,"os":"macOS 15","browser":"Safari","minimum_major":18,"channel":"stable","viewport_css":[1280,800],"dpr":2,"hz":60,"modes":["tab","installed"],"budget":"desktop"},
|
||||||
|
{"id":"android-chrome","device":"Google Pixel 6a 128GB","cpu":"Google Tensor","ram_gib":6,"os":"Android 15","browser":"Chrome","minimum_major":111,"channel":"stable","viewport_css":[393,851],"dpr":2.75,"hz":60,"modes":["tab","installed"],"budget":"mobile"},
|
||||||
|
{"id":"ios-safari","device":"Apple iPhone 13 128GB","cpu":"Apple A15 Bionic","ram_gib":4,"os":"iOS 18","browser":"Safari","minimum_major":18,"channel":"stable","viewport_css":[390,844],"dpr":3,"hz":60,"modes":["tab","home-screen"],"budget":"mobile"}
|
||||||
|
],
|
||||||
|
"freeze_fields":["target_id","run_utc","device_model","cpu","ram_gib","os_full_build","browser_full_build","browser_channel","installer_source","installer_sha256_or_os_build","viewport_css","actual_inner_size_css","dpr","refresh_hz","display_mode","app_commit","asset_sha256","fixture_sha256","profile_reset_recipe","network_profile","measurement_tool_version"],
|
||||||
|
"budgets": {
|
||||||
|
"desktop":{"cold_start_p95_ms":2500,"warm_start_p95_ms":1200,"interaction_p95_ms":100,"raf_gap_p95_ms":20,"raf_gap_max_ms":100,"ram_peak_mib":350,"ram_growth_mib":30,"cache_mib":128},
|
||||||
|
"mobile":{"cold_start_p95_ms":4000,"warm_start_p95_ms":2000,"interaction_p95_ms":150,"raf_gap_p95_ms":25,"raf_gap_max_ms":150,"ram_peak_mib":250,"ram_growth_mib":25,"cache_mib":64}
|
||||||
|
},
|
||||||
|
"fallbacks": {
|
||||||
|
"secure-context":{"detection":"isSecureContext false / endpoint TLS failure","fallback":"Block login and protected operations; explain HTTPS/WSS requirement; never bypass certificates."},
|
||||||
|
"crypto-worker-wasm":{"detection":"Required WebCrypto, Worker, WASM initialization or self-test fails","fallback":"Block encrypted send/unlock; show unsupported capability; never plaintext or fake crypto."},
|
||||||
|
"indexeddb":{"detection":"Open/transaction fails including private-mode policy or quota","fallback":"Report local save failure; no checkpoint advance; preserve bounded pending memory; explicit recovery per ADR 0004."},
|
||||||
|
"persistence":{"detection":"persist absent/false; estimate absent or quota smaller than cache budget","fallback":"Explain best-effort storage and loss risk; lower disposable cache limit; never delete crypto or unsynced history."},
|
||||||
|
"push":{"detection":"Service worker/PushManager/Notifications missing, denied, revoked, subscription invalid, provider absent or iOS ordinary tab","fallback":"Disable background notification claim; show active-tab unread UI; offer Home Screen setup on iOS only by user action."},
|
||||||
|
"install":{"detection":"No install prompt or standalone mode","fallback":"Continue supported tab UI; show manual browser install instructions only where applicable."},
|
||||||
|
"media":{"detection":"getUserMedia/WebRTC absent, NotAllowedError, NotFoundError, NotReadableError or track ended","fallback":"Stop affected capture/call state, release tracks, explain reason; retain text; offer audio only if microphone actually works."},
|
||||||
|
"recording-codec":{"detection":"MediaRecorder absent or isTypeSupported false; runtime encoder/decoder fails","fallback":"Disable affected recording/playback with explanation; allow supported file selection/download; do not claim a sent recording."},
|
||||||
|
"geolocation":{"detection":"API absent, denied, timeout, unavailable or revoked","fallback":"Stop watch, show failure, permit explicit manual location entry; never invent coordinates."},
|
||||||
|
"webauthn":{"detection":"PublicKeyCredential missing, authenticator unavailable, cancel or failure","fallback":"Offer only accepted S00-007 unlock alternative; otherwise stay locked; no claim of hardware vault."},
|
||||||
|
"permissions-query":{"detection":"navigator.permissions absent or query descriptor rejects","fallback":"Use contextual API request after user gesture and observe its real result; no granted-state inference."},
|
||||||
|
"lifecycle":{"detection":"hidden, frozen, discarded, offline or closed tab","fallback":"No socket/media continuity promise; reconcile actual state on resume; closed-tab push remains conditional per ADR 0000."}
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"revision": 118,
|
"revision": 119,
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"S00-001": {
|
"S00-001": {
|
||||||
"status": "done",
|
"status": "done",
|
||||||
@@ -81,8 +81,11 @@
|
|||||||
"completed_at": "2026-10-15T11:37:56+00:00"
|
"completed_at": "2026-10-15T11:37:56+00:00"
|
||||||
},
|
},
|
||||||
"S00-006": {
|
"S00-006": {
|
||||||
"status": "todo",
|
"status": "in_progress",
|
||||||
"owner": null
|
"owner": "auto-orchestrator-710c6ba5d8ea",
|
||||||
|
"started_at": "2026-10-15T11:38:16+00:00",
|
||||||
|
"reason": null,
|
||||||
|
"updated_at": "2026-10-15T11:38:16+00:00"
|
||||||
},
|
},
|
||||||
"S00-007": {
|
"S00-007": {
|
||||||
"status": "todo",
|
"status": "todo",
|
||||||
@@ -2979,6 +2982,17 @@
|
|||||||
"to": "done",
|
"to": "done",
|
||||||
"reason": null,
|
"reason": null,
|
||||||
"evidence": "state/evidence/S00-005-review.md"
|
"evidence": "state/evidence/S00-005-review.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"revision": 119,
|
||||||
|
"at": "2026-10-15T11:38:16+00:00",
|
||||||
|
"action": "claim",
|
||||||
|
"task": "S00-006",
|
||||||
|
"agent": "auto-orchestrator-710c6ba5d8ea",
|
||||||
|
"role": "orchestrator",
|
||||||
|
"from": "todo",
|
||||||
|
"to": "in_progress",
|
||||||
|
"reason": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"retired_agents": [
|
"retired_agents": [
|
||||||
|
|||||||
Reference in new issue
Block a user