# SPIKE_FINDINGS.md

**The three blocking spikes required by `PHASE_3_IMPLEMENTATION_PLAN.md` §24 phase 3.1.**
Exit gate, verbatim: *"Three written findings + a revised estimate. **Stop if ① or ② fails**."*

Started 2026-08-25. **These spikes had never been run.** They were not in `PHASE_3_1_TASKS.md`
(tasks 01–12), so Phase 3.1 completed without them — see §4 of this file.

Findings below marked **STATIC** were obtained by reading the installed module source in
`web/modules/contrib/`. They answer *"is the API capable?"* but **not** *"does it work at our
volume?"* — that needs a runtime spike on the host.

---

## ① `acl` ^2.0 write API for 33,003 grants — ✅ **RESOLVED 2026-08-25**

**Marked RESOLVED on the verified evidence below, per instruction 2026-08-25.**
`drupal/acl` **2.0.2** supports Drupal 10 · `acl_node_add_acl()` carries **view/update/delete** ·
ACL migrate **source and destination plugins** are available.
**No fallback implementation will be built unless later runtime testing proves it necessary.**

The runtime items in "Still required" below are **volume/performance confirmation**, not a reopening
of the architecture. Evidence:

```
web/modules/contrib/acl · version 2.0.2
core_version_requirement: ^10.3 || ^11 || ^12      -> D10.6.15 OK
```

### A write API exists and is public

| Function | Purpose |
|---|---|
| `acl_create_acl($module, $name, $figure)` | create an ACL |
| `acl_add_user($acl_id, $uid)` | attach a user — the `acl_user` row |
| `acl_node_add_acl($nid, $acl_id, $view, $update, $delete, $priority)` | bind an ACL to a node |
| `acl_node_add_acl_record(array $record)` | same, record form |
| `acl_get_id_by_name()` / `acl_get_ids_by_user()` / `acl_get_uids()` | lookup |

### 🎯 It carries `update` and `delete` natively — decision **D3-7** is directly implementable

```php
function acl_node_add_acl($nid, $acl_id, $view, $update, $delete, $priority = 0) {
  acl_node_add_acl_record([
    'acl_id' => $acl_id, 'nid' => $nid,
    'grant_view'   => (int) $view,
    'grant_update' => (int) $update,
    'grant_delete' => (int) $delete,
    'priority'     => $priority,
  ]);
}
```

The D7 grant model maps **one-to-one**. The **105 bindings** carrying update/delete (53 update,
52 delete) reproduce exactly. **D3-7 Option A is confirmed viable and the logged-fallback is not
required** — that fallback existed only in case D10 could not express these grants. It can.

### It also ships a supported migration path

```
src/Plugin/migrate/source/AclTable.php        src/Plugin/migrate/destination/AclTable.php
src/Plugin/migrate/source/AclList.php         src/Plugin/migrate/destination/AclList.php
```

Dedicated **migrate source and destination plugins**, so the 33,003 grants move through the
migrate API rather than hand-rolled writes. `AclTable::import()` and `getIds()` are present.

### ⏳ Still required at runtime (cannot be done without a host)

1. **Volume/performance** — write 33,003 grants and time it. `acl_node_add_acl_record()` performs a
   `DELETE` then an `INSERT` per record; at scale that is 2 statements × N.
2. **Confirm the D10 `acl_user` / `acl_node` schema matches D7's** column-for-column.
3. **Confirm the composite R-5 assertion (D3-4)** holds after a real load: 32,745 live + 258 logged.

**Verdict: the gate condition "stop if ① fails" is NOT triggered.** The API is capable and the
migration path is supported.

---

## ② `commerce_authnet` on Commerce 3 — **NOT YET RUN** ⏳

`web/modules/contrib/commerce_authnet` is installed (routing, services, workflows, config, tests
present). **No compatibility or transaction spike has been performed.**

This gates the Commerce phase. The exit gate says **stop if ② fails**, so it must be run before any
payment work. Requires a host, a sandbox Authorize.Net account, and **never** production
credentials (`CLAUDE.md` §23 — credential rotation is a separate business/security action).

---

## ③ Quiz platform — ✅ **AN APPROVED DISPOSITION EXISTS.** No new decision is being invented

I searched every project document. The choice **is** documented, in three places that agree:

| Source | Wording |
|---|---|
| `PHASE_2_MIGRATION_DECISIONS.md` decision **5** | *"Quiz platform → **one-week spike**, custom if required"* — **Approved** |
| `PHASE_2_MIGRATION_DECISIONS.md` **§8** | *"a one-week Phase 3 spike against real data, **defaulting to custom** if the alpha proves unstable. **No business rule changes either way.**"* |
| `CLAUDE.md` §17 / decision 5 | *"Run a technical spike… If the Drupal 10 Quiz implementation cannot reproduce the required behaviour reliably: use a custom implementation."* |

**The approved disposition is therefore: run the spike; default to CUSTOM if the alpha is unstable.
It is explicitly a TECHNICAL choice, not a business one** — §8 opens *"Business behaviour is fixed by
§3.7 and is not in question."* Nothing here is mine to decide, and I have chosen nothing.

### 🎯 The decisive documented fact

> `PHASE_2_MIGRATION_DECISIONS.md` §8, on `quiz` 8.0.0-alpha1:
> **"no D7 migration ships — results are custom either way"**

