Skip to content
25 changes: 25 additions & 0 deletions docs/frtk-table-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,28 @@ counters. It never exposes private memory or fingerprint material.

The recruiting wrapper additionally reports `RECRUITING_ACTION_UNSUPPORTED`
and `RECRUITING_HOURS_INSUFFICIENT` for its two domain-specific failures.

## Live recruit class replacement POC

With the game and Lua host running, one command generates a class with Brooks's
engine and replaces the existing live Player, Recruit, and fixed Player-name
rows:

```powershell
cfb27lua live-class replace `
--save "C:\path\to\DYNASTY-AUTOSAVE" `
--brooks-root "C:\path\to\cfb27-dynasty-modding" `
--seed poc-1
```

The save is read only and supplies the existing row skeleton; no save is
rewritten and no live row is created. First name, last name, and hometown are
mandatory. A generated portrait/head asset is written when present. Gear is
skipped in this POC.

Before the first write, the command requires one unique live Player surface,
Recruit surface, and Player string surface, then snapshots the complete class.
Writes use guarded batches of at most 32 operations with readback. Any later
failure rolls earlier batches back to that snapshot; an ambiguous mirror aborts
without guessing. Add `--dry-run` to generate, locate, and snapshot without
writing anything.
268 changes: 268 additions & 0 deletions docs/superpowers/plans/2026-07-14-live-recruit-class-replacement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
# Live Recruit Class Replacement POC Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add one command that runs Brooks's recruit generator against a read-only dynasty save and replaces the existing live recruit class, including names, with automatic verification and rollback.

**Architecture:** A Brooks adapter converts the generator's `planApply` output into bit-masked Player/Recruit record patches plus fixed-slot Player strings. A locator finds the three contiguous live surfaces from save-derived anchors. A replacement service snapshots, applies, verifies, and rolls back guarded raw-memory batches; the CLI wires everything into one command.

**Tech Stack:** Node.js CommonJS, existing `@cfb27/lua-hook` SDK memory APIs, Brooks's CommonJS generator modules, Node test runner.

## Global Constraints

- Never modify or resave the dynasty input.
- Replace existing Recruit and Player rows only; do not allocate rows.
- FirstName, LastName, and HomeTown are mandatory and must preflight before any write.
- Portrait/head writes are best-effort; gear is reported as skipped in the POC.
- Use guarded expected/replacement transactions with no more than 32 operations per host transaction.
- Snapshot all targeted live bytes before the first write and roll back completed batches after any failure.
- No UI, daemon, database, or generalized allocation API.

---

### Task 1: Brooks Generator Adapter

**Files:**
- Create: `packages/sdk/src/live-class-generator.cjs`
- Create: `packages/sdk/test/live-class-generator.test.cjs`
- Modify: `packages/sdk/index.cjs`

**Interfaces:**
- Produces: `generateLiveClassPlan({ savePath, brooksRoot, seed }) -> Promise<LiveClassPlan>`
- `LiveClassPlan` contains `sourceRevision`, `classSize`, `playerRecordSize`, `recruitRecordSize`, `playerRows`, `recruitRows`, and `gearSkipped`.
- Each record row contains `{ row, beforeHex, maskHex, valueHex }`.
- Each player row also contains `strings: { FirstName, LastName, HomeTown, GenericHeadAssetName? }` and a 138-byte `beforeStringSlotHex`.

- [ ] **Step 1: Write the failing adapter tests**

Test injected Brooks dependencies that return two generated recruits. Assert that the adapter:

```js
assert.equal(plan.classSize, 2);
assert.deepEqual(plan.playerRows[0].strings, {
FirstName: 'Marcus', LastName: 'Hill', HomeTown: 'Austin',
});
assert.equal(plan.playerRows[0].maskHex.length, plan.playerRows[0].beforeHex.length);
assert.equal(plan.recruitRows[0].row, 30);
assert.equal(await hashFile(savePath), originalHash);
```

Also assert rejection for a missing name, an out-of-range row, unequal record lengths, a Brooks planning error, and any before/after save hash difference.

- [ ] **Step 2: Run the focused tests and confirm red**

Run: `node --test packages/sdk/test/live-class-generator.test.cjs`

Expected: failure because `live-class-generator.cjs` does not exist.

- [ ] **Step 3: Implement the adapter**

Implement these exports:

```js
async function generateLiveClassPlan({ savePath, brooksRoot, seed = 'default', dependencies = {} }) {}
function buildMaskedPatch(before, after) {}
function encodePlayerStringSlot(beforeSlot, strings) {}
```

The adapter must:

1. hash the save before work;
2. dynamically load `runPreview`, `loadRecruitPool`, `planApply`, `openCollegeSave`, and `setRecordField` from `brooksRoot`;
3. run preview output inside `fs.mkdtemp()`;
4. call `planApply` and reject all collected errors;
5. clone each source record, apply only fields present in Brooks's write plan, and calculate `maskHex` from bytes changed between the two offline records;
6. read each existing 138-byte Player table2 slot and encode mandatory strings at offsets FirstName `0/17`, LastName `50/21`, and HomeTown `112/26`; optionally encode GenericHeadAssetName at `17/33`;
7. hash the save again and reject if it changed;
8. remove the temporary preview directory in `finally`.

- [ ] **Step 4: Run focused and full SDK tests**

Run: `node --test packages/sdk/test/live-class-generator.test.cjs`

Expected: all focused tests pass.

Run: `npm test`

Expected: all repository tests pass.

- [ ] **Step 5: Commit the adapter**

```bash
git add packages/sdk/src/live-class-generator.cjs packages/sdk/test/live-class-generator.test.cjs packages/sdk/index.cjs
git commit -m "feat: adapt Brooks recruit classes for live writes"
```

### Task 2: Save-Derived Live Surface Locator

**Files:**
- Create: `packages/sdk/src/live-class-locator.cjs`
- Create: `packages/sdk/test/live-class-locator.test.cjs`

**Interfaces:**
- Consumes: record/string anchors from `LiveClassPlan`.
- Produces: `locateLiveClassSurfaces({ client, plan }) -> Promise<{ playerBase, recruitBase, playerStringsBase }>`.

- [ ] **Step 1: Write the failing locator tests**

Build a fake address space containing contiguous Player records, Recruit records, and 138-byte Player string slots. Assert:

```js
assert.deepEqual(await locateLiveClassSurfaces({ client, plan }), {
playerBase: '0x10000000',
recruitBase: '0x20000000',
playerStringsBase: '0x30000000',
});
```

Cover relocated bases, more than one initial scan hit with only one cross-row-valid candidate, no candidate, two fully valid candidates, short reads, and a save/live verification mismatch.

- [ ] **Step 2: Run focused tests and confirm red**

Run: `node --test packages/sdk/test/live-class-locator.test.cjs`

Expected: failure because the locator module does not exist.

- [ ] **Step 3: Implement exact-anchor location with cross-row verification**

Implement:

```js
async function locateContiguousSurface(client, {
rows, recordSize, anchorRow, anchorHex, verificationRows,
}) {}
async function locateLiveClassSurfaces({ client, plan }) {}
```

For each surface, scan one exact save-derived anchor, calculate `base = match - anchorRow * stride`, then read and exactly verify at least four spread-out rows. Require exactly one fully verified base. Player and Recruit strides come from the plan; Player strings always use 138 bytes. Reject all ambiguity rather than guessing between stale mirror copies.

- [ ] **Step 4: Run locator and full tests**

Run: `node --test packages/sdk/test/live-class-locator.test.cjs`

Expected: all focused tests pass.

Run: `npm test`

Expected: all repository tests pass.

- [ ] **Step 5: Commit the locator**

```bash
git add packages/sdk/src/live-class-locator.cjs packages/sdk/test/live-class-locator.test.cjs
git commit -m "feat: locate live recruit class surfaces"
```

### Task 3: Guarded Replacement and Rollback

**Files:**
- Create: `packages/sdk/src/live-class-replace.cjs`
- Create: `packages/sdk/test/live-class-replace.test.cjs`
- Modify: `packages/sdk/index.cjs`
- Modify: `packages/sdk/src/errors.cjs`

**Interfaces:**
- Consumes: `LiveClassPlan`, located bases, and an SDK client.
- Produces: `replaceLiveClass({ client, plan, surfaces, generation, dryRun }) -> Promise<LiveClassResult>`.
- `LiveClassResult` contains `status`, `classSize`, `batchesApplied`, `playerRowsWritten`, `recruitRowsWritten`, `nameSlotsWritten`, `optionalSkipped`, and `rollbackStatus`.

- [ ] **Step 1: Write the failing replacement tests**

Assert that preflight reads all rows before the first transaction, combines masks with current live bytes, writes no more than 32 operations per batch, rereads every committed batch, and reports gear as skipped. Inject a failure in batch two and assert that batch one is restored from the snapshot. Inject a rollback failure and assert a stable `LIVE_CLASS_ROLLBACK_FAILED` error.

Core replacement rule:

```js
replacement[i] = (current[i] & ~mask[i]) | (value[i] & mask[i]);
```

- [ ] **Step 2: Run focused tests and confirm red**

Run: `node --test packages/sdk/test/live-class-replace.test.cjs`

Expected: failure because the replacement module does not exist.

- [ ] **Step 3: Implement preflight, batching, verification, and rollback**

Implement:

```js
async function replaceLiveClass({ client, plan, surfaces, generation, dryRun = false }) {}
function applyMask(current, mask, value) {}
function makeOperations(snapshot, plan, surfaces) {}
function chunkOperations(operations, maximum = 32) {}
```

Preflight must read every numeric record and Player string slot, validate row bounds and exact lengths, encode required names, and build an immutable rollback snapshot before any transaction. Each forward batch uses the snapshot bytes as `expectedHex`; each rollback batch uses the forward replacement as `expectedHex`. After every forward or rollback batch, reread and compare the complete written ranges.

- [ ] **Step 4: Run replacement and full tests**

Run: `node --test packages/sdk/test/live-class-replace.test.cjs`

Expected: all focused tests pass.

Run: `npm test`

Expected: all repository tests pass.

- [ ] **Step 5: Commit the replacement service**

```bash
git add packages/sdk/src/live-class-replace.cjs packages/sdk/test/live-class-replace.test.cjs packages/sdk/index.cjs packages/sdk/src/errors.cjs
git commit -m "feat: replace live recruit classes with rollback"
```

### Task 4: One-Command CLI and Documentation

**Files:**
- Modify: `packages/cli/src/main.cjs`
- Modify: `packages/cli/test/main.test.cjs`
- Modify: `docs/frtk-table-api.md`
- Modify: `package.json`

**Interfaces:**
- Produces: `cfb27 live-class replace --save <path> --brooks-root <path> [--seed <value>] [--dry-run]`.

- [ ] **Step 1: Write failing CLI tests**

Assert exact parsing, required paths, dry-run propagation, one JSON result under `--json`, refusal of unknown flags, and sanitized errors. Use injected adapter/locator/replacer functions so tests never require a game or real save.

- [ ] **Step 2: Run CLI tests and confirm red**

Run: `node --test packages/cli/test/main.test.cjs`

Expected: the new command assertions fail.

- [ ] **Step 3: Wire the command**

The handler must execute only this sequence:

```js
const plan = await generateLiveClassPlan({ savePath, brooksRoot, seed });
const status = await client.status();
const surfaces = await locateLiveClassSurfaces({ client, plan });
return replaceLiveClass({ client, plan, surfaces, generation: status.generation, dryRun });
```

Document that the save is read-only, names are mandatory, gear is skipped in the POC, ambiguous live mirrors abort, and no in-game operation occurs unless the user invokes the command without `--dry-run`.

- [ ] **Step 4: Run complete verification**

Run: `npm run check`

Expected: exit code 0.

Run: `npm test`

Expected: all tests pass.

Run: `git diff --check`

Expected: no output.

- [ ] **Step 5: Commit the CLI POC**

```bash
git add packages/cli/src/main.cjs packages/cli/test/main.test.cjs docs/frtk-table-api.md package.json
git commit -m "feat: add live recruit class replacement command"
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Live Recruit Class Replacement POC

