synthesisui 0.16.483 → 0.16.485

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.
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import { importMark, writeSyncMark } from "../last-sync.js";
3
3
  import { readStyleSheet } from "../doctor/scss-lens.js";
4
- import { tsTokenObjects, tsTokensAsVars } from "../doctor/ts-tokens-lens.js";
4
+ import { THEME_SOURCE, tsTokenObjects, tsTokensAsVars } from "../doctor/ts-tokens-lens.js";
5
5
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
6
6
  import { anatomyFromSketch } from "../anatomy-from-sketch.js";
7
7
  import { applyAnatomyPatch, hasEdits, } from "../anatomy-patch.js";
@@ -11,7 +11,7 @@ import { PAGES_MAX, pageCompositionOf } from "../census-pages.js";
11
11
  import { readCredentials, readProjectConfig, readToken, resolveRegistry, sameRegistry, } from "../config.js";
12
12
  import { declaredElsewhere } from "../declared-elsewhere.js";
13
13
  import { architectureGap, architectureRule, componentHome, describeArchitecture, describeChoice, describeGap, detectArchitectures, homeLine, packagingOf, proposeNewHome, resolvesAs, countsAsOrganization, } from "../doctor/architecture.js";
14
- import { findBrokenRefs } from "../doctor/broken-refs.js";
14
+ import { findBrokenRefs, findOrphanVars, findSynonyms, synonymLine } from "../doctor/broken-refs.js";
15
15
  import { nestingRules, propRules, readDefinitionProps, readNesting, readRuntime, } from "../doctor/call-sites.js";
16
16
  import { asCatalogueTable, describeFallback, fetchCatalogue, } from "../doctor/catalogue-fetch.js";
17
17
  import { describeClassStyle, detectClassStyle, } from "../doctor/class-style.js";
@@ -36,6 +36,9 @@ import { aliasesOf, describeReachability, edgesIn, reachabilityOf, } from "../do
36
36
  import { rootGroundOf } from "../doctor/root-ground.js";
37
37
  import { diagnose, scanSource } from "../doctor/scan.js";
38
38
  import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
39
+ import { attributeFaces } from "../doctor/scheme-scope.js";
40
+ import { cssImportLists, resolveSheetConflicts, unloadedSheets } from "../doctor/sheet-order.js";
41
+ import { unreadComponentFiles } from "../doctor/other-frameworks.js";
39
42
  import { describeSignals, emptySignals, finishSignals, readSignalsInto, } from "../doctor/signals.js";
40
43
  import { sketchOf } from "../doctor/sketch.js";
41
44
  import { describeForeign, readStyleIslands, } from "../doctor/style-island.js";
@@ -282,6 +285,34 @@ alsoGlobalsFrom) {
282
285
  const body = await readStyleSheet(file);
283
286
  sheets.push({ file, body });
284
287
  }
288
+ /**
289
+ * O QUE UMA FOLHA DELE IMPORTA DE DENTRO DE UMA PASTA QUE A VARREDURA PULA - bateria ampliada, 29/09.
290
+ *
291
+ * O Style Dictionary gera `build/css/variables.css` e o `globals.css` o importa. A varredura pula `build/`
292
+ * (lá costuma morar saída de build, e ler aquilo seria ler o bundle), então as 14 variáveis dele nunca
293
+ * entravam, e o import dizia "your code names no token yet" a quem tinha o sistema mais organizado do kit.
294
+ *
295
+ * A pasta continua pulada; o que muda é que uma folha que ELE importa entra, porque o `@import` é a palavra
296
+ * dele dizendo que aquilo faz parte do CSS dele. Só dentro do escopo medido.
297
+ */
298
+ const lidas = new Set(sheets.map((s) => s.file));
299
+ for (let i = 0; i < sheets.length; i++) {
300
+ for (const spec of sheetImports(sheets[i].body)) {
301
+ for (const candidate of partialCandidates(spec)) {
302
+ const alvo = join(dirname(sheets[i].file), candidate);
303
+ if (lidas.has(alvo))
304
+ break;
305
+ if (!alvo.startsWith(`${root}${sep}`))
306
+ break;
307
+ const body = await readStyleSheet(alvo);
308
+ if (!body)
309
+ continue;
310
+ lidas.add(alvo);
311
+ sheets.push({ file: alvo, body });
312
+ break;
313
+ }
314
+ }
315
+ }
285
316
  /**
286
317
  * AS FOLHAS DO RESTO DO REPOSITÓRIO - lidas com a MESMA régua, e nunca com uma segunda.
287
318
  * `deForaDoEscopo` marca quais são, porque elas só podem virar classe global: nem `css`, nem
@@ -564,13 +595,45 @@ export async function takeCensus(root, opts) {
564
595
  */
565
596
  const tsObjects = [];
566
597
  for await (const file of walkAll([root]))
567
- if (/\.(ts|tsx)$/.test(file) && !/\.d\.ts$/.test(file))
598
+ if (THEME_SOURCE.test(file) && !/\.d\.ts$/.test(file))
568
599
  tsObjects.push(...tsTokenObjects(await readFile(file, "utf8").catch(() => "")));
569
600
  const tsRoot = tsObjects
570
601
  .flatMap((o) => [...o.values].map(([n, v]) => `${n}: ${v};`))
571
602
  .join(" ");
