Skip to content
Open
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
9 changes: 9 additions & 0 deletions .devcontainer/devcontainer-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"version": "2.5.9",
"resolved": "ghcr.io/devcontainers/features/common-utils@sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a",
"integrity": "sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a"
}
}
}
20 changes: 20 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "open code dev container",
"image": "oven/bun:1.3",

"features": {
"ghcr.io/devcontainers/features/common-utils:2": {}
},

"customizations": {
"vscode": {
"extensions": [
"oven.bun-vscode",
"ms-vscode.live-server"
]
}
},

"forwardPorts": [8080],

"postCreateCommand": "apt-get update && apt-get install -y python3 make g++ lcov && bun install"}
1,416 changes: 628 additions & 788 deletions bun.lock

Large diffs are not rendered by default.

46 changes: 33 additions & 13 deletions packages/codemode/src/tool-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,30 @@ type RenderContext = {
readonly pretty: boolean
}

const resolveRef = (
schema: JsonSchema,
definitions: Readonly<Record<string, JsonSchema>>,
seen: ReadonlySet<string>,
): { blocked: boolean; child?: JsonSchema; childSeen?: ReadonlySet<string> } => {
if (schema.$ref === undefined) return { blocked: false }

const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1]
const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment)
const target = name === undefined ? undefined : definitions[name]

if (name === undefined || target === undefined || seen.has(name)) return { blocked: true }
return { blocked: false, child: target, childSeen: new Set([...seen, name]) }
}

const collectChildSchemas = (schema: JsonSchema): JsonSchema[] => [
...(schema.anyOf ?? []),
...(schema.oneOf ?? []),
...(schema.allOf ?? []),
...Object.values(schema.properties ?? {}),
...(schema.items === undefined ? [] : [schema.items]),
...(typeof schema.additionalProperties === "object" ? [schema.additionalProperties] : []),
]

const hasUnresolvedRef = (
schema: JsonSchema,
definitions: Readonly<Record<string, JsonSchema>>,
Expand All @@ -48,20 +72,16 @@ const hasUnresolvedRef = (
): boolean => {
if (visited.has(schema)) return false
const nextVisited = new Set([...visited, schema])
if (schema.$ref !== undefined) {
const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1]
const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment)
if (name === undefined || definitions[name] === undefined || seen.has(name)) return true
if (hasUnresolvedRef(definitions[name], definitions, new Set([...seen, name]), nextVisited)) return true

const ref = resolveRef(schema, definitions, seen)
if (ref.blocked) return true
if (ref.child !== undefined && hasUnresolvedRef(ref.child, definitions, ref.childSeen, nextVisited)) {
return true
}
return [
...(schema.anyOf ?? []),
...(schema.oneOf ?? []),
...(schema.allOf ?? []),
...Object.values(schema.properties ?? {}),
...(schema.items === undefined ? [] : [schema.items]),
...(typeof schema.additionalProperties === "object" ? [schema.additionalProperties] : []),
].some((item) => hasUnresolvedRef(item, definitions, seen, nextVisited))

return collectChildSchemas(schema).some((item) =>
hasUnresolvedRef(item, definitions, seen, nextVisited),
)
}

