@doneisbetter/gds-compliance 2.6.3 → 2.6.4

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 (2) hide show
  1. package/index.js +176 -0
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -10,6 +10,29 @@ const DEFAULT_STALE_DOCUMENTATION_REFERENCES = [
10
10
  '/Users/Shared/Projects/GENERAL_DESIGN_SYSTEM',
11
11
  'GENERAL_DESIGN_SYSTEM',
12
12
  ];
13
+ const STRICT_COMPLIANCE_FIELDS = [
14
+ 'approvedShellPrimitives',
15
+ 'approvedDetailPrimitives',
16
+ 'approvedListingPrimitives',
17
+ 'approvedActionPrimitives',
18
+ 'approvedTemporaryExceptions',
19
+ ];
20
+ const EXCEPTION_CATEGORIES = new Set([
21
+ 'runtime-constraint',
22
+ 'product-authored-experience',
23
+ 'package-coverage-gap',
24
+ 'migration-bridge',
25
+ ]);
26
+ const EXCEPTION_STATUSES = new Set(['temporary', 'approved', 'deprecated', 'removed']);
27
+ const EXCEPTION_REQUIRED_FIELDS = [
28
+ 'category',
29
+ 'scope',
30
+ 'allowedImplementation',
31
+ 'mustStillUse',
32
+ 'mustNotDo',
33
+ 'exitCondition',
34
+ 'status',
35
+ ];
13
36
 
14
37
  export function validateManifest(manifest) {
15
38
  const findings = [];
@@ -63,6 +86,83 @@ export function validateManifest(manifest) {
63
86
  }
64
87
  }
65
88
 
89
+ if (manifest.compliance?.strictMode != null && typeof manifest.compliance.strictMode !== 'boolean') {
90
+ findings.push({
91
+ rule: 'manifest.invalidComplianceConfig',
92
+ severity: 'error',
93
+ message: 'compliance.strictMode must be a boolean when provided.',
94
+ });
95
+ }
96
+
97
+ for (const field of STRICT_COMPLIANCE_FIELDS) {
98
+ const value = manifest.compliance?.[field];
99
+ if (value != null && !Array.isArray(value)) {
100
+ findings.push({
101
+ rule: 'manifest.invalidComplianceConfig',
102
+ severity: 'error',
103
+ message: `compliance.${field} must be an array when provided.`,
104
+ });
105
+ }
106
+ }
107
+
108
+ return findings;
109
+ }
110
+
111
+ function hasBroadScope(scope) {
112
+ return scope.some((entry) =>
113
+ ['*', '**', 'app/**', 'src/**', './**', '/**'].includes(entry) || /(^|\/)\*\*$/.test(entry));
114
+ }
115
+
116
+ function validateApprovedExceptions(manifest) {
117
+ const findings = [];
118
+
119
+ for (const exception of manifest.approvedExceptions ?? []) {
120
+ const missingFields = EXCEPTION_REQUIRED_FIELDS.filter((field) => {
121
+ const value = exception[field];
122
+ if (Array.isArray(value)) {
123
+ return value.length === 0;
124
+ }
125
+ return !value;
126
+ });
127
+
128
+ if (missingFields.length > 0) {
129
+ findings.push({
130
+ rule: 'exception-required-fields',
131
+ severity: 'error',
132
+ file: exception.surface,
133
+ message: `Approved exception "${exception.surface}" must define ${missingFields.join(', ')}. Upgrade legacy exception entries to the canonical exception-surface contract.`,
134
+ });
135
+ continue;
136
+ }
137
+
138
+ if (!EXCEPTION_CATEGORIES.has(exception.category)) {
139
+ findings.push({
140
+ rule: 'exception-invalid-category',
141
+ severity: 'error',
142
+ file: exception.surface,
143
+ message: `Approved exception "${exception.surface}" uses unsupported category "${exception.category}".`,
144
+ });
145
+ }
146
+
147
+ if (!EXCEPTION_STATUSES.has(exception.status)) {
148
+ findings.push({
149
+ rule: 'exception-invalid-status',
150
+ severity: 'error',
151
+ file: exception.surface,
152
+ message: `Approved exception "${exception.surface}" uses unsupported status "${exception.status}".`,
153
+ });
154
+ }
155
+
156
+ if (hasBroadScope(exception.scope ?? [])) {
157
+ findings.push({
158
+ rule: 'exception-broad-scope',
159
+ severity: 'error',
160
+ file: exception.surface,
161
+ message: `Approved exception "${exception.surface}" has an over-broad scope. Exception scopes must stay narrow and reviewable.`,
162
+ });
163
+ }
164
+ }
165
+
66
166
  return findings;
67
167
  }
68
168
 
@@ -150,6 +250,75 @@ function scanDocumentationFile(filePath, staleReferences) {
150
250
  return findings;
151
251
  }
152
252
 
