Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
24d2c12
feat(cli): add `actual repo onboard` for public repository onboarding
davidmiuraactualai Aug 3, 2026
d48cbf0
feat: observer CLI — canonicalization, boundaries, hooks, setup (Phas…
davidmiuraactualai Aug 6, 2026
27c1a63
fix: use matcher+hooks array format for Claude Code hook settings
davidmiuraactualai Aug 6, 2026
28bc5f7
feat: wire observer boundary evaluation to advisor API
davidmiuraactualai Aug 6, 2026
fcf53b7
fix: log warnings for corrupt journal lines instead of silent drop
davidmiuraactualai Aug 6, 2026
5770069
fix: add observe:events scope to CLI OAuth login
davidmiuraactualai Aug 6, 2026
cf0076c
feat: add "Open web app" button to CLI login success page
davidmiuraactualai Aug 7, 2026
780d36e
fix: normalize SSH/git URLs to HTTPS before calling onboard API
davidmiuraactualai Aug 7, 2026
ee92dea
revert: remove CLI-side URL normalization, API now accepts SSH URLs
davidmiuraactualai Aug 7, 2026
854405a
feat: add `actual doctor` command for local dev environment diagnostics
davidmiuraactualai Aug 7, 2026
c2710df
feat: add authentication checks to actual doctor
davidmiuraactualai Aug 7, 2026
36f1968
feat: add UserPromptSubmit and Agent as evaluation boundaries
davidmiuraactualai Aug 10, 2026
348d905
fix: refresh expired tokens in observer boundary evaluations
davidmiuraactualai Aug 10, 2026
9065fc6
refactor: make `actual doctor` customer-facing
davidmiuraactualai Aug 10, 2026
50cef27
feat: chunked synchronous evaluation with ranked result aggregation
davidmiuraactualai Aug 10, 2026
e69a4dd
feat: add serde ADR rules, settings, and reset command
davidmiuraactualai Aug 10, 2026
f99a9ed
fix: increase post_intervention HTTP timeout to 8min for sync-blocking
davidmiuraactualai Aug 10, 2026
919fcad
fix: increase hook timeout to 1200s and add SubagentStart support
davidmiuraactualai Aug 11, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
glob: "**/*.rs"
---

<rule_activation adr-id="cb9f71d8-38b5-431a-95b1-ba9ec0bc5365">
<!-- ADR: Annotate API-Facing Enums with an Explicit serde Tag Strategy -->
</rule_activation>

- Always annotate enums that cross an API or persistence boundary with an explicit tag strategy: `#[serde(tag = "type")]`, `#[serde(tag = "t", content = "c")]`, or `#[serde(untagged)]`
- Never rely on serde's default external tagging for API-facing enums
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
glob: "**/*.rs"
---

<rule_activation adr-id="623b42d7-a222-44c4-a764-c0024a283fd7">
<!-- ADR: Annotate Borrowed Fields with `#[serde(borrow)]` for Zero-Copy Deserialization -->
</rule_activation>

- Add `#[serde(borrow)]` to every field typed as `&'de str`, `&'de [u8]`, or any type containing a borrowed lifetime
- Use owned `String` or `Vec<u8>` for fields in structs that outlive the parsing call site
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
glob: "**/*.rs"
---

<rule_activation adr-id="9d22d261-7ec4-457e-aa69-6e36992dfb0a">
<!-- ADR: Annotate Optional Fields with `#[serde(skip_serializing_if = "Option::is_none")]` -->
</rule_activation>

- Add `#[serde(skip_serializing_if = "Option::is_none")]` to every `Option<T>` field unless the API contract explicitly requires `null` to represent an absent value
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
glob: "**/*.rs"
---

<rule_activation adr-id="7f1866e8-f641-4830-a905-5e2015e85d5f">
<!-- ADR: Apply `#[serde(deny_unknown_fields)]` to Configuration and Strict-Contract Structs -->
</rule_activation>

- Apply `#[serde(deny_unknown_fields)]` to all configuration structs and types that represent strict internal contracts
- Never combine `#[serde(deny_unknown_fields)]` with `#[serde(flatten)]`—they are incompatible and produce incorrect runtime behavior
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
glob: "**/*.rs"
---

<rule_activation adr-id="50fbe4a9-665e-45d8-bd35-b20f48014820">
<!-- ADR: Deserialize into Typed Structs Instead of `serde_json::Value` -->
</rule_activation>

- Define typed structs for all JSON shapes parsed more than once or stored beyond a single function call
- Use `serde_json::Value` only for genuinely schema-less data such as arbitrary user-supplied JSON blobs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
glob: "**/*.rs"
---

<rule_activation adr-id="8c7f2d63-72e2-4225-8577-66ff53cd9ab6">
<!-- ADR: Enable `features = ["derive"]` in Cargo.toml When Using serde Derive Macros -->
</rule_activation>

- Declare `serde = { version = "1", features = ["derive"] }` in every application crate that uses `#[derive(Serialize, Deserialize)]`
- In library crates, declare serde as `optional = true` with the derive feature and gate all derives behind `#[cfg(feature = "serde")]` to avoid forcing the dependency on downstream users
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
glob: "**/*.rs"
---

<rule_activation adr-id="cf3bce7c-83b4-456d-b4d2-4dd01e8fc9ee">
<!-- ADR: Implement Custom `Deserialize` to Enforce Type Invariants at Parse Time -->
</rule_activation>

- Implement a custom `Deserialize` or use `#[serde(try_from)]` for any newtype that enforces invariants such as non-empty strings, valid emails, or bounded integers
- Never expose a public constructor for invariant-enforcing types that bypasses validation
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
glob: "**/*.rs"
---

<rule_activation adr-id="e3001676-c9cb-4858-ade8-df0d479a7d9c">
<!-- ADR: Use `#[serde(default)]` for Absent Fields With Meaningful Defaults Instead of `Option<T>` -->
</rule_activation>

- Use `#[serde(default)]` on fields that have a meaningful zero-value default and should never be `None` in application logic
- Reserve `Option<T>` for fields where `None` carries semantic meaning distinct from any default value
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
glob: "**/*.rs"
---

<rule_activation adr-id="72226678-b630-4646-a769-5d02b7d7619c">
<!-- ADR: Use `#[serde(remote = "ForeignType")]` to Serialize Foreign Types Without Newtype Wrappers -->
</rule_activation>

- Use `#[serde(remote = "path::to::ForeignType")]` when adding serde support to foreign types where per-field attribute control is required
- Prefer `serde_with` `#[serde_as]` adapters over `remote` when the conversion matches a standard adapter such as `DisplayFromStr` or `DurationSeconds`
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
glob: "**/*.rs"
---

<rule_activation adr-id="3d95c725-845b-4ba4-a2c7-508b9fc4687d">
<!-- ADR: Use `#[serde(rename_all)]` on Structs Instead of Per-Field `rename` Attributes -->
</rule_activation>

- Apply `#[serde(rename_all = "camelCase")]` or equivalent at the struct or enum level when all fields share a naming convention
- Use per-field `#[serde(rename = "...")]` only for individual exceptions to the struct-level rule
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
glob: "**/*.rs"
---

<rule_activation adr-id="fc39f638-f694-4e2a-b271-2a3c94ea95e5">
<!-- ADR: Use `#[serde(transparent)]` on Newtype Structs to Serialize as the Inner Type -->
</rule_activation>

- Add `#[serde(transparent)]` to all newtype structs (single-field tuple structs and single-field named structs) that should serialize and deserialize as their inner type
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
glob: "**/*.rs"
---

<rule_activation adr-id="2c68de4d-24b8-4c0d-b926-f9dd98f39647">
<!-- ADR: Use `serde_with` and `#[serde_as]` for Third-Party Type Serialization Adaptations -->
</rule_activation>

- Use `serde_with` with `#[serde_as]` for field-level format conversions such as duration-as-seconds, bytes-as-base64, and display-as-string
- Use `#[serde(remote)]` only when `serde_with` lacks a matching adapter and full per-field attribute control is required
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
glob: "**/*.rs"
---

<rule_activation adr-id="fd3d52a0-e094-4be0-9ce7-15d6e5bec286">
<!-- ADR: Use `T: DeserializeOwned` in Generic Functions That Return Owned Values -->
</rule_activation>

- Bound generic deserialization functions with `T: DeserializeOwned` when the returned value does not borrow from the input buffer
52 changes: 52 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"hooks": {
"PostToolUse": [
{
"command": "actual observe post-tool",
"type": "command"
}
],
"PostToolUseFailure": [
{
"command": "actual observe post-tool-failure",
"type": "command"
}
],
"PreCompact": [
{
"command": "actual observe pre-compact",
"type": "command"
}
],
"PreToolUse": [
{
"command": "actual observe pre-tool",
"type": "command"
}
],
"SessionEnd": [
{
"command": "actual observe session-end",
"type": "command"
}
],
"SessionStart": [
{
"command": "actual observe session-start",
"type": "command"
}
],
"Stop": [
{
"command": "actual observe stop",
"type": "command"
}
],
"UserPromptSubmit": [
{
"command": "actual observe prompt",
"type": "command"
}
]
}
}
16 changes: 15 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,18 @@ verification run must accompany every code change that touches a governed area.