572
- const table = buildTable({ css: tsRoot ? `${css}\n:root { ${tsRoot} }` : css, source: "yours" });
573
- const schemes = parseSchemeBlocks(css);
603
+ /**
604
+ * O MESMO TOKEN EM DUAS FOLHAS: fica o que a TELA mostra - ver `sheet-order.ts` (bateria ampliada, c20). O leitor de
605
+ * raiz fica com a primeira declaração; aqui a folha que o código importa por último vem primeiro, só para os tokens.
606
+ * As classes continuam lendo `css` na ordem de sempre.
607
+ */
608
+ const codeFiles = [];
609
+ const citingFiles = [];
610
+ for await (const file of walkAll([root])) {
611
+ if (/\.(tsx|ts|jsx|js|mjs)$/.test(file) && !/\.d\.ts$/.test(file))
612
+ codeFiles.push(file);
613
+ if (/\.(tsx|ts|jsx|js|mjs|cjs|vue|svelte|astro|html|json)$/.test(file) && !/\.d\.ts$/.test(file))
614
+ citingFiles.push(file);
615
+ }
616
+ // A varredura não entra em `.json` nem em `.html`, e é na RAIZ que eles dizem que folhas carregam: `angular.json`,
617
+ // `index.html` (Vite, site estático). Sem isto o `styles.scss` do Angular parecia não carregado.
618
+ for (const name of await readdir(root).catch(() => []))
619
+ if (/\.(json|html)$/.test(name) && name !== "package-lock.json")
620
+ citingFiles.push(join(root, name));
621
+ const sheetsInScope = globalSheets
622
+ .filter((g) => !g.outside)
623
+ .map((g) => ({ file: resolve(root, g.file), body: g.body }));
624
+ /** A folha que ninguém carrega não dá token - ver `unloadedSheets` (bateria ampliada, c17 e c18). */
625
+ const unloaded = new Set(await unloadedSheets(sheetsInScope, citingFiles));
626
+ const loaded = sheetsInScope.filter((g) => !unloaded.has(g.file));
627
+ const { ordered, conflicts: declaredConflicts } = resolveSheetConflicts(loaded, await cssImportLists(codeFiles));
628
+ const tokenCss = unloaded.size > 0 || declaredConflicts.some((c) => c.decidedBy) ? ordered.map((g) => g.body).join("\n") : css;
629
+ const declaredUnloaded = [...unloaded];
630
+ /** Componentes que esta versão não lê (Vue, Svelte, Astro, Angular) - ver `other-frameworks.ts`. */
631
+ const everyFile = (await readdir(root, { recursive: true }).catch(() => []))
632
+ .filter((f) => !/(^|[/\\])(node_modules|\.git|\.next|\.nuxt|\.svelte-kit|\.astro|\.angular|dist|build|_synthesisui)([/\\]|$)/.test(f))
633
+ .map((f) => join(root, f));
634
+ const unreadComponents = await unreadComponentFiles(everyFile);
635
+ const table = buildTable({ css: tsRoot ? `${tokenCss}\n:root { ${tsRoot} }` : tokenCss, source: "yours" });
636
+ const schemes = parseSchemeBlocks(tokenCss);
574
637
  const reports = [];
575
638
  const tally = emptyTally();
576
639
  const internal = await internalSpecifiers(root);
@@ -2469,6 +2532,7 @@ export async function takeCensus(root, opts) {
2469
2532
  globalSheets
2470
2533
  .filter((g) => !g.outside)
2471
2534
  .map((g) => ({ file: g.file, css: g.body })));
2535
+ const declaredBrands = attributeFaces(css, new Set(schemes.base.keys()));
2472
2536
  const declaredSchemes = {
2473
2537
  base: Object.fromEntries(schemes.base),
2474
2538
  light: Object.fromEntries(schemes.light),
@@ -2620,6 +2684,8 @@ export async function takeCensus(root, opts) {
2620
2684
  }
2621
2685
  }
2622
2686
  const brokenRefs = findBrokenRefs(sources, declaredNames);
2687
+ const orphanVars = findOrphanVars(css, sources);
2688
+ const synonyms = findSynonyms(css, sources);
2623
2689
  const conventions = detectConventions(sources);
2624
2690
  const classStyle = detectClassStyle(sources);
2625
2691
  /**
@@ -2696,6 +2762,8 @@ export async function takeCensus(root, opts) {
2696
2762
  : {}),
2697
2763
  ...(animations.size > 0 ? { animations: [...animations].sort() } : {}),
2698
2764
  ...(brokenRefs.length > 0 ? { brokenRefs } : {}),
2765
+ ...(orphanVars.length > 0 ? { orphanVars } : {}),
2766
+ ...(synonyms.length > 0 ? { synonyms } : {}),
2699
2767
  /**
2700
2768
  * OS SISTEMAS QUE O CSS DELE JÁ CHAMA PELO NOME - ver `wiredSlugs`.
2701
2769
  *
@@ -2757,6 +2825,10 @@ export async function takeCensus(root, opts) {
2757
2825
  ? { declaredAlt: Object.fromEntries(schemes.dark) }
2758
2826
  : {}),
2759
2827
  declaredSchemes,
2828
+ ...(declaredBrands.length > 0 ? { declaredBrands } : {}),
2829
+ ...(declaredConflicts.length > 0 ? { declaredConflicts } : {}),
2830
+ ...(declaredUnloaded.length > 0 ? { declaredUnloaded } : {}),
2831
+ ...(unreadComponents.length > 0 ? { unreadComponents } : {}),
2760
2832
  observed: distinctValues(d),
2761
2833
  ...(components.length > 0 ? { components } : {}),
2762
2834
  ...(defined.length > 0 ? { defined } : {}),
@@ -3051,6 +3123,38 @@ async function summarize(c, root, scope) {
3051
3123
  sayAdoption(c);
3052
3124
  sayCollisions(c);
3053
3125
  await sayBrokenRefs(c, root, scope);
3126
+ sayOrphanVars(c);
3127
+ saySynonyms(c);
3128
+ }
3129
+ /**
3130
+ * A MESMA COR COM VÁRIOS NOMES - uma linha por cor, até três (item 16 da bateria, 29/09). Propõe o nome que o código
3131
+ * mais lê; quem decide se junta é ele.
3132
+ */
3133
+ function saySynonyms(c) {
3134
+ const all = c.synonyms ?? [];
3135
+ if (all.length === 0)
3136
+ return;
3137
+ console.log("");
3138
+ console.log(section("One colour, several names"));
3139
+ for (const g of all.slice(0, 3))
3140
+ console.log(body(synonymLine(g)));
3141
+ if (all.length > 3)
3142
+ console.log(body(paint.faint(`(${all.length - 3} more colours with more than one name)`)));
3143
+ }
3144
+ /**
3145
+ * DECLARADAS E NUNCA USADAS - uma linha, nunca uma acusação (item 15 da bateria, 29/09).
3146
+ *
3147
+ * No b1 eram três sobras de um tema antigo, e o import passava calado. Quem lê decide se apaga: pode ser de propósito.
3148
+ */
3149
+ function sayOrphanVars(c) {
3150
+ const all = c.orphanVars ?? [];
3151
+ if (all.length === 0)
3152
+ return;
3153
+ const names = all.slice(0, 6).map((o) => paint.strong(o.name));
3154
+ const more = all.length > 6 ? ` and ${all.length - 6} more` : "";
3155
+ console.log("");
3156
+ console.log(section("Declared and never used"));
3157
+ console.log(body(`${all.length} variable${all.length === 1 ? "" : "s"} your CSS declares and nothing reads: ${names.join(", ")}${more}. Usually left over from an old theme - yours to delete or keep.`));
3054
3158
  }
