All notable changes to PIACE are recorded here. The format follows Keep a Changelog, and the project follows Semantic Versioning.
0.7.0 - 2026-10-23
-
A top-level
file_content:section in the targets file. Itssuppress_source_warnings: truesilences expectedverify_contentwarnings for source-backed files with unavailable byte evidence across all report formats, preserving comparison evidence, source-reference differences, and operational errors. Defaults to false. -
A top-level
junit:section in the targets file controls the JUnit report.details_as_erroralso writes each passing case's evidence as the message of an<error>element, because Azure Pipelines does not display<system-out>in the Tests tab; it then shows those cases as errored.impact_estimateadds apiace.impactcase with the run-level impact estimates.change_assessmentadds apiace.assessmentcase. All default to false, which is the previous report. -
piace explain --junit-out PATH --targets TARGETS.yamlwrites the JUnit report with the change assessment.--targetsis read for itsjunit:section only, and the two flags must be given together.
piace compare --suppress-source-content-warnings, introduced in 0.6.0, is replaced byfile_content.suppress_source_warningsin the targets file.
0.6.0 - 2026-10-08
-
piace compare --suppress-source-content-warningssilences expectedverify_contentwarnings for source-backed files with unavailable byte evidence across all report formats, preserving comparison evidence, source-reference differences, and operational errors. -
piace compare --junit-out PATHwrites JUnit XML test results from the shared redacted result. Each target becomes a test case; run diagnostics get a separate case. Allowed differences pass, policy-disallowed differences fail, and compilation or operational failures are errors.
0.5.0 - 2026-09-11
- Catalog snapshots retain captured single-file content digests and static compiler metadata inside the checksummed payload. Capture provenance records the requested and effective compiler APIs, fallback, trusted-fact source, and fact source. Comparisons against file baselines expose that provenance and any capture-time v3 trust warning.
- Resource additions and removals carry the existing side's redacted parameter map and one-sided File-content evidence. Aggregate groups and change assessments retain content state, evidence sources, and verification status. Dependency-graph edge groups participate in the assessment budget.
- Service endpoints accept an optional
timeout. Request deadlines use the caller's timeout, then the service timeout, then the transport default, without an additional fixed client timeout shortening the request.
- Artifact formats are incompatible with earlier releases. Result documents
use
schema_version: 3, andexplainrequires that version. Snapshots useformat_version: 2, replacingcompiler_apiwith structured capture provenance. Version-1 snapshots must be recaptured. - File-content evidence is resolved independently for baseline and candidate, even when the source reference is unchanged. Historical baselines use retained evidence only. Source lists preserve order and try the next source only after a recognized missing-file response. Checksums require a supported algorithm and a full hexadecimal digest.
- Unverifiable File content produces a
verify_contentdiagnostic rather than a resource difference and does not drivefail_on_diff. Missing historical digests, local paths, directory or recursive sources, and incompatible digest algorithms produce warnings; failed retrievals and invalid checksums remain errors. Changed source references remain differences even when their bytes cannot be verified. Catalog capture can publish snapshots with limitation warnings. - Configuration requirements follow the command and selected sources.
capture factsno longer requires candidate or baseline configuration, and file-only comparisons with impact estimation disabled need no PuppetDB endpoint or credentials. - Warning diagnostics appear in a counted, collapsed Notices section in HTML. Documentation distinguishes byte-identical rendering of one result from reproducible comparisons across different invocation timestamps.
- Relationship arrays compare without treating order as a change. Missing compiler edges are no longer reconstructed from relationship parameters.
- Regular expressions compare against PuppetDB's measured stringified form when the catalogs have different fidelities. Other unmeasured rich types retain a warning when a difference may be representational.
- Files with
ensure: absentorensure: linkmanage no content bytes and no longer trigger unsupported-content diagnostics. - V4 catalog requests prefer the requested environment rather than allowing the classifier's environment assignment to redirect compilation.
- Reports and assessments publish through atomic replacement; snapshot no-clobber publication is atomic. Conflicting output destinations and outputs that alias input files are rejected before service requests. Publication failures identify which artifacts were written.
- Inference request size and omission accounting include the serialized request, with deterministic evidence reduction when the byte budget is exceeded.
- Resource-level
sensitive_parametersand recursive Sensitive wrappers survive normalization. Either catalog's sensitivity protects both values, aggregate disclosure respects every member, and malformed sensitivity metadata is rejected. Raw evidence and grouping fingerprints stay outside the published projection. - Endpoint URL credentials are rejected, compiler/PuppetDB requests are restricted to the configured HTTPS authority, and untrusted debug metadata is bounded and escaped.
- Requested targets, response certnames, snapshot envelopes and payloads, and supplied trusted facts must agree. Malformed factsets and trusted-fact structures fail validation before compilation.
- Comparisons reject PuppetDB baselines when direct v3 or permitted v3
fallback could overwrite them. Fallback requires explicit opt-in and is
limited to an ambiguous empty-body or literal
Not FoundHTTP 404; HTTP 501 no longer triggers it. - Stored reports are validated for outcome consistency, aggregate references, identity, and disclosure before rendering or inference. Local reads, JSON nesting, numeric expansion, git subprocess output and duration, and inference request sizes are bounded.
- CI examples execute Sigstore signature verification before checksum verification and fetch explicit refs for change-context generation.
0.4.0 - 2026-09-04
- The impact-estimate section is labelled Potential impact estimate with a capital P in every output format.
- A
differences_allowedrun is coloured green in the HTML report. It shares exit0withclean, and yellow is reserved for the advisory medium-risk indication rather than for a run that succeeded.
- The task prompt states the response shape unconditionally. Anthropic's
OpenAI-compatible endpoint documents itself as ignoring
response_formatrather than rejecting it, so a request relying on structured output alone asked for nothing.Interpretalso unwraps a Markdown code fence around a whole reply, recording a warning, rather than discarding a complete assessment over its packaging. - An oversized inference response is reported as oversized rather than as "not a chat completion", which sent an operator looking at the wrong thing.
- A File
sourcevalue can no longer steer the file-content request. Asourcereached the compiler'sfile_contentendpoint as an unescaped path concatenation, sopuppet:///../../pdb/query/v4/catalogs/<node>was attacker-shaped input, compiled from the change under review, aiming an mTLS-authenticated GET at another path on a host PIACE is already authorized against.parsePuppetSourceURInow refuses an empty,.or..segment and a NUL byte. - The inference client has a redirect policy.
net/httpkeeps anAuthorizationheader across a redirect to the same host, so an endpoint answering302 Location: http://<same host>received the bearer token in cleartext. Redirects that leavehttpsor change authority are now refused, matching whatinternal/transportalready enforced for the credential-free clients. - The text report escapes control characters in every interpolated value.
A resource title or diagnostic message carrying
ESC[2K\rfollowed by a forged outcome line could make a run that exits30read as clean in a CI log. C0, DEL and C1 are replaced with a printable escape; the JSON and HTML reports are unchanged, since JSON escaping already makes a control character inert andhtml/templatecovers the HTML. - Refs reaching git are validated.
change-contextrefuses a ref beginning with-before any git command runs and passes--end-of-options, so a base or head ref chosen by whoever opened a fork pull request cannot become an option togit diff. --debug-dump-dirno longer writes through a symlink already sitting at a dump path, and applies0600to the file it writes rather than only to one it creates.
0.3.0 - 2026-09-01
- One path rule. Every relative path named in a config file now resolves
against the directory of the file that names it.
facts.fileandbaseline.filealready did; the TLS paths,token_fileandpolicy_notes_filenow do too. Nothing resolves against the process working directory, so moving a config file takes its paths with it. ca_bundle_env,client_cert_env,private_key_env: each service section may name an environment variable holding a credential's absolute path, mirroring theinference:section's existingtoken_env. Naming both forms of one credential is an error rather than a precedence rule nobody remembers. Together with the path rule this removes the services template entirely: a committed services file is now read in place, unmodified, by a CI job whose credential directory did not exist when the file was written. Nosed, no per-job render, and no tracked file a job rewrites.piace change-context: writes the change context fileexplain --changereads, by exec'ing git. Every untrusted input is taken by variable name (--title-env,--base-ref-env) or file path (--title-file), never on the command line, because every CI system substitutes into script text before a shell runs: GitHub's${{ }}, Azure's$( ... ). There is deliberately no--titleor--descriptionflag. It is optional, andexplain --changestill reads a file produced by any means, so a repository under a different VCS is unaffected.- Keyless signing and provenance: the release job signs
SHA256SUMSwith cosign, using the workflow's own OIDC identity, and attachesSHA256SUMS.sigstore.json. The binaries carry a GitHub build provenance attestation, and both image manifests are signed by digest and attested. Verification is now actionable from the moment a release is published; the release notes carry the exactcosign verify-blobcommand. The OpenPGP signature remains available for sites that require one, as an extra rather than the verification path. ghcr.io/example42/piace: the image is mirrored to GHCR alongside Docker Hub, from the same build. An anonymous Docker Hub pull from a shared CI runner IP is exactly what Docker Hub rate-limits, and a pipeline failing for that reason fails for a reason unrelated to this project.piace compare --candidate-environment ENVIRONMENT: compiles every target's candidate catalog from ENVIRONMENT, overridingcandidate.environmentin both thedefaults:block and any per-targetcandidate:block. The environment CI deployed is the one per-pipeline value in an otherwise static policy file; passing it at the invocation is what lets a pipeline stop rewriting its own committed target file between checkout and run. The override is applied to the target file before resolution, so it is validated and recorded in a report's provenance exactly as a file-supplied value, and with it the target file may omitcandidate.environmententirely.capture catalog --environmentis unchanged and unrelated: it names the environment to snapshot, not the candidate environment under test.piace explain --debug/--debug-dump-dir DIR: the two observation optionscompareandcapturealready accept now work onexplaintoo, so a rejected inference request can be diagnosed without guessing.--debugprints one stderr line for the inference round trip: method, URL, HTTP status, duration, request and response body sizes, and the response body's top-level JSON member names.--debug-dump-diradditionally writes the raw request and response bodies to0600files in DIR: the request-body dump is the exact catalog-derived payload PIACE sent, and the response-body dump of a 4xx is where a provider names the field it rejected. Neither the returned error nor any log line ever carries a response-body value, and the bearer token is a header so it reaches no dump file.services.inference.token_limit_param: selects the request field that carries the output-token bound,max_tokens(the default) ormax_completion_tokens. OpenAI's GPT-5 family rejectsmax_tokensoutright and requiresmax_completion_tokens; OpenAI-compatible servers other than current OpenAI (Ollama, vLLM, llama.cpp) only understandmax_tokens. The value inmax_tokensis unchanged; only the wire field name differs.services.inference.temperature: optional sampling temperature, sent only when set.
-
One services file. The
compiler:,puppetdb:andinference:sections already loaded independently, so the two-file split was policy rather than necessity. The examples and the CI documentation now show one committedservices.yamlfor the whole pipeline. What separates a comparison job from an assessment job is which credentials each is granted, not which file it reads. -
Documentation rewritten to describe the current tool only. The README is a third shorter, with the
explainreference moved to docs/change-assessment.md;docs/ci.mdloses the section explaining why the services file had to be a template, because it no longer does. -
Code comments say what the code does instead of citing a build-time specification. 578 references to
.kiro/specs/piace/across 109 files are gone, along with the specification itself and the ADR directory whose rationale now lives in CONTEXT.md. -
piace explain: no sampling parameter is sent unlessservices.inference.temperatureis configured. PIACE previously hard-codedtemperature: 0andseed: 0into every request; Claude 4+ and OpenAI's GPT-5 family reject any non-defaulttemperaturewith a 400, andseednever left a mark (Anthropic's compat endpoint ignores it, OpenAI deprecated it, reasoning models reject it), so theseedfield is gone. Pinning them never made a model-generated assessment reproducible in the first place: a provider-side model revision still moves the bytes. -
piace explain: dependency-graph edge groups are no longer sent to the inference service. An edge change is a consequence of the resource changes around it, carries no before/after pair to reason about, and a run's edges routinely outnumber its resource changes, so sending them spent the group budget and returned a wall ofunknownrisk indications. The deterministic report still lists every edge group in its own section; only the change assessment skips them, andgroups_totalnow counts what was eligible for assessment.
scripts/change-context.sh, replaced bypiace change-context. Its job was to emit YAML withprintfand leave the caller to append a title and description by hand, which is the step that has to be got right on three CI platforms and is arbitrary code execution on the runner when it is not.examples/ci/services.yaml.tmpl, replaced byexamples/ci/services.yaml. Nothing renders it.- The three
-dockerCI examples. They demonstrated socket-mount gymnastics for a path the same document recommends against;docs/ci.mdnow coversdocker runin one section, for a Kubernetes Job or a workstation.
- HTML report: risk-indication rows in the change assessment's "Group risk indications" list put a full risk badge in a grid track sized for a one-character change sign, so the badge overlapped the group identity and was stretched to the row height. The row now has its own track width.
- docs/ci.md and examples/ci/: the shipped
pipelines never bound
candidate.environmentto the environment CI had just deployed, and never added the merge request title and description to the change context, so a copied pipeline compared against a hardcoded environment and assessed a change without the sentence that says what it is for. Both are now rendered by the pipelines, the first through--candidate-environment. - examples/ci/github-actions.yml: the
pull request base ref, title and body now reach the job through
env:instead of${{ }}inside arun:block, where they were substituted into the script before a shell saw them.
0.2.1 - 2026-08-30
- A published container image: cutting a
v*tag now also pushesexample42/piace:<version>to Docker Hub, as alinux/amd64+linux/arm64manifest list;:latestmoves with every non-prerelease. The image is the release binary the workflow already verified, copied ontodistroless/static, so what adocker pullruns is the bytesSHA256SUMScertifies. It runs as a non-root user out of/work: see the README's Install section for the mount and--userflags. - docs/ci.md and examples/ci/: copy-ready GitHub Actions and GitLab CI pipelines, where each configuration file belongs in a control repository, and what changes when the runner is one you do not control. Two jobs by design, so the catalog-reader identity and the inference token are never held by the same job.
0.2.0 - 2026-08-29
piace explain: an optional, advisory change assessment of a stored result document. It is a second, independent step: it reads a JSON reportcomparealready wrote, asks a configured inference service to judge the aggregate groups in it, and writes a separately versioned assessment artifact (ai_schema_version: 1) plus a re-rendered HTML report whose assessment section sits below the deterministic outcome.services.yamlgains aninference:section: endpoint (https only), model,token_envortoken_file(never an inline token),timeout,max_tokens,max_groups,pseudonymize,structured_output, andpolicy_notes_file. It loads independently: a services file containing nothing but this section is valid forexplain, so an assessment needs no Puppet infrastructure named at all.--change CHANGE.yaml: a caller-supplied change context describing the repository change under test: refs, commit subjects, changed paths, and a capped title and description. PIACE reads the file and never invokes git;scripts/change-context.shgenerates one for the common CI case. Its free text is transmitted inside an explicit fence labelled as untrusted data.- Pseudonymized identities: certnames in an outbound inference request are
replaced by stable per-run substitutes, and the compiler and PuppetDB
authorities are absent from it entirely. Resource identities pass through
untouched:
File[/etc/sudoers]is the signal. A pseudonym never appears in an assessment or any report.pseudonymize: falsesends real certnames and is documented as the deliberate loosening it is. --fail-on-inference-error: exit 30 when the assessment could not be produced. Without it a failed assessment is recorded in the artifact with every risk indicationunknown, and the command still exits 0.report.DecodeJSON: a result document can now be read back into the model it was rendered from, strictly: unknown fields and trailing content are refused, and numbers keep their exact decimal digits.
piace compareis untouched by this feature. Its result document staysschema_version: 1, its reports are byte-identical for identical inputs, its exit codes are the same, and it contacts no inference service. A report rendered without an assessment is byte-for-byte the artifact v0.1.0 wrote, asserted against a golden captured before the feature existed.explaincontacts no compiler and no PuppetDB, asserted by failing the test if either configured endpoint is reached.
0.1.0 - 2026-08-28
First release: the whole tool, so this entry describes what it does rather than what changed.
piace comparecompares each target's baseline catalog (PuppetDB's latest, or a local snapshot) against a candidate catalog compiled by an existing Puppet Server or OpenVox compiler, and reports a per-node diff, a cross-node aggregate view, and an optional PuppetDB-backed impact estimate.- Deterministic semantic normalization: exact
Type[title]identities with no case folding, a canonical value domain with exact-decimal numbers, and resources and edges sorted before comparison and serialization. - Generated catalog noise is excluded before comparison: tags, source
file/line,
exported,aliases, and thealiasparameter the PuppetDB terminus injects into a stored catalog. - Four change kinds: resource added, resource removed, parameter changed, and dependency-graph edge added/removed. A target whose only differences are edges is still reported as changed.
- Managed
Filecontent evidence without content disclosure: inlinecontentdigests, an authoritative compiled checksum, compiler retrieval of asourcereference, and the explicitreference_changed/content_indeterminatestates when bytes cannot be compared. No managed file bytes reach any output, log, PQL query, or aggregate state. - Configurable per-target exclusions (
Type[title], case-sensitive glob) and redaction selectors, applied after equality so redaction cannot alter a comparison. - Impact estimates report only that a node's latest stored catalog contains
the exact
Type[title], bounded by a configured timeout and result limit, and labelled as such in every format.
catalog_api: v4is the supported path. Every request carriespersistence: {facts: false, catalog: false}, so a comparison leaves the target's stored factset and catalog untouched, and sends the target's own trusted facts.catalog_api: v3is a degraded path, with a non-suppressible trusted-fact warning in every output format, and an opt-in, never implicit, v4→v3 fallback.
piace capture factsandpiace capture catalogwrite PIACE envelopes: format version, target identity, source, capture timestamp, SHA-256 payload checksum, and a catalog's requested environment, compiler API version and input factset identity, written atomically at0600and never overwritten without--replace. Every field is validated on reuse.
- Three formats from one redacted result document, so they cannot disagree: text for a CI log (the only format that omits anything), JSON as the complete versioned record, and HTML as a complete, self-contained page with no JavaScript, webfonts, or images.
- The HTML report is an index of the run: every list of rows sits in a closed disclosure whose heading counts what it holds. Failures, the v3 warning and the outcome badges never collapse.
- Exit codes
0(clean / differences allowed),10(policy-disallowed difference),20(compilation failure) and30(operational error), with documented precedence. A run is nevercleanwhile any target has an unreported retrieval, compilation, normalization or content-verification failure.
- Independent hardened mTLS clients per service, with bounded timeouts, response size limits, and no redirect following.
- Private keys, certificate material and authorization headers never reach a diagnostic, report or log.
--debugprints request metadata only (safe for CI);--debug-dump-dirwrites verbatim, unredacted bodies to0600files in a0700directory, never to stdout or stderr.
- Dependency-free and CGO-free; Go 1.22+ is the only build requirement.
- CI runs
gofmt,go vet,go buildandgo test -raceon Linux and macOS, cross-compiles the full platform matrix on every pull request, and publishes a GitHub Release withSHA256SUMSfrom av*tag. The detached signature over the manifest is attached by hand afterwards, since CI holds no signing key, and the release notes say so.
catalog_api: v3withbaseline.source: puppetdbis not rejected. A v3 target is required to use a file baseline, but config validation does not yet enforce it. The configuration loads and the run overwrites the baseline it just read; the symptom on the next run is a baseline-environment mismatch naming the candidate environment. Setbaseline.source: fileyourself. See the README's "If you must use v3, compare against a captured file".- Two PuppetDB impact-endpoint behaviours are unconfirmed against a
deployment: that the impact PQL text is accepted at the root
/pdb/query/v4, and thatlimit/order_byare honoured there. Iforder_byis not honoured, a truncated impact sample is not reproducible. - The Puppet
Sensitivewire shape ({"__ptype":"Sensitive","__pvalue":…}) is derived from Puppet's Ruby serializer source rather than a captured response. A compiler emitting a different encoding would pass the acceptance suite with the value unredacted.
The last two are recorded as skipped tests carrying their confirmation
procedures in cmd/piace/acceptance_assumptions_test.go.