Skip to content

Commit 325f973

Browse files
committed
Merge ts-domain-optocompleteness: type the opto-completeness predicate under strict TS
domain/optoCompleteness.js → strict .ts (behavior-preserving): optoFieldsPresence takes a permissive OptoFields, returns typed OptoFieldsPresence; count provably identical. Payoff: validation/rules/optoRules.ts drops its Parameters<typeof …> cast (Record<string,any> model is assignable to OptoFields). Verified: typecheck clean, validation + sectionStatus + baselines (483) byte-identical, full suite 4793, CI=true build, e2e 104. Code-reviewer: behavior-preserving, zero findings.
2 parents 7363f03 + c216cfd commit 325f973

4 files changed

Lines changed: 58 additions & 32 deletions

File tree

‎.claude/docs/research/architecture-assessment.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ or enforced tokens** (F6), and **deferred lint debt that weakens the build gate*
3636

3737
| Gap | Evidence | Why it bites refactoring |
3838
| --- | --- | --- |
39-
| **No static types** ⏳ PARTIAL | Incremental TS underway (`tsc --noEmit` CI gate live): `io/yaml.ts`, `state/workspaceTypes.ts` + the task-catalog modules, `domain/deviceOverrideMerge.ts`, and the Phase-9a validation leaf modules (`repairRouting`/`geometryProvenance`/`dayOverrideValidation`/`stepStatus`) are `.ts` under `strict`; the **entire `validation/` directory** (leaf predicates + `rules/*` families + `rulesValidation` composer + `index`/`schemaValidation`/`rawShape`/`quickChecks`/`useQuickChecks` + the shared `issueTypes.ts`) is now `.ts` under `strict`; the `state/` migration is underway — the persistence layer (`state/workspaceMigrations.ts` + `state/persistence.ts`) the canonical read layer (`state/workspaceSelectors.ts`, `unknown`-in / typed-container-out), and the first selector-consumers (`state/configDiff.ts`, `state/cameraUsage.ts`) are `.ts`. Remaining: most of `state/` (`workspaceUtils`/`workspaceTransitions`/`workspaceActions`/…) + `domain/`, most components, `useWorkspace`, and the legacy form path stay JS (`checkJs:false`) | Rename a domain fn or change an `Animal`/`Day` shape → **silent until a test happens to exercise it.** No safe IDE rename, no null-safety. Tooling agent scored type-safety **2/10** (improving as the pure core converts). |
39+
| **No static types** ⏳ PARTIAL | Incremental TS underway (`tsc --noEmit` CI gate live): `io/yaml.ts`, `state/workspaceTypes.ts` + the task-catalog modules, `domain/deviceOverrideMerge.ts` + `domain/optoCompleteness.ts`, and the Phase-9a validation leaf modules (`repairRouting`/`geometryProvenance`/`dayOverrideValidation`/`stepStatus`) are `.ts` under `strict`; the **entire `validation/` directory** (leaf predicates + `rules/*` families + `rulesValidation` composer + `index`/`schemaValidation`/`rawShape`/`quickChecks`/`useQuickChecks` + the shared `issueTypes.ts`) is now `.ts` under `strict`; the `state/` migration is underway — the persistence layer (`state/workspaceMigrations.ts` + `state/persistence.ts`) the canonical read layer (`state/workspaceSelectors.ts`, `unknown`-in / typed-container-out), and the first selector-consumers (`state/configDiff.ts`, `state/cameraUsage.ts`) are `.ts`. Remaining: most of `state/` (`workspaceUtils`/`workspaceTransitions`/`workspaceActions`/…) + `domain/`, most components, `useWorkspace`, and the legacy form path stay JS (`checkJs:false`) | Rename a domain fn or change an `Animal`/`Day` shape → **silent until a test happens to exercise it.** No safe IDE rename, no null-safety. Tooling agent scored type-safety **2/10** (improving as the pure core converts). |
4040
| **Oversized files/components** ⏳ PARTIAL | ~~`domain/validation.js` 1142~~ ✅ split (Phase 9a) into routing/provenance/producers/composer/step-status modules · ~~`validation/rulesValidation.js` 1108~~ ✅ split into `validation/rules/*` families (reference / opto / channelMap / electrodeGroup / dandiSubject / identity / behavioralEvent) + a 96-LOC composer · `valueList.js` 1073 · ~~`ValidationSummary/index.jsx` 1033~~ ✅ decomposed (Phase 9c-1) into rows-helpers / batch-action hook / ExportReport / BatchExportPreflight / DayStatusTable (index now 306) · ~~`DevicesStep.jsx` 825~~ ✅ decomposed (Phase 9c-3) into CamerasUsedSection / OverrideCleanupSection / ConfigVersionPanel / ElectrodeGroupsAccordion (step now 424) · `OptogeneticsFields.jsx` 826 · ~~`RecordingDaysTab.jsx` 822~~ ✅ decomposed (Phase 9c-2) into AnimalSetupCard / ExistingDataReview / DayList / DuplicateDayModal (tab now 496) · ~~`useWorkspace.js` 851~~ ✅ split into `workspaceActions` (createWorkspaceActions factory) / `useWorkspacePersistence` (status + autosave + saveNow) / `workspaceHydration` (resolveInitialWorkspace) + a pure `getAnimalDays` in `workspaceSelectors` (hook now 98) | High intrinsic load to change; many concerns per file; merge-conflict magnets. |
4141
| **~2,000 LOC legacy dead-weight** | `OptogeneticsFields.jsx` (826, imported only by `LegacyFormView`), `element/*`, `*Fields`, `LegacyFormView.jsx` — parallel to the workspace path | Every workspace UX/styling change risks needing a legacy twin; or the legacy code rots unmaintained. |
4242
| **CSS: no scoping, partial tokens** (= F6) | Global CSS, `.button-primary` redefined in 6 files, two different error icons, ~60% token adoption, mixed `.css`/`.scss` | Visual inconsistency *and* fragile edits — changing a shared style can leak across components. |

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Changed
1111

