Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/example/server/utils/agent-readability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import manifestJson from "../../src/generated/agent-readability.json" with {
type: "json",
};

export const agentReadabilityManifest: AgentReadabilityManifest = {
export const agentReadabilityManifest = {
...manifestJson,
version: 1,
};
} as unknown as AgentReadabilityManifest;
Comment on lines +17 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all forced casts to AgentReadabilityManifest so they can be replaced consistently.
rg -nP 'as\s+unknown\s+as\s+AgentReadabilityManifest'

Repository: inthhq/leadtype

Length of output: 230


🏁 Script executed:

#!/bin/bash
# Check context around line 20 in agent-readability.ts
echo "=== apps/example/server/utils/agent-readability.ts context ==="
sed -n '10,25p' apps/example/server/utils/agent-readability.ts

echo -e "\n=== apps/example/src/lib/docs-head.ts context ==="
# Check context around line 11 in docs-head.ts
sed -n '5,20p' apps/example/src/lib/docs-head.ts

echo -e "\n=== TypeScript configuration ==="
# Check tsconfig to verify satisfies operator support
if [ -f "tsconfig.json" ]; then
  cat tsconfig.json | jq -r '.compilerOptions.target' 2>/dev/null || cat tsconfig.json
fi

Repository: inthhq/leadtype

Length of output: 963


🏁 Script executed:

#!/bin/bash
# Find AgentReadabilityManifest type definition
echo "=== Finding AgentReadabilityManifest type definition ==="
rg -n 'type AgentReadabilityManifest|interface AgentReadabilityManifest' --type ts --type tsx

echo -e "\n=== Check for any other variations of this problematic pattern ==="
# Check for any other double-cast patterns in the codebase
rg -n 'as unknown as' --type ts --type tsx | head -20

Repository: inthhq/leadtype

Length of output: 258


🏁 Script executed:

#!/bin/bash
# Search for AgentReadabilityManifest more broadly
echo "=== Searching for AgentReadabilityManifest definition ==="
rg 'AgentReadabilityManifest' | head -30

echo -e "\n=== Check for any other variations of double-cast patterns ==="
rg 'as unknown as' | head -20

echo -e "\n=== Find files importing AgentReadabilityManifest ==="
rg "import.*AgentReadabilityManifest"

Repository: inthhq/leadtype

Length of output: 3822


🏁 Script executed:

#!/bin/bash
# Get the AgentReadabilityManifest type definition
echo "=== AgentReadabilityManifest type definition ==="
sed -n '/export type AgentReadabilityManifest/,/^}/p' packages/leadtype/src/llm/readability.ts | head -20

Repository: inthhq/leadtype

Length of output: 403


Fix double-casts to AgentReadabilityManifest in both files using satisfies operator

Two instances of the problematic as unknown as AgentReadabilityManifest pattern exist in the codebase (not just one). Both should be replaced consistently using satisfies for proper structural validation:

  • apps/example/server/utils/agent-readability.ts:20
  • apps/example/src/lib/docs-head.ts:11

The satisfies operator validates the object shape at compile time without forcing a type assertion, catching schema drift that double-casts would mask. Apply the same fix to both:

Proposed fix for both files
 export const agentReadabilityManifest = {
   ...manifestJson,
-  version: 1,
-} as unknown as AgentReadabilityManifest;
+  version: 1 as const,
+} satisfies AgentReadabilityManifest;

(Same pattern applies to docs-head.ts)

