Model A vs Model B
The heart of the comparison: Gizmo v2.0.1 (file-pull) versus v2.2.x (rules + local apply).
Model A: file-pull
The v2.0.1 extension is a strict subset of the v2.2.x one. There is no bridge, no background patch handler, no cache, no ISOLATED world. A single MAIN-world content script fetches the already-patched bundle straight from the repo and injects it via the onreset trick.
// v2.0.1 lib/patch-config.ts
export const PATCHED_URL =
"https://raw.githubusercontent.com/alexey-max-fedorov/gizmo-ai-unlimited/main/patcher/dist/entry.min.js";
// v2.0.1 contents/gizmo-patch.ts → injectPatchedBundle()
fetch(PATCHED_URL, { cache: "no-cache" })
.then(r => r.text())
.then(js => {
const payload = "var URL=window.URL;\n" + js;
document.documentElement.setAttribute("onreset", payload);
document.documentElement.dispatchEvent(new CustomEvent("reset"));
document.documentElement.removeAttribute("onreset");
});The sequence is cache-less and maximally simple: one content script, one fetch, one injection.
Model B: rules + local apply
Instead of treating the 19 MB bundle as the product, the patcher publishes patches.json, 756 bytes of rules. The client downloads the rules and the pristine bundle from the vendor, applies the rules in the background service worker, and caches the result. The onreset injection at the end is identical; everything before it is new architecture.
The cache key is the tuple {bundleFilename, patchesHash}. A vendor redeploy changes bundleFilename; a rule edit changes patchesHash. Either axis independently forces exactly one rebuild, then steady-state cache hits. Both storage and unlimitedStorage permissions are required. unlimitedStorage alone leaves chrome.storage itself undefined at runtime with no compile-time warning.
The trade-off
| Dimension | Model A (file-pull) | Model B (rules + local apply) |
|---|---|---|
| Primary artifact | 19 MB patched bundle | 756 B rules recipe |
| Repo growth per vendor deploy | commits a 19 MB binary | commits a sub-1 KB JSON |
| Client bandwidth (cold) | 19 MB from our repo | 756 B (ours) + 19 MB (vendor) |
| Client bandwidth (warm) | 19 MB every load (or HTTP cache) | 756 B + cache hit (no 19 MB) |
| Who serves the heavy bytes | our GitHub raw CDN | the vendor's own CDN |
| Remote-code posture (store review) | extension executes a remote file we host; strongest remote-code-execution signal | extension executes the vendor's own bytes, transformed locally by a tiny declarative rule set; easier to justify |
| Freshness without extension update | yes (repo file changes) | yes (rules file changes) |
| Self-heal latency | ≤2 h (CI republish) | ≤2 h (CI republish) |
| Tamper surface | whoever controls our repo controls the executed code wholesale | our repo controls only the diff; the bulk is the vendor's signed-by-delivery bytes |
| Local compute | none | one regex pass over ~19 MB per (bundle, rules) change |
| Storage | none required | ~18 MB cached; needs unlimitedStorage |
| Failure containment | a bad publish ships a fully-broken bundle | a bad rule degrades gracefully; original structure still present |
| Implementation complexity | one MAIN content script | MAIN + ISOLATED + background + cache lib |
| Offline/repo-down resilience | breaks if our repo is unreachable | breaks if either our rules or the vendor bundle is unreachable |
Model B trades client-side complexity (a background worker, a bridge, a cache, ~18 MB of storage) for three strategic wins: (1) the repo stops storing a multi-megabyte binary that changes constantly; (2) the heavy bytes are served by the vendor, not us; and (3) the extension's executed payload is mostly the vendor's own code, with our contribution reduced to an auditable, sub-kilobyte declarative diff, a meaningfully better answer to a store reviewer asking "what remote code does this run?" Model A's advantage is sheer simplicity and zero local storage/compute, at the cost of hosting and executing a wholesale remote binary.
The exact transition (git archaeology)
The working repo was a shallow clone (50 commits). Un-shallowing via git fetch --unshallow origin main restored the full 225-commit history, enabling a precise version-to-commit mapping on origin/main:
| Version | Commit | Date | Note |
|---|---|---|---|
| 2.0.0 | caa28b4 | 2026-05-13 | declarativeNetRequest manifest |
| 2.0.1 | c24cec3 | 2026-05-13 | Model A baseline; Gizmo AI Unlimited rename |
| 2.1.0 | 6fa4a69 | 2026-05-18 | broaden reload to all gizmo.ai subdomains |
| 2.2.0 | 5edea20 | 2026-05-18 | version bump; still byte-identical Model A |
| 2.2.1 | bdedade | 2026-05-20 | current HEAD; Model B |
A precise finding from the archaeology: the v2.2.0 bump commit is byte-for-byte identical to v2.0.1's content script. It still fetches entry.min.js directly. The Model-A-to-Model-B refactor landed as a tight commit sequence immediately after the 2.2.0 tag:
2711e44 refactor(lib): replace PATCHED_URL with PATCHES_URL + filename helper
5ea7243 feat(lib): browser-side applyPatchRules + wrapWithMarkers
32782b9 feat(lib): bundle cache wrapper around chrome.storage.local
e64040b feat(extension): ISOLATED-world bridge to background
88158dd feat(extension): MAIN-world script requests patched bundle via bridge
66b0f58 fix(extension): omit world: ISOLATED from bridge PlasmoCSConfig
f1957d6 feat(patcher): emit patches.json; throw on any rule degradationSo "v2.2.0 = Model B" is true of the 2.2.x line as shipped (HEAD), with the mechanism introduced in the commits bridging 2.2.0 to 2.2.1. Both models remain viable; v2.2.x simply chose the rules path.
Where Play Origin sits
Play Origin is the instructive hybrid. P-NP's patcher already emits a Model-B manifest.json (rules plus prefix plus suffix plus defaultMenuUrl, declared the "primary artifact"), yet the shipping ProdigyOrigin extension still file-pulls the whole patched game.min.js:
const defaultGameUrl =
"https://raw.githubusercontent.com/ProdigyPXP/P-NP/master/dist/game.min.js";
// ProdigyOrigin extension/contents/prodigy.ts -> injectGameViaFetch(gameUrl)
fetch(gameUrl).then(r => r.text()).then(js => { /* onreset(js) */ });That is Model A in practice, even though the server side is already provisioned for Model B. The manifest.json (rules) is staged for a future client migration; until then game.min.js (nominally a "verification copy") is what actually loads. This is a textbook example of decoupling server and client release trains: the harder, riskier client rewrite can land later without blocking the patcher's evolution.