Enforce canonical platform table alignment in pre-push audit

Adds canonical-platform-table/no-legacy-align-helper to
canonical-platform-audit.mjs: any platform table file that calls the
legacy `getPlatformTableHeadClass(<align>)` or
`getPlatformTableCellClass(<align>)` helper now fails the audit (which
runs in the pre-push hook). Authors are pointed at the kind-based
wrappers and the canonical column-kind file.

Two intentional allowlist entries:

  - sharedPlatformPage.tsx: the kind-based wrappers internally delegate
    to the align-based helpers; that's the one legitimate call site.

  - KubernetesClustersTable.tsx: another agent has this file
    mid-staged in their working tree. Removing this entry is part of
    that file's migration commit (drop the entry whenever the file
    moves to the *ForKind wrappers).

To support per-rule exemptions cleanly, the audit's inner loop now
honours an optional `allowFiles: Set<string>` on each HELPER_RULE
entry. Existing rules are unchanged; only the new rule uses it.

The platformOverviewLayout.guardrails.test.ts assertions are updated
to match the kind-based forms (regex match on the new
getPlatformTableHeadClassForKind('<kind>') ... <label> shape) for the
migrated files. The two Kubernetes Clusters assertions continue to
match the legacy form until that file migrates.

Workspace-level CLAUDE.md and AGENTS.md (at /Volumes/Development/pulse/,
outside this git repo) also got a one-paragraph entry pointing at the
canonical source-of-truth file and the audit rule, so every agent
session that reads those bootstrap files at startup picks up the
convention.
This commit is contained in:
rcourtman
2026-05-20 11:18:29 +01:00
parent 6d2faf66bf
commit ecd44945bc
2 changed files with 48 additions and 16 deletions
@@ -1338,6 +1338,30 @@ const HELPER_RULES = [
message:
'Do not define local RAID state/device presentation helpers in component code. Use @/utils/raidPresentation instead.',
},
{
// Platform tables must align columns by kind, not by literal align string.
// The kind-based wrappers (getPlatformTableHeadClassForKind /
// getPlatformTableCellClassForKind) live in
// src/features/platformPage/sharedPlatformPage.tsx and resolve to a
// canonical alignment via src/features/platformPage/columnAlignment.ts.
// The legacy align-based helpers still exist so the wrappers can delegate
// to them, but no platform table should call them directly. If you hit
// this rule, switch the call site to the *ForKind variant and pick a
// column kind ('name' | 'text' | 'metric-bar' | 'numeric-value' |
// 'badge') for the column.
rule: 'canonical-platform-table/no-legacy-align-helper',
regex: /\bgetPlatformTable(?:Head|Cell)Class\(/g,
message:
'Use getPlatformTable{Head,Cell}ClassForKind(kind) instead of the legacy align-based helper. See frontend-modern/src/features/platformPage/columnAlignment.ts for the canonical column kinds and their alignments.',
allowFiles: new Set([
// The kind-based wrappers internally delegate to the align-based
// helpers; that's the one legitimate call site.
'src/features/platformPage/sharedPlatformPage.tsx',
// KubernetesClustersTable is pending migration by another agent.
// Remove this entry once that work lands.
'src/features/kubernetes/KubernetesClustersTable.tsx',
]),
},
];
const MAP_RULES = [
@@ -1723,7 +1747,8 @@ for (const dir of TARGET_DIRS) {
const content = fs.readFileSync(filePath, 'utf8');
for (const { rule, regex, message } of HELPER_RULES) {
for (const { rule, regex, message, allowFiles } of HELPER_RULES) {
if (allowFiles && allowFiles.has(relativePath)) continue;
for (const match of content.matchAll(regex)) {
pushMatch(relativePath, content, match.index ?? 0, rule, message);
}
@@ -112,15 +112,22 @@ describe('platform overview layout guardrails', () => {
});
it('keeps mobile host tables focused on useful operational columns', () => {
expect(dockerHostsTableSource).toContain('<TableHead class={getPlatformTableHeadClass()}>Host');
expect(dockerHostsTableSource).toContain(
"<TableHead class={getPlatformTableHeadClass('right')}>CPU",
// Assertions use the canonical kind-based helpers
// (getPlatformTableHeadClassForKind('<kind>')) for files that have been
// migrated. KubernetesClustersTable still uses the legacy align-based
// helper because another agent has it mid-edit; the assertions below
// match that legacy form until its migration lands.
expect(dockerHostsTableSource).toMatch(
/getPlatformTableHeadClassForKind\('name'\)[\s\S]{0,200}?Host/,
);
expect(dockerHostsTableSource).toContain(
"<TableHead class={getPlatformTableHeadClass('right')}>Memory",
expect(dockerHostsTableSource).toMatch(
/getPlatformTableHeadClassForKind\('metric-bar'\)[\s\S]{0,200}?CPU/,
);
expect(dockerHostsTableSource).toContain(
"<TableHead class={getPlatformTableHeadClass('right')}>Disk",
expect(dockerHostsTableSource).toMatch(
/getPlatformTableHeadClassForKind\('metric-bar'\)[\s\S]{0,200}?Memory/,
);
expect(dockerHostsTableSource).toMatch(
/getPlatformTableHeadClassForKind\('metric-bar'\)[\s\S]{0,200}?Disk/,
);
expect(kubernetesClustersTableSource).toContain(
@@ -129,24 +136,24 @@ describe('platform overview layout guardrails', () => {
expect(kubernetesClustersTableSource).toContain(
"<TableHead class={getPlatformTableHeadClass('right')}>Nodes",
);
expect(kubernetesNodesTableSource).toContain(
'<TableHead class={getPlatformTableHeadClass()}>Node',
expect(kubernetesNodesTableSource).toMatch(
/getPlatformTableHeadClassForKind\('name'\)[\s\S]{0,200}?Node/,
);
expect(kubernetesNodesTableSource).toContain(
'<span class="md:hidden">{compactCapacityLabel()}</span>',
);
expect(truenasSystemsTableSource).toContain(
'<TableHead class={getPlatformTableHeadClass()}>System',
expect(truenasSystemsTableSource).toMatch(
/getPlatformTableHeadClassForKind\('name'\)[\s\S]{0,200}?System/,
);
expect(truenasSystemsTableSource).toContain(
'<span class="md:hidden">{formatPercent(storagePercent())}</span>',
);
expect(vsphereHostsTableSource).toContain(
'<TableHead class={getPlatformTableHeadClass()}>Host',
expect(vsphereHostsTableSource).toMatch(
/getPlatformTableHeadClassForKind\('name'\)[\s\S]{0,200}?Host/,
);
expect(vsphereHostsTableSource).toContain(
"<TableHead class={getPlatformTableHeadClass('right')}>VMs",
expect(vsphereHostsTableSource).toMatch(
/getPlatformTableHeadClassForKind\('numeric-value'\)[\s\S]{0,200}?VMs/,
);
expect(vsphereHostsTableSource).toContain('hidden md:table-cell');
});