/**
Expand Down
41 changes: 41 additions & 0 deletions packages/codemode/test/signature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,47 @@ describe("pretty signature rendering", () => {
expect(rendered).toContain("next?:")
}
})
test("treats a $ref to a missing $defs entry as unresolved", () => {
expect(
jsonSchemaToTypeScript({
$ref: "#/$defs/Ghost",
$defs: { Other: { type: "string" } },
}),
).toBe("unknown")
})

test("detects cycles across mutually recursive $defs, not just self-reference", () => {
const mutual = {
$ref: "#/$defs/A",
$defs: {
A: { type: "object", properties: { b: { $ref: "#/$defs/B" } } },
B: { type: "object", properties: { a: { $ref: "#/$defs/A" } } },
},
} as const
expect(jsonSchemaToTypeScript(mutual)).toBe("{ b?: { a?: unknown } }")
})

test("detects a cycle reached only through anyOf/items, not properties", () => {
const cyclicThroughAnyOf = {
$ref: "#/$defs/Node",
$defs: {
Node: {
anyOf: [{ type: "string" }, { type: "array", items: { $ref: "#/$defs/Node" } }],
},
},
} as const
expect(jsonSchemaToTypeScript(cyclicThroughAnyOf)).toBe("string | Array<unknown>")
})

test("propagates an unresolved ref found through a resolved $ref target inside allOf", () => {
expect(
jsonSchemaToTypeScript({
type: "string",
allOf: [{ $ref: "#/$defs/A" }],
$defs: { A: { $ref: "#/$defs/A" } },
}),
).toBe("unknown")
})

test("intersects ref and union siblings instead of discarding them", () => {
expect(
Expand Down
82 changes: 82 additions & 0 deletions tmp/opencode-test-0x6g0nbe6xh/add-plugin.ts.gcov.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html lang="en">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>LCOV - lcov.info - ../../../../tmp/opencode-test-0x6g0nbe6xh/add-plugin.ts</title>
<link rel="stylesheet" type="text/css" href="../../../../../../gcov.css">
</head>

<body>

<table width="100%" border=0 cellspacing=0 cellpadding=0>
<tr><td class="title">LCOV - code coverage report</td></tr>
<tr><td class="ruler"><img src="../../../../../../glass.png" width=3 height=3 alt=""></td></tr>

<tr>
<td width="100%">
<table cellpadding=1 border=0 width="100%">
<tr>
<td width="10%" class="headerItem">Current view:</td>
<td width="10%" class="headerValue"><a href="../../../../../../index.html" title="Click to go to top-level">top level</a> - <a href="index.html" title="Click to go to directory ../../../../tmp/opencode-test-0x6g0nbe6xh">../../../../tmp/opencode-test-0x6g0nbe6xh</a> - add-plugin.ts</td>
<td width="5%"></td>
<td width="5%"></td>
<td width="5%" class="headerCovTableHead">Coverage</td>
<td width="5%" class="headerCovTableHead" title="Covered + Uncovered code">Total</td>
<td width="5%" class="headerCovTableHead" title="Exercised code only">Hit</td>
</tr>
<tr>
<td class="headerItem">Test:</td>
<td class="headerValue">lcov.info</td>
<td></td>
<td class="headerItem">Lines:</td>
<td class="headerCovTableEntryHi">100.0&nbsp;%</td>
<td class="headerCovTableEntry">5</td>
<td class="headerCovTableEntry">5</td>
</tr>
<tr>
<td class="headerItem">Test Date:</td>
<td class="headerValue">2026-08-28 19:50:15</td>
<td></td>
<td class="headerItem">Functions:</td>
<td class="headerCovTableEntryHi">-</td>
<td class="headerCovTableEntry">0</td>
<td class="headerCovTableEntry">0</td>
</tr>
<tr><td><img src="../../../../../../glass.png" width=3 height=3 alt=""></td></tr>
</table>
</td>
</tr>

<tr><td class="ruler"><img src="../../../../../../glass.png" width=3 height=3 alt=""></td></tr>
</table>

<table cellpadding=0 cellspacing=0 border=0>
<tr>
<td><br></td>
</tr>
<tr>
<td>
<pre class="sourceHeading"> Line data Source code</pre>
<pre class="source">
<span id="L1"><span class="lineNum"> 1</span> <span class="tlaGNC"> 17 : /* ../../../../tmp/opencode-test-0x6g0nbe6xh/add-plugin.ts empty or unreadable */</span></span>
<span id="L2"><span class="lineNum"> 2</span> <span class="tlaGNC"> 17 : /* (content generated from coverage data) */</span></span>
<span id="L3"><span class="lineNum"> 3</span> <span class="tlaGNC"> 14 : /* ... */</span></span>
<span id="L4"><span class="lineNum"> 4</span> <span class="tlaGNC"> 72 : /* ... */</span></span>
<span id="L5"><span class="lineNum"> 5</span> : /* ... */</span>
<span id="L6"><span class="lineNum"> 6</span> <span class="tlaGNC"> 1 : /* ... */</span></span>
</pre>
</td>
</tr>
</table>
<br>

<table width="100%" border=0 cellspacing=0 cellpadding=0>
<tr><td class="ruler"><img src="../../../../../../glass.png" width=3 height=3 alt=""></td></tr>
<tr><td class="versionInfo">Generated by: <a href="https://github.com//linux-test-project/lcov" target="_parent">LCOV version 2.0-1</a></td></tr>
</table>
<br>

</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html lang="en">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>LCOV - lcov.info - ../../../../tmp/opencode-test-0xlgkvsokwm9/.opencode/plugin/custom-copilot-auth.ts</title>
<link rel="stylesheet" type="text/css" href="../../../../../../../../gcov.css">
</head>

