{
  "notes": "Episode 2: foundation wave repository-foundation merged at trunk 74367a1d (package.json ESM, Node>=24, exports ./src/index.ts, bin rollup-risk -> ./src/cli.ts, scripts test='node --test test/*.test.ts' and start='node src/cli.ts'; LICENSE MIT; src/types.ts runtime-empty; docs/architecture.md with the frozen signatures, rubric, issue paths, table format, ranking rules and file ownership). Research example-facts accepted: six rollups from L2BEAT read 2026-10-27, every field evidenced, no stand-ins. Facts used (name;stage;proof;permissionless;exitDays;upgrades;DA;force): Arbitrum One;1;fraud;true;10;SC 9/12;blobs;true. OP Mainnet;1;fraud;true;0;SC 10/13;blobs;true. Base;1;fraud;true;0;SC 8/11;blobs;true. ZKsync Era;0;validity;false;5;SC 6/8;blobs;false. Starknet;1;validity;false;7;SC 9/12;blobs;false. Scroll;0;validity;false;0;multisig;blobs;true. Expected scores under the frozen rubric (computed by me; check them against the merged code): Arbitrum One 85 (10+20+10+10+15+10+10), OP Mainnet 75 (10+20+10+0+15+10+10), Base 70 (8/11=0.727 gives 10 council points), Starknet 65 (10+20+0+10+15+10+0), ZKsync Era 50 (0+20+0+5+15+10+0), Scroll 45 (0+20+0+0+5+10+10). Ranking: Arbitrum One, OP Mainnet, Base, Starknet, ZKsync Era, Scroll. Research caveats that the docs must keep: exit windows are regular-path risk-section values (emergency paths have none for all six; pages also show ~17.4d/~4d/8d for Arbitrum/ZKsync/Starknet); council thresholds are the council's own Safe, co-signers not representable; Scroll dropped its council for a 3/4 team multisig 2026-06-01 and can disable enforced-batch mode; ZKsync queue and Starknet council minority are not forced inclusion; Base is hybrid TEE+ZK since June 2026.\n\nDecision this episode (6 of 18 remaining slots; 12 left after): two code waves that queue one after the other. Wave 'implementation', three disjoint items: validator (src/validate.ts, test/validation.test.ts), scoring (src/score.ts, src/rank.ts, test/scoring.test.ts, test/ranking.test.ts), cli (src/cli.ts, src/index.ts, test/cli.test.ts). Each brief points at docs/architecture.md and src/types.ts, forbids any file outside scope (no stubs), and asks for files, exports, commands with pass/fail counts and what could not run. Known gap: the cli item's tests cannot run until the wave merges (siblings missing). Wave 'example-docs' runs after that merge: item example (examples/**, docs/example-sources.md, test/examples.test.ts) generates examples/expected.json and expected.txt from the real CLI; item readme (README.md) writes the README with real CLI output; item check (scope []) runs npm test, CLI table/JSON/help/errors, index import side effects, every rubric branch, dependency-free checks and reports findings with expected versus actual. The previous attempt at this episode was rejected only for vars over 1000 characters; this one keeps every var under 1000 and each filled brief under 3000, which is why the docs work is a separate batch with its own brief and two items.\n\nEpisode 3 plan: read implementation/validator, scoring, cli, example-docs/example, example-docs/readme and example-docs/check. If both waves merged and the check reports no failures and the example result shows the real CLI output matching the expected scores, run one last code wave with a single scope:[] recheck item (npm test on the final trunk plus node src/cli.ts examples/rollups.json table and --json compared with examples/expected.*), then finish in episode 4 if green. If the check, example or readme result lists failures, run one code wave of fix items with disjoint scopes, one per failing file set, each brief quoting the exact failing command and expected versus actual, plus nothing else; recheck in the following episode. If an implementation item failed or was refused, rerun only that item (same scope, brief adding what went wrong) before the docs wave can be useful; if example-docs ran against an incomplete trunk, rerun its example and readme items after the fix. If a wave's merge keeps failing, cancel it and rerun its items on the current trunk with smaller scopes. Keep at least 3 slots in reserve for repairs. Finish only when a scope:[] check on the final trunk is green and README, LICENSE, examples (input, output), src and tests all exist in the trunk; the write-up cites the trunk commit, the three implementation results, the example, readme and check results, and states what the example's facts rest on (L2BEAT read 2026-10-27, snapshot). Never brief a worker to read its machine or any credential.",
  "spawn": [
    {
      "name": "implementation",
      "mode": "code",
      "brief": "Implement the {{component}} of rollup-risk. rollup-risk is a dependency-free TypeScript library and CLI run directly by Node 24 (no build, no tsconfig, no install, no dependencies). Start from the trunk: read docs/architecture.md and src/types.ts first and follow their frozen signatures, rubric, issue paths, output format and ranking rules exactly; never edit them. Change ONLY these files: {{files}}. Never create or edit a file outside that list, not even a stub. Use only node: built-ins, erasable TypeScript (no enums, namespaces or parameter properties) and explicit .ts import specifiers. Tests use node:test and node:assert/strict, no network, and delete temp files they create under os.tmpdir(). Siblings write the other modules at the same time; import a sibling by its documented relative .ts path. Work: {{spec}} Tests: {{tests}} Check: run node --test on your test files and run each source with node. If a test cannot run because a sibling file is missing, say so instead of stubbing (a throwaway copy under test/scratch/ with stand-ins is fine for a smoke test; that folder is discarded). Result (markdown): files written, every export and its behaviour, exact commands run with pass/fail counts, what could not run and why, any deviation from docs/architecture.md.",
      "items": [
        {
          "key": "validator",
          "scope": [
            "src/validate.ts",
            "test/validation.test.ts"
          ],
          "vars": {
            "component": "input validator",
            "files": "src/validate.ts, test/validation.test.ts",
            "spec": "src/validate.ts exports validateRollups(input: unknown): ValidationResult. Input must be an array of plain objects with exactly the Rollup keys of src/types.ts; unknown keys are errors. Check every field independently and accumulate all issues {path, code, message} with paths like $[2].upgrades.threshold; descend into a container only when it has the right type. name: nonempty trimmed string, unique case-sensitive (duplicate reported on the later row). stage 0|1|2; proofSystem fraud|validity|none; permissionlessProving and forceInclusion boolean; exitWindowDays finite number >= 0, fractions allowed; upgrades {type:'securityCouncil', threshold, members} with positive safe integers and threshold <= members, or {type:'multisig'} or {type:'singleKey'}, no extra keys; dataAvailability ethereumBlobs|ethereumCalldata|external. No coercion, mutation or cross-factor rules; empty array valid. Return {valid:true, rollups} else {valid:false, errors}.",
            "tests": "test/validation.test.ts covers every rule above, aggregation of several bad fields in one row and across rows, null, non-array, non-object rows, nested shapes and prototype edge cases, and the valid empty array."
          }
        },
        {
          "key": "scoring",
          "scope": [
            "src/score.ts",
            "src/rank.ts",
            "test/scoring.test.ts",
            "test/ranking.test.ts"
          ],
          "vars": {
            "component": "scorer and ranker",
            "files": "src/score.ts, src/rank.ts, test/scoring.test.ts, test/ranking.test.ts",
            "spec": "src/score.ts exports scoreRollup(rollup: Rollup): ScoredRollup; src/rank.ts exports rankRollups(rollups: readonly Rollup[]): RankedRollup[] and imports ./score.ts. Seven factors in this order (maxPoints): stage(20) 0/1/2 gives 0/10/20. proofSystem(20) fraud 20, validity 20, none 0. permissionlessProving(10) 10 only when true AND proofSystem is not none, else 0; the reason states the live-proof gating. exitWindowDays(15) 0 gives 0; >0 and <7 gives 5; >=7 and <30 gives 10; >=30 gives 15. upgrades(15) singleKey 0; multisig 5; securityCouncil threshold 1 gives 0, else threshold/members >= 3/4 gives 15, >= 2/3 gives 10, lower 5. dataAvailability(10) ethereumBlobs or ethereumCalldata 10, external 0. forceInclusion(10) true 10. score = integer sum, 0..100. Every reason names the observed value and the rule. Ranking: score descending, then name ascending by plain string comparison, stable otherwise; rank 1..n; never mutate input.",
            "tests": "test/scoring.test.ts: every branch and boundary (exit 0, 6.99, 7, 29.99, 30; council 1/1, 1/12, 2/3, 3/4, 7/12, 12/12), proof gating, totals 0 and 100, factor order and maxPoints sum 100. test/ranking.test.ts: ordering, name tie-break, stable exact ties, ranks 1..n, empty input, non-mutation."
          }
        },
        {
          "key": "cli",
          "scope": [
            "src/cli.ts",
            "src/index.ts",
            "test/cli.test.ts"
          ],
          "vars": {
            "component": "command-line tool and public entrypoint",
            "files": "src/cli.ts, src/index.ts, test/cli.test.ts",
            "spec": "src/index.ts re-exports validateRollups (./validate.ts), scoreRollup (./score.ts), rankRollups (./rank.ts) and every public type from ./types.ts, no side effects. src/cli.ts: node src/cli.ts <input.json> [--json], --json before or after the single path; --help or -h prints usage to stdout, exit 0; no path, two paths or an unknown flag: usage to stderr, exit 1. Read the file as UTF-8, JSON.parse, validateRollups, rankRollups. Default output: a table with columns Rank, Rollup, Stage, Score, cells padded to the widest value per column, header row then one row per rollup; empty input prints the header only. --json prints JSON.stringify(ranked, null, 2) plus newline. Success: stdout only, exit 0. Read, parse or validation failure: message to stderr (validation lists every path and message), nonzero exit, nothing on stdout. Guard main so importing cli.ts runs nothing.",
            "tests": "test/cli.test.ts spawns node src/cli.ts via node:child_process on fixtures written to a temp dir: table, --json, both flag positions, help, missing file, invalid JSON, validation errors, empty array; asserts stdout, stderr and exit codes; also imports src/index.ts and asserts it prints nothing."
          }
        }
      ],
      "output": {
        "format": "markdown",
        "maxBytes": 16000
      },
      "merge": {
        "groupSize": 20,
        "brief": "Merge into one report with one section per component: files written, commands run with pass/fail counts, what could not run, deviations."
      }
    },
    {
      "name": "example-docs",
      "mode": "code",
      "brief": "{{component}} for rollup-risk, a dependency-free TypeScript library and CLI run directly by Node 24 (no build, tsconfig, install or dependencies). Start from the trunk: docs/architecture.md and src/types.ts are frozen; follow them exactly and never edit them or anything in src/. The trunk holds src/validate.ts, score.ts, rank.ts, index.ts, cli.ts and their tests: run npm test first and report its result; report a wrong score as a finding, never a fix. Change ONLY these files: {{files}}; never create or edit a file outside that list. Use only node: built-ins and explicit .ts imports; tests use node:test and node:assert/strict, no network, and delete temp files they create. Work: {{spec}} Facts (L2BEAT pages read 2026-10-27, l2beat.com/scaling/projects/ arbitrum, op-mainnet, base, zksync-era, starknet, scroll; name;stage;proofSystem;permissionlessProving;exitWindowDays;upgrades;dataAvailability;forceInclusion): {{facts}} Caveats: {{caveats}} Result (markdown): files written, exact commands run with pass/fail counts, the real CLI table output, every failure with its full message, anything you could not do and why.",
      "items": [
        {
          "key": "example",
          "scope": [
            "examples/**",
            "docs/example-sources.md",
            "test/examples.test.ts"
          ],
          "vars": {
            "component": "Write the six-rollup example, its sources and test",
            "files": "examples/rollups.json, examples/expected.json, examples/expected.txt, docs/example-sources.md, test/examples.test.ts",
            "spec": "examples/rollups.json: the six rollups in the Facts, in that order. examples/expected.json and examples/expected.txt: the exact bytes node src/cli.ts examples/rollups.json prints with --json and as the plain table; report any score that differs from the expected ones. docs/example-sources.md: the six L2BEAT URLs plus l2beat.com/scaling/summary with the read date, a mapping-notes section stating every Caveat. test/examples.test.ts: reads examples/rollups.json with node:fs, validates it, asserts rankRollups deep-equals expected.json and that node src/cli.ts on it prints expected.txt exactly; it must pass in npm test.",
            "facts": "Arbitrum One;1;fraud;true;10;securityCouncil 9/12;ethereumBlobs;true. OP Mainnet;1;fraud;true;0;securityCouncil 10/13;ethereumBlobs;true. Base;1;fraud;true;0;securityCouncil 8/11;ethereumBlobs;true. ZKsync Era;0;validity;false;5;securityCouncil 6/8;ethereumBlobs;false. Starknet;1;validity;false;7;securityCouncil 9/12;ethereumBlobs;false. Scroll;0;validity;false;0;multisig;ethereumBlobs;true. Expected scores: Arbitrum One 85, OP Mainnet 75, Base 70, Starknet 65, ZKsync Era 50, Scroll 45.",
            "caveats": "exitWindowDays is the regular-path value from L2BEAT's risk section; emergency or council paths give no window on all six; Arbitrum, ZKsync and Starknet pages show ~17.4d, ~4d, 8d elsewhere. Council values are the council's own Safe threshold; co-signers (OP Foundation, Coinbase, ZK Guardians) are not representable. Scroll swapped its council for a 3/4 team multisig on 2026-06-01, which can disable its 7-day enforced-batch forced inclusion. ZKsync's L1 queue and Starknet's council-minority path are not forced inclusion. Base: hybrid TEE+ZK fraud proofs since June 2026. A dated snapshot; scores are ours, not L2BEAT's."
          }
        },
        {
          "key": "readme",
          "scope": [
            "README.md"
          ],
          "vars": {
            "component": "Write the README",
            "files": "README.md",
            "spec": "README.md with: what rollup-risk is and what the score means; usage with no install (node src/cli.ts examples/rollups.json, the --json flag, --help, and a library import of validateRollups, scoreRollup, rankRollups from src/index.ts with the result shapes); the input schema with every field and allowed value; the scoring method as a table of factor, max points and rule, the proof-gating rule and the ranking rule, taken from docs/architecture.md; a plain statement that this is a transparent heuristic, not an L2BEAT rating, and its limits; the example input (the six rollups in the Facts as JSON, in that order) and the real table output of node src/cli.ts on that input, pasted from an actual run on a temp file; a note that examples/rollups.json and docs/example-sources.md (written by a sibling) hold the example and its dated sources and caveats; validation errors example; npm test; MIT license.",
            "facts": "Arbitrum One;1;fraud;true;10;securityCouncil 9/12;ethereumBlobs;true. OP Mainnet;1;fraud;true;0;securityCouncil 10/13;ethereumBlobs;true. Base;1;fraud;true;0;securityCouncil 8/11;ethereumBlobs;true. ZKsync Era;0;validity;false;5;securityCouncil 6/8;ethereumBlobs;false. Starknet;1;validity;false;7;securityCouncil 9/12;ethereumBlobs;false. Scroll;0;validity;false;0;multisig;ethereumBlobs;true. Expected scores: Arbitrum One 85, OP Mainnet 75, Base 70, Starknet 65, ZKsync Era 50, Scroll 45.",
            "caveats": "say in one paragraph that the example is a dated L2BEAT snapshot whose mapping caveats are in docs/example-sources.md; exitWindowDays is the regular-path value from L2BEAT's risk section; emergency or council paths give no window on all six; Arbitrum, ZKsync and Starknet pages show ~17.4d, ~4d, 8d elsewhere. Council values are the council's own Safe threshold; co-sig"
          }
        },
        {
          "key": "check",
          "scope": [],
          "vars": {
            "component": "Verify the whole trunk without changing it",
            "files": "none (scope is empty: change nothing, report only)",
            "spec": "Run and record, with output on failure: npm test; node src/cli.ts --help; the Facts as a six-rollup file through node src/cli.ts <file> and <file> --json: columns Rank, Rollup, Stage, Score, scores equal to the expected ones, JSON is RankedRollup[] with seven factors each; an empty array; a file with several bad fields (wrong stage, missing name, unknown key, council threshold > members, duplicate name): every path on stderr, nonzero exit, empty stdout; a missing file; invalid JSON; node --input-type=module -e importing ./src/index.ts: the three functions exist, nothing printed; scoreRollup on inputs covering every rubric branch in docs/architecture.md (stage 0/1/2, fraud/validity/none, gating, exit 0/5/7/30, singleKey/multisig/council 1/1, 2/3, 3/4, 7/12, blobs/calldata/external, force); package.json has no dependencies, no tsconfig, no build; rg for any import that is not node: or a relative .ts path. Each failing check is a finding with the file and expected versus actual.",
            "facts": "Arbitrum One;1;fraud;true;10;securityCouncil 9/12;ethereumBlobs;true. OP Mainnet;1;fraud;true;0;securityCouncil 10/13;ethereumBlobs;true. Base;1;fraud;true;0;securityCouncil 8/11;ethereumBlobs;true. ZKsync Era;0;validity;false;5;securityCouncil 6/8;ethereumBlobs;false. Starknet;1;validity;false;7;securityCouncil 9/12;ethereumBlobs;false. Scroll;0;validity;false;0;multisig;ethereumBlobs;true. Expected scores: Arbitrum One 85, OP Mainnet 75, Base 70, Starknet 65, ZKsync Era 50, Scroll 45.",
            "caveats": "none needed."
          }
        }
      ],
      "output": {
        "format": "markdown",
        "maxBytes": 16000
      },
      "merge": {
        "groupSize": 20,
        "brief": "Merge into one report: files written, every command with pass/fail, every finding with expected versus actual."
      }
    }
  ],
  "read": [
    "implementation/validator",
    "implementation/scoring",
    "implementation/cli",
    "example-docs/example",
    "example-docs/readme",
    "example-docs/check"
  ],
  "cancel": [],
  "wake": {
    "batches": [
      "implementation",
      "example-docs"
    ],
    "afterMinutes": 360
  },
  "finish": false
}
