merge origin/master into llm-recording-and-analysis

both sides independently fixed the same three bugs, so each one had to pick a
winner rather than keep both implementations.

extractor encoding: master's recordableValue in worker.go wins over ours in
marshal.go, since master's is pinned by extractor_encoding_test.go and ours had
no tests. our error semantics stay: encodeExtractorValue still returns an error
instead of nil, so an extractor cannot vanish from the trace silently.

apply errors: only the residual generic branch takes master's unconfirmed copy,
where the device may have committed the action before the call failed. the
finer branches that know nothing was dispatched keep lastAction = nil, and our
actionSkipReason taxonomy stays alongside master's held/skippedVerification.

selector matching: our matchAttr with matchSelectorKind wins over master's
match, since ours also handles idPrefix. matchSelector now calls it, which git
did not flag as a conflict and left calling a function our side had deleted.

the ltl doc comment takes master's correction: an unbounded eventually that
never fires IS violated at run end.
This commit is contained in:
pj committed 2026-08-16 18:10:55 +05:30
commit 6e85cac8b3
130 files changed
+12772 -1617

No files matched your search

+36 -8
View File
@@ -247,7 +247,15 @@ func Execute(ctx context.Context, options Options, stdout io.Writer) error {
// --exit-on-violation a run that found violations is still a successful run
// (the summary reports them), which is the behaviour every existing caller
// depends on.
//
// A run none of whose steps reached the verifier fails whatever the flags say,
// because it holds no verdict to report. The threshold is every step and not a
// fraction of them: a screen that composes now and then costs a healthy android
// run a step or two, and a check that fired on those would be red on every run.
func runOutcome(options Options, summary runner.Summary) error {
if summary.Steps > 0 && summary.SkippedVerification == summary.Steps {
return VacuousRunError{Steps: summary.Steps}
}
if options.ExitOnViolation && len(summary.Violations) > 0 {
return ViolationsError{Count: len(summary.Violations)}
}
@@ -283,6 +291,22 @@ func BundleSpec(specPath string, seed int64) (bundler.Result, error) {
})
}
// VacuousRunError reports a run in which no step reached the verifier, so no
// property ever judged anything. It is not a clean run and it is not a found
// bug: it is a run that produced no evidence either way, and the absence of
// violations in it says nothing about the app. It stays untyped to the CLI's
// violation path on purpose, so it exits 1 as a broken run rather than 2.
type VacuousRunError struct {
Steps int
}
func (e VacuousRunError) Error() string {
return fmt.Sprintf(
"%d step(s) ran and none of them reached the verifier: the screen was "+
"still moving every time it was read, so no property judged this run",
e.Steps)
}
// bundleInputs holds the pre-driver assembly: alias map, seed, esbuild defines,
// and the resolved spec-API/goja-runtime paths the bundler consumes.
type bundleInputs struct {
@@ -372,16 +396,20 @@ func resolveRuntimeSibling(specAPIPath, userSpecPath, filename string) string {
return ""
}
// resolveSpecAPIPath returns the path to pkg/spec/src/index.ts inside
// a sanderling source checkout, searched upward from the spec file and the cwd.
// Returns "" when not found, in which case esbuild resolves @sanderling/spec via
// node_modules the way a downstream user's project would.
// resolveSpecAPIPath returns the path to the spec API's index.ts: a sanderling
// source checkout first, searched upward from the spec file and the cwd, then
// an installed node_modules/@sanderling/spec. Aliasing the installed copy is
// what keeps the spec and the runtime entry on one module graph; resolving the
// bare specifier through package.json "exports" would load dist/ alongside the
// runtime's src/ and give sampler-rng.ts two instances.
func resolveSpecAPIPath(specPath string) string {
var candidates []string
var checkout, installed []string
if absoluteSpec, err := filepath.Abs(specPath); err == nil {
directory := filepath.Dir(absoluteSpec)
for {
candidates = append(candidates, filepath.Join(directory, "pkg/spec/src/index.ts"))
checkout = append(checkout, filepath.Join(directory, "pkg/spec/src/index.ts"))
installed = append(installed,
filepath.Join(directory, "node_modules/@sanderling/spec/src/index.ts"))
parent := filepath.Dir(directory)
if parent == directory {
break
@@ -390,9 +418,9 @@ func resolveSpecAPIPath(specPath string) string {
}
}
if cwd, err := os.Getwd(); err == nil {
candidates = append(candidates, filepath.Join(cwd, "pkg/spec/src/index.ts"))
checkout = append(checkout, filepath.Join(cwd, "pkg/spec/src/index.ts"))
}
for _, candidate := range candidates {
for _, candidate := range append(checkout, installed...) {
if _, err := os.Stat(candidate); err == nil {
return candidate
}