synthesisui 0.16.162 → 0.16.163

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.
@@ -129,13 +129,21 @@ export async function add(slug, opts) {
129
129
  const leftOut = payload.ruleSet
130
130
  ? describeFiltered(payload.ruleSet, stack)
131
131
  : null;
132
+ /**
133
+ * AS REGRAS VIAJAM COMO DADO, e não como prosa materializada.
134
+ *
135
+ * `rules.md` e `philosophy.md` eram escritos SÓ pelo `add` (e o `upgrade` chama o `add`), então
136
+ * entre um upgrade e outro eles eram uma cópia velha de um dado que é VERSIONADO do nosso lado - e
137
+ * o GUIDE chamava um deles de "maximum authority". Uma autoridade máxima que envelhece em silêncio
138
+ * é pior que nenhuma (dono, 06/08).
139
+ *
140
+ * O conteúdo não sai do disco, porque o agente precisa dele offline e sem aprovar servidor nenhum.
141
+ * O que muda é a FORMA e quem o apresenta: aqui fica o dado pinado na versão instalada, e o
142
+ * `system_doctrine` do MCP é quem o serve - dizendo qual versão ele está lendo e se existe uma mais
143
+ * nova, em vez de deixar a pessoa descobrir pela ausência.
144
+ */
132
145
  if (rules.length > 0) {
133
- const body = `# ${payload.name} - Rules\n\n` +
134
- "> Accumulated rules for this design system. **Max authority - follow these first.**\n" +
135
- `> Managed by synthesisui (edit in the studio). ${rules.length} rule(s).\n\n${rules
136
- .map((r) => `- ${r}`)
137
- .join("\n")}\n`;
138
- await writeFile(join(slugDir, "rules.md"), body, "utf8");
146
+ await writeFile(join(slugDir, "doctrine.json"), `${JSON.stringify({ version: payload.version, name: payload.name, rules }, null, 2)}\n`, "utf8");
139
147
  }
140
148
  // NO SILENT DROPS. A rule removed because it belongs to another environment is
141
149
  // a thing the person should hear once, not discover by its absence.
@@ -163,22 +171,17 @@ export async function add(slug, opts) {
163
171
  if (required.length > 0) {
164
172
  await writeFile(join(slugDir, "requires.json"), `${JSON.stringify(required, null, 2)}\n`, "utf8");
165
173
  }
166
- // 5c. structured philosophy (personal DS) → philosophy.md at the slug root.
167
- // Narrative guidance (mission, principles, voice, motion doctrine…); the
168
- // GUIDE points the agent here right after rules.md.
174
+ /** A filosofia continua sendo CONTADA na saída, e ela vive no documento - ver abaixo. */
169
175
  const philosophy = payload.document.philosophy;
170
176
  const sections = philosophy?.sections ?? [];
171
- if (sections.length > 0 || philosophy?.context) {
172
- const parts = [`# ${payload.name} - Philosophy`, ""];
173
- parts.push("> The voice and principles behind this system. Read after rules.md;", "> let it shape every screen. Managed by synthesisui (edit in the studio).", "");
174
- if (philosophy?.context) {
175
- parts.push("## What this product is", "", philosophy.context, "");
176
- }
177
- for (const s of sections) {
178
- parts.push(`## ${s.title}`, "", s.body, "");
179
- }
180
- await writeFile(join(slugDir, "philosophy.md"), `${parts.join("\n")}\n`, "utf8");
181
- }
177
+ /**
178
+ * 5c. A FILOSOFIA NÃO É MATERIALIZADA - ela já está aqui.
179
+ *
180
+ * `philosophy` é campo do DOCUMENTO versionado (`design-system.ts`), e o `design-system.json`
181
+ * escrito no passo 2 a carrega inteira. O `philosophy.md` era uma segunda cópia do mesmo dado, no
182
+ * mesmo commit, que só era reescrita no upgrade seguinte - dois caminhos para uma verdade é como
183
+ * um deles começa a discordar do outro. O `system_doctrine` lê do documento.
184
+ */
182
185
  // 6. discovery by the agent
183
186
  const claudeMd = await syncClaudeMd(projectRoot);
184
187
  // outcome line
@@ -201,12 +204,18 @@ export async function add(slug, opts) {
201
204
  "GUIDE.md",
202
205
  ];
203
206
  console.log(` v${v}/: ${files.join(", ")}`);
204
- if (rules.length > 0) {
205
- console.log(` rules.md → ${rules.length} rule(s) (read these first)`);
206
- }
207
- if (sections.length > 0 || philosophy?.context) {
208
- console.log(` philosophy.md → ${sections.length} section(s) (read after rules)`);
209
- }
207
+ /**
208
+ * O QUE O AGENTE VAI LER, e por qual porta - nomear o arquivo quando a porta é uma ferramenta
209
+ * mandaria alguém procurar um caminho que não existe mais.
210
+ */
211
+ const doctrine = [
212
+ ...(rules.length > 0 ? [`${rules.length} rule(s)`] : []),
213
+ ...(sections.length > 0 || philosophy?.context
214
+ ? [`${sections.length} section(s) of philosophy`]
215
+ : []),
216
+ ];
217
+ if (doctrine.length > 0)
218
+ console.log(` ${doctrine.join(" and ")} → your agent reads them with \`system_doctrine\`, pinned to v${v}`);
210
219
  console.log(` CLAUDE.md ${claudeMd.created ? "created" : "updated"} (${claudeMd.count} system(s) installed)`);
211
220
  if (opts.setupHints === false)
212
221
  return;
@@ -1,6 +1,7 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
+ import { readToken, resolveRegistry } from "../config.js";
4
5
  import { readEvents } from "../doctor/ledger.js";
5
6
  import { measuredScope } from "../measured-scope.js";
6
7
  async function locksIn(root) {
@@ -100,6 +101,38 @@ opts = {}) {
100
101
  });
101
102
  return out;
102
103
  }
