chore(design-system): add extraction artifact and token drift gate
Record the design decisions actually in use and add a static gate that blocks new bare colors, bare sizes, important overrides, and wb token definitions outside token sources.
This commit is contained in:
1 parent
98cf32b6e2
commit
3ceae1e983
4 files changed
+4663
No files matched your search
@@ -0,0 +1,501 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* P0 设计 token 静态漂移守护(只读扫描)。
|
||||
*
|
||||
* 扫描范围:src/renderer/src 下的 .css / .tsx / .ts。
|
||||
* 统计:裸 hex、裸 px、!important、--wb-* 定义、var(--yc-*) / var(--wb-*) 使用。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/check-design-tokens.mjs 对比 baseline,出现回归退出码 1
|
||||
* node scripts/check-design-tokens.mjs --json 输出机器可读 JSON(stdout 仅 JSON)
|
||||
* node scripts/check-design-tokens.mjs --update-baseline 全量重写 baseline(唯一写操作)
|
||||
*
|
||||
* 分类(按顺序匹配):
|
||||
* vendor 路径包含 source-code-assets 目录
|
||||
* recovered 文件名或目录名含 recovered
|
||||
* adapter *-adapter.css / *-adapters.css
|
||||
* source source.css / source-*.css / *-source.css
|
||||
* core 路径包含 styles/ 目录
|
||||
* tokenSource 命中 TOKEN_SOURCE_PATHS 的精确路径(token 源文件,唯一允许写裸值的位置)
|
||||
* local 其余
|
||||
* 门禁分类:core / local / adapter;source / recovered / vendor / tokenSource 只记录与摘要,不阻塞。
|
||||
* 失败条件(仅四项):新增门禁文件含裸 hex / 裸 px / !important / --wb-* 定义;
|
||||
* 存量门禁文件这四项计数高于 baseline。var(--yc-*) / var(--wb-*) 使用数只记录与提示。
|
||||
*/
|
||||
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, extname, join, relative, resolve, sep } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO_ROOT = resolve(SCRIPT_DIR, '..')
|
||||
const SCAN_ROOT = join(REPO_ROOT, 'src', 'renderer', 'src')
|
||||
const SCAN_ROOT_LABEL = 'src/renderer/src'
|
||||
const BASELINE_PATH = join(SCRIPT_DIR, 'design-token-baseline.json')
|
||||
const BASELINE_LABEL = 'scripts/design-token-baseline.json'
|
||||
const BASELINE_VERSION = 1
|
||||
|
||||
const SCAN_EXTENSIONS = new Set(['.css', '.ts', '.tsx'])
|
||||
const CLASS_ORDER = ['vendor', 'recovered', 'adapter', 'source', 'core', 'tokenSource', 'local']
|
||||
const GATED_CLASSES = new Set(['core', 'local', 'adapter'])
|
||||
// token 源文件:唯一允许写裸 hex/px/!important/--wb-* 定义的位置。
|
||||
// 按规范化(小写、/ 分隔)后的精确路径豁免,不做文件名通配,避免页面级 tokens.css 逃逸。
|
||||
const TOKEN_SOURCE_PATHS = new Set([
|
||||
'src/renderer/src/styles/tokens.css',
|
||||
'src/renderer/src/workbuddy/typography/tokens.css',
|
||||
'src/renderer/src/ui/tokens.css'
|
||||
])
|
||||
const METRIC_LABELS = {
|
||||
bareHex: '裸 hex',
|
||||
barePx: '裸 px',
|
||||
important: '!important',
|
||||
wbDefinitions: '--wb-* 定义',
|
||||
ycVarUses: 'var(--yc-*) 使用',
|
||||
wbVarUses: 'var(--wb-*) 使用'
|
||||
}
|
||||
const METRICS = ['bareHex', 'barePx', 'important', 'wbDefinitions', 'ycVarUses', 'wbVarUses']
|
||||
// 门禁计数:新增与存量门禁文件都只对这四个指标的上升失败;var 使用数只提示。
|
||||
const GATE_METRICS = ['bareHex', 'barePx', 'important', 'wbDefinitions']
|
||||
const METRIC_PATTERNS = {
|
||||
bareHex: () => /#[0-9a-fA-F]{3,8}\b/g,
|
||||
barePx: () => /(?<![\w.])\d+(?:\.\d+)?px\b/gi,
|
||||
important: () => /!\s*important/gi,
|
||||
wbDefinitions: () => /--wb-[\w-]+\s*:/g,
|
||||
ycVarUses: () => /var\(\s*--yc-[\w-]*/g,
|
||||
wbVarUses: () => /var\(\s*--wb-[\w-]*/g
|
||||
}
|
||||
const MAX_LISTED = 20
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = { updateBaseline: false, json: false, help: false }
|
||||
for (const arg of argv) {
|
||||
if (arg === '--update-baseline') options.updateBaseline = true
|
||||
else if (arg === '--json') options.json = true
|
||||
else if (arg === '--help' || arg === '-h') options.help = true
|
||||
else throw new Error(`未知参数:${arg}`)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.log(
|
||||
[
|
||||
'用法:node scripts/check-design-tokens.mjs [--json] [--update-baseline]',
|
||||
'',
|
||||
' --json 输出机器可读 JSON(stdout 仅 JSON)',
|
||||
' --update-baseline 全量重写 scripts/design-token-baseline.json',
|
||||
' --help 显示本帮助'
|
||||
].join('\n')
|
||||
)
|
||||
}
|
||||
|
||||
function toPosix(value) {
|
||||
return value.split(sep).join('/')
|
||||
}
|
||||
|
||||
function zeroCounts() {
|
||||
const counts = {}
|
||||
for (const metric of METRICS) counts[metric] = 0
|
||||
return counts
|
||||
}
|
||||
|
||||
function normalizeCounts(value) {
|
||||
const counts = zeroCounts()
|
||||
if (!value || typeof value !== 'object') return counts
|
||||
for (const metric of METRICS) {
|
||||
const raw = value[metric]
|
||||
counts[metric] = typeof raw === 'number' && Number.isFinite(raw) ? raw : 0
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
function isSourceCssName(lowerName) {
|
||||
if (!lowerName.endsWith('.css')) return false
|
||||
return lowerName === 'source.css' || lowerName.startsWith('source-') || lowerName.endsWith('-source.css')
|
||||
}
|
||||
|
||||
function isAdapterCssName(lowerName) {
|
||||
return lowerName.endsWith('-adapter.css') || lowerName.endsWith('-adapters.css')
|
||||
}
|
||||
|
||||
function classifyPath(relPath) {
|
||||
if (TOKEN_SOURCE_PATHS.has(relPath.toLowerCase())) return 'tokenSource'
|
||||
const segments = relPath.split('/')
|
||||
const fileName = segments[segments.length - 1]
|
||||
const lowerName = fileName.toLowerCase()
|
||||
const directories = segments.slice(0, -1)
|
||||
if (directories.includes('source-code-assets')) return 'vendor'
|
||||
if (segments.some((segment) => segment.toLowerCase().includes('recovered'))) return 'recovered'
|
||||
if (isAdapterCssName(lowerName)) return 'adapter'
|
||||
if (isSourceCssName(lowerName)) return 'source'
|
||||
if (directories.includes('styles')) return 'core'
|
||||
return 'local'
|
||||
}
|
||||
|
||||
function listScannableFiles(dir) {
|
||||
const results = []
|
||||
const stack = [dir]
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()
|
||||
let entries
|
||||
try {
|
||||
entries = readdirSync(current, { withFileTypes: true })
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
|
||||
const fullPath = join(current, entry.name)
|
||||
if (entry.isDirectory()) stack.push(fullPath)
|
||||
else if (entry.isFile() && SCAN_EXTENSIONS.has(extname(entry.name).toLowerCase())) results.push(fullPath)
|
||||
}
|
||||
}
|
||||
return results.sort()
|
||||
}
|
||||
|
||||
function analyzeSource(text) {
|
||||
const counts = zeroCounts()
|
||||
for (const metric of METRICS) {
|
||||
const matches = text.match(METRIC_PATTERNS[metric]())
|
||||
counts[metric] = matches ? matches.length : 0
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
function scanFiles() {
|
||||
const startedAt = Date.now()
|
||||
const files = []
|
||||
const unreadable = []
|
||||
for (const absolutePath of listScannableFiles(SCAN_ROOT)) {
|
||||
const path = toPosix(relative(REPO_ROOT, absolutePath))
|
||||
let text
|
||||
try {
|
||||
text = readFileSync(absolutePath, 'utf8')
|
||||
} catch (error) {
|
||||
unreadable.push({ path, error: String(error && error.message ? error.message : error) })
|
||||
continue
|
||||
}
|
||||
files.push({ path, class: classifyPath(path), counts: analyzeSource(text) })
|
||||
}
|
||||
files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
||||
return { files, unreadable, durationMs: Date.now() - startedAt }
|
||||
}
|
||||
|
||||
function buildBaseline(files) {
|
||||
const entries = {}
|
||||
for (const file of files) {
|
||||
entries[file.path] = { class: file.class, counts: file.counts }
|
||||
}
|
||||
return {
|
||||
version: BASELINE_VERSION,
|
||||
description: '设计 token 静态计数 baseline,由 scripts/check-design-tokens.mjs --update-baseline 生成。不要手改。',
|
||||
scanRoot: SCAN_ROOT_LABEL,
|
||||
metrics: METRICS,
|
||||
gatedClasses: CLASS_ORDER.filter((name) => GATED_CLASSES.has(name)),
|
||||
files: entries
|
||||
}
|
||||
}
|
||||
|
||||
function loadBaseline() {
|
||||
if (!existsSync(BASELINE_PATH)) return null
|
||||
let parsed
|
||||
try {
|
||||
parsed = JSON.parse(readFileSync(BASELINE_PATH, 'utf8'))
|
||||
} catch (error) {
|
||||
throw new Error(`baseline 不是合法 JSON:${BASELINE_LABEL}(${error.message})`)
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object' || !parsed.files || typeof parsed.files !== 'object') {
|
||||
throw new Error(`baseline 结构无法识别:${BASELINE_LABEL}`)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
function summarize(files) {
|
||||
const byClass = {}
|
||||
for (const name of CLASS_ORDER) byClass[name] = { files: 0, counts: zeroCounts() }
|
||||
for (const file of files) {
|
||||
const bucket = byClass[file.class] ?? byClass.local
|
||||
bucket.files += 1
|
||||
for (const metric of METRICS) bucket.counts[metric] += file.counts[metric]
|
||||
}
|
||||
const gated = { files: 0, counts: zeroCounts() }
|
||||
const nonGated = { files: 0, counts: zeroCounts() }
|
||||
for (const name of CLASS_ORDER) {
|
||||
const target = GATED_CLASSES.has(name) ? gated : nonGated
|
||||
target.files += byClass[name].files
|
||||
for (const metric of METRICS) target.counts[metric] += byClass[name].counts[metric]
|
||||
}
|
||||
return { byClass, gated, nonGated }
|
||||
}
|
||||
|
||||
function baselineNonGatedTotals(baselineFiles) {
|
||||
const totals = { files: 0, counts: zeroCounts() }
|
||||
for (const entry of Object.values(baselineFiles)) {
|
||||
if (GATED_CLASSES.has(entry && entry.class)) continue
|
||||
totals.files += 1
|
||||
const counts = normalizeCounts(entry && entry.counts)
|
||||
for (const metric of METRICS) totals.counts[metric] += counts[metric]
|
||||
}
|
||||
return totals
|
||||
}
|
||||
|
||||
function compareWithBaseline(scan, baselineFiles) {
|
||||
const violations = []
|
||||
const notices = { deleted: [], newGatedFilesWithoutCounts: [], usageIncreases: [] }
|
||||
const currentByPath = new Set(scan.files.map((file) => file.path))
|
||||
|
||||
for (const file of scan.files) {
|
||||
const previous = baselineFiles[file.path]
|
||||
if (!previous) {
|
||||
if (!GATED_CLASSES.has(file.class)) continue
|
||||
const hits = GATE_METRICS.filter((metric) => file.counts[metric] > 0)
|
||||
if (hits.length > 0) {
|
||||
violations.push({
|
||||
type: 'new-file',
|
||||
path: file.path,
|
||||
class: file.class,
|
||||
metrics: hits.map((metric) => ({ metric, current: file.counts[metric] }))
|
||||
})
|
||||
} else {
|
||||
notices.newGatedFilesWithoutCounts.push({ path: file.path, class: file.class })
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!GATED_CLASSES.has(file.class)) continue
|
||||
const previousCounts = normalizeCounts(previous.counts)
|
||||
const increases = []
|
||||
const usageIncreases = []
|
||||
for (const metric of METRICS) {
|
||||
if (file.counts[metric] > previousCounts[metric]) {
|
||||
const increase = { metric, from: previousCounts[metric], to: file.counts[metric] }
|
||||
if (GATE_METRICS.includes(metric)) increases.push(increase)
|
||||
else usageIncreases.push(increase)
|
||||
}
|
||||
}
|
||||
if (increases.length > 0) {
|
||||
violations.push({ type: 'regression', path: file.path, class: file.class, increases })
|
||||
}
|
||||
if (usageIncreases.length > 0) {
|
||||
notices.usageIncreases.push({ path: file.path, class: file.class, increases: usageIncreases })
|
||||
}
|
||||
}
|
||||
|
||||
for (const [path, entry] of Object.entries(baselineFiles)) {
|
||||
if (!currentByPath.has(path)) notices.deleted.push({ path, class: (entry && entry.class) ?? 'unknown' })
|
||||
}
|
||||
notices.deleted.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
||||
notices.usageIncreases.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
||||
|
||||
for (const unreadable of scan.unreadable) {
|
||||
violations.push({ type: 'unreadable', path: unreadable.path, error: unreadable.error })
|
||||
}
|
||||
|
||||
return { violations, notices }
|
||||
}
|
||||
|
||||
function formatCounts(counts) {
|
||||
return METRICS.map((metric) => `${METRIC_LABELS[metric]}=${counts[metric]}`).join(' ')
|
||||
}
|
||||
|
||||
function formatDelta(delta) {
|
||||
return METRICS.map((metric) => `${METRIC_LABELS[metric]} ${delta[metric] >= 0 ? '+' : ''}${delta[metric]}`).join(' ')
|
||||
}
|
||||
|
||||
function formatList(items, limit = MAX_LISTED) {
|
||||
const shown = items.slice(0, limit)
|
||||
const rest = items.length - shown.length
|
||||
return rest > 0 ? `${shown.join(';')};…(还有 ${rest} 项,详见 --json)` : shown.join(';')
|
||||
}
|
||||
|
||||
function describeViolation(violation) {
|
||||
if (violation.type === 'regression') {
|
||||
const detail = violation.increases
|
||||
.map((item) => `${METRIC_LABELS[item.metric]} ${item.from} → ${item.to}(+${item.to - item.from})`)
|
||||
.join(';')
|
||||
return `[回归] ${violation.path}(${violation.class}):${detail}`
|
||||
}
|
||||
if (violation.type === 'new-file') {
|
||||
const detail = violation.metrics.map((item) => `${METRIC_LABELS[item.metric]}=${item.current}`).join(',')
|
||||
return `[新增门禁文件] ${violation.path}(${violation.class}):${detail}`
|
||||
}
|
||||
return `[无法读取] ${violation.path}:${violation.error}`
|
||||
}
|
||||
|
||||
function printHuman(result) {
|
||||
const lines = []
|
||||
lines.push('设计 token 漂移检查(静态)')
|
||||
const fileCount = result.scan.files.length
|
||||
lines.push(` 扫描:${SCAN_ROOT_LABEL} 共 ${fileCount} 个文件(用时 ${result.scan.durationMs} ms)`)
|
||||
if (result.updated) {
|
||||
lines.push(` baseline:已写入 ${BASELINE_LABEL}(${fileCount} 个文件)`)
|
||||
} else {
|
||||
lines.push(` baseline:${BASELINE_LABEL}(${result.baselineFiles} 个文件)`)
|
||||
}
|
||||
const summary = result.summary
|
||||
lines.push(
|
||||
` 门禁分类:${CLASS_ORDER.filter((name) => GATED_CLASSES.has(name))
|
||||
.map((name) => `${name} ${summary.byClass[name].files}`)
|
||||
.join(' | ')}(合计 ${summary.gated.files} 个文件)`
|
||||
)
|
||||
lines.push(` 合计:${formatCounts(summary.gated.counts)}`)
|
||||
lines.push(
|
||||
` 非门禁分类:${CLASS_ORDER.filter((name) => !GATED_CLASSES.has(name))
|
||||
.map((name) => `${name} ${summary.byClass[name].files}`)
|
||||
.join(' | ')}(合计 ${summary.nonGated.files} 个文件,仅记录不阻塞)`
|
||||
)
|
||||
lines.push(` 合计:${formatCounts(summary.nonGated.counts)}`)
|
||||
if (result.nonGatedDelta) {
|
||||
lines.push(` 非门禁相对 baseline:文件 ${result.nonGatedDelta.files >= 0 ? '+' : ''}${result.nonGatedDelta.files};${formatDelta(result.nonGatedDelta.counts)}`)
|
||||
}
|
||||
lines.push('')
|
||||
|
||||
if (result.updated) {
|
||||
lines.push('结果:baseline 已更新(退出码 0)。')
|
||||
} else if (result.violations.length === 0) {
|
||||
lines.push('结果:通过,无 token 漂移回归。')
|
||||
} else {
|
||||
lines.push(`结果:失败,共 ${result.violations.length} 项:`)
|
||||
lines.push(...result.violations.map((violation) => ` ${describeViolation(violation)}`))
|
||||
lines.push('')
|
||||
lines.push('提示:确认变更符合预期后运行 `node scripts/check-design-tokens.mjs --update-baseline` 刷新 baseline;')
|
||||
lines.push(' 否则请改用 --yc-* token、复用现有值,或去掉重复的 --wb-* 定义。')
|
||||
}
|
||||
|
||||
if (!result.updated) {
|
||||
const notices = result.notices ?? { deleted: [], newGatedFilesWithoutCounts: [], usageIncreases: [] }
|
||||
if (notices.usageIncreases.length > 0) {
|
||||
lines.push(`提示(不失败,仅记录):${notices.usageIncreases.length} 个门禁文件 token 使用数上升:`)
|
||||
lines.push(
|
||||
...notices.usageIncreases
|
||||
.slice(0, MAX_LISTED)
|
||||
.map(
|
||||
(item) =>
|
||||
` ${item.path}(${item.class}):${item.increases
|
||||
.map((increase) => `${METRIC_LABELS[increase.metric]} ${increase.from} → ${increase.to}(+${increase.to - increase.from})`)
|
||||
.join(';')}`
|
||||
)
|
||||
)
|
||||
if (notices.usageIncreases.length > MAX_LISTED) {
|
||||
lines.push(` …(还有 ${notices.usageIncreases.length - MAX_LISTED} 项,详见 --json)`)
|
||||
}
|
||||
}
|
||||
if (notices.deleted.length > 0) {
|
||||
lines.push(`提示(不失败):baseline 中 ${notices.deleted.length} 个文件已不存在:${formatList(notices.deleted.map((item) => item.path))}`)
|
||||
}
|
||||
if (notices.newGatedFilesWithoutCounts.length > 0) {
|
||||
lines.push(
|
||||
`提示:新增门禁文件 ${notices.newGatedFilesWithoutCounts.length} 个且无受控计数(允许):${formatList(
|
||||
notices.newGatedFilesWithoutCounts.map((item) => `${item.path}(${item.class})`)
|
||||
)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
console.log(lines.join('\n'))
|
||||
}
|
||||
|
||||
function buildJsonResult(result) {
|
||||
return {
|
||||
ok: result.violations.length === 0,
|
||||
updated: result.updated,
|
||||
scanRoot: SCAN_ROOT_LABEL,
|
||||
baseline: { path: BASELINE_LABEL, files: result.baselineFiles },
|
||||
metrics: METRICS,
|
||||
gatedClasses: CLASS_ORDER.filter((name) => GATED_CLASSES.has(name)),
|
||||
scan: { files: result.scan.files.length, durationMs: result.scan.durationMs },
|
||||
summary: result.summary,
|
||||
nonGatedDelta: result.nonGatedDelta,
|
||||
violations: result.violations,
|
||||
notices: result.notices
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
let options
|
||||
try {
|
||||
options = parseArgs(process.argv.slice(2))
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
printUsage()
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
if (options.help) {
|
||||
printUsage()
|
||||
return
|
||||
}
|
||||
|
||||
const scan = scanFiles()
|
||||
const summary = summarize(scan.files)
|
||||
|
||||
if (options.updateBaseline) {
|
||||
const baseline = buildBaseline(scan.files)
|
||||
writeFileSync(BASELINE_PATH, `${JSON.stringify(baseline, null, 2)}\n`, 'utf8')
|
||||
const result = {
|
||||
updated: true,
|
||||
scan,
|
||||
summary,
|
||||
baselineFiles: scan.files.length,
|
||||
nonGatedDelta: null,
|
||||
violations: [],
|
||||
notices: { deleted: [], newGatedFilesWithoutCounts: [], usageIncreases: [] }
|
||||
}
|
||||
if (options.json) console.log(JSON.stringify(buildJsonResult(result)))
|
||||
else printHuman(result)
|
||||
process.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
let baseline
|
||||
try {
|
||||
baseline = loadBaseline()
|
||||
} catch (error) {
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({ ok: false, baselineFound: false, baseline: { path: BASELINE_LABEL }, error: error.message }))
|
||||
} else {
|
||||
console.error(`[check:tokens] ${error.message}`)
|
||||
console.error('[check:tokens] 请运行:node scripts/check-design-tokens.mjs --update-baseline')
|
||||
}
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
if (!baseline) {
|
||||
if (options.json) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
baselineFound: false,
|
||||
baseline: { path: BASELINE_LABEL },
|
||||
hint: 'node scripts/check-design-tokens.mjs --update-baseline'
|
||||
})
|
||||
)
|
||||
} else {
|
||||
console.error(`[check:tokens] 未找到 baseline:${BASELINE_LABEL}`)
|
||||
console.error('[check:tokens] 请先运行:node scripts/check-design-tokens.mjs --update-baseline')
|
||||
}
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const baselineFiles = baseline.files
|
||||
const { violations, notices } = compareWithBaseline(scan, baselineFiles)
|
||||
const previousNonGated = baselineNonGatedTotals(baselineFiles)
|
||||
const nonGatedDelta = { files: summary.nonGated.files - previousNonGated.files, counts: zeroCounts() }
|
||||
for (const metric of METRICS) {
|
||||
nonGatedDelta.counts[metric] = summary.nonGated.counts[metric] - previousNonGated.counts[metric]
|
||||
}
|
||||
|
||||
const result = {
|
||||
updated: false,
|
||||
scan,
|
||||
summary,
|
||||
baselineFiles: Object.keys(baselineFiles).length,
|
||||
nonGatedDelta,
|
||||
violations,
|
||||
notices
|
||||
}
|
||||
if (options.json) console.log(JSON.stringify(buildJsonResult(result)))
|
||||
else printHuman(result)
|
||||
process.exitCode = violations.length > 0 ? 1 : 0
|
||||
}
|
||||
|
||||
main()
|
||||
Reference in new issue
Block a user