@birdapi/velinstyle 1.2.0 → 1.2.1

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 (39) hide show
  1. package/README.de.md +22 -19
  2. package/README.md +24 -25
  3. package/cli/cli-manifest.json +1 -1
  4. package/cli/docgen/extract-attributes.js +2 -0
  5. package/cli/index.js +32 -3
  6. package/cli/transparency.js +221 -0
  7. package/core/a11y/component-contracts.json +1 -1
  8. package/core/attributes/registry.js +14 -0
  9. package/core/meta/schema.js +10 -0
  10. package/core/transparency/attach.js +74 -0
  11. package/core/transparency/claims.js +97 -0
  12. package/core/transparency/doctor.js +142 -0
  13. package/core/transparency/engine.js +75 -0
  14. package/core/transparency/export.js +98 -0
  15. package/core/transparency/index.js +30 -0
  16. package/core/transparency/migrate.js +130 -0
  17. package/core/transparency/normalize.js +125 -0
  18. package/core/transparency/policy.js +105 -0
  19. package/core/transparency/providers.js +235 -0
  20. package/core/transparency/registry.js +104 -0
  21. package/core/transparency/renderer.js +111 -0
  22. package/core/transparency/reporter.js +113 -0
  23. package/core/transparency/validator.js +112 -0
  24. package/dist/chunks/attach-H2ZSEAE6.js +365 -0
  25. package/dist/chunks/attributes-DZCXX2RZ.js +404 -0
  26. package/dist/chunks/chunk-CQMTCEI6.js +113 -0
  27. package/dist/chunks/runtime-entry.js +1 -1
  28. package/dist/llms.txt +3 -2
  29. package/dist/search-index.json +28 -2
  30. package/dist/velin-agent.json +10 -6
  31. package/dist/velinstyle-components.iife.js +411 -2
  32. package/dist/velinstyle-components.js +411 -2
  33. package/dist/velinstyle-components.min.js +113 -113
  34. package/dist/velinstyle.css +160 -0
  35. package/dist/velinstyle.min.css +1 -1
  36. package/package.json +2 -1
  37. package/packages/velinstyle-skills/catalog.json +1 -1
  38. package/src/components/transparency.css +138 -0
  39. package/src/velinstyle.css +1 -0
