Fork independence: cut all donor/fork dependencies

- Delete scripts/extract-tier2.mjs; remove fork clone from seam-tests CI.
- Tier-2 tests self-contained: MANIFEST hash/size pins replace donor
  byte-identity; glue tests assert dry-run + anti-revert + additive
  (no regen-from-fork).
- MANIFEST provenance rekeyed (donor->origin, historical note only);
  UPSTREAM_BASELINE drops the donor pin (upstream pin only).
- Docs: ai-studio/README, RETIREMENT (independence done in-repo;
  archival stays owner-side), root README, AGENTS.md.
- Local fork fixture (~2GB) deleted; suite 97/97 green without it.
This commit is contained in:
BillyOutlast committed 2026-10-10 22:12:42 -04:00
1 parent 45565be5d6
commit cf7860e05b
13 files changed
+424 -250

No files matched your search

+3 -6
View File
@@ -15,16 +15,13 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
# Baselines are gitignored locally (multi-GB); CI clones the pins:
# upstream verified-at commit + fork donor baseline (provenance tests).
# Baselines are gitignored locally (multi-GB); CI clones the pin:
# upstream verified-at commit. This repo is fork-independent: no
# donor checkout exists anywhere in test or build paths.
- name: Clone pinned upstream baseline
run: |
git clone --no-checkout https://github.com/4ian/GDevelop review/GDevelop
git -C review/GDevelop checkout 8301cdcaa48b496f48a28a6e3a56fa833eaeddff
- name: Clone fork donor baseline
run: |
git clone --no-checkout https://github.com/Heretek-AI/GDevelop-Heretek review/GDevelop-Heretek
git -C review/GDevelop-Heretek checkout f7d450a1d9aa068e33c0a0c9a3f9caaee3701f2f
- name: Run full suite (seams + patcher + launcher + tier-2 + web)
run: npm test
- name: Patcher gate (seams + patch inventory)
+7 -5
View File
@@ -51,8 +51,10 @@ node scripts/apply-patches.mjs --check # seam gate
- The driver (`apply-patches.mjs`) owns two install paths: launcher `.cjs`
sync and Tier-2 overlay (`syncTier2Files`); `--asar` and `--target` must
both run both. Markers live in `MARKERS` — extend them with any new patch.
- Donor pin (`f7d450a`) lives in three places: `UPSTREAM_BASELINE.json`,
`.github/workflows/seam-tests.yml`, `RETIREMENT.md`. Keep them in sync.
- Upstream sync point awareness: the fork lags upstream, so any new
verbatim delta must be audited for deletions of upstream-new symbols
before shipping (the R1–R3 regression class in `AUDIT_REPORT.md`).
- Upstream pin lives in one place: `UPSTREAM_BASELINE.json`
(`verifiedAtUpstreamCommit`). There is deliberately **no donor/fork pin
anywhere** — this repo is fork-independent (see `RETIREMENT.md`).
- Patch provenance: 07/08/11 are curated forward-ports of fork work
(`patches/REBASE.md`); any *new* verbatim delta must still be audited
for deletions of upstream-new symbols before shipping (the R1–R3
regression class in `AUDIT_REPORT.md`).
+119
View File
@@ -0,0 +1,119 @@
# CLAUDE CODE ADVERSARIAL PROMPT: ZERO-TRUST AUDIT & E2E VERIFICATION ENGINE
> **How to Run**: Copy and paste the prompt below into **Claude Code** (or run `claude` inside `/home/john/Projects/GDevelop-Unlocked` with this file as the instruction context).
***
# MISSION: Adversarial Audit, Gap Analysis, & End-to-End Verification of GDevelop-Unlocked
## 1. Context & Repository Architecture
You are operating in `/home/john/Projects/GDevelop-Unlocked`.
You have three codebases available on disk:
1. **Pristine Upstream GDevelop**: `/home/john/Projects/GDevelop-Unlocked/review/GDevelop`
- Upstream GitHub: https://github.com/4ian/GDevelop
- The unmodified official game engine (C++ core, Wasm `GDevelop.js`, Pixi/Three runtime `GDJS`, and React/Electron `newIDE`).
2. **Donor Fork (GDevelop-Heretek)**: `/home/john/Projects/GDevelop-Unlocked/review/GDevelop-Heretek`
- Fork GitHub: https://github.com/Heretek-AI/GDevelop-Heretek
- A 205-file diverging fork that unlocked premium features and introduced a Bring-Your-Own-Key (BYOK) local AI studio.
3. **Surgical Conversion Project (GDevelop-Unlocked)**: `/home/john/Projects/GDevelop-Unlocked`
- Goal: Replace the 205-file fork with a surgical plugin architecture: 8 Tier-1 patches in `patches/`, runtime Electron hooks in `launcher/`, and an extracted `packages/ai-studio` package.
---
## 2. Operating Mindset: Zero-Trust Adversarial Auditor
You must assume a **strict adversarial posture**:
> **Core Hypothesis**: The unlocking mechanisms and AI features imported from `review/GDevelop-Heretek` into `GDevelop-Unlocked` are **fundamentally broken, leaky, incomplete, or phantom-passing**.
### Strict Operational Rules
1. **Distrust Existing Green Tests**: The 77 tests in `npm test` are largely unit-level assertions against mocks (e.g. `tests/launcher/smoke.test.cjs` uses a fake Electron object `makeFakeElectron`; `tests/patches/apply.test.mjs` only tests dry-run patch string replacements in a temp directory). A passing test does **not** prove functional unlocking in the real editor.
2. **Demand Execution Proof**: You must prove every claim through real network requests, real component lifecycle execution, real HTTP mock backends, and typecheck/compilation checks against upstream.
3. **Hunt for Dropped Fork Glue**: The fork touched 205 files. `GDevelop-Unlocked` attempted to condense this into 8 patches. Any file modified in the fork that was omitted from `patches/` or `launcher/` is a prime suspect for broken functionality or security/data leaks.
---
## 3. Confirmed Vulnerabilities & Failure Points to Investigate
You must specifically investigate and verify the following known failure modes:
### Failure Mode A: The `PrepareAiUserContent.js` Cloud Leak & 401 Crash
- **Location**: `review/GDevelop/newIDE/app/src/AiGeneration/PrepareAiUserContent.js` (~line 112)
- **The Bug**: In pristine upstream, before any AI request is created, `prepareAiUserContent` calls `createAiUserContentPresignedUrls` to upload the project JSON to S3 via `api.gdevelop.io`.
- **The Fork Fix**: Heretek intercepted this with `if (isCustomEndpointEnabled() || userId.startsWith('local-')) return { gameProjectJson: simplifiedProjectJson, ... };`.
- **The Defect in Unlocked**: `GDevelop-Unlocked` **never patched `PrepareAiUserContent.js`**. As a result, running BYOK against pristine upstream attempts to upload local project data to GDevelop's cloud, leaking private game data, and immediately crashes with HTTP 401/403 for `local-byok-user`.
### Failure Mode B: Dead Studio Runtime in `AskAiEditorContainer.js`
- **Location**: `review/GDevelop/newIDE/app/src/AiGeneration/AskAiEditorContainer.js`
- **The Bug**: `packages/ai-studio` extracted 12 files from `AiGeneration/Studio/` (`UseStudioRuntime.js`, `UseStudioNudge.js`, `Roles.js`, etc.). However, `AskAiEditorContainer.js` was **never patched** in `GDevelop-Unlocked` to mount `useStudioRuntime()` or `useStudioNudge()`.
- **The Defect in Unlocked**: Multi-agent studio loops, subagent spawning, and tool orchestration are completely disconnected dead code.
### Failure Mode C: 0-Credit Lockout in `AiRequestChat/Utils.js`
- **Location**: `review/GDevelop/newIDE/app/src/AiGeneration/AiRequestChat/Utils.js`
- **The Bug**: Upstream `canPayForAiRequest` disables the send button if the user does not have enough cloud credits.
- **The Fork Fix**: Heretek replaced this with `canAffordAiRequest`, returning `true` whenever `isCustomEndpointEnabled` is active.
- **The Defect in Unlocked**: `GDevelop-Unlocked` omitted this patch. Users with 0 cloud credits cannot submit prompts to their own local models.
### Failure Mode D: Production Asar & Webpack Hydration Race
- **Location**: `launcher/heretek-hook.cjs` and `launcher/preload-unlock.cjs`
- **The Bug**: In production Electron, `BrowserWindow` is non-configurable, so constructor wrapping can fail. The fallback relies on `did-finish-load` `executeJavaScript`. If React hydrates or Axios initializes before `did-finish-load`, the spoofing wrappers attach too late, exposing upstream paywalls.
---
## 4. Phase-by-Phase Adversarial Tasks
### Phase 1: Upstream Gating & Entitlement Mapping
Audit `review/GDevelop` and catalog all gating points against `review/GDevelop-Heretek`:
1. **Tier-1 Gating Map**:
- `Usage.js`: `hasValidSubscriptionPlan`, capabilities, quotas, credits.
- `SubscriptionChecker.js`: All 4 placement IDs (`'Disable GDevelop splash at startup'`, `'Debugger'`, `'Hot reloading'`, `'Preview over wifi'`).
- `LoadingScreenEditor.js`: Logo checkbox, watermark checkbox, min duration slider.
- `LocalPreviewLauncher/index.js`: Hot reload modulo (`% 16`) check, network preview check.
- `DebuggerEditorContainer.js`: Debugger & profiler access.
2. **Tier-2 AI Gating Map**:
- `Generation.js`: Request creation, message streaming, tool calling.
- `PrepareAiUserContent.js`: Presigned URL creation & project upload.
- `AskAiEditorContainer.js` & `AiRequestChat/`: Credit checks, subagent hooks, stop/retry controls.
- `PreferencesDialog.js` & `PreferencesContext.js`: AI tab and persistent storage.
### Phase 2: Build Real Integration & E2E Test Suites
Do not rely on mock objects. Build real, executable integration tests under `tests/integration/`:
1. **`tests/integration/network-spoof.test.mjs`**:
- Spin up a real local HTTP server using Node's `http` module.
- Exercise real HTTP requests through the preload and hook wrappers (`fetch` and `XMLHttpRequest`).
- Assert:
- `GET /limits` and `GET /subscription-v2` return the unlocked payloads.
- All other URLs, mutation methods (POST, PUT, DELETE), and external domains pass through untouched.
- Axios instances correctly receive spoofed responses without crashing.
2. **`tests/integration/ai-byok-flow.test.mjs`**:
- Spin up the committed mock AI server: `packages/ai-studio/dev-harness/mock-ai-provider.js` (or inline HTTP server).
- Test the complete generation flow from `PrepareAiUserContent` through `Generation.js` to `CustomAIClient`.
- Assert:
- Zero network requests are sent to `api.gdevelop.io` when custom endpoint is active.
- `local-ai-*` request IDs route exclusively to the local provider.
- Fail-closed leak guard (`assertNoLocalIdsLeak`) throws immediately if a local ID is sent to a hosted URL.
- The 39 `GDEVELOP_OPENAI_TOOLS` schemas parse cleanly and can be dispatched without undefined function errors.
3. **`tests/integration/react-entitlements.test.mjs`**:
- Mount the React components (`LoadingScreenEditor`, `LocalPreviewLauncher`, `DebuggerEditorContainer`) in a JSDOM environment.
- Assert:
- Watermark and startup logo can be toggled without mounting `SubscriptionChecker` dialogs.
- 20 consecutive hot reloads do not mount paywall dialogs.
- Debugger container mounts without opening the subscription prompt.
### Phase 3: Author Missing Glue Patches (Remediation)
Generate and apply the missing surgical patches to resolve the confirmed bugs:
1. `patches/09-prepare-ai-content.patch`: Fix `PrepareAiUserContent.js` to prevent cloud upload leaks and 401 crashes in BYOK mode.
2. `patches/10-ai-chat-affordance.patch`: Fix `AiRequestChat/Utils.js` to allow 0-credit prompt submissions when custom endpoint is enabled.
3. `patches/11-studio-runtime-mount.patch`: Patch `AskAiEditorContainer.js` to wire in `useStudioRuntime` and `useStudioNudge`.
Update `scripts/apply-patches.mjs` markers and verify that `node scripts/apply-patches.mjs --target review/GDevelop --no-verify` applies cleanly.
---
## 5. Required Deliverables
When your execution completes, generate a comprehensive markdown report in `AUDIT_REPORT.md` containing:
1. **Executive Audit Verdict**: Detailed PASS/FAIL assessment of whether `GDevelop-Unlocked` replaces the fork without regressions.
2. **Vulnerability & Regression Matrix**: Table of every discovered bug classified by severity (CRITICAL, HIGH, MEDIUM, LOW), root cause, and remediation status.
3. **Integration Test Suite Results**: Full test run logs demonstrating that all real network and BYOK flows pass.
4. **Upstream PR Recommendations**: Guidance on upstreaming neutral AI interfaces (e.g. `docs/upstream-ai-provider-rfc.md`) vs maintaining surgical patches.
+189
View File
@@ -0,0 +1,189 @@
# HANDOFF + ADVERSARIAL AUDIT PROMPT — GDevelop-Unlocked
Run `claude` inside `/home/john/Projects/GDevelop-Unlocked` with this file as the instruction context.
Part A is the state left by the previous review session (2026-10-10). Part B is the adversarial audit mission. Read Part A first: it corrects or sharpens several assumptions in Part B.
---
# PART A — Handoff from the previous session
## A1. What the previous session was
A findings-only, 5-phase review (test authenticity, integration handling, plugin functionality, cross-cutting consistency) with one subagent per phase. It did **not** finish and produced **no synthesized report**. Nothing was fixed; the repo, `review/` and git state were not modified by it.
## A2. Repo state at handoff
- Reviewed commit: `13ddde2` on `feature/ci-e2e` (one commit ahead of `develop`/`main` @ `30da3f8`). `inject-launcher.mjs`, smoke mode, `e2e.yml` exist only on this branch.
- Working tree is **dirty from a different session** (21:27–21:29): `M package.json` (adds `tests/integration/*.test.mjs` to `npm test` + a `test:integration` script), untracked `tests/integration/ai-byok-flow.test.mjs`, `tests/integration/network-spoof.test.mjs`, `CLAUDE_CODE_ADVERSARIAL_PROMPT.md`. **None of these were read or reviewed.** Read them before building on them — Part B Phase 2 asks for files with these exact names.
- Fixtures: `review/GDevelop` @ `8301cdcaa48b496f48a28a6e3a56fa833eaeddff` (`v5.6.283-48`), `review/GDevelop-Heretek` @ `f7d450a1d9aa068e33c0a0c9a3f9caaee3701f2f`. Both gitignored, ~2 GB each.
- `npm test` at `13ddde2`: 77/77 green locally on Node 26 and on Node 20.20.2.
- CI is **red**: `seam-tests` fails on the `main` push and on the PR; `e2e` fails on the PR (`gh run list --repo Heretek-AI/GDevelop-Unlocked`).
- Tools available on this machine: `patch`, `Xvfb`/`xvfb-run`, `ollama`, authenticated `gh`, network. A copy of the v5.6.283 Linux release zip and a flat asar extraction are in `/tmp/opencode` (another session's directory — treat as read-only, copy out).
## A3. Findings confirmed by the lead (re-checked against code/CI, not just reported)
1. **Donor pin is the wrong commit.** `b9109cfbaaaf19c31b5af4d315317fb67d8903c3` (in `.github/workflows/seam-tests.yml:27`, `UPSTREAM_BASELINE.json:3`, `RETIREMENT.md:28`) contains 0 Tier-2 paths; the real donor is fork HEAD `f7d450a` (31 paths, 418 commits later). This is why `tests/tier2/extract.test.mjs` and `tests/tier2/tier2-patches.test.mjs` fail in CI while passing locally.
2. **Committed mock provider cannot start from this repo.** `package.json` has `"type": "module"`; `packages/ai-studio/dev-harness/mock-ai-provider.js:29` uses `require` → `ReferenceError: require is not defined in ES module scope`. This is why the `byok-mock` job fails. It matters for Part B Phase 2 item 2: the mock must be run from outside the package scope (or via an inline server) unless this is fixed.
3. **`packages/unlock-shim/generation-shim.mjs` is not wired into anything shipped.** Zero references to `selectBackend` / `guardHostedCall` / `assertNoLocalIdsLeak` / `unlock-shim` in all eight patches; at `13ddde2` its only importer is `tests/tier2/shim.test.mjs`. `packages/unlock-shim/README.md:13-16` claims patch 07 calls it. So the "fail-closed leak guard" exists only in a test, not in the delivery path.
4. **Smoke check probes `fetch` only** (`launcher/heretek-hook.cjs:331-350`), while `launcher/README.md:18` says the app's client rides on XHR.
5. **Fake Electron is more permissive than production.** `tests/launcher/hook.test.cjs:44-48` defines `BrowserWindow` with `configurable: true`; the production export is non-configurable.
6. **`isSmokeMode` drops the first user argument** (`launcher/heretek-hook.cjs:324-326`, `slice(2)`); `e2e.yml` only works because the flag is passed last.
7. **Generators hard-code `/tmp/opencode`** (`scripts/generate-tier1-patches.mjs:450-472`, `scripts/generate-tier2-patches.mjs:78-80`), ignoring `TMPDIR`. `tests/tier2/tier2-patches.test.mjs` invokes the tier-2 generator, so plain `npm test` writes there; the tier-2 generator never creates the directory (cold-runner behaviour unchecked).
8. **`generate-tier1-patches.mjs:44-55`**: any unrecognised argument resets `--out` to `<repo>/patches`.
9. **`apply.test.mjs:50,109` pass `--no-verify`**, so the driver's `MARKERS`/`verifyTarget` contract is never exercised by a test.
10. **Correction to an earlier assumption:** `apply-patches.mjs:166-171` passes `shell: true`, so `--check` works on Node 20 (ran 29 seam tests, exit 0). CI's `node-version: 20` vs `engines >=22` is a policy mismatch only — nothing needs ≥22.
## A4. Reported by the consistency reviewer, NOT independently re-checked
Treat as strong leads; verify before relying on them.
- **Patches 07/08 are not pure fork deltas.** They are a diff of baseline `8301cdc` against a fork synced only to `b9109cf` (71 upstream commits behind), so they also revert upstream changes in those files — reportedly deleting exports still imported elsewhere at baseline (`createAiAttachmentUploads`/`createAiAttachmentDownloads`, the `newEffectDialogDefaultTab` API). Rests on a static import scan, not a build.
- **Tier-2 is not installed by anything.** After a green `apply-patches.mjs --target`, imports of `'../../AI/CustomAIClient'` from `Generation.js`, `PreferencesDialog.js`, `PreferencesProvider.js` are unresolved — no script places `packages/ai-studio` into the tree. Even when overlaid, 11 of 12 Studio modules have no importer; the fork's wiring (≈17 files, +2650/−307) is not delivered. **This is the same ground as Part B Failure Modes A–C**, which name three of those missing files specifically.
- **Release-asar docs point at a command that cannot work.** `patches/README.md:3-5`, `launcher/README.md:36-38,47-49` and `UPDATER_PIN_MESSAGE` (`launcher/heretek-hook.cjs:42-47`, pinned by `tests/launcher/hook.test.cjs:254`) say `apply-patches.mjs --asar`; a real asar is flat and bundled, and `inject-launcher.mjs` (the path that works) is in no README.
- **XHR path reportedly fails on the code path production uses** — reproduced on a Node test double only, not in real Chromium. Relevant to Part B Failure Mode D; needs a real-browser check.
- **Weekly "drift check" runs against fixed pins**, so it cannot detect new upstream drift.
- **`RETIREMENT.md:10-12,52-54`** precondition and counts are stale/contradictory (now 74 pass / 1 fail / 2 cancelled with an empty `FORK_DIR`; both tier-2 provenance tests need the donor).
- **Seam suite does not cover** the two usage endpoints, the validators, or the targets of patches 02/07/08.
- Determinism: committed patches 01–08 and the extraction equal fresh generator output; only tier-2 is pinned by a test. `MANIFEST.json:2` stores an absolute local path and no donor commit.
- Cosmetic list (stale wording, dead code, redundant tests, userscript README vs behaviour) — re-derive if needed.
## A5. Integration reviewer — partial results only
- Full `--target` apply on a pristine scratch upstream: all 8 applied, launcher files synced, markers verified, exit 0.
- Re-apply refused at patch 01's dry-run, exit 1 (tree-unchanged not confirmed by hash).
- Missing fixture fails loudly ("Missing upstream file … Set UPSTREAM_DIR or refresh review/GDevelop").
- Production Electron is 32.3.3 (`review/GDevelop/newIDE/electron-app/package.json`).
## A6. Not done
- **Test-authenticity phase:** work completed (all 16 test files read, mutation spot-checks run in scratch) but the report never reached the lead. Raw logs may still exist under `/tmp/claude-1000/-home-john-Projects-GDevelop-Unlocked/07a31264-f21d-4519-9dbf-fe97194ba181/scratchpad/phase1/logs/` (`mut-results.jsonl`, one record per mutation). That path is session-scoped and may be gone; the lead never read it.
- **Integration phase remainder:** drifted-hunk / half-applied-tree behaviour, fake-vs-production Electron comparison beyond A3.5, `inject-launcher.mjs` edge cases, smoke exit-code logic vs how `e2e.yml` consumes it, what `byok-mock`/`byok-ollama` actually prove (reported: they exercise no client code from this repo).
- **Functionality phase:** stopped before reporting. No Tier-1 residual diff against the donor, no payload-vs-validator check, no live release → inject → repack → Xvfb smoke run, no negative control on the smoke verdict.
## A7. Process notes for the next session
- **Several write-ups were cut off by a safety classifier** in the previous session (three subagent reports and two lead responses), mostly while writing detailed tables. When that happens, do not retry or reword the blocked content — record the gap plainly and move on to the next item. Expect this again and plan deliverables so a single cut-off does not lose everything: write findings to disk incrementally as short entries.
- **Have subagents write their findings to a file as they go**, not only in a final message — the previous session lost two complete reports that existed only in final messages.
- **Do not patch `review/GDevelop` in place.** Part B §4 Phase 3 says `apply-patches.mjs --target review/GDevelop`; that mutates the pinned fixture every seam/patch test depends on, and patches are non-idempotent by design. Apply to a scratch copy instead, e.g. `git -C review/GDevelop archive HEAD newIDE/app/src newIDE/electron-app | tar -x -C <scratch>`.
- **Part B changes the mode from findings-only to remediation** (new tests under `tests/integration/`, new patches 09–11, edits to `scripts/apply-patches.mjs`). The previous brief's non-goals ("no fixes, do not touch `tests/`") no longer apply if Part B is followed — that is the owner's decision; it is flagged here so it is deliberate.
- New patches must come from the generators, not hand edits (`tests/tier2/tier2-patches.test.mjs` asserts committed patches equal fresh generator output). Fix the donor pin (A3.1) first or CI stays red regardless.
- Part B §2 rule 1 says `tests/patches/apply.test.mjs` "only tests dry-run patch string replacements". The previous session observed that it spawns the real driver with `--target` on a temp copy (with `--no-verify`); check the file rather than assuming either description.
- Part B Failure Modes A–C were **not individually verified** in the previous session; A4's "wiring not delivered" finding is consistent with them but is not proof of each.
---
# PART B — Adversarial audit mission
# MISSION: Adversarial Audit, Gap Analysis, & End-to-End Verification of GDevelop-Unlocked
## 1. Context & Repository Architecture
You are operating in `/home/john/Projects/GDevelop-Unlocked`.
You have three codebases available on disk:
1. **Pristine Upstream GDevelop**: `/home/john/Projects/GDevelop-Unlocked/review/GDevelop`
- Upstream GitHub: https://github.com/4ian/GDevelop
- The unmodified official game engine (C++ core, Wasm `GDevelop.js`, Pixi/Three runtime `GDJS`, and React/Electron `newIDE`).
2. **Donor Fork (GDevelop-Heretek)**: `/home/john/Projects/GDevelop-Unlocked/review/GDevelop-Heretek`
- Fork GitHub: https://github.com/Heretek-AI/GDevelop-Heretek
- A 205-file diverging fork that unlocked premium features and introduced a Bring-Your-Own-Key (BYOK) local AI studio.
3. **Surgical Conversion Project (GDevelop-Unlocked)**: `/home/john/Projects/GDevelop-Unlocked`
- Goal: Replace the 205-file fork with a surgical plugin architecture: 8 Tier-1 patches in `patches/`, runtime Electron hooks in `launcher/`, and an extracted `packages/ai-studio` package.
---
## 2. Operating Mindset: Zero-Trust Adversarial Auditor
You must assume a **strict adversarial posture**:
> **Core Hypothesis**: The unlocking mechanisms and AI features imported from `review/GDevelop-Heretek` into `GDevelop-Unlocked` are **fundamentally broken, leaky, incomplete, or phantom-passing**.
### Strict Operational Rules
1. **Distrust Existing Green Tests**: The 77 tests in `npm test` are largely unit-level assertions against mocks (e.g. `tests/launcher/smoke.test.cjs` uses a fake Electron object `makeFakeElectron`; `tests/patches/apply.test.mjs` only tests dry-run patch string replacements in a temp directory). A passing test does **not** prove functional unlocking in the real editor.
2. **Demand Execution Proof**: You must prove every claim through real network requests, real component lifecycle execution, real HTTP mock backends, and typecheck/compilation checks against upstream.
3. **Hunt for Dropped Fork Glue**: The fork touched 205 files. `GDevelop-Unlocked` attempted to condense this into 8 patches. Any file modified in the fork that was omitted from `patches/` or `launcher/` is a prime suspect for broken functionality or security/data leaks.
---
## 3. Confirmed Vulnerabilities & Failure Points to Investigate
You must specifically investigate and verify the following known failure modes:
### Failure Mode A: The `PrepareAiUserContent.js` Cloud Leak & 401 Crash
- **Location**: `review/GDevelop/newIDE/app/src/AiGeneration/PrepareAiUserContent.js` (~line 112)
- **The Bug**: In pristine upstream, before any AI request is created, `prepareAiUserContent` calls `createAiUserContentPresignedUrls` to upload the project JSON to S3 via `api.gdevelop.io`.
- **The Fork Fix**: Heretek intercepted this with `if (isCustomEndpointEnabled() || userId.startsWith('local-')) return { gameProjectJson: simplifiedProjectJson, ... };`.
- **The Defect in Unlocked**: `GDevelop-Unlocked` **never patched `PrepareAiUserContent.js`**. As a result, running BYOK against pristine upstream attempts to upload local project data to GDevelop's cloud, leaking private game data, and immediately crashes with HTTP 401/403 for `local-byok-user`.
### Failure Mode B: Dead Studio Runtime in `AskAiEditorContainer.js`
- **Location**: `review/GDevelop/newIDE/app/src/AiGeneration/AskAiEditorContainer.js`
- **The Bug**: `packages/ai-studio` extracted 12 files from `AiGeneration/Studio/` (`UseStudioRuntime.js`, `UseStudioNudge.js`, `Roles.js`, etc.). However, `AskAiEditorContainer.js` was **never patched** in `GDevelop-Unlocked` to mount `useStudioRuntime()` or `useStudioNudge()`.
- **The Defect in Unlocked**: Multi-agent studio loops, subagent spawning, and tool orchestration are completely disconnected dead code.
### Failure Mode C: 0-Credit Lockout in `AiRequestChat/Utils.js`
- **Location**: `review/GDevelop/newIDE/app/src/AiGeneration/AiRequestChat/Utils.js`
- **The Bug**: Upstream `canPayForAiRequest` disables the send button if the user does not have enough cloud credits.
- **The Fork Fix**: Heretek replaced this with `canAffordAiRequest`, returning `true` whenever `isCustomEndpointEnabled` is active.
- **The Defect in Unlocked**: `GDevelop-Unlocked` omitted this patch. Users with 0 cloud credits cannot submit prompts to their own local models.
### Failure Mode D: Production Asar & Webpack Hydration Race
- **Location**: `launcher/heretek-hook.cjs` and `launcher/preload-unlock.cjs`
- **The Bug**: In production Electron, `BrowserWindow` is non-configurable, so constructor wrapping can fail. The fallback relies on `did-finish-load` `executeJavaScript`. If React hydrates or Axios initializes before `did-finish-load`, the spoofing wrappers attach too late, exposing upstream paywalls.
---
## 4. Phase-by-Phase Adversarial Tasks
### Phase 1: Upstream Gating & Entitlement Mapping
Audit `review/GDevelop` and catalog all gating points against `review/GDevelop-Heretek`:
1. **Tier-1 Gating Map**:
- `Usage.js`: `hasValidSubscriptionPlan`, capabilities, quotas, credits.
- `SubscriptionChecker.js`: All 4 placement IDs (`'Disable GDevelop splash at startup'`, `'Debugger'`, `'Hot reloading'`, `'Preview over wifi'`).
- `LoadingScreenEditor.js`: Logo checkbox, watermark checkbox, min duration slider.
- `LocalPreviewLauncher/index.js`: Hot reload modulo (`% 16`) check, network preview check.
- `DebuggerEditorContainer.js`: Debugger & profiler access.
2. **Tier-2 AI Gating Map**:
- `Generation.js`: Request creation, message streaming, tool calling.
- `PrepareAiUserContent.js`: Presigned URL creation & project upload.
- `AskAiEditorContainer.js` & `AiRequestChat/`: Credit checks, subagent hooks, stop/retry controls.
- `PreferencesDialog.js` & `PreferencesContext.js`: AI tab and persistent storage.
### Phase 2: Build Real Integration & E2E Test Suites
Do not rely on mock objects. Build real, executable integration tests under `tests/integration/`:
1. **`tests/integration/network-spoof.test.mjs`**:
- Spin up a real local HTTP server using Node's `http` module.
- Exercise real HTTP requests through the preload and hook wrappers (`fetch` and `XMLHttpRequest`).
- Assert:
- `GET /limits` and `GET /subscription-v2` return the unlocked payloads.
- All other URLs, mutation methods (POST, PUT, DELETE), and external domains pass through untouched.
- Axios instances correctly receive spoofed responses without crashing.
2. **`tests/integration/ai-byok-flow.test.mjs`**:
- Spin up the committed mock AI server: `packages/ai-studio/dev-harness/mock-ai-provider.js` (or inline HTTP server).
- Test the complete generation flow from `PrepareAiUserContent` through `Generation.js` to `CustomAIClient`.
- Assert:
- Zero network requests are sent to `api.gdevelop.io` when custom endpoint is active.
- `local-ai-*` request IDs route exclusively to the local provider.
- Fail-closed leak guard (`assertNoLocalIdsLeak`) throws immediately if a local ID is sent to a hosted URL.
- The 39 `GDEVELOP_OPENAI_TOOLS` schemas parse cleanly and can be dispatched without undefined function errors.
3. **`tests/integration/react-entitlements.test.mjs`**:
- Mount the React components (`LoadingScreenEditor`, `LocalPreviewLauncher`, `DebuggerEditorContainer`) in a JSDOM environment.
- Assert:
- Watermark and startup logo can be toggled without mounting `SubscriptionChecker` dialogs.
- 20 consecutive hot reloads do not mount paywall dialogs.
- Debugger container mounts without opening the subscription prompt.
### Phase 3: Author Missing Glue Patches (Remediation)
Generate and apply the missing surgical patches to resolve the confirmed bugs:
1. `patches/09-prepare-ai-content.patch`: Fix `PrepareAiUserContent.js` to prevent cloud upload leaks and 401 crashes in BYOK mode.
2. `patches/10-ai-chat-affordance.patch`: Fix `AiRequestChat/Utils.js` to allow 0-credit prompt submissions when custom endpoint is enabled.
3. `patches/11-studio-runtime-mount.patch`: Patch `AskAiEditorContainer.js` to wire in `useStudioRuntime` and `useStudioNudge`.
Update `scripts/apply-patches.mjs` markers and verify that `node scripts/apply-patches.mjs --target review/GDevelop --no-verify` applies cleanly.
---
## 5. Required Deliverables
When your execution completes, generate a comprehensive markdown report in `AUDIT_REPORT.md` containing:
1. **Executive Audit Verdict**: Detailed PASS/FAIL assessment of whether `GDevelop-Unlocked` replaces the fork without regressions.
2. **Vulnerability & Regression Matrix**: Table of every discovered bug classified by severity (CRITICAL, HIGH, MEDIUM, LOW), root cause, and remediation status.
3. **Integration Test Suite Results**: Full test run logs demonstrating that all real network and BYOK flows pass.
4. **Upstream PR Recommendations**: Guidance on upstreaming neutral AI interfaces (e.g. `docs/upstream-ai-provider-rfc.md`) vs maintaining surgical patches.
+9 -7
View File
@@ -1,9 +1,10 @@
# GDevelop-Unlocked
Surgical, patch-stack conversion of the
[GDevelop-Heretek](https://github.com/Heretek-AI/GDevelop-Heretek) fork
(unlocked premium gates + Bring-Your-Own-Key local AI studio) into a
maintainable delivery shape — no 205-file merge treadmill.
Surgical, patch-stack conversion of the premium unlocks +
Bring-Your-Own-Key local AI studio pioneered in the (now-retired)
[GDevelop-Heretek](https://github.com/Heretek-AI/GDevelop-Heretek) fork —
into a maintainable delivery shape with **zero dependency on that fork**.
No 205-file merge treadmill, no donor checkout in any build/test/CI path.
Three delivery mechanisms, all applied onto stock upstream GDevelop:
@@ -29,14 +30,15 @@ node scripts/apply-patches.mjs --target <upstream-checkout-or-extracted-asar>
node scripts/inject-launcher.mjs --appdir <extracted-asar-dir>
```
Requirements: Node ≥ 22, `patch`, git. Test fixtures (`review/GDevelop`,
`review/GDevelop-Heretek`, multi-GB) are gitignored; CI clones its pins.
Requirements: Node ≥ 22, `patch`, git. The upstream test fixture
(`review/GDevelop`, multi-GB) is gitignored; CI clones the pinned upstream
commit. Nothing else external is needed — this repo is fork-independent.
## Layout
- `patches/` — 11 patches + `README.md` (per-patch targets) + `REBASE.md`
- `scripts/` — `apply-patches.mjs` (driver), `generate-tier1/2-patches.mjs`
(generators), `extract-tier2.mjs`, `inject-launcher.mjs`
(generators), `inject-launcher.mjs`
- `launcher/` — `heretek-hook.cjs`, `preload-unlock.cjs`, `unlock-payloads.cjs`
- `packages/ai-studio/` — extracted Tier-2 runtime + `dev-harness/` mock provider
- `packages/unlock-shim/` — routing/leak-guard invariants (reference impl)
+17 -20
View File
@@ -1,17 +1,18 @@
# Phase 6: Decommission fork — retirement runbook (issue #6)
# Phase 6: Fork independence — DONE in-repo (issue #6)
The 205-file fork (`Heretek-AI/GDevelop-Heretek`) stops being the
delivery vehicle and stays only as the Tier-2 donor archive. Nothing
here is automated: archiving and workflow deletion are destructive and
need a human owner. Do not close issue #6 until every box is ticked.
The 205-file fork (`Heretek-AI/GDevelop-Heretek`) is no longer referenced
by any build, test, or CI path in this repo. Code independence landed
2026-10-11: donor-comparison tests were replaced with self-contained
integrity pins, the fork clone was removed from CI, and the extractor
was deleted. What remains is owner-side archival of the old repo
(destructive, needs a human — do not close issue #6 until ticked).
## Preconditions (M1 + M2 must hold without the fork present)
## Preconditions (re-verified at independence cutover)
- [ ] `npm test` green with the fork checkout absent
(`FORK_DIR` pointed at an empty dir — only `tests/tier2/extract.test.mjs`
needs the donor, and only to re-verify byte-identity).
- [ ] `node scripts/apply-patches.mjs --target <pristine-upstream>` green
(all 8 patches + launcher sync + marker verify).
- [x] `npm test` green with the fork checkout absent (no test reads any
`FORK_DIR`; self-contained `MANIFEST.json` hashes pin the runtime).
- [x] `node scripts/apply-patches.mjs --target <pristine-upstream>` green
(all 11 patches + launcher sync + Tier-2 overlay + marker verify).
- [ ] Desktop smoke on the patched build: logo uncheck, watermark
uncheck, hot reload x17, wifi preview, debugger open (issue #2).
- [ ] Desktop E2E: `http://localhost:11434/v1` generation + subagent
@@ -43,12 +44,8 @@ gh repo archive Heretek-AI/GDevelop-Heretek --yes
## Post-retirement invariant
The delivery path (patcher, hook, preload, shim, userscript) never
*reads* the fork: `GDevelop-Heretek` appears in delivery code only in
provenance comments (`Unlocked in GDevelop-Heretek`) and in donor-dir
defaults owned by the extractor (`scripts/extract-tier2.mjs`,
`scripts/generate-tier2-patches.mjs`) and the two provenance tests
(`tests/tier2/extract.test.mjs`, `tests/tier2/tier2-patches.test.mjs`),
which fail loudly — by design — when the donor is absent. Everything
else in `npm test` is fork-free (verified: 63 pass with `FORK_DIR`
pointed at an empty dir; the 1 failure + 2 cancellations are the
donor-provenance tests failing loudly by design — not silent drift).
*reads* the fork: the name `GDevelop-Heretek` appears only in historical
provenance notes and in unlock-marker strings shipped by design
(`Unlocked in GDevelop-Heretek`). No test, script, or workflow clones,
reads, or regenerates from a donor checkout — verified: the full suite
passes with no fork fixture present.
+1 -2
View File
@@ -1,6 +1,5 @@
{
"comment": "Pinned upstream shape the Phase 1 seam tests were written against. CI re-runs the suite against this commit on schedule; any red suite means a patch hunk or hook target moved.",
"donorBaselineCommit": "f7d450a1d9aa068e33c0a0c9a3f9caaee3701f2f",
"comment": "Pinned upstream shape the Phase 1 seam tests were written against. CI re-runs the suite against this commit on schedule; any red suite means a patch hunk or hook target moved. This repo is fork-independent: the only external source it reads is stock upstream GDevelop.",
"verifiedAtUpstreamCommit": "8301cdcaa4",
"verifiedAt": "2026-10-11",
"seams": {
+33 -33
View File
@@ -1,101 +1,101 @@
{
"donor": "/home/john/Projects/GDevelop-Unlocked/review/GDevelop-Heretek",
"files": {
"AI/CustomAIClient.js": {
"donor": "newIDE/app/src/AI/CustomAIClient.js",
"bytes": 208828,
"origin": "newIDE/app/src/AI/CustomAIClient.js",
"sha256": "2dd976b936908115d08374851121a077d2b2b6c314a029a4bc52ab7125f51723"
},
"AI/CustomAIClient.spec.js": {
"donor": "newIDE/app/src/AI/CustomAIClient.spec.js",
"bytes": 257794,
"origin": "newIDE/app/src/AI/CustomAIClient.spec.js",
"sha256": "0152072739b6590f004e886b8a5787cd769a529f044f14e48ca7e78ecc2a560a"
},
"AiGeneration/Studio/EditApprovalLabel.js": {
"donor": "newIDE/app/src/AiGeneration/Studio/EditApprovalLabel.js",
"bytes": 1308,
"origin": "newIDE/app/src/AiGeneration/Studio/EditApprovalLabel.js",
"sha256": "a924efc714123ab45e7d649d581b4d54557e6452fc0df3bcf8ad387c27c9f194"
},
"AiGeneration/Studio/FinalizeSubAgents.js": {
"donor": "newIDE/app/src/AiGeneration/Studio/FinalizeSubAgents.js",
"bytes": 9111,
"origin": "newIDE/app/src/AiGeneration/Studio/FinalizeSubAgents.js",
"sha256": "708de36210d000af56e795280eaa5dd450e32cedb4a54b7b031353a6cc4a679f"
},
"AiGeneration/Studio/LoopGuard.js": {
"donor": "newIDE/app/src/AiGeneration/Studio/LoopGuard.js",
"bytes": 9401,
"origin": "newIDE/app/src/AiGeneration/Studio/LoopGuard.js",
"sha256": "0ad4c6f5d6312a38f56e0385b8408adec72634c95d7252a3ff193af1e667101e"
},
"AiGeneration/Studio/NudgePolicy.js": {
"donor": "newIDE/app/src/AiGeneration/Studio/NudgePolicy.js",
"bytes": 3821,
"origin": "newIDE/app/src/AiGeneration/Studio/NudgePolicy.js",
"sha256": "c13a8af43b3b3d23ccb2dff2bc3438755d502090fd99f2bad1061d86f8d3c925"
},
"AiGeneration/Studio/PlanStore.js": {
"donor": "newIDE/app/src/AiGeneration/Studio/PlanStore.js",
"bytes": 13166,
"origin": "newIDE/app/src/AiGeneration/Studio/PlanStore.js",
"sha256": "3218ec230b6786d9105fd2854b3389b20b6c68a0889ccc6134f533106b1a3be6"
},
"AiGeneration/Studio/RequestWriteGate.js": {
"donor": "newIDE/app/src/AiGeneration/Studio/RequestWriteGate.js",
"bytes": 5904,
"origin": "newIDE/app/src/AiGeneration/Studio/RequestWriteGate.js",
"sha256": "bcd78ad8d0a22ca58ea413b2d1bbdbd07814184a0e28726dcfe8d466707c5064"
},
"AiGeneration/Studio/Roles.js": {
"donor": "newIDE/app/src/AiGeneration/Studio/Roles.js",
"bytes": 12493,
"sha256": "dac71987229c63fcea55fabd049b43ab48beff68cb19813de9d1c77090c6798f"
},
"AiGeneration/Studio/RoleToolPolicy.js": {
"donor": "newIDE/app/src/AiGeneration/Studio/RoleToolPolicy.js",
"bytes": 2097,
"origin": "newIDE/app/src/AiGeneration/Studio/RoleToolPolicy.js",
"sha256": "66a7d577f12fccaa1b23d89bc7ba02c9bca07058358cc53d3ecfe50d604ef1b8"
},
"AiGeneration/Studio/Roles.js": {
"bytes": 12493,
"origin": "newIDE/app/src/AiGeneration/Studio/Roles.js",
"sha256": "dac71987229c63fcea55fabd049b43ab48beff68cb19813de9d1c77090c6798f"
},
"AiGeneration/Studio/SafeTruncation.js": {
"donor": "newIDE/app/src/AiGeneration/Studio/SafeTruncation.js",
"bytes": 2743,
"origin": "newIDE/app/src/AiGeneration/Studio/SafeTruncation.js",
"sha256": "24e4e11fb5ee360374cd3bfa092fd01755b99228c34df74547473336d4a799ce"
},
"AiGeneration/Studio/SpawnSubAgents.js": {
"donor": "newIDE/app/src/AiGeneration/Studio/SpawnSubAgents.js",
"bytes": 9089,
"origin": "newIDE/app/src/AiGeneration/Studio/SpawnSubAgents.js",
"sha256": "ffc0658c57b027d20cc50c88bc350ac013369c3fb09ed366ff8736a7aa78d2ae"
},
"AiGeneration/Studio/UseStudioNudge.js": {
"donor": "newIDE/app/src/AiGeneration/Studio/UseStudioNudge.js",
"bytes": 5824,
"origin": "newIDE/app/src/AiGeneration/Studio/UseStudioNudge.js",
"sha256": "12df5f3f54a52e3c2c72646e6825360206eec0521dd6f15d73cd39660b21e0d2"
},
"AiGeneration/Studio/UseStudioRuntime.js": {
"donor": "newIDE/app/src/AiGeneration/Studio/UseStudioRuntime.js",
"bytes": 11187,
"origin": "newIDE/app/src/AiGeneration/Studio/UseStudioRuntime.js",
"sha256": "ecc51b28d00baf1ccd7d38d542e4a9237cd145cd863586c6fae6e587d03564c3"
},
"dev-harness/mock-ai-provider.js": {
"donor": "scripts/dev/mock-ai-provider.js",
"bytes": 9862,
"sha256": "117611628583d61a41e0f26de0ea0bfe30fd9b6781127e853995ff964cdfc1f1",
"note": "ESM port (import instead of require): repo root is type:module so the verbatim CJS donor throws ReferenceError. Behavior identical; exempt from donor byte-identity in extract.test.mjs."
"note": "ESM port (import instead of require): repo root is type:module so the donor's CJS original throws ReferenceError here. Behavior identical; pinned by hash like every other file.",
"origin": "scripts/dev/mock-ai-provider.js",
"sha256": "117611628583d61a41e0f26de0ea0bfe30fd9b6781127e853995ff964cdfc1f1"
},
"dev-harness/mock-script.example.json": {
"donor": "scripts/dev/mock-script.example.json",
"bytes": 887,
"sha256": "45c2915841b0a6c0f17db7a297a39ca1dfb8b889d74f018f87d1815cadf17c26"
"dev-harness/mock-child-scripts.example.json": {
"bytes": 532,
"origin": "scripts/dev/mock-child-scripts.example.json",
"sha256": "8f785710966f26a7923793f47a785b18d8e8d326bb381f78682a7eb50ac6bdd7"
},
"dev-harness/mock-script-multi.json": {
"donor": "scripts/dev/mock-script-multi.json",
"bytes": 1314,
"origin": "scripts/dev/mock-script-multi.json",
"sha256": "c94da74ac1251e07ae7a4576ea7354a1ef9f76a9294cca4bf473c0807d5ed824"
},
"dev-harness/mock-script-specialists.json": {
"donor": "scripts/dev/mock-script-specialists.json",
"bytes": 960,
"origin": "scripts/dev/mock-script-specialists.json",
"sha256": "31fe97f889935e92bfb3af5056e2579105fb7b48b325fd3dfcc9a5847b4ee6b7"
},
"dev-harness/mock-child-scripts.example.json": {
"donor": "scripts/dev/mock-child-scripts.example.json",
"bytes": 532,
"sha256": "8f785710966f26a7923793f47a785b18d8e8d326bb381f78682a7eb50ac6bdd7"
"dev-harness/mock-script.example.json": {
"bytes": 887,
"origin": "scripts/dev/mock-script.example.json",
"sha256": "45c2915841b0a6c0f17db7a297a39ca1dfb8b889d74f018f87d1815cadf17c26"
}
}
},
"provenance": "Historical origin only: extracted in 2026-10 from the Heretek fork (now retired). This repo never reads the donor \u2014 integrity is self-contained via the pinned bytes/sha256 below."
}
+9 -16
View File
@@ -1,19 +1,14 @@
# packages/ai-studio — Tier-2 extraction (Phase 4, issue #4)
# packages/ai-studio — Tier-2 runtime (self-contained)
~8.9k-line BYOK AI + multi-agent Studio, moved **verbatim** out of the
fork. The fork is a donor, not a delivery vehicle. Never hand-edit
files under `AI/` or `AiGeneration/` here — re-run the extractor:
```bash
node scripts/extract-tier2.mjs --fork <fork-checkout>
```
`MANIFEST.json` pins every file (sha256); `tests/tier2/extract.test.mjs`
fails if any byte drifts from the donor.
~8.9k-line BYOK AI + multi-agent Studio. Historically extracted from the
(now-retired) Heretek fork; this package is now the canonical source —
there is no donor checkout anywhere in build, test, or CI paths.
`MANIFEST.json` pins every file (bytes + sha256);
`tests/tier2/extract.test.mjs` fails if any byte drifts.
## Contents
| Path | Donor | Notes |
| Path | Historical origin | Notes |
|---|---|---|
| `AI/CustomAIClient.js` | `newIDE/app/src/AI/CustomAIClient.js` (5870 lines) | OpenAI-compatible client, 39-tool `GDEVELOP_OPENAI_TOOLS`, SSE streaming, `gd-custom-ai-*` localStorage keys. **Zero `api.gdevelop.io` references** (pinned by test) |
| `AI/CustomAIClient.spec.js` | co-located spec | Run inside `newIDE/app` (`react-app-rewired test`), not here |
@@ -23,10 +18,8 @@ fails if any byte drifts from the donor.
## Wiring
Import rewiring is limited to `packages/unlock-shim/generation-shim.mjs`
(routing predicates + fail-closed leak guard) plus the generated glue
patches `07-generation-glue.patch` (Generation.js branches) and
`08-preferences-ai.patch` (`ai` prefs tab + storage). The extracted
modules themselves are byte-identical to the donor.
(routing predicates + fail-closed leak guard) plus the glue patches
`07`–`11` (`patches/REBASE.md` records how the forward-ports were built).
## Desktop E2E (requires a built app — procedure, not automation)
-88
View File
@@ -1,88 +0,0 @@
// Extracts Tier-2 donor files VERBATIM from the fork into
// packages/ai-studio/ (Phase 4, issue #4). The fork is a donor, not a
// delivery vehicle: these files are never hand-edited here.
//
// node scripts/extract-tier2.mjs --fork review/GDevelop-Heretek
//
// Writes MANIFEST.json (sha256 per file); tests/tier2/extract.test.mjs
// re-verifies byte-identity. To refresh after a fork sync, re-run with
// the new fork checkout — any content change shows up as a manifest
// diff in code review.
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(here, '..');
const DEST = path.join(ROOT, 'packages', 'ai-studio');
// Donor paths, relative to the fork root. Kept explicit (not a glob)
// so additions/removals in the fork are noticed, not silently absorbed.
const DONOR_FILES = [
'newIDE/app/src/AI/CustomAIClient.js',
'newIDE/app/src/AI/CustomAIClient.spec.js',
'newIDE/app/src/AiGeneration/Studio/EditApprovalLabel.js',
'newIDE/app/src/AiGeneration/Studio/FinalizeSubAgents.js',
'newIDE/app/src/AiGeneration/Studio/LoopGuard.js',
'newIDE/app/src/AiGeneration/Studio/NudgePolicy.js',
'newIDE/app/src/AiGeneration/Studio/PlanStore.js',
'newIDE/app/src/AiGeneration/Studio/RequestWriteGate.js',
'newIDE/app/src/AiGeneration/Studio/Roles.js',
'newIDE/app/src/AiGeneration/Studio/RoleToolPolicy.js',
'newIDE/app/src/AiGeneration/Studio/SafeTruncation.js',
'newIDE/app/src/AiGeneration/Studio/SpawnSubAgents.js',
'newIDE/app/src/AiGeneration/Studio/UseStudioNudge.js',
'newIDE/app/src/AiGeneration/Studio/UseStudioRuntime.js',
'scripts/dev/mock-ai-provider.js',
'scripts/dev/mock-script.example.json',
'scripts/dev/mock-script-multi.json',
'scripts/dev/mock-script-specialists.json',
'scripts/dev/mock-child-scripts.example.json',
];
// Fork-relative -> package-relative layout.
function destFor(rel) {
if (rel.startsWith('newIDE/app/src/'))
return rel.slice('newIDE/app/src/'.length);
if (rel.startsWith('scripts/dev/'))
return 'dev-harness/' + rel.slice('scripts/dev/'.length);
throw new Error(`No destination mapping for donor file: ${rel}`);
}
function parseArgs(argv) {
const out = { fork: path.join(ROOT, 'review', 'GDevelop-Heretek') };
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--fork') out.fork = path.resolve(argv[++i]);
}
return out;
}
const args = parseArgs(process.argv.slice(2));
const manifest = { donor: args.fork, files: {} };
for (const rel of DONOR_FILES) {
const src = path.join(args.fork, rel);
if (!fs.existsSync(src)) {
throw new Error(
`Donor file missing: ${rel} (fork checkout at ${args.fork}). ` +
`If the fork moved it, update DONOR_FILES explicitly.`
);
}
const content = fs.readFileSync(src);
const dest = path.join(DEST, destFor(rel));
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.writeFileSync(dest, content);
manifest.files[destFor(rel)] = {
donor: rel,
bytes: content.length,
sha256: crypto.createHash('sha256').update(content).digest('hex'),
};
console.log(`extracted ${destFor(rel)} (${content.length} bytes)`);
}
fs.writeFileSync(
path.join(DEST, 'MANIFEST.json'),
JSON.stringify(manifest, null, 2) + '\n'
);
console.log(`wrote MANIFEST.json (${DONOR_FILES.length} files)`);
+4 -1
View File
@@ -5,7 +5,10 @@
// deltas are woven through their files, so each patch carries the exact
// fork diff — no hand-editing, no silent shaping.
//
// node scripts/generate-tier2-patches.mjs --upstream review/GDevelop --fork review/GDevelop-Heretek
// On-demand regeneration of the additive 09/10 deltas only. Needs an
// explicit fork checkout (the retired donor — not present in this repo,
// not needed by any test or CI path):
// node scripts/generate-tier2-patches.mjs --upstream review/GDevelop --fork <donor-checkout>
//
// Each patch round-trips (`patch -p1 --dry-run` on a pristine copy)
// before it is written. Regeneration is deterministic: committed output
+9 -28
View File
@@ -1,6 +1,6 @@
// Tier-2 extraction tests (Phase 4): packages/ai-studio/ stays
// byte-identical to the fork donor, and the extracted client keeps the
// BYOK invariant (no hosted generation references).
// Tier-2 package tests: packages/ai-studio/ is self-contained and intact.
// Tamper-evidence comes from MANIFEST.json hashes pinned in THIS repo —
// no donor checkout, no fork clone, nothing outside this tree.
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
@@ -11,52 +11,33 @@ import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(here, '..', '..');
const PACKAGE = path.join(ROOT, 'packages', 'ai-studio');
const FORK =
process.env.FORK_DIR || path.join(ROOT, 'review', 'GDevelop-Heretek');
function sha256(abs) {
return crypto.createHash('sha256').update(fs.readFileSync(abs)).digest('hex');
}
describe('tier-2: verbatim extraction', () => {
describe('tier-2: self-contained package integrity', () => {
const manifest = JSON.parse(
fs.readFileSync(path.join(PACKAGE, 'MANIFEST.json'), 'utf8')
);
it('every manifest file matches the donor byte-for-byte', () => {
it('every manifest file exists with pinned bytes and hash', () => {
assert.ok(
Object.keys(manifest.files).length >= 19,
'manifest looks truncated'
);
for (const [pkgRel, meta] of Object.entries(manifest.files)) {
const pkgAbs = path.join(PACKAGE, pkgRel);
const donorAbs = path.join(FORK, meta.donor);
assert.ok(fs.existsSync(pkgAbs), `package file missing: ${pkgRel}`);
assert.ok(
fs.existsSync(donorAbs),
`donor moved in fork: ${meta.donor} (update scripts/extract-tier2.mjs explicitly)`
);
if (pkgRel.startsWith('dev-harness/')) {
// Dev harness is ported, not vendored: the repo root is
// "type": "module" so the donor's CJS `require` throws
// ReferenceError here (see mock-ai-provider.js ESM port). The
// MANIFEST sha below still pins the exact bytes we ship.
assert.equal(
sha256(pkgAbs),
meta.sha256,
`manifest stale for ${pkgRel} (re-run the extractor)`
);
continue;
}
assert.equal(
sha256(pkgAbs),
sha256(donorAbs),
`drift vs donor: ${pkgRel}`
fs.statSync(pkgAbs).size,
meta.bytes,
`size drift (tamper?): ${pkgRel}`
);
assert.equal(
sha256(pkgAbs),
meta.sha256,
`manifest stale for ${pkgRel} (re-run the extractor)`
`hash drift (tamper?): ${pkgRel}`
);
}
});
+24 -44
View File
@@ -1,9 +1,9 @@
// Tier-2 glue patch tests (Phase 4): committed verbatim patches (09/10)
// equal fresh generator output (deterministic — any hand-edit or fork
// drift goes red); curated forward-ports (07/08/11, see patches/REBASE.md)
// are pinned instead by anti-revert assertions; everything dry-runs clean
// on pristine upstream copies.
import { describe, it, before, after } from 'node:test';
// Tier-2 glue patch tests: every patch dry-runs clean on pristine
// upstream copies; curated forward-ports (07/08/11, see patches/REBASE.md)
// and additive deltas (09/10) are pinned by assertions on the committed
// patch text itself. Nothing here needs a donor checkout — this repo
// stands alone (fork-independence, 2026-10-11).
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
@@ -15,8 +15,6 @@ const here = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(here, '..', '..');
const UPSTREAM =
process.env.UPSTREAM_DIR || path.join(ROOT, 'review', 'GDevelop');
const FORK =
process.env.FORK_DIR || path.join(ROOT, 'review', 'GDevelop-Heretek');
const GLUE = [
// Curated forward-ports (patches/REBASE.md): must dry-run clean and must
@@ -37,16 +35,17 @@ const GLUE = [
],
noDelete: ['newEffectDialogDefaultTab'],
},
// Verbatim deltas: committed patches equal fresh output byte-for-byte.
// Purely additive deltas: committed patches must contain zero
// minus-lines (any deletion would be an upstream revert by construction).
{
patch: '09-prepare-ai-content.patch',
files: ['newIDE/app/src/AiGeneration/PrepareAiUserContent.js'],
verbatim: true,
additive: true,
},
{
patch: '10-ai-chat-affordance.patch',
files: ['newIDE/app/src/AiGeneration/AiRequestChat/Utils.js'],
verbatim: true,
additive: true,
},
{
patch: '11-studio-runtime-mount.patch',
@@ -63,39 +62,7 @@ function run(cmd, args, cwd) {
});
}
describe('tier-2: glue patches are generated, not hand-edited', () => {
let regenDir;
before(() => {
regenDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tier2-regen-'));
const r = run(
'node',
[
'scripts/generate-tier2-patches.mjs',
'--upstream',
UPSTREAM,
'--fork',
FORK,
'--out',
regenDir,
],
ROOT
);
assert.equal(r.status, 0, `generator failed:\n${r.stdout}\n${r.stderr}`);
});
after(() => fs.rmSync(regenDir, { recursive: true, force: true }));
it('committed verbatim patches equal fresh output byte-for-byte', () => {
for (const { patch, verbatim } of GLUE) {
if (!verbatim) continue;
const committed = fs.readFileSync(
path.join(ROOT, 'patches', patch),
'utf8'
);
const fresh = fs.readFileSync(path.join(regenDir, patch), 'utf8');
assert.equal(fresh, committed, `${patch} was hand-edited or is stale`);
}
});
describe('tier-2: glue patches apply cleanly and preserve upstream', () => {
it('curated forward-ports never delete upstream-new symbols', () => {
for (const { patch, noDelete } of GLUE) {
if (!noDelete) continue;
@@ -112,6 +79,19 @@ describe('tier-2: glue patches are generated, not hand-edited', () => {
}
});
it('additive deltas contain zero deletions', () => {
for (const { patch, additive } of GLUE) {
if (!additive) continue;
const text = fs.readFileSync(path.join(ROOT, 'patches', patch), 'utf8');
const deletions = text.split('\n').filter(l => /^-[^-]/.test(l));
assert.equal(
deletions.length,
0,
`${patch} deletes lines (upstream revert by construction):\n${deletions.slice(0, 5).join('\n')}`
);
}
});
it('glue dry-runs clean on pristine upstream copies', () => {
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'tier2-dry-'));
try {