@datagrok/proteomics 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +18 -5
  3. package/dist/package-test.js +1 -1
  4. package/dist/package-test.js.map +1 -1
  5. package/dist/package.js +1 -1
  6. package/dist/package.js.map +1 -1
  7. package/docs/personas-and-capabilities.md +16 -0
  8. package/docs/publishing-design-rationale.md +140 -0
  9. package/package.json +20 -5
  10. package/package.png +0 -0
  11. package/src/analysis/imputation.ts +3 -1
  12. package/src/analysis/normalization.ts +9 -4
  13. package/src/analysis/spc-storage.ts +1 -1
  14. package/src/menu.ts +5 -2
  15. package/src/package-api.ts +10 -2
  16. package/src/package-test.ts +3 -1
  17. package/src/package.g.ts +17 -3
  18. package/src/package.ts +31 -15
  19. package/src/panels/published-analysis-panel.ts +2 -2
  20. package/src/publishing/assert-published-shape.ts +2 -2
  21. package/src/publishing/package-settings-editor.ts +125 -0
  22. package/src/publishing/publish-project.ts +8 -8
  23. package/src/publishing/publish-settings.ts +57 -3
  24. package/src/publishing/publish-state.ts +45 -16
  25. package/src/publishing/reviewer-groups.ts +24 -0
  26. package/src/publishing/share-dialog.ts +42 -27
  27. package/src/publishing/trim-dataframe.ts +8 -1
  28. package/src/tests/abundance-correlation.ts +135 -0
  29. package/src/tests/analysis.ts +63 -0
  30. package/src/tests/enrichment-visualization.ts +95 -9
  31. package/src/tests/enrichment.ts +2 -1
  32. package/src/tests/project-vocabulary.ts +39 -0
  33. package/src/tests/publish-roundtrip.ts +40 -40
  34. package/src/tests/publish-spike.ts +2 -2
  35. package/src/tests/rank-abundance.ts +85 -0
  36. package/src/utils/abundance-detection.ts +145 -0
  37. package/src/viewers/abundance-correlation.ts +208 -0
  38. package/src/viewers/enrichment-viewers.ts +58 -24
  39. package/src/viewers/rank-abundance.ts +153 -0
  40. package/src/viewers/volcano.ts +3 -0
  41. package/test-console-output-1.log +1145 -1090
  42. package/test-record-1.mp4 +0 -0
  43. package/src/tests/group-mean-correlation.ts +0 -139
  44. package/src/viewers/group-mean-correlation.ts +0 -218
@@ -15,6 +15,12 @@ boundary to be real, not conventional.
15
15
  | **Proteomics analyst** (data generator) | Builds the analysis: import → annotate → normalize → impute → differential expression | A defensible, reproducible, FDR-controlled result they can stand behind and hand off |
16
16
  | **Biology scientist** (data consumer / reviewer) | Interprets a finished analysis: volcano, enrichment, per-protein annotation | To understand *what changed and what it means* — without touching (or being able to change) the analysis itself |
17
17
 