12+
- **Typed the optogenetics-completeness predicate under strict TS (refactor only, no behavior change).**
13+
[domain/optoCompleteness.js](src/domain/optoCompleteness.ts) → `.ts`: `optoFieldsPresence` (the single
14+
source of truth for "which of the four opto fields count as present") now takes a permissive
15+
`OptoFields` and returns a typed `OptoFieldsPresence` (`{ …four booleans, count }`); the `isOptoList…`
16+
/ `isOptoSoftware…` helpers take `unknown`. Body equivalent — the `presence.count = …` post-assignment
17+
is rewritten as four `const`s + a single returned object (same values). As a **payoff, the consumer
18+
`validation/rules/optoRules.ts` drops its `model as Parameters<typeof optoFieldsPresence>[0]` cast** —
19+
a `Record<string, any>` model is assignable to `OptoFields` directly. The other consumer
20+
(`domain/sectionStatus.js`) is JS, so the rename is transparent. Validation (incl. opto rules) +
21+
`sectionStatus` + golden baselines (483) pass; `npm run typecheck` + `CI=true` build clean; full suite
22+
4793; e2e 104.
23+
1224
- **Typed the day camera-usage helpers under strict TS (refactor only, no behavior change).**
1325
[state/cameraUsage.js](src/state/cameraUsage.ts) → `.ts`: `inferredCameraKeys` / `referencedCameraKeys`
1426
/ `resolveDayCameraUsage` (→ `Camera[]`) / `findCameraAffectedDays` and the `cameraKey` helper are
Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,49 +18,63 @@
1818
* site reads them from its own source).
1919
*/
2020

