Decision log
Questions that were put, and how they were answered. Append-only: an entry is never edited once its answer is recorded, only superseded by a later one.
Where a decision produced a design still in force, the reasoning lives in docs/decisions/. This log records that it was decided, and when. The two are not duplicates: a decision record explains a design, a log entry dates a choice.
Settled during the v2 work
| # | Question | Decision |
|---|---|---|
| 1 | Laravel floor | Laravel 13, revised in PR 12. Laravel 12 reaches PHP 8.5 and is still supported, but it pins symfony/process ^7.2 while Pest 5 needs ^8.1, so the cell cannot even be installed (0005) |
| 2 | Mutation: Infection or Pest? | pest-plugin-mutate: one tool fewer, one runner, one report (the quality policy) |
| 3 | Attributes and modern features | Adopt, under the criterion "removes code or removes a class of bug" (the quality policy). #[\SensitiveParameter] enters as a security fix |
| 4 | PDF engine | Migrate to tc-lib-pdf: TCPDF 6 is officially deprecated; the migration unlocks LTV and TSA (the engine migration below). Legacy driver kept as optional |
| 5 | Multi-signature | Our own incremental writer, clean-room from ISO 32000-1, since no dependency delivers this (0006) |
| 6 | Use ddn/sapp? | No, under no circumstances: not require, not require-dev, not suggest. LGPL is incompatible with porting code into an MIT package, and it is a legacy project. Conceptual reference only; clean-room implementation over tc-lib-pdf's building blocks, with an arch test enforcing the rule (0006) |
| 7 | PHP floor: 8.4 or 8.3? | ✅ 8.4, applied in PR 1. Keeps the toolchain on one Pest major and unlocks property hooks, private(set) and #[\Deprecated] |
| 8 | Multi-signature in v2.0 or v2.1? | ✅ v2.0. PR 0b closed with 3/3 valid signatures (0006), so the risk that would justify deferring did not materialize |
| 15 | PEM: parallel pipeline, or a second entry onto the existing one? | ✅ Second entry, one pipeline. A separate contract and DTO would fork CadesBuilder, CertificateVault, SealRenderer and the public Data\* shape to gain nothing: PKCS#12 is converted into PEM before anything downstream runs, so the two are not peers. Divergence is confined to the entry point, where it is real, since PEM may be two files, and its key is often unencrypted (0007) |
Left open by the plan, and how they actually landed
Recorded here because the plan closed with them unresolved, and every one was answered by the implementation without the table ever being updated. That gap is what this reorganisation exists to close.
| # | Question | Outcome |
|---|---|---|
| 9 | IncrementalSigner as the default, or only for the second signature? | Default. Preserving the original bytes from the first signature onward is what removes the silent destruction of annotations and form fields, so making it conditional would have kept the defect for anyone signing once. |
| 10 | Keep the legacy driver? | Reframed and kept. Not a driver: SignatureProfile::Legacy reproduces the 1.x /SubFilter through the same pipeline, so byte-for-byte-comparable output survives without carrying deprecated dependencies. |
| 11 | phpseclib now or later? | Never. Validation\DerReader and Pkcs7Reader read ASN.1 in-package, and cryptographic verification shipped in 2.0 rather than 2.1. See 0002. |
| 12 | Full BC layer or a clean v2? | ✅ Clean break. A 3.0 is far enough out that a shim kept "until then" is kept indefinitely, and each one constrains the design it wraps. The PHP 8.4 / Laravel 13 floor already forces a deliberate upgrade, so the marginal cost of renaming call sites is small. The Laravel package's UPGRADE.md carries the mapping. It stayed there with the repository it describes. |
| 13 | PHPStan level: max from the start, or a baseline? | Both, then neither. Measured at the time: 95 errors at level 5, 159 at level 8, 216 at max, so max cost only 57 extra baseline entries over level 8 while gating all new code at the strictest setting. The baseline was then deleted rather than shrunk; the gate is "no errors", not "no new errors". |
| 14 | Line-coverage gate? | No. Type coverage at 100% and mutation testing are more honest gates; line coverage stays informational. |
The original plan proposed a PHP 8.2 / Laravel 10 floor. It was invalidated twice: first by the real tooling requirements, then by the realisation that the PHP ceiling of older Laravel versions, not the floor, is the limiting factor. See 0005.
Answered by 2.2
The four records that were proposed and unbuilt when the specification was split. Each is now implemented, and each was decided by a measurement rather than by the plan.
| # | Question | Outcome |
|---|---|---|
| 16 | Cross-reference streams: append a stream, or a classic table plus /XRefStm? | ✅ Append a stream. Measured, not assumed: appending a classic table to a document whose latest section is a stream produced a file poppler reported as carrying no signatures at all. Reading shipped one release before writing, with signing refusing in between, so the gap was a loud refusal rather than silent corruption (0009) |
| 17 | Does validation have to read back everything signing writes? | ✅ Yes, and the asymmetry was real: the package wrote B-LTA material it could not then report. Signing time came from /M rather than the PKCS#9 attribute, which measurement showed absent (0010) |
| 18 | Certification: is a byte-level test enough? | ❌ No, and it still is not. All three levels write and read back, and poppler confirms the signature verifies, but pdfsig does not surface /DocMDP, so reader enforcement is unverified and needs Adobe Reader or ITI Validar. Recorded as an open gap rather than closed by a passing test (0012) |
| 19 | Signing into a pre-placed field: fill it, or fall back to appending when it is missing? | ✅ Fill, and refuse rather than fall back. The fallback is exactly the failure the feature prevents, and it would happen quietly: a signature that is valid and in the wrong place, with the template's field still empty (0013) |
One defect surfaced that belonged to none of them. DocumentReader::findFirstPage() scanned a fixed 400-byte window from each object's offset, which in a compact document reaches the objects that follow, so the catalog was reported as the first page. It was latent in any document whose objects sit close together, and only a 434-byte fixture built for 0009 was small enough to expose it.