## ADR 2: Adopt Secure Secrets Management in CI/CD Pipeline

1. Implement a dedicated secrets management system integrated into the CI/CD pipeline that separates secret storage from code. This includes using environment variables, encrypted secret stores, or dedicated secrets management services (such as HashiCorp Vault, AWS Secrets Manager, or CI platform-native solutions) to inject credentials at runtime rather than hardcoding them in configuration files or source code. The pattern is consistently applied across CLI commands and testing infrastructure to ensure uniform security practices.
1. Implement a dedicated secrets management system integrated into the CI/CD pipeline that separates secret storage from code. This includes using environment variables, encrypted secret stores, or dedicated secrets management services (such as HashiCorp Vault, AWS Secrets Manager, or CI platform-native solutions) to inject credentials at runtime rather than hardcoding them in configuration files or source code. The pattern is consistently applied across CLI commands and testing infrastructure to ensure uniform security practices.

<!-- managed:actual-start -->
<!-- last-synced: 2026-08-06T00:34:41Z -->
<!-- version: 1 -->
<!-- adr-ids: v2-governance -->

<!-- adr:v2-governance start -->
<adr_governance source="docs/adr/">
ADRs govern validated architectural standards for this project.
Full ADR documents: @docs/adr/
</adr_governance>
<!-- adr:v2-governance end -->

