@manohub/app-kit 0.1.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 (103) hide show
  1. package/CONTRACT.md +592 -0
  2. package/README.md +27 -0
  3. package/dist/atoms/app-button.d.ts +90 -0
  4. package/dist/atoms/app-button.js +74 -0
  5. package/dist/atoms/app-checkbox.d.ts +86 -0
  6. package/dist/atoms/app-checkbox.js +78 -0
  7. package/dist/atoms/app-icon-button.d.ts +79 -0
  8. package/dist/atoms/app-icon-button.js +54 -0
  9. package/dist/atoms/app-input.d.ts +118 -0
  10. package/dist/atoms/app-input.js +83 -0
  11. package/dist/atoms/app-layout.d.ts +348 -0
  12. package/dist/atoms/app-layout.js +102 -0
  13. package/dist/atoms/app-notice.d.ts +90 -0
  14. package/dist/atoms/app-notice.js +59 -0
  15. package/dist/atoms/app-query-state.d.ts +191 -0
  16. package/dist/atoms/app-query-state.js +161 -0
  17. package/dist/atoms/app-radio-card.d.ts +80 -0
  18. package/dist/atoms/app-radio-card.js +72 -0
  19. package/dist/atoms/app-search-box.d.ts +80 -0
  20. package/dist/atoms/app-search-box.js +64 -0
  21. package/dist/atoms/app-select.d.ts +106 -0
  22. package/dist/atoms/app-select.js +74 -0
  23. package/dist/atoms/app-switch.d.ts +72 -0
  24. package/dist/atoms/app-switch.js +66 -0
  25. package/dist/atoms/app-textarea.d.ts +89 -0
  26. package/dist/atoms/app-textarea.js +61 -0
  27. package/dist/atoms/app-tooltip.d.ts +70 -0
  28. package/dist/atoms/app-tooltip.js +48 -0
  29. package/dist/atoms/atoms.css +679 -0
  30. package/dist/atoms/state-illustration.d.ts +21 -0
  31. package/dist/atoms/state-illustration.js +101 -0
  32. package/dist/components/app-badge.d.ts +72 -0
  33. package/dist/components/app-badge.js +40 -0
  34. package/dist/components/app-dialog.d.ts +164 -0
  35. package/dist/components/app-dialog.js +121 -0
  36. package/dist/components/app-filter.d.ts +173 -0
  37. package/dist/components/app-filter.js +200 -0
  38. package/dist/components/app-form.d.ts +446 -0
  39. package/dist/components/app-form.js +159 -0
  40. package/dist/components/app-pagination.d.ts +78 -0
  41. package/dist/components/app-pagination.js +57 -0
  42. package/dist/components/app-panel.d.ts +399 -0
  43. package/dist/components/app-panel.js +213 -0
  44. package/dist/components/app-section.d.ts +68 -0
  45. package/dist/components/app-section.js +28 -0
  46. package/dist/components/app-steps.d.ts +115 -0
  47. package/dist/components/app-steps.js +108 -0
  48. package/dist/components/app-table.d.ts +415 -0
  49. package/dist/components/app-table.js +479 -0
  50. package/dist/components/app-tabs.d.ts +58 -0
  51. package/dist/components/app-tabs.js +54 -0
  52. package/dist/components/app-tree.d.ts +273 -0
  53. package/dist/components/app-tree.js +222 -0
  54. package/dist/components/components.css +597 -0
  55. package/dist/composables/use-client-pagination.d.ts +39 -0
  56. package/dist/composables/use-client-pagination.js +29 -0
  57. package/dist/entry/create-query-client.d.ts +8 -0
  58. package/dist/entry/create-query-client.js +14 -0
  59. package/dist/entry/create-sub-app.d.ts +45 -0
  60. package/dist/entry/create-sub-app.js +110 -0
  61. package/dist/entry/index.d.ts +4 -0
  62. package/dist/entry/index.js +12 -0
  63. package/dist/entry/initial-guard.d.ts +12 -0
  64. package/dist/entry/initial-guard.js +23 -0
  65. package/dist/index.d.ts +37 -0
  66. package/dist/index.js +67 -0
  67. package/dist/providers/setup-i18n.d.ts +19 -0
  68. package/dist/providers/setup-i18n.js +24 -0
  69. package/dist/services/app-container.d.ts +15 -0
  70. package/dist/services/app-container.js +7 -0
  71. package/dist/services/app-context.d.ts +4 -0
  72. package/dist/services/app-context.js +17 -0
  73. package/dist/services/index.d.ts +5 -0
  74. package/dist/services/loading.d.ts +16 -0
  75. package/dist/services/loading.js +27 -0
  76. package/dist/services/message-box.d.ts +84 -0
  77. package/dist/services/message-box.js +65 -0
  78. package/dist/services/modal.d.ts +51 -0
  79. package/dist/services/modal.js +51 -0
  80. package/dist/services/notify.d.ts +24 -0
  81. package/dist/services/notify.js +22 -0
  82. package/dist/shell/AppShell.d.ts +139 -0
  83. package/dist/shell/AppShell.js +171 -0
  84. package/dist/shell/shell.css +153 -0
  85. package/dist/styles/farris-bridge.css +129 -0
  86. package/dist/styles/index.css +16 -0
  87. package/dist/styles/markdown.css +85 -0
  88. package/dist/styles/reset.css +74 -0
  89. package/dist/styles/tokens.css +111 -0
  90. package/lint/__tests__/fixtures/clean-src/styles.css +14 -0
  91. package/lint/__tests__/fixtures/clean-src/views/good-page.tsx +15 -0
  92. package/lint/__tests__/fixtures/violations-src/styles.css +18 -0
  93. package/lint/__tests__/fixtures/violations-src/views/bad-page.tsx +38 -0
  94. package/lint/__tests__/fixtures/violations-src/views/no-shell-page.tsx +4 -0
  95. package/lint/__tests__/guardrails.spec.mjs +144 -0
  96. package/lint/component-audit.mjs +131 -0
  97. package/lint/guardrails.config.schema.json +74 -0
  98. package/lint/pre-commit.sample +26 -0
  99. package/lint/run-all.mjs +59 -0
  100. package/lint/shared.mjs +347 -0
  101. package/lint/structure-audit.mjs +254 -0
  102. package/lint/style-audit.mjs +200 -0
  103. package/package.json +78 -0
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * component-audit —— 组件使用护栏。
4
+ *
5
+ * 规则:
6
+ * api/farris-import 禁 `from '@farris/ui-vue'`(底层组件库必须被收在 app-kit 内)
7
+ * api/old-ui-import 禁 `from '@manohub/ui'`(Tailwind + reka 栈,与本仓样式纪律冲突)
8
+ * api/tailwind 禁 Tailwind 相关 import
9
+ * api/legacy-prop 禁底层组件库风格 prop / 写法(按**键名**判定)
10
+ *
11
+ * 口径(CONTRACT.md §3):
12
+ * - 判定依据是「这个键属于谁」,不是「长得像不像对象」:
13
+ * `selection={{…}}` 是 app-kit 自己的结构化契约,**合法**;
14
+ * 而 `rowOption` / `columnOption` / `enableSelectRow` / `enabelSelectRow` 等属于底层,一律禁止。
15
+ *
16
+ * 用法:node node_modules/@manohub/app-kit/lint/component-audit.mjs [--json] [--changed]
17
+ */
18
+ import { readFileSync } from 'node:fs'
19
+ import { join } from 'node:path'
20
+ import {
21
+ loadConfig,
22
+ parseArgs,
23
+ collectFiles,
24
+ changedFiles,
25
+ stripComments,
26
+ displayPath,
27
+ makeViolation,
28
+ report,
29
+ } from './shared.mjs'
30
+
31
+ const args = parseArgs()
32
+ const cwd = args.cwd
33
+ const config = loadConfig(cwd)
34
+ const changed = args.changed ? changedFiles(cwd) : null
35
+
36
+ /** 禁止的 import 源(子串匹配)。 */
37
+ const BANNED_IMPORTS = [
38
+ { pattern: /from\s+['"]@farris\/ui-vue['"]/, rule: 'api/farris-import', message: '禁止直接 import 底层组件库(@farris/ui-vue)' },
39
+ { pattern: /from\s+['"]@manohub\/ui['"]/, rule: 'api/old-ui-import', message: '禁止 import 旧 UI 包(@manohub/ui)' },
40
+ { pattern: /from\s+['"](?:tailwindcss|@tailwindcss\/[^'"]+)['"]/, rule: 'api/tailwind', message: '禁止 Tailwind 相关 import' },
41
+ // 样式/hook 层面:@gsp-sys 的 request 属另一体系,但本仓值映射已自建客户端,禁止回退
42
+ { pattern: /from\s+['"]@farris\/ui-vue\/[^'"]+['"]/, rule: 'api/farris-import', message: '禁止 import 底层组件库子路径' },
43
+ ]
44
+
45
+ /** 底层风格 prop / 写法黑名单(正则片段,按**键名/属性名**匹配)。 */
46
+ const LEGACY_PROPS = [
47
+ 'valueField',
48
+ 'textField',
49
+ 'idField',
50
+ 'enumValueType',
51
+ 'columnTemplate',
52
+ 'headerFormatter',
53
+ 'emptyTemplate',
54
+ 'rowOption',
55
+ 'columnOption',
56
+ 'treeNodeIconsData',
57
+ 'updateDataSource',
58
+ 'updateColumns',
59
+ 'fitColumns',
60
+ 'fitMode',
61
+ 'multiSelectMode',
62
+ 'keepSelectingOnPaging',
63
+ 'searchPlaceHolder',
64
+ 'showCheckbox',
65
+ 'showSelectAll',
66
+ 'enableSelectRow',
67
+ 'enabelSelectRow',
68
+ 'customClass',
69
+ 'customStyle',
70
+ 'iconClass',
71
+ 'showBorder',
72
+ 'showStripe',
73
+ 'onDoubleClickRow',
74
+ 'onClickRow',
75
+ 'showCloseButton',
76
+ ]
77
+
78
+ const LEGACY_PROP_RE = new RegExp(
79
+ `(?:^|[\\s{,(])(${LEGACY_PROPS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})\\s*(?:=|:|\\s*\\?\\s*:)`,
80
+ )
81
+ const ROW_RAW_RE = /\brow\.raw\b|\bvisualDataRow\b/
82
+
83
+ function violation(app, file, rule, line, message, snippet, severity = 'error') {
84
+ return makeViolation({ app, file: displayPath(cwd, file), rule, line, severity, message, snippet })
85
+ }
86
+
87
+ const violations = []
88
+ for (const app of config.apps) {
89
+ if (args.app && app.name !== args.app && app.dir !== args.app) continue
90
+ const root = join(cwd, app.dir)
91
+ // `.vue` 必须一起扫:SFC 里直连底层组件库、用底层风格 prop 同样违规
92
+ // (vm 子应用的演示页 `views/form.vue` 曾因此漏网:模板里 `<f-input-group>` + farris import 都没被报出来)
93
+ for (const file of collectFiles(root, config, ['.ts', '.tsx', '.vue'], config.srcGlobs)) {
94
+ if (changed) {
95
+ const rel = displayPath(cwd, file)
96
+ if (![...changed].some((c) => c === rel || c.endsWith(rel))) continue
97
+ }
98
+ const source = stripComments(readFileSync(file, 'utf8'))
99
+ const lines = source.split('\n')
100
+ for (let i = 0; i < lines.length; i++) {
101
+ const line = lines[i]
102
+ const lineNo = i + 1
103
+ for (const banned of BANNED_IMPORTS) {
104
+ if (banned.pattern.test(line)) {
105
+ violations.push(violation(app.name, file, banned.rule, lineNo, banned.message, line))
106
+ }
107
+ }
108
+ // 逐行找遗留 prop(跳过 `.d.ts` 已在 collectFiles 过滤)
109
+ const match = LEGACY_PROP_RE.exec(line)
110
+ if (match) {
111
+ violations.push(
112
+ violation(
113
+ app.name,
114
+ file,
115
+ 'api/legacy-prop',
116
+ lineNo,
117
+ `禁止底层风格 prop \`${match[1]}\`(换成 app-kit 统一词表)`,
118
+ line,
119
+ ),
120
+ )
121
+ }
122
+ if (ROW_RAW_RE.test(line)) {
123
+ violations.push(
124
+ violation(app.name, file, 'api/legacy-prop', lineNo, '禁止解包底层包装对象(row.raw 由组件内部消化)', line),
125
+ )
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ process.exit(report('component-audit(组件使用护栏)', violations, args, config))
@@ -0,0 +1,74 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "appkit-guardrails.config.json",
4
+ "description": "护栏配置:放在**应用包根**(即 `pnpm lint` 的 cwd),由 @manohub/app-kit/lint/*.mjs 读取。所有路径相对该目录。",
5
+ "type": "object",
6
+ "required": ["apps"],
7
+ "properties": {
8
+ "apps": {
9
+ "description": "被审计的应用列表。单应用仓写一条即可(dir 用 \".\")。",
10
+ "type": "array",
11
+ "minItems": 1,
12
+ "items": {
13
+ "type": "object",
14
+ "required": ["dir"],
15
+ "properties": {
16
+ "dir": {
17
+ "type": "string",
18
+ "description": "应用目录,相对应用包根。"
19
+ },
20
+ "name": {
21
+ "type": "string",
22
+ "description": "报告里显示的应用名;缺省用 dir。"
23
+ },
24
+ "prefixes": {
25
+ "type": "array",
26
+ "items": { "type": "string" },
27
+ "description": "该应用登记的选择器前缀(样式护栏规则 9)。`.app-container` 恒为允许项,不必登记。"
28
+ },
29
+ "pending": {
30
+ "type": "boolean",
31
+ "default": false,
32
+ "description": "存量挂起:为 true 时该应用仅产出基线报告,不计入退出码(迁移完成后置 false 即自动纳入门禁)。"
33
+ }
34
+ }
35
+ }
36
+ },
37
+ "srcGlobs": {
38
+ "type": "array",
39
+ "items": { "type": "string" },
40
+ "description": "源码扫描范围(相对每个 app.dir)。默认 [\"src/**/*.ts\", \"src/**/*.tsx\"]。"
41
+ },
42
+ "styleGlobs": {
43
+ "type": "array",
44
+ "items": { "type": "string" },
45
+ "description": "样式扫描范围。默认 [\"src/**/*.css\"]。"
46
+ },
47
+ "exemptionRegistry": {
48
+ "type": ["string", "null"],
49
+ "description": "设计还原豁免注册表(Markdown 表格)。文件头带 @figma-fidelity 且登记于此的文件,跳过视觉属性/裸元素/选择器前缀三类检查;!important 与自写 rem 仍然禁止。"
50
+ },
51
+ "contractDoc": {
52
+ "type": "string",
53
+ "description": "correction.doc 的锚点基址(指向 CONTRACT.md)。默认 node_modules/@manohub/app-kit/CONTRACT.md。"
54
+ },
55
+ "ignore": {
56
+ "type": "array",
57
+ "items": { "type": "string" },
58
+ "description": "扫描时跳过的目录名。默认 [\"node_modules\", \"dist\"]。"
59
+ }
60
+ },
61
+ "additionalProperties": false,
62
+ "examples": [
63
+ {
64
+ "apps": [
65
+ { "dir": ".", "name": "sub-app", "prefixes": ["vm-", "app-container"], "pending": false }
66
+ ],
67
+ "srcGlobs": ["src/**/*.ts", "src/**/*.tsx"],
68
+ "styleGlobs": ["src/**/*.css"],
69
+ "exemptionRegistry": "docs/appkit-exemptions.md",
70
+ "contractDoc": "node_modules/@manohub/app-kit/CONTRACT.md",
71
+ "ignore": ["node_modules", "dist"]
72
+ }
73
+ ]
74
+ }
@@ -0,0 +1,26 @@
1
+ #!/bin/sh
2
+ # app-kit 护栏 pre-commit 钩子样本:只检查**已改动文件**,目标 < 3 秒(否则人会在本机把它关掉)。
3
+ #
4
+ # 启用(不需要改 git 配置,默认 hooks 目录即可):
5
+ # cp node_modules/@manohub/app-kit/lint/pre-commit.sample .git/hooks/pre-commit
6
+ # chmod +x .git/hooks/pre-commit
7
+ #
8
+ # 应用包不是仓库根时(monorepo,如 apps/sub-app),用环境变量指出包目录:
9
+ # APPKIT_APP_DIR=apps/sub-app (可在 .git/hooks/pre-commit 里写死,或提交前 export)
10
+ #
11
+ # 说明:护栏以「应用包根」为 cwd 读取 appkit-guardrails.config.json;仓库根不存在该配置时
12
+ # 必须给 APPKIT_APP_DIR,否则会以「未找到配置」失败(这是刻意为之:宁可报错,也不要静默跳过检查)。
13
+ set -e
14
+
15
+ AUDIT="node_modules/@manohub/app-kit/lint/run-all.mjs"
16
+
17
+ if [ ! -f "$AUDIT" ]; then
18
+ echo "[guardrails] 未安装 @manohub/app-kit(找不到 $AUDIT),跳过检查"
19
+ exit 0
20
+ fi
21
+
22
+ if [ -n "$APPKIT_APP_DIR" ]; then
23
+ exec node "$AUDIT" --changed --cwd="$APPKIT_APP_DIR"
24
+ fi
25
+
26
+ exec node "$AUDIT" --changed
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * run-all —— 三条护栏的统一入口(接入方只挂这一条,见 CONTRACT.md §8)。
4
+ *
5
+ * 并发跑 style / component / structure,汇总输出与退出码;支持 `--changed`(只跑改动文件,供 pre-commit)。
6
+ * 任一脚本报 error 即整体失败;`--json` 时输出聚合结果。
7
+ *
8
+ * 用法(接入方的 `package.json` 只挂前两条,另两条是排查单条时才用的):
9
+ * node node_modules/@manohub/app-kit/lint/run-all.mjs # 全量(挂成 "lint")
10
+ * node node_modules/@manohub/app-kit/lint/run-all.mjs --changed # 只跑改动文件(挂成 "lint:changed")
11
+ * node node_modules/@manohub/app-kit/lint/style-audit.mjs # 单条排查:style / component / structure
12
+ * node node_modules/@manohub/app-kit/lint/run-all.mjs --json
13
+ */
14
+ import { spawn } from 'node:child_process'
15
+ import { fileURLToPath } from 'node:url'
16
+ import { dirname, join } from 'node:path'
17
+ import { parseArgs } from './shared.mjs'
18
+
19
+ const HERE = dirname(fileURLToPath(import.meta.url))
20
+ const AUDITS = ['style-audit.mjs', 'component-audit.mjs', 'structure-audit.mjs']
21
+
22
+ const args = parseArgs()
23
+ const passthrough = process.argv.slice(2).filter((a) => a.startsWith('--app=') || a.startsWith('--cwd='))
24
+ if (args.changed) passthrough.push('--changed')
25
+ if (args.json) passthrough.push('--json')
26
+
27
+ const runOne = (script) =>
28
+ new Promise((resolve) => {
29
+ const child = spawn(process.execPath, [join(HERE, script), ...passthrough], {
30
+ cwd: args.cwd,
31
+ stdio: ['ignore', 'pipe', 'pipe'],
32
+ })
33
+ let stdout = ''
34
+ let stderr = ''
35
+ child.stdout.on('data', (d) => (stdout += d))
36
+ child.stderr.on('data', (d) => (stderr += d))
37
+ child.on('close', (code) => resolve({ script, code, stdout, stderr }))
38
+ })
39
+
40
+ const started = Date.now()
41
+ const results = await Promise.all(AUDITS.map(runOne))
42
+ const elapsed = ((Date.now() - started) / 1000).toFixed(2)
43
+
44
+ if (args.json) {
45
+ process.stdout.write(JSON.stringify({ audits: results.map((r) => ({ script: r.script, code: r.code })), elapsed }, null, 2) + '\n')
46
+ } else {
47
+ for (const r of results) {
48
+ if (r.stdout) process.stdout.write(r.stdout)
49
+ if (r.stderr) process.stderr.write(r.stderr)
50
+ }
51
+ const failed = results.filter((r) => r.code !== 0)
52
+ process.stdout.write(
53
+ failed.length === 0
54
+ ? `✓ 护栏(三条):全绿(${elapsed}s)\n`
55
+ : `✗ 护栏(三条):${failed.map((f) => f.script).join(' / ')} 未通过(${elapsed}s)\n`,
56
+ )
57
+ }
58
+
59
+ process.exit(results.some((r) => r.code !== 0) ? 1 : 0)
@@ -0,0 +1,347 @@
1
+ /**
2
+ * 护栏共享基建:配置加载 / 文件遍历 / 注释剥离 / 违规输出与 correction 表。
3
+ *
4
+ * 三条护栏脚本(style / component / structure)与 run-all 统一依赖本文件,
5
+ * 保证「同一份配置 + 同一种输出契约」,消费方只需写一个配置文件。
6
+ *
7
+ * 输出契约(大模型自我修正的前提):
8
+ * { app, file, rule, line, severity, message, snippet,
9
+ * correction: { summary, example, doc } }
10
+ */
11
+ import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'
12
+ import { join, relative, extname, sep } from 'node:path'
13
+ import { execSync } from 'node:child_process'
14
+
15
+ /** 默认配置(消费方可覆盖);`apps[].dir` 相对消费仓根(= process.cwd())。 */
16
+ export const DEFAULT_CONFIG = {
17
+ apps: [],
18
+ srcGlobs: ['src/**/*.ts', 'src/**/*.tsx'],
19
+ styleGlobs: ['src/**/*.css'],
20
+ exemptionRegistry: null,
21
+ contractDoc: 'node_modules/@manohub/app-kit/CONTRACT.md',
22
+ ignore: ['node_modules', 'dist'],
23
+ }
24
+
25
+ export function loadConfig(cwd = process.cwd()) {
26
+ const path = join(cwd, 'appkit-guardrails.config.json')
27
+ if (!existsSync(path)) {
28
+ fail(
29
+ `未找到护栏配置:${path}\n` +
30
+ `请在应用包根创建 appkit-guardrails.config.json(结构见 @manohub/app-kit/lint/guardrails.config.schema.json)。`,
31
+ )
32
+ }
33
+ const raw = JSON.parse(readFileSync(path, 'utf8'))
34
+ const config = { ...DEFAULT_CONFIG, ...raw }
35
+ if (!Array.isArray(config.apps) || config.apps.length === 0) {
36
+ fail('配置里的 apps 不能为空(至少要列出被审计的应用目录)。')
37
+ }
38
+ return config
39
+ }
40
+
41
+ export function fail(message) {
42
+ process.stderr.write(`\n[guardrails] ${message}\n\n`)
43
+ process.exit(2)
44
+ }
45
+
46
+ /** 简易 CLI 参数解析:--json / --strict / --changed / --app=<name> / --cwd=<path> */
47
+ export function parseArgs(argv = process.argv.slice(2)) {
48
+ const args = { json: false, strict: false, changed: false, app: null, cwd: process.cwd() }
49
+ for (const raw of argv) {
50
+ if (raw === '--json') args.json = true
51
+ else if (raw === '--strict') args.strict = true
52
+ else if (raw === '--changed') args.changed = true
53
+ else if (raw.startsWith('--app=')) args.app = raw.slice('--app='.length)
54
+ else if (raw.startsWith('--cwd=')) args.cwd = raw.slice('--cwd='.length)
55
+ }
56
+ return args
57
+ }
58
+
59
+ /** 递归收集文件:按扩展名 + 配置的 glob 范围过滤,跳过 ignore 目录与 .d.ts。 */
60
+ export function collectFiles(root, config, extensions, globs = null) {
61
+ const out = []
62
+ const ignore = new Set(config.ignore)
63
+ const matchers = (globs ?? []).map(globToRegExp)
64
+ const walk = (dir) => {
65
+ if (!existsSync(dir)) return
66
+ for (const entry of readdirSync(dir)) {
67
+ if (ignore.has(entry) || entry.startsWith('.')) continue
68
+ const full = join(dir, entry)
69
+ const st = statSync(full)
70
+ if (st.isDirectory()) {
71
+ walk(full)
72
+ } else if (extensions.includes(extname(entry)) && !entry.endsWith('.d.ts')) {
73
+ if (matchers.length > 0) {
74
+ const rel = relative(root, full).split(sep).join('/')
75
+ if (!matchers.some((m) => m.test(rel))) continue
76
+ }
77
+ out.push(full)
78
+ }
79
+ }
80
+ }
81
+ walk(root)
82
+ return out
83
+ }
84
+
85
+ /**
86
+ * 极简 glob → RegExp(支持 `**` 与 `*`),用于 srcGlobs / styleGlobs。
87
+ *
88
+ * 注意替换顺序:双星号通配(globstar)必须先换成**占位符**,再做单星号替换,
89
+ * 否则刚插入的 `.*` 会被单星号那条规则二次改坏
90
+ * —— 曾因此只匹配到顶层文件、嵌套目录被静默漏扫(自测里有回归用例)。
91
+ */
92
+ const GLOBSTAR_SLASH = '\u0000GS\u0000'
93
+ const GLOBSTAR = '\u0000G\u0000'
94
+
95
+ export function globToRegExp(glob) {
96
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&')
97
+ const re = escaped
98
+ .replace(/\*\*\//g, GLOBSTAR_SLASH)
99
+ .replace(/\*\*/g, GLOBSTAR)
100
+ .replace(/\*/g, '[^/]*')
101
+ .replace(new RegExp(GLOBSTAR_SLASH, 'g'), '(?:.*/)?')
102
+ .replace(new RegExp(GLOBSTAR, 'g'), '.*')
103
+ return new RegExp(`^${re}$`)
104
+ }
105
+
106
+ /** `--changed` 用的 git 改动文件集(相对仓根,posix 分隔符)。 */
107
+ export function changedFiles(cwd) {
108
+ try {
109
+ const raw = execSync('git diff --name-only HEAD', { cwd, encoding: 'utf8' })
110
+ return new Set(raw.split('\n').map((s) => s.trim()).filter(Boolean))
111
+ } catch {
112
+ return null // 非 git 环境或 git 不可用:不裁剪(宁可多检查)
113
+ }
114
+ }
115
+
116
+ /**
117
+ * 剥离注释,但**保留行号**(违规行号必须与源码一致)。
118
+ * 处理 `/* *\/` 与 `//`;引号内的 `//`(如 URL)不会被误判。
119
+ */
120
+ export function stripComments(source) {
121
+ let out = ''
122
+ let i = 0
123
+ const n = source.length
124
+ let inBlock = false
125
+ let inLine = false
126
+ let quote = null
127
+ while (i < n) {
128
+ const ch = source[i]
129
+ const next = source[i + 1]
130
+ if (inBlock) {
131
+ if (ch === '*' && next === '/') {
132
+ out += ' '
133
+ i += 2
134
+ inBlock = false
135
+ continue
136
+ }
137
+ out += ch === '\n' ? '\n' : ' '
138
+ i += 1
139
+ continue
140
+ }
141
+ if (inLine) {
142
+ if (ch === '\n') {
143
+ out += '\n'
144
+ inLine = false
145
+ } else {
146
+ out += ' '
147
+ }
148
+ i += 1
149
+ continue
150
+ }
151
+ if (quote) {
152
+ out += ch
153
+ if (ch === '\\') {
154
+ out += source[i + 1] ?? ''
155
+ i += 2
156
+ continue
157
+ }
158
+ if (ch === quote) quote = null
159
+ i += 1
160
+ continue
161
+ }
162
+ if (ch === '/' && next === '*') {
163
+ out += ' '
164
+ i += 2
165
+ inBlock = true
166
+ continue
167
+ }
168
+ if (ch === '/' && next === '/') {
169
+ out += ' '
170
+ i += 2
171
+ inLine = true
172
+ continue
173
+ }
174
+ if (ch === '"' || ch === "'" || ch === '`') quote = ch
175
+ out += ch
176
+ i += 1
177
+ }
178
+ return out
179
+ }
180
+
181
+ /** 相对仓根的展示路径(统一 posix 分隔符)。 */
182
+ export function displayPath(cwd, file) {
183
+ return relative(cwd, file).split(sep).join('/')
184
+ }
185
+
186
+ /** correction:唯一改法 + 正确写法 + CONTRACT.md 锚点(大模型据此自我修正)。 */
187
+ export const CORRECTIONS = {
188
+ 'style/tailwind': {
189
+ summary: '删掉 Tailwind 引入;样式走 app-kit 三行链与 --ui-* 令牌。',
190
+ example: '@import "@manohub/app-kit/styles.css";',
191
+ doc: 'CONTRACT.md#1-消费方式',
192
+ },
193
+ 'style/token': {
194
+ summary: '不要在应用侧定义 CSS 变量/@theme;需要新令牌请在 app-kit 内加。',
195
+ example: '给 app-kit 提 issue/PR,在 packages/app-kit/src/styles/tokens.css 增加令牌',
196
+ doc: 'CONTRACT.md#6-样式纪律',
197
+ },
198
+ 'style/visual': {
199
+ summary: '视觉属性一律删除,改由组件承载或使用 --ui-* 令牌。',
200
+ example: 'background: var(--ui-base-200); →(删除,交给组件/令牌)',
201
+ doc: 'CONTRACT.md#6-样式纪律',
202
+ },
203
+ 'style/important': {
204
+ summary: '删掉 !important;确需覆写底层组件库内部样式时,写进包内 farris-bridge.css。',
205
+ example: '(app 侧)height: 2rem; // 去掉 !important',
206
+ doc: 'CONTRACT.md#6-样式纪律',
207
+ },
208
+ 'style/bare-element': {
209
+ summary: '裸元素选择器会污染宿主,改为带应用前缀的类名。',
210
+ example: '.vm-manage button { … } → .vm-toolbar-action { … }',
211
+ doc: 'CONTRACT.md#6-样式纪律',
212
+ },
213
+ 'style/prefix': {
214
+ summary: '选择器首段必须匹配本应用登记前缀(见配置文件 apps[].prefixes)。',
215
+ example: '.my-app-header { … }(前缀已在配置中登记)',
216
+ doc: 'CONTRACT.md#6-样式纪律',
217
+ },
218
+ 'style/ak-selector': {
219
+ summary: '删除 .ak-* 选择器:那是 app-kit 的类名,应用侧只能用它的组件。',
220
+ example: '<AppPanel> 替代对 .ak-panel 的样式覆写',
221
+ doc: 'CONTRACT.md#6-样式纪律',
222
+ },
223
+ 'style/rem': {
224
+ summary: '删掉自写 rem 尺寸:全仓统一 px 令牌,不跟随根字号缩放(见已知偏差)。',
225
+ example: 'padding: var(--ui-space-2); // 不要 0.5rem',
226
+ doc: 'CONTRACT.md#9-已知偏差',
227
+ },
228
+ 'api/farris-import': {
229
+ summary: '删掉对底层组件库的直接 import,改用 app-kit 的 App* 件;缺件走建件流程。',
230
+ example: "import { AppTable } from '@manohub/app-kit'",
231
+ doc: 'CONTRACT.md#7-缺件处置流程',
232
+ },
233
+ 'api/old-ui-import': {
234
+ summary: '禁止引入旧 UI 包;组件与图标分别用 @manohub/app-kit 与 @manohub/icon。',
235
+ example: "import { Icon } from '@manohub/icon'",
236
+ doc: 'CONTRACT.md#2-四条铁律',
237
+ },
238
+ 'api/legacy-prop': {
239
+ summary: '这是底层组件库的写法;换成 app-kit 的统一 prop 词表。',
240
+ example: 'options={[{ label, value }]} / modelValue / onChange',
241
+ doc: 'CONTRACT.md#3-统一-prop-词表',
242
+ },
243
+ 'structure/no-shell': {
244
+ summary: '路由级页面必须用 AppShell 承载结构。',
245
+ example: '<AppShell><AppShell.Header title="…" /><AppShell.Body mode="table">…</AppShell.Body></AppShell>',
246
+ doc: 'CONTRACT.md#41-三种页面模板',
247
+ },
248
+ 'structure/header-title': {
249
+ summary: 'AppShell.Header 必须传 title(或登记为自定义页头白名单)。',
250
+ example: '<AppShell.Header title={t("page.title")} />',
251
+ doc: 'CONTRACT.md#41-三种页面模板',
252
+ },
253
+ 'structure/body-mode': {
254
+ summary: 'AppShell.Body 必须显式声明 mode。',
255
+ example: '<AppShell.Body mode="table|scroll|plain">',
256
+ doc: 'CONTRACT.md#44-两级滚动归属不要混',
257
+ },
258
+ 'structure/native-control': {
259
+ summary:
260
+ '原生控件(含无 href 的锚点)不要用来承载交互/外观:按钮 → AppButton、输入 → AppInput/AppTextarea、下拉 → AppSelect。',
261
+ example: '<AppButton shape="link" tone="error" onClick={() => remove(row)}>删除</AppButton>',
262
+ doc: 'CONTRACT.md#7-缺件处置流程',
263
+ },
264
+ 'structure/viewport-height': {
265
+ summary: '禁用 100vh / h-screen;高度用 height:100%(子应用被注入宿主容器)。',
266
+ example: 'height: 100%;',
267
+ doc: 'CONTRACT.md#41-三种页面模板',
268
+ },
269
+ 'structure/self-header': {
270
+ summary: '删掉自绘页头/面板头/表格框:页头→AppShell.Header、面板头→AppPanel、表格框→AppTable framed。',
271
+ example: '<AppPanel title="业务域"><AppTree … /></AppPanel>',
272
+ doc: 'CONTRACT.md#43-apppanel区域容器无框',
273
+ },
274
+ 'structure/toolbar-fields': {
275
+ summary: '单个面板 toolbar 的字段数不得超过 3;超出请上提为页面级 AppShell.Filter。',
276
+ example: '<AppShell.Filter>…4 个以上字段…</AppShell.Filter>',
277
+ doc: 'CONTRACT.md#45-四处操作位唯一化先问过滤谁的数据再问几个字段',
278
+ },
279
+ 'structure/self-border': {
280
+ summary: '删掉 Split 栏子元素上的竖直边框:分隔线由 AppShell.Split 统一提供。',
281
+ example: '(删除 border-right / border-left)',
282
+ doc: 'CONTRACT.md#42-appshellsplit双栏',
283
+ },
284
+ 'structure/pagination-scope': {
285
+ summary:
286
+ '分页要与承载表格的容器同层:表格在 AppPanel 内 → 分页放 AppPanel.Footer;只有表格直接挂在 AppShell.Body 下才用 AppShell.Footer。',
287
+ example: '<AppPanel title="列表"><AppTable … /><AppPanel.Footer><AppPagination … /></AppPanel.Footer></AppPanel>',
288
+ doc: 'CONTRACT.md#43-apppanel区域容器无框',
289
+ },
290
+ }
291
+
292
+ export function makeViolation({ app, file, rule, line, severity = 'error', message, snippet }) {
293
+ return {
294
+ app,
295
+ file,
296
+ rule,
297
+ line,
298
+ severity,
299
+ message,
300
+ snippet: (snippet ?? '').trim().slice(0, 160),
301
+ correction: CORRECTIONS[rule] ?? null,
302
+ }
303
+ }
304
+
305
+ /**
306
+ * 统一报告:人读格式 + 退出码。
307
+ *
308
+ * `pending` 语义:配置里 `apps[].pending = true` 的应用属**存量挂起** —— 照常报告违规,
309
+ * 但**不计入退出码**(迁移完成后把它置回 false 即自动纳入门禁);`--strict` 时 pending 也计入。
310
+ * warn 一律不计入退出码(用于"建议上提"这类不可机械判定的项)。
311
+ */
312
+ export function report(title, violations, args, config = null, extra = '') {
313
+ const pending = new Set(
314
+ (config?.apps ?? []).filter((a) => a.pending).map((a) => a.name ?? a.dir),
315
+ )
316
+ const isPending = (v) => pending.has(v.app)
317
+ const errors = violations.filter((v) => v.severity === 'error')
318
+ const warns = violations.filter((v) => v.severity !== 'error')
319
+ const blocking = args.strict ? errors : errors.filter((v) => !isPending(v))
320
+ const pendingErrors = errors.length - blocking.length
321
+
322
+ if (args.json) {
323
+ process.stdout.write(JSON.stringify({ audit: title, violations }, null, 2) + '\n')
324
+ } else {
325
+ if (violations.length === 0) {
326
+ process.stdout.write(`✓ ${title}:无违规${extra ? `(${extra})` : ''}\n`)
327
+ } else {
328
+ process.stdout.write(
329
+ `\n${title}:${errors.length} 个 error / ${warns.length} 个 warn` +
330
+ (pendingErrors > 0 ? `(其中 ${pendingErrors} 个属存量挂起,不计入退出码)` : '') +
331
+ '\n',
332
+ )
333
+ for (const v of violations) {
334
+ process.stdout.write(
335
+ ` [${v.severity}${isPending(v) ? '/pending' : ''}] ${v.file}:${v.line} ${v.rule}\n` +
336
+ ` ${v.message}\n` +
337
+ (v.snippet ? ` > ${v.snippet}\n` : '') +
338
+ (v.correction
339
+ ? ` 改法:${v.correction.summary}\n 写法:${v.correction.example}\n 出处:${v.correction.doc}\n`
340
+ : ''),
341
+ )
342
+ }
343
+ }
344
+ process.stdout.write('\n')
345
+ }
346
+ return blocking.length > 0 ? 1 : 0
347
+ }