* docs(skills): design implementation synchronization * docs(skills): plan implementation synchronization * fix(release): filter internal notes from public body * docs(release): synchronize release workflow guidance * fix(stalker): normalize catalog series flags * fix(stalker): preserve progress with scoped episode IDs * fix(playback): expose strict position persistence * docs(stalker): record series position compatibility * test(skills): validate repository skill contracts * fix(database): keep SQL trace values private * docs(skills): refresh Nx and SQLite ownership * docs(skills): align provider and UI guidance * docs(skills): tighten validated guidance * docs(release): require exact release pushes * style(electron): remove trailing blank line * fix(ci): classify repository skills coverage
74 KiB
Repository Skills and Implementation Synchronization Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Synchronize IPTVnator's eight repository skills with current implementation, filter internal notes from public releases, and fix Stalker series flag and playback-position identity defects without losing legacy progress.
Architecture: Keep the three workstreams independently testable: release output and guidance, provider-local Stalker compatibility, and repository skill/document maintenance. Release filtering is an additive public-extraction mode; Stalker compatibility uses series-scoped IDs plus an explicit legacy alias; mechanical skill constraints become a small dependency-free validator.
Tech Stack: Node.js node:test, GitHub Actions YAML, Angular 21 signals and standalone services, TypeScript, Jest, Nx, Markdown skills and architecture documentation.
Execution Order and Commit Boundaries
- Bootstrap the locked workspace.
- Add public release extraction.
- Wire the release workflow and guidance.
- Normalize Stalker series flags.
- Add series-scoped Stalker episode IDs.
- Reconcile and migrate legacy Stalker positions.
- Document the Stalker behavior and add its release note.
- Add mechanical repository-skill validation.
- Refresh Nx and SQLite skills/docs.
- Refresh theme, UI, Xtream, and Stalker skills/docs.
- Run skill application scenarios and the complete validation ladder.
Execute Tasks 1-11 strictly in numbered order in the shared worktree. Tasks 5
and 6 must use the same worker because Task 6 consumes Task 5's deliberately
uncommitted interface changes. Do not parallelize edits, tests, staging, or
commits: the workers share one filesystem, Git index, and HEAD, and Task 5's
temporary signature change can also invalidate project-wide checks. Read-only
audits and reviews may run in parallel, but each task's implementation and
commit must finish before the next begins.
Task 1: Bootstrap and establish the baseline
Files:
-
Verify:
package.json -
Verify:
pnpm-lock.yaml -
Verify:
docs/superpowers/specs/2026-07-29-repository-skills-implementation-sync-design.md -
Step 1: Confirm the intended branch and clean starting point
Run:
git branch --show-current
git status --short
Expected: branch is agent/skill-implementation-sync; status is clean.
- Step 2: Install the locked dependencies
Run:
pnpm install --frozen-lockfile
Expected: exit 0 and no change to pnpm-lock.yaml.
- Step 3: Verify Nx workspace discovery
Run:
pnpm nx show projects
Expected: output includes release-tools, portal-stalker-data-access,
portal-stalker-feature, shared-interfaces, web, and web-e2e.
- Step 4: Record fresh behavior baselines
Run:
node --test --test-reporter=tap tools/release/release-notes.test.mjs
pnpm exec jest \
--config libs/portal/stalker/data-access/jest.config.ts \
--runInBand \
libs/portal/stalker/data-access/src/lib/stalker-series.adapters.spec.ts
pnpm exec jest \
--config libs/portal/stalker/feature/jest.config.ts \
--runInBand \
libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.spec.ts
Expected: the existing tests pass before new red tests are introduced.
Task 2: Filter internal notes from the public release body
Files:
-
Modify:
tools/release/release-notes.test.mjs:21 -
Modify:
tools/release/release-notes.test.mjs:446 -
Modify:
tools/release/extract-changelog-section.mjs:27 -
Step 1: Hoist the shared fixture and add seven failing contracts
Move the existing const changelog = [...] fixture from inside
describe('extractSection') to module scope immediately before that suite, so
the existing and new suites share it. Add a module-level
internalOnlyChangelog fixture with the current generated internal block.
Import extractPublicSection, parseExtractArguments, and runExtractorCli
beside extractSection, then add four public-extraction tests:
describe('extractPublicSection', () => {
it('removes the generated internal block from a mixed release', () => {
assert.equal(
extractPublicSection(changelog, '0.24.0'),
['### Features', '', '- **playback** — Up Next rail.'].join('\n')
);
});
it('leaves a public-only release unchanged', () => {
const publicOnly =
'# 0.24.0 (2026-08-01)\n\n### Fixes\n\n- public fix';
assert.equal(
extractPublicSection(publicOnly, '0.24.0'),
'### Fixes\n\n- public fix'
);
});
it('returns an empty body for an internal-only release', () => {
assert.equal(
extractPublicSection(internalOnlyChangelog, '0.24.0'),
''
);
});
it('preserves unrelated details blocks', () => {
const source = [
'# 0.24.0 (2026-08-01)',
'',
'<details>',
'<summary>Migration guide</summary>',
'',
'Keep this text.',
'',
'</details>',
'',
'<details>',
'<summary>Internal changes</summary>',
'',
'- **deps** — parser bump.',
'',
'</details>',
].join('\n');
const result = extractPublicSection(source, '0.24.0');
assert.match(result, /<summary>Migration guide<\/summary>/);
assert.match(result, /Keep this text\./);
assert.doesNotMatch(result, /Internal changes|parser bump/);
});
});
Add direct CLI-contract coverage without spawning a process or reading the repository's real changelog:
describe('extract changelog CLI contract', () => {
it('parses --public before the version', () => {
assert.deepEqual(
parseExtractArguments(['--public', '0.24.0']),
{ version: '0.24.0', publicOnly: true }
);
});
it('rejects malformed, unknown, duplicate, or extra arguments', () => {
for (const args of [
[],
['0.24'],
['--unknown', '0.24.0'],
['--public', '--public', '0.24.0'],
['0.24.0', 'extra'],
]) {
assert.equal(parseExtractArguments(args), null);
const result = runExtractorCli(changelog, args);
assert.equal(result.exitCode, 2);
assert.equal(result.stdout, '');
assert.match(result.stderr, /Usage:/);
}
});
it('exits successfully with no output for an internal-only public body', () => {
assert.deepEqual(
runExtractorCli(
internalOnlyChangelog,
['--public', '0.24.0']
),
{ exitCode: 0, stdout: '', stderr: '' }
);
});
});
- Step 2: Run the test and verify RED
Run:
node --test tools/release/release-notes.test.mjs
Expected: FAIL because the module does not export the three new functions.
- Step 3: Implement exact-block public extraction
Add to extract-changelog-section.mjs:
const INTERNAL_DETAILS_BLOCK =
/(?:^|\n\n)<details>\n<summary>Internal changes<\/summary>\n\n[\s\S]*?\n\n<\/details>(?=\n\n|$)/g;
/**
* Extracts the authored public body while preserving the complete changelog.
*
* @param {string} changelog full CHANGELOG.md content
* @param {string} version bare semver
* @returns {string | null}
*/
export function extractPublicSection(changelog, version) {
const section = extractSection(changelog, version);
return section === null
? null
: section.replace(INTERNAL_DETAILS_BLOCK, '').trim();
}
Add pure argument and CLI-result boundaries:
const CLI_USAGE =
'Usage: extract-changelog-section.mjs [--public] <version>';
export function parseExtractArguments(args) {
const publicFlagCount = args.filter(
(argument) => argument === '--public'
).length;
const positional = args.filter(
(argument) => argument !== '--public'
);
if (
publicFlagCount > 1 ||
positional.length !== 1 ||
!/^\d+\.\d+\.\d+$/.test(positional[0])
) {
return null;
}
return {
version: positional[0],
publicOnly: publicFlagCount === 1,
};
}
Export runExtractorCli(changelog, args) returning
{ exitCode, stdout, stderr }. It must:
- return exit 2 plus
CLI_USAGEfor invalid arguments; - select
extractPublicSectiononly in public mode; - preserve the existing detailed missing-section diagnostic and exit 1;
- preserve the raw-mode empty-section diagnostic and exit 1; and
- return exit 0 with empty stdout/stderr for an internal-only public section.
Refactor main() into a thin adapter: read CHANGELOG.md, call
runExtractorCli(changelog, process.argv.slice(2)), write the returned stdout
and stderr to their matching streams, and assign process.exitCode. Keep the
existing import guard.
- Step 4: Run the test and verify GREEN
Run:
node --test --test-reporter=tap tools/release/release-notes.test.mjs
Expected: 44 tests pass, 0 fail.
- Step 5: Commit the public extractor
git add \
tools/release/extract-changelog-section.mjs \
tools/release/release-notes.test.mjs
git commit -m "fix(release): filter internal notes from public body"
Task 3: Wire public release extraction and synchronize release guidance
Files:
-
Modify:
tools/release/release-notes.test.mjs -
Modify:
tools/release/project.json -
Modify:
.github/workflows/build-and-make.yaml -
Modify:
.changes/README.md -
Modify:
.codex/skills/release-cut/SKILL.md -
Modify:
.claude/skills/release-cut/SKILL.md -
Modify:
.codex/skills/release-notes/SKILL.md -
Modify:
.claude/skills/release-notes/SKILL.md -
Modify:
AGENTS.md -
Modify:
CLAUDE.md -
Step 1: Add a failing workflow contract test
Add readFileSync to the existing node:fs import and add:
it('uses public extraction for authored tag-release text', () => {
const workflow = readFileSync(
new URL(
'../../.github/workflows/build-and-make.yaml',
import.meta.url
),
'utf8'
);
assert.match(
workflow,
/extract-changelog-section\.mjs --public "\$\{VERSION\}"/
);
});
- Step 2: Run the workflow contract test and verify RED
Run:
node --test tools/release/release-notes.test.mjs
Expected: FAIL because build-and-make.yaml does not pass --public.
- Step 3: Make the workflow request public output
Change the tag command to:
BODY="$(node tools/release/extract-changelog-section.mjs --public "${VERSION}")"
Update the nearby workflow comment to state that the complete changelog keeps
internal notes and an internal-only release intentionally has no authored
public text. Add this input to release-tools:test:
"{workspaceRoot}/.github/workflows/build-and-make.yaml"
- Step 4: Run the workflow contract test and verify GREEN
Run:
node --test --test-reporter=tap tools/release/release-notes.test.mjs
Expected: 45 tests pass, 0 fail.
- Step 5: Replace the release-notes skill and its mirror
Apply this exact content to both release-notes skill paths:
---
name: release-notes
description: "Use when a change may need a .changes note, the Release note gate fails, or deciding whether type: internal or no-release-note applies."
---
# Release Notes
Every user-visible change gets one direct `.changes/<area>-<slug>.md` file. The
area matches the conventional-commit scope; the body is present tense, user
language, one to three sentences, and at most 400 characters.
## Format
```markdown
---
type: fix
area: stalker
issues: [1234]
screenshot: optional-manifest-slug
---
Stalker series now resume the correct episode.
```
`type` is `breaking`, `feature`, `fix`, `perf`, or `internal`. Omit optional
fields instead of inventing values. Never add a version or PR number.
`internal` records invisible maintenance. It stays collapsed in `CHANGELOG.md`
but is omitted from the blog and the authored public GitHub body. GitHub's
generated commit list may still mention the underlying commits.
## Skip or Label
The gate auto-exempts website, E2E and mock-server apps, `*.spec.{js,ts}`,
`*.e2e.{js,ts}`, snapshots, any `/testing/` path, and Markdown. Other test-only,
docs, CI, workflow, or pure-refactor PRs use `no-release-note` when the gate
would otherwise require a note. At least one newly added direct
`.changes/*.md` file satisfies the gate.
## Verify
```bash
pnpm run release:notes:validate
```
Full format: `.changes/README.md`. Gate policy:
`tools/release/check-release-note-gate.mjs`.
The `.codex` and `.claude` copies of this skill must remain byte-identical.
- Step 6: Replace the release-cut skill and its mirror
Apply this exact content to both release-cut skill paths:
---
name: release-cut
description: Use when preparing, cutting, tagging, publishing, or verifying an IPTVnator release or its release assets.
---
# Release Cut
The tag build takes authored public text from the new CHANGELOG section. Keep
the full changelog committed before tagging.
## Preflight
Work from clean, current `master` with the intended remote named explicitly.
Confirm `package.json` contains bare semver, the exact `v<version>` tag does not
exist locally or remotely, CI is green, and all notes validate.
```bash
pnpm run release:notes:validate
pnpm run i18n:check
```
## Generate
1. Set `package.json.version`.
2. Run `pnpm run release:notes:changelog`.
3. Minor release: run `pnpm run release:notes:blog` and finish every editorial
field. Patch release: edit the existing `vX-Y` post; do not scaffold or
force-overwrite it.
4. Capture required manifest screenshots only against mock servers:
`pnpm nx run electron-backend:build-e2e`, then
`pnpm run release:screenshots`.
5. Consume notes only after reviewing all generated output:
`node tools/release/build-release-notes.mjs --consume`.
The consume command is the destructive boundary: it deletes the direct note
files. Stage only release-owned files, including exact website post/assets and
`git add -A -- .changes`, then commit and create the exact tag.
```bash
git commit -m "chore(release): v0.24.0"
git tag v0.24.0
```
## Push and External Effects
Push only the intended branch and tag; never use broad `git push --tags`.
```bash
git push --atomic origin \
HEAD:refs/heads/master \
refs/tags/v0.24.0
```
Master and `v*` pushes can publish Docker images. The tag build creates a draft
GitHub release. Verify authored text plus generated commits and all required
macOS, Windows, DEB, RPM, Pacman (`.pacman`/`.pkg.tar.*`), AppImage, Snap,
Flatpak, updater metadata, blockmaps, and
`linux-frame-copy-runtime-sources.tar.xz`.
After verification, manually publish the GitHub release. That publication
automatically verifies its Snap assets and uploads them to `edge`.
Installed-Snap smoke and candidate/stable promotion remain manual. Keep the
blog draft during artifact verification; publish it in a follow-up commit and
verify the website deployment.
## Failure Safety
Missing CHANGELOG section: regenerate, commit, delete the bad tag locally and
remotely only after resolving its exact target, then retag. Never publish a
draft until the source archive and Snap contract pass.
The `.codex` and `.claude` copies of this skill must remain byte-identical.
- Step 7: Synchronize canonical release documentation
Replace the internal paragraph in .changes/README.md with:
`type: internal` records invisible maintenance. Internal notes stay collapsed in
`CHANGELOG.md`, are omitted from the blog scaffold, and are removed from the
authored public GitHub body by
`extract-changelog-section.mjs --public`. GitHub's generated commit list remains
separate. An internal-only release can therefore have an empty authored body.
Replace its skip guidance with the exact gate behavior:
The gate auto-exempts website, E2E and mock-server apps, `*.spec.{js,ts}`,
`*.e2e.{js,ts}`, snapshots, any `/testing/` path, and Markdown. For other
test-only, documentation, CI/workflow, or pure-refactor changes under
`apps/`/`libs/`, apply `no-release-note` when no user-visible note is warranted.
Add the same public/internal distinction and automatic Snap edge/Docker side
effects to the mirrored release-policy sections in AGENTS.md and CLAUDE.md.
Keep their process bullets textually synchronized.
- Step 8: Verify release guidance and mirrors
Run:
cmp -s \
.codex/skills/release-cut/SKILL.md \
.claude/skills/release-cut/SKILL.md
cmp -s \
.codex/skills/release-notes/SKILL.md \
.claude/skills/release-notes/SKILL.md
! rg -n '^[[:space:]]*git push .*--tags' \
.codex/skills/release-cut/SKILL.md \
.claude/skills/release-cut/SKILL.md
! rg -n 'separate manual flow' \
.codex/skills/release-cut/SKILL.md \
.claude/skills/release-cut/SKILL.md
node --test \
tools/release/release-notes.test.mjs \
tools/release/release-note-gate.test.mjs \
tools/release/build-release-notes.test.mjs \
tools/release/screenshot-guards.test.mjs
pnpm nx test release-tools
Expected: both cmp commands and both negated rg checks exit 0; no
executable broad tag push or stale manual-flow wording exists; all release
tests pass.
- Step 9: Commit workflow and release guidance
git add \
.github/workflows/build-and-make.yaml \
tools/release/project.json \
tools/release/release-notes.test.mjs \
.changes/README.md \
.codex/skills/release-cut/SKILL.md \
.codex/skills/release-notes/SKILL.md \
.claude/skills/release-cut/SKILL.md \
.claude/skills/release-notes/SKILL.md \
AGENTS.md \
CLAUDE.md
git commit -m "docs(release): synchronize release workflow guidance"
Task 4: Normalize every Stalker series flag decision
Files:
-
Modify:
libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.spec.ts -
Modify:
libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.ts -
Modify:
libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts -
Modify:
libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.spec.ts -
Modify:
libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.ts -
Verify:
libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts -
Step 1: Add selection characterization and a failing progress regression
In stalker-catalog-facade.service.spec.ts, retain the store mock in a local
variable and add parameterized coverage:
it.each([true, 1, '1'] as const)(
'selects a VOD series when is_series is %p',
(isSeries) => {
const service = TestBed.inject(StalkerCatalogFacadeService);
const store = TestBed.inject(StalkerStore) as unknown as {
setSelectedItem: jest.Mock;
};
service.selectItem({
id: '42',
name: 'Boolean Series',
is_series: isSeries,
});
expect(store.setSelectedItem).toHaveBeenCalledWith(
expect.objectContaining({ id: '42', is_series: true })
);
}
);
it.each([true, 1, '1'] as const)(
'reports series progress semantics when is_series is %p',
(isSeries) => {
const service = TestBed.inject(StalkerCatalogFacadeService);
expect(
service.getItemProgress({
id: '42',
name: 'Series',
is_series: isSeries,
})
).toEqual({ hasSeriesProgress: false });
}
);
Add false and 0 non-series cases that expect an undefined normalized
is_series selection field and ordinary VOD progress
{ progress: 0, isWatched: false }.
Use the smallest valid StalkerVodSource fixture accepted by the compiler; do
not cast away missing required fields if the existing factory already provides
one.
- Step 2: Run the facade spec and verify RED
Run:
pnpm exec jest \
--config libs/portal/stalker/feature/jest.config.ts \
--runInBand \
libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.spec.ts
Expected: the boolean progress case fails because the facade currently checks
only 1 and '1'. Selection cases already pass through normalization inside
buildStalkerSelectedVodItem; they are characterization coverage for removing
the redundant local comparison.
- Step 3: Route facade and detail decisions through the normalizer
Import isStalkerSeriesFlag from
@iptvnator/portal/stalker/data-access. Replace both direct comparisons in
StalkerCatalogFacadeService:
const needsSeriesFetch =
this.contentType() === 'vod' && isStalkerSeriesFlag(item.is_series);
const isSeries =
this.contentType() === 'series' ||
isStalkerSeriesFlag(item.is_series);
Use the same helper in StalkerCatalogDetailComponent.isSeriesDetail:
return Boolean(
item &&
(this.contentType() === 'series' ||
isStalkerSeriesFlag(item.is_series))
);
- Step 4: Lock the store resource to the same three-value contract
Change the vodSeriesSeasonsResource guard in
with-stalker-series.feature.ts from truthiness to:
!isStalkerSeriesFlag(selectedItem.is_series)
Import the helper from ../../stalker-vod.utils. In
with-stalker-series.feature.spec.ts, parameterize the existing
is_series: '1' resource test over true, 1, and '1', and add one
unsupported truthy value such as 'true' that must not issue the season
request.
- Step 5: Run targeted Stalker tests and verify GREEN
Run:
pnpm exec jest \
--config libs/portal/stalker/feature/jest.config.ts \
--runInBand \
libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.spec.ts
pnpm exec jest \
--config libs/portal/stalker/data-access/jest.config.ts \
--runInBand \
libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.spec.ts
! rg -n \
'is_series\s*===|String\(.*is_series|!selectedItem\.is_series' \
libs/portal/stalker
Expected: both Jest commands and the negated rg check pass; no raw
series-flag decisions remain. Logging and value-preserving serialization may
still mention is_series.
- Step 6: Commit the normalized flag contract
git add \
libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.spec.ts \
libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.ts \
libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts \
libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.spec.ts \
libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.ts
git commit -m "fix(stalker): normalize catalog series flags"
Task 5: Give lazy VOD-series episodes parent-scoped identities
Files:
-
Modify:
libs/portal/stalker/data-access/src/lib/stalker-series.adapters.spec.ts -
Modify:
libs/portal/stalker/data-access/src/lib/stalker-series.adapters.ts -
Modify:
libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts -
Step 1: Replace the old identity test with collision regressions
Extend the local episode metadata type:
type EpisodeWithMetadata = {
custom_sid?: string;
id?: string;
legacyTrackingId?: number;
originalId?: string;
originalCmd?: string;
};
Update every mapVodSeriesEpisodes call to pass an options object. Add tests
which prove:
- the same parent, provider episode, season, and episode number are deterministic;
- the same season/episode in parent series
100and200produces differentidvalues; - those two episodes retain the same
legacyTrackingId; - two provider episode IDs within one parent also produce different IDs; and
mapRegularSeriesEpisodeskeeps its existing identity behavior.
Representative calls:
const firstSeries = mapVodSeriesEpisodes(seasons, {
parentSeriesId: 100,
fallbackPoster: 'poster.jpg',
});
const secondSeries = mapVodSeriesEpisodes(seasons, {
parentSeriesId: 200,
fallbackPoster: 'poster.jpg',
});
- Step 2: Run the adapter spec and verify RED
Run:
pnpm exec jest \
--config libs/portal/stalker/data-access/jest.config.ts \
--runInBand \
libs/portal/stalker/data-access/src/lib/stalker-series.adapters.spec.ts
Expected: FAIL because the current adapter has no options object or
legacyTrackingId, and identical season/episode pairs collide across parents.
- Step 3: Add an explicit mapping contract
In stalker-series.adapters.ts, add:
export interface MapVodSeriesEpisodesOptions {
parentSeriesId: string | number;
fallbackPoster?: string;
}
export interface StalkerMappedEpisode extends XtreamSerieEpisode {
legacyTrackingId?: number;
originalId?: string;
originalCmd?: string;
}
Replace the VOD branch of generateEpisodeId with two named helpers:
function generateLegacyVodEpisodeId(
episodeNum: number,
seasonKey: string
): number {
return hashString(`vod_${seasonKey}_${episodeNum}`);
}
function generateVodEpisodeId(options: {
parentSeriesId: string | number;
providerEpisodeId: string;
seasonKey: string;
episodeNum: number;
}): number {
return hashString(
JSON.stringify([
'vod',
String(options.parentSeriesId),
options.providerEpisodeId,
options.seasonKey,
options.episodeNum,
])
);
}
Keep the regular-series generateEpisodeId path unchanged. Change the public
signature to:
export function mapVodSeriesEpisodes(
seasons: ReadonlyArray<VodSeriesSeasonVm>,
options: MapVodSeriesEpisodesOptions
): Record<string, XtreamSerieEpisode[]>
For every VOD episode, set:
const providerEpisodeId = String(episode.id ?? '');
const legacyTrackingId = generateLegacyVodEpisodeId(
episodeNum,
seasonKey
);
const trackingId = generateVodEpisodeId({
parentSeriesId: options.parentSeriesId,
providerEpisodeId,
seasonKey,
episodeNum,
});
Use options.fallbackPoster for the fallback artwork and expose both
originalId: providerEpisodeId and legacyTrackingId on the mapped episode.
- Step 4: Pass the selected parent identity at the only production call
In StalkerSeriesViewComponent.mappedSeasons, capture displayItem once and
call:
mapVodSeriesEpisodes(this.vodSeriesSeasons(), {
parentSeriesId: this.toSeriesId(displayItem?.id ?? 0),
fallbackPoster: displayItem?.info?.movie_image,
})
Do not derive the parent from a season or episode field; the selected catalog
item, normalized through the same toSeriesId path used by persistence, is the
scope represented by seriesXtreamId.
- Step 5: Run the adapter spec and verify GREEN
Run:
pnpm exec jest \
--config libs/portal/stalker/data-access/jest.config.ts \
--runInBand \
libs/portal/stalker/data-access/src/lib/stalker-series.adapters.spec.ts
Expected: all scoped-identity and legacy-ID cases pass.
Do not commit yet. Task 6 wires legacy reconciliation in the same atomic behavior commit so a scoped-ID build can never ship without resume compatibility.
Task 6: Reconcile and migrate legacy Stalker playback positions
Files:
-
Create:
libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-position-compatibility.ts -
Create:
libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-position-compatibility.spec.ts -
Create:
libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.position-compatibility.spec.ts -
Verify:
libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.spec.ts -
Modify:
libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts -
Include uncommitted Task 5 changes in:
libs/portal/stalker/data-access/src/lib/stalker-series.adapters.spec.ts -
Include uncommitted Task 5 changes in:
libs/portal/stalker/data-access/src/lib/stalker-series.adapters.ts -
Step 1: Add failing pure reconciliation tests
Create the compatibility spec and cover these cases:
- an exact new ID wins when both new and legacy rows exist, while the compatible legacy row remains available only as cleanup metadata;
- a legacy row from the current
seriesXtreamIdbecomes an alias under the new ID; - a row with a different parent series is ignored;
- present
seasonNumberorepisodeNumbervalues must match the mapped episode, while absent legacy metadata remains compatible; - episodes without
legacyTrackingIdnever receive an alias; - saving a migrated position awaits the new save before clearing the old ID;
- a rejected save never clears the old row;
- save and clear helpers reject legacy cleanup for a different/missing parent, equal old/new IDs, non-episode content, or conflicting playlist; and
- clearing an exact scoped row clears its confirmed legacy ID before the scoped ID, after which a fresh reconciliation cannot resurrect progress;
- a rejected legacy clear leaves the exact scoped row untouched, while a scoped-clear failure after legacy cleanup also leaves exact progress available.
Use fixtures with two parent series that share a legacy tracking ID. Assert call order with an array pushed by the repository mocks rather than relying only on invocation counts.
- Step 2: Run the new spec and verify RED
Run:
pnpm exec jest \
--config libs/portal/stalker/feature/jest.config.ts \
--runInBand \
libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-position-compatibility.spec.ts
Expected: FAIL because the module does not exist.
- Step 3: Implement the pure compatibility boundary
Export:
export interface ReconciledStalkerSeriesPositions {
positionsByTrackingId: Map<number, PlaybackPositionData>;
legacyPositionByTrackingId: Map<number, PlaybackPositionData>;
}
export function reconcileStalkerSeriesPositions(options: {
seriesXtreamId: number;
episodesBySeason: Readonly<
Record<string, readonly XtreamSerieEpisode[]>
>;
seriesPositions: readonly PlaybackPositionData[];
}): ReconciledStalkerSeriesPositions;
export async function saveStalkerSeriesPosition(options: {
repository: Pick<
PortalPlaybackPositions,
'savePlaybackPosition' | 'clearPlaybackPosition'
>;
playlistId: string;
position: PlaybackPositionData;
legacyPosition?: PlaybackPositionData;
}): Promise<boolean>;
export async function clearStalkerSeriesPosition(options: {
repository: Pick<PortalPlaybackPositions, 'clearPlaybackPosition'>;
playlistId: string;
position: PlaybackPositionData;
legacyPosition?: PlaybackPositionData;
}): Promise<boolean>;
reconcileStalkerSeriesPositions must:
- index only
contentType: 'episode'rows whoseseriesXtreamIdequals the requested parent; - receive those rows only from
getSeriesPlaybackPositions(playlistId, seriesXtreamId); never broaden the migration withgetAllPlaybackPositions; - preserve exact current-ID rows first;
- inspect
legacyTrackingIdonly onStalkerMappedEpisode; - require a present legacy
seasonNumberand/orepisodeNumberto equal the mapped episode (null/undefinedmeans absent; compare other runtime values numerically); - inspect a compatible legacy row even when an exact scoped row already won;
- record that original legacy row in
legacyPositionByTrackingId, keyed by the new tracking ID, so exact-row save/clear operations can remove it; and - only when no exact row exists, clone the compatible legacy row under the new ID, setting the current parent, season, and episode metadata without mutating the source row.
Factor one private ownership predicate shared by both persistence helpers.
saveStalkerSeriesPosition must always await the new save first. It clears the
legacy row and returns true only when all of these hold:
position.contentType === 'episode'
legacyPosition?.contentType === 'episode'
position.contentXtreamId !== legacyPosition.contentXtreamId
position.seriesXtreamId != null
legacyPosition.seriesXtreamId != null
position.seriesXtreamId === legacyPosition.seriesXtreamId
(!position.playlistId || position.playlistId === playlistId)
(!legacyPosition.playlistId || legacyPosition.playlistId === playlistId)
Otherwise it returns false after saving the new row. Let save/clear failures
reject so callers cannot mistake a partial migration for success.
When the same ownership predicate confirms a distinct legacy row,
clearStalkerSeriesPosition must clear the legacy ID first and the scoped
position.contentXtreamId second, then return true. This ordering is
load-bearing: if legacy cleanup fails, the exact row remains; if scoped cleanup
then fails, the exact row still represents the uncleared state and the legacy
row cannot resurrect. Without a confirmed legacy row, clear only the scoped ID
and return false. Tests should back repository rows with a Map, remove rows
in the mock, and rerun reconciliation after a dual clear.
- Step 4: Run the pure compatibility spec and verify GREEN
Run:
pnpm exec jest \
--config libs/portal/stalker/feature/jest.config.ts \
--runInBand \
libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-position-compatibility.spec.ts
Expected: all reconciliation, guard, and save-before-clear tests pass.
- Step 5: Add failing component integration regressions in a focused spec
Create stalker-series-view.position-compatibility.spec.ts with the smallest
TestBed harness needed for position loading, lazy episodes, time updates, and
toggle requests. Do not append these cases to
stalker-series-view.component.spec.ts: it is already close to the 1200-line
test ceiling. Prove:
- positions returned before lazy episodes load are reconciled after
loadEpisodesForSeasonpopulates the season; - an exact scoped position beats a compatible legacy row but retains that legacy row as cleanup metadata;
- a slow response for series A cannot overwrite positions after selection changes to series B;
- inline time updates and watched toggles save the scoped ID and then clear a confirmed legacy ID;
- after an exact or migrated save, an effect/reconciliation rerun does not restore the removed legacy alias from raw rows;
- clearing a scoped watched state also clears its confirmed legacy row, and a reload/reconciliation does not make the old progress reappear; and
- no legacy clear occurs for regular series or an unconfirmed legacy alias.
Use deferred promises for the stale-response case. Instantiate the real component with its injected repository/store mocks so the signals and effects, not only the pure helper, are exercised.
- Step 6: Run the component spec and verify RED
Run:
pnpm exec jest \
--config libs/portal/stalker/feature/jest.config.ts \
--runInBand \
libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.position-compatibility.spec.ts
Expected: the lazy reconciliation, stale-response, and migration assertions fail against the current single map/direct-save implementation.
- Step 7: Store raw rows separately and reconcile whenever episodes change
In StalkerSeriesViewComponent, add:
private readonly rawSeriesPositions =
signal<readonly PlaybackPositionData[]>([]);
private readonly legacyPositionByTrackingId =
signal<Map<number, PlaybackPositionData>>(new Map());
private seriesPositionsLoadGeneration = 0;
Add one effect that reads displayItem, mappedSeasons, and
rawSeriesPositions, calls reconcileStalkerSeriesPositions, then updates
episodePlaybackPositions and legacyPositionByTrackingId. Clear all three
position stores when no playlist/series is selected.
Refactor loadSeriesPositions to capture a monotonically increasing
generation plus the requested playlist/series IDs. After the await, publish
the raw rows only if:
- the generation is still current;
- the current playlist ID still matches; and
toSeriesId(displayItem()?.id)still matches.
This makes a lazy episode load naturally re-run reconciliation and prevents detail-to-detail races from publishing stale rows.
- Step 8: Route position writes through the migration helper
Keep policy and ownership checks in the new helper so the already-baselined production component gains only signal/effect wiring and thin repository coordination.
Add a private method that calls saveStalkerSeriesPosition with the legacy row
looked up by the new contentXtreamId, whether the rendered value came from a
legacy fallback or an already-exact scoped row. On successful save:
- filter both the scoped ID and the confirmed legacy
contentXtreamIdfromrawSeriesPositions, then append the saved scoped row; - remove the consumed legacy alias from
legacyPositionByTrackingId; and - update the rendered map immediately.
Use this method from:
- throttled inline time updates;
handlePlaybackToggleRequestedwhennextPositionis present; and- matching runtime bridge updates.
The runtime bridge path intentionally repeats the facade's idempotent upsert: only the series view has the episode mapping needed to order “save scoped row, then clear confirmed legacy row.”
For a clear toggle, call clearStalkerSeriesPosition, then remove both
confirmed rows from raw/rendered state. Propagate either clear failure rather
than publishing a false success. Never infer a legacy ID from season/episode
numbers at write time; only the reconciliation result may authorize deletion.
The focused component spec must force reconciliation after a successful save
and refetch/reconcile after a successful dual clear to prove the legacy row
cannot reappear.
- Step 9: Run all affected Stalker unit tests
Run:
pnpm nx test portal-stalker-data-access --runInBand
pnpm nx test portal-stalker-feature --runInBand
Expected: both projects pass, including all flag, ID, reconciliation, lazy-load, and migration regressions.
- Step 10: Commit scoped identity and legacy migration atomically
git add \
libs/portal/stalker/data-access/src/lib/stalker-series.adapters.spec.ts \
libs/portal/stalker/data-access/src/lib/stalker-series.adapters.ts \
libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-position-compatibility.spec.ts \
libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-position-compatibility.ts \
libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.position-compatibility.spec.ts \
libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts
git commit -m "fix(stalker): preserve progress with scoped episode IDs"
Task 7: Publish the Stalker compatibility contract
Files:
-
Create:
.changes/stalker-series-position-identity.md -
Modify:
docs/architecture/stalker-portal.md -
Modify:
CLAUDE.md -
Modify:
libs/shared/interfaces/src/lib/stalker-item.normalizer.spec.ts -
Modify:
libs/shared/interfaces/src/lib/stalker-item.normalizer.ts -
Step 1: Add the required user-facing release note
Create:
---
type: fix
area: stalker
---
Stalker VOD series now report progress correctly when portals return boolean series flags, keep episode progress separate between shows, and resume positions saved by earlier versions.
- Step 2: Lock the shared normalizer contract
In stalker-item.normalizer.spec.ts, add parameterized assertions that
extractStalkerItemType classifies is_series: true, 1, and '1' as
series. Update the normalizer's “truthy” doc comment to name those exact shapes.
This is contract coverage/documentation for the already-correct shared
normalizer; do not duplicate the feature fix in shared-interfaces.
Run:
pnpm nx test shared-interfaces --runInBand
Expected: PASS.
- Step 3: Document scoped episode identity and migration
In docs/architecture/stalker-portal.md, update the VOD-series and playback
position sections to state:
is_seriesis normalized only fromtrue,1, or'1';- lazy VOD-series tracking IDs include parent series ID, provider episode ID, season key, and episode number;
- the previous season/episode hash is exposed only as a compatibility alias;
- legacy rows are considered only inside the current parent-series query and must satisfy any stored season/episode metadata;
- exact scoped rows win;
- the new row is saved before a confirmed legacy row is removed; and
- no database schema migration or bulk rewrite is performed.
Add the compatibility helper, pure spec, and focused component integration spec to the regression-coverage file inventory.
- Step 4: Keep the root architecture summary current
Update the Stalker series entry in CLAUDE.md with the same concise identity
and migration rule. Do not add this implementation detail to AGENTS.md; its
process guidance is unaffected.
- Step 5: Validate docs and the note
Run:
pnpm run release:notes:validate
pnpm nx test shared-interfaces --runInBand
git diff --check
Expected: all commands pass.
- Step 6: Commit the Stalker docs and release note
git add \
.changes/stalker-series-position-identity.md \
docs/architecture/stalker-portal.md \
CLAUDE.md \
libs/shared/interfaces/src/lib/stalker-item.normalizer.spec.ts \
libs/shared/interfaces/src/lib/stalker-item.normalizer.ts
git commit -m "docs(stalker): record series position compatibility"
Task 8: Add mechanical validation for committed repository skills
Files:
-
Create:
tools/skills/validate-repository-skills.test.mjs -
Create:
tools/skills/validate-repository-skills.mjs -
Create:
tools/skills/project.json -
Modify:
package.json -
Modify:
AGENTS.md -
Modify:
CLAUDE.md -
Step 1: Write validator tests against temporary repositories
Export a validateRepositorySkills({ rootDir }) function so the tests can use
isolated fixtures. Build one valid temporary repository per test with
node:fs/promises and mkdtemp, then assert diagnostics for:
- a valid trigger-only skill whose quoted description contains a YAML
colon-space value such as
type: internal; - a frontmatter
namethat differs from its directory; - a description that does not start with
Use when; - a description over 500 characters;
- a skill over 500 words;
- a backticked, literal repository path that does not exist; and
- byte-different
.codex/.clauderelease mirrors.
The valid fixture must prove that wildcard/placeholder examples such as
apps/*-e2e and libs/<domain>/feature are skipped by path validation.
- Step 2: Run the validator tests and verify RED
Run:
node --test tools/skills/validate-repository-skills.test.mjs
Expected: FAIL because the validator module does not exist.
- Step 3: Implement the dependency-free validator
validateRepositorySkills must:
- discover direct
.codex/skills/*/SKILL.mdfiles in sorted order; - parse the opening
---frontmatter without adding a YAML dependency, unquoting matching single- or double-quoted one-line scalar values before validation; - require
nameto equal the containing directory; - require a one-line
descriptionthat begins withUse whenand is at most 500 characters; - count Markdown words and reject a complete skill over 500 words;
- inspect backticked tokens beginning with
apps/,libs/,docs/,tools/,.changes/, or.github/, plus tokens exactly equal topackage.json,pnpm-lock.yaml,nx.json,tsconfig.base.json,eslint.config.mjs,CHANGELOG.md,AGENTS.md, orCLAUDE.md; - skip tokens containing glob or placeholder characters (
*,?,[,],{,},<,>) and require every remaining literal path to exist; - compare the
release-notesandrelease-cut.codexfiles byte-for-byte with their.claudemirrors; and - return all diagnostics in deterministic order instead of failing at the first error.
The CLI entrypoint prints one diagnostic per line to stderr and sets a nonzero
exit code. A valid run prints the number of checked skills. Guard CLI execution
with fileURLToPath(import.meta.url) so importing the module in node:test
never runs the repository-root validation as a side effect.
- Step 4: Add an Nx project and package entrypoint
Create tools/skills/project.json:
{
"name": "repository-skills",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "tools/skills",
"tags": ["scope:tools", "domain:skills", "type:tool"],
"targets": {
"test": {
"executor": "nx:run-commands",
"cache": true,
"inputs": [
"{workspaceRoot}/tools/skills/validate-repository-skills.mjs",
"{workspaceRoot}/tools/skills/validate-repository-skills.test.mjs"
],
"options": {
"command": "node --test tools/skills/validate-repository-skills.test.mjs",
"cwd": "{workspaceRoot}"
}
},
"lint": {
"executor": "nx:run-commands",
"options": {
"command": "node --check tools/skills/validate-repository-skills.mjs",
"cwd": "{workspaceRoot}"
}
}
}
}
Add:
"skills:validate": "node tools/skills/validate-repository-skills.mjs"
to package.json scripts without reordering unrelated scripts.
- Step 5: Run the test suite and verify GREEN
Run:
node --test tools/skills/validate-repository-skills.test.mjs
pnpm nx show project repository-skills
pnpm nx test repository-skills
Expected: the unit suite passes and Nx discovers the tagged tool project.
pnpm run skills:validate is intentionally deferred until Tasks 9-10 make the
current eight skills comply.
- Step 6: Document the maintenance hook and refresh the skill inventory
Merge these exact maintenance bullets into the existing Agent Bootstrap/process
section in both AGENTS.md and CLAUDE.md; keep the shared wording identical:
- Repository-specific skills live under `.codex/skills/`.
- Frontmatter descriptions are trigger-only and begin with `Use when`; keep
each skill at or below 500 words.
- Run `pnpm run skills:validate` after editing a committed skill or a literal
path it documents.
- Keep `.codex` and `.claude` copies of `release-notes` and `release-cut`
byte-identical.
Then replace the stale six-entry AGENTS.md ## Repo Skills inventory with
paths only for all eight committed skills:
## Repo Skills
- `.codex/skills/iptvnator-nx-architecture/SKILL.md`
- `.codex/skills/iptvnator-sqlite-db-worker/SKILL.md`
- `.codex/skills/iptvnator-theme-style/SKILL.md`
- `.codex/skills/iptvnator-ui-design/SKILL.md`
- `.codex/skills/release-cut/SKILL.md`
- `.codex/skills/release-notes/SKILL.md`
- `.codex/skills/stalker-portal/SKILL.md`
- `.codex/skills/xtream-electron/SKILL.md`
Descriptions and trigger conditions are canonical in each skill's frontmatter;
do not duplicate them here.
- Step 7: Commit the validator
git add \
tools/skills/validate-repository-skills.test.mjs \
tools/skills/validate-repository-skills.mjs \
tools/skills/project.json \
package.json \
AGENTS.md \
CLAUDE.md
git commit -m "test(skills): validate repository skill contracts"
Task 9: Refresh the Nx and SQLite ownership skills
Files:
-
Modify:
.codex/skills/iptvnator-nx-architecture/SKILL.md -
Modify:
.codex/skills/iptvnator-sqlite-db-worker/SKILL.md -
Modify:
docs/architecture/nx-workspace-boundaries.md -
Modify:
docs/architecture/sqlite-db-worker.md -
Step 1: Replace both frontmatter descriptions with trigger-only text
Use exactly:
description: Use when deciding where IPTVnator code belongs, creating or moving Nx projects, changing scoped aliases or tags, editing lint targets, or validating module boundaries.
and:
description: Use when changing Electron SQLite IPC, database-worker operations, request-scoped progress or cancellation, worker packaging, or runtime verification of non-EPG database work.
Remove the duplicate “Use this skill when…” sentence from each body.
- Step 2: Rewrite the Nx skill around decisions, not a stale inventory
Keep it below 500 words and include these exact contracts:
- bootstrap with
pnpm install --frozen-lockfile, then discover reality withpnpm nx show projects,pnpm nx show project <name>, and the two--withTarget test|e2equeries; - current app groups:
web,electron-backend,web-backend,remote-control-web,website, both E2E apps, and both provider mock servers; current tool groups: ESLint, packaging, release, and repository skills; discovery remains authoritative when this snapshot changes; - use
apps/for runtime/dev/E2E/mock apps,tools/for repo automation, and domain libraries underlibs/; type:featureowns route and screen orchestration,type:uireusable visual components,type:data-accessinjectable state/API/persistence/orchestration, andtype:utilpure code only;- provider-neutral collection services that coordinate persistence belong in
libs/portal/shared/data-access; pure helpers stay inlibs/portal/shared/util; reusable views stay inlibs/portal/shared/ui; - every project keeps one
scope:*,domain:*, andtype:*tag; reproduce the full enforced type direction fromeslint.config.mjs: app/E2E/dev-app → feature/UI/data-access/util, website → UI/util, feature → feature/UI/data-access/util, UI → UI/data-access/util, data-access → data-access/util, and util → util; - domain constraints remain additive to type constraints; do not weaken them
to solve a placement problem, and preserve the
workspace-shell-utilpath/tag exception documented in the canonical architecture guide; - aliases come from
tsconfig.base.json, public imports go throughsrc/index.ts, and a buildable library'spackage.json.namematches its scoped alias; - production TypeScript targets under 300 lines with a hard 400 maximum, tests under 1200, and the legacy baseline may only shrink;
- command-based lint targets quote recursive globs, followed by a file-count comparison; and
- validation is target-aware: run affected lint/test/build and the closest E2E instead of inventing targets.
Link docs/architecture/nx-workspace-boundaries.md,
tools/eslint/max-lines-config.mjs, and
tools/eslint/generate-max-lines-baseline.mjs.
- Step 3: Bring the Nx architecture doc up to the same contract
Add sections for:
- the
apps/libs/toolsplacement decision; scope:toolsfor repository automation projects, matching existing tool projects;type:data-accessversustype:utilusing the portal collection example;- max-lines ownership and the “baseline only shrinks” rule;
- quoted
eslint "apps/<project>/**/*.ts"run-command globs and thefind ... -name '*.ts' | wc -lcomparison; and - target discovery before choosing validation commands.
Do not hard-code an exhaustive project list that will drift; commands are the canonical inventory.
- Step 4: Rewrite the SQLite skill around the actual protocol
Keep it below 500 words and include:
-
ownership paths for the client, protocol, dispatcher, worker connection, runtime paths, operation modules, build script, preload API, and canonical architecture doc;
-
the three bundles produced by
apps/electron-backend/build-worker.js: EPG parser, database, and playlist refresh; -
requestIdis generated per client request and correlates worker event/response transport;operationIdis a renderer-visible long-operation identity used for progress and cooperative cancellation; -
current tracked operations are save content, delete Xtream content, restore Xtream user data, delete playlist, and delete all playlists;
-
the first four are cancellable; delete-all-playlists is deliberately tracked with
cancellable: false; -
already committed chunks stay committed, final cancellation is an
AbortError, and only the terminal event settles UI state; -
SQL-heavy work stays in operation modules; the dispatcher remains thin;
-
EPG parsing remains in its dedicated worker, while lightweight download/EPG-specific main-process handlers are not silently migrated as part of unrelated DB work;
-
.run()is mandatory for prepared writes inside synchronousbetter-sqlite3transactions; -
after source changes, run
pnpm nx run electron-backend:build-worker, verify thedistartifact timestamp, restart Electron, then perform CDP/E2E; and -
use
IPTVNATOR_TRACE_DB=1/IPTVNATOR_TRACE_SQL=1only through redacting logging paths. -
Step 5: Correct stale SQLite architecture inventory
In docs/architecture/sqlite-db-worker.md:
-
add
title-sources.operations.ts,vod-source-pin.operations.ts, and the content-search/token helper modules to the operation inventory; -
replace the stale collection-service paths under
libs/portal/shared/utilwith their actuallibs/portal/shared/data-accessownership; -
distinguish
requestIdfromoperationIdnext to the message contract; -
list the four cancellable operations and the non-cancellable tracked delete-all operation explicitly;
-
change “bundles both” to all three build outputs, including
playlist-refresh.worker.ts/.js; keep the packaged-verifier subsection accurate thatworkerFilescurrently checks the EPG parser and database bundles rather than claiming a third explicit check that is not implemented; -
retain the documented worker rebuild/restart rule and direct-main-thread exceptions; and
-
remove “first cut” wording that implies cancellation or the core portal migration is still pending.
-
Step 6: Validate both refreshed skills and docs
Run:
pnpm nx test repository-skills
pnpm nx lint repository-skills
rg -n '^description: Use when ' \
.codex/skills/iptvnator-nx-architecture/SKILL.md \
.codex/skills/iptvnator-sqlite-db-worker/SKILL.md
wc -w \
.codex/skills/iptvnator-nx-architecture/SKILL.md \
.codex/skills/iptvnator-sqlite-db-worker/SKILL.md
! rg -n 'bundles both' \
.codex/skills/iptvnator-sqlite-db-worker/SKILL.md \
docs/architecture/sqlite-db-worker.md
rg -n \
'requestId.*operationId|operationId.*requestId|playlist-refresh' \
.codex/skills/iptvnator-sqlite-db-worker/SKILL.md \
docs/architecture/sqlite-db-worker.md
git diff --check
Expected: validator unit/lint targets and the negated stale-phrase check pass;
both descriptions are found, each reported word count is at most 500, and both
identifiers plus the playlist-refresh worker are found in current guidance.
The repository-wide skills:validate run remains deferred until Task 10 has
refreshed the other four non-release skills.
- Step 7: Commit Nx and SQLite guidance
git add \
.codex/skills/iptvnator-nx-architecture/SKILL.md \
.codex/skills/iptvnator-sqlite-db-worker/SKILL.md \
docs/architecture/nx-workspace-boundaries.md \
docs/architecture/sqlite-db-worker.md
git commit -m "docs(skills): refresh Nx and SQLite ownership"
Task 10: Refresh theme, UI, Stalker, and Xtream skills
Files:
-
Modify:
.codex/skills/iptvnator-theme-style/SKILL.md -
Modify:
.codex/skills/iptvnator-ui-design/SKILL.md -
Modify:
.codex/skills/stalker-portal/SKILL.md -
Modify:
.codex/skills/xtream-electron/SKILL.md -
Modify:
libs/ui/styles/_index.scss -
Modify:
docs/architecture/iptvnator-ui-guidelines.md -
Modify:
docs/architecture/stalker-epg.md -
Modify:
docs/architecture/stalker-portal.md -
Modify:
docs/architecture/xtream-portal-compatibility.md -
Modify:
CLAUDE.md -
Step 1: Replace all four descriptions with trigger-only text
Use exactly:
description: Use when changing IPTVnator SCSS tokens, shared layout mixins, portal headers, sidebars, detail views, Electron drag regions, or cross-portal visual consistency.
description: Use when changing user-visible Angular UI in IPTVnator, especially channel rows or lists, EPG views, settings and playlist surfaces, selection states, shared portal components, or light/dark styling.
description: Use when changing Stalker or Ministra routes, stores, catalog or series shapes, playback progress, favorites and recent items, EPG, or remote control.
description: Use when changing Xtream routes, Signal Store or data sources, content identity, SQLite-backed import, search or delete, sparse VOD playback, or Electron and PWA behavior.
Remove each body's duplicate trigger paragraph.
- Step 2: Rewrite the theme skill to match the emitted-token boundary
Keep it below 500 words and include:
-
canonical sources under
apps/web/src/m3-theme.scss,libs/ui/styles/, anddocs/architecture/iptvnator-ui-guidelines.md; -
app-owned surfaces, text, selection, separators, hover, and provider accents use
--app-*tokens, including--app-selection-on-colorfor selected foregrounds; -
Angular Material theme mixins and Material component overrides may use their Material tokens;
-
a
--mat-sys-*token outside a Material-owned component is acceptable only after proving it is emitted in both theme contexts and providing an app-token or literal fallback; -
semantic status colors may be local, but existing hard-coded layout, selection, and EPG surface colors are migration debt, not examples to copy;
-
libs/ui/styles/_index.scssis a forwarding inventory, not a configured global Sass load path; current consumers use relative@usepaths; -
extract repeated layout into the canonical shared partial, while provider-shared service/UI styles use the existing forwarding modules rather than copied SCSS;
-
Electron draggable containers explicitly give buttons, links, inputs, overlays, and resize handles
app-region: no-drag; and -
inspect light/dark plus Xtream/Stalker/M3U/workspace consumers when a shared pattern changes.
-
Step 3: Rewrite the UI skill around shared ownership and flexible sizing
Keep it below 500 words and include:
-
inspect
@iptvnator/ui/components,@iptvnator/ui/epg,@iptvnator/ui/playback,@iptvnator/ui/shared-portals,@iptvnator/portal/shared/ui, and@iptvnator/playlist/shared/uibefore local markup; -
shared stateful collection orchestration belongs in
@iptvnator/portal/shared/data-access, not UI or util; -
use app selection/surface/text tokens for app chrome in both themes and do not expand known hard-coded EPG/style debt;
-
channel/list rows use minimum dimensions, flexible text columns, truncation, and trailing controls that do not resize the row; do not turn historical pixel examples into fixed layout contracts;
-
preserve explicit scroll ownership, keyboard/focus state, empty/loading/error state, and the shared selection language;
-
EPG uses shared timeline/list/panel components and provider layouts supply controlled data rather than duplicating them;
-
visible changes require focused component coverage and the closest Playwright/CDP flow; and
-
behavior-only work must avoid opportunistic visual rewrites.
-
Step 4: Correct the canonical UI guide and Sass forwarding comments
In docs/architecture/iptvnator-ui-guidelines.md:
- replace the absolute claim that Material system surface tokens are never emitted with the scoped policy from Step 2;
- retain examples with explicit fallback, for example
var(--mat-sys-surface-container, var(--app-widget-bg)); - label current hard-coded non-semantic EPG/surface colors as debt;
- change exact row dimensions into current reference values plus flexible minimum-size/truncation requirements; and
- add the shared collection data-access ownership distinction.
In libs/ui/styles/_index.scss, remove the untrue instruction that consumers
can already rely on stylePreprocessorOptions.includePaths. Describe it as the
canonical forward list and show the relative @use pattern already used by
production consumers. Do not rewrite existing consumer imports in this task.
- Step 5: Rewrite the Stalker skill around normalized shapes and scoped identity
Keep it below 500 words and preserve links to
docs/architecture/stalker-portal.md, docs/architecture/stalker-epg.md, and
docs/architecture/remote-control.md. Include:
-
provider API/session/normalization in Stalker data access, routed UI in feature, provider-neutral collection orchestration in portal shared data-access;
-
normalize
is_seriesfrom exactlytrue,1, and'1'across catalog, details, favorites/recent, dashboard classification, and lazy resources; -
preserve the three modes: regular
/series, embedded VODseries[], and lazy Ministra VOD series; -
lazy episode IDs are scoped by parent/provider/season/episode, with the old hash retained only as a guarded compatibility alias;
-
exact scoped progress wins; legacy fallback is limited to the current parent and matching optional S/E metadata; save new before clearing confirmed old;
-
playback metadata always carries parent
seriesXtreamIdplus resolved season and episode numbers; -
bulk ITV EPG eagerly starts once a category has channels, row previews read the cache, and only the active channel falls back to short EPG;
-
radio skips EPG and keeps its live/audio collection identity; and
-
targeted data-access, feature, dashboard, and closest E2E commands.
-
Step 6: Remove both contradictory Stalker EPG passages
In docs/architecture/stalker-epg.md, replace “Before the first live-channel
playback, channel rows do not fetch EPG at all” with the actual eager flow:
once non-radio ITV channels render, the post-reset effect calls
ensureBulkItvEpg(168); rows never issue per-row requests and derive previews
from the bulk cache. Keep the active-channel get_short_epg fallback
description unchanged. Make the matching correction in the EPG Integration
summary in docs/architecture/stalker-portal.md, removing its claims that row
previews have no pre-playback request and wait for the first active-channel
fetch.
- Step 7: Rewrite the Xtream skill around capabilities and identities
Keep it below 500 words and include:
-
read
docs/architecture/xtream-portal-compatibility.md,docs/architecture/portal-detail-navigation.md,docs/architecture/vod-multi-source.md, anddocs/architecture/sqlite-db-worker.mdbefore changing those contracts; -
RuntimeCapabilitiesService.supportsXtreamSqliteDataSource, not a generic Electron check, selectsElectronXtreamDataSource; otherwise use PWA; -
feature routes/screens live in portal Xtream feature, API/cache/stores/data sources in portal Xtream data-access, collection services plus reusable multi-source controllers/resolvers in portal shared data-access, screen-specific multi-source session/UI orchestration in the Xtream feature, reusable views in portal shared UI, and only pure contracts/helpers in portal shared util;
-
content identity is playlist- and type-aware; never resolve a colliding live/movie/series ID by number alone, and distinguish normalized SQLite row IDs from provider
xtream_id/stream_id/series_id; -
detail navigation carries enough route/provider identity to recover hidden categories and cross-portal links without sending a local DB ID to the API;
-
sparse VOD metadata does not remove Play/Favorite/Download when an atomic positive stream-ID + extension pair can be recovered;
-
multi-source is Electron-only Xtream↔Xtream movie behavior, capability-gated, owner-scoped, and must not combine partial source fields;
-
current
XtreamStorecomposition is portal, content, selection, search, EPG, player, favorites, recent items, and playback positions; -
large imports/search/delete/restore stay worker-backed with request-scoped progress and
operationIdcancellation; -
Xtream network/request cancellation follows the request or session identity used by the current data-source call (
requestId/sessionIdat those boundaries), while database long-operation progress/cancel usesoperationId; never conflate the two; and -
validate both Electron and PWA data-source specs plus the relevant atomized target:
web-e2e:e2e-ci--src/xtream.e2e.ts,electron-backend-e2e:e2e-ci--src/xtream-responsiveness.e2e.ts,electron-backend-e2e:e2e-ci--src/xtream-vod-details.e2e.ts, orelectron-backend-e2e:e2e-ci--src/vod-multi-source.e2e.ts. -
Step 8: Add the missing Xtream architecture links
Add a short “Runtime selection and ownership” section to
docs/architecture/xtream-portal-compatibility.md covering the exact
supportsXtreamSqliteDataSource gate, shared data-access collection/multi-source
ownership, and type-aware playlist-scoped identity. Link to
docs/architecture/nx-workspace-boundaries.md and
docs/architecture/sqlite-db-worker.md.
Update the Xtream architecture summary in CLAUDE.md only where it still says
environment detection is a generic Electron/PWA branch or places collection
orchestration in util. Preserve the existing detailed sparse-VOD and
multi-source contracts.
- Step 9: Validate the four skills and canonical docs
Run:
pnpm run skills:validate
! rg -n \
'Before the first live-channel playback|no pre-playback network requests|first active-channel fetch|stylePreprocessorOptions.includePaths' \
docs/architecture/stalker-epg.md \
docs/architecture/stalker-portal.md \
libs/ui/styles/_index.scss
rg -n \
'supportsXtreamSqliteDataSource|portal/shared/data-access|legacyTrackingId' \
.codex/skills/xtream-electron/SKILL.md \
.codex/skills/stalker-portal/SKILL.md \
docs/architecture/xtream-portal-compatibility.md \
docs/architecture/stalker-portal.md
git diff --check
Expected: the first rg has no matches; the second finds each current
contract; skill validation and whitespace checks pass.
- Step 10: Commit the remaining skill and doc refresh
git add \
.codex/skills/iptvnator-theme-style/SKILL.md \
.codex/skills/iptvnator-ui-design/SKILL.md \
.codex/skills/stalker-portal/SKILL.md \
.codex/skills/xtream-electron/SKILL.md \
libs/ui/styles/_index.scss \
docs/architecture/iptvnator-ui-guidelines.md \
docs/architecture/stalker-epg.md \
docs/architecture/stalker-portal.md \
docs/architecture/xtream-portal-compatibility.md \
CLAUDE.md
git commit -m "docs(skills): align provider and UI guidance"
Task 11: Exercise every skill and run the final validation ladder
Files:
-
Verify:
.codex/skills/*/SKILL.md -
Verify:
.claude/skills/release-cut/SKILL.md -
Verify:
.claude/skills/release-notes/SKILL.md -
Verify: all files changed in Tasks 2-10
-
Step 1: Run eight fresh-context skill scenarios
Use a fresh worker for each prompt with only the named skill and repository available. Ask for a proposed approach, not code changes. A scenario passes only if it independently reaches all listed decisions:
| Skill | Prompt | Required decisions |
|---|---|---|
iptvnator-nx-architecture |
Add a reusable Xtream/Stalker collection resolver that reads favorites and recent rows, with a new Nx project and command-based lint target. | portal/shared/data-access; three tags; scoped alias/public API; quoted recursive lint glob; target discovery and max-lines check. |
iptvnator-sqlite-db-worker |
Make DB_DELETE_ALL_PLAYLISTS cancellable using its request ID, then verify it in an already-running Electron app. |
Reject both premises: delete-all is non-cancellable; cancellation uses operationId; distinguish transport requestId; rebuild worker and restart before runtime verification. |
iptvnator-theme-style |
Style a CDK overlay inside a draggable Electron header using a Material surface token and a short Sass import. | Prefer app surface token; require proof/fallback for external --mat-sys-*; use current relative Sass import; mark interactions no-drag; check both themes and shared consumers. |
iptvnator-ui-design |
Add a local fixed-size channel row and hard-coded selected EPG card to one portal. | Reuse shared row/EPG components; use app tokens; call hard-coded debt out; keep flexible minimum sizing/truncation; add interaction/theme coverage. |
release-notes |
A packaging verifier changed with no user-visible runtime effect and the release gate asks for a note. | Choose internal only if a note is desired/required, otherwise explain no-release-note; never call it a user fix; internal remains in changelog but not authored public body. |
release-cut |
Cut a patch release through remote upstream; the version's blog post already exists. |
Edit existing post without force scaffolding; push exact branch/tag to named remote, never all tags; verify Pacman/source archive and other assets; identify automatic Docker and Snap-edge side effects plus manual promotion. |
stalker-portal |
A portal returns boolean is_series; two shows share S01E01; old progress must resume. |
Use the normalizer; parent/provider/season/episode-scoped ID; current-parent legacy lookup with optional S/E guards; exact ID wins; save new before clear; test all surfaces. |
xtream-electron |
Live and movie rows share numeric ID, PWA has a partial bridge, a network request is replaced, and a global collection delete needs progress/cancel. | Capability-selected data source; playlist/type-aware identity; collection orchestration in shared data-access; no local DB ID sent to provider route; network request/session identity stays distinct from worker operationId; validate exact Electron/PWA and E2E paths. |
Record pass/fail notes in the task handoff. If a worker misses a required decision, tighten that skill and rerun only its scenario until green.
- Step 2: Commit any scenario-driven corrections
Inspect the working tree after all eight scenarios:
git status --short
git diff --check
If a release scenario required tighter wording, apply the identical correction
to its .claude mirror and rerun that scenario before staging. Confirm the
pair first:
cmp -s \
.codex/skills/release-cut/SKILL.md \
.claude/skills/release-cut/SKILL.md
cmp -s \
.codex/skills/release-notes/SKILL.md \
.claude/skills/release-notes/SKILL.md
Then, if any scenario required tighter wording, stage only the reviewed skill, mirror, root-process, and canonical-doc files from this task:
git add \
.codex/skills/iptvnator-nx-architecture/SKILL.md \
.codex/skills/iptvnator-sqlite-db-worker/SKILL.md \
.codex/skills/iptvnator-theme-style/SKILL.md \
.codex/skills/iptvnator-ui-design/SKILL.md \
.codex/skills/release-cut/SKILL.md \
.codex/skills/release-notes/SKILL.md \
.codex/skills/stalker-portal/SKILL.md \
.codex/skills/xtream-electron/SKILL.md \
.claude/skills/release-cut/SKILL.md \
.claude/skills/release-notes/SKILL.md \
.changes/README.md \
AGENTS.md \
CLAUDE.md \
libs/ui/styles/_index.scss \
docs/architecture/nx-workspace-boundaries.md \
docs/architecture/sqlite-db-worker.md \
docs/architecture/iptvnator-ui-guidelines.md \
docs/architecture/stalker-portal.md \
docs/architecture/stalker-epg.md \
docs/architecture/xtream-portal-compatibility.md
git diff --cached --name-only
If the staged diff is non-empty, commit it:
git commit -m "docs(skills): tighten validated guidance"
If no scenario changed a file, skip this commit. Do not leave scenario-driven edits uncommitted for the final validation.
- Step 3: Run structural and release validation
Run:
pnpm run skills:validate
pnpm run release:notes:validate
node --test \
tools/skills/validate-repository-skills.test.mjs \
tools/release/release-notes.test.mjs \
tools/release/release-note-gate.test.mjs \
tools/release/build-release-notes.test.mjs \
tools/release/screenshot-guards.test.mjs
cmp -s \
.codex/skills/release-cut/SKILL.md \
.claude/skills/release-cut/SKILL.md
cmp -s \
.codex/skills/release-notes/SKILL.md \
.claude/skills/release-notes/SKILL.md
Expected: all checks pass and both mirror comparisons exit 0.
- Step 4: Run all directly affected unit projects
Run:
pnpm nx test release-tools
pnpm nx test repository-skills
pnpm nx test shared-interfaces --runInBand
pnpm nx test portal-stalker-data-access --runInBand
pnpm nx test portal-stalker-feature --runInBand
pnpm nx test workspace-dashboard-data-access --runInBand
Expected: all six projects pass.
- Step 5: Run affected lint and build targets
Run:
pnpm nx run-many \
--target=lint \
--projects=release-tools,repository-skills,shared-interfaces,portal-stalker-data-access,portal-stalker-feature
pnpm nx build shared-interfaces
pnpm nx build web
pnpm nx build electron-backend
Expected: all lint targets and all three builds pass.
- Step 6: Run the closest web and Electron user-workflow E2E
Run:
pnpm nx run web-e2e:e2e-ci--src/stalker.e2e.ts
pnpm nx run electron-backend-e2e:e2e-ci--src/recent.e2e.ts
Expected: the web Stalker catalog/series flow passes, and the Electron recent suite exercises Stalker series persistence across an app restart. The legacy-ID collision migration itself remains covered at pure-helper and Angular component level because the current E2E fixtures cannot seed the old colliding playback-position row before lazy episode mapping. State that fixture limitation and the exact focused coverage in the final summary.
- Step 7: Inspect the final diff for accidental scope
Run:
git diff --check
git status --short
git diff --stat master...HEAD
git log --oneline --decorate master..HEAD
Confirm:
-
exactly eight
.codexskills were refreshed; -
only the two required
.claudemirrors changed; -
no broad SCSS migration or database schema rewrite slipped in;
-
the Stalker release note is the only new
.changesnote; -
AGENTS/CLAUDE shared process wording remains synchronized; and
-
all implementation, tests, docs, and validation tooling are committed.
-
Step 8: Prepare the implementation handoff
Report:
- behavior fixed (public internal-note filtering, Stalker flags/identity);
- skills and canonical docs updated;
- release note added;
- tests added/updated and every validation command with result;
- the E2E fixture limitation from Step 6; and
- the final commit list and clean working-tree status.