<!-- managed:actual-end -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Annotate API-Facing Enums with an Explicit serde Tag Strategy

Status: accepted
Date: 2026-03-31
Deciders: ADR Bank Curation

## Context

- serde's default enum serialization uses external tagging: `{"Circle": {"radius": 5}}`
- REST APIs and event systems almost always expect internally tagged (`{"type":"Circle","radius":5}`) or adjacently tagged formats
- The mismatch between serde defaults and API conventions is not caught at compile time
- Malformed payloads are produced silently, often discovered only at integration testing or in production
- serde provides explicit tag strategy attributes (`tag`, `tag`+`content`, `untagged`) to control serialization format

## Problem Statement

Relying on serde's default external tagging for API-facing enums produces payloads incompatible with standard API conventions, and this mismatch is silent at compile time. Without an explicit tag strategy, malformed payloads reach consumers undetected until runtime failures occur.

## Decision

1. MUST: Annotate enums that cross an API or persistence boundary with an explicit tag strategy: `#[serde(tag = "type")]`, `#[serde(tag = "t", content = "c")]`, or `#[serde(untagged)]`
2. MUST NOT: Rely on serde's default external tagging for API-facing enums
3. SHOULD: Prefer internally tagged (`#[serde(tag = "type")]`) for JSON APIs as the most common convention
4. SHOULD: Use adjacently tagged (`#[serde(tag = "kind", content = "data")]`) when tuple variants are needed
5. MAY: Use untagged (`#[serde(untagged)]`) only when variant shapes are unambiguous
6. SHOULD: Verify serialization format with a round-trip test for all API-facing enums