253
+ function inferStrictSurface(contract) {
254
+ const normalized = contract.toLowerCase();
255
+ if (normalized.includes('shell')) return 'shell';
256
+ if (normalized.includes('detail') || normalized.includes('profile')) return 'detail';
257
+ if (normalized.includes('card') || normalized.includes('listing')) return 'listing';
258
+ if (normalized.includes('action') || normalized.includes('button')) return 'action';
259
+ return null;
260
+ }
261
+
262
+ function runStrictCompliance({ manifest, manifestRoot, sourceFiles }) {
263
+ const findings = [];
264
+ const strict = manifest.compliance ?? {};
265
+ const approvedBySurface = {
266
+ shell: new Set(strict.approvedShellPrimitives ?? []),
267
+ detail: new Set(strict.approvedDetailPrimitives ?? []),
268
+ listing: new Set(strict.approvedListingPrimitives ?? []),
269
+ action: new Set(strict.approvedActionPrimitives ?? []),
270
+ };
271
+ const approvedTemporaryExceptions = new Set(strict.approvedTemporaryExceptions ?? []);
272
+
273
+ for (const adapter of manifest.localAdapters ?? []) {
274
+ if (!['active', 'exception'].includes(adapter.status)) {
275
+ continue;
276
+ }
277
+
278
+ const surface = inferStrictSurface(adapter.contract);
279
+ if (!surface) {
280
+ continue;
281
+ }
282
+
283
+ if (approvedBySurface[surface].has(adapter.contract) || approvedTemporaryExceptions.has(adapter.contract)) {
284
+ continue;
285
+ }
286
+
287
+ findings.push({
288
+ rule: `strict.${surface}.local-adapter`,
289
+ severity: 'error',
290
+ file: adapter.path,
291
+ message: `Strict mode forbids local ${surface} adapter "${adapter.contract}". Migrate to the approved GDS primitive or declare a reviewed temporary exception.`,
292
+ });
293
+ }
294
+
295
+ for (const filePath of sourceFiles) {
296
+ const content = readFileSync(filePath, 'utf8');
297
+
298
+ if (/AppShell\s+as\s+MantineAppShell|<MantineAppShell\b|from\s+['"]@mantine\/core['"][\s\S]{0,120}AppShell/.test(content)) {
299
+ findings.push({
300
+ rule: 'strict.shell.mantine-app-shell',
301
+ severity: 'error',
302
+ file: filePath,
303
+ message: 'Strict mode forbids local Mantine AppShell wrappers. Use DiscoveryShell or the approved GDS shell wrapper.',
304
+ });
305
+ }
306
+
307
+ if (/(interface|type)\s+\w*(ActionBar|ButtonGroup|ButtonStack|Cta)\w*\s*[\{=]|export function \w*(ActionBar|ButtonGroup|ButtonStack|Cta)\w*/.test(content)
308
+ && /from\s+['"]@mantine\/core['"][\s\S]{0,200}\bButton\b/.test(content)
309
+ && !/from\s+['"]@doneisbetter\/gds-core['"][\s\S]{0,200}\bActionBar\b/.test(content)) {
310
+ findings.push({
311
+ rule: 'strict.action.legacy-wrapper',
312
+ severity: 'error',
313
+ file: filePath,
314
+ message: 'Strict mode forbids local button/action wrapper implementations. Use the canonical GDS ActionBar and semantic actions.',
315
+ });
316
+ }
317
+ }
318
+
319
+ return findings;
320
+ }
321
+
153
322
  export function runComplianceCheck({ manifestPath }) {
154
323
  const absoluteManifestPath = resolve(manifestPath);
155
324
  const manifestRoot = dirname(absoluteManifestPath);
@@ -166,6 +335,7 @@ export function runComplianceCheck({ manifestPath }) {
166
335
  ...DEFAULT_FORBIDDEN_IMPORTS,
167
336
  ...(manifest.compliance?.bannedImports ?? []),
168
337
  ];
338
+ const strictMode = manifest.compliance?.strictMode === true;
169
339
 
170
340
  for (const exception of manifest.approvedExceptions ?? []) {
171
341
  if (exception.dependency) {
@@ -222,6 +392,8 @@ export function runComplianceCheck({ manifestPath }) {
222
392
  findings.push(...scanSourceFile(filePath, allowedImports, forbiddenImports));
223
393
  }
224
394
 
395
+ findings.push(...validateApprovedExceptions(manifest));
396
+
225
397
  if (protectedSurfacePaths.length) {
226
398
  const normalizedProtectedSurfacePaths = protectedSurfacePaths.map((value) => normalizePath(resolve(manifestRoot, value)));
227
399
 
@@ -246,6 +418,10 @@ export function runComplianceCheck({ manifestPath }) {
246
418
  }
247
419
  }
248
420
 
421
+ if (strictMode) {
422
+ findings.push(...runStrictCompliance({ manifest, manifestRoot, sourceFiles }));
423
+ }
424
+
249
425
  return {
250
426
  manifest,
251
427
  findings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doneisbetter/gds-compliance",
3
- "version": "2.6.3",
3
+ "version": "2.6.4",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "bin": {