diff --git a/.changeset/await-ai-diagnostics.md b/.changeset/await-ai-diagnostics.md new file mode 100644 index 00000000000..7412e185a95 --- /dev/null +++ b/.changeset/await-ai-diagnostics.md @@ -0,0 +1,5 @@ +--- +"@hashintel/petrinaut": patch +--- + +Describe a clean net-code diagnostic check without claiming the whole model compiles. diff --git a/.changeset/snapshot-language-diagnostics.md b/.changeset/snapshot-language-diagnostics.md new file mode 100644 index 00000000000..2674a469523 --- /dev/null +++ b/.changeset/snapshot-language-diagnostics.md @@ -0,0 +1,5 @@ +--- +"@hashintel/petrinaut-core": patch +--- + +Describe snapshot validation and its TypeScript and HIR diagnostics in the AI tool. diff --git a/apps/brunch-agent/test/compiler-feedback.integration.ts b/apps/brunch-agent/test/compiler-feedback.integration.ts index a1eb83d57ee..2db5b938b37 100644 --- a/apps/brunch-agent/test/compiler-feedback.integration.ts +++ b/apps/brunch-agent/test/compiler-feedback.integration.ts @@ -34,8 +34,7 @@ import { openBrowserFixture } from "./browser-fixture.ts"; import { browserResultFrom } from "./browser-result.ts"; import { nativeSchemaProvider } from "./native-schema-provider.ts"; -const cleanCompilation = - "No errors detected in your model – everything compiles!"; +const cleanCompilation = "No errors or warnings found in net function code."; const output = mkdtempSync(join(tmpdir(), "m7c-compiler-feedback-")); const save = (name: string, value: unknown) => writeFileSync(join(output, `${name}.json`), JSON.stringify(value, null, 2)); diff --git a/apps/brunch-agent/test/integration/compiler-feedback.test.ts b/apps/brunch-agent/test/integration/compiler-feedback.test.ts index bd28ce147ad..2ea0e6a5e1b 100644 --- a/apps/brunch-agent/test/integration/compiler-feedback.test.ts +++ b/apps/brunch-agent/test/integration/compiler-feedback.test.ts @@ -37,7 +37,7 @@ test.skipIf(!enabled)( expect(summary.mode).toBe("batched-construction"); expect(summary.dirtyCompilation).toContain("definitelyNotDefined"); expect(summary.cleanCompilation).toBe( - "No errors detected in your model – everything compiles!", + "No errors or warnings found in net function code.", ); expect(summary.repairHash).toMatch(/^[a-f0-9]{64}$/u); expect(summary.layoutHash).toMatch(/^[a-f0-9]{64}$/u); diff --git a/apps/brunch-agent/test/root-creation.integration.ts b/apps/brunch-agent/test/root-creation.integration.ts index 6ed43ad1d61..f12e4d74afa 100644 --- a/apps/brunch-agent/test/root-creation.integration.ts +++ b/apps/brunch-agent/test/root-creation.integration.ts @@ -301,7 +301,7 @@ try { save("compilation", result); assert.equal( result.output, - "No errors detected in your model – everything compiles!", + "No errors or warnings found in net function code.", ); return text("Native creation and canonical check completed."); }), @@ -504,7 +504,7 @@ try { requests: contexts.length, applied: records.length, schemaClasses: observedNodeMutationNames, - compilation: "No errors detected in your model – everything compiles!", + compilation: "No errors or warnings found in net function code.", scope: "Same-session synthetic creation/correction only; reopen assertion follows.", }); diff --git a/apps/brunch-agent/test/typed-state.integration.ts b/apps/brunch-agent/test/typed-state.integration.ts index ccabbba7ab1..74a659b1701 100644 --- a/apps/brunch-agent/test/typed-state.integration.ts +++ b/apps/brunch-agent/test/typed-state.integration.ts @@ -631,7 +631,7 @@ try { compilations.push(result); assert.equal( result.output, - "No errors detected in your model – everything compiles!", + "No errors or warnings found in net function code.", "Final corrected net must report clean canonical diagnostics; no scenario execution follows", ); return text( diff --git a/libs/@hashintel/petrinaut-core/src/ai.ts b/libs/@hashintel/petrinaut-core/src/ai.ts index 5d2c9afdac5..867ba5871c9 100644 --- a/libs/@hashintel/petrinaut-core/src/ai.ts +++ b/libs/@hashintel/petrinaut-core/src/ai.ts @@ -141,7 +141,7 @@ const getLatestNetDefinitionToolInputSchema = z const getNetCompilationErrorsToolInputSchema = z .strictObject({}) .describe( - "Get the current TypeScript diagnostics for the Petrinaut net code. Use this after the net to check whether the model compiles.", + "Validate the current Petrinaut net snapshot and return its TypeScript and HIR diagnostics.", ); export const setNetTitleToolInputSchema = z diff --git a/libs/@hashintel/petrinaut-core/src/lsp/language-client.test.ts b/libs/@hashintel/petrinaut-core/src/lsp/language-client.test.ts index 0e70656d2f8..e90e6c9bfbf 100644 --- a/libs/@hashintel/petrinaut-core/src/lsp/language-client.test.ts +++ b/libs/@hashintel/petrinaut-core/src/lsp/language-client.test.ts @@ -102,6 +102,55 @@ describe("createLanguageClient diagnostics", () => { ]); }); + it("correlates snapshot responses without waiting for diagnostic changes", async () => { + const { transport, publish, respond, sent } = createFakeTransport(); + const client = createLanguageClient({ transport }); + const definition = { + places: [], + transitions: [], + parameters: [], + types: [], + differentialEquations: [], + }; + const first = client.requestDiagnostics(definition); + const second = client.requestDiagnostics(definition); + publish([ + { + uri: "inmemory://session", + diagnostics: [diagnostic("unrelated session")], + }, + ]); + respond(1, []); + respond(0, [ + { uri: "inmemory://net", diagnostics: [diagnostic("net error")] }, + ]); + + expect((await first).byUri.get("inmemory://net")?.[0]?.message).toBe( + "net error", + ); + expect((await second).errorCount).toBe(0); + expect(client.diagnostics.get().byUri.has("inmemory://session")).toBe(true); + expect(sent).toContainEqual({ + jsonrpc: "2.0", + id: 0, + method: "sdcpn/diagnostics", + params: { sdcpn: definition, extensions: undefined }, + }); + }); + + it("rejects a snapshot request when the worker is disposed", async () => { + const { transport } = createFakeTransport(); + const client = createLanguageClient({ transport }); + const response = client.requestDiagnostics({ + places: [], + transitions: [], + parameters: [], + types: [], + differentialEquations: [], + }); + client.dispose(); + await expect(response).rejects.toThrow(); + }); it("notifies subscribers only when a publish changed a diagnostic", () => { const { transport, publish } = createFakeTransport(); const client = createLanguageClient({ transport }); diff --git a/libs/@hashintel/petrinaut/docs/ai-assistant.md b/libs/@hashintel/petrinaut/docs/ai-assistant.md index e97d66f9125..b828d8223ff 100644 --- a/libs/@hashintel/petrinaut/docs/ai-assistant.md +++ b/libs/@hashintel/petrinaut/docs/ai-assistant.md @@ -179,7 +179,11 @@ The composer stays open in all of these cases, so you can still ask questions, r ## Diagnostics integration -When the assistant edits code surfaces (lambdas, kernels, dynamics, visualizers, metric/scenario code), it sees the resulting TypeScript diagnostics on the next turn and can iteratively fix them. You don't have to relay errors manually -- the post-edit re-check happens automatically. The same diagnostics also appear in the bottom **Diagnostics** tab as usual; the assistant just sees them in addition. +The assistant can request a fresh TypeScript check of the current net and +use the returned errors to revise its code. An unchanged set of errors still +counts as a completed check. If checking fails, the assistant receives an +error. The bottom **Diagnostics** tab continues to show diagnostics for the +code you are editing. ## Host configuration diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/experiments/create-experiment-drawer.test.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/experiments/create-experiment-drawer.test.tsx index 917380f4c19..8af63912bdf 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/experiments/create-experiment-drawer.test.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/experiments/create-experiment-drawer.test.tsx @@ -149,10 +149,12 @@ vi.mock("@hashintel/ds-components", async (importOriginal) => { */ function makeLanguageClient(): LanguageClientContextValue { return { + requestDiagnostics: vi.fn(() => + Promise.resolve({ byUri: new Map(), total: 0, errorCount: 0 }), + ), diagnosticsByUri: new Map(), totalDiagnosticsCount: 0, errorDiagnosticsCount: 0, - requestDiagnostics: vi.fn(), notifyDocumentChanged: vi.fn(), requestCompletion: vi.fn(() => Promise.resolve({ isIncomplete: false, items: [] }), diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/metrics/create-metric-drawer.test.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/metrics/create-metric-drawer.test.tsx index 4dc1535c624..c6924fd78a0 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/metrics/create-metric-drawer.test.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/metrics/create-metric-drawer.test.tsx @@ -30,10 +30,12 @@ vi.mock("@hashintel/ds-components", () => { function makeLanguageClientValue(): LanguageClientContextValue { return { + requestDiagnostics: vi.fn(() => + Promise.resolve({ byUri: new Map(), total: 0, errorCount: 0 }), + ), diagnosticsByUri: new Map(), totalDiagnosticsCount: 0, errorDiagnosticsCount: 0, - requestDiagnostics: vi.fn(), notifyDocumentChanged: vi.fn(), requestCompletion: vi.fn(() => Promise.resolve({ isIncomplete: false, items: [] }), diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel.test.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel.test.tsx index 388e52a173d..81cd533b4bf 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel.test.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel.test.tsx @@ -5279,7 +5279,7 @@ describe("AiAssistantPanel host interactive tools", () => { expect(diagnosticsOutputs).toHaveLength(2); expect( diagnosticsOutputs.every((output) => - output.includes("everything compiles"), + output.includes("No errors or warnings found in net function code."), ), ).toBe(true); expect( diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/format-diagnostics-for-ai.test.ts b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/format-diagnostics-for-ai.test.ts index 45e72d55820..c8e2a24b72d 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/format-diagnostics-for-ai.test.ts +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/format-diagnostics-for-ai.test.ts @@ -50,13 +50,13 @@ const diagnostic = ( }); describe("formatDiagnosticsForAi", () => { - test("reports an empty diagnostics state", () => { + test("reports that no errors or warnings were found in net function code", () => { expect( formatDiagnosticsForAi({ definition, diagnosticsByUri: new Map(), }), - ).toBe("No errors detected in your model – everything compiles!"); + ).toBe("No errors or warnings found in net function code."); }); test("formats transition and differential-equation diagnostics", () => { diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/format-diagnostics-for-ai.ts b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/format-diagnostics-for-ai.ts index ca313681623..0ddaa881f4f 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/format-diagnostics-for-ai.ts +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/format-diagnostics-for-ai.ts @@ -62,7 +62,7 @@ export const formatDiagnosticsForAi = ({ ); if (diagnostics.length === 0) { - return "No errors detected in your model – everything compiles!"; + return "No errors or warnings found in net function code."; } const shownDiagnostics = diagnostics.slice(0, maxDiagnostics); diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/read-current-diagnostics.test.ts b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/read-current-diagnostics.test.ts index 3075b189e70..825d3579a94 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/read-current-diagnostics.test.ts +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/read-current-diagnostics.test.ts @@ -30,7 +30,7 @@ describe("readCurrentDiagnostics", () => { try { await expect( readCurrentDiagnostics(instance, request), - ).resolves.toContain("everything compiles"); + ).resolves.toContain("No errors or warnings found in net function code."); instance.mutations.addParameter({ id: "rate", name: "Rate", @@ -40,7 +40,7 @@ describe("readCurrentDiagnostics", () => { }); await expect( readCurrentDiagnostics(instance, request), - ).resolves.toContain("everything compiles"); + ).resolves.toContain("No errors or warnings found in net function code."); expect( request.mock.calls.map(([definition]) => definition.parameters.length), ).toEqual([0, 1]); @@ -69,7 +69,9 @@ describe("readCurrentDiagnostics", () => { }); result.resolve({ byUri: new Map(), total: 0, errorCount: 0 }); await expect(read).resolves.toMatch(/changed.*check again/iu); - await expect(read).resolves.not.toMatch(/everything compiles/iu); + await expect(read).resolves.not.toContain( + "No errors or warnings found in net function code.", + ); } finally { instance.dispose(); } diff --git a/libs/@hashintel/petrinaut/vite.config.ts b/libs/@hashintel/petrinaut/vite.config.ts index e8cd7b777c3..3d8a969a454 100644 --- a/libs/@hashintel/petrinaut/vite.config.ts +++ b/libs/@hashintel/petrinaut/vite.config.ts @@ -8,8 +8,8 @@ const externalDependencies = [ "@hashintel/ds-components", "@hashintel/ds-helpers", /^@hashintel\/petrinaut-core(\/.*)?$/, - "react", - "react-dom", + /^react(\/.*)?$/, + /^react-dom(\/.*)?$/, "@xyflow/react", "@babel/standalone", // Pure-CJS dep pulled in transitively by @tanstack/react-form → diff --git a/libs/@local/petrinaut-arch-docs/content/ai/snapshot-diagnostics.mdx b/libs/@local/petrinaut-arch-docs/content/ai/snapshot-diagnostics.mdx new file mode 100644 index 00000000000..454a64091e5 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/ai/snapshot-diagnostics.mdx @@ -0,0 +1,35 @@ +--- +title: Snapshot diagnostics +description: Check a captured net through a correlated language-worker request. +attachTo: core.lsp +sidebar_order: 20 +--- + +`LanguageClient.requestDiagnostics(sdcpn, extensions)` returns TypeScript and HIR +diagnostics for the supplied net snapshot. Callers receive a response even +when the diagnostics match the previous check. + +![Snapshot diagnostics flow](@diagrams/snapshot-diagnostics.svg) + +The [language worker](layer:core.lsp.worker) checks the snapshot separately +from temporary editor sessions. Its response carries the request ID, so an +editor diagnostic update cannot complete another caller's check. Worker +errors and language-client disposal reject pending requests. + +The [React language provider](layer:react.lsp) exposes this method to the AI's +`getNetCompilationErrors` tool. The tool captures the current net and formats +the returned diagnostics. The editor continues to receive its own diagnostic +updates. See [compiling user code](doc:simulation/user-code) for the distinction +between TypeScript diagnostics and simulation compilation. + +:::danger[Important: worker timeout] +Requests have no timeout. An unresponsive worker can leave a check pending +until the language client is disposed. +Tracked in [FE-1665](https://linear.app/hash/issue/FE-1665). +::: + +:::danger[Important: instance API] +AI diagnostics still use the React language provider. Exposing this capability +through `PetrinautInstance` requires a headless diagnostics contract. +Tracked separately in [FE-754](https://linear.app/hash/issue/FE-754). +::: diff --git a/libs/@local/petrinaut-arch-docs/content/diagrams/snapshot-diagnostics.d2 b/libs/@local/petrinaut-arch-docs/content/diagrams/snapshot-diagnostics.d2 new file mode 100644 index 00000000000..c810504f6ba --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/diagrams/snapshot-diagnostics.d2 @@ -0,0 +1,10 @@ +direction: down +vars: {d2-config: {theme-id: 0}} +client: "Language client" +worker: "Language worker" +snapshot: "Check supplied snapshot" +reply: "Resolve matching request" +client -> worker: "snapshot + ID" +worker -> snapshot +snapshot -> reply: "diagnostics + ID" +reply -> client: "result / error"