## Policy Block

- MUST annotate enums that cross an API or persistence boundary with an explicit serde tag strategy
- MUST NOT rely on serde's default external tagging for API-facing enums
- SHOULD prefer internally tagged enums (`#[serde(tag = "type")]`) for JSON APIs
- SHOULD use adjacently tagged enums when tuple variants are required
- MAY use `#[serde(untagged)]` only when variant shapes are unambiguous
- SHOULD include round-trip serialization tests for all API-facing enums

In scope:
- Rust enums derived with `Serialize` and/or `Deserialize` that appear in HTTP request/response bodies
- Enums serialized to event queues, message buses, or persistent storage
- Choice between internally tagged, adjacently tagged, and untagged strategies
- Variant compatibility constraints per tag strategy (e.g., tuple variants with internal tagging)

Out of scope:
- Internal-only enums that never leave the process boundary
- Enum serialization in non-serde formats (e.g., protobuf, flatbuffers)
- Struct field naming conventions (`rename_all`) which are covered separately
- Serde container-level attributes unrelated to tagging (e.g., `deny_unknown_fields`)

Exceptions:
- EXC-001: Internal enums used only within a single crate for in-memory serialization (e.g., caching) may use default tagging if no external consumer exists

## Rationale

- Explicit tag strategies make the wire format self-documenting in the type definition, reducing surprises during API integration
- Catching format mismatches at definition time prevents silent production failures from malformed payloads
- Internally tagged enums align with the dominant JSON API convention (`{"type": "...", ...}`), reducing friction with frontend and third-party consumers

## Consequences

Positive:
- API payloads match consumer expectations without post-hoc transformation layers
- New team members can read the enum definition and immediately understand the wire format
- Round-trip tests catch format regressions early in the development cycle

Negative:
- Developers must understand the constraints of each tag strategy (e.g., internally tagged enums cannot contain tuple variants), adding a learning curve
- Changing a tag strategy on a deployed API is a breaking change requiring versioning or migration

## Alternatives

- Rely on serde's default external tagging and transform payloads in middleware (rejected)
Rejected because: Adds a runtime transformation layer that obscures the actual wire format and creates a maintenance burden
When valid: When wrapping a third-party library's enums that cannot be annotated and the external format is acceptable to consumers

- Use `#[serde(untagged)]` everywhere to match arbitrary JSON shapes (rejected)
Rejected because: Deserialization error messages are extremely poor ("data did not match any variant"), and ambiguous variant shapes cause silent misparses
When valid: When consuming polymorphic external APIs where the discriminator field is absent and variant shapes are guaranteed to be non-overlapping

## Risks

- Choosing the wrong tag strategy for an API locks in a wire format that is costly to change after deployment
Mitigation: Default to internally tagged for new APIs, add round-trip tests before release, and use API versioning for future changes
- `#[serde(untagged)]` enums silently deserialize to the wrong variant when shapes overlap
Mitigation: Avoid `untagged` unless variant shapes are provably disjoint; add explicit deserialization tests for each variant
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Annotate Borrowed Fields with `#[serde(borrow)]` for Zero-Copy Deserialization

Status: accepted
Date: 2026-03-31
Deciders: ADR Bank Curation

## Context