Per coding guidelines: "Leverage TypeScript's type narrowing instead of type assertions" and "Use const assertions (as const) for immutable values and literal types."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const agentReadabilityManifest = {
...manifestJson,
version: 1,
};
} as unknown as AgentReadabilityManifest;
export const agentReadabilityManifest = {
...manifestJson,
version: 1 as const,
} satisfies AgentReadabilityManifest;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/example/server/utils/agent-readability.ts` around lines 17 - 20, The
object currently assigned to agentReadabilityManifest uses a double-cast ("as
unknown as AgentReadabilityManifest") which masks shape errors; replace that
double-cast with the TypeScript satisfies operator so the manifest is validated
structurally (e.g., export const agentReadabilityManifest = { ...manifestJson,
version: 1 } as const satisfies AgentReadabilityManifest) and apply the
identical change in apps/example/src/lib/docs-head.ts for the other manifest
export; keep the spread of manifestJson and add as const where appropriate to
preserve literal types while using satisfies for compile-time validation.


export function getRequestOrigin(event: H3Event): string | undefined {
const forwardedHost = getHeader(event, "x-forwarded-host")
Expand Down
14 changes: 12 additions & 2 deletions apps/example/src/components/docs-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,29 @@

import { Link, useRouterState } from "@tanstack/react-router";
import type { ReactNode } from "react";
import { docsSidebarSections } from "@/lib/docs";
import { docsSidebarSections, findDocsNavigationPage } from "@/lib/docs";
import { cn } from "@/lib/utils";
import { SiteFooter } from "./site-footer";
import { SiteHeader } from "./site-header";
import { TableOfContents } from "./table-of-contents";

export function DocsShell({ children }: { children: ReactNode }) {
const pathname = useRouterState({
select: (state) => state.location.pathname,
});
const currentPage = findDocsNavigationPage(pathname);
const tocItems = currentPage?.toc ?? [];
const hasToc = tocItems.length > 0;

return (
<div className="flex min-h-svh flex-col">
<SiteHeader />
<div className="mx-auto grid w-full max-w-7xl flex-1 gap-6 px-4 py-7 sm:px-6 lg:grid-cols-[200px_minmax(0,1fr)]">
<div
className={cn(
"mx-auto grid w-full max-w-[90rem] flex-1 gap-6 px-4 py-7 sm:px-6 lg:grid-cols-[200px_minmax(0,1fr)]",
hasToc && "lg:grid-cols-[200px_minmax(0,1fr)_220px]"
)}
>
<aside className="space-y-5">
{docsSidebarSections.map((section) => (
<div className="space-y-2" key={section.title}>
Expand Down Expand Up @@ -48,6 +57,7 @@ export function DocsShell({ children }: { children: ReactNode }) {
{children}
</section>
</main>
<TableOfContents items={tocItems} />
</div>
<SiteFooter />
</div>
Expand Down
179 changes: 179 additions & 0 deletions apps/example/src/components/table-of-contents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"use client";

import type { DocsTableOfContentsItem } from "leadtype/llm";
import { useEffect, useMemo, useState } from "react";
import { cn } from "@/lib/utils";
import { Separator } from "./ui/separator";

const HEADING_TOP_OFFSET = 104;
const ACTIVE_LINE_RATIO = 0.35;
const ACTIVE_LINE_MAX_OFFSET = 220;
const BOTTOM_SCROLL_TOLERANCE = 4;

interface TableOfContentsProps {
items: DocsTableOfContentsItem[];
}

function flattenTocItems(
items: DocsTableOfContentsItem[]
): DocsTableOfContentsItem[] {
return items.flatMap((item) => [item, ...flattenTocItems(item.children)]);
}

function TocItems({
activeId,
onSelect,
items,
depth = 0,
}: {
activeId?: string;
onSelect: (id: string) => void;
items: DocsTableOfContentsItem[];
depth?: number;
}) {
return (
<ul className={cn("space-y-1", depth > 0 && "mt-1 pl-3")}>
{items.map((item) => (
<li key={item.urlWithHash}>
<a
aria-current={activeId === item.id ? "location" : undefined}
className={cn(
"relative block rounded-md px-2 py-1 text-muted-foreground text-sm leading-5 transition-all duration-200 hover:bg-secondary hover:text-foreground",
"before:absolute before:inset-y-1 before:left-0 before:w-px before:origin-top before:scale-y-0 before:bg-foreground before:transition-transform before:duration-200",
depth > 0 && "text-xs",
activeId === item.id &&
"translate-x-1 bg-secondary text-foreground before:scale-y-100"
)}
href={item.urlWithHash}
onClick={() => {
onSelect(item.id);
}}
>
{item.title}
</a>
{item.children.length > 0 ? (
<TocItems
activeId={activeId}
depth={depth + 1}
items={item.children}
onSelect={onSelect}
/>
) : null}
</li>
))}
</ul>
);
}

export function TableOfContents({ items }: TableOfContentsProps) {
const flatItems = useMemo(() => flattenTocItems(items), [items]);
const [activeId, setActiveId] = useState<string | undefined>(
flatItems[0]?.id
);

useEffect(() => {
setActiveId(flatItems[0]?.id);
}, [flatItems]);

useEffect(() => {
if (flatItems.length === 0) {
return;
}

const headings = flatItems
.map((item) => document.getElementById(item.id))
.filter((heading): heading is HTMLElement => Boolean(heading));

const getActiveHeading = () => {
const scrollBottom = window.scrollY + window.innerHeight;
const pageBottom = document.documentElement.scrollHeight;

if (pageBottom - scrollBottom <= BOTTOM_SCROLL_TOLERANCE) {
return headings.at(-1);
}

const activeLine = Math.max(
HEADING_TOP_OFFSET,
Math.min(
window.innerHeight * ACTIVE_LINE_RATIO,
HEADING_TOP_OFFSET + ACTIVE_LINE_MAX_OFFSET
)
);

let activeHeading = headings[0];
for (const heading of headings) {
if (heading.getBoundingClientRect().top <= activeLine) {
activeHeading = heading;
continue;
}

break;
}

return activeHeading;
};

let animationFrame = 0;

const updateActiveHeading = () => {
animationFrame = 0;
setActiveId(getActiveHeading()?.id);
};

const scheduleActiveHeadingUpdate = () => {
if (animationFrame !== 0) {
return;
}

animationFrame = window.requestAnimationFrame(updateActiveHeading);
};

const updateFromHash = () => {
const rawHash = window.location.hash.slice(1);
let hashId = rawHash;
try {
hashId = decodeURIComponent(rawHash);
} catch {
// malformed % sequence; fall back to the raw hash
}
const hashHeading = headings.find((heading) => heading.id === hashId);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (hashHeading) {
setActiveId(hashHeading.id);
}
scheduleActiveHeadingUpdate();
};

updateFromHash();

window.addEventListener("scroll", scheduleActiveHeadingUpdate, {
passive: true,
});
window.addEventListener("resize", scheduleActiveHeadingUpdate);
window.addEventListener("hashchange", updateFromHash);

return () => {
if (animationFrame !== 0) {
window.cancelAnimationFrame(animationFrame);
}
window.removeEventListener("scroll", scheduleActiveHeadingUpdate);
window.removeEventListener("resize", scheduleActiveHeadingUpdate);
window.removeEventListener("hashchange", updateFromHash);
};
}, [flatItems]);

if (items.length === 0) {
return null;
}

return (
<aside className="sticky top-[calc(var(--docs-anchor-offset-rem)+0.75rem)] hidden max-h-[calc(100svh-var(--docs-anchor-offset-rem)-1.5rem)] self-start overflow-y-auto lg:block">
<nav aria-label="On this page" className="space-y-3">
<h2 className="px-2 font-medium text-foreground text-xs uppercase tracking-wider">
On this page
</h2>
<Separator />
<TocItems activeId={activeId} items={items} onSelect={setActiveId} />
</nav>
</aside>
);
}
Loading
Loading