18
+ A third role sits **outside** this pair: the **package administrator** (deployment owner)
19
+ configures the Share-for-review governance — the controlled list of projects analysts may
20
+ share under, the default reviewer team, and the review Spaces (see
21
+ [Configuration](../README.md#configuration)). They neither build nor interpret analyses; they
22
+ set, once per deployment, *where* shared results land and *under what project names*.
23
+
18
24
  The dividing line is **"the interpretation"** — the set of decisions that determine what the
19
25
  result *says*: group assignment, normalization, imputation, the DE method, and the
20
26
  significance thresholds. The analyst owns these. The biologist reads the consequences and
@@ -85,6 +91,13 @@ group has any Edit / Share / Delete right at the project, child-space, or umbrel
85
91
  level. So through the **shared snapshot**, a biologist genuinely cannot change the
86
92
  interpretation — they get a read-only copy.
87
93
 
94
+ The dialog itself is **governed by package settings the administrator maintains** (Plugins →
95
+ Proteomics → Settings). The analyst files the snapshot under a **project** chosen from an
96
+ admin-curated controlled vocabulary — they cannot invent project names, and cannot share at
97
+ all until an admin has populated the list — and it goes to a reviewer **team** whose default
98
+ the admin can preset. This keeps shared analyses landing under approved project names, in the
99
+ intended teams and Spaces, rather than letting each analyst decide that governance ad hoc.
100
+
88
101
  ## The gap
89
102
 
90
103
  Enforcement today is **data-shape + access**, not **identity**. Concretely:
@@ -163,3 +176,6 @@ an explicit read-only "Reviewer mode" toggle vs. inferring it from group members
163
176
  - `src/menu.ts` — `sampleLevelDisabledReason`, the `sampleOnly` gate
164
177
  - `src/package.ts` — `requireSampleLevelData`, the handler-level guards
165
178
  - `src/publishing/` — the Share for Review view-only publish flow
179
+ - `src/publishing/package-settings-editor.ts` — the administrator's settings editor
180
+ (project vocabulary, default reviewer team)
181
+ - [`README.md` → Configuration](../README.md#configuration) — the admin-configurable package settings
@@ -0,0 +1,140 @@
1
+ # Publishing (Share for Review) — design rationale
2
+
3
+ Why the Share-for-review subsystem is built the way it is, and **which parts are permanent
4
+ architecture vs. workaround weight that the base platform could absorb.**
5
+
6
+ This is the reference behind the [personas & capabilities](./personas-and-capabilities.md) doc's
7
+ enforcement claims. That doc says *what* the read-only boundary is; this says *why the code takes
8
+ the shape it does to make that boundary real* — and where we'd rather the platform did the work.
9
+
10
+ Audience: anyone questioning the complexity of `src/publishing/`, and anyone planning what
11
+ platform capabilities to grow so this package can shed code. Per our standing preference, a
12
+ workaround defended as *temporary, with a named platform successor* is a stronger position than
13
+ one defended as permanent design.
14
+
15
+ ## The one-sentence defense
16
+
17
+ Most of `src/publishing/` (~1,900 lines across 9 files) is not "sharing." It is the machinery to
18
+ **guarantee three properties** a compliance-sensitive review needs, none of which the platform
19
+ gave us for free at build time:
20
+
21
+ 1. **Provably read-only** — the reviewer group *cannot* alter the interpretation.
22
+ 2. **Provably minimized** — only the interpretation ships; raw/again-identifying data does not.
23
+ 3. **Provably reload-survivable + versioned** — what the reviewer opens is what was published,
24
+ every version is immutable, and supersession is auditable.
25
+
26
+ The naming scheme and the use of Spaces (the two most visible choices) are *downstream* of these
27
+ guarantees, not the point of them.
28
+
29
+ ## Design choices, grouped by the guarantee they serve
30
+
31
+ Legend for **Verdict**: 🟢 keep in package (genuinely Proteomics-specific) · 🟡 push to platform
32
+ (generic capability we had to hand-roll) · 🔵 hybrid (platform does the mechanism, package keeps the policy)
33
+
34
+ ### A. Read-only is enforced, not assumed
35
+
36
+ | # | Choice | Where | Why | Verdict |
37
+ |---|---|---|---|---|
38
+ | A1 | Grant **View directly on the Project**, not via Space inheritance | `publish-project.ts:323` | Spike A2: `permissions.get(project)` does **not** surface Space-inherited grants, so an inherited grant is unverifiable. | 🟡 |
39
+ | A2 | **Verify-and-rollback ACL gate** — after granting, re-read permissions across three rings (project, child Space, umbrella Space); any `edit`/`share`/`delete` for the reviewer group ⇒ delete the project and abort | `publish-project.ts:331-370` | The feature refuses to ship what it cannot prove is read-only. This is the single most important — and most expensive — thing the code does. | 🔵 |
40
+ | A3 | Share to a **group/team, never individuals**; eligible groups exclude hidden, personal, and "All users" | `reviewer-groups.ts:9-19` | Review is a team activity; "All users" would defeat the point of a scoped share. | 🔵 |
41
+
42
+ ### B. Data minimization — a trimmed snapshot, not a live link
43
+
44
+ | # | Choice | Where | Why | Verdict |
45
+ |---|---|---|---|---|
46
+ | B1 | **Deep clone; source never mutated** — the publish path operates only on the clone | `trim-dataframe.ts:75-120` | Reviewers get a frozen point-in-time copy; later edits to the working analysis cannot leak. | 🟢 |
47
+ | B2 | **Column allowlist** (Protein ID, Gene, log2FC, p, adj.p, significant, [direction, quantities]); everything else dropped | `trim-dataframe.ts:108-120` | The generalized form of the 1.2.0 "client-name removal." A *whitelist* means a new, unexpected column is excluded by default — safer than a blocklist. | 🟢 |
48
+
49
+ ### C. Provenance is embedded and redundant
50
+
51
+ | # | Choice | Where | Why | Verdict |
52
+ |---|---|---|---|---|
53
+ | C1 | Rich metadata baked into the snapshot (who/email/DE method/thresholds/version/UUID/date) | `publish-state.ts:71-92`, `trim-dataframe.ts:158-174` | The snapshot is self-describing — provenance travels with the data, not a side channel. | 🟢 |
54
+ | C2 | **Dual-write belt-and-braces** — every field written as *both* a DataFrame tag *and* a hidden `_meta_*` column; read column-first, tag-second | `publish-state.ts:24-60,137-225` | Defends against serialization paths that strip tags but keep columns (or vice versa) — an observed failure mode. | 🟡 |
55
+ | C3 | Numeric metadata stored **as strings** | `trim-dataframe.ts:153-167` | `addNewFloat` is Float32; string storage preserves exact double thresholds. | 🔵 |
56
+
57
+ ### D. Versioning is first-class and non-destructive
58
+
59
+ | # | Choice | Where | Why | Verdict |
60
+ |---|---|---|---|---|
61
+ | D1 | **Opinionated naming** `Proteomics-Review-<slug>-v<N>-<date>`; structural suffix fixed (not configurable) | `publish-project.ts:244-250`, `publish-settings.ts` | The `-v<N>-` segment is parsed to detect prior versions; letting it vary would break version detection. Prefix/Space *are* configurable. | 🔵 |
62
+ | D2 | **Republish detection** by name pattern → bump to v(N+1); prior version stays available (never overwritten) | `publish-state.ts:279-328`, `publish-project.ts:138-140` | Immutable, auditable version history. | 🔵 |
63
+ | D3 | **Bidirectional supersede pointers** (`supersedes`/`supersededBy`) on both projects, dual-written to project options *and* the DataFrame | `publish-project.ts:436-478` | Either version can be navigated to the other; survives whichever persistence path holds. | 🟡 |
64
+
65
+ ### E. Round-trip fidelity is verified
66
+
67
+ | # | Choice | Where | Why | Verdict |
68
+ |---|---|---|---|---|
69
+ | E1 | **Reopen-and-assert gate** (default on) — `closeAll` → reopen → assert published shape survives a reload; rollback on failure | `publish-project.ts:372-434`, `assert-published-shape.ts` | Proves the reviewer sees exactly what was published, before telling the analyst it worked. | 🟡 |
70
+ | E2 | **Formula-line self-heal** — a serializer strips volcano threshold lines; re-apply from tags and re-check once before rollback | `publish-project.ts:401-433`, `post-open-recovery.ts` | Works around look-config loss on project round-trip (Phase 13 evidence). | 🟡 |
71
+ | E3 | **Re-render viewers on the trimmed snapshot** so the shared dashboard is self-contained | `publish-project.ts:265-295` | The published project must stand alone — no dependency on the analyst's session tables. | 🔵 |
72
+ | E4 | **Post-open recovery autostart** — re-applies formula lines and re-wires enrichment↔volcano cross-highlight on reopen | `post-open-recovery.ts`, `package.ts:1010-1037` | Interaction subscriptions and look config don't survive serialization; rebuild them on open. | 🟡 |
73
+
74
+ ### F. Where content lands + idempotency
75
+
76
+ | # | Choice | Where | Why | Verdict |
77
+ |---|---|---|---|---|
78
+ | F1 | Publish to **named Spaces, not a personal workspace** | `publish-project.ts:178-239` | A shared, governed location the reviewer team can browse — not tied to one user's private space. | 🔵 |
79
+ | F2 | **Two-level Space hierarchy** — umbrella `Proteomics-Reviews` + per-project child Space | `publish-project.ts:178-239` | Groups all reviews under one root, one child per project — a navigable tree instead of a flat pile. | 🔵 |
80
+ | F3 | **Create-first-then-enumerate idempotency** for Spaces; fall back to `spaces.list()` on "already exists" | `publish-project.ts:184-238` | The smart-filter API does not reliably surface Space-flagged Projects, so enumeration is the reliable lookup. | 🟡 |
81
+
82
+ ### G. Governance + workflow UX
83
+
84
+ | # | Choice | Where | Why | Verdict |
85
+ |---|---|---|---|---|
86
+ | G1 | **Controlled project vocabulary** — analyst picks from an admin-maintained list, never free-types | `share-dialog.ts:57-73`, `publish-settings.ts` | Keeps shared results filed under sanctioned project names; a governance control. | 🟢 |
87
+ | G2 | **Precondition gate** — share only available after DE completes | `share-dialog.ts:31-34` | There is no interpretation to review until DE has run. | 🟢 |
88
+ | G3 | **Reactive confirmation summary** — exact resulting name/team/supersede status shown before OK | `share-dialog.ts:109-151` | No surprises; the analyst sees the outcome before committing. | 🟢 |
89
+ | G4 | **Workspace restoration** after publish — tear down verification artifacts, restore the analyst's original view | `publish-project.ts:480-506` | The verify/supersede steps `closeAll` repeatedly; put the analyst back where they were. | 🔵 |
90
+ | G5 | **Full package-settings configurability** (Space, prefix, default team, vocabulary, verify default) | `publish-settings.ts` | Retarget a deployment without a code change. | 🟢 |
91
+
92
+ ## Mapping to platform capabilities
93
+
94
+ Reading the 🟡/🔵 rows top-down, the workaround weight collapses to a small number of platform
95
+ gaps. If the platform closed these, the package could delete or thin most of `src/publishing/`.
96
+
97
+ | Platform gap (observed) | Evidence in code | What a platform capability would give us | Package code it retires |
98
+ |---|---|---|---|
99
+ | **P1. No first-class "publish read-only snapshot to a group" primitive** | The entire orchestrator exists to compose this from lower-level parts | A single `publishReadOnly(entity, group, {snapshot, immutable})` that grants view-only, freezes, and returns a stable handle | Most of `publish-project.ts` (A1, A2, F1–F3) |
100
+ | **P2. `permissions.get` doesn't reflect Space-inherited grants; may not report share/delete** | Spike A1/A2 notes at `publish-project.ts:113-118,331-370` | A permissions read that returns the *effective* ACL (own + inherited) across all verbs | A2 three-ring check collapses to one `get`; A1 direct-grant workaround unneeded |
101
+ | **P3. Serialization strips look config (formula lines) and interaction subscriptions** | E2/E4; `post-open-recovery.ts`, `assert-published-shape.ts:11-17` | Viewer look + formula lines + declared subscriptions persist across `save→open` | E2, E4 self-heal and recovery hook; part of E1's assertions |
102
+ | **P4. Tag map / metadata not reliably durable across `clone`/`save`** | C2 dual-write; `publish-state.ts:227-255`, `trim-dataframe.ts:20` | Durable, typed entity/DataFrame metadata that survives clone and round-trip | C2 `_meta_*` columns; the column-first/tag-second reader in `getPublishedMetadata` |
103
+ | **P5. Smart-filter can't reliably find Space-flagged Projects** | F3 note at `publish-project.ts:184-203` | `dapi.projects.filter(isSpace=…).first()` that actually works | F3 create-first-then-enumerate dance |
104
+ | **P6. No immutable/versioned entity semantics** | D1–D3 name-parsing + manual supersede pointers | Native entity versioning with supersede links | D2/D3 name-pattern versioning and dual-written pointers; D1's suffix could relax |
105
+ | **P7. No round-trip integrity check** | E1; `assert-published-shape.ts` | If P3+P4 land, publish *is* faithful by construction and E1 becomes optional | E1 gate downgrades to a test-only assertion |
106
+
107
+ ### What stays in the package regardless
108
+
109
+ The 🟢 rows are the genuine Proteomics policy and don't belong in the platform:
110
+
111
+ - **B1/B2** — *which* columns constitute "the interpretation" (the allowlist) is domain knowledge.
112
+ - **C1** — *which* provenance fields matter (DE method, FC/p thresholds) is domain knowledge.
113
+ - **G1/G2/G3/G5** — the vocabulary, the DE precondition, the confirmation copy, the settings surface.
114
+
115
+ Even under a fully-featured platform, the package would still call a `publishReadOnly` primitive,
116
+ hand it a domain-trimmed snapshot with domain provenance, and gate the UI on DE completion. That
117
+ residue is small — roughly `trim-dataframe.ts` + `share-dialog.ts` + `publish-settings.ts`.
118
+
119
+ ## Recommended platform asks, prioritized
120
+
121
+ 1. **P1 + P2 together** — the read-only-to-group primitive with a trustworthy effective-permissions
122
+ read. Highest leverage: retires the bulk of the orchestrator *and* the code we're least confident
123
+ in (the ACL gate can only be as correct as `permissions.get`).
124
+ 2. **P3 + P4** — durable look/subscription/metadata across round-trip. Retires the self-heal and
125
+ recovery machinery, which is the most brittle code in the subsystem.
126
+ 3. **P6** — native immutable versioning. Retires the name-parsing version scheme and lets the naming
127
+ suffix relax.
128
+ 4. **P5, P7** — fall out naturally once the above land.
129
+
130
+ ## Honest caveats to raise proactively
131
+
132
+ - **The ACL gate is only as strong as `permissions.get`.** If P2 is real (the API may not report
133
+ `share`/`delete`), the three-ring check at `publish-project.ts:358-361` cannot detect those leaks
134
+ today. Worth verifying against the current platform release before claiming the guarantee is
135
+ airtight. *(This claim is inferred from the A1 spike note and should be re-checked live.)*
136
+ - **Several verify/rollback steps `closeAll` the user's session.** G4 restoration mitigates it, but
137
+ it's inherent to doing round-trip verification in the same client session; P7 landing would remove
138
+ the need entirely.
139
+ - **This is workaround-heavy by design, and that's defensible** *because* each workaround has a named
140
+ platform successor above — not because the complexity is intrinsic to sharing.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datagrok/proteomics",
3
3
  "friendlyName": "Proteomics",
4
- "version": "1.2.0",
4
+ "version": "1.3.0",
5
5
  "author": {
6
6
  "name": "Ed Jaeger",
7
7
  "email": "ejaeger@datagrok.ai"
@@ -17,8 +17,8 @@
17
17
  "@datagrok-libraries/math": "^1.2.6",
18
18
  "@datagrok-libraries/ml": "^6.10.7",
19
19
  "@datagrok-libraries/statistics": "^1.2.12",
20
- "@datagrok-libraries/utils": "^4.6.9",
21
20
  "@datagrok-libraries/test": "^1.1.0",
21
+ "@datagrok-libraries/utils": "^4.6.9",
22
22
  "cash-dom": "^8.1.5",
23
23
  "datagrok-api": "^1.25.0",
24
24
  "rxjs": "^6.5.5",
@@ -61,27 +61,42 @@
61
61
  "properties": [
62
62
  {
63
63
  "name": "reviewSpaceName",
64
- "description": "Umbrella Space that shared review snapshots are published under. Each target gets a child Space inside it.",
64
+ "description": "Umbrella Space that shared review snapshots are published under. Each project gets a child Space inside it.",
65
65
  "propertyType": "string",
66
66
  "defaultValue": "Proteomics-Reviews",
67
67
  "nullable": false
68
68
  },
69
69
  {
70
70
  "name": "reviewNamePrefix",
71
- "description": "Prefix for shared review project names. The structural '-<target>-v<version>-<date>' suffix stays fixed so re-share version detection keeps working.",
71
+ "description": "Prefix for shared review project names. The structural '-<project>-v<version>-<date>' suffix stays fixed so re-share version detection keeps working.",
72
72
  "propertyType": "string",
73
73
  "defaultValue": "Proteomics-Review",
74
74
  "nullable": false
75
75
  },
76
+ {
77
+ "name": "defaultReviewerGroup",
78
+ "description": "Team pre-selected in the Share for Review dialog's 'Share with team' picker. Leave blank to default to the first available team.",
79
+ "propertyType": "string",
80
+ "defaultValue": "",
81
+ "nullable": true
82
+ },
76
83
  {
77
84
  "name": "verifyPublishedDashboard",
78
85
  "description": "After publishing, re-open the shared dashboard to verify it survives a reload. Safer for client deliverables; turn off for faster demo shares.",
79
86
  "propertyType": "bool",
80
87
  "defaultValue": true,
81
88
  "nullable": false
89
+ },
90
+ {
91
+ "name": "projectVocabulary",
92
+ "description": "Comma-separated controlled list of project names an analyst can share under. Package administrators add, edit, or remove names here; the Share for Review dialog offers exactly these as a dropdown (no free text).",
93
+ "propertyType": "string",
94
+ "defaultValue": "",
95
+ "nullable": true
82
96
  }
83
97
  ],
84
98
  "overrides": {
85
- "datagrok-api": "$datagrok-api"
99
+ "datagrok-api": "$datagrok-api",
100
+ "uuid": "^11.1.1"
86
101
  }
87
102
  }
package/package.png ADDED
Binary file
@@ -7,7 +7,9 @@ import {getGroups} from './experiment-setup';
7
7
 
8
8
  /** Generates a random number from a normal distribution using Box-Muller transform. */
9
9
  function randomNormal(mean: number, stdev: number): number {
10
- const u1 = Math.random();
10
+ // Draw u1 in (0, 1] rather than [0, 1): Math.random() can return exactly 0,
11
+ // which would give log(0) = -Infinity and an Infinity/NaN draw.
12
+ const u1 = 1 - Math.random();
11
13
  const u2 = Math.random();
12
14
  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
13
15
  return mean + stdev * z;
@@ -59,7 +59,10 @@ export function quantileNormalize(df: DG.DataFrame, colNames: string[]): void {
59
59
 
60
60
  // Find the maximum number of non-null values across columns
61
61
  const maxValid = Math.max(...colData.map((d) => d.indices.length));
62
- if (maxValid === 0) return;
62
+ // Need at least 2 values in the most-populated column: the rank interpolation
63
+ // below divides by (maxValid - 1), and quantile normalization is undefined for
64
+ // a single quantile point.
65
+ if (maxValid < 2) return;
63
66
 
64
67
  // Compute rank means using scaled rank alignment
65
68
  // For each rank position r (0..maxValid-1), compute the mean of values
@@ -69,7 +72,7 @@ export function quantileNormalize(df: DG.DataFrame, colNames: string[]): void {
69
72
  let sum = 0;
70
73
  let count = 0;
71
74
  for (const d of colData) {
72
- if (d.indices.length === 0) continue;
75
+ if (d.indices.length < 2) continue;
73
76
  // Map rank r in [0..maxValid-1] to position in this column's sorted values
74
77
  const pos = (r / (maxValid - 1)) * (d.indices.length - 1);
75
78
  // Interpolate between floor and ceil positions
@@ -86,10 +89,12 @@ export function quantileNormalize(df: DG.DataFrame, colNames: string[]): void {
86
89
  // Replace each column's sorted values with the corresponding rank mean
87
90
  for (const d of colData) {
88
91
  const n = d.indices.length;
89
- if (n === 0) continue;
92
+ // n < 2: a single-value column has no rank spread; (r / (n - 1)) would be
93
+ // 0/0 = NaN. Leave its lone value unchanged rather than corrupting it.
94
+ if (n < 2) continue;
90
95
  for (let r = 0; r < n; r++) {
91
96
  // Map this column's rank to the global rank mean position
92
- const globalPos = maxValid === 1 ? 0 : (r / (n - 1)) * (maxValid - 1);
97
+ const globalPos = (r / (n - 1)) * (maxValid - 1);
93
98
  const lo = Math.floor(globalPos);
94
99
  const hi = Math.min(Math.ceil(globalPos), maxValid - 1);
95
100
  const frac = globalPos - lo;
@@ -111,7 +111,7 @@ function baselinePath(instrumentId: string, opts?: StorageOpts): string {
111
111
  }
112
112
 
113
113
  // =====================================================================================
114
- // Slugify — T-16-01 mitigation. Mirrors publishing/publish-state.ts:slugifyTarget but
114
+ // Slugify — T-16-01 mitigation. Mirrors publishing/publish-state.ts:slugifyProject but
115
115
  // PRESERVES case so instrument names like 'QExactive-01' round-trip exactly.
116
116
  // =====================================================================================
117
117
 
package/src/menu.ts CHANGED
@@ -40,10 +40,11 @@ export interface ProteomicsMenuHandlers {
40
40
  showVolcanoPlot: () => void;
41
41
  showHeatmap: () => void;
42
42
  showPcaPlot: () => void;
43
- showGroupMeanCorrelation: () => void;
44
43
  showQcDashboard: () => void;
45
44
  showSpcDashboard: () => void;
46
45
  enrichmentCharts: () => void;
46
+ rankAbundance: () => void;
47
+ abundanceCorrelation: () => void;
47
48
  showAllVisualizations: () => void;
48
49
  shareAnalysisForReview: () => void;
49
50
  }
@@ -121,10 +122,12 @@ export function buildProteomicsRibbonMenu(view: DG.TableView, h: ProteomicsMenuH
121
122
  viz.item('Volcano Plot...', h.showVolcanoPlot);
122
123
  viz.item('Heatmap...', h.showHeatmap, null, sampleOnly);
123
124
  viz.item('PCA...', h.showPcaPlot, null, sampleOnly);
124
- viz.item('Group-Mean Correlation...', h.showGroupMeanCorrelation, null, sampleOnly);
125
125
  viz.item('QC Dashboard...', h.showQcDashboard, null, sampleOnly);
126
126
  viz.item('SPC Dashboard...', h.showSpcDashboard);
127
127
  viz.item('Enrichment Charts...', h.enrichmentCharts);
128
+ // Not sampleOnly: both work on Candidates (AVG Group Quantity) and annotated Reports.
129
+ viz.item('Rank–Abundance', h.rankAbundance);
130
+ viz.item('Abundance Correlation', h.abundanceCorrelation);
128
131
  viz.item('Show All Visualizations...', h.showAllVisualizations, null, sampleOnly);
129
132
 
130
133
  const share = root.group('Share');
@@ -95,8 +95,8 @@ export namespace funcs {
95
95
  return await grok.functions.call('Proteomics:ShowPcaPlot', {});
96
96
  }
97
97
 
98
- export async function showGroupMeanCorrelation(): Promise<void> {
99
- return await grok.functions.call('Proteomics:ShowGroupMeanCorrelation', {});
98
+ export async function abundanceCorrelation(): Promise<void> {
99
+ return await grok.functions.call('Proteomics:AbundanceCorrelation', {});
100
100
  }
101
101
 
102
102
  export async function showQcDashboard(): Promise<void> {
@@ -119,6 +119,10 @@ export namespace funcs {
119
119
  return await grok.functions.call('Proteomics:ExportEnrichmentInputs', {});
120
120
  }
121
121
 
122
+ export async function rankAbundance(): Promise<void> {
123
+ return await grok.functions.call('Proteomics:RankAbundance', {});
124
+ }
125
+
122
126
  export async function enrichmentCharts(): Promise<void> {
123
127
  return await grok.functions.call('Proteomics:EnrichmentCharts', {});
124
128
  }
@@ -153,6 +157,10 @@ export namespace funcs {
153
157
  return await grok.functions.call('Proteomics:PublishedAnalysisPanelWidget', { proteinId });
154
158
  }
155
159
 
160
+ export async function proteomicsSettingsEditor(propList: any ): Promise<any> {
161
+ return await grok.functions.call('Proteomics:ProteomicsSettingsEditor', { propList });
162
+ }
163
+
156
164
  export async function recoverPublishedProjectsOnStartup(): Promise<void> {
157
165
  return await grok.functions.call('Proteomics:RecoverPublishedProjectsOnStartup', {});
158
166
  }
@@ -11,6 +11,8 @@ import './tests/enrichment-visualization';
11
11
  import './tests/spectronaut-parser';
12
12
  import './tests/spectronaut-candidates-parser';
13
13
  import './tests/spectronaut-candidates-e2e';
14
+ import './tests/rank-abundance';
15
+ import './tests/abundance-correlation';
14
16
  import './tests/fragpipe-parser';
15
17
  import './tests/fragpipe-e2e';
16
18
  import './tests/subcellular-location';
@@ -20,9 +22,9 @@ import './tests/volcano';
20
22
  import './tests/gene-label-resolver';
21
23
  import './tests/smart-pathway-filter';
22
24
  import './tests/uniprot-panel';
23
- import './tests/group-mean-correlation';
24
25
  import './tests/publish-spike';
25
26
  import './tests/publish-roundtrip';
27
+ import './tests/project-vocabulary';
26
28
  import './tests/spc-formula-lines-spike';
27
29
  import './tests/spc';
28
30
 
package/src/package.g.ts CHANGED
@@ -83,9 +83,9 @@ export async function showPcaPlot() : Promise<void> {
83
83
  await PackageFunctions.showPcaPlot();
84
84
  }
85
85
 
86
- //name: showGroupMeanCorrelation
87
- export async function showGroupMeanCorrelation() : Promise<void> {
88
- await PackageFunctions.showGroupMeanCorrelation();
86
+ //name: abundanceCorrelation
87
+ export async function abundanceCorrelation() : Promise<void> {
88
+ await PackageFunctions.abundanceCorrelation();
89
89
  }
90
90
 
91
91
  //name: showQcDashboard
@@ -113,6 +113,11 @@ export async function exportEnrichmentInputs() : Promise<void> {
113
113
  await PackageFunctions.exportEnrichmentInputs();
114
114
  }
115
115
 
116
+ //name: rankAbundance
117
+ export async function rankAbundance() : Promise<void> {
118
+ await PackageFunctions.rankAbundance();
119
+ }
120
+
116
121
  //name: enrichmentCharts
117
122
  export async function enrichmentCharts() : Promise<void> {
118
123
  await PackageFunctions.enrichmentCharts();
@@ -155,6 +160,15 @@ export function publishedAnalysisPanelWidget(proteinId: string) : any {
155
160
  return PackageFunctions.publishedAnalysisPanelWidget(proteinId);
156
161
  }
157
162
 
163
+ //name: Proteomics package settings editor
164
+ //tags: packageSettingsEditor
165
+ //input: object propList
166
+ //output: widget result
167
+ //meta.role: packageSettingsEditor
168
+ export async function proteomicsSettingsEditor(propList: any) : Promise<any> {
169
+ return await PackageFunctions.proteomicsSettingsEditor(propList);
170
+ }
171
+
158
172
  //tags: autostart
159
173
  //meta.autostartImmediate: true
160
174
  export async function recoverPublishedProjectsOnStartup() : Promise<void> {
package/src/package.ts CHANGED
@@ -30,15 +30,17 @@ import {createExpressionHeatmap} from './viewers/heatmap';
30
30
  import {createPcaPlot} from './viewers/pca-plot';
31
31
  import {openQcDashboard} from './viewers/qc-dashboard';
32
32
  import {openSpcDashboard} from './viewers/spc-dashboard';
33
- import {createGroupMeanCorrelation} from './viewers/group-mean-correlation';
33
+ import {openAbundanceCorrelation} from './viewers/abundance-correlation';
34
34
  import {uniprotPanel} from './panels/uniprot-panel';
35
35
  import {focusProtein} from './panels/protein-focus';
36
36
  import {publishedAnalysisPanel} from './panels/published-analysis-panel';
37
37
  import {showShareForReviewDialog} from './publishing/share-dialog';
38
+ import {proteomicsSettingsEditorWidget} from './publishing/package-settings-editor';
38
39
  import {recoverPublishedProject} from './publishing/post-open-recovery';
39
40
  import {isPublished} from './publishing/publish-state';
40
41
  import {showEnrichmentDialog} from './analysis/enrichment';
41
42
  import {openEnrichmentVisualization} from './viewers/enrichment-viewers';
43
+ import {openRankAbundance} from './viewers/rank-abundance';
42
44
  import {findColumn} from './utils/column-detection';
43
45
  import {SEMTYPE, DEFAULT_FC_THRESHOLD, DEFAULT_P_THRESHOLD} from './utils/proteomics-types';
44
46
  import {buildProteomicsRibbonMenu} from './menu';
@@ -341,10 +343,11 @@ export class PackageFunctions {
341
343
  showVolcanoPlot: () => PackageFunctions.showVolcanoPlot(),
342
344
  showHeatmap: () => PackageFunctions.showHeatmap(),
343
345
  showPcaPlot: () => PackageFunctions.showPcaPlot(),
344
- showGroupMeanCorrelation: () => PackageFunctions.showGroupMeanCorrelation(),
346
+ abundanceCorrelation: () => PackageFunctions.abundanceCorrelation(),
345
347
  showQcDashboard: () => PackageFunctions.showQcDashboard(),
346
348
  showSpcDashboard: () => PackageFunctions.showSpcDashboard(),
347
349
  enrichmentCharts: () => PackageFunctions.enrichmentCharts(),
350
+ rankAbundance: () => PackageFunctions.rankAbundance(),
348
351
  showAllVisualizations: () => PackageFunctions.showAllVisualizations(),
349
352
  shareAnalysisForReview: () => PackageFunctions.shareAnalysisForReview(),
350
353
  };
@@ -824,19 +827,12 @@ export class PackageFunctions {
824
827
  }
825
828
 
826
829
  @grok.decorators.func()
827
- static async showGroupMeanCorrelation(): Promise<void> {
828
- const tv = grok.shell.tv;
829
- const df = tv?.dataFrame;
830
- if (!tv || !df) { grok.shell.warning('No table open'); return; }
831
- if (!requireDifferentialExpression(df,
832
- 'Run Differential Expression first (Proteomics | Analyze | Differential Expression)')) return;
833
- const groups = getGroups(df);
834
- if (!groups) {
835
- grok.shell.warning('Annotate experimental groups first (Proteomics | Annotate Experiment)');
836
- return;
837
- }
838
- const sp = createGroupMeanCorrelation(df);
839
- tv.addViewer(sp);
830
+ static async abundanceCorrelation(): Promise<void> {
831
+ const df = grok.shell.tv?.dataFrame;
832
+ if (!df) { grok.shell.warning('No table open'); return; }
833
+ // Works on any analyzed protein table (Candidates or annotated Report)
834
+ // openAbundanceCorrelation itself warns when no per-condition abundance is present.
835
+ openAbundanceCorrelation(df);
840
836
  }
841
837
 
842
838
  @grok.decorators.func()
@@ -898,6 +894,15 @@ export class PackageFunctions {
898
894
  showEnrichmentInputExportDialog(df);
899
895
  }
900
896
 
897
+ @grok.decorators.func()
898
+ static async rankAbundance(): Promise<void> {
899
+ const df = grok.shell.tv?.dataFrame;
900
+ if (!df) { grok.shell.warning('No table open'); return; }
901
+ // Works on any analyzed protein table (Candidates or annotated Report) —
902
+ // openRankAbundance itself warns when no per-condition abundance is present.
903
+ openRankAbundance(df);
904
+ }
905
+
901
906
  @grok.decorators.func()
902
907
  static async enrichmentCharts(): Promise<void> {
903
908
  const tv = grok.shell.tv;
@@ -990,6 +995,17 @@ export class PackageFunctions {
990
995
  return publishedAnalysisPanel(proteinId);
991
996
  }
992
997
 
998
+ @grok.decorators.func({
999
+ name: 'Proteomics package settings editor',
1000
+ meta: {role: 'packageSettingsEditor'},
1001
+ tags: ['packageSettingsEditor'],
1002
+ })
1003
+ static async proteomicsSettingsEditor(
1004
+ @grok.decorators.param({name: 'propList', type: 'object'}) propList: DG.Property[],
1005
+ ): Promise<DG.Widget> {
1006
+ return proteomicsSettingsEditorWidget(propList);
1007
+ }
1008
+
993
1009
  @grok.decorators.func({tags: ['autostart'], meta: {autostartImmediate: 'true'}})
994
1010
  static async recoverPublishedProjectsOnStartup(): Promise<void> {
995
1011
  const tryEvent: any =
@@ -12,7 +12,7 @@ import {getGroups} from '../analysis/experiment-setup';
12
12
  /**
13
13
  * Reviewer-side audit context panel. Opens when a reviewer clicks a protein
14
14
  * row in a published Project. Surfaces the metadata the analyst stamped at
15
- * publish time (DE method, thresholds, group names, target, share date, sharer
15
+ * publish time (DE method, thresholds, group names, project, share date, sharer
16
16
  * name) plus PUB-13 mailto button. First-line `isPublished(df)` guard ensures
17
17
  * it does NOT render on the analyst's live working DataFrame.
18
18
  *
@@ -123,7 +123,7 @@ export function publishedAnalysisPanel(proteinId: string): DG.Widget {
123
123
  ? `${meta.pThreshold} (FDR-adjusted)`
124
124
  : '(unknown)';
125
125
 
126
- body.appendChild(fieldRow('Target', meta.target || '(unknown)'));
126
+ body.appendChild(fieldRow('Project', meta.project || '(unknown)'));
127
127
  body.appendChild(fieldRow('Shared', dateSlice(meta.publishedAt)));
128
128
  body.appendChild(fieldRow('Shared by', meta.publishedBy || '(unknown)'));
129
129
  body.appendChild(fieldRow('Method', methodLabel(meta.deMethod)));
@@ -127,8 +127,8 @@ export async function assertPublishedShape(
127
127
  }
128
128
  const expectedMeta = contract.expectedMeta;
129
129
  const eqFloat = (a: number, b: number) => !(Math.abs(a - b) > 1e-9);
130
- if (meta.target !== expectedMeta.target)
131
- throw new Error(`assertPublishedShape: meta.target: got "${meta.target}", expected "${expectedMeta.target}"`);
130
+ if (meta.project !== expectedMeta.project)
131
+ throw new Error(`assertPublishedShape: meta.project: got "${meta.project}", expected "${expectedMeta.project}"`);
132
132
  if (meta.deMethod !== expectedMeta.deMethod)
133
133
  throw new Error(`assertPublishedShape: meta.deMethod: got "${meta.deMethod}", expected "${expectedMeta.deMethod}"`);
134
134
  if (!eqFloat(meta.fcThreshold, expectedMeta.fcThreshold))