mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(studio): a primitive's callbacks are declared, not spelled
The action census placed a certified primitive's call site by the DOM event its handler prop is named after. Picker's is onPick, which names no event, so every Picker call site sat outside the interaction universe — and one of them switches the model, a kernel write with no identity. Seal A read as held the whole time. It was true, over a denominator that had a hole in it, which is the shape of assurance this tree exists to refuse. What a callback means is now the primitive's to declare. One list says which components are primitives and, for each, which props carry a person's interaction and which DOM event inside the component raises them. onPick is an interaction because Menu says so and its body does it, never because the name starts with "on". The declaration is checked rather than believed. primitives.test.ts parses each primitive and requires a handler for the declared event from which the declared prop is actually reachable, so a certified callback with nothing behind it fails. Its own negative control asks the same question about a prop no body raises and requires the answer to be no. There were two copies of the primitive list, the vitest gate's and the census tool's, and adding Seg to one of them is what broke a seal at 1 — reported as an input proven to write the host that the product names no action for, which is a true sentence about a bookkeeping slip. There is now one declaration, both callers pass nothing, and Options has no skip to pass: the way to keep two lists in agreement is to have one. A third list, CTL, went with them; nothing had read it. Three fixtures and three sabotages, because a rule nobody has seen fail is not a rule. A certified call site becomes a root; an ordinary component with a prop spelled the same does not; a prop the primitive does not declare does not. Each was broken in turn — spelling restored, every component promoted, undeclared props admitted — and each turned its own fixture red. The tool's own options are held to naming a resolver and nothing else, which reddens the moment it starts deciding this again. One of those fixtures was testing nothing and passed a sabotage that promoted every component in the tree. It resolved its imports against the product's files, where its ordinary component does not exist, so a test about ordinary components was a test about unresolved ones. Fixtures now resolve within their own universe. The frozen fixture corpus caught the first implementation. Anchoring a primitive's identity to a literal "/src/" un-certified the corpus's own Switch, whose two call sites lost their separate actions to one root inside the component — the exact rule that corpus was written to hold. A primitive is matched by its path within whichever tree is being scanned, and those reports are byte-identical again rather than rewritten. The denominator moves and does not compare across this change: 413 inputs to 415, unnamed 249 to 250, Seal B 97 to 98. The two arrivals are the Picker call sites that were never visible. One is the composer's model switch, which takes model.select — the id the settings sheet has always rendered for the same intent — and Seal A is held over the corrected universe rather than the incomplete one. The other is onboarding's, and it joins the 98 that Seal B is still open on.
This commit is contained in:
1 parent
49b574687c
commit
637c0ff6e1
7 files changed
+298
-23
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ACTIONS } from "./actions";
|
||||
import { certifyRoots } from "./ui/roots";
|
||||
import { PRIMITIVES, certifyRoots } from "./ui/roots";
|
||||
import { parse } from "@babel/parser";
|
||||
|
||||
// What Studio says a person can do, held to what it actually renders. The
|
||||
// registry is the oracle and production writes the same id again as a literal:
|
||||
@@ -22,7 +23,9 @@ const SOURCES = import.meta.glob(["./**/*.{ts,tsx}", "!./**/*.test.*", "!./actio
|
||||
}) as Record<string, string>;
|
||||
|
||||
const sources = new Map(Object.entries(SOURCES).map(([key, src]) => ["src/" + key.slice(2), src]));
|
||||
const resolve = (from: string, spec: string) => {
|
||||
// The census tool's own sources, for the one rule that is about it.
|
||||
const TOOLS = import.meta.glob(["../tools/ui-census/*.mjs"], { query: "?raw", import: "default", eager: true }) as Record<string, string>;
|
||||
const resolve = (from: string, spec: string, over: Map<string, string> = sources) => {
|
||||
if (!spec.startsWith(".")) return null;
|
||||
const parts = from.split("/").slice(0, -1).concat(spec.split("/"));
|
||||
const out: string[] = [];
|
||||
@@ -32,12 +35,12 @@ const resolve = (from: string, spec: string) => {
|
||||
else out.push(p);
|
||||
}
|
||||
const base = out.join("/");
|
||||
return [base + ".ts", base + ".tsx", base + "/index.ts", base + "/index.tsx"].find((c) => sources.has(c)) ?? null;
|
||||
return [base + ".ts", base + ".tsx", base + "/index.ts", base + "/index.tsx"].find((c) => over.has(c)) ?? null;
|
||||
};
|
||||
// The controls whose own internals are not entry points: a person acts on the
|
||||
// call site that hands them a handler.
|
||||
const PRIMITIVES = new Set(["src/ui/Switch.tsx", "src/ui/Menu.tsx", "src/ui/Seg.tsx"]);
|
||||
const census = certifyRoots(sources, { skip: (p) => PRIMITIVES.has(p), resolve });
|
||||
// Which components are interaction primitives is src/ui/primitives.ts's to
|
||||
// say, and this passes nothing: a second list here is the drift that broke a
|
||||
// seal, and the way to make it impossible is to have nowhere to write one.
|
||||
const census = certifyRoots(sources, { resolve });
|
||||
|
||||
const registered = new Set(ACTIONS.map((a) => a.id));
|
||||
const rendered = new Set(census.roots.flatMap((r) => r.actions));
|
||||
@@ -47,10 +50,30 @@ const undeclared = census.roots.filter((r) => !r.actions.length);
|
||||
// counted over every kind of input, not over the ones that happen to be
|
||||
// buttons. Lower it when it drops; a rise means an input was added without a
|
||||
// name, which is exactly what this is for.
|
||||
const UNDECLARED_CEILING = 270;
|
||||
//
|
||||
// It rose once, on purpose: certifying a primitive's callbacks brought the
|
||||
// Picker call sites into the universe, and they had been outside it. A
|
||||
// denominator that grows because the instrument was corrected is not the same
|
||||
// event as one that grows because debt was added, and numbers either side of
|
||||
// that change do not compare.
|
||||
const UNDECLARED_CEILING = 250;
|
||||
|
||||
/** One file, for holding the discovery rules themselves. */
|
||||
const only = (src: string) => certifyRoots(new Map([["src/ui/X.tsx", src]]), { resolve });
|
||||
/** A fixture universe of several files, for the rules about what a call site
|
||||
* resolves to. The primitive named here is the real one: its contract is what
|
||||
* is under test, so restating it would test the restatement.
|
||||
*
|
||||
* It resolves against its own files and not the product's. Sharing the outer
|
||||
* resolver made a fixture component that does not exist in src resolve to
|
||||
* nothing, so a test about ordinary components was quietly a test about
|
||||
* unresolved ones and stayed green through a sabotage that promoted every
|
||||
* component in the tree. */
|
||||
const among = (files: Record<string, string>) => {
|
||||
const map = new Map(Object.entries(files));
|
||||
return certifyRoots(map, { resolve: (from, spec) => resolve(from, spec, map) });
|
||||
};
|
||||
const STUB = "export function Picker() { return null }";
|
||||
|
||||
describe("the actions Studio says a person can perform", () => {
|
||||
it("names every action it renders", () => {
|
||||
@@ -140,3 +163,85 @@ describe("what counts as an input a person performs", () => {
|
||||
expect(c.refused).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// A primitive hands a person's interaction out through a callback, and which
|
||||
// callbacks those are is the primitive's to declare. Reading it off the prop's
|
||||
// name instead is what left every Picker call site outside the universe — with
|
||||
// a kernel write among them — while Seal A read as held.
|
||||
describe("what a certified primitive's call site counts as", () => {
|
||||
it("makes the call site the input, on the contract rather than the spelling", () => {
|
||||
const c = among({
|
||||
"src/ui/X.tsx": `import { Picker } from "./Menu";
|
||||
export const A = () => <Picker data-action="a.b" onPick={(v) => save(v)} />;`,
|
||||
"src/ui/Menu.tsx": STUB,
|
||||
});
|
||||
expect(c.roots.map((r) => `${r.kind}:${r.event}:${r.prop}`)).toEqual(["jsx-handler:click:onPick"]);
|
||||
expect(c.roots[0].actions).toEqual(["a.b"]);
|
||||
expect(c.refused).toEqual([]);
|
||||
});
|
||||
|
||||
it("leaves an ordinary component alone when its prop is spelled the same", () => {
|
||||
const c = among({
|
||||
"src/ui/X.tsx": `import { Foo } from "./Foo";
|
||||
export const A = () => <Foo data-action="a.b" onPick={(v) => save(v)} />;`,
|
||||
"src/ui/Foo.tsx": "export function Foo() { return null }",
|
||||
});
|
||||
// Not an input here: whatever Foo does with that callback, the thing a
|
||||
// person performs is inside Foo and is counted there.
|
||||
expect(c.roots).toEqual([]);
|
||||
expect(c.uncertified).toEqual([]);
|
||||
});
|
||||
|
||||
it("counts only the callbacks the primitive declares", () => {
|
||||
const c = among({
|
||||
"src/ui/X.tsx": `import { Picker } from "./Menu";
|
||||
export const A = () => <Picker onHover={(v) => peek(v)} />;`,
|
||||
"src/ui/Menu.tsx": STUB,
|
||||
});
|
||||
expect(c.roots).toEqual([]);
|
||||
});
|
||||
|
||||
// The destructive control. Without it the three above would still pass on an
|
||||
// implementation that promoted every component prop it liked the look of.
|
||||
it("rests on that declaration: production loses those roots without it", () => {
|
||||
const blind = certifyRoots(sources, {
|
||||
resolve,
|
||||
primitives: PRIMITIVES.map((p) => (p.file === "ui/Menu.tsx" ? { ...p, callbacks: {} } : p)),
|
||||
});
|
||||
const picks = (c: { roots: { prop?: string; actions: string[] }[] }) => c.roots.filter((r) => r.prop === "onPick");
|
||||
expect(picks(census).length).toBeGreaterThan(0);
|
||||
expect(picks(blind)).toEqual([]);
|
||||
// Including the one that switches the model, which is a kernel write and
|
||||
// was outside the universe until the contract was declared.
|
||||
expect(picks(census).some((r) => r.actions.includes("model.select"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// The drift that broke a seal: the same fact written down twice, and only one
|
||||
// copy updated. There is now one declaration and no way to pass another, so
|
||||
// what this holds is that the census tool never starts keeping its own again.
|
||||
describe("who decides which components are primitives", () => {
|
||||
it("is not the census tool: it passes certifyRoots nothing but a resolver", () => {
|
||||
const src = TOOLS["../tools/ui-census/certified.mjs"];
|
||||
expect(src, "certified.mjs is not being read").toBeTruthy();
|
||||
const tree = parse(src, { sourceType: "module" }) as unknown as { program: unknown };
|
||||
const keys: string[] = [];
|
||||
const walkAny = (n: unknown) => {
|
||||
if (!n || typeof (n as { type?: string }).type !== "string") return;
|
||||
const node = n as Record<string, unknown> & { type: string };
|
||||
if (node.type === "CallExpression" && (node.callee as { name?: string })?.name === "certifyRoots") {
|
||||
const opts = (node.arguments as Record<string, unknown>[])[1];
|
||||
for (const prop of ((opts?.properties as Record<string, unknown>[]) ?? [])) {
|
||||
keys.push(((prop.key as { name?: string })?.name) ?? "<computed>");
|
||||
}
|
||||
}
|
||||
for (const [k, v] of Object.entries(node)) {
|
||||
if (k === "loc") continue;
|
||||
if (Array.isArray(v)) v.forEach(walkAny);
|
||||
else walkAny(v);
|
||||
}
|
||||
};
|
||||
walkAny(tree.program);
|
||||
expect(keys, "the tool decides which files are primitives again").toEqual(["resolve"]);
|
||||
});
|
||||
});
|
||||
@@ -461,6 +461,7 @@ export function Composer({ port, status, running, focus, onSubmit, onChanged, on
|
||||
shortened from stays readable on hover. */}
|
||||
<Picker
|
||||
className="mode"
|
||||
data-action="model.select"
|
||||
place="bottom"
|
||||
title={status?.modelRef ?? modelLb}
|
||||
current={status?.modelRef}
|
||||
|
||||
@@ -33,7 +33,12 @@ interface Props {
|
||||
pending?: boolean;
|
||||
}
|
||||
|
||||
export function Picker({ label, items, current, onPick, place, className, title, pending }: Props) {
|
||||
// data-* rides through to the item that raises the pick, the way Switch and Seg
|
||||
// carry theirs: the action's identity is written at the call site, and the
|
||||
// answer this menu gives is the item's own value.
|
||||
export function Picker({
|
||||
label, items, current, onPick, place, className, title, pending, ...id
|
||||
}: Props & { [K in `data-${string}`]?: string }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
const wrap = useRef<HTMLDivElement>(null);
|
||||
@@ -151,6 +156,8 @@ export function Picker({ label, items, current, onPick, place, className, title,
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
{...id}
|
||||
data-value={it.value}
|
||||
className={it.plain ? "mi plain" : "mi"}
|
||||
role="menuitem"
|
||||
data-on={it.value === current ? "" : undefined}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parse } from "@babel/parser";
|
||||
import { PRIMITIVES, USER_INPUT, eventOfProp, walk, type Node } from "./roots";
|
||||
|
||||
// A declaration nobody checks is a claim. These hold each certified callback
|
||||
// to the component that declares it: the prop has to be raised by a real DOM
|
||||
// interaction in that file, and by one of the event it names. Without this the
|
||||
// authority would be a place to write "onWhatever is a click" and have the
|
||||
// census believe it.
|
||||
const SOURCES = import.meta.glob(["./**/*.tsx"], { query: "?raw", import: "default", eager: true }) as Record<string, string>;
|
||||
const sources = new Map(Object.entries(SOURCES).map(([k, src]) => ["ui/" + k.slice(2), src]));
|
||||
|
||||
const tree = (src: string) =>
|
||||
parse(src, { sourceType: "module", plugins: ["typescript", "jsx"], errorRecovery: true }) as unknown as Node;
|
||||
|
||||
/** Every identifier named inside a node. The question is only which names a
|
||||
* handler can reach, so a reference is enough — calling it is not required to
|
||||
* hand it on. */
|
||||
function namesIn(n: Node): Set<string> {
|
||||
const out = new Set<string>();
|
||||
walk(n, (d) => {
|
||||
if (d.type === "Identifier") out.add(d.name as string);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Functions this file declares, by the name they are reached through. */
|
||||
function localsOf(t: Node): Map<string, Node> {
|
||||
const out = new Map<string, Node>();
|
||||
walk(t, (n) => {
|
||||
if (n.type === "FunctionDeclaration" && (n.id as Node)?.name) out.set((n.id as Node).name as string, n);
|
||||
if (n.type === "VariableDeclarator" && (n.id as Node)?.type === "Identifier" && n.init) {
|
||||
out.set((n.id as Node).name as string, n.init as Node);
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The JSX handler expressions in this file for one DOM event. */
|
||||
function handlersFor(t: Node, event: string): Node[] {
|
||||
const out: Node[] = [];
|
||||
walk(t, (n) => {
|
||||
if (n.type !== "JSXAttribute") return;
|
||||
if (eventOfProp((n.name as Node)?.name as string) !== event) return;
|
||||
const v = n.value as Node | undefined;
|
||||
if (v) out.push(v.type === "JSXExpressionContainer" ? (v.expression as Node) : v);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// Four hops: a menu item's onClick calls take(), take calls the prop. Deeper
|
||||
// than anything a primitive should need, and bounded so a cycle cannot spin.
|
||||
const HOPS = 4;
|
||||
|
||||
function reaches(t: Node, from: Node[], want: string): boolean {
|
||||
const locals = localsOf(t);
|
||||
const seen = new Set<string>();
|
||||
let frontier = from.flatMap((n) => [...namesIn(n)]);
|
||||
for (let hop = 0; hop <= HOPS; hop++) {
|
||||
if (frontier.includes(want)) return true;
|
||||
const next: string[] = [];
|
||||
for (const name of frontier) {
|
||||
if (seen.has(name)) continue;
|
||||
seen.add(name);
|
||||
const body = locals.get(name);
|
||||
if (body) next.push(...namesIn(body));
|
||||
}
|
||||
if (next.length === 0) return false;
|
||||
frontier = next;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
describe("the components certified as interaction primitives", () => {
|
||||
it("declares each one against a file that is really in the tree", () => {
|
||||
for (const p of PRIMITIVES) expect(sources.has(p.file), `${p.file} is declared and not present`).toBe(true);
|
||||
});
|
||||
|
||||
it("names only events a person performs", () => {
|
||||
for (const p of PRIMITIVES) {
|
||||
for (const [prop, event] of Object.entries(p.callbacks)) {
|
||||
expect(USER_INPUT.has(event), `${p.file} ${prop} names ${event}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// The contract, held to the body. This is what makes onPick an interaction:
|
||||
// Menu really does raise it from a click, and the claim is checked rather
|
||||
// than read off the prop's spelling.
|
||||
it("raises every callback it declares, from the event it declares", () => {
|
||||
const unbacked: string[] = [];
|
||||
for (const p of PRIMITIVES) {
|
||||
const t = tree(sources.get(p.file)!);
|
||||
for (const [prop, event] of Object.entries(p.callbacks)) {
|
||||
const from = handlersFor(t, event);
|
||||
if (from.length === 0) unbacked.push(`${p.file} declares ${prop} on ${event} and has no ${event} handler`);
|
||||
else if (!reaches(t, from, prop)) unbacked.push(`${p.file}: no ${event} handler reaches ${prop}`);
|
||||
}
|
||||
}
|
||||
expect(unbacked, "a certified callback with nothing behind it").toEqual([]);
|
||||
});
|
||||
|
||||
// The negative control for the one above: a claim about a prop the component
|
||||
// never hands on has to fail, or the check proves nothing.
|
||||
it("refuses a callback the body never raises", () => {
|
||||
const t = tree(sources.get("ui/Menu.tsx")!);
|
||||
expect(reaches(t, handlersFor(t, "click"), "onPick")).toBe(true);
|
||||
expect(reaches(t, handlersFor(t, "click"), "onNeverCalled")).toBe(false);
|
||||
// And an event the file has no handler for reaches nothing at all.
|
||||
expect(handlersFor(t, "drop")).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -14,12 +14,52 @@
|
||||
|
||||
import { parse } from "@babel/parser";
|
||||
|
||||
type Node = { type: string; loc?: { start: { line: number } } } & Record<string, unknown>;
|
||||
export type Node = { type: string; loc?: { start: { line: number } } } & Record<string, unknown>;
|
||||
|
||||
// The line these tables draw: does a person's action on the page content
|
||||
// produce it? Scrolling, wheeling, pointing, dragging and selecting do.
|
||||
// Resizing the window, focusing it, going offline, a transition ending and a
|
||||
// media query flipping do not — those are the environment's.
|
||||
// The components whose own internals are not entry points: a person acts on
|
||||
// the call site that hands them a handler, not on the button inside them.
|
||||
//
|
||||
// One declaration, because there were two. The vitest gate and the census tool
|
||||
// each kept a list of the same fact, and adding Seg to only one of them read
|
||||
// as "an input proven to write the host that the product names no action for"
|
||||
// — a seal breaking on a bookkeeping slip rather than on a real gap.
|
||||
//
|
||||
// A callback here is certified, not guessed. `onPick` is an interaction
|
||||
// because Menu says so and its body raises one, never because the name starts
|
||||
// with "on": that spelling test is what left every Picker call site outside
|
||||
// the interaction universe, model switching among them. The event named is
|
||||
// the DOM event inside the component that ends up calling the prop, and
|
||||
// primitives.test.ts holds each declaration to a body that really reaches it.
|
||||
export interface Primitive {
|
||||
/** Path within src/, however the caller spells the rest of it. */
|
||||
file: string;
|
||||
/** Callback prop → the DOM event inside this component that raises it. A
|
||||
* prop absent here is not an input: a primitive forwards what it declares
|
||||
* and nothing else. */
|
||||
callbacks: Record<string, string>;
|
||||
}
|
||||
|
||||
export const PRIMITIVES: Primitive[] = [
|
||||
{ file: "ui/Switch.tsx", callbacks: { onClick: "click" } },
|
||||
{ file: "ui/Menu.tsx", callbacks: { onPick: "click" } },
|
||||
{ file: "ui/Seg.tsx", callbacks: { onClick: "click" } },
|
||||
];
|
||||
|
||||
/** A primitive is the file, wherever the tree being scanned is rooted. The
|
||||
* callers key their trees three different ways — the gate relative to the
|
||||
* package, the census absolute, and the fixture corpus under a root that is
|
||||
* not src at all — so this matches the path's tail rather than assuming one
|
||||
* of them. Anchoring it to a literal "/src/" quietly un-certified the fixture
|
||||
* corpus's own Switch, and the frozen reports caught it: two call sites lost
|
||||
* their identity to one root inside the component, which is the exact rule
|
||||
* that corpus exists to hold. */
|
||||
export const primitiveAt = (list: Primitive[], path: string): Primitive | undefined =>
|
||||
list.find((p) => path === p.file || path.endsWith("/" + p.file));
|
||||
|
||||
export const USER_INPUT = new Set([
|
||||
"click", "dblclick", "auxclick", "contextmenu", "keydown", "keyup", "keypress",
|
||||
"pointerdown", "pointerup", "pointermove", "pointercancel", "mousedown", "mouseup",
|
||||
@@ -135,8 +175,12 @@ export interface Census {
|
||||
refused: Note[];
|
||||
}
|
||||
export interface Options {
|
||||
skip?: (path: string) => boolean;
|
||||
resolve?: (from: string, spec: string) => string | null;
|
||||
/** Which components are certified interaction primitives, and which of their
|
||||
* callbacks carry a person's input. Defaults to the one declaration both
|
||||
* callers share; passing a list is for fixtures that need to state a
|
||||
* contract, never for a caller keeping a second copy of this one. */
|
||||
primitives?: Primitive[];
|
||||
}
|
||||
|
||||
/** Sources, or trees already parsed by the caller. A second reader that parses
|
||||
@@ -430,7 +474,10 @@ export function certifyRoots(sources: Map<string, string | Node>, opts: Options
|
||||
|
||||
|
||||
for (const [path, tree] of trees) {
|
||||
const skipped = opts.skip?.(path) ?? false;
|
||||
const certified = opts.primitives ?? PRIMITIVES;
|
||||
// A primitive's own body is not where its input is performed, so its
|
||||
// handlers are not roots — the call site that hands it the callback is.
|
||||
const skipped = !!primitiveAt(certified, path);
|
||||
// 1. JSX handlers. One root per handler, so membership is per handler too:
|
||||
// a generic data-action on an element carrying several of them cannot say
|
||||
// which input it names, and naming all of them called typing in a box the
|
||||
@@ -454,10 +501,15 @@ export function certifyRoots(sources: Map<string, string | Node>, opts: Options
|
||||
// where its interaction lives, and a prop spelled onClick is not proof.
|
||||
const el = jsxName(n.name as Node);
|
||||
let abstraction: "host" | "primitive" | "wiring" | "unknown" = "host";
|
||||
// Set only for a certified primitive: the contract that says which of
|
||||
// its props are inputs, so this does not fall back to reading names.
|
||||
let contract: Primitive | undefined;
|
||||
if (/^[A-Z]/.test(el)) {
|
||||
const imp = importsOf.get(path)?.get(el);
|
||||
const decl = imp?.file ?? (declaresLocally(trees.get(path)!, imp?.name ?? el) ? path : null);
|
||||
abstraction = decl === null ? "unknown" : (opts.skip?.(decl) ?? false) ? "primitive" : "wiring";
|
||||
const prim = decl === null ? undefined : primitiveAt(certified, decl);
|
||||
contract = prim;
|
||||
abstraction = decl === null ? "unknown" : prim ? "primitive" : "wiring";
|
||||
}
|
||||
const a: Record<string, unknown> = {};
|
||||
for (const at of (n.attributes as Node[])) {
|
||||
@@ -471,8 +523,12 @@ export function certifyRoots(sources: Map<string, string | Node>, opts: Options
|
||||
: c.type === "JSXExpressionContainer" ? firstString(c.expression) ?? "" : "")
|
||||
.filter(Boolean).join(" ").replace(/\s+/g, " ").slice(0, 60)
|
||||
: "";
|
||||
// A host element's props are the DOM's, so the name is the event. A
|
||||
// certified primitive's are not: what `onPick` raises is Menu's to
|
||||
// declare, and a prop it does not declare is not an input however it
|
||||
// is spelled — Picker takes an onHover that opens nothing.
|
||||
const handlers = Object.keys(a)
|
||||
.map((name) => ({ name, event: eventOfProp(name) }))
|
||||
.map((name) => ({ name, event: contract ? contract.callbacks[name] ?? null : eventOfProp(name) }))
|
||||
.filter((h): h is { name: string; event: string } => !!h.event && USER_INPUT.has(h.event));
|
||||
if (handlers.length && abstraction !== "host" && abstraction !== "primitive") {
|
||||
const note = { path, line: n.loc!.start.line, why: abstraction === "wiring" ? "COMPONENT_PROP_WIRING" : "UNRESOLVED_INTERACTION_ABSTRACTION", detail: "<" + el + " " + handlers.map((h) => h.name).join(" ") + ">" };
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
// depending on each other.
|
||||
import { certifyRoots } from "../../src/ui/roots.ts";
|
||||
import { productFiles, resolveSpec, trees } from "./source.mjs";
|
||||
import { PRIMITIVES } from "./sites.mjs";
|
||||
|
||||
// Which files are primitives is not this tool's to decide: certifyRoots reads
|
||||
// the one declaration, so there is no list here to drift from the gate's.
|
||||
const census = certifyRoots(new Map([...trees].filter(([p]) => productFiles.has(p))), {
|
||||
skip: (p) => PRIMITIVES.has(p),
|
||||
resolve: (from, spec) => resolveSpec(from, spec),
|
||||
});
|
||||
|
||||
|
||||
@@ -6,12 +6,6 @@ import { bodyOf } from "./component_identity.mjs";
|
||||
|
||||
const HOPS = 24;
|
||||
const ROLES = new Set(["tab", "option", "radio", "switch", "menuitem", "checkbox", "button", "menuitemradio", "treeitem"]);
|
||||
const CTL = new Set(["Switch", "Menu"]);
|
||||
// Declared against the tree being scanned, not against one working directory.
|
||||
// As two literals these matched only when the census ran on `src` from the
|
||||
// package root: scanning a copy elsewhere silently counted the primitives' own
|
||||
// handlers as three extra interaction roots.
|
||||
const PRIMITIVES = new Set(["Switch.tsx", "Menu.tsx", "Seg.tsx"].map((f) => join(SRC, "ui", f)));
|
||||
// Callees whose implementation is outside the module graph: the platform. Not
|
||||
// a list of helpers believed pure — those are decided by the fixpoint.
|
||||
const PLATFORM = /^(String|Number|Boolean|Object|Array|JSON|Math|Date|Promise|Set|Map|console|window|document|navigator|localStorage|sessionStorage|requestAnimationFrame|cancelAnimationFrame|setTimeout|clearTimeout|setInterval|clearInterval|structuredClone|encodeURIComponent|decodeURIComponent|alert|confirm|prompt|matchMedia|getComputedStyle|URL|Blob|FileReader|IntersectionObserver|ResizeObserver|MutationObserver|AbortController|crypto|performance|queueMicrotask|isNaN|parseInt|parseFloat)$/;
|
||||
@@ -74,4 +68,4 @@ const localsOf = (path) => {
|
||||
return m;
|
||||
};
|
||||
|
||||
export { CTL, HOPS, PLATFORM, PRIMITIVES, ROLES, declares, localsCache, localsOf, renders };
|
||||
export { HOPS, PLATFORM, ROLES, declares, localsCache, localsOf, renders };
|
||||
Reference in new issue
Block a user