Milestone Alpha follow-up 1 Plan
Plan · approved by the author (2026-10-19); in execution

Implementation Plan, Milestone Alpha follow-up 1

EXPERTA

Implementation plan for Milestone Alpha follow-up 1 of EXPERTA, the Fractional Factorial Design Translator.

Plan
replication_materials/plans/2026-10-19-alpha-minors-plan.md
Specification
replication_materials/plans/2026-10-14-ff-design-translator-design.md
Status
Approved by the author (2026-10-19); in execution
Tasks
8, each test-first and committed on its own
Maintained by
Alan R. Vazquez (Tecnológico de Monterrey) and Fadel M. Megahed (Miami University)

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.

1Context

The alpha release is live at https://expertadoe.app (main 96c8d9f, 2026-10-19). Its execution ledger (2026-10-17-alpha-execution-ledger.md) parked 51 ledger lines of minor review findings. On 2026-10-19 Fadel M. Megahed asked that all of them be done. A read-only audit of main (33fa98e) found 63 findings in those lines and in the later sections: 47 are open, 11 are already fixed and 5 cannot be acted on. Two open findings share one cause, so there are 46 fixes. None is large. This plan does all 46, grouped by area into eight tasks.

The review of Alan R. Vazquez runs while this plan runs. No task changes a text that the review asks about (the guarantee table and its statements, the glossary, R23 and the references).

Goal: Every open deferred minor of the alpha ledger is fixed, with a test where the finding is about behaviour, and the ledger records each finding id with its commit or its reason.

Architecture: No new module except one shared helper for the store choice (lib/ops/stores.ts). The fixes stay in the files that the findings name. Production behaviour changes in four places only, each with a test: the Redis keys get their time-to-live once (not on each request), the in-memory spend tracker keeps one total per UTC date, the translate handler computes the reserve amounts before its store try, and the review route keeps a fetched issue list only if no answer was filed meanwhile.

Tech Stack: Next.js 16.3.8, React 19.3.0, TypeScript 5.9.3, zod 4.6.5, vitest 4.1.11, Playwright 1.63.0 (Chromium), Lua scripts on Upstash Redis (tests on ioredis-mock 8.13.1). No new package.

Spec: replication_materials/plans/2026-10-14-ff-design-translator-design.md (version 6), Sections 13.1, 13.2, 14.4 and 17.1. The finding ids (T1-a to T9-f, FR-1, B2-a, B2-b) are those of the audit table replication_materials/plans/2026-10-19-alpha-minors-audit.md (read-only audit of main 33fa98e), which Task 8 copies into the ledger.

2Global Constraints

  • No package changes. Implementers do not run npm install. The repository is in Dropbox; only the controller installs packages, after the user pauses Dropbox.
  • Secrets. Never read .env. Tests use made-up values. Never print a secret.
  • Outside contact. Subagents make no network call to Vercel, Upstash, GitHub or an LLM provider. Only the controller pushes, deploys or runs the deployed checks (Task 3), after the author's approval.
  • Texts under review stay. Do not change content/glossary.json, data/guarantee-table.json, the R23 text or the reference list.
  • Fixed texts. Every new user-visible text is a fixed string in one module with a unit test of its exact text. No advice words (ADVICE_WORDS in lib/translate/messages.ts).
  • Wording. Comments and documents about 80% ASD-STE100, plain academic tone; at most 25 words in a sentence of a spec cell.
  • Tests. All tests in replication_materials/tests/. Test first: each behaviour fix starts with a failing test, and the report names the failure. npm run check -- --fast at the end of each task; npm run check (with Chromium) at the end of Tasks 5, 7 and 8.
  • Commits. One commit per task. Each message ends with the attribution lines of the session (Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> and the Claude-Session: line).

3Review Focus

  1. A Redis key that already has no time-to-live (written by the old script, or by a manual edit): the new scripts must give it one, or it lives for ever. Test in Task 4.
  2. The in-memory tracker sees an older time after a newer one (two requests near UTC midnight): the reserve of one day must not erase the total of the other. Test in Task 4.
  3. A review answer filed while a list request is in flight: the next read must show the new answer, not the list from before. Test in Task 6.
  4. A review comment of 4,000 characters that JSON escapes to 6 bytes each: a valid answer must never get "The answer does not have the expected form." Test in Task 6.
  5. The review key is rejected while the reviewer files an answer: the page must go to the state without a key, as it does on load. Test in Task 7.

4Task 1: Secret scan (T1-a to T1-g)

Files:

  • Modify: scripts/lib/secret-scan.mjs, scripts/secret-scan.mjs
  • Test: replication_materials/tests/unit/secret-scan.test.ts