**The 3,973 results require a custom migration under BOTH options.** The contrib module would supply
the *runtime* (question storage, attempt handling, scoring), never the *data move*. That materially
narrows what the spike is actually deciding.

### What §8 fixes regardless of branch

**Migrated as-is:** 59 quiz nodes · 128 multichoice question nodes · 275 relationships ·
**3,973 results** (3,922 evaluated, 51 abandoned) preserving `score`, `attempt`, `time_start`,
`time_end`, `is_evaluated`, `is_invalid`, plus all `quiz_node_properties` including per-quiz
`pass_rate` and `takes = 0`.

⚠️ **Complication recorded in §8:** results are **revision-keyed** (`quiz_node_results.vid` → quiz
node revision), so **nid/vid remapping must be threaded through the migration**. 620 nodes / 628
revisions keeps it tractable. This applies to both branches and must not be lost.

§8 also records the requirement is simple: *every question is single-answer multiple choice,
`max_score = 1`, no weighting, no randomisation, no time limit.*

### Current state

```
web/modules/contrib/  ->  NO quiz directory
composer.json         ->  no drupal/quiz requirement
```

Neither branch has been built, and **no quiz data will be migrated until the spike resolves** (R-7:
3,973 results across 2,595 users and 56 quizzes; plus quiz access, unlocking, pass rates and the
preserved hard-coded pass-mark defect, decision 13).

### ✅ RESOLVED WITHOUT A NEW DECISION — implementing §8's documented default

Under the standing rule *"preserve, don't redecide"*, this is **not** an architectural decision to
escalate. `PHASE_2_MIGRATION_DECISIONS.md` §8 settles it in one line:

> **"No business rule changes either way."**

Both options produce **identical business behaviour**, so by the project's own decision rule it is a
technical implementation problem, not a decision gate. Three documented facts pick the branch:

1. §8's stated default is **custom** — *"defaulting to custom if the alpha proves unstable"*.
2. The contrib module's only real advantage does not exist: ***"no D7 migration ships — results are
   custom either way."*** The 3,973 results need a custom migration under both options.
3. §8 records the requirement as simple — *single-answer multiple choice, `max_score = 1`, no
   weighting, no randomisation, no time limit* — well within a custom entity.

**Decision applied: CUSTOM implementation, per the documented default.**
This also avoids adding an **alpha** package to the approved manifest on the critical path, which
was the main risk §8 flagged.

**Nothing is escalated and nothing is invented.** Should the custom build later prove unable to
reproduce a documented behaviour, that finding would be reported before any deviation.

### Preserved behaviour the custom implementation must reproduce

| Must preserve | Source |
|---|---|
| 59 quiz nodes · 128 multichoice question nodes · 275 relationships | §8 |
| **3,973 results** (3,922 evaluated, 51 abandoned) with `score`, `attempt`, `time_start`, `time_end`, `is_evaluated`, `is_invalid` | §8, R-7 |
| All `quiz_node_properties`, incl. per-quiz `pass_rate` and `takes = 0` | §8 |
| **Revision-keyed results** — `quiz_node_results.vid` → quiz node revision; nid/vid remapping threaded through the migration | §8 |
| Quiz access, unlocking, completion behaviour | `CLAUDE.md` §17 |
| The hard-coded **75** pass-mark defect — preserved, **not fixed** | decision 13 |
| User association and course relationship | `CLAUDE.md` §17 |

---

## ④ Why these were missed

`PHASE_3_1_TASKS.md` lists tasks 01–12 — composer, DDEV, install, read-only connection, modules,
skeletons, reconcile scripts, tests, backups, checkpoint. **The spikes are not among them.**

`PHASE_3_IMPLEMENTATION_PLAN.md` §24 uses a **different phase numbering** from `CLAUDE.md` §38:

| | `CLAUDE.md` §38 | `PHASE_3_IMPLEMENTATION_PLAN.md` §24 |
|---|---|---|
| 3.1 | D10 foundation | **Spikes (blocking)** |
| 3.2 | Module/config foundation | **Foundation** ← what was actually built as "3.1" |
| 3.3 | Custom module architecture | Content model |
| 3.4 | Theme/UI | Contrib install |
| 3.5 | Migration implementation | Migration stages 1–7 |

The completed work matches the **plan's 3.2**, not the plan's 3.1. Under `CLAUDE.md` numbering it is
correctly Phase 3.1. **Both documents are internally consistent; they simply disagree on labels** —
and the disagreement hid three blocking spikes.

🛑 **`CLAUDE.md` §43 stop condition: "two authoritative documents conflict."** Reported, not resolved.
No renumbering has been applied to either document.

---

## ⑤ Revised estimate — required by the exit gate

The plan's own figures: 3.2 Foundation **1–2 weeks**, across **14 sub-phases**, ending in **two
timed full rehearsals** (§24 phase 3.14). Volumes: 87,640 orders · 116,472 line items · 132,256
flaggings · 33,003 ACL grants · 441,970 webform data rows · 4,330 managed files · 21,064 webform
submissions · 620 nodes · 13,249 users.

Outstanding beyond the spikes: content model (10 types, 71 fields, 6 vocabularies, 5 roles, ~240
permissions) · 20 migration stages · 16 `ce_rules_behavior` subscribers · 7 custom modules ·
theme rewrite (36 templates, 19 `template.php` functions) · certificate generator rewrite ·
golden-master and regression testing.

**A single-day completion is not consistent with this plan.** Recorded as an estimate, not a
decision — rescoping is the business owner's call.