## Goal

Prove that Brooks's external recruit generator can replace the game's already-generated recruit class directly in live FrTk data without modifying or resaving the dynasty file.

## Command

Expose one command:

```text
live-class replace --save <autosave> --brooks-root <path>
```

The command reads the save as a skeleton, runs Brooks's generator, and immediately applies the generated class to the running game. There is no user-reviewed intermediate plan.

## Scope

The POC must write:

- existing `Player` rows: position, archetype, height, weight, body type, development trait, stars, ratings, physical abilities, mental abilities, home state, and pipeline;
- existing `Recruit` rows: quality modifier, ranks, and alternate positions;
- first name, last name, and hometown through a verified live string path.

Portrait/head and gear/`CharacterVisuals` are best-effort. They may be reported as skipped without failing the core replacement. The POC does not add or remove recruit or player rows.

## Data Flow

1. Read the supplied save without writing it.
2. Use its existing recruit-to-player row pairs as Brooks's generation skeleton.
3. Run Brooks's generator from the existing local checkout, reset to a known `origin/main` revision before integration.
4. Normalize Brooks's generated writes in memory.
5. Discover the live `Player`, `Recruit`, and string surfaces and verify their identities, capacities, and generation.
6. Preflight every target row, field, value, and string before the first write.
7. Capture a rollback snapshot of all affected live records and strings.
8. Apply guarded batches, rereading each batch after it commits.
9. On any failure, stop and restore every completed batch from the snapshot.
10. Return a compact summary of generated recruits, fields written, optional surfaces skipped, and rollback status.

