@manta-eu/horns-client 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/component-api.d.ts +7 -0
  2. package/dist/app/coverage-run.js +4 -3
  3. package/dist/app/globals.css +2 -0
  4. package/dist/app/home-page.js +6 -4
  5. package/dist/app/horn-page.js +7 -10
  6. package/dist/app/horn-studio.js +63 -10
  7. package/dist/app/horn-view-page.js +13 -9
  8. package/dist/app/horns-page.js +6 -9
  9. package/dist/app/new-horn-page.js +52 -10
  10. package/dist/app/paths.d.ts +15 -10
  11. package/dist/app/paths.js +18 -29
  12. package/dist/app/plans-page.d.ts +1 -0
  13. package/dist/app/plans-page.js +59 -0
  14. package/dist/app/reference-page.d.ts +1 -0
  15. package/dist/app/reference-page.js +55 -0
  16. package/dist/app/routes.js +7 -5
  17. package/dist/app/rule-page.js +18 -11
  18. package/dist/app/rules-page.js +12 -12
  19. package/dist/app/shape-entity-page.js +7 -9
  20. package/dist/app/shape-zoo-page.js +9 -10
  21. package/dist/app/shapes-page.js +13 -15
  22. package/dist/app/showcase-page.js +8 -11
  23. package/dist/components/app-shell.js +8 -7
  24. package/dist/components/catalog-card-view.js +4 -5
  25. package/dist/components/explain-dialog.d.ts +5 -0
  26. package/dist/components/explain-dialog.js +26 -0
  27. package/dist/components/plan-runs.js +2 -2
  28. package/dist/components/read-the-code.d.ts +19 -0
  29. package/dist/components/read-the-code.js +17 -3
  30. package/dist/index.d.ts +2 -3
  31. package/dist/index.js +2 -3
  32. package/dist/lib/horn-review.d.ts +7 -0
  33. package/dist/lib/horn-review.js +15 -0
  34. package/dist/lib/horn-variables.d.ts +8 -0
  35. package/dist/lib/horn-variables.js +39 -0
  36. package/dist/lib/manta/sign-in.d.ts +2 -0
  37. package/dist/lib/manta/sign-in.js +9 -0
  38. package/dist/lib/manta/sparql-select.d.ts +6 -2
  39. package/dist/lib/manta/sparql-select.js +17 -14
  40. package/dist/lib/reference/fake-sparql.d.ts +4 -0
  41. package/dist/lib/reference/fake-sparql.js +216 -0
  42. package/dist/lib/shapes/ldkit-source.d.ts +1 -1
  43. package/dist/lib/shapes/ldkit-source.js +1 -1
  44. package/dist/lib/studio-progress.js +3 -1
  45. package/dist/lib/surface/declarations.d.ts +5 -0
  46. package/dist/lib/surface/declarations.js +77 -0
  47. package/dist/lib/written-surface.js +13 -2
  48. package/dist/reference/supply-chain-360.horn.json +413 -0
  49. package/dist/standalone/app.d.ts +2 -0
  50. package/dist/standalone/app.js +57 -0
  51. package/dist/standalone/client.d.ts +15 -0
  52. package/dist/{client.js → standalone/client.js} +12 -8
  53. package/dist/standalone/gates.d.ts +10 -0
  54. package/dist/standalone/gates.js +15 -0
  55. package/dist/styles.css +1 -1
  56. package/dist/vite/component-api.d.ts +8 -0
  57. package/dist/vite/component-api.js +24 -0
  58. package/dist/vite/index.d.ts +3 -0
  59. package/dist/vite/index.js +3 -0
  60. package/dist/vite/shape-catalog.d.ts +10 -0
  61. package/dist/vite/shape-catalog.js +35 -0
  62. package/dist/vite/typecheck-fs.d.ts +6 -0
  63. package/dist/vite/typecheck-fs.js +68 -0
  64. package/package.json +58 -20
  65. package/shape-catalog.d.ts +9 -0
  66. package/typecheck-fs.d.ts +6 -0
  67. package/dist/app/session-gate.d.ts +0 -6
  68. package/dist/app/session-gate.js +0 -19
  69. package/dist/client.d.ts +0 -10
  70. package/dist/lib/manta/providers.d.ts +0 -10
  71. package/dist/lib/manta/providers.js +0 -48
  72. package/dist/lib/manta/session.d.ts +0 -47
  73. package/dist/lib/manta/session.js +0 -204
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react";
3
3
  import { planStatusQuery, readPlanStatus } from "@manta-eu/horns/render";
4
4
  import { planSummary } from "@manta-eu/horns-kit";
5
5
  import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@manta-eu/react";
6
- import { browserSparqlSelect } from "../lib/manta/sparql-select";
6
+ import { useSparqlSelect } from "../lib/manta/sparql-select";
7
7
  import { planPollAttempts, planPollInterval, runSeen, runSettled, startPlanRun } from "../lib/plan-runs";
8
8
  /**
9
9
  * Previews and runs of the action plans a Horn shows. A preview starts straight away: it changes nothing. A real
@@ -11,6 +11,7 @@ import { planPollAttempts, planPollInterval, runSeen, runSettled, startPlanRun }
11
11
  * and a real run that finished redraws the Horn, because the things it changed now look different.
12
12
  */
