Skip to content

feat: Add deterministic behavior and logs#7

Merged
math280h merged 1 commit intomainfrom
feat/deterministic-snapshots
Feb 26, 2026
Merged

feat: Add deterministic behavior and logs#7
math280h merged 1 commit intomainfrom
feat/deterministic-snapshots

Conversation

@math280h
Copy link
Copy Markdown
Owner

No description provided.

@math280h math280h self-assigned this Feb 26, 2026
Copilot AI review requested due to automatic review settings February 26, 2026 01:50
@github-actions
Copy link
Copy Markdown

Coverage

Coverage Report

Metric Value
Total lines 5512
Covered lines 4614
Missed lines 898
Coverage 83.71%
Tests passed 163
Tests failed 0
Tests ignored 0
Coverage command exit 0
Per-file coverage (top 40 by missed lines)
File Stmts Miss Cover Missing
/home/runner/work/safe-pkgs/safe-pkgs/src/service.rs 342 137 59.94% 40, 41, 42, 43, 44, 45, 89, 90, ...
/home/runner/work/safe-pkgs/safe-pkgs/src/custom_rules.rs 194 119 38.66% 41, 70, 71, 80, 81, 82, 83, 96, ...
/home/runner/work/safe-pkgs/safe-pkgs/src/config/custom_rules.rs 155 67 56.77% 132, 159, 165, 172, 175, 194, 195, 197, ...
/home/runner/work/safe-pkgs/safe-pkgs/crates/checks/staleness/src/lib.rs 210 55 73.81% 63, 64, 65, 66, 67, 68, 69, 70, ...
/home/runner/work/safe-pkgs/safe-pkgs/src/policy_snapshot.rs 209 48 77.03% 120, 146, 147, 148, 149, 150, 151, 152, ...
/home/runner/work/safe-pkgs/safe-pkgs/src/main.rs 93 44 52.69% 43, 115, 116, 118, 119, 120, 121, 122, ...
/home/runner/work/safe-pkgs/safe-pkgs/crates/core/src/lib.rs 347 37 89.34% 82, 83, 84, 100, 101, 102, 118, 119, ...
/home/runner/work/safe-pkgs/safe-pkgs/crates/registry/npm/src/registry.rs 292 33 88.70% 64, 90, 91, 94, 95, 96, 97, 98, ...
/home/runner/work/safe-pkgs/safe-pkgs/src/mcp/server.rs 97 32 67.01% 104, 105, 106, 128, 129, 131, 132, 133, ...
/home/runner/work/safe-pkgs/safe-pkgs/src/checks.rs 431 31 92.81% 184, 198, 199, 200, 201, 202, 203, 204, ...
/home/runner/work/safe-pkgs/safe-pkgs/crates/registry/pypi/src/registry.rs 225 25 88.89% 47, 48, 49, 54, 55, 56, 71, 72, ...
/home/runner/work/safe-pkgs/safe-pkgs/crates/registry/pypi/src/lockfile.rs 393 25 93.64% 26, 27, 28, 43, 44, 45, 62, 63, ...
/home/runner/work/safe-pkgs/safe-pkgs/src/cache.rs 123 22 82.11% 25, 26, 27, 28, 29, 30, 31, 32, ...
/home/runner/work/safe-pkgs/safe-pkgs/src/config/mod.rs 171 20 88.30% 196, 197, 198, 209, 295, 296, 297, 298, ...
/home/runner/work/safe-pkgs/safe-pkgs/crates/registry/cargo/src/registry.rs 201 20 90.05% 36, 37, 38, 43, 44, 45, 60, 61, ...
/home/runner/work/safe-pkgs/safe-pkgs/crates/registry/cargo/src/lockfile.rs 308 20 93.51% 27, 28, 29, 44, 45, 46, 60, 67, ...
/home/runner/work/safe-pkgs/safe-pkgs/src/bin/safe-pkgs-mcp.rs 37 16 56.76% 12, 13, 14, 15, 16, 18, 21, 22, ...
/home/runner/work/safe-pkgs/safe-pkgs/crates/osv/src/lib.rs 162 14 91.36% 9, 10, 11, 12, 13, 14, 15, 16, ...
/home/runner/work/safe-pkgs/safe-pkgs/crates/registry/npm/src/lockfile.rs 294 13 95.58% 27, 28, 29, 44, 45, 46, 57, 93, ...
/home/runner/work/safe-pkgs/safe-pkgs/crates/checks/typosquat/src/lib.rs 116 13 88.79% 64, 75, 78, 79, 87, 112, 154, 155, ...
/home/runner/work/safe-pkgs/safe-pkgs/src/registries/mod.rs 152 9 94.08% 58, 59, 60, 134, 154, 163, 164, 165, ...
/home/runner/work/safe-pkgs/safe-pkgs/crates/checks/existence/src/lib.rs 48 3 93.75% 36, 37, 38
/home/runner/work/safe-pkgs/safe-pkgs/crates/checks/advisory/src/lib.rs 116 3 97.41% 68, 129, 142
/home/runner/work/safe-pkgs/safe-pkgs/src/audit_log.rs 58 2 96.55% 70, 124
/home/runner/work/safe-pkgs/safe-pkgs/src/support_map.rs 142 1 99.30% 58
/home/runner/work/safe-pkgs/safe-pkgs/crates/registry/pypi/src/lib.rs 13 0 100.00% -
/home/runner/work/safe-pkgs/safe-pkgs/crates/registry/npm/src/lib.rs 13 0 100.00% -
/home/runner/work/safe-pkgs/safe-pkgs/crates/registry/cargo/src/lib.rs 13 0 100.00% -
/home/runner/work/safe-pkgs/safe-pkgs/crates/checks/version-age/src/lib.rs 66 0 100.00% -
/home/runner/work/safe-pkgs/safe-pkgs/crates/checks/popularity/src/lib.rs 81 0 100.00% -
/home/runner/work/safe-pkgs/safe-pkgs/crates/checks/install-script/src/lib.rs 55 0 100.00% -

