Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 1.1.1 — 2026-07-27

### Fixed

- Probabilistic provider evidence that omits its observed confidence can no
longer inherit the obligation's minimum confidence threshold and pass.
- The release validator now executes native Go fuzz targets for every
property-based invariant required by v1 §34.5.
- Performance evidence now records peak resident memory for the no-op
`version` invocation as the v1 idle-memory proxy.

## 1.1.0 — 2026-07-27

First release validated against the complete normative v1 contract.
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ FAIL REQ-AUTH-001

## Status

**v1.1.0** is the first release validated against the complete normative
**v1.1.1** is the current release validated against the complete normative
[`v1.md`](v1.md), including the executable
[§38 acceptance matrix](docs/acceptance-v1.md). The v1.0.x releases remain
available as immutable historical tags.
[§38 acceptance matrix](docs/acceptance-v1.md). It supersedes v1.1.0 by
correcting missing-confidence handling for probabilistic evidence. Earlier
releases remain available as immutable historical tags.

Breaking change from v0.x Product Contracts: [docs/migration-v0-to-v1.md](docs/migration-v0-to-v1.md).
Existing v1.0.x users: [v1.0.x → v1.1 migration](docs/migration-v1.0-to-v1.1.md).
Expand All @@ -44,7 +45,7 @@ intentci version
Requires Go 1.23+.

```bash
go install github.com/hypertrial/intentci/cmd/intentci@v1.1.0
go install github.com/hypertrial/intentci/cmd/intentci@v1.1.1
```

