# Business rules — DigitalBio

Evidence date: 2026-10-17. Revision: `master` `2739231` (2023-03-07).

No business-rules document existed in the repository or in `silicon_ark/DigitalBio/` before this file. The README states local run steps and an API host. It states no commercial rule, no pricing rule, and no approval rule.

Rules below are labelled:

- **Documented** — written in the README, a UI string, or a code comment.
- **Implemented** — enforced by the code that runs.
- **Inferred** — a reasonable reading of that code, not executed against the live API for this document.

Where a database constraint or a deployed AWS setting might differ from the repo, the rule is **Unknown / Requires Confirmation**.

## Identity and access

### BR-001 — The shell is visible only after Cognito sign-in

**Implemented.** `src/App.js` exports `withAuthenticator(App)`. The ML Results, Jobs, and Canvasses layout is inside `App`.

The identity pool does not allow unauthenticated identities (`amplify/backend/auth/digitalbio5fd52944/cli-inputs.json`).

### BR-002 — Sign-in uses a username, and email is a required attribute

**Implemented** in the same Cognito config. `usernameAttributes` is empty and `aliasAttributes` is empty, so the pool is not configured to use email as the username. `requiredAttributes` is `email`. `autoVerifiedAttributes` and `verificationMechanisms` are email. `usernameCaseSensitive` is false.

Commit `e45c8ee` is titled "Change auth to allow username".

`SignIn.jsx` labels a field "Email", and nothing imports that file. The on-screen label of the Amplify widget is **Unknown / Requires Confirmation** because `aws-exports.js` is not in the clone.

### BR-003 — Password length is at least 8, MFA is off, and there are no groups

**Implemented** in `cli-inputs.json` and `amplify/backend/backend-config.json`.

- `passwordPolicyMinLength` is 8.
- `passwordPolicyCharacters` is empty and `defaultPasswordPolicy` is false.
- `mfaConfiguration` is `OFF`.
- `userPoolGroupList` is empty.
- No screen reads a group or a role.

There is no second persona in the UI. Header shows `user.username` only.

### BR-004 — Sign-up is not hidden by this app

**Inferred.** The `withAuthenticator` call passes a header component and does not pass `hideSignUp`. Amplify UI shows a sign-up path unless that flag is set. Not executed in a browser for this document.

`SignUp.jsx` implements `Auth.signUp` plus a confirmation code and is **Unused**. It does not add a rule of its own.

Who is allowed to register is **Unknown / Requires Confirmation**. The repo has no invite list and no admin approval step.

### BR-005 — The data API paths are configured as open

**Implemented** in `amplify/backend/api/apia19f7356/cli-inputs.json`. Every path sets `permissions.setting` to `open`.

The React shell still sits behind BR-001. A caller who can reach the API host does not need the React gate for these routes, according to this config. Whether the deployed API Gateway matches the file is **Unknown / Requires Confirmation**.

S3 presigns do require Cognito credentials (`Auth.currentCredentials()` in `src/util/s3Utils.js`).

### BR-006 — Sign out is the header button

**Implemented.** `AppHeader` calls the `signOut` prop from `withAuthenticator`. There is no separate session timeout in this repo. The pool's refresh-token validity is 30 (the unit in this Amplify field is days). **Documented** by the config key `userpoolClientRefreshTokenValidity`.

## What the person can see

### BR-007 — Three destinations, and three more pages that the sidebar does not list

**Implemented** in `src/App.js`.

| Control | Label | Page state |
|---|---|---|
| Sidebar button | ML Results | `Dashboard` |
| Sidebar button | Jobs | `Jobs` |
| Sidebar button | Canvasses | `Canvas Search` |
| Search submit | (no sidebar label) | `Slide Explorer` |
| Tile click | (no sidebar label) | `Result Tile Viewer` |
| Canvas click | (no sidebar label) | `Canvas Result Viewer` |

The page state is React state. Refresh starts again at `Dashboard`. There is no router and no deep link. **Implemented.**

### BR-008 — ML Results lists tile names for a slide search

**Implemented** in `src/Pages/Dashboard.jsx` and `GET /searchslides/:searchValue`.

- The field placeholder is "Slide No. Search" (**Documented** by that UI string).
- The client calls the API only when the text length is at least 3.
- The server keeps `image` rows whose `slide` contains the text, case-insensitive, ordered by `slide`, at most 10.
- The table cell is `name`, not `slide` (commit `2739231`).
- One or two characters show no rows and do not call the API.
- An empty field loads `GET /getlastslides`: at most 10 `image` rows ordered by `slide`.