@math280h math280h merged commit 5dd288b into main Feb 26, 2026
9 checks passed
@math280h math280h deleted the feat/deterministic-snapshots branch February 26, 2026 01:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds deterministic policy evaluation context and correlation data across tool responses, caching, and audit logs to make outputs reproducible and auditable.

Changes:

  • Introduces deterministic fingerprints (config, policy) in ToolResponse/LockfileResponse and updates tests/docs accordingly.
  • Adds policy snapshot + fingerprinting utilities (policy_snapshot module) and logs richer audit context (snapshot version, enabled checks, evaluation time).
  • Threads a deterministic evaluation_time through check execution (with optional SAFE_PKGS_EVALUATION_TIME override).

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/mcp_stdio.rs Asserts MCP JSON responses include correctly-shaped fingerprints.
tests/check_package_mock_http.rs Adds optional debug printing of JSON responses and asserts fingerprints exist.
src/types.rs Adds DecisionFingerprints and embeds it in response types.
src/tests/service.rs Updates service tests to validate fingerprints and cache key behavior.
src/tests/policy_snapshot.rs Adds tests for stable config fingerprinting and policy fingerprint changes.
src/tests/checks.rs Adds test ensuring enabled check IDs are normalized/sorted deterministically.
src/tests/audit_log.rs Extends audit log tests to cover new deterministic context fields.
src/service.rs Builds per-registry policy snapshots, adds deterministic evaluation clock, updates caching + audit logging, returns fingerprints.
src/policy_snapshot.rs New canonicalization + hashing implementation for deterministic config/policy fingerprints.
src/mcp/server.rs Updates tool descriptions to document fingerprints in outputs.
src/main.rs Registers the new policy_snapshot module.
src/custom_rules.rs Makes custom rule evaluation time deterministic via the execution context.
src/checks.rs Adds run_all_checks_at_time and deterministic enabled-check ID collection.
src/audit_log.rs Adds deterministic policy context fields to audit records.
docs/cache-deep-dive.md Updates cache key format and documents fingerprint calculation/canonicalization.
crates/core/src/lib.rs Extends CheckExecutionContext with evaluation_time.
crates/checks/version-age/src/lib.rs Uses evaluation_time to compute age deterministically.
crates/checks/staleness/src/lib.rs Uses evaluation_time to compute staleness deterministically.
crates/checks/popularity/src/lib.rs Uses evaluation_time to compute package age deterministically.
README.md Documents fingerprints and deterministic evaluation clock env var.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/policy_snapshot.rs
Comment on lines +150 to +156
left.reason.as_deref(),
)
.cmp(&(
right.id.as_str(),
right.enabled,
right.severity,
right.reason.as_deref(),
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom_rules sort key omits registries and conditions. If two rules share the same (id, enabled, severity, reason), the comparator returns Equal; Rust’s sort_by is unstable for equal keys, which can reorder those entries nondeterministically and change the fingerprint across runs. To guarantee determinism, include additional fields in the ordering key (e.g., registries + a canonical string of conditions) or sort by a fully-canonical serialized representation.

Suggested change
left.reason.as_deref(),
)
.cmp(&(
right.id.as_str(),
right.enabled,
right.severity,
right.reason.as_deref(),
left.reason.as_deref(),
&left.registries,
&left.conditions,
)
.cmp(&(
right.id.as_str(),
right.enabled,
right.severity,
right.reason.as_deref(),
&right.registries,
&right.conditions,

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +39
let age_days = resolved_version
.published
.map(|published| (context.evaluation_time - published).num_days());

Ok(run(
context.package_name,
resolved_version,
context.policy.min_version_age_days,
age_days,
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

age_days can become negative when evaluation_time is earlier than published (possible when SAFE_PKGS_EVALUATION_TIME is set). That would treat a future publish date as “very young” and could trigger incorrect findings. Consider clamping age_days to 0 (or explicitly handling negative durations) before passing it into run.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to 47
let age_days = resolved_version
.published
.map(|published| (context.evaluation_time - published).num_days());

Ok(run(
context.package_name,
resolved_version,
context.weekly_downloads,
context.policy.min_weekly_downloads,
DEFAULT_YOUNG_PACKAGE_AGE_DAYS,
age_days,
)
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

age_days can be negative when evaluation_time is earlier than published (e.g., via SAFE_PKGS_EVALUATION_TIME). That would cause popularity findings to treat a future publish date as “young” and potentially flag incorrectly. Consider clamping to 0 (or otherwise handling negative durations) before using age_days.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +40
context
.evaluation_time
.signed_duration_since(published)
.num_days()
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requested_age_days is derived from evaluation_time.signed_duration_since(published).num_days(), which can be negative if evaluation_time is before published (possible with the deterministic clock override). Consider clamping negative values to 0 (or handling them explicitly) so “future” publish dates don’t lead to surprising staleness behavior.

Suggested change
context
.evaluation_time
.signed_duration_since(published)
.num_days()
let days = context
.evaluation_time
.signed_duration_since(published)
.num_days();
if days < 0 { 0 } else { days }

Copilot uses AI. Check for mistakes.
Comment thread src/service.rs
spec.version.as_deref(),
registry_key,
context,
evaluation_time,
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

evaluation_time is passed by value into evaluate_package_at_time inside the for spec in package_specs loop. chrono::DateTime<Utc> is not Copy, so this will move evaluation_time on the first iteration and fail to compile (or force unintended cloning elsewhere). Consider passing evaluation_time.clone() here, or change evaluate_package_at_time to take &DateTime<Utc> (and clone only where needed).

Suggested change
evaluation_time,
evaluation_time.clone(),

Copilot uses AI. Check for mistakes.
Comment thread src/service.rs
return Ok(None);
};
let raw = raw.to_string_lossy();
let parsed = chrono::DateTime::parse_from_rfc3339(raw.as_ref())
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SAFE_PKGS_EVALUATION_TIME parsing treats an empty value as an error (e.g., SAFE_PKGS_EVALUATION_TIME=), which can unexpectedly break execution in environments that set empty vars. Consider trimming and returning Ok(None) when the value is empty/whitespace before attempting RFC3339 parsing.

Suggested change
let parsed = chrono::DateTime::parse_from_rfc3339(raw.as_ref())
let trimmed = raw.trim();
if trimmed.is_empty() {
return Ok(None);
}
let parsed = chrono::DateTime::parse_from_rfc3339(trimmed)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants