Pre-3B Review Changes
Plan · done on 2026-10-16

Implementation Plan, Milestone pre-3B

EXPERTA

Implementation plan for the changes from the review by Alan R. Vazquez, before Milestone 3B.

Plan
replication_materials/plans/2026-10-16-pre3b-review-changes-plan.md
Specification
replication_materials/plans/2026-10-14-ff-design-translator-design.md
Status
Done on 2026-10-16. Five tasks committed test first; the full check, the full catalogue sweep and a whole-branch review passed; merge into main pending the authors' decision.
Tasks
5, 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.

Goal: Apply the review by Alan R. Vazquez (2026-10-16) before Milestone 3B starts: the name EXPERTA, the number of clear 2FIs and the factors with all 2FIs clear in the report (with the full-factorial fix), the reply for required estimable interactions, the Dark2-based colours, and specification version 3.

Architecture: Five small, independent changes on one branch. The R report adds one field (all_2fis_clear) and takes the clear 2FIs of a full factorial from the definition, because a full factorial has no catalogue entry. The name lives in lib/site.ts; the colours live in app/globals.css; the scope reply lives in content/messages.json. The specification records the decisions as R18 to R23 and is published again.

Tech Stack: R 4.6.1 with FrF2 2.3-5 and DoE.base 1.2-5 (native tests with testthat), TypeScript with zod and vitest, Next.js 16 with Tailwind v4, Playwright, pandoc 3.12 for the specification page.

Spec: replication_materials/plans/2026-10-14-ff-design-translator-design.md (version 2 now; Task 5 makes version 3). Decisions of this plan: the answers of F. M. Megahed on 2026-10-16 (recorded in Task 5 as R18 to R23).

1Global Constraints

  • R code: every package function call is pkg::fun(); no library(); no function reads an object from the search path (spec 7.1, 7.2).
  • Native R tests and fixtures use R 4.6.1: "C:/Program Files/R/R-4.6.1/bin/Rscript.exe".
  • No live API calls. Do not change lib/translate/schema.ts, lib/translate/prompt.ts or lib/translate/jev-voter.ts: the model-facing schema, the prompts, the Jev questions and the recorded fixtures in replication_materials/tests/fixtures/translate/ stay as they are.
  • Fixed messages contain none of the advice words try, instead, recommend, consider, should (spec 11; ADVICE_WORDS in lib/translate/messages.ts).
  • Accessibility: WCAG 2.1 AA (spec 1.4 item 7). Text and white text on a coloured background need 4.5:1; chart marks need 3:1 on white (SC 1.4.11).
  • Documents: about 80% ASD-STE100, plain academic tone, formulas as LaTeX ($...$, rendered to MathML by pandoc).
  • Historical records keep the old name: ledgers, earlier plans and plan pages, mockups, findings, refs/.
  • Playwright specs cannot import modules that import package.json (shell.spec.ts keeps literal constants).
  • The repository is in Dropbox: pause Dropbox sync before npm install (EBUSY otherwise).
  • Each commit message ends with:
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01Wc7WXMFHs2SXBhwsTi2xnf

2Review Focus

  1. A full factorial, also with center points: the report must list every 2FI as clear and every factor in all_2fis_clear, and must not stop. Test in Task 1 (Step 1, design-reading and JSON tests).
  2. Exactly one factor with all 2FIs clear (32 runs, 9 factors: factor J): all_2fis_clear must stay a JSON array ["J"], not the string "J". Test in Task 1 (Step 1, JSON test).
  3. Factor names typed by the user (for example "Feed rate"): all_2fis_clear must show the typed name, not FrF2's Feed.rate or the letter. Test in Task 1 (Step 1, design-reading test).
  4. A component that still uses the old red tokens after the colour change: the page would lose its accent silently, because Tailwind drops unknown classes. Test in Task 4 (Step 1, brand-colours test).
  5. The new name in the browser title, the landing heading, the header subtitle and the workspace title. Test in Task 2 (Step 1, shell e2e).

3File Structure

