@birdapi/velinstyle 1.2.1 → 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 (156) hide show
  1. package/README.de.md +148 -11
  2. package/README.md +174 -16
  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 +441 -161
  17. package/cli/docgen/extract-cli.js +2 -1
  18. package/cli/docs-generate.js +9 -0
  19. package/cli/experience.js +173 -0
  20. package/cli/index.js +1384 -361
  21. package/cli/motion.js +738 -0
  22. package/cli/production/component-graph.json +166 -0
  23. package/cli/production/explain.js +52 -0
  24. package/cli/production/extract.js +278 -0
  25. package/cli/production/graph.js +102 -0
  26. package/cli/production/report.js +168 -0
  27. package/cli/production/run.js +355 -0
  28. package/cli/production/trim-css.js +177 -0
  29. package/cli/production/trim-fonts.js +23 -0
  30. package/cli/production/trim-icons.js +38 -0
  31. package/cli/production/trim-js.js +48 -0
  32. package/cli/production/trim-motion.js +22 -0
  33. package/cli/production/trim-themes.js +50 -0
  34. package/cli/production/watch.js +38 -0
  35. package/cli/review.js +48 -1
  36. package/cli/scripts/write-atelier-fixtures.mjs +33 -0
  37. package/cli/security.js +190 -0
  38. package/cli/skills.js +53 -10
  39. package/cli/transparency.js +140 -41
  40. package/cli/workflow.js +32 -17
  41. package/components/index.js +3 -0
  42. package/components/runtime/component-loaders.js +3 -0
  43. package/components/velin-drawer.js +43 -4
  44. package/components/velin-empty-state.js +83 -0
  45. package/components/velin-modal.js +76 -15
  46. package/components/velin-otp-input.js +220 -0
  47. package/components/velin-password-strength.js +147 -0
  48. package/components/velin-sheet.js +49 -4
  49. package/components/velin-theme-toggle.js +15 -1
  50. package/core/a11y/component-contracts.json +391 -255
  51. package/core/attributes/registry.js +1 -1
  52. package/core/meta/knowledge/components.json +75 -3
  53. package/core/motion/analyze.js +38 -0
  54. package/core/motion/blueprint.js +51 -0
  55. package/core/motion/composer.js +114 -0
  56. package/core/motion/data/registry.json +352 -0
  57. package/core/motion/effects.js +17 -1
  58. package/core/motion/index.js +112 -9
  59. package/core/motion/optimize.js +44 -0
  60. package/core/motion/policy.js +77 -0
  61. package/core/motion/registry.js +131 -0
  62. package/core/motion/review.js +59 -0
  63. package/core/motion/scan.js +150 -0
  64. package/core/motion/timeline.js +68 -0
  65. package/core/motion/transitions.js +49 -0
  66. package/core/motion/triggers.js +168 -0
  67. package/core/security/checks/fs.js +92 -0
  68. package/core/security/detect.js +35 -0
  69. package/core/security/engine.js +116 -0
  70. package/core/security/index.js +18 -0
  71. package/core/security/registry.js +85 -0
  72. package/core/security/report/report.js +113 -0
  73. package/core/security/rules/ci/index.js +146 -0
  74. package/core/security/rules/consumer/index.js +174 -0
  75. package/core/security/rules/deps/index.js +182 -0
  76. package/core/security/rules/index.js +24 -0
  77. package/core/security/rules/publish/index.js +230 -0
  78. package/core/security/rules/repo/index.js +143 -0
  79. package/core/security/rules/secrets/index.js +133 -0
  80. package/core/security/score/dependency-health.js +53 -0
  81. package/core/security/score/release-health.js +66 -0
  82. package/core/security/score/score.js +102 -0
  83. package/core/security/timeline/timeline.js +76 -0
  84. package/dist/chunks/attributes-2ORR27KA.js +404 -0
  85. package/dist/chunks/attributes-HK7VIOLA.js +1080 -0
  86. package/dist/chunks/attributes-VRHHVNDO.js +1080 -0
  87. package/dist/chunks/chunk-MYKUI364.js +116 -0
  88. package/dist/chunks/chunk-NEVBPD5T.js +116 -0
  89. package/dist/chunks/chunk-Y6HN7HWX.js +116 -0
  90. package/dist/chunks/runtime-entry.js +1 -1
  91. package/dist/chunks/velin-drawer-A7Q7EBOS.js +162 -0
  92. package/dist/chunks/velin-empty-state-3NTUH2RF.js +81 -0
  93. package/dist/chunks/velin-modal-3MV4FYBK.js +231 -0
  94. package/dist/chunks/velin-otp-input-PSK42MM6.js +207 -0
  95. package/dist/chunks/velin-password-strength-TAXMLSED.js +136 -0
  96. package/dist/chunks/velin-sheet-N2VVYXFP.js +157 -0
  97. package/dist/chunks/velin-theme-toggle-J2NHC7IM.js +304 -0
  98. package/dist/llms.txt +4 -4
  99. package/dist/search-index.json +232 -5
  100. package/dist/velin-agent.json +433 -46
  101. package/dist/velinstyle-components.iife.js +3695 -2272
  102. package/dist/velinstyle-components.js +3698 -2271
  103. package/dist/velinstyle-components.min.js +262 -120
  104. package/dist/velinstyle.css +96 -13
  105. package/dist/velinstyle.d.ts +3 -0
  106. package/dist/velinstyle.min.css +1 -1
  107. package/package.json +19 -4
  108. package/packages/velinstyle-cli-core/package.json +11 -0
  109. package/packages/velinstyle-cli-core/src/config.js +84 -0
  110. package/packages/velinstyle-cli-core/src/contract.js +56 -0
  111. package/packages/velinstyle-cli-core/src/event-bus.js +50 -0
  112. package/packages/velinstyle-cli-core/src/index.js +18 -0
  113. package/packages/velinstyle-cli-core/src/lifecycle.js +33 -0
  114. package/packages/velinstyle-cli-core/src/runner.js +129 -0
  115. package/packages/velinstyle-cli-registry/data/commands.json +1216 -0
  116. package/packages/velinstyle-cli-registry/data/layouts.json +33 -0
  117. package/packages/velinstyle-cli-registry/data/widgets.json +35 -0
  118. package/packages/velinstyle-cli-registry/package.json +13 -0
  119. package/packages/velinstyle-cli-registry/src/index.js +88 -0
  120. package/packages/velinstyle-cli-renderer/package.json +8 -0
  121. package/packages/velinstyle-cli-renderer/src/index.js +286 -0
  122. package/packages/velinstyle-cli-theme/package.json +8 -0
  123. package/packages/velinstyle-cli-theme/src/index.js +84 -0
  124. package/packages/velinstyle-cli-ui/package.json +8 -0
  125. package/packages/velinstyle-cli-ui/src/index.js +34 -0
  126. package/packages/velinstyle-motion/README.md +81 -0
  127. package/packages/velinstyle-motion/package.json +37 -0
  128. package/packages/velinstyle-motion/src/ai/mini-prompt.js +119 -0
  129. package/packages/velinstyle-motion/src/ai/provider.js +59 -0
  130. package/packages/velinstyle-motion/src/ai/providers/pixverse-setup.js +402 -0
  131. package/packages/velinstyle-motion/src/ai/providers/pixverse.js +245 -0
  132. package/packages/velinstyle-motion/src/config.js +257 -0
  133. package/packages/velinstyle-motion/src/export/html-clip.js +141 -0
  134. package/packages/velinstyle-motion/src/index.js +304 -0
  135. package/packages/velinstyle-motion/src/jobs/cache.js +30 -0
  136. package/packages/velinstyle-motion/src/jobs/queue.js +69 -0
  137. package/packages/velinstyle-motion/src/jobs/usage.js +53 -0
  138. package/packages/velinstyle-motion/src/local/effects.js +139 -0
  139. package/packages/velinstyle-motion/src/local/engine.js +96 -0
  140. package/packages/velinstyle-motion/src/providers-catalog.js +44 -0
  141. package/packages/velinstyle-motion/src/registry/presets.js +25 -0
  142. package/packages/velinstyle-motion/src/registry/presets.json +85 -0
  143. package/packages/velinstyle-motion/src/ux/cost-gate.js +37 -0
  144. package/packages/velinstyle-motion/src/ux/pixverse-gate.js +153 -0
  145. package/packages/velinstyle-motion/src/ux/wizard.js +171 -0
  146. package/packages/velinstyle-motion/src/validate-image.js +91 -0
  147. package/packages/velinstyle-skills/catalog.json +1 -1
  148. package/packages/velinstyle-skills/registry.json +1 -142
  149. package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +1 -1
  150. package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +1 -1
  151. package/src/components/data-table.css +19 -0
  152. package/src/components/empty-auth.css +33 -0
  153. package/src/components/table.css +16 -6
  154. package/src/tokens/motion.css +7 -0
  155. package/src/utilities/scroll-animation.css +66 -0
  156. package/src/velinstyle.css +1 -0
