synthesisui 0.16.417 → 0.16.418

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.
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { agentsToMaintain } from "../agents-chosen.js";
4
4
  import { syncClaudeMd } from "../claude-md.js";
5
5
  import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
6
+ import { parseRules } from "../doctrine.js";
6
7
  import { customFontFamilies, googleFontsHref, nextFontSnippet, } from "../fonts.js";
7
8
  import { detectAppDirs, globalSheetOf, prefixFrom } from "../global-sheet.js";
8
9
  import { lockReference } from "../group-role.js";
@@ -375,9 +376,17 @@ export async function add(slug, opts) {
375
376
  * registry working unchanged.
376
377
  */
377
378
  const stack = await detectStack(projectRoot);
379
+ /**
380
+ * AS DUAS ENTRADAS VIRAM A MESMA FORMA ANTES DE TOCAR O DISCO.
381
+ *
382
+ * `ruleSet` vem estruturado e `rules` vem como frase - e enquanto a segunda caía direto no
383
+ * arquivo, o disco dele podia receber uma lista mista sem nada reclamar. `parseRules` normaliza
384
+ * as duas para `{ text, applies }`, e desfaz o prefixo `ds-a + ds-b:` da forma antiga no
385
+ * caminho, então um registry que ainda manda frase não perde o alcance da regra.
386
+ */
378
387
  const rules = payload.ruleSet && payload.ruleSet.length > 0
379
388
  ? rulesForProject(payload.ruleSet, stack)
380
- : (payload.rules ?? []);
389
+ : parseRules(payload.rules ?? []);
381
390
  const leftOut = payload.ruleSet
382
391
  ? describeFiltered(payload.ruleSet, stack)
383
392
  : null;
@@ -16,6 +16,7 @@ import { diagnose, nameToWrite, scanSource, siblingTokens, } from "../doctor/sca
16
16
  import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from "../doctor/self-conflict.js";
17
17
  import { withTheirNames } from "../doctor/their-names.js";
18
18
  import { buildTable, EMPTY_TABLE, nearestToken, } from "../doctor/tokens.js";
19
+ import { parseRules } from "../doctrine.js";
19
20
  /**
20
21
  * `detectAppDirs` SAIU DESTE IMPORT em 09/09: quem pergunta "quais pastas são app?" agora é
21
22
  * `readWiringPerApp`, que responde a fiação de cada uma. O comando não escolhe mais um app.
@@ -320,13 +321,23 @@ measured) {
320
321
  if (doctrineRaw) {
321
322
  try {
322
323
  const parsed = JSON.parse(doctrineRaw);
323
- if (Array.isArray(parsed.rules))
324
+ /**
325
+ * AS DUAS FORMAS DO DISCO, lidas pela mesma porta (`parseRules`).
326
+ *
327
+ * Este filtro era `typeof r === "string"`, e com a regra passando a viajar como dado ele
328
+ * DESCARTARIA calado toda regra de uma instalação nova - o pior resultado possível para
329
+ * uma doutrina, que é sumir sem ninguém saber. E a leitura pela porta única também
330
+ * devolve o alcance às instalações ANTIGAS: o prefixo `ds-a + ds-b:` é desfeito ali, sem
331
+ * reimportar nada.
332
+ */
333
+ const parsedRules = parseRules(parsed.rules);
334
+ if (parsedRules.length > 0)
324
335
  doctrines.push({
325
336
  ...(typeof parsed.version === "number"
326
337
  ? { version: parsed.version }
327
338
  : {}),
328
339
  ...(typeof parsed.name === "string" ? { name: parsed.name } : {}),
329
- rules: parsed.rules.filter((r) => typeof r === "string"),
340
+ rules: parsedRules,
330
341
  });
331
342
  }