For development:
Expand Down
8 changes: 6 additions & 2 deletions docs/performance-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ Performance is recorded rather than gated by noisy hosted-runner wall time.
./scripts/record_performance.sh
```

The record contains platform, Go version, commit, packaged binary size, and
five-run benchmark samples for:
The record contains platform, Go version, commit, packaged binary size, peak
resident memory for the no-op `version` command, and five-run benchmark samples
for:

- CLI startup;
- compilation of 100 and 1,000 requirements;
- change-impact analysis over 10,000 files;
- aggregation of 10,000 obligation/test-case verdicts;
- bounded scheduler overhead.

Peak resident memory for `intentci version` is the v1 idle-memory proxy because
IntentCI is a terminating CLI rather than a resident service.

The targets remain those in [v1.md §28](../v1.md). Absolute Linux and macOS
records are uploaded as release evidence. Regressions are reviewed against the
same platform's previous record; hosted wall time is not an absolute merge
Expand Down
14 changes: 14 additions & 0 deletions docs/releases/v1.1.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# IntentCI v1.1.1

IntentCI v1.1.1 is the recommended v1 release.

This patch prevents probabilistic evidence with no observed confidence from
inheriting the obligation's minimum threshold and incorrectly passing. It also
adds native Go fuzz targets for every v1 §34.5 property invariant and records
peak resident memory in release performance evidence.

The release preserves valid v1.0.x and v1.1.0 inputs. The v1.1.0 tag remains
immutable but is superseded by v1.1.1.

The release contains reproducible Linux amd64 and macOS amd64/arm64 archives.
Verify every download against `checksums.txt` before execution.
2 changes: 1 addition & 1 deletion docs/v1-conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ no manually asserted pass state.
| V1-S31 | §31 internal component and persistence boundaries | `go test ./...` |
| V1-S32 | §32 internal Go interface behavior | `go test ./internal/provider ./internal/evidence ./internal/report` |
| V1-S33 | §33 external provider v1 subprocess protocol | `go test ./internal/provider -run 'External'` |
| V1-S34 | §34 unit, golden, integration, E2E, fuzz, mutation, race, and coverage strategy | `INTENTCI_RUN_MUTATION=1 ./scripts/validate_v1_release.sh` |
| V1-S34 | §34 unit, golden, integration, E2E, fuzz, mutation, race, and coverage strategy | `./scripts/check_fuzz.sh`; `INTENTCI_RUN_MUTATION=1 ./scripts/validate_v1_release.sh` |
| V1-S35 | §35 functional requirements | `go test ./tests/acceptance -run '^TestV1Acceptance$'` |
| V1-S36 | §36 first-run and error-message UX | `go test ./internal/initcmd ./internal/cli` |
| V1-S37 | §37 milestone exit criteria | staged green PR history plus `./scripts/validate_v1_release.sh` |
Expand Down
41 changes: 41 additions & 0 deletions internal/compiler/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package compiler

import (
"reflect"
"testing"

"github.com/hypertrial/intentci/internal/ir"
)

func FuzzV1DependencyGraphs(f *testing.F) {
f.Add([]byte{0, 1, 2}, false)
f.Add([]byte{1, 0}, true)
f.Fuzz(func(t *testing.T, edges []byte, selfCycle bool) {
if len(edges) > 64 {
t.Skip()
}
count := len(edges)%8 + 1
document := &ir.Document{SchemaVersion: ir.SchemaVersion}
for index := 0; index < count; index++ {
requirement := ir.Requirement{ID: "R" + string(rune('A'+index))}
if len(edges) > 0 {
target := int(edges[index%len(edges)]) % count
if target != index {
requirement.DependsOn = []string{"R" + string(rune('A'+target))}
}
}
document.Requirements = append(document.Requirements, requirement)
}
if selfCycle {
document.Requirements[0].DependsOn = []string{document.Requirements[0].ID}
}
first := validateGraph(document)
second := validateGraph(document)
if !reflect.DeepEqual(first, second) {
t.Fatalf("dependency diagnostics are nondeterministic:\n%+v\n%+v", first, second)
}
if selfCycle && len(first) == 0 {
t.Fatal("self dependency was not rejected")
}
})
}
60 changes: 60 additions & 0 deletions internal/evidence/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package evidence

import (
"os"
"path/filepath"
"reflect"
"testing"

"github.com/oklog/ulid/v2"
)

func FuzzV1ManifestHashing(f *testing.F) {
f.Add([]byte("alpha"), []byte("beta"))
f.Add([]byte{}, []byte{0, 1, 2})
f.Fuzz(func(t *testing.T, firstContent, secondContent []byte) {
if len(firstContent)+len(secondContent) > 4096 {
t.Skip()
}
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "a"), firstContent, 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "b"), secondContent, 0o600); err != nil {
t.Fatal(err)
}
first, err := hashArtifacts(root)
if err != nil {
t.Fatal(err)
}
second, err := hashArtifacts(root)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(first, second) {
t.Fatalf("manifest hashing is nondeterministic:\n%+v\n%+v", first, second)
}
})
}

func FuzzV1RunIDOrdering(f *testing.F) {
f.Add(uint8(2))
f.Add(uint8(16))
f.Fuzz(func(t *testing.T, requested uint8) {
count := int(requested%32) + 2
previous := NewRunID()
if _, err := ulid.ParseStrict(previous); err != nil {
t.Fatal(err)
}
for index := 1; index < count; index++ {
current := NewRunID()
if _, err := ulid.ParseStrict(current); err != nil {
t.Fatal(err)
}
if current <= previous {
t.Fatalf("run IDs are not strictly ordered: %q then %q", previous, current)
}
previous = current
}
})
}
3 changes: 0 additions & 3 deletions internal/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,9 +675,6 @@ func enrichEvidence(result *provider.Result, request provider.Request, implement
if evidence.Class == "" {
evidence.Class = firstNonEmpty(request.Spec.EvidenceClass, request.EvidenceClass, "deterministic")
}
if evidence.Confidence == nil {
evidence.Confidence = request.ConfidenceThreshold
}
if evidence.Status == "" {
switch {
case result.Status == "error":
Expand Down
11 changes: 10 additions & 1 deletion internal/executor/executor_v1_edges_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,18 @@ func TestEvidenceOutputRedactionAndCacheHelpers(t *testing.T) {
}, implementation)
if result.Evidence[0].Status != testCase.want ||
result.Evidence[0].Class != "probabilistic" ||
result.Evidence[0].Confidence == nil {
result.Evidence[0].Confidence != nil {
t.Fatalf("%+v", result)
}
node := ir.VerifyNode{Provider: &ir.ProviderSpec{Provider: "static", ID: "id"}}
got, _, _ := verdict.EvaluateNodeWithPolicy(
node,
map[string]provider.Result{"id": result},
verdict.EvidencePolicy{Class: "probabilistic", ConfidenceThreshold: &threshold},
)
if got == verdict.Pass {
t.Fatalf("missing observed confidence must not pass: %+v", result)
}
}
resultWithVersion := provider.Result{
Status: "completed", ProviderVersion: "custom",
Expand Down
26 changes: 26 additions & 0 deletions internal/impact/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package impact

import (
"path/filepath"
"testing"

"github.com/bmatcuk/doublestar/v4"
)

func FuzzV1PathMatching(f *testing.F) {
f.Add("src/**/*.go", "src/pkg/file.go")
f.Add("[", "src/file.go")
f.Add("docs/**", `docs\v1.md`)
f.Fuzz(func(t *testing.T, pattern, file string) {
if len(pattern) > 512 || len(file) > 512 {
t.Skip()
}
pattern = filepath.ToSlash(pattern)
file = filepath.ToSlash(file)
matched, err := doublestar.Match(pattern, file)
want := err == nil && matched
if got := PathMatches([]string{pattern}, file); got != want {
t.Fatalf("PathMatches(%q, %q)=%t, want %t", pattern, file, got, want)
}
})
}
45 changes: 45 additions & 0 deletions internal/ir/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package ir

import (
"reflect"
"testing"
)

func FuzzV1LogicalExpressionNormalization(f *testing.F) {
f.Add([]byte{0, 1, 2, 3})
f.Add([]byte{3, 3, 0})
f.Fuzz(func(t *testing.T, shape []byte) {
if len(shape) > 64 {
t.Skip()
}
nodes := make([]VerifyNode, 0, len(shape))
for _, value := range shape {
provider := &ProviderSpec{Provider: "command"}
if value%3 == 0 {
provider.ID = "explicit"
}
nodes = append(nodes, VerifyNode{Provider: provider})
}
if len(nodes) == 0 {
nodes = append(nodes, VerifyNode{Provider: &ProviderSpec{Provider: "command"}})
}
document := &Document{SchemaVersion: SchemaVersion, Hash: "document"}
requirements := []Requirement{{
ID: "R", Hash: "requirement",
Obligations: []Obligation{{
ID: "O", Hash: "obligation", Verify: VerifyNode{All: nodes},
}},
}}
first, err := BuildVerificationPlan(document, requirements)
if err != nil {
t.Fatal(err)
}
second, err := BuildVerificationPlan(document, requirements)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(first, second) {
t.Fatalf("logical normalization is nondeterministic:\n%+v\n%+v", first, second)
}
})
}
6 changes: 3 additions & 3 deletions internal/provider/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ func (p *CommandProvider) Execute(ctx context.Context, req Request) Result {
}
}
res.Evidence = []Evidence{{
ID: firstNonEmpty(req.Spec.ID, "command"),
Class: firstNonEmpty(req.Spec.EvidenceClass, req.EvidenceClass, "deterministic"),
Confidence: req.ConfidenceThreshold, Summary: summary, Passed: boolPtr(passed),
ID: firstNonEmpty(req.Spec.ID, "command"),
Class: firstNonEmpty(req.Spec.EvidenceClass, req.EvidenceClass, "deterministic"),
Summary: summary, Passed: boolPtr(passed),
Data: map[string]any{"exit_code": code, "run": req.Spec.Run},
StartedAt: process.StartedAt, CompletedAt: process.EndedAt,
}}
Expand Down
12 changes: 12 additions & 0 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ func TestCommandAndBoundary(t *testing.T) {
if res.Status != "completed" || res.Evidence[0].Passed == nil || !*res.Evidence[0].Passed {
t.Fatalf("%+v", res)
}
threshold := 0.8
res = p.Execute(context.Background(), provider.Request{
Root: t.TempDir(),
Spec: ir.ProviderSpec{
Provider: "command", ID: "probabilistic", Run: "true",
EvidenceClass: "probabilistic",
},
ConfidenceThreshold: &threshold,
})
if res.Evidence[0].Confidence != nil {
t.Fatalf("command provider invented observed confidence: %+v", res)
}
b, _ := reg.Get("boundary")
res = b.Execute(context.Background(), provider.Request{
ChangedFiles: []string{"migrations/1.sql"},
Expand Down
29 changes: 29 additions & 0 deletions internal/security/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package security

import (
"crypto/sha256"
"encoding/hex"
"strings"
"testing"
)

func FuzzV1Redaction(f *testing.F) {
f.Add([]byte("token"), "before %s after")
f.Add([]byte{0, 1, 2}, "TOKEN=%s")
f.Fuzz(func(t *testing.T, secretInput []byte, format string) {
if len(secretInput)+len(format) > 4096 {
t.Skip()
}
sum := sha256.Sum256(secretInput)
secret := "intentci-secret-" + hex.EncodeToString(sum[:])
redactor := NewRedactor([]string{"TOKEN"}, []string{"TOKEN=" + secret})
content := strings.ReplaceAll(format, "%s", secret)
redacted := redactor.Redact(content)
if strings.Contains(redacted, secret) {
t.Fatalf("secret remained after redaction: %q", redacted)
}
if twice := redactor.Redact(redacted); twice != redacted {
t.Fatalf("redaction is not idempotent: %q != %q", twice, redacted)
}
})
}
29 changes: 29 additions & 0 deletions internal/verdict/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package verdict

import (
"slices"
"testing"
)

func FuzzV1VerdictAggregation(f *testing.F) {
f.Add([]byte{0, 1, 2, 3, 4, 5, 6})
f.Add([]byte{6, 0})
f.Fuzz(func(t *testing.T, encoded []byte) {
if len(encoded) > 128 {
t.Skip()
}
values := []string{Pass, Skipped, Unproven, Uncertain, ReviewRequired, Error, Fail}
requirements := make([]RequirementResult, 0, len(encoded))
for _, value := range encoded {
requirements = append(requirements, RequirementResult{
ID: "R", Priority: "required", Verdict: values[int(value)%len(values)],
})
}
forward := AggregateRun(requirements).Verdict
slices.Reverse(requirements)
reverse := AggregateRun(requirements).Verdict
if forward != reverse {
t.Fatalf("aggregation changed with ordering: %s != %s", forward, reverse)
}
})
}
4 changes: 2 additions & 2 deletions internal/version/version.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package version

// Version is set via ldflags at release time.
var Version = "1.1.0"
var Version = "1.1.1"

// String returns the version string.
func String() string {
if Version == "" {
return "1.1.0"
return "1.1.1"
}
return Version
}
Loading