A movie that exists in several imported Xtream playlists now shows a "Sources N" chip on its detail page and in the player. Switching playlist mid-film keeps the timecode, a preferred source can be pinned per movie, and a failed stream offers the alternatives instead of a dead end. The governing rule is that a guess is never presented as a fact. Every metadata value carries where it came from — `api` (the provider said so), `parsed` (inferred from the title) or `probe` (we contacted the stream). Facts render as plain tags, guesses are prefixed `~` in a warning colour, and an unknown value renders no tag at all plus a "check" affordance. Ranking and failover read through `factualOnly()`, so a filename claiming 4K is structurally unable to outrank a source that was actually reached. A probe that could not complete reports "unknown", never "unavailable". Scope is deliberately narrow: Xtream to Xtream, movies only, Electron only. Stalker never reaches the `content` table and M3U is a JSON blob whose search forces live content; both are additive later, since the candidate type already carries all three portal kinds. In the PWA every entry point is gated off and the chip renders nothing. Auto-failover is opt-in and off by default. Each source is tried at most once per session, so it terminates structurally, and the switch is never silent — the toast names the new playlist, offers an undo, and warns that the dub may differ only when both sides state an audio track as fact. Notable details: - Playlist names are routinely the pasted URL, credentials included. They are never rendered raw; a short host-only label is derived instead. - Quality is derived from pixel width, not height: a 2.39:1 1080p master is 1920x800, and bucketing that by height would publish "720p" as a fact. - Switching is a single `inlinePlayback.set()` so the player and engine survive and re-seek; the carried position is read before the 15s persistence throttle so it does not rewind. - Sources from one playlist collapse into a group, since the same film often appears there several times under different stream ids. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.3 KiB
Validation Map
This map records the lowest-cost validation commands agents should reach for before broad CI-sized runs.
Discovery
pnpm nx show projects --withTarget test
pnpm nx show projects --withTarget lint
pnpm nx show projects --withTarget e2e
Unit And Type Checks
| Area | Command |
|---|---|
| Angular renderer entry points | pnpm run typecheck:web |
| Electron main process entry points | pnpm run typecheck:backend |
| Full unit suite (all projects) | pnpm run test:unit:ci |
| EPG data access | pnpm nx test epg-data-access |
| Workspace shell utilities | pnpm nx test workspace-shell-util |
| Shared SQLite schema/connection | pnpm nx test database |
| Packaging metadata | pnpm nx test packaging |
Lint
pnpm run lint # nx run-many --target=lint --all
pnpm nx lint <project> # single project
The CI workflow (.github/workflows/ci.yml) lints affected projects on PRs
(nx affected) and every project on master pushes.
This enforces @nx/enforce-module-boundaries (scope/domain/type tag
constraints), the legacy bare-alias ban, and the max-lines file-size rule
(hard maximum 400 lines per TypeScript file). Files that predate the
max-lines rule are baselined in tools/eslint/max-lines-baseline.mjs; after
splitting a baselined file below the limit, regenerate the list with
node tools/eslint/generate-max-lines-baseline.mjs. Never add new files to
the baseline.
Coverage Tiers
Use tools/coverage/coverage-policy.json as the source of truth for coverage
ownership. Every project with a test target must be classified in a tier;
pnpm run coverage:policy:check (part of coverage:ci) fails CI when a new
project is missing from the policy, a listed project no longer exists, or a
Tier A entry has no test target. CI runs Tier A with coverage (uploaded to
Codecov) and each Tier B/C project's validationCommand (falling back to
nx test) without coverage; projects with an e2e target are skipped there
because the E2E workflow already runs them on every PR that touches app code.
Docs-only changes (Markdown, docs/, .plans/, .codex/, .claude/) and
apps/website/** changes skip the E2E workflow via paths-ignore — for those
PRs no E2E validation runs in CI, which is intentional: they cannot affect app
behavior.
Tier A coverage is fail-closed. coverage:unit:ci relays Jest output but exits
nonzero on a Failed to collect coverage marker, a missing or invalid project
report, or a runtime-owning production TypeScript file absent from that report.
coverage:merge requires every configured Tier A report before replacing the
merged output. Strict health validation also requires the merged Istanbul map
itself to contain usable instrumentation for every runtime-owning Tier A file,
recomputes its summary, and then applies aggregate and selected critical-file
ratchets.
Runtime-owning files are discovered from the TypeScript AST. Specs,
declarations, test setup and stubs, generated and environment files, index.ts,
type-only files, and pure re-export shims are excluded. Ratchets live under
reporting.coverageRatchet in tools/coverage/coverage-policy.json; update
them only from a fresh full coverage:ci report when every value stays level
or rises, and never lower one to accept a regression. The only exception is an
explicitly reviewed production source shrink: minimumCovered may follow a
lower total statement count when the PR documents the removed executable
statements and fresh coverage proves that the corresponding
minimumPercent, every aggregate ratchet, and the remaining behavioral
coverage do not decrease.
| Tier | Rule | Validation |
|---|---|---|
| A | Product/runtime Angular, Electron, backend, data-access, portal, playlist, workspace, playback, EPG, and shared UI code collects source coverage. | pnpm run coverage:ci |
| B | Validate behavior without percentage coverage, such as website, packaging, and Playwright E2E projects. |
pnpm nx test website, pnpm nx test packaging, or the closest E2E target |
| C | Excluded from the source coverage baseline, such as mock servers, test helper libraries, and untested feature shells. | Validate through dependent flows, or add focused tests when changing behavior directly |
apps/website is an Astro marketing site. Its useful signal is a successful
static build plus targeted output checks, not a merged code coverage percentage.
Projects with a test target but no specs, such as remote-control-web and
remote-control today, should not be in Tier A until focused specs exist.
For local coverage inspection:
pnpm run coverage:tools:test
pnpm run coverage:unit:ci
pnpm run coverage:merge
pnpm run coverage:health -- --require-report
The merged report is written to coverage/merged/ as HTML, LCOV, Cobertura,
and JSON summary output. CI uploads the merged Tier A report to Codecov with the
unit flag and keeps the HTML report as a GitHub artifact.
E2E
| Area | Command |
|---|---|
| Web app browser flows | pnpm nx run web-e2e:e2e -- --project=chromium |
| Electron flows | pnpm nx run electron-backend-e2e:e2e |
| VOD multi-source | pnpm nx run electron-backend-e2e:e2e-ci--src/vod-multi-source.e2e.ts |
Use atomized E2E targets when available, for example
pnpm nx run web-e2e:e2e-ci--src/xtream.e2e.ts.
Playwright coverage is measured semantically by tags and critical journeys, not
by a source-line percentage. E2E reports should use tags such as @critical,
@electron, @web, @xtream, @stalker, @m3u, @search, @epg,
@persistence, @settings, @pwa, and @self-hosted.
After an E2E run, generate the semantic summary with:
pnpm run coverage:e2e:summary
For local investigation only, Chromium browser V8 coverage can be explored with:
pnpm run coverage:e2e:v8:web
I18n
pnpm run i18n:check
The i18n check is non-mutating. It compares every locale file in
apps/web/src/assets/i18n/ against en.json and fails on missing or extra keys.
Identical English fallback values are reported as warnings by default; use
node tools/i18n/check-drift.mjs --fail-on-identical for a stricter translation
audit.
Logging
Runtime playback and EPG debug logs should use the existing logger or trace
helpers instead of unconditional console.log. Electron external-player traces
are gated by:
IPTVNATOR_TRACE_PLAYER=1 pnpm run serve:backend