fix: resolve lint and formatting errors in AI Link Enhancer implementation

This commit is contained in:
Kodai Nakamura 2026-04-06 23:19:38 +09:00
parent 82ac311afb
commit 76e0da3ac9
7 changed files with 33 additions and 34 deletions

View file

@ -1,4 +1,4 @@
export const request = async () => ({});
export const request = async () => ({})
export class Notice {
constructor() {}
hide() {}

View file

@ -17,26 +17,26 @@ describe("replaceLinks with AI disambiguation", () => {
it("should use the AI-resolved path for unlinked words", () => {
const body = "I have a meeting."
const resolvedAmbiguities = new Map([["meeting", "work/meeting"]])
const result = replaceLinks({
body,
linkResolverContext: context,
resolvedAmbiguities,
})
expect(result).toBe("I have a [[work/meeting|meeting]].")
})
it("should correct existing links if resolvedAmbiguities contains them", () => {
const body = "Check [[private/meeting|meeting]] notes."
const resolvedAmbiguities = new Map([["[[private/meeting|meeting]]", "work/meeting"]])
const result = replaceLinks({
body,
linkResolverContext: context,
resolvedAmbiguities,
})
expect(result).toBe("Check [[work/meeting|meeting]] notes.")
})
@ -44,13 +44,13 @@ describe("replaceLinks with AI disambiguation", () => {
const body = "Check [[private/meeting]] notes."
// The resolveAmbiguities scanner uses the full link as key
const resolvedAmbiguities = new Map([["[[private/meeting]]", "work/meeting"]])
const result = replaceLinks({
body,
linkResolverContext: context,
resolvedAmbiguities,
})
expect(result).toBe("Check [[work/meeting|private/meeting]] notes.")
})
})

View file

@ -734,9 +734,6 @@ const processStandardText = (
// Case comparison happened during trie traversal if ignoreCase is true.
const candidateData = candidateMap.get(trieCandidateKey)
// Store the original text matched for potential use as display text
const originalMatchedText = text.substring(i, i + lastCandidate.length)
if (candidateData) {
// Check if this is a self-link and should be prevented
if (isSelfLink(candidateData, filePath, settings)) {
@ -968,23 +965,23 @@ export const replaceLinks = ({
const mIndex = match.index
const segment = bodyWithPlaceholders.slice(lastIndex, mIndex)
resultBody += processTextSegment(segment)
const fullMatch = match[0]
if (resolvedAmbiguities?.has(fullMatch)) {
// Existing link replacement
const resolvedPath = resolvedAmbiguities.get(fullMatch)!
const { linkPath, alias: resolvedAlias } = extractLinkParts(resolvedPath)
// Try to extract existing alias from the matched link
const existingLinkRegex = /\[\[([^|\]]+)(?:\|([^\]]+))?\]\]/
const linkMatch = fullMatch.match(existingLinkRegex)
const existingPath = linkMatch ? linkMatch[1] : ""
const existingAlias = linkMatch ? linkMatch[2] : undefined
// Use resolved alias if present, otherwise use existing alias,
// Use resolved alias if present, otherwise use existing alias,
// otherwise use existing path (as alias if it was a simple link)
const finalAlias = resolvedAlias || existingAlias || (fullMatch.includes("|") ? undefined : existingPath)
const isInTable = isIndexInsideMarkdownTable(bodyWithPlaceholders, mIndex)
resultBody += linkGenerator({
linkPath,

View file

@ -37,7 +37,7 @@ describe("resolveAmbiguities", () => {
it("should identify ambiguous unlinked words", async () => {
const text = "I have a meeting tomorrow."
vi.mocked(aiClient.resolveAmbiguitiesBatch).mockResolvedValue(
new Map([["meeting", "work/meeting"]])
new Map([["meeting", "work/meeting"]]),
)
const result = await resolveAmbiguities(text, candidateMap, trie, mockSettings)
@ -48,8 +48,8 @@ describe("resolveAmbiguities", () => {
expect.objectContaining({
word: "meeting",
candidates: ["work/meeting", "private/meeting"],
})
])
}),
]),
)
expect(result.get("meeting")).toBe("work/meeting")
})
@ -57,7 +57,7 @@ describe("resolveAmbiguities", () => {
it("should identify ambiguous existing links for verification", async () => {
const text = "Check the [[private/meeting|meeting]] notes."
vi.mocked(aiClient.resolveAmbiguitiesBatch).mockResolvedValue(
new Map([["[[private/meeting|meeting]]", "work/meeting"]])
new Map([["[[private/meeting|meeting]]", "work/meeting"]]),
)
const result = await resolveAmbiguities(text, candidateMap, trie, mockSettings)
@ -68,8 +68,8 @@ describe("resolveAmbiguities", () => {
expect.objectContaining({
word: "[[private/meeting|meeting]]",
candidates: ["work/meeting", "private/meeting"],
})
])
}),
]),
)
expect(result.get("[[private/meeting|meeting]]")).toBe("work/meeting")
})

View file

@ -53,7 +53,8 @@ export const callAI = async (
const data = JSON.parse(response)
return data.choices[0].message.content
} catch (error) {
}
catch (error) {
console.error("AI Link Enhancer API Error:", error)
throw error
}
@ -83,10 +84,10 @@ ${JSON.stringify(requests, null, 2)}
`
const resultRaw = await callAI(settings, prompt)
// Clean up potential markdown code blocks if the AI included them
const cleanJson = resultRaw.replace(/```json\n?/, "").replace(/\n?```/, "").trim()
const result = JSON.parse(cleanJson)
const resultMap = new Map<string, string>()

View file

@ -10,7 +10,7 @@ export const resolveAmbiguities = async (
settings: AutomaticLinkerSettings,
): Promise<Map<string, string>> => {
const requests: AIResolveRequest[] = []
// 1. Scan for existing links [[Path|Alias]] or [[Path]]
const existingLinkRegex = /\[\[([^|\]]+)(?:\|([^\]]+))?\]\]/g
let match: RegExpExecArray | null
@ -18,12 +18,12 @@ export const resolveAmbiguities = async (
const fullMatch = match[0]
const path = match[1]
const alias = match[2] || path
const candidateData = candidateMap.get(alias)
if (candidateData && candidateData.candidates.length > 1) {
const start = Math.max(0, match.index - settings.aiMaxContext)
const end = Math.min(text.length, match.index + fullMatch.length + settings.aiMaxContext)
requests.push({
word: fullMatch, // Using the full link as the "word" key for replacement
text: text.slice(start, end),
@ -38,7 +38,7 @@ export const resolveAmbiguities = async (
while (i < text.length) {
let node = trie
let j = i
let lastMatch: { word: string; data: CandidateData; index: number } | null = null
let lastMatch: { word: string, data: CandidateData, index: number } | null = null
while (j < text.length && node.children.has(text[j].toLowerCase())) {
node = node.children.get(text[j].toLowerCase())!
@ -53,13 +53,13 @@ export const resolveAmbiguities = async (
if (lastMatch && lastMatch.data.candidates.length > 1) {
// Check if it's already inside a link (simple check)
const isInsideLink = text.slice(Math.max(0, i - 2), i) === "[[" ||
text.slice(i + lastMatch.word.length, i + lastMatch.word.length + 2) === "]]"
const isInsideLink = text.slice(Math.max(0, i - 2), i) === "[["
|| text.slice(i + lastMatch.word.length, i + lastMatch.word.length + 2) === "]]"
if (!isInsideLink) {
const start = Math.max(0, i - settings.aiMaxContext)
const end = Math.min(text.length, i + lastMatch.word.length + settings.aiMaxContext)
requests.push({
word: lastMatch.word,
text: text.slice(start, end),
@ -67,7 +67,8 @@ export const resolveAmbiguities = async (
})
}
i += lastMatch.word.length
} else {
}
else {
i++
}
}

View file

@ -5,7 +5,7 @@ export default defineConfig({
test: {
includeSource: ["src/**/*.{js,ts}"],
alias: {
"obsidian": path.resolve(__dirname, "./src/__mocks__/obsidian.ts"),
obsidian: path.resolve(__dirname, "./src/__mocks__/obsidian.ts"),
},
},
})