<body>

<table width="100%" border=0 cellspacing=0 cellpadding=0>
<tr><td class="title">LCOV - code coverage report</td></tr>
<tr><td class="ruler"><img src="../../../../../../../../glass.png" width=3 height=3 alt=""></td></tr>

<tr>
<td width="100%">
<table cellpadding=1 border=0 width="100%">
<tr>
<td width="10%" class="headerItem">Current view:</td>
<td width="10%" class="headerValue"><a href="../../../../../../../../index.html" title="Click to go to top-level">top level</a> - <a href="index.html" title="Click to go to directory ../../../../tmp/opencode-test-0xlgkvsokwm9/.opencode/plugin">../../../../tmp/opencode-test-0xlgkvsokwm9/.opencode/plugin</a> - custom-copilot-auth.ts</td>
<td width="5%"></td>
<td width="5%"></td>
<td width="5%" class="headerCovTableHead">Coverage</td>
<td width="5%" class="headerCovTableHead" title="Covered + Uncovered code">Total</td>
<td width="5%" class="headerCovTableHead" title="Exercised code only">Hit</td>
</tr>
<tr>
<td class="headerItem">Test:</td>
<td class="headerValue">lcov.info</td>
<td></td>
<td class="headerItem">Lines:</td>
<td class="headerCovTableEntryHi">100.0&nbsp;%</td>
<td class="headerCovTableEntry">12</td>
<td class="headerCovTableEntry">12</td>
</tr>
<tr>
<td class="headerItem">Test Date:</td>
<td class="headerValue">2026-08-28 19:50:15</td>
<td></td>
<td class="headerItem">Functions:</td>
<td class="headerCovTableEntryHi">-</td>
<td class="headerCovTableEntry">0</td>
<td class="headerCovTableEntry">0</td>
</tr>
<tr><td><img src="../../../../../../../../glass.png" width=3 height=3 alt=""></td></tr>
</table>
</td>
</tr>

<tr><td class="ruler"><img src="../../../../../../../../glass.png" width=3 height=3 alt=""></td></tr>
</table>

<table cellpadding=0 cellspacing=0 border=0>
<tr>
<td><br></td>
</tr>
<tr>
<td>
<pre class="sourceHeading"> Line data Source code</pre>
<pre class="source">
<span id="L1"><span class="lineNum"> 1</span> <span class="tlaGNC"> 17 : /* ../../../../tmp/opencode-test-0xlgkvsokwm9/.opencode/plugin/custom-copilot-auth.ts empty or unreadable */</span></span>
<span id="L2"><span class="lineNum"> 2</span> <span class="tlaGNC"> 33 : /* (content generated from coverage data) */</span></span>
<span id="L3"><span class="lineNum"> 3</span> <span class="tlaGNC"> 19 : /* ... */</span></span>
<span id="L4"><span class="lineNum"> 4</span> <span class="tlaGNC"> 13 : /* ... */</span></span>
<span id="L5"><span class="lineNum"> 5</span> <span class="tlaGNC"> 33 : /* ... */</span></span>
<span id="L6"><span class="lineNum"> 6</span> <span class="tlaGNC"> 18 : /* ... */</span></span>
<span id="L7"><span class="lineNum"> 7</span> <span class="tlaGNC"> 50 : /* ... */</span></span>
<span id="L8"><span class="lineNum"> 8</span> <span class="tlaGNC"> 8 : /* ... */</span></span>
<span id="L9"><span class="lineNum"> 9</span> <span class="tlaGNC"> 11 : /* ... */</span></span>
<span id="L10"><span class="lineNum"> 10</span> <span class="tlaGNC"> 3 : /* ... */</span></span>
<span id="L11"><span class="lineNum"> 11</span> <span class="tlaGNC"> 2 : /* ... */</span></span>
<span id="L12"><span class="lineNum"> 12</span> <span class="tlaGNC"> 1 : /* ... */</span></span>
</pre>
</td>
</tr>
</table>
<br>

<table width="100%" border=0 cellspacing=0 cellpadding=0>
<tr><td class="ruler"><img src="../../../../../../../../glass.png" width=3 height=3 alt=""></td></tr>
<tr><td class="versionInfo">Generated by: <a href="https://github.com//linux-test-project/lcov" target="_parent">LCOV version 2.0-1</a></td></tr>
</table>
<br>

