gbs-add-block 2.0.4 → 2.3.0
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.
- package/.gbs/skills/gbs-components/SKILL.md +190 -134
- package/.gbs/skills/gbs-components/references/install.md +25 -3
- package/.gbs/skills/gbs-components/references/styling.md +246 -201
- package/CHANGELOG.md +74 -0
- package/README.md +157 -11
- package/index.cjs +212 -3
- package/package.json +41 -10
- package/schema/passport-v1.schema.json +204 -0
- package/source/beta-components/accordion/passport.json +259 -0
- package/source/beta-components/accordion/styles.css +207 -208
- package/source/beta-components/alert/passport.json +250 -0
- package/source/beta-components/alert/styles.css +154 -155
- package/source/beta-components/avatar/passport.json +294 -0
- package/source/beta-components/avatar/styles.css +201 -203
- package/source/beta-components/badge/passport.json +332 -0
- package/source/beta-components/badge/styles.css +203 -204
- package/source/beta-components/breadcrumb/passport.json +243 -0
- package/source/beta-components/breadcrumb/styles.css +138 -139
- package/source/beta-components/button/passport.json +402 -0
- package/source/beta-components/button/passport.manual.json +31 -0
- package/source/beta-components/button/styles.css +232 -233
- package/source/beta-components/card/passport.json +337 -0
- package/source/beta-components/card/styles.css +230 -231
- package/source/beta-components/checkbox/passport.json +456 -0
- package/source/beta-components/checkbox/styles.css +211 -212
- package/source/beta-components/combobox/passport.json +456 -0
- package/source/beta-components/combobox/styles.css +419 -417
- package/source/beta-components/data-grid/agent/coerce.ts +368 -0
- package/source/beta-components/data-grid/agent/contract.ts +410 -0
- package/source/beta-components/data-grid/agent/dataset.ts +92 -0
- package/source/beta-components/data-grid/agent/engine.ts +470 -0
- package/source/beta-components/data-grid/agent/executors.ts +155 -0
- package/source/beta-components/data-grid/agent/index.ts +79 -0
- package/source/beta-components/data-grid/agent/intent.ts +324 -0
- package/source/beta-components/data-grid/agent/operations.ts +335 -0
- package/source/beta-components/data-grid/agent/validate.ts +630 -0
- package/source/beta-components/data-grid/agent/webmcp.ts +107 -0
- package/source/beta-components/data-grid/index.ts +14 -7
- package/source/beta-components/data-grid/passport.json +1051 -0
- package/source/beta-components/data-grid/passport.manual.json +255 -0
- package/source/beta-components/data-grid/react/AskGrid.tsx +164 -0
- package/source/beta-components/data-grid/react/DataGrid.tsx +39 -0
- package/source/beta-components/data-grid/styles.css +874 -717
- package/source/beta-components/date-picker/passport.json +407 -0
- package/source/beta-components/date-picker/styles.css +445 -446
- package/source/beta-components/dialog/passport.json +344 -0
- package/source/beta-components/dialog/styles.css +280 -278
- package/source/beta-components/file-uploader/passport.json +518 -0
- package/source/beta-components/file-uploader/styles.css +394 -395
- package/source/beta-components/input/passport.json +536 -0
- package/source/beta-components/input/styles.css +295 -296
- package/source/beta-components/menu/passport.json +322 -0
- package/source/beta-components/menu/styles.css +224 -222
- package/source/beta-components/modal/passport.json +289 -0
- package/source/beta-components/modal/styles.css +241 -239
- package/source/beta-components/number-input/passport.json +541 -0
- package/source/beta-components/number-input/styles.css +230 -231
- package/source/beta-components/popover/passport.json +238 -0
- package/source/beta-components/popover/styles.css +148 -146
- package/source/beta-components/progress/passport.json +270 -0
- package/source/beta-components/progress/styles.css +200 -202
- package/source/beta-components/radio-group/passport.json +477 -0
- package/source/beta-components/radio-group/styles.css +241 -242
- package/source/beta-components/shared/core/agent/adapter.ts +65 -0
- package/source/beta-components/shared/core/agent/history.ts +120 -0
- package/source/beta-components/shared/core/agent/index.ts +46 -0
- package/source/beta-components/shared/core/agent/numbers.ts +217 -0
- package/source/beta-components/shared/core/agent/schema.ts +180 -0
- package/source/beta-components/shared/core/agent/types.ts +169 -0
- package/source/beta-components/shared/core/agent/webmcp.ts +328 -0
- package/source/beta-components/shared/index.ts +9 -0
- package/source/beta-components/shared/react/GramproAIProvider.tsx +50 -0
- package/source/beta-components/shared/react/useAskAgent.ts +217 -0
- package/source/beta-components/shared/styles.css +79 -0
- package/source/beta-components/shared/version.json +4 -4
- package/source/beta-components/shared/version.ts +6 -6
- package/source/beta-components/skeleton/passport.json +251 -0
- package/source/beta-components/skeleton/styles.css +185 -186
- package/source/beta-components/spinner/passport.json +245 -0
- package/source/beta-components/spinner/styles.css +145 -146
- package/source/beta-components/switch/passport.json +421 -0
- package/source/beta-components/switch/styles.css +194 -196
- package/source/beta-components/tabs/passport.json +315 -0
- package/source/beta-components/tabs/styles.css +234 -235
- package/source/beta-components/textarea/passport.json +382 -0
- package/source/beta-components/textarea/styles.css +158 -159
- package/source/beta-components/toaster/passport.json +221 -0
- package/source/beta-components/toaster/styles.css +282 -283
- package/source/beta-components/tooltip/passport.json +170 -0
- package/source/beta-components/tooltip/styles.css +71 -72
- package/tools/env.cjs +61 -0
- package/tools/passport/cli.cjs +79 -0
- package/tools/passport/extract.cjs +493 -0
- package/tools/passport/index.cjs +185 -0
- package/tools/passport/merge.cjs +131 -0
- package/tools/passport/policy.cjs +65 -0
- package/tools/passport/validate.cjs +277 -0
- package/tools/ts-require.cjs +79 -0
- package/source/beta-components/accordion/__tests__/core.test.ts +0 -58
- package/source/beta-components/alert/__tests__/core.test.ts +0 -17
- package/source/beta-components/avatar/__tests__/core.test.ts +0 -88
- package/source/beta-components/badge/__tests__/core.test.ts +0 -46
- package/source/beta-components/breadcrumb/__tests__/core.test.ts +0 -58
- package/source/beta-components/button/__tests__/core.test.ts +0 -31
- package/source/beta-components/card/__tests__/core.test.ts +0 -57
- package/source/beta-components/checkbox/__tests__/core.test.ts +0 -40
- package/source/beta-components/combobox/__tests__/core.test.ts +0 -134
- package/source/beta-components/data-grid/__tests__/core.test.ts +0 -356
- package/source/beta-components/data-grid/__tests__/export.test.ts +0 -70
- package/source/beta-components/data-grid/__tests__/pdf.test.ts +0 -209
- package/source/beta-components/date-picker/__tests__/core.test.ts +0 -273
- package/source/beta-components/dialog/__tests__/core.test.ts +0 -86
- package/source/beta-components/file-uploader/__tests__/core.test.ts +0 -395
- package/source/beta-components/input/__tests__/core.test.ts +0 -75
- package/source/beta-components/menu/__tests__/core.test.ts +0 -120
- package/source/beta-components/modal/__tests__/core.test.ts +0 -55
- package/source/beta-components/number-input/__tests__/core.test.ts +0 -151
- package/source/beta-components/progress/__tests__/core.test.ts +0 -56
- package/source/beta-components/radio-group/__tests__/core.test.ts +0 -64
- package/source/beta-components/shared/__tests__/boundaries.test.ts +0 -95
- package/source/beta-components/shared/__tests__/core.test.ts +0 -55
- package/source/beta-components/shared/__tests__/position.test.ts +0 -143
- package/source/beta-components/skeleton/__tests__/core.test.ts +0 -41
- package/source/beta-components/spinner/__tests__/core.test.ts +0 -48
- package/source/beta-components/switch/__tests__/core.test.ts +0 -64
- package/source/beta-components/tabs/__tests__/core.test.ts +0 -51
- package/source/beta-components/textarea/__tests__/core.test.ts +0 -38
- package/source/beta-components/toaster/__tests__/core.test.ts +0 -256
- package/source/beta-components/tooltip/__tests__/core.test.ts +0 -42
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Passport merge: derived <- manual <- local.
|
|
3
|
+
*
|
|
4
|
+
* derived generator output; regenerated from source every run
|
|
5
|
+
* manual passport.manual.json, authored by the library, never overwritten
|
|
6
|
+
* local passport.local.json, owned by the consuming developer, never
|
|
7
|
+
* written by the CLI
|
|
8
|
+
*
|
|
9
|
+
* Precedence is explicit per field rather than "deep merge everything", because
|
|
10
|
+
* some fields must always come from source. A manual file that could redefine a
|
|
11
|
+
* prop's type would make the passport lie about the code.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Fields that are ALWAYS regenerated. Manual/local may annotate, never replace. */
|
|
15
|
+
const SOURCE_OWNED = new Set(["passportVersion", "source", "inherits", "events", "slots", "states"]);
|
|
16
|
+
|
|
17
|
+
/** Fields that only manual/local provide. The generator emits empty shells. */
|
|
18
|
+
const AUTHORED = new Set([
|
|
19
|
+
"accessibility",
|
|
20
|
+
"composition",
|
|
21
|
+
"operations",
|
|
22
|
+
"intentDomains",
|
|
23
|
+
"safeMutations",
|
|
24
|
+
"examples",
|
|
25
|
+
"purpose",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* Keys that mean something to JavaScript's object model rather than to a
|
|
30
|
+
* passport. `JSON.parse` hands `__proto__` back as an ordinary own property,
|
|
31
|
+
* but `out[k] = v` would run Object.prototype's setter and reparent the
|
|
32
|
+
* merged object. No passport field needs these names, so an overlay carrying
|
|
33
|
+
* one has it dropped here and flagged as an error by `validateReferences` —
|
|
34
|
+
* dropped silently would be the worse half of that pair.
|
|
35
|
+
*/
|
|
36
|
+
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
37
|
+
|
|
38
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
39
|
+
|
|
40
|
+
function deepMerge(base, overlay) {
|
|
41
|
+
if (!isObject(base) || !isObject(overlay)) return overlay === undefined ? base : overlay;
|
|
42
|
+
const out = { ...base };
|
|
43
|
+
for (const [k, v] of Object.entries(overlay)) {
|
|
44
|
+
if (UNSAFE_KEYS.has(k)) continue;
|
|
45
|
+
out[k] = isObject(v) && isObject(base[k]) ? deepMerge(base[k], v) : v;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Overlay prop annotations by name. An overlay entry may add `description`,
|
|
52
|
+
* `note` or `deprecated` to a derived prop, and may introduce a wholly new prop
|
|
53
|
+
* (a locally added one) — which is marked with the overlay's origin.
|
|
54
|
+
*/
|
|
55
|
+
function mergeProps(derived, overlayProps, origin) {
|
|
56
|
+
if (!Array.isArray(overlayProps) || overlayProps.length === 0) return derived;
|
|
57
|
+
const byName = new Map(derived.map((p) => [p.name, { ...p }]));
|
|
58
|
+
|
|
59
|
+
for (const entry of overlayProps) {
|
|
60
|
+
if (!entry || typeof entry.name !== "string") continue;
|
|
61
|
+
if (UNSAFE_KEYS.has(entry.name)) continue;
|
|
62
|
+
const existing = byName.get(entry.name);
|
|
63
|
+
if (existing) {
|
|
64
|
+
// Annotate only. Type/required/origin stay source-derived.
|
|
65
|
+
const { name, type, values, signature, typeText, required, origin: _o, ...annotations } = entry;
|
|
66
|
+
Object.assign(existing, annotations);
|
|
67
|
+
if (annotations.description) existing.descriptionOrigin = origin;
|
|
68
|
+
} else {
|
|
69
|
+
byName.set(entry.name, { ...entry, origin });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Produce the effective passport written to passport.json.
|
|
77
|
+
*
|
|
78
|
+
* @param {object} derived generator output
|
|
79
|
+
* @param {object} manual passport.manual.json contents, or {}
|
|
80
|
+
* @param {object} local passport.local.json contents, or {} (consumer side)
|
|
81
|
+
*/
|
|
82
|
+
function mergePassport(derived, manual = {}, local = {}) {
|
|
83
|
+
let out = { ...derived };
|
|
84
|
+
|
|
85
|
+
for (const [overlay, origin] of [
|
|
86
|
+
[manual, "manual"],
|
|
87
|
+
[local, "local"],
|
|
88
|
+
]) {
|
|
89
|
+
if (!overlay || Object.keys(overlay).length === 0) continue;
|
|
90
|
+
|
|
91
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
92
|
+
if (key.startsWith("$") || key === "component") continue;
|
|
93
|
+
if (UNSAFE_KEYS.has(key)) continue; // validator reports it
|
|
94
|
+
if (SOURCE_OWNED.has(key)) continue; // silently ignored; validator reports it
|
|
95
|
+
|
|
96
|
+
if (key === "props") {
|
|
97
|
+
out.props = mergeProps(out.props, value, origin);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (key === "propNotes" && isObject(value)) {
|
|
101
|
+
out.props = out.props.map((p) =>
|
|
102
|
+
value[p.name] ? { ...p, note: value[p.name] } : p,
|
|
103
|
+
);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (AUTHORED.has(key)) {
|
|
107
|
+
out[key] = isObject(out[key]) && isObject(value) ? deepMerge(out[key], value) : value;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
out[key] = isObject(out[key]) && isObject(value) ? deepMerge(out[key], value) : value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Coverage is recomputed after overlays, since manual descriptions count.
|
|
115
|
+
const described = out.props.filter((p) => p.description).length;
|
|
116
|
+
const authoredSections = [...AUTHORED].filter((k) => {
|
|
117
|
+
const v = out[k];
|
|
118
|
+
return Array.isArray(v) ? v.length > 0 : isObject(v) ? Object.keys(v).length > 0 : Boolean(v);
|
|
119
|
+
}).sort();
|
|
120
|
+
|
|
121
|
+
out.coverage = {
|
|
122
|
+
propsTotal: out.props.length,
|
|
123
|
+
propsDescribed: described,
|
|
124
|
+
describedPct: out.props.length ? Math.round((described / out.props.length) * 100) : 0,
|
|
125
|
+
authoredSections,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { mergePassport, deepMerge, mergeProps, SOURCE_OWNED, AUTHORED, UNSAFE_KEYS };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Which inherited (library) properties are worth surfacing inline.
|
|
3
|
+
*
|
|
4
|
+
* A component like Button extends `ButtonHTMLAttributes<HTMLButtonElement>`,
|
|
5
|
+
* which resolves to several hundred DOM properties. Enumerating them buries the
|
|
6
|
+
* real API; omitting them entirely hides props people genuinely pass. So the
|
|
7
|
+
* generator keeps the inheritance edge AND inlines a short, per-element list.
|
|
8
|
+
*
|
|
9
|
+
* This file is policy only. It states what is WORTH surfacing; the extractor
|
|
10
|
+
* emits an entry only when the TypeChecker confirms the property actually
|
|
11
|
+
* exists on that component's resolved type. Nothing here is assumed.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Surfaced for every element kind, when present. */
|
|
15
|
+
const COMMON = ["className", "style", "id", "title", "tabIndex", "role", "hidden"];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Keyed by the React attributes interface a component extends. The extractor
|
|
19
|
+
* reads the resolved base type name and looks it up here; an unknown base gets
|
|
20
|
+
* COMMON only.
|
|
21
|
+
*/
|
|
22
|
+
const BY_BASE = {
|
|
23
|
+
ButtonHTMLAttributes: ["disabled", "type", "form", "name", "value", "autoFocus"],
|
|
24
|
+
InputHTMLAttributes: [
|
|
25
|
+
"disabled", "readOnly", "required", "placeholder", "name", "type", "value",
|
|
26
|
+
"defaultValue", "checked", "defaultChecked", "autoComplete", "autoFocus",
|
|
27
|
+
"maxLength", "minLength", "min", "max", "step", "pattern", "inputMode", "form",
|
|
28
|
+
],
|
|
29
|
+
TextareaHTMLAttributes: [
|
|
30
|
+
"disabled", "readOnly", "required", "placeholder", "name", "value",
|
|
31
|
+
"defaultValue", "rows", "cols", "maxLength", "autoFocus", "form",
|
|
32
|
+
],
|
|
33
|
+
SelectHTMLAttributes: ["disabled", "required", "name", "value", "defaultValue", "multiple", "form"],
|
|
34
|
+
FormHTMLAttributes: ["action", "method", "noValidate", "name"],
|
|
35
|
+
AnchorHTMLAttributes: ["href", "target", "rel", "download"],
|
|
36
|
+
ImgHTMLAttributes: ["src", "alt", "loading", "width", "height"],
|
|
37
|
+
DialogHTMLAttributes: ["open"],
|
|
38
|
+
FieldsetHTMLAttributes: ["disabled", "form", "name"],
|
|
39
|
+
LabelHTMLAttributes: ["htmlFor", "form"],
|
|
40
|
+
HTMLAttributes: [],
|
|
41
|
+
// `children` is meaningful on every container; the checker decides.
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** ARIA props are never bulk-inlined — too many — but these carry real meaning. */
|
|
45
|
+
const ARIA = ["aria-label", "aria-labelledby", "aria-describedby"];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The names worth surfacing for a component, given the external base types its
|
|
49
|
+
* Props interface extends. Returns a de-duplicated, stable-ordered list.
|
|
50
|
+
*
|
|
51
|
+
* @param {string[]} baseNames e.g. ["ButtonHTMLAttributes"]
|
|
52
|
+
* @returns {string[]}
|
|
53
|
+
*/
|
|
54
|
+
function allowlistFor(baseNames) {
|
|
55
|
+
const out = [...COMMON];
|
|
56
|
+
for (const base of baseNames) {
|
|
57
|
+
for (const name of BY_BASE[base] ?? []) {
|
|
58
|
+
if (!out.includes(name)) out.push(name);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const name of ARIA) if (!out.includes(name)) out.push(name);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { COMMON, BY_BASE, ARIA, allowlistFor };
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Passport validation.
|
|
3
|
+
*
|
|
4
|
+
* Two layers, both returning typed issues rather than throwing:
|
|
5
|
+
*
|
|
6
|
+
* structural the passport matches Passport v1's shape
|
|
7
|
+
* referential the authored overlays point at things that still exist
|
|
8
|
+
*
|
|
9
|
+
* A dependency-free structural check is used deliberately: the schema is ours,
|
|
10
|
+
* and adding a JSON Schema runtime would be a dependency the source-first model
|
|
11
|
+
* does not want. `schema/passport-v1.schema.json` is still published so external
|
|
12
|
+
* consumers can validate with whatever they like.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const issue = (level, code, component, message, detail) => ({
|
|
16
|
+
level, code, component, message, ...(detail ? { detail } : {}),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
20
|
+
const PROP_TYPES = new Set(["string", "number", "boolean", "enum", "function", "other"]);
|
|
21
|
+
const ORIGINS = new Set(["derived", "inherited", "manual", "local"]);
|
|
22
|
+
const DESCRIPTION_ORIGINS = new Set(["jsdoc", "readme", "manual", "local"]);
|
|
23
|
+
|
|
24
|
+
/** Structural validation against Passport v1. */
|
|
25
|
+
function validateStructure(passport, component) {
|
|
26
|
+
const out = [];
|
|
27
|
+
const need = (cond, code, message, detail) => {
|
|
28
|
+
if (!cond) out.push(issue("error", code, component, message, detail));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
need(passport.passportVersion === "1.0.0", "bad-version",
|
|
32
|
+
`passportVersion must be "1.0.0", got ${JSON.stringify(passport.passportVersion)}`);
|
|
33
|
+
|
|
34
|
+
need(isObject(passport.identity) && typeof passport.identity.component === "string",
|
|
35
|
+
"bad-identity", "identity.component is required");
|
|
36
|
+
need(isObject(passport.source) && typeof passport.source.sourceHash === "string",
|
|
37
|
+
"bad-source", "source.sourceHash is required");
|
|
38
|
+
need(Array.isArray(passport.source?.files) && passport.source.files.length > 0,
|
|
39
|
+
"bad-source-files", "source.files must be a non-empty array");
|
|
40
|
+
|
|
41
|
+
need(Array.isArray(passport.props), "bad-props", "props must be an array");
|
|
42
|
+
if (Array.isArray(passport.props)) {
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
for (const p of passport.props) {
|
|
45
|
+
if (!isObject(p) || typeof p.name !== "string") {
|
|
46
|
+
out.push(issue("error", "bad-prop", component, "every prop needs a string name"));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (seen.has(p.name)) out.push(issue("error", "duplicate-prop", component, `duplicate prop "${p.name}"`));
|
|
50
|
+
seen.add(p.name);
|
|
51
|
+
if (!PROP_TYPES.has(p.type))
|
|
52
|
+
out.push(issue("error", "bad-prop-type", component, `prop "${p.name}" has unknown type ${JSON.stringify(p.type)}`));
|
|
53
|
+
if (typeof p.required !== "boolean")
|
|
54
|
+
out.push(issue("error", "bad-prop-required", component, `prop "${p.name}" needs a boolean required`));
|
|
55
|
+
if (!ORIGINS.has(p.origin))
|
|
56
|
+
out.push(issue("error", "bad-origin", component, `prop "${p.name}" has unknown origin ${JSON.stringify(p.origin)}`));
|
|
57
|
+
if (p.type === "enum" && !Array.isArray(p.values))
|
|
58
|
+
out.push(issue("error", "enum-without-values", component, `enum prop "${p.name}" has no values`));
|
|
59
|
+
if (p.description !== undefined && !DESCRIPTION_ORIGINS.has(p.descriptionOrigin))
|
|
60
|
+
out.push(issue("error", "description-without-origin", component,
|
|
61
|
+
`prop "${p.name}" has a description but no valid descriptionOrigin`));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (passport.inherits !== undefined) {
|
|
66
|
+
need(Array.isArray(passport.inherits), "bad-inherits", "inherits must be an array");
|
|
67
|
+
for (const edge of passport.inherits ?? []) {
|
|
68
|
+
if (!isObject(edge) || typeof edge.from !== "string")
|
|
69
|
+
out.push(issue("error", "bad-inherits-edge", component, "each inherits entry needs a string `from`"));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const key of ["events", "slots", "states", "intentDomains", "examples"]) {
|
|
74
|
+
need(Array.isArray(passport[key]), `bad-${key}`, `${key} must be an array`);
|
|
75
|
+
}
|
|
76
|
+
for (const key of ["accessibility", "composition", "operations", "safeMutations", "coverage"]) {
|
|
77
|
+
need(isObject(passport[key]), `bad-${key}`, `${key} must be an object`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
|
|
84
|
+
|
|
85
|
+
/** Object-model keys anywhere in an overlay, by their path. */
|
|
86
|
+
function unsafeKeysIn(value, path = "") {
|
|
87
|
+
if (Array.isArray(value)) return value.flatMap((item, i) => unsafeKeysIn(item, `${path}[${i}]`));
|
|
88
|
+
if (!isObject(value)) return [];
|
|
89
|
+
const found = [];
|
|
90
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
91
|
+
const at = path ? `${path}.${key}` : key;
|
|
92
|
+
if (UNSAFE_KEYS.includes(key)) found.push(at);
|
|
93
|
+
else found.push(...unsafeKeysIn(value[key], at));
|
|
94
|
+
}
|
|
95
|
+
return found;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Referential validation: do the authored overlays still match the source?
|
|
100
|
+
* This is what catches a manual file left behind by a refactor.
|
|
101
|
+
*/
|
|
102
|
+
function validateReferences(effective, { manual = {}, local = {}, component, api = [] }) {
|
|
103
|
+
const out = [];
|
|
104
|
+
const propNames = new Set(effective.props.map((p) => p.name));
|
|
105
|
+
|
|
106
|
+
const checkPropRefs = (names, where, origin) => {
|
|
107
|
+
for (const name of names ?? []) {
|
|
108
|
+
if (!propNames.has(name)) {
|
|
109
|
+
out.push(issue("error", "missing-prop-reference", component,
|
|
110
|
+
`${origin} metadata references prop "${name}", which no longer exists in the source`,
|
|
111
|
+
{ where, prop: name }));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
for (const [overlay, origin] of [[manual, "manual"], [local, "local"]]) {
|
|
117
|
+
if (!overlay || Object.keys(overlay).length === 0) continue;
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
* `JSON.parse` returns `__proto__` as an ordinary own property, so an
|
|
121
|
+
* overlay can carry one. The merge drops it; this is what makes that
|
|
122
|
+
* visible, because a silent drop looks the same as a key that was never
|
|
123
|
+
* there.
|
|
124
|
+
*/
|
|
125
|
+
for (const key of unsafeKeysIn(overlay)) {
|
|
126
|
+
out.push(issue("error", "unsafe-key", component,
|
|
127
|
+
`${origin} metadata contains "${key}", which is a JavaScript object-model key ` +
|
|
128
|
+
`and not a passport field; it was ignored`, { where: key }));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
checkPropRefs(overlay.safeMutations?.allowed, "safeMutations.allowed", origin);
|
|
132
|
+
checkPropRefs(overlay.safeMutations?.forbidden, "safeMutations.forbidden", origin);
|
|
133
|
+
checkPropRefs(Object.keys(overlay.propNotes ?? {}), "propNotes", origin);
|
|
134
|
+
|
|
135
|
+
// Manual must not try to redefine source-owned facts.
|
|
136
|
+
for (const key of ["source", "inherits", "slots", "states", "events", "passportVersion"]) {
|
|
137
|
+
if (overlay[key] !== undefined) {
|
|
138
|
+
out.push(issue("error", "overrides-source-owned", component,
|
|
139
|
+
`${origin} metadata sets "${key}", which is always regenerated from source`,
|
|
140
|
+
{ where: key }));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Overlay props may annotate or add, but not contradict a derived type.
|
|
145
|
+
for (const p of overlay.props ?? []) {
|
|
146
|
+
const existing = effective.props.find((e) => e.name === p.name);
|
|
147
|
+
if (existing && p.type && p.type !== existing.type) {
|
|
148
|
+
out.push(issue("error", "contradicts-source", component,
|
|
149
|
+
`${origin} metadata gives prop "${p.name}" type "${p.type}" but the source says "${existing.type}"`,
|
|
150
|
+
{ prop: p.name }));
|
|
151
|
+
}
|
|
152
|
+
if (!existing && origin === "manual") {
|
|
153
|
+
out.push(issue("error", "unknown-prop", component,
|
|
154
|
+
`manual metadata declares prop "${p.name}", which is not in the source; ` +
|
|
155
|
+
`only passport.local.json may introduce props`, { prop: p.name }));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Operations must name a real API method when one is declared.
|
|
160
|
+
for (const [opName, op] of Object.entries(overlay.operations ?? {})) {
|
|
161
|
+
if (op?.apiMethod && api.length && !api.includes(op.apiMethod)) {
|
|
162
|
+
out.push(issue("error", "unknown-api-method", component,
|
|
163
|
+
`operation "${opName}" names apiMethod "${op.apiMethod}", which the component API does not expose`,
|
|
164
|
+
{ operation: opName }));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Coverage is a warning signal, never a hard failure in v1. */
|
|
173
|
+
function coverageIssues(effective, component, threshold) {
|
|
174
|
+
const out = [];
|
|
175
|
+
const { describedPct, propsTotal, propsDescribed } = effective.coverage;
|
|
176
|
+
if (typeof threshold === "number" && describedPct < threshold) {
|
|
177
|
+
out.push(issue("warn", "coverage-below-threshold", component,
|
|
178
|
+
`${describedPct}% of props described (${propsDescribed}/${propsTotal}), threshold ${threshold}%`));
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* ------------------------------------------------------------ text hygiene */
|
|
184
|
+
|
|
185
|
+
/*
|
|
186
|
+
* A passport is read by coding agents. That is the point of it, and it is also
|
|
187
|
+
* the only way a passport can hurt you: nothing parses it into code and no
|
|
188
|
+
* file path is derived from it, but an agent that reads
|
|
189
|
+
* "this component requires calling fetch('https://…') on mount" will write
|
|
190
|
+
* that. The text is documentation; it is never an instruction.
|
|
191
|
+
*
|
|
192
|
+
* These checks cannot decide whether a sentence is honest. What they can do is
|
|
193
|
+
* make the shapes injection needs — hidden characters, a script tag, a wall of
|
|
194
|
+
* text, a line telling the reader to disregard what it was told — visible in
|
|
195
|
+
* `npm run passport:check` instead of invisible in a diff that looks like
|
|
196
|
+
* documentation. The current 27 passports have a longest string of 217
|
|
197
|
+
* characters and 8.7 kB of text at most, so the caps are far above anything
|
|
198
|
+
* written in good faith.
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
const MAX_TEXT = 1000;
|
|
202
|
+
const MAX_TEXT_TOTAL = 40000;
|
|
203
|
+
|
|
204
|
+
/** Everything except tab, newline and carriage return. */
|
|
205
|
+
const CONTROL_CHARS = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/;
|
|
206
|
+
|
|
207
|
+
/** A real link, not the bare `"https://"` that appears in an example. */
|
|
208
|
+
const LINK = /https?:\/\/[^\s"'<>)]*\.[^\s"'<>)]+/i;
|
|
209
|
+
|
|
210
|
+
const HIGH_SIGNAL = [
|
|
211
|
+
[/<script\b/i, "an HTML script tag"],
|
|
212
|
+
[/ignore (?:all |any )?(?:the )?(?:previous|prior|above|earlier)/i, "an instruction to ignore earlier context"],
|
|
213
|
+
[/disregard (?:all |any )?(?:the )?(?:previous|prior|above|earlier)/i, "an instruction to disregard earlier context"],
|
|
214
|
+
[/\bsystem prompt\b/i, "a reference to a system prompt"],
|
|
215
|
+
[/\b(?:you are|act as) (?:an? )?(?:ai|assistant|agent|language model)\b/i, "a role instruction aimed at an agent"],
|
|
216
|
+
[/\bnew instructions?\b/i, "a claim of new instructions"],
|
|
217
|
+
[/\bdo not (?:tell|mention|inform|reveal)\b/i, "an instruction to conceal something"],
|
|
218
|
+
[/\bexfiltrat/i, "exfiltration"],
|
|
219
|
+
[/\bcurl\s+https?:/i, "a shell download"],
|
|
220
|
+
[/\bnpm (?:install|i|exec)\s/i, "a package installation"],
|
|
221
|
+
[/\bchild_process\b|\bprocess\.env\b|\beval\(/i, "code that reaches outside the component"],
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
/** Every string in the passport, with a readable path to it. */
|
|
225
|
+
function walkStrings(value, path, visit) {
|
|
226
|
+
if (typeof value === "string") visit(value, path);
|
|
227
|
+
else if (Array.isArray(value)) value.forEach((item, i) => walkStrings(item, `${path}[${i}]`, visit));
|
|
228
|
+
else if (isObject(value)) {
|
|
229
|
+
for (const [key, item] of Object.entries(value)) walkStrings(item, path ? `${path}.${key}` : key, visit);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Length, hidden characters, and the phrasings injection needs. */
|
|
234
|
+
function textIssues(passport, component) {
|
|
235
|
+
const out = [];
|
|
236
|
+
let total = 0;
|
|
237
|
+
|
|
238
|
+
walkStrings(passport, "", (text, where) => {
|
|
239
|
+
total += text.length;
|
|
240
|
+
|
|
241
|
+
if (text.length > MAX_TEXT) {
|
|
242
|
+
out.push(issue("error", "text-too-long", component,
|
|
243
|
+
`${where} is ${text.length} characters; the limit is ${MAX_TEXT}`, { where }));
|
|
244
|
+
}
|
|
245
|
+
if (CONTROL_CHARS.test(text)) {
|
|
246
|
+
out.push(issue("error", "control-characters", component,
|
|
247
|
+
`${where} contains control characters, which can hide text from a reader`, { where }));
|
|
248
|
+
}
|
|
249
|
+
for (const [pattern, what] of HIGH_SIGNAL) {
|
|
250
|
+
if (pattern.test(text)) {
|
|
251
|
+
out.push(issue("error", "suspicious-text", component,
|
|
252
|
+
`${where} contains ${what}. Passport text is documentation an agent reads; ` +
|
|
253
|
+
`it must not instruct one.`, { where }));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (text.includes("```")) {
|
|
257
|
+
out.push(issue("warn", "code-fence", component,
|
|
258
|
+
`${where} contains a fenced code block, which an agent may copy verbatim`, { where }));
|
|
259
|
+
}
|
|
260
|
+
if (LINK.test(text)) {
|
|
261
|
+
out.push(issue("warn", "contains-link", component,
|
|
262
|
+
`${where} contains a link; an agent may follow it`, { where }));
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
if (total > MAX_TEXT_TOTAL) {
|
|
267
|
+
out.push(issue("error", "text-budget-exceeded", component,
|
|
268
|
+
`${total} characters of text across the passport; the limit is ${MAX_TEXT_TOTAL}`));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = {
|
|
275
|
+
validateStructure, validateReferences, coverageIssues, textIssues, issue,
|
|
276
|
+
MAX_TEXT, MAX_TEXT_TOTAL,
|
|
277
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Lets a CommonJS tool `require()` the library's TypeScript directly.
|
|
3
|
+
*
|
|
4
|
+
* The evaluation harness has to run the *real* runtime — the same contract
|
|
5
|
+
* emitter, the same validator, the same coercion — because a harness that
|
|
6
|
+
* reimplements any of that is measuring itself. The runtime is TypeScript and
|
|
7
|
+
* the harness is a CLI, so something has to bridge them.
|
|
8
|
+
*
|
|
9
|
+
* Three options were available: add a loader dependency (tsx, jiti), compile
|
|
10
|
+
* to a build directory, or transpile on demand with the compiler that is
|
|
11
|
+
* already a devDependency for the passport generator. The third adds nothing
|
|
12
|
+
* to install and nothing to keep in sync, so that is this file.
|
|
13
|
+
*
|
|
14
|
+
* Transpile-only, like `isolatedModules`: no type checking happens here. That
|
|
15
|
+
* is fine — `tsc --noEmit` and the test suite do the checking, and doing it
|
|
16
|
+
* twice would only make the CLI slow.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require("fs");
|
|
20
|
+
const path = require("path");
|
|
21
|
+
const Module = require("module");
|
|
22
|
+
|
|
23
|
+
let registered = false;
|
|
24
|
+
|
|
25
|
+
/** Resolve the same way a bundler would: bare path, then `.ts`, then `/index.ts`. */
|
|
26
|
+
function resolveTs(request, parent) {
|
|
27
|
+
if (!request.startsWith(".") && !path.isAbsolute(request)) return null;
|
|
28
|
+
const base = path.resolve(path.dirname(parent?.filename ?? process.cwd()), request);
|
|
29
|
+
for (const candidate of [base, `${base}.ts`, `${base}.tsx`, path.join(base, "index.ts")]) {
|
|
30
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function register() {
|
|
36
|
+
if (registered) return;
|
|
37
|
+
registered = true;
|
|
38
|
+
|
|
39
|
+
let ts;
|
|
40
|
+
try {
|
|
41
|
+
ts = require("typescript");
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"The evaluation harness needs the TypeScript compiler to load the runtime. " +
|
|
45
|
+
"Run `npm install` at the repository root.",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const compile = (module_, filename) => {
|
|
50
|
+
const source = fs.readFileSync(filename, "utf8");
|
|
51
|
+
const { outputText } = ts.transpileModule(source, {
|
|
52
|
+
fileName: filename,
|
|
53
|
+
compilerOptions: {
|
|
54
|
+
module: ts.ModuleKind.CommonJS,
|
|
55
|
+
target: ts.ScriptTarget.ES2022,
|
|
56
|
+
esModuleInterop: true,
|
|
57
|
+
isolatedModules: true,
|
|
58
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
module_._compile(outputText, filename);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
Module._extensions[".ts"] = compile;
|
|
65
|
+
Module._extensions[".tsx"] = compile;
|
|
66
|
+
|
|
67
|
+
const resolveFilename = Module._resolveFilename;
|
|
68
|
+
Module._resolveFilename = function (request, parent, ...rest) {
|
|
69
|
+
try {
|
|
70
|
+
return resolveFilename.call(this, request, parent, ...rest);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const found = resolveTs(request, parent);
|
|
73
|
+
if (found) return found;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { register };
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { normalizeOpen, toggleOpen } from "../core/open";
|
|
3
|
-
|
|
4
|
-
describe("toggleOpen", () => {
|
|
5
|
-
describe("one at a time", () => {
|
|
6
|
-
it("opens a panel and closes whatever was open", () => {
|
|
7
|
-
expect(toggleOpen(["a"], "b")).toEqual(["b"]);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("closes the open panel when pressed again", () => {
|
|
11
|
-
expect(toggleOpen(["a"], "a")).toEqual([]);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("keeps it open when the accordion cannot be empty", () => {
|
|
15
|
-
// Panels that are the whole page: closing the last leaves a dead end.
|
|
16
|
-
expect(toggleOpen(["a"], "a", { collapsible: false })).toEqual(["a"]);
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
describe("several at a time", () => {
|
|
21
|
-
it("adds and removes without touching the others", () => {
|
|
22
|
-
expect(toggleOpen(["a"], "b", { multiple: true })).toEqual(["a", "b"]);
|
|
23
|
-
expect(toggleOpen(["a", "b"], "a", { multiple: true })).toEqual(["b"]);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("keeps the order panels were opened in", () => {
|
|
27
|
-
expect(toggleOpen(["b"], "a", { multiple: true })).toEqual(["b", "a"]);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("refuses to close the last one when the accordion cannot be empty", () => {
|
|
31
|
-
expect(toggleOpen(["a"], "a", { multiple: true, collapsible: false })).toEqual(["a"]);
|
|
32
|
-
// Any other panel is still free to close, because one stays open.
|
|
33
|
-
expect(toggleOpen(["a", "b"], "a", { multiple: true, collapsible: false })).toEqual(["b"]);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe("normalizeOpen", () => {
|
|
39
|
-
const values = ["a", "b", "c"];
|
|
40
|
-
|
|
41
|
-
it("drops values the accordion does not have", () => {
|
|
42
|
-
expect(normalizeOpen(["a", "z"], values, { multiple: true })).toEqual(["a"]);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("keeps only the first for a single accordion", () => {
|
|
46
|
-
// Otherwise it starts showing two panels and can never show two again.
|
|
47
|
-
expect(normalizeOpen(["a", "b"], values)).toEqual(["a"]);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("opens the first panel when the accordion cannot be empty", () => {
|
|
51
|
-
expect(normalizeOpen([], values, { collapsible: false })).toEqual(["a"]);
|
|
52
|
-
expect(normalizeOpen([], values)).toEqual([]);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("has nothing to open when there are no panels", () => {
|
|
56
|
-
expect(normalizeOpen([], [], { collapsible: false })).toEqual([]);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { liveness } from "../core/live";
|
|
3
|
-
|
|
4
|
-
describe("liveness", () => {
|
|
5
|
-
it("interrupts for problems", () => {
|
|
6
|
-
// Someone whose card was declined needs to hear it now, not after the
|
|
7
|
-
// paragraph being read finishes.
|
|
8
|
-
expect(liveness("danger")).toEqual({ role: "alert", live: "assertive" });
|
|
9
|
-
expect(liveness("warning")).toEqual({ role: "alert", live: "assertive" });
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("waits its turn for everything else", () => {
|
|
13
|
-
for (const variant of ["info", "success", "neutral"] as const) {
|
|
14
|
-
expect(liveness(variant)).toEqual({ role: "status", live: "polite" });
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
});
|