104
+ /**
105
+ * A VERSÃO PUBLICADA CONTRA A INSTALADA - o estado que nenhuma verificação cobria.
106
+ *
107
+ * É o que faz disto um produto de TIME e não de uma pessoa: alguém edita o sistema no estúdio e
108
+ * publica, e o repo de todo mundo continua com o CSS e as regras da versão anterior, sem nada dizer.
109
+ * A doutrina que o agente lê é a da versão instalada de propósito (ver `system_doctrine`), então as
110
+ * duas metades andam juntas - e é justamente por isso que a defasagem precisa ser DITA.
111
+ *
112
+ * Melhor esforço: offline, sem sessão, ou registry fora do ar não produzem linha nenhuma. Uma
113
+ * verificação de abertura não pode transformar um voo sem internet num aviso.
114
+ */
115
+ export async function versionBehind(root, opts = {}) {
116
+ const locks = (await locksIn(root)).filter((l) => l.slug && !l.adopted);
117
+ const lock = locks[0];
118
+ if (!lock?.slug || lock.version == null)
119
+ return null;
120
+ if (!(await credentials(opts.home ?? homedir())))
121
+ return null;
122
+ const token = await readToken();
123
+ if (!token)
124
+ return null;
125
+ const res = await fetch(`${lock.registry ?? resolveRegistry()}/api/registry/ds/${lock.slug}`, { headers: { Authorization: `Bearer ${token}` } }).catch(() => null);
126
+ if (!res?.ok)
127
+ return null;
128
+ const body = (await res.json().catch(() => null));
129
+ if (!body?.version || body.version <= lock.version)
130
+ return null;
131
+ return {
132
+ says: `v${body.version} of "${lock.slug}" is published and this repo is on v${lock.version}. The CSS here and the rules your agent reads are both v${lock.version} - they move together, which is why this is worth saying rather than applying.`,
133
+ run: `npx synthesisui upgrade ${lock.slug}`,
134
+ };
135
+ }
103
136
  /**
104
137
  * A linha que a sessão vê.
105
138
  *
@@ -127,6 +160,10 @@ export async function align(opts) {
127
160
  const items = await localMisalignments(root, {
128
161
  ...(opts.cli ? { cli: opts.cli } : {}),
129
162
  }).catch(() => []);
163
+ /** A única linha que custa rede, e ela some inteira quando não há rede. */
164
+ const remote = await versionBehind(root).catch(() => null);
165
+ if (remote)
166
+ items.push(remote);
130
167
  const text = describeMisalignments(items);