## Safety Boundary

- The save path is read-only; the command never calls a save writer.
- Names are mandatory. If the live string path cannot safely write all required names and hometowns, preflight fails before any mutation.
- All record changes carry expected old values and lifecycle generation guards.
- Player/Recruit row relationships must match the read-only save skeleton before writing.
- The operation is multi-batch rather than globally atomic, so rollback is mandatory.
- Managed or derived fields that fail reread verification abort the operation.

## MVP Architecture

- A thin Brooks adapter invokes the existing generator and converts `buildRecruitWrites` output into a normalized in-memory plan.
- A live-class service performs discovery, preflight, snapshot, guarded batching, verification, and rollback.
- A minimal CLI command wires the two together.
- No UI, daemon, persistent database, generalized allocation API, or recruit creation is included.

## Verification

Automated tests use synthetic Player, Recruit, and string mirrors to prove:

- Brooks output maps to the expected existing row pairs;
- the save is never opened for writing;
- names are a hard preflight gate;
- guarded batches stop on stale data;
- a mid-operation failure restores earlier batches;
- successful rereads match the normalized generated class;
- unsupported portrait or gear writes are reported but do not fail the core operation.

The POC is complete when one command can generate and transactionally replace a synthetic full class offline. Actual game execution is a separate user-controlled gate.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"packages/cli"
],
"scripts": {
"check": "node --check packages/sdk/index.cjs && node --check packages/sdk/src/validation.cjs && node --check packages/sdk/src/frtk-fields.cjs && node --check packages/sdk/src/frtk-profile.cjs && node --check packages/sdk/src/live-recruiting-layout.cjs && node --check packages/sdk/src/live-recruiting.cjs && node --check packages/cli/bin/cfb27lua.cjs && node --check packages/cli/src/main.cjs && node --check scripts/build-frtk-profile.cjs && node --check scripts/package-release.cjs && node --check scripts/run-tests.cjs",
"check": "node --check packages/sdk/index.cjs && node --check packages/sdk/src/validation.cjs && node --check packages/sdk/src/frtk-fields.cjs && node --check packages/sdk/src/frtk-profile.cjs && node --check packages/sdk/src/live-recruiting-layout.cjs && node --check packages/sdk/src/live-recruiting.cjs && node --check packages/sdk/src/live-class-generator.cjs && node --check packages/sdk/src/live-class-locator.cjs && node --check packages/sdk/src/live-class-replace.cjs && node --check packages/cli/bin/cfb27lua.cjs && node --check packages/cli/src/main.cjs && node --check scripts/build-frtk-profile.cjs && node --check scripts/package-release.cjs && node --check scripts/run-tests.cjs",
"test": "node scripts/run-tests.cjs",
"build:frtk-profile": "node scripts/build-frtk-profile.cjs",
"pack:preview": "node scripts/package-release.cjs"
Expand Down
Loading
Loading