Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8a267f4
feat: add recommendation pipeline contract
nash87 May 19, 2026
a195e6f
ci: harden zizmor advisory workflow
nash87 May 19, 2026
4ed39b9
fix: address recommendation review findings
nash87 May 19, 2026
e4608ac
fix: clear recommendation gate blockers
nash87 May 19, 2026
310f19d
test: simplify recommendation contract gate
nash87 May 19, 2026
11afc35
Merge remote-tracking branch 'github-https/main' into t-6318-recommen…
nash87 May 20, 2026
6815aa7
feat: add exact-cover allocation strategy
nash87 May 20, 2026
f09e878
feat: audit exact-cover allocations
nash87 May 20, 2026
bb6c352
Merge GitHub main into exact-cover contract
nash87 May 21, 2026
9c8e77d
Fix recommendation stats allocation move
nash87 May 21, 2026
e8bbb47
Fix exact-cover audit and limits
nash87 May 21, 2026
8c88a34
docs: capture exact-cover legal catalog evidence
nash87 May 21, 2026
e3dda2e
docs: keep legal evidence tied to current PR head
nash87 May 21, 2026
9d60b11
feat(reco): exact-cover allocation-strategy normalization + lock-free…
nash87 Jun 3, 2026
f71b017
Merge GitHub main into exact-cover contract (dep bumps #664-673)
nash87 Jun 3, 2026
980323b
fix(reco): take &str in normalize_allocation_strategy (clippy needles…
nash87 Jun 3, 2026
c89a212
fix(reco): address PR #663 review threads — audit fail-closed, bounds…
nash87 Jun 9, 2026
68e7e46
fix(security): suppress RUSTSEC-2026-0173 proc-macro-error2 unmaintai…
nash87 Jun 9, 2026
2e81915
fix(security): suppress RUSTSEC-2026-0173 proc-macro-error2 unmaintai…
nash87 Jun 9, 2026
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
25 changes: 24 additions & 1 deletion .nido/local-ci.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
# ParkHub Rust local CI entrypoints for Nido-first tabs.
#
# `nido ci run --gate legal-docs` is the cheap author-time evidence gate.
# `nido ci run --gate recommendation-contract` is the cheap author-time
# cross-language recommendation fixture and policy gate.
# `nido ci run --gate legal-docs` keeps legal-readiness evidence current.
# `nido ci run --gate pr` delegates to the repository's existing SHA-keyed
# fop/Nido local CI attestation script and must pass before pushing/releasing.

[[gates]]
name = "recommendation-contract"

[[gates.steps]]
name = "recommendation-contract"
command = "scripts/check-recommendation-contract.sh"
timeout_secs = 90
allow_failure = false

[[gates.steps]]
name = "working-tree-whitespace"
command = "git diff --check"
timeout_secs = 60
allow_failure = false

[[gates]]
name = "legal-docs"

Expand All @@ -19,6 +36,12 @@ command = "scripts/tests/test-legal-openapi-contract.sh"
timeout_secs = 60
allow_failure = false

[[gates.steps]]
name = "recommendation-contract"
command = "scripts/check-recommendation-contract.sh"
timeout_secs = 90
allow_failure = false

[[gates.steps]]
name = "working-tree-whitespace"
command = "git diff --check"
Expand Down
1 change: 1 addition & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ ignore = [
"RUSTSEC-2024-0420", # gtk-rs GTK3 unmaintained (gtk3-macros)
"RUSTSEC-2024-0436", # paste unmaintained
"RUSTSEC-2024-0370", # proc-macro-error unmaintained
"RUSTSEC-2026-0173", # proc-macro-error2 unmaintained (transitive via validator_derive -> validator 0.20; no fixed version)
"RUSTSEC-2023-0071", # rsa Marvin Attack (transitive dep)
"RUSTSEC-2025-0057", # fxhash unmaintained (transitive via selectors)
"RUSTSEC-2023-0019", # kuchiki unmaintained (transitive via printpdf)
Expand Down
6 changes: 6 additions & 0 deletions docs/deployment-readiness-record.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ Attach the JSON output or copy the fields below. Treat the catalog as
reference-only; attorney review, citation verification, deployment-specific
configuration review, human signoff, and final legal judgment remain required.

For the current `exact_cover_v1` release candidate, a captured catalog summary
lives at
`docs/release-evidence/2026-05-21-exact-cover-nido-legal-catalog.md`. Replace
that evidence with a fresh capture when the release candidate SHA, deployment
target, or legal catalog revision changes.

| Field | Captured value |
| --- | --- |
| Capture command | |
Expand Down
57 changes: 52 additions & 5 deletions docs/recommendation-engine-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ adapter algorithm for the external fop-pipeline service and must fall back to
`weighted_v1` on every missing endpoint, timeout, non-2xx response, invalid
response, or unknown slot ID.

`exact_cover_v1` is a separate batch-allocation strategy for operational
scheduling workflows: recurring reservations, tenant quotas, EV/fleet/zone
constraints, accessible-space facility constraints, and maintenance-window
exclusions. It is not the default quick-booking scorer and must not replace
`weighted_v1` for ordinary single-slot recommendations. It reports solved vs
fallback status, selected option IDs, covered constraints, search-node count,
and the same legal boundary used by served recommendations.

Default weights:

| Key | Default | Meaning |
Expand Down Expand Up @@ -55,6 +63,18 @@ Changing `weighted_v1` semantics is not allowed. Any ML or tenant-specific
strategy must be introduced as a new algorithm version and must pass parity
fixtures against `weighted_v1` before rollout.

Changing `exact_cover_v1` semantics must be done through new shared fixtures.
The first parity fixture is
`docs/recommendation-engine-fixtures/exact_cover_v1.batch_basic.json`; Rust and
PHP must keep the selected option IDs and covered constraints identical.

`exact_cover_v1` is deterministic by contract. The same normalized input must
return the same selected option IDs, covered constraints, fallback status, and
search-node count in Rust and PHP. Candidate ordering must not use randomness,
wall-clock time, database iteration order, or locale-sensitive sorting. Equal
weights are ordered by stable option ID. No-solution cases must return an
explicit fallback status rather than a partial allocation.

## Config Boundary

The Rust module registry exposes a JSON Schema for `recommendations` through the
Expand All @@ -64,6 +84,16 @@ legacy-safe defaults. The `explain` and `profile_safe_mode` settings are
reserved, fail-closed fields: attempts to set them to `false` are rejected by
schema and ignored by runtime loading.

Batch allocation has its own config surface under the same module:
`allocation_strategy` (`weighted_v1` or `exact_cover_v1`),
`exact_cover_max_options` (1..256), and `exact_cover_max_search_nodes`
(1..10000). The quick-booking endpoint does not switch to exact-cover when this
is enabled; `exact_cover_v1` is exposed through the admin-only
`POST /api/v1/recommendations/allocation/exact-cover` utility for batch or
recurring scheduling inputs. The endpoint returns `allocation_trace_id` so an
operator can connect a solver result to the immutable audit trace without
logging raw request payloads in customer-facing surfaces.

`fop_pipeline_v1` uses the fop-pipeline JSON/HTTP boundary:
`POST {pipeline_endpoint}/pipeline/{pipeline_name}/run`. ParkHub sends the
candidate slots, weights, `profile_safe_mode`, explanation requirement, and
Expand All @@ -90,15 +120,34 @@ audit events. Acceptance metrics remain `null` with
`acceptance_metric_source=not_tracked` until explicit accept/reject events exist,
so the endpoint does not infer acceptance from unrelated booking state.

Every served `exact_cover_v1` allocation must also preserve an immutable
allocation trace. At minimum the trace stores request ID, solver name and
version, SHA-256 config hash, constraint set hash, candidate set hash,
selected option IDs, rejected candidate IDs, tie-break inputs, actor or service
principal, tenant ID, timestamp, fallback status, and retention/deletion class.
These traces are operational evidence and may be personal data if they can be
linked to a person or vehicle, so export and erasure handling must be designed
before customer rollout.

## Compliance Boundary

This is engineering compliance, not legal advice. For German/EU/international
use, the recommendation surface must keep:

- data minimization: no sensitive categories, location history beyond parking
usage, or unrelated profile attributes in the score inputs;
usage, or unrelated profile attributes in the score inputs; recommendation
and allocation inputs/outputs must use pseudonymous IDs only and must not
include names, emails, license plates, IP addresses, or free-text personal
data;
- explainability: every score must keep a reason or badge that can be audited;
- operator control: weight changes must be authenticated, audited, and reversible;
- hard constraints: accessible-space, EV/fleet, tenant quota, reserved-inventory,
and maintenance-window requirements that represent user-declared needs, legal
obligations, or operator policy must be modeled as eligibility constraints,
not as soft scoring bonuses that weights can override;
- config governance: fairness/accessibility/eligibility parameters need explicit
min/max bounds, per-tenant defaults, role-based update permission, change
reason, rollback evidence, and a legal-review flag before sensitive changes;
- security evidence: SBOM/provenance/vulnerability handling remains part of the
ParkHub CI/CD baseline before business rollout;
- legal review: public ToS/privacy/profiling wording must go through `fop legal`
Expand Down Expand Up @@ -140,10 +189,8 @@ Relevant current public references:

## Legal Review Packet

`fop legal` can draft the supporting documents, and
`fop legal catalog --json` can provide the current review catalog provenance and
safety flags, but generated text and catalog entries are not shipping approval.
Treat the commands below as review inputs only:
`fop legal` can draft the supporting documents, but the generated text is not a
shipping approval. Treat the commands below as review inputs only:

```bash
NO_COLOR=true fop legal privacy "ParkHub"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"schema_version": "parkhub.recommendation.fixture.v1",
"algorithm": "exact_cover_v1",
"workflow": "batch_allocation",
"profile_safe_mode": true,
"required_constraints": [
"accessible",
"ev",
"tenant:alpha",
"tenant:beta"
],
"options": [
{
"id": "slot-a",
"covers": ["tenant:alpha", "ev"],
"weight": 90
},
{
"id": "slot-b",
"covers": ["tenant:beta", "accessible"],
"weight": 80
},
{
"id": "slot-c",
"covers": ["tenant:beta"],
"weight": 70
}
],
"expected": {
"status": "solved",
"selected_option_ids": ["slot-a", "slot-b"],
"covered_constraints": [
"accessible",
"ev",
"tenant:alpha",
"tenant:beta"
]
},
"legal_boundary": {
"legal_review_required": true,
"attorney_review_status": "required_before_customer_wording",
"execution_allowed": false,
"disclaimer": "Exact-cover allocation is operational scheduling support; attorney review, citation verification, client authorization, and final legal judgment remain required before customer-facing legal or profiling claims ship."
}
}
25 changes: 25 additions & 0 deletions docs/recommendation-engine-fixtures/exact_cover_v1.empty.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"schema_version": "parkhub.recommendation.fixture.v1",
"algorithm": "exact_cover_v1",
"workflow": "batch_allocation",
"profile_safe_mode": true,
"required_constraints": [],
"options": [
{
"id": "slot-a",
"covers": ["tenant:alpha"],
"weight": 90
}
],
"expected": {
"status": "solved",
"selected_option_ids": [],
"covered_constraints": []
},
"legal_boundary": {
"legal_review_required": true,
"attorney_review_status": "required_before_customer_wording",
"execution_allowed": false,
"disclaimer": "No constraints means no exact-cover allocation is required; attorney review, citation verification, client authorization, and final legal judgment remain required before customer-facing legal or profiling claims ship."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"schema_version": "parkhub.recommendation.fixture.v1",
"algorithm": "exact_cover_v1",
"workflow": "batch_allocation",
"profile_safe_mode": true,
"required_constraints": ["tenant:alpha"],
"options": [
{
"id": "slot-b",
"covers": ["tenant:alpha"],
"weight": 80
},
{
"id": "slot-a",
"covers": ["tenant:alpha"],
"weight": 80
},
{
"id": "slot-c",
"covers": ["tenant:alpha"],
"weight": 70
}
],
"expected": {
"status": "solved",
"selected_option_ids": ["slot-a"],
"covered_constraints": ["tenant:alpha"]
},
"legal_boundary": {
"legal_review_required": true,
"attorney_review_status": "required_before_customer_wording",
"execution_allowed": false,
"disclaimer": "Equal-weight candidates use stable option-id ordering as the deterministic fairness tie-break; attorney review, citation verification, client authorization, and final legal judgment remain required before customer-facing legal or profiling claims ship."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"schema_version": "parkhub.recommendation.fixture.v1",
"algorithm": "exact_cover_v1",
"workflow": "batch_allocation",
"profile_safe_mode": true,
"required_constraints": ["tenant:alpha", "maintenance:open"],
"options": [
{
"id": "slot-a",
"covers": ["tenant:alpha"],
"weight": 90
}
],
"expected": {
"status": "fallback_no_solution",
"selected_option_ids": [],
"covered_constraints": []
},
"legal_boundary": {
"legal_review_required": true,
"attorney_review_status": "required_before_customer_wording",
"execution_allowed": false,
"disclaimer": "Missing maintenance-window coverage must fail closed and let the caller use a safe fallback; attorney review, citation verification, client authorization, and final legal judgment remain required before customer-facing legal or profiling claims ship."
}
}
4 changes: 4 additions & 0 deletions docs/release-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ Use this before tagging a ParkHub release from this repo.
- Capture the current legal catalog `source_revision`, `generated_at`,
`requires_attorney_review`, `requires_human_signoff`, `execution_allowed`, and
`safety_boundary` values in the deployment readiness record before release.
- For the current `exact_cover_v1` release candidate, review
`docs/release-evidence/2026-05-21-exact-cover-nido-legal-catalog.md` and
replace it with a fresh capture if the head SHA, legal catalog revision, or
deployment target changes before release.

## Quality bar

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 2026-05-21 Exact-Cover Nido/fop Legal Catalog Evidence

This release-candidate evidence was captured for the ParkHub Rust
`exact_cover_v1` allocation work before release or customer-facing claims.

## Candidate

| Field | Value |
| --- | --- |
| Repository | `nash87/parkhub-rust` |
| Pull request | `#663` |
| Head SHA | Verify against the current PR head before release. |
| Local Nido PR gate report | `.fop/reports/local-ci-pr-<current-pr-head-sha>.json` must exist and pass for the current PR head. |
| Candidate feature SHA | `e8bbb475d638469d4d9a0421ef3d7a7c4d61d3fa` |

## Capture

| Field | Value |
| --- | --- |
| Capture command | `NO_COLOR=true fop legal catalog --json` |
| Captured by / date | Codex on 2026-05-21 |
| Catalog id / source / version | `anthropic-claude-for-legal` / `claude-for-legal` / `2026-05-15-review` |
| Catalog `source_revision` | `9cecd91` |
| Catalog `generated_at` | `2026-05-21T21:32:11.995244005Z` |
| Catalog `requires_attorney_review` | `true` |
| Catalog `requires_human_signoff` | `true` |
| Catalog `execution_allowed` | `false` |
| Installed Nido legal entrypoint | Not exposed by the installed Nido CLI; use `fop legal catalog --json` until `nido legal` exists. |
| Catalog `safety_boundary` | Reference catalog only. Claude for Legal plugins can help draft, triage, and monitor legal work, but attorney review, citation verification, client authorization, and final legal judgment remain required. fop exposes install and deploy commands as text only. |

## Release Interpretation

- `exact_cover_v1` remains operational scheduling support only.
- This catalog output is reference-only evidence, not legal advice.
- Public ToS, privacy, profiling, accessibility, or compliance wording still
requires attorney review, citation verification, deployment-specific configuration review,
and final human signoff.
- The release remains blocked for production/customer-facing legal claims until
the deployment readiness record is completed for the actual operator,
processors, jurisdictions, enabled modules, and hosting model.
4 changes: 4 additions & 0 deletions osv-scanner.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,7 @@ reason = "glib gtk-rs unchecked-callback PSK leak; not used by parkhub-server (T
[[IgnoredVulns]]
id = "GHSA-cq8v-f236-94qc"
reason = "rand 0.7.3 transitive; bumped where direct, locked elsewhere"

[[IgnoredVulns]]
id = "RUSTSEC-2026-0173"
reason = "proc-macro-error2 unmaintained (transitive via validator_derive -> validator 0.20); no fixed version published"
8 changes: 8 additions & 0 deletions parkhub-server/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ pub mod rate_dashboard;
#[cfg(feature = "mod-rbac")]
pub mod rbac;
#[cfg(feature = "mod-recommendations")]
pub mod recommendation_allocation;
#[cfg(feature = "mod-recommendations")]
pub mod recommendations;
#[cfg(feature = "mod-recurring")]
pub mod recurring;
Expand Down Expand Up @@ -291,6 +293,8 @@ use notification_center::{
#[cfg(feature = "mod-notifications")]
use notifications::{list_notifications, mark_all_notifications_read, mark_notification_read};
#[cfg(feature = "mod-recommendations")]
use recommendation_allocation::solve_exact_cover_allocation;
#[cfg(feature = "mod-recommendations")]
use recommendations::{get_recommendation_stats, get_recommendations};
#[cfg(feature = "mod-recurring")]
use recurring::{
Expand Down Expand Up @@ -1397,6 +1401,10 @@ fn booking_protected_routes() -> Router<SharedState> {
{
router = router
.route("/api/v1/bookings/recommendations", get(get_recommendations))
.route(
"/api/v1/recommendations/allocation/exact-cover",
post(solve_exact_cover_allocation),
)
.route(
"/api/v1/recommendations/stats",
get(get_recommendation_stats),
Expand Down
Loading
Loading