These six pre-date the migration. They were unfixable while the files
holding them were JavaScript with `checkJs`, because the fix in each case
is an annotation JSDoc cannot express. `src/` is TypeScript now, so:
- `setWidthState.timer` in `AssistantPanelContext` is a debounce handle
hung on the callback itself, so it survives re-renders without a ref.
TypeScript has no way to describe an expando on a `useCallback`
result, so the binding is `any`.
- `runAction(name, { skill, ...payload } = {})` — the `= {}` default
types the parameter `{}`, so reading `skill` off it looked wrong.
- `Progression` in `Positions` is a local presentational component
whose `className` is omitted at both call sites, like its siblings in
the same file.
- `normalizeSection`'s `page` reads as required because it has no
default, but `owliverConfig` deliberately calls it without one: `page`
only builds a fallback `where` label, and that caller passes `where`
explicitly, so the branch that would read it never runs. Stating the
parameter object makes `page` optional, which is what the function has
always accepted.
`npm run typecheck` is now 0 errors, from 71 when this migration started
and 20 when Phase 11 began. Nothing was suppressed to reach it: there is
no `@ts-ignore` or `@ts-expect-error` added anywhere in this branch.
4/4 byte-identical, suite and bundle unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
The last nine admin pages. `src/` now contains no `.js` or `.jsx` at all.
One error, and it was the same kind of thing the whole phase has been
finding: `getBandMembers(min, max)` in `TalentPool` is called with one
argument for the top band, and its own body reads
`max == null || s < max`. The parameter has always been optional
everywhere except the signature. The marker erases.
9/9 byte-identical, and the bundle still hashes to 74d17e2d…
Three of these nine — `Analytics`, `Candidates`, `HiredHistory` — are
the pages the Owliver baselines cover, and they carry six of the seven
pre-existing suite failures. Those failures are unchanged and still match
`ecf5e75` line for line, which is the point: if a rename had altered what
these pages render, it would have altered how they fail, and it did not.
The baselines are untouched, as they have been in every batch.
Phase 11 is 122/122 files and eleven batches. Across all of them:
type erasure 122/122 byte-identical
bundle 74d17e2d… unchanged from `ecf5e75` in every batch
npm test 1684/1691 with the same 7 failures, in every batch
typecheck 20 errors at the start, 6 now
The remaining 6 all pre-date the migration. They are no longer in
JavaScript, so they are now fixable; that is the next commit, not this
one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Nine admin pages: `AgentDetail`, both skill editors, the three Workspace
pages, `SkillDevelopment`, `Settings` and `Profile`. 9/9 byte-identical,
bundle hash unchanged.
Six errors, five of them the same shape this phase has met repeatedly:
`Object.values(skill.ui || {}).flatMap((page) => page.sections)` yields
`unknown` because inference into the union parameter of `values` does not
distribute. Three copies of that line across the two editors and
`WorkspaceSkills`, plus one `Object.entries` and one `new Set` whose
element type reached a React `key`, where `unknown` is not allowed.
The sixth is worth its own line. `WorkspaceSkills.toggleSkill` builds a
preferences patch as `{ disabledSkills }` and then adds `customSkills` to
it conditionally, several lines later, when re-enabling a skill flips an
inactive definition back to active. The literal's inferred type does not
carry a key assigned after the fact, so the later write looked wrong.
The annotation names both keys and marks the conditional one optional,
which is what the function does.
Measured against `b99dc7c`:
typecheck 6 errors, unchanged; no new error anywhere
lint exit 0, 0 errors, 289 warnings
npm test 1684/1691, the same 7 failures verbatim
build exit 0, identical bundle hash 74d17e2d…
type erasure 113/113 byte-identical across Phase 11 so far
Nine files left in Phase 11, all data pages. No baseline artifact
touched — three of those nine are the pages the baselines cover, so the
recapture question arrives with the next batch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
The eight `nodes` files under `src/pages/admin/*/`, which declare what
each admin page is made of. Renamed with no annotations needed at all:
zero new errors, 8/8 byte-identical, bundle hash unchanged.
They came through clean because everything they call was typed first —
`makeNode` and the registry in batch 1, the skill sections in batch 2.
A descriptor table is only as checkable as the constructor it feeds.
`positions/nodes.js` became `.tsx`, joining its seven `nodes.jsx`
siblings. That is the exact case `scripts/ssr-resolve.mjs` was written
for and says so in its comment: it is the only `.js` under `src/pages`,
`skill-check.mjs` loads it by literal path as `.js`, and the resolver
tries `.ts` and `.tsx` in turn. The suite loads it and still passes,
which is the first time that particular branch has been exercised.
Two of these files are also read as source TEXT rather than loaded —
`candidates/nodes.jsx` at `skill-check.mjs:6049` and
`talent-pool/nodes.jsx` at `:6298`, both through `resolveSourcePath`.
That is the second channel, the one that was missed in Phase 0 and found
by an ENOContent failure in Phase 4b-ii. Both resolve.
Measured against `7a95d95`:
typecheck 6 errors, unchanged; no new error anywhere
lint exit 0
npm test 1684/1691, the same 7 failures verbatim
build exit 0, identical bundle hash 74d17e2d…
type erasure 104/104 byte-identical across Phase 11 so far
No baseline artifact touched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Phase 10: `App`, `main`, `index.html`, and the four remaining `src/pages` files.
ROUTING IS PROVEN UNCHANGED, not assumed. The 49 `<Route>` elements were
fingerprinted before the rename and compared after: byte-identical, all 44
paths, all 13 `<Navigate>` redirects, the same nesting under `ProtectedRoute`
and `AdminRoute`, and the same provider order - AuthProvider, then
QueryClientProvider, then Router, with ScrollToTop and AuthenticatedApp inside
and both toasters as siblings. There is no lazy loading to preserve; there never
was any.
Two coordinated edits the rename forced. `main` imported `@/App.jsx` by explicit
extension, which stops resolving the moment `App` is `.tsx`; it is now
extensionless `@/App`. `index.html` pointed its module script at
`/src/main.jsx`; it points at `/src/main.tsx`. Both are required, and missing
either would have been a blank page rather than a type error.
ONE GENUINE SEMANTIC DIFFERENCE, INVESTIGATED AND ACCEPTED. esbuild elides
unused imports under the TypeScript loader but keeps them under the JavaScript
one, so `App` drops thirteen imports: `Layout`, `Overview`, `Positions`,
`Candidates`, `HiredHistory`, `TalentPool`, `UserTracking`, `Analytics`,
`Profile`, `WorkerProfile`, `KrowIdentity`, `Owliver` and `DesignSystem`. All
thirteen are dead - each has zero JSX uses in `App`, because every route that
once rendered them now `<Navigate>`s to an `/admin/*` equivalent. Before
accepting it I checked that none of the thirteen modules can do anything when
evaluated: no bare side-effect imports, no top-level calls, and every top-level
binding a literal or a function declaration. The consequence is 942 fewer bytes
in the index chunk and thirteen modules no longer evaluated at startup. Nothing
observable changes, and the routes those pages are reached through are
unaffected - they are reached through the `/admin` tree, which is untouched.
That is the first time in this migration the production bundle has changed for
a reason other than a comment, so it is recorded here rather than left to be
noticed later.
`CreatePosition` needed the only real typing. `vetting_criteria` is a `jsonb`
column holding the five weighting percentages the page edits, typed `unknown` by
the registry, and both the total and the three render sites read through it.
`onDone` is called with the created id, so it takes arguments - the generator
had classed it as zero-arg, and that rule is now narrowed to `onClose` alone.
The generator's entity-import rule was narrowed first, as instructed: it now
counts a type as used only when it appears in a type position inside a generated
interface, rather than anywhere in the file text. That is what produced four
unused-import lint errors in the previous batch.
Verified: tsc 21 -> 20, set-difference showing one removed and none added; zero
errors in any of the six files; all four pages emit byte-identical JavaScript;
route fingerprint identical; npm test 1684/1691 with the same seven failures;
Owliver baseline 59/59; lint 0 errors; no deferred agent-region file touched;
baseline artifacts untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Phase 9, first batch: 18 pages. Seventeen emit byte-identical JavaScript, the
eighteenth differs only by a JSDoc cast becoming a real annotation, and the
production bundle is byte-identical to be49184.
SCOPE, decided by imports. Twenty-two of the forty pages import `lib/skills`,
`lib/agents`, `ai-assistant`, `components/skills`, `ui-tree` or `ui-editor`, and
are deferred to Phase 11 with the rest of that region. `Owliver.jsx` is in THIS
batch despite the name: it imports only `krowHooks`, `krowScore`, `krowAi` and
`ui/button`, and is a profile-building conversation page rather than any part of
the agent runtime.
`DesignSystem` is the headline result. It was the single worst file in Phase 6 -
111 errors when the design system was first renamed - and it arrived here with
none, because those were never its errors: they were the primitives' inferred
props, and fixing them at the source fixed every consumer.
Three pages needed real work, and each was the author's own intent made
explicit:
- `admin/Login` carried `useState(/** @type {{email?: string, password?: string}} */ ({}))`.
JSDoc casts stop applying in a `.tsx` file, so that became a real type
argument, and `validate`'s accumulator - built empty and filled per failed
rule - needed the same shape. The login flow itself is untouched: the
generic 401 message, the 429 branch and the `remember` field all stand.
- `Owliver` gets `new Promise<void>`, because its `resolve()` takes no
argument.
- `Candidates` names the element type of a `Set` built from `any[]`, which
otherwise infers `Set<unknown>` and makes every option a `ReactNode` error.
LINT CAUGHT A REGRESSION THE OTHER CHECKS DID NOT. The generator adds an entity
import when it sees the type NAME anywhere in the file, which for four pages
with no props at all left an import nothing used - four `unused-imports` ERRORS,
taking `npm run lint` from exit 0 to exit 1 while typecheck, tests, the bundle
and seventeen of eighteen emit comparisons all stayed green. Removed. The
generator's import rule is too eager and wants narrowing before the next batch.
Verified: tsc 21 -> 21, set-difference showing zero introduced and zero removed;
zero errors in any of the 18 pages; lint back to 0 errors and 289 warnings;
npm test 1684/1691 with the same seven failures; Owliver baseline 59/59;
production bundle byte-identical; baseline artifacts untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Authored in a parallel session alongside the TypeScript migration; committed
separately so the two never share a commit. No TypeScript migration file is
included here.
Candidates becomes the queue of hiring decisions waiting on a person, rather
than a second Talent Pool listing every application the org ever took. Final
selection is DERIVED - there is no `final_selection` value in the
`application_status` enum and none is added. The fact it reads is the existence
of an interview row, a NOT NULL foreign key, rather than
`job_applications.interview_id`, which the schema keeps as an unconstrained soft
reference precisely so it may dangle. `status = 'interview'` is set both when an
interview is arranged and when one is completed, so status alone cannot tell a
queue of people who have been interviewed from a queue of people merely booked
in.
Seats on a position are counted from the employment records instead of a stored
column. A `filled` counter would be a second source of truth, and the day it
disagreed with `staff` nothing could say which was lying. Someone who has left
frees their seat, and over-hiring floors at zero rather than going negative.
`DEMO_FILL` is gone. `hiringRecords.js` padded the hires list with five invented
people so Hired History read as a history rather than as three rows; the padding
reached Analytics too, where "total hires" counted eight against a database
holding three. Hires now come only from `staff`.
Both paths that file an application on somebody's behalf now carry
`worker_profile_id`, the link back to the talent-pool record. The column is
nullable, so omitting it saved cleanly and failed silently: the application
belonged to an email address rather than to a person, and the hire it became
could not be traced back to the profile it came from.
`HiredChronology` used to `return null` with no hires, taking the `chronology`
node identity out of the DOM with it - so on an honest empty dataset the section
could not be addressed by Owliver or the layout editor at all. It now renders an
empty state inside the section it keeps.
Nine new checks cover the above; `npm test` reports 1684/1691.
SIX SSR PARITY CHECKS FAIL ON PURPOSE, and `scripts/__baseline__/README.md`
documents each with verified tag counts. Five are the `DEMO_FILL` removal: the
Hired History and Analytics baselines were captured while the padding was in
effect and, because they render with queries disabled, the padding is all they
contain. The sixth is this change to what Candidates says. Do not regenerate
those baselines to clear them - two of the checks exist to prove the UI node
tree migration added exactly two `<div>`s, and that proof needs the baseline to
be pre-migration markup. Recapturing now would write post-migration markup into
a file named `pre-migration` and the check would compare the current render
against itself forever. The debt is held until the migration work lands, when
both files are recaptured together.
The seventh failure, the stale backend seed fixture, predates all of this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
TWO fixes, both found by using the product rather than reading it.
A question asked on a page that cannot answer it was lost. resolveIntent routes
to the page that can, and routing.js answered "That is on Candidates. Taking
you there now. Ask again once the page loads." — but the panel is page-scoped,
so it re-mounts on the new route and the navigation destroys the very message
explaining why the reader moved. What the reader saw was a different page and a
fresh greeting, with no trace of what they asked.
The question now travels with the destination. AssistantPanelContext sits ABOVE
the router and already has `ask` for exactly this — a page handing a question to
the panel — so the panel that mounts on the other side asks it. That is what
the reader wanted, and what "ask again once the page loads" was apologising for.
It cannot loop: resolveIntent only routes when the destination differs from the
page you are on, and answerableHere short-circuits before that.
The Test tab did not test. "Simulation & Scope Diagnostics" reads where a
question WOULD route — which skills are reachable, which tools are in scope,
what the classifier makes of it — and never calls the model. That is genuinely
useful and it is not what a tab called Test leads anyone to expect. A real test
did exist, but under Skills, as "Test in Owliver" on a capability card.
So the Simulated User Query the tab has always shown is now runnable, through
the same path that card uses: the existing Owliver panel, scoped to the draft's
own agent and disabled skills, so the answer comes from the agent being edited
rather than the published one. The diagnostics stay — they answer a different
and still useful question. The button is disabled when the agent does not cover
the selected page, with the reason in its title, rather than offering a run that
would decline.
924/924 checks pass and the production build is clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g