This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch refactor/layer-dashboards-admin-decompose
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git

commit 25f77a8156d73e3740b3f385262300291f8e50a9
Author: Wu Sheng <[email protected]>
AuthorDate: Sun Jun 28 00:21:20 2026 +0800

    chore(ci): enforce per-file source-size budget (2000 code / 500 comment 
lines)
    
    ESLint max-lines (2000 lines of code, comments and blank lines excluded)
    across ui + bff, plus a comment-volume budget script (≤500 comment lines per
    file) that ESLint can't express, wired into `pnpm lint` and `pnpm 
lint:budget`.
    A new CI `lint` job runs both and is added to the Required status rollup, so
    per-file source size and style are CI-gated for the first time.
    
    The previous 1000-line cap is raised to 2000 (a dense single-file component
    legitimately carries a large cohesive template/style block; 1000 shredded
    coupled CSS). LayerDashboardsAdmin.vue stays grandfathered here; its
    decomposition lands in the next commit and removes the exemption.
---
 .github/workflows/ci.yaml       | 21 +++++++++-
 apps/bff/eslint.config.mjs      |  7 ++--
 apps/ui/eslint.config.mjs       | 34 +++++-----------
 package.json                    |  3 +-
 scripts/check-source-budget.mjs | 87 +++++++++++++++++++++++++++++++++++++++++
 5 files changed, 121 insertions(+), 31 deletions(-)

diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index d9781fe..118b374 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -161,13 +161,31 @@ jobs:
       # needed — keeps the CI line robust against pnpm-flag parsing quirks.
       - run: pnpm -r run test:unit
 
+  lint:
+    name: Lint + source budget
+    runs-on: ubuntu-latest
+    timeout-minutes: 15
+    steps:
+      - uses: actions/checkout@v6
+        with:
+          persist-credentials: false
+      - uses: pnpm/action-setup@v4
+      - uses: actions/setup-node@v4
+        with:
+          node-version: '24'
+          cache: 'pnpm'
+      - run: pnpm install --frozen-lockfile
+      # ESLint (rules + 2000-line cap, comments excluded) across ui + bff,
+      # then the source-budget gate (≤2000 code / ≤500 comment lines per file).
+      - run: pnpm lint
+
   required:
     # Status-check name pinned to "Required" so Apache branch protection
     # (`.asf.yaml: protected_branches.main.required_status_checks.contexts:
     # [Required]`) can gate on a single rolled-up signal.
     if: always() && !cancelled()
     name: Required
-    needs: [license-header, dependency-license, type-check, build-ui, 
build-bff, test]
+    needs: [license-header, dependency-license, type-check, build-ui, 
build-bff, test, lint]
     runs-on: ubuntu-latest
     timeout-minutes: 5
     steps:
@@ -179,3 +197,4 @@ jobs:
           [[ ${{ needs.build-ui.result }} == 'success' ]] || exit 1
           [[ ${{ needs.build-bff.result }} == 'success' ]] || exit 1
           [[ ${{ needs.test.result }} == 'success' ]] || exit 1
+          [[ ${{ needs.lint.result }} == 'success' ]] || exit 1
diff --git a/apps/bff/eslint.config.mjs b/apps/bff/eslint.config.mjs
index 9c1dd18..c55fb19 100644
--- a/apps/bff/eslint.config.mjs
+++ b/apps/bff/eslint.config.mjs
@@ -39,11 +39,10 @@ export default tseslint.config(
     },
   },
   {
-    // File-size guardrail. No NEW file may exceed 1000 lines — split it
-    // instead. The grandfather list below is the decomposition backlog: remove
-    // a file when its split lands; never add to it.
+    // File-size guardrail. No file may exceed 2000 lines of code (comments
+    // and blank lines excluded) — split it instead.
     files: ['src/**/*.ts'],
     ignores: ['**/*.test.ts'],
-    rules: { 'max-lines': ['error', 1000] },
+    rules: { 'max-lines': ['error', { max: 2000, skipComments: true, 
skipBlankLines: true }] },
   },
 );
diff --git a/apps/ui/eslint.config.mjs b/apps/ui/eslint.config.mjs
index d8b006d..e6bc781 100644
--- a/apps/ui/eslint.config.mjs
+++ b/apps/ui/eslint.config.mjs
@@ -38,35 +38,19 @@ export default [
     },
   },
   {
-    // File-size guardrail. No NEW file may exceed 1000 lines — split it
-    // (extract composables / sub-components) instead. The grandfather list
-    // below is the live decomposition backlog: remove a file when its split
-    // lands; never add to it.
+    // File-size guardrail. No file may exceed 2000 lines of code (comments
+    // and blank lines excluded) — split it (extract composables /
+    // sub-components) instead. The grandfather list below is the live
+    // decomposition backlog: remove a file when its split lands; never add to 
it.
     files: ['src/**/*.vue', 'src/**/*.ts'],
     ignores: ['**/*.test.ts', '**/*.d.ts'],
-    rules: { 'max-lines': ['error', 1000] },
+    rules: { 'max-lines': ['error', { max: 2000, skipComments: true, 
skipBlankLines: true }] },
   },
   {
-    // ── DECOMPOSE BACKLOG ─ files still >1000 loc after the decomposition
-    //    pass. This list only ever SHRINKS. Two groups:
-    //    • cohesive cores (borderline) — the seam was extracted but the
-    //      d3 / scene / dense-render core legitimately stays large; forcing
-    //      it under 1000 would scatter coupled state. May stay grandfathered:
-    //      Infra3DScene, LayerServiceMapView, LayerEndpointDependencyView,
-    //      AlarmsView, DebugMal, DebugLal.
-    //    • awaiting further / dedicated decomposition: OverviewTemplatesAdmin
-    //      (just over), InspectView, and LayerDashboardsAdmin (XL — its own 
PR).
-    files: [
-      'src/features/admin/layer-templates/LayerDashboardsAdmin.vue',
-      'src/features/admin/overview-templates/OverviewTemplatesAdmin.vue',
-      'src/features/alarms/AlarmsView.vue',
-      'src/features/infra-3d/Infra3DScene.vue',
-      'src/features/operate/inspect/InspectView.vue',
-      'src/features/operate/live-debug/DebugLal.vue',
-      'src/features/operate/live-debug/DebugMal.vue',
-      'src/layer/endpoint-dependency/LayerEndpointDependencyView.vue',
-      'src/layer/service-map/LayerServiceMapView.vue',
-    ],
+    // ── DECOMPOSE BACKLOG ─ the only file still over 2000 loc; its split is
+    //    in flight (LayerDashboardsAdmin → per-scope editors + composables).
+    //    This list only ever SHRINKS — remove the entry when the split lands.
+    files: ['src/features/admin/layer-templates/LayerDashboardsAdmin.vue'],
     rules: { 'max-lines': 'off' },
   },
 ];
