Teardown · Factory v2
The Bugzilla teardown
A 25-year-old open-source codebase, run through the Greenfield Production System end to end, with the output published here. Bugzilla's source is public, so every citation on this page points at code anyone can read.
- Bounded contexts
- 7
- Cross-service events
- 18
- Services built
- 5
01 · Selection
Why Bugzilla
Client estates are confidential by default, which rules them out as public demonstrations. Bugzilla solves that. First released in 1998 and written in Perl, it's still running real installations, and it's gnarly in the specific way 25-year-old production code is gnarly: behavior accreted across decades, enforced partly in CGI scripts and partly deep in module internals.
It's also recognizable. Most engineering leaders have used it or have inherited something shaped like it. Because the source is open, there's no confidentiality screen between you and the evidence: clone the original and check the cited files yourself.
02 · Discovery
What discovery found
Before anything was built, the discovery track read the Bugzilla source and mapped it: 7 bounded contexts and 18 cross-service events. Five of the contexts became services; two were deliberately left behind, and the honest section says why.
-
Rebuilt as the user service
Users & groups
Accounts, login validation, group membership, permission grants.
-
Rebuilt as the product service
Products & components
Products, components, versions, milestones, default assignees.
-
Rebuilt as the bug service
Bug lifecycle
Filing, triage, the status workflow, comments, duplicates, CC lists.
-
Rebuilt as the attachment service
Attachments & flags
Uploads, size limits, obsoletion, review-flag requests.
-
Rebuilt as the notification service
Notifications
Change mail, recipient resolution, self-mail suppression.
-
Not rebuilt
Search & reporting
Stored queries, charts, tabular reports. Found in discovery; not rebuilt.
-
Not rebuilt
Administration & parameters
Installation parameters, custom fields, sanity checks. Found in discovery; not rebuilt.
| Event | Published by | Consumed by |
|---|---|---|
| user.Events.UserCreated | Users & groups | Notifications |
| user.Events.UserDisabled | Users & groups | Bug lifecycle, Attachments & flags, Notifications |
| user.Events.UserAddedToGroup | Users & groups | Bug lifecycle |
| product.Events.ProductCreated | Products & components | Bug lifecycle |
| product.Events.ComponentCreated | Products & components | Bug lifecycle |
| product.Events.ComponentUpdated | Products & components | Bug lifecycle |
| product.Events.GroupControlsChanged | Products & components | Bug lifecycle |
| bug.Events.BugCreated | Bug lifecycle | Notifications |
| bug.Events.BugStatusChanged | Bug lifecycle | Notifications |
| bug.Events.BugMarkedDuplicate | Bug lifecycle | Notifications |
| bug.Events.CommentAdded | Bug lifecycle | Notifications |
| bug.Events.BugFieldUpdated | Bug lifecycle | Notifications |
| bug.Events.BugGroupAdded | Bug lifecycle | Notifications |
| bug.Events.BugMovedToProduct | Bug lifecycle | Attachments & flags, Notifications |
| attachment.Events.AttachmentCreated | Attachments & flags | Bug lifecycle, Notifications |
| attachment.Events.AttachmentMarkedObsolete | Attachments & flags | Notifications |
| attachment.Events.FlagRequested | Attachments & flags | Notifications |
| attachment.Events.FlagGranted | Attachments & flags | Notifications |
03 · Build
What was built
Five services: user, product, bug, attachment, and notification. Each follows the same internal structure, with typed contracts at the edge, commands and queries behind them, aggregates holding the rules, and read models serving the views.
services/service-bug
├── packages/contracts/ command, query, and event schemas, typed at the edge
├── service/src/commands/ create-bug · assign-bug · update-bug-status
│ mark-bug-duplicate · set-bug-resolution · add-comment
├── service/src/queries/ get-bug · get-bug-history · get-bug-comments
├── service/src/domain/ BugAggregate · WorkflowConfigAggregate
└── service/src/read-models/ bug-detail · bug-activity · bug-dependency A web frontend was built against the same contracts, and end-to-end journeys exercise the rebuilt stack the way a user would: filing a bug, triaging and assigning it, resolving and verifying, attaching a patch and requesting review, marking a duplicate.
The run is versioned in the factory's release notes as v2: the port ran end to end through roughly 30 stations and 40 gates, without an architect reviewing between them.
04 · Behavior catalog
The catalog, browsable
This is the artifact the rest of the teardown hangs off. Every behavior carries typed semantics, a provenance citation into Bugzilla's source, and its epistemic tier. Filter the excerpt by context or kind; the full catalog ships with engagements, and the catalog sample page shows the same rows with their migration decisions and the enforcement matrix.
20 of 20 behaviors shown
| ID | Behavior | Kind | Result | Provenance | Tier |
|---|---|---|---|---|---|
| attachment.create-attachment.happy-path | Authenticated user who can see the bug and has product edit access uploads a file attachment | happy-path | emits AttachmentCreated | Bugzilla/Attachment.pm Attachment creation and field validation | Traced in source Read from code, not yet run against a live system. |
| attachment.create-attachment.empty-description | Attachment creation requires a non-empty description | validation | rejects · MISSING_ATTACHMENT_DESCRIPTION | Bugzilla/Attachment.pm ATT-DR-7 — description must be non-empty | Traced in source Read from code, not yet run against a live system. |
| attachment.create-attachment.authorization-denied | User without permission to see the bug or edit the product is denied attachment creation | authorization | rejects · PERMISSION_DENIED | Bugzilla/Attachment.pm ATT-DR-1 — requires can_see_bug and can_edit_product | Traced in source Read from code, not yet run against a live system. |
| attachment.create-attachment.patch-forces-text-plain | When isPatch is true, the MIME type is forced to text/plain regardless of user input | business-rule | emits AttachmentCreated | Bugzilla/Attachment.pm ATT-DR-4 — patch attachments force text/plain | Traced in source Read from code, not yet run against a live system. |
| bug.add-bug-group.happy-path | User with editbugs/assignee/QA role adds a group restriction to a bug | happy-path | emits BugGroupAdded | Bugzilla/Bug.pm AddBugGroup command - group restriction addition | Traced in source Read from code, not yet run against a live system. |
| bug.add-bug-group.invalid-group-for-product | Cannot add a group that is not valid for the bug's product | validation | rejects · INVALID_GROUP | Bugzilla/Bug.pm Group validation via GroupControlMapReadModel | Traced in source Read from code, not yet run against a live system. |
| bug.add-bug-group.authorization-denied | User without editbugs/assignee/QA role cannot add group restrictions | authorization | rejects · PERMISSION_DENIED | Bugzilla/Bug.pm AddBugGroup requires editbugs/assignee/QA role | Traced in source Read from code, not yet run against a live system. |
| bug.add-bug-group.already-member | Adding a group the bug already has is a no-op or returns success idempotently | business-rule | returns success without duplicating the group | Bugzilla/Bug.pm Group addition idempotency | Traced in source Read from code, not yet run against a live system. |
| notification.get-notification-preferences.happy-path | An authenticated user retrieves their own notification preference matrix, including email-disabled flag, ignored bug IDs, and the full role × change-type preference matrix | happy-path | returns the preference matrix | Bugzilla/BugMail.pm Notification preference resolution for bugmail recipients | Traced in source Read from code, not yet run against a live system. |
| notification.get-notification-log.filter-by-date-range | Notification log query supports optional date range filtering to narrow results to a specific time window | validation | returns records within the date range | Bugzilla/BugMail.pm Notification log filtering by date | Traced in source Read from code, not yet run against a live system. |
| notification.get-notification-preferences.unauthorized | A user without notifications:read permission is denied access to notification preferences | authorization | rejects · PERMISSION_DENIED | Bugzilla/User.pm Authorization gate for user notification preferences | Traced in source Read from code, not yet run against a live system. |
| notification.get-notification-preferences.owns-preferences-policy | A user can only retrieve their own notification preferences; attempting to read another user's preferences is denied by OwnsPreferencesPolicy | business-rule | rejects · PERMISSION_DENIED | Bugzilla/User.pm OwnsPreferencesPolicy — users can only read their own preferences | Traced in source Read from code, not yet run against a live system. |
| product.create-component.happy-path | A user with components:manage permission successfully creates a new component under a product | happy-path | emits ComponentCreated | Bugzilla/Component.pm Component creation logic and required fields | Traced in source Read from code, not yet run against a live system. |
| product.create-component.name-required | Component creation fails when name is empty or blank | validation | rejects · component_blank_name | Bugzilla/Component.pm _check_name validator rejects empty strings with component_blank_name | Traced in source Read from code, not yet run against a live system. |
| product.create-component.unauthorized-caller | Component creation is denied when caller lacks components:manage permission | authorization | rejects · PERMISSION_DENIED | Bugzilla/User.pm check_can_admin_product method gates component management | Traced in source Read from code, not yet run against a live system. |
| product.create-component.name-unique-per-product | Component creation fails when name already exists within the same product | business-rule | rejects · component_name_not_unique | Bugzilla/Component.pm _check_name validator checks uniqueness within product via name collision detection | Traced in source Read from code, not yet run against a live system. |
| user.create-user.happy-path | Admin creates a new user account with a unique email and valid password | happy-path | emits UserCreated | Bugzilla/User.pm User creation logic, email uniqueness validation, password complexity check | Traced in source Read from code, not yet run against a live system. |
| user.create-user.email-not-unique | Cannot create user with an email that already belongs to an active user | validation | rejects · email_already_exists | Bugzilla/User.pm Email uniqueness validation on create — INV-1 | Traced in source Read from code, not yet run against a live system. |
| user.create-user.unauthorized | User without users:create permission cannot create users | authorization | rejects · PERMISSION_DENIED | Bugzilla/User.pm Layer 1 permission: users:create required on CreateUser command | Traced in source Read from code, not yet run against a live system. |
| user.update-user-profile.disabled-user-self-modify | Disabled users cannot update their own profile through self-service | business-rule | rejects · account_disabled | Bugzilla/User.pm INV-6: Disabled accounts cannot self-modify — admin override allowed | Traced in source Read from code, not yet run against a live system. |
05 · Gates
What the gates caught
Work that fails a gate doesn't move forward: it's rejected with the reason, fixed, and resubmitted, and the transcript keeps all of it. Two excerpts from the Bugzilla run follow, failures included, because a transcript with no red entries reads as theater.
- handler-casing det ✓ pass 102 ms
- mapfromevent-namespace det ✕ fail 388 ms33 @MapFromEvent calls use short event names; expected fully-qualified bug.Events.*Annotation: The read-model gate rejects short event names: a handler that subscribes to
BugAssignedinstead ofbug.Events.BugAssignedwould never catch up, so it's a hard failure, not a warning. - mapfromevent-namespace det ✓ pass 391 msre-run after namespacing; all 41 @MapFromEvent calls fully qualified
- subscription-events-exist det ✓ pass 96 ms
- judge-score llm ✓ pass 8.4 srubric 94/100
- scenarios-typecheck det ✓ pass 6.2 s
- assertion-floor det ✕ fail 540 msscenario asserts only success:true; below the floor for a business-rule behaviorAnnotation: The floor rejects a test that only checks the command returned success; a business rule has to assert the emitted event or the rejection code it claims.
- assertion-floor det ✓ pass 530 msre-run after asserting the emitted event and the rejection code
- no-implement-todos det ✓ pass 88 ms
- scenario-scoped-ids det ✓ pass 120 ms
A full run's transcript, annotated gate by gate, is at /proof/transcript.
06 · Boundary
What wasn't built
A teardown that only lists what worked is advertising. This one diverges from upstream Bugzilla in ways worth stating plainly.
-
Search and reporting weren't rebuilt
Bugzilla's stored queries, charting, and tabular reports stayed behind. List views in the rebuild are read models projected from the catalog, not a port of the query engine.
-
Administration collapsed into configuration
Installation parameters became service configuration rather than a rebuilt admin context. Behaviors keyed to a parameter were either fixed one way or excised, and each decision is recorded in the catalog's migration column.
-
Mail delivery stays a probe candidate
The notification service consumes the events and resolves recipients, but delivery against live mail infrastructure hasn't run, so those behaviors keep their probe candidate tag instead of being promoted.
-
Scheduled reminder mail was excised
Bugzilla's "whining" feature (cron-driven nag mail) was excised rather than ported; it is cataloged, with the excision recorded as its migration decision.