synthesisui 0.16.318 → 0.16.320

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.
@@ -308,6 +308,8 @@ export async function add(slug, opts) {
308
308
  const reference = lockReference(payload, prev);
309
309
  return reference ? { reference } : {};
310
310
  })(),
311
+ /** A memória do que NÓS escrevemos atravessa a re-materialização - ver `RootLock.written`. */
312
+ ...(prev?.written ? { written: prev.written } : {}),
311
313
  };
312
314
  const landed = (() => {
313
315
  if (!prev?.fetchedAt)
@@ -46,7 +46,7 @@ export async function ci(opts) {
46
46
  console.log(section("The contract"));
47
47
  console.log(body("--format=github annotations on the exact line, always warnings"));
48
48
  console.log(body("--format=sarif for code-scanning or SonarQube, same findings"));
49
- console.log(body("--baseline exit 1 only when drift went UP, in total or in any file"));
49
+ console.log(body("--baseline exit 1 only when the count went UP, in total or in any file"));
50
50
  console.log(body("--write-baseline freeze where you are today, and commit it - yesterday's"));
51
51
  console.log(body(" debt stops hurting without going uncounted"));
52
52
  console.log(body("--strict exit 1 on ANY finding. Honest, and red on day one in a"));
@@ -11,6 +11,7 @@ import { fetchComponent, RegistryError } from "../registry.js";
11
11
  import { flavourResolver } from "../styles-flavour.js";
12
12
  import { inTheirTongue, projectTongue } from "../their-tongue.js";
13
13
  import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
14
+ import { recordWritten } from "../written.js";
14
15
  /**
15
16
  * Writes the shared `cn.ts` next to the components, built from THIS project's
16
17
  * installed theme.
@@ -219,10 +220,16 @@ export async function component(slug, name, opts) {
219
220
  // it wears (.css) + the barrel. Ignores the css|tailwind flavor - the
220
221
  // template drives itself off the .ds-* classes.
221
222
  const tsx = interactiveTemplate(res.name);
222
- await writeFile(join(compDir, `${local}.tsx`), tsx, "utf8");
223
- await writeFile(join(compDir, `${local}.css`), `${css}\n`, "utf8");
224
- await writeFile(join(compDir, "index.ts"), `export * from "./${local}";\n`, "utf8");
225
- filenames = [`${local}.tsx`, `${local}.css`, "index.ts"];
223
+ const written = [
224
+ { filename: `${local}.tsx`, content: tsx },
225
+ { filename: `${local}.css`, content: `${css}\n` },
226
+ { filename: "index.ts", content: `export * from "./${local}";\n` },
227
+ ];
228
+ for (const f of written)
229
+ await writeFile(join(compDir, f.filename), f.content, "utf8");
230
+ /** O fingerprint do que escrevemos - é o que protege a edição dele no upgrade (T7). */
231
+ await recordWritten(join(root, "_synthesisui", "ds", slug), local, written);
232
+ filenames = written.map((f) => f.filename);
226
233
  }
227
234
  else {
228
235
  const files = generateComponentFiles(slug, res.name, res.recipe, css, res.version, flavour, await reactMajorOf(root),
@@ -258,12 +265,19 @@ export async function component(slug, name, opts) {
258
265
  * sistema não tem censo nenhum, e ele recebe o que sempre recebeu.
259
266
  */
260
267
  const nota = unreadComment(unreadForComponent(await readCensus(root), res.name));
261
- for (const file of files) {
262
- await writeFile(join(compDir, file.filename), file.filename.endsWith(".tsx") ? `${nota}${file.code}` : file.code, "utf8");
263
- }
268
+ /** Os bytes finais, para o fingerprint lembrar EXATAMENTE o que foi a disco (T7). */
269
+ const written = files.map((file) => ({
270
+ filename: file.filename,
271
+ content: file.filename.endsWith(".tsx")
272
+ ? `${nota}${file.code}`
273
+ : file.code,
274
+ }));
275
+ for (const f of written)
276
+ await writeFile(join(compDir, f.filename), f.content, "utf8");
277
+ await recordWritten(join(root, "_synthesisui", "ds", slug), local, written);
264
278
  if (flavour === "tailwind")
265
279
  await writeCn(root, compDir, slug);
266
- filenames = files.map((f) => f.filename);
280
+ filenames = written.map((f) => f.filename);
267
281
  }
268
282
  const flavor = wantInteractive ? "interactive" : `styles: ${flavour}`;
269
283
  console.log(`✓ ${config.componentsDir}/${local}/ → ${filenames.join(", ")} (${flavor})${local === res.name ? "" : ` - the "${res.name}" blueprint, under your name`}`);
@@ -388,7 +388,7 @@ export async function connect(opts) {
388
388
  }
389
389
  else {
390
390
  console.log("");
391
- console.log(body(paint.dim("Your PRs can carry this too: annotations on the exact line, and a ratchet that fails only when drift goes UP - never on the debt you already have.")));
391
+ console.log(body(paint.dim("Your PRs can carry this too: annotations on the exact line, and a check that fails only when the count goes UP - never on the debt you already have.")));
392
392
  console.log(paint.blue(snippet(["npx synthesisui@latest ci"])));
393
393
  }
394
394
  /** A mesma versão que a fiação do editor recebe - os dois pinam no mesmo número. */
@@ -6,6 +6,7 @@ import { installedSlugs } from "../installed.js";
6
6
  import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
7
7
  import { postGenerate, RegistryError } from "../registry.js";
8
8
  import { flavourResolver } from "../styles-flavour.js";
9
+ import { recordWritten } from "../written.js";
9
10
  /** PascalCase para o hint de import (course-card → CourseCard). */
10
11
  function pascalName(name) {
11
12
  return name.replace(/(^|[-_])([a-z0-9])/g, (_, __, c) => c.toUpperCase());
@@ -75,6 +76,8 @@ export async function generate(description, opts) {
75
76
  for (const file of files) {
76
77
  await writeFile(join(compDir, file.filename), file.code, "utf8");
77
78
  }
79
+ /** O fingerprint do que escrevemos - protege a edição dele no upgrade (T7). */
80
+ await recordWritten(join(root, "_synthesisui", "ds", slug), res.name, files.map((x) => ({ filename: x.filename, content: x.code })));
78
81
  materialized = true;
79
82
  console.log(`✓ ${config.componentsDir}/${res.name}/ → ${files.map((f) => f.filename).join(", ")} (styles: ${config.styles})`);
80
83
  }
@@ -507,7 +507,7 @@ export async function takeCensus(root, opts) {
507
507
  * tipo de surpresa que ninguém pede.
508
508
  */
509
509
  if (globalClasses.size > MAX_GLOBAL_CLASSES) {
510
- console.log(body(paint.faint(`${globalClasses.size} classes in your global sheet, and ${globalClasses.size - MAX_GLOBAL_CLASSES} of them are not travelling with this census - the first ${MAX_GLOBAL_CLASSES} are.`)));
510
+ console.log(body(paint.faint(`${globalClasses.size} classes in your global sheet, and ${globalClasses.size - MAX_GLOBAL_CLASSES} of them are not travelling with this measurement - the first ${MAX_GLOBAL_CLASSES} are.`)));
511
511
  }
512
512
  /**
513
513
  * OS KEYFRAMES DA FOLHA DELE, colhidos onde as folhas globais já estão na mão.
@@ -1831,7 +1831,7 @@ export async function takeCensus(root, opts) {
1831
1831
  say(body(paint.faint(`${reachability.unreached
1832
1832
  .slice(0, 4)
1833
1833
  .map((f) => relative(root, f))
1834
- .join(", ")}${reachability.unreached.length > 4 ? `, and ${reachability.unreached.length - 4} more` : ""} - the full list travels in the census, and nothing was left out because of it`)));
1834
+ .join(", ")}${reachability.unreached.length > 4 ? `, and ${reachability.unreached.length - 4} more` : ""} - the full list travels in \`_synthesisui/census.json\`, and nothing was left out because of it`)));
1835
1835
  }
1836
1836
  /**
1837
1837
  * WHAT THE GATE TURNED AWAY - said out loud, grouped, with the reason.
@@ -2916,7 +2916,7 @@ function printCrosswalk(mine) {
2916
2916
  function printAgentContract() {
2917
2917
  console.log("");
2918
2918
  console.log(section("If you are an agent reading this"));
2919
- console.log(body("The census is deterministic and complete. What it cannot do is judge, so"));
2919
+ console.log(body("The measurement is deterministic and complete. What it cannot do is judge, so"));
2920
2920
  console.log(body("add a `reading` object to the file and send that back to us:"));
2921
2921
  console.log("");
2922
2922
  console.log(body(paint.faint(' "reading": {')));
@@ -2926,7 +2926,7 @@ function printAgentContract() {
2926
2926
  console.log(body(paint.faint(' "themes": { "default": "dark", "has": ["dark"] }')));
2927
2927
  console.log(body(paint.faint(" }")));
2928
2928
  console.log("");
2929
- console.log(body("Every hex in `roles` must be one the census already observed - a value"));
2929
+ console.log(body("Every hex in `roles` must be one the measurement already observed - a value"));
2930
2930
  console.log(body("we did not find in their code is dropped. Then send the file:"));
2931
2931
  console.log("");
2932
2932
  console.log(body(` ${paint.strong("synthesisui import --census _synthesisui/census.json")}`));
@@ -3020,7 +3020,7 @@ async function sayAboutPlace(root, scope) {
3020
3020
  console.log(body(` ${paint.strong(`synthesisui import --dir ${up} --scope ${vocab.rel} ${usage}`)}`));
3021
3021
  console.log(body(paint.faint(` --dir points at the workspace root, so --scope and --usage are read from there.`)));
3022
3022
  console.log("");
3023
- console.log(body(paint.dim("Nothing you ran is wasted - this census is a fine diagnosis of this app. It is a system for one consumer, which is the part worth knowing before you publish it.")));
3023
+ console.log(body(paint.dim("Nothing you ran is wasted - this measurement is a fine diagnosis of this app. It is a system for one consumer, which is the part worth knowing before you publish it.")));
3024
3024
  }
3025
3025
  async function sayIfWorkspace(root) {
3026
3026
  const { apps, shared } = await siblingProjects(root);
@@ -3028,7 +3028,7 @@ async function sayIfWorkspace(root) {
3028
3028
  return;
3029
3029
  console.log("");
3030
3030
  console.log(section("This root holds several projects"));
3031
- console.log(body(`${apps.length} apps (${apps.slice(0, 3).join(", ")}${apps.length > 3 ? ", …" : ""}) - and this census is the average of all of them.`));
3031
+ console.log(body(`${apps.length} apps (${apps.slice(0, 3).join(", ")}${apps.length > 3 ? ", …" : ""}) - and this measurement is the average of all of them.`));
3032
3032
  console.log(body("That is a fine DIAGNOSIS and a poor system: a light app and a dark one"));
3033
3033
  console.log(body("average into a palette that is neither."));
3034
3034
  console.log("");
@@ -3836,7 +3836,7 @@ export async function runImport(opts) {
3836
3836
  }
3837
3837
  if (census.census !== 1) {
3838
3838
  console.log(section("Import"));
3839
- console.log(body("That file is not a census this version can send."));
3839
+ console.log(body("That file is not a measurement this version can send."));
3840
3840
  return;
3841
3841
  }
3842
3842
  /**
@@ -3850,7 +3850,7 @@ export async function runImport(opts) {
3850
3850
  * Resolved here rather than at measure time because the reading arrives
3851
3851
  * AFTER the census was taken - this is the first moment both halves exist.
3852
3852
  */
3853
- phase(2, 3, "Resolving the reading against the census");
3853
+ phase(2, 3, "Resolving your reading against the measurement");
3854
3854
  await resolveReadParts(census, root);
3855
3855
  }
3856
3856
  else {
@@ -7,6 +7,7 @@ import { body, section, snippet } from "../output.js";
7
7
  import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
8
8
  import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
9
9
  import { flavourResolver } from "../styles-flavour.js";
10
+ import { recordWritten } from "../written.js";
10
11
  /** Slugs INSTALLED under `_synthesisui/ds/` (a `.lock` marks a real install -
11
12
  * a folder holding only refit artifacts doesn't count). */
12
13
  /** True when the system is actually installed (tokens.css present). */
@@ -121,6 +122,8 @@ export async function refit(file, opts) {
121
122
  for (const f of files) {
122
123
  await writeFile(join(compDir, f.filename), f.code, "utf8");
123
124
  }
125
+ /** O fingerprint do que escrevemos - protege a edição dele no upgrade (T7). */
126
+ await recordWritten(join(root, "_synthesisui", "ds", slug), res.name, files.map((x) => ({ filename: x.filename, content: x.code })));
124
127
  materialized = true;
125
128
  console.log(`✓ ${config.componentsDir}/${res.name}/ → ${files.map((f) => f.filename).join(", ")} (styles: ${config.styles})`);
126
129
  }
@@ -105,11 +105,11 @@ export async function status(opts) {
105
105
  console.log(body(`Ratchet at ${parsed.totals?.drift ?? "?"} findings, frozen ${parsed.at ? ageOf(parsed.at) : "at an unrecorded time"} - CI fails only above it.`));
106
106
  }
107
107
  catch {
108
- console.log(body("A drift baseline exists but is not readable JSON."));
108
+ console.log(body("`_synthesisui/drift-baseline.json` is here but is not readable JSON."));
109
109
  }
110
110
  }
111
111
  else {
112
- console.log(body(paint.dim("No ratchet yet, so nothing in CI can fail on drift. `synthesisui ci` prints the two steps.")));
112
+ console.log(body(paint.dim("Nothing in your pipeline can fail on a hard-coded value yet. `synthesisui ci` prints the two steps.")));
113
113
  }
114
114
  /**
115
115
  * TODO REQUEST LIDO ESTÁ ABERTO: `closeRequest` REMOVE a linha do arquivo, então o arquivo é a fila
@@ -11,6 +11,7 @@ import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../
11
11
  import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
12
12
  import { flavourResolver } from "../styles-flavour.js";
13
13
  import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
14
+ import { editedSinceWritten, keptLine, readWritten, recordWritten, } from "../written.js";
14
15
  import { add } from "./add.js";
15
16
  import { reportWhatIsLeft } from "./align.js";
16
17
  import { doctor } from "./doctor.js";
@@ -283,6 +284,9 @@ export async function upgrade(asked, opts) {
283
284
  const config = await readProjectConfig(root);
284
285
  const regenerated = [];
285
286
  const failed = [];
287
+ /** Componentes que ELE editou desde que os escrevemos - mantidos intactos (T7). */
288
+ const kept = [];
289
+ const writtenMap = await readWritten(slugDir);
286
290
  if (config.target === "next") {
287
291
  const componentsRoot = join(root, config.componentsDir);
288
292
  const marker = `from the "${slug}" design system`;
@@ -308,6 +312,17 @@ export async function upgrade(asked, opts) {
308
312
  }
309
313
  if (!head.includes("Generated by SynthesisUI") || !head.includes(marker))
310
314
  continue;
315
+ /**
316
+ * A EDIÇÃO DELE VENCE A REGENERAÇÃO (T7) - ver `written.ts`. O fingerprint do que NÓS
317
+ * escrevemos foi gravado no `.lock` na hora da escrita; se o disco divergiu, ele editou, e o
318
+ * arquivo é DELE - fica intacto e o terminal diz qual. `null` é install de antes do
319
+ * fingerprint: sem memória não há veredito, e o comportamento de sempre continua.
320
+ */
321
+ const edited = await editedSinceWritten(writtenMap[entry], join(componentsRoot, entry));
322
+ if (edited && edited.length > 0) {
323
+ kept.push({ entry, edited });
324
+ continue;
325
+ }
311
326
  try {
312
327
  const res = await fetchComponent(base, slug, entry);
313
328
  const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, flavourOf(res.name),
@@ -325,9 +340,17 @@ export async function upgrade(asked, opts) {
325
340
  * Ver `unread-for-component.ts`: censo ausente responde vazio, e vazio não vira comentário.
326
341
  */
327
342
  const nota = unreadComment(unreadForComponent(await readCensus(root), res.name));
328
- for (const file of files) {
329
- await writeFile(join(componentsRoot, entry, file.filename), file.filename.endsWith(".tsx") ? `${nota}${file.code}` : file.code, "utf8");
343
+ /** Os bytes finais, para o fingerprint lembrar EXATAMENTE o que foi a disco. */
344
+ const finalFiles = files.map((file) => ({
345
+ filename: file.filename,
346
+ content: file.filename.endsWith(".tsx")
347
+ ? `${nota}${file.code}`
348
+ : file.code,
349
+ }));
350
+ for (const file of finalFiles) {
351
+ await writeFile(join(componentsRoot, entry, file.filename), file.content, "utf8");
330
352
  }
353
+ await recordWritten(slugDir, entry, finalFiles);
331
354
  regenerated.push(entry);
332
355
  }
333
356
  catch {
@@ -372,6 +395,13 @@ export async function upgrade(asked, opts) {
372
395
  .map((n) => `\`${n}\``)
373
396
  .join(", ")}.`
374
397
  : "- No materialized components needed regeneration.",
398
+ ...(kept.length > 0
399
+ ? [
400
+ `- These components were edited by hand after we wrote them, so the upgrade KEPT your files: ${kept
401
+ .map((k) => `\`${k.entry}\``)
402
+ .join(", ")}. To take the new version of one, rerun \`npx synthesisui component ${slug} <name>\`.`,
403
+ ]
404
+ : []),
375
405
  "- The full new contract lives in `design-system.json` / `GUIDE.md` next to this file.",
376
406
  "",
377
407
  ].join("\n");
@@ -394,6 +424,12 @@ export async function upgrade(asked, opts) {
394
424
  console.log("");
395
425
  console.log(body(`⚠ Could not regenerate: ${failed.join(", ")} (removed/renamed in v${latest.version}?)`));
396
426
  }
427
+ /** A prova VISÍVEL do T7: o dono fica sabendo AGORA, com o arquivo nomeado e a saída dita. */
428
+ if (kept.length > 0) {
429
+ console.log("");
430
+ for (const k of kept)
431
+ console.log(body(keptLine(slug, k.entry, k.edited)));
432
+ }
397
433
  await checkAfterWriting(root, `v${latest.version}`);
398
434
  console.log(section("Migrate the app"));
399
435
  console.log(body(`The migration brief is at _synthesisui/ds/${slug}/UPGRADE.md`));
@@ -137,7 +137,14 @@
137
137
  * ali, que é exatamente o que esta marca existe para dizer. Quem avisa versão nova passa a ser o
138
138
  * próprio align, com o `cli` que o servidor manda no `?meta=1`.
139
139
  */
140
- export const MATERIALISER_SINCE = "0.16.306";
140
+ /**
141
+ * 0.16.306 -> 0.16.319 em 26/08 (T7), e o SIM é sobre DUAS coisas que caem na pasta: o `.lock`
142
+ * ganha `written` (o fingerprint do que NÓS escrevemos, por componente e arquivo) e o `upgrade`
143
+ * muda QUAIS bytes reescreve - um componente que o dono editou à mão fica INTACTO, com a frase no
144
+ * terminal dizendo qual arquivo e como tomar a versão nova por escolha. Antes desta versão a
145
+ * reescrita era incondicional e o único aviso era prosa depois do fato.
146
+ */
147
+ export const MATERIALISER_SINCE = "0.16.319";
141
148
  /**
142
149
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
143
150
  *
@@ -0,0 +1,70 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ export function fingerprintOf(content) {
5
+ return createHash("sha256")
6
+ .update(content, "utf8")
7
+ .digest("hex")
8
+ .slice(0, 12);
9
+ }
10
+ /** O mapa gravado, ou vazio - um `.lock` ausente/ilegível responde como install legado. */
11
+ export async function readWritten(slugDir) {
12
+ try {
13
+ const lock = JSON.parse(await readFile(join(slugDir, ".lock"), "utf8"));
14
+ return lock.written ?? {};
15
+ }
16
+ catch {
17
+ return {};
18
+ }
19
+ }
20
+ /**
21
+ * Grava o fingerprint do que acabou de ser escrito. Lê-modifica-escreve o `.lock` inteiro para
22
+ * não perder nenhum campo irmão; sem `.lock` (componente materializado antes do install), não há
23
+ * onde lembrar - silêncio de propósito, o próximo `add` cria o arquivo e o próximo write grava.
24
+ */
25
+ export async function recordWritten(slugDir, entry, files) {
26
+ const lockPath = join(slugDir, ".lock");
27
+ let lock;
28
+ try {
29
+ lock = JSON.parse(await readFile(lockPath, "utf8"));
30
+ }
31
+ catch {
32
+ return;
33
+ }
34
+ const written = (lock.written ?? {});
35
+ written[entry] = Object.fromEntries(files.map((f) => [f.filename, fingerprintOf(f.content)]));
36
+ lock.written = written;
37
+ await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
38
+ }
39
+ /**
40
+ * QUAIS ARQUIVOS ELE EDITOU desde que nós os escrevemos.
41
+ *
42
+ * Um arquivo que sumiu também conta: apagar é uma edição - regenerá-lo desfaria a decisão dele
43
+ * do mesmo jeito. Devolve vazio quando nada divergiu (nosso para regenerar) e `null` quando não
44
+ * há fingerprint gravado (install legado - o chamador mantém o comportamento de sempre).
45
+ */
46
+ export async function editedSinceWritten(recorded, dir) {
47
+ if (!recorded)
48
+ return null;
49
+ const edited = [];
50
+ for (const [filename, fp] of Object.entries(recorded)) {
51
+ try {
52
+ const current = await readFile(join(dir, filename), "utf8");
53
+ if (fingerprintOf(current) !== fp)
54
+ edited.push(filename);
55
+ }
56
+ catch {
57
+ edited.push(filename);
58
+ }
59
+ }
60
+ return edited;
61
+ }
62
+ /**
63
+ * A FRASE DO TERMINAL, uma por componente mantido - o critério do T7 é a prova VISÍVEL: o dono
64
+ * fica sabendo na hora, com o arquivo nomeado e a saída dita, não num diff depois do fato. A
65
+ * saída oferecida regenera por ESCOLHA (o `component` reescreve e regrava o fingerprint).
66
+ */
67
+ export function keptLine(slug, entry, edited) {
68
+ return (`! ${entry}: you edited ${edited.join(", ")} after we wrote it - kept YOUR file(s).\n` +
69
+ ` To take the new version anyway: npx synthesisui component ${slug} ${entry}`);
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.318",
3
+ "version": "0.16.320",
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": {