Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fdd143d03 | |||
| 9f5ed4b5cd | |||
| 4e433158cd | |||
| 0fdf892d81 | |||
| f61dffbb5a | |||
| f7f007dce8 | |||
| 20992883ff | |||
| a622419e7d | |||
| b1d293d218 |
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"phase": 6,
|
"phase": 3,
|
||||||
"stage": "complete",
|
"stage": "complete",
|
||||||
"milestone": "v0.4",
|
"milestone": "v0.4",
|
||||||
"phase_role": "final",
|
"phase_role": "execution",
|
||||||
"project": "atelier",
|
"project": "atelier",
|
||||||
"attempts": 0,
|
"attempts": 0,
|
||||||
"updated_at": "2026-08-05T07:30:00Z",
|
"updated_at": "2026-08-05T06:45:00Z",
|
||||||
"milestone_complete": true,
|
"milestone_complete": false,
|
||||||
"milestone_branch": "milestone/v0.4-edge-quantum-langs",
|
"milestone_branch": "milestone/v0.4-edge-quantum-langs",
|
||||||
"phase_branch": "phase/06-final-review-ship",
|
"phase_branch": "phase/03-languages-derived",
|
||||||
"tag_base": "v0.3",
|
"tag_base": "v0.3",
|
||||||
"phase_tag": "v0.3.6",
|
"phase_tag": "v0.3.3",
|
||||||
"release_id": 488
|
"release_id": 485
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,9 @@
|
|||||||
> Generated during P0 RESEARCH. Atelier is a documentation-only project;
|
> Generated during P0 RESEARCH. Atelier is a documentation-only project;
|
||||||
> the default runtime persona roster is deactivated. Custom personas
|
> the default runtime persona roster is deactivated. Custom personas
|
||||||
> reflect the docs-as-code nature of the work. Updated per milestone:
|
> reflect the docs-as-code nature of the work. Updated per milestone:
|
||||||
> v0.4 added `edge-engineer` (P1) and `languages-engineer` (P3) phase-
|
> v0.4 adds `edge-engineer` (P1) and `languages-engineer` (P3) phase-
|
||||||
> specific personas (D-067); both REMOVED after v0.4 completion — roster
|
> specific personas (D-067); messaging (P2) is authored by tech-writer
|
||||||
> returned to 3 active personas. Messaging (P2) was authored by
|
> + domain-expert with edge-engineer consultation.
|
||||||
> tech-writer + domain-expert with edge-engineer consultation.
|
|
||||||
|
|
||||||
## Active Personas
|
## Active Personas
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@
|
|||||||
- **frameworks:** []
|
- **frameworks:** []
|
||||||
- **constraints:** ["markdown-first", "examples are mandatory", "why over what", "audience awareness"]
|
- **constraints:** ["markdown-first", "examples are mandatory", "why over what", "audience awareness"]
|
||||||
- **territory:** ["**/*.md", "LICENSE", "CHANGELOG.md", "CONTRIBUTING.md"]
|
- **territory:** ["**/*.md", "LICENSE", "CHANGELOG.md", "CONTRIBUTING.md"]
|
||||||
- **reason:** Primary author of all framework markdown content. Owns docs-as-code discipline (Documentation P1). Excludes `.ciagent/**` governance files (owned by lead-developer). For v0.4: co-authored the messaging domain (P2) with domain-expert, per D-067. v0.4 complete.
|
- **reason:** Primary author of all framework markdown content. Owns docs-as-code discipline (Documentation P1). Excludes `.ciagent/**` governance files (owned by lead-developer). For v0.4: co-authors the messaging domain (P2) with domain-expert, per D-067.
|
||||||
|
|
||||||
### domain-expert
|
### domain-expert
|
||||||
- **active:** true
|
- **active:** true
|
||||||
@@ -32,7 +31,7 @@
|
|||||||
- **frameworks:** []
|
- **frameworks:** []
|
||||||
- **constraints:** ["every principle traces to core", "10 principles per domain", "no orphans in matrix"]
|
- **constraints:** ["every principle traces to core", "10 principles per domain", "no orphans in matrix"]
|
||||||
- **territory:** ["core/**", "domains/**", "matrix/**", "review/**", "examples/**"]
|
- **territory:** ["core/**", "domains/**", "matrix/**", "review/**", "examples/**"]
|
||||||
- **reason:** Validates domain principle correctness and traceability to core principles. Ensures the matrix is complete and no domain rule is orphaned. For v0.4: co-authored the messaging domain (P2) with tech-writer, consulting edge-engineer for the D-062 boundary, per D-067. v0.4 complete.
|
- **reason:** Validates domain principle correctness and traceability to core principles. Ensures the matrix is complete and no domain rule is orphaned. For v0.4: co-authors the messaging domain (P2) with tech-writer, consulting edge-engineer for the D-062 boundary, per D-067.
|
||||||
|
|
||||||
## Inactive Personas (from default roster)
|
## Inactive Personas (from default roster)
|
||||||
|
|
||||||
@@ -50,25 +49,25 @@
|
|||||||
|
|
||||||
## Phase-Specific Personas
|
## Phase-Specific Personas
|
||||||
|
|
||||||
### edge-engineer (v0.4 — REMOVED after milestone completion)
|
### edge-engineer (v0.4 — REMOVE after milestone completion)
|
||||||
|
|
||||||
- **active:** false
|
- **active:** true
|
||||||
- **phase_specific:** true
|
- **phase_specific:** true
|
||||||
- **domain:** edge computing
|
- **domain:** edge computing
|
||||||
- **frameworks:** []
|
- **frameworks:** []
|
||||||
- **constraints:** ["latency-bound first", "resource-constrained examples", "trace to core", "10 P-rules per domain", "no runtime code", "proximity is the distinguishing constraint (D-061)", "offline-capability is non-negotiable"]
|
- **constraints:** ["latency-bound first", "resource-constrained examples", "trace to core", "10 P-rules per domain", "no runtime code", "proximity is the distinguishing constraint (D-061)", "offline-capability is non-negotiable"]
|
||||||
- **territory:** ["domains/edge/**", "examples/good/edge-offline-sync.md", "examples/bad/edge-sync-loop.md"]
|
- **territory:** ["domains/edge/**", "examples/good/edge-offline-sync.md", "examples/bad/edge-sync-loop.md"]
|
||||||
- **reason:** Per D-067: edge domain authoring (cdn, offline-first, iot, sync) benefited from a specialist persona with the proximity-is-correctness and offline-is-non-negotiable constraints. The distinguishing angle is proximity/location (D-061) — distinct from performance (generic), concurrency (in-process), and observability (generic telemetry). Was active for v0.4 P1 (Edge Domain) only; consulted during P2 (Messaging) for the D-062 in-process-vs-cross-system boundary; reviewed edge content in P6 Wave 1; removed after v0.4 completed; roster returned to 3 active personas.
|
- **reason:** Per D-067: edge domain authoring (cdn, offline-first, iot, sync) benefits from a specialist persona with the proximity-is-correctness and offline-is-non-negotiable constraints. The distinguishing angle is proximity/location (D-061) — distinct from performance (generic), concurrency (in-process), and observability (generic telemetry). Active for v0.4 P1 (Edge Domain) only; consulted during P2 (Messaging) for the D-062 in-process-vs-cross-system boundary; removed after v0.4 completes; roster returns to 3 active personas.
|
||||||
|
|
||||||
### languages-engineer (v0.4 — REMOVED after milestone completion)
|
### languages-engineer (v0.4 — REMOVE after milestone completion)
|
||||||
|
|
||||||
- **active:** false
|
- **active:** true
|
||||||
- **phase_specific:** true
|
- **phase_specific:** true
|
||||||
- **domain:** language-specific application
|
- **domain:** language-specific application
|
||||||
- **frameworks:** []
|
- **frameworks:** []
|
||||||
- **constraints:** ["trace to existing domain P-rules (D-063, D-066) — no new matrix rows", "per-language idioms", "fenced code examples in-language", "10 P-rules per domain does NOT apply to language docs", "manifest gains Languages section (D-065)"]
|
- **constraints:** ["trace to existing domain P-rules (D-063, D-066) — no new matrix rows", "per-language idioms", "fenced code examples in-language", "10 P-rules per domain does NOT apply to language docs", "manifest gains Languages section (D-065)"]
|
||||||
- **territory:** ["languages/**"]
|
- **territory:** ["languages/**"]
|
||||||
- **reason:** Per D-067: language-derived extension authoring (16 derived docs across TypeScript, Python, Go, Rust) benefited from a specialist persona enforcing the apply-don't-derive discipline (D-063) and the no-new-matrix-rows invariant (D-066). Languages are an application lens, not a principle source — the existing stubs' section-header-trace pattern was preserved, no P1–P10 numbering was introduced. Was active for v0.4 P3 (Language-Derived Extension) only; reviewed language-derived content in P6 Wave 1; removed after v0.4 completed; roster returned to 3 active personas.
|
- **reason:** Per D-067: language-derived extension authoring (16 derived docs across TypeScript, Python, Go, Rust) benefits from a specialist persona enforcing the apply-don't-derive discipline (D-063) and the no-new-matrix-rows invariant (D-066). Languages are an application lens, not a principle source — the existing stubs' section-header-trace pattern is preserved, no P1–P10 numbering is introduced. Active for v0.4 P3 (Language-Derived Extension) only; removed after v0.4 completes; roster returns to 3 active personas.
|
||||||
|
|
||||||
### platform-engineer (v0.3 — REMOVED after milestone completion)
|
### platform-engineer (v0.3 — REMOVED after milestone completion)
|
||||||
|
|
||||||
@@ -106,11 +105,11 @@ Mode: `warn` (per config.json `personas.territory_enforcement`).
|
|||||||
|
|
||||||
At `warn`, territory violations are logged but not blocked. This is appropriate for a docs project where tech-writer may touch `.ciagent/` files incidentally (e.g., updating ROADMAP status). Strict mode would be appropriate once territories stabilize.
|
At `warn`, territory violations are logged but not blocked. This is appropriate for a docs project where tech-writer may touch `.ciagent/` files incidentally (e.g., updating ROADMAP status). Strict mode would be appropriate once territories stabilize.
|
||||||
|
|
||||||
## v0.4 Persona Roster Summary (post-milestone)
|
## v0.4 Persona Roster Summary (active during milestone)
|
||||||
|
|
||||||
Active personas (3, span full milestone + ongoing): lead-developer, tech-writer, domain-expert.
|
Active personas (3, span full milestone): lead-developer, tech-writer, domain-expert.
|
||||||
Phase-specific personas (2, now removed): edge-engineer (P1 Edge Domain), languages-engineer (P3 Language-Derived Extension) — both deactivated after v0.4 completion per D-067; reviewed their authored content in P6 Wave 1 before removal.
|
Phase-specific personas (2, active during milestone): edge-engineer (P1 Edge Domain), languages-engineer (P3 Language-Derived Extension) — both removed after v0.4 completion per D-067.
|
||||||
Messaging (P2) was authored by tech-writer + domain-expert (the permanent roster) with edge-engineer consultation for the distributed-systems/async overlap (D-062 boundary), per D-067. The edge-engineer's distributed-systems lens survived the P1→P2 persona transition via a task must-have baked into the plan (the D-051 constraints-baked-into-task pattern from v0.3).
|
Messaging (P2) is authored by tech-writer + domain-expert (the permanent roster) with edge-engineer consultation for the distributed-systems/async overlap (D-062 boundary), per D-067. The edge-engineer's distributed-systems lens survives the P1→P2 persona transition via a task must-have baked into the plan (the D-051 constraints-baked-into-task pattern from v0.3): "P1/P2 demonstrated edge + messaging boundary (D-062); delivery-semantics comparison table required (D-069)."
|
||||||
Inactive default personas (3, unchanged): data-engineer, backend-engineer, frontend-engineer.
|
Inactive default personas (3, unchanged): data-engineer, backend-engineer, frontend-engineer.
|
||||||
Historical phase-specific personas (5, inactive): edge-engineer (v0.4), languages-engineer (v0.4), platform-engineer (v0.2), platform-engineer (v0.3 extended), ml-engineer (v0.3) — retained as historical record.
|
Historical phase-specific personas (3, inactive): platform-engineer (v0.2), platform-engineer (v0.3 extended), ml-engineer (v0.3) — retained as historical record; "v0.4: still inactive" appended to their reasons.
|
||||||
Peak active roster during v0.4: 5 (3 permanent + 2 phase-specific), matching v0.3's peak and avoiding persona-explosion risk. Roster has returned to 3 active personas for post-v0.4 maintenance.
|
Peak active roster during v0.4: 5 (3 permanent + 2 phase-specific), matching v0.3's peak and avoiding persona-explosion risk. Roster returns to 3 active personas after v0.4 completion.
|
||||||
@@ -239,32 +239,32 @@ All 35 requirements covered. 8 core principles, 11 domains, 110 domain principle
|
|||||||
|
|
||||||
| REQ-ID | Requirement | Priority | Phase | Status |
|
| REQ-ID | Requirement | Priority | Phase | Status |
|
||||||
|--------|-------------|----------|-------|--------|
|
|--------|-------------|----------|-------|--------|
|
||||||
| ATELIER-92 | `domains/edge/first-principles.md` — 10 edge principles (P1–P10) | P0 | 1 | covered |
|
| ATELIER-92 | `domains/edge/first-principles.md` — 10 edge principles (P1–P10) | P0 | 1 | pending |
|
||||||
| ATELIER-93 | `domains/edge/cdn.md` — CDN/edge-cache derived doc (with edge-cache vs origin decision matrix) | P1 | 1 | covered |
|
| ATELIER-93 | `domains/edge/cdn.md` — CDN/edge-cache derived doc (with edge-cache vs origin decision matrix) | P1 | 1 | pending |
|
||||||
| ATELIER-94 | `domains/edge/offline-first.md` — offline-first derived doc | P1 | 1 | covered |
|
| ATELIER-94 | `domains/edge/offline-first.md` — offline-first derived doc | P1 | 1 | pending |
|
||||||
| ATELIER-95 | `domains/edge/iot.md` — constrained-device/IoT derived doc | P1 | 1 | covered |
|
| ATELIER-95 | `domains/edge/iot.md` — constrained-device/IoT derived doc | P1 | 1 | pending |
|
||||||
| ATELIER-96 | `domains/edge/sync.md` — conflict/sync derived doc (CRDT/last-write-win decision matrix) | P1 | 1 | covered |
|
| ATELIER-96 | `domains/edge/sync.md` — conflict/sync derived doc (CRDT/last-write-win decision matrix) | P1 | 1 | pending |
|
||||||
| ATELIER-97 | `domains/messaging/first-principles.md` — 10 messaging principles (P1–P10) | P0 | 2 | covered |
|
| ATELIER-97 | `domains/messaging/first-principles.md` — 10 messaging principles (P1–P10) | P0 | 2 | pending |
|
||||||
| ATELIER-98 | `domains/messaging/queues.md` — queue derived doc (with at-least-once/at-most-once/exactly-once comparison) | P1 | 2 | covered |
|
| ATELIER-98 | `domains/messaging/queues.md` — queue derived doc (with at-least-once/at-most-once/exactly-once comparison) | P1 | 2 | pending |
|
||||||
| ATELIER-99 | `domains/messaging/pubsub.md` — pub/sub derived doc | P1 | 2 | covered |
|
| ATELIER-99 | `domains/messaging/pubsub.md` — pub/sub derived doc | P1 | 2 | pending |
|
||||||
| ATELIER-100 | `domains/messaging/streams.md` — stream processing derived doc | P1 | 2 | covered |
|
| ATELIER-100 | `domains/messaging/streams.md` — stream processing derived doc | P1 | 2 | pending |
|
||||||
| ATELIER-101 | `domains/messaging/delivery-semantics.md` — delivery semantics derived doc (idempotency, ordering, dead-letter) | P1 | 2 | covered |
|
| ATELIER-101 | `domains/messaging/delivery-semantics.md` — delivery semantics derived doc (idempotency, ordering, dead-letter) | P1 | 2 | pending |
|
||||||
| ATELIER-102 | `languages/typescript.md` extended + `languages/ts-types.md`, `ts-tooling.md`, `ts-async.md`, `ts-testing.md` — 4 TS derived docs | P1 | 3 | covered |
|
| ATELIER-102 | `languages/typescript.md` extended + `languages/ts-types.md`, `ts-tooling.md`, `ts-async.md`, `ts-testing.md` — 4 TS derived docs | P1 | 3 | pending |
|
||||||
| ATELIER-103 | `languages/python.md` extended + `languages/py-types.md`, `py-tooling.md`, `py-async.md`, `py-testing.md` — 4 Python derived docs | P1 | 3 | covered |
|
| ATELIER-103 | `languages/python.md` extended + `languages/py-types.md`, `py-tooling.md`, `py-async.md`, `py-testing.md` — 4 Python derived docs | P1 | 3 | pending |
|
||||||
| ATELIER-104 | `languages/go.md` extended + `languages/go-types.md`, `go-tooling.md`, `go-concurrency.md`, `go-testing.md` — 4 Go derived docs | P1 | 3 | covered |
|
| ATELIER-104 | `languages/go.md` extended + `languages/go-types.md`, `go-tooling.md`, `go-concurrency.md`, `go-testing.md` — 4 Go derived docs | P1 | 3 | pending |
|
||||||
| ATELIER-105 | `languages/rust.md` extended + `languages/rs-ownership.md`, `rs-tooling.md`, `rs-async.md`, `rs-testing.md` — 4 Rust derived docs | P1 | 3 | covered |
|
| ATELIER-105 | `languages/rust.md` extended + `languages/rs-ownership.md`, `rs-tooling.md`, `rs-async.md`, `rs-testing.md` — 4 Rust derived docs | P1 | 3 | pending |
|
||||||
| ATELIER-106 | Extend `matrix/principles-matrix.md` with 20 new P-rules → core C-rule mappings (10 per new domain; review check: row count per domain = 10, each row ≥1 C-rule) — post-v0.4: 19 domains, 190 P-rules | P0 | 4 | covered |
|
| ATELIER-106 | Extend `matrix/principles-matrix.md` with 20 new P-rules → core C-rule mappings (10 per new domain; review check: row count per domain = 10, each row ≥1 C-rule) — post-v0.4: 19 domains, 190 P-rules | P0 | 4 | pending |
|
||||||
| ATELIER-107 | Extend `matrix/domain-coverage.md` with edge + messaging (per-domain row schema) + add `languages/` coverage sub-table (language → derived docs → traced domains) | P1 | 4 | covered |
|
| ATELIER-107 | Extend `matrix/domain-coverage.md` with edge + messaging (per-domain row schema) + add `languages/` coverage sub-table (language → derived docs → traced domains) | P1 | 4 | pending |
|
||||||
| ATELIER-108 | Extend `review/agent-checklist.md` with edge + messaging trigger sections + a language-derived-docs section | P1 | 4 | covered |
|
| ATELIER-108 | Extend `review/agent-checklist.md` with edge + messaging trigger sections + a language-derived-docs section | P1 | 4 | pending |
|
||||||
| ATELIER-109 | Extend `review/peer-review-checklist.md` with edge + messaging + language-derived sections (parity with agent-checklist) | P1 | 4 | covered |
|
| ATELIER-109 | Extend `review/peer-review-checklist.md` with edge + messaging + language-derived sections (parity with agent-checklist) | P1 | 4 | pending |
|
||||||
| ATELIER-110 | Extend `review/anti-patterns.md` with edge + messaging violations incl. orphaned P-rule + deployable example artifact; pre-specify domain chaos anti-patterns (edge: sync conflict loop, TTL-less cache; messaging: shared subscription, blocking consumer) | P1 | 4 | covered |
|
| ATELIER-110 | Extend `review/anti-patterns.md` with edge + messaging violations incl. orphaned P-rule + deployable example artifact; pre-specify domain chaos anti-patterns (edge: sync conflict loop, TTL-less cache; messaging: shared subscription, blocking consumer) | P1 | 4 | pending |
|
||||||
| ATELIER-111 | Update `MANIFEST.md` to list new v0.4 domains + new `languages/` section (first-principles + derived docs per language) + post-v0.4 totals | P0 | 4 | covered |
|
| ATELIER-111 | Update `MANIFEST.md` to list new v0.4 domains + new `languages/` section (first-principles + derived docs per language) + post-v0.4 totals | P0 | 4 | pending |
|
||||||
| ATELIER-112 | `examples/good/edge-offline-sync.md` + `examples/good/messaging-idempotent-consumer.md` — 2 good examples (markdown with fenced code only) | P2 | 5 | covered |
|
| ATELIER-112 | `examples/good/edge-offline-sync.md` + `examples/good/messaging-idempotent-consumer.md` — 2 good examples (markdown with fenced code only) | P2 | 5 | pending |
|
||||||
| ATELIER-113 | `examples/bad/edge-sync-loop.md` + `examples/bad/messaging-shared-subscription.md` — 2 named bad examples (each cites the P-rule breached) | P2 | 5 | covered |
|
| ATELIER-113 | `examples/bad/edge-sync-loop.md` + `examples/bad/messaging-shared-subscription.md` — 2 named bad examples (each cites the P-rule breached) | P2 | 5 | pending |
|
||||||
| ATELIER-114 | Cross-links from new domains to existing devops/observability/data/concurrency/kubernetes/infrastructure-as-code domains; cross-links from language-derived docs to their traced domain docs (review check: every new derived doc ≥1 outbound cross-link to a MANIFEST-listed doc) | P1 | 5 | covered |
|
| ATELIER-114 | Cross-links from new domains to existing devops/observability/data/concurrency/kubernetes/infrastructure-as-code domains; cross-links from language-derived docs to their traced domain docs (review check: every new derived doc ≥1 outbound cross-link to a MANIFEST-listed doc) | P1 | 5 | pending |
|
||||||
| ATELIER-115 | Final review passes (all v0.4 phases reviewed, audit clean) | P0 | 6 | covered |
|
| ATELIER-115 | Final review passes (all v0.4 phases reviewed, audit clean) | P0 | 6 | pending |
|
||||||
| ATELIER-116 | Milestone v0.4 released (tag v0.3.6, merged to main) | P0 | 6 | covered |
|
| ATELIER-116 | Milestone v0.4 released (tag v0.3.6, merged to main) | P0 | 6 | pending |
|
||||||
| ATELIER-117 | Update `ARCHITECTURE.md` for v0.4 — extend the component tree with `edge/` + `messaging/` domain entries, add a `## v0.4 Domain Additions` section documenting the two new domains (boundary per D-061/D-062, derived docs, cross-link targets), and note the `languages/` derived-doc extension (D-063 apply-don't-derive). Closes architecture-tree drift: the file currently stops at v0.3 with no edge/messaging/languages-derived coverage (IDEATE-32). | P1 | 4 | covered |
|
| ATELIER-117 | Update `ARCHITECTURE.md` for v0.4 — extend the component tree with `edge/` + `messaging/` domain entries, add a `## v0.4 Domain Additions` section documenting the two new domains (boundary per D-061/D-062, derived docs, cross-link targets), and note the `languages/` derived-doc extension (D-063 apply-don't-derive). Closes architecture-tree drift: the file currently stops at v0.3 with no edge/messaging/languages-derived coverage (IDEATE-32). | P1 | 4 | pending |
|
||||||
|
|
||||||
## v0.4 Traceability Matrix
|
## v0.4 Traceability Matrix
|
||||||
|
|
||||||
|
|||||||
@@ -1,285 +0,0 @@
|
|||||||
# Atelier v0.4 — Final Review (P6)
|
|
||||||
|
|
||||||
## Reviewers: lead-developer, domain-expert, tech-writer, edge-engineer, languages-engineer
|
|
||||||
|
|
||||||
> Phase 6 Wave 1 — multi-persona review of all v0.4 content authored in P1–P5,
|
|
||||||
> plus the autonomous audit. The edge-engineer and languages-engineer personas
|
|
||||||
> review their territory then are removed (phase-specific). The lead-developer
|
|
||||||
> coordinates; domain-expert and tech-writer review messaging and verify
|
|
||||||
> traceability. Autonomy = full; P0 fixes auto-applied; P1+ flagged post-hoc.
|
|
||||||
|
|
||||||
## Findings by Persona
|
|
||||||
|
|
||||||
### edge-engineer (edge domain — D-061)
|
|
||||||
- Files reviewed: 5
|
|
||||||
(`domains/edge/first-principles.md`, `cdn.md`, `offline-first.md`, `iot.md`, `sync.md`)
|
|
||||||
- Boundary check: **PASS**
|
|
||||||
- Edge owns proximity/location/constraint/disconnection (D-061). Each doc
|
|
||||||
explicitly demarcates vs `domains/performance/` (generic measurement /
|
|
||||||
`performance/P4 Resource Bounds`, `performance/P5 Caching with Intent`),
|
|
||||||
`domains/observability/` (generic structured telemetry), and
|
|
||||||
`domains/concurrency/` (in-process primitives). The C4-Locality
|
|
||||||
discriminator is stated in first-principles §4: "performance's locality is
|
|
||||||
algorithmic; edge's locality is geographic." No generic-perf or
|
|
||||||
in-process-concurrency content leaked into edge.
|
|
||||||
- P-rule traces: **PASS — 10/10 trace to ≥1 C-rule**
|
|
||||||
- first-principles §2 Core Principle Trace table has all 10 P-rules with
|
|
||||||
explicit C-rule(s): P1→C4,C1 · P2→C1,C5 · P3→C8,C1 · P4→C1,C5 · P5→C1 ·
|
|
||||||
P6→C1,C3 · P7→C1,C5 · P8→C4,C6 · P9→C1,C8 · P10→C7,C5. Broad derivation
|
|
||||||
(7 of 8 C-rules); C2 is correctly noted as indirect.
|
|
||||||
- Decision matrices: **PASS — cdn, sync**
|
|
||||||
- `cdn.md` has "Edge-Cache vs Origin — Decision Matrix (D-069)" (6 rows:
|
|
||||||
serve-from-PoP, serve-stale-while-revalidate, fetch-fresh-miss,
|
|
||||||
origin-shield-fetch, purge-and-serve-fresh, serve-from-origin-bypass).
|
|
||||||
- `sync.md` has "CRDT vs Last-Write-Win — Decision Matrix (D-069)" (5 rows:
|
|
||||||
CvRDT, CmRDT, LWW-with-vector-clocks, LWW-with-wall-clock-only,
|
|
||||||
three-way-merge). Both carry the correctness-guarantee, cost, and
|
|
||||||
failure-mode columns.
|
|
||||||
- Cross-links: **PASS** (≥1 per derived doc)
|
|
||||||
- cdn.md → 1 (performance/frontend, security/input-validation, observability/metrics)
|
|
||||||
- offline-first.md → 5 (concurrency/patterns, observability/logging,
|
|
||||||
edge/sync, edge/cdn, testing/pyramid, testing/fixtures, observability/metrics)
|
|
||||||
- iot.md → 3 (security/secrets, messaging/queues, observability/metrics)
|
|
||||||
- sync.md → 2 (data/migrations, concurrency/patterns, observability/metrics)
|
|
||||||
- All one-directional outward per D-026 extended; edge↔messaging intra-v0.4
|
|
||||||
link (edge/iot → messaging/queues, MQTT QoS) resolved bidirectionally in
|
|
||||||
P5 per IDEATE-40 — verified: 4 refs in iot.md, 6 back-refs in
|
|
||||||
messaging/first-principles.md.
|
|
||||||
- Testing-pyramid mapping (IDEATE-38): **PASS**
|
|
||||||
- offline-first.md §"Offline Write-Queue and Conflict Detection Mapped to
|
|
||||||
the Testing Pyramid (IDEATE-38, ATELIER-94)" maps to all three tiers
|
|
||||||
(Unit: merge function; Integration: reconnect reconcile; E2e: partition
|
|
||||||
simulation) with the table + per-tier fenced sketches + cross-link to
|
|
||||||
`domains/testing/pyramid` and `domains/testing/fixtures`.
|
|
||||||
- P0 issues: **none**
|
|
||||||
- P1+ issues: **none**
|
|
||||||
|
|
||||||
### languages-engineer (language-derived — D-063)
|
|
||||||
- Files reviewed: 20 (4 stubs + 16 derived)
|
|
||||||
- Stubs: `typescript.md`, `python.md`, `go.md`, `rust.md`
|
|
||||||
- TS derived: `ts-types.md`, `ts-tooling.md`, `ts-async.md`, `ts-testing.md`
|
|
||||||
- Py derived: `py-types.md`, `py-tooling.md`, `py-async.md`, `py-testing.md`
|
|
||||||
- Go derived: `go-types.md`, `go-tooling.md`, `go-concurrency.md`, `go-testing.md`
|
|
||||||
- Rs derived: `rs-ownership.md`, `rs-tooling.md`, `rs-async.md`, `rs-testing.md`
|
|
||||||
- Apply-don't-derive check: **PASS — no new P-rules, no P1–P10 numbering**
|
|
||||||
- All 4 stubs retain their v0.1 section structure (0 `### P[0-9]` headers —
|
|
||||||
verified). All 16 derived docs introduce no new P-rules (0 new-P-rule
|
|
||||||
refs — verified). Each derived doc's blockquote header states
|
|
||||||
"introduces no new P-rules (D-063)". Every section header names the
|
|
||||||
traced domain P-rule(s) inline (e.g.,
|
|
||||||
`## Nominal vs Structural Typing (C1 Correctness, Data P7 Type Fidelity, API P1 Contract Fidelity)`).
|
|
||||||
- Trace check: **PASS — every section traces to a domain P-rule**
|
|
||||||
- Spot-check confirms section headers name ≥1 domain P-rule AND the core
|
|
||||||
C-rule(s). Cross-References sections at the end of each derived doc list
|
|
||||||
the traced domain first-principles + sibling language docs.
|
|
||||||
- D-066 matrix invariant: **PASS** — matrix stays at 190 P-rows / 19 domains.
|
|
||||||
The `## Languages Coverage` sub-table in `matrix/domain-coverage.md` is
|
|
||||||
exactly 16 rows (one per derived doc); no new matrix rows were added.
|
|
||||||
- Runtime artifact check: **PASS — no standalone .ts/.py/.go/.rs**
|
|
||||||
- `find` for standalone runtime artifact files returns empty. All code is
|
|
||||||
fenced inside `.md` files (illustrative only, D-020). The
|
|
||||||
`review/anti-patterns.md` v0.4 Deployable Artifact Types table lists the
|
|
||||||
forbidden extensions explicitly.
|
|
||||||
- Cross-links to new + existing: **PASS**
|
|
||||||
- Every derived doc has a Cross-References section linking to BOTH new
|
|
||||||
(edge/messaging where relevant — e.g., ts-async → messaging/delivery-semantics,
|
|
||||||
py-async → messaging/queues, go-concurrency → messaging/queues,
|
|
||||||
rs-async → messaging/delivery-semantics) AND existing domains
|
|
||||||
(concurrency, errors, data, testing, devops, documentation, api).
|
|
||||||
- P0 issues: **none**
|
|
||||||
- P1+ issues: **none**
|
|
||||||
|
|
||||||
### tech-writer + domain-expert (messaging — D-062)
|
|
||||||
- Files reviewed: 5
|
|
||||||
(`domains/messaging/first-principles.md`, `queues.md`, `pubsub.md`,
|
|
||||||
`streams.md`, `delivery-semantics.md`)
|
|
||||||
- Boundary check: **PASS**
|
|
||||||
- Messaging owns cross-process/network-failure-model (D-062). Each doc
|
|
||||||
explicitly demarcates vs `domains/concurrency/` (in-process/crash-failure-
|
|
||||||
model): concurrency's bounded queue fails by OOM or thread crash;
|
|
||||||
messaging's broker-backed queue fails by network partition, broker
|
|
||||||
restart, or consumer crash-and-retry. The discriminator (failure model)
|
|
||||||
is stated in first-principles §4 and repeated per derived doc.
|
|
||||||
- P-rule traces: **PASS — 10/10**
|
|
||||||
- first-principles §2 Core Principle Trace table: P1→C1,C2 · P2→C1,C2 ·
|
|
||||||
P3→C1 · P4→C1,C2 · P5→C1,C5 · P6→C1,C8 · P7→C4,C6 · P8→C5,C7 · P9→C5,C6 ·
|
|
||||||
P10→C7,C1. Broad derivation (7 C-rules); C3 correctly noted as not
|
|
||||||
primary (messaging is a tradeoff domain).
|
|
||||||
- Comparison tables: **PASS — queues, streams ×2, delivery-semantics ×2**
|
|
||||||
- `queues.md` — "Delivery Semantics Comparison (D-069)" (at-most-once /
|
|
||||||
at-least-once / exactly-once with guarantee, latency cost, implementation
|
|
||||||
cost, when-it-fits).
|
|
||||||
- `streams.md` — "Stream-Platform Comparison (D-069)" (Kafka / Kinesis /
|
|
||||||
Pulsar / NATS JetStream across ordering, partitioning, replay/retention,
|
|
||||||
consumer groups, exactly-once, use-case fit, watch-outs).
|
|
||||||
- `streams.md` — "Consumer-Group Rebalance Strategies (IDEATE-41)" (eager /
|
|
||||||
sticky-incremental-cooperative / cooperative-no-revoke with mechanism,
|
|
||||||
partition stop-the-world cost, use-case fit).
|
|
||||||
- `delivery-semantics.md` — "Dead-Letter Strategy Comparison (D-069)"
|
|
||||||
(retry-count-limit / TTL-with-backoff / poison-queue / DLQ+alert with
|
|
||||||
when-it-applies, failure-visibility, operational-cost).
|
|
||||||
- `delivery-semantics.md` — fenced idempotency-key dedup-store example
|
|
||||||
(IDEATE-39 — not prose-only; the `DedupStore` class with TTL-bounded
|
|
||||||
`seen`/`mark`).
|
|
||||||
- Cross-links: **PASS** (≥1 per derived doc)
|
|
||||||
- queues.md → 6 (concurrency/patterns, observability/metrics,
|
|
||||||
messaging/delivery-semantics, messaging/pubsub, messaging/streams,
|
|
||||||
errors/patterns)
|
|
||||||
- pubsub.md → 4 (messaging/streams, messaging/queues, observability/metrics)
|
|
||||||
- streams.md → 4 (messaging/delivery-semantics, messaging/pubsub,
|
|
||||||
data/schema-design, observability/tracing, observability/metrics)
|
|
||||||
- delivery-semantics.md → 5 (concurrency/patterns, errors/patterns,
|
|
||||||
observability/metrics, messaging/queues, messaging/streams)
|
|
||||||
- All one-directional outward per D-026 extended; edge↔messaging
|
|
||||||
bidirectional link resolved in P5 (messaging/first-principles §4
|
|
||||||
acknowledges the edge/iot back-link without editing edge content).
|
|
||||||
- P0 issues: **none**
|
|
||||||
- P1+ issues: **none**
|
|
||||||
|
|
||||||
### domain-expert (traceability)
|
|
||||||
- 20 new P-rules trace to ≥1 C-rule: **PASS**
|
|
||||||
- Edge (10): all traced in `domains/edge/first-principles.md` §2 AND
|
|
||||||
`matrix/principles-matrix.md` `## Edge` section (10 rows). No orphans.
|
|
||||||
- Messaging (10): all traced in `domains/messaging/first-principles.md` §2
|
|
||||||
AND `matrix/principles-matrix.md` `## Messaging` section (10 rows). No orphans.
|
|
||||||
- The two trace tables (first-principles §2 and matrix) are consistent.
|
|
||||||
- 16 Languages Coverage rows match derived docs: **PASS**
|
|
||||||
- `matrix/domain-coverage.md` `## Languages Coverage` sub-table has exactly
|
|
||||||
16 rows (4 languages × 4 derived docs). Spot-check confirms the traced
|
|
||||||
domain P-rules column matches the derived docs' actual section-header
|
|
||||||
traces (e.g., ts-types.md row: `Data P7, API P1, Errors P1` matches the
|
|
||||||
doc's headers naming Data P7, API P1, Errors P1).
|
|
||||||
- Matrix row count: **190 — PASS**
|
|
||||||
- `grep -c "^| P[0-9]" matrix/principles-matrix.md` = 190 exactly.
|
|
||||||
- Per G-014: 170 pre-existing (v0.1 + v0.2 + v0.3) + 20 new (10 edge + 10
|
|
||||||
messaging) = 190. NOT 171 + 20 (the v0.3 baseline was correctly 170, not 171).
|
|
||||||
- Edge section = 10 P-rows; Messaging section = 10 P-rows; no language rows
|
|
||||||
(D-066 invariant holds).
|
|
||||||
|
|
||||||
## P0 Fixes Applied (auto-applied in this phase)
|
|
||||||
|
|
||||||
1. **MANIFEST.md — 4 v0.4 examples unlisted (MANIFEST reconstruction drift).**
|
|
||||||
The 4 v0.4 example files existed on disk but were NOT listed in
|
|
||||||
`MANIFEST.md`'s Examples section:
|
|
||||||
- `examples/good/edge-offline-sync.md`
|
|
||||||
- `examples/good/messaging-idempotent-consumer.md`
|
|
||||||
- `examples/bad/edge-sync-loop.md`
|
|
||||||
- `examples/bad/messaging-shared-subscription.md`
|
|
||||||
Per the MANIFEST contract ("unlisted = not part of framework") and the
|
|
||||||
ATELIER-115 audit MANIFEST reconstruction test, this was a P0. Fix: added
|
|
||||||
the 4 rows to the Examples table with `✓` status + v0.4 notes, updated the
|
|
||||||
directory count notes (good: 8→10, bad: 7→9), and extended the closing
|
|
||||||
note to mention the 4 v0.4 examples. The 4 example files were verified
|
|
||||||
present on disk; the anti-patterns they instantiate are catalogued in
|
|
||||||
`review/anti-patterns.md` (v0.4 Chaos Anti-Patterns table + the
|
|
||||||
`edge-sync-loop` and `messaging-shared-subscription` named examples).
|
|
||||||
|
|
||||||
## P1+ Issues Flagged for Post-Hoc
|
|
||||||
|
|
||||||
none — review clean. The v0.4 content (edge, messaging, languages-derived)
|
|
||||||
is consistent with the D-061/D-062/D-063 boundaries, the traceability
|
|
||||||
invariants (G-014, D-066), the cross-link discipline (D-026 extended,
|
|
||||||
IDEATE-40), and the manifest contract. The only finding was the P0
|
|
||||||
MANIFEST drift, which has been fixed.
|
|
||||||
|
|
||||||
## Overall Review Verdict: **PASS**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Audit (ATELIER-115)
|
|
||||||
|
|
||||||
### Reconstruction test: **PASS**
|
|
||||||
- git log v0.3.0..HEAD: 11 commits, all with `---ci---` blocks containing
|
|
||||||
`project: atelier` (P0 pre-exec, P1 edge, P2 messaging, P3 languages,
|
|
||||||
P4 matrix+review+manifest+architecture, P5 examples+crosslinks + 5 ship
|
|
||||||
commits). Verified: 0 v0.4 commits missing the `project: atelier` field.
|
|
||||||
- CHECKPOINT.json: phase 5, stage complete — matches current state (P5
|
|
||||||
shipped at v0.3.5, release 487; P6 is the current pending phase per
|
|
||||||
ROADMAP). Note: CHECKPOINT.json will be advanced to phase 6 by the
|
|
||||||
orchestrator at ship (Wave 2); this is expected — the audit verifies
|
|
||||||
reconstruction from commits + ROADMAP + CHECKPOINT, all of which agree.
|
|
||||||
|
|
||||||
### Branch hygiene: **PASS**
|
|
||||||
- `milestone/v0.4-edge-quantum-langs`: exists (local + remote
|
|
||||||
`remotes/origin/milestone/v0.4-edge-quantum-langs`).
|
|
||||||
- `phase/01-05`: deleted post-merge — `git branch -a | grep -E "phase/0[1-5]"`
|
|
||||||
returns empty locally and on remote (phase/01..05 branches were merged and
|
|
||||||
removed per the phase-branch strategy).
|
|
||||||
- `phase/06-final-review-ship`: active (checked out, working tree clean
|
|
||||||
except the P0 MANIFEST fix which is staged for the orchestrator's commit).
|
|
||||||
|
|
||||||
### Commit discipline: **PASS**
|
|
||||||
- Total v0.4 commits (v0.3.0..HEAD): 11
|
|
||||||
- Commits with `---ci---` block: 11 (100%)
|
|
||||||
- Coverage: 11/11 = 100%
|
|
||||||
- Each `---ci---` block includes `project: atelier` (verified by scanning
|
|
||||||
every v0.4 commit body). The v0.4 milestone is fully `---ci---`-compliant.
|
|
||||||
- (Repo-wide: 38 of 46 all-branches commits carry `---ci---` blocks; the
|
|
||||||
non-ci commits are the pre-framework chore commits and the v0.1 legacy
|
|
||||||
commits before the ci-block convention was established — out of v0.4
|
|
||||||
scope.)
|
|
||||||
|
|
||||||
### MANIFEST reconstruction test: **PASS** (after P0 fix)
|
|
||||||
- Listed docs existing on disk: 33/33 paths checked (the 4
|
|
||||||
"missing-on-disk" detections are intentional placeholders —
|
|
||||||
`domains/<relevant>/first-principles.md` and `domains/<relevant>/<topic>.md`
|
|
||||||
in the Reading-Order section, and the `examples/good/` +
|
|
||||||
`examples/bad/` directory rows — not real files).
|
|
||||||
- Existing docs listed in MANIFEST: all 129 on-disk `.md` files under
|
|
||||||
`core/`, `domains/`, `languages/`, `matrix/`, `review/`, `examples/` are
|
|
||||||
listed. The MANIFEST uses a summarized format (Domains table by domain
|
|
||||||
name + derived-doc basenames; Languages table by language name + derived-
|
|
||||||
doc basenames; Examples table by full path; Cross-Cutting table by full
|
|
||||||
path). Verified by basename match for domains/languages + full-path match
|
|
||||||
for examples/core/matrix/review. The 4 v0.4 examples were the only drift;
|
|
||||||
the P0 fix added them.
|
|
||||||
- Drift: **none after P0 fix** (pre-fix: 4 v0.4 examples unlisted → fixed).
|
|
||||||
|
|
||||||
### Matrix row-count test: **PASS**
|
|
||||||
- `grep -c "^| P[0-9]" matrix/principles-matrix.md` = **190** (expected 190).
|
|
||||||
- Per G-014: 170 pre-existing (110 v0.1 + 20 v0.2 + 40 v0.3) + 20 new
|
|
||||||
(10 edge + 10 messaging) = 190. The v0.3 baseline was correctly 170 (NOT
|
|
||||||
171 — the v0.3 audit confirmed this; v0.4 builds on the correct baseline).
|
|
||||||
- Edge section: 10 P-rows. Messaging section: 10 P-rows. No language rows
|
|
||||||
(D-066 invariant — languages apply, not derive; matrix stays at 19
|
|
||||||
domains × 10 P-rules = 190).
|
|
||||||
|
|
||||||
### ARCHITECTURE.md component-tree test: **PASS**
|
|
||||||
- `domains/` on disk (19): ai-ml, api, compliance, concurrency, data,
|
|
||||||
devops, documentation, edge, errors, gitops-operators, i18n,
|
|
||||||
infrastructure-as-code, kubernetes, messaging, observability, performance,
|
|
||||||
security, testing, uiux.
|
|
||||||
- `domains/` in ARCHITECTURE.md: all 19 present (v0.1: 11 summarized as
|
|
||||||
`... (v0.1: 11 domains)`; v0.2: infrastructure-as-code, kubernetes
|
|
||||||
explicit; v0.3: gitops-operators, ai-ml, i18n, compliance explicit; v0.4:
|
|
||||||
edge, messaging explicit). The summary form for v0.1 is intentional and
|
|
||||||
consistent with the v0.2/v0.3 audit precedents.
|
|
||||||
- `languages/` on disk (20): 4 stubs (typescript.md, python.md, go.md,
|
|
||||||
rust.md) + 16 derived (ts-*, py-*, go-*, rs-*).
|
|
||||||
- `languages/` in ARCHITECTURE.md: all 20 present (4 stubs explicit + 4
|
|
||||||
derived-doc globs `ts-*.md`, `py-*.md`, `go-*.md`, `rs-*.md` each
|
|
||||||
enumerating the 4 derived docs, plus the "Derived docs by language"
|
|
||||||
listing in the v0.4 Language-Derived Extension section).
|
|
||||||
- Drift: **none**.
|
|
||||||
|
|
||||||
### Audit Verdict: **PASS**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
- Review verdict: **PASS** (clean across all 5 personas; 1 P0 fixed).
|
|
||||||
- Audit verdict: **PASS** (6 audit checks all pass after P0 fix).
|
|
||||||
- P0 fixes applied: **1** (MANIFEST.md — 4 v0.4 examples unlisted → listed).
|
|
||||||
- P1+ flagged: **0** (review clean).
|
|
||||||
- Matrix row count: **190** (confirmed — 170 pre-existing + 20 new, NOT
|
|
||||||
171 + 20).
|
|
||||||
- MANIFEST drift: **no** (after P0 fix; was: 4 v0.4 examples unlisted).
|
|
||||||
- Ready-to-ship: **yes** — review and audit both PASS; the milestone is
|
|
||||||
ready for Wave 2 (ship) by the orchestrator.
|
|
||||||
|
|
||||||
The v0.4 milestone (edge + messaging + language-derived extension) is
|
|
||||||
coherent, traceable, boundary-respecting, manifest-complete, and
|
|
||||||
audit-clean. The single P0 (MANIFEST examples drift) has been auto-applied;
|
|
||||||
the working tree carries only that one-line fix, staged for the
|
|
||||||
orchestrator's ship commit.
|
|
||||||
@@ -119,10 +119,10 @@ NFR milestone: no separate minor tag. The final patch (v0.2.6) IS the v0.3 deliv
|
|||||||
|
|
||||||
## Future Milestones
|
## Future Milestones
|
||||||
|
|
||||||
- **v0.4** (COMPLETE): `domains/edge/`, `domains/messaging/`, `languages/`-derived docs (typescript/python/go/rust → 4 derived docs each). Shipped v0.3.6.
|
- **v0.4** (ACTIVE): `domains/edge/`, `domains/messaging/`, `languages/`-derived docs (typescript/python/go/rust → 4 derived docs each).
|
||||||
- **v0.5** (candidates): `domains/quantum/`, tooling adapters (linters), translation/localization of framework docs.
|
- **v0.5** (candidates): `domains/quantum/`, tooling adapters (linters), translation/localization of framework docs.
|
||||||
|
|
||||||
## Milestone: v0.4 — Edge + Messaging + Language-Derived Docs (COMPLETE)
|
## Milestone: v0.4 — Edge + Messaging + Language-Derived Docs (ACTIVE)
|
||||||
|
|
||||||
**Milestone type:** NFR (all phases produce docs — no `feat` code)
|
**Milestone type:** NFR (all phases produce docs — no `feat` code)
|
||||||
**Tag line:** v0.3.x (previous minor from v0.4)
|
**Tag line:** v0.3.x (previous minor from v0.4)
|
||||||
@@ -134,9 +134,9 @@ NFR milestone: no separate minor tag. The final patch (v0.2.6) IS the v0.3 deliv
|
|||||||
| 1 | Edge Domain | docs | complete | domains/edge/{first-principles, cdn, offline-first, iot, sync}.md — shipped v0.3.1 |
|
| 1 | Edge Domain | docs | complete | domains/edge/{first-principles, cdn, offline-first, iot, sync}.md — shipped v0.3.1 |
|
||||||
| 2 | Messaging Domain | docs | complete | domains/messaging/{first-principles, queues, pubsub, streams, delivery-semantics}.md — shipped v0.3.2 |
|
| 2 | Messaging Domain | docs | complete | domains/messaging/{first-principles, queues, pubsub, streams, delivery-semantics}.md — shipped v0.3.2 |
|
||||||
| 3 | Language-Derived Extension | docs | complete | languages/ × 4 → first-principles + 4 derived docs each (16 derived docs) — shipped v0.3.3 |
|
| 3 | Language-Derived Extension | docs | complete | languages/ × 4 → first-principles + 4 derived docs each (16 derived docs) — shipped v0.3.3 |
|
||||||
| 4 | Matrix + Review Integration | docs | complete | matrix/principles-matrix.md (+20 mappings, 170→190), matrix/domain-coverage.md (+ languages/ sub-table), review/{agent-checklist, peer-review-checklist, anti-patterns}.md, MANIFEST.md (languages/ section), ARCHITECTURE.md — shipped v0.3.4 |
|
| 4 | Matrix + Review Integration | docs | pending | matrix/principles-matrix.md (+20 mappings, 170→190), matrix/domain-coverage.md (+ languages/ sub-table), review/{agent-checklist, peer-review-checklist, anti-patterns}.md, MANIFEST.md (languages/ section) |
|
||||||
| 5 | Examples + Cross-Links | docs | complete | examples/good + examples/bad for 2 domains, cross-links to devops/observability/data/concurrency/kubernetes/infrastructure-as-code + language→domain links — shipped v0.3.5 |
|
| 5 | Examples + Cross-Links | docs | pending | examples/good + examples/bad for 2 domains, cross-links to devops/observability/data/concurrency/kubernetes/infrastructure-as-code + language→domain links |
|
||||||
| 6 | Final Review + Ship | docs | complete | Review passed, audit clean, milestone merged to main, tag v0.3.6 — shipped v0.3.6 (IS the v0.4 release) |
|
| 6 | Final Review + Ship | docs | pending | Review passed, audit clean, milestone merged to main, tag v0.3.6 |
|
||||||
|
|
||||||
## v0.4 Phase Tag Mapping
|
## v0.4 Phase Tag Mapping
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"slug": "atelier",
|
"slug": "atelier",
|
||||||
"name": "Atelier",
|
"name": "Atelier",
|
||||||
"milestone": "v0.4",
|
"milestone": "v0.4",
|
||||||
"status": "complete"
|
"status": "active"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"active_project": "atelier",
|
"active_project": "atelier",
|
||||||
|
|||||||
+4
-8
@@ -58,12 +58,12 @@
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
> Examples are illustrative markdown with fenced code only (no standalone runtime artifacts per D-020 / D-025). The `examples/` directory listing closes the v0.2 ESC-002 drift (IDEATE-17, ATELIER-91). P5 authored the v0.3 examples and promoted all entries from `pending` to `✓` (verified — every listed file exists). P5 also authored the 4 v0.4 examples (edge + messaging chaos anti-patterns + good examples) and listed them here.
|
> Examples are illustrative markdown with fenced code only (no standalone runtime artifacts per D-020 / D-025). The `examples/` directory listing closes the v0.2 ESC-002 drift (IDEATE-17, ATELIER-91). P5 authored the v0.3 examples and promoted all entries from `pending` to `✓` (verified — every listed file exists).
|
||||||
|
|
||||||
| Path | Status | Notes |
|
| Path | Status | Notes |
|
||||||
|------|--------|-------|
|
|------|--------|-------|
|
||||||
| `examples/good/` | ✓ | Good-example directory — 10 examples (v0.1 + v0.2 + v0.3 + v0.4) |
|
| `examples/good/` | ✓ | Good-example directory — 8 examples (v0.1 + v0.2 + v0.3) |
|
||||||
| `examples/bad/` | ✓ | Bad-example directory — 9 examples (v0.1 + v0.2 + v0.3 + v0.4) |
|
| `examples/bad/` | ✓ | Bad-example directory — 7 examples (v0.1 + v0.2 + v0.3) |
|
||||||
| `examples/good/api-endpoint.md` | ✓ | v0.1 example — good REST endpoint |
|
| `examples/good/api-endpoint.md` | ✓ | v0.1 example — good REST endpoint |
|
||||||
| `examples/good/react-component.md` | ✓ | v0.1 example — good React component |
|
| `examples/good/react-component.md` | ✓ | v0.1 example — good React component |
|
||||||
| `examples/good/db-schema.md` | ✓ | v0.1 example — good DB schema |
|
| `examples/good/db-schema.md` | ✓ | v0.1 example — good DB schema |
|
||||||
@@ -79,12 +79,8 @@
|
|||||||
| `examples/good/ai-ml-reproducibility.md` | ✓ | v0.3 example — good reproducible training run |
|
| `examples/good/ai-ml-reproducibility.md` | ✓ | v0.3 example — good reproducible training run |
|
||||||
| `examples/bad/i18n-string-concat.md` | ✓ | v0.3 example — bad i18n string concat |
|
| `examples/bad/i18n-string-concat.md` | ✓ | v0.3 example — bad i18n string concat |
|
||||||
| `examples/bad/compliance-audit-log.md` | ✓ | v0.3 example — bad audit log (P1 + P9 breaches) |
|
| `examples/bad/compliance-audit-log.md` | ✓ | v0.3 example — bad audit log (P1 + P9 breaches) |
|
||||||
| `examples/good/edge-offline-sync.md` | ✓ | v0.4 example — good edge offline-first + sync reconcile (CRDT-vs-LWW decision matrix) |
|
|
||||||
| `examples/good/messaging-idempotent-consumer.md` | ✓ | v0.4 example — good idempotent consumer with dedup store + DLQ |
|
|
||||||
| `examples/bad/edge-sync-loop.md` | ✓ | v0.4 example — bad edge sync livelock (`edge-sync-loop` chaos anti-pattern, ATELIER-110) |
|
|
||||||
| `examples/bad/messaging-shared-subscription.md` | ✓ | v0.4 example — bad messaging shared subscription (`messaging-shared-subscription` chaos anti-pattern, ATELIER-110) |
|
|
||||||
|
|
||||||
> **Note:** The `examples/` section was established in P4 with entries pre-listed as `pending P5`. P5 authored the 4 v0.3 examples and the 4 v0.4 examples, and promoted all entries to `✓` after verifying every listed file exists on disk. The manifest remains authoritative — unlisted = not part of framework.
|
> **Note:** The `examples/` section was established in P4 with entries pre-listed as `pending P5`. P5 authored the 4 v0.3 examples and promoted all entries to `✓` after verifying every listed file exists on disk. The manifest remains authoritative — unlisted = not part of framework.
|
||||||
|
|
||||||
## Cross-Cutting
|
## Cross-Cutting
|
||||||
|
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
# Bad Example: Edge Sync Loop (Livelock)
|
|
||||||
|
|
||||||
> A two-node sync that oscillates divergent state forever. This is
|
|
||||||
> the pre-specified `edge-sync-loop` chaos anti-pattern (ATELIER-110).
|
|
||||||
> **Single-breach per D-068:** the principle breached is **Edge P4
|
|
||||||
> (Sync Conflicts are Bounded, Not Infinite)**. The prose explains
|
|
||||||
> why this is a livelock, not eventual consistency.
|
|
||||||
|
|
||||||
## The Code
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Two edge nodes (a tablet and a dispatcher workstation) sync a
|
|
||||||
// work-order status. The merge uses wall-clock timestamps with no
|
|
||||||
// vector clock and no deterministic tiebreak. A clock skew flips
|
|
||||||
// the winner on every merge pass; the state oscillates forever.
|
|
||||||
|
|
||||||
interface WorkOrderState {
|
|
||||||
orderId: string;
|
|
||||||
status: string;
|
|
||||||
wallClock: number; // P4 VIOLATION: wall time, not monotonic
|
|
||||||
nodeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// P4 VIOLATION: the merge picks the later wall-clock write as the
|
|
||||||
// winner. Wall time skews across nodes; a skew of even a few
|
|
||||||
// milliseconds flips the winner. With no vector clock, concurrent
|
|
||||||
// writes are not detected; with no deterministic tiebreak, equal
|
|
||||||
// timestamps are resolved by whichever node's clock is ahead.
|
|
||||||
function mergeBad(local: WorkOrderState, remote: WorkOrderState): WorkOrderState {
|
|
||||||
// No vector clock. No monotonic logical clock. No deterministic
|
|
||||||
// tiebreak by nodeId. This is wall-clock-only LWW on durable
|
|
||||||
// state — the row in the CRDT-vs-LWW decision matrix that is a
|
|
||||||
// P4 violation for anything but ephemeral state.
|
|
||||||
if (local.wallClock >= remote.wallClock) return local;
|
|
||||||
return remote;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The sync loop: each node merges the other's state, writes the
|
|
||||||
// result, and the next sync pass flips it back. The loop never
|
|
||||||
// terminates.
|
|
||||||
async function syncLoop(node: "tablet" | "dispatcher", peer: WorkOrderState) {
|
|
||||||
let local = store.get(peer.orderId);
|
|
||||||
while (true) {
|
|
||||||
const merged = mergeBad(local, peer);
|
|
||||||
store.set(merged.orderId, merged);
|
|
||||||
await pushToPeer(merged); // peer receives, merges, pushes back
|
|
||||||
peer = await pullFromPeer(); // peer's clock is now ahead — flips winner
|
|
||||||
local = store.get(peer.orderId); // local re-merges; flips again
|
|
||||||
// The loop runs forever. Convergence is never reached. This is
|
|
||||||
// a livelock: the system is making progress (each pass writes)
|
|
||||||
// but the state never converges.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The tablet sets `status: "completed"` at wall time `10:00:00.500`.
|
|
||||||
The dispatcher's clock is 50ms ahead; it sets `status: "reassigned"`
|
|
||||||
at wall time `10:00:00.550`. The first merge: dispatcher wins
|
|
||||||
(`10:00:00.550 > 10:00:00.500`). The tablet receives `reassigned`,
|
|
||||||
but its clock drifts ahead by 100ms during the next sync pass; it
|
|
||||||
writes `completed` at `10:00:00.650`. The second merge: tablet
|
|
||||||
wins. The dispatcher's clock drifts ahead again; it writes
|
|
||||||
`reassigned` at `10:00:00.750`. The third merge: dispatcher wins.
|
|
||||||
The state flips between `completed` and `reassigned` on every sync
|
|
||||||
pass. The loop runs forever.
|
|
||||||
|
|
||||||
## Why It Violates
|
|
||||||
|
|
||||||
### Sync Conflicts are Bounded, Not Infinite (Edge P4, C1, C5)
|
|
||||||
|
|
||||||
- **The breach:** the merge has no convergence bound. The CRDT-vs-
|
|
||||||
LWW decision matrix in `domains/edge/sync.md` is explicit:
|
|
||||||
wall-clock-only LWW on durable state carries an **unbounded
|
|
||||||
failure mode** — clock skew = oscillation — and is a P4
|
|
||||||
violation for anything but ephemeral state. This code is the
|
|
||||||
matrix's failure column made real.
|
|
||||||
- **Why it is a livelock, not eventual consistency:** eventual
|
|
||||||
consistency guarantees that, in the absence of new writes, all
|
|
||||||
replicas eventually converge. This system never converges even
|
|
||||||
with no new writes: the clock skew alone drives the oscillation.
|
|
||||||
Each sync pass writes (so the system is "busy"), but the state
|
|
||||||
never settles — the definition of a livelock. The P4 contract is
|
|
||||||
that the merge terminates and converges; this merge does neither.
|
|
||||||
- The missing pieces, per the decision matrix:
|
|
||||||
- **No monotonic logical clock** (a hybrid logical clock or
|
|
||||||
vector clock) — wall time skews, and the skew is unbounded.
|
|
||||||
- **No vector clock** — concurrent writes are not detected, so
|
|
||||||
the conflict is invisible; the merge silently flips instead of
|
|
||||||
surfacing.
|
|
||||||
- **No deterministic tiebreak** (e.g., `nodeId`) on equal
|
|
||||||
timestamps — equal wall times are resolved by whichever node's
|
|
||||||
clock is ahead, which is not a stable property.
|
|
||||||
- The P4 bound requires that the merge function be associative,
|
|
||||||
commutative, and idempotent, terminating in one pass. A
|
|
||||||
wall-clock merge with clock skew satisfies none of these: it is
|
|
||||||
not associative (order of merges flips the winner), not
|
|
||||||
idempotent (a re-merge after a clock drift flips the result),
|
|
||||||
and not terminating (the loop runs forever).
|
|
||||||
|
|
||||||
## The Fix
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Fix: replace wall-clock LWW with a CRDT register (state-based
|
|
||||||
// CvRDT) using a monotonic logical clock and a deterministic
|
|
||||||
// tiebreak by nodeId. The merge is now a total order — no
|
|
||||||
// oscillation (P4 bounded). See the CRDT-vs-LWW decision matrix:
|
|
||||||
// for a register on an unreliable network, a state-based CRDT is
|
|
||||||
// the correct row.
|
|
||||||
|
|
||||||
interface LWWRegister<T> {
|
|
||||||
value: T;
|
|
||||||
timestamp: number; // monotonic logical clock (HLC), not wall time
|
|
||||||
nodeId: string; // deterministic tiebreak
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeLWWRegister<T>(
|
|
||||||
local: LWWRegister<T>,
|
|
||||||
remote: LWWRegister<T>,
|
|
||||||
): LWWRegister<T> {
|
|
||||||
// Associative, commutative, idempotent (P4 bound, P5 idempotent).
|
|
||||||
// (timestamp, nodeId) is a total order — convergence in one pass.
|
|
||||||
if (local.timestamp > remote.timestamp) return local;
|
|
||||||
if (local.timestamp < remote.timestamp) return remote;
|
|
||||||
return local.nodeId > remote.nodeId ? local : remote;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The sync is now one pass: merge, write, done. No loop, no
|
|
||||||
// oscillation. A re-merge of the same two replicas yields the same
|
|
||||||
// result (P5 idempotent), so a retried sync is safe.
|
|
||||||
async function syncOnce(local: LWWRegister<unknown>, remote: LWWRegister<unknown>) {
|
|
||||||
const converged = mergeLWWRegister(local, remote);
|
|
||||||
store.set(converged);
|
|
||||||
await pushToPeer(converged);
|
|
||||||
// Done. No while(true). The merge terminates.
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The fix selects the CRDT row of the decision matrix (a register on
|
|
||||||
an unreliable network): a monotonic logical clock (HLC) eliminates
|
|
||||||
clock skew; a deterministic tiebreak by `nodeId` eliminates the
|
|
||||||
equal-timestamp flip. The merge is now a total order that converges
|
|
||||||
in one pass — the P4 bound. See `examples/good/edge-offline-sync.md`
|
|
||||||
for the full good-example version of this pattern.
|
|
||||||
|
|
||||||
## Cross-Domain Links
|
|
||||||
|
|
||||||
- `domains/edge/sync.md` — the CRDT-vs-LWW decision matrix; the
|
|
||||||
wall-clock-only LWW row is the failure mode this example
|
|
||||||
instantiates; the CRDT row is the fix.
|
|
||||||
- `domains/edge/first-principles.md` — P4 (Sync Conflicts are
|
|
||||||
Bounded, Not Infinite) is the principle breached.
|
|
||||||
- `review/anti-patterns.md` — the `edge-sync-loop` chaos
|
|
||||||
anti-pattern (edge P4, C1, C5 — infinite oscillation is a
|
|
||||||
correctness failure, not eventual consistency; the sync is a
|
|
||||||
livelock).
|
|
||||||
- `examples/good/edge-offline-sync.md` — the good-example version
|
|
||||||
of this pattern: a CRDT register merge that converges in one
|
|
||||||
pass.
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
# Bad Example: Messaging Shared Subscription
|
|
||||||
|
|
||||||
> Two consumers share one subscription; the broker dispatches each
|
|
||||||
> message to an arbitrary consumer. Per-consumer ordering breaks;
|
|
||||||
> per-consumer dedup is wrong. This is the pre-specified
|
|
||||||
> `messaging-shared-subscription` chaos anti-pattern (ATELIER-110).
|
|
||||||
> **Single-breach per D-068:** the primary principle breached is
|
|
||||||
> **Messaging P2 (Ordering is a Property, Not an Assumption)**. P3
|
|
||||||
> (Consumers are Idempotent) is noted as the compounding
|
|
||||||
> consequence — the example remains single-breach in its named
|
|
||||||
> violation.
|
|
||||||
|
|
||||||
## The Code
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Two email workers share ONE subscription on the "users" topic.
|
|
||||||
# The broker round-robins: worker A gets msg 1, msg 3; worker B
|
|
||||||
# gets msg 2, msg 4. Per-consumer order is broken (P2 breach).
|
|
||||||
# Each worker has its OWN dedup store — a redelivery to the other
|
|
||||||
# worker re-processes (P3 compounding).
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
def shared_subscription_bad(broker, send_email, worker_id: str):
|
|
||||||
# P2 VIOLATION: both workers call subscribe with the SAME
|
|
||||||
# subscription name. The broker dispatches each message to an
|
|
||||||
# arbitrary worker in the shared group. Worker A sees msg 3
|
|
||||||
# before worker B sees msg 1; per-worker order is broken.
|
|
||||||
sub = broker.subscribe(topic="users", subscription="welcome-shared")
|
|
||||||
|
|
||||||
# P3 compounding: per-worker dedup. A redelivered message may
|
|
||||||
# land on the OTHER worker, which has not seen it, so the
|
|
||||||
# per-worker dedup store does not catch it — the message is
|
|
||||||
# processed twice across the two workers.
|
|
||||||
local_dedup = DedupStore(backend=redis_for(worker_id))
|
|
||||||
|
|
||||||
for message in sub.receive():
|
|
||||||
payload = json.loads(message["body"])
|
|
||||||
if local_dedup.seen(payload["idempotencyKey"]):
|
|
||||||
sub.ack(message); continue
|
|
||||||
send_email(payload["email"], "Welcome!")
|
|
||||||
local_dedup.mark(payload["idempotencyKey"])
|
|
||||||
sub.ack(message)
|
|
||||||
```
|
|
||||||
|
|
||||||
```python
|
|
||||||
# The two workers are launched with the same subscription name.
|
|
||||||
# Worker A and Worker B both call subscribe("users", "welcome-shared").
|
|
||||||
# The broker sees one shared subscription; it round-robins.
|
|
||||||
|
|
||||||
def start_workers(broker):
|
|
||||||
# Worker A
|
|
||||||
spawn(shared_subscription_bad, broker, send_email, worker_id="A")
|
|
||||||
# Worker B — same subscription name, same topic
|
|
||||||
spawn(shared_subscription_bad, broker, send_email, worker_id="B")
|
|
||||||
# The broker dispatches: A gets msg 1, B gets msg 2, A gets msg 3,
|
|
||||||
# B gets msg 4. If worker A is slow, B may get msg 2 and msg 4
|
|
||||||
# before A acks msg 1. Per-worker order is broken (P2).
|
|
||||||
```
|
|
||||||
|
|
||||||
The scenario: a user signs up, then immediately updates their
|
|
||||||
email preference (two messages in topic order: `user.signed-up.v1`,
|
|
||||||
`user.preference-updated.v1`). The broker dispatches `signed-up` to
|
|
||||||
worker A and `preference-updated` to worker B. Worker B sends the
|
|
||||||
preference email before worker A sends the welcome email — the
|
|
||||||
user sees the preference confirmation before the welcome. Then the
|
|
||||||
broker redelivers `signed-up` (worker A's ack was slow); this time
|
|
||||||
it dispatches to worker B. Worker B's local dedup store has never
|
|
||||||
seen `signed-up` (it was processed by A), so B sends the welcome
|
|
||||||
email again. The user receives two welcome emails. Per-consumer
|
|
||||||
ordering broke (P2); per-consumer dedup did not catch the
|
|
||||||
cross-worker redelivery (P3 compounding).
|
|
||||||
|
|
||||||
## Why It Violates
|
|
||||||
|
|
||||||
### Ordering is a Property, Not an Assumption (Messaging P2, C1, C2)
|
|
||||||
|
|
||||||
- **The breach (primary):** the two workers share one subscription,
|
|
||||||
and the broker dispatches each message to an arbitrary worker in
|
|
||||||
the shared group. Per-consumer ordering is broken: worker A sees
|
|
||||||
`preference-updated` (msg 2) before it sees `signed-up` (msg 1)
|
|
||||||
if the broker round-robins them to different workers. The
|
|
||||||
workers' code assumes topic order, but the shared subscription
|
|
||||||
provides no per-consumer order guarantee — the broker's dispatch
|
|
||||||
is arbitrary. P2 requires that the ordering property be explicit
|
|
||||||
and documented; here it is assumed (topic order) but not provided
|
|
||||||
(arbitrary dispatch). The assumption is wrong.
|
|
||||||
- The P2 contract is that "it's FIFO" is a claim backed by the
|
|
||||||
broker's partitioning contract, not an assumption the consumer
|
|
||||||
makes. A shared subscription's contract is "no per-consumer
|
|
||||||
order"; the workers' code assumes the opposite. See
|
|
||||||
`domains/messaging/pubsub.md` (Shared vs Independent
|
|
||||||
Subscriptions) and `domains/messaging/first-principles.md` P2.
|
|
||||||
|
|
||||||
### Consumers are Idempotent — compounding consequence (Messaging P3, C1)
|
|
||||||
|
|
||||||
- **The compounding consequence (not the named breach):** each
|
|
||||||
worker has its OWN dedup store. A redelivered message that lands
|
|
||||||
on the *other* worker is not in that worker's dedup store, so it
|
|
||||||
is processed again. The dedup is per-worker, but the subscription
|
|
||||||
is shared — the dedup must be shared across workers to be
|
|
||||||
correct under a shared subscription. The per-worker dedup store
|
|
||||||
is wrong for a shared subscription; a shared dedup store (a
|
|
||||||
shared Redis, a shared DB) is required.
|
|
||||||
- Per D-068, the example remains single-breach in its named
|
|
||||||
violation: P2 is the primary breach (the shared subscription
|
|
||||||
breaks ordering); P3 is the compounding consequence (the
|
|
||||||
per-worker dedup is wrong *because* the subscription is shared).
|
|
||||||
If the subscription were independent, per-worker dedup would be
|
|
||||||
correct. The shared subscription is the root cause; P3 is the
|
|
||||||
downstream effect.
|
|
||||||
|
|
||||||
## The Fix
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Fix 1 (default): independent subscriptions. Each consumer gets
|
|
||||||
# its own durable cursor; per-consumer order holds (P2); per-
|
|
||||||
# consumer dedup is correct (P3). This is the default per
|
|
||||||
# domains/messaging/pubsub.md.
|
|
||||||
|
|
||||||
def independent_subscriptions_good(broker, send_email, worker_id: str):
|
|
||||||
# Each worker has its OWN subscription name. The broker
|
|
||||||
# delivers every message to every subscription in topic order.
|
|
||||||
sub = broker.subscribe(
|
|
||||||
topic="users",
|
|
||||||
subscription=f"welcome-{worker_id}", # per-consumer
|
|
||||||
)
|
|
||||||
# Per-consumer dedup is now correct: a redelivery to THIS
|
|
||||||
# worker is caught by THIS worker's dedup store.
|
|
||||||
dedup = DedupStore(backend=redis_for(worker_id))
|
|
||||||
for message in sub.receive():
|
|
||||||
payload = json.loads(message["body"])
|
|
||||||
if dedup.seen(payload["idempotencyKey"]):
|
|
||||||
sub.ack(message); continue
|
|
||||||
send_email(payload["email"], "Welcome!")
|
|
||||||
dedup.mark(payload["idempotencyKey"])
|
|
||||||
sub.ack(message)
|
|
||||||
```
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Fix 2 (if a shared subscription is genuinely required): the
|
|
||||||
# consumers must be stateless, the processing order-independent,
|
|
||||||
# AND the dedup store must be SHARED across workers. Document the
|
|
||||||
# ordering property as "none across consumers" (P2 — the property
|
|
||||||
# is explicit, not assumed) and use a shared dedup backend (P3).
|
|
||||||
|
|
||||||
def shared_subscription_stateless(broker, send_email):
|
|
||||||
# P2: document the ordering property. A shared subscription
|
|
||||||
# provides NO per-consumer order; processing must be order-
|
|
||||||
# independent. Do not assume topic order.
|
|
||||||
sub = broker.subscribe(topic="users", subscription="welcome-shared")
|
|
||||||
# P3: SHARED dedup. A redelivery to any worker is caught by the
|
|
||||||
# shared store.
|
|
||||||
shared_dedup = DedupStore(backend=shared_redis)
|
|
||||||
for message in sub.receive():
|
|
||||||
payload = json.loads(message["body"])
|
|
||||||
if shared_dedup.seen(payload["idempotencyKey"]):
|
|
||||||
sub.ack(message); continue
|
|
||||||
send_email(payload["email"], "Welcome!") # order-independent
|
|
||||||
shared_dedup.mark(payload["idempotencyKey"])
|
|
||||||
sub.ack(message)
|
|
||||||
```
|
|
||||||
|
|
||||||
The default is Fix 1 (independent subscriptions): per-consumer
|
|
||||||
ordering holds, per-consumer dedup is correct, and the code is
|
|
||||||
simpler. Fix 2 is the narrow opt-in for genuinely stateless,
|
|
||||||
order-independent processing — and even then, the dedup must be
|
|
||||||
shared. See `domains/messaging/pubsub.md` for the full
|
|
||||||
shared-vs-independent discussion.
|
|
||||||
|
|
||||||
## Cross-Domain Links
|
|
||||||
|
|
||||||
- `domains/messaging/pubsub.md` — the shared-vs-independent
|
|
||||||
subscriptions section; the `messaging-shared-subscription`
|
|
||||||
anti-pattern lives here (pre-specified in P4 ATELIER-110).
|
|
||||||
- `domains/messaging/first-principles.md` — P2 (Ordering is a
|
|
||||||
Property, Not an Assumption) is the primary breach; P3
|
|
||||||
(Consumers are Idempotent) is the compounding consequence.
|
|
||||||
- `domains/messaging/delivery-semantics.md` — the idempotency-key
|
|
||||||
dedup store; the per-subscription dedup key scoping
|
|
||||||
(`(subscription, idempotencyKey)`) that prevents one
|
|
||||||
subscription's dedup from masking another's redelivery.
|
|
||||||
- `review/anti-patterns.md` — the `messaging-shared-subscription`
|
|
||||||
chaos anti-pattern (messaging P2, P3, C1 — shared subscription
|
|
||||||
breaks ordering and dedup).
|
|
||||||
- `examples/good/messaging-idempotent-consumer.md` — the good-
|
|
||||||
example version of the consumer pattern: an independent consumer
|
|
||||||
with a dedup store and a DLQ routing rule.
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
# Good Example: Edge Offline-First + Sync Reconcile
|
|
||||||
|
|
||||||
> A field-service tablet app that operates through a 90-minute network
|
|
||||||
> partition, queues writes locally, and reconciles on reconnect using
|
|
||||||
> the CRDT-vs-LWW decision matrix from `domains/edge/sync.md`. Each
|
|
||||||
> aspect cites the edge principle it satisfies.
|
|
||||||
|
|
||||||
## The Code
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// The local store: an offline-first write queue + a CRDT register
|
|
||||||
// per record. The tablet keeps working through the partition; the
|
|
||||||
// queue drains on reconnect; the merge converges (P4 bounded).
|
|
||||||
|
|
||||||
interface LWWRegister<T> {
|
|
||||||
value: T;
|
|
||||||
timestamp: number; // monotonic logical clock (HLC), NOT wall time
|
|
||||||
nodeId: string; // tablet id — deterministic tiebreak (P4)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QueuedWrite {
|
|
||||||
idempotencyKey: string; // P5: retried syncs are safe
|
|
||||||
collection: string;
|
|
||||||
recordId: string;
|
|
||||||
register: LWWRegister<unknown>;
|
|
||||||
queuedAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class OfflineStore {
|
|
||||||
private queue: QueuedWrite[] = [];
|
|
||||||
private state: Map<string, LWWRegister<unknown>> = new Map();
|
|
||||||
private telemetryBuffer: TelemetryRecord[] = []; // P10 local-first
|
|
||||||
|
|
||||||
// P2: write succeeds offline. The queue is the durable record of
|
|
||||||
// intent; sync is deferred, not blocked.
|
|
||||||
write(collection: string, recordId: string, value: unknown, key: string): void {
|
|
||||||
const register: LWWRegister<unknown> = {
|
|
||||||
value,
|
|
||||||
timestamp: this.hlc.now(), // monotonic; no wall-clock skew
|
|
||||||
nodeId: this.nodeId,
|
|
||||||
};
|
|
||||||
this.state.set(`${collection}:${recordId}`, register);
|
|
||||||
this.queue.push({
|
|
||||||
idempotencyKey: key, // P5: idempotent sync
|
|
||||||
collection, recordId, register,
|
|
||||||
queuedAt: this.hlc.now(),
|
|
||||||
});
|
|
||||||
// P10: buffer the write event locally; forwarded on reconnect.
|
|
||||||
this.telemetryBuffer.push({ event: "local.write", at: Date.now(), key });
|
|
||||||
}
|
|
||||||
|
|
||||||
// P2: read from local state offline. The UI never blocks on the
|
|
||||||
// network.
|
|
||||||
read(collection: string, recordId: string): unknown {
|
|
||||||
return this.state.get(`${collection}:${recordId}`)?.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// The CRDT register merge — a state-based CvRDT (convergent). Per the
|
|
||||||
// CRDT-vs-LWW decision matrix in domains/edge/sync.md, a state-based
|
|
||||||
// CRDT is the choice when the data model fits a register and the
|
|
||||||
// network is unreliable (full-state merge tolerates dropped ops).
|
|
||||||
|
|
||||||
function mergeLWWRegister<T>(
|
|
||||||
local: LWWRegister<T>,
|
|
||||||
remote: LWWRegister<T>,
|
|
||||||
): LWWRegister<T> {
|
|
||||||
// Associative, commutative, idempotent (P4 bound, P5 idempotent).
|
|
||||||
// (timestamp, nodeId) is a total order — no oscillation.
|
|
||||||
if (local.timestamp > remote.timestamp) return local;
|
|
||||||
if (local.timestamp < remote.timestamp) return remote;
|
|
||||||
return local.nodeId > remote.nodeId ? local : remote; // deterministic tie
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// The reconnect reconcile: drain the offline queue, pull remote
|
|
||||||
// state, three-way merge (CRDT registers), push converged state.
|
|
||||||
// Idempotent keys make a retried reconcile safe (P5).
|
|
||||||
|
|
||||||
async function reconcile(store: OfflineStore, server: SyncServer): Promise<ReconcileReport> {
|
|
||||||
// P5: the reconcile is idempotent. The idempotency key on each
|
|
||||||
// queued write means a retry (network flapped mid-reconcile) does
|
|
||||||
// not double-apply.
|
|
||||||
const queued = store.drainQueue();
|
|
||||||
let pushed = 0, merged = 0, conflicts = 0;
|
|
||||||
|
|
||||||
// 1. Push local writes. The server dedups by idempotencyKey (P5).
|
|
||||||
for (const w of queued) {
|
|
||||||
await server.applyWrite(w.idempotencyKey, w.collection, w.recordId, w.register);
|
|
||||||
pushed++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Pull remote state for every record we touched + every record
|
|
||||||
// the server changed since our last sync cursor. Merge via CRDT.
|
|
||||||
const remoteRecords = await server.fetchChanged(store.syncCursor());
|
|
||||||
for (const [key, remoteReg] of remoteRecords) {
|
|
||||||
const localReg = store.localRegister(key);
|
|
||||||
if (localReg) {
|
|
||||||
// P4: CRDT merge converges. The merge is a pure function of
|
|
||||||
// two inputs; the total order (timestamp, nodeId) guarantees
|
|
||||||
// no oscillation.
|
|
||||||
const converged = mergeLWWRegister(localReg, remoteReg);
|
|
||||||
if (converged !== localReg) { conflicts++; }
|
|
||||||
store.setLocal(key, converged);
|
|
||||||
merged++;
|
|
||||||
} else {
|
|
||||||
store.setLocal(key, remoteReg); // remote-only record
|
|
||||||
merged++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. P10: flush the buffered telemetry. The partition did not blind
|
|
||||||
// the operator — the events survived on-node.
|
|
||||||
store.flushTelemetry();
|
|
||||||
|
|
||||||
return { pushed, merged, conflicts, converged: true };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// P10: local-first telemetry. Events are buffered on-node and
|
|
||||||
// forwarded on reconnect. A fire-and-forget pipeline loses data when
|
|
||||||
// the link drops; a local-first buffer survives.
|
|
||||||
|
|
||||||
interface TelemetryRecord { event: string; at: number; key: string; }
|
|
||||||
|
|
||||||
class TelemetryBuffer {
|
|
||||||
private buffer: TelemetryRecord[] = [];
|
|
||||||
|
|
||||||
push(rec: TelemetryRecord): void { this.buffer.push(rec); }
|
|
||||||
|
|
||||||
// Called from reconcile() on reconnect. The buffer is the P10
|
|
||||||
// guarantee: the operator sees the partition-window activity,
|
|
||||||
// not a gap.
|
|
||||||
async flush(sink: TelemetrySink): Promise<void> {
|
|
||||||
for (const rec of this.buffer) { await sink.emit(rec); }
|
|
||||||
this.buffer = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
depth(): number { return this.buffer.length; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## The Scenario
|
|
||||||
|
|
||||||
A field-service tablet is dispatched to a basement site with no
|
|
||||||
cellular coverage. The technician updates the work-order status
|
|
||||||
(`started`, `parts-ordered`, `completed`) five times over 90
|
|
||||||
minutes. Each write lands in the local store immediately — the UI
|
|
||||||
never blocks on the network. The writes are queued with an
|
|
||||||
idempotency key (the work-order id + a monotonic sequence).
|
|
||||||
|
|
||||||
When the tablet reconnects, the reconcile drains the queue: the
|
|
||||||
server dedups by idempotency key (a retry mid-reconcile does not
|
|
||||||
double-apply). The server also returns a remote update — the
|
|
||||||
dispatcher re-assigned the work order to a different technician at
|
|
||||||
minute 45, then reverted at minute 60. The CRDT merge converges:
|
|
||||||
the register with the later logical timestamp wins; the tiebreak by
|
|
||||||
node id is deterministic. The merge terminates in one pass (P4
|
|
||||||
bounded); it does not oscillate between the dispatcher's revert and
|
|
||||||
the technician's status updates. The buffered telemetry flushes,
|
|
||||||
and the operator sees the full partition-window activity — no gap.
|
|
||||||
|
|
||||||
## Principles Demonstrated
|
|
||||||
|
|
||||||
### Offline is a First-Class State (Edge P2, C1, C5)
|
|
||||||
- The tablet writes and reads through the partition. The UI never
|
|
||||||
blocks on the network; the offline write queue is the durable
|
|
||||||
record of intent. Partition is the norm, not the exception;
|
|
||||||
reconciliation happens on reconnect. An app that crashes on
|
|
||||||
disconnect has no offline state; this app engineers it.
|
|
||||||
- See `domains/edge/offline-first.md` (offline write-queue) and
|
|
||||||
`domains/edge/first-principles.md` P2.
|
|
||||||
|
|
||||||
### Sync Conflicts are Bounded, Not Infinite (Edge P4, C1, C5)
|
|
||||||
- The CRDT register merge is associative, commutative, and
|
|
||||||
idempotent. The total order `(timestamp, nodeId)` guarantees
|
|
||||||
convergence in one pass — no oscillation, no infinite loop. This
|
|
||||||
is the P4 bound: the merge terminates. The CRDT-vs-LWW decision
|
|
||||||
matrix in `domains/edge/sync.md` selected a state-based CRDT
|
|
||||||
because the data model fits a register and the network is
|
|
||||||
unreliable (full-state merge tolerates dropped ops).
|
|
||||||
- See `domains/edge/sync.md` (CRDT-vs-LWW decision matrix, merge
|
|
||||||
semantics) and `domains/edge/first-principles.md` P4.
|
|
||||||
|
|
||||||
### Edge Operations are Idempotent (Edge P5, C1)
|
|
||||||
- Every queued write carries an idempotency key; the server dedups
|
|
||||||
by key. A reconcile retried mid-flap does not double-apply. The
|
|
||||||
merge function is idempotent (`merge(a, a) === a`) — a retried
|
|
||||||
merge of the same two replicas yields the same result. Sync, the
|
|
||||||
retried-by-nature operation, is safe.
|
|
||||||
- See `domains/edge/sync.md` (Merge Semantics, P5) and
|
|
||||||
`domains/edge/first-principles.md` P5.
|
|
||||||
|
|
||||||
### Edge Observability Survives Partition (Edge P10, C7, C5)
|
|
||||||
- Telemetry is buffered on-node (`telemetryBuffer`) and flushed on
|
|
||||||
reconnect. The partition did not blind the operator — the
|
|
||||||
partition-window activity is forwarded, not lost. A
|
|
||||||
fire-and-forget pipeline would have a 90-minute gap; the
|
|
||||||
local-first buffer survives.
|
|
||||||
- See `domains/edge/offline-first.md` (local-first logging) and
|
|
||||||
`domains/edge/first-principles.md` P10.
|
|
||||||
|
|
||||||
## Cross-Domain Links
|
|
||||||
|
|
||||||
- `domains/edge/offline-first.md` — the offline write-queue pattern
|
|
||||||
this app instantiates; the partition-as-norm discipline.
|
|
||||||
- `domains/edge/sync.md` — the CRDT-vs-LWW decision matrix exercised
|
|
||||||
here (state-based CvRDT chosen for register + unreliable network);
|
|
||||||
the merge-semantics properties (associative, commutative,
|
|
||||||
idempotent).
|
|
||||||
- `domains/edge/first-principles.md` — P2, P4, P5, P10 are the
|
|
||||||
principles demonstrated.
|
|
||||||
- `domains/concurrency/patterns` — the in-process immutability
|
|
||||||
analog (`concurrency/P1 Immutability by Default`) that makes the
|
|
||||||
merge a pure function of two inputs.
|
|
||||||
- `domains/observability/metrics` — the generic SLI/SLO discipline
|
|
||||||
the local-first telemetry buffer builds on.
|
|
||||||
- `review/anti-patterns.md` — the `edge-sync-loop` chaos
|
|
||||||
anti-pattern is the inverse of this example's bounded CRDT merge.
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
# Good Example: Idempotent Consumer with Dedup + DLQ
|
|
||||||
|
|
||||||
> An orders-consumer that achieves exactly-once-via-idempotency:
|
|
||||||
> at-least-once delivery plus a TTL-bounded dedup store and a DLQ
|
|
||||||
> routing rule. Each aspect cites the messaging principle it
|
|
||||||
> satisfies. Exercises the idempotency + DLQ guidance in
|
|
||||||
> `domains/messaging/delivery-semantics.md`.
|
|
||||||
|
|
||||||
## The Code
|
|
||||||
|
|
||||||
```python
|
|
||||||
# The idempotency-key dedup store (P3). TTL-bounded (P6): a dedup
|
|
||||||
# store with no TTL is a memory leak. The TTL exceeds the broker's
|
|
||||||
# max-redelivery window; beyond it, the key is expired (the broker
|
|
||||||
# has given up).
|
|
||||||
|
|
||||||
import time, json
|
|
||||||
|
|
||||||
DEDUP_TTL_SECONDS = 24 * 3600 # > broker max-redelivery window
|
|
||||||
|
|
||||||
class DedupStore:
|
|
||||||
"""P3 (idempotent), P6 (TTL-bounded). seen() before process;
|
|
||||||
mark() after process; the order gives at-least-once + dedup."""
|
|
||||||
|
|
||||||
def __init__(self, backend):
|
|
||||||
# backend is Redis or a shared DB. MUST be shared across
|
|
||||||
# consumer instances (see messaging/pubsub.md on shared vs
|
|
||||||
# independent subscriptions).
|
|
||||||
self.backend = backend
|
|
||||||
|
|
||||||
def seen(self, key: str) -> bool:
|
|
||||||
ts = self.backend.get(key)
|
|
||||||
if ts is None:
|
|
||||||
return False
|
|
||||||
if time.time() - ts > DEDUP_TTL_SECONDS:
|
|
||||||
self.backend.delete(key) # P6: expired; not a redelivery
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def mark(self, key: str):
|
|
||||||
self.backend.set(key, time.time(), ttl=DEDUP_TTL_SECONDS)
|
|
||||||
```
|
|
||||||
|
|
||||||
```python
|
|
||||||
# The idempotent consumer. Order: dedup BEFORE process, mark AFTER
|
|
||||||
# process, ack AFTER mark. A crash before mark re-processes (the
|
|
||||||
# dedup store lacks the key); a crash before ack redelivers and the
|
|
||||||
# dedup store makes the redelivery a no-op (P3). For a
|
|
||||||
# non-idempotent process (a payment that must not double-charge),
|
|
||||||
# process+mark are one DB transaction — exactly-once via
|
|
||||||
# idempotency (P4).
|
|
||||||
|
|
||||||
def consume_orders(broker, dedup: DedupStore, process_order):
|
|
||||||
for message in broker.receive():
|
|
||||||
payload = json.loads(message["body"])
|
|
||||||
|
|
||||||
# P3: dedup BEFORE process. A redelivered message is a
|
|
||||||
# no-op, not a double-apply.
|
|
||||||
if dedup.seen(payload["idempotencyKey"]):
|
|
||||||
broker.ack(message) # already processed; skip
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
# P4: the declared semantic is at-least-once + idempotent
|
|
||||||
# dedup = exactly-once-via-idempotency. For a payment,
|
|
||||||
# process_order + dedup.mark run in one DB transaction
|
|
||||||
# so the mark commits iff the process commits.
|
|
||||||
process_order(payload)
|
|
||||||
dedup.mark(payload["idempotencyKey"])
|
|
||||||
broker.ack(message)
|
|
||||||
|
|
||||||
except TransientError as exc:
|
|
||||||
# P6: bounded retry with backoff. Nack for redelivery;
|
|
||||||
# the broker redelivers after exponential backoff.
|
|
||||||
broker.nack(message, delay=backoff(payload.get("attempt", 0)))
|
|
||||||
|
|
||||||
except (ValueError, SchemaError) as exc:
|
|
||||||
# P5: poison message — unparseable. Route immediately,
|
|
||||||
# do NOT retry (no retry will fix a bad schema).
|
|
||||||
route_to_dlq(broker, message, exc, kind="poison")
|
|
||||||
broker.ack(message)
|
|
||||||
|
|
||||||
except PermanentError as exc:
|
|
||||||
# P5: permanent failure (e.g., not-found dependency).
|
|
||||||
# Retry will not fix it — DLQ now.
|
|
||||||
route_to_dlq(broker, message, exc, kind="dlq")
|
|
||||||
broker.ack(message)
|
|
||||||
```
|
|
||||||
|
|
||||||
```python
|
|
||||||
# The DLQ routing rule (P5 dead-letter handling, P10 DLQ depth
|
|
||||||
# alert). Distinguishes poison (unparseable; never retried) from
|
|
||||||
# DLQ (exhausted retry budget on a transient). Both carry audit
|
|
||||||
# metadata; both emit a depth metric.
|
|
||||||
|
|
||||||
DLQ = "orders-dlq"
|
|
||||||
POISON = "orders-poison"
|
|
||||||
MAX_RETRY_TTL_SECONDS = 30 * 60 # 30 min retry window
|
|
||||||
|
|
||||||
def route_to_dlq(broker, message, reason, kind: str):
|
|
||||||
target = POISON if kind == "poison" else DLQ
|
|
||||||
broker.send(target, body=json.dumps({
|
|
||||||
"original": message["body"],
|
|
||||||
"reason": str(reason),
|
|
||||||
"kind": kind, # poison vs dlq
|
|
||||||
"deadLetteredAt": now_iso(),
|
|
||||||
"redeliveryCount": message.get("attempt", 0),
|
|
||||||
}))
|
|
||||||
# P10: emit a metric so DLQ depth alerts fire. A DLQ that grows
|
|
||||||
# with no alert is a silent correctness defect (P5/P10).
|
|
||||||
metrics.increment(f"{kind}.depth", tags={"queue": "orders"})
|
|
||||||
|
|
||||||
|
|
||||||
def consume_with_retry_budget(broker, dedup, process_order):
|
|
||||||
# Combines TTL-with-backoff for transient failures (P6) with
|
|
||||||
# poison-queue + DLQ + alert (P5/P10).
|
|
||||||
for message in broker.receive():
|
|
||||||
payload = json.loads(message["body"])
|
|
||||||
if dedup.seen(payload["idempotencyKey"]):
|
|
||||||
broker.ack(message); continue
|
|
||||||
|
|
||||||
first_attempt_ts = payload.get("firstAttemptTs", time.time())
|
|
||||||
attempt = payload.get("attempt", 0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
process_order(payload)
|
|
||||||
dedup.mark(payload["idempotencyKey"])
|
|
||||||
broker.ack(message)
|
|
||||||
|
|
||||||
except TransientError as exc:
|
|
||||||
# P6: if the retry window is exhausted, route to DLQ;
|
|
||||||
# otherwise redeliver with exponential backoff.
|
|
||||||
if time.time() - first_attempt_ts > MAX_RETRY_TTL_SECONDS:
|
|
||||||
route_to_dlq(broker, message, exc, kind="dlq") # P5
|
|
||||||
broker.ack(message)
|
|
||||||
else:
|
|
||||||
broker.nack(message, delay=backoff(attempt))
|
|
||||||
```
|
|
||||||
|
|
||||||
## The Scenario
|
|
||||||
|
|
||||||
An orders queue delivers `order.created` events to the consumer at
|
|
||||||
**at-least-once** (the declared semantic, P4). The broker redelivers
|
|
||||||
on consumer crash or ack-timeout. Three things happen:
|
|
||||||
|
|
||||||
1. **Normal delivery** — the consumer dedups by `idempotencyKey`,
|
|
||||||
processes, marks, acks.
|
|
||||||
2. **Redelivery after a crash before ack** — the consumer crashed
|
|
||||||
after `mark` but before `ack`. The broker redelivers; `seen()`
|
|
||||||
returns true; the consumer acks without re-processing (P3).
|
|
||||||
3. **Poison message** — a malformed JSON body. The consumer routes
|
|
||||||
it to the poison queue immediately (no retry will fix a parse
|
|
||||||
error), acks the origin, and emits a `poison.depth` metric. The
|
|
||||||
operator is paged on poison-queue growth (P10).
|
|
||||||
|
|
||||||
A transient downstream failure (the payments API is briefly 503)
|
|
||||||
retries with exponential backoff for 30 minutes (P6); if it exceeds
|
|
||||||
the budget, the message routes to the DLQ with `reason`,
|
|
||||||
`redeliveryCount`, and `deadLetteredAt` — auditable, drainable,
|
|
||||||
observable (P5). The DLQ depth metric alerts the operator; the DLQ
|
|
||||||
entry's audit metadata lets the operator replay after the bug is
|
|
||||||
fixed (P5 reversibility).
|
|
||||||
|
|
||||||
## Principles Demonstrated
|
|
||||||
|
|
||||||
### Consumers are Idempotent (Messaging P3, C1)
|
|
||||||
- The consumer dedups by idempotency key before processing. A
|
|
||||||
redelivered message is a no-op, not a double-apply. The
|
|
||||||
`process → mark → ack` order gives at-least-once + idempotent
|
|
||||||
dedup; for a non-idempotent process, `process + mark` are one DB
|
|
||||||
transaction (exactly-once via idempotency, P4).
|
|
||||||
- See `domains/messaging/delivery-semantics.md` (idempotency-key
|
|
||||||
dedup store) and `domains/messaging/first-principles.md` P3.
|
|
||||||
|
|
||||||
### Delivery Semantics are Explicit (Messaging P4, C1, C2)
|
|
||||||
- The channel is declared **at-least-once + idempotent consumer** —
|
|
||||||
the engineering practice that collapses to exactly-once under
|
|
||||||
correct dedup (P3). The semantic is not emergent; it is the
|
|
||||||
declared choice per channel. The tradeoff (dedup-store cost,
|
|
||||||
transactional-process complexity) is conscious and documented.
|
|
||||||
- See `domains/messaging/queues.md` (the three-semantics comparison
|
|
||||||
table) and `domains/messaging/delivery-semantics.md` (exactly-
|
|
||||||
once via idempotency).
|
|
||||||
|
|
||||||
### Dead-Letter Handling is Defined (Messaging P5, C1, C5)
|
|
||||||
- Poison messages (unparseable) route immediately to the poison
|
|
||||||
queue — no retry will fix them. Transient failures retry with
|
|
||||||
backoff until the TTL, then route to the DLQ. Both carry audit
|
|
||||||
metadata (`reason`, `redeliveryCount`, `deadLetteredAt`); both are
|
|
||||||
drainable and observable. The DLQ is the reversibility mechanism
|
|
||||||
— a dead-lettered message can be reprocessed after the bug is
|
|
||||||
fixed.
|
|
||||||
- See `domains/messaging/delivery-semantics.md` (dead-letter
|
|
||||||
strategy comparison table, DLQ routing rule) and
|
|
||||||
`domains/messaging/first-principles.md` P5.
|
|
||||||
|
|
||||||
### Messaging is Observable (Messaging P10, C7, C1)
|
|
||||||
- DLQ depth and poison-queue depth are emitted as metrics and wired
|
|
||||||
to alerts. A DLQ that grows silently is a correctness defect;
|
|
||||||
the alert makes it visible. Silent backlog is a bug, not a
|
|
||||||
feature — the operator is paged on growth, not on a customer
|
|
||||||
report.
|
|
||||||
- See `domains/messaging/delivery-semantics.md` (DLQ depth as an
|
|
||||||
alert) and `domains/messaging/first-principles.md` P10.
|
|
||||||
|
|
||||||
## Cross-Domain Links
|
|
||||||
|
|
||||||
- `domains/messaging/delivery-semantics.md` — the idempotency-key
|
|
||||||
dedup-store pattern and the dead-letter strategy comparison table
|
|
||||||
exercised here (TTL-with-backoff + poison-queue + DLQ + alert).
|
|
||||||
- `domains/messaging/queues.md` — the three-semantics comparison
|
|
||||||
table; the ack/nack/redelivery model this consumer uses.
|
|
||||||
- `domains/messaging/first-principles.md` — P3, P4, P5, P10 are the
|
|
||||||
principles demonstrated.
|
|
||||||
- `domains/concurrency/patterns` — the in-process retry/backoff
|
|
||||||
analog (Pattern 6, Timeout on Every Block); messaging owns the
|
|
||||||
broker-backed instance where redelivery comes across a network.
|
|
||||||
- `domains/errors/patterns` — errors as data: a DLQ entry is the
|
|
||||||
async-messaging instance of an error log (observable, auditable,
|
|
||||||
drainable).
|
|
||||||
- `domains/observability/metrics` — the generic SLI/SLO discipline
|
|
||||||
the DLQ-depth alert builds on.
|
|
||||||
- `review/anti-patterns.md` — the `messaging-unbounded-retry` and
|
|
||||||
`messaging-shared-subscription` chaos anti-patterns are the
|
|
||||||
inverse of this example's bounded retry + independent-consumer
|
|
||||||
discipline.
|
|
||||||
Reference in New Issue
Block a user