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: A static site with a landing page and three pages (Records, Decisions, Progress), built from the records in the repository and from GitHub, deployed to GitHub Pages at project.expertadoe.app by a GitHub Actions workflow.
Architecture: One Node script, scripts/build-project-site.mjs, with modules in scripts/project-site/. Parsers read the Markdown records (plans, ledgers, decision tables, spec Section 17.1), three JSON files in replication_materials/project/, git log and the GitHub API (through gh api). Renderers write HTML with inline CSS, inline SVG figures and one small inline script. The workflow runs the script and deploys the folder. The switch-on waits for the end of the review by Alan R. Vazquez (Checkpoint).
Tech Stack: Node 22 ESM (.mjs), no npm package. Vitest for unit tests, Playwright with @axe-core/playwright (already in the repository) for the page tests. GitHub Actions, GitHub Pages.
Spec: replication_materials/plans/2026-10-19-project-pages-design.md (the design of this plan; decisions PS1 to PS16). The app specification 2026-10-14-ff-design-translator-design.md does not change.
1Global Constraints
- Node 22.14 or later. No new npm package, in
dependenciesordevDependencies. The build script imports onlynode:modules and repository modules, so the workflow runs it withoutnpm ci. - People by name (PS5): "Fadel", "Alan" and later co-authors by name, from
roadmap.json. Never the words "Author" or "Reviewer" as a label for a person. - Chart colors: at most four, Dark2 reordered: purple
#7570B3(people), orange#D95F02(Claude), teal#1B9E77(measurement), gray#666666(other or unknown). The interface accent stays#625d96(fromscripts/spec-page.css). - WCAG 2.1 AA: axe reports no violation (tags
wcag2a,wcag2aa) on each page, in the light and the dark theme. Each figure hasrole="img", anaria-label, and a table or a list with the same data. - Layout works at a width of 360 px: 16 px side gutter, no horizontal page scroll; wide tables scroll in their own box.
- Every link inside the site is relative, so the site works at
project.expertadoe.app/and atfmegahed.github.io/experta/. - Text on the pages: plain academic tone, about 80% ASD-STE100. No marketing words.
- No secret on any page or in any data file. The code never reads, prints or passes a token (PS15): GitHub data comes from
gh api, which takes its credentials from its own configuration or fromGH_TOKENin the workflow. mainstays frozen. All work is on the branchproject-pages. Commits end with the trailers of the session (Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>andClaude-Session: https://claude.ai/code/session_01P666jingg9iAXQGi9VGGd6).- Subagents run
node scripts/secret-scan.mjsonly with--env <made-up file>; they never open.env. - Escape every text from a record before it goes into HTML (
&,<,>,").
2Review Focus
- A decision table in an unusual form. Headers differ between plans (
| # | Decision | Reason | Cost if wrong |,| # | Decision | By |,| # | Topic | Decision | Source |,| # | Question | Decision |), cells contain\|, and files on Windows have CRLF line ends. Each row must parse to the right fields. Test: Task 2,parses tables with escaped pipes, CRLF and four header forms. - A ruling written in another form. Ledgers write
Ruling: …,- Ruling 3: …,Ruling 29: Parked: …andFinal: parked — … — Ruling: …; prose mentions "Rulings 1-17". Each ruling must count once, and prose must not count. Test: Task 2,counts each ruling form once and ignores prose. - GitHub is not available (no
ghlogin, rate limit, no network). The build must finish, and the pages must say "GitHub data is not available" where the data would be. Test: Task 5,readGitHub returns available false when the runner throws; Task 9,builds offline. - Markup in a record. A decision text with
<script>,&or"must show as text. Test: Task 6,escapes markup from records. - A renamed or missing document. If
roadmap.jsonnames a file that does not exist, the build must stop with the file name, not drop the row. Test: Task 1,reports a missing document; Task 9,exits 1 when the roadmap names a missing file.
3File structure
| File | Responsibility |
|---|---|
replication_materials/project/roadmap.json |
People, areas, milestones in display order, the next milestone, blockers that are not in spec Section 17.1 |
replication_materials/project/decisions.json |
Who proposed and who decided each decision, with evidence (Task 4) |
replication_materials/project/decision-corrections.json |
Corrections by a person |
replication_materials/project/README.md |
What the files are, how the attribution was made, how to build the site |
scripts/project-site/data.mjs |
Read and validate the three JSON files; map owner text to people |
scripts/project-site/decisions.mjs |
Parse decision tables and rulings; attribution; --list command |
scripts/project-site/git.mjs |
Dates of files and the head commit from git log |
scripts/project-site/records.mjs |
The list of documents with kind, date, size and status |
scripts/project-site/github.mjs |
Pull requests, issues, branches and the state of main through gh api |
scripts/project-site/progress.mjs |
Open items of Section 17.1, blockers, progress by milestone |
scripts/project-site/client.mjs |
The browser script: filters (pure functions plus DOM glue) |
scripts/project-site/site.css |
Styles that scripts/spec-page.css does not have |
scripts/project-site/render/html.mjs |
esc, the page shell, the navigation, the footer |
scripts/project-site/render/figures.mjs |
Stacked bars, Gantt bars, the roadmap strip |
scripts/project-site/render/landing.mjs, records.mjs, decisions.mjs, progress.mjs |
One renderer for each page |
scripts/lib/process-figure.mjs |
The process figure, moved out of build-spec-page.mjs so both pages use it |
scripts/build-project-site.mjs |
Entry: reads, validates, renders, writes the site |
.github/workflows/project-site.yml |
Build and deploy to GitHub Pages |
playwright.site.config.ts, replication_materials/tests/site/site.spec.ts |
Page tests on the built site (axe, filters) |
replication_materials/tests/unit/project-site/*.test.ts |
Unit tests |
replication_materials/tests/fixtures/project-site/ |
Small Markdown records for the tests |
Shared types (JSDoc in the modules; listed here so that each task uses the same names):
/** @typedef {{ id: string, name: string, full_name: string, affiliation: string, url: string }} Person */
/** @typedef {{ id: string, phase: number|null, title: string, state: "done"|"now"|"next"|"planned",
* area: string, documents: string[], page: string|null, probes: string|null, prs: number[],
* merged: string|null, deployed: string|null, review: string|null, needed_by: string[] }} Milestone */
/** @typedef {{ milestone: string, text: string, owners: string[], since: string, source: string,
* github_label?: string }} Blocker */
/** @typedef {{ key: string, file: string, id: string, kind: "table"|"ruling", text: string,
* topic: string|null, source: string, cost: string|null, proposedRaw: string|null, decidedRaw: string|null }} Decision */
/** @typedef {Decision & { proposedBy: string[], decidedBy: string[], how: string, evidence: string,
* outcome: "stood"|"changed"|null, replacedBy: string|null, basis: "corrected"|"table"|"classified"|"text"|"rule"|"unknown",
* checkedBy: string|null, date: string, milestone: string|null, copies: string[] }} AttributedDecision */
Owner and decider ids: the id of a person, "claude", or "measurement".
4Task 1: Project data files and their validation
Files:
- Create:
replication_materials/project/roadmap.json,replication_materials/project/decision-corrections.json,replication_materials/project/README.md - Create:
scripts/project-site/data.mjs - Test:
replication_materials/tests/unit/project-site/data.test.ts
Interfaces:
- Produces:
readProject(root: string) → { roadmap, records: { records: Record<string, object> }, corrections: { corrections: object[] } }(a missingdecisions.jsonreads as{ records: {} });validateRoadmap(roadmap, exists: (relPath) => boolean) → string[];ownerIds(text: string, people: Person[]) → string[];PLANS_DIR = "replication_materials/plans",PROJECT_DIR = "replication_materials/project".
import { describe, expect, it } from "vitest";
import { ownerIds, readProject, validateRoadmap } from "@/scripts/project-site/data.mjs";
import { existsSync } from "node:fs";
import path from "node:path";
// Vitest runs from the repository root, as the other unit tests assume (paths relative to it).
const ROOT = process.cwd();
describe("project data", () => {
it("the committed roadmap is valid", () => {
const { roadmap } = readProject(ROOT);
expect(validateRoadmap(roadmap, (p) => existsSync(path.join(ROOT, p)))).toEqual([]);
});
it("reports a missing document", () => {
const { roadmap } = readProject(ROOT);
const broken = structuredClone(roadmap);
broken.milestones[0].documents.push("2026-01-01-gone-plan.md");
expect(validateRoadmap(broken, (p) => !p.endsWith("gone-plan.md"))).toEqual([
"Milestone m1: the document replication_materials/plans/2026-01-01-gone-plan.md does not exist.",
]);
});
it("reports an unknown next milestone, area and owner", () => {
const { roadmap } = readProject(ROOT);
const b = structuredClone(roadmap);
b.next = "nope"; b.milestones[0].area = "Cooking"; b.blockers[0].owners = ["bob"];
const errors = validateRoadmap(b, () => true);
expect(errors).toContain("The next milestone nope is not in the list of milestones.");
expect(errors).toContain("Milestone m1: the area Cooking is not in the list of areas.");
expect(errors).toContain("Blocker 1: the owner bob is not a person, claude or measurement.");
});
it("maps owner text of Section 17.1 to people", () => {
const { roadmap } = readProject(ROOT);
const p = roadmap.people;
expect(ownerIds("Alan R. Vazquez", p)).toEqual(["alan"]);
expect(ownerIds("Fadel M. Megahed", p)).toEqual(["fadel"]);
expect(ownerIds("Both authors", p)).toEqual(["fadel", "alan"]);
expect(ownerIds("Fadel M. Megahed, Alan R. Vazquez", p)).toEqual(["fadel", "alan"]);
expect(ownerIds("A Spanish speaker", p)).toEqual([]);
});
it("names people only by name", () => {
const { roadmap } = readProject(ROOT);
expect(roadmap.people.map((x) => x.name)).toEqual(["Fadel", "Alan"]);
expect(JSON.stringify(roadmap)).not.toMatch(/\b(Author|Reviewer)\b/);
});
});
{
"repo": "fmegahed/experta",
"app": "https://expertadoe.app",
"site": "https://project.expertadoe.app",
"people": [
{ "id": "fadel", "name": "Fadel", "full_name": "Fadel M. Megahed", "affiliation": "Miami University", "url": "https://miamioh.edu/fsb/directory/?up=/directory/megahefm" },
{ "id": "alan", "name": "Alan", "full_name": "Alan R. Vazquez", "affiliation": "Tecnológico de Monterrey", "url": "https://research.tec.mx/vivo-tec/display/PID_115457" }
],
"areas": ["Specification and scope", "R and design rules", "Browser engine", "Translation and LLMs", "Interface and help", "Exports and report", "Deployment and operations", "Evaluation", "Analysis", "Project records"],
"specification": { "documents": ["2026-10-14-ff-design-translator-design.md", "2026-10-14-spec-review-decisions.md", "2026-10-16-spec-review-decisions-v3.md"], "page": "spec-page", "area": "Specification and scope" },
"milestones": [
{ "id": "m1", "phase": 1, "title": "Milestone 1: R core", "state": "done", "area": "R and design rules", "documents": ["2026-10-15-m1-r-core-plan.md", "2026-10-15-m1-execution-ledger.md"], "page": null, "probes": "m1-probes", "prs": [], "merged": "2026-10-15", "deployed": null, "review": "One fix wave; re-review: all findings addressed", "needed_by": [] },
{ "id": "m2", "phase": 1, "title": "Milestone 2: Browser engine and projects", "state": "done", "area": "Browser engine", "documents": ["2026-10-15-m2-browser-engine-plan.md", "2026-10-15-m2-execution-ledger.md"], "page": "m2-plan-page", "probes": "m2-probes", "prs": [], "merged": "2026-10-15", "deployed": null, "review": "Ready with fixes; one fix wave", "needed_by": [] },
{ "id": "m3a", "phase": 1, "title": "Milestone 3A: Translation service", "state": "done", "area": "Translation and LLMs", "documents": ["2026-10-15-m3a-translation-service-plan.md", "2026-10-16-m3a-execution-ledger.md"], "page": "m3a-plan-page", "probes": "m3a-probes", "prs": [], "merged": "2026-10-16", "deployed": null, "review": "Clean after one fix wave", "needed_by": [] },
{ "id": "pre3b", "phase": 1, "title": "Changes from the first review", "state": "done", "area": "Specification and scope", "documents": ["2026-10-16-pre3b-review-changes-plan.md", "2026-10-16-pre3b-execution-ledger.md"], "page": "pre3b-plan-page", "probes": null, "prs": [], "merged": "2026-10-16", "deployed": null, "review": "Ready to merge; 7 minor findings deferred", "needed_by": [] },
{ "id": "m3b", "phase": 1, "title": "Milestone 3B: Interface", "state": "done", "area": "Interface and help", "documents": ["2026-10-16-m3b-interface-plan.md", "2026-10-16-m3b-execution-ledger.md"], "page": "m3b-plan-page", "probes": "m3b-probes", "prs": [], "merged": "2026-10-17", "deployed": null, "review": "1 Important finding; one fix wave, re-review clean", "needed_by": [] },
{ "id": "m3c", "phase": 1, "title": "Milestone 3C: Exports, import and report", "state": "done", "area": "Exports and report", "documents": ["2026-10-17-m3c-exports-plan.md", "2026-10-17-m3c-execution-ledger.md"], "page": "m3c-plan-page", "probes": "m3c-probes", "prs": [], "merged": "2026-10-17", "deployed": null, "review": "3 Important findings; one fix wave, re-review clean", "needed_by": [] },
{ "id": "alpha", "phase": 1, "title": "Alpha release", "state": "done", "area": "Deployment and operations", "documents": ["2026-10-17-alpha-release-design.md", "2026-10-17-alpha-release-plan.md", "2026-10-17-alpha-execution-ledger.md"], "page": "alpha-plan-page", "probes": "alpha-probes", "prs": [1, 3, 4], "merged": "2026-10-18", "deployed": "2026-10-18", "review": "One fix wave, re-review clean", "needed_by": [] },
{ "id": "minors", "phase": 1, "title": "Alpha minor fixes", "state": "done", "area": "Interface and help", "documents": ["2026-10-19-alpha-minors-audit.md", "2026-10-19-alpha-minors-plan.md", "2026-10-19-alpha-minors-execution-ledger.md"], "page": "minors-plan-page", "probes": null, "prs": [5], "merged": "2026-10-19", "deployed": "2026-10-19", "review": "One fix wave (11 items), re-review clean", "needed_by": [] },
{ "id": "print", "phase": 1, "title": "Report print redesign", "state": "done", "area": "Exports and report", "documents": ["2026-10-19-report-print-plan.md", "2026-10-19-report-print-execution-ledger.md"], "page": "print-plan-page", "probes": null, "prs": [6], "merged": "2026-10-19", "deployed": "2026-10-19", "review": "One fix wave (10 items), re-review clean", "needed_by": [] },
{ "id": "review", "phase": 1, "title": "Alan's review and the fixes", "state": "next", "area": "Specification and scope", "documents": [], "page": null, "probes": null, "prs": [], "merged": null, "deployed": null, "review": null, "needed_by": [] },
{ "id": "m4", "phase": 1, "title": "Milestone 4: Evaluation and documentation", "state": "planned", "area": "Evaluation", "documents": [], "page": null, "probes": null, "prs": [], "merged": null, "deployed": null, "review": null, "needed_by": ["Milestone 4", "test-set run"] },
{ "id": "phase2", "phase": 2, "title": "Phase 2: Analysis", "state": "planned", "area": "Analysis", "documents": [], "page": null, "probes": null, "prs": [], "merged": null, "deployed": null, "review": null, "needed_by": ["Phase 2"] },
{ "id": "video", "phase": 1, "title": "Introduction video", "state": "planned", "area": "Interface and help", "documents": [], "page": null, "probes": null, "prs": [], "merged": null, "deployed": null, "review": null, "needed_by": ["Video release"] },
{ "id": "phase3", "phase": 3, "title": "Phase 3: Blocking, split-plot and foldover", "state": "planned", "area": "R and design rules", "documents": [], "page": null, "probes": null, "prs": [], "merged": null, "deployed": null, "review": null, "needed_by": ["Phase 3"] },
{ "id": "phase4", "phase": 4, "title": "Phase 4: Other design families", "state": "planned", "area": "R and design rules", "documents": ["2026-10-19-phase4-investigation.md"], "page": "phase4-investigation-page", "probes": "phase4-probes", "prs": [], "merged": null, "deployed": null, "review": null, "needed_by": ["Phase 4"] },
{ "id": "pages", "phase": null, "title": "Project pages", "state": "now", "area": "Project records", "documents": ["2026-10-19-project-pages-design.md", "2026-10-19-project-pages-plan.md"], "page": null, "probes": null, "prs": [], "merged": null, "deployed": null, "review": null, "needed_by": [] }
],
"next": "review",
"public_release": "Public release",
"blockers": [
{ "milestone": "review", "text": "Answers to the review questions (GitHub issues with the label alpha-review)", "owners": ["alan"], "since": "2026-10-19", "source": "https://github.com/fmegahed/experta/issues?q=label%3Aalpha-review", "github_label": "alpha-review" },
{ "milestone": "review", "text": "Check in the Upstash console that the counter keys have a time limit", "owners": ["fadel"], "since": "2026-10-18", "source": "replication_materials/plans/2026-10-17-alpha-execution-ledger.md" },
{ "milestone": "review", "text": "One fix plan from the answers (waits for the answers)", "owners": ["claude"], "since": "2026-10-19", "source": "replication_materials/project/roadmap.json" }
]
}
decision-corrections.json:
{
"corrections": [
{ "key": "2026-10-17-alpha-release-design.md#A3", "decided_by": ["fadel"], "how": "chat",
"evidence": "Fadel M. Megahed, 2026-10-19: he decided on Vercel hosting, with his Vercel Pro plan as the deciding input.",
"checked_by": "fadel", "date": "2026-10-19" }
]
}
A correction overrides only the fields that it has.
-
- each milestone document and the specification documents exist (
exists(PLANS_DIR + "/" + file)); eachpageandprobesfolder exists; - each milestone
areais inareas; nextis a milestone id;- milestone ids are unique;
- each blocker owner is a person id,
"claude"or"measurement", and each blocker milestone exists.
Blockers are numbered from 1 in the messages.
ownerIdssplits on,andand, matches thefull_nameof a person, and maps "Both authors" to all people in roadmap order.The page folder of this plan (
pages-plan-page) is made in Step 6 of Task 9, sopages.pagestaysnulluntil then; Task 9 sets it. - each milestone document and the specification documents exist (
5Task 2: Decision tables and rulings
Files:
- Create:
scripts/project-site/decisions.mjs - Create:
replication_materials/tests/fixtures/project-site/plans/with2026-01-01-sample-plan.md(CRLF line ends, four header forms, an escaped pipe, a<script>text) and2026-01-02-sample-execution-ledger.md(each ruling form, the prose "Rulings 1-17 in …", a line "Task 1: complete (abc1234)") - Test:
replication_materials/tests/unit/project-site/decisions.test.ts
Interfaces:
- Consumes:
readProject,PLANS_DIR(Task 1). - Produces:
splitRow(line: string) → string[]parseDecisionTables(markdown, file) → Decision[]parseRulings(markdown, file) → Decision[]keywordAttribution(text, people) → { proposedBy, decidedBy, how }namesToIds(text, people) → string[](accepts "Fadel", "Alan", "Claude", "Measured", "Measurement", full names, "FMM", "ARV")attribute(decision, project, context) → AttributedDecision, wherecontext = { date, milestone, area }collectDecisions(root, project) → AttributedDecision[]- CLI:
node scripts/project-site/decisions.mjs --listprints a JSON array of{ key, file, id, kind, text, source, cost }for all decisions.
-
parses tables with escaped pipes, CRLF and four header forms: for the fixture, the IDs are["P1","P2","R1","D1","S1"].P2.textcontainsa | b(the pipe is unescaped). The header| # | Decision | Reason | Cost if wrong |givescost. The header| # | Decision | By |givessource= the "By" cell. The header| # | Topic | Decision | Source |givestopicandsource. The header| # | Question | Decision |givestext= "Question: Decision". The header| # | Decision | Proposed by | Decided by |givesproposedRaw/decidedRaw. Keys are2026-01-01-sample-plan.md#P1.ignores tables that do not start with # or ID and rows without an ID: a table| Item | Value |gives nothing.counts each ruling form once and ignores prose: the fixture ledger has the formsRuling: x — cost if wrong: y,- Ruling 3: …,Ruling 29: Parked: …,Final: parked — z — Ruling: w — cost if wrong: v, and the prose line. Expected: 4 rulings with keys…ledger.md#ruling-1to#ruling-4;costof the first isy; idsRuling,Ruling 3,Ruling 29,Ruling.keyword attribution:- "AskUserQuestion: Build both" → decidedBy
["fadel"], how"chat". - "Review by Alan R. Vazquez" →
["alan"], how"review". - "Measured: 40 ms" →
["measurement"], how"measurement". - "FMM (b)" →
["fadel"]. - "Research report" →
[], how"unknown".
- "AskUserQuestion: Build both" → decidedBy
attribution precedence, with the same decision and three sources:- A correction gives basis
"corrected"andcheckedBy "fadel". - Without a correction, the table columns "Proposed by"/"Decided by" give basis
"table". - Then a record in
decisions.jsongives"classified". - Then keywords give
"text". - A ruling with none of these gives proposedBy and decidedBy
["claude"], how"ruling", outcome"stood", basis"rule".
- A correction gives basis
marks the copies in the specification: with a project whose specification listsspec.mdandlog.md, an ID that is in both gives one decision fromlog.mdwithcopies: ["spec.md"].
-
- A table starts at a row whose first cell is
#orID. The next row is the separator. Data rows follow until the first line that does not start with|. - A data row needs a first cell that matches
/^[A-Z]{1,3}\d+[a-z]?$/. - Field mapping by header name, ignoring case:
text= Decision; or "Question: Decision" when both exist; else Topic.topic= Topic.source= the first of Source, By, Reason, Why.cost= "Cost if wrong".proposedRawanddecidedRaw= "Proposed by" and "Decided by".
- A ruling is a line that matches
/\bRuling(?: [A-Z]?\d+[a-z]?)?:\s*/. The text is everything after the match, up to— cost if wrong:. The cost is the rest. - Normalize
\r\nbefore parsing. collectDecisionsreads every top-level.mdfile inPLANS_DIR. It takes the date from the file name prefix, and the milestone and area from the roadmap (a specification document has the area ofspecification). It removes the copies as tested and sorts by date, then file, then order in the file.
- A table starts at a row whose first cell is
6Task 3: Documents and dates
Files:
- Create:
scripts/project-site/git.mjs,scripts/project-site/records.mjs - Test:
replication_materials/tests/unit/project-site/records.test.ts
Interfaces:
- Consumes:
parseDecisionTables,parseRulings(Task 2);readProject(Task 1). - Produces:
gitRunner(root) → (args: string[]) => stringfileDates(relPaths, run) → Record<string, { first: string, last: string } | null>, fromgit log --follow --format=%cs -- <path>, oldest = firstheadCommit(run) → { sha: string, short: string, date: string }documentKind(file, project) → "specification"|"decision-log"|"design"|"plan"|"ledger"|"audit"|"investigation"|"other"summarize(markdown, file) → { title, tasks, completedTasks, rulings, decisions }listDocuments(root, project, dates) → Document[], where Document ={ file, path, kind, date, title, milestone, phase, area, tasks, completedTasks, rulings, decisions, page, probes, prs, status, first, last }
-
documentKind:- the first specification document →
"specification"; the other two →"decision-log"; x-plan.md→"plan";x-execution-ledger.md→"ledger";x-audit.md→"audit";x-investigation.md→"investigation";x-design.md→"design";notes.md→"other".
- the first specification document →
summarize, on the fixtures:- The plan fixture with
### Task 1:and### Task 2:gives tasks 2. - The ledger fixture gives completedTasks 1 and rulings 4.
- The title is the H1 without
#. - Task ids count once: "Task 9b-1: complete" twice is 1.
- The plan fixture with
fileDateswith a fake runner that returns"2026-10-19\n2026-10-17\n2026-10-16\n"gives{ first: "2026-10-16", last: "2026-10-19" }. An empty output givesnull.listDocumentson the real repository:- The row of
2026-10-19-report-print-plan.mdhas milestoneprint, pageprint-plan-page, prs[6]and status"live". - The specification row has status
"living". - A document of a milestone in state
nextorplannedhas status"draft"; one in statenowhas"in progress". - A done milestone gives
"live"with adeployeddate, else"merged".
- The row of
7Task 4: Who proposed and who decided (data)
This task writes data, not code. The implementer is a research agent (the most capable model). The work is a classification from the records and the session transcripts.
Files:
- Create:
replication_materials/project/decisions.json - Test:
replication_materials/tests/unit/project-site/decisions-data.test.ts
Interfaces:
- Consumes:
node scripts/project-site/decisions.mjs --list(Task 2);roadmap.jsonareas and people (Task 1). - Produces:
decisions.json={ "method": string, "classified_on": "2026-10-19", "records": { [key]: { "proposed_by": string[], "decided_by": string[], "how": "chat"|"review"|"measurement"|"plan-approval"|"ruling"|"unknown", "topic": string, "evidence": string, "outcome"?: "stood"|"changed", "replaced_by"?: string } } }
-
- every key of kind
tablefromcollectDecisionshas a record (rulings may have none); - every record key is a key of
collectDecisions; proposed_byanddecided_byhold only person ids,"claude"or"measurement", anddecided_byis not empty unlesshowis"unknown";topicis one ofroadmap.areas;evidencehas at most 240 characters;- no value matches a pattern of
KEY_PATTERNSfromscripts/lib/secret-scan.mjs, no value has an e-mail address, and no value has a string of 32 or more letters and digits without a space.
- every key of kind
-
- read its row and the text around it in its file;
- search the session transcripts in
C:\Users\megahefm\.claude\projects\C--Users-megahefm-Dropbox-Miami-Code-claude-fractional-factorial\*.jsonlwithgrepfor its ID and its words. Read the author's messages and the answers toAskUserQuestion.
Rules:
- "chat": a person chose it in chat (an answer to a question, or his own instruction). decided_by is that person. proposed_by is the one who first wrote the option: "claude" when Claude offered it as a choice or a recommendation.
- "review": it came from a review comment of Alan R. Vazquez. Both fields hold "alan" if he wrote the change. decided_by holds "fadel" too if Fadel M. Megahed applied it by his decision.
- "measurement": a measurement with a rule fixed before the measurement set the value. decided_by is
["measurement"]. If a person chose the rule, add that person. - "plan-approval": Claude wrote it in a plan, and the author approved the plan without a change to it. proposed_by
["claude"], decided_by["fadel"]. - Rulings: add a record only when a person is named in the ruling or when a person later changed it. Use outcome "changed" and replaced_by = the key of the new decision.
- evidence: one line. Quote at most 25 words of a person's message, with the date, or name the record line. Never copy a value of a secret, a key or a token, and never copy a person's e-mail address.
- "unknown": use it when the records and transcripts do not show who decided.
The file must also have:
topic, chosen fromroadmap.areas;method: one paragraph on how the classification was made, for the page.
8Task 5: GitHub data, open items and progress
Files:
- Create:
scripts/project-site/github.mjs,scripts/project-site/progress.mjs - Test:
replication_materials/tests/unit/project-site/progress.test.ts
Interfaces:
- Consumes:
readProject,ownerIds(Task 1);listDocuments(Task 3). - Produces:
ghRunner() → (apiPath: string) => any, which runsgh api <apiPath>withexecFileSyncand parses JSON.readGitHub(repo, run) → { available: true, fetchedAt, pulls, issues, branches, main } | { available: false, reason }:- pulls:
{ number, title, state, merged, mergedAt, createdAt, branch, url }[], fromrepos/{repo}/pulls?state=all&per_page=100; - issues without pull requests:
{ number, title, state, labels: string[], createdAt, url }[], fromrepos/{repo}/issues?state=all&per_page=100; - branches other than
main:{ name, sha, date, message }[], fromrepos/{repo}/branches?per_page=100andrepos/{repo}/commits/{sha}; - main:
{ sha, state, vercel }, fromrepos/{repo}/commits/main/status, wherevercelis the state of the context "Vercel" or null.
- pulls:
openItems(specMarkdown, people) → { item, ownerText, owners, neededBy }[]: the rows of the table under### 17.1 Open itemswhose "Needed by" does not start with "Done".milestoneRows(project, documents) → { id, title, phase, state, area, tasks, completedTasks, start, end, merged, deployed, review, prs }[]. Herestartis the earliestfirstdate of its documents, andendismergedor null.blockersFor(milestoneId, project, items, github, today) → { text, owners, since, ageDays, source, detail }[].progressModel(project, documents, items, github, today) → { next, following, nextBlockers, followingBlockers, publicRelease, otherItems, milestones, now }. Herefollowingis the first milestone afternextin roadmap order whose state isplanned.
-
readGitHub returns available false when the runner throws: reason is the error message.readGitHub drops pull requests from the issue list and main from the branches.openItemson the real spec:- "The decision table of Phase 4 and the gold families." is open with owners
["alan"]. - The row "Rate limit and spend cap values." is not open (it starts with "Done").
- "The decision table of Phase 4 and the gold families." is open with owners
blockersFor("review", …):- The three roadmap blockers, in order.
- With 3 issues of the label
alpha-review, one of which also has the labelwithdrawn, the first blocker's detail is "2 answers received (3 filed)". An answer counts when it has the blocker'sgithub_labeland not the labelwithdrawn. - With GitHub unavailable, the detail is "GitHub data is not available".
- The ages are counted from
today.
blockersFor("m4", …)includes the open items whose "Needed by" contains "Milestone 4" or "test-set run".progressModel(...).publicReleaseholds the open items whose "Needed by" contains "Public release".milestoneRows: the print milestone has tasks 8, completedTasks 8, merged "2026-10-19". The review milestone has tasks 0 and start null.
9Task 6: Page shell, shared figures, landing and records pages
Files:
- Create:
scripts/lib/process-figure.mjs; Modify:scripts/build-spec-page.mjs(import it; no output change) - Create:
scripts/project-site/render/html.mjs,render/figures.mjs,render/landing.mjs,render/records.mjs,scripts/project-site/site.css,scripts/project-site/client.mjs - Test:
replication_materials/tests/unit/project-site/render.test.ts
Interfaces:
- Produces:
processFigure(processJson, esc) → string(the markup of today's function, unchanged);esc(s);pageHtml({ title, description, active: "home"|"records"|"decisions"|"progress", depth: 0|1, body, build: { short, date } }) → string. It inlinesscripts/spec-page.css,site.css,scripts/spec-page.jsandclient.mjs. The navigation has Home, Records, Decisions, Progress, "App" (https://expertadoe.app) and "Repository" (https://github.com/fmegahed/experta). The footer says "Built from commit {short} on {date}" and names the maintainers by full name;stackedBars({ bins: { label, counts: Record<kind, number> }[], kinds: { id, label, color }[], title }) → string(SVG);ganttBars({ rows: { label, start, end, state }[], from, to, title }) → string(SVG);roadmapStrip(milestones) → string(HTML list);renderLanding(model) → string;renderRecords(model) → string.
-
the spec page is unchanged: runnode scripts/build-spec-page.mjs --out <tmp>before the change (save the file in the test fixture folder asspec-page-before.htmlin Step 2) and after. The twoindex.htmlfiles are byte-equal.escapes markup from records:renderRecordswith a document title<script>alert(1)</script> & "x"contains<script>and no<script>alert.pageHtml:- It has
<!doctype html>,lang="en", a viewport meta tag, the title, andaria-current="page"on the active link. - Each internal
hrefis relative (no leading/, nohttpexcept the two external links and the record links to GitHub).
- It has
stackedBars: the result hasrole="img"and anaria-labelthat names the title. Each segment has a<title>with the bin, kind and count. The colors are only the four Dark2 colors.ganttBars: a row withend: nullreachesto.renderRecords: one row for each document, in date order. It has kind filters as buttons witharia-pressed, a caption, and links to the page folder (../plans/{page}/index.html), the Markdown on GitHub (https://github.com/fmegahed/experta/blob/main/replication_materials/plans/{file}) and each pull request.client.mjsexportsmatches(dataset, filters). For{kind:"plan"}and filters{kind:["plan","ledger"]}it gives true. For filters{}it gives true. With topic and decider filters, both must hold.
-
- The landing page shows the project name, the status line (current phase and milestone, the next step, the state of production), three cards (one sentence each, from Section 1 of the design) and the links.
- The records page shows the table of Section 3.2 of the design, and the note on how to read a row.
- The figures are plain SVG strings: no script,
viewBoxsizing, text at least 12 px.
10Task 7: Decisions page
Files:
- Create:
scripts/project-site/render/decisions.mjs - Test: add to
replication_materials/tests/unit/project-site/render.test.ts
Interfaces:
- Consumes:
collectDecisions(Task 2),processFigure(Task 6),stackedBars,pageHtml. - Produces:
decisionsModel(decisions, project) → { grid, bins, rows, counts, method };renderDecisions(model, processJson) → string.
-
grid columns are people by name, then Claude and Measured: the header cells are "Fadel", "Alan", "Claude", "Measured", in this order. With a third person in the roadmap, that name comes after "Alan".grid counts: a decision decided by["fadel","alan"]counts once in each column. A decision with decidedBy[]counts in an "Unknown" column, and that column shows only when its count is above 0.bins by day when the span is 60 days or less, else by week.the kinds of the bars: people are purple (#7570B3), Claude orange, measurement teal, unknown gray.each grid cell and each bar is a button that sets the filter: elements withdata-filter-topic/data-filter-decideranddata-filter-date.table rows carry data attributesdata-topic,data-decider(space-separated ids) anddata-date. Each row shows proposed by and decided by as names ("Fadel", "Claude", "Measured"), the basis ("Classified by Claude", "Checked by Fadel", "From the table of the plan", "From the text", "Rule for rulings"), and for a ruling its outcome.the method notecontains themethodtext ofdecisions.jsonand the counts by basis.the process figure is on the page(idprocess-figure).
-
- Page order: the grid, the bars, the clear-filter button, the table, the process figure, the method note.
- The table starts sorted by date, newest first.
- The filter state is in the URL hash, so that a filtered view can be shared.
11Task 8: Progress page
Files:
- Create:
scripts/project-site/render/progress.mjs - Test: add to
replication_materials/tests/unit/project-site/render.test.ts
Interfaces:
- Consumes:
progressModel(Task 5),ganttBars,roadmapStrip(Task 6). - Produces:
renderProgress(model, project, today) → string.
-
leads with the next milestone and its blockers: the firsth2after the roadmap strip is "Next: Alan's review and the fixes". Each blocker shows its owners by name, "open {n} days" and a link to its source.shows the following milestone and its blockers: "Then: Milestone 4: Evaluation and documentation".the now panel:- It lists the branches with their last commit date and message, and the open pull requests.
- It shows the state of
mainas "Production: success at {short sha}" from the Vercel status. - With GitHub unavailable, it shows "GitHub data is not available".
completed work by plan:- One row for each milestone with documents, with "8 of 8 tasks" and a meter (
<meter>or a bar witharia-valuenow). - The review text, the merge date and the deployment date are in the row.
- One row for each milestone with documents, with "8 of 8 tasks" and a meter (
the Gantt barscover the milestones with a start date. A milestone in statenowreaches today.before the public releaselists the open items of Section 17.1 for the public release, with owners by name.
12Task 9: Build script, workflow, page tests and documents
Files:
- Create:
scripts/build-project-site.mjs,.github/workflows/project-site.yml,playwright.site.config.ts,replication_materials/tests/site/site.spec.ts - Modify:
.gitignore(add_site/),scripts/check.mjs(add the step "Project site tests" after "Browser tests", browser-only),README.md(Status section and a section "Project pages"),replication_materials/project/roadmap.json(pages.page="pages-plan-page"after Step 6) - Test:
replication_materials/tests/unit/project-site/build.test.ts
Interfaces:
Consumes: all modules above.
Produces:
node scripts/build-project-site.mjs [--out _site] [--offline] [--today YYYY-MM-DD]. It writes:index.html,records/index.html,decisions/index.htmlandprogress/index.html;plans/{page}/index.html: a copy of eachpagefolder of the roadmap and ofspec-page;data/records.json,data/decisions.json,data/progress.json;.nojekyll.
On a validation error, it prints each message and exits with status 1.
-
- Unit (
build.test.ts), onnode scripts/build-project-site.mjs --out <tmp> --offline --today 2026-10-20:builds offline: exit status 0, and the files above exist.every internal link resolves: for eachhrefandsrcin the built HTML that is nothttp(s):,mailto:or#…, the target file exists in<tmp>. Strip the hash first.exits 1 when the roadmap names a missing file: run with--roadmap <tmp copy with a wrong file name>(add this option for tests), and see the message of Task 1.the workflow pins each action by commit SHA: everyuses:line in.github/workflows/project-site.ymlmatches@[0-9a-f]{40} # v…the workflow has the triggers and permissions: it has the triggerspushtomain,pull_request_target(opened, reopened, closed),issues,scheduleandworkflow_dispatch, and the permissionscontents: read,pages: write,id-token: write,issues: read,pull-requests: read.
- Page tests (
site.spec.ts, run bynpx playwright test -c playwright.site.config.ts). The config builds the site offline intotest-results/site/inglobalSetupand opens the pages throughfile://URLs:axe finds no violation on each page in the light and the dark theme: setcolorSchemetolight, thendark, with tagswcag2aandwcag2aa.a grid cell filters the decision table: click the cell (topic "Translation and LLMs", decider "Claude"). Then every visible row has that topic and "claude" indata-decider, and the visible count equals the number in the cell. "Clear filter" shows all rows again.the kind filter on the records page shows only plans.no horizontal scroll at 360 pxon each page:document.documentElement.scrollWidth <= 360.
- Unit (
-
actions/checkoutv7.0.1;actions/configure-pagesv6.0.0;actions/upload-pages-artifactv5.0.0;actions/deploy-pagesv5.0.1;actions/setup-node: the newest release that is at least 14 days old on the day of the commit (gh api repos/actions/setup-node/releases).
Write the tag after the SHA as a comment.
name: Project site
on:
push:
branches: [main]
pull_request_target:
types: [opened, reopened, closed]
issues:
types: [opened, edited, closed, reopened, labeled, unlabeled]
schedule:
- cron: "17 6 * * *"
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
issues: read
pull-requests: read
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-24.04
steps:
# pull_request_target runs in the context of main; check out main only, never the pull request.
- uses: actions/checkout@<sha> # v7.0.1
with:
ref: main
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@<sha> # v<x>
with:
node-version: 22
- name: Build the site
run: node scripts/build-project-site.mjs --out _site
env:
GH_TOKEN: ${{ github.token }}
- uses: actions/configure-pages@<sha> # v6.0.0
- uses: actions/upload-pages-artifact@<sha> # v5.0.0
with:
path: _site
deploy:
needs: build
runs-on: ubuntu-24.04
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@<sha> # v5.0.1
-
In
README.md, replace the outdated "Status" section with three sentences:- Phase 1 is live as an alpha at https://expertadoe.app.
- Alan R. Vazquez reviews it.
- The next steps are in the project pages.
Then add a section "Project pages" with the address
https://project.expertadoe.app(it goes live after the review), what each page shows, and how to build the pages locally (node scripts/build-project-site.mjs --out _site --offline).Add
_site/to.gitignore.Add the step to
scripts/check.mjs.
13Checkpoint: switch-on (after the review by Alan R. Vazquez; each step needs the approval of Fadel M. Megahed)
The controller does these steps. A subagent does not.
- Merge
project-pagesintomainwith a pull request and a fast-forward. This is the usual pattern, and the same push redeploys the app. - Enable GitHub Pages with the workflow as the source:
gh api -X POST repos/fmegahed/experta/pages -f build_type=workflow. This needs GitHub Pro while the repository is private. If the call fails for the plan, the authors decide: make the repository public, or move the address to a second Vercel project. - In Vercel DNS, add the record
projectCNAMEfmegahed.github.ioforexpertadoe.app. - Set the custom domain
project.expertadoe.appin the Pages settings, wait for the certificate, and turn on "Enforce HTTPS". - Start the workflow (
gh workflow run project-site.yml). Check that the four pages answer 200 athttps://project.expertadoe.app/and that the progress page shows the open pull requests. - Record the switch-on in the ledger and in
roadmap.json: thepagesmilestone is done, with its merge date.