feat: Phase 3 leftovers — Saga kind, catalog smoke, Cycle card#4
Conversation
…DR 0008 The narrated half of the two-family model (Phase 3 leftover). Adds a custom `Saga` kind — a thin, Git-backed index entity for an authored After-Action-Report: - `SagaProcessor` (new-backend-system catalog module) validates `spec.skald` (author, a User ref), `spec.timeframe`, non-empty `spec.touches[]` (entity refs it narrates), optional `spec.owner`, and the required `siliconsaga.org/saga-doc` annotation (path to the narrative). Emits built-in relations: skald/owner → ownedBy/ownerOf, touches[] → dependsOn/dependencyOf. - Seed: a `saga-soccer-2026-spring` example over the MTL season, with its narrative body at `examples/sagas/soccer-2026-spring.md` (Git-backed — Backstage indexes it, the DB never holds unique data). Allowed the `Saga` kind on the mtl.yaml location. - ADR 0008 records the decision. Deferred (Phase 4+): the authoring UX (catalog-backed touches pickers) and blog/TechDocs rendering of the narrative — this is the catalog kind only. Verified: unit tests 7/7, `make ci` green (units 16/16, envelope 23/23), and a headless boot-smoke confirms the Saga ingests at runtime with all relations. Co-Authored-By: Cervator <cervator@gmail.com>
The committed, real end-to-end ingestion test for the custom kinds — the piece the design tracked as "a real startTestBackend catalog test" to supersede the source-assertion BDD smokes. Realized as a headless smoke script (the idiomatic pattern here — the committed counterpart to smoke-gitea, and what the design called startTestBackend's "live cousin"). A pure jest startTestBackend for catalog *location processing* is awkward and flaky: the catalog ingests locations asynchronously on a scheduler, so a test polls the API anyway — which is exactly what this script does, without the in-process-backend fragility. `make smoke-catalog` boots the backend headlessly in stub mode (no cluster, no secrets), waits for the MTL seed to process, and asserts via the catalog API that `Cycle` and `Saga` ingest WITH their emitted relations (partOf/ownedBy/dependsOn), then tears the backend down. Safe to run anywhere, including CI. Verified PASS. Co-Authored-By: Cervator <cervator@gmail.com>
…nsion)
The last Phase 3 leftover: a curated entity view for the `Cycle` kind, replacing
the generic default page with something that surfaces the fields that matter.
New-frontend-system extension (mirrors the existing nav module): an
`EntityCardBlueprint` info-card gated by `filter: { kind: 'cycle' }`, wired via a
`cycleModule` added to `createApp({ features })`. It attaches to the catalog
default Overview automatically. The card (`CycleCard.tsx`, `useEntity` +
core-components `InfoCard`/`StructuredMetadataTable`) shows the Cycle's type,
timeframe (start → end), what it is `of`, and where it `happensAt`.
Adds `@backstage/plugin-catalog-react` (^3.1.0) and `@backstage/catalog-model` to
packages/app (were transitive-only).
Verified headlessly: `tsc` clean; `make ci` green (lint + `App.test.tsx` renders
the app with the module registered). The visual render is a `make dev` eyeball —
the card only appears on `kind: Cycle` entities (e.g. soccer-2026-spring).
Co-Authored-By: Cervator <cervator@gmail.com>
|
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 (3)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis PR adds a Saga catalog kind with backend validation, relation emission, catalog module wiring, example content, documentation updates, a frontend Cycle overview card, and a new smoke-catalog verification path. ChangesSaga catalog kind and supporting changes
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Catalog
participant SagaProcessor
participant Entity
Catalog->>SagaProcessor: validateEntityKind(entity)
SagaProcessor->>Entity: check spec.skald, spec.touches, saga-doc annotation
SagaProcessor-->>Catalog: throw error or return true
Catalog->>SagaProcessor: postProcessEntity(entity)
SagaProcessor->>SagaProcessor: parse skald/owner/touches refs
SagaProcessor-->>Catalog: emit ownedBy/ownerOf, dependsOn/dependencyOf relations
sequenceDiagram
participant Makefile
participant SmokeScript
participant Backend
participant CatalogAPI
Makefile->>SmokeScript: run smoke-catalog.sh
SmokeScript->>Backend: start in stub mode with static token config
SmokeScript->>Backend: poll .dev/backend.log for "Listening on"
SmokeScript->>CatalogAPI: GET entities/by-name (Cycle, Saga, Group)
CatalogAPI-->>SmokeScript: entity JSON with relations
SmokeScript->>SmokeScript: run PASS/FAIL assertions
SmokeScript-->>Makefile: exit 0 (pass) or exit 1 (fail with log tail)
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: 6
🤖 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 `@Makefile`:
- Line 12: The Makefile’s ci composite target does not include smoke-catalog, so
the headless stub-mode verification for the new Saga/Cycle ingestion path is
skipped in CI. Update the ci target to invoke smoke-catalog alongside the
existing config-check, lint, test, and test-app steps, or split it into a
dedicated CI job if that better fits the pipeline. Use the existing
smoke-catalog target and the ci target definition as the symbols to update.
In `@packages/app/src/modules/cycle/CycleCard.tsx`:
- Around line 23-30: The CycleCard metadata is rendering entity refs as plain
strings instead of clickable catalog links. Update the rendering in CycleCard to
use EntityRefLink for spec.of and EntityRefLinks for each spec.happensAt entry,
replacing the raw values while keeping the same fallback behavior for missing
data. Use the existing metadata setup in CycleCard and the Backstage
catalog-react link components to show resolved, user-friendly entity names.
In `@packages/backend/src/modules/saga/SagaProcessor.test.ts`:
- Around line 31-65: Add a test in SagaProcessor.validateEntityKind coverage for
the new malformed entity-ref path: the current cases in SagaProcessor.test.ts
validate missing and empty fields but not the isParseableRef failure branch.
Extend the existing validateEntityKind describe block with a case that passes a
Saga whose spec.skald is an invalid ref string, and assert it rejects with the
specific spec.skald invalid entity ref error so the parsing logic is directly
covered.
In `@scripts/smoke-catalog.sh`:
- Line 12: The smoke-catalog script does not guard the directory change, so if
the cd in the script startup fails it will continue from the wrong working
directory. Update the shell setup around the existing cd command to fail fast on
directory-change errors, using the script’s top-level initialization logic so
the script exits immediately instead of proceeding with an invalid cwd.
- Around line 53-54: The byname() helper in the smoke-catalog script calls curl
without any timeout, so the polling loop can hang if the backend stalls. Update
the curl invocation in byname() to include both a connection timeout and an
overall max time, keeping the existing fallback behavior and hdr array usage
intact so the smoke-catalog loop respects its intended budget.
- Around line 69-88: The smoke test in the check block is short-circuiting
because the individual check calls are chained with &&, so a single failure
prevents later assertions from running. Update the sequence around check, CYCLE,
GROUP, and SAGA to execute each check unconditionally while still tracking
whether any failed, and only set pass=1 if all required checks succeed. Keep the
PASS/FAIL reporting inside check so every assertion prints its result.
🪄 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: 253fd368-39d7-4747-88d1-1850222daff8
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (16)
Makefileapp-config.yamldocs/adrs/0008-saga-git-backed-kind.mddocs/adrs/README.mddocs/development/testing.mdexamples/mtl.yamlexamples/sagas/soccer-2026-spring.mdpackages/app/package.jsonpackages/app/src/App.tsxpackages/app/src/modules/cycle/CycleCard.tsxpackages/app/src/modules/cycle/index.tsxpackages/backend/src/index.tspackages/backend/src/modules/saga/SagaProcessor.test.tspackages/backend/src/modules/saga/SagaProcessor.tspackages/backend/src/modules/saga/catalogModuleSaga.tsscripts/smoke-catalog.sh
There was a problem hiding this comment.
Pull request overview
Completes Phase 3 “leftovers” by adding the narrated Saga catalog kind (Git-backed index entity), introducing a committed end-to-end stub-mode catalog ingestion smoke (make smoke-catalog), and adding a curated Cycle overview card to the new frontend system so kind: Cycle shows key fields on the entity Overview.
Changes:
- Add
SagaProcessor+ backend catalog module wiring, seed entity + ADR, and catalog rules/config updates to allow/ingestSaga. - Add
make smoke-catalog+scripts/smoke-catalog.shto boot the backend headlessly and assert Cycle/Saga ingestion via the catalog API. - Add a Cycle-specific overview card extension (new frontend system) and required app deps.
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Locks new app dependencies for catalog model/react usage. |
| scripts/smoke-catalog.sh | New stub-mode, headless backend boot + catalog API assertions for Cycle/Saga ingestion. |
| packages/backend/src/modules/saga/SagaProcessor.ts | Implements Saga kind validation + relation emission. |
| packages/backend/src/modules/saga/SagaProcessor.test.ts | Unit tests for SagaProcessor validation and emitted relations. |
| packages/backend/src/modules/saga/catalogModuleSaga.ts | Registers SagaProcessor via the catalog processing extension point. |
| packages/backend/src/index.ts | Wires the saga backend module into the backend runtime. |
| packages/app/src/modules/cycle/index.tsx | Registers a Cycle-only overview card extension via EntityCardBlueprint. |
| packages/app/src/modules/cycle/CycleCard.tsx | Implements the curated Cycle overview card UI. |
| packages/app/src/App.tsx | Adds the cycle frontend module to app features. |
| packages/app/package.json | Adds required dependencies for the new cycle module implementation. |
| Makefile | Adds smoke-catalog target. |
| examples/sagas/soccer-2026-spring.md | Adds example narrative body referenced by the Saga entity annotation. |
| examples/mtl.yaml | Seeds a Saga entity referencing the narrative and touches relations. |
| docs/development/testing.md | Documents the new real-ingestion smoke and test layout updates. |
| docs/adrs/README.md | Adds ADR 0008 to the index. |
| docs/adrs/0008-saga-git-backed-kind.md | Records the Saga-kind decision and scope. |
| app-config.yaml | Allows Saga in the example location’s catalog rules. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Two small gate improvements so a single `make ci` (and, once the realm adapter points at it, `ws test leidangr`) is a reliable full local gate: - `test-app` now runs with `CI=true`, forcing backstage-cli repo test to a single run. Without it, backstage-cli watches when attached to a TTY and never exits — which would hang `make ci` / `ws test` in an interactive terminal. - Added a `tsc` target and folded it into `ci`, so the gate also typechecks (catches wiring/import errors the linter misses — the class of thing the Phase 3 module work leaned on `tsc` for). `ci` is now: config-check → lint → tsc → test (envelope) → test-app. Verified it runs green and exits cleanly without an external `CI=true`. Co-Authored-By: Cervator <cervator@gmail.com>
…ardening Addresses the CR #4 review (5 of 6 findings; the 6th declined — see the thread). - CycleCard: render `spec.of` / `spec.happensAt` as clickable `EntityRefLink`(s) via `@backstage/plugin-catalog-react`, instead of raw ref strings. - SagaProcessor test: add a malformed-`skald`-ref case exercising the `isParseableRef` failure branch (parses fine for multi-colon strings, so the test uses an empty-name ref `user:default/` which parseEntityRef rejects). - smoke-catalog.sh: guard `cd` (SC2164, `|| exit 1`); add curl `--connect-timeout 3 --max-time 5` so a stalled backend can't hang the poll loop past its budget; run every check unconditionally with `|| pass=0` instead of `&&`-chaining, so one failure no longer hides the remaining assertions. Declined: adding `smoke-catalog` to the `ci` target — `ci` is the fast inner-loop gate (and, via the adapter, `ws test`); a ~40s backend boot belongs in a dedicated CI job, not the quick gate (CodeRabbit offered that alternative too). Verified: `make ci` green (23 envelope + 17 units incl. the new saga case, tsc, lint); `make smoke-catalog` PASS (Cycle + Saga ingest with all relations). Co-Authored-By: Cervator <cervator@gmail.com>
Copilot: the smoke asserted relations by grepping an ordered JSON substring (`"type":…,"targetRef":…`), which is brittle — a harmless serialization change in key order would fail the smoke even though the relation is present. Add a `check_rel` helper that parses the entity JSON with `jq` and asserts `(.relations // []) | any(.type == $t and .targetRef == $r)` — order-independent. Relation checks use it; single-field presence checks (kind, annotation, group type) keep the plain substring grep. Verified: `make smoke-catalog` still PASS. Co-Authored-By: Cervator <cervator@gmail.com>
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/app/src/modules/cycle/CycleCard.tsx`:
- Around line 31-36: `CycleCard` currently calls `parseEntityRef` directly for
`spec.of` and each entry in `spec.happensAt`, so malformed refs can throw and
break rendering instead of showing `—`. Update the `CycleCard` render path to
safely parse these values with a guard or try/catch helper, and only pass valid
refs into `EntityRefLink` and `EntityRefLinks`; if parsing fails or the value is
invalid, fall back to the existing dash placeholder.
🪄 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: 9f9f9c97-7b18-4db5-b1f6-599487ad08f3
📒 Files selected for processing (4)
Makefilepackages/app/src/modules/cycle/CycleCard.tsxpackages/backend/src/modules/saga/SagaProcessor.test.tsscripts/smoke-catalog.sh
…ke hardening Round-2 review (all 4 findings valid, all addressed): - CycleCard (CodeRabbit, Major): `parseEntityRef` throws on a malformed ref, which would break the card render. Added a `safeRef` try/catch helper; `spec.of` and each `spec.happensAt` entry parse safely and fall back to the `—` placeholder if invalid, so only valid refs reach EntityRefLink(s). - testing.md (Copilot): the `ci`-gate doc now lists `tsc` (folded into `make ci` via the cherry-picked Makefile change). - smoke-catalog.sh (Copilot): `set -e` + a fail-fast prerequisite check (corepack/curl/jq), matching smoke-gitea; and `check()` now greps fixed-string (`-qF`) so metacharacters like the `.` in `siliconsaga.org/saga-doc` can't cause an imprecise match. Verified: `make ci` green (23 envelope + 17 units, tsc, lint); `make smoke-catalog` PASS (set -e guards all hold). Co-Authored-By: Cervator <cervator@gmail.com>
| # Fail fast if a prerequisite is missing rather than half-running. | ||
| for _cmd in corepack curl jq; do | ||
| command -v "$_cmd" >/dev/null 2>&1 || { echo "smoke-catalog: missing prerequisite '$_cmd'" >&2; exit 1; } | ||
| done |
| up="" | ||
| for _ in $(seq 1 150); do | ||
| if grep -q "Listening on" "$LOG" 2>/dev/null; then up=1; break; fi | ||
| if ! kill -0 "$PID" 2>/dev/null; then break; fi | ||
| sleep 1 | ||
| done |
| # Backend readiness != catalog-ingestion readiness. Poll until the custom entities | ||
| # appear (or the timeout expires) rather than sleeping once and querying once. | ||
| CYCLE='{}'; SAGA='{}'; GROUP='{}' | ||
| for _ in $(seq 1 120); do | ||
| CYCLE="$(byname cycle/default/soccer-2026-spring)" | ||
| SAGA="$(byname saga/default/saga-soccer-2026-spring)" | ||
| GROUP="$(byname group/default/mtl)" | ||
| if printf '%s' "$CYCLE" | grep -q 'soccer-2026-spring' \ | ||
| && printf '%s' "$SAGA" | grep -q 'saga-soccer-2026-spring' \ | ||
| && printf '%s' "$GROUP" | grep -q '"name":"mtl"'; then break; fi | ||
| sleep 1 | ||
| done |
Summary
Completes the Phase 3 leftovers (on top of the merged Cycle kind + MTL seed):
Sagacustom kind — the narrated half of the two-family model. A thin,Git-backed index entity for an authored After-Action-Report:
SagaProcessorvalidates
spec.skald(author),spec.timeframe, non-emptyspec.touches[],optional
spec.owner, and the requiredsiliconsaga.org/saga-docannotation;emits built-in relations (skald/owner → ownedBy, touches[] → dependsOn). Seed
saga-soccer-2026-springwith its narrative body inexamples/sagas/. ADR 0008.Authoring UI / blog-render remain deferred (Phase 4+).
make smoke-catalog— a committed, real end-to-end catalog-ingestion test(boots the backend headless in stub mode, asserts
Cycle+Sagaingest withtheir relations via the catalog API, tears down). The idiomatic realization of
the "real startTestBackend catalog test" leftover; supersedes the source-assertion
smokes for runtime coverage.
Cycleoverview card — a new-frontend-systemEntityCardBlueprintextension gated to
kind: cycle, showing type / timeframe / part-of / happens-atin place of the generic default page.
Test plan
make cigreen — config-check, lint, envelope BDD 23/23, app+backend units16/16 (incl. the new
SagaProcessorsuite and the app render with the Cyclemodule registered).
tscclean (backend module + frontend extension).make smoke-catalogPASS — Cycle + Saga ingest at runtime with all relations,no catalog errors.
soccer-2026-spring.Related
docs/plans/2026-07-06-leidangr-phase3-community-domain-{design,plan}.md.