@texuli/tools 0.0.0-stage → 0.1.0-next.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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -2
  3. package/dist/a2ui/from-a2ui.d.ts +35 -0
  4. package/dist/a2ui/from-a2ui.js +180 -0
  5. package/dist/a2ui/index.d.ts +11 -0
  6. package/dist/a2ui/index.js +11 -0
  7. package/dist/a2ui/props.d.ts +10 -0
  8. package/dist/a2ui/props.js +94 -0
  9. package/dist/a2ui/schema.d.ts +26 -0
  10. package/dist/a2ui/schema.js +193 -0
  11. package/dist/a2ui/to-a2ui.d.ts +30 -0
  12. package/dist/a2ui/to-a2ui.js +119 -0
  13. package/dist/a2ui/types.d.ts +71 -0
  14. package/dist/a2ui/types.js +11 -0
  15. package/dist/cli-add/add.d.ts +21 -0
  16. package/dist/cli-add/add.js +241 -0
  17. package/dist/cli-add/cli.d.ts +25 -0
  18. package/dist/cli-add/cli.js +157 -0
  19. package/dist/cli-add/index.d.ts +12 -0
  20. package/dist/cli-add/index.js +11 -0
  21. package/dist/cli-add/paths.d.ts +4 -0
  22. package/dist/cli-add/paths.js +4 -0
  23. package/dist/cli-add/provenance.d.ts +32 -0
  24. package/dist/cli-add/provenance.js +78 -0
  25. package/dist/cli-add/registry.d.ts +19 -0
  26. package/dist/cli-add/registry.js +50 -0
  27. package/dist/cli-add/types.d.ts +71 -0
  28. package/dist/cli-add/types.js +4 -0
  29. package/dist/cli-init/cli.d.ts +25 -0
  30. package/dist/cli-init/cli.js +212 -0
  31. package/dist/cli-init/config.d.ts +10 -0
  32. package/dist/cli-init/config.js +62 -0
  33. package/dist/cli-init/detect.d.ts +13 -0
  34. package/dist/cli-init/detect.js +42 -0
  35. package/dist/cli-init/files.d.ts +18 -0
  36. package/dist/cli-init/files.js +59 -0
  37. package/dist/cli-init/index.d.ts +14 -0
  38. package/dist/cli-init/index.js +13 -0
  39. package/dist/cli-init/init.d.ts +14 -0
  40. package/dist/cli-init/init.js +157 -0
  41. package/dist/cli-init/safe-path.d.ts +2 -0
  42. package/dist/cli-init/safe-path.js +26 -0
  43. package/dist/cli-init/theme-apply.d.ts +2 -0
  44. package/dist/cli-init/theme-apply.js +69 -0
  45. package/dist/cli-init/themes.d.ts +37 -0
  46. package/dist/cli-init/themes.js +83 -0
  47. package/dist/cli-init/types.d.ts +50 -0
  48. package/dist/cli-init/types.js +1 -0
  49. package/dist/cli-init/wire.d.ts +20 -0
  50. package/dist/cli-init/wire.js +90 -0
  51. package/dist/cli-update/cli.d.ts +26 -0
  52. package/dist/cli-update/cli.js +161 -0
  53. package/dist/cli-update/index.d.ts +11 -0
  54. package/dist/cli-update/index.js +10 -0
  55. package/dist/cli-update/lines.d.ts +38 -0
  56. package/dist/cli-update/lines.js +189 -0
  57. package/dist/cli-update/provenance.d.ts +14 -0
  58. package/dist/cli-update/provenance.js +51 -0
  59. package/dist/cli-update/types.d.ts +83 -0
  60. package/dist/cli-update/types.js +1 -0
  61. package/dist/cli-update/update.d.ts +12 -0
  62. package/dist/cli-update/update.js +261 -0
  63. package/dist/elements/content.d.ts +15 -0
  64. package/dist/elements/content.js +53 -0
  65. package/dist/elements/define.d.ts +49 -0
  66. package/dist/elements/define.js +281 -0
  67. package/dist/elements/index.d.ts +6 -0
  68. package/dist/elements/index.js +6 -0
  69. package/dist/golden/axe.d.ts +32 -0
  70. package/dist/golden/axe.js +22 -0
  71. package/dist/golden/catalog.d.ts +24 -0
  72. package/dist/golden/catalog.js +85 -0
  73. package/dist/golden/cli.d.ts +9 -0
  74. package/dist/golden/cli.js +45 -0
  75. package/dist/golden/guidance.d.ts +12 -0
  76. package/dist/golden/guidance.js +171 -0
  77. package/dist/golden/index.d.ts +17 -0
  78. package/dist/golden/index.js +17 -0
  79. package/dist/golden/outline.d.ts +21 -0
  80. package/dist/golden/outline.js +68 -0
  81. package/dist/golden/paint.d.ts +21 -0
  82. package/dist/golden/paint.js +103 -0
  83. package/dist/golden/report.d.ts +28 -0
  84. package/dist/golden/report.js +64 -0
  85. package/dist/golden/score.d.ts +16 -0
  86. package/dist/golden/score.js +116 -0
  87. package/dist/golden/sinks.d.ts +8 -0
  88. package/dist/golden/sinks.js +11 -0
  89. package/dist/golden/tasks.d.ts +4 -0
  90. package/dist/golden/tasks.js +686 -0
  91. package/dist/golden/types.d.ts +54 -0
  92. package/dist/golden/types.js +9 -0
  93. package/dist/index.d.ts +8 -0
  94. package/dist/index.js +8 -0
  95. package/dist/internal/paths.d.ts +56 -0
  96. package/dist/internal/paths.js +186 -0
  97. package/dist/llms/bundle.d.ts +6 -0
  98. package/dist/llms/bundle.js +47 -0
  99. package/dist/llms/cli.d.ts +24 -0
  100. package/dist/llms/cli.js +104 -0
  101. package/dist/llms/format.d.ts +35 -0
  102. package/dist/llms/format.js +166 -0
  103. package/dist/llms/index.d.ts +9 -0
  104. package/dist/llms/index.js +8 -0
  105. package/dist/llms/types.d.ts +51 -0
  106. package/dist/llms/types.js +1 -0
  107. package/dist/llms/write.d.ts +14 -0
  108. package/dist/llms/write.js +49 -0
  109. package/dist/mcp/index.d.ts +2 -0
  110. package/dist/mcp/index.js +2 -0
  111. package/dist/mcp/server.d.ts +20 -0
  112. package/dist/mcp/server.js +326 -0
  113. package/dist/mcp/stdio.d.ts +3 -0
  114. package/dist/mcp/stdio.js +34 -0
  115. package/package.json +63 -4
@@ -0,0 +1,32 @@
1
+ import type { GoldenAxeRunner } from './types.js';
2
+ /**
3
+ * The axe rules the golden set checks: WCAG 2.0 and 2.1 A and AA plus best
4
+ * practice. `color-contrast` is off here because the paint has no theme CSS
5
+ * (an offline DOM computes no colors); contrast is gated by the token AA
6
+ * checks and the browser axe suites instead.
7
+ */
8
+ export declare const GOLDEN_AXE_OPTIONS: Readonly<{
9
+ runOnly: {
10
+ type: string;
11
+ values: string[];
12
+ };
13
+ rules: {
14
+ 'color-contrast': {
15
+ enabled: boolean;
16
+ };
17
+ };
18
+ }>;
19
+ /** The part of axe-core's API the runner needs. */
20
+ export interface AxeLike {
21
+ run(context: Document | Element, options: object): Promise<{
22
+ violations: readonly {
23
+ id: string;
24
+ impact?: string | null;
25
+ nodes: readonly {
26
+ html?: string;
27
+ }[];
28
+ }[];
29
+ }>;
30
+ }
31
+ /** Wraps an axe-core instance (the caller supplies it) as a golden axe runner. */
32
+ export declare function createAxeRunner(axe: AxeLike): GoldenAxeRunner;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The axe rules the golden set checks: WCAG 2.0 and 2.1 A and AA plus best
3
+ * practice. `color-contrast` is off here because the paint has no theme CSS
4
+ * (an offline DOM computes no colors); contrast is gated by the token AA
5
+ * checks and the browser axe suites instead.
6
+ */
7
+ export const GOLDEN_AXE_OPTIONS = Object.freeze({
8
+ runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice'] },
9
+ rules: { 'color-contrast': { enabled: false } },
10
+ });
11
+ /** Wraps an axe-core instance (the caller supplies it) as a golden axe runner. */
12
+ export function createAxeRunner(axe) {
13
+ return async (doc) => {
14
+ const result = await axe.run(doc, GOLDEN_AXE_OPTIONS);
15
+ return result.violations.map((violation) => ({
16
+ id: violation.id,
17
+ impact: violation.impact ?? null,
18
+ nodes: violation.nodes.length,
19
+ sample: violation.nodes[0]?.html?.slice(0, 160),
20
+ }));
21
+ };
22
+ }
@@ -0,0 +1,24 @@
1
+ import type { CatalogManifest } from '@texuli/catalog/manifest';
2
+ import { type GrammarComponentMeta, type GrammarRoleCatalog } from '@texuli/catalog/render/grammar';
3
+ import { type RenderCatalog } from '@texuli/catalog/render/renderer';
4
+ /**
5
+ * The agent surface the golden set targets: every agent-emittable catalog
6
+ * component (the content components and the five content layouts).
7
+ */
8
+ export declare const GOLDEN_LAYOUT_MANIFESTS: readonly CatalogManifest[];
9
+ export declare const GOLDEN_MANIFESTS: readonly CatalogManifest[];
10
+ /** Layouts a golden interface starts from: each paints the page's `main`. */
11
+ export declare const GOLDEN_FRAME_IDS: readonly ["layout-page", "layout-doc", "layout-deck"];
12
+ export declare function goldenManifest(id: string): CatalogManifest | undefined;
13
+ export declare function goldenRenderCatalog(): RenderCatalog;
14
+ /**
15
+ * Heading rank each chart renders for its `title` (read from the components:
16
+ * cartesian and matrix charts use `h2`; flow, geo and part-whole charts `h3`).
17
+ * Compact charts render no heading.
18
+ */
19
+ export declare const CHART_TITLE_LEVELS: Readonly<Record<string, 2 | 3>>;
20
+ /** The id the heading projection uses for every outline entry. */
21
+ export declare const GOLDEN_HEADING_ID = "golden-heading";
22
+ /** Grammar roles for the projected golden tree (see `projectOutline`). */
23
+ export declare const GOLDEN_GRAMMAR_ROLES: readonly GrammarComponentMeta[];
24
+ export declare const goldenGrammarRoles: GrammarRoleCatalog;
@@ -0,0 +1,85 @@
1
+ import { CONTENT_MANIFESTS } from '@texuli/catalog/manifests/content';
2
+ import { DISPLAY_MANIFESTS } from '@texuli/catalog/manifests/display';
3
+ import { FORMS_MANIFESTS } from '@texuli/catalog/manifests/forms';
4
+ import { cardLayoutManifest, deckLayoutManifest, docLayoutManifest, gridLayoutManifest, LAYOUTS_MANIFESTS, pageLayoutManifest, } from '@texuli/catalog/manifests/layouts';
5
+ import { OVERLAYS_MANIFESTS } from '@texuli/catalog/manifests/overlays';
6
+ import { grammarRolesFromList, } from '@texuli/catalog/render/grammar';
7
+ import { renderCatalogFromSpecs, } from '@texuli/catalog/render/renderer';
8
+ /**
9
+ * The agent surface the golden set targets: every agent-emittable catalog
10
+ * component (the content components and the five content layouts).
11
+ */
12
+ export const GOLDEN_LAYOUT_MANIFESTS = Object.freeze([
13
+ pageLayoutManifest,
14
+ docLayoutManifest,
15
+ deckLayoutManifest,
16
+ cardLayoutManifest,
17
+ gridLayoutManifest,
18
+ ]);
19
+ export const GOLDEN_MANIFESTS = Object.freeze([
20
+ ...GOLDEN_LAYOUT_MANIFESTS,
21
+ ...CONTENT_MANIFESTS,
22
+ ]);
23
+ /** Layouts a golden interface starts from: each paints the page's `main`. */
24
+ export const GOLDEN_FRAME_IDS = ['layout-page', 'layout-doc', 'layout-deck'];
25
+ export function goldenManifest(id) {
26
+ return GOLDEN_MANIFESTS.find((manifest) => manifest.id === id);
27
+ }
28
+ /**
29
+ * Every catalog component, so an agent that emits app chrome (a `dialog`, a
30
+ * `sidebar`) is told `tier-denied` rather than `unknown-id`. Only the agent
31
+ * surface (`GOLDEN_MANIFESTS`) is emittable under the `untrusted` tier.
32
+ */
33
+ const CATALOG_MANIFESTS = [
34
+ ...CONTENT_MANIFESTS,
35
+ ...DISPLAY_MANIFESTS,
36
+ ...FORMS_MANIFESTS,
37
+ ...LAYOUTS_MANIFESTS,
38
+ ...OVERLAYS_MANIFESTS,
39
+ ];
40
+ export function goldenRenderCatalog() {
41
+ return renderCatalogFromSpecs(CATALOG_MANIFESTS.map((manifest) => ({
42
+ id: manifest.id,
43
+ label: manifest.label,
44
+ props: manifest.props,
45
+ agentEmittable: manifest.agentEmittable === true,
46
+ emit: manifest.agentEmittable === true ? ['developer', 'untrusted'] : ['developer'],
47
+ })));
48
+ }
49
+ /**
50
+ * Heading rank each chart renders for its `title` (read from the components:
51
+ * cartesian and matrix charts use `h2`; flow, geo and part-whole charts `h3`).
52
+ * Compact charts render no heading.
53
+ */
54
+ export const CHART_TITLE_LEVELS = Object.freeze({
55
+ 'charts-area': 2,
56
+ 'charts-bar': 2,
57
+ 'charts-cartesian': 2,
58
+ 'charts-combo': 2,
59
+ 'charts-line': 2,
60
+ 'charts-stacked-bar': 2,
61
+ 'charts-bubble': 2,
62
+ 'charts-heatmap': 2,
63
+ 'charts-scatter': 2,
64
+ 'charts-funnel': 3,
65
+ 'charts-network': 3,
66
+ 'charts-sankey': 3,
67
+ 'charts-geo-map': 3,
68
+ 'charts-geo-point-map': 3,
69
+ 'charts-donut': 3,
70
+ 'charts-pie': 3,
71
+ 'charts-sunburst': 3,
72
+ 'charts-treemap': 3,
73
+ });
74
+ /** The id the heading projection uses for every outline entry. */
75
+ export const GOLDEN_HEADING_ID = 'golden-heading';
76
+ /** Grammar roles for the projected golden tree (see `projectOutline`). */
77
+ export const GOLDEN_GRAMMAR_ROLES = Object.freeze([
78
+ { id: GOLDEN_HEADING_ID, role: 'heading', headingLevelProp: 'level' },
79
+ { id: 'layout-page', role: 'landmark', landmark: 'main' },
80
+ { id: 'layout-doc', role: 'landmark', landmark: 'main' },
81
+ { id: 'layout-deck', role: 'landmark', landmark: 'main' },
82
+ { id: 'layout-card', role: 'container' },
83
+ { id: 'layout-grid', role: 'container' },
84
+ ]);
85
+ export const goldenGrammarRoles = grammarRolesFromList(GOLDEN_GRAMMAR_ROLES);
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `texuli-golden <outputs.json> [--json]`: scores a model run, an object of
4
+ * `{ "<task id>": <catalog tree> }`, and prints the report. Report-only: the
5
+ * exit code is 0 whatever the scores; only unreadable input exits 2. The axe
6
+ * check needs a DOM and is skipped here (the test suite runs it).
7
+ */
8
+ declare function main(argv: readonly string[]): Promise<number>;
9
+ export { main as runGoldenCli };
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { formatGoldenReport, scoreGoldenRun } from "./report.js";
5
+ /**
6
+ * `texuli-golden <outputs.json> [--json]`: scores a model run, an object of
7
+ * `{ "<task id>": <catalog tree> }`, and prints the report. Report-only: the
8
+ * exit code is 0 whatever the scores; only unreadable input exits 2. The axe
9
+ * check needs a DOM and is skipped here (the test suite runs it).
10
+ */
11
+ async function main(argv) {
12
+ const file = argv.find((arg) => !arg.startsWith('--'));
13
+ if (!file) {
14
+ process.stderr.write('usage: texuli-golden <outputs.json> [--json]\n');
15
+ return 2;
16
+ }
17
+ let outputs;
18
+ try {
19
+ outputs = JSON.parse(readFileSync(file, 'utf8'));
20
+ }
21
+ catch (error) {
22
+ process.stderr.write(`texuli-golden: cannot read ${file}: ${error.message}\n`);
23
+ return 2;
24
+ }
25
+ const report = await scoreGoldenRun(outputs);
26
+ process.stdout.write(argv.includes('--json') ? `${JSON.stringify(report, null, 2)}\n` : formatGoldenReport(report));
27
+ return 0;
28
+ }
29
+ function isMainModule(metaUrl) {
30
+ const entry = process.argv[1];
31
+ if (!entry)
32
+ return false;
33
+ try {
34
+ return fileURLToPath(metaUrl) === entry;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ if (isMainModule(import.meta.url)) {
41
+ main(process.argv.slice(2)).then((code) => {
42
+ process.exitCode = code;
43
+ });
44
+ }
45
+ export { main as runGoldenCli };
@@ -0,0 +1,12 @@
1
+ import type { CatalogTreeInput } from '@texuli/catalog/render/renderer';
2
+ export interface GuidanceDetector {
3
+ /** The exact `dontUseWhen` line, as the manifests write it. */
4
+ line: string;
5
+ /** Component ids whose manifests carry the line. */
6
+ appliesTo: (id: string) => boolean;
7
+ /** Returns why `node` breaks the guidance, or `null`. */
8
+ detect: (node: CatalogTreeInput) => string | null;
9
+ }
10
+ export declare const GUIDANCE_DETECTORS: readonly GuidanceDetector[];
11
+ /** `dontUseWhen` findings for a tree, one line per node and broken guidance. */
12
+ export declare function checkGuidance(tree: CatalogTreeInput): string[];
@@ -0,0 +1,171 @@
1
+ import { goldenManifest } from "./catalog.js";
2
+ /**
3
+ * Machine checks for the `dontUseWhen` guidance of the agent surface. Each
4
+ * detector is keyed by the exact manifest line it enforces; a test fails when
5
+ * a manifest gains a line no detector covers, so guidance never drifts silently.
6
+ */
7
+ const NAV_LINK = /^\s*\[[^\]]+\]\([^)]+\)\s*$/;
8
+ const LINK_LABEL = /\[([^\]]+)\]\([^)]*\)/g;
9
+ const ACCOUNT = /\b(?:sign[\s-]?(?:in|out|up)|log[\s-]?(?:in|out)|my account|account settings|password)\b/i;
10
+ const MUTATION = /^\s*(?:delete|remove|save|submit|pay|buy|purchase|subscribe|cancel|archive|approve|reject|send|upgrade)\b/i;
11
+ const CHROME_TITLE = /\b(?:navigation|nav|menu|sidebar|top[\s-]?nav|toolbar|dialog|modal)\b/i;
12
+ const SINK_KEY = /^(?:innerHTML|outerHTML|srcdoc|html|dangerouslySetInnerHTML)$/i;
13
+ const CODE_TEXT = /\beval\s*\(|\bnew\s+Function\b|<script\b|javascript:/i;
14
+ const IFRAME_TAG = /<iframe\b[^>]*>/gi;
15
+ const ORIGIN_KEY = /^(?:targetOrigin|origin|origins|allowedOrigins|frameSrc|frame-src)$/i;
16
+ /** Every key/value pair inside an embed-app block's `app` JSON, depth first. */
17
+ function appEntries(block) {
18
+ if (block.type !== 'embed-app')
19
+ return [];
20
+ const out = [];
21
+ const visit = (value, key, depth) => {
22
+ if (depth > 12)
23
+ return;
24
+ out.push([key, value]);
25
+ if (Array.isArray(value)) {
26
+ for (const item of value)
27
+ visit(item, key, depth + 1);
28
+ }
29
+ else if (value && typeof value === 'object') {
30
+ for (const [k, v] of Object.entries(value))
31
+ visit(v, k, depth + 1);
32
+ }
33
+ };
34
+ visit(block.app, 'app', 0);
35
+ return out;
36
+ }
37
+ function asBlock(node) {
38
+ const block = node.props?.block;
39
+ return block && typeof block === 'object' && !Array.isArray(block) ? block : undefined;
40
+ }
41
+ function linkLabels(text) {
42
+ return [...text.matchAll(LINK_LABEL)].map((match) => match[1] ?? '');
43
+ }
44
+ /** Text a block shows as links: list items and Markdown link labels. */
45
+ function blockLinks(block) {
46
+ const items = block.type === 'list' && Array.isArray(block.items)
47
+ ? block.items.map((item) => item && typeof item === 'object' ? String(item.text ?? '') : '')
48
+ : [];
49
+ const text = [block.markdown, block.body, ...items]
50
+ .filter((v) => typeof v === 'string')
51
+ .join('\n');
52
+ const ctaLabels = block.type === 'cta' && Array.isArray(block.actions)
53
+ ? block.actions.map((action) => action && typeof action === 'object' ? String(action.label ?? '') : '')
54
+ : [];
55
+ return { items, labels: [...linkLabels(text), ...ctaLabels] };
56
+ }
57
+ /** A list of three or more items that are each only a link: a navigation menu. */
58
+ function isNavLinkList(block) {
59
+ const { items } = blockLinks(block);
60
+ return items.length >= 3 && items.every((item) => NAV_LINK.test(item));
61
+ }
62
+ export const GUIDANCE_DETECTORS = Object.freeze([
63
+ {
64
+ line: 'Application navigation, account controls or mutations.',
65
+ appliesTo: (id) => id.startsWith('blocks-') || id.startsWith('charts-') || id.startsWith('media-'),
66
+ detect(node) {
67
+ const block = asBlock(node);
68
+ if (!block)
69
+ return null;
70
+ if (isNavLinkList(block))
71
+ return 'a list of bare links is application navigation';
72
+ if (block.type === 'cta')
73
+ return 'call-to-action buttons are mutations';
74
+ const labels = blockLinks(block).labels;
75
+ const account = labels.find((label) => ACCOUNT.test(label));
76
+ if (account)
77
+ return `"${account}" is an account control`;
78
+ const mutation = labels.find((label) => MUTATION.test(label));
79
+ if (mutation)
80
+ return `"${mutation}" is a mutation`;
81
+ return null;
82
+ },
83
+ },
84
+ {
85
+ line: 'Running untrusted app JSON with innerHTML, eval, or an unsandboxed iframe.',
86
+ appliesTo: (id) => id === 'blocks-embed',
87
+ detect(node) {
88
+ const block = asBlock(node);
89
+ if (!block)
90
+ return null;
91
+ for (const [key, value] of appEntries(block)) {
92
+ if (SINK_KEY.test(key))
93
+ return `app JSON asks for an HTML sink ("${key}")`;
94
+ if (typeof value !== 'string')
95
+ continue;
96
+ if (CODE_TEXT.test(value))
97
+ return 'app JSON carries code to evaluate';
98
+ const iframes = value.match(IFRAME_TAG) ?? [];
99
+ if (iframes.some((tag) => !/\bsandbox\b/i.test(tag))) {
100
+ return 'app JSON embeds an iframe without a sandbox';
101
+ }
102
+ }
103
+ return null;
104
+ },
105
+ },
106
+ {
107
+ line: 'Relaxing iframe sandbox (especially adding allow-same-origin) or skipping CSP frame-src / postMessage origin checks.',
108
+ appliesTo: (id) => id === 'blocks-embed',
109
+ detect(node) {
110
+ const block = asBlock(node);
111
+ if (!block)
112
+ return null;
113
+ for (const [key, value] of appEntries(block)) {
114
+ if (/^sandbox$/i.test(key) &&
115
+ typeof value === 'string' &&
116
+ /allow-same-origin/i.test(value)) {
117
+ return 'the sandbox adds allow-same-origin';
118
+ }
119
+ const wildcard = Array.isArray(value) ? value.includes('*') : value === '*';
120
+ if (ORIGIN_KEY.test(key) && wildcard)
121
+ return `"${key}" accepts any origin`;
122
+ }
123
+ return null;
124
+ },
125
+ },
126
+ {
127
+ line: 'App chrome shells (sidebar, top-nav, dialogs)',
128
+ appliesTo: (id) => id.startsWith('layout-'),
129
+ detect(node) {
130
+ const title = node.props?.title;
131
+ if (typeof title === 'string' && CHROME_TITLE.test(title)) {
132
+ return `a layout titled "${title}" is app chrome`;
133
+ }
134
+ const children = node.children ?? [];
135
+ const navs = children.filter((child) => {
136
+ const block = asBlock(child);
137
+ return block !== undefined && isNavLinkList(block);
138
+ });
139
+ if (children.length > 0 && navs.length === children.length) {
140
+ return 'a layout that holds only link lists is a navigation shell';
141
+ }
142
+ return null;
143
+ },
144
+ },
145
+ ]);
146
+ function walk(nodes, path, visit) {
147
+ nodes.forEach((node, index) => {
148
+ if (!node || typeof node !== 'object')
149
+ return;
150
+ const here = path ? `${path}.children.${index}` : String(index);
151
+ visit(node, here);
152
+ if (Array.isArray(node.children))
153
+ walk(node.children, here, visit);
154
+ });
155
+ }
156
+ /** `dontUseWhen` findings for a tree, one line per node and broken guidance. */
157
+ export function checkGuidance(tree) {
158
+ const issues = [];
159
+ walk([tree], '', (node, path) => {
160
+ const id = typeof node.id === 'string' ? node.id : '';
161
+ const lines = goldenManifest(id)?.dontUseWhen ?? [];
162
+ for (const detector of GUIDANCE_DETECTORS) {
163
+ if (!detector.appliesTo(id) || !lines.includes(detector.line))
164
+ continue;
165
+ const why = detector.detect(node);
166
+ if (why)
167
+ issues.push(`${path} ${id}: ${why} ("${detector.line}")`);
168
+ }
169
+ });
170
+ return issues;
171
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `@texuli/tools/golden` (T-tool-golden): the agent golden set, 30
3
+ * pre-registered interface tasks with reference trees, and an offline scorer.
4
+ * A tree is checked for schema validity, `dontUseWhen` guidance, sinks,
5
+ * composition grammar, axe on a real DOM paint, and task coverage. Model runs
6
+ * are report-only (`scoreGoldenRun`, the `texuli-golden` CLI).
7
+ */
8
+ export { type AxeLike, createAxeRunner, GOLDEN_AXE_OPTIONS } from './axe.js';
9
+ export { CHART_TITLE_LEVELS, GOLDEN_FRAME_IDS, GOLDEN_GRAMMAR_ROLES, GOLDEN_MANIFESTS, goldenGrammarRoles, goldenManifest, goldenRenderCatalog, } from './catalog.js';
10
+ export { checkGuidance, GUIDANCE_DETECTORS, type GuidanceDetector } from './guidance.js';
11
+ export { projectOutline } from './outline.js';
12
+ export { type GoldenPaint, type GoldenPaintOptions, paintGoldenTree } from './paint.js';
13
+ export { formatGoldenReport, type GoldenRunReport, scoreGoldenRun } from './report.js';
14
+ export { type GoldenScoreOptions, scoreGoldenTree } from './score.js';
15
+ export { checkSinks } from './sinks.js';
16
+ export { GOLDEN_TASKS, goldenTask } from './tasks.js';
17
+ export { GOLDEN_CHECKS, type GoldenAxeRunner, type GoldenAxeViolation, type GoldenCheckId, type GoldenCheckResult, type GoldenCheckStatus, type GoldenScore, type GoldenTask, } from './types.js';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `@texuli/tools/golden` (T-tool-golden): the agent golden set, 30
3
+ * pre-registered interface tasks with reference trees, and an offline scorer.
4
+ * A tree is checked for schema validity, `dontUseWhen` guidance, sinks,
5
+ * composition grammar, axe on a real DOM paint, and task coverage. Model runs
6
+ * are report-only (`scoreGoldenRun`, the `texuli-golden` CLI).
7
+ */
8
+ export { createAxeRunner, GOLDEN_AXE_OPTIONS } from "./axe.js";
9
+ export { CHART_TITLE_LEVELS, GOLDEN_FRAME_IDS, GOLDEN_GRAMMAR_ROLES, GOLDEN_MANIFESTS, goldenGrammarRoles, goldenManifest, goldenRenderCatalog, } from "./catalog.js";
10
+ export { checkGuidance, GUIDANCE_DETECTORS } from "./guidance.js";
11
+ export { projectOutline } from "./outline.js";
12
+ export { paintGoldenTree } from "./paint.js";
13
+ export { formatGoldenReport, scoreGoldenRun } from "./report.js";
14
+ export { scoreGoldenTree } from "./score.js";
15
+ export { checkSinks } from "./sinks.js";
16
+ export { GOLDEN_TASKS, goldenTask } from "./tasks.js";
17
+ export { GOLDEN_CHECKS, } from "./types.js";
@@ -0,0 +1,21 @@
1
+ import type { RenderNode } from '@texuli/catalog/render/renderer';
2
+ /**
3
+ * The heading outline of a golden interface. The grammar check and the DOM
4
+ * paint both use these rules, so the checked outline is the painted one:
5
+ *
6
+ * - a page, doc or deck `title` is the `h1`;
7
+ * - a card `title` is one rank below the nearest titled frame around it;
8
+ * - a heading text block keeps its own `level`;
9
+ * - a chart `title` renders at the chart's fixed rank (`CHART_TITLE_LEVELS`).
10
+ */
11
+ export declare function isFrame(id: string): boolean;
12
+ /** The rank a titled card renders its heading at, under `parentLevel`. */
13
+ export declare function cardHeadingLevel(parentLevel: number): number;
14
+ export declare function titleOf(node: RenderNode): string | undefined;
15
+ /** A heading text block's level, or `undefined` for any other node. */
16
+ export declare function textHeadingLevel(node: RenderNode): number | undefined;
17
+ /**
18
+ * Rewrites a render tree so every outline entry is a `golden-heading` node
19
+ * with its rank, in document order, for `checkCompositionGrammar`.
20
+ */
21
+ export declare function projectOutline(roots: readonly RenderNode[], parentLevel?: number): RenderNode[];
@@ -0,0 +1,68 @@
1
+ import { CHART_TITLE_LEVELS, GOLDEN_FRAME_IDS, GOLDEN_HEADING_ID } from "./catalog.js";
2
+ /**
3
+ * The heading outline of a golden interface. The grammar check and the DOM
4
+ * paint both use these rules, so the checked outline is the painted one:
5
+ *
6
+ * - a page, doc or deck `title` is the `h1`;
7
+ * - a card `title` is one rank below the nearest titled frame around it;
8
+ * - a heading text block keeps its own `level`;
9
+ * - a chart `title` renders at the chart's fixed rank (`CHART_TITLE_LEVELS`).
10
+ */
11
+ export function isFrame(id) {
12
+ return GOLDEN_FRAME_IDS.includes(id);
13
+ }
14
+ /** The rank a titled card renders its heading at, under `parentLevel`. */
15
+ export function cardHeadingLevel(parentLevel) {
16
+ return Math.min(parentLevel + 1, 6);
17
+ }
18
+ export function titleOf(node) {
19
+ if (node.kind !== 'component')
20
+ return undefined;
21
+ const title = node.props.title;
22
+ return typeof title === 'string' && title.trim() !== '' ? title : undefined;
23
+ }
24
+ /** A heading text block's level, or `undefined` for any other node. */
25
+ export function textHeadingLevel(node) {
26
+ if (node.kind !== 'component' || node.id !== 'blocks-text')
27
+ return undefined;
28
+ const block = node.props.block;
29
+ return block?.type === 'heading' && typeof block.level === 'number' ? block.level : undefined;
30
+ }
31
+ function heading(level) {
32
+ return { kind: 'component', id: GOLDEN_HEADING_ID, props: { level }, children: [] };
33
+ }
34
+ /**
35
+ * Rewrites a render tree so every outline entry is a `golden-heading` node
36
+ * with its rank, in document order, for `checkCompositionGrammar`.
37
+ */
38
+ export function projectOutline(roots, parentLevel = 0) {
39
+ return roots.map((node) => {
40
+ if (node.kind !== 'component')
41
+ return node;
42
+ const textLevel = textHeadingLevel(node);
43
+ if (textLevel !== undefined)
44
+ return heading(textLevel);
45
+ const title = titleOf(node);
46
+ let level = parentLevel;
47
+ const lead = [];
48
+ if (isFrame(node.id)) {
49
+ if (title) {
50
+ level = 1;
51
+ lead.push(heading(1));
52
+ }
53
+ }
54
+ else if (node.id === 'layout-card') {
55
+ if (title) {
56
+ level = cardHeadingLevel(parentLevel);
57
+ lead.push(heading(level));
58
+ }
59
+ }
60
+ else if (title && CHART_TITLE_LEVELS[node.id]) {
61
+ lead.push(heading(CHART_TITLE_LEVELS[node.id]));
62
+ }
63
+ return {
64
+ ...node,
65
+ children: [...lead, ...projectOutline(node.children, level)],
66
+ };
67
+ });
68
+ }
@@ -0,0 +1,21 @@
1
+ import type { RenderNode } from '@texuli/catalog/render/renderer';
2
+ import { type ElementComponentLoader } from '../elements/content.js';
3
+ /**
4
+ * Paints a rendered golden tree into a document so axe can check it. Content
5
+ * components mount for real (the element loaders); the five content layouts
6
+ * become the semantic containers a host maps them to: a frame is `main` with
7
+ * its title as the `h1`, a card is a labelled `section`, a grid a plain group.
8
+ * Markup is never built from strings: `createElement` and `textContent` only.
9
+ */
10
+ export interface GoldenPaint {
11
+ /** Components that failed to load or mount, or refused their props. */
12
+ issues: readonly string[];
13
+ destroy(): void;
14
+ }
15
+ export interface GoldenPaintOptions {
16
+ loaders?: Readonly<Record<string, ElementComponentLoader>>;
17
+ /** The document title and `lang` (axe checks both). */
18
+ title?: string;
19
+ lang?: string;
20
+ }
21
+ export declare function paintGoldenTree(roots: readonly RenderNode[], doc: Document, options?: GoldenPaintOptions): Promise<GoldenPaint>;
@@ -0,0 +1,103 @@
1
+ import { mount, tick, unmount } from 'svelte';
2
+ import { CONTENT_ELEMENT_LOADERS } from "../elements/content.js";
3
+ import { goldenManifest } from "./catalog.js";
4
+ import { cardHeadingLevel, isFrame, titleOf } from "./outline.js";
5
+ let sequence = 0;
6
+ export async function paintGoldenTree(roots, doc, options = {}) {
7
+ const loaders = options.loaders ?? CONTENT_ELEMENT_LOADERS;
8
+ const issues = [];
9
+ const instances = [];
10
+ doc.documentElement.lang = options.lang ?? 'en';
11
+ doc.title = options.title ?? 'Golden interface';
12
+ doc.body.replaceChildren();
13
+ const headingEl = (level, text) => {
14
+ const el = doc.createElement(`h${Math.min(Math.max(level, 1), 6)}`);
15
+ el.textContent = text;
16
+ return el;
17
+ };
18
+ const paint = async (node, parent, level, path) => {
19
+ if (node.kind === 'placeholder') {
20
+ const note = doc.createElement('p');
21
+ note.setAttribute('role', 'note');
22
+ note.textContent = node.label;
23
+ parent.append(note);
24
+ return;
25
+ }
26
+ const title = titleOf(node);
27
+ let container = parent;
28
+ let childLevel = level;
29
+ if (isFrame(node.id)) {
30
+ container = doc.createElement('main');
31
+ if (title) {
32
+ container.append(headingEl(1, title));
33
+ childLevel = 1;
34
+ }
35
+ const description = node.props.description;
36
+ if (typeof description === 'string') {
37
+ const p = doc.createElement('p');
38
+ p.textContent = description;
39
+ container.append(p);
40
+ }
41
+ parent.append(container);
42
+ }
43
+ else if (node.id === 'layout-card') {
44
+ container = doc.createElement(title ? 'section' : 'div');
45
+ if (title) {
46
+ childLevel = cardHeadingLevel(level);
47
+ const h = headingEl(childLevel, title);
48
+ sequence += 1;
49
+ h.id = `golden-card-${sequence}`;
50
+ container.setAttribute('aria-labelledby', h.id);
51
+ container.append(h);
52
+ }
53
+ parent.append(container);
54
+ }
55
+ else if (node.id === 'layout-grid') {
56
+ container = doc.createElement('div');
57
+ container.setAttribute('data-golden-grid', '');
58
+ parent.append(container);
59
+ }
60
+ else {
61
+ const loader = loaders[node.id];
62
+ const target = doc.createElement('div');
63
+ target.setAttribute('data-golden-component', node.id);
64
+ parent.append(target);
65
+ if (!loader) {
66
+ issues.push(`${path} ${node.id}: no component to paint`);
67
+ return;
68
+ }
69
+ try {
70
+ const Component = await loader();
71
+ const props = { ...node.props };
72
+ // Blocks report a refused block through `oninvalid` instead of throwing.
73
+ if (goldenManifest(node.id)?.props.some((prop) => prop.name === 'oninvalid')) {
74
+ props.oninvalid = (detail) => issues.push(`${path} ${node.id}: refused its props ${JSON.stringify(detail).slice(0, 200)}`);
75
+ }
76
+ instances.push(mount(Component, { target, props }));
77
+ }
78
+ catch (error) {
79
+ issues.push(`${path} ${node.id}: failed to render (${error instanceof Error ? error.message : String(error)})`);
80
+ }
81
+ }
82
+ for (let index = 0; index < node.children.length; index += 1) {
83
+ await paint(node.children[index], container, childLevel, `${path}.children.${index}`);
84
+ }
85
+ };
86
+ for (let index = 0; index < roots.length; index += 1) {
87
+ await paint(roots[index], doc.body, 0, String(index));
88
+ }
89
+ await tick();
90
+ // Lazy families resolve their own chunks after mount; let them settle.
91
+ for (let round = 0; round < 3; round += 1) {
92
+ await new Promise((resolve) => setTimeout(resolve, 0));
93
+ await tick();
94
+ }
95
+ return {
96
+ issues,
97
+ destroy() {
98
+ for (const instance of instances)
99
+ void unmount(instance);
100
+ doc.body.replaceChildren();
101
+ },
102
+ };
103
+ }