feat: Phase 3 community domain — Cycle kind + MTL seed#3
Conversation
First slice of the custom `Cycle` catalog kind (Phase 3). Adds a `CycleProcessor`
implementing `CatalogProcessor.validateEntityKind`: it recognises `kind: Cycle`,
requires `spec.type`, `spec.of`, and `spec.timeframe.{start,end}`, and throws a
descriptive error on malformed entities (returning false for other kinds so it
stays out of their way).
Adds the backend deps needed to author a catalog module — `@backstage/backend-plugin-api`,
`@backstage/catalog-model`, `@backstage/plugin-catalog-node` (previously transitive).
`LocationSpec` is imported from `plugin-catalog-node` (which re-exports it), avoiding
an extra `plugin-catalog-common` dependency.
Relation emission and module registration follow in subsequent commits.
Co-Authored-By: Cervator <cervator@gmail.com>
Implements `CycleProcessor.postProcessEntity` to emit relations for `Cycle` entities, reusing built-in relation types so the catalog graph/relations cards integrate for free (rather than inventing new relation strings): - `spec.of` → `partOf` / `hasPart` (the Cycle is part of its league/app) - `spec.owner` → `ownedBy` / `ownerOf` - `spec.happensAt`→ `dependsOn` / `dependencyOf` (the field/resource it uses) Refs are parsed with sensible default kinds (`of`/`owner` → Group, `happensAt` → Resource). The test asserts the emitted relation types and endpoints; note `getCompoundEntityRef` preserves the entity's kind casing (`Cycle`) while `parseEntityRef` on the ref strings yields lowercase `group`/`resource` — the catalog normalises on read, so this is correct. Co-Authored-By: Cervator <cervator@gmail.com>
Wires the `CycleProcessor` into the catalog via a new-backend-system module
(`createBackendModule`, `pluginId: 'catalog'`) that adds the processor through
`catalogProcessingExtensionPoint`. Registered in the backend entrypoint with
`backend.add(import('./modules/cycle/catalogModuleCycle'))`.
Verified with `yarn tsc` (clean — confirms the module + LocationSpec import
resolve and the wiring is type-safe); the CycleProcessor unit tests remain green.
Runtime boot is verified in the manual step (make dev).
Co-Authored-By: Cervator <cervator@gmail.com>
Adds a hand-authored MTL community-domain seed and registers it as a catalog location. Shapes (identical to what the future TeamSnap entity provider will emit): - typed `Group` tree — organization → sport → division → team, with a small sport (basketball) whose team hangs directly (no division level); - asset graph — `Domain: mtl`, facilities as `System` + `Resource`(bookable-space), and a "real" software `System` (mtl-registration) with two `Component`s; - one `Cycle` (soccer-2026-spring, type season) wired to its league and field. The new location carries per-location rules allowing `Group, Domain, System, Resource, Component, Cycle` so the custom kind ingests. `config:check` passes. Co-Authored-By: Cervator <cervator@gmail.com>
Adds checkpoint-3 to the envelope BDD suite (jest-cucumber), mirroring the checkpoint-1/2 source-assertion style: asserts the MTL seed declares the org Group tree (organization → … → team), a season `Cycle` wired to its league (`of`) and field (`happensAt`), and that app-config allows the `Cycle` kind on the mtl.yaml location. `ws test leidangr` = 23/23 green. These are source-assertion smokes (per the design); a real startTestBackend catalog-ingestion test is deferred. Co-Authored-By: Cervator <cervator@gmail.com>
Closes out the Phase 3 build with docs + CI coverage. - `make ci` now also runs `test-app` (`backstage-cli repo test`), so the `CycleProcessor` unit tests are part of the gate — previously the gate ran only the envelope suite, which doesn't discover `packages/*`. - ADR 0007 records the `Cycle` decision (single custom kind, open `spec.type` vocabulary, reused built-in relation types; occurrences queried-not-minted; Saga/TeamSnap deferred), indexed in the ADR README. - testing.md notes the CycleProcessor units run under `make test-app` and that `make ci` runs the full gate. Verified: `make ci` green (config-check, lint, envelope 23/23, app+backend units). Co-Authored-By: Cervator <cervator@gmail.com>
The soccer-2026-spring Cycle's description sat in a flow mapping
(`{ name: …, description: MTL Soccer, Spring 2026 season }`) with an unquoted
comma, so YAML split it — the entity ingested with `description: "MTL Soccer"`
plus a bogus metadata key `"Spring 2026 season": null`. Quote the value.
Caught by a headless backend boot-smoke that asserts the seed ingests via the
catalog API (the source-assertion BDD checks the seed file text, not the
ingested description). Post-fix the entity ingests cleanly with its three
relations (partOf/ownedBy/dependsOn) and correct description.
Co-Authored-By: Cervator <cervator@gmail.com>
There was a problem hiding this comment.
Pull request overview
Implements Phase 3 of the community-domain model for this Backstage control-plane by introducing a custom Cycle catalog kind (validated + graph-wired via a catalog processor), seeding an MTL example hierarchy, and extending CI/testing/docs to cover the new behavior.
Changes:
- Added a
CycleProcessor(customCyclekind) and registered it via a new-backend-system catalog module. - Added an MTL multi-entity seed (
examples/mtl.yaml) + catalog location/rules to allowCycle. - Added unit tests for the processor and new envelope BDD acceptance checks; updated
make ciand docs/ADR to reflect the decision and test strategy.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Locks new Backstage deps needed for the Cycle catalog module/processor. |
| packages/backend/package.json | Adds runtime deps for backend module + catalog processor APIs. |
| packages/backend/src/index.ts | Registers the Cycle catalog backend module into the backend runtime. |
| packages/backend/src/modules/cycle/catalogModuleCycle.ts | New backend module wiring that installs CycleProcessor into catalog processing. |
| packages/backend/src/modules/cycle/CycleProcessor.ts | New catalog processor: validates Cycle required fields and emits built-in relations. |
| packages/backend/src/modules/cycle/CycleProcessor.test.ts | Unit tests for Cycle validation and relation emission. |
| app-config.yaml | Registers the MTL seed as a catalog location and allows Cycle kind for that location. |
| examples/mtl.yaml | Adds Phase 3 seed: Group tree + facilities/resources + real software + one Cycle. |
| tests/acceptance/checkpoint-3-community-domain.feature | New BDD feature describing Phase 3 seed expectations. |
| tests/acceptance/checkpoint-3.steps.ts | Step implementations for the Phase 3 seed contract checks. |
| Makefile | Expands make ci gate to include test-app (app/backend unit tests). |
| docs/development/testing.md | Documents that make test-app includes backend module unit tests and make ci runs both suites. |
| docs/adrs/README.md | Indexes new ADR 0007. |
| docs/adrs/0007-cycle-custom-kind.md | New ADR capturing the Cycle-kind design decision and consequences. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@CodeRabbit full review |
✅ Action performedFull review finished. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis PR adds a backend ChangesCycle catalog kind implementation
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/backend/src/modules/cycle/CycleProcessor.ts`:
- Around line 41-56: Update validateEntityKind in CycleProcessor so spec.of,
spec.owner, and spec.happensAt are validated as proper entity refs instead of
only non-empty strings. Use parseEntityRef (or equivalent ref validation) inside
validateEntityKind to reject malformed refs early and add the failure to the
existing errors array, so invalid Cycle entities still fail with the processor’s
Invalid Cycle entity message rather than later in postProcessEntity.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1a3de906-cb8b-43de-b149-fbbec3810381
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (13)
Makefileapp-config.yamldocs/adrs/0007-cycle-custom-kind.mddocs/adrs/README.mddocs/development/testing.mdexamples/mtl.yamlpackages/backend/package.jsonpackages/backend/src/index.tspackages/backend/src/modules/cycle/CycleProcessor.test.tspackages/backend/src/modules/cycle/CycleProcessor.tspackages/backend/src/modules/cycle/catalogModuleCycle.tstests/acceptance/checkpoint-3-community-domain.featuretests/acceptance/checkpoint-3.steps.ts
…, scope BDD assertions Addresses CR #3 review (all findings valid). CycleProcessor.validateEntityKind: - CodeRabbit: validate `spec.of`/`spec.owner`/`spec.happensAt` as parseable entity refs (via parseEntityRef in a helper), so malformed refs fail early with the processor's "Invalid Cycle entity …" message instead of a generic error later in postProcessEntity. - Copilot: require non-empty `spec.timeframe.start`/`end` (previously an empty string passed, unlike type/of). Added unit tests for both. checkpoint-3 BDD steps: - Copilot: import `readFileSync` from `node:fs` to match the other step files. - Copilot: scope the Group and Cycle assertions to a single YAML document (found by kind + name) instead of matching name/type independently across the file, and escape interpolated regex values — removes false-positive risk as the seed grows. - Copilot: tie the app-config allow-rule check to the same location stanza that targets mtl.yaml, so the kind can't be satisfied by a different location's rules. Verified: `make ci` green (config-check, lint, envelope 23/23, app+backend units 9/9). Co-Authored-By: Cervator <cervator@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/backend/src/modules/cycle/CycleProcessor.ts`:
- Around line 29-37: The refs in CycleProcessor are parsed twice: once in
isParseableRef for validation and again in postProcessEntity when building
relations. Update the CycleProcessor flow so postProcessEntity can reuse the
parsed EntityRef values collected during validation of spec.of, spec.owner, and
spec.happensAt, or otherwise cache them instead of re-running parseEntityRef on
the same strings. Keep the existing symbols isParseableRef and postProcessEntity
as the main places to wire this reuse.
In `@tests/acceptance/checkpoint-3.steps.ts`:
- Around line 56-59: The current matcher in the checkpoint-3 step only accepts a
single inline happensAt entry, so it fails on valid YAML reformatting or
additional refs. Update the assertion inside the step definition that uses
cycleDoc! and toMatch so it matches the Cycle’s happensAt content more flexibly,
allowing multiline/reflowed arrays and extra entries while still confirming the
expected resource is present.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 41615de5-9b5a-4cfc-8e24-a330e6ba3c42
📒 Files selected for processing (3)
packages/backend/src/modules/cycle/CycleProcessor.test.tspackages/backend/src/modules/cycle/CycleProcessor.tstests/acceptance/checkpoint-3.steps.ts
CodeRabbit (Major): the `happensAt` assertion only matched a single inline `[resource:default/field-1]`, so a reflowed array or an added ref would fail the step even with valid Cycle data. Match the resource *within* the array (`\[[^\]]*\bresource:default/<res>\b[^\]]*\]`), tolerating extra entries. Declined the sibling nitpick (parse refs once in validation, reuse in postProcessEntity): validate and postProcess are separate processor lifecycle calls, so reuse would mean caching parsed refs across phases — added state on a stateless processor for an O(short-string) parse off any hot path. Explained on the thread. Co-Authored-By: Cervator <cervator@gmail.com>
Summary
Implements Phase 3 — community domain model (design + plan already on
main):Cyclecustom kind (apiVersion: siliconsaga.org/v1alpha1) — aCycleProcessor(
packages/backend/src/modules/cycle/) that validates the kind (spec.type/of/timeframerequired) and emits built-in relation types so the graph integrates for free:
spec.of→partOf/hasPart,spec.owner→ownedBy/ownerOf,spec.happensAt→dependsOn/dependencyOf.Wired via a new-backend-system catalog module (
catalogProcessingExtensionPoint).examples/mtl.yaml) — a typedGrouptree (org→sport→division→team, with adivisionless small sport), facilities as
System+Resource(bookable-space), a "real" softwareSystem, and one season
Cycle. Registered as a catalog location with a per-location ruleallowing the
Cyclekind.CycleProcessorunit suite (validation + relation emission) undermake test-app, and a BDD checkpoint-3 (jest-cucumber) source-assertion suite under theenvelope (
make test);make cinow runs both. Adds ADR 0007.Occurrences (matches, single events) are queried-not-minted;
ResourceTypedropped;Activitydissolved. The narrated
Sagakind and TeamSnap ingestion are designed-but-deferred.Test plan
make cigreen — config-check, lint, envelope BDD (23/23), app+backend units (7/7).tscclean.Cycleingests via the catalog API with all threerelations (
partOf/ownedBy/dependsOn) and theGrouptree present, no catalog errors.(This caught and fixed a YAML flow-comma bug in the Cycle description.)
make dev, filter catalog by kind →Cycle; opensoccer-2026-springand confirm its relations render; open Groupmtland see the tree.Related
main):docs/plans/2026-07-06-leidangr-phase3-community-domain-{design,plan}.md.ws test leidangrto the fullmake cigate.Sagakind, TeamSnap scrape→provider, curatedCycleentity page, realstartTestBackendcatalog-ingestion tests.