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.
18 of 18 behaviors shown
| ID | Behavior | Kind | Semantics | Provenance | Tier |
|---|---|---|---|---|---|
| bug.create-bug.missing-summary | A bug cannot be filed with an empty summary. | validation | cmd create-bug · require summary ≠ '' | Bugzilla/Bug.pm:1163 | Proven An artifact is on disk or a test ran green. |
| bug.create-bug.dependency-cycle | A bug's dependencies may not form a cycle. | business-rule | cmd create-bug · reject cycle(dependsOn) | Bugzilla/Bug.pm:1402 | Proven An artifact is on disk or a test ran green. |
| bug.assign-bug.strict-isolation | Under strict isolation, the assignee must have edit access to the bug's product. | business-rule | cmd assign-bug · require productAccess(assignee) when strict_isolation | Bugzilla/Bug.pm:2241 | Traced in source Read from code, not yet run against a live system. |
| bug.mark-bug-duplicate.missing-dup-id | Resolving a bug as a duplicate requires the id of the bug it duplicates. | validation | cmd mark-bug-duplicate · require dupId present | Bugzilla/Bug.pm:1488 | Proven An artifact is on disk or a test ran green. |
| bug.mark-bug-duplicate.auto-cc-reporter | Marking a bug as a duplicate adds its reporter to the target bug's CC list. | business-rule | on mark-bug-duplicate · effect target.cc += reporter | Bugzilla/Bug.pm:1530 | Traced in source Read from code, not yet run against a live system. |
| bug.update-bug-status.authorization-assignee | Only the assignee, QA contact, or an editbugs member may change a bug's status. | authorization | cmd update-bug-status · require actor ∈ {assignee, qaContact} ∨ editbugs | Bugzilla/Bug.pm:1377 | Traced in source Read from code, not yet run against a live system. |
| bug.update-bug-status.sideeffect-remaining-time | Closing a bug zeroes its remaining time. | state-transition | cmd update-bug-status(closed) · effect remainingTime → 0 | Bugzilla/Bug.pm:1455 | Proven An artifact is on disk or a test ran green. |
| bug.add-comment.body-max-length | A comment body may not exceed 65,535 characters. | validation | cmd add-comment · require len(body) ≤ 65535 | Bugzilla/Bug.pm:2840 | Proven An artifact is on disk or a test ran green. |
| user.create-user.invalid-email-regexp | A login name must match the system's email pattern. | validation | cmd create-user · require login ~ emailregexp | Bugzilla/User.pm:1684 | Proven An artifact is on disk or a test ran green. |
| user.update-user-profile.disabled-user-self-modify | A disabled user cannot edit their own profile. | business-rule | cmd update-user-profile · deny actor when disabled(actor) | Bugzilla/User.pm:2105 | Traced in source Read from code, not yet run against a live system. |
| product.create-component.assignee-required | A component cannot be created without a default assignee. | business-rule | cmd create-component · require defaultAssignee present | Bugzilla/Component.pm:142 | Proven An artifact is on disk or a test ran green. |
| product.create-component.name-unique-per-product | A component name must be unique within its product. | business-rule | cmd create-component · require unique(name) within product | Bugzilla/Component.pm:171 | Traced in source Read from code, not yet run against a live system. |
| attachment.create-attachment.file-too-large | An attachment larger than the configured limit is rejected. | validation | cmd create-attachment · require size ≤ maxattachmentsize | Bugzilla/Attachment.pm:512 | Proven An artifact is on disk or a test ran green. |
| attachment.create-attachment.non-insider-cannot-set-private | A non-insider user cannot create a private attachment. | authorization | cmd create-attachment · deny isPrivate when ¬insider(actor) | Bugzilla/Attachment.pm:540 | Traced in source Read from code, not yet run against a live system. |
| attachment.mark-attachment-obsolete.cancels-pending-flags | Obsoleting an attachment cancels its pending review flags. | business-rule | on mark-attachment-obsolete · effect flags(pending) → cancelled | Bugzilla/Flag.pm:611 | Traced in source Read from code, not yet run against a live system. |
| attachment.request-flag.type-not-requestable | A flag request is rejected when its flag type is not requestable. | validation | cmd request-flag · require flagType.isRequestable | Bugzilla/Flag.pm:388 | Traced in source Read from code, not yet run against a live system. |
| notification.get-notification-preferences.default-preferences | When a user has set no preferences, the system applies the defaults. | business-rule | query get-notification-preferences · default when none(prefs) | Bugzilla/BugMail.pm:204 | Traced in source Read from code, not yet run against a live system. |
| notification.send-test-notification.email-transport-not-configured | A test notification fails when no email transport is configured. | validation | cmd send-test-notification · require emailTransport configured | Bugzilla/BugMail.pm:331 | Probe candidate Needs a live environment to settle. Never presented as a confirmed defect. |
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.