"Tiles Found" is the heading when the array is non-empty (**Documented** by that string).

### BR-009 — Submitting search opens every matching tile as a card

**Implemented.** Submit sets the page to Slide Explorer unless the placeholder is "Job No. Search" or "Canvas No. Search".

`GET /getslides/:slideNumber` uses the same contains match on `image.slide` and does not set a limit.

The card label is `name`. Clicking a card opens that tile.

### BR-010 — A tile shows a slide number, a tile number, an image, and result rows

**Implemented** in `src/Pages/ResultTileViewer.jsx`.

| On screen | Source |
|---|---|
| Slide Number | `image.slide` |
| Tile Number | `image.name` |
| Image | presigned `uri`, or the no-image asset |
| Result table ID | `bbox.id` |
| Result | `bbox.results` |
| Confidence | `Math.round(bbox.score * 100)` with a percent sign |

`GET /imageresult/:imageID` selects `bbox` rows with `image_id` equal to that id.

What `results` is allowed to contain is **Unknown / Requires Confirmation**. No code list, enum, or dictionary is in the repo.

The confidence rule treats `score` as a fraction. If the column is already a percentage, the screen will still multiply by 100. The stored scale is **Unknown / Requires Confirmation**.

### BR-011 — Previous and next move by numeric id and skip gaps

**Implemented** in the tile viewer and, with the same shape, in the canvas viewer.

- Next increments the id by 1 when the id is below `max(id)`.
- Previous decrements the id by 1 when the id is not 1.
- If the GET-by-id response has no row, the viewer increments or decrements again in the current direction, with the same bounds.
- Previous is disabled at id 1 (and when the id is still 0). Next is disabled when the id equals the max.

**Inferred:** ids are expected to be positive integers. A non-numeric id would not walk reliably. Not executed.

### BR-012 — A job is done only when perc is exactly 1

**Implemented** in `src/Pages/Jobs.jsx`.

- `perc === 1` displays `Done`.
- Any other value displays `Math.round(perc * 100)` with a percent sign.
- The comparison is strict. A string `"1"` would not match `Done`.

**Inferred** from that expression. The column type is **Unknown / Requires Confirmation**.

The Started column is the browser-local calendar date and time of `created_date`, as `YYYY/MM/DD` plus hour and minute without padding. **Implemented.**

### BR-013 — Job search filters the name, and the default list is ten names

**Implemented.**

- Placeholder text is "Job No. Search".
- `GET /getjobbyname/:jobname` matches `batch_transform_jobs.name`, case-insensitive, ordered by `name`, no limit.
- The client sends the request for every non-empty change. There is no minimum length.
- `GET /getlastjobs` returns at most 10 rows ordered by `name`.

The placeholder says "Job No." and the query does not filter `id`. Whether `name` is the job number is **Unknown / Requires Confirmation**.

Rows are not selectable. No rule in the UI opens a job.

The commented title cell reads `result.title`. That is not a displayed rule. Whether the column exists is **Unknown**.

### BR-014 — Canvas search filters the slide column, and the viewer titles the canvas by name

**Implemented.**

- `GET /getCanvas/:searchValue` matches `canvas.slide`, case-insensitive, ordered by `slide`, no limit.
- The client sends that request when the text length is at least 1.
- `GET /getlastcanvas` returns at most 10 rows ordered by `name`.
- The viewer heading "Canvas Number" renders `canvas.name`.
- The card caption that would have shown `canvas.name` is commented out.

The search field on this page says "Slide No. Search". **Documented** by the placeholder. **Inferred:** the person is searching a slide identifier stored on the canvas, while the viewer calls `name` the canvas number. Whether those two columns are the same kind of identifier is **Unknown / Requires Confirmation**.

### BR-015 — A canvas lists tiles by grid position

**Implemented.** `GET /canvastiles/:canvasid` selects `tiles` where `canvas_id` equals that id. The table shows `row/col` and `name`.

No code sorts the tiles. Order is whatever PostgreSQL returns for that statement. **Implemented** (no `ORDER BY`).

### BR-016 — The Dashboard button on a canvas returns to ML Results

**Implemented.** The button label is `Dashboard`. The click sets page name `Dashboard`, which is the ML Results screen. It does not return to the canvas list.

### BR-017 — Images come from a presigned URL that lasts one hour

**Implemented** in `src/util/getDataWithSignedURL.js` and `src/util/s3Utils.js`.

- Rows with a `uri` get a `signedUrl`.
- Expiry argument is `3600`.
- Region is `eu-west-1`.
- The bucket is the first path segment after `//`. The rest is the object key.

