Status: Phases 1 to 4 implemented, with the real-wire fixture gate in 2.2 still
open and phase 5 outstanding. Updated 2026-09-09. Based on the
codebase review of commit e2b5090 on 2026-09-08.
PIACE is published but has no deployments. Treat 0.5.0 as the first deployment target: choose the correct interfaces, configuration, and artifact formats directly. Update producers, consumers, fixtures, and examples together.
The objective is a comparison whose identity, content evidence, disclosure, provenance, and outcome can be trusted. Preserve the separate advisory assessment command and the single comparison outcome reducer.
Complete the phases in this order. Within each phase, follow the numbered steps. Each step includes the findings it closes, implementation scope, and the evidence required for completion.
- Fix sensitivity handling, URL credentials, and input identity validation.
- Redesign file-content evidence and block hazardous v3 configurations.
- Correct aggregation disclosure, evidence preservation, and capture provenance.
- Repair command configuration, artifact publication, and resource limits.
- Validate real wire formats, align documentation and CI, and prepare 0.5.0.
Before changing an existing module, read its implementation, callers, tests, and package documentation. Add a regression assertion for each reproduced failure at the narrowest interface that exercises the behavior. Update the corresponding implementation documentation in the same step; phase 5 checks the entire published contract for agreement.
Mark a step complete only when its acceptance criteria pass. Record relevant commands and fixture provenance beside its checkbox. Live integration evidence must be distinguished from synthetic test results.
- Implemented and verified 2026-09-09. Synthetic compiler/PuppetDB
sensitivity fixtures cover publication, debug output, and inference;
go test -race ./...passes. Deployment representation coverage remains 5.1.
Finding: high severity, reproduced. internal/normalize/wire.go drops
resource-level sensitive_parameters. internal/diff/redact.go recognizes
Pcore wrappers only. A User resource with sensitive_parameters: [password]
and a plaintext parameter value exposed the changed password in the JSON
report and the inference request.
Changes
- Represent resource-level sensitivity in the normalized model and decode it from supported compiler and PuppetDB representations.
- Preserve recursive Pcore wrapper detection. Define malformed sensitivity metadata as an explicit validation failure rather than silently discarding it.
- Treat a parameter as sensitive on both sides when either side marks it sensitive. Apply the same rule to additions and removals when phase 3 adds their evidence.
- Separate raw comparison evidence from the publishable projection. Make the transition explicit so serialization, renderers, and inference cannot receive the raw representation accidentally.
- Keep grouping fingerprints internal. Redaction must preserve real differences without publishing secret-derived fingerprints.
Acceptance
- Plaintext values accompanied by
sensitive_parametersremain absent from text, JSON, HTML, diagnostics, normal debug output, and inference requests. - Sensitivity introduced or removed on one side still protects both values.
- Nested wrappers, configured selectors, and sensitive File content are covered.
- Different sensitive changes remain distinct when grouped.
- Implemented and verified 2026-09-09. Local TLS and adversarial metadata
tests cover credential rejection, authority enforcement, and safe diagnostics;
go test -race ./...passes.
Finding: high severity, reproduced. HTTPS endpoint validation accepts
userinfo. Deleting Authorization before sending does not prevent Go from
constructing Basic authentication from URL.User. A local TLS probe received
that header and observed a plaintext password in the debug event URL.
Scope: internal/config/resolve/validate.go, internal/transport/,
internal/inference/, cmd/piace/debug.go.
Changes
- Reject endpoint userinfo during configuration validation and client construction. Errors must identify the invalid field without echoing credentials.
- Enforce the compiler/PuppetDB client's configured HTTPS authority on every request, including initial requests and redirects.
- Define a safe URL projection for debug events and errors. Retain explicitly supported nonsecret query metadata, such as an inference API version, without assuming arbitrary query values are safe to print.
- Treat response member names and content-type strings as untrusted metadata: bound their length and escape control characters before displaying them.
- Preserve the dedicated inference bearer-token client and its redirect policy.
Acceptance
- Userinfo cannot produce an Authorization header or disclose a credential in configuration errors, transport errors, or debug output.
- Cross-authority requests and redirects are rejected before credentials leave.
- Legitimate configured inference requests retain their required authentication.
- Malicious metadata cannot inject extra log lines or unbounded debug output.
- Implemented and verified 2026-09-09. Synthetic wrong-target responses,
snapshot envelopes, malformed factsets, and trusted-fact inputs are rejected;
go test -race ./...passes.
Finding: high severity. PuppetDB factsets and baselines are not checked
against the requested certname. Snapshot envelopes are checked, but payload
certnames are not. Trusted facts require a nonempty certname and a present
authenticated field without verifying their meaning or target agreement.
Reproduced: a valid, checksummed catalog envelope for requested-node
containing a payload for different-node was accepted.
Scope: internal/puppetdb/, internal/compiler/wire.go,
internal/snapshot/store.go, and workflow callers.
Changes
- Require agreement between requested target, response certname, snapshot envelope target, payload certname, and supplied trusted-fact certname.
- Validate trusted-fact field types and supported authentication values against the actual Puppet wire contract. Preserve the explicit compiler-lookup policy.
- Validate factset containers and entries before flattening: distinguish an explicitly empty factset from missing or malformed data, reject duplicate fact names, and reject malformed entries.
- Validate required snapshot metadata and compiler API values explicitly.
- Apply consistent failure classification before compilation or comparison.
Acceptance
- Wrong-target responses and inconsistent envelopes fail before candidate compilation or diffing, even when checksums and environments match.
- Invalid trusted facts cannot be labeled as validated input.
- Missing fact collections and duplicate fact names cannot silently become an empty or overwritten factset.
- Valid file-backed and PuppetDB-backed inputs follow the same identity rules.
- Implemented and verified 2026-09-09.
acceptance_phase2_test.goandinternal/filecontent/phase2_test.gocover independent contexts, unchanged sources, missing historical evidence, capture/reuse, static metadata, mixed representations, and directory/recursive limitations. Existing diff tests retain excluded content diagnostics.go test -race ./...passes. Static fixtures are documentation-derived, not live deployment captures.
Finding: high severity, reproduced. The differ resolves file content only
when a content-bearing parameter changes. Identical source strings cause
zero retrievals and no difference. When retrieval does run, both sides use the
candidate environment and can incorrectly compare equal.
Scope: internal/model/, internal/filecontent/, internal/diff/,
internal/compare/, internal/compiler/, and internal/snapshot/.
Changes
- Replace the single-environment content interface with explicit baseline and candidate evidence inputs. Each side identifies its source, environment, and any immutable content or capture identity available.
- Define the evidence hierarchy for inline content, validated checksums, captured digest evidence, and live retrieval. Keep evidence source and verification status visible in the result.
- Evaluate source-backed content even when the source URL is unchanged.
- Distinguish current environment content from historical baseline content. Extend capture to retain digest evidence needed to compare source-backed historical snapshots. Define missing historical evidence as indeterminate; fetching today's baseline environment must not imply historical verification.
- Inspect supported static-catalog metadata and retain the content evidence needed by this model instead of discarding it during compiler decoding.
- Keep directory and recursive-source semantics explicit. Unsupported byte comparisons must state their limitation and cannot silently imply unchanged content. Preserve reference changes when that is the evidence available.
- Specify how exclusions interact with retrieval and diagnostics. Keep requested failures visible, while ensuring the documented exclusion policy matches the implemented outcome rule.
Acceptance
- A module file edit with an unchanged
sourceURL is detected when verified evidence exists, and is explicitly indeterminate otherwise. - Baseline and candidate retrievals use their own evidence contexts.
- A historical snapshot cannot be declared equivalent using unrelated current bytes. Missing historical evidence cannot yield a clean result.
- Inline, source-backed, static-metadata, directory, recursive, and mixed representation cases have end-to-end coverage.
- Real-wire verification pending; implementation and synthetic regressions
pass as of 2026-09-09. Ordered missing-source fallback, fail-closed errors,
authority rejection, traversal checks, supported digest lengths/prefixes,
and invalid-checksum disclosure are covered by
go test -race ./....internal/compiler/testdata/README.mdrecords the static fixture's origin: a reduced published Puppet example, not a live capture. Acquire reproducible supported-version wire fixtures under 5.1 before closing this checkbox.
Findings: source arrays use the first element instead of the first existing
source; explicit source authorities are ignored; arbitrary nonempty strings
are accepted as compiled checksums. The checksum probe published
audit-new-secret as a SHA-256 digest without a diagnostic.
Scope: internal/filecontent/evidence.go, resolver.go, and interfaces.
Changes
- Preserve ordered source lists through resolution. Try subsequent sources only for a verified absence, not for authentication or transport failures.
- Handle recursive source-selection rules separately from single-file fallback.
- Define supported source authorities. Reject unsupported explicit authorities with safe diagnostics rather than silently retrieving from a different server.
- Validate checksum representation, algorithm, encoding, and length using real wire fixtures. Normalize supported equivalent forms before comparison.
- Publish digest fields only after validation or local hashing. Retain the existing protection against source-path traversal.
Acceptance
- A missing first source and existing second source resolve correctly.
- Authentication and retrieval failures are not hidden by fallback.
- Unsupported authorities never produce purported evidence from the compiler.
- Invalid checksum strings cannot reach a report as verified digest evidence.
- Implemented and verified 2026-09-09. Local TLS/CLI tests reject direct-v3
and fallback PuppetDB baselines before network activity; successful and failed
v3 comparison/capture paths expose effective API and persistence warnings.
Existing request tests assert v4 disables both persistence operations.
go test -race ./...passes. Fallback is explicit policy for empty-body or literalNot FoundHTTP 404 only, with an ambiguity warning. Neither that response nor HTTP 501 is claimed to prove v4 unsupported; 501 never falls back.
Finding: high severity, confirmed by code inspection and already acknowledged for direct v3 in README.md. Both direct v3 and permitted v4-to-v3 fallback can compile against a PuppetDB baseline. The compiler can persist candidate facts and catalogs, affecting later comparisons and other PuppetDB consumers.
Scope: target resolution, compiler fallback, comparison and capture reporting.
Changes
- Reject a PuppetDB comparison baseline whenever the selected policy can invoke v3, whether directly or through fallback.
- Keep this validation specific to comparison. Capture has a different purpose and must report the side effects of the API it actually invokes.
- Report v3 persistence effects alongside trusted-fact semantics wherever v3 executes. A file baseline protects the comparison input, not other consumers.
- Revisit the claim that status 404 or 501 proves v4 is unsupported. Validate the supported fallback signals against server behavior and document any remaining ambiguity. Authentication and malformed-response failures must not trigger fallback.
Acceptance
- Invalid direct-v3 and fallback configurations fail before network activity.
- Every accepted v4 request disables both fact and catalog persistence.
- Every executed v3 path exposes effective API and persistence consequences.
On 2026-09-09, local macOS verification passed: go vet ./...,
go build ./..., and go test -race ./.... Formatting was applied with
gofmt -w cmd/piace internal. Tests use local TLS servers and synthetic
infrastructure values. No live Puppet/OpenVox deployment was changed or used
for these new tests; the supported-version conformance matrix remains open.
Phase 2 necessarily implements part of 3.3: capture reports actual compiler effects, stores the effective API, and retains static/captured content evidence inside the payload checksum. The full requested/effective/fallback provenance envelope and snapshot-fidelity audit in 3.3 are still outstanding.
- Implemented and verified 2026-09-09. Synthetic regressions in
internal/aggregate/disclosure_test.goexercise the differ, aggregation, JSON/text/HTML publication, and inference payload construction with selectors, sensitivity on either catalog, and nested wrappers. Target renaming and reordering preserve disclosure and references.build_test.gocovers recursive marker union without member mutation and separation of distinct sensitive fingerprints. Local macOS verification passed:go test -race ./...,go vet ./...,go build ./..., andgit diff --check. These tests establish local disclosure behavior, not live wire conformance. File-content projection remains part of 3.2.
Finding: medium severity, reproduced. Aggregation groups equal raw changes,
then publishes the alphabetically first target's projection. A group containing
targets a and z exposed plaintext although z redacted that parameter.
Scope: internal/aggregate/build.go, published model, and renderer consumers.
Changes
- Define the group's projection as the most restrictive disclosure policy of its members. Preserve raw-evidence equality as a separate grouping concern.
- Combine sensitivity and selector outcomes explicitly rather than choosing a representative whose value happens to be first.
- Preserve links to individual target changes and deterministic group ordering.
Acceptance
- Reordering or renaming targets cannot change a group's disclosure policy.
- A group containing a redacted member cannot publish that member's protected evidence through another member's projection.
- Distinct sensitive changes remain separate, with correct target references.
- Implemented and verified 2026-09-09. Additions/removals retain redacted
parameter maps and group by raw managed settings. File membership changes
resolve only the existing catalog side; missing or unsupported evidence is
indeterminate with a diagnostic. Aggregate File summaries carry state, source,
verification and redaction without digests or target-specific provenance.
Assessment includes ordered edge endpoints under the shared group budget.
Result schema is now 2; producers, consumers, fixtures and implementation
documentation agree. Synthetic regressions in
internal/aggregate/evidence_test.go,internal/filecontent/membership_test.goandinternal/assess/request_test.gocover grouping, disclosure, stored-report assessment, all report formats, and edge-only/truncated requests. Local macOS verification passed:go test ./...,go test -race ./...,go vet ./...,go build ./..., formatting andgit diff HEAD --check. No live service conformance evidence was added; the 2.2/5.1 fixture gate remains open.
Finding: medium severity. Additions and removals retain identity only, so different added configurations can group as equivalent. File-content evidence does not reach the assessment projection. Edge groups are excluded from assessment even though edge-only comparisons are supported.
Scope: resource differ, aggregate model, all report formats, and
internal/assess/request.go.
Changes
- Preserve publishable before/after resource evidence for removals and additions. Apply phase 1 sensitivity rules and File-content disclosure rules throughout.
- Include the actual resource evidence in equivalence decisions so materially different additions or removals do not collapse by identity alone.
- Carry safe File-content state and evidence source into aggregate groups and inference payloads. Keep file bytes and content digests out of inference.
- Include meaningful edge evidence in assessment planning, with explicit budget accounting. If evidence is omitted, report the omission and resulting assessment scope rather than implying a complete review.
- Keep deterministic comparison outcomes independent of assessment output.
Acceptance
- Same-identity additions with different managed settings group separately.
- Added and removed sensitive values never reach publishable evidence.
- Assessment inputs distinguish verified changes, reference changes, and indeterminate File content without transmitting content or digests.
- Edge-only runs receive useful assessment evidence or an explicit scope limit.
- Implemented and verified 2026-09-09. Snapshot format version 2 replaces
compiler_apiwith acaptureprovenance block built entirely from the compiler adapter's returned record: requested API, effective API, fallback, trusted-fact source, and fact source. The compiler adapter now reports the same factset identity the envelope stores,puppetdb.FactsetIdentity, instead of PuppetDB's server-side hash, which a file-backed factset does not carry. Load performs the integrity check and Validate the attribution check, including capture-provenance consistency; captured content evidence is shape-checked when a baseline snapshot is loaded. Synthetic regressions ininternal/snapshot/identity_test.go,internal/capture/workflow_test.go,internal/puppetdb/filesource_test.go,cmd/piace/acceptance_phase2_test.goandcmd/piace/acceptance_snapshot_test.gocover fallback provenance, reported factset identity, impossible request sequences, unusable captured digests, incomplete provenance, and the capture/compare round trip. Format version 1 snapshots are rejected with a recapture instruction rather than reinterpreted. Local macOS verification passed:go test ./...,go test -race ./...,go vet ./...,go build ./...,gofmt -l, andgit diff --check. No live service evidence was added; the 2.2/5.1 fixture gate remains open. Deferred to 5.2: a comparison against a file baseline still does not surface that baseline's capture-time v3 trust semantics in the result document, whose scope is a result-schema change rather than a capture change. Closed on 2026-09-10: a file baseline'sSourceProvenancenow carries acaptureblock projected from the snapshot envelope, with the v3 warning derived from the effective API rather than copied out of the file, and the text and HTML reports carry a distinct baseline-capture warning banner.report.Validatechecks the block's coherence: the fallback pair, the warning matching the effective API, and a v3 capture naming no trusted-fact source. Landed onschema_version3 alongside the other 0.5.0 document changes.
Finding: medium severity. Capture discards compiler provenance and warnings,
then stamps the requested API into the envelope. A v4-to-v3 capture can therefore
claim compiler_api: v4. Snapshot payloads are typed projections, although some
documentation describes them as unmodified service payloads.
Scope: internal/capture/workflow.go, snapshot envelope, compiler carriers,
and capture output.
Changes
- Build capture metadata from the returned compiler provenance: requested API, effective API, fallback, trust semantics, and factset identity.
- Surface capture warnings and preserve the metadata needed to audit the snapshot.
- Define snapshots as a validated PIACE projection. Retain every field needed for identity, sensitivity, comparison, and phase 2 content evidence; document which service fields are intentionally outside that projection.
- Define checksum scope explicitly for payload and captured evidence. Validate relationships among metadata and payload when loading a snapshot.
Acceptance
- A fallback capture records effective v3 and exposes its warnings.
- Capture and reuse preserve all evidence required by the comparison contract.
- Snapshot integrity checks and attribution checks are distinct and both tested.
- Implemented and verified 2026-09-09. Configuration resolution is
parameterized by
resolve.Command: presence requirements are per command (capture factsneeds a certname and a fact destination;capture catalogadds a catalog API and a baseline destination and takes its environment from--environment; compare needs everything), while validity checks stay unconditional for every command.Command.servicesderives the required endpoints from the decoded targets, so a services file naming nopuppetdb:loads for a file-backed comparison with impact disabled, and one naming nocompiler:loads forcapture facts;Config.Requireddrives client construction. Compare and capture catalog now build one client per service rather than one per use, keeping the two services' credentials separate. Services sections accept an optionaltimeout;*http.Client.Timeoutno longer caps it, so the precedence is caller, then service, thentransport.DefaultTimeout(documented in that package's decision 1), and a 90s impact deadline is no longer silently cut to 30s. Every v4 request setsoptions.prefer_requested_environment: true, verified against the published v4 catalog API contract, with returned-environment validation unchanged. Synthetic regressions:internal/config/resolve/requirements_test.go,internal/transport/client_test.go,internal/compiler/adapter_test.go, andcmd/piace/acceptance_phase4_test.go. Local macOS verification passed:go test ./...,go test -race ./...,go vet ./...,go build ./...,gofmt -l, andgit diff --check. The "unused inference section causes no inference request" acceptance was already covered bycmd/piace/acceptance_explain_test.goand is unchanged. No live service was contacted; the classifier interaction behindprefer_requested_environmentis a 5.1 fixture item.
Finding: medium severity. Service resolution always requires compiler and
PuppetDB configuration. Comparison constructs four clients even when PuppetDB
is unused. Capture requires a comparison candidate environment despite its
own --environment flag, or despite capturing facts only.
Changes
- Share strict decoding and common field validation, then resolve the requirements of compare, capture facts, capture catalog, and explain separately.
- Require and instantiate only services used by the selected sources and enabled analysis. Reuse each configured service's hardened client where appropriate.
- Let catalog capture use its requested environment directly. Fact capture must not require comparison-only fields.
- Keep compiler and PuppetDB credentials isolated, and keep inference independent.
- Add configurable request timeouts with explicit precedence. Fix the implicit 30-second ceiling that currently caps longer impact-estimate deadlines.
- Verify candidate-environment selection against supported classifiers. Decide
explicitly whether v4 requests require
prefer_requested_environment, and keep returned-environment validation unconditional.
Acceptance
- File facts plus file baseline with impact disabled need no PuppetDB identity.
- Fact capture requires only its actual retrieval and destination inputs.
- Catalog capture accepts its environment without an unrelated candidate value.
- An unused inference section causes no inference request from comparison.
- Configured longer deadlines work; shorter caller deadlines still win.
- Implemented and verified 2026-09-09. New
internal/artifactpublishes every output through a same-directory temporary file with the mode applied before content, an fsync, and one atomic replacement; a no-replace snapshot is published by hard link, so the refusal and the write are a single operation rather than a check followed by a clobbering rename. Snapshots, reports and assessments all go through it, andsnapshot.ErrExistsstays the sentinel callers already match. A destination that resolves to a pipe or device is written in place; a symlink to a regular file is replaced by the artifact.artifact.Validaterejects two artifacts sharing a destination and any artifact written over a file the run reads (configuration or snapshot), comparing normalized paths and, where both exist, file identity, so symlink and hard-link aliases are caught; it runs before any service request. Compare and explain render every artifact before publishing any, and a publication failure reports which artifacts exist. Synthetic regressions:internal/artifact/publish_test.go(concurrentWriteNewwith exactly one winner and no torn file, concurrent replacement never observed partial, FIFO and symlink destinations),internal/artifact/destinations_test.go, and four cases incmd/piace/acceptance_phase4_test.go. Local macOS verification passed:go test ./...,go test -race ./...,go vet ./...,go build ./...,gofmt -l, andgit diff --check. Linux coverage is the CI matrix leg, not local evidence. Not detectable and not claimed: two not-yet-existing destinations that would become hard links to each other.
Finding: medium severity. Snapshot no-clobber behavior is a check followed by an overwriting rename. Reports and assessments use direct writes, and output paths can collide with each other or their inputs.
Changes
- Implement atomic no-clobber snapshot publication when replacement is disabled.
- Use same-directory temporary files and atomic replacement for generated reports and assessments, with explicit permissions and durability behavior.
- Reject conflicting output destinations and input/output aliases before service requests. Account for normalized paths, existing symlinks, and hard links.
- Specify multi-artifact failure behavior. Prepare requested artifacts before publication where possible; do not claim an atomic transaction across files.
- Preserve nonzero outcomes on requested-output failure and keep logs honest about what was produced.
Acceptance
- Concurrent no-replace snapshot writers cannot overwrite each other.
- Readers never observe partially written individual artifacts.
explain --json-in report.json --ai-out report.jsonfails before inference.- Colliding text, JSON, HTML, assessment, and snapshot destinations are rejected.
- Filesystem behavior is verified on Linux and macOS.
- Implemented and verified 2026-09-09.
report.Validateruns insideDecodeJSON, so no consumer reads an unvalidated stored document: it checks invocation metadata, target identity and uniqueness, per-change kind and identity shape, aggregate references (target exists, index in range, referenced change of the group's kind, certname list matching the references), outcome and exit-code consistency against what the document's own targets and diagnostics reduce to, and content-evidence coherence. Partial comparisons stay valid: a failed target legitimately carries no node diff, but must record the failure, so missing structure cannot pass as a reported one. Disclosure is enforced on the way in: a published value carrying a Pcore Sensitive wrapper, or a File content-bearing parameter's value, is refused, while redaction markers stay readable and distinct from omission. Decoding is bounded (report.MaxDocumentBytes), and configuration files now get the same guarantees: bounded reads, strict fields, and exactly one YAML document (a second---document was previously ignored in silence). Sensitivity-wrapper detection and the File content-bearing parameter set moved intointernal/model, so the differ that redacts and the reader that validates share one definition. Synthetic regressions:internal/report/validate_test.go,internal/config/resolve/decode_test.go, and two acceptance cases covering a sparse, contradictory, invalid-reference or forged-wrapper document reaching no inference service, and a partial comparison remaining explainable. Local macOS verification passed:go test ./...,go test -race ./...,go vet ./...,go build ./...,gofmt -l, andgit diff --check. Numeric precision on round trip is covered by the existinginternal/report/roundtrip_test.go. The read of a result file or stdin is bounded before the decode's own check, so an oversized document is refused without being allocated; the remaining budgets (canonicalization cost, inference request size, change context) are 4.4.
Finding: reproduced hardening gap. DecodeJSON accepts
{"schema_version":1}. Syntactic decoding and a version tag do not establish
that a stored document is a complete, internally consistent comparison.
Changes
- Add semantic validation for required fields, enums, target uniqueness, identity agreement, aggregate references, content evidence, and outcome consistency.
- Recognize legitimate partial comparisons without confusing missing structure with recorded retrieval or compilation failures.
- Validate that publishable evidence obeys disclosure rules before inference. Reject contradictory documents rather than silently repairing their outcome.
- Apply bounded decoding and reject duplicate or trailing document content where it creates ambiguous interpretation. Give configuration documents equivalent single-document guarantees.
Acceptance
- Sparse, contradictory, invalid-reference, and malformed documents fail before inference. Valid partial reports remain usable and explicitly partial.
- A stored report round-trips without loss of numeric precision or safe evidence.
- Forged sensitivity wrappers or raw File content cannot bypass request disclosure policy merely by arriving through a stored result.
- Implemented and verified 2026-09-09. New
internal/limitsholds every budget with its reasoning. Numeric tokens are bounded by significant digits and exponent magnitude before parsing, which is where the expansion happens: the recorded1e-10000case (8 bytes in, 10,002 out, confirmed by probe before the fix) is now refused, and the quadratic zero-prepending in the decimal expansion is one allocation. The exponent bound cannot go below 324, the smallest denormal float64, without refusing values a real factset can hold. Canonical encoding bounds nesting depth. Local reads are bounded before allocation: snapshots, stored result documents and stdin, configuration, policy-notes and token files, and change-context files, which also now get the single-YAML-document guarantee.change-contextbounds each git invocation by output size and wall-clock time. The inference request has a total byte budget: when it would be exceeded, group values are replaced whole by[omitted: inference request size budget]largest-first and counted invalues_omitted, then whole groups are dropped lowest-reach-first, with nothing cut mid-value; the omission marker is deliberately distinct from the redaction marker, and the budget holds room for the one retry, which is checked before it is sent.assess.BuildRequestnow returns aRequestScopeso the artifact records what was actually sent;AISchemaVersionis 2 for the addedvalues_omitted. A refused numeric value produces a diagnostic naming the budget class and never the token, since a number in a catalog can be a secret. Synthetic regressions and benchmarks:internal/snapshot/canonical_test.go(expansion, nesting, exact values kept,BenchmarkCanonicalNumberString),internal/assess/request_test.go(whole- request budget, shedding order, retry room, context crowding),internal/diff/diff_test.go(BenchmarkDiff_LargeCatalog, 5,000 resources), and two acceptance cases (adversarial number, oversized snapshot). The end-to-end half of the third acceptance bullet isTestAcceptance_LargeComparisonProducesABoundedRequestincmd/piace/acceptance_phase4_test.go: a comparison carrying six 200 KiB values reaches the stub inference service as a request inside the budget, and the written artifact reports the omissions. Two accounting corrections came out of it. A group whose values were omitted and which was then dropped entirely no longer counts as a group reviewed without its values (TestBuildRequest_CountsOnlyGroupsItStillSends, which fails against the previous accounting withValuesOmitted = 200overGroupsAssessed = 121), and the explain stub's group-id pattern did not match the indented payload, so every explain acceptance case had been asserting against a silently degraded assessment; the pattern now tolerates the indentation and the success case asserts the id round trip. Local macOS verification passed:go test ./...,go test -race ./...,go vet ./...,go build ./...,gofmt -l, andgit diff --check.
Findings: response-byte limits do not bound canonicalization cost, local
file/stdin reads, or inference request size. The eight-byte numeric input
1e-10000 expanded into 10,002 output bytes. Group-count limits do not constrain
large values, all target records, or all impact estimates.
Changes
- Define numeric digit/exponent, nesting, total input, and canonical output budgets. Preserve exact supported decimal semantics without repeated string prepending or unbounded rational expansion.
- Apply limits to snapshots, result files/stdin, policy notes, and change-context acquisition before expensive allocation or parsing.
- Bound git output collection and execution time for change-context generation.
- Introduce a total inference request budget covering values, targets, groups, impact entries, context, and policy notes. Account for the retry request too.
- Make omitted evidence and truncation explicit. Preserve valid UTF-8 and valid structured values; never truncate raw JSON bytes into malformed payloads.
Acceptance
- Small exponent inputs cannot cause unbounded memory or CPU work.
- Oversized local and remote inputs fail with controlled diagnostics.
- Large catalogs produce a bounded, valid inference request with truthful scope and truncation accounting.
- Benchmarks cover realistic large catalogs and adversarial numeric shapes.
- Implement and verify.
The existing suite passed race tests, vet, build, and formatting during review. That does not establish real-service conformance: several fixtures encode the same assumptions as the code. Acquire real wire evidence as early as a preceding step needs it; this step is the final integration gate.
PIACE ran against a deployed OpenVox 8.15.2 compiler (puppet 8.28.1) and its
PuppetDB for the first time on 2026-09-09. Two runs: one comparing a node's
production baseline against an upstream candidate, and one comparing
production against itself, which is the controlled case because every
reported difference in it is by construction an artifact.
The self-comparison exited 30 with 28 resource differences and 57 edge differences. All of them were artifacts. They have one root cause.
A PuppetDB baseline is Puppet's own lossy projection of a catalog, and PIACE
compares it against a full-fidelity compiler response as though the two were
the same format. Puppet::Resource::Catalog::Puppetdb#munge_catalog, in the
terminus deployed on that server, rewrites a catalog before storing it. Each
rewrite is a place where a stored baseline and a compiled candidate can differ
while describing the same desired state. The full list, from the deployed
source, is the conformance matrix this step owes:
| Terminus munge | Effect on a stored catalog | Status |
|---|---|---|
add_namevar_aliases |
injects an alias parameter absent from a compiler response |
closed 2026-08-28, normalize drops it |
sort_unordered_metaparams |
sorts alias, audit, before, check, notify, require, subscribe, tag |
finding L2 below |
synthesize_edges |
adds relationship edges a compiler response does not carry | finding L3 below |
stringify_rich around to_data_hash |
rewrites every rich value to a lossy string | finding L4 below |
redact_sensitive_params |
deletes sensitive parameters and the sensitive_parameters key |
finding L5 below |
stringify_titles |
stringifies resource titles | checked: no non-string title in the sample |
stringify_version, add_producer*, add_code_id_if_missing, filter_keys, change_name_to_certname, hashify_tags |
catalog-level metadata only | not compared by PIACE |
munge_catalog_inputs |
Hiera lookup recording, separate PuppetDB command | not compared by PIACE |
ToStringifiedConverter, the class the fourth of those uses, documents itself:
"The conversion is lossy - the result cannot be deserialized to produce the
original data types." Puppet::Resource#to_data_hash adds "the
ToStringifiedConverter output is lossy and should not be used when producing a
catalog serialization". PIACE compares against exactly that output.
Finding L1: ensure => absent files were reported as unsupported
comparisons. High severity, reproduced, fixed. Sixteen of the
self-comparison's differences were File resources both catalogs held
identically with ensure => absent. filecontent.nonByteComparable grouped
ensure => absent and ensure => link with directories and recursive sources,
so each produced a spurious change, a diagnostic misdescribing a plain file as
a directory, and an error severity that made the run an operational error. A
catalog that manages no bytes at a path is a determinate answer, not missing
evidence. Now not_managed; see the commit and
internal/filecontent/unmanaged_test.go.
Finding L2: relationship order was reported as a change. Medium severity,
reproduced, fixed. Service[pabawi]'s require and subscribe held the same
entries on both sides in different orders. Puppet's terminus calls these
metaparameters "fundamentally unordered" and sorts them; a compiler response is
in declaration order. normalize now sorts the same seven on both sides. Two
compilations of the same catalog returned byte-identical parameters, so this is
the terminus's reordering and not compilation nondeterminism.
Finding L3: synthesized relationship edges were reported as removed. Medium
severity, reproduced. The stored baseline carried 296 edges, the compiled
candidate 239. The 239 containment edges were an identical multiset; every one
of the extra 57 was a relationship edge the terminus synthesized from a
require, before, notify or subscribe metaparameter. A compiler catalog
carries containment edges only, because relationship edges are resolved by the
agent at apply time. So a PuppetDB-backed comparison reports every relationship
in the catalog as removed, and reports it twice, since the metaparameter it was
derived from is compared as a parameter in its own right. The text report hides
edge groups by design, so these were invisible there while reaching the JSON
document, the aggregate and the inference request.
Finding L4: rich values were reported as changed. Medium severity,
reproduced. Class[Psick::Puppet]'s facts_file_exclude_regex compared
"/^(...)$/" against {"__ptype":"Regexp","__pvalue":"^(...)$"}. The baseline
holds ToStringifiedConverter's output and the candidate holds Pcore rich
data. Neither side is wrong; they are different fidelities of the same value.
The measured evidence covers Regexp and no other type. Every other Pcore type
that could appear (Timestamp, Timespan, Binary, SemVer, Deferred,
Default, object types) is an open gate until a fixture measures it: reading
Ruby is not evidence.
Finding L5: sensitive parameters are absent from a PuppetDB baseline. Open,
partially measured. redact_sensitive_params deletes every parameter named in
sensitive_parameters, and the key itself, before a catalog is stored. So a
PuppetDB baseline cannot carry either. What the compiler's v4 response carries
is not yet measured: neither catalog in the sample contained a sensitive
parameter at all, so sensitive_parameters appeared zero times on both sides.
Until a fixture settles it, phase 1.1's "decode it from supported compiler and
PuppetDB representations" is unexercised against real wire data on both sides,
and 5.2's disclosure row cannot state a verified representation.
Finding L6: a null query response would have read as "no node affected".
Low severity, hardening, fixed. encoding/json decodes null into a nil slice
without error, so an impact-estimate body saying nothing was indistinguishable
from one saying no node is affected, and a zero-node estimate is a claim a
reader acts on. The response shape is now decided from its first non-whitespace
byte, as internal/normalize's shapeContainer already does. A deployed
PuppetDB 8.15.0 returns [] with HTTP 200 for a query matching nothing, so
this refuses a body that service does not send.
Finding L7: capture failed on any catalog holding a local-path File source.
High severity, reproduced, fixed. The first live piace capture catalog wrote
no snapshot and exited 30. One File in the catalog had
source => /etc/puppetlabs/puppet/ssl/certs/ca.pem, a path the agent reads
from its own filesystem, which is not a retrieval PIACE can make rather than
one that failed. Capture treated it as a failed observation, which by policy
prevents publication. Six such resources and three recursive directories were
in that one catalog. Capture also obtained its diagnostic by re-resolving with
a nil retriever, so the message named the absence of a retriever instead of the
reason. Both the severity split and the safe reason text now live in one place,
filecontent.classifyEvidenceError, used by the differ and by capture.
-
PuppetDB 8.15.0 impact query: the root
/pdb/query/v4endpoint accepts the PQL text this project specifies;limitandorder_bytravel besidequeryand are honoured server-side (ascanddescreturn different samples of the same result, and the unlimited query returns neither order, soorder_byis load-bearing for a reproducible truncated sample); an empty result is[]with HTTP 200; a malformed PQL is HTTP 400 with a text/plain body quoting the query back, which is never echoed; every escapequotePQLStringemits is accepted by the parser. Recorded ininternal/impact/doc.go. -
Catalog compilation is deterministic: two v4 compilations of one node in one environment, minutes apart, returned byte-identical resource parameters. That is what establishes L2's reordering as the terminus's rather than compilation's.
-
Pcore
Regexpstringification: twelve patterns measured by callingPRegexpType.regexp_to_sandregexp_to_s_with_delimiterson the deployed Ruby. The table isinternal/model.StringifyRich's doc comment and its test. -
The capture and reuse round trip:
piace capture catalogfor one node'sproductionenvironment, then a comparison of that node against the snapshot it wrote. Format version 2 loads, itscaptureblock records requested and effective v4, and the comparison reports exactly the nine File resources capture had warned it could not observe, with no other difference. That exercises 3.3's round trip and the file-baseline fidelity path end to end.
Still unverified, and each is an open gate: other supported Puppet, OpenVox and
PuppetDB versions; every Pcore rich type but Regexp; whether the v4 catalog
response carries sensitive_parameters at all; the File shapes 2.2 needs; and
the inference provider matrix.
One gate is sharper than it was. No node in the lab has a File with a
single-file puppet:/// source: a PQL query across every stored catalog
returned three resources, all recursive directories. So the captured-digest
path, which is 2.1's headline acceptance ("a module file edit with an unchanged
source URL is detected when verified evidence exists"), has no live coverage
at all, and the snapshot written from that lab has an empty captured_content
map. Only the fixture manifest can close it.
The fixture manifest this step still owes must therefore cover, at minimum:
each Pcore rich type as an actual parameter value; a resource with
sensitive_parameters; a class with a Sensitive parameter; a regexp carrying
flags, to settle whether regexp_to_s_with_delimiters round-trips them; and
the File shapes 2.2 needs.
Changes
- Generate catalogs and facts from dedicated test manifests containing synthetic values. Exercise supported Puppet and OpenVox versions and PuppetDB baselines.
- Record fixture producer/version, request shape, generation procedure, and expected semantic result. Keep credentials and real infrastructure data out.
- Cover sensitivity metadata and nested wrappers, trusted facts, v3/v4 envelopes, environment classification, static file metadata, source arrays, directories, recursive sources, and graph relationships.
- Verify PuppetDB impact query syntax, ordering, and limits. Reject
nullas a successful empty row collection and test truthful truncation accounting. - Verify inference structured-output behavior for the supported provider matrix, retaining unconditional local reply validation and the bounded retry policy.
- Replace relevant assumption-only tests with recorded evidence and executable conformance assertions. Keep genuinely unverified claims explicitly scoped.
Acceptance
- Every load-bearing wire assumption has a reproducible fixture or a clearly identified integration gate still preventing completion.
- The initial disclosure and false-clean failures are permanent regression tests.
- Model-generated prose never changes deterministic outcomes or exit codes.
- Implemented and verified 2026-09-10.
report.SemanticProjectiondefines the comparison projection: the canonical result document with its invocation metadata removed, which is also what a reader can compute withjq -S 'del(.invocation)', so the guarantee is checkable outside this codebase. Two claims are now stated separately and both tested: one result encodes to the same bytes every time, and two runs over the same catalogs and configuration reach the same comparison.TestAcceptance_TheComparisonIsReproducibleAcrossClocksruns the pipeline twice under two deliberately different clocks, asserts the documents differ and the projections do not; the existing byte-identical case keeps its frozen clock and now says in its own comment that this is what limits it. Every row of the alignment checklist below is applied. Thedesign.md/requirements.mdreferences are gone fromci.ymland from two test names. Local macOS verification passed:go test -race ./...,go vet ./...,go build ./...,gofmt -landgit diff --check.
Finding: documentation promises identical artifact bytes for identical catalogs and configuration, while production includes a changing invocation timestamp and the acceptance harness freezes the clock.
Changes
- Define a canonical semantic comparison projection separately from invocation metadata. Preserve real timestamps while making semantic reproducibility independently testable. Keep assessment linkage to its source report explicit.
- Test semantic equality with different clocks and input ordering, and exact byte equality when the complete result, including metadata, is identical.
- Update README.md, CONTEXT.md, docs/development.md, docs/change-assessment.md, docs/ci.md, docs/release.md, examples, CLI help, and package comments using the following alignment checklist.
Each row is now applied; the second column is what the documentation says.
| Existing mismatch | Required documented contract |
|---|---|
| Identical catalogs imply identical full artifact bytes | Separate semantic determinism from invocation metadata |
| Explain sends exactly one request | One attempt, plus at most one retry for an unusable reply |
| Sensitive values are always absent under wrapper-only detection | State the verified sensitivity representations and disclosure scope |
| Redacted parameters are absent from inference | Distinguish omitted data from entries carrying redaction markers |
| Pseudonymization implies certnames cannot appear elsewhere | State which structured identity fields are substituted and which free-text values remain |
| Changing pseudonymization leaves assessment output identical | Assessment prose is nondeterministic; identity restoration is the local guarantee |
| Snapshots retain an unmodified service payload | Describe the validated PIACE projection and retained evidence |
| Capture records compiler API correctly | Document requested and effective API plus fallback provenance |
| All relative paths have only one resolution rule | State snapshot containment restrictions as well as the config-relative base |
{certname} is a whole component only |
State the accepted extension-suffix grammar |
| Reports never rewrite inputs or overwrite snapshots unexpectedly | State and test the implemented publication and destination rules |
| Every edge is a consequence of a resource change | Describe edge-only changes and assessment scope accurately |
- Remove stale references to missing
design.mdandrequirements.mdfiles. Keep architectural decisions with the module that owns them. Remove routine procedural commentary and unsupported absolutes.
Acceptance
- Every guarantee has a matching test or an explicit evidence limitation.
- Examples load under command-specific validation and CLI help agrees with them.
- Documentation distinguishes result completeness, verification, redaction, pseudonymization, and advisory assessment without conflating them.
-
Partially implemented 2026-09-10. Items 1 and 2 are done and verified against the published release; items 3 to 6 are outstanding.
Item 1, signature verification, done. All three CI examples now verify
SHA256SUMS.sigstore.jsonbefore running anything, as an executed step rather than a commented suggestion, and treat the checksum as the second check rather than the only one. The GitHub example installs cosign withsigstore/cosign-installer@v3; the GitLab and Azure examples download cosign pinned by version and by SHA-256, since the digest reviewed in the pipeline file is what makes the signature check a check rather than another unverified download. Verified end to end against the published v0.4.0 release with cosign 3.1.3:Verified OK, and both negatives fail, a wrong signer identity with "none of the expected identities matched" and a tampered manifest with a payload mismatch. The certificate's SAN ishttps://github.com/example42/piace/.github/workflows/ci.yml@refs/tags/v0.4.0with issuerhttps://token.actions.githubusercontent.com, exactly what the examples pin.Item 2, change-context refs, done. Reproduced first: in a checkout detached at a merge commit with no local branches, a bare branch name does not resolve, because git looks for
refs/heads/NAMEandrefs/remotes/NAMEand neverrefs/remotes/origin/NAME.git merge-basefails with "Not a valid object name" and the assessment runs with no change context at all. All three examples had it on the head ref, and the GitLab one on both refs. Each now fetches both refs explicitly into their remote-tracking form and names them that way; confirmed against a local repository reduced to exactly that shape.Outstanding. Item 3, job-specific credential scope, especially GitLab protected-variable availability and environment scoping. Item 4, the candidate-environment mapping against the actual deploy step: the examples and
docs/ci.mdalready say to match whatever mapping the deploy tooling applies rather than assuming the dash substitution shown, but this has not been checked against a real deploy step. Item 5, the release toolchain:.github/workflows/ci.yml's build job still usesgo-version-file: go.mod, which couples the release compiler to the declared minimum; pin an explicitgo-versionthere and record it indocs/release.md, keeping the 1.22 matrix leg and adding notoolchaindirective togo.mod. Item 6, consistency of the verification story across the release workflow and consumer instructions.
Findings: CI installation examples verify checksums but leave signature verification inactive. Change-context examples rely on branch names that may not exist locally in detached checkouts. Release compilation selects the Go version from go.mod, coupling the release compiler to the minimum language version.
Changes
- Make signature verification an executed prerequisite to running downloaded binaries in each CI example. Fetch the signature bundle and constrain signer identity and issuer. Keep checksum verification as the integrity check.
- Use explicitly fetched refs or commit identities for change-context. Test detached merge checkouts and branches represented only by remote refs.
- Verify job-specific credential scope in each example, especially GitLab protected-variable availability and environment scoping. State setup needed to enforce the claimed separation between compare and explain jobs.
- Check candidate-environment mapping against the actual deploy step. Avoid implying a dash substitution implements every site's branch mapping.
- Select and record a supported release Go toolchain explicitly, while retaining separate minimum-version testing. Review action references and container base identity so published artifacts have traceable build inputs.
- Keep binary, container, checksum, signature, and attestation verification consistent across the release workflow and consumer instructions.
Acceptance
- Each example's install and change-context sequence works in its intended checkout shape and refuses invalid signatures.
- Credential separation is enforced by CI configuration, not just comments.
- Release artifacts identify their actual toolchain and pass the documented consumer verification process.
- Implement and verify.
- Run formatting, vet, build, the full race-enabled suite, and the supported Linux/macOS matrix. Run the focused integration and resource-budget checks introduced above.
- Inspect representative text, JSON, and HTML artifacts for clean, allowed difference, policy failure, compilation failure, operational failure, redacted, source-content, edge-only, and partial-assessment runs.
- Update CHANGELOG.md and release examples for 0.5.0, describing resulting behavior and verified limitations. Update artifact schema identifiers and fixtures to match the final formats.
- Build and verify the release candidate locally or in the authorized CI flow. Record the commit, toolchain, test results, and conformance evidence here.
- Obtain confirmation before pushing, creating a PR, publishing a release or image, or performing any other externally visible release action.
Acceptance: all phase checkboxes are complete, release evidence is recorded, and no unresolved finding is hidden behind a passing synthetic fixture.
- Puppet parser sensitivity handling
- Puppet resource serialization
- Puppet File source selection and checksums
- Puppet file-content endpoint
- Puppet v4 catalog request contract
These references establish upstream behavior, not conformance of a particular deployment. Pin source revisions when producing the phase 5 fixture records.