</body>
</html>
86 changes: 86 additions & 0 deletions tmp/opencode-test-1phthz7r6l9/bad-plugin.ts.gcov.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html lang="en">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>LCOV - lcov.info - ../../../../tmp/opencode-test-1phthz7r6l9/bad-plugin.ts</title>
<link rel="stylesheet" type="text/css" href="../../../../../../gcov.css">
</head>

<body>

<table width="100%" border=0 cellspacing=0 cellpadding=0>
<tr><td class="title">LCOV - code coverage report</td></tr>
<tr><td class="ruler"><img src="../../../../../../glass.png" width=3 height=3 alt=""></td></tr>

<tr>
<td width="100%">
<table cellpadding=1 border=0 width="100%">
<tr>
<td width="10%" class="headerItem">Current view:</td>
<td width="10%" class="headerValue"><a href="../../../../../../index.html" title="Click to go to top-level">top level</a> - <a href="index.html" title="Click to go to directory ../../../../tmp/opencode-test-1phthz7r6l9">../../../../tmp/opencode-test-1phthz7r6l9</a> - bad-plugin.ts</td>
<td width="5%"></td>
<td width="5%"></td>
<td width="5%" class="headerCovTableHead">Coverage</td>
<td width="5%" class="headerCovTableHead" title="Covered + Uncovered code">Total</td>
<td width="5%" class="headerCovTableHead" title="Exercised code only">Hit</td>
</tr>
<tr>
<td class="headerItem">Test:</td>
<td class="headerValue">lcov.info</td>
<td></td>
<td class="headerItem">Lines:</td>
<td class="headerCovTableEntryHi">100.0&nbsp;%</td>
<td class="headerCovTableEntry">9</td>
<td class="headerCovTableEntry">9</td>
</tr>
<tr>
<td class="headerItem">Test Date:</td>
<td class="headerValue">2026-08-28 19:50:15</td>
<td></td>
<td class="headerItem">Functions:</td>
<td class="headerCovTableEntryHi">-</td>
<td class="headerCovTableEntry">0</td>
<td class="headerCovTableEntry">0</td>
</tr>
<tr><td><img src="../../../../../../glass.png" width=3 height=3 alt=""></td></tr>
</table>
</td>
</tr>

<tr><td class="ruler"><img src="../../../../../../glass.png" width=3 height=3 alt=""></td></tr>
</table>

<table cellpadding=0 cellspacing=0 border=0>
<tr>
<td><br></td>
</tr>
<tr>
<td>
<pre class="sourceHeading"> Line data Source code</pre>
<pre class="source">
<span id="L1"><span class="lineNum"> 1</span> <span class="tlaGNC"> 17 : /* ../../../../tmp/opencode-test-1phthz7r6l9/bad-plugin.ts empty or unreadable */</span></span>
<span id="L2"><span class="lineNum"> 2</span> <span class="tlaGNC"> 17 : /* (content generated from coverage data) */</span></span>
<span id="L3"><span class="lineNum"> 3</span> <span class="tlaGNC"> 27 : /* ... */</span></span>
<span id="L4"><span class="lineNum"> 4</span> <span class="tlaGNC"> 57 : /* ... */</span></span>
<span id="L5"><span class="lineNum"> 5</span> <span class="tlaGNC"> 35 : /* ... */</span></span>
<span id="L6"><span class="lineNum"> 6</span> <span class="tlaGNC"> 52 : /* ... */</span></span>
<span id="L7"><span class="lineNum"> 7</span> <span class="tlaGNC"> 6 : /* ... */</span></span>
<span id="L8"><span class="lineNum"> 8</span> <span class="tlaGNC"> 30 : /* ... */</span></span>
<span id="L9"><span class="lineNum"> 9</span> : /* ... */</span>
<span id="L10"><span class="lineNum"> 10</span> <span class="tlaGNC"> 1 : /* ... */</span></span>
</pre>
</td>
</tr>
</table>
<br>

<table width="100%" border=0 cellspacing=0 cellpadding=0>
<tr><td class="ruler"><img src="../../../../../../glass.png" width=3 height=3 alt=""></td></tr>
<tr><td class="versionInfo">Generated by: <a href="https://github.com//linux-test-project/lcov" target="_parent">LCOV version 2.0-1</a></td></tr>
</table>
<br>

</body>
</html>
Loading
Loading