3055
3159
  /**
3056
3160
  * OS ESCOPOS QUE VIRARAM UM SISTEMA, e o que cada um perdeu no empate.
@@ -3908,6 +4012,11 @@ export function defaultScheme(declared) {
3908
4012
  * transcrito em vez de serem reescritas: a régua que as produziu é a mesma, então a tela nunca
3909
4013
  * diverge do relatório.
3910
4014
  */
4015
+ /** O caminho a partir da pasta do projeto, que é como ele o reconhece. */
4016
+ function relativeish(file) {
4017
+ const cwd = process.cwd();
4018
+ return file.startsWith(`${cwd}${sep}`) ? file.slice(cwd.length + 1) : file;
4019
+ }
3911
4020
  function mostrarLeitura(c, secoes, titulos = DECIDEM) {
3912
4021
  const valores = c.values?.classes;
3913
4022
  const decisoes = valores ? valores.seen - valores.structure : 0;
@@ -3920,6 +4029,22 @@ function mostrarLeitura(c, secoes, titulos = DECIDEM) {
3920
4029
  console.log(body(`${paint.strong(`${lidos} of ${total}`)} components came out with a blueprint${pct === null
3921
4030
  ? ""
3922
4031
  : `, and ${paint.strong(`${pct}%`)} of your ${decisoes} style decisions carry your own vocabulary`}.`));
4032
+ for (const u of c.unreadComponents ?? []) {
4033
+ const ext = { Vue: ".vue", Svelte: ".svelte", Astro: ".astro", Angular: "Angular" }[u.framework] ?? u.framework;
4034
+ console.log(body(`Your components are written in ${u.framework} (${u.files} ${ext === "Angular" ? "Angular component" : ext} file${u.files === 1 ? "" : "s"}), and this version reads components written in React only. Your colours, spacing and type came in; your components did not - the system starts without them.`));
4035
+ }
4036
+ for (const f of c.declaredUnloaded ?? [])
4037
+ console.log(body(`${relativeish(f)} declares variables, and nothing in this project loads it - no import, no link, no config. They were left out of this system.`));
4038
+ for (const k of c.declaredConflicts ?? []) {
4039
+ const onde = k.values.map((v) => `${v.value} in ${relativeish(v.file)}`).join(", ");
4040
+ console.log(body(k.decidedBy
4041
+ ? `${k.name} is declared twice (${onde}). Your screen shows ${k.shown}, the one ${relativeish(k.decidedBy)} imports last, and that is the value this system keeps.`
4042
+ : `${k.name} is declared twice (${onde}), and nothing here says which one loads last. This system keeps ${k.shown} - check which one your screen shows.`));
4043
+ }
4044
+ if (c.declaredBrands?.length) {
4045
+ const outras = c.declaredBrands.map((b) => b.selector).join(", ");
4046
+ console.log(body(`Your code names ${c.declaredBrands.length === 1 ? "one more brand" : `${c.declaredBrands.length} more brands`} with the same names (${outras}). This system is the one in :root; ${c.declaredBrands.length === 1 ? "the other was" : "the others were"} read and left out, not mixed in.`));
4047
+ }
3923
4048
  /**
3924
4049
  * TODAS as ocorrências, e não a primeira: um import com dois escopos declara a organização de
3925
4050
  * CADA um, e `find` mostraria `packages/ui` e engoliria `packages/charts` - a segunda lacuna
@@ -4138,6 +4263,13 @@ const DECIDEM = [
4138
4263
  const NA_TELA_DO_IMPORT = [
4139
4264
  "How this project is organised",
4140
4265
  ...DECIDEM,
4266
+ /**
4267
+ * E AS ÓRFÃS, numa linha (item 15 da bateria, 29/09): no b1 o dev contava três sobras de tema e o import passava
4268
+ * calado. Cabe porque é uma linha só, e ela muda o que ele faz a seguir: apagar ou manter.
4269
+ */
4270
+ "Declared and never used",
4271
+ /** E a mesma cor com vários nomes (item 16 da bateria, 29/09): até três linhas, e cada uma propõe um nome. */
4272
+ "One colour, several names",
4141
4273
  ];
4142
4274
  function sayReach(c) {
4143
4275
  const reach = ladderReach(c.declared);
@@ -4654,6 +4786,20 @@ quiet = false) {
4654
4786
  * token nenhum o sistema inteiro é a semente, e "v1 is your tokens" levava ao contrário. Ver
4655
4787
  * `nao-espelha-o-que-nao-existe.spec.ts`.
4656
4788
  */
4789
+ /**
4790
+ * AS CORES QUE O PROJETO NÃO TEM - item 18 da bateria (28/09), liberado por ele em 29/09. No b4 a borda, o sucesso e o
4791
+ * perigo vinham do nosso ponto de partida, e o dev achava que eram dele. O servidor manda quais (`fromSeed`).
4792
+ */
4793
+ export function fromSeedLine(roles) {
4794
+ if (roles.length === 0)
4795
+ return null;
4796
+ const list = roles.length === 1
4797
+ ? roles[0]
4798
+ : `${roles.slice(0, -1).join(", ")} or ${roles[roles.length - 1]}`;
4799
+ return roles.length === 1
4800
+ ? `Your code names no ${list} colour, so it comes from our starting point - yours to change on the colours page.`
4801
+ : `Your code names no ${list} colour, so those come from our starting point - yours to change on the colours page.`;
4802
+ }
4657
4803
  export function birthLine(name, declared) {
4658
4804
  return declared === 0
4659
4805
  ? `${name} - your code names no token yet, so this system is our starting point, with your colours and sizes measured next to it (listed in the report).`
@@ -5160,6 +5306,15 @@ export async function runImport(opts) {
5160
5306
  */
5161
5307
  if (payload?.primary)
5162
5308
  console.log(body(payload.primary));
5309
+ /**
5310
+ * E AS QUE ELE NÃO TEM (item 18 da bateria, 29/09). Com a porta das cores aberta a frase dela já diz tudo, e com zero
5311
+ * token a frase do nascimento já diz que o sistema inteiro é o ponto de partida - aí esta linha seria repetição.
5312
+ */
5313
+ const seedLine = !payload?.coloursDoor && Object.keys(census.declared ?? {}).length > 0
5314
+ ? fromSeedLine(payload?.fromSeed ?? [])
5315
+ : null;
5316
+ if (seedLine)
5317
+ console.log(body(seedLine));
5163
5318
  /**
5164
5319
  * A PORTA - convite, nunca bloqueio (o ponto 1 que ele aprovou em 28/09). O servidor manda a frase pronta, a mesma
5165
5320
  * que o quadro do sistema mostra, e o link da tela de escolher daquele sistema.
@@ -5298,6 +5453,8 @@ export async function runImport(opts) {
5298
5453
  * `import-installs.spec.ts` provava que a chamada existe e não que ela leva o que precisa.
5299
5454
  */
5300
5455
  cli: opts.cli,
5456
+ /** O projeto é o de origem: nenhum `fonts.ts` morto no código dele - ver `AddOptions.fromImport`. */
5457
+ fromImport: true,
5301
5458
  });
5302
5459
  }
5303
5460
  catch (error) {
@@ -6,6 +6,8 @@ import { readFile } from "node:fs/promises";
6
6
  import { join } from "node:path";
7
7
  import { readProjectTheme, writeBesideTheirs, writeNewProjectRoot, writeNewProjectTheme } from "../new-project-theme.js";
8
8
  import { add } from "./add.js";
9
+ import { resolveRegistry } from "../config.js";
10
+ import { fetchDesignSystem } from "../registry.js";
9
11
  const whereOf = (answer) => answer === "file" || answer === "1"
10
12
  ? "file"
11
13
  : answer === "separate" || answer === "2"
@@ -48,6 +50,14 @@ async function askWhere(tokensFile, separate, entry, intro) {
48
50
  */
49
51
  export async function init(opts) {
50
52
  const root = opts.dir ?? process.cwd();
53
+ /**
54
+ * O SISTEMA EXISTE ANTES DE QUALQUER ARQUIVO - bateria ampliada, item 21 (29/09). `init --ds teste-nunca-existiu`
55
+ * recusava bem ("No design system ... that this machine can see"), mas só depois de escrever `_synthesisui/config.json`,
56
+ * o `ledger.jsonl` e a statusline: o projeto ficava com o rastro de uma instalação que não aconteceu. A pergunta
57
+ * vai ao registry primeiro; a recusa é a mesma, e agora nada fica para trás.
58
+ */
59
+ if (opts.ds)
60
+ await fetchDesignSystem(resolveRegistry(opts.registry), opts.ds);
51
61
  const target = opts.target === "general" ? "general" : "next";
52
62
  const config = {
53
63
  target,
@@ -405,6 +405,16 @@ export async function remeasure(args) {
405
405
  * a frase diz que é o esperado, e o `.lock` guarda isso, para o `align` não pedir de novo.
406
406
  */
407
407
  if (head?.status === 404) {
408
+ /**
409
+ * MAS O 404 TAMBÉM É A RESPOSTA A UMA SESSÃO RECUSADA (29/09): a rota não diz "não te conheço" para não revelar
410
+ * sistema de ninguém. O token de www.synthesisui.com num sync contra o localhost caía aqui, e o `teste-e09-fechamento`,
411
+ * que TEM leitura, foi marcado como nunca medido. Antes de concluir, pergunta à sessão.
412
+ */
413
+ if (!(await sessionAccepted(base, token))) {
414
+ console.log(section("Sync"));
415
+ console.log(body(`${base} does not accept this machine's login, so nothing was re-measured - and nothing was marked. Sign in there and run this again: npx synthesisui login --registry ${base}`));
416
+ return null;
417
+ }
408
418
  await markNoReading(root, slug);
409
419
  console.log(section("Sync"));
410
420
  console.log(body(`"${slug}" was not read from a repository - it comes from the gallery or was built on the platform - so there is nothing here to re-measure. That is expected, and this repo will stop asking.`));
@@ -422,10 +432,22 @@ export async function remeasure(args) {
422
432
  * SEM CENSO GRAVADO NÃO HÁ O QUE REMEDIR CONTRA. Um sistema que nasceu da galeria não tem escopo
423
433
  * nem leitura, e medir o repo dele produziria um censo de um projeto que ninguém mapeou para ele.
424
434
  */
435
+ /**
436
+ * A LEITURA EXISTE, então uma marca "nunca medido" no `.lock` está errada - a que uma sessão recusada deixou antes de
437
+ * 29/09 (ver o 404 acima). Sai aqui, para o `align` voltar a pedir o que precisa.
438
+ */
439
+ if (stored.hasCensus)
440
+ await unmarkNoReading(root, slug);
441
+ /**
442
+ * O SISTEMA DA GALERIA APRENDE COM O QUE ELE CONSTRÓI - item 8 da bateria (28/09), liberado por ele em 29/09.
443
+ *
444
+ * Esta resposta era "did not come from an import (...) Import it first", e no b11 tudo o que o agente construiu em cima
445
+ * do sistema da galeria ficava fora da plataforma. Sem leitura gravada, a medição vai sobre o repositório inteiro (a
446
+ * mesma queda de escopo que `scopes` abaixo já faz), e a frase diz que é a primeira.
447
+ */
425
448
  if (!stored.hasCensus) {
426
449
  console.log(section("Sync"));
427
- console.log(body(`"${slug}" did not come from an import, so there is no measurement to refresh. Import it first: npx synthesisui import`));
428
- return null;
450
+ console.log(body(`This is the first measurement of what this project built on "${slug}" - it came from the gallery, so the platform had not read this repo yet. What you created goes to your system's draft; the system's own colours and type stay as they are.`));
429
451
  }
430
452
  /**
431
453
  * A DISTÂNCIA ATÉ O CLI PUBLICADO, dita onde ele já está.
@@ -1094,6 +1116,32 @@ async function askForScope(root) {
1094
1116
  export async function installedSlug(root) {
1095
1117
  return (await installedSlugs(root))[0] ?? null;
1096
1118
  }
1119
+ /** A sessão desta máquina vale em `base`? - a mesma pergunta que o `status` faz. Sem resposta, diz que não. */
1120
+ async function sessionAccepted(base, token) {
1121
+ const res = await fetch(`${base}/api/auth/get-session`, {
1122
+ headers: { Authorization: `Bearer ${token}` },
1123
+ }).catch(() => null);
1124
+ if (!res?.ok)
1125
+ return false;
1126
+ const who = (await res.json().catch(() => null));
1127
+ return Boolean(who?.user);
1128
+ }
1129
+ /** Tira do `.lock` a marca de "sem leitura", quando a plataforma diz que a leitura existe. */
1130
+ async function unmarkNoReading(root, slug) {
1131
+ const path = join(root, "_synthesisui", "ds", slug, ".lock");
1132
+ const raw = await readFile(path, "utf8").catch(() => "");
1133
+ if (!raw)
1134
+ return;
1135
+ try {
1136
+ const { reading, ...rest } = JSON.parse(raw);
1137
+ if (reading !== "none")
1138
+ return;
1139
+ await writeFile(path, `${JSON.stringify(rest, null, 2)}\n`, "utf8");
1140
+ }
1141
+ catch {
1142
+ /** Um `.lock` ilegível não é deste passo consertar. */
1143
+ }
1144
+ }
1097
1145
  /** Anota no `.lock` que a plataforma não tem leitura deste sistema - ver o 404 em `remeasure`. */
1098
1146
  async function markNoReading(root, slug) {
1099
1147
  const path = join(root, "_synthesisui", "ds", slug, ".lock");
@@ -72,8 +72,11 @@ const SHAPES = [
72
72
  * Next (app e pages) são do framework: dizem onde a tela mora, não onde um componente nasce.
73
73
  */
74
74
  const ROUTE_FILE = /(^|\/)(page|layout|loading|error|global-error|not-found|template|default|route|head|_app|_document|_error)\.(tsx|jsx|ts|js)$/i;
75
+ /** E todo arquivo de `pages/` do pages router - bateria ampliada, c30: `index.tsx` e `about.tsx` são rotas, não
76
+ * componentes "ao lado da rota". */
77
+ const PAGES_DIR = /^(src\/)?pages\//i;
75
78
  export function countsAsOrganization(rel) {
76
- return /\.(tsx|jsx|vue|svelte)$/i.test(rel) && !ROUTE_FILE.test(rel);
79
+ return /\.(tsx|jsx|vue|svelte)$/i.test(rel) && !ROUTE_FILE.test(rel) && !PAGES_DIR.test(rel.replace(/\\/g, "/"));
77
80
  }
78
81
  /**
79
82
  * O MESMO NOME, MÓDULO CAIXA E SEPARADOR - a única normalização deste arquivo.
@@ -174,3 +174,64 @@ export function findBrokenRefs(sources, declared) {
174
174
  b.count - a.count ||
175
175
  a.name.localeCompare(b.name));
176
176
  }
177
+ const THEME_BLOCK = /@theme\b[^{]*\{[^}]*\}/g;
178
+ const DECLARES_VAR = /(--[a-zA-Z0-9_-]+)\s*:/g;
179
+ const READS_IN_JS = /getPropertyValue\(\s*['"`](--[a-zA-Z0-9_-]+)/g;
180
+ const TAILWIND_SHORT = /\((--[a-zA-Z0-9_-]+)\)/g;
181
+ export function findOrphanVars(css, sources) {
182
+ const declared = new Set();
183
+ for (const m of css.replace(THEME_BLOCK, "").matchAll(DECLARES_VAR))
184
+ declared.add(m[1]);
185
+ if (declared.size === 0)
186
+ return [];
187
+ const used = new Set();
188
+ for (const text of [css, ...sources.map((s) => s.source)]) {
189
+ for (const m of text.matchAll(VAR_REF))
190
+ used.add(m[1]);
191
+ for (const m of text.matchAll(READS_IN_JS))
192
+ used.add(m[1]);
193
+ for (const m of text.matchAll(TAILWIND_SHORT))
194
+ used.add(m[1]);
195
+ }
196
+ return [...declared].filter((n) => !used.has(n)).map((name) => ({ name }));
197
+ }
198
+ const DECLARES_COLOUR = /(--[a-zA-Z0-9_-]+)\s*:\s*(#[0-9a-fA-F]{3,8}|rgba?\([^)]*\)|hsla?\([^)]*\))\s*[;}]/g;
199
+ export function findSynonyms(css, sources) {
200
+ const byValue = new Map();
201
+ for (const m of css.replace(THEME_BLOCK, "").matchAll(DECLARES_COLOUR)) {
202
+ const value = m[2].replace(/\s+/g, "").toLowerCase();
203
+ const names = byValue.get(value) ?? [];
204
+ if (!names.includes(m[1]))
205
+ names.push(m[1]);
206
+ byValue.set(value, names);
207
+ }
208
+ const uses = new Map();
209
+ /** Só nas fontes: a folha `css` é a soma das folhas que também estão lá, e contar as duas dobraria cada leitura. */
210
+ for (const text of sources.length > 0 ? sources.map((s) => s.source) : [css])
211
+ for (const m of text.matchAll(VAR_REF))
212
+ uses.set(m[1], (uses.get(m[1]) ?? 0) + 1);
213
+ const groups = [];
214
+ for (const [value, names] of byValue) {
215
+ if (names.length < 2)
216
+ continue;
217
+ groups.push({
218
+ value,
219
+ names: names
220
+ .map((name) => ({ name, uses: uses.get(name) ?? 0 }))
221
+ .sort((a, b) => b.uses - a.uses),
222
+ });
223
+ }
224
+ return groups.sort((a, b) => b.names.length - a.names.length);
225
+ }
226
+ /** "#2563eb has 4 names: ... Your code reads --brand the most - keeping one would make the rest aliases." */
227
+ export function synonymLine(g) {
228
+ const listed = g.names
229
+ .map((n) => `${n.name} (${n.uses > 0 ? `${n.uses}×` : "never read"})`)
230
+ .join(", ");
231
+ const top = g.names[0]?.uses ?? 0;
232
+ const most = g.names.filter((n) => n.uses === top && top > 0).map((n) => n.name);
233
+ const tail = most.length === 0
234
+ ? "Nothing reads any of them yet - keeping one would make the rest aliases."
235
+ : `Your code reads ${most.length === 1 ? most[0] : `${most.slice(0, -1).join(", ")} and ${most[most.length - 1]}`} the most - keeping one would make the rest aliases.`;
236
+ return `${g.value} has ${g.names.length} names: ${listed}. ${tail}`;
237
+ }
@@ -30,6 +30,8 @@ import { localOnlyNames } from "./components-scan.js";
30
30
  * so a file called `page.tsx` IS a route in every Next codebase there is.
31
31
  */
32
32
  const RESERVED_FILE = /(^|[/\\])(page|layout|template|default|error|global-error|not-found|loading|route|middleware|instrumentation|sitemap|robots|opengraph-image|icon|apple-icon|manifest)\.[jt]sx?$/;
33
+ /** `pages/…` na raiz ou em `src/` - o roteador por arquivo do Next antigo (e as telas de um app Vite). */
34
+ const PAGES_ROUTER = /^(src[/\\])?pages[/\\].+\.[jt]sx?$/;
33
35
  /** Nuxt, SvelteKit and Remix equivalents, for the same exact reason. */
34
36
  const RESERVED_FILE_OTHER = /(^|[/\\])(\+page|\+layout|\+server|_app|_document|_layout)\.[a-z]+$/;
35
37
  /**
@@ -229,6 +231,18 @@ export function gateComponent(input) {
229
231
  because: `\`${file}\` is the root the app mounts - the whole screen, not a component somebody can import`,
230
232
  };
231
233
  }
234
+ /**
235
+ * O PAGES ROUTER - bateria ampliada, c30 (29/09). No Next antigo cada arquivo de `pages/` É uma rota
236
+ * (`pages/index.tsx` é `/`), e o nome dele não é reservado: o census contava 0 páginas, e o import chamava
237
+ * `pages/` de "componentes ao lado da rota". Em um app Vite, `src/pages/Home.tsx` também é uma tela inteira.
238
+ */
239
+ if (PAGES_ROUTER.test(file)) {
240
+ return {
241
+ ok: false,
242
+ why: "route",
243
+ because: `\`${file}\` lives in pages/ - each file there is a route, not a component somebody can import`,
244
+ };
245
+ }
232
246
  if (RESERVED_FILE.test(file) || RESERVED_FILE_OTHER.test(file)) {
233
247
  return {
234
248
  ok: false,
@@ -348,6 +348,15 @@ internal = []) {
348
348
  return;
349
349
  const owners = importedNames(source, internal);
350
350
  const private_ = localOnlyNames(source);
351
+ /**
352
+ * `function App({ Component, pageProps })` - A TELA QUE CHEGA COMO PROP (bateria ampliada, c30, 29/09). O `_app.tsx`
353
+ * do pages router renderiza `<Component {...pageProps} />`, e `Component` entrava no sistema como um componente
354
+ * dele. Um nome com maiúscula que o arquivo recebe como parâmetro desestruturado é fiação, igual a um local privado.
355
+ */
356
+ for (const m of source.matchAll(/\(\s*\{([^}]*)\}\s*(?::[^)]*)?\)/g))
357
+ for (const part of m[1].split(","))
358
+ for (const id of /^\s*(?:[\w$]+\s*:\s*)?([A-Z][\w$]*)\b/.exec(part)?.slice(1) ?? [])
359
+ private_.add(id);
351
360
  /**
352
361
  * Abertura por regex, corpo por SCANNER - ver `bodyOf`. A regex só encontra `<Nome`; onde a
353
362
  * tag termina é uma pergunta que só um scanner com estado responde.
@@ -0,0 +1,26 @@
1
+ import { readFile } from "node:fs/promises";
2
+ export async function unreadComponentFiles(files) {
3
+ const count = { Vue: 0, Svelte: 0, Astro: 0, Angular: 0 };
4
+ for (const f of files) {
5
+ if (/\.vue$/i.test(f)) {
6
+ if (!/(^|\/)(pages|layouts)\//.test(f) && !/(^|\/)app\.vue$/i.test(f))
7
+ count.Vue += 1;
8
+ }
9
+ else if (/\.svelte$/i.test(f) &&
10
+ !/(^|\/)\+(page|layout|error)\.svelte$/.test(f))
11
+ count.Svelte += 1;
12
+ else if (/\.astro$/i.test(f) && !/(^|\/)pages\//.test(f))
13
+ count.Astro += 1;
14
+ else if (/\.ts$/i.test(f) && !/\.(spec|test)\.ts$/.test(f)) {
15
+ const src = await readFile(f, "utf8").catch(() => "");
16
+ const root = /(^|\/)app(\.component)?\.ts$/.test(f);
17
+ if (!root &&
18
+ /@Component\s*\(\s*\{/.test(src) &&
19
+ /from\s+["']@angular\/core["']/.test(src))
20
+ count.Angular += 1;
21
+ }
22
+ }
23
+ return Object.entries(count)
24
+ .filter(([, n]) => n > 0)
25
+ .map(([framework, n]) => ({ framework, files: n }));
26
+ }
@@ -12,6 +12,7 @@
12
12
  import { propertyAt, roleFits } from "./their-names.js";
13
13
  import { utilitiesOn } from "./idiom-names.js";
14
14
  import { normalizeValue, tokenMatch } from "./tokens.js";
15
+ import { THEME_SOURCE, tsTokenObjects } from "./ts-tokens-lens.js";
15
16
  /**
16
17
  * O NOME QUE SE ESCREVE NO ARQUIVO DELE - e ele é SEMPRE do vocabulário dele (`INV-GERAL-13`).
17
18
  *
@@ -488,7 +489,74 @@ export function scanSource(file, source, table) {
488
489
  : null),
489
490
  };
490
491
  }
491
- return scanCore(file, source, table);
492
+ return stretchedStack(file, source, themeDefinitionsAside(file, source, scanCore(file, source, table)));
493
+ }
494
+ /**
495
+ * A GRADE QUE DIVIDE A ALTURA DA TELA - bateria ampliada, item 17 (29/09).
496
+ *
497
+ * Em 11 dos 28 projetos com tela, o celular abria com meia tela vazia antes do conteúdo, e o agente dizia "works on
498
+ * mobile" nos 11. A causa é uma só, escrita pelo próprio agente na casca: `display: grid` com `min-height: 100dvh` e as
499
+ * colunas empilhando em `1fr` no celular. Sem `grid-template-rows`, as duas linhas automáticas dividem a altura - a da
500
+ * barra lateral cresce até a metade da tela. O conserto é uma linha (`grid-template-rows: auto 1fr`, ou
501
+ * `align-content: start`), e é dito aqui porque o agente não vê o celular.
502
+ */
503
+ function stretchedStack(file, source, report) {
504
+ if (!/\.(css|scss|sass|less|tsx|jsx)$/i.test(file))
505
+ return report;
506
+ const stacks = /grid-template-columns\s*:\s*1fr\s*[;}]|gridTemplateColumns\s*:[^,}\n]*["']1fr["']/.test(source);
507
+ if (!stacks)
508
+ return report;
509
+ const found = [];
510
+ const ROWS = /grid-template-rows|gridTemplateRows|align-content|alignContent|place-content|placeContent/;
511
+ /** O seletor que recebe a regra de linhas em QUALQUER bloco (o `@media` do c06 põe `grid-template-rows` ali). */
512
+ const settled = new Set();
513
+ const blocks = /([^{};]*)\{([^{}]*)\}/g;
514
+ for (const m of source.matchAll(blocks))
515
+ if (ROWS.test(m[2]))
516
+ settled.add(m[1].trim());
517
+ for (const m of source.matchAll(blocks)) {
518
+ const body = m[2];
519
+ const selector = m[1].trim();
520
+ const grid = /display\s*:\s*["']?grid\b/.test(body);
521
+ const tall = /min-?[hH]eight\s*:\s*["']?100(dvh|svh|lvh|vh)\b/.exec(body);
522
+ if (!grid || !tall || ROWS.test(body) || (selector.startsWith(".") && settled.has(selector)))
523
+ continue;
524
+ const at = source.slice(0, (m.index ?? 0) + m[1].length + 1 + body.indexOf(tall[0])).split("\n").length;
525
+ found.push({ line: at, literal: `min-height: 100${tall[1]}`, why: "stretch" });
526
+ }
527
+ if (found.length === 0)
528
+ return report;
529
+ return { ...report, offScale: [...(report.offScale ?? []), ...found] };
530
+ }
531
+ /**
532
+ * O ARQUIVO ONDE O TEMA NASCE NÃO É DERIVA - bateria ampliada, item 13 (29/09).
533
+ *
534
+ * A checagem do fim do turno dizia "11 hand-written values in src/theme.ts" (c03), 18 no do Emotion (c04), 13 no
535
+ * `theme.css.ts` (c05), 18 no `uno.config.ts` (c15) e 7 no `custom.scss` do Bootstrap (c17). São as DEFINIÇÕES
536
+ * dele: o único lugar onde um valor pode e deve estar escrito. O agente gastava o fim de cada turno explicando isso.
537
+ *
538
+ * Postos de lado com o motivo, como os de fixture: não acusados e não creditados. Num `.ts`/`.js` com objeto de tema
539
+ * (o mesmo leitor de `ts-tokens-lens`), o arquivo todo; num `.scss`, as linhas `$nome: valor`.
540
+ */
541
+ function themeDefinitionsAside(file, source, report) {
542
+ if (report.findings.length === 0)
543
+ return report;
544
+ let aside = [];
545
+ if (THEME_SOURCE.test(file) && !/\.(tsx|jsx)$/i.test(file) && tsTokenObjects(source).length > 0)
546
+ aside = report.findings;
547
+ else if (/\.(scss|sass)$/i.test(file)) {
548
+ const lines = source.split("\n");
549
+ // `$primary: #7c2d12;` - a definição simples. Um mapa (`$r: (radius: 6px, …)`) segue lido como antes.
550
+ aside = report.findings.filter((f) => /^\s*\$[\w-]+\s*:\s*[^(]*;?\s*$/.test(lines[f.line - 1] ?? ""));
551
+ }
552
+ if (aside.length === 0)
553
+ return report;
554
+ const kept = report.findings.filter((f) => !aside.includes(f));
555
+ return {
556
+ ...report,
557
+ findings: kept,
558
+ setAside: [...(report.setAside ?? []), { reason: "your theme is defined here - these are its definitions", count: aside.length }],
559
+ };
492
560
  }
493
561
  /**
494
562
  * OS SELETORES QUE DECLARAM VOCABULÁRIO - `@theme`, `:root` e a redefinição por tema (bateria, 28/09: b5 escreve
@@ -1072,7 +1140,9 @@ context = []) {
1072
1140
  }
1073
1141
  const MEDIA_WIDTH = /\b(?:(?:min|max)-)?width\s*(?::|[<>]=?)\s*([\d.]+)(px|rem|em)\b/g;
1074
1142
  const TOKEN_ARITHMETIC = /(?:\$\{\s*|:\s*)([A-Za-z_$][\w$]*(?:\.[\w$]+)*\.(?:[\w$]*(?:space|spacing|gap|radius|size|font)[\w$]*)(?:\.[\w$]+)*)\s*([+\-*/])\s*(\d+(?:\.\d+)?)\b/gi;
1075
- const BORDER_WIDTH = /\bborder(?:-(?:top|right|bottom|left))?(?:-width)?\s*:\s*(\d+(?:\.\d+)?px)\b/g;
1143
+ const BORDER_WIDTH = /\b(?:border(?:-(?:top|right|bottom|left|inline|block)(?:-(?:start|end))?)?|outline)(?:-width|-offset)?\s*:\s*(\d+(?:\.\d+)?px)\b/g;
1144
+ /** `100dvh`, `50vw` - uma medida da janela, que nenhum token guarda. */
1145
+ const VIEWPORT = /(?<![\w.-])(\d+(?:\.\d+)?(?:dvh|svh|lvh|vh|dvw|svw|lvw|vw))\b/g;
1076
1146
  const BORDER_NAME = /border|line|hairline|stroke|rule|divider|thin/i;
1077
1147
  /**
1078
1148
  * FORA DA ESCALA, linha a linha - ver `OffScale`. `setAside` recebe o que foi medido e está certo: o ponto de quebra
@@ -1105,7 +1175,14 @@ function offScaleOf(file, line, table, setAside) {
1105
1175
  const theirs = [table.aliases.get(`spacing:${v}`), table.aliases.get(`radius:${v}`)].find((a) => a && BORDER_NAME.test(a.name));
1106
1176
  if (theirs)
1107
1177
  out.push({ literal: m[1], why: "named", nearest: `var(${theirs.name})` });
1178
+ /**
1179
+ * SEM NOME DELE, PASSA - E É DITO (a régua de 28/09, "a checagem diz isso uma vez"; bateria ampliada, item 14). O
1180
+ * fim do turno do c06 dizia "nothing to name" com 1px e 2px nos arquivos, e o agente os achava à mão.
1181
+ */ else if (/^[12]px$/.test(v))
1182
+ setAside(`a ${v} border or outline - nothing in this project names a border width, so it passes`);
1108
1183
  }
1184
+ for (const m of line.matchAll(VIEWPORT))
1185
+ setAside(`${m[1]} - a size of the window, which no token holds`);
1109
1186
  return out;
1110
1187
  }
1111
1188
  /** As linhas que o `check_file` e o fim do turno dizem sobre `OffScale` - uma frase por caso, a mesma nos dois. */
@@ -1114,6 +1191,8 @@ export function describeOffScale(files) {
1114
1191
  ? ` ${f.file}:${o.line} ${o.literal} - a breakpoint off your system's widths; the nearest is ${o.nearest} (a media query cannot use a token, so match the width)`
1115
1192
  : o.why === "arithmetic"
1116
1193
  ? ` ${f.file}:${o.line} ${o.literal} - arithmetic on a token lands between the steps; use a step the system has, or ask for one`
1117
- : ` ${f.file}:${o.line} ${o.literal} - this repo calls it ${o.nearest}`));
1194
+ : o.why === "stretch"
1195
+ ? ` ${f.file}:${o.line} ${o.literal} on a grid that stacks to one column - on a phone its rows split the screen's height, so the bar above the content grows to half the screen; add grid-template-rows: auto 1fr (or align-content: start)`
1196
+ : ` ${f.file}:${o.line} ${o.literal} - this repo calls it ${o.nearest}`));
1118
1197
  return rows.length > 0 ? ["", "Off this system's scale:", ...rows] : [];
1119
1198
  }