Skip to content

Commit fd34a9a

Browse files
committed
Merge remote-tracking branch 'origin/development' into release/v1.13.0
2 parents dd43840 + 693ab6d commit fd34a9a

80 files changed

Lines changed: 8326 additions & 52 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@ The governance model for this project follows a strict top-down traceability cha
1414

1515
```
1616
PRD (docs/prd.md)
17-
└── Features (.github/ISSUE_TEMPLATE/feature.yml)
18-
└── Stories (.github/ISSUE_TEMPLATE/story.yml)
17+
└── Features (docs/features.md + .github/ISSUE_TEMPLATE/feature.yml)
18+
└── Stories (docs/features.md + .github/ISSUE_TEMPLATE/story.yml)
1919
└── Tasks (.github/ISSUE_TEMPLATE/task.yml)
2020
└── Implementation (code, PRs)
2121
```
2222

23+
Stories are tracked in two places: `docs/features.md` (the stakeholder-facing, stable record) and
24+
GitHub issues (the implementation tracking record). When a story moves from draft (discussion in
25+
SharePoint Word doc) to approved (ready for implementation), it receives a stable Story ID and is
26+
documented in both locations simultaneously. See **§11.1 Story Lifecycle** for the full workflow.
27+
2328
- **PRD** defines product vision, user personas, and business objectives.
2429
- **Features** group related Stories under a named, user-visible capability. A Feature references one or more requirement IDs and describes the high-level scope.
2530
- **Stories** describe user-facing workflows derived from a Feature. Each Story references a parent Feature and one or more requirement IDs.
@@ -92,6 +97,33 @@ Where:
9297

9398
---
9499

100+
### Story ID Schema
101+
102+
Stories are identified using a short, human-readable slug derived from their parent Feature:
103+
104+
FEAT-<SLUG>-<NN>
105+
106+
Where:
107+
108+
- **SLUG** --- a concise, abbreviated identifier derived from the parent Feature slug (e.g., `IP-MEAS` for `FEAT-IMMUNOPEPTIDOMICS-MEASUREMENT`). Abbreviations must be short enough for readability but descriptive enough to be self-explanatory within the Feature context.
109+
- **NN** --- sequential number, zero-padded to exactly two digits (e.g., `01`, `02`, `10`).
110+
111+
#### Examples
112+
113+
FEAT-IP-MEAS-01
114+
FEAT-IP-MEAS-05
115+
FEAT-SAMPLE-03
116+
117+
#### Rules
118+
119+
- Story IDs are assigned when a story moves from draft (discussion/refinement) to approved (ready for implementation).
120+
- Story IDs must be stable and must never be renumbered.
121+
- Tasks reference stories by their stable ID, not by GitHub issue number.
122+
- GitHub issues for stories are updated to carry the stable story ID in the title and body.
123+
- Abbreviated slugs must be unique within their parent Feature, but do not need to be globally unique across Features.
124+
125+
---
126+
95127
### Requirement Structure
96128

97129
All requirements must be documented in `docs/requirements.md`.
@@ -200,15 +232,37 @@ Rules:
200232

201233
- Tasks must not redefine acceptance criteria.
202234
- Tasks must not expand requirement scope.
235+
- Tasks reference stories by their stable ID (e.g., `FEAT-IP-MEAS-01`), not by GitHub issue number.
236+
237+
---
238+
239+
#### Story Lifecycle
240+
241+
Stories move through a lifecycle from draft to implementation. This flow keeps the GitHub issue history clean and ensures stakeholders have a stable, reviewable document.
242+
243+
1. **Draft (Discussion)** — The story is captured in a SharePoint Word document or similar internal medium where discussion, refinement, and iteration happen. No stable ID is assigned. The story is not yet tracked in `docs/features.md`.
244+
2. **Approved (Ready for Implementation)** — The story is finalised, a stable `FEAT-<SLUG>-<NN>` ID is assigned, and it is:
245+
- Written into `docs/features.md` with full narrative and acceptance criteria
246+
- A GitHub issue is created (if it does not already exist) or an existing issue is updated with the stable ID in the title and body
247+
- The status in `docs/features.md` is updated from 🔴 (Open) to 🟡 (In Progress) when implementation begins, and 🟢 (Done) when complete
248+
3. **Implementation** — Tasks are created in GitHub referencing the stable story ID (not the GitHub issue number). Implementation traces: Task → Story (stable ID) → Feature → Requirement → PRD.
249+
250+
**Rules:**
251+
252+
- A story must never be implemented without a stable ID in `docs/features.md`.
253+
- When a story was created on GitHub before this flow existed (e.g., old issue numbers), update the existing issue with the stable ID rather than creating a new one.
254+
- Tasks reference stories by their stable ID in the "Parent Story" field, not by GitHub issue number.
255+
- The GitHub issue remains the source of truth for implementation tracking (comments, sub-issues, assignees), but `docs/features.md` is the source of truth for story content (narrative, acceptance criteria).
203256