21+
/** The four optogenetics field VALUES (each call site reads them from its own source). */
22+
export interface OptoFields {
23+
/** The excitation-source list. */
24+
opto_excitation_source?: unknown;
25+
/** The optical-fiber list. */
26+
optical_fiber?: unknown;
27+
/** The virus-injection list. */
28+
virus_injection?: unknown;
29+
/** The stimulation-software string. */
30+
optogenetic_stimulation_software?: unknown;
31+
}
32+
33+
/** Each optogenetics field's presence boolean plus the total count of present fields. */
34+
export interface OptoFieldsPresence {
35+
opto_excitation_source: boolean;
36+
optical_fiber: boolean;
37+
virus_injection: boolean;
38+
optogenetic_stimulation_software: boolean;
39+
count: number;
40+
}
41+
2142
/**
2243
* A list opto field counts as present only when it's a non-empty array (corrupt non-array → absent).
2344
* @param value
2445
*/
25-
const isOptoListPresent = (value) => Array.isArray(value) && value.length > 0;
46+
const isOptoListPresent = (value: unknown): boolean => Array.isArray(value) && value.length > 0;
2647

2748
/**
2849
* The software field counts as present only when it's a non-empty trimmed string.
2950
* @param value
3051
*/
31-
const isOptoSoftwarePresent = (value) => typeof value === 'string' && value.trim() !== '';
52+
const isOptoSoftwarePresent = (value: unknown): boolean =>
53+
typeof value === 'string' && value.trim() !== '';
3254

3355
/**
3456
* Compute the per-field presence of the four optogenetics fields from their VALUES.
3557
*
36-
* @param {object} fields - The four field values (read from the caller's own source).
37-
* @param {*} fields.opto_excitation_source - The excitation-source list.
38-
* @param {*} fields.optical_fiber - The optical-fiber list.
39-
* @param {*} fields.virus_injection - The virus-injection list.
40-
* @param {*} fields.optogenetic_stimulation_software - The stimulation-software string.
41-
* @returns {{
42-
* opto_excitation_source: boolean,
43-
* optical_fiber: boolean,
44-
* virus_injection: boolean,
45-
* optogenetic_stimulation_software: boolean,
46-
* count: number,
47-
* }} Each field's presence boolean plus the total count of present fields.
58+
* @param fields - The four field values (read from the caller's own source).
4859
*/
49-
export function optoFieldsPresence(fields) {
50-
const source = fields || {};
51-
const presence = {
52-
opto_excitation_source: isOptoListPresent(source.opto_excitation_source),
53-
optical_fiber: isOptoListPresent(source.optical_fiber),
54-
virus_injection: isOptoListPresent(source.virus_injection),
55-
optogenetic_stimulation_software: isOptoSoftwarePresent(
56-
source.optogenetic_stimulation_software
57-
),
60+
export function optoFieldsPresence(fields: OptoFields | null | undefined): OptoFieldsPresence {
61+
const source: OptoFields = fields || {};
62+
const opto_excitation_source = isOptoListPresent(source.opto_excitation_source);
63+
const optical_fiber = isOptoListPresent(source.optical_fiber);
64+
const virus_injection = isOptoListPresent(source.virus_injection);
65+
const optogenetic_stimulation_software = isOptoSoftwarePresent(
66+
source.optogenetic_stimulation_software
67+
);
68+
return {
69+
opto_excitation_source,
70+
optical_fiber,
71+
virus_injection,
72+
optogenetic_stimulation_software,
73+
count: [
74+
opto_excitation_source,
75+
optical_fiber,
76+
virus_injection,
77+
optogenetic_stimulation_software,
78+
].filter(Boolean).length,
5879
};
59-
presence.count = [
60-
presence.opto_excitation_source,
61-
presence.optical_fiber,
62-
presence.virus_injection,
63-
presence.optogenetic_stimulation_software,
64-
].filter(Boolean).length;
65-
return presence;
6680
}

‎src/validation/rules/optoRules.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function optogeneticsRules(model: ValidationModel): ValidationIssue[] {
3131
// nav count can never drift. The arrays use Array.isArray(...) && length > 0 (a corrupt non-array
3232
// value is NOT present); the software is a non-empty trimmed string — matching the converter's
3333
// len()>0 on the string.
34-
const optoPresence = optoFieldsPresence(model as Parameters<typeof optoFieldsPresence>[0]);
34+
const optoPresence = optoFieldsPresence(model);
3535
const hasOptoSource = optoPresence.opto_excitation_source;
3636
const hasOpticalFiber = optoPresence.optical_fiber;
3737
const hasVirusInjection = optoPresence.virus_injection;

0 commit comments

Comments
 (0)