File Change Task
r/ffd_report.R ffd_clear_positions(), ffd_clear_2fis(), ffd_all_2fis_clear(); report field all_2fis_clear; version 1.2.0 1 (line 1 comment: 2)
lib/report/types.ts schema field all_2fis_clear 1
replication_materials/tests/r/test-design-reading.R, test-report-json.R, test-catalogue-sweep.R tests 1
replication_materials/tests/fixtures/reports/*.json regenerated 1
replication_materials/tests/unit/report-types.test.ts, engine-client.test.ts, status-text.test.ts, replication_materials/tests/webr/engine.test.ts, replication_materials/tests/e2e/worker.spec.ts report version 1.2.0 1
r/README.md report parts list 1
lib/site.ts, app/layout.tsx, app/page.tsx, components/shell/SiteHeader.tsx name and subtitle 2
replication_materials/tests/unit/site.test.ts, replication_materials/tests/e2e/shell.spec.ts tests 2
scripts/build-plan-page.mjs, README.md name 2
content/messages.json, lib/translate/messages.ts, replication_materials/tests/unit/translate/messages.test.ts note for required estimable interactions 3
app/globals.css, app/page.tsx, components/shell/ProjectSidebar.tsx, components/shell/StepRail.tsx, public/brand/favicon.svg accent colour 4
replication_materials/tests/unit/brand-colours.test.ts new test 4
replication_materials/plans/2026-10-14-ff-design-translator-design.md version 3 5
replication_materials/plans/2026-10-16-spec-review-decisions-v3.md new decision log 5
replication_materials/findings/palette-check.mjs, palette-check-output.txt new measurement 5
replication_materials/process/process.json, scripts/build-spec-page.mjs, replication_materials/plans/spec-page/* process event, page defaults, rebuilt page 5

Task 2 and Task 4 both edit app/page.tsx, on different lines (Task 2: the heading; Task 4: the link class). Run them in order.

4Setup


5Task 1: Report: clear 2FIs, the factors with all 2FIs clear, and full factorials

Files:

  • Modify: r/ffd_report.R:9 (version), r/ffd_report.R:172-180 (helper), r/ffd_report.R:395 (report list)
  • Modify: lib/report/types.ts:36
  • Modify: replication_materials/tests/r/test-design-reading.R:96-108
  • Modify: replication_materials/tests/r/test-report-json.R:3-11 and :32
  • Modify: replication_materials/tests/r/test-catalogue-sweep.R:28-55 and :102
  • Modify: replication_materials/tests/unit/report-types.test.ts
  • Modify: version strings in replication_materials/tests/unit/engine-client.test.ts:7, replication_materials/tests/unit/status-text.test.ts:4, replication_materials/tests/webr/engine.test.ts:15, replication_materials/tests/e2e/worker.spec.ts:14
  • Regenerate: replication_materials/tests/fixtures/reports/coating.json, failed-8x4.json
  • Modify: r/README.md:27

Interfaces:

  • Consumes: ffd_display_map(d, labels) (named character vector, one display name per factor position), ffd_resolution(wlp)$full_factorial (logical).
  • Produces: ffd_clear_2fis(d, to_display, full_factorial) → character vector of "Name1:Name2"; ffd_all_2fis_clear(d, to_display, full_factorial) → character vector of display names; report field all_2fis_clear (JSON array of strings) right after clear_2fis; FFD_REPORT_VERSION <- "1.2.0"; ReportSchema field all_2fis_clear: z.array(z.string()). Milestone 3B shows report.clear_2fis.length as the number of clear 2FIs and report.all_2fis_clear as the factors with all 2FIs clear.

Background for the implementer: a 2FI is clear if no main effect and no other 2FI is aliased with it; interactions of three or more factors are not looked at. This is FrF2's definition: the FrF2 help for catlg says nclear.2fis is the "number of clear 2-factor interactions (i.e. free of aliasing with main effects or other 2-factor interactions)", clear.2fis is the 2 × nclear.2fis matrix of their factor positions, and all.2fis.clear is the "vector of factors with all 2-factor interactions clear". A full factorial has no aliasing at all, so every one of its (k2)\binom{k}{2} 2FIs is clear. FrF2 makes full factorials without a catalogue entry, so the present helper returns no clear 2FIs for them. That is the defect this task fixes.

In replication_materials/tests/r/test-design-reading.R, replace lines 96-108 (the tests "clear 2FIs come from the catalogue entry" and "a full factorial has no generators, aliases or clear 2FIs") with:

test_that("clear 2FIs and the factors with all 2FIs clear come from the catalogue entry", {
  d <- FrF2::FrF2(nruns = 32, nfactors = 9, randomize = FALSE)
  m <- ffd_display_map(d)
  expect_identical(ffd_clear_2fis(d, m, full_factorial = FALSE),
                   c("A:J", "B:J", "C:J", "D:J", "E:J", "F:J", "G:J", "H:J"))
  expect_identical(ffd_all_2fis_clear(d, m, full_factorial = FALSE), "J")
  coating <- ffd_coating(randomize = FALSE)
  expect_identical(ffd_all_2fis_clear(coating, ffd_display_map(coating), full_factorial = FALSE), character())
})

test_that("the factors with all 2FIs clear have the names that the user typed", {
  d <- FrF2::FrF2(nruns = 32, nfactors = 9, randomize = FALSE)
  labels <- c("Temp", "Pressure", "Speed", "Time", "Feed", "Depth", "Coolant", "Load", "Feed rate")
  expect_identical(ffd_all_2fis_clear(d, ffd_display_map(d, labels), full_factorial = FALSE), "Feed rate")
})

test_that("a full factorial has no generators or aliases, and all its 2FIs are clear", {
  d <- suppressMessages(FrF2::FrF2(nfactors = 4, resolution = 5, randomize = FALSE))
  m <- ffd_display_map(d)
  expect_identical(nrow(ffd_generators(d, m)), 0L)
  expect_identical(ffd_alias_chains(DoE.base::design.info(d)$aliased$fi2, m), character())
  expect_identical(ffd_clear_2fis(d, m, full_factorial = TRUE), c("A:B", "A:C", "A:D", "B:C", "B:D", "C:D"))
  expect_identical(ffd_all_2fis_clear(d, m, full_factorial = TRUE), c("A", "B", "C", "D"))
})

test_that("a design without a catalogue entry must be a full factorial", {
  d <- suppressMessages(FrF2::FrF2(nfactors = 4, resolution = 5, randomize = FALSE))
  expect_error(ffd_clear_2fis(d, ffd_display_map(d), full_factorial = FALSE),
               "clear 2FIs: the design has no catalogue entry and is not a full factorial", fixed = TRUE)
})

In replication_materials/tests/r/test-report-json.R, add this line after line 9 (expect_true(grepl('"clear_2fis":[]', js, fixed = TRUE))):

  expect_true(grepl('"all_2fis_clear":[]', js, fixed = TRUE))

change line 32 to:

  expect_identical(j$report_version, "1.2.0")

and add these two tests after the first test (the test "arrays stay arrays when they hold one element or none", which ends with expect_true(grepl('"ok":false', js, fixed = TRUE)) and })). If FrF2 refuses center points for the full factorial in the second test, stop and report it; do not drop the case.

test_that("the factors with all 2FIs clear stay an array with one element", {
  d <- FrF2::FrF2(nruns = 32, nfactors = 9, randomize = FALSE)
  js <- report_json(d, ffd_spec(9, nruns = 32))
  expect_true(grepl('"all_2fis_clear":["J"]', js, fixed = TRUE))
})

test_that("the report of a full factorial lists all 2FIs as clear, also with center points", {
  full <- suppressMessages(FrF2::FrF2(nfactors = 3, resolution = 5, randomize = FALSE))
  js <- report_json(full, ffd_spec(3, min_resolution = 5))
  expect_true(grepl('"clear_2fis":["A:B","A:C","B:C"],"all_2fis_clear":["A","B","C"]', js, fixed = TRUE))
  centred <- suppressMessages(FrF2::FrF2(nfactors = 3, resolution = 5, ncenter = 2, randomize = FALSE))
  r <- ffd_report(centred, ffd_spec(3, min_resolution = 5, center_points = 2))
  expect_identical(r$clear_2fis, c("A:B", "A:C", "B:C"))
  expect_identical(r$all_2fis_clear, c("A", "B", "C"))
})

In replication_materials/tests/r/test-catalogue-sweep.R, add this helper after clear_2fis_from_cube (after line 39):

# Factors whose 2FIs with every other factor are all in the "A:B" pairs, in factor order.
all_2fis_clear_from_pairs <- function(pairs, factors) {
  if (length(factors) < 2) return(character())
  ends <- unlist(strsplit(pairs, ":", fixed = TRUE))
  factors[vapply(factors, function(f) sum(ends == f) == length(factors) - 1, logical(1))]
}

replace the test at lines 52-55 ("the clear-2FI helper of the tests agrees with the catalogue for a clean entry") with:

test_that("the clear-2FI helpers of the tests agree with the report for a catalogue entry and a full factorial", {
  d <- FrF2::FrF2(nruns = 32, nfactors = 9, randomize = FALSE)
  cube_pairs <- clear_2fis_from_cube(ffd_cube_matrix(d))
  m <- ffd_display_map(d)
  expect_identical(sort(cube_pairs), sort(ffd_clear_2fis(d, m, full_factorial = FALSE)))
  expect_identical(all_2fis_clear_from_pairs(cube_pairs, colnames(ffd_cube_matrix(d))),
                   ffd_all_2fis_clear(d, m, full_factorial = FALSE))
  full <- suppressMessages(FrF2::FrF2(nfactors = 5, resolution = 6, randomize = FALSE))
  r <- ffd_report(full, ffd_spec(5, min_resolution = 6))
  expect_true(r$resolution$full_factorial)
  # desnum() names the columns of a full factorial A1, B1, ...: use the factor names instead.
  cube <- ffd_cube_matrix(full)
  colnames(cube) <- names(DoE.base::design.info(full)$factor.names)
  expect_identical(sort(r$clear_2fis), sort(clear_2fis_from_cube(cube)))
  expect_identical(r$all_2fis_clear, all_2fis_clear_from_pairs(r$clear_2fis, colnames(cube)))
})

and in the full sweep test, after line 102 (expect_identical(sort(r$clear_2fis), sort(clear_2fis_from_cube(ffd_cube_matrix(d))), info = info)), add:

    expect_identical(r$all_2fis_clear,
                     all_2fis_clear_from_pairs(r$clear_2fis, colnames(ffd_cube_matrix(d))), info = info)

Run: npm run test:r Expected: FAIL. The first failure names ffd_all_2fis_clear ("could not find function") or the argument full_factorial ("unused argument").

In r/ffd_report.R, change line 9 to:

FFD_REPORT_VERSION <- "1.2.0"

Replace lines 172-180 (the comment "# Clear two-factor interactions from the catalogue entry (pairs of factor positions)." and the function ffd_clear_2fis) with:

# Clear two-factor interactions (spec Section 7.5). A 2FI is clear if no main effect and no
# other 2FI is aliased with it; interactions of three or more factors are not looked at
# (FrF2's definition; hierarchy principle, Section 1.8). A catalogue design takes the values
# that its catalogue entry stores (clear.2fis and all.2fis.clear, as factor positions). A full
# factorial has no catalogue entry and no aliasing, so each of its 2FIs is clear (R23).
# Returns list(pairs = a 2-row matrix of positions, one column per clear 2FI;
#              factors = the positions of the factors with all 2FIs clear).
ffd_clear_positions <- function(d, full_factorial) {
  info <- DoE.base::design.info(d)
  none <- matrix(integer(), nrow = 2)
  entry <- info$catlg.entry
  if (!is.null(entry) && length(entry) > 0) {
    pairs <- entry[[1]]$clear.2fis
    pairs <- if (length(pairs) == 0) none else matrix(as.integer(pairs), nrow = 2)
    return(list(pairs = pairs, factors = as.integer(entry[[1]]$all.2fis.clear)))
  }
  if (!isTRUE(full_factorial)) {
    stop("clear 2FIs: the design has no catalogue entry and is not a full factorial", call. = FALSE)
  }
  k <- info$nfactors
  if (k < 2) return(list(pairs = none, factors = integer()))
  list(pairs = utils::combn(k, 2), factors = seq_len(k))
}

# Clear 2FIs as "Name1:Name2", with the display names.
ffd_clear_2fis <- function(d, to_display, full_factorial) {
  pairs <- ffd_clear_positions(d, full_factorial)$pairs
  if (ncol(pairs) == 0) return(character())
  apply(pairs, 2, function(p) paste(to_display[p], collapse = ":"))
}

# Display names of the factors with all 2FIs clear.
ffd_all_2fis_clear <- function(d, to_display, full_factorial) {
  unname(to_display[ffd_clear_positions(d, full_factorial)$factors])
}

In ffd_report(), replace the line clear_2fis = ffd_clear_2fis(d, to_display), with:

    clear_2fis = ffd_clear_2fis(d, to_display, resolution$full_factorial),
    all_2fis_clear = ffd_all_2fis_clear(d, to_display, resolution$full_factorial),

ffd_report_json() needs no change: with auto_unbox = FALSE, a character vector stays an array.

In r/README.md line 27, change `clear_2fis`, `run_sheet` to `clear_2fis`, `all_2fis_clear`, `run_sheet`.

Run: npm run test:r Expected: PASS (the full sweep tests are skipped without FFD_FULL_CATALOGUE=1).

In replication_materials/tests/unit/report-types.test.ts, replace the first test with:

  it("accepts a passing report", () => {
    const report = ReportSchema.parse(read("coating"));
    expect(report.ok).toBe(true);
    expect(report.catalogue.entry).toBe("7-3.1");
    expect(report.run_sheet.levels).toHaveLength(16);
    expect(report.clear_2fis).toEqual([]);
    expect(report.all_2fis_clear).toEqual([]);
    expect(report.versions.report).toBe("1.2.0");
  });

and add this test after "refuses a report whose arrays were unboxed":

  it("refuses a report without the factors with all 2FIs clear", () => {
    const report = read("coating");
    delete report.all_2fis_clear;
    expect(ReportSchema.safeParse(report).success).toBe(false);
  });

Run: "C:/Program Files/R/R-4.6.1/bin/Rscript.exe" replication_materials/tests/fixtures/make-report-fixtures.R Then run: npx vitest run --project unit replication_materials/tests/unit/report-types.test.ts Expected: FAIL in "accepts a passing report" (the schema drops the unknown field, so all_2fis_clear is undefined after the parse) and in "refuses a report without the factors with all 2FIs clear". Check git diff --stat replication_materials/tests/fixtures/reports: both files change; the only differences are "all_2fis_clear":[] after "clear_2fis":[] and 1.1.0 → 1.2.0 (two places per file).

In lib/report/types.ts, after line 36 ( clear_2fis: z.array(z.string()),), add:

  all_2fis_clear: z.array(z.string()),

Change report: "1.1.0" to report: "1.2.0" in:

  • replication_materials/tests/unit/engine-client.test.ts:7
  • replication_materials/tests/unit/status-text.test.ts:4
  • replication_materials/tests/webr/engine.test.ts:15
  • replication_materials/tests/e2e/worker.spec.ts:14

Run: npm test then npm run typecheck Expected: PASS, no type errors.

git add r/ffd_report.R r/README.md lib/report/types.ts replication_materials/tests/r replication_materials/tests/fixtures/reports replication_materials/tests/unit/report-types.test.ts replication_materials/tests/unit/engine-client.test.ts replication_materials/tests/unit/status-text.test.ts replication_materials/tests/webr/engine.test.ts replication_materials/tests/e2e/worker.spec.ts
git commit -m "feat: report the factors with all 2FIs clear, and all 2FIs of a full factorial as clear (R21, R23)"

(with the two trailer lines of the Global Constraints)


6Task 2: Name: EXPERTA, with the subtitle

Files:

  • Modify: lib/site.ts:3-12
  • Modify: app/layout.tsx:3,10
  • Modify: app/page.tsx:6,16
  • Modify: components/shell/SiteHeader.tsx:1,13
  • Modify: replication_materials/tests/unit/site.test.ts
  • Modify: replication_materials/tests/e2e/shell.spec.ts:4-25
  • Modify: r/ffd_report.R:1
  • Modify: scripts/build-plan-page.mjs:69,81,82
  • Modify: README.md:1,11-13

Interfaces:

  • Produces: APP_NAME = "EXPERTA", APP_SUBTITLE = "Fractional Factorial Design Translator", APP_FULL_NAME = "EXPERTA: Fractional Factorial Design Translator" from lib/site.ts. Milestone 3B and 3C use these constants for any text that names the tool (exports, report, privacy page).

In replication_materials/tests/unit/site.test.ts, change the import on line 3 to:

import { APP_FULL_NAME, APP_NAME, APP_SUBTITLE, APP_VERSION, CITATION, MAINTAINERS } from "@/lib/site";

replace the test "spells out the working title (spec 9.8)" with:

  it("uses the name of review decision R18, with the subtitle (spec 9.8)", () => {
    expect(APP_NAME).toBe("EXPERTA");
    expect(APP_SUBTITLE).toBe("Fractional Factorial Design Translator");
    expect(APP_FULL_NAME).toBe("EXPERTA: Fractional Factorial Design Translator");
  });

and in "names both maintainers with their middle initials", replace the CITATION line with:

    expect(CITATION).toContain("Vazquez, A. R., and Megahed, F. M. (2026). EXPERTA: Fractional Factorial Design Translator (version ");

In replication_materials/tests/e2e/shell.spec.ts, replace lines 4-6 with:

// Not imported from lib/site: it imports package.json, which Playwright cannot load without
// an import attribute (Global Constraints). site.test.ts checks the same values.
const APP_NAME = "EXPERTA";
const APP_SUBTITLE = "Fractional Factorial Design Translator";

replace the first two lines of the body of "the first visit shows the landing page" (await page.goto("/"); stays; the toHaveTitle and heading lines change) so that the test starts:

test("the first visit shows the landing page", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveTitle(`${APP_NAME}: ${APP_SUBTITLE}`);
  await expect(page.getByRole("heading", { level: 1, name: APP_NAME, exact: true })).toBeVisible();
  await expect(page.getByRole("main").getByText(APP_SUBTITLE, { exact: true })).toBeVisible();
  await expect(page.getByRole("banner").getByText(APP_SUBTITLE, { exact: true })).toBeVisible();

(the rest of that test stays). The test "Start a design opens the workspace" keeps `Workspace | ${APP_NAME}`, which now reads "Workspace | EXPERTA".

Run: npx vitest run --project unit replication_materials/tests/unit/site.test.ts Expected: FAIL (APP_SUBTITLE and APP_FULL_NAME are not exported; APP_NAME is the old name).

Replace lines 3-12 of lib/site.ts with:

// Names and texts that the whole app uses (spec Sections 9.2, 9.7 and 9.8; review decision R18).
export const APP_NAME = "EXPERTA";
export const APP_SUBTITLE = "Fractional Factorial Design Translator";
export const APP_FULL_NAME = `${APP_NAME}: ${APP_SUBTITLE}`;
export const APP_VERSION: string = pkg.version;

export const MAINTAINERS = [
  { name: "Alan R. Vazquez", affiliation: "Tecnológico de Monterrey", url: "https://research.tec.mx/vivo-tec/display/PID_115457" },
  { name: "Fadel M. Megahed", affiliation: "Miami University", url: "https://miamioh.edu/fsb/directory/?up=/directory/megahefm" },
] as const;

export const CITATION = `Vazquez, A. R., and Megahed, F. M. (2026). ${APP_FULL_NAME} (version ${APP_VERSION}) [Computer software].`;

In app/layout.tsx, change line 3 to import { APP_FULL_NAME, APP_NAME } from "@/lib/site"; and line 10 to:

  title: { default: APP_FULL_NAME, template: `%s | ${APP_NAME}` },

In app/page.tsx, change line 6 to import { APP_NAME, APP_SUBTITLE, CITATION, MAINTAINERS } from "@/lib/site"; and replace line 16 (<h1 className="font-display text-4xl">{APP_NAME}</h1>) with:

        <h1 className="font-display text-4xl">{APP_NAME}</h1>
        <p className="mt-1 font-display text-2xl">{APP_SUBTITLE}</p>

In components/shell/SiteHeader.tsx, change line 1 to import { APP_NAME, APP_SUBTITLE } from "@/lib/site"; and replace line 13 (<p className="font-display text-xl font-semibold">{APP_NAME}</p>) with:

        <div>
          <p className="font-display text-xl font-semibold">{APP_NAME}</p>
          <p className="text-sm text-tan-dark">{APP_SUBTITLE}</p>
        </div>

In r/ffd_report.R, change line 1 to:

# Report helper of EXPERTA, the Fractional Factorial Design Translator (spec Section 7).

In scripts/build-plan-page.mjs, change line 69 to:

const page = `# EXPERTA: Implementation Plan, Milestone ${MILESTONE}\n\n${meta}\n\n---\n\n${body}`;

and in lines 81-82 change both default texts to `Implementation plan for Milestone ${MILESTONE} of EXPERTA, the Fractional Factorial Design Translator.`.

In README.md, change line 1 to # EXPERTA: Fractional Factorial Design Translator and replace the paragraph under ## Status with:

Milestone 3A of Phase 1 is complete: the translation service (`POST /api/translate`) with three language models and TypeSafe Jev. The interface for the translation step and the design view come in Milestone 3B.

Run: npm test then npm run typecheck Expected: PASS. Run: npx playwright test replication_materials/tests/e2e/shell.spec.ts Expected: PASS (9 tests).

git add lib/site.ts app/layout.tsx app/page.tsx components/shell/SiteHeader.tsx r/ffd_report.R scripts/build-plan-page.mjs README.md replication_materials/tests/unit/site.test.ts replication_materials/tests/e2e/shell.spec.ts
git commit -m "feat: name the tool EXPERTA, with the subtitle Fractional Factorial Design Translator (R18)"

7Task 3: Reply for required estimable interactions

Files:

  • Modify: content/messages.json (new key feature_notes after features)
  • Modify: lib/translate/messages.ts:30-34
  • Modify: replication_materials/tests/unit/translate/messages.test.ts

Interfaces:

  • Consumes: NOT_SUPPORTED_CATEGORIES, NotSupportedCategory from lib/translate/schema.ts (unchanged).
  • Produces: scopeMessage("estimable_interactions") returns the not-supported sentence plus the note. All other categories return the same text as before.

In replication_materials/tests/unit/translate/messages.test.ts, add after the test "gives the two scope templates of spec 5.1":

  it("adds the note for required estimable interactions, and only for them (review decision R22)", () => {
    expect(scopeMessage("estimable_interactions")).toBe(
      "This version does not support required estimable interactions. Step 3 lists the clear two-factor interactions (2FIs). To change which factors have them, change the factor order in step 2.",
    );
    for (const c of NOT_SUPPORTED_CATEGORIES.filter((c) => c !== "estimable_interactions")) {
      expect(scopeMessage(c), c).toBe(`This version does not support ${MESSAGES.features[c]}.`);
    }
  });

Run: npx vitest run --project unit replication_materials/tests/unit/translate/messages.test.ts Expected: FAIL (the message has no note).

In content/messages.json, add this key after the features object (keep the file's indentation):

  "feature_notes": {
    "estimable_interactions": "Step 3 lists the clear two-factor interactions (2FIs). To change which factors have them, change the factor order in step 2."
  }

In lib/translate/messages.ts, replace the function scopeMessage and its comment with:

/** The fixed message for an out-of-scope or not-supported request (spec 5.1). A feature can have
 *  a note that names the Phase 1 control for the same need (review decision R22). */