204257
---
205258

206259
#### Feature, Story, and Task Sequencing
207260

208261
- A Feature must be created before any Story references it. Create the Feature first.
209262
- A Story must always reference a parent Feature. Never create a Story without a parent Feature issue.
210-
- A Task must always be preceded by a Story. Create the Story first, then create the Task referencing it.
211-
- Never create a Task without a parent Story issue.
263+
- A Story must receive a stable `FEAT-<SLUG>-<NN>` ID before any Task is created for it.
264+
- A Task must always be preceded by a Story. Create the Story first (draft → approved → stable ID), then create the Task referencing the stable story ID.
265+
- Never create a Task without a parent Story issue. Tasks must reference the story by its stable ID.
212266
- If no parent Feature exists for a piece of work (e.g., when acting on a direct implementation request), create the Feature first, then the Story, then the Task.
213267
- A single Story may have multiple Tasks; a Task must not span multiple Stories.
214268

@@ -621,6 +675,14 @@ When working on this codebase, an AI agent should:
621675
- A Feature slug (`FEAT-<SLUG>`) must be unique and must not be changed once Stories reference it.
622676
- If you are unsure whether a new Feature is needed or an existing Feature should be extended, pause and ask a human reviewer.
623677

678+
### Creating Stories
679+
680+
- Stories start as drafts (discussion/refinement in SharePoint Word or similar internal medium). They are NOT tracked in `docs/features.md` or GitHub while in draft.
681+
- When a story is finalised and approved for implementation: assign a stable `FEAT-<SLUG>-<NN>` ID, write it into `docs/features.md` with full narrative and acceptance criteria, and create or update the corresponding GitHub issue with the stable ID in the title and body.
682+
- For stories that were created on GitHub before this workflow existed, update the existing issue with the stable ID rather than creating a new one.
683+
- Tasks reference the story by its stable ID (e.g., `FEAT-IP-MEAS-01`), not by GitHub issue number.
684+
- If you are unsure whether a new Story is needed or an existing Feature should be extended, pause and ask a human reviewer.
685+
624686
### Making domain changes
625687

626688
- New domain concepts go in `domain/model/` of the relevant bounded context.
@@ -679,11 +741,13 @@ An agent should pause and request human review/approval before:
679741
|---|---|
680742
| `docs/requirements.md` | Authoritative requirement registry — all R/NFR/C requirements documented here; must be updated before new capabilities are implemented |
681743
| `docs/requirements-guide.md` | Authoring conventions for creating, editing, and retiring requirements |
744+
| `docs/features.md` | Stakeholder-facing features and user stories tracker — stable story records with narrative, acceptance criteria, and status; stories move here from draft once approved |
682745
| `README.md` | Setup, configuration reference, how to run |
683746
| `ExceptionHandling.md` | Exception handling conventions (read before touching error handling) |
684747
| `service_api.md` | Service API design patterns (Mono/Flux, request/response shapes) |
685748
| `datamanager-app/front-end-components.md` | UI component composition patterns |
686749
| `docs/fair/research-objects.md` | FAIR/RO-Crate integration notes |
750+
| `docs/stakeholder-artifacts/measurement-metadata/` | Authoritative stakeholder Excel specification for immunopeptidomics measurement metadata (field definitions, positions, categories) — read before implementing template generation, parsing, or validation |
687751
| `docs/processes/` | Sequence diagrams (sample registration process) |
688752
| `sql/complete-schema.sql` | Full database DDL |
689753
| `sql/insert-default-values.sql` | Seed data (roles, permissions, ACL classes) |

datamanager-app/frontend/themes/datamanager/components/span.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,8 @@ span.inline {
209209
background-color: var(--lumo-warning-color-10pct);
210210
color: var(--lumo-warning-text-color);
211211
}
212+
213+
.tag.gold {
214+
background-color: #FFF3CC;
215+
color: #A68500;
216+
}

