The calculator lets anyone, logged in or not, check the D-score of a list of elements without saving anything. `calculateDScore(itemIds)` is in the schema, but it answers `NOT_IMPLEMENTED` (PR #65). This issue builds it, and the D-score function that `Routine.dScore` will use too (see the issue for the gymnast and routine resolvers). ## Scope - **A pure function** `calculateDScore(items)` in `backend/src/dscore.ts` that follows constraint 13 in `docs/DATA_MODEL.md`: the sum of the eight highest values, each code counted once; on vault the D-value of the single vault. It works in tenths (whole numbers), so the sum has no rounding errors, and converts to a number at the end. An empty list gives `0`. - **The query** `calculateDScore(itemIds: [ID!]!): Float!` loads the elements and vaults for the ids (one query for each table, no loop) and calls the function. Open to everyone. - **Errors.** More than 50 ids, or an id that does not exist, gives `BAD_USER_INPUT`. `INVALID_ITEMS` (a new error class and code in `backend/src/errors.ts`) is thrown when the items do not fit together (a vault together with elements, more than one vault, items from different apparatus) or when an item has been removed. The same id twice is accepted and counts once. - **Tests.** The function without a database (fewer than eight items, exactly eight, more than eight, the same code twice, a vault, an empty list, tenths that would give `0.30000000000000004` as floats), and the query against the seeded catalog for every error above. - **Docs.** `docs/API.md` (the rule and the errors) and the README if it mentions the calculator. ## Done when - [ ] `calculateDScore` returns the right score for elements and for a vault, and `0` for an empty list - [ ] Each code counts once, and only the eight highest values count - [ ] The error cases above give the right code, and a removed item gives `INVALID_ITEMS` - [ ] The query count does not grow with the number of ids - [ ] The tests pass, and lint, typecheck and build pass - [ ] `docs/API.md` describes the rule and the errors Depends on the GraphQL server (#28) and the missing-field changes in PR #49 (the `removed` rule).