Event-sourced applicant/subscriber/project aggregates, generalized resources
Deploy / deploy (push) Failing after 3s

Replaces the free-string, direct-KV-mutate state model in answers.rs
with a proper event log (events/store.rs, JetStream-backed, CAS via
expected_last_subject_sequence) and three pure state machines
(aggregates/{applicant,subscriber,project}.rs). Closes a real
lost-update race in the old transition_answer (concurrent decisions on
the same item could both win, publishing contradictory events). KV
buckets become best-effort read-model projections, not the source of
truth. Content-declared transition targets are now validated at
load/reload time against the real compiled transition tables, not
accepted as arbitrary strings.

Buckets renamed to describe their content, not their relation to the
app (portal_applicants -> applicants, etc); "inquiry" folded into a
richer "project" concept.

ResourceSpec generalized beyond a single KV bucket: Kv | GiteaStarred |
GiteaOrgRepos | Url sources, with an optional jq filter (via the jaq
crate) to shape live data for the frontend. Url source is SSRF-guarded
(https-only, rejects loopback/private/link-local, real DNS resolve).

New headless question_lint binary (validates content against compiled
transition tables with no NATS/OIDC/server involved) and a one-time
backfill_events binary (dry-run by default) for migrating existing KV
data onto the new event log.

Questions get an optional `responsible` contact plus a lightweight
"report this question" action.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-08-06 08:52:29 +02:00
co-authored by Claude Sonnet 5
parent 213b1130bb
commit ebf4bf91b3
16 changed files with 1811 additions and 67 deletions
+22 -1
View File
@@ -31,12 +31,17 @@ openidconnect = { version = "4", optional = true }
# token's raw JWT payload - see server/oidc.rs::extract_groups_claim.
base64 = { version = "0.22", optional = true }
chrono = { version = "0.4", features = ["serde"], optional = true }
uuid = { version = "1", features = ["v4"], optional = true }
uuid = { version = "1", features = ["v4", "serde"], optional = true }
dotenvy = { version = "0.15", optional = true }
anyhow = { version = "1", optional = true }
futures = { version = "0.3", optional = true }
tracing = { version = "0.1", optional = true }
tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true }
# jq-filter evaluation for ResourceSource's optional `jq` field
# (src/resource.rs) - pure Rust, no shell-out to a `jq` binary.
jaq-core = { version = "3", optional = true }
jaq-std = { version = "3", optional = true }
jaq-json = { version = "2", features = ["sync"], optional = true }
# --- browser only ---
wasm-bindgen = { version = "0.2", optional = true }
@@ -90,8 +95,24 @@ ssr = [
"dep:futures",
"dep:tracing",
"dep:tracing-subscriber",
"dep:jaq-core",
"dep:jaq-std",
"dep:jaq-json",
]
[dev-dependencies]
proptest = "1"
[[bin]]
name = "question_lint"
path = "src/bin/question_lint.rs"
required-features = ["ssr"]
[[bin]]
name = "backfill_events"
path = "src/bin/backfill_events.rs"
required-features = ["ssr"]
[profile.wasm-release]
inherits = "release"
opt-level = 'z'