synthesisui 0.16.234 → 0.16.238

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.
@@ -250,15 +250,13 @@ export async function add(slug, opts) {
250
250
  await writeFile(join(slugDir, "requires.json"), `${JSON.stringify(required, null, 2)}\n`, "utf8");
251
251
  }
252
252
  /** A filosofia continua sendo CONTADA na saída, e ela vive no documento - ver abaixo. */
253
- const philosophy = payload.document.philosophy;
254
- const sections = philosophy?.sections ?? [];
253
+ const voice = payload.voice ?? { hasContext: false, sections: 0 };
255
254
  /**
256
- * 5c. A FILOSOFIA NÃO É MATERIALIZADA - ela já está aqui.
257
- *
258
- * `philosophy` é campo do DOCUMENTO versionado (`design-system.ts`), e o `design-system.json`
259
- * escrito no passo 2 a carrega inteira. O `philosophy.md` era uma segunda cópia do mesmo dado, no
260
- * mesmo commit, que só era reescrita no upgrade seguinte - dois caminhos para uma verdade é como
261
- * um deles começa a discordar do outro. O `system_doctrine` lê do documento.
255
+ * 5c. A VOZ NÃO É MATERIALIZADA - nem no `.md` (aposentado acima) nem no
256
+ * `design-system.json` (R2, 16/08: o payload viaja sem `philosophy`). Ela
257
+ * mora na plataforma e o `system_doctrine` a busca servida - sempre a
258
+ * versão atual, nada em texto plano no repo. As REGRAS continuam locais
259
+ * (`doctrine.json`): o doctor e o hook precisam delas offline.
262
260
  */
263
261
  // 6. discovery by the agent
264
262
  const claudeMd = await syncClaudeMd(projectRoot);
