@birdapi/velinstyle 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.de.md +18 -4
  2. package/README.md +42 -7
  3. package/cli/cli-manifest.json +441 -174
  4. package/cli/docgen/extract-cli.js +2 -1
  5. package/cli/docs-generate.js +9 -0
  6. package/cli/experience.js +173 -0
  7. package/cli/index.js +1294 -417
  8. package/cli/motion.js +738 -0
  9. package/cli/production/extract.js +46 -6
  10. package/cli/production/report.js +104 -14
  11. package/cli/production/run.js +44 -1
  12. package/cli/security.js +190 -0
  13. package/cli/skills.js +53 -10
  14. package/cli/transparency.js +140 -41
  15. package/cli/workflow.js +32 -17
  16. package/components/velin-theme-toggle.js +15 -1
  17. package/core/a11y/component-contracts.json +391 -274
  18. package/core/attributes/registry.js +1 -1
  19. package/core/motion/analyze.js +38 -0
  20. package/core/motion/blueprint.js +51 -0
  21. package/core/motion/composer.js +114 -0
  22. package/core/motion/data/registry.json +352 -0
  23. package/core/motion/effects.js +17 -1
  24. package/core/motion/index.js +112 -9
  25. package/core/motion/optimize.js +44 -0
  26. package/core/motion/policy.js +77 -0
  27. package/core/motion/registry.js +131 -0
  28. package/core/motion/review.js +59 -0
  29. package/core/motion/scan.js +150 -0
  30. package/core/motion/timeline.js +68 -0
  31. package/core/motion/transitions.js +49 -0
  32. package/core/motion/triggers.js +168 -0
  33. package/core/security/checks/fs.js +92 -0
  34. package/core/security/detect.js +35 -0
  35. package/core/security/engine.js +116 -0
  36. package/core/security/index.js +18 -0
  37. package/core/security/registry.js +85 -0
  38. package/core/security/report/report.js +113 -0
  39. package/core/security/rules/ci/index.js +146 -0
  40. package/core/security/rules/consumer/index.js +174 -0
  41. package/core/security/rules/deps/index.js +182 -0
  42. package/core/security/rules/index.js +24 -0
  43. package/core/security/rules/publish/index.js +230 -0
  44. package/core/security/rules/repo/index.js +143 -0
  45. package/core/security/rules/secrets/index.js +133 -0
  46. package/core/security/score/dependency-health.js +53 -0
  47. package/core/security/score/release-health.js +66 -0
  48. package/core/security/score/score.js +102 -0
  49. package/core/security/timeline/timeline.js +76 -0
  50. package/dist/chunks/attributes-HK7VIOLA.js +1080 -0
  51. package/dist/chunks/attributes-VRHHVNDO.js +1080 -0
  52. package/dist/chunks/chunk-MYKUI364.js +116 -0
  53. package/dist/chunks/chunk-NEVBPD5T.js +116 -0
  54. package/dist/chunks/runtime-entry.js +1 -1
  55. package/dist/chunks/velin-theme-toggle-J2NHC7IM.js +304 -0
  56. package/dist/llms.txt +2 -2
  57. package/dist/search-index.json +166 -2
  58. package/dist/velin-agent.json +156 -32
  59. package/dist/velinstyle-components.iife.js +862 -17
  60. package/dist/velinstyle-components.js +862 -17
  61. package/dist/velinstyle-components.min.js +126 -126
  62. package/dist/velinstyle.css +42 -7
  63. package/dist/velinstyle.min.css +1 -1
  64. package/package.json +158 -143
  65. package/packages/velinstyle-cli-core/package.json +11 -0
  66. package/packages/velinstyle-cli-core/src/config.js +84 -0
  67. package/packages/velinstyle-cli-core/src/contract.js +56 -0
  68. package/packages/velinstyle-cli-core/src/event-bus.js +50 -0
  69. package/packages/velinstyle-cli-core/src/index.js +18 -0
  70. package/packages/velinstyle-cli-core/src/lifecycle.js +33 -0
  71. package/packages/velinstyle-cli-core/src/runner.js +129 -0
  72. package/packages/velinstyle-cli-registry/data/commands.json +1216 -0
  73. package/packages/velinstyle-cli-registry/data/layouts.json +33 -0
  74. package/packages/velinstyle-cli-registry/data/widgets.json +35 -0
  75. package/packages/velinstyle-cli-registry/package.json +13 -0
  76. package/packages/velinstyle-cli-registry/src/index.js +88 -0
  77. package/packages/velinstyle-cli-renderer/package.json +8 -0
  78. package/packages/velinstyle-cli-renderer/src/index.js +286 -0
  79. package/packages/velinstyle-cli-theme/package.json +8 -0
  80. package/packages/velinstyle-cli-theme/src/index.js +84 -0
  81. package/packages/velinstyle-cli-ui/package.json +8 -0
  82. package/packages/velinstyle-cli-ui/src/index.js +34 -0
  83. package/packages/velinstyle-motion/README.md +81 -0
  84. package/packages/velinstyle-motion/package.json +37 -0
  85. package/packages/velinstyle-motion/src/ai/mini-prompt.js +119 -0
  86. package/packages/velinstyle-motion/src/ai/provider.js +59 -0
  87. package/packages/velinstyle-motion/src/ai/providers/pixverse-setup.js +402 -0
  88. package/packages/velinstyle-motion/src/ai/providers/pixverse.js +245 -0
  89. package/packages/velinstyle-motion/src/config.js +257 -0
  90. package/packages/velinstyle-motion/src/export/html-clip.js +141 -0
  91. package/packages/velinstyle-motion/src/index.js +304 -0
  92. package/packages/velinstyle-motion/src/jobs/cache.js +30 -0
  93. package/packages/velinstyle-motion/src/jobs/queue.js +69 -0
  94. package/packages/velinstyle-motion/src/jobs/usage.js +53 -0
  95. package/packages/velinstyle-motion/src/local/effects.js +139 -0
  96. package/packages/velinstyle-motion/src/local/engine.js +96 -0
  97. package/packages/velinstyle-motion/src/providers-catalog.js +44 -0
  98. package/packages/velinstyle-motion/src/registry/presets.js +25 -0
  99. package/packages/velinstyle-motion/src/registry/presets.json +85 -0
  100. package/packages/velinstyle-motion/src/ux/cost-gate.js +37 -0
  101. package/packages/velinstyle-motion/src/ux/pixverse-gate.js +153 -0
  102. package/packages/velinstyle-motion/src/ux/wizard.js +171 -0
  103. package/packages/velinstyle-motion/src/validate-image.js +91 -0
  104. package/packages/velinstyle-skills/catalog.json +1 -1
  105. package/packages/velinstyle-skills/registry.json +1 -142
  106. package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +1 -1
  107. package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +1 -1
  108. package/src/tokens/motion.css +7 -0
  109. package/src/utilities/scroll-animation.css +66 -0
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Timeline Engine — Scene → Timeline → Steps (Registry-backed).
3
+ */
4
+ import { getTimeline } from './registry.js';
5
+ import { markVisible, prefersReducedMotion } from './effects.js';
6
+
7
+ /**
8
+ * Run a registered timeline against a root element.
9
+ * Steps resolve `[data-velin-step="id"]` or `#id` or `[velin-step="id"]`.
10
+ * @param {string} timelineId
11
+ * @param {ParentNode} root
12
+ * @param {{ stepMs?: number, policyDisabled?: boolean }} [opts]
13
+ * @returns {{ ok: boolean, error?: string, ran: string[] }}
14
+ */
15
+ export function runTimeline(timelineId, root, opts = {}) {
16
+ const tl = getTimeline(timelineId);
17
+ if (!tl) return { ok: false, error: `Unknown timeline "${timelineId}"`, ran: [] };
18
+ const stepMs = opts.stepMs ?? 120;
19
+ const ran = [];
20
+
21
+ if (opts.policyDisabled || prefersReducedMotion()) {
22
+ for (const step of tl.steps || []) {
23
+ const el = resolveStep(root, step);
24
+ if (el) markVisible(el);
25
+ ran.push(step);
26
+ }
27
+ return { ok: true, ran };
28
+ }
29
+
30
+ let delay = 0;
31
+ for (const step of tl.steps || []) {
32
+ const el = resolveStep(root, step);
33
+ if (!el) continue;
34
+ const d = delay;
35
+ delay += stepMs;
36
+ if (typeof window !== 'undefined' && window.setTimeout) {
37
+ window.setTimeout(() => markVisible(el), d);
38
+ } else {
39
+ markVisible(el);
40
+ }
41
+ ran.push(step);
42
+ }
43
+ return { ok: true, ran, timeline: tl };
44
+ }
45
+
46
+ function resolveStep(root, step) {
47
+ if (!root?.querySelector) return null;
48
+ return (
49
+ root.querySelector(`[data-velin-step="${step}"]`)
50
+ || root.querySelector(`[velin-step="${step}"]`)
51
+ || root.querySelector(`#${CSS.escape?.(step) || step}`)
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Enhance `[velin-timeline="id"]` roots.
57
+ */
58
+ export function enhanceTimelines(root) {
59
+ if (!root?.querySelectorAll) return () => {};
60
+ const nodes = root.querySelectorAll('[velin-timeline]');
61
+ const teardowns = [];
62
+ for (const el of nodes) {
63
+ const id = el.getAttribute('velin-timeline');
64
+ const result = runTimeline(id, el);
65
+ teardowns.push(() => result);
66
+ }
67
+ return () => {};
68
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Transition helpers (View Transition + overlay hooks).
3
+ */
4
+ import { getMotion } from './registry.js';
5
+
6
+ /**
7
+ * Run a document view transition if supported.
8
+ * @param {() => void|Promise<void>} updateCallback
9
+ * @param {{ name?: string }} [opts]
10
+ */
11
+ export async function runViewTransition(updateCallback, opts = {}) {
12
+ const doc = typeof document !== 'undefined' ? document : null;
13
+ if (!doc) {
14
+ await updateCallback();
15
+ return { ok: true, mode: 'direct' };
16
+ }
17
+ if (typeof doc.startViewTransition === 'function') {
18
+ const vt = doc.startViewTransition(async () => {
19
+ if (opts.name && doc.documentElement) {
20
+ doc.documentElement.style.viewTransitionName = opts.name;
21
+ }
22
+ await updateCallback();
23
+ });
24
+ await vt.finished.catch(() => {});
25
+ return { ok: true, mode: 'view-transition', id: opts.name || null };
26
+ }
27
+ await updateCallback();
28
+ return { ok: true, mode: 'fallback' };
29
+ }
30
+
31
+ export function getTransitionPreset(id) {
32
+ return getMotion('transitions', id);
33
+ }
34
+
35
+ /**
36
+ * Apply transition class hints for modal/drawer hosts.
37
+ * @param {HTMLElement} el
38
+ * @param {'modal-enter'|'drawer-enter'|'cross-fade'} id
39
+ */
40
+ export function applyTransitionHint(el, id) {
41
+ if (!el) return;
42
+ const map = {
43
+ 'modal-enter': 'velin-animate-fade-in',
44
+ 'drawer-enter': 'velin-animate-on-scroll--slide-right',
45
+ 'cross-fade': 'velin-animate-fade-in',
46
+ };
47
+ const cls = map[id];
48
+ if (cls) el.classList.add(cls);
49
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Motion Trigger Engine — when motion starts (First-Class, Studio-ready).
3
+ */
4
+ import { observeInView } from './scheduler.js';
5
+ import { markVisible, prefersReducedMotion } from './effects.js';
6
+ import { getTrigger } from './registry.js';
7
+
8
+ export const TRIGGER_V1 = ['load', 'visible', 'hover', 'focus', 'click', 'scroll'];
9
+ export const TRIGGER_V2 = ['hidden', 'idle', 'media', 'container', 'theme', 'custom'];
10
+ export const ALL_TRIGGERS = [...TRIGGER_V1, ...TRIGGER_V2];
11
+
12
+ /**
13
+ * Read trigger id from element (velin-trigger or default).
14
+ * @param {HTMLElement} el
15
+ * @param {string} [fallback='visible']
16
+ */
17
+ export function readTrigger(el, fallback = 'visible') {
18
+ const raw = (el.getAttribute('velin-trigger') || fallback || 'visible').trim().toLowerCase();
19
+ return raw || fallback;
20
+ }
21
+
22
+ /**
23
+ * Bind a trigger to run `onFire` once (or per event).
24
+ * @param {HTMLElement} el
25
+ * @param {string} triggerId
26
+ * @param {() => void} onFire
27
+ * @param {{ once?: boolean, reduced?: boolean }} [opts]
28
+ * @returns {() => void} teardown
29
+ */
30
+ export function bindTrigger(el, triggerId, onFire, opts = {}) {
31
+ const once = opts.once !== false;
32
+ const reduced = opts.reduced ?? prefersReducedMotion();
33
+ const id = (triggerId || 'visible').toLowerCase();
34
+ const meta = getTrigger(id);
35
+
36
+ if (reduced || el.dataset.velinMotion === 'off') {
37
+ onFire();
38
+ return () => {};
39
+ }
40
+
41
+ if (meta?.status === 'planned' && !TRIGGER_V1.includes(id)) {
42
+ // Planned V2 triggers are not first-class yet: fall back to visible for runtime safety
43
+ return bindTrigger(el, 'visible', onFire, opts);
44
+ }
45
+
46
+ let fired = false;
47
+ const fire = () => {
48
+ if (once && fired) return;
49
+ fired = true;
50
+ onFire();
51
+ };
52
+
53
+ if (id === 'load') {
54
+ fire();
55
+ return () => {};
56
+ }
57
+
58
+ if (id === 'visible' || id === 'scroll') {
59
+ return observeInView(el, () => fire());
60
+ }
61
+
62
+ if (id === 'hover') {
63
+ const enter = () => fire();
64
+ el.addEventListener('pointerenter', enter);
65
+ return () => el.removeEventListener('pointerenter', enter);
66
+ }
67
+
68
+ if (id === 'focus') {
69
+ const onFocus = () => fire();
70
+ el.addEventListener('focusin', onFocus);
71
+ return () => el.removeEventListener('focusin', onFocus);
72
+ }
73
+
74
+ if (id === 'click') {
75
+ const onClick = () => fire();
76
+ el.addEventListener('click', onClick);
77
+ return () => el.removeEventListener('click', onClick);
78
+ }
79
+
80
+ if (id === 'hidden') {
81
+ const stop = observeInView(el, () => {
82
+ /* enter */
83
+ });
84
+ // Fallback: use IntersectionObserver with leave
85
+ if (typeof IntersectionObserver === 'undefined') return () => {};
86
+ const io = new IntersectionObserver((entries) => {
87
+ for (const entry of entries) {
88
+ if (!entry.isIntersecting) fire();
89
+ }
90
+ }, { threshold: 0 });
91
+ io.observe(el);
92
+ return () => {
93
+ stop();
94
+ io.disconnect();
95
+ };
96
+ }
97
+
98
+ if (id === 'idle') {
99
+ const ms = Number(el.getAttribute('velin-trigger-delay') || 3000);
100
+ const t = setTimeout(fire, ms);
101
+ return () => clearTimeout(t);
102
+ }
103
+
104
+ if (id === 'media') {
105
+ const q = el.getAttribute('velin-trigger-media') || '(min-width: 768px)';
106
+ if (typeof window === 'undefined' || !window.matchMedia) {
107
+ fire();
108
+ return () => {};
109
+ }
110
+ const mql = window.matchMedia(q);
111
+ const handler = () => {
112
+ if (mql.matches) fire();
113
+ };
114
+ handler();
115
+ mql.addEventListener?.('change', handler);
116
+ return () => mql.removeEventListener?.('change', handler);
117
+ }
118
+
119
+ if (id === 'theme') {
120
+ const root = el.ownerDocument?.documentElement;
121
+ if (!root || typeof MutationObserver === 'undefined') {
122
+ fire();
123
+ return () => {};
124
+ }
125
+ const mo = new MutationObserver(() => fire());
126
+ mo.observe(root, { attributes: true, attributeFilter: ['data-velin-theme', 'class'] });
127
+ return () => mo.disconnect();
128
+ }
129
+
130
+ if (id === 'custom') {
131
+ const evt = el.getAttribute('velin-trigger-event') || 'velin:motion';
132
+ const handler = () => fire();
133
+ el.addEventListener(evt, handler);
134
+ return () => el.removeEventListener(evt, handler);
135
+ }
136
+
137
+ if (id === 'container') {
138
+ // Approximate with visible until container queries runtime ships
139
+ return observeInView(el, () => fire());
140
+ }
141
+
142
+ // Unknown → visible
143
+ return observeInView(el, () => fire());
144
+ }
145
+
146
+ /**
147
+ * Apply motion classes then bind trigger to markVisible.
148
+ * @param {HTMLElement} el
149
+ * @param {() => void} applyPreset
150
+ */
151
+ export function armMotionElement(el, applyPreset) {
152
+ applyPreset();
153
+ const trigger = readTrigger(el, el.hasAttribute('velin-hover') ? 'hover' : 'visible');
154
+ if (prefersReducedMotion() || el.dataset.velinMotion === 'off') {
155
+ markVisible(el);
156
+ return () => {};
157
+ }
158
+ if (trigger === 'hover' || trigger === 'focus') {
159
+ // CSS hover classes already apply; still mark visible for scroll classes
160
+ markVisible(el);
161
+ return () => {};
162
+ }
163
+ if (trigger === 'load') {
164
+ markVisible(el);
165
+ return () => {};
166
+ }
167
+ return bindTrigger(el, trigger, () => markVisible(el));
168
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Shared FS helpers for Security Engine rules.
3
+ */
4
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
5
+ import { join, relative } from 'path';
6
+
7
+ const SKIP = new Set(['node_modules', '.git', 'dist', 'vendor', 'coverage', '.velin']);
8
+
9
+ /**
10
+ * @param {string} root
11
+ * @param {(name: string, abs: string) => boolean} [filter]
12
+ * @param {number} [max=400]
13
+ */
14
+ export function walkFiles(root, filter, max = 400) {
15
+ const out = [];
16
+ const walk = (dir) => {
17
+ if (out.length >= max) return;
18
+ let entries;
19
+ try {
20
+ entries = readdirSync(dir);
21
+ } catch {
22
+ return;
23
+ }
24
+ for (const name of entries) {
25
+ if (SKIP.has(name)) continue;
26
+ const abs = join(dir, name);
27
+ let st;
28
+ try {
29
+ st = statSync(abs);
30
+ } catch {
31
+ continue;
32
+ }
33
+ if (st.isDirectory()) walk(abs);
34
+ else if (!filter || filter(name, abs)) out.push(abs);
35
+ if (out.length >= max) return;
36
+ }
37
+ };
38
+ if (existsSync(root)) walk(root);
39
+ return out;
40
+ }
41
+
42
+ export function readText(path) {
43
+ try {
44
+ return readFileSync(path, 'utf-8');
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ export function readJson(path) {
51
+ const t = readText(path);
52
+ if (!t) return null;
53
+ try {
54
+ return JSON.parse(t);
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ export function rel(root, abs) {
61
+ return relative(root, abs).replace(/\\/g, '/');
62
+ }
63
+
64
+ export function fileExists(path) {
65
+ return existsSync(path);
66
+ }
67
+
68
+ /**
69
+ * Lightweight YAML-ish peek for GitHub Actions (no full parser).
70
+ * @param {string} text
71
+ */
72
+ export function workflowHints(text) {
73
+ const lines = text.split(/\r?\n/);
74
+ const uses = [];
75
+ let hasPullRequestTarget = /pull_request_target\s*:/.test(text);
76
+ let hasPermissions = /^\s*permissions\s*:/m.test(text);
77
+ let hasIdToken = /id-token\s*:\s*write/.test(text);
78
+ let mentionsNpmToken = /NPM_TOKEN|NODE_AUTH_TOKEN/.test(text);
79
+ for (const line of lines) {
80
+ const m = line.match(/^\s*-\s*uses:\s*(.+)\s*$/);
81
+ if (m) uses.push(m[1].trim().replace(/['"]/g, ''));
82
+ }
83
+ return { uses, hasPullRequestTarget, hasPermissions, hasIdToken, mentionsNpmToken };
84
+ }
85
+
86
+ /** Pin check: owner/action@sha40 vs @v1 */
87
+ export function isShaPinned(usesRef) {
88
+ const at = usesRef.lastIndexOf('@');
89
+ if (at < 0) return false;
90
+ const ref = usesRef.slice(at + 1);
91
+ return /^[0-9a-f]{40}$/i.test(ref);
92
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Detect repo vs consumer vs both for Security Engine.
3
+ */
4
+ import { join } from 'path';
5
+ import { fileExists, readJson } from './checks/fs.js';
6
+
7
+ /**
8
+ * @param {string} root
9
+ * @returns {'repo'|'consumer'|'both'}
10
+ */
11
+ export function detectSecurityMode(root) {
12
+ const pkg = readJson(join(root, 'package.json'));
13
+ const isFramework =
14
+ pkg?.name === '@birdapi/velinstyle'
15
+ || fileExists(join(root, '.github', 'workflows', 'publish-npm.yml'))
16
+ || (fileExists(join(root, 'SECURITY.md')) && fileExists(join(root, 'cli', 'index.js')));
17
+
18
+ const hasVelinDep =
19
+ Boolean(pkg?.dependencies?.['@birdapi/velinstyle'] || pkg?.devDependencies?.['@birdapi/velinstyle'])
20
+ || fileExists(join(root, 'vendor', 'velinstyle'))
21
+ || fileExists(join(root, 'vendor', 'velinstyle.min.css'));
22
+
23
+ const consumerMarkers =
24
+ fileExists(join(root, 'artisan'))
25
+ || fileExists(join(root, 'composer.json'))
26
+ || hasVelinDep
27
+ || Boolean(pkg && pkg.name !== '@birdapi/velinstyle');
28
+
29
+ if (isFramework && consumerMarkers && hasVelinDep && pkg?.name !== '@birdapi/velinstyle') {
30
+ return 'both';
31
+ }
32
+ if (isFramework) return 'repo';
33
+ if (consumerMarkers || pkg) return 'consumer';
34
+ return 'consumer';
35
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Velin Security Engine — run registered rules.
3
+ */
4
+ import { selectSecurityRules } from './registry.js';
5
+ import { loadBuiltinSecurityRules } from './rules/index.js';
6
+ import { detectSecurityMode } from './detect.js';
7
+ import { scoreFindings } from './score/score.js';
8
+ import { computeDependencyHealth } from './score/dependency-health.js';
9
+ import { computeReleaseHealth } from './score/release-health.js';
10
+ import { loadTimeline, touchTimeline } from './timeline/timeline.js';
11
+ import { resolve } from 'path';
12
+
13
+ /**
14
+ * @param {object} opts
15
+ * @param {string} [opts.root]
16
+ * @param {string} [opts.mode] — doctor|repo|consumer|ci|publish|deps|secrets|scan|audit
17
+ * @param {string[]} [opts.categories]
18
+ * @param {boolean} [opts.updateTimeline=true]
19
+ * @param {boolean} [opts.includeReleaseHealth]
20
+ * @param {boolean} [opts.includeDependencyHealth]
21
+ */
22
+ export async function runSecurityEngine(opts = {}) {
23
+ loadBuiltinSecurityRules();
24
+ const root = resolve(opts.root || '.');
25
+ let mode = opts.mode || 'doctor';
26
+
27
+ let detected = detectSecurityMode(root);
28
+ if (mode === 'doctor') {
29
+ mode = detected === 'both' ? 'audit' : detected;
30
+ }
31
+
32
+ const categoryMap = {
33
+ deps: ['deps', 'malware'],
34
+ secrets: ['secrets'],
35
+ ci: ['ci'],
36
+ publish: ['publish', 'repo', 'secrets', 'deps', 'malware'],
37
+ scan: ['consumer', 'secrets', 'repo', 'malware'],
38
+ repo: ['repo', 'ci', 'secrets', 'deps', 'publish', 'malware'],
39
+ consumer: ['consumer', 'secrets', 'deps', 'malware'],
40
+ audit: null,
41
+ };
42
+
43
+ const categories = opts.categories || categoryMap[mode] || null;
44
+ const rules = selectSecurityRules({
45
+ mode: mode === 'audit' ? 'audit' : mode,
46
+ categories: categories || undefined,
47
+ });
48
+
49
+ const ctx = { root, mode, categories: categories || undefined, meta: { detected } };
50
+ /** @type {import('./registry.js').SecurityFinding[]} */
51
+ const findings = [];
52
+
53
+ for (const rule of rules) {
54
+ try {
55
+ const out = await rule.run(ctx);
56
+ for (const f of out || []) {
57
+ findings.push({
58
+ category: rule.category,
59
+ ...f,
60
+ ruleId: f.ruleId || rule.id,
61
+ });
62
+ }
63
+ } catch (err) {
64
+ findings.push({
65
+ ruleId: rule.id,
66
+ category: rule.category,
67
+ severity: 'warning',
68
+ message: `Rule failed: ${err?.message || err}`,
69
+ });
70
+ }
71
+ }
72
+
73
+ const scored = scoreFindings(findings);
74
+ const includeDep = opts.includeDependencyHealth !== false;
75
+ const includeRel =
76
+ opts.includeReleaseHealth === true
77
+ || mode === 'publish'
78
+ || (mode === 'audit' && detected === 'repo');
79
+
80
+ const dependencyHealth = includeDep ? computeDependencyHealth(root, findings) : null;
81
+ const releaseHealth = includeRel ? computeReleaseHealth(root, findings) : null;
82
+
83
+ const touched = [];
84
+ if (mode === 'ci' || categories?.includes('ci')) touched.push('ci');
85
+ if (mode === 'deps' || categories?.includes('deps')) touched.push('dependencies');
86
+ if (mode === 'secrets' || categories?.includes('secrets')) touched.push('secrets');
87
+ if (mode === 'publish') touched.push('publish');
88
+ if (mode === 'repo' || detected === 'repo') touched.push('repository');
89
+ if (mode === 'consumer' || detected === 'consumer') touched.push('consumer');
90
+ if (mode === 'audit' || mode === 'scan') touched.push('audit');
91
+
92
+ let timeline = loadTimeline(root);
93
+ if (opts.updateTimeline !== false && touched.length) {
94
+ timeline = touchTimeline(root, touched);
95
+ }
96
+
97
+ return {
98
+ ...scored,
99
+ root,
100
+ mode,
101
+ detected,
102
+ rulesRun: rules.length,
103
+ dependencyHealth,
104
+ releaseHealth,
105
+ timeline,
106
+ ok: scored.overallGate !== 'fail',
107
+ };
108
+ }
109
+
110
+ export function createSecurityEngine(options = {}) {
111
+ loadBuiltinSecurityRules();
112
+ return {
113
+ detect: (root) => detectSecurityMode(resolve(root || options.root || '.')),
114
+ run: (opts = {}) => runSecurityEngine({ ...options, ...opts }),
115
+ };
116
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Velin Security Engine — public API.
3
+ */
4
+ export {
5
+ registerSecurityRule,
6
+ clearSecurityRules,
7
+ listSecurityRules,
8
+ getSecurityRule,
9
+ selectSecurityRules,
10
+ } from './registry.js';
11
+ export { detectSecurityMode } from './detect.js';
12
+ export { runSecurityEngine, createSecurityEngine } from './engine.js';
13
+ export { scoreFindings, gateLabel } from './score/score.js';
14
+ export { computeDependencyHealth } from './score/dependency-health.js';
15
+ export { computeReleaseHealth } from './score/release-health.js';
16
+ export { loadTimeline, touchTimeline, timelineRows, timelinePath } from './timeline/timeline.js';
17
+ export { toExperiencePayload, toMarkdownReport } from './report/report.js';
18
+ export { loadBuiltinSecurityRules } from './rules/index.js';
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Velin Security Engine — rule registry (Studio-/CLI-ready).
3
+ */
4
+
5
+ /** @typedef {'ci'|'secrets'|'deps'|'publish'|'repo'|'consumer'|'malware'} SecurityCategory */
6
+ /** @typedef {'info'|'warning'|'error'} SecuritySeverity */
7
+ /** @typedef {'repo'|'consumer'|'ci'|'publish'|'deps'|'secrets'|'scan'|'audit'} SecurityMode */
8
+
9
+ /**
10
+ * @typedef {object} SecurityFinding
11
+ * @property {string} ruleId
12
+ * @property {SecuritySeverity} severity
13
+ * @property {string} message
14
+ * @property {string} [path]
15
+ * @property {string} [fix]
16
+ * @property {string} [code]
17
+ */
18
+
19
+ /**
20
+ * @typedef {object} SecurityRule
21
+ * @property {string} id
22
+ * @property {SecurityCategory} category
23
+ * @property {SecuritySeverity} severity
24
+ * @property {SecurityMode[]} [modes]
25
+ * @property {string} [title]
26
+ * @property {string} [description]
27
+ * @property {(ctx: SecurityContext) => Promise<SecurityFinding[]>|SecurityFinding[]} run
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} SecurityContext
32
+ * @property {string} root
33
+ * @property {SecurityMode} mode
34
+ * @property {string[]} [categories]
35
+ * @property {import('fs').PathLike} [fs]
36
+ * @property {Record<string, unknown>} [meta]
37
+ */
38
+
39
+ /** @type {Map<string, SecurityRule>} */
40
+ const RULES = new Map();
41
+
42
+ /**
43
+ * @param {SecurityRule} rule
44
+ */
45
+ export function registerSecurityRule(rule) {
46
+ if (!rule?.id || typeof rule.run !== 'function') {
47
+ throw new Error('registerSecurityRule: id and run() required');
48
+ }
49
+ RULES.set(rule.id, {
50
+ severity: 'warning',
51
+ modes: ['repo', 'consumer', 'audit'],
52
+ title: rule.id,
53
+ description: '',
54
+ ...rule,
55
+ });
56
+ return rule.id;
57
+ }
58
+
59
+ export function clearSecurityRules() {
60
+ RULES.clear();
61
+ }
62
+
63
+ export function listSecurityRules() {
64
+ return [...RULES.values()];
65
+ }
66
+
67
+ export function getSecurityRule(id) {
68
+ return RULES.get(id) || null;
69
+ }
70
+
71
+ /**
72
+ * @param {{ mode?: SecurityMode, categories?: string[] }} [filter]
73
+ */
74
+ export function selectSecurityRules(filter = {}) {
75
+ const mode = filter.mode;
76
+ const cats = filter.categories?.length ? new Set(filter.categories) : null;
77
+ return listSecurityRules().filter((r) => {
78
+ if (cats && !cats.has(r.category)) return false;
79
+ if (!mode) return true;
80
+ const modes = r.modes || [];
81
+ if (mode === 'audit' || mode === 'doctor') return true;
82
+ if (mode === 'scan') return modes.includes('scan') || modes.includes('consumer') || modes.includes('secrets');
83
+ return modes.includes(mode) || modes.includes('audit');
84
+ });
85
+ }