Interfaces:

  • Produces: scanLog(log, values) and secretValues(text) keep their signatures. failureLine(error) keeps its output form The secret scan failed (<code>).
    • "a deleted file does not take the next path" (T1-a): a log with +++ /dev/null after a file header, then an added secret line in the next file; the finding has the next file's path.
    • "the path resets at each commit" (T1-a): two commits, the second without a +++ line before its first added line; the finding of the second commit has no path from the first commit (path "(unknown)", the value the code uses today for no path, or the code's existing equivalent).
    • "an added line that starts with ++ is content" (T1-a, T1-b): an added line +++ token=<value> inside a hunk is scanned, not read as a header.
    • "quoted and tab-suffixed paths" (T1-b): +++ "b/dir/a b.txt" gives dir/a b.txt; +++ b/c.txt\t gives c.txt.
    • "CRLF in the env file" (T1-a): secretValues("A_KEY=abcdefghijklmnop\r\n") gives the value without \r.
    • "export prefix and inline comment" (T1-c): export A_KEY=abcdefghijklmnop # note gives abcdefghijklmnop; a quoted value "abc#def123456789" keeps the #; an unquoted value keeps a # that has no white space before it.
    • "failureLine prints only a safe code" (T1-f): code "ENOENT" prints it; code "bad code; rm -rf" and a 40-character code print error; numeric status 2 prints 2.
    • "exit status 2 with the fixed line" (T1-g): spawn node scripts/secret-scan.mjs --env <tmp>/missing.env with cwd in a new empty temp directory (not a git repository); expect status 2 and stdout The secret scan failed (ENV_NOT_FOUND).
    • Header state (T1-b): a file header starts at a diff --git line and ends at the first @@ line. Only inside a header are --- and +++ lines headers. +++ /dev/null sets the path to none. A commit: line resets the path. Unquote a C-quoted path ("b/...", with \", \\ and \t escapes) and drop a trailing tab and what follows it.
    • secretValues (T1-a, T1-c): strip \r; strip a leading export ; for an unquoted value, drop # (white space then #) and the rest.
    • Entry point (T1-d, T1-e): when --env is given without an argument, throw an error with code ENV_ARGUMENT; when the given file does not exist, code ENV_NOT_FOUND. Without --env, a missing .env stays allowed. The log command becomes git -c core.quotepath=off log -p -m --no-ext-diff --no-textconv --all --format=commit:%H (-m shows the diff of a merge against each parent; findings are already unique by key).
    • failureLine (T1-f): a string code must match /^[A-Z0-9_]{1,32}$/; a number must be an integer from 0 to 255; else error.

5Task 2: Measurement helpers and budget scripts (T2-a, T2-b, T2-c, B2-a, B2-b, T9-b)

Files:

  • Modify: replication_materials/tests/deployed/helpers.ts, replication_materials/tests/deployed/site.spec.ts, replication_materials/tests/deployed/slow-start.spec.ts, scripts/derive-budgets.mjs, README.md
  • Test: replication_materials/tests/unit/deployed-helpers.test.ts (new), replication_materials/tests/unit/budgets.test.ts

Interfaces:

  • Produces: maxLoadingGap(timeline) returns null when the timeline has no "loading (" text (was 0). slowRuns(env: string | undefined): number (new export of slow-start.spec.ts or of helpers.ts): 20 when unset, the integer when it is a positive integer, else it throws FFD_SLOW_RUNS must be a positive integer.
    • deployed-helpers.test.ts: maxLoadingGap of a timeline without "loading (" is null; with "loading (" but no "preparing" text, the window ends at the last "loading (" entry, not at the end of the timeline; slowRuns(undefined) is 20, slowRuns("5") is 5, slowRuns("0"), slowRuns("x") and slowRuns("2.5") throw.
    • budgets.test.ts: deriveBudgets with a used cell "" or "abc" throws Not a finite number in <file>, row <n>, column <name>.

6Task 3: CPU throttling of the engine worker (T2-d)

The slow-profile runs set a 4x CPU slowdown on the page through CDP. The R engine runs in a dedicated worker, which is another CDP target. Nothing shows that the slowdown reaches it.

Files:

  • Create: replication_materials/tests/deployed/throttle-probe.spec.ts
  • Modify: replication_materials/tests/deployed/helpers.ts
  • Data: replication_materials/tests/perf/throttle-probe.csv (new)

Interfaces:

  • Produces: applySlowProfile(page) in helpers.ts sets the network and CPU throttling on the page and on each worker of the page (CDP Target.setAutoAttach with flatten: true and waitForDebuggerOnStart: true; on each attached worker session, Emulation.setCPUThrottlingRate and then Runtime.runIfWaitingForDebugger).

7Task 4: Store and spend (T3-a to T3-d, T4-a, T4-b with FR-1, T4-d to T4-g)

Files:

  • Modify: lib/ops/scripts.ts, lib/ops/spend.ts, lib/ops/stores.ts, lib/translate/service.ts, lib/translate/handler.ts, lib/translate/reserve.ts, lib/security-headers.ts, app/api/review/route.ts, content/messages.json, spec 17.1 (self-hosting row)
  • Test: replication_materials/tests/unit/ops/scripts.test.ts, replication_materials/tests/unit/ops/spend.test.ts, replication_materials/tests/unit/ops/stores.test.ts, replication_materials/tests/unit/translate/handler.test.ts, replication_materials/tests/unit/privacy.test.ts

Interfaces:

  • Produces: chooseLimiter(store: Store, limits: RateLimits): RateLimiter exported from lib/ops/stores.ts (moved from service.ts and app/api/review/route.ts; the type names are the ones the two copies use today). productionDeps calls chooseStore once and passes the result to chooseLimiter and to chooseSpend.
    • scripts.test.ts (T3-d): after two allowed requests 10 s apart (TIME advanced in the mock, or TTL read after each call), the day key's TTL is the TTL of the first request minus 10 s, not 172,800 again. A key set beforehand with SET and no expiry gets a TTL after one allowed request. The same two checks for the spend key of RESERVE_SCRIPT and the non-block path of SETTLE_SCRIPT.
    • spend.test.ts (T4-a): reserve on day D at 23:59:59 UTC, reserve on day D+1 at 00:00:01, then a settle stamped D; the total of D+1 is unchanged. (T4-b): with a cap of 1e7 USD, a voter blocked by a settle cannot pass a reserve of 0; settleStep with a reserved amount above the block value returns a finite safe integer.
    • stores.test.ts (T3-b): chooseLimiter gives the Redis limiter for a Redis store, the memory limiter for a memory store and the "unavailable" limiter for no store (the three branches of today's code).
    • handler.test.ts (T4-d): a reserve-amount function that throws gives status 500, not 503 store_unavailable. (T4-f): a valid run with non-zero unread settles usage + unread; a Jev call that fails without a deadline settles usage only.
    • privacy.test.ts (T3-c): assert that the sentence about the counters is the exact sentence of today's page, instead of not.toContain("in memory") on the whole page.
    • Lua (T3-d): after each INCR or INCRBY, if redis.call('TTL', KEYS[n]) < 0 then redis.call('EXPIRE', KEYS[n], ARGV[ttl]) end. The key gets its life once, at its first write, and a key without a TTL gets one. Update the script comments.
    • Memory tracker (T4-a): totals keyed by voter + ":" + utcDate(now); drop dates older than the day before the newest date seen. Remove the midnight sentence from the spec 17.1 self-hosting row.
    • Clamps (T4-b, FR-1): clamp the cap to BLOCKED_MICRO_USD - 1; cap the reserved micro-dollars at BLOCKED_MICRO_USD in settleStep.
    • Handler (T4-d): compute costUsd, llmReserveUsage (once) and the Jev amount before the try; only the spend.reserve awaits stay inside it.
    • Wiring (T3-b, T4-e): move the limiter choice to lib/ops/stores.ts; one chooseStore call in productionDeps and one in the review route.
    • Comments (T4-g): replace the plan labels "(Task 4)" and "Task 5" in spend.ts, reserve.ts and security-headers.ts with the decision ids (A10, AP2, AP3; AP12 for the headers, or the id that the alpha plan gives).
    • JSON (T3-a): "rate_limited": "..." with a space after the colon (the text does not change).

8Task 5: Security policy and import guards in the tests (T5-a to T5-f)

Files:

  • Modify: replication_materials/tests/e2e/helpers/test.ts, replication_materials/tests/e2e/perf.spec.ts, replication_materials/tests/e2e/csp.spec.ts, replication_materials/tests/unit/zod-import.test.ts, replication_materials/tests/unit/isolation.test.ts

Interfaces:

  • Produces: guardedContext(browser: Browser, options?: BrowserContextOptions): Promise<BrowserContext> in helpers/test.ts: a new context with the same init script and the same CSP listeners as the default fixture. A violation in it fails the test as in the default context.
    • csp.spec.ts (T5-c): the worker path also has COOP same-origin and X-Robots-Tag noindex.
    • csp.spec.ts (T5-d): inside allowCspViolations(), a control test starts a worker from a blob URL whose script logs a message in the form of a CSP violation to the console; consoleHits catches it.
    • csp.spec.ts (T5-a): a page in a guardedContext that loads a blocked image fails the guard (run inside allowCspViolations() and assert the hit).
    • zod-import.test.ts (T5-e): the pattern also catches import("zod") and import( 'zod' ); scan scripts/ and next.config.ts too.

9Task 6: Review route (T6-b to T6-f)

Files:

  • Modify: lib/review/handler.ts, lib/review/github.ts, spec 14.4
  • Test: replication_materials/tests/unit/review/handler.test.ts, replication_materials/tests/unit/review/github.test.ts, replication_materials/tests/unit/review/questions.test.ts

Interfaces:

  • Produces: MAX_BODY_BYTES exported from lib/review/handler.ts and derived from the answer limits: Math.ceil((6 * (MAX_COMMENT_CHARS + MAX_NAME_CHARS) + 512) / 1024) * 1024 (= 25,600). JSON escapes a control character or a lone surrogate to 6 bytes; 512 bytes cover the keys, the longest question id and the choice. MAX_ISSUE_PAGES = 10 in github.ts (1,000 issues, the same bound as scripts/export-review.mjs).
    • handler.test.ts (T6-b): a GET whose GitHub list is held, then a POST that completes, then the held list returns; the next GET reads GitHub again (the old list was not cached).
    • handler.test.ts (T6-c): a good GET (warm cache), then a GET with a wrong key, gives 401.
    • handler.test.ts (T6-d): an answer with a 4,000-character comment of "\u0001" and a 100-character name of "\u0001" gets 201 (the stub accepts), and a body of MAX_BODY_BYTES + 1 bytes gets 400.
    • github.test.ts (T6-f): a stub that always returns 100 issues stops after MAX_ISSUE_PAGES pages and throws; the route answers 503 with REVIEW_MESSAGES.unavailable.
    • questions.test.ts (T6-e): the advice-word test also reads the glossary entry texts and the reference citations. If an existing text has an advice word, list it in an explicit allow list in the test with the entry key, and report it; do not change the text (it is under review).

10Task 7: Review page (T7-a to T7-e, T7-g, T7-h)

Files:

  • Modify: components/review/ReviewPage.tsx, components/review/QuestionCard.tsx, lib/review/answer.ts
  • Test: replication_materials/tests/unit/review/answer.test.ts, replication_materials/tests/unit/review/review-messages.test.ts, replication_materials/tests/e2e/review.spec.ts

Interfaces:

  • Produces: the failure result of checkAnswer gets field: "name" | "comment" | "choice" | null (T7-g); REVIEW_MESSAGES.filing = "Filing the answer." (T7-d). FileResult gets status?: number so the page can tell a rejected key (401) from other failures.
    • review-messages.test.ts: the exact-text table gets filing: "Filing the answer.".
    • answer.test.ts (T7-g): a missing name gives field: "name"; a missing comment for "change" gives field: "comment"; an unknown question gives field: null.
    • review.spec.ts (T7-a): POST answers 429, 502 and 503 each show their message under the card and announce it; after a POST error the focus is on the card's first control or its error text (not the body); the arrow keys move between the radios of one question; while the POST is held, the button has aria-disabled="true", a second click files nothing, and "Filing the answer." is shown and announced politely (T7-d).
    • review.spec.ts (T7-b): a POST that answers 401 removes the key: the controls go and the page shows "This page files answers only from the link with the review key." and the bad-key message.
    • review.spec.ts (T7-c): an error under a card goes when the user changes the choice or the comment.
    • review.spec.ts (T7-e): with the GET held, the progress line is not shown; it shows after the GET returns.
    • review.spec.ts (T7-h): when card B reports a missing name, the old error under card A goes; the 401-on-load test also asserts the announcement in the live region.

11Task 8: Test and document cleanups, and the record (T8-a, T9-d, T9-e, T9-f)

Files:

  • Modify: replication_materials/tests/e2e/describe.spec.ts, components/shell/Workspace.tsx, spec Sections 13.1, 13.2 and 14.4, replication_materials/process/process.json, scripts/build-spec-page.mjs, replication_materials/plans/2026-10-17-alpha-execution-ledger.md, the spec page files

12Execution notes

  • The controller merges by pull request and fast-forward after the author's approval of the merge, as for pull requests 1, 3 and 4. A push to main redeploys production.
  • After the deploy: site.spec with FFD_EXPECT_CSP=1 against https://expertadoe.app, and GET /api/review without a key (401). Task 4 also changed RESERVE_SCRIPT and SETTLE_SCRIPT, which run on every translation, and the free check runs only RATE_SCRIPT. So one smoke translation (about USD 0.05) after the deploy checks them; it needs the author's approval.