@@ -288,8 +286,8 @@ export async function add(slug, opts) {
288
286
  */
289
287
  const doctrine = [
290
288
  ...(rules.length > 0 ? [`${rules.length} rule(s)`] : []),
291
- ...(sections.length > 0 || philosophy?.context
292
- ? [`${sections.length} section(s) of philosophy`]
289
+ ...(voice.sections > 0 || voice.hasContext
290
+ ? [`the voice (${voice.sections} section(s), served from the platform)`]
293
291
  : []),
294
292
  ];
295
293
  if (doctrine.length > 0)
@@ -59,14 +59,21 @@ export async function component(slug, name, opts) {
59
59
  if (!SAFE_NAME.test(res.name)) {
60
60
  throw new RegistryError(`Registry returned an unsafe component name.`);
61
61
  }
62
- const dir = join(root, "_synthesisui", "ds", slug, "components");
63
- await mkdir(dir, { recursive: true });
64
- await writeFile(join(dir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
65
- await writeFile(join(dir, `${res.name}.css`), `${res.css}\n`, "utf8");
66
- console.log(`✓ ${res.name} → _synthesisui/ds/${slug}/components/${res.name}.{json,css} (${slug} v${res.version})`);
62
+ // R3 (dono, 16/08): as cópias .json/.css em _synthesisui só nascem quando
63
+ // são o PRODUTO do comando - `--artifacts-only` e targets não-Next. Quando
64
+ // o .tsx é materializado logo abaixo, ninguém as lê depois (medido na
65
+ // auditoria de 16/08), e cada arquivo a mais no repo dele é superfície.
66
+ const config = await readProjectConfig(root);
67
+ const artifactsAreTheProduct = opts.artifactsOnly === true || config.target !== "next";
68
+ if (artifactsAreTheProduct) {
69
+ const dir = join(root, "_synthesisui", "ds", slug, "components");
70
+ await mkdir(dir, { recursive: true });
71
+ await writeFile(join(dir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
72
+ await writeFile(join(dir, `${res.name}.css`), `${res.css}\n`, "utf8");
73
+ console.log(`✓ ${res.name} → _synthesisui/ds/${slug}/components/${res.name}.{json,css} (${slug} v${res.version})`);
74
+ }
67
75
  // 2. YOUR component - a real, importable `export function <Pascal>()` in the
68
76
  // project's flavor (config: styles css|tailwind), under componentsDir.
69
- const config = await readProjectConfig(root);
70
77
  const wantInteractive = opts.interactive && hasInteractiveTemplate(res.name);
71
78
  if (!opts.artifactsOnly && config.target === "next") {
72
79
  /**
@@ -55,17 +55,21 @@ export async function generate(description, opts) {
55
55
  }
56
56
  console.log(`→ generating a component for "${slug}" at ${base} …`);
57
57
  const res = await postGenerate(base, { slug, description, name: opts.name });
58
- const dir = join(root, "_synthesisui", "ds", slug, "generated");
59
- await mkdir(dir, { recursive: true });
60
- await writeFile(join(dir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
61
- await writeFile(join(dir, `${res.name}.css`), `${res.css}\n`, "utf8");
62
58
  const tries = `${res.tries} ${res.tries === 1 ? "try" : "tries"}`;
63
59
  console.log(`✓ ${res.name} generated (${res.model}, ${tries})`);
64
- console.log(` → _synthesisui/ds/${slug}/generated/${res.name}.{json,css}`);
65
60
  // Materialize YOUR component (.tsx) too - the SAME codegen `component` uses -
66
61
  // so a generated component is as usable as a brought-in one, not just a
67
62
  // recipe you have to wire by hand.
68
63
  const config = await readProjectConfig(root);
64
+ // R3 (dono, 16/08): as cópias .json/.css só quando são o produto - num
65
+ // target não-Next o .tsx abaixo não nasce, e aí elas são a entrega.
66
+ if (config.target !== "next") {
67
+ const dir = join(root, "_synthesisui", "ds", slug, "generated");
68
+ await mkdir(dir, { recursive: true });
69
+ await writeFile(join(dir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
70
+ await writeFile(join(dir, `${res.name}.css`), `${res.css}\n`, "utf8");
71
+ console.log(` → _synthesisui/ds/${slug}/generated/${res.name}.{json,css}`);
72
+ }
69
73
  let materialized = false;
70
74
  if (config.target === "next") {
71
75
  const version = await readActiveVersion(root, slug);
@@ -109,6 +109,25 @@ const TOOLS = [
109
109
  required: ["name"],
110
110
  },
111
111
  },
112
+ {
113
+ name: "playbook",
114
+ description: "The skill playbooks (init, import, adapt) - SERVED, not shipped, so they are always current and your context only carries the step you are on. Call with { skill } to get the framing and a table of contents; then fetch ONLY the chapter for your current step with { skill, section }. Never fetch more than the step needs.",
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ skill: {
119
+ type: "string",
120
+ enum: ["init", "import", "adapt"],
121
+ description: "Which playbook.",
122
+ },
123
+ section: {
124
+ type: "string",
125
+ description: "A chapter id from the toc. Omit to get the framing + toc.",
126
+ },
127
+ },
128
+ required: ["skill"],
129
+ },
130
+ },
112
131
  {
113
132
  name: "recipe_vocabulary",
114
133
  description: "What a recipe CAN hold: every state that compiles, every preview form, the floor per kind, and the rules a media region needs. Call this BEFORE writing a recipe, not after - the reader that skipped it sent a `dark:` inside a variant and a state the compiler cannot spell, and both were dropped in silence. Served from the catalogue, so it grows as the contract grows.",
@@ -297,18 +316,55 @@ async function findToken(root, value) {
297
316
  * palavra que a pessoa já tem. Sem rede, a doutrina responde igual e a checagem é que se cala: o
298
317
  * dado é local justamente para não depender de servidor nenhum.
299
318
  */
319
+ /** A voz do sistema, servida da plataforma - ver o comentário no chamador. */
320
+ async function fetchVoice(root) {
321
+ const slug = await installedSlug(root);
322
+ const token = await readToken();
323
+ if (!slug || !token)
324
+ return { kind: "unreachable" };
325
+ const res = await fetch(`${resolveRegistry()}/api/registry/ds/${slug}?voice=1`, { headers: { Authorization: `Bearer ${token}` } }).catch(() => null);
326
+ if (!res?.ok)
327
+ return { kind: "unreachable" };
328
+ const body = (await res.json().catch(() => null));
329
+ if (!body)
330
+ return { kind: "unreachable" };
331
+ return {
332
+ kind: "ok",
333
+ voice: {
334
+ context: body.context ?? undefined,
335
+ sections: body.sections ?? [],
336
+ },
337
+ };
338
+ }
300
339
  async function systemDoctrine(root) {
301
340
  const { documents, doctrines } = await loadSystem(root);
302
341
  const rules = doctrines.flatMap((d) => d.rules);
303
342
  const parts = [];
304
343
  const pinned = doctrines[0]?.version;
305
- for (const doc of documents) {
306
- const p = doc.philosophy;
307
- if (p?.context)
344
+ /**
345
+ * A VOZ É SERVIDA, NÃO LIDA DO DISCO (R2, dono 16/08). As REGRAS continuam
346
+ * locais - o doctor e o hook precisam delas offline, e essa decisão está
347
+ * declarada três vezes. A voz nunca foi lida por nenhum loop local: só esta
348
+ * ferramenta a renderiza, e ela agora pergunta à plataforma - sempre a
349
+ * versão atual, e nada dela em texto plano no repo. Sem rede, as regras
350
+ * respondem igual e a voz se declara indisponível em vez de sumir calada.
351
+ *
352
+ * Installs antigos (design-system.json com `philosophy` dentro) continuam
353
+ * funcionando: o disco é o fallback quando a rede não responde.
354
+ */
355
+ const localVoice = documents.map((doc) => doc.philosophy ?? {});
356
+ const served = await fetchVoice(root);
357
+ const voices = served.kind === "ok"
358
+ ? [served.voice]
359
+ : localVoice.filter((p) => p.context || (p.sections?.length ?? 0) > 0);
360
+ for (const p of voices) {
361
+ if (p.context)
308
362
  parts.push(`## What this product is\n\n${p.context}`);
309
- for (const sec of p?.sections ?? [])
363
+ for (const sec of p.sections ?? [])
310
364
  parts.push(`## ${sec.title}\n\n${sec.body}`);
311
365
  }
366
+ if (served.kind === "unreachable" && voices.length === 0)
367
+ parts.push("## Voice\n\nThe voice is served from the platform and could not be reached right now. The rules above still apply in full - build with them, and fetch the voice again before writing user-facing copy.");
312
368
  if (rules.length === 0 && parts.length === 0)
313
369
  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.";
314
370
  const head = [
@@ -408,6 +464,28 @@ async function listComponents(root) {
408
464
  * properties need to be dynamic - the more we map, the more precision". A list written
409
465
  * into a published package is a list that stops matching the contract.
410
466
  */
467
+ /**
468
+ * O PLAYBOOK, na fatia pedida (R1, dono 16/08): sem `section`, a moldura + o
469
+ * sumário; com, UM capítulo. O corpo inteiro nunca viaja - a dieta de contexto
470
+ * é servida por construção.
471
+ */
472
+ async function playbook(skill, section) {
473
+ const answer = await askCatalogue("playbook", undefined, section ? { skill, section } : { skill });
474
+ if (!answer.ok)
475
+ return answer.because;
476
+ const body = answer.body;
477
+ if ("error" in body)
478
+ return [body.error, body.hint].filter(Boolean).join("\n");
479
+ if ("sections" in body) {
480
+ return [
481
+ body.intro,
482
+ "",
483
+ "## Chapters - fetch ONLY the one for your current step",
484
+ ...body.sections.map((c) => `- ${c.id} - ${c.title}`),
485
+ ].join("\n");
486
+ }
487
+ return body.body;
488
+ }
411
489
  async function recipeVocabulary() {
412
490
  const answer = await askCatalogue("vocabulary");
413
491
  if (!answer.ok)
@@ -769,7 +847,9 @@ cli) {
769
847
  }
770
848
  })();
771
849
  }
772
- async function askCatalogue(want, post) {
850
+ async function askCatalogue(want, post,
851
+ /** Parâmetros extras do GET (ex.: o playbook e o capítulo pedidos). */
852
+ query) {
773
853
  const token = await readToken();
774
854
  if (!token) {
775
855
  return {
@@ -779,7 +859,8 @@ async function askCatalogue(want, post) {
779
859
  }
780
860
  const base = resolveRegistry();
781
861
  try {
782
- const res = await fetch(`${base}/api/catalogue${post ? "" : `?want=${want}`}`, post
862
+ const extra = query ? `&${new URLSearchParams(query).toString()}` : "";
863
+ const res = await fetch(`${base}/api/catalogue${post ? "" : `?want=${want}${extra}`}`, post
783
864
  ? {
784
865
  method: "POST",
785
866
  headers: {
@@ -966,6 +1047,11 @@ cli) {
966
1047
  case "add_component":
967
1048
  countAgentRead(root, String(args.name ?? ""), "add", cli);
968
1049
  return text(await addComponent(root, String(args.name ?? "")));
1050
+ case "playbook": {
1051
+ const skill = String(args?.skill ?? "");
1052
+ const section = args?.section ? String(args.section) : undefined;
1053
+ return text(await playbook(skill, section));
1054
+ }
969
1055
  case "recipe_vocabulary":
970
1056
  return text(await recipeVocabulary());
971
1057
  case "validate_recipe": {
@@ -123,12 +123,15 @@ export async function refit(file, opts) {
123
123
  recipe: res.recipe,
124
124
  });
125
125
  console.log(`✓ saved into "${slug}" (draft v${saved.version} - ships with your next publish)`);
126
- // 5. materialize back into the project: artifacts + YOUR typed component
127
- const artifactsDir = join(root, "_synthesisui", "ds", slug, "components");
128
- await mkdir(artifactsDir, { recursive: true });
129
- await writeFile(join(artifactsDir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
130
- await writeFile(join(artifactsDir, `${res.name}.css`), `${res.css}\n`, "utf8");
126
+ // 5. materialize back into the project: YOUR typed component - e as cópias
127
+ // .json/.css só quando são o produto (target não-Next; R3, dono 16/08).
131
128
  const config = await readProjectConfig(root);
129
+ if (config.target !== "next") {
130
+ const artifactsDir = join(root, "_synthesisui", "ds", slug, "components");
131
+ await mkdir(artifactsDir, { recursive: true });
132
+ await writeFile(join(artifactsDir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
133
+ await writeFile(join(artifactsDir, `${res.name}.css`), `${res.css}\n`, "utf8");
134
+ }
132
135
  let materialized = false;
133
136
  if (config.target === "next") {
134
137
  const compDir = join(root, config.componentsDir, res.name);
@@ -136,7 +136,7 @@ composition) {
136
136
  * look próprio nos seus arquivos"; a receita final é decidida depois, com a paleta em
137
137
  * mão, e as duas contas não são a mesma. Prometer a segunda aqui seria inventar precisão.
138
138
  */
139
- ` and ${composition} have no look of their own in your files - a screen that composes others, in a grid, passing data down. Those arrive as COMPOSITION recipes: the tree, the slots, and what each child receives, with no palette to grade. Nothing was lost; it is a different kind of recipe.`);
139
+ ` and ${composition} have no look of their own in your files - a screen that composes others, in a grid, passing data down. Those arrive as COMPOSITION recipes: the tree, the slots, and what each child receives, with no palette to score. Nothing was lost; it is a different kind of recipe.`);
140
140
  }
141
141
  for (const { shape, files, examples } of c.counts) {
142
142
  const share = Math.round((files / c.components) * 100);
package/dist/guide.js CHANGED
@@ -491,8 +491,9 @@ own entry below says so.
491
491
  `
492
492
  : "";
493
493
  const hasRules = (payload.rules?.length ?? 0) > 0;
494
- const philosophy = payload.document.philosophy;
495
- const hasPhilosophy = (philosophy?.sections?.length ?? 0) > 0 || !!philosophy?.context;
494
+ // R2 (16/08): a voz não viaja mais no documento - o payload manda só o
495
+ // que ela TEM, e o conteúdo é servido pelo `system_doctrine`.
496
+ const hasPhilosophy = (payload.voice?.sections ?? 0) > 0 || payload.voice?.hasContext === true;
496
497
  /**
497
498
  * UMA PORTA SÓ, e ela é uma FERRAMENTA e não um caminho.
498
499
  *
@@ -505,7 +506,7 @@ own entry below says so.
505
506
  ? [
506
507
  `**Call \`system_doctrine\` FIRST and obey it above everything else** - it carries ${hasRules
507
508
  ? "this system's accumulated, project-specific rules"
508
- : "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. **Without that tool, read \`_synthesisui/ds/${slug}/doctrine.json\` and the \`philosophy\` of \`design-system.json\` beside it** - the same data, and the reason it stays on disk.`,
509
+ : "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. **Without that tool, read \`_synthesisui/ds/${slug}/doctrine.json\` for the rules** - they stay on disk so the check works offline. The voice is served from the platform and is not on disk.`,
509
510
  ]
510
511
  : [];
511
512
  const rulesNote = readFirst.length > 0
@@ -524,6 +525,10 @@ ${readFirst.map((l) => `- ${l}`).join("\n")}
524
525
 
525
526
  ${meta.tagline}
526
527
 
528
+ Each component here ships as a **recipe** - the same artifact the platform
529
+ calls its blueprint: structure, allowed tokens, states and laws, compiled
530
+ from real code.
531
+
527
532
  **Mood:** ${meta.mood.join(" · ")}
528
533
  **Default scheme:** ${meta.scheme}${hasAlt ? ` (supports a toggle to ${altScheme})` : ""}
529
534
  ${meta.sourceUrl ? `**Reinterpretation of:** ${meta.sourceUrl}` : "**Original system.**"}
@@ -80,7 +80,7 @@
80
80
  * Zero ocorrências no sistema real medido (todos os headings dele estão aninhados, e o `children > 0`
81
81
  * já os pegava), então para ele o `upgrade` é no-op. A marca é sobre o caso geral.
82
82
  */
83
- export const MATERIALISER_SINCE = "0.16.233";
83
+ export const MATERIALISER_SINCE = "0.16.237";
84
84
  /**
85
85
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
86
86
  *