Skip to content

Commit 96627ff

Browse files
authored
docs(spec): state the command log tenant_id exception, bump to v0.5 (#60)
Section 11.2 gains the write-outside-the-transaction discipline and the three conditions a table must meet for a nullable tenant_id. CLAUDE.md invariant 3 names command_log as the one holder and points at the rule. A new exception is a maintainer decision recorded in decisions.md. Closes #49 Signed-off-by: Matthew Wren <info@origindev.com>
1 parent 7c6c249 commit 96627ff

2 files changed

Lines changed: 29 additions & 8 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ These are architectural commitments, not preferences. A change that violates one
1919
1. **Core never imports adapter code.** No vendor name - `stripe`, `gohighlevel`, `twilio` - may appear anywhere under `packages/core/`. Not in an import, not in a conditional, not in a type name. Core knows capability names only.
2020
2. **Core owns entitlement; adapters own money.** Adapters report payment outcomes. Core decides tier consequences. There is no command that sets a listing's tier directly.
2121
3. **Every table carries `tenant_id`.** Every query is tenant-scoped. Single-directory mode is a UI toggle, never a different data model. A migration that adds a table without `tenant_id` is wrong.
22+
23+
One table has a **nullable** `tenant_id`: `command_log`, because a command naming an unresolvable tenant still has to leave a trace. The three conditions a table must meet to qualify are in `docs/spec/` §11.2, and a new exception is a maintainer decision recorded in `docs/decisions.md` - do not claim it yourself.
24+
2225
4. **No data-source connectors.** OSDS ships no importer, scraper, hook, or plugin interface for any external listing dataset. This is a legal position, not a missing feature. See `docs/spec/` §4.1.1 and `CONTRIBUTING.md`. Contributions adding one are declined on sight - say so politely and link the section.
2326
5. **Events are facts, past tense, immutable.** `listing.claimed`, never `claim_listing`. Event type names are permanent; renaming means adding a new type and deprecating the old.
2427
6. **Commands are validated at the core boundary.** Adapters never write to the database. They send commands; core validates, applies, emits.

docs/spec/events-and-adapters.md

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
# OSDS — Event Schema, Adapter Interface & Entitlements
22

33
**Open Source Directory Site**
4-
**Status:** Draft v0.4 · **License:** Apache-2.0 · **Steward:** Origin Development & IT, Inc.
4+
**Status:** Draft v0.5 · **License:** Apache-2.0 · **Steward:** Origin Development & IT, Inc.
55
**Audience:** core maintainers, adapter authors
66

77
This document defines the contract between the OSDS core and everything outside it. The core is a multi-tenant directory engine. It knows nothing about email providers, CRMs, payment gateways, or messaging platforms. It emits facts and accepts commands. Adapters translate.
88

99
If you are writing an adapter, sections 3, 7 and 8 are the ones you need.
1010
If you are implementing the paid tiers, section 5 is the whole job.
1111

12-
### Changes from v0.3
12+
### Changes from v0.4
1313

14-
- **§3.3 added: the complete event catalogue.** v0.3 restructured section 3 and in doing so lost the per-group event tables for `claim.*`, `billing.*`, `lead.*`, `call.*`, `moderation.*`, `compliance.*`, `agent.*`, `tenant.*`, `import.*` and `postal.*`, along with six of the ten `listing.*` events. §3.3 is now the single canonical list of every event type OSDS emits. Detailed payloads remain in their behavioural sections; §3.3 is the index and the source of truth for the type union.
15-
- `listing.owner_assigned` restored to the catalogue.
16-
- `listing.claimed` is **not** an event. It appeared only in the v0.2 envelope example and was never in a catalogue. The real sequence is `claim.approved` followed by `listing.owner_assigned`.
17-
- Section numbering below §3 shifted by one. Core entities moved 3→4,
18-
reviews 4→5, entitlements 5→6, commands 6→7, adapter interface 7→8,
19-
claim verification 8→9. The data-source position is now §4.1.1.
14+
- **§7.1 added.** `listing.upsert` payload, match key, partial-state semantics, rejected fields.
15+
- **§9.5 added.** Verification code lifetime per method, tenant-configurable within core-enforced bounds. §9.2's flat 21 days becomes the postcard default.
16+
- **§4.3 added**, plus the `user.*` namespace in §3.2 and §3.3. There is no `user.create` command; a row is minted inside the command that needs one and `user.created` is emitted.
17+
- **§11.2 expanded.** The command log is written outside the command transaction, and `command_log`'s nullable `tenant_id` is stated as a bounded exception rather than a violation.
2018

2119
---
2220

@@ -1111,6 +1109,26 @@ The envelope is small and free of personal data, so keeping it indefinitely is a
11111109

11121110
The command log exists because a rejected command otherwise leaves no trace. "The agent attempted to delete a listing and was blocked" is precisely the record worth having.
11131111

1112+
#### The command log is written outside the command transaction
1113+
1114+
The attempt is recorded **before** the command transaction opens, and concluded **after** it settles, each in its own independently-committed transaction.
1115+
1116+
A log written inside the transaction it is logging disappears when that transaction rolls back — which is exactly the case the log exists for. A command that throws mid-apply leaves a row with a null outcome. That is the record, not a gap.
1117+
1118+
A concluded row is never rewritten. An audit trail the application can edit is not one.
1119+
1120+
#### Nullable `tenant_id`
1121+
1122+
Design rule 4 makes every event tenant-scoped, and every table carries `tenant_id`. **`command_log` is the exception:** a command may be malformed, or name a tenant that does not exist, and that attempt still has to leave a trace. A row that cannot be written because its tenant could not be resolved is the specific failure the log is for.
1123+
1124+
The exception is narrow, and a table qualifies for it only when all three hold:
1125+
1126+
1. It records an **attempt**, not a state change. Attempts can fail before a tenant is resolvable; state changes cannot.
1127+
2. Rows with a null `tenant_id` are **unreadable by the application role**. The tenant-scoped policy matches no null row, so `osds_app` never sees another party's malformed payload.
1128+
3. Those rows are written **already concluded**, so the application never needs to read one back to update it. `UPDATE ... WHERE id` still consults the SELECT policy; a row the session cannot read is a row it cannot update.
1129+
1130+
**A new exception is a maintainer decision, recorded in `docs/decisions.md`.** The rule is general enough to be argued into, which is why claiming it is not self-service. Today `command_log` is the only table that holds it.
1131+
11141132
---
11151133

11161134
## 12. Search, sitemaps and SEO

0 commit comments

Comments
 (0)