diff --git a/specs/001-interactor-confidence-filter/contracts/ui-contract.md b/specs/001-interactor-confidence-filter/contracts/ui-contract.md new file mode 100644 index 00000000..2b50d334 --- /dev/null +++ b/specs/001-interactor-confidence-filter/contracts/ui-contract.md @@ -0,0 +1,58 @@ +# UI contract: Interactor confidence filtering and download + +This feature exposes no new service API — it reads data already fetched. Its +contract is therefore the surface a reader and a test can address: the URL, the +control, and the file. + +## 1. The URL + +| Parameter | Type | Default | In the address when | +| ----------------- | ----------- | ------- | ------------------------- | +| `interactorScore` | number, 0–1 | `0.45` | the reader has changed it | + +- Absent means 0.45, and the app **must not** rewrite the address to add it + (FR-007). This follows from declaring it as `urlParam(0.45, 'number')`: + `currentQueryParams()` omits any value equal to its initial. +- A value that is malformed, negative or above 1 is replaced by 0.45 and must not + prevent the pathway or its interactors from opening (FR-008). +- Clearing the interactors removes it from the address (FR-013). + +Shareability is the point: the same address opened elsewhere shows the same +interactors (SC-003). + +## 2. The control + +Rendered beneath the diagram, only while interactors are shown. + +| Addressable by | Contract | +| ---------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `cr-interactor-threshold` | present exactly when interactor nodes are on the graph; absent otherwise (FR-002) | +| `[data-threshold]` on that element | the threshold in force, so a test can read it without inspecting a slider's pixel position | +| its slider | `min=0`, `max=1`; changing it updates the URL and the diagram together (FR-003) | +| its empty state | when the threshold hides every interaction, says so — distinguishably from the entity having none (FR-012) | + +**Tests assert on the interactors present on the diagram**, not on the slider's +position and not on the parameter. The control moving is not evidence that +anything was filtered. + +## 3. The file + +| Property | Value | +| -------- | -------------------------------------------------------------------------------------------- | +| Format | TSV, `text/tab-separated-values` | +| Name | `Interactors [] [].tsv` | +| Header | `geneName`, `identifier`, `speciesName`, `entitiesCount`, `evidenceCount`, `score` | +| Rows | exactly the interactions currently on the diagram — those at or above the threshold (FR-010) | + +- Produced in the browser from data already held; no request is made. +- The object URL is revoked after use, which the existing participant export + omits to do. +- Because it is synchronous there is no progress and no failure state to report; + FR-011 reduces to "must not produce a silently empty or partial file", which is + asserted by comparing the row count against the interactors on the diagram. + +## 4. What does not change + +- No change to any ContentService request or response. +- No change to how interactors are requested, drawn or cleared. +- No new dbId anywhere: the export names its entity by stable id. diff --git a/specs/001-interactor-confidence-filter/data-model.md b/specs/001-interactor-confidence-filter/data-model.md new file mode 100644 index 00000000..81c2ec9f --- /dev/null +++ b/specs/001-interactor-confidence-filter/data-model.md @@ -0,0 +1,79 @@ +# Data model: Interactor confidence filtering and download + +No persisted storage and no new service payload. Everything here is state already +in the browser, plus one value in the URL. + +## Interaction + +What the interactor service already fetches and draws. Held on the cytoscape node +that owns it, as `occurrenceNode.data('interactors')`. + +| Field | Type | Notes | +| -------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | +| `score` | number | 0–1. Measured 0.482–0.98 across the 33 interactions of Q13158. The only field this feature reads to decide visibility. | +| `identifier` | string | With `databaseName`, forms the `DB:ID` shown in the table. | +| `geneName` | string[] | First entry, falling back to `variantIdentifier`, is what the table shows. | +| `speciesName` | string | Exported. | +| `entitiesCount`, `evidenceCount` | number | Exported. | + +**Validation**: an interaction with no `score` was not seen in the measured data. +If one arrives it is treated as **below every threshold** — hidden rather than +shown — because a claim with no confidence behind it is the one a curator raising +the threshold is trying to remove. This rule is asserted in +`interactor-threshold.spec.ts` rather than left to chance. + +## Confidence threshold + +The lowest score a curator wants to see. + +| Property | Value | +| ------------------------ | -------------------------------------------------------------------------------------------------- | +| Type | number, 0–1 | +| Default | **0.45** — `DEFAULT_SCORE` in `pwp-diagram`'s `InteractorsContent.java` | +| Scope | one per interaction resource | +| Lives in | the URL, as a `urlParam` with initial value 0.45 | +| Absent from the URL when | equal to 0.45, because `currentQueryParams()` omits a value equal to its initial — which is FR-007 | + +**Clamping**: a value outside 0–1, or one that does not parse, falls back to 0.45 +rather than blocking the pathway from opening (FR-008). The clamp is a pure +function so it can be tested against the hand-edited addresses FR-008 describes. + +**Per-resource memory**: a `Map` on `InteractorService`, keyed by +resource name, mirroring `interactorsThreshold` in the old browser. Switching +resource writes that resource's remembered value into the single URL param. +Session-only; the old browser also forgets on reload. + +## Interaction resource + +Already modelled: `InteractorService.currentResource = signal`, +where `ResourceType` is STATIC, PSICQUIC or CUSTOM. This feature adds no fields — +it uses the resource's **name** as the key for the threshold map. + +## Shown interactor set + +Derived, not stored: the interactions of the entities whose interactors were +requested, with `score >= threshold`. It is what the diagram draws and exactly +what the export writes (FR-010). + +**State transitions** that must hold: + +| From | Event | To | +| -------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| no interactors shown | reader opens interactors for an entity | shown at the threshold in force; control appears | +| shown | threshold raised above some scores | those interactions leave the diagram; the rest stay | +| shown | threshold lowered to 0 | every interaction the resource returned is on the diagram | +| shown | threshold above every score | nothing drawn, and the control says the threshold is hiding them — distinct from having none (FR-012) | +| shown | resource switched | that resource's remembered threshold applies, not the previous one (FR-004a) | +| shown | interactors cleared | control and export go; the param leaves the URL (FR-013) | + +## Export row + +One line of TSV per interaction currently shown. + +Columns, in the order the interactors table already presents them, so the file +matches what the curator was looking at: `geneName`, `identifier`, +`speciesName`, `entitiesCount`, `evidenceCount`, `score`. + +Filename follows the existing participant export +(`Participating Molecules [R-HSA-109606].tsv`), naming the entity and the +resource: `Interactors [] [].tsv`. diff --git a/specs/001-interactor-confidence-filter/plan.md b/specs/001-interactor-confidence-filter/plan.md new file mode 100644 index 00000000..44cecc14 --- /dev/null +++ b/specs/001-interactor-confidence-filter/plan.md @@ -0,0 +1,124 @@ +# Implementation Plan: Interactor confidence filtering and download + +**Branch**: `001-interactor-confidence-filter` | **Date**: 2026-09-14 | **Spec**: [spec.md](./spec.md) + +**Input**: Feature specification from `specs/001-interactor-confidence-filter/spec.md` + +## Summary + +Give the interactor overlay a confidence threshold and an export, which are the +two rows of the curator release checklist (`RELEASE-TESTING.md:117-118`) that +cannot be signed off because the behaviour does not exist here. + +Both are additions to machinery that already exists. The scores are already +fetched and already displayed in the interactors table; the interactions are +already held on the cytoscape node that owns them +(`occurrenceNode.data('interactors')`), so filtering and export are both +in-memory. Nothing new is fetched, and no service contract changes. + +The threshold goes in the URL, because `UrlStateService`'s reader resets any +param the URL does not mention — a signal alone does not survive the turn, let +alone a reload. Declaring it with an initial value of `0.45` gets FR-007 free: +`currentQueryParams()` omits a value equal to its initial, so a threshold nobody +changed is absent from the address rather than written into it. + +## Technical Context + +**Language/Version**: TypeScript 5.x, Angular 21 (zoneless, signals, `strictTemplates`) + +**Primary Dependencies**: cytoscape (the diagram), Angular Material (the control), existing `InteractorService` and `UrlStateService` + +**Storage**: none. The threshold lives in the URL; per-resource memory is a `Map` in the service for the session only, matching the old browser, which also forgets on reload. + +**Testing**: vitest for units, Playwright for e2e (`npm run e2e`, project `code`) + +**Target Platform**: the browser, same as the rest of the pathway browser + +**Project Type**: feature inside an existing Angular workspace — no new project + +**Performance Goals**: dragging the control must not make the diagram +unresponsive (SC-005). Filtering is a class or style toggle over elements already +on the graph, so the cost is cytoscape's restyle, not a re-layout or a fetch. + +**Constraints**: the six quality gates, with two ratcheted baselines — `check:lint` 653 and `check:dead` 146 — which a new component must not raise. + +**Scale/Scope**: one new component, one new URL param, one export function, plus +changes to the interactor service. Measured worst case in the data: 33 +interactions on a single entity; a diagram may show several entities' at once. + +## Constitution Check + +| Principle | How this plan satisfies it | +| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **I. Verify the instrument** | Every assertion counts interactors **on the diagram**, never the control's position or the param's value. "Interactors are shown" is read from the graph holding interactor nodes, not from `currentResource()` being set, because the graph is what the reader sees. | +| **II. Measure in the running app** | The threshold is URL state, and URL state has twice behaved in ways invisible from the source (#185, #191). Every acceptance scenario gets an e2e case; unit tests cover only the pure parts (the filter predicate, the TSV shape). | +| **III. Prove a test fails first** | The two checklist rows are currently **missing**, so each new e2e case must be shown red against `main` before the feature exists — that is the cheapest possible "prove it fails", and it is recorded per task. | +| **IV. Never leave a reader on an unstable id** | The export names its entity by stable id; the threshold param carries a number, no ids. Nothing here introduces a dbId. | +| **V. Comments carry the failure, with measured figures** | The only figures that appear in code comments are 0.45 (from `InteractorsContent.java`) and the 0.482–0.98 range (measured for Q13158). Both are cited where used. | + +**Gate result: pass.** No violations to justify; Complexity Tracking below is empty. + +One deviation from the input brief, recorded rather than silently taken: +`FileDownloadService` / `ManagedDownloadDirective` are **not** reused. They exist +for server downloads — progress, cancellation, a 180s ceiling, a failure reason +from a response. The export is synchronous and in-memory, so those states cannot +occur, and wiring them in would add a spinner that never spins. See research.md +§3, which also records the consequence for FR-011. + +## Project Structure + +### Documentation (this feature) + +``` +specs/001-interactor-confidence-filter/ +├── spec.md +├── plan.md # this file +├── research.md # Phase 0 +├── data-model.md # Phase 1 +├── quickstart.md # Phase 1 +├── contracts/ +│ └── ui-contract.md # Phase 1 — the UI surface, there being no new service API +└── checklists/ + └── requirements.md +``` + +### Source code (repository root) + +``` +projects/pathway-browser/src/app/ +├── interactors/ +│ ├── interactor-threshold/ # NEW: the control +│ │ ├── interactor-threshold.component.ts +│ │ ├── interactor-threshold.component.html +│ │ └── interactor-threshold.component.scss +│ ├── interactor-export.ts # NEW: pure — rows to TSV +│ ├── interactor-export.spec.ts # NEW +│ ├── interactor-threshold.ts # NEW: pure — the filter predicate + clamping +│ ├── interactor-threshold.spec.ts # NEW +│ └── services/interactor.service.ts # CHANGED: per-resource thresholds, apply the filter +├── services/url-state.service.ts # CHANGED: one new param +└── viewport/ + ├── viewport.component.html # CHANGED: render the control under the diagram + └── viewport.component.ts # CHANGED: whether to show it + +e2e/ +└── interactor-threshold.spec.ts # NEW + +RELEASE-TESTING.md # CHANGED: :117 and :118 missing -> auto +CURATOR-REPORT.md # CHANGED: drop the two matching gaps +``` + +**Structure Decision**: the feature lives beside the interactor code it extends, +under `projects/pathway-browser/src/app/interactors/`. The two pure modules are +separate files so they can be unit-tested without a browser — the pattern +`url-state.service.spec.ts` uses for `FRAGMENT_PATTERN` and `isContentRoute`. + +## Complexity Tracking + +No constitution violations, so nothing to justify here. + +## Phase status + +- [x] Phase 0 — research.md: the five decisions, all evidenced +- [x] Phase 1 — data-model.md, contracts/ui-contract.md, quickstart.md +- [ ] Phase 2 — tasks.md, by `/speckit-tasks` diff --git a/specs/001-interactor-confidence-filter/quickstart.md b/specs/001-interactor-confidence-filter/quickstart.md new file mode 100644 index 00000000..fc4c4947 --- /dev/null +++ b/specs/001-interactor-confidence-filter/quickstart.md @@ -0,0 +1,91 @@ +# Quickstart: validating interactor confidence filtering and download + +How to prove the feature works, end to end, on a running app. Details of the +surface are in [contracts/ui-contract.md](./contracts/ui-contract.md); the +entities are in [data-model.md](./data-model.md). + +## Prerequisites + +A backend that answers `/ContentService/interactors/...`. On the dev host the +local Tomcat does; otherwise point at production: + +```bash +REACTOME_BACKEND=https://reactome.org npm run start:simple +``` + +Use an npm script, not a bare `ng serve` — the CMS content is generated, and a +bare serve produces a site with empty content pages and no warning. + +## A worked entity + +**Q13158 (FADD)** returns 33 interactions scoring **0.482 – 0.98**, measured +against the local ContentService. That spread straddles the 0.45 default and has +values on both sides of 0.6, which makes it a good subject: raising the threshold +to 0.6 must visibly remove some and keep others. + +```bash +curl -s "http://localhost:8080/ContentService/interactors/static/molecule/Q13158/details" \ + | python3 -c "import json,sys; d=json.load(sys.stdin); s=[i['score'] for i in d['entities'][0]['interactors']]; print(len(s), min(s), max(s))" +``` + +Re-run that before trusting any count below: the data moves between releases, and +a figure you did not measure is not a figure. + +## Scenario 1 — filtering (FR-003, FR-004; SC-001, SC-002) + +1. Open a pathway containing that entity and show its interactors. +2. The control appears beneath the diagram. Read the count of interactor nodes on + the graph, not the control: + + ```js + document.querySelector('#cytoscape')._cyreg.cy.elements('.interactor').length; + ``` + +3. Raise the threshold to 0.6. The count drops, and equals the number of + interactions scoring ≥ 0.6. +4. Lower it to 0. Every interaction the resource returned is on the diagram. +5. Raise it above the highest score. Nothing is drawn, and the control says the + threshold is hiding them — not that there are none (FR-012). + +**The count on the graph is the measurement.** A slider that moved proves +nothing. + +## Scenario 2 — sharing (FR-006, FR-007; SC-003) + +1. With a threshold set, copy the address. It carries `interactorScore`. +2. Open it in a new session: the same interactors are shown. +3. Set the threshold back to 0.45 and look at the address — `interactorScore` is + **gone**, because a value equal to its initial is omitted. A default nobody + chose does not belong in a reader's address. +4. Hand-edit it to `interactorScore=banana` and reload: the pathway and its + interactors still open, at 0.45 (FR-008). + +## Scenario 3 — resource memory (FR-004a) + +1. Set a threshold on one resource. +2. Switch resource, set a different one. +3. Switch back: the first resource's threshold is the one in force, not the + second's. + +## Scenario 4 — the download (FR-009, FR-010; SC-004) + +1. With a threshold hiding some interactions, download. +2. The file is `Interactors [] [].tsv`. +3. Its row count equals the interactor count on the graph — not the count the + resource returned. That equality is the assertion; a file that merely exists + is not evidence. +4. Every row carries a score, and none is below the threshold. + +## Gates + +```bash +npm test && npm run check:types && npm run check:lint && npm run check:dead && npm run format:check +npm run e2e -- e2e/interactor-threshold.spec.ts +``` + +## Before this is done + +`RELEASE-TESTING.md:117` and `:118` move from **missing** to **auto**, each naming +`e2e/interactor-threshold.spec.ts`, and the two matching gaps leave +`CURATOR-REPORT.md:225-229`. A row may only be called **auto** once a named spec +asserts it. diff --git a/specs/001-interactor-confidence-filter/research.md b/specs/001-interactor-confidence-filter/research.md new file mode 100644 index 00000000..faf2eaa9 --- /dev/null +++ b/specs/001-interactor-confidence-filter/research.md @@ -0,0 +1,123 @@ +# Research: Interactor confidence filtering and download + +Everything below was read out of the code or measured against a running service. +Nothing here is inferred from the feature description. + +## 1. The score is already there, and it is 0–1 + +`/ContentService/interactors/static/molecule/{acc}/details` returns a `score` on +every interaction. Measured against the local ContentService for **Q13158** +(FADD): **33 interactions, scores 0.482 – 0.98**. + +`interactors-table.component.ts:62` already lists `score` among its displayed +columns, so the number is one curators have seen before. + +**Decision**: no new data source and no service change for the data itself. + +## 2. The threshold is per resource, and its default is 0.45 + +Read from `reactome/pwp-diagram`, +`src/main/java/org/reactome/web/diagram/data/InteractorsContent.java`: + +```java +static final double DEFAULT_SCORE = 0.45; +static Map interactorsThreshold = new HashMap<>(); +``` + +Keyed by **resource**, not by entity and not globally. + +This app already models resources: `InteractorService.currentResource = +signal(…)` with `ResourceType` STATIC / PSICQUIC / CUSTOM +(`projects/pathway-browser/src/app/interactors/`). + +**Decision**: one threshold in force at a time, belonging to the current +resource. **Alternative rejected**: a global threshold — it would carry a score +from IntAct across to a resource where the number means something else. + +## 3. The interactors are already in memory + +`InteractorService.addInteractorNodes()` reads +`occurrenceNode.data('interactors')` — the fetched array is stored on the +cytoscape node that owns it. + +**Decision**: filtering and export both read the graph. Neither needs a request. + +**This corrects an assumption carried into planning.** `FileDownloadService` and +`ManagedDownloadDirective` were suggested for the download, but they exist for +_server_ downloads — progress, cancellation, a 180s ceiling, a failure reason +from an HTTP response. An in-memory export has none of those states: there is +nothing to wait for and nothing to fail. + +**Consequence for the spec**: FR-011 ("MUST tell the curator while the download +is being prepared, and MUST tell them if it fails") is close to vacuous for a +synchronous export. It is kept only in the weaker, honest form — the export must +not silently produce an empty or partial file — and the plan records why the +progress machinery is not reused. Using it anyway would add a spinner that never +spins. + +## 4. The file format: TSV, matching the existing participant export + +Both formats exist in this repo, so the question is which precedent applies: + +| Export | Format | Produced by | +| ------------------------------------------------------------------ | ------- | ------------------------------------------- | +| `molecule-download-table.component.ts` — participating molecules | **TSV** | client-side, from a table already on screen | +| `download-tab.component.ts` — analysis results, mapping, not-found | CSV | the analysis service, server-side | +| `idg-page.component.ts` | CSV | client-side | + +The interactor download is the first kind: a table already on screen, exported +client-side, from the details panel. `molecule-download-table` is its direct +analogue and produces +`Participating Molecules [R-HSA-109606].tsv` with `text/tab-separated-values`. + +**Decision**: TSV, named for the entity and resource it came from. **Rationale**: +scores and gene names are safe in TSV without quoting, curators already receive +one TSV from this panel, and matching the neighbouring feature beats matching the +server-generated ones. + +**One thing not to copy from it**: it does +`a.href = URL.createObjectURL(blob)` and never revokes. `FileDownloadService` +revokes after 60s, and the new export should do the same. + +## 5. A threshold that survives a reload has to be in the URL + +`UrlStateService` (`projects/pathway-browser/src/app/services/url-state.service.ts`): + +- params are declared in `values` via `urlParam(initialValue, type, otherTokens?, otherTransform?)`; +- the reader **resets any param the URL does not mention** to its initial value; +- the writer replaces the whole query string when state settles. + +So a signal alone is undone within the same turn — demonstrated twice while +mapping `#TOOL=AT` in #191. + +**Decision**: a `number` param, initial value `0.45`. Because +`currentQueryParams()` omits any value equal to its `initialValue`, a threshold +left at the default is **absent from the URL** — which is exactly FR-007 ("MUST +apply the default and MUST NOT be rewritten to name it"), obtained from the +existing mechanism rather than from special-casing. + +**Consequence**: the default cannot differ per resource without a second +mechanism, because a `urlParam` has one initial value. Per-resource _memory_ +(FR-004a) therefore lives in the service — a `Map` mirroring the +old browser — while the URL always carries the threshold in force. Switching +resource writes that resource's remembered threshold into the same param. + +**Alternative rejected**: one param per resource (`?threshold.IntAct=`). It +multiplies params, cannot be typed, and no shared link would survive a resource +rename. + +## 6. Where the control goes + +FR-001 says beneath the diagram. The analysis form already occupies that region +as a `.dropdown` inside the `as-split-area` in `viewport.component.html`, opened +from `dropdown()`. The interactor control is not a dropdown — it is visible +whenever interactors are shown — so it is a sibling of the diagram, not another +dropdown state. + +**Decision**: a `cr-interactor-threshold` component rendered in the viewport under +the diagram, with `@if` on "interactors are currently shown". + +**Open, and deliberately left to implementation**: what exactly "interactors are +currently shown" reads from. The candidates are `currentResource()` being +non-null and the graph holding interactor nodes; the second is what the reader +can see, so it is the one to prefer under constitution principle I. diff --git a/specs/001-interactor-confidence-filter/tasks.md b/specs/001-interactor-confidence-filter/tasks.md new file mode 100644 index 00000000..e68105e3 --- /dev/null +++ b/specs/001-interactor-confidence-filter/tasks.md @@ -0,0 +1,141 @@ +# Tasks: Interactor confidence filtering and download + +**Feature**: `specs/001-interactor-confidence-filter` | **Plan**: [plan.md](./plan.md) + +Three independently deliverable slices, in priority order. **US1 alone closes the +blocked checklist row** and is the MVP; US2 and US3 each add value without +needing the next. + +## How to read the red-first tasks + +Constitution principle III: a test never seen fail describes the fix rather than +guarding it. Both checklist rows are currently **missing**, so every e2e case +here can be shown red simply by running it against `main` before the code exists. +Each such task says to record the failure text in the commit. _Recorded_ means +pasted, not asserted. + +--- + +## Phase 1: Setup + +- [ ] T001 Create the feature directory `projects/pathway-browser/src/app/interactors/interactor-threshold/` per plan.md's structure decision +- [ ] T002 Capture the worked-entity baseline: run the `curl` in [quickstart.md](./quickstart.md) against the local ContentService and record the interaction count and score range for Q13158 in the implementation commit, so every later figure is one measured today rather than copied from this plan + +--- + +## Phase 2: Foundational (blocks every story) + +**Nothing in Phase 3+ can be asserted until the graph can be counted and the +threshold exists.** + +- [ ] T003 [P] Add `clampThreshold(raw: unknown): number` to `projects/pathway-browser/src/app/interactors/interactor-threshold.ts` — returns 0.45 for anything unparseable, negative or above 1, per data-model.md's clamping rule +- [ ] T004 [P] Add `passesThreshold(interaction, threshold): boolean` to the same file — `score >= threshold`, and **an interaction with no score is hidden**, per data-model.md's validation rule +- [ ] T005 Add `interactor-threshold.spec.ts` beside it covering both: the clamp against the hand-edited addresses FR-008 names (`banana`, `-1`, `2`, `''`), and the missing-score rule. Unit-testable without a browser, the pattern `url-state.service.spec.ts` uses +- [ ] T006 Add `interactorScore: urlParam(0.45, 'number')` to the `values` object in `projects/pathway-browser/src/app/services/url-state.service.ts`, with a comment citing `DEFAULT_SCORE` in `InteractorsContent.java` as the source of 0.45 and noting that FR-007 comes from `currentQueryParams()` skipping a value equal to its initial +- [ ] T007 Expose it as `public readonly interactorScore = this.values.interactorScore;` beside the other params in the same file + +**Checkpoint**: `npm test` green, `check:types` green. `check:dead` will complain +about the two new exports until T009 imports them — expected, and resolved within +the same story rather than by raising the baseline. + +--- + +## Phase 3: User Story 1 — narrow the interactors on the diagram (P1) 🎯 MVP + +**Goal**: raising the threshold removes interactions below it from the diagram, +lowering it brings them back. + +**Independent test**: open a pathway, show interactors for Q13158's entity, count +interactor nodes on the graph, raise the threshold, count again. + +- [ ] T008 [US1] Write `e2e/interactor-threshold.spec.ts` with the US1 cases from [quickstart.md](./quickstart.md) Scenario 1, asserting on `cy.elements('.interactor').length` — **not** on the control. Run it against `main` first and record the failure text in the commit +- [ ] T009 [US1] In `projects/pathway-browser/src/app/interactors/services/interactor.service.ts`, apply `passesThreshold` when drawing interactor nodes, reading the threshold from `UrlStateService.interactorScore` +- [ ] T010 [US1] Make a threshold change restyle rather than refetch or re-layout — toggle visibility on elements already on the graph, per plan.md's performance goal (SC-005) +- [ ] T011 [P] [US1] Create `interactor-threshold.component.ts/.html/.scss` in the directory from T001: a Material slider `min=0 max=1`, a `[data-threshold]` attribute carrying the value in force, and the empty state FR-012 requires +- [ ] T012 [US1] Render it in `projects/pathway-browser/src/app/viewport/viewport.component.html` beneath the diagram, with `@if` on interactors being shown; add the backing field to `viewport.component.ts` +- [ ] T013 [US1] Decide "interactors are shown" by reading the graph for interactor nodes, not `currentResource()` — research.md §6 leaves this open and principle I settles it +- [ ] T014 [US1] Verify SC-002 by hand against the quickstart's worked entity: the count on the graph equals the number of interactions at or above each threshold tried + +**Checkpoint**: US1 is shippable. `RELEASE-TESTING.md:117` can move to **auto**. + +--- + +## Phase 4: User Story 2 — keep and share the view (P2) + +**Goal**: the threshold survives a reload and travels in a shared address. + +**Independent test**: set a threshold, copy the address, open it fresh, count. + +- [ ] T015 [US2] Add the US2 cases from quickstart Scenario 2 to `e2e/interactor-threshold.spec.ts`, including the **absence** assertion — at 0.45 the address must not name `interactorScore` — and the `banana` case from FR-008. Run against `main` first and record the failure +- [ ] T016 [US2] Wire the control's changes through `UrlStateService.interactorScore` rather than local component state, so the URL is the source of truth (research.md §5) +- [ ] T017 [US2] Apply `clampThreshold` when reading the param, so a hand-edited address opens the pathway rather than blocking it (FR-008) +- [ ] T018 [US2] Add the per-resource `Map` to `interactor.service.ts` and write the remembered value into the param when the resource changes (FR-004a) +- [ ] T019 [US2] Add the resource-memory case from quickstart Scenario 3 to the e2e spec +- [ ] T020 [US2] Clear the param when interactors are cleared (FR-013), and assert it in the e2e spec + +**Checkpoint**: US1 + US2 shippable together. + +--- + +## Phase 5: User Story 3 — take the interactors away (P3) + +**Goal**: download exactly what is on the diagram. + +**Independent test**: filter, download, compare the file's row count with the +graph's interactor count. + +- [ ] T021 [P] [US3] Add `interactorsToTsv(rows): string` to `projects/pathway-browser/src/app/interactors/interactor-export.ts` — header and column order from data-model.md's Export row +- [ ] T022 [P] [US3] Add `interactor-export.spec.ts`: header order, one row per interaction, a tab-free field, and an empty set producing a header and nothing else +- [ ] T023 [US3] Add the US3 cases from quickstart Scenario 4 to the e2e spec, asserting the **row count equals the graph's interactor count**. Run against `main` first and record the failure +- [ ] T024 [US3] Add the download control beside the threshold control in the component from T011, naming the file `Interactors [] [].tsv` +- [ ] T025 [US3] Revoke the object URL after use — the existing `molecule-download-table` export does not, and research.md §4 says not to copy that + +**Checkpoint**: all three stories shippable. `RELEASE-TESTING.md:118` can move to +**auto**. + +--- + +## Phase 6: Polish and the documents + +- [ ] T026 Move `RELEASE-TESTING.md:117` from **missing** to **auto**, naming `e2e/interactor-threshold.spec.ts`. Do not mark it auto unless that spec actually asserts it +- [ ] T027 Move `RELEASE-TESTING.md:118` from **missing** to **auto**, same spec, same condition +- [ ] T028 Remove the two matching gaps from `CURATOR-REPORT.md:225-229` — "No confidence threshold for interactors" and "No interactor download" +- [ ] T029 Run the full gates: `npm test && npm run check:types && npm run check:lint && npm run check:dead && npm run format:check && npm run e2e`. `check:lint` must not exceed 653 and `check:dead` must not exceed 146; if either rises, fix the cause rather than the baseline +- [ ] T030 Rebuild beta from the merged main and confirm the feature through Apache, not only on localhost — the dev server and beta have disagreed before + +--- + +## Dependencies + +``` +Setup (T001-T002) + └─> Foundational (T003-T007) ← blocks everything + ├─> US1 (T008-T014) ← MVP, closes RELEASE-TESTING:117 + │ └─> US2 (T015-T020) needs the control to exist + │ └─> US3 (T021-T025) needs a filtered set to export + └─> Polish (T026-T030) +``` + +US2 depends on US1 only for the control; US3 depends on US2 only in that its +"exactly what is shown" assertion is more meaningful once filtering is shareable. +Each is independently demonstrable. + +## Parallel opportunities + +- T003 and T004 — same file, different functions, no shared state +- T011 alongside T009/T010 — the component's markup does not depend on the service change +- T021 and T022 — pure module and its spec +- T026, T027, T028 — three documents, no overlap + +## MVP + +**US1 alone.** It closes the row a curator is blocked on, and a threshold that +does not survive a reload is still a threshold that works. Ship it before US2 if +time is short. + +## The ratchets + +`check:dead` counts an exported symbol nothing imports. T003, T004 and T021 each +add one, and each is imported within its own story — so run `check:dead` at the +**story** checkpoint, not after every task, and never raise the baseline to make +it pass.