Phase 8, third batch: the three root components and both layouts. All five emit
byte-identical JavaScript and the production bundle is byte-identical to
3d5f54b.
Small batch, and almost all of it was the rename. `ScrollToTop`,
`UserNotRegisteredError` and both layouts take no props at all - the layouts
render through `<Outlet />` - so inference already described them.
`ProtectedRoute` gains the only real interface, and it clears a standing error.
Both its props have fallbacks - `fallback` defaults to a spinner and
`unauthenticatedElement` falls through `??` to the login redirect - and its one
call site is a bare `<ProtectedRoute />` in `App.jsx`. Typed optional, which is
what the defaults already said, that call site stops being an error: tsc goes
22 -> 21 with nothing introduced.
The auth boundary itself is untouched. The `authError.type === 'user_not_registered'`
branch, the `<Navigate to="/admin/login" state={{ from }}>` redirect and the
`isLoadingAuth || !authChecked` gate are all exactly as they were - which
matters, because `AdminLayout` is one of the files `skill-check.mjs` reads as
source text, and it is read through the resolver added in 02a2ab0.
Verified: tsc 22 -> 21, set-difference showing one removed and none added; zero
errors in any of the five; all five emit byte-identical JavaScript; production
bundle byte-identical; npm test 1684/1691 with the same seven failures; Owliver
baseline 59/59; lint 0 errors; baseline artifacts untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Phase 8, second batch: 16 files. All 16 emit byte-identical JavaScript and the
production bundle is byte-identical to e7e1e98.
`ProfileView` and `CourseView` move from `components/krow/types.ts` to
`@/types/views`, because `components/forge` reads the same `jsonb` columns off
the same courses and profiles. `components/krow/types.ts` stays as a re-export
so that folder's imports are untouched. Two copies of one narrowing would be
two things to keep in step.
SCOPE, decided by imports rather than by folder name. `components/skills` was in
the batch as requested and is DEFERRED: it imports `lib/skills/registry`,
`lib/agents/runtime`, `ai-assistant/PageContext` and `ai-assistant/AgentContext`,
which makes it agent region by the same test this batch used. `ui-tree` and
`ui-editor` are deferred for the same reason - all eleven files drive the UI-node
system, which is Owliver's "move this card" capability. `forge` was checked and
kept: despite the name, it imports nothing from `lib/skills`, `lib/agents` or
`ai-assistant`. It is the worker learning product.
Twenty-six components now declare real props. Two corrections to the pattern
came out of this batch, both from call sites:
- Optionality. Batch 1 made a prop required when it had no default. That is
wrong here: `AdminPage` has seventeen call sites and most pass only `title`.
Required is now reserved for entity-typed props and `children` - what a
component genuinely cannot render without - and everything else is optional,
which is what the JavaScript always allowed.
- Callback arity. `() => void` was too strict: `onOpen`, `onCreate` and
`onBrowse` are called WITH arguments, and `University` passes a `useState`
setter straight through, which has one parameter and is therefore not
assignable to a zero-parameter type. Callbacks take `(...args: any[])`.
`RoleGlyph`'s `GLYPHS` table gets `[RegExp, ComponentType<any>][]` - the same
widening `aiEngine`'s router had, where the element becomes the union of both
positions and neither `pattern.test` nor `<Icon />` works. The author had
already written that exact type as a JSDoc comment; it is now the real
annotation and the comment is gone.
`ForgeHeader` receives `onBrowse` and destructures `_onBrowse`, so the prop is
passed and silently dropped - the same shape as `TalentHero`'s `jobRecs` in the
previous batch. Recorded rather than changed.
Two automated passes were reverted rather than shipped. One added `?` to object
members inside component bodies, not just interface fields, producing
`TS1162: An object member cannot be declared optional` - it was rerun scoped to
`interface XProps` blocks. The other was the generator itself, which annotated
only the FIRST component in each file and so missed `SectionTitle` in
`PageShell`; rewritten to walk every match and splice in reverse, it went from
14 components to 26 and took the error count from 93 to 38.
Two runtime imports were caught by the emitted-JavaScript check and would not
have been caught any other way. The generator added `import * as React` to
`RoleGlyph` for a type-only reference - a real import in the bundle - now
`import type { ComponentType }`. Fixing that, I then removed the React import
`PageShell` genuinely had; restored.
Verified: tsc 22 -> 22, set-difference showing zero introduced and zero removed;
zero errors in any of the 16 files; all 16 emit byte-identical JavaScript;
production bundle byte-identical; npm test 1684/1691 with the same seven
failures; Owliver baseline 59/59; lint 0 errors; baseline artifacts untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Phase 8, first batch: 56 files, plus three supporting edits outside the folder.
All 56 emit byte-identical JavaScript and the production bundle is byte-identical
to 543da9d.
This batch establishes the pattern for the remaining component work: real prop
interfaces built from actual call sites, reusing the generated entity types.
Fifty-four components now declare what they take -
`CandidateCard({ application: JobApplication, jobTitle?: string, rank: number })`,
`MatchedCandidates({ job: JobPosting, profiles: ProfileView[], ... })` - rather
than carrying a compatibility bag. Callbacks are optional because call sites
omit them; entity props are required because call sites always pass them. Where
a call site proved otherwise, the call site won: `ScheduleInterviewModal`'s
`position`, `CandidateCard`'s `jobTitle`, `TalentDetailModal`'s `matchScore` and
`matchReasons` are optional because real callers omit them.
Two view types absorb what the registry cannot describe. `ProfileView` and
`CourseView` narrow the `jsonb` columns these components read through -
`experience`, `completed_courses`, `earned_badges`, `capabilities`,
`score_breakdown`, `challenge`, `quiz` - to arrays and objects, while every
field backed by a real column keeps the registry's type. They are declared once
for the folder, and their narrowed fields stay REQUIRED: the registry has those
columns NOT NULL, and making them optional broke assignment back to the entity
where `ChallengeRunner` hands a course to a mutation.
TWO REAL DEFECTS FOUND, BOTH PRESERVED RATHER THAN FIXED:
1. `SuggestedTalent` calls `matchTalent.mutate({...}).then(...)`. React
Query's `mutate` returns `void`, so that `.then` throws at run time, and
`runMatch` is reachable from a button. `mutateAsync` is what the code
means. Swapping it turns a crash into a working feature, which is a
product change, not a migration one - so it is cast to compile and left
behaving exactly as it did. This one deserves a fix on its own terms.
2. `TalentPoolCard` renders a location row behind `profile.location &&`, but
`worker_profiles` has no `location` column - the registry has none and the
API cannot send one, so the row has never rendered. Recorded as an
optional field on `ProfileView` with a note, the same treatment as
`user.avatar_url` in Phase 7.
Also fixed, all type-only: the Web Speech API declared as the optional `Window`
members `AIInterviewModal` already feature-detects; four `new Promise<void>`
where `resolve()` takes no argument; `toast`'s options bag made optional on
every method, which callers had always omitted; five `krowHooks` query
arguments made optional, which callers had always omitted.
One automated pass was reverted rather than shipped. A local-component
annotator captured words out of the preceding JSDoc as prop names - producing
`interface FrameProps { one?: any; every?: any }` from a sentence about
"one surface, one padding, every step" - the same class of regex error as
Phase 6's. The whole folder was restored from the index and the sound steps
re-run, then local components were annotated with an index-spliced rewrite that
reads only the destructuring.
Verified: tsc 35 -> 22, set-difference showing thirteen removed and none added;
zero errors in any of the 56 files; all 56 emit byte-identical JavaScript;
production bundle byte-identical; npm test 1684/1691 with the same seven
failures; Owliver baseline 59/59; lint 0 errors; baseline artifacts untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Phase 7. Four files, and the production bundle is byte-identical.
This is the phase where the generated entity types finally pay off. React Query
v5 infers a `mutationFn`'s parameter as `void` when nothing constrains it, so
every destructuring in `krowHooks` needed its shape written down - and those
shapes are real contracts, not guesses. Thirteen mutations now name what they
take, reusing `JobApplication`, `JobPosting`, `WorkerProfile` and `Course` from
`@/types/entities`: `useHireCandidate` takes `{ application, job }`,
`useCompleteCourse` takes `{ profile, course, quizScore }`. Five `{ id, data }`
mutations share one `IdPatch`, where `data` stays `any` on purpose - a PATCH
body is whichever fields the caller is changing, and naming a subset would
describe one call site rather than the endpoint.
Two local types absorb places where an object gains fields after it is built,
which TypeScript does not allow on a literal. `AssignmentEntry` declares
`application_id` and `application` as the alternatives they are - an existing
application named by id, or one described for the server to file in the same
transaction. `LearningProfile` narrows four `jsonb` columns the learning
mutations append to and spread; what the elements hold is still unstated,
because it still is.
`AuthContextValue` writes down the twelve keys every consumer reads. Two are
permanently inert and say so. `user` is `any` rather than `User`, and that is a
narrow, documented exception: `auth.me()` resolves either to the server record
or to the localStorage mirror over `DEMO_USER`, and `admin/Profile` reads
`user.avatar_url`, which is neither a column on `users` nor in the `/me`
projection - so typing it `User` would be accurate about the server and would
turn an always-undefined read into a compile error in a file this phase does not
touch.
`authError` is typed `{ type?: string } | null` rather than `null`, and
TypeScript is the reason. Typed as the provider actually behaves - always
`null` - it made `ProtectedRoute`'s `authError.type === 'user_not_registered'`
a property access on `never`: correct, and a report that the branch cannot be
reached in this build. The branch and `UserNotRegisteredError` are real, so what
a consumer may be handed is what is written down. That this build never produces
one is current behaviour, not the contract.
`use-size` gets a `Size` interface and a typed ref - a small, entirely clear
contract.
Thirteen JSDoc `@param {any}` comments became real annotations. That was not
cosmetic: left in place they changed esbuild's parenthesisation under the `.ts`
loader and put two redundant bytes into the production bundle. Converting them
brought the bundle back to byte-identical, which is how the difference was found
at all.
Verified: tsc 35 -> 35, set-difference showing zero introduced and zero removed;
zero errors in any Phase 7 file; production bundle byte-identical to d440036;
npm test 1684/1691 with the same seven failures; lint 0 errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Phase 6. 57 files: 23 vendored shadcn primitives, 30 design-system components,
4 charts. Plus `src/components/ds/props.ts`, which is types only.
Renaming these alone took typecheck from 37 to 1008, and the reason is worth
recording because it is the shape of every remaining phase.
These components had NO prop contract. No PropTypes, no validation: in the
JavaScript every prop was optional and every extra prop was spread onto the
underlying element. TypeScript infers a destructured parameter WITHOUT a default
as REQUIRED, so the moment the files became `.tsx` it invented a rule the
components never had and rejected several hundred call sites that have always
worked. That is the compiler describing its own inference, not a defect it
found.
Three mechanical fixes, each restoring a contract that already existed:
- 57 JSDoc `@type {React.ForwardRefExoticComponent<any>}` annotations become
real TypeScript annotations. Those comments were the previous authors'
deliberate compatibility types; JSDoc stops applying in a `.tsx` file, so
converting them preserves an intent that was already written down.
- 61 `React.forwardRef(...)` calls gain `<any, any>`. Without generics `ref`
infers `ForwardedRef<unknown>`, which no element's `Ref<T>` accepts - so
every primitive that forwards a ref to a `div` failed on the ref, not the
props.
- 78 component signatures take `DsProps`, a documented alias for
`Record<string, any>`. It exists so the decision is recorded once and is
greppable when someone tightens it, rather than being 78 bare `any`s with
no explanation between them. The prop NAMES are not lost: every component
still destructures them by name, which is where a reader looks.
Four files needed real types rather than compatibility ones. `ds/toast` takes
react-hot-toast's own `ToastOptions`, which narrows `position` to its
`ToastPosition` union instead of widening to `string` - the widening was what
made all six calls unassignable. `ds/Pagination`'s page range is genuinely
`(number | string)[]`, because it interleaves page numbers with '…' markers that
the renderer tests for. `ds/Field` narrows `children.props` at three reads, and
`ds/Avatar` needed the ref generic.
Two of my own automated passes were wrong and were caught rather than shipped. A
props-interface generator dropped alternating props, because non-overlapping
regex matches consume the separating comma - it made things worse (83 file
errors to 146) and was reverted wholesale. A second pass missed every
multi-line signature whose defaults contain a `)`, such as `onClose = () => {}`;
that needed a brace matcher rather than a character class.
56 of 57 files emit byte-identical JavaScript. The one exception is `ds/toast`,
where a JSDoc type CAST - `/** @type {ToastPosition} */ ('bottom-center')` -
became a real annotation, so the emitted output loses a comment and a pair of
now-redundant parentheses. The value is `"bottom-center"` either way; the
minified outputs differ only in esbuild's choice of mangled local names.
Verified: tsc 37 -> 35, set-difference showing zero introduced and two removed;
zero errors remain in any Phase 6 file; npm test 1684/1691 with the same seven
failures; lint 0 errors; build succeeds with the API origin inlined; baseline
artifacts untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Phase 5. Twelve modules under `src/lib` and `src/lib/admin`. All twelve emit
byte-identical JavaScript; eight needed no annotation at all.
Where the generated entity types fit, they are used. `positionModel` is typed
against `JobPosting` — and that is where TypeScript earned its keep. Annotating
the label functions made `experienceLabel`'s `years === ''` guard a comparison
the compiler called impossible, because the registry types
`min_experience_years` as `number`, which is correct for a record the API has
returned. The guard is not dead: the same functions are handed drafts, and an
untouched numeric form input yields `''` — which is why `toPositionPayload`
coerces all five numerics with `Number(...)`.
So the module now has two types rather than one. `PositionRecord` is a saved
posting with the registry's column types; `PositionDraft` widens the five
numerics to `number | string` and is taken by `toPositionPayload` alone. The
one comparison the split cannot express keeps its guard and carries a cast with
the reason written next to it. Deleting a live guard to satisfy a type would be
the type rewriting the code.
`workforce` keeps its records as `any`: 574 lines of demand and availability
arithmetic over profiles, postings, assignments and staff read largely through
jsonb columns the registry does not describe. What IS described is the module's
own contract — the `WorkforceContext` option bag and the `Availability` result,
whose two shapes differ by whether a worker's commitments are known.
Two of my own type declarations were too narrow and were caught by the
set-difference rather than by inspection. `activitySignals`' accumulator seeds
`{ email, name, count, privileged }` and I had named only the two counters;
`WorkforceContext` omitted `profiles` and `courses`, which `PositionDetail`
passes in a single call with three more. The bag now carries an index signature,
because that is what the call site assumes: callers hand the whole thing over
and each function picks what it needs.
`skillGraph` gains a `SkillLevel` interface with an optional `earned`, set in a
second pass that stops at the first incomplete rung — so the levels above the
gap never receive it, and optional is the honest description.
Verified: tsc 40 -> 37, zero introduced; all twelve emitted outputs
byte-identical; npm test 1684/1691 with the same seven failures; lint 0 errors;
build succeeds with the API origin inlined; baseline artifacts untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Phase 4c, completing `src/api`. Both files emit byte-identical JavaScript -
86,831 and 6,449 bytes - and neither needed a single annotation: they are data
and pure helpers, and inference already describes them.
Neither reaches the production bundle. No module under `src` imports either one;
`base44Client` mentions `attendanceSeed` in a comment and nothing more. They
exist for `skill-check.mjs`, `owliver-capture.mjs` and `seed-fixture.mjs`, which
is why this was the safest phase in the whole migration and why it was left
until the transport layer was done.
`npm run seed:check` still answers "seed.json is stale", which it has since
before this migration began - the backend fixture drifted from `src/api/seed`
independently of any of this. What matters here is that it ANSWERS: the
generator loaded the renamed module through the Phase 0 resolver rather than
failing to find it.
tsc unchanged at 40, npm test 1684/1691 with the same seven failures, lint 0
errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Phase 4b-ii, and the last file in the transport layer. Type-only: the plain
emitted JavaScript is byte-identical at 9,302 bytes.
This file owns the session, so the annotations stay at the edges and nothing
about the auth model moves. `SESSION_KEY`, the localStorage mirror, the three
module-level mutables, the shared hydration promise, the `{ ...user }` spreads,
`window.location.href` on logout and `window.location.reload()` in
`resetDemoData` are all exactly as they were.
Nine sites, eight of them parameter types. The ninth is `hydration`, which needs
`Promise<any> | null` because the runtime already assigns both: the shared first
`GET /me`, then `null` once it has been consumed or superseded by a login.
Inference would have fixed it at `Promise<any>` and rejected the assignments
that make the sharing work.
`entities` is deliberately NOT annotated, and neither is `createEntity`'s
return. A probe run during the inspection showed what annotating it costs:
`Record<EntityName, EntityClient<any>>` makes `list()` return `Promise<any[]>`
where inference gives `Promise<any>`, and `agentStore.js` does
`qc.getQueryData(KEY) || (await ...list(...))`. React Query types
`getQueryData` as `unknown`; `unknown || any` collapses to `any` while
`unknown || any[]` stays `unknown`, and `.find` on the next line stops
compiling. Two new errors in a file this phase does not migrate. The generated
record types and `EntityClientFor<K>` are ready for the phase that migrates the
forty-five call sites in `krowHooks.js`.
The auth returns are likewise left inferred rather than typed `User`. That was
checked, not assumed: `admin/Profile.jsx` reads `user.avatar_url`, which is
neither a column on `users` nor part of the `/me` projection in `me.go` - it is
always undefined at run time. Typing the return would have turned a latent dead
branch into two compile errors in a file this phase does not touch. Worth
knowing about separately; not this commit's business.
One stale comment is kept verbatim - `@param {any} request` above
`owliver.suggestions`, which names a parameter that does not exist. Removing it
was the only remaining difference in the emitted output, and byte-identity is
worth more here than tidying a comment. Flagged for a later docs pass.
Verified: tsc 42 -> 40 with a set-difference showing two removed and none added;
npm test 1684/1691, the same seven failures as the parent commit (six documented
as deliberate in scripts/__baseline__/README.md, one the stale backend fixture);
lint 0 errors; build succeeds with the API origin inlined. This is also the
first run of the suite with a source-text-inspected file migrated - the harness
fix in 02a2ab0 is what makes it possible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
The check suite consumes source files through two entirely separate channels,
and Phase 0 only hardened one of them.
`withSourceResolution` wraps `ssrLoadModule`, which covers the 159 module loads.
It does not and cannot see the other 30 sites, which read source as TEXT through
`readFileSync` to assert structural facts - "the panel imports no local
suggestion ranker", "no runtime path writes a definition". Renaming
`base44Client.js` to `.ts` is what surfaced the difference: ENOENT in the middle
of a suite that had been passing, from a line no grep for `ssrLoadModule` would
ever have found.
`resolveSourcePath()` in `ssr-resolve.mjs` reuses the existing `candidatesFor()`
ordering - the written path first, then `.ts`, then `.tsx` - and returns a path
relative to the root, so every call site keeps its `join(ROOT, ...)` as it was.
An unresolvable path comes back unchanged, which keeps the two absence
assertions honest: a check proving `store.js` is GONE still asks about the path
it means, and now also notices if the file returns under another extension.
Thirty-seven lines change, each a one-for-one replacement. No assertion text, no
record() message, no ordering, no logic.
The audit found the reads in four shapes, and two of them a path grep cannot
see:
- direct readFileSync(join(ROOT, 'src/x.jsx'), 'utf8')
- via a const const P = join(ROOT, 'src/x.jsx')
- dir + name ['node.js', 'patch.js'].map((f) => readFileSync(join(ROOT, 'src/lib/ui', f)))
- path array for (const f of files) readFileSync(join(ROOT, f))
The third and fourth hide thirteen filenames in adjacent arrays, which is why
the first estimate of this work was twenty-two files and the real number is
thirty-five.
One directory scan also filtered `/\.jsx?$/` over `src/pages/admin` and
`src/components/agents`. That one does not crash - it quietly matches nothing
once those directories are TypeScript, and the check passes having inspected an
empty set. Widened to `/\.[jt]sx?$/`. A silent shrink is worse than a failure,
and CI's FLOOR of 900 would not have caught it.
Deliberately NOT touched, because they are correctly extension-specific: the
`dist/assets` filter reads built bundles, which are `.js` whatever the source
was; `scripts/stubs/react-hot-toast.js` and `scripts/browser-flows.js` are
scripts, not migrated source; and every comment that mentions a `.js` filename
says something true about a file that still has that name.
Proven rather than assumed. `npm test` reports 1684/1691 before and after, the
same seven failures. Then `src/api/base44Client.js` - the exact file whose
rename broke the suite - was renamed to `.ts`, the suite re-run, and the check
that reads it as text passed: "the app does not import the seed fixture". The
rename was reverted and the file verified byte-identical.
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
Phase 4b-i. Type-only, and the production bundle proves it: built from this
commit's parent and from this commit, all six chunk hashes match. The plain
emitted JavaScript is byte-identical at 34,095 bytes.
Four annotation sites, no logic touched:
- `ROLE_TITLES` and `AVAILABILITY_TOKENS` get `[RegExp, string][]`. Left to
inference the element widens to `string | RegExp`, which has no `.test`.
Explicit tuples rather than `as const`, which would also have worked and
would additionally have made the arrays readonly - a change to the type this
module publishes for no benefit it uses.
- `ROUTES` gets `[RegExp, (prompt: string) => any, number][]`, because all
three positions are used for what they are: `.test()` on the first, a call
on the second, `think(ms)` on the third.
- `invokeLLM` gets a real parameter type. It inferred `{ prompt?: string }`
from its own destructuring defaults, and that single inference was
responsible for nine errors in files this commit does not touch - eight in
`krowAi.js`, one in `provingGround.ts` - every one of them a caller passing
`response_json_schema` or `model`, which the real integration accepts.
Naming the options type fixes all nine from here.
- `uploadFile` gets `{ file?: File }`.
The return type of `invokeLLM` stays `Promise<any>`, deliberately. The ten
handlers behind the router return ten different shapes, and `krowAi.js` branches
on the result at run time - `typeof res === 'string' ? res : res.text || String(res)`
- which a precise union would reject on every branch without a `.text`. The
looseness is the contract, not an omission.
`InvokeLLMOptions` is exported as a type only; the runtime exports are still
exactly `invokeLLM` and `uploadFile`.
Verified in isolation from the parallel feature work (f96f128 plus this file):
tsc 53 -> 37, a set-difference against the baseline showing sixteen removed and
none added; skill-check 1641/1642 with only the known stale-fixture failure;
Owliver baseline 59/59; lint 0 errors; build succeeds with the API origin
inlined; the nine Owliver baseline artifacts unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Phase 4a: `demoUser` and `httpClient`. Type-only. The production bundle is
byte-identical - built from this commit's parent and from this commit, all six
chunk hashes match.
`httpClient` is the contract boundary, so the annotations are deliberately
conservative:
- `KrowApiError` is a CAST, not a class. The three error sites still build a
plain `Error` and assign `.name`, `.status`, `.code` and `.details` onto it
exactly as before. `class KrowApiError extends Error` would have read
better and changed three things that callers depend on: the prototype
chain, `instanceof`, and how `name` comes to be set.
- `RESOURCE_PATHS` becomes `Record<EntityName, EntityResourcePath>`. That is
the first thing in the repo to check the eighteen entity names against the
eighteen in `ENTITY_NAMES`; until now the two lists agreed only by habit,
and a divergence would have surfaced as `base44.entities.Whatever` being
undefined with nothing to say why.
- `createEntity`'s parameter stays `string` and its return stays inferred.
Annotating the return `EntityClient<any>` was tried and reverted: it makes
`list()` return `Promise<any[]>` where inference gives `Promise<any>`, and
`agentStore.js` does `qc.getQueryData(KEY) || (await ...list(...))`. React
Query types `getQueryData` as `unknown`; `unknown || any` collapses to
`any`, `unknown || any[]` stays `unknown`, and `.find` on the next line
stopped compiling. Two new errors in a file this phase does not migrate,
for no gain. `EntityClientFor<K>` is ready for the phase that migrates
those consumers.
Unchanged and verified in the emitted output: `credentials: 'include'`, both
header branches on `body === undefined`, the URLSearchParams query encoding with
its repeated-array and undefined-omission rules, the `payload.data` unwrap that
drops `meta`, the verbatim server message, `status: 0` / `code: 'unreachable'`
for a transport failure, the `-created_date` and `limit` defaults, path
construction through `encodeURIComponent`, and `bulkCreate`'s sequential
`this.create` loop.
`DEMO_USER` is annotated `User`, which does real work: unannotated,
`role: 'admin'` widens to `string` and the default shape did not satisfy the
type the app uses for the thing it defaults.
Verified in isolation from the parallel feature work (d1425f9 plus these two
files): tsc 64 -> 53, the eleven removed being exactly this file's, and a
set-difference against the baseline showing none added. skill-check 1641/1642
with only the known stale-fixture failure, Owliver baseline 59/59, lint 0
errors, build succeeds with the API origin inlined.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Phase 3.5. Entity record shapes, generated from the backend's resource
registry rather than transcribed from it. Type-only: every touched file emits
byte-identical JavaScript, and the two type modules emit nothing at all.
`scripts/gen-entity-types.mjs` reads
`krow-backend/go-api/internal/domain/resources_gen.go` - itself generated out of
information_schema, so it cannot drift from the migrations - and writes
`src/types/entities.generated.ts`: 15 resources, 298 columns. It checks for
drift by default and rewrites with --write, the same arrangement seed-fixture.mjs
uses, and skips cleanly when the backend is not checked out beside this repo.
Field types come from `Column.SelectExpr()` in `domain/resource.go`, which is
what the read projection actually emits, not from the Postgres type. The two
differ: uuid and citext are cast to text, numeric to float8, dates and
timestamps to formatted strings, and - the case that justifies generating rather
than typing by hand - `user_activity.id` is an identity bigint cast to text, so
it arrives as a STRING. Written by hand it would have been called a number, and
nothing would have contradicted that until a comparison quietly stopped
matching.
The five Phase 3 leaf utilities swap their `any` placeholders for these records,
as `Partial<...>`: each takes `= {}` or guards every read because it renders
before the query resolves, and requiring the whole record would force those
defaults out - a behaviour change in a scoring path.
jsonb is where the generator stops. Thirteen columns across eight entities are
typed `unknown`, correctly: what sits inside a jsonb column is not in
information_schema and nothing on the backend declares it. Where a module reads
through one it is narrowed to `unknown[]`, `any[]` or `any` - what kind of value
it is, and nothing about its contents. An earlier draft declared the fields
these modules read off them; it was removed. That would have been inventing a
schema the database does not hold, with the compiler then defending the guess.
Not wired into skill-check.mjs: that file is being changed concurrently by
unrelated feature work. Adding `"types:check": "node scripts/gen-entity-types.mjs"`
beside the existing seed:check is the natural next step and is deliberately left
for when that file is quiet.
Verified in isolation from the parallel feature work (commit 1775395 plus these
eight files only): tsc 64 errors with an unchanged histogram, skill-check
1641/1642 with only the known stale-fixture failure, Owliver baseline 59/59,
lint 0 errors, production build succeeds with the API origin inlined.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Phases 0-3 of the JS/JSX -> TS/TSX migration. No runtime behaviour changes:
every converted file emits byte-identical JavaScript, verified file by file.
Phase 0 - harness hardening, before any rename:
- scripts/ssr-resolve.mjs wraps `ssrLoadModule` so the ~170 literal module
paths in the check scripts resolve .js/.jsx/.ts/.tsx. Without it the first
rename would have silently destroyed the 1642-check suite that guards the
Owliver flow.
- eslint.config.js gains a TypeScript block. Its `files` globs listed only
{js,mjs,cjs,jsx}, so a renamed file would have dropped out of the run while
`eslint .` went on exiting 0 - the quietest failure mode available.
- MIGRATION_BASELINE.md records the measured starting point, including the
pre-existing seed-fixture failure and the already-broken standalone
owliver-baseline.mjs, so neither is later mistaken for migration damage.
Phase 1 - tsconfig.json succeeds jsconfig.json, carrying every option across at
its old value. `types` moves from [] to ["vite/client"], which fixes the eight
import.meta errors; @types/node is deliberately excluded so setTimeout stays a
number in browser code. allowJs and checkJs stay on, strict stays off.
Phase 2 - src/types/{api,entities,user}.ts. Transport envelope, error shape,
the entity-name union (the same 18 names are written down twice today, in
httpClient and base44Client, with nothing checking they agree), and the user
record. Every field transcribed from the API contract, the migrations and the
/me projection in me.go - not inferred. Entity record shapes are deliberately
absent: derivable, but nothing consumes them yet.
Phase 3 - nine leaf utilities renamed to .ts with annotations added only where
they could be established from existing usage. Entity records are typed `any`
with a comment naming what they are, rather than a guessed interface.
Verified: tsc 64 errors (65 before; one pre-existing TS2559 genuinely fixed,
none introduced), lint unchanged at 0 errors, skill-check 1641/1642 with only
the known failure, Owliver baseline section 59/59 green, production build
succeeds with the API origin inlined.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
`positionFlow.js` was five per-domain concerns in one file — a field table, an
`@`-token resolver, a sentence extractor, a commit vocabulary and a set of
outcome renderers — and only the control flow between them was general.
Everything else knew it was creating a job posting. Adding a second
conversation meant a second copy of all of it.
So the control flow is now `conversationFlow.js` and each kind of record is a
REGISTRY. Which conversation a skill runs is the skill's own `flow:` line,
resolved through a map: `routing.js` used to say `if (skill.id ===
'create-position')`, which made a second conversational skill a change to the
router rather than a file on disk — the `if agent_key == ...` shape the
platform rules out one level up. The panel's write callback is likewise a map
keyed by flow id instead of an `onCreatePosition` prop, and the outcome wording
comes off the registry, so nothing in the panel names a kind of record any
more.
`create-employee-role` is the second registry. The worker is asked for and
never assumed: a conversation that names nobody re-asks rather than falling
back to the session, because an operator records this on somebody's behalf. Its
`extract` is deliberately narrower than the posting's — "bartender, weekends,
$30/hr" settles three fields and leaves the subject alone, since guessing WHO a
record is about from a fragment is how a role gets filed against the wrong
person.
THE CONFIRMATION STEP ACCEPTED "create position" AND SILENTLY REJECTED "create
positions" — the plural the Positions page itself uses. An anchored regex missed
it, and the reader got the summary back with no indication of what was wrong
with what they said, which is indistinguishable from the screen not having
updated. Matching is now exact membership against a normalized reply, so a
vocabulary is a list of phrases somebody can read rather than an expression
somebody has to parse.
Two bugs in `extractRole`, both of which fabricated a value nobody typed on the
one field a position cannot be created without:
- The phrase pattern marks "new" as the role by the same grammar that marks
"sous chef", so "create new position" opened the conversation titled "New".
The scaffolding is a PHRASE at least as often as a single word, so a
per-word test still produced "Brand New" and "One More". Scaffolding words
are now stripped to DECIDE whether the phrase named anything, and the
ORIGINAL phrase is returned when it did — strip to test, never to rewrite,
or "second chef" becomes "Chef" and the cure is worse than the bug.
- `(?:a|an)?\s*` has no word boundary, so it matched the leading "a" of
"another" and the capture began mid-word. That mangled scaffolding into
"Nother New" and, worse, corrupted every role introduced with "an":
"create an open kitchen lead position" titled the position "N Open Kitchen
Lead". A real role, typed correctly, silently wrong. Found by mutation
testing the first fix.
`@companies` and `@workers` resolve from data the panel already holds — postings
and profiles the API has already scoped to the caller — so neither widens
anybody's view and neither costs a request. The company list is deliberately
unsorted: `useJobPostings` asks for `-created_date`, so the clients staffed for
most recently come first, and the panel does no ranking of its own. That last
part is a rule the suite enforces structurally, and it is the right rule — a
second opinion formed in the panel outranking the server's is exactly the kind
of thing that decays quietly.
`npm test` now refuses a conversation step whose field its registry does not
define. `stepsOf` drops unknown fields, so a typo means the flow asks fewer
questions than the file lists — and a skill whose steps are ALL unknown asks
none, jumps to the summary, and offers to write an empty record. Nothing errors
and the Markdown still reads correctly. 970 checks, up from 924; the new ones
walk both conversations end to end, because a wrong answer at the confirmation
step re-renders the same summary a right answer does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
The key had two incompatible meanings running at once. CLAUDE.md §3 defines
subagents as "keys of other specs this may DELEGATE to" and §6 as a tool call
from the parent's perspective — the subagent runs its own turn, as the same
caller, out of the parent's budget, and returns an answer. The backend
implements exactly that.
This side did something else: agentSkillIds folded one level of subagent skills
into the parent's carried set, so a parent silently gained everything its
subagents carried, and the UI described it that way — "other agents whose
skills this one may also use", "borrowing them cannot reach data this page does
not hold".
Both are defensible readings. Only one is the specification, and running both
meant krow-workforce-agent carried eight delegation tools AND the flattened
skills of those same eight agents — able to answer a question directly or to
ask an agent that had already lent it the means to answer. Two ways to do one
thing, differing in cost and in what the trajectory records.
So an agent carries what it declares. Reaching another agent is delegation,
which the runtime does with its own budget and its own trajectory.
The blast radius was one check, which is the useful part of the answer: only
"a subagent cycle terminates" depended on the folding, because that traversal
was the only thing that could loop. Nothing walks subagents here now, so the
cycle question moved to where it belongs — refused at publish by
definition.FindSubagentCycle, bounded at run time by the depth cap. The
replacement checks assert the new meaning rather than deleting the old ones,
because the previous behaviour reads as perfectly reasonable and will be
reinvented otherwise.
The `agents` parameter stays on agentSkillIds and is no longer read. Removing
it is a wider edit for no behavioural gain, and agentScopedDisabledWith exists
precisely to pass it.
924/924 checks pass; production build clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
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
.gitignore excludes .env and .env.*, with !.env.example negating it for the
documented template. Those rules are correct — the comment above them records
a previous fix to a "#env" line that was commented out and so matched nothing.
But .env was committed while that typo was live, and an ignore rule does not
untrack a file that is already in the index. The result is that everyone's
local backend choice shows up as a modification to a committed file:
M .env VITE_API_PROXY_TARGET: production → 127.0.0.1:8080
Nothing secret is in there today; it holds URLs. The risk is the next value
that is not a URL, committed by someone who had no reason to think that file
was tracked. .env.example stays, so a fresh checkout still knows what to fill
in.
The file itself is untouched on disk.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
The checks this repository already had, run on every push rather than when
somebody remembers: lint, skill-check, a production build, and the fixture
drift check against the backend.
Two of the steps guard against a suite that stops running rather than one
that fails:
- npm test prints "N/M checks passed", and a suite that stopped reaching
half its files still prints a pass line for the half that ran. The count
now has a floor, so the number going down is itself a failure. Verified:
a shrunken 120/120 fails, 900/924 fails, 924/924 passes.
- the build asserts the API origin reached the bundle. VITE_* is inlined
at build time with no runtime configuration, so a bundle built without
it calls a relative /api/v1 that nginx serves as a static file — and a
login POST answers 405 rather than failing anywhere visible.
The fixture job needs the sibling repository checked out. Where it is not,
it says so and fails rather than passing quietly: a check that cannot run
should not look like one that did.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186JgqQUCDS8ZwGmyw3ymWu