13
13
  export function usePlanRuns(args) {
14
+ const select = useSparqlSelect();
14
15
  const { datastoreId, realm, onChanged } = args;
15
16
  const [status, setStatus] = useState({});
16
17
  const [pending, setPending] = useState(new Set());
@@ -26,7 +27,6 @@ export function usePlanRuns(args) {
26
27
  };
27
28
  }, []);
28
29
  const follow = async (plan, preview, startedBefore) => {
29
- const select = browserSparqlSelect(datastoreId);
30
30
  for (let attempt = 0; attempt < planPollAttempts && alive.current; attempt += 1) {
31
31
  await new Promise((resolve) => setTimeout(resolve, planPollInterval));
32
32
  if (!alive.current)
@@ -10,5 +10,24 @@ export type ReadTheCodeProps = {
10
10
  onKeep: (edit: VisualEdit) => void;
11
11
  };
12
12
  export declare function ReadTheCode({ viewId, title, source, state, onKeep }: ReadTheCodeProps): import("react").JSX.Element;
13
+ export type ReadTheCodeDialogProps = ReadTheCodeProps & {
14
+ open: boolean;
15
+ onClose: () => void;
16
+ /** Where this one sits when a save is walking every unread view in turn. */
17
+ standing?: {
18
+ at: number;
19
+ of: number;
20
+ };
21
+ };
22
+ /**
23
+ * The reading itself, without the button that opens it.
24
+ *
25
+ * Saving drives this too: a Horn that will not save because nobody read its code is a dead end, so Save
26
+ * offers the reading instead of refusing. It is still the person's action that keeps the code.
27
+ *
28
+ * Keeping does not close it — the owner decides what follows, because when a save is walking several views
29
+ * what follows is the next one.
30
+ */
31
+ export declare function ReadTheCodeDialog({ viewId, title, source, state, open, onClose, onKeep, standing, }: ReadTheCodeDialogProps): import("react").JSX.Element;
13
32
  /** The review state of every written view in a draft, by view id, for the Studio to show on each card. */
14
33
  export declare function writtenReviewStates(horn: Horn): Promise<Record<string, ReviewState["state"]>>;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { Badge, Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@manta-eu/react";
4
4
  import { reviewOf, reviewStateOf } from "@manta-eu/horns/render";
@@ -24,12 +24,26 @@ const wording = {
24
24
  };
25
25
  export function ReadTheCode({ viewId, title, source, state, onKeep }) {
26
26
  const [open, setOpen] = useState(false);
27
+ return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: state === "reviewed" ? "ghost" : "secondary", size: "xs", onClick: () => setOpen(true), "aria-label": `${wording[state].title}: ${title}`, children: wording[state].badge }), _jsx(ReadTheCodeDialog, { viewId: viewId, title: title, source: source, state: state, open: open, onClose: () => setOpen(false), onKeep: (change) => {
28
+ onKeep(change);
29
+ setOpen(false);
30
+ } })] }));
31
+ }
32
+ /**
33
+ * The reading itself, without the button that opens it.
34
+ *
35
+ * Saving drives this too: a Horn that will not save because nobody read its code is a dead end, so Save
36
+ * offers the reading instead of refusing. It is still the person's action that keeps the code.
37
+ *
38
+ * Keeping does not close it — the owner decides what follows, because when a save is walking several views
39
+ * what follows is the next one.
40
+ */
41
+ export function ReadTheCodeDialog({ viewId, title, source, state, open, onClose, onKeep, standing, }) {
27
42
  const words = wording[state];
28
43
  async function keep() {
29
44
  onKeep({ kind: "keep-code", viewId, reviewed: await reviewOf(source, new Date()) });
30
- setOpen(false);
31
45
  }
32
- return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: state === "reviewed" ? "ghost" : "secondary", size: "xs", onClick: () => setOpen(true), "aria-label": `${words.title}: ${title}`, children: words.badge }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-3xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { text: words.title }) }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { children: title }), _jsx("span", { className: "text-muted-foreground text-sm", children: words.badge })] }), _jsx("p", { className: "text-muted-foreground m-0 text-sm", children: words.lead }), _jsx("pre", { className: "border-muted-border max-h-[50vh] overflow-auto rounded-lg border p-3 text-xs leading-relaxed", children: _jsx("code", { children: source }) }), _jsx("div", { className: "flex justify-end gap-2", children: state === "reviewed" ? (_jsx(Button, { onClick: () => setOpen(false), children: words.action })) : (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", onClick: () => setOpen(false), children: "Not yet" }), _jsx(Button, { onClick: () => void keep(), children: words.action })] })) })] })] }) })] }));
46
+ return (_jsx(Dialog, { open: open, onOpenChange: (next) => (next ? undefined : onClose()), children: _jsxs(DialogContent, { className: "flex h-[92vh] w-[95vw] max-w-[95vw] flex-col sm:max-w-[95vw]", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { text: words.title }) }), _jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { children: title }), _jsx("span", { className: "text-muted-foreground text-sm", children: standing === undefined ? words.badge : `${words.badge} · ${standing.at} of ${standing.of}` })] }), _jsx("p", { className: "text-muted-foreground m-0 text-sm", children: words.lead }), _jsx("pre", { className: "border-muted-border min-h-0 flex-1 overflow-auto rounded-lg border p-4 text-sm leading-relaxed", children: _jsx("code", { children: source }) }), _jsx("div", { className: "flex justify-end gap-2", children: state === "reviewed" ? (_jsx(Button, { onClick: onClose, children: words.action })) : (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", onClick: onClose, children: "Not yet" }), _jsx(Button, { onClick: () => void keep(), children: words.action })] })) })] })] }) }));
33
47
  }
34
48
  /** The review state of every written view in a draft, by view id, for the Studio to show on each card. */
35
49
  export async function writtenReviewStates(horn) {
package/dist/index.d.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  export { AppRoutes, hornsRoutes } from "./app/routes.tsx";
2
2
  export { AppShell } from "./components/app-shell.tsx";
3
- export { MantaSparqlProvider, datastoreSparqlPath, queryClientFor } from "./lib/manta/providers.tsx";
4
- export { MantaSessionProvider, useMantaSession } from "./lib/manta/session.tsx";
5
- export { browserSparqlSelect } from "./lib/manta/sparql-select.ts";
3
+ export { signInHref } from "./lib/manta/sign-in.ts";
4
+ export { useSparqlSelect } from "./lib/manta/sparql-select.ts";
6
5
  export { ldkitShapeCatalog, provideShapeCatalog, shapesRealm, type ShapeCatalogSource } from "./lib/shapes/catalog.ts";
7
6
  export { rasterizeHorn, sendHornScreenshot } from "./components/rasterize.tsx";
8
7
  export { reviewHorn, type HornReview } from "./lib/horn-review.ts";
package/dist/index.js CHANGED
@@ -5,9 +5,8 @@
5
5
  // than compiled by the host, so a host with no Tailwind still gets the app looking like Manta.
6
6
  export { AppRoutes, hornsRoutes } from "./app/routes.js";
7
7
  export { AppShell } from "./components/app-shell.js";
8
- export { MantaSparqlProvider, datastoreSparqlPath, queryClientFor } from "./lib/manta/providers.js";
9
- export { MantaSessionProvider, useMantaSession } from "./lib/manta/session.js";
10
- export { browserSparqlSelect } from "./lib/manta/sparql-select.js";
8
+ export { signInHref } from "./lib/manta/sign-in.js";
9
+ export { useSparqlSelect } from "./lib/manta/sparql-select.js";
11
10
  export { ldkitShapeCatalog, provideShapeCatalog, shapesRealm } from "./lib/shapes/catalog.js";
12
11
  export { rasterizeHorn, sendHornScreenshot } from "./components/rasterize.js";
13
12
  export { reviewHorn } from "./lib/horn-review.js";
@@ -12,6 +12,13 @@ export type HornReview = {
12
12
  problems: RenderProblem[];
13
13
  unavailable?: string;
14
14
  };
15
+ /**
16
+ * What the picture calls a view.
17
+ *
18
+ * A draw failure knows the id and the reviewer only ever sees the title, so one broken view was arriving as
19
+ * two findings under two names, and a turn spent itself fixing the second one.
20
+ */
21
+ export declare function nameOfView(horn: Pick<Horn, "views">, viewId: string): string;
15
22
  /**
16
23
  * Draw the Horn that was just made, look at it, and say what is wrong.
17
24
  *
@@ -1,6 +1,16 @@
1
1
  import { hornSnapshotSchema } from "@manta-eu/horns/render";
2
2
  import { rasterizeHorn } from "../components/rasterize";
3
3
  import { isRecord } from "@manta-eu/horns";
4
+ /**
5
+ * What the picture calls a view.
6
+ *
7
+ * A draw failure knows the id and the reviewer only ever sees the title, so one broken view was arriving as
8
+ * two findings under two names, and a turn spent itself fixing the second one.
9
+ */
10
+ export function nameOfView(horn, viewId) {
11
+ const view = horn.views.find((candidate) => candidate.id === viewId);
12
+ return view === undefined || "ref" in view ? viewId : view.title;
13
+ }
4
14
  /**
5
15
  * Draw the Horn that was just made, look at it, and say what is wrong.
6
16
  *
@@ -14,6 +24,11 @@ export async function reviewHorn(args) {
14
24
  const snapshot = await snapshotOf(args);
15
25
  const drawn = await rasterizeHorn(snapshot);
16
26
  problems = drawn.problems;
27
+ // A view that threw leaves a card with an error where the drawing should be. Asking a model what it
28
+ // thinks of that picture buys three ways of saying "this card is empty", each of which reads like a
29
+ // separate thing to fix. The failure is the finding, and the reviewer costs money per turn.
30
+ if (problems.length > 0)
31
+ return { advice: [], problems };
17
32
  const image = await dataUrl(drawn.png);
18
33
  const response = await fetch(`/api/d/${encodeURIComponent(args.datastoreId)}/horns/review`, {
19
34
  method: "POST",
@@ -0,0 +1,8 @@
1
+ import type { Horn } from "@manta-eu/horns";
2
+ export type HornVariables = {
3
+ /** What each variable is set to, ready for `buildHornSnapshot`. Absent keys are the variable's default. */
4
+ values: Record<string, unknown>;
5
+ /** Narrows the Horn, or clears the variable with `undefined`. */
6
+ set: (id: string, value: unknown) => void;
7
+ };
8
+ export declare function useHornVariables(horn: Horn | undefined): HornVariables;
@@ -0,0 +1,39 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import { parseAsString, useQueryStates } from "nuqs";
3
+ /**
4
+ * The parameter a variable rides on, or nothing.
5
+ *
6
+ * Its `urlParam` and only that: `variablesFromSearch` reads that name and no other, so writing a variable's
7
+ * id instead would change the URL and leave every binding reading the value it had before.
8
+ */
9
+ function paramOf(variable) {
10
+ return variable.urlParam;
11
+ }
12
+ export function useHornVariables(horn) {
13
+ const variables = useMemo(() => horn?.variables ?? [], [horn]);
14
+ const keys = useMemo(() => Object.fromEntries(variables.flatMap((variable) => {
15
+ const param = paramOf(variable);
16
+ return param === undefined ? [] : [[param, parseAsString]];
17
+ })), [variables]);
18
+ const [params, setParams] = useQueryStates(keys);
19
+ const values = useMemo(() => {
20
+ const chosen = {};
21
+ for (const variable of variables) {
22
+ const param = paramOf(variable);
23
+ const held = param === undefined ? undefined : params[param];
24
+ if (held !== null && held !== undefined)
25
+ chosen[variable.id] = held;
26
+ }
27
+ return chosen;
28
+ }, [variables, params]);
29
+ const set = useCallback((id, value) => {
30
+ const variable = variables.find((candidate) => candidate.id === id);
31
+ const param = variable === undefined ? undefined : paramOf(variable);
32
+ if (param === undefined)
33
+ return;
34
+ // null is how nuqs drops a parameter; anything else rides as text, which is what a URL holds.
35
+ const next = value === undefined || value === null || value === "" ? null : String(value);
36
+ void setParams({ [param]: next });
37
+ }, [variables, setParams]);
38
+ return { values, set };
39
+ }
@@ -0,0 +1,2 @@
1
+ /** Horns' own `/login`, which proxies to agents and stamps the marker a loop is diagnosed by. */
2
+ export declare function signInHref(redirect?: string): string;
@@ -0,0 +1,9 @@
1
+ import { isPresent } from "@manta-eu/lib/present";
2
+ /** Horns' own `/login`, which proxies to agents and stamps the marker a loop is diagnosed by. */
3
+ export function signInHref(redirect) {
4
+ const params = new URLSearchParams();
5
+ if (isPresent(redirect) && redirect !== "/")
6
+ params.set("redirect", redirect);
7
+ const query = params.toString();
8
+ return `/login${query ? `?${query}` : ""}`;
9
+ }
@@ -1,2 +1,6 @@
1
- import { type SparqlSelect } from "@manta-eu/horns";
2
- export declare function browserSparqlSelect(datastoreId: string): SparqlSelect;
1
+ import { SparqlReadError, type SparqlSelect } from "@manta-eu/horns";
2
+ import type { GraphQLClient } from "graphql-request";
3
+ export declare function graphqlSparqlSelect(client: GraphQLClient): SparqlSelect;
4
+ /** The reader this app's pages use. The datastore rides on the client, from the route. */
5
+ export declare function useSparqlSelect(): SparqlSelect;
6
+ export { SparqlReadError };
@@ -1,19 +1,22 @@
1
- import { sparqlResultsSchema } from "@manta-eu/horns";
2
- import { datastoreSparqlPath } from "./providers";
3
- import { readJsonError } from "./session";
4
- // SELECTs from the browser through the worker's read-only, grant-checked `/d/:ds/sparql` proxy.
5
- export function browserSparqlSelect(datastoreId) {
1
+ import { useMemo } from "react";
2
+ import { sparqlResultsOfGraphqlField, SparqlReadError } from "@manta-eu/horns";
3
+ import { SparqlQueryDocument } from "@manta-eu/client";
4
+ import { useMantaClient } from "@manta-eu/react/providers";
5
+ // Imperative rather than `useSparqlQueryQuery`: a `SparqlSelect` is called from inside query functions and
6
+ // from readers that issue one query after another, which a hook cannot serve.
7
+ export function graphqlSparqlSelect(client) {
6
8
  return async (query, signal) => {
7
- const response = await fetch(datastoreSparqlPath(datastoreId), {
8
- method: "POST",
9
- credentials: "same-origin",
10
- headers: { "content-type": "application/sparql-query", accept: "application/sparql-results+json" },
11
- body: query,
9
+ const answer = await client.request({
10
+ document: SparqlQueryDocument,
11
+ variables: { query },
12
12
  ...(signal ? { signal } : {}),
13
13
  });
14
- if (!response.ok)
15
- throw new Error(await readJsonError(response, `The datastore answered ${response.status}.`));
16
- const body = await response.json();
17
- return sparqlResultsSchema.parse(body);
14
+ return sparqlResultsOfGraphqlField(answer.sparqlQuery);
18
15
  };
19
16
  }
17
+ /** The reader this app's pages use. The datastore rides on the client, from the route. */
18
+ export function useSparqlSelect() {
19
+ const { mantaClient } = useMantaClient();
20
+ return useMemo(() => graphqlSparqlSelect(mantaClient), [mantaClient]);
21
+ }
22
+ export { SparqlReadError };
@@ -0,0 +1,4 @@
1
+ import type { SparqlSelect } from "@manta-eu/horns";
2
+ export declare const referenceEntity = "https://data.example/entity/cisco-systems";
3
+ /** A `SparqlSelect` over the fixture. Async because every other one is, not because anything waits. */
4
+ export declare const fakeSparqlSelect: SparqlSelect;
@@ -0,0 +1,216 @@
1
+ import { xsd } from "ldkit/namespaces";
2
+ // A datastore that does not exist, for the reference Horn (docs/standard.md). It answers the queries that
3
+ // Horn's bindings compile to and nothing else, so the page can be looked at without a datastore, a session or
4
+ // a worker — which is the only way to judge whether the thing is any good before there is data behind it.
5
+ //
6
+ // Matching on what a query mentions rather than parsing it: these are eight known queries, and the marker is
7
+ // the predicate each one is about. Anything unrecognised answers with no rows, which is what a real datastore
8
+ // does for a thing it has not got.
9
+ const entity = "https://data.example/entity/";
10
+ const country = "https://data.example/country/";
11
+ const alert = "https://data.example/alert/";
12
+ export const referenceEntity = `${entity}cisco-systems`;
13
+ const uri = (value) => ({ type: "uri", value });
14
+ const text = (value) => ({ type: "literal", value });
15
+ const number = (value) => ({
16
+ type: "literal",
17
+ value: String(value),
18
+ datatype: Number.isInteger(value) ? xsd.integer : xsd.decimal,
19
+ });
20
+ const date = (value) => ({ type: "literal", value, datatype: xsd.dateTime });
21
+ const answer = (vars, bindings) => ({
22
+ head: { vars },
23
+ results: { bindings },
24
+ });
25
+ const nothing = answer([], []);
26
+ /** What each IRI is called. The label pass asks for these, and every view shows the name and never the IRI. */
27
+ const names = {
28
+ [referenceEntity]: "Cisco Systems, Inc.",
29
+ [`${entity}qualcomm`]: "QUALCOMM, Inc.",
30
+ [`${entity}cisco-supplier`]: "Cisco Systems, Inc.",
31
+ [`${entity}069hj1e`]: "069HJ1-E",
32
+ [`${entity}interdigital`]: "InterDigital, Inc.",
33
+ [`${entity}samsung-electro`]: "Samsung Electro-Mechanics Co., Ltd.",
34
+ [`${entity}au-optronics`]: "AU Optronics Corp.",
35
+ [`${entity}huawei`]: "Huawei Investment & Holding Co., Ltd.",
36
+ [`${country}us`]: "United States",
37
+ [`${country}cn`]: "China",
38
+ [`${country}tw`]: "Taiwan",
39
+ [`${country}kr`]: "South Korea",
40
+ [`${country}id`]: "Indonesia",
41
+ [`${country}jp`]: "Japan",
42
+ };
43
+ const suppliers = [
44
+ { id: "qualcomm", concentration: 78.08, yearOnYear: 72.04, country: "us" },
45
+ { id: "cisco-supplier", concentration: 1.48, yearOnYear: -4.23, country: "us" },
46
+ { id: "069hj1e", concentration: 0.94, yearOnYear: -1.72, country: "us" },
47
+ { id: "interdigital", concentration: 0.75, yearOnYear: -1.88, country: "us" },
48
+ { id: "samsung-electro", concentration: 0.75, yearOnYear: -1.86, country: "kr" },
49
+ { id: "au-optronics", concentration: 0.74, yearOnYear: -1.85, country: "tw" },
50
+ { id: "huawei", concentration: 0.74, yearOnYear: -2.9, country: "cn" },
51
+ ];
52
+ const exposures = [
53
+ { id: "us", buyer: 82, supplier: 64, disruptions: 3 },
54
+ { id: "cn", buyer: 41, supplier: 88, disruptions: 2 },
55
+ { id: "tw", buyer: 12, supplier: 74, disruptions: 4 },
56
+ { id: "kr", buyer: 18, supplier: 46, disruptions: 0 },
57
+ { id: "id", buyer: 6, supplier: 21, disruptions: 5 },
58
+ { id: "jp", buyer: 22, supplier: 33, disruptions: 1 },
59
+ ];
60
+ const flags = [
61
+ { kind: "Total supply chain alerts", total: 32, severity: "neutral" },
62
+ { kind: "Concentration alerts", total: 6, qualifier: "Sources: CN", severity: "medium" },
63
+ { kind: "Sanction exposures", total: 2, qualifier: "Sources: CN", severity: "high" },
64
+ { kind: "Tariff exposures", total: 13, qualifier: "Sources: CN", severity: "medium" },
65
+ { kind: "Disruption exposures", total: 2, qualifier: "1 high priority", severity: "high" },
66
+ { kind: "ESG alerts", total: 8, qualifier: "Benchmark 4", severity: "medium" },
67
+ ];
68
+ const notifications = [
69
+ {
70
+ id: "concentration-change",
71
+ category: "Concentration alerts",
72
+ headline: "Country concentration change",
73
+ source: "Transactions",
74
+ priority: "amber",
75
+ raised: "2026-09-28T09:14:00Z",
76
+ },
77
+ {
78
+ id: "quake-c9400-a",
79
+ category: "Disruption exposures",
80
+ headline: "Earthquake impacts order 2021-11-16_C9400",
81
+ source: "GDACS",
82
+ priority: "red",
83
+ raised: "2026-09-27T22:02:00Z",
84
+ },
85
+ {
86
+ id: "quake-c9400-b",
87
+ category: "Disruption exposures",
88
+ headline: "Earthquake impacts order 2022-01-24_C9400",
89
+ source: "GDACS",
90
+ priority: "red",
91
+ raised: "2026-09-27T18:40:00Z",
92
+ },
93
+ {
94
+ id: "tariff-cn",
95
+ category: "Tariff exposures",
96
+ headline: "Tariff exposure raised on 13 inbound lanes from CN",
97
+ source: "Trade",
98
+ priority: "amber",
99
+ raised: "2026-09-26T11:20:00Z",
100
+ },
101
+ ];
102
+ const disruptions = [
103
+ {
104
+ id: "taiwan-quake",
105
+ category: "Disruption exposures",
106
+ headline: "Red earthquake alert (Magnitude 9.8M, Depth 35km) in Taiwan 09/11/2021 06:25 UTC, 1,030,000 people within 100km",
107
+ source: "GDACS",
108
+ priority: "red",
109
+ raised: "2026-09-25T06:25:00Z",
110
+ },
111
+ {
112
+ id: "krakatau",
113
+ category: "Disruption exposures",
114
+ headline: "Volcanic eruption is ongoing for Krakatau in Indonesia",
115
+ source: "GDACS",
116
+ priority: "green",
117
+ raised: "2026-09-24T14:10:00Z",
118
+ },
119
+ ];
120
+ /** Concentration climbing through 2019–2022, the way the reference screen's history does. */
121
+ const history = Array.from({ length: 28 }, (_, index) => {
122
+ const month = new Date(Date.UTC(2019, 9 + index, 1)).toISOString().slice(0, 7);
123
+ return { month, concentration: Math.round((38 + index * 1.6 + Math.sin(index / 2) * 4) * 100) / 100 };
124
+ });
125
+ const raisedRows = (rows, category) => answer(["this", "headline", "source", "priority", "raised", "category"], rows
126
+ .filter((row) => category === undefined || row.category === category)
127
+ .map((row) => ({
128
+ this: uri(`${alert}${row.id}`),
129
+ headline: text(row.headline),
130
+ source: text(row.source),
131
+ priority: text(row.priority),
132
+ raised: date(row.raised),
133
+ category: text(row.category),
134
+ })));
135
+ /**
136
+ * What a variable was narrowed to, as the compiled query carries it.
137
+ *
138
+ * `compileBinding` writes a bound parameter in as `VALUES ?name { <iri> }` or `VALUES ?name { "text" }`, so
139
+ * a fixture that wants to behave like a datastore reads it back out of the text it was handed.
140
+ */
141
+ const narrowedTo = (query, name) => {
142
+ const match = new RegExp(`VALUES \\?${name} \\{\\s*(?:<([^>]+)>|"([^"]*)")`).exec(query);
143
+ return match?.[1] ?? match?.[2];
144
+ };
145
+ /** The IRIs a label pass is asking about, in the order the `VALUES` names them. */
146
+ const askedFor = (query) => {
147
+ const values = /VALUES \?entity \{([^}]*)\}/.exec(query)?.[1] ?? "";
148
+ return [...values.matchAll(/<([^>]+)>/g)].flatMap((match) => (match[1] === undefined ? [] : [match[1]]));
149
+ };
150
+ function respond(query) {
151
+ // The label pass, which every view depends on: a row carries an IRI and shows the thing's name.
152
+ if (query.includes("SELECT ?entity ?name") && query.includes("VALUES ?entity")) {
153
+ return answer(["entity", "name"], askedFor(query).flatMap((iri) => {
154
+ const name = names[iri];
155
+ return name === undefined ? [] : [{ entity: uri(iri), name: text(name) }];
156
+ }));
157
+ }
158
+ if (query.includes("factsetIndustry")) {
159
+ return answer(["this", "name", "industry", "country", "entityType"], [
160
+ {
161
+ this: uri(referenceEntity),
162
+ name: text("Cisco Systems, Inc."),
163
+ industry: text("Factset Industry · Information Technology Services"),
164
+ country: uri(`${country}us`),
165
+ entityType: text("Public company"),
166
+ },
167
+ ]);
168
+ }
169
+ if (query.includes("placedOrder")) {
170
+ return answer(["outgoing", "outgoingValue", "suppliers", "customers"], [
171
+ {
172
+ outgoing: number(7739),
173
+ outgoingValue: number(2923174261),
174
+ suppliers: number(8),
175
+ customers: number(125),
176
+ },
177
+ ]);
178
+ }
179
+ if (query.includes('alertKind "notification"'))
180
+ return raisedRows(notifications, narrowedTo(query, "category"));
181
+ if (query.includes('alertKind "disruption"'))
182
+ return raisedRows(disruptions, narrowedTo(query, "category"));
183
+ if (query.includes("alertKind")) {
184
+ return answer(["kind", "total", "qualifier", "severity"], flags.map((flag) => ({
185
+ kind: text(flag.kind),
186
+ total: number(flag.total),
187
+ severity: text(flag.severity),
188
+ ...(flag.qualifier === undefined ? {} : { qualifier: text(flag.qualifier) }),
189
+ })));
190
+ }
191
+ if (query.includes("buyerConcentration")) {
192
+ return answer(["this", "buyerConcentration", "supplierConcentration", "disruptions"], exposures.map((one) => ({
193
+ this: uri(`${country}${one.id}`),
194
+ buyerConcentration: number(one.buyer),
195
+ supplierConcentration: number(one.supplier),
196
+ disruptions: number(one.disruptions),
197
+ })));
198
+ }
199
+ if (query.includes("concentrationScore")) {
200
+ return answer(["month", "concentration"], history.map((one) => ({ month: text(one.month), concentration: number(one.concentration) })));
201
+ }
202
+ if (query.includes("ltmConcentration")) {
203
+ const only = narrowedTo(query, "country");
204
+ return answer(["this", "concentration", "yearOnYear", "country"], suppliers
205
+ .filter((one) => only === undefined || `${country}${one.country}` === only)
206
+ .map((one) => ({
207
+ this: uri(`${entity}${one.id}`),
208
+ concentration: number(one.concentration),
209
+ yearOnYear: number(one.yearOnYear),
210
+ country: uri(`${country}${one.country}`),
211
+ })));
212
+ }
213
+ return nothing;
214
+ }
215
+ /** A `SparqlSelect` over the fixture. Async because every other one is, not because anything waits. */
216
+ export const fakeSparqlSelect = (query) => Promise.resolve(respond(query));
@@ -10,5 +10,5 @@ export declare const generatedRoot: URL;
10
10
  /** Whether `just generate` has been run for this realm in this checkout. */
11
11
  export declare function hasGeneratedSchemas(realm: Realm, root?: URL): boolean;
12
12
  export declare const generatedNotice = "built from this realm's own datastore shapes, which are a subset of the canonical set";
13
- /** Node only: it reads the filesystem. The worker reads the snapshot `shapes:snapshot` wrote. */
13
+ /** Node only: it reads the filesystem. `hornsShapeCatalog` calls it at build time; a worker never does. */
14
14
  export declare function ldkitSchemasFor(realm: Realm, root?: URL, preferGenerated?: boolean): Promise<LdkitSource>;
@@ -17,7 +17,7 @@ export function hasGeneratedSchemas(realm, root = generatedRoot) {
17
17
  return existsSync(new URL("index.ts", generatedDirFor(realm, root)));
18
18
  }
19
19
  export const generatedNotice = "built from this realm's own datastore shapes, which are a subset of the canonical set";
20
- /** Node only: it reads the filesystem. The worker reads the snapshot `shapes:snapshot` wrote. */
20
+ /** Node only: it reads the filesystem. `hornsShapeCatalog` calls it at build time; a worker never does. */
21
21
  export async function ldkitSchemasFor(realm, root = generatedRoot, preferGenerated = false) {
22
22
  if (preferGenerated && hasGeneratedSchemas(realm, root)) {
23
23
  return { module: await import(__rewriteRelativeImportExtension(new URL("index.ts", generatedDirFor(realm, root)).href)), origin: "generated" };
@@ -28,7 +28,9 @@ export function studioProgressLine(line) {
28
28
  return match(event["type"])
29
29
  .with("reading", () => step("Reading the Horn and the data behind it"))
30
30
  .with("round", () => (event["step"] === 1 && agent === undefined ? step("Thinking") : undefined))
31
- .with("tool-call", () => event["repeated"] === true || name === "propose_horn_patch" ? undefined : step(toolLines[name] ?? `Using ${name}`))
31
+ .with("tool-call", () => event["repeated"] === true || name === "propose_horn_patch"
32
+ ? undefined
33
+ : step(toolLines[name] ?? `Using ${name}`))
32
34
  .with("proposal-accepted", () => (name === "propose_horn_patch" ? step("Changed the Horn") : undefined))
33
35
  .with("proposal-rejected", () => step("That change did not hold up against the data, trying again"))
34
36
  .with("rehydrated", () => step("Starting over from the Horn as it stands"))
@@ -0,0 +1,5 @@
1
+ /** One entry per surfaced export: the declaration text the Studio may read. */
2
+ export type ComponentApi = Readonly<Record<string, string>>;
3
+ /** Node only: `import.meta.resolve` is unavailable under vitest's transform, so a test passes its own root. */
4
+ export declare function sdkDistRoot(): string;
5
+ export declare function componentApiFrom(root: string, hops?: number): ComponentApi;
@@ -0,0 +1,77 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { moduleOfExport, sdkModules, surfaceExports } from "@manta-eu/horns/render";
5
+ /** Above this an entry is a type tree nobody can read, and the walk stopped somewhere it should not have. */
6
+ const maxEntryChars = 6000;
7
+ /** Node only: `import.meta.resolve` is unavailable under vitest's transform, so a test passes its own root. */
8
+ export function sdkDistRoot() {
9
+ return join(dirname(dirname(fileURLToPath(import.meta.resolve("@manta-eu/react")))), "dist");
10
+ }
11
+ function declarationFilesIn(directory) {
12
+ return readdirSync(directory).flatMap((entry) => {
13
+ const path = join(directory, entry);
14
+ if (statSync(path).isDirectory())
15
+ return declarationFilesIn(path);
16
+ return path.endsWith(".d.ts") ? [path] : [];
17
+ });
18
+ }
19
+ /** A declaration and its body, from `export …` to the `;` that closes it. */
20
+ function blockOf(texts, name) {
21
+ const pattern = new RegExp(`^export (?:declare (?:const|function|class)|type|interface) ${name}\\b`, "m");
22
+ for (const text of texts) {
23
+ const start = text.search(pattern);
24
+ if (start < 0)
25
+ continue;
26
+ let depth = 0;
27
+ for (let at = start; at < text.length; at += 1) {
28
+ const character = text[at];
29
+ if (character === "{")
30
+ depth += 1;
31
+ else if (character === "}")
32
+ depth -= 1;
33
+ else if (character === ";" && depth === 0)
34
+ return text.slice(start, at + 1);
35
+ }
36
+ return text.slice(start);
37
+ }
38
+ return undefined;
39
+ }
40
+ /**
41
+ * The export's declaration plus the ones it names, transitively.
42
+ *
43
+ * A props type sits beside its component and may name a schema elsewhere — `BarChartProps` reaches
44
+ * `BarChartData` reaches `barChartSchema`. Following the names keeps the answer to what the export is,
45
+ * where a whole directory would hand over every chart in the design system.
46
+ */
47
+ function scopeOf(texts, name, hops) {
48
+ const first = blockOf(texts, name);
49
+ if (first === undefined)
50
+ return undefined;
51
+ const seen = new Set([name]);
52
+ const blocks = [first];
53
+ for (let hop = 0; hop < hops; hop += 1) {
54
+ const named = [...blocks.join("\n").matchAll(/\b([A-Za-z_$][\w$]*)\b/g)].flatMap((match) => match[1] ?? []);
55
+ for (const referenced of named) {
56
+ if (seen.has(referenced))
57
+ continue;
58
+ seen.add(referenced);
59
+ const block = blockOf(texts, referenced);
60
+ if (block !== undefined)
61
+ blocks.push(block);
62
+ }
63
+ }
64
+ return blocks.join("\n");
65
+ }
66
+ export function componentApiFrom(root, hops = 3) {
67
+ const texts = declarationFilesIn(root).map((path) => readFileSync(path, "utf8"));
68
+ // Only the design system's own: React's hooks and the kit's plan module are not its to declare.
69
+ const names = [...new Set(sdkModules.flatMap((module) => [...(surfaceExports(module) ?? [])]))].sort();
70
+ return Object.freeze(Object.fromEntries(names.flatMap((name) => {
71
+ const scope = scopeOf(texts, name, hops);
72
+ if (scope === undefined || scope.length > maxEntryChars)
73
+ return [];
74
+ const from = moduleOfExport(name);
75
+ return [[name, `// ${name}, from "${from ?? "@manta-eu/react"}"\n${scope}`]];
76
+ })));
77
+ }
@@ -1,10 +1,21 @@
1
1
  import { useCallback, useId, useMemo, useState } from "react";
2
+ import { viewPowersModule } from "@manta-eu/horns/render";
2
3
  // The whole barrel, because a view may reach for any of it; `createSurfaceLoader` narrows it to the declared
3
4
  // exports before the view sees it. Dynamic, and held at module scope: `WrittenView` recompiles whenever this
4
5
  // identity changes.
5
6
  export const loadWrittenSurface = async () => {
6
- const [react, chart] = await Promise.all([import("@manta-eu/react"), import("@manta-eu/react/chart")]);
7
- return { "@manta-eu/react": react, "@manta-eu/react/chart": chart };
7
+ const [react, chart, plan, explain] = await Promise.all([
8
+ import("@manta-eu/react"),
9
+ import("@manta-eu/react/chart"),
10
+ import("@manta-eu/horns-kit/plan"),
11
+ import("@manta-eu/horns-kit/view"),
12
+ ]);
13
+ return {
14
+ "@manta-eu/react": react,
15
+ "@manta-eu/react/chart": chart,
16
+ "@manta-eu/horns-kit/plan": plan,
17
+ [viewPowersModule]: explain,
18
+ };
8
19
  };
9
20
  // This app's React, so a view's state lives in the same reconciler as the page rather than in a second copy.
10
21
  export const writtenHooks = { useState, useMemo, useCallback, useId };