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.
Files changed (129) hide show
  1. package/.gbs/skills/gbs-components/SKILL.md +190 -134
  2. package/.gbs/skills/gbs-components/references/install.md +25 -3
  3. package/.gbs/skills/gbs-components/references/styling.md +246 -201
  4. package/CHANGELOG.md +74 -0
  5. package/README.md +157 -11
  6. package/index.cjs +212 -3
  7. package/package.json +41 -10
  8. package/schema/passport-v1.schema.json +204 -0
  9. package/source/beta-components/accordion/passport.json +259 -0
  10. package/source/beta-components/accordion/styles.css +207 -208
  11. package/source/beta-components/alert/passport.json +250 -0
  12. package/source/beta-components/alert/styles.css +154 -155
  13. package/source/beta-components/avatar/passport.json +294 -0
  14. package/source/beta-components/avatar/styles.css +201 -203
  15. package/source/beta-components/badge/passport.json +332 -0
  16. package/source/beta-components/badge/styles.css +203 -204
  17. package/source/beta-components/breadcrumb/passport.json +243 -0
  18. package/source/beta-components/breadcrumb/styles.css +138 -139
  19. package/source/beta-components/button/passport.json +402 -0
  20. package/source/beta-components/button/passport.manual.json +31 -0
  21. package/source/beta-components/button/styles.css +232 -233
  22. package/source/beta-components/card/passport.json +337 -0
  23. package/source/beta-components/card/styles.css +230 -231
  24. package/source/beta-components/checkbox/passport.json +456 -0
  25. package/source/beta-components/checkbox/styles.css +211 -212
  26. package/source/beta-components/combobox/passport.json +456 -0
  27. package/source/beta-components/combobox/styles.css +419 -417
  28. package/source/beta-components/data-grid/agent/coerce.ts +368 -0
  29. package/source/beta-components/data-grid/agent/contract.ts +410 -0
  30. package/source/beta-components/data-grid/agent/dataset.ts +92 -0
  31. package/source/beta-components/data-grid/agent/engine.ts +470 -0
  32. package/source/beta-components/data-grid/agent/executors.ts +155 -0
  33. package/source/beta-components/data-grid/agent/index.ts +79 -0
  34. package/source/beta-components/data-grid/agent/intent.ts +324 -0
  35. package/source/beta-components/data-grid/agent/operations.ts +335 -0
  36. package/source/beta-components/data-grid/agent/validate.ts +630 -0
  37. package/source/beta-components/data-grid/agent/webmcp.ts +107 -0
  38. package/source/beta-components/data-grid/index.ts +14 -7
  39. package/source/beta-components/data-grid/passport.json +1051 -0
  40. package/source/beta-components/data-grid/passport.manual.json +255 -0
  41. package/source/beta-components/data-grid/react/AskGrid.tsx +164 -0
  42. package/source/beta-components/data-grid/react/DataGrid.tsx +39 -0
  43. package/source/beta-components/data-grid/styles.css +874 -717
  44. package/source/beta-components/date-picker/passport.json +407 -0
  45. package/source/beta-components/date-picker/styles.css +445 -446
  46. package/source/beta-components/dialog/passport.json +344 -0
  47. package/source/beta-components/dialog/styles.css +280 -278
  48. package/source/beta-components/file-uploader/passport.json +518 -0
  49. package/source/beta-components/file-uploader/styles.css +394 -395
  50. package/source/beta-components/input/passport.json +536 -0
  51. package/source/beta-components/input/styles.css +295 -296
  52. package/source/beta-components/menu/passport.json +322 -0
  53. package/source/beta-components/menu/styles.css +224 -222
  54. package/source/beta-components/modal/passport.json +289 -0
  55. package/source/beta-components/modal/styles.css +241 -239
  56. package/source/beta-components/number-input/passport.json +541 -0
  57. package/source/beta-components/number-input/styles.css +230 -231
  58. package/source/beta-components/popover/passport.json +238 -0
  59. package/source/beta-components/popover/styles.css +148 -146
  60. package/source/beta-components/progress/passport.json +270 -0
  61. package/source/beta-components/progress/styles.css +200 -202
  62. package/source/beta-components/radio-group/passport.json +477 -0
  63. package/source/beta-components/radio-group/styles.css +241 -242
  64. package/source/beta-components/shared/core/agent/adapter.ts +65 -0
  65. package/source/beta-components/shared/core/agent/history.ts +120 -0
  66. package/source/beta-components/shared/core/agent/index.ts +46 -0
  67. package/source/beta-components/shared/core/agent/numbers.ts +217 -0
  68. package/source/beta-components/shared/core/agent/schema.ts +180 -0
  69. package/source/beta-components/shared/core/agent/types.ts +169 -0
  70. package/source/beta-components/shared/core/agent/webmcp.ts +328 -0
  71. package/source/beta-components/shared/index.ts +9 -0
  72. package/source/beta-components/shared/react/GramproAIProvider.tsx +50 -0
  73. package/source/beta-components/shared/react/useAskAgent.ts +217 -0
  74. package/source/beta-components/shared/styles.css +79 -0
  75. package/source/beta-components/shared/version.json +4 -4
  76. package/source/beta-components/shared/version.ts +6 -6
  77. package/source/beta-components/skeleton/passport.json +251 -0
  78. package/source/beta-components/skeleton/styles.css +185 -186
  79. package/source/beta-components/spinner/passport.json +245 -0
  80. package/source/beta-components/spinner/styles.css +145 -146
  81. package/source/beta-components/switch/passport.json +421 -0
  82. package/source/beta-components/switch/styles.css +194 -196
  83. package/source/beta-components/tabs/passport.json +315 -0
  84. package/source/beta-components/tabs/styles.css +234 -235
  85. package/source/beta-components/textarea/passport.json +382 -0
  86. package/source/beta-components/textarea/styles.css +158 -159
  87. package/source/beta-components/toaster/passport.json +221 -0
  88. package/source/beta-components/toaster/styles.css +282 -283
  89. package/source/beta-components/tooltip/passport.json +170 -0
  90. package/source/beta-components/tooltip/styles.css +71 -72
  91. package/tools/env.cjs +61 -0
  92. package/tools/passport/cli.cjs +79 -0
  93. package/tools/passport/extract.cjs +493 -0
  94. package/tools/passport/index.cjs +185 -0
  95. package/tools/passport/merge.cjs +131 -0
  96. package/tools/passport/policy.cjs +65 -0
  97. package/tools/passport/validate.cjs +277 -0
  98. package/tools/ts-require.cjs +79 -0
  99. package/source/beta-components/accordion/__tests__/core.test.ts +0 -58
  100. package/source/beta-components/alert/__tests__/core.test.ts +0 -17
  101. package/source/beta-components/avatar/__tests__/core.test.ts +0 -88
  102. package/source/beta-components/badge/__tests__/core.test.ts +0 -46
  103. package/source/beta-components/breadcrumb/__tests__/core.test.ts +0 -58
  104. package/source/beta-components/button/__tests__/core.test.ts +0 -31
  105. package/source/beta-components/card/__tests__/core.test.ts +0 -57
  106. package/source/beta-components/checkbox/__tests__/core.test.ts +0 -40
  107. package/source/beta-components/combobox/__tests__/core.test.ts +0 -134
  108. package/source/beta-components/data-grid/__tests__/core.test.ts +0 -356
  109. package/source/beta-components/data-grid/__tests__/export.test.ts +0 -70
  110. package/source/beta-components/data-grid/__tests__/pdf.test.ts +0 -209
  111. package/source/beta-components/date-picker/__tests__/core.test.ts +0 -273
  112. package/source/beta-components/dialog/__tests__/core.test.ts +0 -86
  113. package/source/beta-components/file-uploader/__tests__/core.test.ts +0 -395
  114. package/source/beta-components/input/__tests__/core.test.ts +0 -75
  115. package/source/beta-components/menu/__tests__/core.test.ts +0 -120
  116. package/source/beta-components/modal/__tests__/core.test.ts +0 -55
  117. package/source/beta-components/number-input/__tests__/core.test.ts +0 -151
  118. package/source/beta-components/progress/__tests__/core.test.ts +0 -56
  119. package/source/beta-components/radio-group/__tests__/core.test.ts +0 -64
  120. package/source/beta-components/shared/__tests__/boundaries.test.ts +0 -95
  121. package/source/beta-components/shared/__tests__/core.test.ts +0 -55
  122. package/source/beta-components/shared/__tests__/position.test.ts +0 -143
  123. package/source/beta-components/skeleton/__tests__/core.test.ts +0 -41
  124. package/source/beta-components/spinner/__tests__/core.test.ts +0 -48
  125. package/source/beta-components/switch/__tests__/core.test.ts +0 -64
  126. package/source/beta-components/tabs/__tests__/core.test.ts +0 -51
  127. package/source/beta-components/textarea/__tests__/core.test.ts +0 -38
  128. package/source/beta-components/toaster/__tests__/core.test.ts +0 -256
  129. 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
- });