@@ -0,0 +1,74 @@
1
+ import { normalizeDisclosure } from './normalize.js';
2
+ import { renderDisclosure } from './renderer.js';
3
+ import { createRegistry } from './registry.js';
4
+
5
+ const defaultRegistry = createRegistry();
6
+
7
+ /**
8
+ * Attach a disclosure to an element (browser API).
9
+ * @param {HTMLElement} el
10
+ * @param {Record<string, unknown>} options
11
+ * @param {{ registry?: ReturnType<typeof createRegistry>, lang?: string }} [ctx]
12
+ */
13
+ export function attach(el, options = {}, ctx = {}) {
14
+ if (!el) throw new Error('VelinTransparency.attach requires an element');
15
+ const registry = ctx.registry || defaultRegistry;
16
+ const draft = {
17
+ ...options,
18
+ tag: el.tagName,
19
+ src: el.getAttribute?.('src') || el.getAttribute?.('href') || options.src,
20
+ selector: el.id ? `#${el.id}` : options.selector,
21
+ id: options.id || el.getAttribute?.('velin-transparency-id') || el.id,
22
+ provider: options.provider || 'api',
23
+ };
24
+ // Mirror useful attributes if present
25
+ if (el.hasAttribute?.('velin-transparency') || el.hasAttribute?.('velin-disclosure')) {
26
+ draft.type = draft.type || el.getAttribute('velin-type') || 'ai';
27
+ draft.status = draft.status || el.getAttribute('velin-status');
28
+ draft.review = draft.review || el.getAttribute('velin-review');
29
+ draft.license = draft.license || el.getAttribute('velin-license');
30
+ draft.label = draft.label || el.getAttribute('velin-label');
31
+ draft.description = draft.description || el.getAttribute('velin-description');
32
+ draft.overlay = draft.overlay || el.getAttribute('velin-overlay') || el.getAttribute('velin-renderer');
33
+ draft.provenance = {
34
+ createdBy: el.getAttribute('velin-created-by'),
35
+ createdAt: el.getAttribute('velin-created-at'),
36
+ reviewedAt: el.getAttribute('velin-reviewed-at'),
37
+ approvedBy: el.getAttribute('velin-approved-by'),
38
+ source: el.getAttribute('velin-source'),
39
+ license: el.getAttribute('velin-license'),
40
+ version: el.getAttribute('velin-version'),
41
+ publishedAt: el.getAttribute('velin-published-at'),
42
+ ...(options.provenance || {}),
43
+ };
44
+ }
45
+ const record = normalizeDisclosure(draft, { provider: draft.provider, lang: ctx.lang });
46
+ registry.register(record);
47
+ if (!el.hasAttribute('velin-transparency')) el.setAttribute('velin-transparency', '');
48
+ if (!el.hasAttribute('velin-transparency-id')) el.setAttribute('velin-transparency-id', record.id);
49
+ renderDisclosure(el, record);
50
+ return record;
51
+ }
52
+
53
+ /**
54
+ * Enhance all [velin-transparency] under root.
55
+ * @param {ParentNode} [root]
56
+ */
57
+ export function enhanceAll(root = typeof document !== 'undefined' ? document : null) {
58
+ if (!root?.querySelectorAll) return [];
59
+ const out = [];
60
+ root.querySelectorAll('[velin-transparency], [velin-disclosure]').forEach((el) => {
61
+ out.push(attach(el, {}, { registry: defaultRegistry }));
62
+ });
63
+ return out;
64
+ }
65
+
66
+ export function getDefaultRegistry() {
67
+ return defaultRegistry;
68
+ }
69
+
70
+ export const VelinTransparency = {
71
+ attach,
72
+ enhanceAll,
73
+ getRegistry: getDefaultRegistry,
74
+ };
@@ -0,0 +1,97 @@
1
+ /** Standardized claim taxonomy for Velin Transparency Framework. */
2
+
3
+ export const CLAIM_CATALOG = {
4
+ 'ai.generated': { pillar: 'ai', label: { en: 'AI generated', de: 'KI-generiert' } },
5
+ 'ai.assisted': { pillar: 'ai', label: { en: 'AI assisted', de: 'KI-unterstützt' } },
6
+ 'review.human': { pillar: 'ai', label: { en: 'Human reviewed', de: 'Mensch geprüft' } },
7
+ 'review.verified': { pillar: 'trust', label: { en: 'Verified', de: 'Verifiziert' } },
8
+ 'security.checked': { pillar: 'trust', label: { en: 'Security checked', de: 'Security geprüft' } },
9
+ 'accessibility.checked': { pillar: 'trust', label: { en: 'Accessibility checked', de: 'Barrierefreiheit geprüft' } },
10
+ 'trust.official': { pillar: 'trust', label: { en: 'Official', de: 'Offiziell' } },
11
+ 'trust.signed': { pillar: 'trust', label: { en: 'Signed', de: 'Signiert' } },
12
+ 'trust.opensource': { pillar: 'trust', label: { en: 'Open source', de: 'Open Source' } },
13
+ 'privacy.gdpr': { pillar: 'compliance', label: { en: 'GDPR', de: 'DSGVO' } },
14
+ 'license.cc-by': { pillar: 'compliance', label: { en: 'CC BY', de: 'CC BY' } },
15
+ 'license.mit': { pillar: 'compliance', label: { en: 'MIT', de: 'MIT' } },
16
+ 'license.apache-2': { pillar: 'compliance', label: { en: 'Apache-2.0', de: 'Apache-2.0' } },
17
+ 'content.updated': { pillar: 'metadata', label: { en: 'Updated', de: 'Aktualisiert' } },
18
+ 'content.author': { pillar: 'metadata', label: { en: 'Author', de: 'Autor' } },
19
+ 'content.source': { pillar: 'metadata', label: { en: 'Source', de: 'Quelle' } },
20
+ 'content.language': { pillar: 'metadata', label: { en: 'Language', de: 'Sprache' } },
21
+ 'version.current': { pillar: 'metadata', label: { en: 'Version', de: 'Version' } },
22
+ 'provenance.complete': { pillar: 'provenance', label: { en: 'Provenance complete', de: 'Nachweis vollständig' } },
23
+ };
24
+
25
+ const STATUS_TO_CLAIM = {
26
+ generated: 'ai.generated',
27
+ assisted: 'ai.assisted',
28
+ 'ai-assisted': 'ai.assisted',
29
+ 'human-reviewed': 'review.human',
30
+ 'human-edited': 'review.human',
31
+ verified: 'review.verified',
32
+ edited: 'review.human',
33
+ draft: null,
34
+ };
35
+
36
+ const REVIEW_TO_CLAIM = {
37
+ human: 'review.human',
38
+ 'human-reviewed': 'review.human',
39
+ verified: 'review.verified',
40
+ none: null,
41
+ };
42
+
43
+ const LICENSE_TO_CLAIM = {
44
+ 'cc by 4.0': 'license.cc-by',
45
+ 'cc-by': 'license.cc-by',
46
+ 'cc-by-4.0': 'license.cc-by',
47
+ mit: 'license.mit',
48
+ 'apache-2.0': 'license.apache-2',
49
+ apache: 'license.apache-2',
50
+ };
51
+
52
+ /**
53
+ * @param {string} [status]
54
+ * @param {string} [review]
55
+ * @param {string} [license]
56
+ * @param {string[]} [extra]
57
+ */
58
+ export function deriveClaims({ status, review, license, claims = [] } = {}) {
59
+ const out = new Set(Array.isArray(claims) ? claims.filter(Boolean) : []);
60
+ const statusClaim = STATUS_TO_CLAIM[String(status || '').toLowerCase()];
61
+ if (statusClaim) out.add(statusClaim);
62
+ const reviewClaim = REVIEW_TO_CLAIM[String(review || '').toLowerCase()];
63
+ if (reviewClaim) out.add(reviewClaim);
64
+ if (license) {
65
+ const key = String(license).toLowerCase().trim();
66
+ const mapped = LICENSE_TO_CLAIM[key] || (key.startsWith('cc') ? 'license.cc-by' : null);
67
+ if (mapped) out.add(mapped);
68
+ }
69
+ return [...out];
70
+ }
71
+
72
+ /**
73
+ * @param {string} claim
74
+ * @param {'en'|'de'} [lang]
75
+ */
76
+ export function claimLabel(claim, lang = 'en') {
77
+ const entry = CLAIM_CATALOG[claim];
78
+ if (!entry) return claim;
79
+ return entry.label[lang] || entry.label.en || claim;
80
+ }
81
+
82
+ /**
83
+ * @param {string[]} claims
84
+ * @param {'en'|'de'} [lang]
85
+ */
86
+ export function primaryLabel(claims = [], lang = 'en') {
87
+ const order = ['ai.generated', 'ai.assisted', 'review.human', 'review.verified', 'trust.official', 'license.cc-by', 'license.mit'];
88
+ for (const c of order) {
89
+ if (claims.includes(c)) return claimLabel(c, lang);
90
+ }
91
+ if (claims[0]) return claimLabel(claims[0], lang);
92
+ return lang === 'de' ? 'Transparenz' : 'Transparency';
93
+ }
94
+
95
+ export function pillarForClaim(claim) {
96
+ return CLAIM_CATALOG[claim]?.pillar || (String(claim).startsWith('custom.') ? 'custom' : 'metadata');
97
+ }
@@ -0,0 +1,142 @@
1
+ import { collectAllDisclosures } from './providers.js';
2
+ import { validateRecords, inferMediaKind } from './validator.js';
3
+ import { mediaRequirement, requiredProvenanceFields } from './policy.js';
4
+ import { createRegistry } from './registry.js';
5
+ import { pillarForClaim } from './claims.js';
6
+
7
+ /**
8
+ * Run transparency doctor on HTML string (or precollected context).
9
+ * @param {string} html
10
+ * @param {{ policy: object, file?: string, lang?: string, meta?: object }} ctx
11
+ */
12
+ export async function transparencyDoctor(html, ctx) {
13
+ const { records, conflicts } = await collectAllDisclosures(html, ctx);
14
+ const registry = createRegistry();
15
+ for (const r of records) registry.register(r);
16
+
17
+ const findings = [];
18
+ for (const c of conflicts) {
19
+ findings.push({
20
+ severity: 'warning',
21
+ code: 'conflict',
22
+ message: `Conflicting fields [${c.keys.join(', ')}] from providers ${(c.providers || []).join(' > ')}`,
23
+ id: c.id,
24
+ });
25
+ }
26
+ findings.push(...validateRecords(records, ctx.policy));
27
+
28
+ // Media coverage
29
+ const media = scanMediaTargets(html);
30
+ const coveredSrc = new Set(
31
+ records.map((r) => (r.target?.src || '').split('?')[0].toLowerCase()).filter(Boolean),
32
+ );
33
+ const coveredSelectors = new Set(records.map((r) => r.target?.selector).filter(Boolean));
34
+
35
+ for (const item of media) {
36
+ const req = mediaRequirement(item.kind, ctx.policy);
37
+ if (req !== 'required') continue;
38
+ const srcKey = (item.src || '').split('?')[0].toLowerCase();
39
+ const has =
40
+ (item.id && coveredSelectors.has(`#${item.id}`))
41
+ || (srcKey && coveredSrc.has(srcKey))
42
+ || records.some((r) => r.target?.tag === item.tag && !r.target?.src && item.kind === inferMediaKind(r));
43
+ // Better: check if element itself has velin-transparency in opening tag
44
+ if (item.disclosed) continue;
45
+ if (!has) {
46
+ findings.push({
47
+ severity: 'error',
48
+ code: `missing-disclosure.${item.kind}`,
49
+ message: `Required ${item.kind} lacks velin-transparency (${item.tag}${item.src ? ` ${item.src}` : ''})`,
50
+ id: item.id || null,
51
+ target: item,
52
+ });
53
+ }
54
+ }
55
+
56
+ const scores = computeScores(records, findings, media, ctx.policy);
57
+ return {
58
+ ok: !findings.some((f) => f.severity === 'error'),
59
+ file: ctx.file || null,
60
+ registry: registry.export(),
61
+ findings,
62
+ scores,
63
+ summary: {
64
+ disclosures: records.length,
65
+ media: media.length,
66
+ errors: findings.filter((f) => f.severity === 'error').length,
67
+ warnings: findings.filter((f) => f.severity === 'warning').length,
68
+ info: findings.filter((f) => f.severity === 'info').length,
69
+ },
70
+ };
71
+ }
72
+
73
+ function scanMediaTargets(html) {
74
+ const out = [];
75
+ const re = /<(img|video|audio|a|embed|object)(\s[^>]*)?>/gi;
76
+ let m;
77
+ while ((m = re.exec(html))) {
78
+ const tag = m[1].toUpperCase();
79
+ const attrs = m[2] || '';
80
+ const src = attrs.match(/\b(?:src|href)=["']([^"']+)["']/i)?.[1] || '';
81
+ const id = attrs.match(/\bid=["']([^"']+)["']/i)?.[1];
82
+ const disclosed = /\bvelin-transparency\b/i.test(attrs) || /\bvelin-disclosure\b/i.test(attrs);
83
+ let kind = 'text';
84
+ if (tag === 'IMG') kind = 'images';
85
+ else if (tag === 'VIDEO') kind = 'videos';
86
+ else if (tag === 'AUDIO') kind = 'audio';
87
+ else if (/\.pdf(\?|$)/i.test(src) || /application\/pdf/i.test(attrs)) kind = 'pdf';
88
+ else if (tag === 'A' && !/\.pdf(\?|$)/i.test(src)) continue;
89
+ else if (tag === 'EMBED' || tag === 'OBJECT') {
90
+ if (/\.pdf/i.test(src) || /pdf/i.test(attrs)) kind = 'pdf';
91
+ else continue;
92
+ }
93
+ out.push({ tag, src, id, kind, disclosed });
94
+ }
95
+ return out;
96
+ }
97
+
98
+ function computeScores(records, findings, media, policy) {
99
+ const pillars = { ai: [], trust: [], compliance: [], metadata: [], provenance: [] };
100
+ for (const r of records) {
101
+ for (const c of r.claims || []) {
102
+ const p = pillarForClaim(c);
103
+ if (pillars[p]) pillars[p].push(true);
104
+ }
105
+ const kind = inferMediaKind(r);
106
+ const req = requiredCount(kind, policy);
107
+ const have = Object.keys(r.provenance || {}).length;
108
+ pillars.provenance.push(req === 0 ? true : have >= req);
109
+ }
110
+
111
+ const requiredMedia = media.filter((m) => mediaRequirement(m.kind, policy) === 'required');
112
+ const missingMedia = findings.filter((f) => String(f.code).startsWith('missing-disclosure.')).length;
113
+ const coverage = requiredMedia.length
114
+ ? Math.round(((requiredMedia.length - missingMedia) / requiredMedia.length) * 100)
115
+ : 100;
116
+
117
+ const pillarScore = (arr) => {
118
+ if (!arr.length) return records.length ? 100 : coverage;
119
+ return Math.round((arr.filter(Boolean).length / arr.length) * 100);
120
+ };
121
+
122
+ const errors = findings.filter((f) => f.severity === 'error').length;
123
+ const transparency = Math.max(0, Math.min(100, Math.round(
124
+ (coverage * 0.45)
125
+ + (pillarScore(pillars.provenance) * 0.25)
126
+ + (Math.max(0, 100 - errors * 8) * 0.3),
127
+ )));
128
+
129
+ return {
130
+ transparency,
131
+ ai: pillarScore(pillars.ai),
132
+ trust: pillarScore(pillars.trust),
133
+ metadata: pillarScore(pillars.metadata),
134
+ compliance: pillarScore(pillars.compliance),
135
+ provenance: pillarScore(pillars.provenance),
136
+ coverage,
137
+ };
138
+ }
139
+
140
+ function requiredCount(kind, policy) {
141
+ return requiredProvenanceFields(kind, policy).length;
142
+ }
@@ -0,0 +1,75 @@
1
+ import { normalizePolicy } from './policy.js';
2
+ import { createRegistry } from './registry.js';
3
+ import { collectAllDisclosures } from './providers.js';
4
+ import { validateRecords } from './validator.js';
5
+ import { transparencyDoctor } from './doctor.js';
6
+ import { buildTransparencyReports } from './reporter.js';
7
+ import { exportDisclosures } from './export.js';
8
+ import { transparencyMigrate } from './migrate.js';
9
+ import { normalizeDisclosure } from './normalize.js';
10
+
11
+ /**
12
+ * Create a Transparency Engine instance.
13
+ * @param {{ policy?: object, lang?: 'en'|'de' }} [options]
14
+ */
15
+ export function createTransparencyEngine(options = {}) {
16
+ const policy = normalizePolicy(options.policy);
17
+ const registry = createRegistry();
18
+ const lang = options.lang === 'de' ? 'de' : 'en';
19
+
20
+ return {
21
+ policy,
22
+ registry,
23
+ lang,
24
+
25
+ /**
26
+ * Collect → validate → register from HTML string / DOM / JSON.
27
+ */
28
+ async ingest(root, ctx = {}) {
29
+ const { records, conflicts } = await collectAllDisclosures(root, {
30
+ policy,
31
+ lang,
32
+ file: ctx.file,
33
+ meta: ctx.meta,
34
+ apiDisclosures: ctx.apiDisclosures,
35
+ });
36
+ registry.clear();
37
+ for (const r of records) registry.register(r);
38
+ const findings = validateRecords(records, policy);
39
+ return { records, conflicts, findings, registry: registry.export() };
40
+ },
41
+
42
+ register(draft, ctx = {}) {
43
+ const record = normalizeDisclosure(draft, { provider: draft.provider || 'api', lang, file: ctx.file });
44
+ return registry.register(record);
45
+ },
46
+
47
+ async doctor(html, ctx = {}) {
48
+ return transparencyDoctor(html, { policy, lang, ...ctx });
49
+ },
50
+
51
+ async validate(html, ctx = {}) {
52
+ const report = await transparencyDoctor(html, { policy, lang, ...ctx });
53
+ return {
54
+ ok: report.ok && !report.findings.some((f) => f.severity === 'error'),
55
+ findings: report.findings,
56
+ scores: report.scores,
57
+ };
58
+ },
59
+
60
+ async report(html, ctx = {}) {
61
+ const doctor = await transparencyDoctor(html, { policy, lang, ...ctx });
62
+ return buildTransparencyReports(doctor, { title: ctx.title });
63
+ },
64
+
65
+ export(format = 'json') {
66
+ return exportDisclosures(registry.list(), format);
67
+ },
68
+
69
+ async migrate(html, ctx = {}) {
70
+ return transparencyMigrate(html, { policy, lang, ...ctx });
71
+ },
72
+ };
73
+ }
74
+
75
+ export const TransparencyMIME = 'application/vnd.velinstyle.transparency+json';
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Export registry items to interoperable formats.
3
+ * @param {import('./registry.js').DisclosureRecord[]} items
4
+ * @param {'json'|'json-ld'|'csv'|'html'} format
5
+ */
6
+ export function exportDisclosures(items, format = 'json') {
7
+ switch (format) {
8
+ case 'json-ld':
9
+ return JSON.stringify(toJsonLd(items), null, 2);
10
+ case 'csv':
11
+ return toCsv(items);
12
+ case 'html':
13
+ return toHtmlTable(items);
14
+ case 'json':
15
+ default:
16
+ return JSON.stringify({
17
+ schema: 'velinstyle.transparency.export',
18
+ mime: 'application/vnd.velinstyle.transparency+json',
19
+ version: 1,
20
+ exportedAt: new Date().toISOString(),
21
+ items,
22
+ }, null, 2);
23
+ }
24
+ }
25
+
26
+ function toJsonLd(items) {
27
+ return {
28
+ '@context': 'https://schema.org',
29
+ '@graph': items.map((r) => ({
30
+ '@type': 'CreativeWork',
31
+ '@id': r.id,
32
+ name: r.label,
33
+ description: r.description,
34
+ creativeWorkStatus: r.status,
35
+ dateCreated: r.provenance?.createdAt,
36
+ dateModified: r.updated || r.provenance?.reviewedAt,
37
+ version: r.provenance?.version,
38
+ license: r.provenance?.license,
39
+ author: r.provenance?.createdBy
40
+ ? { '@type': 'Person', name: r.provenance.createdBy }
41
+ : undefined,
42
+ contributor: r.provenance?.approvedBy
43
+ ? { '@type': 'Person', name: r.provenance.approvedBy }
44
+ : undefined,
45
+ isBasedOn: r.provenance?.source,
46
+ additionalProperty: (r.claims || []).map((c) => ({
47
+ '@type': 'PropertyValue',
48
+ name: 'velin.claim',
49
+ value: c,
50
+ })),
51
+ })),
52
+ };
53
+ }
54
+
55
+ function toCsv(items) {
56
+ const headers = [
57
+ 'id', 'type', 'status', 'review', 'label', 'claims',
58
+ 'createdBy', 'createdAt', 'reviewedAt', 'approvedBy', 'source', 'license', 'version', 'publishedAt',
59
+ ];
60
+ const lines = [headers.join(',')];
61
+ for (const r of items) {
62
+ const p = r.provenance || {};
63
+ const row = [
64
+ r.id, r.type, r.status, r.review, r.label, (r.claims || []).join('|'),
65
+ p.createdBy, p.createdAt, p.reviewedAt, p.approvedBy, p.source, p.license, p.version, p.publishedAt,
66
+ ].map(csvEscape);
67
+ lines.push(row.join(','));
68
+ }
69
+ return `${lines.join('\n')}\n`;
70
+ }
71
+
72
+ function csvEscape(v) {
73
+ const s = String(v ?? '');
74
+ if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
75
+ return s;
76
+ }
77
+
78
+ function toHtmlTable(items) {
79
+ const rows = items.map((r) => {
80
+ const p = r.provenance || {};
81
+ return `<tr>
82
+ <td>${esc(r.id)}</td><td>${esc(r.label)}</td><td>${esc((r.claims || []).join(', '))}</td>
83
+ <td>${esc(p.createdBy)}</td><td>${esc(p.createdAt)}</td><td>${esc(p.approvedBy)}</td>
84
+ <td>${esc(p.license)}</td><td>${esc(p.version)}</td><td>${esc(p.source)}</td>
85
+ </tr>`;
86
+ }).join('');
87
+ return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Transparency export</title></head>
88
+ <body><table border="1" cellpadding="6">
89
+ <thead><tr><th>Id</th><th>Label</th><th>Claims</th><th>Created by</th><th>Created</th><th>Approved by</th><th>License</th><th>Version</th><th>Source</th></tr></thead>
90
+ <tbody>${rows}</tbody></table></body></html>`;
91
+ }
92
+
93
+ function esc(s) {
94
+ return String(s ?? '')
95
+ .replace(/&/g, '&amp;')
96
+ .replace(/</g, '&lt;')
97
+ .replace(/>/g, '&gt;');
98
+ }
@@ -0,0 +1,30 @@
1
+ export { CLAIM_CATALOG, deriveClaims, claimLabel, primaryLabel, pillarForClaim } from './claims.js';
2
+ export {
3
+ DEFAULT_POLICY,
4
+ STRICT_MEDIA_POLICY,
5
+ normalizePolicy,
6
+ mediaRequirement,
7
+ requiredProvenanceFields,
8
+ providerRank,
9
+ } from './policy.js';
10
+ export { createRegistry, stableDisclosureId } from './registry.js';
11
+ export { normalizeDisclosure, mergeDisclosures } from './normalize.js';
12
+ export {
13
+ registerTransparencyProvider,
14
+ listTransparencyProviders,
15
+ getTransparencyProvider,
16
+ collectAllDisclosures,
17
+ collectFromHtmlString,
18
+ } from './providers.js';
19
+ export { validateRecord, validateRecords, inferMediaKind } from './validator.js';
20
+ export { transparencyDoctor } from './doctor.js';
21
+ export { buildTransparencyReports } from './reporter.js';
22
+ export { exportDisclosures } from './export.js';
23
+ export { transparencyMigrate } from './migrate.js';
24
+ export { createTransparencyEngine, TransparencyMIME } from './engine.js';
25
+ export {
26
+ registerTransparencyRenderer,
27
+ listTransparencyRenderers,
28
+ renderDisclosure,
29
+ } from './renderer.js';
30
+ export { attach, enhanceAll, getDefaultRegistry, VelinTransparency } from './attach.js';
@@ -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
+ }