@@ -0,0 +1,143 @@
1
+ import { registerSecurityRule } from '../../registry.js';
2
+ import { fileExists, readJson, readText, walkFiles, rel } from '../../checks/fs.js';
3
+ import { join } from 'path';
4
+
5
+ export function registerRepoRules() {
6
+ registerSecurityRule({
7
+ id: 'repo.package-json',
8
+ category: 'repo',
9
+ severity: 'error',
10
+ modes: ['repo', 'consumer', 'audit'],
11
+ title: 'package.json',
12
+ run(ctx) {
13
+ if (fileExists(join(ctx.root, 'package.json'))) return [];
14
+ return [{
15
+ ruleId: 'repo.package-json',
16
+ category: 'repo',
17
+ severity: 'error',
18
+ message: 'package.json missing',
19
+ path: 'package.json',
20
+ }];
21
+ },
22
+ });
23
+
24
+ registerSecurityRule({
25
+ id: 'repo.codeowners',
26
+ category: 'repo',
27
+ severity: 'info',
28
+ modes: ['repo', 'audit'],
29
+ title: 'CODEOWNERS',
30
+ run(ctx) {
31
+ if (
32
+ fileExists(join(ctx.root, 'CODEOWNERS'))
33
+ || fileExists(join(ctx.root, '.github', 'CODEOWNERS'))
34
+ ) return [];
35
+ return [{
36
+ ruleId: 'repo.codeowners',
37
+ category: 'repo',
38
+ severity: 'info',
39
+ message: 'CODEOWNERS not found',
40
+ fix: 'Add .github/CODEOWNERS for review ownership',
41
+ }];
42
+ },
43
+ });
44
+
45
+ registerSecurityRule({
46
+ id: 'repo.dependabot',
47
+ category: 'repo',
48
+ severity: 'info',
49
+ modes: ['repo', 'audit'],
50
+ title: 'Dependabot / Renovate',
51
+ run(ctx) {
52
+ if (
53
+ fileExists(join(ctx.root, '.github', 'dependabot.yml'))
54
+ || fileExists(join(ctx.root, 'renovate.json'))
55
+ || fileExists(join(ctx.root, '.github', 'renovate.json'))
56
+ ) return [];
57
+ return [{
58
+ ruleId: 'repo.dependabot',
59
+ category: 'repo',
60
+ severity: 'info',
61
+ message: 'No Dependabot/Renovate config',
62
+ fix: 'Enable dependency update automation',
63
+ }];
64
+ },
65
+ });
66
+
67
+ registerSecurityRule({
68
+ id: 'repo.backup-files',
69
+ category: 'repo',
70
+ severity: 'warning',
71
+ modes: ['repo', 'consumer', 'audit', 'scan'],
72
+ title: 'Backup / debug files',
73
+ run(ctx) {
74
+ const files = walkFiles(
75
+ ctx.root,
76
+ (n) => /\.(bak|old|tmp|swp)$/i.test(n) || /^debug\.log$/i.test(n),
77
+ 40,
78
+ );
79
+ return files.map((abs) => ({
80
+ ruleId: 'repo.backup-files',
81
+ category: 'repo',
82
+ severity: 'warning',
83
+ message: 'Backup/debug file in tree',
84
+ path: rel(ctx.root, abs),
85
+ fix: 'Delete or gitignore',
86
+ }));
87
+ },
88
+ });
89
+
90
+ registerSecurityRule({
91
+ id: 'repo.exec-spawn',
92
+ category: 'repo',
93
+ severity: 'info',
94
+ modes: ['repo', 'audit'],
95
+ title: 'child_process usage',
96
+ run(ctx) {
97
+ const files = walkFiles(
98
+ ctx.root,
99
+ (n, abs) => /\.(js|mjs|cjs)$/i.test(n) && !abs.includes(`${join('node_modules')}`),
100
+ 120,
101
+ );
102
+ const findings = [];
103
+ for (const abs of files) {
104
+ const t = readText(abs);
105
+ if (!t) continue;
106
+ if (/child_process|execSync|spawnSync|\bexec\(|\bspawn\(/.test(t)) {
107
+ const path = rel(ctx.root, abs);
108
+ if (path.startsWith('cli/') || path.startsWith('scripts/') || path.startsWith('core/security/')) {
109
+ continue; // allowlisted tooling
110
+ }
111
+ findings.push({
112
+ ruleId: 'repo.exec-spawn',
113
+ category: 'repo',
114
+ severity: 'info',
115
+ message: 'Process spawn/exec detected — review for injection',
116
+ path,
117
+ });
118
+ }
119
+ }
120
+ return findings.slice(0, 25);
121
+ },
122
+ });
123
+
124
+ registerSecurityRule({
125
+ id: 'repo.publishconfig',
126
+ category: 'repo',
127
+ severity: 'info',
128
+ modes: ['repo', 'publish', 'audit'],
129
+ title: 'publishConfig',
130
+ run(ctx) {
131
+ const pkg = readJson(join(ctx.root, 'package.json'));
132
+ if (!pkg || pkg.name !== '@birdapi/velinstyle') return [];
133
+ if (pkg.publishConfig?.access === 'public') return [];
134
+ return [{
135
+ ruleId: 'repo.publishconfig',
136
+ category: 'repo',
137
+ severity: 'info',
138
+ message: 'Consider publishConfig.access=public for scoped package',
139
+ path: 'package.json',
140
+ }];
141
+ },
142
+ });
143
+ }
@@ -0,0 +1,133 @@
1
+ import { registerSecurityRule } from '../../registry.js';
2
+ import { fileExists, readText, walkFiles, rel } from '../../checks/fs.js';
3
+ import { join } from 'path';
4
+
5
+ const SECRET_RE =
6
+ /(?:api[_-]?key|secret|token|password|passwd|auth[_-]?token)\s*[=:]\s*['"][^'"]{8,}['"]/i;
7
+ const AWS_RE = /AKIA[0-9A-Z]{16}/;
8
+ const PRIVATE_KEY = /-----BEGIN (?:RSA |OPENSSH )?PRIVATE KEY-----/;
9
+
10
+ export function registerSecretsRules() {
11
+ registerSecurityRule({
12
+ id: 'secrets.env-tracked',
13
+ category: 'secrets',
14
+ severity: 'error',
15
+ modes: ['repo', 'consumer', 'secrets', 'audit', 'publish'],
16
+ title: 'Tracked .env',
17
+ description: '.env must not be committed.',
18
+ run(ctx) {
19
+ const findings = [];
20
+ for (const name of ['.env', '.env.local', '.env.production']) {
21
+ const p = join(ctx.root, name);
22
+ if (fileExists(p)) {
23
+ findings.push({
24
+ ruleId: 'secrets.env-tracked',
25
+ category: 'secrets',
26
+ severity: 'error',
27
+ message: `${name} exists in project root — ensure it is gitignored and not committed`,
28
+ path: name,
29
+ fix: 'Add to .gitignore; use secrets manager / CI secrets',
30
+ });
31
+ }
32
+ }
33
+ return findings;
34
+ },
35
+ });
36
+
37
+ registerSecurityRule({
38
+ id: 'secrets.gitignore-env',
39
+ category: 'secrets',
40
+ severity: 'warning',
41
+ modes: ['repo', 'consumer', 'secrets', 'audit'],
42
+ title: '.gitignore covers .env',
43
+ run(ctx) {
44
+ const gi = readText(join(ctx.root, '.gitignore')) || '';
45
+ if (!gi.includes('.env')) {
46
+ return [{
47
+ ruleId: 'secrets.gitignore-env',
48
+ category: 'secrets',
49
+ severity: 'warning',
50
+ message: '.gitignore does not mention .env',
51
+ path: '.gitignore',
52
+ fix: 'Add .env and .env.* to .gitignore',
53
+ }];
54
+ }
55
+ return [];
56
+ },
57
+ });
58
+
59
+ registerSecurityRule({
60
+ id: 'secrets.npmrc-auth',
61
+ category: 'secrets',
62
+ severity: 'error',
63
+ modes: ['repo', 'consumer', 'secrets', 'audit', 'publish'],
64
+ title: 'npmrc auth token',
65
+ run(ctx) {
66
+ const findings = [];
67
+ for (const name of ['.npmrc', '.npmrc.local']) {
68
+ const t = readText(join(ctx.root, name));
69
+ if (t && /_authToken|_auth\s*=|\/\/.*:_password=/i.test(t)) {
70
+ findings.push({
71
+ ruleId: 'secrets.npmrc-auth',
72
+ category: 'secrets',
73
+ severity: 'error',
74
+ message: `${name} contains auth credentials`,
75
+ path: name,
76
+ fix: 'Use env NPM_TOKEN; never commit tokens',
77
+ });
78
+ }
79
+ }
80
+ return findings;
81
+ },
82
+ });
83
+
84
+ registerSecurityRule({
85
+ id: 'secrets.hardcoded',
86
+ category: 'secrets',
87
+ severity: 'error',
88
+ modes: ['repo', 'consumer', 'secrets', 'scan', 'audit'],
89
+ title: 'Hardcoded secrets',
90
+ run(ctx) {
91
+ const findings = [];
92
+ const files = walkFiles(
93
+ ctx.root,
94
+ (n) => /\.(js|mjs|cjs|ts|json|env|yml|yaml|sh|ps1|html)$/i.test(n),
95
+ 200,
96
+ );
97
+ for (const abs of files) {
98
+ const t = readText(abs);
99
+ if (!t) continue;
100
+ if (PRIVATE_KEY.test(t) || AWS_RE.test(t) || SECRET_RE.test(t)) {
101
+ findings.push({
102
+ ruleId: 'secrets.hardcoded',
103
+ category: 'secrets',
104
+ severity: 'error',
105
+ message: 'Possible hardcoded secret or private key',
106
+ path: rel(ctx.root, abs),
107
+ fix: 'Remove secret; rotate if exposed',
108
+ });
109
+ }
110
+ }
111
+ return findings;
112
+ },
113
+ });
114
+
115
+ registerSecurityRule({
116
+ id: 'secrets.pem-files',
117
+ category: 'secrets',
118
+ severity: 'error',
119
+ modes: ['repo', 'consumer', 'secrets', 'audit'],
120
+ title: 'PEM / key files',
121
+ run(ctx) {
122
+ const files = walkFiles(ctx.root, (n) => /\.(pem|key|p12|pfx)$/i.test(n), 50);
123
+ return files.map((abs) => ({
124
+ ruleId: 'secrets.pem-files',
125
+ category: 'secrets',
126
+ severity: 'error',
127
+ message: 'Key/certificate file present in tree',
128
+ path: rel(ctx.root, abs),
129
+ fix: 'Store outside repo / use secret store',
130
+ }));
131
+ },
132
+ });
133
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Dependency Health summary from findings + package.json heuristics.
3
+ * Note: "outdated" here is heuristic (message/code), not live `npm outdated`.
4
+ */
5
+ import { join } from 'path';
6
+ import { fileExists, readJson } from '../checks/fs.js';
7
+
8
+ /**
9
+ * @param {string} root
10
+ * @param {import('../registry.js').SecurityFinding[]} findings
11
+ */
12
+ export function computeDependencyHealth(root, findings) {
13
+ const depFindings = findings.filter(
14
+ (f) =>
15
+ (f.category === 'deps' || f.ruleId?.startsWith('deps.'))
16
+ || f.category === 'malware'
17
+ || f.ruleId?.includes('lifecycle')
18
+ || f.ruleId?.includes('lockfile')
19
+ || f.ruleId?.includes('typosquat'),
20
+ );
21
+
22
+ const pkg = readJson(join(root, 'package.json')) || {};
23
+ const hasLock = fileExists(join(root, 'package-lock.json')) || fileExists(join(root, 'pnpm-lock.yaml')) || fileExists(join(root, 'yarn.lock'));
24
+
25
+ let deprecated = depFindings.filter((f) => /deprecat/i.test(f.message) || f.code === 'deprecated').length;
26
+ let noLockfile = depFindings.filter((f) => f.code === 'no-lockfile' || f.ruleId === 'deps.lockfile').length;
27
+ if (!hasLock && noLockfile === 0) noLockfile = 1;
28
+ // Heuristic "outdated" only when explicitly coded/messaged — not lockfile absence
29
+ let outdated = depFindings.filter((f) =>
30
+ f.code === 'outdated' || (/outdated/i.test(f.message) && f.code !== 'no-lockfile'),
31
+ ).length;
32
+ let critical = depFindings.filter((f) => f.severity === 'error').length;
33
+
34
+ let score = 100;
35
+ score -= critical * 20;
36
+ score -= deprecated * 8;
37
+ score -= outdated * 5;
38
+ score -= noLockfile * 15;
39
+ if (!pkg.engines?.node) score -= 3;
40
+ score = Math.max(0, Math.min(100, score));
41
+
42
+ return {
43
+ score,
44
+ deprecated,
45
+ outdated,
46
+ noLockfile,
47
+ critical,
48
+ hasLockfile: hasLock,
49
+ hasEngines: Boolean(pkg.engines?.node),
50
+ note: 'outdated is heuristic only (no live npm outdated)',
51
+ gate: critical > 0 ? 'fail' : score < 80 ? 'warn' : 'pass',
52
+ };
53
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Release Health — pre-publish gate summary.
3
+ */
4
+ import { join } from 'path';
5
+ import { fileExists, readJson, readText } from '../checks/fs.js';
6
+ import { execSync } from 'child_process';
7
+
8
+ /**
9
+ * @param {string} root
10
+ * @param {import('../registry.js').SecurityFinding[]} findings
11
+ */
12
+ export function computeReleaseHealth(root, findings) {
13
+ const pkg = readJson(join(root, 'package.json')) || {};
14
+ const version = pkg.version || '';
15
+
16
+ const checks = {
17
+ readme: fileExists(join(root, 'README.md')),
18
+ changelog: fileExists(join(root, 'CHANGELOG.md')),
19
+ license: fileExists(join(root, 'LICENSE')) || fileExists(join(root, 'LICENSE.md')) || Boolean(pkg.license),
20
+ securityMd: fileExists(join(root, 'SECURITY.md')),
21
+ releaseNotes:
22
+ fileExists(join(root, `RELEASE_NOTES_${version}.md`))
23
+ || (version && (readText(join(root, 'CHANGELOG.md')) || '').includes(`[${version}]`)),
24
+ provenance:
25
+ Boolean(pkg.publishConfig?.provenance)
26
+ || /id-token\s*:\s*write/.test(readText(join(root, '.github', 'workflows', 'publish-npm.yml')) || ''),
27
+ };
28
+
29
+ let tagOk = null;
30
+ let cleanTree = null;
31
+ try {
32
+ const tag = execSync(`git tag -l "v${version}"`, { cwd: root, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
33
+ tagOk = Boolean(tag);
34
+ } catch {
35
+ tagOk = null;
36
+ }
37
+ try {
38
+ const dirty = execSync('git status --porcelain', { cwd: root, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
39
+ cleanTree = dirty.length === 0;
40
+ } catch {
41
+ cleanTree = null;
42
+ }
43
+
44
+ const publishErrors = findings.filter(
45
+ (f) => (f.category === 'publish' || f.ruleId?.startsWith('publish.') || f.ruleId?.startsWith('release.')) && f.severity === 'error',
46
+ ).length;
47
+
48
+ const missing = Object.entries(checks).filter(([, v]) => !v).map(([k]) => k);
49
+ let score = 100;
50
+ score -= missing.length * 10;
51
+ score -= publishErrors * 15;
52
+ if (tagOk === false) score -= 8;
53
+ if (cleanTree === false) score -= 5;
54
+ score = Math.max(0, Math.min(100, score));
55
+
56
+ const ready = publishErrors === 0 && checks.readme && checks.changelog && checks.license && checks.securityMd;
57
+ return {
58
+ score,
59
+ gate: ready ? (missing.length || cleanTree === false ? 'warn' : 'pass') : 'fail',
60
+ ready: ready && publishErrors === 0,
61
+ status: ready && publishErrors === 0 ? 'READY FOR PUBLISH' : 'BLOCKED',
62
+ checks: { ...checks, tagMatchesVersion: tagOk, workingTreeClean: cleanTree },
63
+ missing,
64
+ version,
65
+ };
66
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Aggregate Security Engine findings into category scores + overall.
3
+ */
4
+
5
+ const SEV_WEIGHT = { error: 18, warning: 8, info: 2 };
6
+
7
+ /**
8
+ * @param {import('../registry.js').SecurityFinding[]} findings
9
+ */
10
+ export function scoreFindings(findings) {
11
+ const byCat = {};
12
+ for (const f of findings) {
13
+ const cat = f.code || f.category || 'repo';
14
+ // findings carry rule category via ruleId prefix or explicit
15
+ }
16
+
17
+ const categories = {
18
+ supplyChain: { label: 'Supply Chain', findings: [] },
19
+ secrets: { label: 'Secrets', findings: [] },
20
+ dependencies: { label: 'Dependencies', findings: [] },
21
+ publish: { label: 'Publish', findings: [] },
22
+ ci: { label: 'CI', findings: [] },
23
+ workflows: { label: 'Workflows', findings: [] },
24
+ malware: { label: 'Malware', findings: [] },
25
+ repo: { label: 'Repository', findings: [] },
26
+ consumer: { label: 'Consumer', findings: [] },
27
+ };
28
+
29
+ for (const f of findings) {
30
+ const cat = mapFindingCategory(f);
31
+ if (!categories[cat]) categories[cat] = { label: cat, findings: [] };
32
+ categories[cat].findings.push(f);
33
+ }
34
+
35
+ /** @type {Record<string, { score: number, gate: string, label: string, count: number }>} */
36
+ const scores = {};
37
+ let totalDeduction = 0;
38
+ let errorCount = 0;
39
+
40
+ for (const [key, bucket] of Object.entries(categories)) {
41
+ let ded = 0;
42
+ for (const f of bucket.findings) {
43
+ ded += SEV_WEIGHT[f.severity] || 4;
44
+ if (f.severity === 'error') errorCount += 1;
45
+ }
46
+ ded = Math.min(100, ded);
47
+ const score = Math.max(0, 100 - ded);
48
+ const gate = gateFromFindings(bucket.findings);
49
+ if (key === 'malware' && bucket.findings.length === 0) {
50
+ scores[key] = {
51
+ score: null,
52
+ na: true,
53
+ gate: 'pass',
54
+ label: bucket.label,
55
+ count: 0,
56
+ note: 'no malware rules fired',
57
+ };
58
+ } else {
59
+ scores[key] = { score, gate, label: bucket.label, count: bucket.findings.length };
60
+ totalDeduction += ded * 0.12;
61
+ }
62
+ }
63
+
64
+ const securityScore = Math.max(0, Math.min(100, Math.round(100 - totalDeduction)));
65
+ const overallGate = errorCount > 0 ? 'fail' : Object.values(scores).some((s) => s.gate === 'warn') ? 'warn' : 'pass';
66
+ const overall = overallGate === 'fail' ? 'BLOCKED' : overallGate === 'warn' ? 'READY_WITH_WARNINGS' : 'READY';
67
+
68
+ return { securityScore, scores, overall, overallGate, findings };
69
+ }
70
+
71
+ function mapFindingCategory(f) {
72
+ if (f.category) {
73
+ if (f.category === 'deps') return 'dependencies';
74
+ if (f.category === 'ci') return 'ci';
75
+ if (f.category === 'secrets') return 'secrets';
76
+ if (f.category === 'publish') return 'publish';
77
+ if (f.category === 'malware') return 'malware';
78
+ if (f.category === 'consumer') return 'consumer';
79
+ if (f.category === 'repo') return 'repo';
80
+ }
81
+ const id = f.ruleId || '';
82
+ if (id.startsWith('secrets.') || id.startsWith('secret.')) return 'secrets';
83
+ if (id === 'deps.lifecycle-hooks' || id === 'deps.typosquat' || id.startsWith('malware.')) return 'malware';
84
+ if (id.startsWith('deps.') || id.startsWith('dependency.')) return 'dependencies';
85
+ if (id.startsWith('workflow.') || id.startsWith('ci.')) return 'ci';
86
+ if (id.startsWith('publish.') || id.startsWith('release.')) return 'publish';
87
+ if (id.startsWith('consumer.')) return 'consumer';
88
+ if (id.includes('supply') || id.startsWith('lockfile') || id.startsWith('lifecycle')) return 'supplyChain';
89
+ return 'repo';
90
+ }
91
+
92
+ function gateFromFindings(findings) {
93
+ if (findings.some((f) => f.severity === 'error')) return 'fail';
94
+ if (findings.some((f) => f.severity === 'warning')) return 'warn';
95
+ return 'pass';
96
+ }
97
+
98
+ export function gateLabel(gate) {
99
+ if (gate === 'fail') return 'FAIL';
100
+ if (gate === 'warn') return 'WARNING';
101
+ return 'PASS';
102
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Security Timeline — local last-run stamps.
3
+ */
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
5
+ import { dirname, join } from 'path';
6
+
7
+ const CATEGORIES = ['repository', 'dependencies', 'publish', 'ci', 'secrets', 'consumer', 'audit'];
8
+
9
+ /**
10
+ * @param {string} root
11
+ */
12
+ export function timelinePath(root) {
13
+ return join(root, '.velin', 'security', 'timeline.json');
14
+ }
15
+
16
+ /**
17
+ * @param {string} root
18
+ */
19
+ export function loadTimeline(root) {
20
+ const p = timelinePath(root);
21
+ if (!existsSync(p)) {
22
+ return { version: 1, entries: {} };
23
+ }
24
+ try {
25
+ return JSON.parse(readFileSync(p, 'utf-8'));
26
+ } catch {
27
+ return { version: 1, entries: {} };
28
+ }
29
+ }
30
+
31
+ /**
32
+ * @param {string} root
33
+ * @param {string[]} categories
34
+ * @param {Date} [when]
35
+ */
36
+ export function touchTimeline(root, categories, when = new Date()) {
37
+ const data = loadTimeline(root);
38
+ const iso = when.toISOString();
39
+ const dateLabel = when.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
40
+ for (const c of categories) {
41
+ const key = normalizeCat(c);
42
+ data.entries[key] = { at: iso, label: dateLabel };
43
+ }
44
+ const p = timelinePath(root);
45
+ mkdirSync(dirname(p), { recursive: true });
46
+ writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, 'utf-8');
47
+ return data;
48
+ }
49
+
50
+ function normalizeCat(c) {
51
+ if (c === 'repo') return 'repository';
52
+ if (c === 'deps') return 'dependencies';
53
+ return c;
54
+ }
55
+
56
+ /**
57
+ * Rows for Experience / report.
58
+ * @param {ReturnType<typeof loadTimeline>} data
59
+ */
60
+ export function timelineRows(data) {
61
+ const labels = {
62
+ repository: 'Repository geprüft',
63
+ dependencies: 'Dependencies geprüft',
64
+ publish: 'Publish geprüft',
65
+ ci: 'CI geprüft',
66
+ secrets: 'Secrets geprüft',
67
+ consumer: 'Consumer geprüft',
68
+ audit: 'Audit geprüft',
69
+ };
70
+ return CATEGORIES.filter((k) => data.entries?.[k]).map((k) => ({
71
+ key: k,
72
+ title: labels[k] || k,
73
+ date: data.entries[k].label || data.entries[k].at?.slice(0, 10),
74
+ at: data.entries[k].at,
75
+ }));
76
+ }