Skip to content

ADR-0076 — OpenProject is the single source of truth for project memory

Status: active Date: 2026-04-28 Introduced by: Issue #747 ; operator scale-up directive Supersedes: extends ADR-69 (OpenProject as orchestrator) with the SSoT invariant + discrepancy resolution rule


Context

ADR-69 established OpenProject as the project orchestrator: every dev work pulls a Story from an active version (sprint), and ends by closing the Story (and the version when its last Story closes). That ADR covered the workflow discipline — what is being worked on right now.

As the project scales (more concurrent Stories, more committee sessions, more cross-references between PRs / GH issues / OP work packages / claude memory / chat scrollback / docs site), a stricter requirement surfaces : without a single source of truth, project memory diverges across surfaces within days. We have observed this divergence already :

  • A GitHub issue closed but the corresponding OP Story still In progress
  • A committee verdict applied to the code without a corresponding OP audit trail entry
  • A claude-memory file mentioning an ADR that doesn't exist in documentation/adr/
  • An open PR (e.g., #659 — closed) referencing a cvn_id that conflicts with another Epic
  • A "we already discussed this" memory of a decision with no OP wp + no GH issue + no committee session

ADR-69 mandates that work starts and ends in OP, but does not codify that OP is the only authoritative source. Without that rule, an operator (or AI assistant) reasonably asks "where do I check the truth ?" with no defensible answer ; reconciliation cost grows quadratically with project surface.

Decision

OpenProject is the single source of truth for project memory. Every Need / Epic / Story / Release MUST exist as a work package in OP. The OP work package is the authoritative state ; any other source (GitHub issue, code comment, claude memory, chat transcript, docs site, design doc) is a projection of the OP truth.

Concrete rules :

  1. Existence : if a need / feature / decision is not in OP, it does not exist as project work — it is a draft in someone's head, no more.
  2. Update cadence : OP MUST be updated after each : progress, code change, decision, run, test, committee verdict, PR merge, sprint roll-over, retro outcome. The update lag from event → OP record SHOULD be < 24h ; for committee verdicts and PR merges, < 1h.
  3. Discrepancy resolution : when OP and another source disagree, OP wins. The reconciliation flow is always other source → OP, never the reverse. Example : if a GitHub issue is closed but the OP Story is still In progress, the OP Story is the truth ; the GitHub issue is updated to match (or the OP Story is updated to Closed if the issue closure was correct — never the other way around).
  4. Migration policy : open GitHub issues / PRs that pre-date this ADR and are not yet in OP are migrated to OP when worked on next — not retroactively in one batch (which would be busywork). Issues never worked on stay in GH alone.
  5. No exception : no new Need / Epic / Story / Release without an OP entry. The scripts/openproject_import_gh.py importer is the standard path for creating OP work packages from GH issues per ADR-69.

Invariants

  • I1 — OP is authoritative : every project work package state (status, version, parent, comment thread, custom fields) is what OP says it is. Other systems mirror OP, never the inverse. Discrepancy = bug, fix is "update the other source".
  • I2 — Every work package has a stable cvn_id : every Need (CVN-N<nnn>), Epic (CVN-N<nnn>-E<letter>), Story (CVN-N<nnn>-E<letter>-S<m>), Release (CVN-R<yyyymmdd>-<n>) has a unique cvn_id used in branches, PR titles, commit messages, ADR references. Two work packages MUST NOT share a cvn_id. Current enforcement gap : scripts/openproject_import_gh.py does idempotency on github_issue_url only ; an explicit cvn_id uniqueness pre-check is planned in #749. Until #749 ships, compliance rests on operator vigilance + the cvn_id naming convention's natural collision-resistance (sequential per-Need allocation). When #749 merges, this invariant flips to enforced (the importer fails-fast on cvn_id collision per ADR-25).
  • I3 — Every work package has a github_issue_url cf set : the bidirectional link is enforced by openproject_import_gh.py (per ADR-69). A work package without a github_issue_url is a violation that gates further work on it.
  • I4 — Update lag bounded : committee verdict applied → OP wp comment best-effort < 1h, hard < 24h ; PR merged → OP Story status Closed best-effort < 1h, hard < 24h ; sprint version closed → outcome note in version description within 72h. The "best-effort" tier reflects solo-operator workflow realism (per committee fae718cb reco 2 + dissent on < 1h realism) ; the "hard" tier is the late-binding reconciliation deadline beyond which the divergence is treated as a process incident worth a retrospective note.
  • I5 — No silent divergence : when an operator (or AI assistant) discovers a discrepancy, it MUST be resolved in the same session and noted in the OP wp comment (audit trail). "Will fix later" is not acceptable for SSoT integrity.
  • I6 — Migration on touch : pre-ADR-76 open GH issues / PRs are migrated to OP the first time work resumes on them. The migrating actor opens the OP wp (via importer) before pushing further changes.

Contingency plan : OpenProject unavailability (committee fae718cb blocker)

The "OP wins" rule cannot be followed if OP is unreachable. This section defines the procedure operators MUST follow during an OpenProject outage (e.g., Scaleway managed PG outage, OP container down, network partition).

Detection

  • Primary indicator : https://openproject.cvntrade.eu returns 5xx / connection timeout for > 5 minutes ; OR scripts/openproject_import_gh.py fails the prerequisite _op_get(/api/v3/projects/cvntrade) call.
  • Secondary indicator : Grafana cvntrade-platform-health dashboard shows the OP probe in red.

Degraded-mode procedure

  1. Stop new OP-bound work : do not attempt to update OP from the script ; do not retry on failures (avoid filling logs with timeouts).
  2. Switch to a structured offline log : append every event that would have updated OP to committee/sessions/op_outage_<YYYYMMDD>.md (or a similarly time-stamped file) using a strict format :
## op_outage_event 2026-04-28T14:23:00Z
- actor: operator
- event: pr_merged
- target_wp: 60
- source: PR #742 commit abc1234
- intended_action: status -> Closed + comment "merged via 742"
- status: deferred (op unavailable)
  1. Continue work that doesn't need OP write : code, CR review, docs site, CI runs proceed normally. Only the OP wp updates are deferred.
  2. Operator decision tier : if the outage exceeds 4 hours during business hours, the operator MAY proceed with merging substantive PRs without the OP closure step ; the offline log is the audit trail. Outside business hours, work is paused until OP is back.
  3. Recovery (post-outage) : run a reconciliation script (scripts/op_reconcile_outage.py — to be implemented under #747 follow-up Story) that reads committee/sessions/op_outage_*.md and replays the events to OP. Each replayed event is logged with its original timestamp in the OP wp comment.
  4. Post-mortem : every OP outage > 1h triggers a one-paragraph entry in documentation/OPERATIONS.md §Incident log ; outages > 4h trigger a committee experiment_review on the impacted Stories' SSoT integrity.

Why this design

  • Offline log as audit trail : the markdown event file IS the SSoT during the outage window. It is committed to git like any other artefact (per ADR-77), so multi-operator scenarios stay auditable. After replay, OP becomes SSoT again ; the offline log stays in git as the historical record.
  • No silent fallback to "make decisions as if OP doesn't exist" : the offline log makes the deferred state explicit. Per ADR-25 (no silent fallback), the degradation is loud.
  • Bounded blast radius : the worst case is a 4h outage during business hours where the operator proceeds with PR merges. Reconciliation post-outage is mechanical (replay script) ; the divergence is bounded by the duration of the outage and the rate of work during it.

Alternatives rejected

  • Multiple sources of truth (no SSoT) : the failure mode this ADR addresses. Already observed (see Context). Rejected.
  • GitHub Issues as SSoT : doesn't model Need → Epic → Story → Release hierarchy natively, no per-Need cumulative progress, no first-class versions. Rejected (also rejected in ADR-69 §Alternatives).
  • Claude memory as SSoT : ephemeral, not auditable across operators, lost on context resets. Rejected — claude memory is a cache, not a truth source.
  • MkDocs docs site as SSoT for project memory : conflates work tracking with knowledge base. ADR-77 makes mkdocs the SSoT for documentation — the two are complementary, not competing. Rejected for project memory.
  • Migration of all open GH issues to OP at once (big-bang) : busywork ; many open issues will never be touched again. Rejected — migrate-on-touch is the pragmatic compromise.

Consequences

  • Positive : single answer to "where is the truth ?" — OP. Discrepancy resolution flow is unambiguous (one direction). Reconciliation cost grows linearly, not quadratically, with project surface.
  • Positive : synergistic with ADR-69 (orchestrator) + ADR-68 (committee) + ADR-77 (docs SSoT). The 4 ADRs together form a coherent process spine : OP for work, mkdocs for docs, committee for review, ADR-69 for sprint discipline.
  • Positive : forces explicit reconciliation — if a discrepancy exists, the operator must close it in the same session, which surfaces process gaps early.
  • Negative : ~2 minutes overhead per committee verdict / PR merge to update the OP wp comment. Acceptable given the cost of a divergence (multi-hour reconciliation later).
  • Negative : the migrate-on-touch policy means some pre-ADR-76 GH issues will stay outside OP indefinitely. This is intentional — those issues are stale anyway.
  • Neutral : the GitHub issue stays as the bidirectional CR / discussion surface (CodeRabbit comments, code review threads). OP and GH are complementary : GH for the conversation, OP for the state.

Rollback

This ADR is process. Rollback = remove this file + revert CLAUDE.md / OPERATIONS.md edits introduced by issue #747. The OP work packages and cvn_ids stay (descriptive metadata regardless of policy).

If the SSoT discipline proves systematically wasteful (e.g., > 30 % of operator time spent on OP updates over a 1-month sample, with no measurable reduction in reconciliation incidents), revisit the update-cadence I4 thresholds before retiring the policy.

References

  • ADR-52 — Auditabilité des sessions du comité d'experts (sister discipline)
  • ADR-57 — ADRs in English
  • ADR-68 — Expert Committee = default review channel (sister discipline)
  • ADR-69 — OpenProject is the project orchestrator (extended by this ADR)
  • ADR-70 — MLOps readiness template (lives in OP Story dossier)
  • ADR-77 — MkDocs / Structurizr SSoT for documentation (sister ADR, same scale-up directive)
  • CLAUDE.md §Process de développement — steps 0 + 14 (OP-bound)
  • documentation/OPERATIONS.md §16 — Sprint orchestration runbook (extended with SSoT rules)
  • scripts/openproject_import_gh.py — issue → OP wp import (idempotent)
  • Issues : #593 (OP infra Phase 1), #747 (this ADR + ADR-77)
  • Committee sessions : fae718cb (this ADR's plan_review, PASSED EXECUTION_RISK avg 7.1, 1 BLOCKER addressed via §Contingency plan + reco 2 lag-tier relaxation in I4 ; 5 forward-looking recos for follow-up : reco 1 contingency plan ✅ done in this revision, reco 3 automated lag monitoring → future Story under CVN-N00X observability, reco 4 tooling for manual updates → can extend openproject_import_gh.py, reco 5 SSoT ownership role + metrics → operator decision when team grows, reco 6 quarterly stale-issue sweep → operator-led ritual)