@1agh/maude 1.3.1 → 1.4.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.
@@ -329,6 +329,96 @@ function resolveRepoRoot(): string {
329
329
  return process.cwd();
330
330
  }
331
331
 
332
+ /** Where the sync runtime keeps what the linked project says about itself. */
333
+ export const PROJECT_CONFIG_CACHE_REL = '_state/project-config.json';
334
+
335
+ export interface ProjectConfig {
336
+ canvasGroups: { label: string; path: string }[];
337
+ designSystems: { name: string; path: string; tokensCssRel?: string }[];
338
+ }
339
+
340
+ /**
341
+ * The project's own labels and design systems, as the hub reported them —
342
+ * UNTRUSTED (DDR-054): only short names and contained relative paths survive.
343
+ */
344
+ export function sanitizeProjectConfig(raw: unknown): ProjectConfig | null {
345
+ if (!raw || typeof raw !== 'object') return null;
346
+ const r = raw as { canvasGroups?: unknown; designSystems?: unknown };
347
+ const groups = Array.isArray(r.canvasGroups) ? r.canvasGroups : [];
348
+ const systems = Array.isArray(r.designSystems) ? r.designSystems : [];
349
+ const canvasGroups = groups
350
+ .filter(
351
+ (g): g is { label: string; path: string } =>
352
+ !!g &&
353
+ typeof g === 'object' &&
354
+ typeof (g as { label?: unknown }).label === 'string' &&
355
+ (g as { label: string }).label.length > 0 &&
356
+ (g as { label: string }).label.length <= 64 &&
357
+ isContainedRel((g as { path?: unknown }).path)
358
+ )
359
+ .slice(0, 32)
360
+ .map((g) => ({ label: g.label, path: g.path.replace(/^\/+|\/+$/g, '') }));
361
+ const designSystems = systems
362
+ .filter(
363
+ (d): d is { name: string; path: string; tokensCssRel?: string } =>
364
+ !!d &&
365
+ typeof d === 'object' &&
366
+ typeof (d as { name?: unknown }).name === 'string' &&
367
+ /^[\w .-]{1,64}$/.test((d as { name: string }).name) &&
368
+ isContainedRel((d as { path?: unknown }).path) &&
369
+ ((d as { tokensCssRel?: unknown }).tokensCssRel == null ||
370
+ isContainedRel((d as { tokensCssRel?: unknown }).tokensCssRel))
371
+ )
372
+ .slice(0, 16)
373
+ .map((d) => ({
374
+ name: d.name,
375
+ path: d.path.replace(/^\/+|\/+$/g, ''),
376
+ ...(typeof d.tokensCssRel === 'string' ? { tokensCssRel: d.tokensCssRel } : {}),
377
+ }));
378
+ return { canvasGroups, designSystems };
379
+ }
380
+
381
+ /**
382
+ * What the linked project says about itself that this copy's own config does
383
+ * not. A managed desktop copy is declared from group PATHS alone, so its
384
+ * groups had no labels ("system", never "Design system") and it knew no
385
+ * design systems: the project's design system never appeared on a teammate's
386
+ * desktop (plan T31/L16). This only FILLS IN — a label the local config chose
387
+ * and a local design system of the same name always win — and only for a
388
+ * group this copy already declares.
389
+ */
390
+ function withProjectConfig(cfg: DevServerConfig, repoRoot: string): DevServerConfig {
391
+ if (!cfg.linkedHub) return cfg;
392
+ let project: ProjectConfig | null = null;
393
+ try {
394
+ const designRel = String(cfg.designRoot ?? '.design').replace(/^\/+|\/+$/g, '');
395
+ project = sanitizeProjectConfig(
396
+ JSON.parse(readFileSync(path.join(repoRoot, designRel, PROJECT_CONFIG_CACHE_REL), 'utf8'))
397
+ );
398
+ } catch {
399
+ return cfg;
400
+ }
401
+ if (!project) return cfg;
402
+ const labels = new Map(project.canvasGroups.map((g) => [g.path, g.label]));
403
+ const canvasGroups = (cfg.canvasGroups ?? []).map((g) => {
404
+ const p = String(g.path ?? '').replace(/^\/+|\/+$/g, '');
405
+ const label = labels.get(p);
406
+ return label && (!g.label || g.label === p) ? { ...g, label } : g;
407
+ });
408
+ const declared = new Set((cfg.designSystems ?? []).map((d) => d.name));
409
+ const groupPaths = canvasGroups.map((g) => String(g.path).replace(/^\/+|\/+$/g, ''));
410
+ const adopted = project.designSystems.filter(
411
+ (d) =>
412
+ !declared.has(d.name) && groupPaths.some((g) => d.path === g || d.path.startsWith(`${g}/`))
413
+ );
414
+ if (adopted.length === 0 && canvasGroups.every((g, i) => g === cfg.canvasGroups[i])) return cfg;
415
+ return {
416
+ ...cfg,
417
+ canvasGroups,
418
+ ...(adopted.length ? { designSystems: [...(cfg.designSystems ?? []), ...adopted] } : {}),
419
+ };
420
+ }
421
+
332
422
  function loadConfig(repoRoot: string): DevServerConfig {
333
423
  const configPath = path.join(repoRoot, '.design', 'config.json');
334
424
  let raw: string;
@@ -339,7 +429,9 @@ function loadConfig(repoRoot: string): DevServerConfig {
339
429
  }
340
430
  try {
341
431
  const parsed = JSON.parse(raw);
342
- return normalizeConfig({ ...DEFAULT_CONFIG, ...parsed, _source: '.design/config.json' });
432
+ return normalizeConfig(
433
+ withProjectConfig({ ...DEFAULT_CONFIG, ...parsed, _source: '.design/config.json' }, repoRoot)
434
+ );
343
435
  } catch (e) {
344
436
  const msg = e instanceof Error ? e.message : String(e);
345
437
  console.error(` warn: ${configPath} is not valid JSON: ${msg}. Using defaults.`);