chore(ts-migration): resolve source-text reads across .js/.jsx/.ts/.tsx
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
This commit is contained in:
@@ -19,7 +19,7 @@ import React from 'react';
|
|||||||
import { renderToStaticMarkup } from 'react-dom/server';
|
import { renderToStaticMarkup } from 'react-dom/server';
|
||||||
import { BASELINE_PATH, captureBaseline } from './owliver-capture.mjs';
|
import { BASELINE_PATH, captureBaseline } from './owliver-capture.mjs';
|
||||||
import { buildFixture, FIXTURE_PATH } from './seed-fixture.mjs';
|
import { buildFixture, FIXTURE_PATH } from './seed-fixture.mjs';
|
||||||
import { withSourceResolution } from './ssr-resolve.mjs';
|
import { resolveSourcePath, withSourceResolution } from './ssr-resolve.mjs';
|
||||||
|
|
||||||
const ROOT = process.cwd();
|
const ROOT = process.cwd();
|
||||||
const results = [];
|
const results = [];
|
||||||
@@ -1618,10 +1618,10 @@ record('...and stops asking a backend that does not have the route',
|
|||||||
now does — and the failure mode of it returning is not an error but a second
|
now does — and the failure mode of it returning is not an error but a second
|
||||||
opinion silently outranking the server's. */
|
opinion silently outranking the server's. */
|
||||||
record('the panel imports no local suggestion ranker',
|
record('the panel imports no local suggestion ranker',
|
||||||
!existsSync(join(ROOT, 'src/components/ai-assistant/matchPrompts.js')),
|
!existsSync(join(ROOT, resolveSourcePath('src/components/ai-assistant/matchPrompts.js'))),
|
||||||
'matchPrompts.js removed');
|
'matchPrompts.js removed');
|
||||||
|
|
||||||
const panelSource = readFileSync(join(ROOT, 'src/components/ai-assistant/KrowAssistant.jsx'), 'utf8');
|
const panelSource = readFileSync(join(ROOT, resolveSourcePath('src/components/ai-assistant/KrowAssistant.jsx')), 'utf8');
|
||||||
record('...and does not rank, score or sort suggestions itself',
|
record('...and does not rank, score or sort suggestions itself',
|
||||||
!/rankPrompts|\.sort\(|score\(/.test(panelSource),
|
!/rankPrompts|\.sort\(|score\(/.test(panelSource),
|
||||||
'no ranking in the panel');
|
'no ranking in the panel');
|
||||||
@@ -1630,10 +1630,10 @@ record('...it asks the API instead',
|
|||||||
|
|
||||||
/* The production data client must not carry the fixture. */
|
/* The production data client must not carry the fixture. */
|
||||||
record('the app does not import the seed fixture',
|
record('the app does not import the seed fixture',
|
||||||
!/from '\.\/seed'/.test(readFileSync(join(ROOT, 'src/api/base44Client.js'), 'utf8')),
|
!/from '\.\/seed'/.test(readFileSync(join(ROOT, resolveSourcePath('src/api/base44Client.js')), 'utf8')),
|
||||||
'base44Client reads demoUser.js, not seed.js');
|
'base44Client reads demoUser.js, not seed.js');
|
||||||
record('the local entity store is gone',
|
record('the local entity store is gone',
|
||||||
!existsSync(join(ROOT, 'src/api/store.js')),
|
!existsSync(join(ROOT, resolveSourcePath('src/api/store.js'))),
|
||||||
'store.js removed');
|
'store.js removed');
|
||||||
|
|
||||||
/* ── 11. A fresh Add Owliver Skill screen ─────────────────────────────────
|
/* ── 11. A fresh Add Owliver Skill screen ─────────────────────────────────
|
||||||
@@ -2958,7 +2958,7 @@ const runtime = await server.ssrLoadModule('/src/lib/agents/runtime.js');
|
|||||||
})().pass);
|
})().pass);
|
||||||
|
|
||||||
record('ownership: the rule is the one the page actually uses', (() => {
|
record('ownership: the rule is the one the page actually uses', (() => {
|
||||||
const source = readFileSync(join(ROOT, 'src/components/skills/SkillSurface.jsx'), 'utf8');
|
const source = readFileSync(join(ROOT, resolveSourcePath('src/components/skills/SkillSurface.jsx')), 'utf8');
|
||||||
return {
|
return {
|
||||||
pass: /agentPermitsSkill\(skill\.id, scope\)/.test(source)
|
pass: /agentPermitsSkill\(skill\.id, scope\)/.test(source)
|
||||||
&& /from '@\/lib\/agents\/runtime'/.test(source),
|
&& /from '@\/lib\/agents\/runtime'/.test(source),
|
||||||
@@ -2969,7 +2969,7 @@ const runtime = await server.ssrLoadModule('/src/lib/agents/runtime.js');
|
|||||||
record('ownership: the panel reads agents from the definitions store', (() => {
|
record('ownership: the panel reads agents from the definitions store', (() => {
|
||||||
/* The live failure: this file read `preferences.customAgents`, which authored
|
/* The live failure: this file read `preferences.customAgents`, which authored
|
||||||
agents had already moved out of, so no attachment was ever visible here. */
|
agents had already moved out of, so no attachment was ever visible here. */
|
||||||
const source = readFileSync(join(ROOT, 'src/components/ai-assistant/AgentContext.jsx'), 'utf8');
|
const source = readFileSync(join(ROOT, resolveSourcePath('src/components/ai-assistant/AgentContext.jsx')), 'utf8');
|
||||||
return {
|
return {
|
||||||
pass: /useAgentDefinitions\(\)/.test(source)
|
pass: /useAgentDefinitions\(\)/.test(source)
|
||||||
&& /sourcesFrom\(/.test(source)
|
&& /sourcesFrom\(/.test(source)
|
||||||
@@ -3788,7 +3788,7 @@ record('agent starters carry no capability, so they cannot address an unoffered
|
|||||||
* Read out of `App.jsx` rather than asserted against a written list, so a route
|
* Read out of `App.jsx` rather than asserted against a written list, so a route
|
||||||
* that is renamed or removed fails here rather than in someone's browser.
|
* that is renamed or removed fails here rather than in someone's browser.
|
||||||
*/
|
*/
|
||||||
const appSource = readFileSync(join(ROOT, 'src/App.jsx'), 'utf8');
|
const appSource = readFileSync(join(ROOT, resolveSourcePath('src/App.jsx')), 'utf8');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The switcher, if there still is one.
|
* The switcher, if there still is one.
|
||||||
@@ -3800,7 +3800,7 @@ const appSource = readFileSync(join(ROOT, 'src/App.jsx'), 'utf8');
|
|||||||
* ENOENT and stopped the whole run here, taking every section after it with it.
|
* ENOENT and stopped the whole run here, taking every section after it with it.
|
||||||
* Absent is a state to report, not to crash on.
|
* Absent is a state to report, not to crash on.
|
||||||
*/
|
*/
|
||||||
const SWITCHER_PATH = join(ROOT, 'src/components/ai-assistant/AgentSwitcher.jsx');
|
const SWITCHER_PATH = join(ROOT, resolveSourcePath('src/components/ai-assistant/AgentSwitcher.jsx'));
|
||||||
const switcherSource = existsSync(SWITCHER_PATH) ? readFileSync(SWITCHER_PATH, 'utf8') : null;
|
const switcherSource = existsSync(SWITCHER_PATH) ? readFileSync(SWITCHER_PATH, 'utf8') : null;
|
||||||
|
|
||||||
/* Only live navigations count: a disabled control goes nowhere by design. */
|
/* Only live navigations count: a disabled control goes nowhere by design. */
|
||||||
@@ -3836,7 +3836,7 @@ record('every route the agent switcher navigates to exists',
|
|||||||
* keeping — offered somewhere, and the route resolves — so it is asserted
|
* keeping — offered somewhere, and the route resolves — so it is asserted
|
||||||
* against where the control actually is.
|
* against where the control actually is.
|
||||||
*/
|
*/
|
||||||
const agentsListSource = readFileSync(join(ROOT, 'src/pages/admin/WorkspaceAgents.jsx'), 'utf8');
|
const agentsListSource = readFileSync(join(ROOT, resolveSourcePath('src/pages/admin/WorkspaceAgents.jsx')), 'utf8');
|
||||||
record('creating an agent is reachable, and its screen exists',
|
record('creating an agent is reachable, and its screen exists',
|
||||||
/navigate\('\/admin\/workspace\/agents\/new'\)/.test(agentsListSource)
|
/navigate\('\/admin\/workspace\/agents\/new'\)/.test(agentsListSource)
|
||||||
&& routed.has('/admin/workspace/agents/new'),
|
&& routed.has('/admin/workspace/agents/new'),
|
||||||
@@ -4497,7 +4497,7 @@ record('every category offered by the picker matches at least one skill',
|
|||||||
|
|
||||||
const managementRoutes = ['/admin/workspace/agents', '/admin/workspace/agents/new'];
|
const managementRoutes = ['/admin/workspace/agents', '/admin/workspace/agents/new'];
|
||||||
const appRoutes = new Set(
|
const appRoutes = new Set(
|
||||||
[...readFileSync(join(ROOT, 'src/App.jsx'), 'utf8').matchAll(/<Route\s+path="([^"]+)"/g)]
|
[...readFileSync(join(ROOT, resolveSourcePath('src/App.jsx')), 'utf8').matchAll(/<Route\s+path="([^"]+)"/g)]
|
||||||
.map((m) => m[1])
|
.map((m) => m[1])
|
||||||
.map((path) => (path.startsWith('/') ? path : `/admin/${path}`))
|
.map((path) => (path.startsWith('/') ? path : `/admin/${path}`))
|
||||||
);
|
);
|
||||||
@@ -4523,7 +4523,7 @@ record('every surface route is registered in the router',
|
|||||||
|
|
||||||
record('the dynamic agent route is registered after the static one',
|
record('the dynamic agent route is registered after the static one',
|
||||||
(() => {
|
(() => {
|
||||||
const source = readFileSync(join(ROOT, 'src/App.jsx'), 'utf8');
|
const source = readFileSync(join(ROOT, resolveSourcePath('src/App.jsx')), 'utf8');
|
||||||
return source.indexOf('workspace/agents/new') < source.indexOf('workspace/agents/:id');
|
return source.indexOf('workspace/agents/new') < source.indexOf('workspace/agents/:id');
|
||||||
})(),
|
})(),
|
||||||
'so `agents/new` cannot be read as an agent whose id is "new"');
|
'so `agents/new` cannot be read as an agent whose id is "new"');
|
||||||
@@ -4710,8 +4710,8 @@ record('...and never answered from the configure page itself',
|
|||||||
* Asserted structurally: the configure screen must not import or define a chat.
|
* Asserted structurally: the configure screen must not import or define a chat.
|
||||||
* The panel it gets is the one the Admin shell already mounts.
|
* The panel it gets is the one the Admin shell already mounts.
|
||||||
*/
|
*/
|
||||||
const detailSource = readFileSync(join(ROOT, 'src/pages/admin/AgentDetail.jsx'), 'utf8');
|
const detailSource = readFileSync(join(ROOT, resolveSourcePath('src/pages/admin/AgentDetail.jsx')), 'utf8');
|
||||||
const configureSource = readFileSync(join(ROOT, 'src/components/agents/AgentConfigure.jsx'), 'utf8');
|
const configureSource = readFileSync(join(ROOT, resolveSourcePath('src/components/agents/AgentConfigure.jsx')), 'utf8');
|
||||||
|
|
||||||
/* The property is right and the old pattern was too blunt. It matched any
|
/* The property is right and the old pattern was too blunt. It matched any
|
||||||
occurrence of the substring, so `useAssistantPanel` — the hook for talking to
|
occurrence of the substring, so `useAssistantPanel` — the hook for talking to
|
||||||
@@ -4735,18 +4735,18 @@ record('the configure screen defines no chat of its own',
|
|||||||
record('the panel is mounted once, by the shell',
|
record('the panel is mounted once, by the shell',
|
||||||
/* `<AssistantPanel` alone also matches `<AssistantPanelProvider`, which is a
|
/* `<AssistantPanel` alone also matches `<AssistantPanelProvider`, which is a
|
||||||
different component and legitimately present. */
|
different component and legitimately present. */
|
||||||
(readFileSync(join(ROOT, 'src/layouts/AdminLayout.jsx'), 'utf8').match(/<AssistantPanel\b(?!Provider)/g) || []).length === 1);
|
(readFileSync(join(ROOT, resolveSourcePath('src/layouts/AdminLayout.jsx')), 'utf8').match(/<AssistantPanel\b(?!Provider)/g) || []).length === 1);
|
||||||
|
|
||||||
/* The switcher is gone — see the note above. What replaced it is a label, and
|
/* The switcher is gone — see the note above. What replaced it is a label, and
|
||||||
the property worth keeping is that it stayed one: a header that can be
|
the property worth keeping is that it stayed one: a header that can be
|
||||||
clicked is a second place to choose an agent. */
|
clicked is a second place to choose an agent. */
|
||||||
const badgeSource = readFileSync(join(ROOT, 'src/components/ai-assistant/AgentBadge.jsx'), 'utf8');
|
const badgeSource = readFileSync(join(ROOT, resolveSourcePath('src/components/ai-assistant/AgentBadge.jsx')), 'utf8');
|
||||||
record('the panel header names the agent and does not switch it',
|
record('the panel header names the agent and does not switch it',
|
||||||
!/<button|onClick=|navigate\(/.test(badgeSource),
|
!/<button|onClick=|navigate\(/.test(badgeSource),
|
||||||
'a label, not a control');
|
'a label, not a control');
|
||||||
|
|
||||||
record('PageContext is untouched',
|
record('PageContext is untouched',
|
||||||
!/agentConfigure/.test(readFileSync(join(ROOT, 'src/components/ai-assistant/PageContext.jsx'), 'utf8')));
|
!/agentConfigure/.test(readFileSync(join(ROOT, resolveSourcePath('src/components/ai-assistant/PageContext.jsx')), 'utf8')));
|
||||||
|
|
||||||
/* ── The configure surface declares no placements ───────────────────────── */
|
/* ── The configure surface declares no placements ───────────────────────── */
|
||||||
|
|
||||||
@@ -4781,7 +4781,7 @@ record('no skill declares the configure page',
|
|||||||
* **It stays Krow.** The palette is the tokens, and nothing else.
|
* **It stays Krow.** The palette is the tokens, and nothing else.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const canvasSource = readFileSync(join(ROOT, 'src/components/agents/AgentCanvas.jsx'), 'utf8');
|
const canvasSource = readFileSync(join(ROOT, resolveSourcePath('src/components/agents/AgentCanvas.jsx')), 'utf8');
|
||||||
const agentUi = detailSource + configureSource + canvasSource;
|
const agentUi = detailSource + configureSource + canvasSource;
|
||||||
|
|
||||||
/* One surface with sections inside it, not four cards side by side. */
|
/* One surface with sections inside it, not four cards side by side. */
|
||||||
@@ -4922,10 +4922,10 @@ const canvasImporters = [
|
|||||||
...readdirSync(join(ROOT, 'src/pages/admin')).map((f) => ['src/pages/admin', f]),
|
...readdirSync(join(ROOT, 'src/pages/admin')).map((f) => ['src/pages/admin', f]),
|
||||||
...readdirSync(join(ROOT, 'src/components/agents')).map((f) => ['src/components/agents', f]),
|
...readdirSync(join(ROOT, 'src/components/agents')).map((f) => ['src/components/agents', f]),
|
||||||
]
|
]
|
||||||
.filter(([, f]) => /\.jsx?$/.test(f))
|
.filter(([, f]) => /\.[jt]sx?$/.test(f))
|
||||||
.filter(([, f]) => !/^(AgentConfigure|AgentDetail|AgentCanvas)\./.test(f))
|
.filter(([, f]) => !/^(AgentConfigure|AgentDetail|AgentCanvas)\./.test(f))
|
||||||
.filter(([dir, f]) => /agents\/AgentCanvas|from '\.\/AgentCanvas'/
|
.filter(([dir, f]) => /agents\/AgentCanvas|from '\.\/AgentCanvas'/
|
||||||
.test(readFileSync(join(ROOT, dir, f), 'utf8')))
|
.test(readFileSync(join(ROOT, resolveSourcePath(`${dir}/${f}`)), 'utf8')))
|
||||||
.map(([dir, f]) => `${dir}/${f}`);
|
.map(([dir, f]) => `${dir}/${f}`);
|
||||||
record('the workspace treatment is used by the configure screen alone',
|
record('the workspace treatment is used by the configure screen alone',
|
||||||
canvasImporters.length === 0,
|
canvasImporters.length === 0,
|
||||||
@@ -6045,8 +6045,8 @@ record('a candidate who scored zero still reaches the queue',
|
|||||||
/* Comments are stripped first: these assert what the page DOES, and a
|
/* Comments are stripped first: these assert what the page DOES, and a
|
||||||
comment explaining the bug that was fixed must not read as the bug. */
|
comment explaining the bug that was fixed must not read as the bug. */
|
||||||
const decomment = (src) => src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
const decomment = (src) => src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||||
const candidatesSrc = decomment(readFileSync(join(ROOT, 'src/pages/admin/Candidates.jsx'), 'utf8'));
|
const candidatesSrc = decomment(readFileSync(join(ROOT, resolveSourcePath('src/pages/admin/Candidates.jsx')), 'utf8'));
|
||||||
const nodesSrc = decomment(readFileSync(join(ROOT, 'src/pages/admin/candidates/nodes.jsx'), 'utf8'));
|
const nodesSrc = decomment(readFileSync(join(ROOT, resolveSourcePath('src/pages/admin/candidates/nodes.jsx')), 'utf8'));
|
||||||
/* Read from source rather than imported: the page pulls in the interview
|
/* Read from source rather than imported: the page pulls in the interview
|
||||||
modal, which touches `window` at module scope, and standing up a DOM to
|
modal, which touches `window` at module scope, and standing up a DOM to
|
||||||
read one string constant would be a worse test than reading the string. */
|
read one string constant would be a worse test than reading the string. */
|
||||||
@@ -6177,7 +6177,7 @@ const human = await server.ssrLoadModule('/src/lib/humanInterviews.js');
|
|||||||
* a real screening score with one this interview never took.
|
* a real screening score with one this interview never took.
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
const hooksSrc = readFileSync(join(ROOT, 'src/lib/krowHooks.js'), 'utf8');
|
const hooksSrc = readFileSync(join(ROOT, resolveSourcePath('src/lib/krowHooks.js')), 'utf8');
|
||||||
const complete = hooksSrc.slice(
|
const complete = hooksSrc.slice(
|
||||||
hooksSrc.indexOf('export function useCompleteHumanInterview'),
|
hooksSrc.indexOf('export function useCompleteHumanInterview'),
|
||||||
hooksSrc.indexOf('export function useRecordInterviewNotHeld')
|
hooksSrc.indexOf('export function useRecordInterviewNotHeld')
|
||||||
@@ -6223,7 +6223,7 @@ const human = await server.ssrLoadModule('/src/lib/humanInterviews.js');
|
|||||||
/* The AI flow is untouched: it still writes its own record at completion, and
|
/* The AI flow is untouched: it still writes its own record at completion, and
|
||||||
still does it through the same client this now shares. */
|
still does it through the same client this now shares. */
|
||||||
{
|
{
|
||||||
const aiSrc = readFileSync(join(ROOT, 'src/components/krow/AIInterviewModal.jsx'), 'utf8');
|
const aiSrc = readFileSync(join(ROOT, resolveSourcePath('src/components/krow/AIInterviewModal.jsx')), 'utf8');
|
||||||
record('the AI interview still records itself on completion',
|
record('the AI interview still records itself on completion',
|
||||||
/const finishInterview = useCallback/.test(aiSrc)
|
/const finishInterview = useCallback/.test(aiSrc)
|
||||||
&& /createInterview\.mutateAsync\(\{/.test(aiSrc)
|
&& /createInterview\.mutateAsync\(\{/.test(aiSrc)
|
||||||
@@ -6233,7 +6233,7 @@ const human = await server.ssrLoadModule('/src/lib/humanInterviews.js');
|
|||||||
|
|
||||||
/* The modal is no longer theatre. */
|
/* The modal is no longer theatre. */
|
||||||
{
|
{
|
||||||
const modalSrc = readFileSync(join(ROOT, 'src/components/krow/ScheduleInterviewModal.jsx'), 'utf8');
|
const modalSrc = readFileSync(join(ROOT, resolveSourcePath('src/components/krow/ScheduleInterviewModal.jsx')), 'utf8');
|
||||||
record('scheduling an interview persists it',
|
record('scheduling an interview persists it',
|
||||||
/useScheduleHumanInterview/.test(modalSrc) && /schedule\.mutateAsync/.test(modalSrc));
|
/useScheduleHumanInterview/.test(modalSrc) && /schedule\.mutateAsync/.test(modalSrc));
|
||||||
record('...the props its six call sites pass are unchanged',
|
record('...the props its six call sites pass are unchanged',
|
||||||
@@ -6250,8 +6250,8 @@ const human = await server.ssrLoadModule('/src/lib/humanInterviews.js');
|
|||||||
instead of to a person, and the hire it becomes cannot be traced back to the
|
instead of to a person, and the hire it becomes cannot be traced back to the
|
||||||
profile it came from. Both call sites are asserted because they were written
|
profile it came from. Both call sites are asserted because they were written
|
||||||
at different times and only one of them is on the position page. */
|
at different times and only one of them is on the position page. */
|
||||||
const positionDetailSource = readFileSync(join(ROOT, 'src/pages/PositionDetail.jsx'), 'utf8');
|
const positionDetailSource = readFileSync(join(ROOT, resolveSourcePath('src/pages/PositionDetail.jsx')), 'utf8');
|
||||||
const hooksSource = readFileSync(join(ROOT, 'src/lib/krowHooks.js'), 'utf8');
|
const hooksSource = readFileSync(join(ROOT, resolveSourcePath('src/lib/krowHooks.js')), 'utf8');
|
||||||
record('admitting talent to a position links the application to the profile',
|
record('admitting talent to a position links the application to the profile',
|
||||||
/worker_profile_id: profile\.id/.test(positionDetailSource),
|
/worker_profile_id: profile\.id/.test(positionDetailSource),
|
||||||
'PositionDetail.admitTalent');
|
'PositionDetail.admitTalent');
|
||||||
@@ -6295,7 +6295,7 @@ record('...and asks for nobody when there is no profile',
|
|||||||
records.applicationsForProfile(whoApps, null).length === 0, 'empty, not everything');
|
records.applicationsForProfile(whoApps, null).length === 0, 'empty, not everything');
|
||||||
|
|
||||||
/* The reported symptom: a talent pool you could read and not use. */
|
/* The reported symptom: a talent pool you could read and not use. */
|
||||||
const talentPoolSource = readFileSync(join(ROOT, 'src/pages/admin/talent-pool/nodes.jsx'), 'utf8');
|
const talentPoolSource = readFileSync(join(ROOT, resolveSourcePath('src/pages/admin/talent-pool/nodes.jsx')), 'utf8');
|
||||||
record('a talent pool row opens that person',
|
record('a talent pool row opens that person',
|
||||||
/onRowClick=\{\(p\) => navigate\(`\/admin\/talent\/\$\{p\.id\}`\)\}/.test(talentPoolSource),
|
/onRowClick=\{\(p\) => navigate\(`\/admin\/talent\/\$\{p\.id\}`\)\}/.test(talentPoolSource),
|
||||||
'DataTable wires the handler only when it is given one');
|
'DataTable wires the handler only when it is given one');
|
||||||
@@ -7695,7 +7695,7 @@ console.log('\n── Candidates vs Talent Pool ──');
|
|||||||
...registryMod.nodeRegistry.list(),
|
...registryMod.nodeRegistry.list(),
|
||||||
];
|
];
|
||||||
const engine = ['node.js', 'registry.js', 'operations.js', 'validate.js', 'patch.js', 'inspect.js']
|
const engine = ['node.js', 'registry.js', 'operations.js', 'validate.js', 'patch.js', 'inspect.js']
|
||||||
.map((f) => readFileSync(join(ROOT, 'src/lib/ui', f), 'utf8'));
|
.map((f) => readFileSync(join(ROOT, resolveSourcePath(`src/lib/ui/${f}`)), 'utf8'));
|
||||||
|
|
||||||
const offences = [];
|
const offences = [];
|
||||||
engine.forEach((source, i) => {
|
engine.forEach((source, i) => {
|
||||||
@@ -7922,7 +7922,7 @@ console.log('\n── Candidates vs Talent Pool ──');
|
|||||||
entry is only created by a `register` call that was handed a reference.
|
entry is only created by a `register` call that was handed a reference.
|
||||||
A node naming a type nobody registered draws nothing — asserted above —
|
A node naming a type nobody registered draws nothing — asserted above —
|
||||||
so there is no string that can become code. */
|
so there is no string that can become code. */
|
||||||
const source = readFileSync(join(ROOT, 'src/components/ui-tree/UiTreeRenderer.jsx'), 'utf8')
|
const source = readFileSync(join(ROOT, resolveSourcePath('src/components/ui-tree/UiTreeRenderer.jsx')), 'utf8')
|
||||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||||
.replace(/^\s*\/\/.*$/gm, '');
|
.replace(/^\s*\/\/.*$/gm, '');
|
||||||
return !/\bimport\s*\(/.test(source)
|
return !/\bimport\s*\(/.test(source)
|
||||||
@@ -8107,7 +8107,7 @@ console.log('\n── Candidates vs Talent Pool ──');
|
|||||||
record('applying writes only the uiLayouts key, so no other preference moves', (() => {
|
record('applying writes only the uiLayouts key, so no other preference moves', (() => {
|
||||||
/* `useUiLayouts.save` sends `{ uiLayouts }` and nothing else; the endpoint
|
/* `useUiLayouts.save` sends `{ uiLayouts }` and nothing else; the endpoint
|
||||||
shallow-merges, so `customSkills` and the rest survive untouched. */
|
shallow-merges, so `customSkills` and the rest survive untouched. */
|
||||||
const source = readFileSync(join(ROOT, 'src/lib/krowHooks.js'), 'utf8');
|
const source = readFileSync(join(ROOT, resolveSourcePath('src/lib/krowHooks.js')), 'utf8');
|
||||||
const fn = source.slice(source.indexOf('export function useUiLayouts'), source.indexOf('export function useJobPostings'));
|
const fn = source.slice(source.indexOf('export function useUiLayouts'), source.indexOf('export function useJobPostings'));
|
||||||
const writes = [...fn.matchAll(/mutateAsync\(\{([^}]*)\}/g)].map((m) => m[1].trim());
|
const writes = [...fn.matchAll(/mutateAsync\(\{([^}]*)\}/g)].map((m) => m[1].trim());
|
||||||
return writes.length === 1 && writes[0].startsWith('uiLayouts:');
|
return writes.length === 1 && writes[0].startsWith('uiLayouts:');
|
||||||
@@ -8207,7 +8207,7 @@ console.log('\n── Candidates vs Talent Pool ──');
|
|||||||
record('an operation that would be refused is never stored', (() => {
|
record('an operation that would be refused is never stored', (() => {
|
||||||
/* `propose` validates before it previews, so a refused change never becomes
|
/* `propose` validates before it previews, so a refused change never becomes
|
||||||
a preview and therefore never reaches an apply. */
|
a preview and therefore never reaches an apply. */
|
||||||
const provider = readFileSync(join(ROOT, 'src/components/ui-tree/UiEditingProvider.jsx'), 'utf8');
|
const provider = readFileSync(join(ROOT, resolveSourcePath('src/components/ui-tree/UiEditingProvider.jsx')), 'utf8');
|
||||||
const proposeBody = provider.slice(provider.indexOf('const propose'), provider.indexOf('/** Throw the experiment away'));
|
const proposeBody = provider.slice(provider.indexOf('const propose'), provider.indexOf('/** Throw the experiment away'));
|
||||||
return /applyOperation\(/.test(proposeBody)
|
return /applyOperation\(/.test(proposeBody)
|
||||||
&& /if \(!result\.ok\)/.test(proposeBody)
|
&& /if \(!result\.ok\)/.test(proposeBody)
|
||||||
@@ -8225,7 +8225,7 @@ console.log('\n── Candidates vs Talent Pool ──');
|
|||||||
];
|
];
|
||||||
const offences = [];
|
const offences = [];
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
const code = readFileSync(join(ROOT, f), 'utf8')
|
const code = readFileSync(join(ROOT, resolveSourcePath(f)), 'utf8')
|
||||||
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||||
for (const bad of ['writeFile', 'customSkills', 'skillDefinition', 'skill-definitions', '.md']) {
|
for (const bad of ['writeFile', 'customSkills', 'skillDefinition', 'skill-definitions', '.md']) {
|
||||||
if (code.includes(bad)) offences.push(`${f}: ${bad}`);
|
if (code.includes(bad)) offences.push(`${f}: ${bad}`);
|
||||||
@@ -8479,7 +8479,7 @@ console.log('\n── Candidates vs Talent Pool ──');
|
|||||||
})().pass);
|
})().pass);
|
||||||
|
|
||||||
record('the language layer names no page and no component', (() => {
|
record('the language layer names no page and no component', (() => {
|
||||||
const code = readFileSync(join(ROOT, 'src/lib/ui/intent.js'), 'utf8')
|
const code = readFileSync(join(ROOT, resolveSourcePath('src/lib/ui/intent.js')), 'utf8')
|
||||||
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||||
const banned = [...surfaces.SUPPORTED_SKILL_PAGES, ...reg4.list()];
|
const banned = [...surfaces.SUPPORTED_SKILL_PAGES, ...reg4.list()];
|
||||||
const hits = banned.filter((w) => new RegExp(`['"\`]${w}['"\`]`).test(code));
|
const hits = banned.filter((w) => new RegExp(`['"\`]${w}['"\`]`).test(code));
|
||||||
@@ -9518,7 +9518,7 @@ console.log('\n── Candidates vs Talent Pool ──');
|
|||||||
|
|
||||||
record('the editor never evaluates a string as code', (() => {
|
record('the editor never evaluates a string as code', (() => {
|
||||||
const files = ['UiEditor.jsx', 'TreePanel.jsx', 'NodeInspector.jsx', 'NodePicker.jsx', 'ops.js']
|
const files = ['UiEditor.jsx', 'TreePanel.jsx', 'NodeInspector.jsx', 'NodePicker.jsx', 'ops.js']
|
||||||
.map((f) => readFileSync(join(ROOT, 'src/components/ui-editor', f), 'utf8'))
|
.map((f) => readFileSync(join(ROOT, resolveSourcePath(`src/components/ui-editor/${f}`)), 'utf8'))
|
||||||
.join('\n')
|
.join('\n')
|
||||||
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||||
return !/\beval\s*\(/.test(files) && !/new\s+Function/.test(files)
|
return !/\beval\s*\(/.test(files) && !/new\s+Function/.test(files)
|
||||||
@@ -9527,7 +9527,7 @@ console.log('\n── Candidates vs Talent Pool ──');
|
|||||||
|
|
||||||
record('the editor contains no page name and no component branch', (() => {
|
record('the editor contains no page name and no component branch', (() => {
|
||||||
const files = ['UiEditor.jsx', 'TreePanel.jsx', 'NodeInspector.jsx', 'NodePicker.jsx', 'ops.js']
|
const files = ['UiEditor.jsx', 'TreePanel.jsx', 'NodeInspector.jsx', 'NodePicker.jsx', 'ops.js']
|
||||||
.map((f) => readFileSync(join(ROOT, 'src/components/ui-editor', f), 'utf8'))
|
.map((f) => readFileSync(join(ROOT, resolveSourcePath(`src/components/ui-editor/${f}`)), 'utf8'))
|
||||||
.join('\n')
|
.join('\n')
|
||||||
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||||
const banned = [...surfaces.SUPPORTED_SKILL_PAGES, ...nreg.list()];
|
const banned = [...surfaces.SUPPORTED_SKILL_PAGES, ...nreg.list()];
|
||||||
@@ -9740,7 +9740,7 @@ console.log('\n── Candidates vs Talent Pool ──');
|
|||||||
* `scripts/browser-flows.js`, and only held to its contract here.
|
* `scripts/browser-flows.js`, and only held to its contract here.
|
||||||
*/
|
*/
|
||||||
record('every node is rendered inside a boundary', (() => {
|
record('every node is rendered inside a boundary', (() => {
|
||||||
const source = readFileSync(join(ROOT, 'src/components/ui-tree/UiTreeRenderer.jsx'), 'utf8');
|
const source = readFileSync(join(ROOT, resolveSourcePath('src/components/ui-tree/UiTreeRenderer.jsx')), 'utf8');
|
||||||
return {
|
return {
|
||||||
pass: /<UiNodeBoundary node=\{node\}>/.test(source)
|
pass: /<UiNodeBoundary node=\{node\}>/.test(source)
|
||||||
&& /import \{ UiNodeBoundary \}/.test(source),
|
&& /import \{ UiNodeBoundary \}/.test(source),
|
||||||
|
|||||||
@@ -78,3 +78,31 @@ export function withSourceResolution(server, root = process.cwd()) {
|
|||||||
|
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same resolution, for source read as TEXT rather than loaded as a module.
|
||||||
|
*
|
||||||
|
* `skill-check.mjs` asserts structural facts by reading source files and
|
||||||
|
* matching against their contents — "the panel imports no local suggestion
|
||||||
|
* ranker", "no runtime path writes a definition". Those reads go through
|
||||||
|
* `readFileSync`, not `ssrLoadModule`, so `withSourceResolution` above never
|
||||||
|
* sees them: it wraps the loader, and this is a second, entirely separate
|
||||||
|
* channel. Renaming `base44Client.js` to `.ts` is what surfaced the difference,
|
||||||
|
* as an ENOENT in the middle of a suite that had been passing.
|
||||||
|
*
|
||||||
|
* Takes and returns a path RELATIVE to the project root, so the call site keeps
|
||||||
|
* its `join(ROOT, …)` exactly as it was:
|
||||||
|
*
|
||||||
|
* readFileSync(join(ROOT, resolveSourcePath('src/api/base44Client.js')), 'utf8')
|
||||||
|
*
|
||||||
|
* Unresolvable paths come back unchanged, so the resulting error still names
|
||||||
|
* the file the caller asked for rather than a candidate it invented. That also
|
||||||
|
* keeps the deliberately absent ones honest: a check asserting a file is GONE
|
||||||
|
* gets the path it asked about, and `existsSync` still answers false.
|
||||||
|
*/
|
||||||
|
export function resolveSourcePath(path, root = process.cwd()) {
|
||||||
|
for (const candidate of candidatesFor(path)) {
|
||||||
|
if (existsSync(join(root, candidate))) return candidate;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user