datamanager-app/src/main/java/life/qbic/datamanager/files/parsing/converters/ConverterRegistry.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import java.util.function.Supplier;
55
import life.qbic.projectmanagement.application.api.AsyncProjectService.MeasurementRegistrationInformationNGS;
66
import life.qbic.projectmanagement.application.api.AsyncProjectService.MeasurementRegistrationInformationPxP;
7+
import life.qbic.projectmanagement.application.api.AsyncProjectService.MeasurementRegistrationInformationIP;
78
import life.qbic.projectmanagement.application.api.AsyncProjectService.MeasurementUpdateInformationNGS;
89
import life.qbic.projectmanagement.application.api.AsyncProjectService.MeasurementUpdateInformationPxP;
10+
import life.qbic.projectmanagement.application.api.AsyncProjectService.MeasurementUpdateInformationIP;
911
import life.qbic.projectmanagement.application.api.AsyncProjectService.SampleRegistrationInformation;
1012
import life.qbic.projectmanagement.application.api.AsyncProjectService.SampleUpdateInformation;
1113
import org.apache.commons.collections.map.HashedMap;
@@ -45,10 +47,14 @@ public class ConverterRegistry {
4547
MeasurementRegistrationMetadataConverterNGS::new);
4648
registry.put(MeasurementRegistrationInformationPxP.class,
4749
MeasurementRegistrationMetadataConverterPxP::new);
50+
registry.put(MeasurementRegistrationInformationIP.class,
51+
MeasurementRegistrationMetadataConverterIP::new);
4852
registry.put(MeasurementUpdateInformationNGS.class,
4953
MeasurementUpdateMetadataConverterNGS::new);
5054
registry.put(MeasurementUpdateInformationPxP.class,
5155
MeasurementUpdateMetadataConverterPxP::new);
56+
registry.put(MeasurementUpdateInformationIP.class,
57+
MeasurementUpdateMetadataConverterIP::new);
5258
// Add more mappings ...
5359
}
5460

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package life.qbic.datamanager.files.parsing.converters;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Map;
6+
import life.qbic.datamanager.files.parsing.ParsingResult;
7+
import life.qbic.datamanager.files.structure.measurement.IPMeasurementRegisterColumn;
8+
import life.qbic.projectmanagement.application.api.AsyncProjectService.MeasurementRegistrationInformationIP;
9+
import life.qbic.projectmanagement.application.api.AsyncProjectService.MeasurementSpecificIP;
10+
11+
/**
12+
* Measurement Registration Metadata Converter Immunopeptidomics
13+
* <p>
14+
* Converter that converts a {@link ParsingResult} into a list of
15+
* {@link MeasurementRegistrationInformationIP}.
16+
*
17+
* @since 1.11.0
18+
*/
19+
public class MeasurementRegistrationMetadataConverterIP implements
20+
MetadataConverterV2<MeasurementRegistrationInformationIP> {
21+
22+
@Override
23+
public List<MeasurementRegistrationInformationIP> convert(ParsingResult parsingResult) {
24+
var convertedElements = new ArrayList<MeasurementRegistrationInformationIP>();
25+
26+
for (int i = 0; i < parsingResult.rows().size(); i++) {
27+
var sampleId = parsingResult.getValueOrDefault(i,
28+
IPMeasurementRegisterColumn.SAMPLE_ID.headerName(), "");
29+
var organisationId = parsingResult.getValueOrDefault(i,
30+
IPMeasurementRegisterColumn.ORGANISATION_URL.headerName(), "");
31+
var instrument = parsingResult.getValueOrDefault(i,
32+
IPMeasurementRegisterColumn.INSTRUMENT.headerName(), "");
33+
var facility = parsingResult.getValueOrDefault(i,
34+
IPMeasurementRegisterColumn.FACILITY.headerName(), "");
35+
var sampleMass = parsingResult.getValueOrDefault(i,
36+
IPMeasurementRegisterColumn.SAMPLE_MASS.headerName(), "");
37+
var sampleVolume = parsingResult.getValueOrDefault(i,
38+
IPMeasurementRegisterColumn.SAMPLE_VOLUME.headerName(), "");
39+
var cycleFractionName = parsingResult.getValueOrDefault(i,
40+
IPMeasurementRegisterColumn.CYCLE_FRACTION_NAME.headerName(), "");
41+
var mhcAntibody = parsingResult.getValueOrDefault(i,
42+
IPMeasurementRegisterColumn.MHC_ANTIBODY.headerName(), "");
43+
var mhcTypingMethod = parsingResult.getValueOrDefault(i,
44+
IPMeasurementRegisterColumn.MHC_TYPING_METHOD.headerName(), "");
45+
var enrichmentMethod = parsingResult.getValueOrDefault(i,
46+
IPMeasurementRegisterColumn.ENRICHMENT_METHOD.headerName(), "");
47+
var prepDate = parsingResult.getValueOrDefault(i,
48+
IPMeasurementRegisterColumn.PREP_DATE.headerName(), "");
49+
var msRunDate = parsingResult.getValueOrDefault(i,
50+
IPMeasurementRegisterColumn.MS_RUN_DATE.headerName(), "");
51+
var lcmsMethod = parsingResult.getValueOrDefault(i,
52+
IPMeasurementRegisterColumn.LCMS_METHOD.headerName(), "");
53+
var lcColumn = parsingResult.getValueOrDefault(i,
54+
IPMeasurementRegisterColumn.LC_COLUMN.headerName(), "");
55+
var dataAcquisition = parsingResult.getValueOrDefault(i,
56+
IPMeasurementRegisterColumn.DATA_ACQUISITION.headerName(), "");
57+
var massRange = parsingResult.getValueOrDefault(i,
58+
IPMeasurementRegisterColumn.MASS_RANGE.headerName(), "");
59+
var retentionTimeRange = parsingResult.getValueOrDefault(i,
60+
IPMeasurementRegisterColumn.RETENTION_TIME_RANGE.headerName(), "");
61+
var chargeRange = parsingResult.getValueOrDefault(i,
62+
IPMeasurementRegisterColumn.CHARGE_RANGE.headerName(), "");
63+
var ionMobilityRange = parsingResult.getValueOrDefault(i,
64+
IPMeasurementRegisterColumn.ION_MOBILITY_RANGE.headerName(), "");
65+
var comment = parsingResult.getValueOrDefault(i,
66+
IPMeasurementRegisterColumn.COMMENT.headerName(), "");
67+
var measurementName = parsingResult.getValueOrDefault(i,
68+
IPMeasurementRegisterColumn.MEASUREMENT_NAME.headerName(), "");
69+
70+
var specificMetadata = new MeasurementSpecificIP(
71+
sampleMass,
72+
sampleVolume,
73+
cycleFractionName,
74+
mhcAntibody,
75+
mhcTypingMethod,
76+
enrichmentMethod,
77+
prepDate,
78+
msRunDate,
79+
lcmsMethod,
80+
lcColumn,
81+
dataAcquisition,
82+
massRange,
83+
retentionTimeRange,
84+
chargeRange,
85+
ionMobilityRange,
86+
comment
87+
);
88+
89+
var metadatum = new MeasurementRegistrationInformationIP(
90+
organisationId,
91+
instrument,
92+
"", // instrumentName - not present in registration template, default empty
93+
facility,
94+
"", // pool group - empty for now, can be added if needed
95+
Map.of(sampleId, specificMetadata),
96+
measurementName
97+
);
98+
convertedElements.add(metadatum);
99+
}
100+
return convertedElements;
101+
}
102+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package life.qbic.datamanager.files.parsing.converters;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Map;
6+
import life.qbic.datamanager.files.parsing.ParsingResult;
7+
import life.qbic.datamanager.files.structure.measurement.IPMeasurementEditColumn;
8+
import life.qbic.projectmanagement.application.api.AsyncProjectService.MeasurementSpecificIP;
9+
import life.qbic.projectmanagement.application.api.AsyncProjectService.MeasurementUpdateInformationIP;
10+
11+
/**
12+
* Measurement Update Metadata Converter IP
13+
* <p>
14+
* Converter that converts a {@link ParsingResult} into a list of
15+
* {@link MeasurementUpdateInformationIP}.
16+
*
17+
* @since 1.11.0
18+
*/
19+
public class MeasurementUpdateMetadataConverterIP implements
20+
MetadataConverterV2<MeasurementUpdateInformationIP> {
21+
22+
@Override
23+
public List<MeasurementUpdateInformationIP> convert(ParsingResult parsingResult) {
24+
var convertedElements = new ArrayList<MeasurementUpdateInformationIP>();
25+
26+
for (int i = 0; i < parsingResult.rows().size(); i++) {
27+
var measurementId = parsingResult.getValueOrDefault(i,
28+
IPMeasurementEditColumn.MEASUREMENT_ID.headerName(), "");
29+
var sampleId = parsingResult.getValueOrDefault(i,
30+
IPMeasurementEditColumn.SAMPLE_ID.headerName(), "");
31+
var organisationId = parsingResult.getValueOrDefault(i,
32+
IPMeasurementEditColumn.ORGANISATION_URL.headerName(), "");
33+
var instrument = parsingResult.getValueOrDefault(i,
34+
IPMeasurementEditColumn.INSTRUMENT.headerName(), "");
35+
var instrumentName = parsingResult.getValueOrDefault(i,
36+
IPMeasurementEditColumn.INSTRUMENT_NAME.headerName(), "");
37+
var facility = parsingResult.getValueOrDefault(i,
38+
IPMeasurementEditColumn.FACILITY.headerName(), "");
39+
var samplePoolGroup = parsingResult.getValueOrDefault(i,
40+
IPMeasurementEditColumn.SAMPLE_NAME.headerName(), "");
41+
var measurementName = parsingResult.getValueOrDefault(i,
42+
IPMeasurementEditColumn.MEASUREMENT_NAME.headerName(), "");
43+
44+
// Sample-specific metadata
45+
var sampleMass = parsingResult.getValueOrDefault(i,
46+
IPMeasurementEditColumn.SAMPLE_MASS.headerName(), "");
47+
var sampleVolume = parsingResult.getValueOrDefault(i,
48+
IPMeasurementEditColumn.SAMPLE_VOLUME.headerName(), "");
49+
var cycleFractionName = parsingResult.getValueOrDefault(i,
50+
IPMeasurementEditColumn.CYCLE_FRACTION_NAME.headerName(), "");
51+
var mhcAntibody = parsingResult.getValueOrDefault(i,
52+
IPMeasurementEditColumn.MHC_ANTIBODY.headerName(), "");
53+
var mhcTypingMethod = parsingResult.getValueOrDefault(i,
54+
IPMeasurementEditColumn.MHC_TYPING_METHOD.headerName(), "");
55+
var enrichmentMethod = parsingResult.getValueOrDefault(i,
56+
IPMeasurementEditColumn.ENRICHMENT_METHOD.headerName(), "");
57+
var prepDate = parsingResult.getValueOrDefault(i,
58+
IPMeasurementEditColumn.PREP_DATE.headerName(), "");
59+
var msRunDate = parsingResult.getValueOrDefault(i,
60+
IPMeasurementEditColumn.MS_RUN_DATE.headerName(), "");
61+
var lcmsMethod = parsingResult.getValueOrDefault(i,
62+
IPMeasurementEditColumn.LCMS_METHOD.headerName(), "");
63+
var lcColumn = parsingResult.getValueOrDefault(i,
64+
IPMeasurementEditColumn.LC_COLUMN.headerName(), "");
65+
var dataAcquisition = parsingResult.getValueOrDefault(i,
66+
IPMeasurementEditColumn.DATA_ACQUISITION.headerName(), "");
67+
var massRange = parsingResult.getValueOrDefault(i,
68+
IPMeasurementEditColumn.MASS_RANGE.headerName(), "");
69+
var retentionTimeRange = parsingResult.getValueOrDefault(i,
70+
IPMeasurementEditColumn.RETENTION_TIME_RANGE.headerName(), "");
71+
var chargeRange = parsingResult.getValueOrDefault(i,
72+
IPMeasurementEditColumn.CHARGE_RANGE.headerName(), "");
73+
var ionMobilityRange = parsingResult.getValueOrDefault(i,
74+
IPMeasurementEditColumn.ION_MOBILITY_RANGE.headerName(), "");
75+
var comment = parsingResult.getValueOrDefault(i,
76+
IPMeasurementEditColumn.COMMENT.headerName(), "");
77+
78+
var specificMetadata = new MeasurementSpecificIP(
79+
sampleMass,
80+
sampleVolume,
81+
cycleFractionName,
82+
mhcAntibody,
83+
mhcTypingMethod,
84+
enrichmentMethod,
85+
prepDate,
86+
msRunDate,
87+
lcmsMethod,
88+
lcColumn,
89+
dataAcquisition,
90+
massRange,
91+
retentionTimeRange,
92+
chargeRange,
93+
ionMobilityRange,
94+
comment
95+
);
96+
97+
var metaDatum = new MeasurementUpdateInformationIP(
98+
measurementId,
99+
organisationId,
100+
instrument,
101+
instrumentName,
102+
facility,
103+
samplePoolGroup,
104+
Map.of(sampleId, specificMetadata),
105+
measurementName
106+
);
107+
convertedElements.add(metaDatum);
108+
}
109+
return convertedElements;
110+
}
111+
}

0 commit comments

Comments
 (0)