332
343
  catch {
@@ -12,6 +12,7 @@ import { readEvents } from "../doctor/ledger.js";
12
12
  import { fileRequest } from "../doctor/requests.js";
13
13
  import { diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
14
14
  import { nearestToken, normalizeValue, tokenFor } from "../doctor/tokens.js";
15
+ import { ruleLine } from "../doctrine.js";
15
16
  import { fromCensus } from "../memory/observation.js";
16
17
  import { handleRecall, handleRemember, MEMORY_TOOLS } from "../memory/tools.js";
17
18
  import { readInstalledConvention } from "../project-facts.js";
@@ -523,7 +524,13 @@ async function systemDoctrine(root) {
523
524
  rules.length > 0
524
525
  ? `# Rules - maximum authority, ${rules.length} of them${pinned ? `, as of v${pinned}` : ""}`
525
526
  : "",
526
- ...rules.map((r) => `- ${r}`),
527
+ /**
528
+ * `ruleLine` É O QUE MANTÉM ESTA SAÍDA IDÊNTICA. A regra passou a viajar como dado
529
+ * (`{ text, applies }`), e interpolar o objeto aqui imprimiria `[object Object]` sem o `tsc`
530
+ * reclamar - um template literal aceita qualquer coisa. O alcance volta a ser prefixo na hora
531
+ * de MOSTRAR, que é onde ele sempre esteve para quem lê.
532
+ */
533
+ ...rules.map((r) => `- ${ruleLine(r)}`),
527
534
  ].filter(Boolean);
528
535
  const pulse = await sessionPulse(root, pinned);
529
536
  return [
@@ -1706,7 +1713,7 @@ cli) {
1706
1713
  const plan = composePlan({
1707
1714
  documents,
1708
1715
  table,
1709
- rules: doctrines.flatMap((d) => d.rules),
1716
+ rules: doctrines.flatMap((d) => d.rules).map(ruleLine),
1710
1717
  /**
1711
1718
  * A regra tem a FRASE e o PACOTE em campos separados (`text`,
1712
1719
  * `requires`) - mandar a frase sozinha esconderia o que instalar, e
@@ -0,0 +1,96 @@
1
+ /**
2
+ * A REGRA DO SISTEMA DELE, COMO DADO - e não como frase.
3
+ *
4
+ * ═══ O QUE ABRIU ESTE ARQUIVO, medido em 12/09 ═══
5
+ *
6
+ * O agente dele apagou a serif itálica de seis lugares da landing do `codelevel`, e nada avisou.
7
+ * A regra existia, instalada, em disco, no `doctrine.json` daquele projeto - a décima de vinte e
8
+ * cinco, verbatim:
9
+ *
10
+ * "A display headline breaks onto a line set in the serif, italic - that line carries the
11
+ * emphasis of the sentence, never the whole headline."
12
+ *
13
+ * O conhecimento estava a dois diretórios de distância da edição e ninguém o entregou. Palavras
14
+ * dele: *"o sistema fala antes do dano"*.
15
+ *
16
+ * ═══ A CAUSA, e ela é de uma linha ═══
17
+ *
18
+ * Do NOSSO lado a regra é estruturada: `applies` diz quais componentes ela governa, e
19
+ * `rules-for-component.ts` casa por esse campo há semanas. No caminho até o disco dele,
20
+ * `rulesForProject` achatava a estrutura num PREFIXO DE PROSA - `ds-button + ds-icon-button: <o
21
+ * texto>` - e a partir dali nada mais podia perguntar *"esta regra vale para o arquivo que está
22
+ * sendo escrito?"*. A resposta existia e era jogada fora na exportação.
23
+ *
24
+ * ═══ POR QUE UM MÓDULO, e não um `JSON.parse` em cada leitor ═══
25
+ *
26
+ * O `doctrine.json` é lido em três lugares - o doctor, o `system_doctrine` do MCP e o compositor
27
+ * de plano -, e a partir de agora existem DUAS formas no disco do mundo: a nova, estruturada, e a
28
+ * antiga, em string, que está instalada em todo projeto que rodou `connect` antes de hoje. Três
29
+ * leitores decidindo sozinhos o que fazer com duas formas são três chances de discordar sobre a
30
+ * mesma regra.
31
+ *
32
+ * ═══ E A FORMA ANTIGA NÃO PERDE O ALCANCE ═══
33
+ *
34
+ * O prefixo era um achatamento REVERSÍVEL: `ds-card + ds-noise-overlay: <texto>` carrega os dois
35
+ * nomes, só que em prosa. `parseRule` desfaz isso na leitura, então uma instalação antiga ganha o
36
+ * alcance de volta sem reimportar nada - que é a diferença entre uma correção que alcança quem já
37
+ * é cliente e uma que só alcança quem instalar amanhã.
38
+ */
39
+ /**
40
+ * O PREFIXO QUE A FORMA ANTIGA USA: `ds-card + ds-noise-overlay: `.
41
+ *
42
+ * Ancorado no começo e exigindo o `ds-` em cada nome, porque uma frase pode conter dois pontos -
43
+ * *"Dark is a class, never the OS preference: @custom-variant dark"* é a segunda regra real do
44
+ * sistema dele, e sem a âncora ela viraria um componente chamado `Dark is a class, never the OS
45
+ * preference`.
46
+ */
47
+ const LEGACY_SCOPE = /^((?:ds-[a-z0-9-]+)(?:\s*\+\s*ds-[a-z0-9-]+)*)\s*:\s*/;
48
+ /** Uma regra vinda do disco, em qualquer das duas formas - ou `null` quando não há frase. */
49
+ export function parseRule(raw) {
50
+ if (typeof raw === "string") {
51
+ const text = raw.trim();
52
+ if (!text)
53
+ return null;
54
+ const m = LEGACY_SCOPE.exec(text);
55
+ if (!m)
56
+ return { text, applies: [] };
57
+ return {
58
+ text: text.slice(m[0].length).trim(),
59
+ applies: m[1].split("+").map((n) => n.trim().replace(/^ds-/, "")),
60
+ };
61
+ }
62
+ if (!raw || typeof raw !== "object")
63
+ return null;
64
+ const r = raw;
65
+ const text = typeof r.text === "string" ? r.text.trim() : "";
66
+ if (!text)
67
+ return null;
68
+ const applies = Array.isArray(r.applies)
69
+ ? r.applies.filter((a) => typeof a === "string" && !!a.trim())
70
+ : [];
71
+ return { text, applies: applies.map((a) => a.trim().replace(/^ds-/, "")) };
72
+ }
73
+ /** Todas as regras de um `doctrine.json`, nas duas formas, sem as ilegíveis. */
74
+ export function parseRules(raw) {
75
+ if (!Array.isArray(raw))
76
+ return [];
77
+ const out = [];
78
+ for (const entry of raw) {
79
+ const rule = parseRule(entry);
80
+ if (rule)
81
+ out.push(rule);
82
+ }
83
+ return out;
84
+ }
85
+ /**
86
+ * A REGRA COMO ALGUÉM A LÊ - a mesma frase que o disco carregava antes.
87
+ *
88
+ * O alcance volta a ser prefixo AQUI, na hora de mostrar, e não no dado. Quem imprime continua
89
+ * imprimindo o que sempre imprimiu; quem PERGUNTA passa a ter o campo.
90
+ */
91
+ export function ruleLine(rule) {
92
+ const scope = rule.applies.length > 0
93
+ ? `${rule.applies.map((a) => `ds-${a}`).join(" + ")}: `
94
+ : "";
95
+ return `${scope}${rule.text}`;
96
+ }
@@ -1,18 +1,3 @@
1
- /**
2
- * WHICH RULES BELONG IN *THIS* PROJECT.
3
- *
4
- * A rule travels with the design system, and the system gets installed places it
5
- * was never written for. "The state arrives through this hook" is true about Next
6
- * and actively wrong about Vue - so a rule carries the environments it holds in,
7
- * and the filtering happens HERE, because the consumer's stack is the one thing
8
- * the platform cannot know (dono, 01/08).
9
- *
10
- * THE DEFAULT WHEN WE CANNOT TELL IS TO KEEP IT. A rule whose condition does not
11
- * match is removed, because obeying it would be a mistake. A rule we cannot
12
- * evaluate - the stack came back empty, or it names something we do not detect -
13
- * stays, because a rule nobody can judge is safer present than silently gone.
14
- * Those are two different situations and they get two different answers.
15
- */
16
1
  const norm = (v) => v.trim().toLowerCase();
17
2
  /**
18
3
  * True when this rule holds in a project built with `stack`.
@@ -45,10 +30,18 @@ export function rulesForProject(rules, stack) {
45
30
  const text = rule.text?.trim();
46
31
  if (!text)
47
32
  continue;
48
- const scope = rule.applies?.length > 0
49
- ? `${rule.applies.map((a) => `ds-${a}`).join(" + ")}: `
50
- : "";
51
- out.push(`${scope}${text}`);
33
+ /**
34
+ * O ALCANCE SAI DAQUI COMO CAMPO, e não mais como prefixo de prosa.
35
+ *
36
+ * Esta função achatava `applies` em `ds-a + ds-b: <texto>` e era o último ponto onde a
37
+ * estrutura existia: do disco em diante, nada mais podia perguntar se a regra vale para o
38
+ * arquivo que está sendo escrito. Foi assim que a regra da serif do `codelevel` - a décima
39
+ * de vinte e cinco, instalada, correta - não disse nada quando um agente a apagou de seis
40
+ * lugares (12/09).
41
+ *
42
+ * A frase não mudou: `ruleLine` volta a montar o prefixo na hora de MOSTRAR.
43
+ */
44
+ out.push({ text, applies: (rule.applies ?? []).map((a) => a.trim()) });
52
45
  }
53
46
  return out;
54
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.417",
3
+ "version": "0.16.418",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {