@birdapi/velinstyle 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.de.md +150 -24
  2. package/README.md +155 -33
  3. package/cli/__fixtures__/atelier-library/showcases/01-login/app.css +1 -0
  4. package/cli/__fixtures__/atelier-library/showcases/01-login/app.js +2 -0
  5. package/cli/__fixtures__/atelier-library/showcases/01-login/index.html +5 -0
  6. package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.css +1 -0
  7. package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.js +2 -0
  8. package/cli/__fixtures__/atelier-library/showcases/04-pricing/index.html +5 -0
  9. package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.css +1 -0
  10. package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.js +2 -0
  11. package/cli/__fixtures__/atelier-library/showcases/36-calendar/index.html +5 -0
  12. package/cli/atelier-catalog.json +1267 -0
  13. package/cli/atelier-formats.js +159 -0
  14. package/cli/atelier.js +382 -0
  15. package/cli/blueprints/empty-state.html +3 -5
  16. package/cli/cli-manifest.json +23 -10
  17. package/cli/docgen/extract-attributes.js +2 -0
  18. package/cli/docgen/extract-cli.js +1 -1
  19. package/cli/index.js +181 -6
  20. package/cli/production/component-graph.json +166 -0
  21. package/cli/production/explain.js +52 -0
  22. package/cli/production/extract.js +238 -0
  23. package/cli/production/graph.js +102 -0
  24. package/cli/production/report.js +78 -0
  25. package/cli/production/run.js +312 -0
  26. package/cli/production/trim-css.js +177 -0
  27. package/cli/production/trim-fonts.js +23 -0
  28. package/cli/production/trim-icons.js +38 -0
  29. package/cli/production/trim-js.js +48 -0
  30. package/cli/production/trim-motion.js +22 -0
  31. package/cli/production/trim-themes.js +50 -0
  32. package/cli/production/watch.js +38 -0
  33. package/cli/review.js +48 -1
  34. package/cli/scripts/write-atelier-fixtures.mjs +33 -0
  35. package/cli/transparency.js +221 -0
  36. package/components/index.js +3 -0
  37. package/components/runtime/component-loaders.js +3 -0
  38. package/components/velin-drawer.js +43 -4
  39. package/components/velin-empty-state.js +83 -0
  40. package/components/velin-modal.js +76 -15
  41. package/components/velin-otp-input.js +220 -0
  42. package/components/velin-password-strength.js +147 -0
  43. package/components/velin-sheet.js +49 -4
  44. package/core/a11y/component-contracts.json +23 -4
  45. package/core/attributes/registry.js +14 -0
  46. package/core/meta/knowledge/components.json +75 -3
  47. package/core/meta/schema.js +10 -0
  48. package/core/transparency/attach.js +74 -0
  49. package/core/transparency/claims.js +97 -0
  50. package/core/transparency/doctor.js +142 -0
  51. package/core/transparency/engine.js +75 -0
  52. package/core/transparency/export.js +98 -0
  53. package/core/transparency/index.js +30 -0
  54. package/core/transparency/migrate.js +130 -0
  55. package/core/transparency/normalize.js +125 -0
  56. package/core/transparency/policy.js +105 -0
  57. package/core/transparency/providers.js +235 -0
  58. package/core/transparency/registry.js +104 -0
  59. package/core/transparency/renderer.js +111 -0
  60. package/core/transparency/reporter.js +113 -0
  61. package/core/transparency/validator.js +112 -0
  62. package/dist/chunks/attach-H2ZSEAE6.js +365 -0
  63. package/dist/chunks/attributes-2ORR27KA.js +404 -0
  64. package/dist/chunks/attributes-DZCXX2RZ.js +404 -0
  65. package/dist/chunks/chunk-CQMTCEI6.js +113 -0
  66. package/dist/chunks/chunk-Y6HN7HWX.js +116 -0
  67. package/dist/chunks/runtime-entry.js +1 -1
  68. package/dist/chunks/velin-drawer-A7Q7EBOS.js +162 -0
  69. package/dist/chunks/velin-empty-state-3NTUH2RF.js +81 -0
  70. package/dist/chunks/velin-modal-3MV4FYBK.js +231 -0
  71. package/dist/chunks/velin-otp-input-PSK42MM6.js +207 -0
  72. package/dist/chunks/velin-password-strength-TAXMLSED.js +136 -0
  73. package/dist/chunks/velin-sheet-N2VVYXFP.js +157 -0
  74. package/dist/llms.txt +5 -4
  75. package/dist/search-index.json +94 -5
  76. package/dist/velin-agent.json +290 -23
  77. package/dist/velinstyle-components.iife.js +3189 -2202
  78. package/dist/velinstyle-components.js +3187 -2196
  79. package/dist/velinstyle-components.min.js +261 -119
  80. package/dist/velinstyle.css +214 -6
  81. package/dist/velinstyle.d.ts +3 -0
  82. package/dist/velinstyle.min.css +1 -1
  83. package/package.json +143 -142
  84. package/packages/velinstyle-skills/catalog.json +1 -1
  85. package/src/components/data-table.css +19 -0
  86. package/src/components/empty-auth.css +33 -0
  87. package/src/components/table.css +16 -6
  88. package/src/components/transparency.css +138 -0
  89. package/src/velinstyle.css +2 -0
@@ -0,0 +1,130 @@
1
+ import { transparencyDoctor } from './doctor.js';
2
+ import { stableDisclosureId } from './registry.js';
3
+
4
+ /**
5
+ * Analyze HTML and suggest disclosures + optional apply.
6
+ * @param {string} html
7
+ * @param {{ policy: object, file?: string, apply?: boolean, dryRun?: boolean }} opts
8
+ */
9
+ export async function transparencyMigrate(html, opts) {
10
+ const doctor = await transparencyDoctor(html, opts);
11
+ const suggestions = [];
12
+
13
+ for (const f of doctor.findings) {
14
+ if (!String(f.code).startsWith('missing-disclosure.')) continue;
15
+ const target = f.target || {};
16
+ const kind = f.code.replace('missing-disclosure.', '');
17
+ const type = kind === 'images' || kind === 'videos' || kind === 'audio' ? 'ai' : kind === 'pdf' ? 'license' : 'ai';
18
+ const id = stableDisclosureId({
19
+ id: target.id,
20
+ src: target.src,
21
+ tag: target.tag,
22
+ type,
23
+ file: opts.file,
24
+ });
25
+ const attrs = {
26
+ 'velin-transparency': '',
27
+ 'velin-transparency-id': id,
28
+ 'velin-type': type,
29
+ 'velin-status': type === 'ai' ? 'generated' : 'verified',
30
+ 'velin-review': 'human-reviewed',
31
+ };
32
+ if (kind === 'images' || kind === 'pdf') {
33
+ attrs['velin-created-at'] = new Date().toISOString().slice(0, 10);
34
+ attrs['velin-license'] = 'CC BY 4.0';
35
+ }
36
+ if (kind === 'images') attrs['velin-source'] = target.src || '';
37
+ suggestions.push({
38
+ id,
39
+ kind,
40
+ target,
41
+ attrs,
42
+ reason: f.message,
43
+ });
44
+ }
45
+
46
+ // Provenance gaps on existing disclosures
47
+ for (const f of doctor.findings) {
48
+ if (!String(f.code).startsWith('missing-provenance.')) continue;
49
+ const field = f.code.replace('missing-provenance.', '');
50
+ suggestions.push({
51
+ id: f.id,
52
+ kind: 'provenance',
53
+ field,
54
+ attrs: { [`velin-${kebab(field)}`]: field === 'license' ? 'CC BY 4.0' : field.includes('At') ? new Date().toISOString().slice(0, 10) : 'unknown' },
55
+ reason: f.message,
56
+ });
57
+ }
58
+
59
+ let nextHtml = html;
60
+ let applied = 0;
61
+ if (opts.apply && opts.dryRun !== true) {
62
+ const result = applySuggestions(html, suggestions.filter((s) => s.kind !== 'provenance' || s.target));
63
+ nextHtml = result.html;
64
+ applied = result.applied;
65
+ // provenance-only patches on existing tags
66
+ for (const s of suggestions.filter((x) => x.kind === 'provenance' && x.id)) {
67
+ const patched = patchAttrsById(nextHtml, s.id, s.attrs);
68
+ if (patched.changed) {
69
+ nextHtml = patched.html;
70
+ applied += 1;
71
+ }
72
+ }
73
+ }
74
+
75
+ return {
76
+ doctor,
77
+ suggestions,
78
+ applied,
79
+ html: nextHtml,
80
+ dryRun: opts.dryRun !== false && !opts.apply,
81
+ };
82
+ }
83
+
84
+ function kebab(s) {
85
+ return String(s).replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
86
+ }
87
+
88
+ function applySuggestions(html, suggestions) {
89
+ let out = html;
90
+ let applied = 0;
91
+ for (const s of suggestions) {
92
+ if (!s.target?.tag) continue;
93
+ const tag = s.target.tag.toLowerCase();
94
+ const src = s.target.src;
95
+ const id = s.target.id;
96
+ let re;
97
+ if (id) {
98
+ re = new RegExp(`<${tag}\\b([^>]*\\bid=["']${escapeRe(id)}["'][^>]*)>`, 'i');
99
+ } else if (src) {
100
+ re = new RegExp(`<${tag}\\b([^>]*\\b(?:src|href)=["']${escapeRe(src)}["'][^>]*)>`, 'i');
101
+ } else continue;
102
+ const m = out.match(re);
103
+ if (!m) continue;
104
+ if (/\bvelin-transparency\b/i.test(m[0])) continue;
105
+ const attrStr = Object.entries(s.attrs)
106
+ .map(([k, v]) => (v === '' ? k : `${k}="${String(v).replace(/"/g, '&quot;')}"`))
107
+ .join(' ');
108
+ const next = m[0].replace(/>$/, ` ${attrStr}>`);
109
+ out = out.replace(m[0], next);
110
+ applied += 1;
111
+ }
112
+ return { html: out, applied };
113
+ }
114
+
115
+ function patchAttrsById(html, id, attrs) {
116
+ const re = new RegExp(`<([a-z0-9-]+)\\b([^>]*\\b(?:velin-transparency-id|id)=["']${escapeRe(id)}["'][^>]*)>`, 'i');
117
+ const m = html.match(re);
118
+ if (!m) return { html, changed: false };
119
+ let open = m[0];
120
+ for (const [k, v] of Object.entries(attrs)) {
121
+ if (new RegExp(`\\b${k}=`, 'i').test(open)) continue;
122
+ open = open.replace(/>$/, ` ${k}="${String(v).replace(/"/g, '&quot;')}">`);
123
+ }
124
+ if (open === m[0]) return { html, changed: false };
125
+ return { html: html.replace(m[0], open), changed: true };
126
+ }
127
+
128
+ function escapeRe(s) {
129
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
130
+ }
@@ -0,0 +1,125 @@
1
+ import { deriveClaims, primaryLabel } from './claims.js';
2
+ import { stableDisclosureId } from './registry.js';
3
+
4
+ const PROVENANCE_KEYS = [
5
+ 'createdBy', 'createdAt', 'reviewedAt', 'approvedBy', 'source', 'license', 'version', 'publishedAt',
6
+ ];
7
+
8
+ /**
9
+ * Normalize a draft disclosure into a registry record.
10
+ * @param {Record<string, unknown>} draft
11
+ * @param {{ provider?: string, file?: string, lang?: 'en'|'de' }} [ctx]
12
+ */
13
+ export function normalizeDisclosure(draft = {}, ctx = {}) {
14
+ const provider = String(draft.provider || ctx.provider || 'api');
15
+ const type = String(draft.type || 'ai').toLowerCase();
16
+ const status = draft.status != null ? String(draft.status).toLowerCase() : undefined;
17
+ const review = draft.review != null ? String(draft.review).toLowerCase() : undefined;
18
+ const provenance = normalizeProvenance(draft.provenance || draft);
19
+ if (draft.license && !provenance.license) provenance.license = String(draft.license);
20
+ if (draft.model && !provenance.source) provenance.source = `model:${draft.model}`;
21
+ if (draft.generated === true && !status) {
22
+ // generated flag without status
23
+ }
24
+ const resolvedStatus = status || (draft.generated === true ? 'generated' : undefined);
25
+ const claims = deriveClaims({
26
+ status: resolvedStatus,
27
+ review,
28
+ license: provenance.license,
29
+ claims: draft.claims,
30
+ });
31
+ const target = {
32
+ selector: draft.target?.selector || draft.selector || undefined,
33
+ tag: draft.target?.tag || draft.tag || undefined,
34
+ src: draft.target?.src || draft.src || undefined,
35
+ file: draft.target?.file || ctx.file || draft.file || undefined,
36
+ };
37
+ const id = stableDisclosureId({
38
+ id: draft.id || draft.velinTransparencyId,
39
+ selector: target.selector,
40
+ src: target.src,
41
+ tag: target.tag,
42
+ type,
43
+ file: target.file,
44
+ });
45
+ const lang = ctx.lang === 'de' ? 'de' : 'en';
46
+ const label = draft.label ? String(draft.label) : primaryLabel(claims, lang);
47
+ const updated = draft.updated || provenance.publishedAt || provenance.reviewedAt || provenance.createdAt || undefined;
48
+
49
+ return {
50
+ id,
51
+ type,
52
+ status: resolvedStatus,
53
+ review,
54
+ provider,
55
+ claims,
56
+ provenance,
57
+ updated,
58
+ label,
59
+ description: draft.description ? String(draft.description) : undefined,
60
+ renderer: draft.renderer || draft.overlay || 'badge',
61
+ tone: draft.tone || toneFromClaims(claims),
62
+ position: draft.position || 'top-right',
63
+ target,
64
+ meta: draft.meta && typeof draft.meta === 'object' ? draft.meta : undefined,
65
+ };
66
+ }
67
+
68
+ function normalizeProvenance(src = {}) {
69
+ /** @type {Record<string, string>} */
70
+ const out = {};
71
+ for (const key of PROVENANCE_KEYS) {
72
+ const v = src[key] ?? src[key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)];
73
+ if (v != null && String(v).trim()) out[key] = String(v).trim();
74
+ }
75
+ // attribute-style aliases
76
+ if (src['created-by']) out.createdBy = String(src['created-by']);
77
+ if (src['created-at']) out.createdAt = String(src['created-at']);
78
+ if (src['reviewed-at']) out.reviewedAt = String(src['reviewed-at']);
79
+ if (src['approved-by']) out.approvedBy = String(src['approved-by']);
80
+ if (src['published-at']) out.publishedAt = String(src['published-at']);
81
+ return out;
82
+ }
83
+
84
+ function toneFromClaims(claims) {
85
+ if (claims.includes('ai.generated')) return 'generated';
86
+ if (claims.includes('review.verified') || claims.includes('trust.official')) return 'verified';
87
+ if (claims.includes('review.human')) return 'human';
88
+ if (claims.includes('ai.assisted')) return 'mixed';
89
+ return 'neutral';
90
+ }
91
+
92
+ /**
93
+ * Merge two records; higher priority (lower rank number) wins field-by-field.
94
+ * @param {import('./registry.js').DisclosureRecord} a
95
+ * @param {import('./registry.js').DisclosureRecord} b
96
+ * @param {(id: string) => number} rankFn
97
+ */
98
+ export function mergeDisclosures(a, b, rankFn) {
99
+ const aRank = rankFn(a.provider);
100
+ const bRank = rankFn(b.provider);
101
+ const primary = aRank <= bRank ? a : b;
102
+ const secondary = aRank <= bRank ? b : a;
103
+ const conflictKeys = [];
104
+ for (const key of ['type', 'status', 'review']) {
105
+ if (secondary[key] && primary[key] && secondary[key] !== primary[key]) conflictKeys.push(key);
106
+ }
107
+ const claims = [...new Set([...(primary.claims || []), ...(secondary.claims || [])])];
108
+ const provenance = { ...secondary.provenance, ...primary.provenance };
109
+ return {
110
+ record: {
111
+ ...secondary,
112
+ ...primary,
113
+ claims,
114
+ provenance,
115
+ provider: primary.provider,
116
+ meta: {
117
+ ...(secondary.meta || {}),
118
+ ...(primary.meta || {}),
119
+ mergedFrom: [secondary.provider, primary.provider],
120
+ conflicts: conflictKeys,
121
+ },
122
+ },
123
+ conflicts: conflictKeys,
124
+ };
125
+ }
@@ -0,0 +1,105 @@
1
+ /** Default and merge helpers for transparency policy. */
2
+
3
+ /**
4
+ * Soft framework defaults so existing sites are not hard-failed by `velinstyle check`.
5
+ * Projects opt into strict media/provenance rules via velin.transparency.policy.json.
6
+ */
7
+ export const DEFAULT_POLICY = {
8
+ media: {
9
+ images: 'optional',
10
+ videos: 'optional',
11
+ audio: 'optional',
12
+ text: 'optional',
13
+ pdf: 'optional',
14
+ },
15
+ rules: {
16
+ requireReview: false,
17
+ allowGenerated: true,
18
+ minimumStatus: null,
19
+ staleDays: 365,
20
+ },
21
+ provenance: {
22
+ required: [],
23
+ recommended: ['approvedBy', 'source', 'version', 'createdAt', 'license'],
24
+ images: { required: [] },
25
+ videos: { required: [] },
26
+ audio: { required: [] },
27
+ pdf: { required: [] },
28
+ },
29
+ providers: {
30
+ priority: ['api', 'json', 'meta', 'html'],
31
+ },
32
+ };
33
+
34
+ /** Strict example policy for teams that require disclosure on media. */
35
+ export const STRICT_MEDIA_POLICY = {
36
+ media: {
37
+ images: 'required',
38
+ videos: 'required',
39
+ audio: 'required',
40
+ text: 'optional',
41
+ pdf: 'required',
42
+ },
43
+ provenance: {
44
+ required: [],
45
+ recommended: ['approvedBy', 'source', 'version'],
46
+ images: { required: ['createdAt', 'license', 'source'] },
47
+ videos: { required: ['createdAt'] },
48
+ audio: { required: ['createdAt'] },
49
+ pdf: { required: ['license'] },
50
+ },
51
+ };
52
+
53
+ /**
54
+ * @param {unknown} raw
55
+ */
56
+ export function normalizePolicy(raw) {
57
+ const input = raw && typeof raw === 'object' ? raw : {};
58
+ const nested = input.policy && typeof input.policy === 'object' ? input.policy : input;
59
+ return {
60
+ media: { ...DEFAULT_POLICY.media, ...(nested.media || {}) },
61
+ rules: { ...DEFAULT_POLICY.rules, ...(nested.rules || {}) },
62
+ provenance: {
63
+ ...DEFAULT_POLICY.provenance,
64
+ ...(nested.provenance || {}),
65
+ images: { ...DEFAULT_POLICY.provenance.images, ...(nested.provenance?.images || {}) },
66
+ videos: { ...DEFAULT_POLICY.provenance.videos, ...(nested.provenance?.videos || {}) },
67
+ audio: { ...DEFAULT_POLICY.provenance.audio, ...(nested.provenance?.audio || {}) },
68
+ pdf: { ...DEFAULT_POLICY.provenance.pdf, ...(nested.provenance?.pdf || {}) },
69
+ },
70
+ providers: {
71
+ ...DEFAULT_POLICY.providers,
72
+ ...(nested.providers || {}),
73
+ priority: nested.providers?.priority || DEFAULT_POLICY.providers.priority,
74
+ },
75
+ };
76
+ }
77
+
78
+ /**
79
+ * @param {string} mediaKind images|videos|audio|text|pdf
80
+ * @param {ReturnType<typeof normalizePolicy>} policy
81
+ */
82
+ export function mediaRequirement(mediaKind, policy) {
83
+ return policy.media[mediaKind] || 'optional';
84
+ }
85
+
86
+ /**
87
+ * @param {string} mediaKind
88
+ * @param {ReturnType<typeof normalizePolicy>} policy
89
+ */
90
+ export function requiredProvenanceFields(mediaKind, policy) {
91
+ const base = policy.provenance.required || [];
92
+ const specific = policy.provenance[mediaKind]?.required || [];
93
+ return [...new Set([...base, ...specific])];
94
+ }
95
+
96
+ /**
97
+ * Provider priority index (lower = higher priority).
98
+ * @param {string} providerId
99
+ * @param {ReturnType<typeof normalizePolicy>} policy
100
+ */
101
+ export function providerRank(providerId, policy) {
102
+ const list = policy.providers.priority || DEFAULT_POLICY.providers.priority;
103
+ const idx = list.indexOf(providerId);
104
+ return idx === -1 ? 999 : idx;
105
+ }
@@ -0,0 +1,235 @@
1
+ import { normalizeDisclosure, mergeDisclosures } from './normalize.js';
2
+ import { providerRank } from './policy.js';
3
+
4
+ /** @type {Map<string, { collect: Function, apply?: Function }>} */
5
+ const providers = new Map();
6
+
7
+ export function registerTransparencyProvider(id, handler) {
8
+ if (!id || typeof handler?.collect !== 'function') {
9
+ throw new Error('registerTransparencyProvider requires id and collect()');
10
+ }
11
+ providers.set(id, handler);
12
+ }
13
+
14
+ export function listTransparencyProviders() {
15
+ return [...providers.keys()];
16
+ }
17
+
18
+ export function getTransparencyProvider(id) {
19
+ return providers.get(id) || null;
20
+ }
21
+
22
+ function mergeById(records, policy) {
23
+ const map = new Map();
24
+ const conflicts = [];
25
+ const rankFn = (pid) => providerRank(pid, policy);
26
+ for (const rec of records) {
27
+ if (!map.has(rec.id)) {
28
+ map.set(rec.id, rec);
29
+ continue;
30
+ }
31
+ const { record, conflicts: c } = mergeDisclosures(map.get(rec.id), rec, rankFn);
32
+ map.set(rec.id, record);
33
+ if (c.length) {
34
+ conflicts.push({ id: rec.id, keys: c, providers: record.meta?.mergedFrom });
35
+ }
36
+ }
37
+ return { records: [...map.values()], conflicts };
38
+ }
39
+
40
+ /**
41
+ * Collect from all providers, normalize, merge by id using policy priority.
42
+ * @param {string|Document|ParentNode|object} root
43
+ * @param {{ policy: object, file?: string, lang?: string, meta?: object, apiDisclosures?: object[] }} ctx
44
+ */
45
+ export async function collectAllDisclosures(root, ctx) {
46
+ const drafts = [];
47
+ for (const [id, handler] of providers) {
48
+ const items = await handler.collect(root, { ...ctx, providerId: id });
49
+ for (const item of items || []) {
50
+ drafts.push(normalizeDisclosure({ ...item, provider: item.provider || id }, {
51
+ provider: item.provider || id,
52
+ file: ctx.file,
53
+ lang: ctx.lang,
54
+ }));
55
+ }
56
+ }
57
+ return mergeById(drafts, ctx.policy);
58
+ }
59
+
60
+ function attr(tag, name) {
61
+ const re = new RegExp(`\\b${name}=["']([^"']*)["']`, 'i');
62
+ return tag.match(re)?.[1];
63
+ }
64
+
65
+ function hasAttr(tag, name) {
66
+ return new RegExp(`(?:\\s|^|<)${name}(?:[\\s>=/]|$)`, 'i').test(tag);
67
+ }
68
+
69
+ /**
70
+ * Parse HTML string for velin-transparency hosts and JSON blocks.
71
+ */
72
+ export function collectFromHtmlString(html) {
73
+ const items = [];
74
+ const tagRe = /<([a-z0-9-]+)(\s[^>]*)?>/gi;
75
+ let m;
76
+ while ((m = tagRe.exec(html))) {
77
+ const full = m[0];
78
+ if (!hasAttr(full, 'velin-transparency') && !hasAttr(full, 'velin-disclosure')) continue;
79
+ const tagName = m[1].toUpperCase();
80
+ const id = attr(full, 'velin-transparency-id') || attr(full, 'id');
81
+ const type = attr(full, 'velin-type') || 'ai';
82
+ const status = attr(full, 'velin-status');
83
+ const review = attr(full, 'velin-review');
84
+ const license = attr(full, 'velin-license');
85
+ const label = attr(full, 'velin-label') || attr(full, 'velin-ai-label');
86
+ const description = attr(full, 'velin-description') || attr(full, 'velin-ai-description');
87
+ const overlay = attr(full, 'velin-overlay') || attr(full, 'velin-renderer') || attr(full, 'velin-ai-overlay');
88
+ const tone = attr(full, 'velin-tone') || attr(full, 'velin-ai-tone');
89
+ const position = attr(full, 'velin-position') || attr(full, 'velin-ai-position');
90
+ const src = attr(full, 'src') || attr(full, 'href');
91
+ items.push({
92
+ id,
93
+ type,
94
+ status,
95
+ review,
96
+ license,
97
+ label,
98
+ description,
99
+ overlay,
100
+ tone,
101
+ position,
102
+ src,
103
+ tag: tagName,
104
+ selector: id ? `#${id}` : undefined,
105
+ provenance: {
106
+ createdBy: attr(full, 'velin-created-by'),
107
+ createdAt: attr(full, 'velin-created-at'),
108
+ reviewedAt: attr(full, 'velin-reviewed-at'),
109
+ approvedBy: attr(full, 'velin-approved-by'),
110
+ source: attr(full, 'velin-source'),
111
+ license: license || undefined,
112
+ version: attr(full, 'velin-version'),
113
+ publishedAt: attr(full, 'velin-published-at'),
114
+ },
115
+ provider: 'html',
116
+ });
117
+ }
118
+
119
+ const scriptRe = /<script\b[^>]*type=["']application\/vnd\.velinstyle\.transparency\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
120
+ let sm;
121
+ while ((sm = scriptRe.exec(html))) {
122
+ try {
123
+ const data = JSON.parse(sm[1].trim());
124
+ const list = Array.isArray(data) ? data : data.items || [data];
125
+ for (const item of list) items.push({ ...item, provider: item.provider || 'json' });
126
+ } catch { /* ignore */ }
127
+ }
128
+
129
+ const jsonAttrRe = /<([a-z0-9-]+)([^>]*\bvelin-transparency-json=["']([^"']+)["'][^>]*)>/gi;
130
+ let jm;
131
+ while ((jm = jsonAttrRe.exec(html))) {
132
+ try {
133
+ const data = JSON.parse(jm[3].replace(/&quot;/g, '"'));
134
+ items.push({ ...data, tag: jm[1].toUpperCase(), provider: 'json' });
135
+ } catch { /* ignore */ }
136
+ }
137
+
138
+ return items;
139
+ }
140
+
141
+ function collectFromDom(root) {
142
+ if (typeof root?.querySelectorAll !== 'function') return [];
143
+ const items = [];
144
+ root.querySelectorAll('[velin-transparency], [velin-disclosure]').forEach((el) => {
145
+ const get = (n) => el.getAttribute(n);
146
+ items.push({
147
+ id: get('velin-transparency-id') || el.id || undefined,
148
+ type: get('velin-type') || 'ai',
149
+ status: get('velin-status'),
150
+ review: get('velin-review'),
151
+ license: get('velin-license'),
152
+ label: get('velin-label') || get('velin-ai-label'),
153
+ description: get('velin-description'),
154
+ overlay: get('velin-overlay') || get('velin-renderer'),
155
+ tone: get('velin-tone'),
156
+ position: get('velin-position'),
157
+ src: el.getAttribute('src') || el.getAttribute('href') || undefined,
158
+ tag: el.tagName,
159
+ selector: el.id ? `#${el.id}` : undefined,
160
+ provenance: {
161
+ createdBy: get('velin-created-by'),
162
+ createdAt: get('velin-created-at'),
163
+ reviewedAt: get('velin-reviewed-at'),
164
+ approvedBy: get('velin-approved-by'),
165
+ source: get('velin-source'),
166
+ license: get('velin-license'),
167
+ version: get('velin-version'),
168
+ publishedAt: get('velin-published-at'),
169
+ },
170
+ provider: 'html',
171
+ });
172
+ });
173
+ root.querySelectorAll('script[type="application/vnd.velinstyle.transparency+json"]').forEach((script) => {
174
+ try {
175
+ const data = JSON.parse(script.textContent || '{}');
176
+ const list = Array.isArray(data) ? data : data.items || [data];
177
+ for (const item of list) items.push({ ...item, provider: item.provider || 'json' });
178
+ } catch { /* ignore */ }
179
+ });
180
+ return items;
181
+ }
182
+
183
+ registerTransparencyProvider('html', {
184
+ async collect(root) {
185
+ if (typeof root === 'string') return collectFromHtmlString(root).filter((i) => i.provider === 'html');
186
+ return collectFromDom(root);
187
+ },
188
+ });
189
+
190
+ registerTransparencyProvider('json', {
191
+ async collect(root) {
192
+ if (root && typeof root === 'object' && !root.querySelectorAll && (Array.isArray(root) || root.type || root.items)) {
193
+ const list = Array.isArray(root) ? root : root.items || [root];
194
+ return list.map((item) => ({ ...item, provider: 'json' }));
195
+ }
196
+ if (typeof root === 'string') {
197
+ return collectFromHtmlString(root).filter((i) => i.provider === 'json');
198
+ }
199
+ return [];
200
+ },
201
+ });
202
+
203
+ registerTransparencyProvider('meta', {
204
+ async collect(root, ctx = {}) {
205
+ if (ctx.meta?.transparency) {
206
+ const t = ctx.meta.transparency;
207
+ const list = Array.isArray(t) ? t : t.disclosures || t.items || [];
208
+ return list.map((item) => ({ ...item, provider: 'meta' }));
209
+ }
210
+ if (typeof root === 'string') {
211
+ const m = root.match(
212
+ /<script\b[^>]*(?:id=["']velin-meta["']|type=["']application\/vnd\.velinstyle\.meta\+json["'])[^>]*>([\s\S]*?)<\/script>/i,
213
+ );
214
+ if (!m) return [];
215
+ try {
216
+ const meta = JSON.parse(m[1].trim());
217
+ const t = meta.transparency || meta.aiDisclosure;
218
+ if (!t) return [];
219
+ const list = Array.isArray(t) ? t : t.disclosures || t.items || [];
220
+ return list.map((item) => ({ ...item, provider: 'meta' }));
221
+ } catch {
222
+ return [];
223
+ }
224
+ }
225
+ return [];
226
+ },
227
+ });
228
+
229
+ registerTransparencyProvider('api', {
230
+ async collect(_root, ctx = {}) {
231
+ return Array.isArray(ctx.apiDisclosures)
232
+ ? ctx.apiDisclosures.map((i) => ({ ...i, provider: 'api' }))
233
+ : [];
234
+ },
235
+ });
@@ -0,0 +1,104 @@
1
+ /** In-memory disclosure registry. */
2
+
3
+ /**
4
+ * @typedef {object} Provenance
5
+ * @property {string} [createdBy]
6
+ * @property {string} [createdAt]
7
+ * @property {string} [reviewedAt]
8
+ * @property {string} [approvedBy]
9
+ * @property {string} [source]
10
+ * @property {string} [license]
11
+ * @property {string} [version]
12
+ * @property {string} [publishedAt]
13
+ */
14
+
15
+ /**
16
+ * @typedef {object} DisclosureRecord
17
+ * @property {string} id
18
+ * @property {string} [type]
19
+ * @property {string} [status]
20
+ * @property {string} [review]
21
+ * @property {string} provider
22
+ * @property {string[]} claims
23
+ * @property {Provenance} provenance
24
+ * @property {string} [updated]
25
+ * @property {string} [label]
26
+ * @property {string} [description]
27
+ * @property {string} [renderer]
28
+ * @property {string} [tone]
29
+ * @property {string} [position]
30
+ * @property {{ selector?: string, tag?: string, src?: string, file?: string }} [target]
31
+ * @property {Record<string, unknown>} [meta]
32
+ */
33
+
34
+ export function createRegistry() {
35
+ /** @type {Map<string, DisclosureRecord>} */
36
+ const map = new Map();
37
+
38
+ return {
39
+ register(record) {
40
+ if (!record?.id) throw new Error('DisclosureRecord requires id');
41
+ map.set(record.id, structuredCloneSafe(record));
42
+ return map.get(record.id);
43
+ },
44
+ get(id) {
45
+ return map.get(id) || null;
46
+ },
47
+ has(id) {
48
+ return map.has(id);
49
+ },
50
+ remove(id) {
51
+ return map.delete(id);
52
+ },
53
+ list() {
54
+ return [...map.values()].map((r) => structuredCloneSafe(r));
55
+ },
56
+ query(predicate) {
57
+ return this.list().filter(predicate);
58
+ },
59
+ clear() {
60
+ map.clear();
61
+ },
62
+ size() {
63
+ return map.size;
64
+ },
65
+ export() {
66
+ return { schema: 'velinstyle.transparency.registry', version: 1, items: this.list() };
67
+ },
68
+ diff(otherList = []) {
69
+ const other = new Map(otherList.map((r) => [r.id, r]));
70
+ const added = [];
71
+ const removed = [];
72
+ const changed = [];
73
+ for (const r of map.values()) {
74
+ if (!other.has(r.id)) added.push(r.id);
75
+ else if (JSON.stringify(r) !== JSON.stringify(other.get(r.id))) changed.push(r.id);
76
+ }
77
+ for (const id of other.keys()) {
78
+ if (!map.has(id)) removed.push(id);
79
+ }
80
+ return { added, removed, changed };
81
+ },
82
+ };
83
+ }
84
+
85
+ function structuredCloneSafe(obj) {
86
+ return JSON.parse(JSON.stringify(obj));
87
+ }
88
+
89
+ /**
90
+ * Stable id from target hints.
91
+ * @param {{ id?: string, selector?: string, src?: string, tag?: string, type?: string, file?: string }} parts
92
+ */
93
+ export function stableDisclosureId(parts = {}) {
94
+ if (parts.id) return String(parts.id).trim();
95
+ const raw = [parts.file || '', parts.selector || '', parts.src || '', parts.tag || '', parts.type || '']
96
+ .join('|')
97
+ .toLowerCase();
98
+ let h = 2166136261;
99
+ for (let i = 0; i < raw.length; i += 1) {
100
+ h ^= raw.charCodeAt(i);
101
+ h = Math.imul(h, 16777619);
102
+ }
103
+ return `tx-${(h >>> 0).toString(16)}`;
104
+ }