If `uri` is an HTTPS virtual-hosted S3 URL, that parse treats the hostname as the bucket. The real URI format is **Unknown / Requires Confirmation**.

Missing presign on a card uses `No_Image_Available.jpg`. In the viewers, a row with no `signedUrl` and with either `uri` or `name` uses that asset. A row with neither `uri` nor `name` and no `signedUrl` stays a placeholder.

## Data rules enforced in the Lambda

These are the filters the handler actually runs. They are **Implemented** in `amplify/backend/function/SearchTiles/src/app.js`. There is no schema file, so uniqueness, nullability, and foreign keys are **Unknown / Requires Confirmation**.

| Route | Table | Rule in the SQL |
|---|---|---|
| `/getlastslides` | `image` | All columns, order by `slide`, limit 10 |
| `/searchslides/:searchValue` | `image` | `slide` contains the value, case-insensitive, order by `slide`, limit 10 |
| `/getslides/:slideNumber` | `image` | `slide` contains the value, case-insensitive, order by `slide`, no limit |
| `/getslidebyid/:slideid` | `image` | `id` equals the value |
| `/maximageid` | `image` | `max(id)` |
| `/imageresult/:imageID` | `bbox` | `image_id` equals the value |
| `/getlastjobs` | `batch_transform_jobs` | All columns, order by `name`, limit 10 |
| `/getjobbyname/:jobname` | `batch_transform_jobs` | `name` contains the value, case-insensitive, order by `name`, no limit |
| `/getlastcanvas` | `canvas` | All columns, order by `name`, limit 10 |
| `/getCanvas/:searchValue` | `canvas` | `slide` contains the value, case-insensitive, order by `slide`, no limit |
| `/getcanvasbyid/:canvasid` | `canvas` | `id` equals the value |
| `/maxcanvasid` | `canvas` | `max(id)` |
| `/canvastiles/:canvasid` | `tiles` | `canvas_id` equals the value |

The handler has no other routes. Nothing in this repository inserts, updates, or deletes those rows. The system that writes them is **Unknown / Requires Confirmation**.

### BR-018 — Search text is concatenated into the SQL string

**Implemented.** The path parameters for the contains-matches and for the id equalities are inserted into the SQL text.

This is a property of the code, not a business policy that was written down. A request value that changes the SQL changes the query. Human review is required before treating it as accepted behaviour.

### BR-019 — A database error does not produce an HTTP error body

**Implemented.** Each route's `catch` logs the error and does not call `res.status`. The function timeout in the template is 25 seconds.

The React `catch` logs and, on the list screens, does not set `isLoading` back to false.

### BR-020 — Database credentials are read on every request

**Implemented.** Express middleware calls Secrets Manager in `eu-west-1` for secret id `webapp_admin_user` and reads `username`, `host`, `dbname`, `password`, and `port`.

On failure it logs and continues. The `credentials` object is module-scoped, so a failed refresh leaves the previous value in place. The first failure leaves it unset, and the query then fails. **Implemented.**

The checked-in Lambda policy does not allow `secretsmanager:GetSecretValue`. Whether the live role does is **Unknown / Requires Confirmation**.

## Display strings that look like rules

These are **Documented** because they are the words on the screen. They are not defined further anywhere in the repo.

| String | Where |
|---|---|
| Digical Digital | Browser title and manifest |
| Logged In | Header |
| ML Results | Sidebar. Page state is still `Dashboard` |
| Jobs | Sidebar |
| Canvasses | Sidebar |
| Slide No. Search | ML Results and Canvasses placeholders |
| Job No. Search | Jobs placeholder |
| Tiles Found / No results found | ML Results table |
| No Results found | Slide Explorer |
| No Jobs Found | Jobs |
| No data to display | Canvasses |
| Done | Job status when `perc === 1` |
| Search Number | Slide Explorer |
| Slide Number / Tile Number | Tile viewer |
| Image Viewer / Search Results | Tile viewer |
| Canvas Number / Scan Result / Grid Index | Canvas viewer |
| Dashboard | Button on the canvas viewer |

## Conflicts to confirm

- Sidebar **ML Results** and the canvas button **Dashboard** are the same page.
- **Job No. Search** filters `name`.
- **Canvas Number** shows `canvas.name`, while canvas search filters `canvas.slide`, and the canvas placeholder says **Slide No. Search**.
- Routes named "last" sort by name or slide.
- The UI requires Cognito. The API config marks every route open.
- Title **Digical Digital** and icon path `digicalBio-icon.png` do not match the package name `digitalbio` or the file `public/digitalBio-icon.png`.