131
168
  if (text)
132
169
  console.log(text);
@@ -144,6 +144,7 @@ export async function loadSystem(root) {
144
144
  recipes: new Map(),
145
145
  documents: [],
146
146
  requires: [],
147
+ doctrines: [],
147
148
  };
148
149
  }
149
150
  // Several systems can live side by side; every token is prefixed --ds-, so
@@ -154,6 +155,7 @@ export async function loadSystem(root) {
154
155
  const recipes = new Map();
155
156
  const documents = [];
156
157
  const requires = [];
158
+ const doctrines = [];
157
159
  for (const slug of slugs) {
158
160
  const dir = join(dsDir, slug);
159
161
  const raw = await readFile(join(dir, ".lock"), "utf8").catch(() => "");
@@ -226,12 +228,31 @@ export async function loadSystem(root) {
226
228
  // Unreadable costs this check, not the run.
227
229
  }
228
230
  }
231
+ /** Ausente é o caso normal: um sistema sem regra nenhuma não deixa arquivo. */
232
+ const doctrineRaw = await readFile(join(dir, "doctrine.json"), "utf8").catch(() => "");
233
+ if (doctrineRaw) {
234
+ try {
235
+ const parsed = JSON.parse(doctrineRaw);
236
+ if (Array.isArray(parsed.rules))
237
+ doctrines.push({
238
+ ...(typeof parsed.version === "number"
239
+ ? { version: parsed.version }
240
+ : {}),
241
+ ...(typeof parsed.name === "string" ? { name: parsed.name } : {}),
242
+ rules: parsed.rules.filter((r) => typeof r === "string"),
243
+ });
244
+ }
245
+ catch {
246
+ // Ilegível custa a doutrina, nunca a rodada.
247
+ }
248
+ }
229
249
  }
230
250
  return {
231
251
  table: buildTable({ css, lock, source: adopted ? "adopted" : "installed" }),
232
252
  recipes,
233
253
  documents,
234
254
  requires,
255
+ doctrines,
235
256
  };
236
257
  }
