diff --git a/.github/actions/prune-repository/prune.py b/.github/actions/prune-repository/prune.py index fcb64d187cd..b4438b26844 100644 --- a/.github/actions/prune-repository/prune.py +++ b/.github/actions/prune-repository/prune.py @@ -44,39 +44,18 @@ REQUESTED_DEPENDENCIES: dict[str, list[str]] = { # The frontend's wire-conformance suite decodes the atlas crate's checked-in fixtures "@apps/hash-frontend": ["@rust/hash-graph-atlas"], - # Brunch core's architecture and contract tests inspect the app and shipped - # plugins, but a job whose requested scope is only a sibling or a consumer - # of core must not pull those fixtures. - "@hashintel/brunch-agent": [ - "@apps/brunch-agent", - "@hashintel/brunch-agent-plugin-gherkin", - "@hashintel/brunch-agent-plugin-sdcpn", - ], } # Non-workspace paths required by packages in the *requested* scope. # `turbo prune` copies workspace directories and root manifests only. REQUESTED_PATHS: dict[str, list[str]] = { - # Core's shipped-definition and baseline tests read the non-workspace - # context root alongside their plugin task dependencies. - "@hashintel/brunch-agent": [ - ".config/oxlint/brunch", - "libs/@hashintel/brunch-agent/AGENTS.md", - "libs/@hashintel/brunch-agent/CONTEXT.md", - "libs/@hashintel/brunch-agent/docs", - "libs/@hashintel/brunch-agent/evaluations", - "libs/@hashintel/brunch-agent/scripts", - ], - # The app's tests execute evaluation runners and govern the complete Brunch - # composition. Its context root is not a workspace, so copy the docs, - # scripts, and agent contract files explicitly. + "@hashintel/brunch-agent": [".config/oxlint/brunch"], + # The app's product tests execute evaluation runners and inspect committed + # evidence. Those non-workspace inputs are copied explicitly. "@apps/brunch-agent": [ ".config/oxlint/brunch", - "libs/@hashintel/brunch-agent/AGENTS.md", - "libs/@hashintel/brunch-agent/CONTEXT.md", "libs/@hashintel/brunch-agent/docs", "libs/@hashintel/brunch-agent/evaluations", - "libs/@hashintel/brunch-agent/scripts", "libs/@hashintel/petrinaut/docs", ], } diff --git a/.github/actions/prune-repository/prune_test.py b/.github/actions/prune-repository/prune_test.py index 1e895793f25..24e4e17b647 100644 --- a/.github/actions/prune-repository/prune_test.py +++ b/.github/actions/prune-repository/prune_test.py @@ -15,8 +15,6 @@ CORE = "@hashintel/brunch-agent" TRANSPORT = "@hashintel/brunch-agent-transport-aisdk" APP = "@apps/brunch-agent" -PLUGIN_GHERKIN = "@hashintel/brunch-agent-plugin-gherkin" -PLUGIN_SDCPN = "@hashintel/brunch-agent-plugin-sdcpn" WEBSITE = "@apps/petrinaut-website" FRONTEND = "@apps/hash-frontend" PLAYWRIGHT = "@tests/hash-playwright" @@ -45,58 +43,22 @@ def test_frontend_transitive(self) -> None: class BrunchRequestedExtras(unittest.TestCase): - def test_core_adds_the_app_and_plugins(self) -> None: - expected_workspaces = frozenset({APP, PLUGIN_GHERKIN, PLUGIN_SDCPN}) - self.assertEqual(extras_for_requested({CORE}), expected_workspaces) - self.assertTrue( - expected_workspaces.issubset( - fixpoint_expand( - {CORE} | extras_for_requested({CORE}), - { - CORE: frozenset(), - APP: frozenset({CORE}), - PLUGIN_GHERKIN: frozenset({CORE}), - PLUGIN_SDCPN: frozenset({CORE}), - }, - ) - ) - ) - - def test_app_task_adds_the_core_plugins_and_context_paths(self) -> None: - expected_workspaces = frozenset({CORE, PLUGIN_GHERKIN, PLUGIN_SDCPN}) - expanded = fixpoint_expand( - {APP}, - { - APP: expected_workspaces, - PLUGIN_GHERKIN: frozenset({CORE}), - PLUGIN_SDCPN: frozenset({CORE}), - }, - ) - self.assertTrue(expected_workspaces.issubset(expanded)) + def test_app_keeps_only_its_non_workspace_product_inputs(self) -> None: self.assertEqual( extra_paths_for_requested({APP}), [ ".config/oxlint/brunch", - "libs/@hashintel/brunch-agent/AGENTS.md", - "libs/@hashintel/brunch-agent/CONTEXT.md", "libs/@hashintel/brunch-agent/docs", "libs/@hashintel/brunch-agent/evaluations", - "libs/@hashintel/brunch-agent/scripts", "libs/@hashintel/petrinaut/docs", ], ) - def test_core_adds_its_non_workspace_test_fixtures(self) -> None: + def test_core_adds_only_its_shared_lint_config(self) -> None: + self.assertEqual(extras_for_requested({CORE}), frozenset()) self.assertEqual( extra_paths_for_requested({CORE}), - [ - ".config/oxlint/brunch", - "libs/@hashintel/brunch-agent/AGENTS.md", - "libs/@hashintel/brunch-agent/CONTEXT.md", - "libs/@hashintel/brunch-agent/docs", - "libs/@hashintel/brunch-agent/evaluations", - "libs/@hashintel/brunch-agent/scripts", - ], + [".config/oxlint/brunch"], ) def test_sibling_or_website_job_does_not_add_context_paths(self) -> None: diff --git a/apps/brunch-agent/docs/task-dependencies.json b/apps/brunch-agent/docs/task-dependencies.json index 1f3bc60e5fa..f85e288f2a2 100644 --- a/apps/brunch-agent/docs/task-dependencies.json +++ b/apps/brunch-agent/docs/task-dependencies.json @@ -75,8 +75,6 @@ "test:unit": [ "@hashintel/brunch-agent#build", "@hashintel/brunch-agent-binding-flue#build", - "@hashintel/brunch-agent-plugin-dafny#build", - "@hashintel/brunch-agent-plugin-gherkin#build", "@hashintel/brunch-agent-plugin-sdcpn#build", "@hashintel/brunch-agent-transport-aisdk#build", "@hashintel/petrinaut-core#build", diff --git a/apps/brunch-agent/test/architecture/boundaries.integration.ts b/apps/brunch-agent/test/architecture/boundaries.integration.ts deleted file mode 100644 index 8bd4e9aed0e..00000000000 --- a/apps/brunch-agent/test/architecture/boundaries.integration.ts +++ /dev/null @@ -1,503 +0,0 @@ -/** - * The architectural boundaries, as tests rather than as documentation. - * - * Spec §4 and §12.2 state the dependency direction as invariants; an invariant - * nobody can run is a wish. These are the mechanical checks — they read the - * real tree, so a package added later is governed without opting in. - * - * Two of them are load-bearing beyond tidiness, because the Flue build is - * silent about the failure: a `'use agent'` directive that is not the file's - * first statement builds green and simply never registers the agent. - */ - -import { readFileSync } from "node:fs"; -import { join } from "node:path"; - -import { describe, expect, test } from "vitest"; - -import { - AGENT_DIRECTIVE_STATEMENT, - agentModules, - allDependencies, - importedPackages, - MODEL_KEY_NAME, - packageOf, - pinnedIdentities, - runtimeDependencies, - sourceFiles, - testFiles, - workspacePackages, - type WorkspacePackage, -} from "./workspace"; - -const PACKAGES = workspacePackages(); - -const CORE = "@hashintel/brunch-agent"; -/** Flue is the selected agent runtime; lower-level Pi packages remain binding/test concerns. */ -const SUBSTRATE_SCOPES = ["@flue/", "@earendil-works/"]; -const FLUE_RUNTIME = "@flue/runtime"; - -const isSubstrate = (name: string): boolean => - SUBSTRATE_SCOPES.some((scope) => name.startsWith(scope)); -const byRole = (role: string): WorkspacePackage[] => - PACKAGES.filter((pkg) => pkg.dir.startsWith(`${role}-`)); - -test("every package is actually scanned", () => { - // Without this, a package the file walker misses passes every file-level - // invariant vacuously — the substrate-import ban, the plugin-resolves-core - // rule, and the schema-library ban would all iterate an empty list and go - // green. A silent exemption is worse than no check at all. - for (const pkg of PACKAGES) { - expect({ pkg: pkg.relPath, scanned: sourceFiles(pkg).length > 0 }).toEqual({ - pkg: pkg.relPath, - scanned: true, - }); - } -}); - -describe("role prefixes name what a package is architecturally (spec §12.2)", () => { - test("every package under packages/ is core or carries a role prefix", () => { - for (const pkg of PACKAGES.filter((p) => p.kind === "package")) { - expect(pkg.dir).toMatch( - /^(core|plugin-[a-z0-9-]+|binding-[a-z0-9-]+|transport-[a-z0-9-]+)$/, - ); - } - }); - - test("no package uses an avoided role noun", () => { - // The glossary's own noun is `binding`; `adapter-*` and `wrapper-*` are - // avoided terms, and `elicit-*` names function rather than identity. - for (const pkg of PACKAGES) { - expect(pkg.dir).not.toMatch(/^(adapter|wrapper|elicit)-/); - } - }); - - test("the manifest name matches the role-prefixed directory", () => { - for (const pkg of PACKAGES) { - const expectedName = - pkg.kind === "app" - ? "@apps/brunch-agent" - : pkg.dir === "core" - ? CORE - : `@hashintel/brunch-agent-${pkg.dir}`; - expect(pkg.name).toBe(expectedName); - } - }); -}); - -describe("dependency direction", () => { - test("core's only agent-runtime dependency is Flue", () => { - const core = PACKAGES.find((pkg) => pkg.name === CORE); - expect(core).toBeDefined(); - expect(allDependencies(core!).filter(isSubstrate)).toEqual([FLUE_RUNTIME]); - for (const file of sourceFiles(core!)) { - const substrateImports = importedPackages(file).filter((specifier) => - isSubstrate(packageOf(specifier)), - ); - expect({ file: file.relPath, substrateImports }).toEqual({ - file: file.relPath, - substrateImports: - file.relPath.endsWith("/src/flue.ts") || - file.relPath.endsWith("/src/skills/skill-markdown.ts") - ? [FLUE_RUNTIME] - : [], - }); - } - }); - - test("the harness depends on no binding and no plugin", () => { - const core = PACKAGES.find((pkg) => pkg.name === CORE)!; - for (const dependency of runtimeDependencies(core)) { - expect(dependency).not.toMatch( - /^@hashintel\/brunch-agent-(binding|plugin)-/u, - ); - } - }); - - test("plugins depend inward on core and may contribute through Flue directly", () => { - const plugins = byRole("plugin"); - expect(plugins.length).toBeGreaterThan(0); - for (const plugin of plugins) { - const workspaceDeps = runtimeDependencies(plugin).filter((dependency) => - dependency.startsWith("@hashintel/brunch-agent"), - ); - expect(workspaceDeps).toEqual([CORE]); - expect( - allDependencies(plugin).filter( - (dependency) => - isSubstrate(dependency) && dependency !== FLUE_RUNTIME, - ), - ).toEqual([]); - - for (const file of sourceFiles(plugin)) { - for (const specifier of importedPackages(file)) { - const pkg = packageOf(specifier); - expect(isSubstrate(pkg) && pkg !== FLUE_RUNTIME).toBe(false); - expect(pkg.startsWith("@hashintel/brunch-agent") ? pkg : CORE).toBe( - CORE, - ); - expect(specifier).not.toBe(`${CORE}/storage`); - } - } - } - }); - - test("a binding imports both", () => { - const bindings = byRole("binding"); - expect(bindings.length).toBeGreaterThan(0); - for (const binding of bindings) { - const deps = runtimeDependencies(binding); - expect(deps).toContain(CORE); - expect(deps.some(isSubstrate)).toBe(true); - } - }); - - test("bindings depend on no plugin — the harness discovers plugins, not the substrate", () => { - for (const binding of byRole("binding")) { - for (const dependency of runtimeDependencies(binding)) { - expect(dependency).not.toMatch(/^@hashintel\/brunch-agent-plugin-/u); - } - } - }); - - test("transports consume their wire encoder and the public Flue client only — never core, a binding, or the runtime", () => { - const transports = byRole("transport"); - expect(transports.length).toBeGreaterThan(0); - for (const transport of transports) { - expect(runtimeDependencies(transport).sort()).toEqual([ - "@flue/sdk", - "ai", - ]); - for (const file of sourceFiles(transport).filter((file) => - file.path.startsWith(join(transport.path, "src")), - )) { - for (const specifier of importedPackages(file)) { - if (specifier.startsWith("node:")) continue; - expect(["@flue/sdk", "ai"]).toContain(packageOf(specifier)); - } - } - } - }); -}); - -describe("the direction is enforced under HASH's linker", () => { - // HASH may hoist dependencies, so physical resolution is not an authority: - // an undeclared package can be resolvable by accident. Runtime manifests and - // authored imports are the stable boundary surfaces. - test("every imported Brunch workspace is a declared runtime dependency", () => { - for (const pkg of PACKAGES) { - const declared = runtimeDependencies(pkg); - for (const file of sourceFiles(pkg)) { - const importedWorkspaces = importedPackages(file) - .map(packageOf) - .filter( - (imported) => - imported === CORE || - imported.startsWith("@hashintel/brunch-agent-"), - ); - for (const imported of importedWorkspaces) { - expect({ - file: file.relPath, - imported, - declared: declared.includes(imported), - }).toEqual({ - file: file.relPath, - imported, - declared: true, - }); - } - } - } - }); -}); - -describe("Valibot is the schema library at every boundary (spec §12.4)", () => { - // Flue locks Valibot at every boundary. A Standard-Schema waist would buy - // comfort at the cost of a conversion seam that can silently drop - // constraints — the silent-coercion smell. - const OTHER_SCHEMA_LIBRARIES = [ - "zod", - "yup", - "joi", - "ajv", - "superstruct", - "arktype", - "io-ts", - "runtypes", - "@sinclair/typebox", - "@standard-schema/spec", - "@standard-schema/utils", - ]; - - test("no package declares another schema library", () => { - for (const pkg of PACKAGES) { - for (const forbidden of OTHER_SCHEMA_LIBRARIES) { - expect(allDependencies(pkg)).not.toContain(forbidden); - } - } - }); - - test("no source file imports another schema library", () => { - for (const pkg of PACKAGES) { - for (const file of sourceFiles(pkg)) { - for (const specifier of importedPackages(file)) { - expect(OTHER_SCHEMA_LIBRARIES).not.toContain(packageOf(specifier)); - } - } - } - }); - - test("a package declares valibot exactly when its source imports it", () => { - for (const pkg of PACKAGES) { - const importsValibot = sourceFiles(pkg).some((file) => - importedPackages(file).some( - (specifier) => packageOf(specifier) === "valibot", - ), - ); - const declaresValibot = allDependencies(pkg).includes("valibot"); - expect({ pkg: pkg.relPath, declaresValibot }).toEqual({ - pkg: pkg.relPath, - declaresValibot: importsValibot, - }); - } - }); -}); - -describe("recorded Flue constraints hold by construction (spec §10)", () => { - const dev = PACKAGES.find((pkg) => pkg.relPath === "apps/brunch-agent")!; - // Statement-anchored detection (see workspace.ts): a comment mentioning the - // directive is not an agent module, but a *misplaced* directive still is — - // so the first-statement test below sees it and goes red. - const devAgentModules = agentModules(dev); - - test("the dev app has at least one agent module", () => { - expect(devAgentModules.length).toBeGreaterThan(0); - }); - - test("'use agent' is the module's first statement", () => { - // The build does NOT catch this: a misplaced directive builds green and - // the module simply stops being an agent. Nothing else would notice until - // a conversation failed to start. - for (const file of devAgentModules) { - // Comments are not statements, so a leading doc block is legal and must - // not read as a violation. - const withoutLeadingComments = file.text - .replace(/^/, "") - .replace(/^(?:\s*(?:\/\/[^\n]*|\/\*[\s\S]*?\*\/))*\s*/, ""); - // Judged by the same pattern that detects the directive at all - // (workspace.ts), so every form the lexicon declares legal — either - // quote style, optional semicolon, trailing comment — passes and only - // placement can fail. The old literal comparison spuriously failed - // forms this suite's own fixtures assert are legal. - const firstStatement = withoutLeadingComments.split("\n")[0] ?? ""; - expect({ - file: file.relPath, - firstStatementIsDirective: - AGENT_DIRECTIVE_STATEMENT.test(firstStatement), - }).toEqual({ file: file.relPath, firstStatementIsDirective: true }); - } - }); - - test("agentName is a pinned string literal", () => { - // Conversation storage keys on it, so a computed value is unsupportable - // and a changed value orphans every existing conversation. Extraction - // goes through the shared pinnedIdentities pattern — a third hand-copied - // regex here could silently disagree with the other suites on what - // counts as an identity — and this test adds the shape constraint. - for (const file of devAgentModules) { - const identities = pinnedIdentities(file); - expect({ file: file.relPath, pinned: identities.length > 0 }).toEqual({ - file: file.relPath, - pinned: true, - }); - for (const identity of identities) { - expect(identity).toMatch(/^[a-z][a-z0-9-]*$/); - } - } - }); - - test("a pinned identity appears only in its own agent module", () => { - // The mount path (and anything else naming the agent) must derive from - // `agentName`, not copy it: a duplicated literal is the seam the FE-1361 - // review verified — a copy-pasted second agent shadows the mount while - // every test stays green, because nothing ties the copies together. - const identities = devAgentModules.flatMap((file) => - pinnedIdentities(file).map((identity) => ({ - identity, - pinnedIn: file.relPath, - })), - ); - expect(identities.length).toBeGreaterThan(0); - // Quoted occurrences only: prose in a comment may *name* the agent without - // duplicating its identity into anything the runtime reads — flagging that - // would be the cry-wolf failure this ticket removed from the directive - // check. A string literal carrying the identity anywhere else is real - // duplication, including inside a longer path like '/agents/…'. - const STRING_LITERAL = /(['"`])(?:\\.|(?!\1)[^\\\n])*\1/g; - for (const { identity, pinnedIn } of identities) { - const duplicatedIn = sourceFiles(dev) - .filter((file) => file.relPath !== pinnedIn) - .filter((file) => - (file.text.match(STRING_LITERAL) ?? []).some((literal) => - literal.includes(identity), - ), - ) - .map((file) => file.relPath); - expect({ identity, duplicatedIn }).toEqual({ - identity, - duplicatedIn: [], - }); - } - }); - - test("vite is pinned to 8, which @flue/vite requires", () => { - const pinned = PACKAGES.flatMap((pkg) => { - const range = - pkg.manifest.devDependencies?.vite ?? pkg.manifest.dependencies?.vite; - return range ? [range] : []; - }); - expect(pinned.length).toBeGreaterThan(0); - for (const range of pinned) expect(range).toMatch(/^\^?8(\.|$)/); - }); - - test("the dev app owns the agent module, the mount, and the conversation store", () => { - // Every host authors its own thin `'use agent'` module, `app.ts` and - // `db.ts` — Flue's build-time scan makes shipping a pre-registered agent - // from a library structurally unavailable (spec §12.1). - for (const file of ["src/app.ts", "src/db.ts"]) { - expect(() => readFileSync(join(dev.path, file), "utf8")).not.toThrow(); - } - }); - - test("runtime entrypoints never select Flue's Bun adapter", () => { - for (const pkg of PACKAGES) { - for (const file of [...sourceFiles(pkg), ...testFiles(pkg)]) { - expect(importedPackages(file)).not.toContain("@flue/runtime/bun"); - } - } - }); -}); - -describe("core auxiliary subpaths stay in their assigned lanes", () => { - test("core exposes Flue composition, browser, storage, and workpiece contracts as explicit subpaths", () => { - const core = PACKAGES.find((pkg) => pkg.name === CORE)!; - expect(Object.keys(core.manifest.exports ?? {})).toEqual([ - ".", - "./client-tools", - "./flue", - "./question-marker", - "./storage", - "./workpiece", - ]); - }); - - test("only bindings import core/storage", () => { - for (const pkg of PACKAGES.filter( - (candidate) => !candidate.dir.startsWith("binding-"), - )) { - for (const file of sourceFiles(pkg)) { - expect(importedPackages(file)).not.toContain(`${CORE}/storage`); - } - } - }); -}); - -describe("the HASH smoke is runnable without a model key or a network (spec §12.5)", () => { - test("every Brunch workspace exposes HASH lint, typecheck, and unit-test tasks", () => { - for (const pkg of PACKAGES) { - expect(typeof pkg.manifest.scripts?.["lint:eslint"]).toBe("string"); - expect(typeof pkg.manifest.scripts?.["lint:tsc"]).toBe("string"); - expect(pkg.manifest.scripts?.["test:unit"]).toContain("vitest run"); - } - }); - - /** Every Brunch test file Turbo can reach through the workspace tasks. */ - const suite = PACKAGES.flatMap((pkg) => testFiles(pkg)); - - /** - * The test files permitted to import a substrate package, each reviewed once - * and recorded here with what makes it hermetic. - * - * Declared data, not a claim a file makes about itself. The previous gate - * admitted any `*.integration.ts` containing the string - * `hermetic-substrate-test: faux-provider`, so any new file granted itself - * substrate access by copying a comment — the FE-1389 deep read's finding. A - * path enters here by review only. - */ - const SUBSTRATE_INTEGRATION_ENTRY_POINTS: Readonly> = { - "libs/@hashintel/brunch-agent/packages/core/test/question-marker.test.ts": - "Types the Flue logger and calls the core marker tool with a mocked data-part writer and logger; no runtime boot, provider, key or socket.", - "apps/brunch-agent/test/brunch-turn.test.ts": - "Types Flue's client, admission, and conversation snapshot and constructs FlueExecutionError so the persona bridge can be unit-tested against a stubbed client — no provider key, no socket, no model call, no runtime boot.", - "apps/brunch-agent/test/flue-transcript.test.ts": - "Types Flue's public conversation snapshot so the transcript projector can be unit-tested; the import is type-only — no provider key, no socket, no model call, no runtime boot.", - "apps/brunch-agent/test/petrinaut-chat.integration.ts": - "Boots the plain Flue chat agent on Flue's node runtime with pi-ai's faux provider, drives the browser ChatTransport against the mounted Flue route over app.fetch, and proves streamed reasoning/text, server tools, client-tool resume, SDK history ownership, SQLite restart, and harness-side idempotent apply-sweep — no provider key, no socket, no extraction model call. Run as a child process by petrinaut-chat.test.ts.", - "apps/brunch-agent/test/prepared-workpiece.integration.ts": - "Boots the built Flue ChatAgent with pi-ai's faux provider, creates a prepared fixture through one tagged public signal with fixture-scoped initial data, retries its deterministic idempotency key, and proves prepared/model workpiece selection from canonical history — no provider key, socket, or network model call.", - "apps/brunch-agent/test/proof-artifacts.test.ts": - "Types Flue's public conversation snapshot so canonical trace derivation, workpiece binding, and atomic evidence retention can be unit-tested against an in-memory fixture — no provider key, no socket, no model call, no runtime boot.", - "apps/brunch-agent/test/runbook-artifacts.test.ts": - "Types Flue's public conversation snapshot so runbook artifact recovery can be unit-tested; the import is type-only — no provider key, no socket, no model call, no runtime boot.", - "apps/brunch-agent/test/runbook-elicitation-faux-provider.ts": - "Defines the scripted pi-ai faux provider loaded only by the hermetic prospective-runner test — no provider key, no socket, and no network model call.", - "apps/brunch-agent/test/runbook-headless.integration.ts": - "Boots the built Flue ChatAgent with pi-ai's faux provider and a headless Petrinaut client to prove validated construct-only tool flow without a provider key, socket, or network model call.", - "apps/brunch-agent/test/telemetry.test.ts": - "Constructs Flue's content-free OpenTelemetry instrumentation with an injected exporter setup to prove disposal order; it registers no global instrumentation, opens no socket, and makes no provider call.", - "apps/brunch-agent/test/workpiece.test.ts": - "Types Flue's public conversation snapshot so the substrate-neutral workpiece selector and app-owned SHA-256 projection can be unit-tested against in-memory messages — no provider key, no socket, no model call, no runtime boot.", - "libs/@hashintel/brunch-agent/packages/transport-aisdk/test/chat-transport.test.ts": - "Types a stubbed public Flue client and stream chunks to prove finite AI SDK projection and client-tool signal admission — no runtime boot, provider key, socket, or model call.", - "libs/@hashintel/brunch-agent/packages/transport-aisdk/test/transcript.test.ts": - "Types Flue's public conversation snapshot so history can be projected into AI SDK messages without a runtime boot, provider key, socket, or model call.", - "libs/@hashintel/brunch-agent/packages/transport-aisdk/test/ui-stream.test.ts": - "Types Flue conversation-stream chunks so the finite AI SDK projector can be unit-tested without a runtime boot, provider key, socket, or model call.", - }; - - test("no test file carries a live model credential", () => { - // The spec names an optional secret-gated real-model `flue run` smoke; it - // is deliberately not part of this run, and this is what stops it drifting - // in unnoticed. - expect(suite.length).toBeGreaterThan(0); - // Composed in workspace.ts rather than written literally, so this check - // does not flag its own source or the pattern's. - const modelKey = new RegExp(MODEL_KEY_NAME, "g"); - for (const file of suite) { - expect({ - file: file.relPath, - keys: file.text.match(modelKey) ?? [], - }).toEqual({ - file: file.relPath, - keys: [], - }); - } - }); - - test("the substrate is imported by exactly the reviewed entry points", () => { - // Set equality in both directions. An unlisted importer is substrate - // access nobody reviewed; a listed file that no longer imports the - // substrate is a permission standing for nothing, which is how an - // inventory stops describing the tree it governs. - const importers = suite - .filter((file) => - importedPackages(file).some((s) => isSubstrate(packageOf(s))), - ) - .map((file) => file.relPath) - .sort(); - expect(importers).toEqual( - Object.keys(SUBSTRATE_INTEGRATION_ENTRY_POINTS).sort(), - ); - }); - - test("every reviewed entry point records what makes it hermetic", () => { - for (const [path, review] of Object.entries( - SUBSTRATE_INTEGRATION_ENTRY_POINTS, - )) { - expect({ path, reviewed: review.trim().length > 0 }).toEqual({ - path, - reviewed: true, - }); - } - }); -}); diff --git a/apps/brunch-agent/test/architecture/boundaries.test.ts b/apps/brunch-agent/test/architecture/boundaries.test.ts deleted file mode 100644 index 712624bb2b6..00000000000 --- a/apps/brunch-agent/test/architecture/boundaries.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Keep the filesystem-wide architecture suite runnable by Vitest while its - * implementation remains a non-test entry point for boundary self-inspection. - */ -import "./boundaries.integration.ts"; diff --git a/apps/brunch-agent/test/architecture/workspace.test.ts b/apps/brunch-agent/test/architecture/workspace.test.ts deleted file mode 100644 index ffc9df70dac..00000000000 --- a/apps/brunch-agent/test/architecture/workspace.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * The introspection helpers themselves, at the seams where a wrong answer - * makes some other invariant pass vacuously or fail spuriously. - * - * Agent-module detection is the load-bearing one: the FE-1361 review found the - * old bare substring match turned any file whose *comments* mention the - * `'use agent'` directive into an "agent module", failing two assertions on a - * comment-only change. Detection is now anchored to a directive *statement* — - * and deliberately not to the first statement only, so a misplaced directive - * is still detected and then failed by the first-statement invariant instead - * of escaping it. - */ - -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { basename, join, relative } from "node:path"; - -import { afterAll, describe, expect, test } from "vitest"; - -import { - isAgentModule, - sourceFiles, - testFiles, - type SourceFile, - type WorkspacePackage, -} from "./workspace"; - -const file = (text: string): SourceFile => ({ - path: "/fake/module.ts", - relPath: "fake/module.ts", - text, -}); - -describe("isAgentModule", () => { - test("a module whose first statement is the directive is one", () => { - expect(isAgentModule(file("'use agent';\nexport function A() {}\n"))).toBe( - true, - ); - expect(isAgentModule(file('"use agent";\nexport function A() {}\n'))).toBe( - true, - ); - }); - - test("a leading doc block before the directive still counts", () => { - expect( - isAgentModule(file("/** The agent. */\n'use agent';\nexport {};\n")), - ).toBe(true); - }); - - test("a comment mentioning the directive is not a directive", () => { - // The regression the FE-1361 review verified: CI went red on a - // comment-only change because detection matched raw file text. - expect( - isAgentModule( - file("// the 'use agent' directive must come first\nexport {};\n"), - ), - ).toBe(false); - expect( - isAgentModule( - file( - '/**\n * A file about the "use agent" directive.\n */\nexport {};\n', - ), - ), - ).toBe(false); - }); - - test("a trailing comment on the directive line does not hide the module", () => { - // `'use agent'; // registers X` is still a directive at runtime — comments - // are not statements — so it must still be detected, or every downstream - // invariant passes vacuously for that module. - expect( - isAgentModule( - file("'use agent'; // registers the elicitor\nexport {};\n"), - ), - ).toBe(true); - expect( - isAgentModule(file("'use agent'; /* registered */\nexport {};\n")), - ).toBe(true); - }); - - test("a misplaced directive is still detected, so the first-statement invariant can fail it", () => { - // If detection required the directive to be first, a module that misplaced - // it would silently stop being checked at all — the exact silent pass the - // first-statement test exists to prevent. - expect(isAgentModule(file("import 'x';\n'use agent';\nexport {};\n"))).toBe( - true, - ); - }); - - test("quotes must match", () => { - expect(isAgentModule(file("'use agent\";\nexport {};\n"))).toBe(false); - }); -}); - -describe("the source/test partition is total", () => { - // A real directory rather than fakes, because the property under test is the - // walk itself: the FE-1400 review's verified escape was a file in a *nested* - // test directory (`src/test/`), pruned by the source walk but never collected - // by the old top-level-only test walk — governed by nothing. - const dir = mkdtempSync(join(tmpdir(), "brunch-partition-")); - mkdirSync(join(dir, "src/test"), { recursive: true }); - mkdirSync(join(dir, "__tests__")); - writeFileSync(join(dir, "src/index.ts"), "export {};\n"); - writeFileSync(join(dir, "src/test/nested.ts"), "export {};\n"); - writeFileSync(join(dir, "__tests__/top.test.ts"), "export {};\n"); - const pkg: WorkspacePackage = { - name: "@hashintel/brunch-agent-fixture", - dir: basename(dir), - path: dir, - relPath: `packages/${basename(dir)}`, - kind: "package", - manifest: { name: "@hashintel/brunch-agent-fixture" }, - }; - - afterAll(() => rmSync(dir, { recursive: true, force: true })); - - test("every file is exactly one of source or test — nested test dirs included", () => { - const source = sourceFiles(pkg) - .map((f) => relative(dir, f.path)) - .sort(); - const test = testFiles(pkg) - .map((f) => relative(dir, f.path)) - .sort(); - expect(source).toEqual(["src/index.ts"]); - expect(test).toEqual(["__tests__/top.test.ts", "src/test/nested.ts"]); - }); -}); diff --git a/apps/brunch-agent/test/architecture/workspace.ts b/apps/brunch-agent/test/architecture/workspace.ts deleted file mode 100644 index 35633612110..00000000000 --- a/apps/brunch-agent/test/architecture/workspace.ts +++ /dev/null @@ -1,273 +0,0 @@ -/** - * Workspace introspection for the boundary tests. - * - * The point of reading the tree rather than listing packages by hand is that a - * package added later is governed by the same invariants without anyone - * remembering to opt it in. Two rules keep that promise honest: - * - * - the scan covers a package's whole directory, not a `src/` convention, so a - * package laid out differently cannot fall through every file-level check; - * - a workspace directory that cannot be read is reported, never silently - * skipped into a vacuous pass. - */ - -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; -import { join, relative } from "node:path"; -import { fileURLToPath } from "node:url"; - -/** Resolved through `fileURLToPath` — a raw `URL.pathname` is percent-encoded. */ -export const HASH_ROOT = fileURLToPath( - new URL("../../../..", import.meta.url), -).replace(/[/\\]$/, ""); -export const CONTEXT_ROOT = join(HASH_ROOT, "libs/@hashintel/brunch-agent"); -export const REPO_ROOT = CONTEXT_ROOT; - -/** - * The context root's `docs/` and `scripts/` belong to no workspace, so - * `turbo prune` never copies them; CI's prune-repository action copies them - * into its pruned checkout via a requested-scope extra-path rule. This guard is the - * fallback for a pruned tree where that rule is absent or has drifted: the - * context-root tests skip there instead of failing on missing files, and run - * in every full checkout, where this is always true. - */ -export const contextRootPresent = - existsSync(join(CONTEXT_ROOT, "docs")) && - existsSync(join(CONTEXT_ROOT, "scripts")); -const PACKAGES_ROOT = join(CONTEXT_ROOT, "packages"); -const APP_ROOT = join(HASH_ROOT, "apps/brunch-agent"); - -export interface WorkspacePackage { - /** Package name from its manifest, e.g. `@hashintel/brunch-agent-plugin-gherkin`. */ - readonly name: string; - /** Directory basename — the role-prefixed one, e.g. `plugin-gherkin`. */ - readonly dir: string; - /** Absolute path to the package directory. */ - readonly path: string; - /** Path relative to HASH root. */ - readonly relPath: string; - /** `packages/` holds shells; `apps/` holds hosts. */ - readonly kind: "package" | "app"; - readonly manifest: PackageManifest; -} - -export interface PackageManifest { - name: string; - dependencies?: Record; - devDependencies?: Record; - exports?: Record; - scripts?: Record; -} - -function directoriesIn(parent: string): string[] { - return readdirSync(parent).filter((entry) => - statSync(join(parent, entry)).isDirectory(), - ); -} - -export function workspacePackages(): WorkspacePackage[] { - const packagePaths = directoriesIn(PACKAGES_ROOT).map((dir) => ({ - dir, - kind: "package" as const, - path: join(PACKAGES_ROOT, dir), - })); - const workspacePaths = [ - ...packagePaths, - { dir: "brunch-agent", kind: "app" as const, path: APP_ROOT }, - ]; - - return workspacePaths.map(({ dir, kind, path }) => { - const manifestPath = join(path, "package.json"); - if (!existsSync(manifestPath)) { - throw new Error( - `${relative(HASH_ROOT, path)} has no package.json — every Brunch workspace needs one.`, - ); - } - const manifest = JSON.parse( - readFileSync(manifestPath, "utf8"), - ) as PackageManifest; - return { - name: manifest.name, - dir, - path, - relPath: relative(HASH_ROOT, path).replaceAll("\\", "/"), - kind, - manifest, - }; - }); -} - -/** Every declared dependency of a package, production and dev alike. */ -export function allDependencies(pkg: WorkspacePackage): string[] { - return [ - ...Object.keys(pkg.manifest.dependencies ?? {}), - ...Object.keys(pkg.manifest.devDependencies ?? {}), - ]; -} - -/** Runtime dependencies encode the production package direction. */ -export function runtimeDependencies(pkg: WorkspacePackage): string[] { - return Object.keys(pkg.manifest.dependencies ?? {}); -} - -export interface SourceFile { - readonly path: string; - readonly relPath: string; - readonly text: string; -} - -const SOURCE_EXTENSIONS = /\.(ts|tsx|mts|mjs|js|jsx)$/; -/** Never scanned: not authored here, or build output. */ -const SKIP_DIRECTORIES = ["node_modules", "dist", ".flue", ".git", ".turbo"]; -const TEST_DIRECTORIES = new Set(["test", "tests", "__tests__"]); - -/** Every source file under a directory, recursively. A missing directory yields none. */ -export function filesIn( - dir: string, - skip: readonly string[] = SKIP_DIRECTORIES, -): SourceFile[] { - if (!existsSync(dir)) return []; - const skipped = new Set(skip); - const found: SourceFile[] = []; - const walk = (current: string): void => { - for (const entry of readdirSync(current)) { - if (skipped.has(entry)) continue; - const path = join(current, entry); - if (statSync(path).isDirectory()) walk(path); - else if (SOURCE_EXTENSIONS.test(entry)) { - found.push({ - path, - relPath: relative(HASH_ROOT, path).replaceAll("\\", "/"), - text: readFileSync(path, "utf8"), - }); - } - } - }; - walk(dir); - return found; -} - -/** - * Every file of a package, partitioned into source and test — totally: a file - * under a test directory at any depth is a test file, and every other file is - * source. One walk feeding both sides is what makes the partition total by - * construction. The previous pair of independent walks left a hole (pruning - * test dirs at any depth on one side, collecting only top-level test dirs on - * the other), so a file in e.g. `src/test/` was governed by neither the - * source invariants nor the test-hermeticity rules. - */ -function partitionedFiles(pkg: WorkspacePackage): { - source: SourceFile[]; - test: SourceFile[]; -} { - const source: SourceFile[] = []; - const test: SourceFile[] = []; - for (const file of filesIn(pkg.path)) { - const segments = relative(pkg.path, file.path).split(/[/\\]/); - (segments.some((segment) => TEST_DIRECTORIES.has(segment)) - ? test - : source - ).push(file); - } - return { source, test }; -} - -/** - * A package's own source files — everything it ships, wherever it puts them. - * - * Deliberately not `src/`-only: keying the scan off a directory convention - * would silently exempt a package laid out any other way from every file-level - * invariant, which is the exact failure the boundary suite exists to prevent. - * Tests are excluded because they are governed by their own rules. - */ -export function sourceFiles(pkg: WorkspacePackage): SourceFile[] { - return partitionedFiles(pkg).source; -} - -/** Every test file belonging to a package, at any depth. */ -export function testFiles(pkg: WorkspacePackage): SourceFile[] { - return partitionedFiles(pkg).test; -} - -/** - * The `'use agent'` directive as a statement: alone on its line, terminated, - * quotes matching. Anchoring to a statement rather than matching raw text - * keeps a comment that merely *mentions* the directive from turning a file - * into an agent module (the FE-1361 review's verified failure: CI red on a - * comment-only change). - * - * Deliberately not anchored to the *first* statement: a misplaced directive - * must still be detected, so the first-statement invariant in - * `test/boundaries.test.ts` can fail it loudly instead of never seeing it. - */ -export const AGENT_DIRECTIVE_STATEMENT = - /^\s*(["'])use agent\1;?\s*(?:$|\/\/|\/\*)/mu; - -/** Whether a file declares itself an agent module (well-placed or not). */ -export function isAgentModule(file: SourceFile): boolean { - return AGENT_DIRECTIVE_STATEMENT.test(file.text); -} - -/** - * The pinned identities a file assigns, extracted by the one pattern both - * suites share: `test/boundaries.test.ts` checks each identity is never - * duplicated in source, `test/build-artifact.test.ts` checks each is bound in - * the emitted bundle. Two hand-copies of the regex would let the two checks - * silently diverge on what counts as an identity. - */ -export function pinnedIdentities(file: SourceFile): string[] { - return [...file.text.matchAll(/\w+\.agentName\s*=\s*(["'])([^"']+)\1/gu)].map( - (match) => match[2]!, - ); -} - -/** - * A model-key environment variable name, as a pattern source for callers to - * anchor or extend. Composed rather than written literally, so the hermeticity - * check that reuses it does not flag this file's own source. - */ -export const MODEL_KEY_NAME = `[A-Z_]*${"API"}_${"KEY"}`; - -/** Every agent module a package ships. */ -export function agentModules(pkg: WorkspacePackage): SourceFile[] { - return sourceFiles(pkg).filter(isAgentModule); -} - -/** - * The module specifiers a source file imports or re-exports. - * - * Deliberately textual: the invariants being checked are about which packages - * a shell is *allowed to name at all*, so a regex over the source is the right - * granularity — and it catches type-only imports, which erase at runtime but - * still encode a forbidden dependency direction. - */ -export function importedModules(file: SourceFile): string[] { - const specifiers = new Set(); - const patterns = [ - /(?:^|\n)\s*import\s[^;]*?from\s*['"]([^'"]+)['"]/g, - /(?:^|\n)\s*import\s*['"]([^'"]+)['"]/g, - /(?:^|\n)\s*export\s[^;]*?from\s*['"]([^'"]+)['"]/g, - /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g, - /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g, - ]; - for (const pattern of patterns) { - for (const match of file.text.matchAll(pattern)) { - if (match[1]) specifiers.add(match[1]); - } - } - return [...specifiers]; -} - -/** Bare package specifiers only — relative and absolute paths dropped. */ -export function importedPackages(file: SourceFile): string[] { - return importedModules(file).filter( - (specifier) => !specifier.startsWith(".") && !specifier.startsWith("/"), - ); -} - -/** `@scope/name` or `name` — the installable unit a specifier resolves to. */ -export function packageOf(specifier: string): string { - const parts = specifier.split("/"); - return specifier.startsWith("@") - ? parts.slice(0, 2).join("/") - : (parts[0] ?? specifier); -} diff --git a/apps/brunch-agent/test/build-artifact.test.ts b/apps/brunch-agent/test/build-artifact.test.ts index 1a71004eb07..f7b873b97ae 100644 --- a/apps/brunch-agent/test/build-artifact.test.ts +++ b/apps/brunch-agent/test/build-artifact.test.ts @@ -1,16 +1,13 @@ /** * What the build actually emitted — checked against the artifact, not the source. * - * `test/boundaries.test.ts` catches a misplaced `'use agent'` directive by - * reading the source. This checks the same property from the other end: that - * the agent really is registered in the emitted bundle. The distinction earns - * its keep because the failure mode here is silent — `@flue/vite` drops a - * module that stops looking like an agent module and the build stays green, so - * "it compiled" says nothing about whether the app has any agents in it. + * The failure mode here is silent — `@flue/vite` drops a module that stops + * looking like an agent module and the build stays green, so "it compiled" + * says nothing about whether the app has its agent in the bundle. * - * Any future change that quietly stops an agent, its route, or the conversation + * Any future change that quietly stops the agent, its route, or the conversation * store from reaching the bundle fails here, whatever the cause: a directive - * moved, a config path changed, an entry dropped from the scan glob. + * moved or a config path changed. */ import { existsSync, readdirSync, readFileSync } from "node:fs"; @@ -19,6 +16,7 @@ import { fileURLToPath } from "node:url"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; +import { ChatAgent } from "../src/agents/chat-agent/agent"; import { loadBuiltBrunchApplication } from "../src/evaluations/runbook/load-built-application"; const DEV_APP = fileURLToPath(new URL("..", import.meta.url)).replace( @@ -51,35 +49,13 @@ afterAll(() => { } }); -/** The pinned identity of every agent module in the app, read from source. */ -function declaredAgentIdentities(): string[] { - const agentsDirectory = join(DEV_APP, "src/agents"); - return readdirSync(agentsDirectory, { - recursive: true, - encoding: "utf8", - }) - .filter((entry) => entry.endsWith(".ts")) - .flatMap((entry) => - Array.from( - readFileSync(join(agentsDirectory, entry), "utf8").matchAll( - /\w+\.agentName\s*=\s*(["'])([^"']+)\1/gu, - ), - ), - ) - .map((match) => match[2]!); -} - describe("the emitted server bundle", () => { test("exists", () => { expect(existsSync(DIST)).toBe(true); expect(bundle.length).toBeGreaterThan(0); }); - test("registers every declared agent under its pinned identity", () => { - // The check that matters. A `'use agent'` directive that is not the first - // statement builds green and simply never registers — the app boots with no - // agents and nothing says so until a conversation fails to start. - // + test("registers the chat agent under its pinned identity", () => { // Asserted against the emitted `__flueBindAgentModule(Fn, { identity })` // call rather than the bare string, because the string survives that // failure: the `agentName` assignment is still in the bundle as ordinary @@ -91,14 +67,7 @@ describe("the emitted server bundle", () => { ), ].map((match) => match[1]!), ); - const identities = declaredAgentIdentities(); - expect(identities.length).toBeGreaterThan(0); - for (const identity of identities) { - expect({ identity, bound: bound.has(identity) }).toEqual({ - identity, - bound: true, - }); - } + expect(bound.has(ChatAgent.agentName)).toBe(true); }); test("includes the fail-closed production store", () => { diff --git a/apps/brunch-agent/turbo.json b/apps/brunch-agent/turbo.json index d560b99652d..0912a4e7f13 100644 --- a/apps/brunch-agent/turbo.json +++ b/apps/brunch-agent/turbo.json @@ -38,15 +38,14 @@ "codegen", "^build", "@hashintel/brunch-agent#build", - "@hashintel/brunch-agent-plugin-dafny#build", - "@hashintel/brunch-agent-plugin-gherkin#build", "@hashintel/brunch-agent-plugin-sdcpn#build" ], "env": ["TEST_COVERAGE"], "inputs": [ "$TURBO_DEFAULT$", - "$TURBO_ROOT$/libs/@hashintel/brunch-agent/**", - "!$TURBO_ROOT$/libs/@hashintel/brunch-agent/**/{.flue,.turbo,dist,node_modules}/**" + "$TURBO_ROOT$/libs/@hashintel/brunch-agent/docs/**", + "$TURBO_ROOT$/libs/@hashintel/brunch-agent/evaluations/**", + "$TURBO_ROOT$/libs/@hashintel/petrinaut/docs/**" ] } } diff --git a/libs/@hashintel/brunch-agent/MISSION.md b/libs/@hashintel/brunch-agent/MISSION.md index 571858e5b9b..b463d16025a 100644 --- a/libs/@hashintel/brunch-agent/MISSION.md +++ b/libs/@hashintel/brunch-agent/MISSION.md @@ -1,188 +1,120 @@ -# Brunch remote browser-origin policy +# Brunch architecture-check ownership ## Status -**Live as of 2026-09-08** for -[SRE-1042](https://linear.app/hash/issue/SRE-1042/configure-petrinauts-deployment-variables-for-the-brunch-agent-chat) -on `t/sre-1042-allow-wildcard-origins-for-brunch-previews`, cut from `main` after -[FE-1626](https://github.com/hashintel/hash/pull/9583) established the exact-origin allow-list for -`/agents/*`. This file is the branch's sole execution authority. - -Exact origins alone do not fit the deployment: every Petrinaut preview has its own -`https://petrinaut-git-.stage.hash.ai` origin, so the allow-list additionally accepts a -wildcard for exactly one leading host label. CORS governs whether a conforming browser exposes a cross-origin response -to client code; it does not authenticate or restrict non-browser callers, authorize a -conversation, or make public exposure safe by itself. - -## Supplemental FE-1580 settlement follow-up - -**Live as of 2026-09-08** for -[PR #9588](https://github.com/hashintel/hash/pull/9588) on -`kostandin/fe-1580-port-voice-settlement-fixes`, based directly on current -`main` after #9564 and #9537 merged. This supplement preserves the accepted -Voice contract without changing the CORS authority in this file. - -- **Imperative:** semantically port the omitted #9531 commit `9415e1b007`; - release silent Voice ownership and settle completed submissions without - canonical prose. Preserve failed durable Stop errors and remove the stale - browser `brunch_ask` catalogue entry. -- **Throughline:** OpenAI terminal output → session/bridge/controller ownership; - correlated Brunch settlement → next Voice turn; panel Stop rejection → - deferred browser-tool termination; shared browser catalogue → - transport/history. -- **Proof:** donor session/bridge/controller and preview regressions; panel DOM - tests for persistent Stop failure and withheld continuation; catalogue and - fixture tests; focused unit, build, TypeScript, ESLint and formatting checks. - These tests establish local settlement behavior, not paid-provider behavior, - audible latency or a new microphone witness. -- **Constraints:** preserve current `main`'s accepted Voice and CORS joins; no - #9538 grounding, #9550 VAD/interruption, snapshot-overlay or provenance - rollback work, generic interactive tools, obsolete shim, or `brunch_ask` - restoration. -- **Stop or reorient:** stop if the port erases errors, releases unrelated - playback, revives withheld tools, weakens the CORS policy, or disturbs other - work. +**Live as of 2026-09-09** for +[SRE-1010](https://linear.app/hash/issue/SRE-1010/move-brunch-architecture-checks-out-of-core-unit-tests) +on `ln/sre-1010-move-brunch-checks`. This file is the branch's sole execution authority. ## Imperative -Let a deployed Petrinaut website use the Brunch `/agents/*` Flue routes from an explicitly trusted -browser origin while causing browsers to withhold cross-origin access from unlisted origins. Do -this now because the deployed website and Brunch service are separate origins and -[SRE-1042](https://linear.app/hash/issue/SRE-1042/configure-petrinauts-deployment-variables-for-the-brunch-agent-chat) -cannot point the browser at the deployed Brunch route until preflight and response headers work. +Remove static-policy source walkers from Brunch unit tests and let each existing enforcement +mechanism own the contract it can prove: Oxlint owns authored imports, Yarn owns manifests, and +behavioral tests own emitted behavior. Do this now because the SRE-1007 hotfix modeled sibling +packages and the app as inputs to core, creating a reverse `libs -> apps` edge and requiring +downstream trees in a core prune. ## Throughline ```text -Petrinaut browser at one configured exact origin -→ OPTIONS /agents// with requested method and headers -→ route-scoped Hono CORS middleware before ownership middleware -→ 204 preflight carrying the matching origin, GET/POST/OPTIONS, and Flue request headers -→ browser FlueClient GET/POST with x-brunch-principal + x-brunch-conversation -→ existing agentOwnershipGuard and createAgentRouter -→ response exposes the Flue/Durable Streams headers the browser SDK reads +authored dependency-boundary violation +→ the owning workspace's package-local Oxlint configuration +→ an AST-backed lint diagnostic in the existing package lint task + +forbidden manifest dependency +→ the repository's Yarn constraints +→ install and Global constraint lint fail + +change in apps/brunch-agent +→ @apps/brunch-agent#test:unit +→ the built artifact must contain every declared agent registration + +change in Brunch core +→ @hashintel/brunch-agent#test:unit +→ core-local tests and utilities, without context-root, app, or sibling-package inputs ``` -`BRUNCH_CORS_ALLOWED_ORIGINS` is read once at startup as a comma-separated list of HTTP(S) -origins, each either exact or with a wildcard as the whole leading host label in front of a domain -with at least two labels (`https://*.stage.hash.ai`). A wildcard matches exactly one label, like a -wildcard TLS certificate. Parsing trims whitespace, normalizes an optional trailing slash through -`URL.origin`, and deduplicates values. Credentials, non-root paths, queries, fragments, wildcards in -any other position, opaque origins, and non-HTTP(S) schemes are startup configuration errors. Missing or blank configuration means an -empty allowlist: same-origin and non-browser callers continue through the existing route, but -browser code at another origin receives no CORS grant. See the -[Brunch application README](../../../apps/brunch-agent/README.md#production-container) for -operator configuration details. - -The middleware applies only to `/agents/*` and runs before `agentOwnershipGuard`, so a valid -preflight does not need conversation headers. It permits `GET`, `POST`, and `OPTIONS`; permits -`Content-Type`, `x-brunch-principal`, and `x-brunch-conversation`; does not permit credentials; and -uses a 600-second preflight cache. It exposes the non-safelisted response headers read by the -installed Flue 2.0.3 and Durable Streams 0.2.6 clients: - -- `flue-error-ref` -- `Stream-Next-Offset` -- `Stream-Cursor` -- `Stream-Up-To-Date` -- `Stream-Closed` -- `stream-sse-data-encoding` - -Hono's maintained CORS middleware owns header emission, `Vary` handling, and the `OPTIONS` response. -Non-browser callers can still send requests and receive ordinary HTTP responses because CORS is -enforced by browsers, not by the service as caller authentication. A response to an unlisted -browser origin carries no `Access-Control-Allow-Origin`, so the browser withholds that response -from client code. +No replacement family scanner is introduced. Assertions that restate package manifests, filenames, +source strings, review inventories, or tool configuration are deleted when the existing tool, +compiler, build, or review is already authoritative. ## Proof -This mission establishes the application-side CORS contract required by the deployed browser -transport. It does **not** establish authentication, authorization, rate limiting, infrastructure -configuration, a deployed endpoint, or end-to-end remote verification. - -1. **Configuration is exact and fail-closed.** Missing and blank configuration produce no allowed - origins; whitespace, trailing slashes, duplicates, and multiple exact origins normalize - deterministically; malformed or broader-than-origin entries fail with the offending variable - named. Oracle: focused unit cases in `apps/brunch-agent/test/cors.test.ts`. -2. **Allowed browser traffic receives the complete grant.** An allowed origin receives its exact - value on an `/agents/*` response. Its preflight receives 204 before ownership, the three allowed - methods, the three allowed request headers, the six exposed response headers, no credentials - grant, and the required `Vary` values. Oracle: in-process Hono requests in - `apps/brunch-agent/test/cors.test.ts`. -3. **Rejected origins receive no grant.** An unlisted origin's preflight and ordinary response omit - `Access-Control-Allow-Origin`; an allowed origin does not make another origin pass. Oracle: - focused negative cases in `apps/brunch-agent/test/cors.test.ts`. -4. **The policy cannot widen unrelated routes.** `/health`, `/`, and `/assets/*` carry no Brunch - CORS grant. Existing ownership checks still return 401/403 for actual agent requests with - missing or mismatched identity. Oracle: CORS route-scope tests plus the existing - `apps/brunch-agent/test/agent-ownership.test.ts`. -5. **The shipped artifact and operator contract agree.** Brunch's README documents the variable, - exact-origin configuration, empty-list behavior, and the fact that CORS governs browser access - rather than authenticating or restricting non-browser callers. Oracle: - `yarn workspace @apps/brunch-agent test:unit`, - `yarn workspace @apps/brunch-agent lint:tsc`, - `yarn workspace @apps/brunch-agent lint:eslint`, and - `yarn workspace @apps/brunch-agent build`. +This mission establishes native enforcement ownership and prune closure. It does **not** add a new +policy surface merely to preserve every historical assertion. + +1. **Import boundaries use existing lint infrastructure.** Each Brunch workspace's existing + `no-restricted-imports` policy catches forbidden authored imports using Oxlint's parser. Oracle: + representative negative lint probes in the owning packages. +2. **Manifest boundaries use existing repository infrastructure.** The Brunch transport Yarn + constraint continues to reject forbidden runtime edges. Oracle: `yarn constraints` and a + representative negative manifest probe. +3. **Agent registration is behavioral.** The app build-artifact test proves every declared agent + reaches the emitted registration bundle; source-walking directive and filename checks are + removed. Oracle: `yarn workspace @apps/brunch-agent test:unit`. +4. **A core prune owns no downstream or context-root Brunch tree.** The core task has no app, + sibling-package, or context-root input glob, and requesting only core no longer adds them through + prune exceptions. The Linear graph utility and its tests are co-located in core. Oracle: + `.github/actions/prune-repository/prune_test.py` plus inspection of + `turbo run test:unit --filter @hashintel/brunch-agent --dry=json`. +5. **No bespoke checker remains.** The temporary architecture workspace and repo-chores command, + task, scanner, CI step, and coverage constraint are absent. Oracle: repository search plus + relevant package lint, typecheck, tests, constraints, and formatting. ## Constraints -- Use Hono's built-in CORS middleware; do not create a parallel HTTP server or hand-maintain generic - CORS response logic. -- Keep one Flue product route and the existing ownership guard. CORS must not add, proxy, rename, or - reinterpret an agent route. -- The origin list is explicit: exact origins or one-label wildcards, matched by scheme, host and - port. Do not hard-code Petrinaut domains, reflect arbitrary `Origin` values, or silently skip - malformed entries. -- Keep credentials disabled. The current browser client uses explicit ownership headers, not - cookies, and those headers are not authentication. -- Answer preflight before ownership while preserving ownership enforcement on every non-preflight - agent request. -- Read configuration once at startup. Dynamic policy storage or hot reload is not earned by this - deployment. -- Preserve local same-origin proxying when the variable is unset. +- Keep Yarn constraints and package-local Oxlint rules with their current owning packages; do not + duplicate them in tests or repo-chores. +- Preserve behavioral tests that can fail while source and manifests remain unchanged. +- Delete static assertions whose only oracle is a hard-coded mirror of source, package metadata, + review inventory, or file layout. +- Move the Linear graph utility into core so its real unit tests use static imports and never skip + based on checkout shape. +- Never make a governed workspace depend on a checker or recreate a family-wide source walker. - No implementation begins until this authority cut is committed separately. Material changes to this contract require owner review and another focused authority commit. ### Expected touched paths ```text -~ libs/@hashintel/brunch-agent/MISSION.md branch authority -~ apps/brunch-agent/src/http/cors.ts exact and one-label wildcard origins, Hono middleware -~ apps/brunch-agent/src/app.ts mount CORS before ownership on /agents/* -+ apps/brunch-agent/test/cors.test.ts parser, allowed, rejected, preflight, route-scope tests -~ apps/brunch-agent/README.md deployment variable and security boundary -~ apps/brunch-agent/turbo.json pass the variable into the local dev task +~ libs/@hashintel/brunch-agent/MISSION.md +~ apps/brunch-agent/test/build-artifact.test.ts +- apps/brunch-agent/test/architecture/* +> libs/@hashintel/brunch-agent/scripts/linear-project-graph.ts + -> libs/@hashintel/brunch-agent/packages/core/src/linear-project-graph.ts +~ libs/@hashintel/brunch-agent/packages/core/test/architecture/linear-project-graph.test.ts +- libs/@hashintel/brunch-agent/packages/core/test/architecture/context-root.ts +- libs/@hashintel/brunch-agent/packages/core/test/architecture/open-gaps* +~ libs/@hashintel/brunch-agent/packages/core/package.json +~ libs/@hashintel/brunch-agent/packages/core/turbo.json +~ apps/brunch-agent/turbo.json +~ .github/actions/prune-repository/prune.py +~ .github/actions/prune-repository/prune_test.py +~ yarn.lock ``` ## Fog-line -- Infrastructure repository access is unavailable in this worktree, so this branch can prove only - the application contract. Runtime deployment configuration must supply the chosen origins before - remote verification. -- A one-label wildcard admits every host directly under the configured domain, not only Petrinaut - previews. Narrow the deployed pattern or return to exact origins if that breadth becomes a - problem in practice. -- The allowed and exposed headers are pinned to the installed Flue and Durable Streams clients. - Re-evaluate them from client source when either dependency changes. +- If an import boundary lacks package-local lint coverage, extend the owning workspace's Oxlint + rule rather than introducing a cross-workspace scanner. +- Some app unit tests independently read non-workspace Brunch evaluation and documentation assets. + This mission removes family architecture ownership from the app but does not remove inputs those + product tests still genuinely consume. +- The non-blocking review identifies similar tautological tests outside this architecture cluster. + They are evidence of the same defect pattern, but broad product-test cleanup is deferred unless a + touched test blocks this ownership change. ## Stop or reorient -Stop if the real browser client emits a request method or non-safelisted request header outside the -pinned contract, reads another non-safelisted response header, or needs cookie credentials. Bring -that evidence back to the contract before broadening the grant. - -Stop if middleware ordering bypasses ownership for a non-`OPTIONS` request, if an invalid -configuration widens access or is ignored, if an unlisted origin receives -`Access-Control-Allow-Origin`, or if `/health`, `/`, or `/assets/*` inherit the policy. +Stop if deleting a source-walking assertion removes the only enforcement of a security boundary or +observable runtime behavior. Preserve that contract in its native tool before deleting the mirror. -Do not represent a green CORS test as permission for unauthenticated public exposure. Authentication, -per-conversation authorization, rate/spend controls, and the infrastructure ingress boundary remain -separate release gates. +Stop if removing a prune exception makes a non-architecture core or app test lose a genuine +fixture. Preserve that proven dependency narrowly and report it instead of deleting it to satisfy +the desired graph shape. ## Deferred -- SRE-1013 owns injection of the allowlist into the Brunch runtime deployment. SRE-1042 owns - `VITE_BRUNCH_CHAT_ENDPOINT`, Voice deployment variables, and the deployed browser verification - after this application contract lands. -- FE-1615 and FE-1616 retain authentication and rate-limit work. CORS does not discharge either. -- A same-origin Petrinaut proxy stays deferred; the one-label wildcard covers the preview - deployments the exact list could not. +The canonical future planning record remains [`MISSION.next.md`](MISSION.next.md). SRE-1008 owns +repository-wide detection of undeclared Turbo task inputs. The broader non-blocking cleanup of +tautological core tests is deferred. diff --git a/libs/@hashintel/brunch-agent/packages/core/package.json b/libs/@hashintel/brunch-agent/packages/core/package.json index 70593a8eb94..f09a83c0f7a 100644 --- a/libs/@hashintel/brunch-agent/packages/core/package.json +++ b/libs/@hashintel/brunch-agent/packages/core/package.json @@ -34,7 +34,7 @@ "scripts": { "build": "vite build", "fix:eslint": "oxlint --fix --type-aware --type-check --report-unused-disable-directives-severity=error .", - "linear:graph": "node --experimental-strip-types ../../scripts/linear-project-graph.ts", + "linear:graph": "node --experimental-strip-types src/linear-project-graph.ts", "lint:eslint": "oxlint --type-aware --type-check --report-unused-disable-directives-severity=error .", "lint:tsc": "tsgo --noEmit", "test:unit": "vitest run" diff --git a/libs/@hashintel/brunch-agent/scripts/linear-project-graph.ts b/libs/@hashintel/brunch-agent/packages/core/src/linear-project-graph.ts similarity index 98% rename from libs/@hashintel/brunch-agent/scripts/linear-project-graph.ts rename to libs/@hashintel/brunch-agent/packages/core/src/linear-project-graph.ts index 7d8ee640ff7..57b8f88f3dd 100644 --- a/libs/@hashintel/brunch-agent/scripts/linear-project-graph.ts +++ b/libs/@hashintel/brunch-agent/packages/core/src/linear-project-graph.ts @@ -44,7 +44,7 @@ interface LinearRelation { readonly relatedIssue?: LinearIssueRef; } -interface LinearIssueRecord extends LinearIssueRef { +export interface LinearIssueRecord extends LinearIssueRef { readonly parent: { readonly identifier: string } | null; readonly relations: { readonly pageInfo: { readonly hasNextPage: boolean }; @@ -56,7 +56,7 @@ interface LinearIssueRecord extends LinearIssueRef { }; } -interface ProjectIssuePage { +export interface ProjectIssuePage { readonly projectName: string; readonly viewer: { readonly id: string; readonly name: string }; readonly issues: readonly LinearIssueRecord[]; @@ -569,20 +569,20 @@ if (isMain) { try { const options = parseArguments(process.argv.slice(2)); if (options.help) { - console.log(usage); + process.stdout.write(`${usage}\n`); } else { - console.log( - renderProjectGraph( + process.stdout.write( + `${renderProjectGraph( fetchProjectGraph(options.projectName, options.includeClosed), - ), + )}\n`, ); } } catch (error) { - console.error( - `linear:graph: ${error instanceof Error ? error.message : String(error)}`, + process.stderr.write( + `linear:graph: ${error instanceof Error ? error.message : String(error)}\n`, ); - console.error( - "Run `turbo run linear:graph --filter '@hashintel/brunch-agent' -- --help` for usage.", + process.stderr.write( + "Run `turbo run linear:graph --filter '@hashintel/brunch-agent' -- --help` for usage.\n", ); process.exitCode = 1; } diff --git a/libs/@hashintel/brunch-agent/packages/core/test/architecture/context-root.ts b/libs/@hashintel/brunch-agent/packages/core/test/architecture/context-root.ts deleted file mode 100644 index 4525fc8fbe1..00000000000 --- a/libs/@hashintel/brunch-agent/packages/core/test/architecture/context-root.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { existsSync } from "node:fs"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; - -/** The non-workspace root shared by Brunch packages and evaluation assets. */ -export const CONTEXT_ROOT = fileURLToPath( - new URL("../../../..", import.meta.url), -).replace(/[/\\]$/, ""); - -/** - * Pruned jobs copy these non-workspace paths explicitly. Skip the affected - * tests instead of failing opaquely if that CI contract drifts. - */ -export const contextRootPresent = - existsSync(join(CONTEXT_ROOT, "docs")) && - existsSync(join(CONTEXT_ROOT, "scripts")); diff --git a/libs/@hashintel/brunch-agent/packages/core/test/architecture/linear-project-graph.test.ts b/libs/@hashintel/brunch-agent/packages/core/test/architecture/linear-project-graph.test.ts index 77563d891b0..09658fc5f13 100644 --- a/libs/@hashintel/brunch-agent/packages/core/test/architecture/linear-project-graph.test.ts +++ b/libs/@hashintel/brunch-agent/packages/core/test/architecture/linear-project-graph.test.ts @@ -1,77 +1,14 @@ -import { join } from "node:path"; -import { pathToFileURL } from "node:url"; - import { describe, expect, test } from "vitest"; -import { CONTEXT_ROOT, contextRootPresent } from "./context-root"; - -/** - * The module under test lives at the context root, outside every workspace, so - * CI's pruned checkout lacks it. The specifier is computed so `lint:tsc` does - * not resolve it statically; the structural types below restate the contract - * the tests exercise. - */ -const SCRIPT_MODULE_URL = pathToFileURL( - join(CONTEXT_ROOT, "scripts/linear-project-graph.ts"), -).href; - -interface ProjectIssue { - readonly identifier: string; - readonly title: string; - readonly stateName: string; - readonly parentIdentifier?: string; - readonly assigneeName?: string; - readonly assignedToViewer: boolean; - readonly external: boolean; -} - -interface ProjectGraph { - readonly projectName: string; - readonly viewerName: string; - readonly includeClosed: boolean; - readonly issues: readonly ProjectIssue[]; - readonly hardEdges: readonly { readonly from: string; readonly to: string }[]; -} - -interface LinearProjectGraphModule { - readonly renderProjectGraph: (graph: ProjectGraph) => string; - readonly readProjectIssuePage: (value: unknown) => ProjectIssuePage; - readonly fetchProjectGraph: ( - projectName: string, - includeClosed: boolean, - queryPage: (projectName: string, after: string | null) => ProjectIssuePage, - ) => ProjectGraph; - readonly parseArguments: (arguments_: readonly string[]) => { - readonly projectName: string; - readonly includeClosed: boolean; - readonly help: boolean; - }; -} - -interface ProjectIssuePage { - readonly projectName: string; - readonly viewer: { readonly id: string; readonly name: string }; - readonly issues: readonly LinearIssueRecord[]; - readonly hasNextPage: boolean; - readonly endCursor: string | null; -} - -interface LinearIssueRecord { - readonly identifier: string; - readonly title: string; - readonly state: { readonly name: string; readonly type: string }; - readonly project: { readonly name: string } | null; - readonly assignee?: { readonly id: string; readonly name: string } | null; - readonly parent: { readonly identifier: string } | null; - readonly relations: { - readonly pageInfo: { readonly hasNextPage: boolean }; - readonly nodes: readonly []; - }; - readonly inverseRelations: { - readonly pageInfo: { readonly hasNextPage: boolean }; - readonly nodes: readonly []; - }; -} +import { + fetchProjectGraph, + parseArguments, + readProjectIssuePage, + renderProjectGraph, + type LinearIssueRecord, + type ProjectGraph, + type ProjectIssuePage, +} from "../../src/linear-project-graph"; const issue = ( identifier: string, @@ -108,18 +45,8 @@ const response = ( }, }); -async function loadRenderProjectGraph(): Promise< - LinearProjectGraphModule["renderProjectGraph"] -> { - const module = (await import(SCRIPT_MODULE_URL)) as LinearProjectGraphModule; - return module.renderProjectGraph; -} - -describe.skipIf(!contextRootPresent)("the compact Linear project graph", () => { - test("parses viewer identity separately from its display name", async () => { - const { readProjectIssuePage } = (await import( - SCRIPT_MODULE_URL - )) as LinearProjectGraphModule; +describe("the compact Linear project graph", () => { + test("parses viewer identity separately from its display name", () => { const page = readProjectIssuePage( response({ id: "viewer-id", name: "Same Display Name" }, [ issue("FE-1", { id: "other-id", name: "Same Display Name" }), @@ -130,20 +57,14 @@ describe.skipIf(!contextRootPresent)("the compact Linear project graph", () => { test.each([undefined, null, {}, { id: "", name: "Lu" }])( "rejects missing or malformed viewer: %j", - async (viewer) => { - const { readProjectIssuePage } = (await import( - SCRIPT_MODULE_URL - )) as LinearProjectGraphModule; + (viewer) => { expect(() => readProjectIssuePage(response(viewer))).toThrow( "missing or malformed authenticated viewer", ); }, ); - test("classifies viewer, wrong, unassigned, null, and absent assignees by ID", async () => { - const { readProjectIssuePage, fetchProjectGraph } = (await import( - SCRIPT_MODULE_URL - )) as LinearProjectGraphModule; + test("classifies viewer, wrong, unassigned, null, and absent assignees by ID", () => { const parsed = readProjectIssuePage( response({ id: "viewer-id", name: "Lu" }, [ issue("FE-1", { id: "viewer-id", name: "Lu" }), @@ -167,10 +88,7 @@ describe.skipIf(!contextRootPresent)("the compact Linear project graph", () => { ]); }); - test("accumulates two pages and passes the returned cursor", async () => { - const { fetchProjectGraph } = (await import( - SCRIPT_MODULE_URL - )) as LinearProjectGraphModule; + test("accumulates two pages and passes the returned cursor", () => { const calls: Array = []; const pages: ProjectIssuePage[] = [ { @@ -203,9 +121,7 @@ describe.skipIf(!contextRootPresent)("the compact Linear project graph", () => { ]); }); - test("defaults to open issues and --all includes closed issues", async () => { - const { fetchProjectGraph, parseArguments, renderProjectGraph } = - (await import(SCRIPT_MODULE_URL)) as LinearProjectGraphModule; + test("defaults to open issues and --all includes closed issues", () => { const page: ProjectIssuePage = { projectName: "brunch-agent", viewer: { id: "viewer-id", name: "Lu" }, @@ -230,8 +146,7 @@ describe.skipIf(!contextRootPresent)("the compact Linear project graph", () => { ); }); - test("renders hard-dependency layers with enough issue context for agent inference", async () => { - const renderProjectGraph = await loadRenderProjectGraph(); + test("renders hard-dependency layers with enough issue context for agent inference", () => { const graph: ProjectGraph = { projectName: "brunch-agent", viewerName: "Lu Nelson", @@ -288,8 +203,7 @@ L2 FE-103 [Todo p:FE-1 a:self] <=FE-101 | Ship the integration cycles: none`); }); - test("makes a hard-dependency cycle explicit instead of inventing an order", async () => { - const renderProjectGraph = await loadRenderProjectGraph(); + test("makes a hard-dependency cycle explicit instead of inventing an order", () => { const graph: ProjectGraph = { projectName: "brunch-agent", viewerName: "Lu Nelson", diff --git a/libs/@hashintel/brunch-agent/packages/core/test/architecture/open-gaps.test.ts b/libs/@hashintel/brunch-agent/packages/core/test/architecture/open-gaps.test.ts deleted file mode 100644 index b4717bf4040..00000000000 --- a/libs/@hashintel/brunch-agent/packages/core/test/architecture/open-gaps.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Keeps the open gaps in front of whoever is working, and keeps each entry - * well formed enough to act on. - * - * See `open-gaps.ts` for why the ledger exists and why closing a gap is - * deleting its entry rather than satisfying a predicate. - */ - -import { describe, expect, test } from "vitest"; - -import { OPEN_GAPS } from "./open-gaps"; - -// Printed once per run, so the list is in front of whoever is working rather -// than filed somewhere they would have to think to look. Silent when the ledger -// is empty, because that is the goal state and not a warning. -if (OPEN_GAPS.length > 0) { - process.stderr.write( - [ - "", - `⚠ ${OPEN_GAPS.length} verification gaps are open (spec §14.5 and friends):`, - ...OPEN_GAPS.map( - (entry) => ` · ${entry.id} — ${entry.ticket} (spec ${entry.spec})`, - ), - " Closing one means deleting its entry in the commit that lands its proof.", - "", - ].join("\n"), - ); -} - -describe("open gaps", () => { - test("every entry names its ticket, spec section, gap, and the proof that will delete it", () => { - for (const entry of OPEN_GAPS) { - // The entry id is pinned on both sides so a failure says which record is - // malformed even when the malformed field is the id itself. - expect({ - entry: entry.id, - id: entry.id, - ticket: entry.ticket, - spec: entry.spec, - statesGap: entry.gap.trim().length > 0, - statesProof: entry.proof.trim().length > 0, - }).toEqual({ - entry: entry.id, - id: expect.stringMatching( - /^[a-z0-9]+(?:-[a-z0-9]+)*$/, - ) as unknown as string, - ticket: expect.stringMatching(/^[A-Z]+-\d+$/) as unknown as string, - spec: expect.stringMatching( - /^§\d+(?:\.\d+)*(?:, §\d+(?:\.\d+)*)*$/, - ) as unknown as string, - statesGap: true, - statesProof: true, - }); - } - }); - - test("gap ids are unique", () => { - const ids = OPEN_GAPS.map((entry) => entry.id); - expect(new Set(ids).size).toBe(ids.length); - }); -}); diff --git a/libs/@hashintel/brunch-agent/packages/core/test/architecture/open-gaps.ts b/libs/@hashintel/brunch-agent/packages/core/test/architecture/open-gaps.ts deleted file mode 100644 index bc6de8fc722..00000000000 --- a/libs/@hashintel/brunch-agent/packages/core/test/architecture/open-gaps.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * The verification gaps this codebase knows are open. - * - * Spec §14.5 names open verification items, and prose decays: an item nobody - * runs into is an item nobody closes. The ones here are exactly that kind — a - * substrate behaviour nobody has driven, a durability claim nobody has - * restarted into. - * - * So the ledger is data, and an entry's *existence* is the claim that its gap - * is still open. Closing a gap is an explicit repository action: the commit - * that lands the behavioural proof deletes the entry, so a single diff carries - * the proof and the closure together and a reviewer can check one against the - * other. - * - * Nothing here infers closure from source text, and nothing may be added that - * does. The mechanism this replaces searched the tree for a citation token - * alongside a `test(`/`expect(` in the same file: it passed for a test that - * cited a gap while proving something else, and it went red on any closure - * written outside the shape it guessed. A banner that can be wrong in both - * directions is a banner nobody reads. - */ - -export interface OpenGap { - /** Short stable handle, used in the run banner and on the ticket. */ - readonly id: string; - /** Spec section that names the item. */ - readonly spec: string; - /** The Linear issue that owns closing it. */ - readonly ticket: string; - /** What is not yet known, in one sentence. */ - readonly gap: string; - /** The behavioural proof whose landing commit deletes this entry. */ - readonly proof: string; -} - -export const OPEN_GAPS: readonly OpenGap[] = [ - { - id: "compaction-vs-durable-history", - spec: "§9.7, §14.5", - ticket: "FE-1386", - gap: "Flue 2.0.3 source shows compaction appends to the canonical stream and rewrites model context only, but no session has behaviorally pinned that the public message projection and persistent state survive a real compaction boundary.", - proof: - "A test under libs/@hashintel/brunch-agent/packages/binding-flue/test driving one genuine compaction, deep-comparing complete public messages and settlements aside from offset/incarnation, verifying persistent state, and asserting an FE-1391 archive pointer still resolves.", - }, -]; diff --git a/libs/@hashintel/brunch-agent/packages/core/turbo.json b/libs/@hashintel/brunch-agent/packages/core/turbo.json index 2bde83ff3e6..6623a93364c 100644 --- a/libs/@hashintel/brunch-agent/packages/core/turbo.json +++ b/libs/@hashintel/brunch-agent/packages/core/turbo.json @@ -7,13 +7,6 @@ }, "linear:graph": { "cache": false - }, - "test:unit": { - "inputs": [ - "$TURBO_DEFAULT$", - "$TURBO_ROOT$/libs/@hashintel/brunch-agent/**", - "!$TURBO_ROOT$/libs/@hashintel/brunch-agent/**/{.flue,.turbo,dist,node_modules}/**" - ] } } }