diff --git a/package.json b/package.json
index 61fe0f1..c88a354 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,8 @@
     "dev:bff": "pnpm --filter @skywalking-horizon-ui/bff dev",
     "build": "pnpm -r run build",
     "type-check": "pnpm -r run type-check",
-    "lint": "pnpm -r run lint",
+    "lint": "pnpm -r run lint && node scripts/check-source-budget.mjs",
+    "lint:budget": "node scripts/check-source-budget.mjs",
     "test:unit": "pnpm -r run test:unit",
     "license:check": "license-eye -c .licenserc.yaml header check",
     "license:fix": "license-eye -c .licenserc.yaml header fix",
diff --git a/scripts/check-source-budget.mjs b/scripts/check-source-budget.mjs
new file mode 100644
index 0000000..59f50b7
--- /dev/null
+++ b/scripts/check-source-budget.mjs
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Source-budget guardrail. ESLint's max-lines covers code size in-editor;
+// this gate adds the comment-VOLUME cap ESLint cannot express, and is the
+// CI-runnable enforcer of both. A file is over budget when it carries more
+// than CODE_MAX lines of code OR more than COMMENT_MAX lines of comment.
+// SKIP lists files still mid-decomposition (mirrors eslint's grandfather
+// block) — it only ever shrinks.
+
+import { readFileSync } from 'node:fs';
+import { execSync } from 'node:child_process';
+
+const CODE_MAX = 2000;
+const COMMENT_MAX = 500;
+const SKIP = new Set([
+  'apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue',
+]);
+
+const files = execSync('git ls-files', { encoding: 'utf8' })
+  .split('\n')
+  .filter((f) => /\.(ts|vue|js|mjs)$/.test(f) && !f.endsWith('.d.ts'))
+  .filter((f) => f.startsWith('apps/') || f.startsWith('packages/') || 
f.startsWith('scripts/'))
+  .filter((f) => !/\.(test|spec)\.ts$/.test(f) && !SKIP.has(f));
+
+// Line-level classification: a line is comment if it sits inside a block
+// comment (/* */ or <!-- -->) or opens with // or /* or <!--. Inline trailing
+// comments count as code (the line carries code). Approximate by design — a
+// guardrail flags comment-heavy files, it isn't a forensic tokenizer.
+function classify(src, isVue) {
+  let comment = 0;
+  let blank = 0;
+  let inBlock = false;
+  let close = '';
+  const lines = src.split('\n');
+  for (const raw of lines) {
+    const line = raw.trim();
+    if (inBlock) {
+      comment++;
+      if (line.includes(close)) inBlock = false;
+      continue;
+    }
+    if (line === '') { blank++; continue; }
+    if (line.startsWith('//')) { comment++; continue; }
+    if (line.startsWith('/*')) {
+      comment++;
+      if (!line.includes('*/')) { inBlock = true; close = '*/'; }
+      continue;
+    }
+    if (isVue && line.startsWith('<!--')) {
+      comment++;
+      if (!line.includes('-->')) { inBlock = true; close = '-->'; }
+      continue;
+    }
+  }
+  return { code: lines.length - comment - blank, comment };
+}
+
+const over = [];
+for (const f of files) {
+  const { code, comment } = classify(readFileSync(f, 'utf8'), 
f.endsWith('.vue'));
+  const probs = [];
+  if (code > CODE_MAX) probs.push(`code ${code} > ${CODE_MAX}`);
+  if (comment > COMMENT_MAX) probs.push(`comments ${comment} > 
${COMMENT_MAX}`);
+  if (probs.length) over.push(`  ${f} — ${probs.join('; ')}`);
+}
+
+if (over.length) {
+  console.error(`✗ source-budget: ${over.length} file(s) over budget (code ≤ 
${CODE_MAX}, comments ≤ ${COMMENT_MAX}):`);
+  console.error(over.join('\n'));
+  process.exit(1);
+}
+console.log(`✓ source-budget OK: ${files.length} files within ${CODE_MAX} code 
/ ${COMMENT_MAX} comment lines`);

Reply via email to