237
258
  /**
@@ -70,6 +70,11 @@ const TOOLS = [
70
70
  required: ["value"],
71
71
  },
72
72
  },
73
+ {
74
+ name: "system_doctrine",
75
+ description: "The rules and the philosophy of the installed system - maximum authority, read this BEFORE building anything. It answers from the version this project has pinned, which is the same version whose tokens are on disk, so the rules you follow and the values you write come from one place. It also says when a newer version has been published, without applying it.",
76
+ inputSchema: { type: "object", properties: {} },
77
+ },
73
78
  {
74
79
  name: "list_components",
75
80
  description: "The components this design system defines, with what each is for. Look here BEFORE writing any UI element from scratch - if something covers the purpose, materialize it with add_component instead.",
@@ -252,6 +257,73 @@ async function findToken(root, value) {
252
257
  // same refusal every time.
253
258
  return `No token in this system holds ${value}, and nothing is close. Do NOT invent one. Say which value you need and what you would call it, and let a person decide.`;
254
259
  }
260
+ /**
261
+ * A DOUTRINA DO SISTEMA - as regras e a filosofia, servidas em vez de materializadas.
262
+ *
263
+ * `rules.md` e `philosophy.md` eram escritos SÓ pelo `add` (e o `upgrade` chama o `add`), então entre
264
+ * um upgrade e outro eram uma cópia velha de dado que é VERSIONADO do nosso lado - e o GUIDE chamava
265
+ * um deles de "maximum authority". Uma autoridade máxima que envelhece em silêncio é pior que
266
+ * nenhuma (dono, 06/08).
267
+ *
268
+ * DA VERSÃO PINADA, e essa é a decisão que mantém as duas metades coerentes: os tokens no disco são
269
+ * os da v2, então a regra que o agente segue tem que ser a da v2. Servir o rascunho faria o agente
270
+ * escrever contra uma regra que ainda não está instalada.
271
+ *
272
+ * A existência de uma versão mais nova é DITA, nunca aplicada - aplicar é o `upgrade`, que é uma
273
+ * palavra que a pessoa já tem. Sem rede, a doutrina responde igual e a checagem é que se cala: o
274
+ * dado é local justamente para não depender de servidor nenhum.
275
+ */
276
+ async function systemDoctrine(root) {
277
+ const { documents, doctrines } = await loadSystem(root);
278
+ const rules = doctrines.flatMap((d) => d.rules);
279
+ const parts = [];
280
+ const pinned = doctrines[0]?.version;
281
+ for (const doc of documents) {
282
+ const p = doc.philosophy;
283
+ if (p?.context)
284
+ parts.push(`## What this product is\n\n${p.context}`);
285
+ for (const sec of p?.sections ?? [])
286
+ parts.push(`## ${sec.title}\n\n${sec.body}`);
287
+ }
288
+ if (rules.length === 0 && parts.length === 0)
289
+ return "This system carries no rules and no philosophy yet. Nothing here overrides your judgement - build with its tokens and its components, and say what you needed and could not find.";
290
+ const head = [
291
+ rules.length > 0
292
+ ? `# Rules - maximum authority, ${rules.length} of them${pinned ? `, as of v${pinned}` : ""}`
293
+ : "",
294
+ ...rules.map((r) => `- ${r}`),
295
+ ].filter(Boolean);
296
+ const behind = await newerVersion(root, pinned);
297
+ return [
298
+ ...head,
299
+ ...(parts.length > 0 ? ["", "# Philosophy", "", ...parts] : []),
300
+ ...(behind ? ["", behind] : []),
301
+ ].join("\n");
302
+ }
303
+ /**
304
+ * Existe versão publicada acima da instalada?
305
+ *
306
+ * Melhor esforço e nunca bloqueante: offline, sem sessão, ou registry fora do ar devolvem `null` e a
307
+ * doutrina responde igual. Dizer "não consegui checar" a cada chamada seria ruído numa ferramenta que
308
+ * o agente chama antes de cada trabalho.
309
+ */
310
+ async function newerVersion(root, pinned) {
311
+ if (pinned == null)
312
+ return null;
313
+ const slug = await installedSlug(root);
314
+ const token = await readToken();
315
+ if (!slug || !token)
316
+ return null;
317
+ const res = await fetch(`${resolveRegistry()}/api/registry/ds/${slug}`, {
318
+ headers: { Authorization: `Bearer ${token}` },
319
+ }).catch(() => null);
320
+ if (!res?.ok)
321
+ return null;
322
+ const body = (await res.json().catch(() => null));
323
+ if (!body?.version || body.version <= pinned)
324
+ return null;
325
+ return `> v${body.version} of this system has been published and this project is on v${pinned}. These rules are v${pinned}'s, and so are the tokens on disk - the two move together. \`npx synthesisui upgrade ${slug}\` brings both.`;
326
+ }
255
327
  async function listComponents(root) {
256
328
  const { documents } = await loadSystem(root);
257
329
  const rows = [];
@@ -839,6 +911,8 @@ cli) {
839
911
  return text(await checkFile(root, String(args.path ?? ".")));
840
912
  case "find_token":
841
913
  return text(await findToken(root, String(args.value ?? "")));
914
+ case "system_doctrine":
915
+ return text(await systemDoctrine(root));
842
916
  case "list_components":
843
917
  return text(await listComponents(root));
844
918
  case "describe_component":
@@ -43,15 +43,17 @@ export async function use(slug, intent, opts) {
43
43
  const config = await readProjectConfig(root);
44
44
  const { version, name } = lock;
45
45
  const base = `_synthesisui/ds/${slug}`;
46
- // Highest-authority guidance first; only list files that actually exist.
47
- const hasRules = await exists(join(slugDir, "rules.md"));
48
- const hasPhilosophy = await exists(join(slugDir, "philosophy.md"));
46
+ /**
47
+ * A AUTORIDADE MÁXIMA VEM DE UMA FERRAMENTA, e o resto continua sendo caminho.
48
+ *
49
+ * `rules.md` e `philosophy.md` eram cópias velhas de dado versionado entre um upgrade e outro -
50
+ * ver `system_doctrine`. Um prompt que manda alguém abrir um arquivo que não existe mais é pior
51
+ * que um prompt sem a linha, então a condição passou a ser o DADO e não o arquivo.
52
+ */
53
+ const hasDoctrine = await exists(join(slugDir, "doctrine.json"));
49
54
  const readFirst = [
50
- hasRules
51
- ? `- ${base}/rules.md — project rules for this system; obey them above everything else`
52
- : "",
53
- hasPhilosophy
54
- ? `- ${base}/philosophy.md — the mission, voice and principles; let it shape every screen`
55
+ hasDoctrine
56
+ ? "- Call `system_doctrine` first — this system's rules and philosophy, pinned to the installed version; obey them above everything else"
55
57
  : "",
56
58
  `- ${base}/v${version}/GUIDE.md — how to apply the system, the recipes and the token vocabulary`,
57
59
  ].filter(Boolean);
package/dist/guide.js CHANGED
@@ -426,14 +426,21 @@ own entry below says so.
426
426
  const hasRules = (payload.rules?.length ?? 0) > 0;
427
427
  const philosophy = payload.document.philosophy;
428
428
  const hasPhilosophy = (philosophy?.sections?.length ?? 0) > 0 || !!philosophy?.context;
429
- const readFirst = [
430
- hasRules
431
- ? `**Read \`_synthesisui/ds/${slug}/rules.md\` FIRST and obey it above everything else** - it carries this system's accumulated, project-specific rules; on any conflict they win.`
432
- : "",
433
- hasPhilosophy
434
- ? `**${hasRules ? "Then read" : "Read"} \`_synthesisui/ds/${slug}/philosophy.md\`** - the mission, principles, voice and motion doctrine. Let it shape every screen you build.`
435
- : "",
436
- ].filter(Boolean);
429
+ /**
430
+ * UMA PORTA SÓ, e ela é uma FERRAMENTA e não um caminho.
431
+ *
432
+ * Isto apontava para `rules.md` e `philosophy.md`, escritos apenas pelo `add` - então entre um
433
+ * upgrade e outro o agente lia uma cópia velha de dado versionado, sob a etiqueta "highest
434
+ * authority". `system_doctrine` serve os dois da versão PINADA (a mesma dos tokens no disco) e diz
435
+ * quando existe uma mais nova, em vez de deixar a defasagem ser descoberta pela ausência.
436
+ */
437
+ const readFirst = hasRules || hasPhilosophy
438
+ ? [
439
+ `**Call \`system_doctrine\` FIRST and obey it above everything else** - it carries ${hasRules
440
+ ? "this system's accumulated, project-specific rules"
441
+ : "this system's mission, principles, voice and motion doctrine"}${hasRules && hasPhilosophy ? " and its philosophy" : ""}, pinned to the version installed here. On any conflict they win.`,
442
+ ]
443
+ : [];
437
444
  const rulesNote = readFirst.length > 0
438
445
  ? `
439
446
  ## Read first - highest authority
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.162",
3
+ "version": "0.16.163",
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": {