@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.
- package/README.de.md +150 -24
- package/README.md +155 -33
- package/cli/__fixtures__/atelier-library/showcases/01-login/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/01-login/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/01-login/index.html +5 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/index.html +5 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/index.html +5 -0
- package/cli/atelier-catalog.json +1267 -0
- package/cli/atelier-formats.js +159 -0
- package/cli/atelier.js +382 -0
- package/cli/blueprints/empty-state.html +3 -5
- package/cli/cli-manifest.json +23 -10
- package/cli/docgen/extract-attributes.js +2 -0
- package/cli/docgen/extract-cli.js +1 -1
- package/cli/index.js +181 -6
- package/cli/production/component-graph.json +166 -0
- package/cli/production/explain.js +52 -0
- package/cli/production/extract.js +238 -0
- package/cli/production/graph.js +102 -0
- package/cli/production/report.js +78 -0
- package/cli/production/run.js +312 -0
- package/cli/production/trim-css.js +177 -0
- package/cli/production/trim-fonts.js +23 -0
- package/cli/production/trim-icons.js +38 -0
- package/cli/production/trim-js.js +48 -0
- package/cli/production/trim-motion.js +22 -0
- package/cli/production/trim-themes.js +50 -0
- package/cli/production/watch.js +38 -0
- package/cli/review.js +48 -1
- package/cli/scripts/write-atelier-fixtures.mjs +33 -0
- package/cli/transparency.js +221 -0
- package/components/index.js +3 -0
- package/components/runtime/component-loaders.js +3 -0
- package/components/velin-drawer.js +43 -4
- package/components/velin-empty-state.js +83 -0
- package/components/velin-modal.js +76 -15
- package/components/velin-otp-input.js +220 -0
- package/components/velin-password-strength.js +147 -0
- package/components/velin-sheet.js +49 -4
- package/core/a11y/component-contracts.json +23 -4
- package/core/attributes/registry.js +14 -0
- package/core/meta/knowledge/components.json +75 -3
- package/core/meta/schema.js +10 -0
- package/core/transparency/attach.js +74 -0
- package/core/transparency/claims.js +97 -0
- package/core/transparency/doctor.js +142 -0
- package/core/transparency/engine.js +75 -0
- package/core/transparency/export.js +98 -0
- package/core/transparency/index.js +30 -0
- package/core/transparency/migrate.js +130 -0
- package/core/transparency/normalize.js +125 -0
- package/core/transparency/policy.js +105 -0
- package/core/transparency/providers.js +235 -0
- package/core/transparency/registry.js +104 -0
- package/core/transparency/renderer.js +111 -0
- package/core/transparency/reporter.js +113 -0
- package/core/transparency/validator.js +112 -0
- package/dist/chunks/attach-H2ZSEAE6.js +365 -0
- package/dist/chunks/attributes-2ORR27KA.js +404 -0
- package/dist/chunks/attributes-DZCXX2RZ.js +404 -0
- package/dist/chunks/chunk-CQMTCEI6.js +113 -0
- package/dist/chunks/chunk-Y6HN7HWX.js +116 -0
- package/dist/chunks/runtime-entry.js +1 -1
- package/dist/chunks/velin-drawer-A7Q7EBOS.js +162 -0
- package/dist/chunks/velin-empty-state-3NTUH2RF.js +81 -0
- package/dist/chunks/velin-modal-3MV4FYBK.js +231 -0
- package/dist/chunks/velin-otp-input-PSK42MM6.js +207 -0
- package/dist/chunks/velin-password-strength-TAXMLSED.js +136 -0
- package/dist/chunks/velin-sheet-N2VVYXFP.js +157 -0
- package/dist/llms.txt +5 -4
- package/dist/search-index.json +94 -5
- package/dist/velin-agent.json +290 -23
- package/dist/velinstyle-components.iife.js +3189 -2202
- package/dist/velinstyle-components.js +3187 -2196
- package/dist/velinstyle-components.min.js +261 -119
- package/dist/velinstyle.css +214 -6
- package/dist/velinstyle.d.ts +3 -0
- package/dist/velinstyle.min.css +1 -1
- package/package.json +143 -142
- package/packages/velinstyle-skills/catalog.json +1 -1
- package/src/components/data-table.css +19 -0
- package/src/components/empty-auth.css +33 -0
- package/src/components/table.css +16 -6
- package/src/components/transparency.css +138 -0
- package/src/velinstyle.css +2 -0
|
@@ -201,7 +201,7 @@
|
|
|
201
201
|
"density": ["default"],
|
|
202
202
|
"themeSupport": ["light", "dark"],
|
|
203
203
|
"motion": { "reducedMotionSafe": true },
|
|
204
|
-
"examples": ["<velin-modal><h2 slot=\"title\">…</h2></velin-modal>"],
|
|
204
|
+
"examples": ["<velin-modal title=\"Confirm\"></velin-modal>", "<velin-modal><h2 slot=\"title\">…</h2></velin-modal>"],
|
|
205
205
|
"promptKeywords": ["modal", "dialog overlay", "popup"],
|
|
206
206
|
"designIntelligence": {
|
|
207
207
|
"visualWeight": 5,
|
|
@@ -491,7 +491,7 @@
|
|
|
491
491
|
"density": ["default", "compact"],
|
|
492
492
|
"themeSupport": ["light", "dark"],
|
|
493
493
|
"motion": { "reducedMotionSafe": true },
|
|
494
|
-
"examples": ["<velin-drawer></velin-drawer>"],
|
|
494
|
+
"examples": ["<velin-drawer title=\"Filters\"></velin-drawer>", "<velin-drawer><span slot=\"title\">Filters</span></velin-drawer>"],
|
|
495
495
|
"promptKeywords": ["drawer", "side panel", "sidebar overlay"],
|
|
496
496
|
"designIntelligence": {
|
|
497
497
|
"visualWeight": 4,
|
|
@@ -528,7 +528,7 @@
|
|
|
528
528
|
"density": ["default", "compact"],
|
|
529
529
|
"themeSupport": ["light", "dark"],
|
|
530
530
|
"motion": { "reducedMotionSafe": true },
|
|
531
|
-
"examples": ["<velin-sheet></velin-sheet>"],
|
|
531
|
+
"examples": ["<velin-sheet title=\"Actions\"></velin-sheet>", "<velin-sheet><span slot=\"title\">Actions</span></velin-sheet>"],
|
|
532
532
|
"promptKeywords": ["sheet", "bottom sheet", "action sheet"],
|
|
533
533
|
"designIntelligence": {
|
|
534
534
|
"visualWeight": 4,
|
|
@@ -617,6 +617,78 @@
|
|
|
617
617
|
"commonMistakes": ["toasts for persistent form errors"],
|
|
618
618
|
"bestPractices": ["auto-dismiss with pause on focus/hover"]
|
|
619
619
|
}
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
"id": "velin-otp-input",
|
|
623
|
+
"kind": "web-component",
|
|
624
|
+
"tag": "velin-otp-input",
|
|
625
|
+
"category": "forms",
|
|
626
|
+
"subcategory": "otp",
|
|
627
|
+
"purpose": "Segmented one-time-password / PIN digit entry with paste and keyboard support.",
|
|
628
|
+
"family": "forms",
|
|
629
|
+
"maturity": "core",
|
|
630
|
+
"recommendedPages": ["auth", "saas"],
|
|
631
|
+
"recommendedSections": [],
|
|
632
|
+
"compatibleWith": ["css-button", "velin-form-summary", "velin-secure-field"],
|
|
633
|
+
"incompatibleWith": [],
|
|
634
|
+
"accessibility": { "role": "group", "notes": "Per-digit labels; autocomplete one-time-code" },
|
|
635
|
+
"seo": { "impact": "none" },
|
|
636
|
+
"performance": { "cost": "lazy-ok" },
|
|
637
|
+
"variants": [],
|
|
638
|
+
"density": ["default"],
|
|
639
|
+
"themeSupport": ["light", "dark"],
|
|
640
|
+
"motion": { "reducedMotionSafe": true },
|
|
641
|
+
"examples": ["<velin-otp-input length=\"6\"></velin-otp-input>"],
|
|
642
|
+
"promptKeywords": ["otp", "2fa", "pin", "one-time password"],
|
|
643
|
+
"api": { "events": ["velin-change", "velin-complete"], "attributes": ["length", "value", "disabled", "name"] }
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
"id": "velin-password-strength",
|
|
647
|
+
"kind": "web-component",
|
|
648
|
+
"tag": "velin-password-strength",
|
|
649
|
+
"category": "forms",
|
|
650
|
+
"subcategory": "password",
|
|
651
|
+
"purpose": "Heuristic password strength meter linked to an input via for= or value.",
|
|
652
|
+
"family": "forms",
|
|
653
|
+
"maturity": "core",
|
|
654
|
+
"recommendedPages": ["auth", "saas"],
|
|
655
|
+
"recommendedSections": [],
|
|
656
|
+
"compatibleWith": ["css-input", "velin-secure-field", "velin-form-summary"],
|
|
657
|
+
"incompatibleWith": [],
|
|
658
|
+
"accessibility": { "role": "progressbar", "notes": "aria-live polite label" },
|
|
659
|
+
"seo": { "impact": "none" },
|
|
660
|
+
"performance": { "cost": "lazy-ok" },
|
|
661
|
+
"variants": [],
|
|
662
|
+
"density": ["default"],
|
|
663
|
+
"themeSupport": ["light", "dark"],
|
|
664
|
+
"motion": { "reducedMotionSafe": true },
|
|
665
|
+
"examples": ["<input id=\"pw\" type=\"password\"><velin-password-strength for=\"pw\"></velin-password-strength>"],
|
|
666
|
+
"promptKeywords": ["password strength", "meter", "signup"],
|
|
667
|
+
"api": { "events": ["velin-strength"], "attributes": ["for", "value", "label"] }
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
"id": "velin-empty-state",
|
|
671
|
+
"kind": "web-component",
|
|
672
|
+
"tag": "velin-empty-state",
|
|
673
|
+
"category": "feedback",
|
|
674
|
+
"subcategory": "empty",
|
|
675
|
+
"purpose": "Consistent empty / zero-data surface with illustration, title, description, and actions slots.",
|
|
676
|
+
"family": "feedback",
|
|
677
|
+
"maturity": "core",
|
|
678
|
+
"recommendedPages": ["dashboard", "saas", "shop", "docs"],
|
|
679
|
+
"recommendedSections": [],
|
|
680
|
+
"compatibleWith": ["css-button", "velin-icon"],
|
|
681
|
+
"incompatibleWith": [],
|
|
682
|
+
"accessibility": { "role": "status" },
|
|
683
|
+
"seo": { "impact": "none" },
|
|
684
|
+
"performance": { "cost": "lazy-ok" },
|
|
685
|
+
"variants": [],
|
|
686
|
+
"density": ["default"],
|
|
687
|
+
"themeSupport": ["light", "dark"],
|
|
688
|
+
"motion": { "reducedMotionSafe": true },
|
|
689
|
+
"examples": ["<velin-empty-state heading=\"No items yet\" description=\"Create your first entry.\"><button slot=\"actions\" class=\"velin-btn velin-btn--primary\">Create</button></velin-empty-state>"],
|
|
690
|
+
"promptKeywords": ["empty state", "no results", "zero data"],
|
|
691
|
+
"api": { "attributes": ["heading", "description"], "slots": ["illustration", "title", "description", "actions"] }
|
|
620
692
|
}
|
|
621
693
|
]
|
|
622
694
|
}
|
package/core/meta/schema.js
CHANGED
|
@@ -48,4 +48,14 @@ export const AGENT_CONVENTIONS = [
|
|
|
48
48
|
'Honor skill status, confidence, compatibility, and onlyIf predicates when automating.',
|
|
49
49
|
'Do not put secrets, API keys, or raw emails in velin-meta or page meta.',
|
|
50
50
|
'Resolve doc links against docs root (/docs/), not the current page folder.',
|
|
51
|
+
'Prefer Velin Transparency Framework (velin-transparency + provenance) for AI/trust/compliance labeling; run `velinstyle transparency doctor`.',
|
|
51
52
|
];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Optional page-level transparency block for Velin Meta.
|
|
56
|
+
* @typedef {Object} VelinPageTransparency
|
|
57
|
+
* @property {object} [policy]
|
|
58
|
+
* @property {object[]} [disclosures]
|
|
59
|
+
* @property {object[]} [items]
|
|
60
|
+
*/
|
|
61
|
+
|
|
@@ -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, '&')
|
|
96
|
+
.replace(/</g, '<')
|
|
97
|
+
.replace(/>/g, '>');
|
|
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';
|