-
Notifications
You must be signed in to change notification settings - Fork 0
Determinism and Evidence
PHP Upgrade Preflight is designed to produce traceable, stable planning output from the evidence available to one analysis run.
Deterministic does not mean omniscient.
It does not mean that Packagist, private repositories, Composer caches, executable versions, or network state are controlled automatically.
It means the tool normalizes the inputs and outputs it owns and records uncertainty for important limits it cannot remove.
Developers need to reproduce a surprising finding.
Technical managers need to know whether two reports are comparable.
Automation needs stable fields, ordering, identifiers, and schemas.
Reviewers need to trace a conclusion back to evidence.
These needs are related but not identical.
| Idea | Meaning |
|---|---|
| Determinism | Equivalent controlled inputs and observations produce stable modeled output |
| Reproducibility | Another run can recreate sufficiently equivalent inputs and environment |
| Evidence | A structured record supporting a report claim |
| Confidence | How directly the available evidence supports that claim |
A deterministic heuristic remains a heuristic.
A high-confidence solver result can still depend on repository state.
A reproducible report can correctly contain unknown.
Evidence defines five classes.
| Class | Constant | Typical source | Example |
|---|---|---|---|
| E1 | E1_SOLVER |
Composer solver/execution evidence | A reproducible dependency conflict |
| E2 | E2_PACKAGE_METADATA |
Composer manifest or lock metadata | Locked version or root requirement |
| E3 | E3_PROJECT_SOURCE |
Parsed project source | Symbol use at a file and line |
| E4 | E4_MAINTAINER_DOCUMENTATION |
Maintained adapter knowledge with sources | Laravel upgrade-guide requirement |
| E5 | E5_HEURISTIC |
Deterministic inference | Risk or planning hint based on modeled signals |
The class is not a ranking that automatically decides severity.
Severity describes impact.
Confidence describes support.
Evidence class describes source type.
Every evidence item contains:
{
"id": "example-1",
"class": "E2",
"summary": "The lock file contains vendor/package 1.4.0.",
"confidence": "high",
"context": {
"package": "vendor/package",
"version": "1.4.0"
}
}The example shape matches Evidence::toArray().
Evidence summaries and contexts are redacted during construction.
This prevents a later consumer from accidentally serializing the unredacted original.
EvidenceLedger owns evidence registration for one analysis.
add() creates a new item.
Its namespace must match:
^[a-z][a-z0-9_-]*$
IDs are namespace plus sequence.
composer-blocker-1
composer-blocker-2
laravel-stage-target-1
Sequence is maintained separately per namespace.
Existing IDs are skipped when finding the next available sequence.
Use add() when separate observations deserve separate evidence records.
Use addOnce() when content-identical evidence in one namespace should be reused.
addOnce() compares:
- evidence class;
- summary;
- confidence;
- context;
- namespace prefix.
It builds a SHA-256 content bucket from serialized values for efficiency.
It still applies strict equality to candidates in that bucket.
If a value cannot be serialized, it falls back to an exhaustive scan.
The fallback preserves behavior rather than changing deduplication semantics.
Evidence IDs are stable only when evidence creation order and content remain stable.
Adding a new earlier item in the same namespace can shift later sequence numbers.
Therefore:
- iterate inputs in deterministic order;
- use focused namespaces;
- avoid creating unused evidence;
- do not treat an evidence ID as a permanent database identity across schema changes.
Within one canonical report, IDs are precise foreign keys.
The report enforces both directions of evidence integrity.
First, every referenced ID must exist.
Second, every registered evidence item must be referenced.
EvidenceLedger::validateReferences() rejects missing references.
It also rejects orphan evidence.
UpgradeReport gathers references from:
- blockers;
- source inventory;
- actionable source impact;
- framework findings;
- framework guidance and hops;
- root constraint changes;
- staged resolution;
- plan stages;
- uncertainties that contain explicit evidence references.
This makes the ledger a support graph, not an appendix of unused observations.
flowchart LR
E1[composer-blocker-1: E1 solver] --> B[Blocker]
E2[package-metadata-1: E2] --> B
E3[source-usage-1: E3] --> I[Source impact]
E2 --> I
E4[laravel-hop-1: E4] --> G[Framework guidance]
G --> F[Framework finding]
E3 --> F
One item may support several claims.
One claim may cite several evidence items.
No evidence item may remain disconnected.
PHP arrays preserve insertion order.
That makes traversal order visible in JSON.
The code sorts at ownership boundaries where unordered input is possible.
Examples include:
- package names in
LockDiffBuilder; - package families attached to changes;
- installed integration names;
- framework guidance;
- source files in
SourceUsageScanner; - symbol declarations;
- autoload paths and files;
- ownership names and mapping types;
- relevant source-impact package maps;
- provider names in stage-plan conflict handling;
- platform decisions in fingerprints.
Do not rely on filesystem enumeration order.
Do not rely on Composer metadata object order when semantics are map-like.
Not every array should be sorted.
A map has key-based semantics and can be canonicalized by key.
A list may encode meaningful priority or execution order.
Examples of meaningful list order:
- scenario order;
- stage order;
- attempt order;
- package dependency path;
- source occurrence order;
- report section order.
Sorting a meaningful list can change behavior.
Canonicalization must know the difference.
ScenarioSelector constructs candidates in a fixed order.
It then deduplicates by an execution key containing:
- normalized targets;
- effective with-all-dependencies flag;
- minimal-changes flag.
Baseline validation has its own fixed key.
The selector does not run redundant scenarios merely because they have different display names.
Successful target-feasibility candidates are ranked by:
- package-change count;
- strategy rank;
- scenario index.
Strategy rank is exact target, then minimal changes, then with all dependencies.
This makes selection stable when multiple Composer strategies succeed.
The selected candidate determines the direct lock diff.
SourceUsageScanner canonicalizes and sorts discovered file paths.
Visitors retain AST-derived line numbers and symbols.
SymbolDeclarationVisitor sorts declarations by symbol and type.
AutoloadOwnershipIndexBuilder sorts locked packages, paths, and files.
SymbolOwnershipIndex sorts owner names and mapping types.
These choices prevent operating-system directory iteration from changing report order.
Shareable reports should not depend on an absolute checkout location.
PathExposurePolicy replaces relevant roots with markers:
| Marker | Meaning |
|---|---|
[PROJECT_ROOT] |
Analyzed project root |
[REPORT_OUTPUT] |
Requested report destination |
[LOCAL_REPOSITORY] |
Local Composer repository reference |
[ANALYZER_WORKSPACE] |
Temporary analyzer workspace |
It recognizes path separator variants, escaped forms, and encoded forms.
Longer matching paths are processed before shorter ones.
This avoids a parent path hiding only part of a more specific path.
UpgradeReport::toArray() builds the canonical array.
It then calls PathExposurePolicy::sanitizeCanonicalReport().
Sanitization:
- identifies project, output, and local-repository paths;
- recursively replaces path occurrences;
- forces known project and output fields to markers;
- applies structured sensitive-output redaction.
Both keys and values can be redacted.
JSON-serializable objects are traversed safely.
Recursive object cycles produce a redacted marker instead of infinite recursion.
SensitiveOutputRedactor uses stable markers.
Examples are:
-
[REDACTED]; -
[REDACTED_TOKEN]; -
[REDACTED_URL]; -
[REDACTION_FAILED].
It recognizes Composer auth assignments, authorization headers, credential-bearing URLs, named credential fields, bearer/basic tokens, and common token formats.
Structured keys known to be sensitive cause their values to be withheld.
A regex failure returns a failure marker rather than leaking the original value.
Composer stdout and stderr are external and potentially large.
OutputExcerpt::bounded() truncates by byte budget while preserving valid UTF-8 boundaries.
Bounded output supports stable report sizes.
It also reduces accidental exposure surface.
Bounding does not replace redaction.
CandidateLockFileReader fingerprints the bytes Composer wrote.
It normalizes CRLF and CR line endings to LF before SHA-256.
This avoids a candidate lock fingerprint changing only because of line-ending convention.
CandidateLockEvidence records:
- lowercase SHA-256;
- Composer
content-hashwhen present; - package count.
The byte fingerprint and semantic package data answer different questions.
ProjectStateFingerprint identifies state used between staged analyses.
It records SHA-256 values for:
- manifest;
- lock;
- effective platform;
- execution policy;
- combined state.
Before hashing, it sanitizes private paths.
It canonicalizes map keys recursively.
It preserves list order.
It normalizes separators after exposure markers.
It excludes lock content-hash from the semantic lock digest.
Temporary workspaces rewrite relative local repositories to absolute paths.
Composer can therefore write a location-dependent content-hash.
The manifest already has its own semantic fingerprint.
Keeping the derived lock field in the staged state fingerprint would make the same project state differ by temporary directory.
Candidate lock evidence still records the value Composer actually wrote.
The platform digest includes:
- exact analysis PHP;
- whether the profile is closed-world;
- explicit semantic platform decisions.
Platform package decisions are sorted by package name.
In a complete profile, absent packages that are not toolchain-bound can be omitted from the digest because closed-world state already expresses their absence.
This prevents redundant representation from changing identity.
JsonReportWriter calls UpgradeReport::toArray().
It encodes with:
- pretty printing;
- unescaped slashes;
- exceptions on encoding failure;
- one trailing newline.
The top-level field order is controlled by UpgradeReport.
Schema version is controlled by ReportMetadata::SCHEMA_VERSION.
Consumers should validate the schema version before interpreting fields.
MarkdownReportWriter consumes canonical report data.
It does not run Composer or adapter rules.
Markdown formatting can differ while conclusions must remain equivalent to JSON.
If the formats disagree semantically, JSON and the schema remain canonical and the writer difference is a bug.
| Variable | Controlled or recorded? |
|---|---|
| Normalized request | Controlled by model validation |
| Scenario order | Controlled by selector |
| Source file order | Controlled by scanner sorting |
| Composer executable/version | Configured and version evidence recorded when available |
| Repository metadata at a point in time | External; can change |
| Network availability | External; restricted mode can intentionally remove it |
| Private repository credentials | External and redacted |
| Host extension set | Not accepted as target truth unless represented by request/profile evidence |
| Temporary path | Normalized in shareable output |
| External process duration | Recorded but inherently variable |
Two reports can differ legitimately when an uncontrolled variable changes.
When two runs differ, compare in this order:
-
metadata.schema_versionand tool version; - normalized request summary;
- Composer execution provenance;
- platform provenance;
- scenario outcomes and Composer version;
- candidate lock fingerprint and package count;
- project-state fingerprints for stages;
- uncertainties;
- evidence contexts;
- final findings and assessments.
This identifies cause before debating the final risk label.
Run A resolves vendor/package to 2.1.0.
Run B resolves it to 2.1.1.
The request can be identical.
The tool can still be deterministic relative to each observed repository state.
The candidate lock fingerprints reveal different external evidence.
Pinning repository content is a reproducibility responsibility outside the analyzer.
One developer analyzes /home/alex/shop.
Another analyzes D:\work\shop.
Canonical reports use [PROJECT_ROOT].
Portable staged fingerprints normalize path separators after markers.
Location alone should not manufacture a different semantic project state.
Debug paths can still differ when debug mode intentionally exposes them.
A compatible-mode run succeeds using cached credentials and network access.
A restricted-mode run cannot fetch a package.
Those runs have different execution policies.
The difference is real and should affect provenance or fingerprints.
Do not label the restricted operational failure as a dependency blocker.
High confidence means evidence directly supports the modeled statement.
Medium confidence means useful support exists with material limits.
Low confidence means the conclusion is tentative.
Read the actual context and uncertainty.
Do not translate confidence labels to percentages.
Do not use confidence to erase contradictory evidence.
Uncertainty is a required output when the system lacks reliable support.
Examples include:
- Composer executable unavailable;
- scenario timeout;
- source parse failure;
- unreadable candidate lock;
- incomplete platform evidence;
- current PHP unknown for a diagnostic scenario;
- adapter rule exception;
- stage guidance gap;
- workspace cleanup failure.
Recording uncertainty is more deterministic than guessing.
- Sort map-like external input before iteration.
- Preserve meaningful list order.
- Normalize paths before hashing or sharing.
- Use stable vocabulary constants.
- Create evidence at the observation boundary.
- Reference every created evidence item.
- Never reference an unregistered ID.
- Deduplicate only when semantic equality is proven.
- Keep external failures distinct from solver findings.
- Add snapshot tests when canonical output changes.
- Add cross-platform tests for path or fingerprint changes.
- Update schema and Wiki in the same behavior change.
- Is the evidence class correct?
- Is the namespace valid and specific?
- Is creation order stable?
- Is summary concise and factual?
- Is context structured rather than embedded prose?
- Are secrets and paths sanitized?
- Does a report claim reference the ID?
- Could identical observations use
addOnce()? - Are confidence and severity kept separate?
- Are schema and snapshots updated if shape changes?
- Is the input a list or a map?
- If map-like, where is sorting owned?
- If list-like, is order semantically documented?
- Can filesystem order leak into output?
- Can an absolute path leak into a digest?
- Can line endings change a fingerprint?
- Can external process noise change a stable field?
- Is variable evidence recorded as provenance or uncertainty?
- Does JSON retain canonical authority?
- Do tests cover at least two input orderings?
PHP Upgrade Preflight — common product and monorepo Wiki · Common repository
- Home
- Key Concepts
- Package Map
- Class and Service Index
- Getting Started
- CLI Reference
- Artisan Command
- Reading the Report
- Safety and Trust Boundaries
- Troubleshooting and FAQ
- Architecture Overview
- Core Package Guide
- Core Analysis Pipeline
- Core Service Reference
- Determinism and Evidence
- Report Schema
- CLI Package Internals
- Laravel Package Internals
- Test Adapters
- Writing a Framework Adapter
- Laravel Adapter Internals
- Contributing
- Roadmap and Status
- Tools Reference
- Quality and Release Tooling
- Release Wiki Strategy