export function scopeMessage(category: ScopeCategory): string {
  if (isOutOfScope(category)) return fill(MESSAGES.out_of_scope, { category: MESSAGES.categories[category] });
  const feature = category as NotSupportedCategory;
  const text = fill(MESSAGES.not_supported_in_version, { feature: MESSAGES.features[feature] });
  const note = (MESSAGES.feature_notes as Partial<Record<NotSupportedCategory, string>>)[feature];
  return note ? `${text} ${note}` : text;
}

Run: npm test then npm run typecheck Expected: PASS. The existing test "contains no advice word in any message" also checks the new note.

git add content/messages.json lib/translate/messages.ts replication_materials/tests/unit/translate/messages.test.ts
git commit -m "feat: point a request for required estimable interactions to the clear 2FIs and the factor order (R22)"

8Task 4: Interface accent from the Dark2 purple

Files:

  • Create: replication_materials/tests/unit/brand-colours.test.ts
  • Modify: app/globals.css:3-6,24
  • Modify: app/page.tsx:23
  • Modify: components/shell/ProjectSidebar.tsx:42,54,80,85
  • Modify: components/shell/StepRail.tsx:20
  • Modify: public/brand/favicon.svg:8

Interfaces:

  • Produces: Tailwind tokens --color-accent (#625d96) and --color-accent-dark (#4c4977), used as bg-accent, border-accent, hover:bg-accent-dark. The tokens --color-miami-red and --color-accent-red are removed. Milestone 3B uses accent and accent-dark for every accent.

The values come from measurement (Task 5 commits the script): the Dark2 purple #7570B3 has 4.44:1 on white, 4.22:1 on warm white and 3.75:1 on light tan, so it fails AA for text. #625d96 is the lightest shade of the same hue with 5:1 on light tan (5.96:1 on white, 5.66:1 on warm white). #4c4977 has 7:1 on light tan and 8.31:1 on white.

Create replication_materials/tests/unit/brand-colours.test.ts:

import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";

// Interface colours (spec 9.8, review decision R19; WCAG 2.1 AA, spec 9.10). The values come
// from app/globals.css, so a later colour change must pass the same checks.
const css = readFileSync("app/globals.css", "utf8");

function token(name: string): string {
  const m = new RegExp(`--color-${name}:\\s*(#[0-9a-fA-F]{6})`).exec(css);
  if (!m) throw new Error(`No token --color-${name} in app/globals.css`);
  return m[1].toLowerCase();
}

function luminance(hex: string): number {
  const [r, g, b] = [1, 3, 5].map((i) => {
    const c = parseInt(hex.slice(i, i + 2), 16) / 255;
    return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
  });
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function contrast(a: string, b: string): number {
  const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
  return (hi + 0.05) / (lo + 0.05);
}

function sourceFiles(dir: string): string[] {
  return readdirSync(dir, { recursive: true, encoding: "utf8" })
    .filter((f) => /\.(tsx?|css)$/.test(f))
    .map((f) => join(dir, f));
}

describe("interface colours (spec 9.8)", () => {
  it("uses the Dark2 purple, darkened for text, as the accent (R19)", () => {
    expect(token("accent")).toBe("#625d96");
    expect(token("accent-dark")).toBe("#4c4977");
  });
  it("gives the accent at least 4.5:1 on every page background", () => {
    for (const bg of ["#ffffff", token("warm-white"), token("tan-light")]) {
      expect(contrast(token("accent"), bg), bg).toBeGreaterThanOrEqual(4.5);
    }
  });
  it("gives white text at least 4.5:1 on the accent and on its hover colour", () => {
    for (const c of [token("accent"), token("accent-dark")]) {
      expect(contrast("#ffffff", c), c).toBeGreaterThanOrEqual(4.5);
    }
  });
  it("has no component that uses the removed red tokens", () => {
    for (const file of [...sourceFiles("app"), ...sourceFiles("components")]) {
      expect(readFileSync(file, "utf8"), file).not.toMatch(/miami-red|accent-red/);
    }
  });
  it("draws the favicon corners in the accent", () => {
    const svg = readFileSync("public/brand/favicon.svg", "utf8").toLowerCase();
    expect(svg).toContain(`fill="${token("accent")}"`);
    expect(svg).not.toContain("#c41230");
  });
});

Run: npx vitest run --project unit replication_materials/tests/unit/brand-colours.test.ts Expected: FAIL ("No token --color-accent in app/globals.css").

In app/globals.css, replace lines 3-6 with:

/* Interface tokens (spec Section 9.8, review decision R19). The accent is the Dark2 purple
   #7570B3, darkened to pass WCAG AA for text. The tans, the ink and the warm white come from
   ChatISA. Light theme only in Phase 1. */
@theme {
  --color-accent: #625d96;
  --color-accent-dark: #4c4977;

and change the outline line in :focus-visible to outline: 2px solid var(--color-accent);.

Change the classes:

  • app/page.tsx:23: bg-miami-red px-5 py-3 font-semibold text-white hover:bg-accent-red → bg-accent px-5 py-3 font-semibold text-white hover:bg-accent-dark
  • components/shell/ProjectSidebar.tsx:42: bg-miami-red → bg-accent and hover:bg-accent-red → hover:bg-accent-dark
  • components/shell/ProjectSidebar.tsx:54 and :80: border-miami-red → border-accent
  • components/shell/ProjectSidebar.tsx:85: bg-miami-red → bg-accent
  • components/shell/StepRail.tsx:20: border-miami-red bg-miami-red → border-accent bg-accent

In public/brand/favicon.svg, change line 8 <g fill="#c41230"> to <g fill="#625d96">.

Run: npm test then npm run typecheck Expected: PASS. Run: npx playwright test replication_materials/tests/e2e/shell.spec.ts Expected: PASS.

git add app/globals.css app/page.tsx components/shell/ProjectSidebar.tsx components/shell/StepRail.tsx public/brand/favicon.svg replication_materials/tests/unit/brand-colours.test.ts
git commit -m "feat: take the interface accent from the Dark2 purple, darkened for AA contrast (R19)"

9Task 5: Specification version 3, decision log, palette check and process event

Files:

  • Create: replication_materials/findings/palette-check.mjs, replication_materials/findings/palette-check-output.txt
  • Create: replication_materials/plans/2026-10-16-spec-review-decisions-v3.md
  • Modify: replication_materials/plans/2026-10-14-ff-design-translator-design.md (the edits in Step 3)
  • Modify: replication_materials/process/process.json
  • Modify: scripts/build-spec-page.mjs:36,38,62
  • Rebuild: replication_materials/plans/spec-page/index.html, artifact.html

Interfaces:

  • Consumes: the decisions of Tasks 1-4 (R18 to R23 below).
  • Produces: specification version 3, which the Milestone 3B plan argues from.

Create replication_materials/findings/palette-check.mjs:

// Colour checks behind spec Section 9.8 (review decision R19). Run from the repository root:
//   node replication_materials/findings/palette-check.mjs > replication_materials/findings/palette-check-output.txt
// 1. WCAG 2.1 contrast: the interface accent on the page backgrounds (text, 4.5:1) and the
//    chart colours on the white plot surface (graphics, 3:1, SC 1.4.11).
// 2. Separation of chart series: OKLab distance x 100 (Ottosson 2020), the worst case of normal
//    vision and protanopia, deuteranopia and tritanopia at severity 1 (Machado et al. 2009).
const DARK2 = { teal: "#1B9E77", orange: "#D95F02", purple: "#7570B3", magenta: "#E7298A",
                green: "#66A61E", mustard: "#E6AB02", brown: "#A6761D", gray: "#666666" };
const CHOSEN = ["purple", "orange", "teal", "gray"];
const PAGE = { white: "#ffffff", "warm white": "#faf9f7", "light tan": "#edece2" };
const ACCENT = { "Dark2 purple": "#7570B3", accent: "#625d96", "accent dark": "#4c4977" };
const CVD = {
  normal: [[1, 0, 0], [0, 1, 0], [0, 0, 1]],
  protanopia: [[0.152286, 1.052583, -0.204868], [0.114503, 0.786281, 0.099216], [-0.003882, -0.048116, 1.051998]],
  deuteranopia: [[0.367322, 0.860646, -0.227968], [0.280085, 0.672501, 0.047413], [-0.01182, 0.04294, 0.968881]],
  tritanopia: [[1.255528, -0.076749, -0.178779], [-0.078411, 0.930809, 0.147602], [0.004733, 0.691367, 0.3039]],
};
const lin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
const rgb = (hex) => [1, 3, 5].map((i) => lin(parseInt(hex.slice(i, i + 2), 16) / 255));
const luminance = (hex) => { const [r, g, b] = rgb(hex); return 0.2126 * r + 0.7152 * g + 0.0722 * b; };
const contrast = (a, b) => { const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x); return (hi + 0.05) / (lo + 0.05); };
const apply = (m, v) => m.map((row) => Math.min(1, Math.max(0, row[0] * v[0] + row[1] * v[1] + row[2] * v[2])));
function oklab([r, g, b]) {
  const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
  const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
  const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
  return [0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
          1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
          0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s];
}
const distance = (a, b) => Math.min(...Object.values(CVD).map((m) => {
  const [p, q] = [oklab(apply(m, rgb(a))), oklab(apply(m, rgb(b)))];
  return 100 * Math.hypot(p[0] - q[0], p[1] - q[1], p[2] - q[2]);
}));
const smallest = (hexes) => { let v = Infinity; for (let i = 0; i < hexes.length; i++) for (let j = i + 1; j < hexes.length; j++) v = Math.min(v, distance(hexes[i], hexes[j])); return v; };
const prefixes = (names) => names.slice(1).map((_, i) => smallest(names.slice(0, i + 2).map((n) => DARK2[n])).toFixed(1));

console.log("Interface accent: contrast on the page backgrounds (text needs 4.5:1)");
for (const [name, hex] of Object.entries(ACCENT)) {
  console.log(`  ${name.padEnd(13)} ${hex}  ` + Object.entries(PAGE).map(([p, bg]) => `${p} ${contrast(hex, bg).toFixed(2)}`).join("  "));
}
console.log("\nDark2 colours: contrast on white (chart marks need 3:1)");
for (const [name, hex] of Object.entries(DARK2)) {
  const c = contrast(hex, "#ffffff");
  console.log(`  ${name.padEnd(8)} ${hex}  ${c.toFixed(2)}  ${c >= 3 ? "pass" : "fail"}`);
}
console.log("\nSmallest separation of the first 2, 3, ... series (OKLab x 100, worst vision type)");
console.log(`  ColorBrewer order: ${prefixes(Object.keys(DARK2)).join("  ")}`);
console.log(`  Chosen order (${CHOSEN.join(", ")}): ${prefixes(CHOSEN).join("  ")}`);
const names = Object.keys(DARK2);
const pairs = [];
for (let i = 0; i < names.length; i++) for (let j = i + 1; j < names.length; j++) pairs.push([distance(DARK2[names[i]], DARK2[names[j]]), `${names[i]}-${names[j]}`]);
pairs.sort((a, b) => a[0] - b[0]);
console.log(`\nThe four closest Dark2 pairs: ${pairs.slice(0, 4).map(([v, n]) => `${n} ${v.toFixed(1)}`).join(", ")}`);

Run: node replication_materials/findings/palette-check.mjs > replication_materials/findings/palette-check-output.txt Expected output values (check them in the file): Dark2 purple 4.44 / 4.22 / 3.75; accent 5.96 / 5.66 / 5.00; accent dark 8.31 / 7.90 / 7.00; green 2.98 fail; mustard 2.06 fail; all other Dark2 colours pass; ColorBrewer order 11.6 10.7 1.7 1.7 1.7 1.7 1.7; chosen order 21.4 10.7 7.2; closest pairs start with teal-magenta 1.7, orange-brown 1.8. If a value differs, stop and report it: the specification text in Step 3 quotes these values.

Create replication_materials/plans/2026-10-16-spec-review-decisions-v3.md:

# Spec review: decision log, version 3 (2026-10-16)

This log records the decisions from the review of the specification page by Alan R. Vazquez. He marked his changes on a copy of the page in blue and sent it as a PDF on 2026-10-16. The PDF is not in the repository. This log and the process record (`replication_materials/process/process.json`, event e21, shown in the process figure of the specification page) record each change and how the authors decided it. Fadel M. Megahed and Claude sorted the changes by their risk to the merged work (Milestones 1 to 3A) and measured the colours. Specification version 3 applies R18 to R23.

## Decisions

| # | Topic | Decision | Source |
|---|---|---|---|
| R18 | Name | The tool is EXPERTA, with the subtitle "Fractional Factorial Design Translator". The full name "EXPERTA: Fractional Factorial Design Translator" is in the browser title and the citation. Constants `APP_NAME`, `APP_SUBTITLE` and `APP_FULL_NAME` in `lib/site.ts`. Replaces R7. | Alan R. Vazquez (PDF: "Name of the application: EXPERTA"); F. M. Megahed chose the subtitle form. |
| R19 | Colours | Charts: the Dark2 colours purple `#7570B3`, orange `#D95F02`, teal `#1B9E77` and gray `#666666`, in this order, at most four. Interface accent: `#625d96` (the Dark2 purple, darkened to pass WCAG AA for text), hover `#4c4977`. The favicon and the video title-card rule use the accent. Replaces the Miami Red accent and the ChatISA chart palette. | Alan R. Vazquez (PDF, Section 9.8: purple, the combination of Miami red and Tec blue); F. M. Megahed: purple in the interface too, then Dark2 after the measurement (`replication_materials/findings/palette-check-output.txt`). |
| R20 | Principles | New Section 1.8: effect hierarchy, effect sparsity and effect heredity (strong and weak). The Phase 1 report follows the hierarchy principle. Phase 2 uses sparsity and heredity in the analysis. | Alan R. Vazquez (PDF: "General"). |
| R21 | Clear 2FIs | The report shows the number of clear 2FIs and the factors with all 2FIs clear, as FrF2 does (`nclear.2fis`, `clear.2fis`, `all.2fis.clear`). This is a measure only: minimum aberration stays the selection rule, and requests for MaxC2 stay "not supported". Estimable 2FIs move from Phase 3 to "Later (not planned)". | Alan R. Vazquez (PDF: Sections 1.1, 1.5, 2.4 D4 and 9.4); F. M. Megahed: "Measure only", "Later (not planned)". |
| R22 | Reply for required estimable interactions | The not-supported message adds: "Step 3 lists the clear two-factor interactions (2FIs). To change which factors have them, change the factor order in step 2." The scope rule against alternatives (D1, Section 1.6) applies to out-of-scope requests; this request is in scope. | F. M. Megahed: "Point to factor order". |
| R23 | Clear 2FIs of a full factorial | All $\binom{k}{2}$ 2FIs of a full factorial are clear, and all its factors have all 2FIs clear. Before this decision the report gave none, because a full factorial has no catalogue entry. The independent check of the tests (`clear_2fis_from_cube`) already counted them as clear. | Found during the review. Alan R. Vazquez confirms the definition (Section 17.1). |

## Facts from the review that need no decision

- The translation prompts do not contain the app name. R18 changes no prompt hash, no recorded fixture and not the 40 s deadline.
- `estimable_interactions` and `alternative_criterion` stay in the model-facing schema (Section 5.3). Only the message text changes (R22).
- FrF2's help says that `MaxC2=TRUE` "is not a recommended choice" (`FrF2()`, argument `MaxC2`).
- For blocked designs (Phase 3), FrF2 counts a 2FI as clear only if no block main effect is aliased with it either. Phase 3 takes the value from FrF2's blocked-design output, not from the Phase 1 rule.
- ChatISA's chart orange `#FF7436` has 2.69:1 on white and fails the 3:1 rule for chart marks.

## Open in this review

- Alan R. Vazquez confirms R23, and the Dark2 purple of R19 (it leans to blue-violet).
- References for Section 1.8.
- A check of the name EXPERTA for conflicts before the public release (Section 17.1).

Make these edits in replication_materials/plans/2026-10-14-ff-design-translator-design.md. Each edit quotes the present text; replace it with the new text.

  1. Line 1: # Fractional Factorial Design Translator: Design Specification → # EXPERTA: Design Specification

  2. Header rows:

    • | Working title | Fractional Factorial Design Translator. The final name is open (Section 17). | → | Name | EXPERTA: Fractional Factorial Design Translator (review decision R18, 2026-10-16). |
    • | Version | 2, dated 2026-10-15. Version 1 is dated 2026-10-14. Version 2 applies the review decisions R1 to R17. | → | Version | 3, dated 2026-10-16. Version 2 is dated 2026-10-15 and version 1 is dated 2026-10-14. Version 2 applies the review decisions R1 to R17. Version 3 applies R18 to R23, from the review by Alan R. Vazquez. |
    • In the Drafted with row, Claude Code 2.1.299 (version 1) and 2.1.300 (version 2), model Claude Opus 5.5 (claude-opus-5-5), effort xhigh → Claude Code 2.1.299 (version 1), 2.1.300 (version 2) and 2.1.302 (version 3), model Claude Opus 5.5 (claude-opus-5-5), effort xhigh for versions 1 and 2
    • | Decision log | [2026-10-14-spec-review-decisions.md](2026-10-14-spec-review-decisions.md) | → | Decision logs | [2026-10-14-spec-review-decisions.md](2026-10-14-spec-review-decisions.md) (R1 to R17) and [2026-10-16-spec-review-decisions-v3.md](2026-10-16-spec-review-decisions-v3.md) (R18 to R23) |
  3. Section 1.1, third bullet: - The tool shows the design table, the resolution, the word length pattern (WLP) and the minimum aberration (MA) status. → - The tool shows the design table, the resolution, the word length pattern (WLP), the minimum aberration (MA) status, the number of clear two-factor interactions (2FIs) and the factors that have all 2FIs clear (if any).

  4. Section 1.5:

    • Phase 3 row: | 3 | Blocking, split-plot and hard-to-change factors, estimable two-factor interactions (2FIs), user-given generators, choice of fraction, and foldover as an action on a saved project ("Augment this design"). Each family gets its own benchmark slice. | → | 3 | Blocking, split-plot and hard-to-change factors, user-given generators, choice of fraction, and foldover as an action on a saved project ("Augment this design"). Each family gets its own benchmark slice. |
    • Later row: after ...need different theory (partial aliasing, generalized resolution) and a new benchmark slice. add Estimable 2FIs (a list of 2FIs that the design must keep estimable): review decision R21 moves them here from Phase 3. In Phase 1, the clear 2FIs in the report and the factor order (Section 9.6) give the user part of this control. (inside the same table cell).
  5. Section 1.6: after the bullet - If a request is a two-level fractional factorial feature that this version does not support, the tool says "This version does not support {feature}." add the bullet - For required estimable interactions, the message adds: "Step 3 lists the clear two-factor interactions (2FIs). To change which factors have them, change the factor order in step 2." (R22). This request is in scope, so the first rule does not apply to it.

  6. New Section 1.8, inserted after Section 1.7 (before the --- line that comes before ## 2. How this specification was made):

    ### 1.8 Principles of two-level fractional factorial designs
    
    Two-level fractional factorial designs add three principles to the classical principles of experimentation (randomization, blocking and replication). The text comes from Alan R. Vazquez (review decision R20).
    
    - **Effect hierarchy.** Low-order effects are more likely to be active than high-order effects. Main effects and 2FIs are usually more important than interactions of three or more factors. Thus, the evaluation of a two-level fractional factorial design usually does not look at interactions of three or more factors.
    - **Effect sparsity.** Only a few effects (main effects or 2FIs) are active. An active effect has a larger absolute size than the noise.
    - **Effect heredity.** This principle applies only to interactions. Strong heredity: an interaction is more likely to be active if the main effects of all its factors are active. Weak heredity: an interaction is more likely to be active if the main effect of at least one of its factors is active.
    
    Published studies show that these principles hold in practice (references: Section 17.1).
    
    How the tool uses the principles:
    
    - Phase 1 follows the hierarchy principle, as FrF2 does. The report shows the aliasing among main effects and 2FIs, the number of clear 2FIs and the factors with all 2FIs clear (Section 7.5). The WLP still counts words of every length (Section 7.6), because minimum aberration is defined on the full WLP.
    - Phase 2 uses sparsity and heredity in the analysis. Lenth's method and the half-normal plot assume effect sparsity. The alias-aware linear model can use heredity to select among aliased 2FIs. The Phase 2 plan sets the rules.
    - The glossary (Section 9.5) has entries for effect hierarchy, effect sparsity, effect heredity and clear 2FI.
  7. Section 2.4:

    • D4 row: | D4 | Staging of design families | Core first (Phase 1). Then analysis (Phase 2). Then blocking, split-plot, estimable 2FIs and foldover (Phase 3). Plackett-Burman later. | → | D4 | Staging of design families | Core first (Phase 1). Then analysis (Phase 2). Then blocking, split-plot and foldover (Phase 3). Plackett-Burman later. R21 moves estimable 2FIs to Later. |
    • R7 row: | R7 | Name | "Fractional Factorial Design Translator", spelled out, until the team selects a final name. | 9.8 | → | R7 | Name | "Fractional Factorial Design Translator", spelled out, until the team selects a final name. Replaced by R18. | 9.8 |
    • R16 row: | R16 | Video title cards | Warm-white field, Miami Red rule, both logos with equal weight. | 10.3 | → | R16 | Video title cards | Warm-white field, Miami Red rule, both logos with equal weight. R19 changes the rule to the accent colour. | 10.3 |
    • After the R17 row and before the --- line, add:
    
    Review decisions (version 3), from the review by Alan R. Vazquez on 2026-10-16. The decision log gives the full text and the evidence for each.
    
    | # | Topic | Decision | Section |
    |---|---|---|---|
    | R18 | Name | EXPERTA, with the subtitle "Fractional Factorial Design Translator". | 9.8 |
    | R19 | Colours | Charts: Dark2 purple, orange, teal and gray, at most four. Interface accent: the Dark2 purple, darkened to `#625d96`. | 9.8, 10.3 |
    | R20 | Principles | Effect hierarchy, sparsity and heredity. | 1.8 |
    | R21 | Clear 2FIs | The report shows the number of clear 2FIs and the factors with all 2FIs clear. A measure only; minimum aberration stays the selection rule. Estimable 2FIs move to Later. | 1.1, 1.5, 7.5, 9.4 |
    | R22 | Reply for required estimable interactions | The not-supported message points to the clear 2FIs in step 3 and the factor order in step 2. | 1.6 |
    | R23 | Full factorials | All 2FIs of a full factorial are clear. | 7.5 |
  8. Section 3.4, the sentence This tool reuses its stack, its WebR worker pattern, its design tokens, its chart palette and its accessibility fixes. → This tool reuses its stack, its WebR worker pattern, its tan and ink design tokens and its accessibility fixes. Review decision R19 replaced the accent colour and the chart palette (Section 9.8).

  9. Section 5.3, in the not_supported_in_version bullet: `estimable_interactions`, → `estimable_interactions` (Later, R21), and `alternative_criterion` (for example MaxC2), → `alternative_criterion` (for example MaxC2; minimum aberration stays the selection rule, R21),.

  10. Section 7.5, replace the row | Clear 2FIs | Catalogue entry clear.2fis, mapped to the real factor names | with these two rows:

    | Clear 2FIs | Catalogue entry `clear.2fis`, mapped to the real factor names. A 2FI is clear if no main effect and no other 2FI is aliased with it; interactions of three or more factors are not looked at (FrF2's definition; Section 1.8). A full factorial has no catalogue entry and no aliasing, so all its $\binom{k}{2}$ 2FIs are clear (R23). The number of clear 2FIs is the length of the list. |
    | Factors with all 2FIs clear | Catalogue entry `all.2fis.clear`, mapped to the real factor names (report field `all_2fis_clear`). For a full factorial: all factors. |
  11. Section 8.3, in the exports table: Each chart in the ChatISA palette, and the numbers behind it. → Each chart in the palette of Section 9.8, and the numbers behind it.

  12. Section 9.4:

    • - Right: design properties: resolution, WLP, generators, clear 2FIs, run order and seed, FrF2 messages, and the aliased 2FIs with the real factor names. → - Right: design properties: resolution, WLP, generators, the number of clear 2FIs and the list of clear 2FIs, the factors that have all 2FIs clear (if any), run order and seed, FrF2 messages, and the aliased 2FIs with the real factor names. "Clear" does not look at interactions of three or more factors (hierarchy principle, Section 1.8).
    • - A note says: "To change which effects are aliased, change the factor order in step 2." → - A note says: "To change which effects are aliased, change the factor order in step 2." If some factors, but not all, have all 2FIs clear, the note also names them. Example for 9 factors in 32 runs: "Only the factor in position 9 (now Feed) has all 2FIs clear. To give this property to a different factor, move that factor to position 9 in step 2."
  13. Section 9.6, the bullet - A ? panel explains: the order does not change the WLP, the resolution or the number of clear 2FIs. The order changes which named effects are aliased. → - A ? panel explains: the order does not change the WLP, the resolution or the number of clear 2FIs. The order changes which named effects are aliased and which named factors have all 2FIs clear. Example: in the FrF2 design for 9 factors in 32 runs, only position 9 has all 2FIs clear.

  14. Section 9.8:

    • - **Name:** "Fractional Factorial Design Translator", spelled out, from one APP_NAME constant. → - **Name:** EXPERTA, with the subtitle "Fractional Factorial Design Translator" (R18). Constants APP_NAME, APP_SUBTITLEandAPP_FULL_NAMEinlib/site.ts. The browser title and the citation use the full name "EXPERTA: Fractional Factorial Design Translator".
    • - **Interface tokens (ChatISA):** Miami Red #c41230, accent red #ad102a, light tan #edece2, medium tan #ccc9b8, dark tan #70685c, ink #000000, warm white #faf9f7. → - **Interface tokens:** accent #625d96and accent dark#4c4977(R19: the Dark2 purple#7570B3, darkened, because #7570B3has only 4.44:1 on white); from ChatISA: light tan#edece2, medium tan #ccc9b8, dark tan #70685c, ink #000000, warm white #faf9f7. The accent has 5.96:1 on white, 5.66:1 on warm white and 5.00:1 on light tan; a unit test checks the WCAG AA values (brand-colours.test.ts).
    • - **Chart palette (ChatISA, measured):** series #C3142D, #585E60, #1D5FAD, #FF7436, then ColorBrewer Dark2 for 5 to 8 series. Black is for text and axes only. From 3 series up, direct labels or distinct shapes are required. FrF2's own plot functions get the palette through their colour arguments. → - **Chart palette (ColorBrewer Dark2, measured, R19):** series purple #7570B3, orange #D95F02, teal #1B9E77, gray #666666, in this order, at most four colours. Dark2 green (2.98:1) and mustard (2.06:1) fail the 3:1 contrast that chart marks need on white. In ColorBrewer's order, teal and magenta are almost the same colour for colour-blind readers (OKLab distance 1.7), and so are orange and brown (1.8). The smallest OKLab distance of the chosen order, for normal vision and simulated protanopia, deuteranopia and tritanopia, is 21.4 for 2 series, 10.7 for 3 and 7.2 for 4 (replication_materials/findings/palette-check.mjs). From 3 series up, direct labels or distinct shapes are required; above 4 series, shapes and labels carry the difference. Black is for text and axes only. FrF2's own plot functions get the palette through their colour arguments.
    • - **Favicon:** an SVG cube with four of its eight corners filled: the $2^{3-1}$ half fraction. → - **Favicon:** an SVG cube with four of its eight corners filled in the accent: the $2^{3-1}$ half fraction.
  15. Section 10.3:

    • - Warm-white field #faf9f7, a Miami Red rule at the top, both logos in their own colours with equal weight, and black titles. → - Warm-white field #faf9f7, an accent rule (#625d96, R19) at the top, both logos in their own colours with equal weight, and black titles.
    • - Measured contrast for normal vision and for simulated protanopia, deuteranopia, tritanopia and achromatopsia: black titles 19.9:1 or more, Tec blue 8.3:1 or more, Miami Red 4.8:1 or more (card_contrast.py). → - Measured contrast for normal vision and for simulated protanopia, deuteranopia, tritanopia and achromatopsia (version 2, with the Miami Red rule): black titles 19.9:1 or more, Tec blue 8.3:1 or more, Miami Red 4.8:1 or more (card_contrast.py). The accent rule is measured again before the video release (Section 17.1).
  16. Section 17.1:

    • Replace the row | Final app name. | Both authors | Public release | with | A check of the name EXPERTA for conflicts with other software and organizations. | Both authors | Public release |
    • Add these rows at the end of the table:
    | Confirm the clear-2FI definition for full factorials (R23) and the Dark2 purple of R19. | Alan R. Vazquez | Milestone 3B |
    | References for the principles in Section 1.8, and the glossary entries for effect hierarchy, effect sparsity, effect heredity and clear 2FI. | Alan R. Vazquez | Milestone 3B |
    | Measure the video title cards again with the accent rule (Section 10.3). | Fadel M. Megahed | Video release (Milestone 4) |
  17. Section 19, add in alphabetical order (after Groemping, and after Megahed):

    - Harrower, M., and Brewer, C. A. (2003). ColorBrewer.org: An online tool for selecting colour schemes for maps. The Cartographic Journal, 40(1), 27-37.
    - Ottosson, B. (2020). A perceptual color space for image processing. https://bottosson.github.io/posts/oklab/

After the edits, check: grep -n "Miami Red\|ChatISA palette\|estimable two-factor" replication_materials/plans/2026-10-14-ff-design-translator-design.md shows only the R16 row and the version-2 contrast sentence in Section 10.3.

In replication_materials/process/process.json:

  • change "title" to "How EXPERTA was built" and "status_date" to "2026-10-16";
  • add these two events after e20 (the review, then this plan; the build event follows in Verification):
{
  "id": "e21",
  "time": "2026-10-16T20:36:51Z",
  "stage": "review",
  "actors": ["people", "claude"],
  "title": "Review by Alan R. Vazquez",
  "detail": "A. R. Vazquez marks his changes on the specification page: the name EXPERTA, the principles of effect hierarchy, sparsity and heredity, the number of clear 2FIs in place of estimable 2FIs, and purple in the palette. F. M. Megahed and Claude sort the changes by their risk to the merged work and measure the colours. Specification version 3 applies the decisions R18 to R23.",
  "records": ["plans/2026-10-16-spec-review-decisions-v3.md"],
  "source": { "transcript": "1a47922b", "time": "2026-10-16T20:36:51Z" }
},
{
  "id": "e22",
  "time": "2026-10-16T21:38:15Z",
  "stage": "plan",
  "actors": ["people", "claude"],
  "title": "Implementation plan, pre-3B review changes",
  "detail": "F. M. Megahed and Claude turn the review decisions into a plan of five tasks before Milestone 3B. Colour measurements replace the first purple with the Dark2 purple. F. M. Megahed decides that every plan is published as a page.",
  "records": ["plans/2026-10-16-pre3b-review-changes-plan.md"],
  "source": { "transcript": "1a47922b", "time": "2026-10-16T21:38:15Z" }
}

In scripts/build-spec-page.mjs:

  • line 36: const RIBBON = opt("ribbon", "Draft for review · version 2"); → const RIBBON = opt("ribbon", "Draft for review · version 3");
  • line 38: the default description → "Design specification, version 3, of EXPERTA, a web tool that makes two-level fractional factorial designs from a plain-language request with FrF2 in the browser."
  • line 62: .filter((r) => !["Document", "Working title"].includes(r.key)); → .filter((r) => !["Document", "Name"].includes(r.key));

Run: node scripts/build-spec-page.mjs Expected: it writes replication_materials/plans/spec-page/index.html and artifact.html without errors. Check: grep -c "EXPERTA" replication_materials/plans/spec-page/artifact.html is greater than 5, grep -c "<math" replication_materials/plans/spec-page/artifact.html is greater than 0 (the (k2)\binom{k}{2} renders as MathML), and the page shows event e21 in the process figure.

git add replication_materials/findings/palette-check.mjs replication_materials/findings/palette-check-output.txt replication_materials/plans/2026-10-16-spec-review-decisions-v3.md replication_materials/plans/2026-10-14-ff-design-translator-design.md replication_materials/process/process.json scripts/build-spec-page.mjs replication_materials/plans/spec-page
git commit -m "docs: specification version 3 with the review decisions R18 to R23 of Alan R. Vazquez"

10Verification (main session)