- serde supports zero-copy deserialization by borrowing `&str` or `&[u8]` directly from the input buffer, avoiding heap allocations for string data
- Without `#[serde(borrow)]`, the compiler cannot infer the lifetime relationship between the input buffer and the deserialized struct
- The resulting lifetime errors are confusing and obscure the actual one-line fix (`#[serde(borrow)]`)
- Developers often reach for owned types unnecessarily when the real issue is a missing annotation

## Problem Statement

Without `#[serde(borrow)]` on borrowed fields, Rust's compiler produces opaque lifetime errors during zero-copy deserialization, leading developers to either abandon zero-copy entirely or spend significant time debugging what is ultimately a one-line annotation fix.

## Decision

1. MUST: Add `#[serde(borrow)]` to every field typed as `&'de str`, `&'de [u8]`, or any type containing a borrowed lifetime
2. MUST: Use owned `String` or `Vec<u8>` for fields in structs that outlive the parsing call site
3. MUST: Use `serde_json::from_slice` or `serde_json::from_str` (not `from_reader`) so the input buffer remains in scope for the struct's lifetime
4. SHOULD: Switch to owned types for values stored in async tasks, returned across await points, or placed in long-lived data structures

## Policy Block

- MUST add `#[serde(borrow)]` to every field typed as `&'de str`, `&'de [u8]`, or any type containing a borrowed lifetime
- MUST use owned `String` or `Vec<u8>` for fields in structs that outlive the parsing call site
- MUST use `serde_json::from_slice` or `serde_json::from_str` (not `from_reader`) when deserializing into borrowing structs
- SHOULD switch to owned types for values stored in async tasks, returned across await points, or placed in long-lived data structures

In scope:
- Structs deriving `Deserialize` with lifetime parameters (`<'de>`)
- Fields borrowing `&str` or `&[u8]` from the input buffer
- Choosing between borrowed and owned field types based on struct lifetime requirements
- serde zero-copy deserialization with `from_slice` and `from_str`

Out of scope:
- Custom `Deserialize` implementations (manual `impl<'de> Deserialize<'de>`)
- Non-serde deserialization frameworks
- Serialization-side concerns (`Serialize` derive)
- Binary deserialization formats (bincode, postcard) which have their own borrowing rules

Exceptions:
- EXC-001: Structs used exclusively in benchmarks or hot paths where zero-copy is measured as unnecessary may omit `#[serde(borrow)]` and use owned types for simplicity

## Rationale

- Zero-copy deserialization significantly reduces heap allocations and improves throughput for large payloads, but only works when the lifetime relationship is explicitly annotated
- The `#[serde(borrow)]` annotation is a single-line fix that eliminates an entire class of confusing lifetime compilation errors
- Making the borrowed-vs-owned decision explicit at the struct level communicates intent about data ownership to future readers

## Consequences

Positive:
- Developers can leverage zero-copy deserialization without fighting opaque lifetime errors
- Reduced heap allocations for read-heavy parsing workloads improve performance
- Clear ownership semantics at the struct level make code easier to reason about

Negative:
- Developers must reason about whether the deserialized struct outlives the input buffer, adding cognitive overhead to struct design
- Using `from_slice`/`from_str` instead of `from_reader` requires the entire input to be in memory, which may increase memory usage for streaming scenarios

## Alternatives

- Always use owned types (`String`, `Vec<u8>`) and avoid zero-copy entirely (rejected)
Rejected because: Sacrifices significant performance gains for parsing-heavy workloads and ignores a core serde capability
When valid: When structs are long-lived, passed across async boundaries, or when the performance difference is unmeasurable for the use case

## Risks

- Borrowing structs tied to input buffer lifetime can cause borrow-checker issues if the struct is accidentally stored beyond the buffer's scope
Mitigation: Default to owned types unless profiling shows zero-copy is beneficial; document lifetime requirements in struct-level comments
Loading
Loading