@datagrok/proteomics 1.0.1 → 1.2.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.
- package/CHANGELOG.md +52 -3
- package/CLAUDE.md +178 -0
- package/README.md +195 -3
- package/detectors.js +152 -9
- package/dist/package-test.js +1 -1744
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -146
- package/dist/package.js.map +1 -1
- package/docs/personas-and-capabilities.md +165 -0
- package/files/demo/README.md +264 -0
- package/files/demo/cptac-spike-in.txt +1571 -0
- package/files/demo/enrichment-demo.csv +120 -0
- package/files/demo/fragpipe-smoke-test.tsv +11 -0
- package/files/demo/proteinGroups.txt +28 -0
- package/files/demo/spectronaut-hye-candidates.tsv +94 -0
- package/files/demo/spectronaut-hye-demo.tsv +8761 -0
- package/files/demo/spectronaut-hye-mix.tsv +8761 -0
- package/files/demo/spectronaut-hye-precursor-golden.json +938 -0
- package/files/demo/spectronaut-hye-precursor-golden.tsv +235 -0
- package/files/demo/spectronaut-hye-precursor.tsv +493 -0
- package/images/enrichment-crosslink.png +0 -0
- package/images/enrichment-term-selected.png +0 -0
- package/images/hero.png +0 -0
- package/images/pipeline.svg +80 -0
- package/package.json +87 -63
- package/scripts/deqms_de.R +60 -0
- package/scripts/limma_de.R +42 -0
- package/scripts/vsn_normalize.R +19 -0
- package/src/analysis/differential-expression.ts +450 -0
- package/src/analysis/enrichment-export.ts +101 -0
- package/src/analysis/enrichment.ts +602 -0
- package/src/analysis/experiment-setup.ts +199 -0
- package/src/analysis/imputation.ts +407 -0
- package/src/analysis/log2-scale.ts +139 -0
- package/src/analysis/normalization.ts +255 -0
- package/src/analysis/pca.ts +254 -0
- package/src/analysis/spc-storage.ts +515 -0
- package/src/analysis/spc.ts +544 -0
- package/src/analysis/subcellular-location.ts +431 -0
- package/src/demo/enrichment-demo.ts +94 -0
- package/src/demo/proteomics-demo.ts +123 -0
- package/src/global.d.ts +15 -0
- package/src/menu.ts +133 -0
- package/src/package-api.ts +136 -14
- package/src/package-test.ts +45 -20
- package/src/package.g.ts +161 -0
- package/src/package.ts +1029 -17
- package/src/panels/protein-focus.ts +63 -0
- package/src/panels/published-analysis-panel.ts +151 -0
- package/src/panels/uniprot-panel.ts +349 -0
- package/src/parsers/fragpipe-parser.ts +200 -0
- package/src/parsers/generic-parser.ts +197 -0
- package/src/parsers/maxquant-parser.ts +162 -0
- package/src/parsers/shared-utils.ts +163 -0
- package/src/parsers/spectronaut-candidates-parser.ts +307 -0
- package/src/parsers/spectronaut-parser.ts +604 -0
- package/src/publishing/assert-published-shape.ts +260 -0
- package/src/publishing/post-open-recovery.ts +104 -0
- package/src/publishing/publish-project.ts +515 -0
- package/src/publishing/publish-settings.ts +59 -0
- package/src/publishing/publish-state.ts +316 -0
- package/src/publishing/share-dialog.ts +171 -0
- package/src/publishing/trim-dataframe.ts +247 -0
- package/src/tests/analysis.ts +658 -0
- package/src/tests/enrichment-export.ts +61 -0
- package/src/tests/enrichment-visualization.ts +340 -0
- package/src/tests/enrichment.ts +224 -0
- package/src/tests/fragpipe-e2e.ts +74 -0
- package/src/tests/fragpipe-parser.ts +147 -0
- package/src/tests/gene-label-resolver.ts +387 -0
- package/src/tests/generic-parser.ts +152 -0
- package/src/tests/group-mean-correlation.ts +139 -0
- package/src/tests/log2-scale.ts +93 -0
- package/src/tests/organisms.ts +56 -0
- package/src/tests/parsers.ts +182 -0
- package/src/tests/publish-roundtrip.ts +584 -0
- package/src/tests/publish-spike.ts +377 -0
- package/src/tests/qc-dashboard.ts +210 -0
- package/src/tests/smart-pathway-filter.ts +193 -0
- package/src/tests/spc-formula-lines-spike.ts +129 -0
- package/src/tests/spc.ts +640 -0
- package/src/tests/spectronaut-candidates-e2e.ts +140 -0
- package/src/tests/spectronaut-candidates-parser.ts +398 -0
- package/src/tests/spectronaut-parser.ts +668 -0
- package/src/tests/subcellular-location.ts +361 -0
- package/src/tests/uniprot-panel.ts +202 -0
- package/src/tests/volcano.ts +603 -0
- package/src/utils/column-detection.ts +28 -0
- package/src/utils/gene-label-resolver.ts +443 -0
- package/src/utils/organisms.ts +82 -0
- package/src/utils/proteomics-types.ts +30 -0
- package/src/viewers/enrichment-viewers.ts +274 -0
- package/src/viewers/group-mean-correlation.ts +218 -0
- package/src/viewers/heatmap.ts +168 -0
- package/src/viewers/pca-plot.ts +169 -0
- package/src/viewers/qc-computations.ts +266 -0
- package/src/viewers/qc-dashboard.ts +176 -0
- package/src/viewers/spc-dashboard.ts +755 -0
- package/src/viewers/volcano.ts +690 -0
- package/test-console-output-1.log +2055 -0
- package/test-record-1.mp4 +0 -0
- package/tools/derive-precursor-golden-sidecar.mjs +81 -0
- package/tools/generate-enrichment-fixture.sh +160 -0
- package/tools/generate-spectronaut-candidates-fixture.mjs +212 -0
- package/tools/generate-spectronaut-precursor-fixture.mjs +128 -0
- package/tools/spectronaut-aggregate.sh +46 -0
- package/tools/spectronaut-aggregate.sql +80 -0
- package/tsconfig.json +18 -71
- package/webpack.config.js +86 -45
- package/.eslintignore +0 -1
- package/.eslintrc.json +0 -56
- package/LICENSE +0 -674
- package/package.png +0 -0
- package/scripts/number_antibody.py +0 -190
- package/scripts/number_antibody_abnumber.py +0 -177
- package/scripts/number_antibody_anarci.py +0 -200
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as DG from 'datagrok-api/dg';
|
|
3
|
+
import {awaitCheck, delay} from '@datagrok-libraries/test/src/test';
|
|
4
|
+
|
|
5
|
+
import {SEMTYPE} from '../utils/proteomics-types';
|
|
6
|
+
import {findColumn} from '../utils/column-detection';
|
|
7
|
+
import {
|
|
8
|
+
META_COLUMNS, PUBLISHED_TAGS, PublishedMetadata, getPublishedMetadata,
|
|
9
|
+
} from './publish-state';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load-bearing distinguishing marker. Plan 04 step 8's catch block matches on
|
|
13
|
+
* this prefix to identify the "look/filter config stripped by serializer"
|
|
14
|
+
* failure mode and trigger the post-open formula-line recovery hook (B-2 / W-7)
|
|
15
|
+
* BEFORE rolling back the published Project.
|
|
16
|
+
*/
|
|
17
|
+
export const FORMULA_LINE_ASSERTION_PREFIX = 'FORMULA_LINE_MISSING:' as const;
|
|
18
|
+
|
|
19
|
+
/** What the publisher claimed at publish time, used as the expected baseline
|
|
20
|
+
* for {@link assertPublishedShape}. */
|
|
21
|
+
export interface PublishedShapeContract {
|
|
22
|
+
/** The trimmed DF's name (e.g. `<source>_published_<YYYY-MM-DD>`). */
|
|
23
|
+
expectedName: string;
|
|
24
|
+
/** The Project's name (e.g. `Proteomics-Review-<slug>-v<N>-<YYYY-MM-DD>`). */
|
|
25
|
+
expectedProjectName: string;
|
|
26
|
+
/** The 7-or-fewer column names the trim chose. */
|
|
27
|
+
expectedAllowlist: string[];
|
|
28
|
+
expectedMeta: PublishedMetadata;
|
|
29
|
+
/** Whether a volcano viewer should be present on reopen. */
|
|
30
|
+
expectVolcano: boolean;
|
|
31
|
+
/** Whether an enrichment DataFrame should be present in `grok.shell.tables`. */
|
|
32
|
+
expectEnrichment: boolean;
|
|
33
|
+
/** Whether FC/p threshold formula lines should be present on the volcano.
|
|
34
|
+
* PUB-06 / SC-2 — defaults to true when `expectVolcano` is true. */
|
|
35
|
+
expectFormulaLines: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reopens the saved Project in a fresh session and asserts every observable
|
|
40
|
+
* invariant of the publish contract. Throws on first violation with a
|
|
41
|
+
* specific error including which assertion failed and what was observed.
|
|
42
|
+
*
|
|
43
|
+
* Single source of truth: BOTH the Plan 04 orchestrator (step 8 non-negotiable
|
|
44
|
+
* gate) AND the Plan 08 regression test (`src/tests/publish-roundtrip.ts`)
|
|
45
|
+
* call this helper. If the contract changes, it changes here.
|
|
46
|
+
*
|
|
47
|
+
* The volcano-formula-line assertion (8a) throws an error PREFIXED with
|
|
48
|
+
* {@link FORMULA_LINE_ASSERTION_PREFIX}. Plan 04 step 8 catches this specific
|
|
49
|
+
* prefix to drive its self-healing recovery (W-7) — re-add the formula lines
|
|
50
|
+
* from `proteomics.published_fc_threshold` / `proteomics.published_p_threshold`
|
|
51
|
+
* tags and re-run this assertion exactly once before rolling back.
|
|
52
|
+
*
|
|
53
|
+
* Caller is responsible for `grok.shell.closeAll()` after the assertion if
|
|
54
|
+
* additional cleanup is desired.
|
|
55
|
+
*/
|
|
56
|
+
export async function assertPublishedShape(
|
|
57
|
+
project: DG.Project,
|
|
58
|
+
contract: PublishedShapeContract,
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
const projId = project.id;
|
|
61
|
+
grok.shell.closeAll();
|
|
62
|
+
await delay(100);
|
|
63
|
+
|
|
64
|
+
const reopened = await grok.dapi.projects.find(projId);
|
|
65
|
+
await reopened.open();
|
|
66
|
+
|
|
67
|
+
await awaitCheck(
|
|
68
|
+
() => !!grok.shell.tv && !!grok.shell.tv.dataFrame,
|
|
69
|
+
'assertPublishedShape: TableView never materialized after project.open()',
|
|
70
|
+
5000,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const reDf = grok.shell.tv!.dataFrame;
|
|
74
|
+
|
|
75
|
+
// ASSERTION 1 — df.name matches
|
|
76
|
+
if (reDf.name !== contract.expectedName) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`assertPublishedShape: df.name: got "${reDf.name}", expected "${contract.expectedName}"`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ASSERTION 2 — Project name matches (PUB-09)
|
|
82
|
+
if ((reopened as any).name !== contract.expectedProjectName) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`assertPublishedShape: project.name: got "${(reopened as any).name}", expected "${contract.expectedProjectName}"`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ASSERTION 3 — Every allowlist column present
|
|
88
|
+
for (const colName of contract.expectedAllowlist) {
|
|
89
|
+
if (!reDf.col(colName)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`assertPublishedShape: allowlist column missing on reopen: "${colName}"`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ASSERTION 4 — Column count matches allowlist (no extras except hidden
|
|
96
|
+
// _meta_* belt-and-braces columns and ~-prefixed technical columns such as
|
|
97
|
+
// the volcano's '~Volcano label' — neither is part of the published shape).
|
|
98
|
+
const visibleCols = reDf.columns.toList()
|
|
99
|
+
.map((c) => c.name)
|
|
100
|
+
.filter((n) => !n.startsWith('_meta_') && !n.startsWith('~'));
|
|
101
|
+
if (visibleCols.length !== contract.expectedAllowlist.length) {
|
|
102
|
+
const unexpected = visibleCols.filter((n) => !contract.expectedAllowlist.includes(n));
|
|
103
|
+
const missing = contract.expectedAllowlist.filter((n) => !visibleCols.includes(n));
|
|
104
|
+
throw new Error(
|
|
105
|
+
`assertPublishedShape: visible column count: got ${visibleCols.length}, expected ${contract.expectedAllowlist.length}. ` +
|
|
106
|
+
`Unexpected: [${unexpected.join(', ')}]. Missing: [${missing.join(', ')}].`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ASSERTION 5 — PROTEIN_ID semType present on protein-id column
|
|
110
|
+
const proteinIdCol = findColumn(reDf, SEMTYPE.PROTEIN_ID, ['protein id', 'majority protein id', 'accession']);
|
|
111
|
+
if (proteinIdCol == null) {
|
|
112
|
+
throw new Error('assertPublishedShape: protein-id column not found via findColumn');
|
|
113
|
+
}
|
|
114
|
+
if (proteinIdCol.semType !== SEMTYPE.PROTEIN_ID) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`assertPublishedShape: protein-id semType: got "${proteinIdCol.semType ?? 'null'}", ` +
|
|
117
|
+
`expected "${SEMTYPE.PROTEIN_ID}". Detectors may not have re-fired on reopen — ` +
|
|
118
|
+
`re-assign in trim-dataframe.ts Step E.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ASSERTION 6 — Required proteomics.published* metadata readable (column FIRST, tag SECOND)
|
|
122
|
+
const meta = getPublishedMetadata(reDf);
|
|
123
|
+
if (meta == null) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
'assertPublishedShape: getPublishedMetadata returned null — published flag ' +
|
|
126
|
+
'not set or all critical fields unreadable');
|
|
127
|
+
}
|
|
128
|
+
const expectedMeta = contract.expectedMeta;
|
|
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}"`);
|
|
132
|
+
if (meta.deMethod !== expectedMeta.deMethod)
|
|
133
|
+
throw new Error(`assertPublishedShape: meta.deMethod: got "${meta.deMethod}", expected "${expectedMeta.deMethod}"`);
|
|
134
|
+
if (!eqFloat(meta.fcThreshold, expectedMeta.fcThreshold))
|
|
135
|
+
throw new Error(`assertPublishedShape: meta.fcThreshold: got ${meta.fcThreshold}, expected ${expectedMeta.fcThreshold}`);
|
|
136
|
+
if (!eqFloat(meta.pThreshold, expectedMeta.pThreshold))
|
|
137
|
+
throw new Error(`assertPublishedShape: meta.pThreshold: got ${meta.pThreshold}, expected ${expectedMeta.pThreshold}`);
|
|
138
|
+
if (meta.version !== expectedMeta.version)
|
|
139
|
+
throw new Error(`assertPublishedShape: meta.version: got ${meta.version}, expected ${expectedMeta.version}`);
|
|
140
|
+
if (meta.publishId !== expectedMeta.publishId)
|
|
141
|
+
throw new Error(`assertPublishedShape: meta.publishId: got "${meta.publishId}", expected "${expectedMeta.publishId}"`);
|
|
142
|
+
if (meta.includesEnrichment !== expectedMeta.includesEnrichment)
|
|
143
|
+
throw new Error(`assertPublishedShape: meta.includesEnrichment: got ${meta.includesEnrichment}, expected ${expectedMeta.includesEnrichment}`);
|
|
144
|
+
if (meta.publishedBy !== expectedMeta.publishedBy)
|
|
145
|
+
throw new Error(`assertPublishedShape: meta.publishedBy: got "${meta.publishedBy}", expected "${expectedMeta.publishedBy}"`);
|
|
146
|
+
|
|
147
|
+
// ASSERTION 7 — Belt-and-braces metadata column present (PUB-11)
|
|
148
|
+
for (const key of Object.keys(META_COLUMNS) as Array<keyof typeof META_COLUMNS>) {
|
|
149
|
+
const colName = META_COLUMNS[key];
|
|
150
|
+
if (!reDf.col(colName)) {
|
|
151
|
+
throw new Error(`assertPublishedShape: metadata column missing: "${colName}"`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ASSERTION 8 — Volcano viewer present (PUB-06)
|
|
156
|
+
let volcanoViewer: DG.Viewer | null = null;
|
|
157
|
+
if (contract.expectVolcano) {
|
|
158
|
+
try {
|
|
159
|
+
await awaitCheck(
|
|
160
|
+
() => {
|
|
161
|
+
const tv = grok.shell.tv;
|
|
162
|
+
if (!tv) return false;
|
|
163
|
+
for (const v of tv.viewers) {
|
|
164
|
+
if (v.type === DG.VIEWER.SCATTER_PLOT) {
|
|
165
|
+
volcanoViewer = v;
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
},
|
|
171
|
+
'assertPublishedShape: volcano scatter plot not present on reopen — consider post-open recomputeVolcano in publish-project.ts',
|
|
172
|
+
5000,
|
|
173
|
+
);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
throw new Error(`assertPublishedShape: volcano viewer assertion failed: ${(e as Error)?.message ?? e}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ASSERTION 8a — Volcano formula lines for FC + p thresholds (PUB-06 / SC-2, B-2 / W-7)
|
|
180
|
+
if (contract.expectVolcano && contract.expectFormulaLines && volcanoViewer != null) {
|
|
181
|
+
const fcStr = String(expectedMeta.fcThreshold);
|
|
182
|
+
const pStr = String(expectedMeta.pThreshold);
|
|
183
|
+
const minusLogPStr = String(-Math.log10(expectedMeta.pThreshold));
|
|
184
|
+
|
|
185
|
+
type FormulaLine = {formula?: string; title?: string};
|
|
186
|
+
const collected: FormulaLine[] = [];
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const dfLines = ((reDf as any).meta?.formulaLines?.items ?? []) as FormulaLine[];
|
|
190
|
+
if (Array.isArray(dfLines)) collected.push(...dfLines);
|
|
191
|
+
} catch { /* fall through */ }
|
|
192
|
+
try {
|
|
193
|
+
const opts: any = (volcanoViewer as DG.Viewer).getOptions?.();
|
|
194
|
+
const lookLines = opts?.look?.formulaLines;
|
|
195
|
+
const parsed = typeof lookLines === 'string' ? JSON.parse(lookLines) : lookLines;
|
|
196
|
+
if (Array.isArray(parsed)) collected.push(...(parsed as FormulaLine[]));
|
|
197
|
+
} catch { /* fall through */ }
|
|
198
|
+
try {
|
|
199
|
+
const propLines = (volcanoViewer as any)?.props?.formulaLines;
|
|
200
|
+
const parsed = typeof propLines === 'string' ? JSON.parse(propLines) : propLines;
|
|
201
|
+
if (Array.isArray(parsed)) collected.push(...(parsed as FormulaLine[]));
|
|
202
|
+
} catch { /* fall through */ }
|
|
203
|
+
|
|
204
|
+
const formulas = collected
|
|
205
|
+
.map((l) => (typeof l?.formula === 'string' ? l.formula : ''))
|
|
206
|
+
.filter((f) => f.length > 0);
|
|
207
|
+
|
|
208
|
+
const hasFcLine = formulas.some((f) => f.includes(fcStr) || f.includes(`-${fcStr}`));
|
|
209
|
+
const hasPLine = formulas.some((f) => f.includes(minusLogPStr) || f.includes(pStr));
|
|
210
|
+
|
|
211
|
+
if (!hasFcLine || !hasPLine) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`${FORMULA_LINE_ASSERTION_PREFIX} volcano reopened without formula lines for ` +
|
|
214
|
+
`FC=${expectedMeta.fcThreshold} and/or p=${expectedMeta.pThreshold}. ` +
|
|
215
|
+
`The look/filter config was stripped by the serializer (Phase 13 e527d07ba1 evidence). ` +
|
|
216
|
+
`Plan 04 step 8 catch should invoke the post-open recovery hook from tags ` +
|
|
217
|
+
`${PUBLISHED_TAGS.PUBLISHED_FC_THRESHOLD} / ${PUBLISHED_TAGS.PUBLISHED_P_THRESHOLD} ` +
|
|
218
|
+
`and re-run this assertion once.`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ASSERTION 9 — Enrichment DF presence (PUB-12, conditional)
|
|
223
|
+
if (contract.expectEnrichment) {
|
|
224
|
+
const tables = (grok.shell as any).tables as DG.DataFrame[] | undefined;
|
|
225
|
+
if (!Array.isArray(tables) || tables.length < 2) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`assertPublishedShape: expectEnrichment=true but grok.shell.tables has ${tables?.length ?? 0} DataFrames (need ≥ 2)`);
|
|
228
|
+
}
|
|
229
|
+
const proteinDf = tables.find((d) => {
|
|
230
|
+
try {
|
|
231
|
+
const col = d.col(META_COLUMNS.PUBLISHED_INCLUDES_ENRICHMENT);
|
|
232
|
+
return col != null && String(col.get(0)) === 'true';
|
|
233
|
+
} catch { return false; }
|
|
234
|
+
});
|
|
235
|
+
if (!proteinDf)
|
|
236
|
+
throw new Error('assertPublishedShape: protein DF (with _meta_published_includes_enrichment=true) not found among reopened tables');
|
|
237
|
+
const enrichDf = tables.find((d) => d.getTag('proteomics.enrichment') === 'true');
|
|
238
|
+
if (!enrichDf)
|
|
239
|
+
throw new Error('assertPublishedShape: enrichment DF (proteomics.enrichment=true tag) not found among reopened tables');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ASSERTION 10 — Project options bidirectional supersede pointers (PUB-10, conditional)
|
|
243
|
+
const opts: any = (reopened as any).options ?? {};
|
|
244
|
+
if (expectedMeta.supersedes != null) {
|
|
245
|
+
const got = opts[PUBLISHED_TAGS.SUPERSEDES];
|
|
246
|
+
if (got !== expectedMeta.supersedes) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`assertPublishedShape: project.options['${PUBLISHED_TAGS.SUPERSEDES}']: got "${got ?? 'null'}", ` +
|
|
249
|
+
`expected "${expectedMeta.supersedes}"`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (expectedMeta.supersededBy != null) {
|
|
253
|
+
const got = opts[PUBLISHED_TAGS.SUPERSEDED_BY];
|
|
254
|
+
if (got !== expectedMeta.supersededBy) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`assertPublishedShape: project.options['${PUBLISHED_TAGS.SUPERSEDED_BY}']: got "${got ?? 'null'}", ` +
|
|
257
|
+
`expected "${expectedMeta.supersededBy}"`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as DG from 'datagrok-api/dg';
|
|
3
|
+
|
|
4
|
+
import {PUBLISHED_TAGS, isPublished} from './publish-state';
|
|
5
|
+
import {applyVolcanoFormulaLines} from './publish-project';
|
|
6
|
+
import {wireEnrichmentToVolcano} from '../viewers/enrichment-viewers';
|
|
7
|
+
|
|
8
|
+
const ENRICHMENT_WIRED_TAG = 'proteomics.enrichment_wired';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Post-open recovery hook for reopened published Projects. Idempotent — safe
|
|
12
|
+
* to call multiple times on the same DataFrame.
|
|
13
|
+
*
|
|
14
|
+
* Self-heals two failure modes:
|
|
15
|
+
*
|
|
16
|
+
* - **B-2 / W-7 — volcano formula lines stripped by serializer.** Reads FC
|
|
17
|
+
* and p threshold values from the `proteomics.published_fc_threshold` /
|
|
18
|
+
* `proteomics.published_p_threshold` tags (Plan 02's belt-and-braces #2
|
|
19
|
+
* write via `setPublishedTags`) and re-applies via the exported
|
|
20
|
+
* {@link applyVolcanoFormulaLines}. No-op when formula lines are already
|
|
21
|
+
* present.
|
|
22
|
+
*
|
|
23
|
+
* - **W-5 — enrichment cross-DF highlight subscription not re-established
|
|
24
|
+
* on reopen.** When the published Project carries both the protein DF
|
|
25
|
+
* AND a published enrichment DF (both tagged `proteomics.published=true`),
|
|
26
|
+
* wires the existing `wireEnrichmentToVolcano` subscription pattern from
|
|
27
|
+
* `src/viewers/enrichment-viewers.ts`. Sentinel-tag duplicate-guard
|
|
28
|
+
* (`proteomics.enrichment_wired`) prevents double subscriptions when the
|
|
29
|
+
* user reopens the project repeatedly in one session.
|
|
30
|
+
*/
|
|
31
|
+
export async function recoverPublishedProject(df: DG.DataFrame): Promise<void> {
|
|
32
|
+
if (!isPublished(df)) return;
|
|
33
|
+
|
|
34
|
+
// PART 1 — Re-apply formula lines on the volcano (B-2 / W-7)
|
|
35
|
+
const tv = findTableViewFor(df);
|
|
36
|
+
if (tv) {
|
|
37
|
+
const volcano = Array.from(tv.viewers).find((v) => v.type === DG.VIEWER.SCATTER_PLOT);
|
|
38
|
+
if (volcano) {
|
|
39
|
+
const fcRaw = df.getTag(PUBLISHED_TAGS.PUBLISHED_FC_THRESHOLD);
|
|
40
|
+
const pRaw = df.getTag(PUBLISHED_TAGS.PUBLISHED_P_THRESHOLD);
|
|
41
|
+
if (fcRaw && pRaw) {
|
|
42
|
+
const fc = parseFloat(fcRaw);
|
|
43
|
+
const p = parseFloat(pRaw);
|
|
44
|
+
if (Number.isFinite(fc) && Number.isFinite(p)) {
|
|
45
|
+
if (!hasFormulaLines(df, volcano)) {
|
|
46
|
+
try { applyVolcanoFormulaLines(volcano, fc, p); } catch { /* best-effort */ }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// PART 2 — Re-establish enrichment cross-DF subscription (W-5)
|
|
54
|
+
const enrichDf = findPublishedEnrichmentSibling(df);
|
|
55
|
+
if (enrichDf && enrichDf.getTag(ENRICHMENT_WIRED_TAG) !== 'true') {
|
|
56
|
+
try {
|
|
57
|
+
wireEnrichmentToVolcano(enrichDf, df);
|
|
58
|
+
enrichDf.setTag(ENRICHMENT_WIRED_TAG, 'true');
|
|
59
|
+
} catch { /* best-effort */ }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function findTableViewFor(df: DG.DataFrame): DG.TableView | null {
|
|
64
|
+
try {
|
|
65
|
+
const views = (grok.shell as any).tableViews as DG.TableView[] | undefined;
|
|
66
|
+
if (Array.isArray(views)) {
|
|
67
|
+
const match = views.find((v) => v?.dataFrame === df);
|
|
68
|
+
if (match) return match;
|
|
69
|
+
}
|
|
70
|
+
} catch { /* fall through */ }
|
|
71
|
+
try {
|
|
72
|
+
const tv = grok.shell.tv;
|
|
73
|
+
if (tv && tv.dataFrame === df) return tv;
|
|
74
|
+
} catch { /* fall through */ }
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hasFormulaLines(df: DG.DataFrame, viewer: DG.Viewer): boolean {
|
|
79
|
+
try {
|
|
80
|
+
const items = ((df as any).meta?.formulaLines?.items ?? []) as Array<{formula?: string}>;
|
|
81
|
+
if (Array.isArray(items) && items.length > 0) return true;
|
|
82
|
+
} catch { /* fall through */ }
|
|
83
|
+
try {
|
|
84
|
+
const opts: any = (viewer as any).getOptions?.();
|
|
85
|
+
const lookLines = opts?.look?.formulaLines;
|
|
86
|
+
if (typeof lookLines === 'string' && lookLines !== '' && lookLines !== '[]') return true;
|
|
87
|
+
if (Array.isArray(lookLines) && lookLines.length > 0) return true;
|
|
88
|
+
} catch { /* fall through */ }
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function findPublishedEnrichmentSibling(proteinDf: DG.DataFrame): DG.DataFrame | null {
|
|
93
|
+
try {
|
|
94
|
+
const tables = (grok.shell as any).tables as DG.DataFrame[] | undefined;
|
|
95
|
+
if (!Array.isArray(tables)) return null;
|
|
96
|
+
return tables.find((t) =>
|
|
97
|
+
t !== proteinDf &&
|
|
98
|
+
t?.getTag?.('proteomics.enrichment') === 'true' &&
|
|
99
|
+
t?.getTag?.(PUBLISHED_TAGS.PUBLISHED) === 'true',
|
|
100
|
+
) ?? null;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|