synthesisui 0.16.243 → 0.16.247
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.
- package/dist/agent-provenance.js +92 -0
- package/dist/commands/mcp.js +247 -44
- package/dist/compose-context.js +210 -0
- package/dist/skill-compose.js +22 -0
- package/dist/skills.js +17 -0
- package/package.json +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DE ONDE VEIO A RESPOSTA - e é um DADO, não uma frase (dono, 18/08).
|
|
3
|
+
*
|
|
4
|
+
* O pedido veio com a razão embutida: *"eu trataria a linha Source como algo
|
|
5
|
+
* estrutural mesmo, não só visual. Isso pode virar uma métrica de maturidade
|
|
6
|
+
* depois - porcentagem de respostas por receita, por composição, por
|
|
7
|
+
* improviso."*
|
|
8
|
+
*
|
|
9
|
+
* Então a procedência nasce como estrutura e o texto é DERIVADO dela. O
|
|
10
|
+
* contrário - escrever a frase à mão em cada resposta - produz duas verdades no
|
|
11
|
+
* dia em que uma delas for editada, e a que vai para a métrica é sempre a que
|
|
12
|
+
* ninguém leu.
|
|
13
|
+
*
|
|
14
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
15
|
+
* O QUE CADA KIND SIGNIFICA, e a fronteira é a GARANTIA
|
|
16
|
+
*
|
|
17
|
+
* contract veio do que este sistema declara - receita, token, regra,
|
|
18
|
+
* vocabulário. A plataforma responde por isso.
|
|
19
|
+
* composed montado a partir de camadas porque não havia receita para o
|
|
20
|
+
* pedido. Vale, e vale menos que um contrato. (Nasce com o
|
|
21
|
+
* compose_context; hoje nada devolve isto, e é de propósito -
|
|
22
|
+
* um kind que ninguém emite é um número que mente por otimismo.)
|
|
23
|
+
* pointer um ponteiro para conhecimento de terceiro. Não é nosso, não é
|
|
24
|
+
* garantido, e a resposta diz o pacote. Mesma gramática que a
|
|
25
|
+
* plataforma já usa: "3rd-party · <pacote>".
|
|
26
|
+
* none este sistema não tinha nada para o pedido. É o kind mais
|
|
27
|
+
* valioso dos quatro: é ele que mede o improviso, e é o evento
|
|
28
|
+
* que a plataforma não contava.
|
|
29
|
+
*
|
|
30
|
+
* A TAXA DE IMPROVISO é `none` sobre o total, e ela é a pergunta que decide se
|
|
31
|
+
* a composição vale ser construída - respondida com dado em vez de sensação.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* A LINHA QUE O AGENTE E A PESSOA LEEM - derivada, nunca escrita à mão.
|
|
35
|
+
*
|
|
36
|
+
* Ela serve dois leitores de uma vez: o dev sabe o quanto confiar no que
|
|
37
|
+
* acabou de receber, e o agente sabe se está pisando em contrato ou em
|
|
38
|
+
* composição. Um agente que não sabe a diferença trata as duas com a mesma
|
|
39
|
+
* confiança, e é assim que uma suposição nossa entra no repositório dele com
|
|
40
|
+
* cara de regra.
|
|
41
|
+
*/
|
|
42
|
+
export function sourceLine(source) {
|
|
43
|
+
const label = source.kind === "pointer"
|
|
44
|
+
? "3rd-party pointer"
|
|
45
|
+
: source.kind === "none"
|
|
46
|
+
? "none"
|
|
47
|
+
: source.kind;
|
|
48
|
+
return source.detail
|
|
49
|
+
? `Source: ${label} · ${source.detail}`
|
|
50
|
+
: `Source: ${label}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* O texto com a procedência colada no fim - uma linha em branco antes, para
|
|
54
|
+
* ela não se misturar com a última frase da resposta.
|
|
55
|
+
*/
|
|
56
|
+
export function withSource(answer) {
|
|
57
|
+
return `${answer.body}\n\n${sourceLine(answer.source)}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* O VALOR GRAVADO, e o prefixo existe para o eixo ser legível numa tabela que
|
|
61
|
+
* já carrega outra coisa.
|
|
62
|
+
*
|
|
63
|
+
* `agent_reads.action` é `text` sem CHECK no banco (medido em produção), então
|
|
64
|
+
* um valor novo não custa migração - e uma coluna nova custaria a tabela
|
|
65
|
+
* inteira até a migração rodar. `src-` marca de qual eixo a linha fala:
|
|
66
|
+
* `describe`/`add` são gesto, `tool` é chamada, `src-*` é procedência.
|
|
67
|
+
*
|
|
68
|
+
* E UMA LACUNA LEVA O MOTIVO NO PRÓPRIO VALOR: `src-none-recipe`. Assim a
|
|
69
|
+
* pergunta agregada continua sendo um `like 'src-none%'` e a pergunta por
|
|
70
|
+
* motivo é a mesma consulta com o valor inteiro - sem coluna nova, e sem
|
|
71
|
+
* perder o total.
|
|
72
|
+
*
|
|
73
|
+
* Recebe a procedência INTEIRA e não o `kind`: com o kind sozinho, o motivo
|
|
74
|
+
* seria um segundo argumento que alguém esquece, e a linha gravada voltaria a
|
|
75
|
+
* ser um `none` cego.
|
|
76
|
+
*/
|
|
77
|
+
export function provenanceAction(source) {
|
|
78
|
+
return source.kind === "none"
|
|
79
|
+
? `src-none-${source.reason}`
|
|
80
|
+
: `src-${source.kind}`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* O NOME DA FERRAMENTA COMO A TABELA O GUARDA.
|
|
84
|
+
*
|
|
85
|
+
* A coluna `component` aceita `^[a-z][a-z0-9-]*$` na rota - a mesma forma de um
|
|
86
|
+
* slug de receita. As ferramentas são snake_case, então elas viajam em
|
|
87
|
+
* kebab-case; sem isto o ping seria recusado com 400 e a instrumentação
|
|
88
|
+
* silenciaria justamente na ferramenta nova.
|
|
89
|
+
*/
|
|
90
|
+
export function toolSlug(tool) {
|
|
91
|
+
return tool.replace(/_/g, "-").toLowerCase();
|
|
92
|
+
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { readdir, readFile } from "node:fs/promises";
|
|
3
3
|
import { join, relative, resolve } from "node:path";
|
|
4
|
+
import { provenanceAction, toolSlug, withSource, } from "../agent-provenance.js";
|
|
4
5
|
import { pinnedHookVersion } from "../agent-wiring.js";
|
|
6
|
+
import { composePlan, FAMILIES, isFamily, noFamilyAnswer, renderPlan, } from "../compose-context.js";
|
|
5
7
|
import { readToken, resolveRegistry } from "../config.js";
|
|
6
8
|
import { readEvents } from "../doctor/ledger.js";
|
|
7
9
|
import { fileRequest } from "../doctor/requests.js";
|
|
@@ -14,6 +16,16 @@ import { installedSlug, remeasure } from "./sync.js";
|
|
|
14
16
|
const send = (msg) => {
|
|
15
17
|
process.stdout.write(`${JSON.stringify(msg)}\n`);
|
|
16
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* A RESPOSTA QUE VEIO DO CONTRATO - o atalho, porque é o caso comum.
|
|
21
|
+
*
|
|
22
|
+
* `detail` fica de fora quando a própria resposta já nomeia a fonte (o token,
|
|
23
|
+
* o componente, a versão): repetir aquilo na linha de procedência é ruído.
|
|
24
|
+
*/
|
|
25
|
+
const fromContract = (body, detail) => ({
|
|
26
|
+
body,
|
|
27
|
+
source: { kind: "contract", ...(detail ? { detail } : null) },
|
|
28
|
+
});
|
|
17
29
|
/** A tool result is always text: readable by the model, and by a person
|
|
18
30
|
* watching the transcript when something goes wrong. */
|
|
19
31
|
const text = (body, isError = false) => ({
|
|
@@ -111,14 +123,14 @@ const TOOLS = [
|
|
|
111
123
|
},
|
|
112
124
|
{
|
|
113
125
|
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.",
|
|
126
|
+
description: "The skill playbooks (init, import, adapt, compose) - 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. `compose` is the hierarchy to walk when this system has no recipe for what you were asked to build - the layers it DOES have, in order.",
|
|
115
127
|
inputSchema: {
|
|
116
128
|
type: "object",
|
|
117
129
|
properties: {
|
|
118
130
|
skill: {
|
|
119
131
|
type: "string",
|
|
120
|
-
enum: ["init", "import", "adapt"],
|
|
121
|
-
description: "Which playbook.",
|
|
132
|
+
enum: ["init", "import", "adapt", "compose"],
|
|
133
|
+
description: "Which playbook. `compose` is the one for building something this system has no recipe for.",
|
|
122
134
|
},
|
|
123
135
|
section: {
|
|
124
136
|
type: "string",
|
|
@@ -173,6 +185,24 @@ const TOOLS = [
|
|
|
173
185
|
required: ["recipes"],
|
|
174
186
|
},
|
|
175
187
|
},
|
|
188
|
+
{
|
|
189
|
+
name: "compose_context",
|
|
190
|
+
description: "The layers to build something this system has NO recipe for, in one call: the recipes of that family to copy from, the tokens they already use with their values, the system's rules, and the floor and checklist for the family. YOU say which family it is - you just read the request, and a keyword guess of ours would be worse. Call this instead of asking for doctrine, vocabulary and each token separately; the laws of markup and the voice are named in the answer with the one fetch each needs.",
|
|
191
|
+
inputSchema: {
|
|
192
|
+
type: "object",
|
|
193
|
+
properties: {
|
|
194
|
+
family: {
|
|
195
|
+
type: "string",
|
|
196
|
+
description: `Which family this behaves like: ${FAMILIES.join(", ")}. Pick by BEHAVIOUR, never by the name in the request. If a part fits none of them, send what you would call it and the answer says what to do.`,
|
|
197
|
+
},
|
|
198
|
+
intent: {
|
|
199
|
+
type: "string",
|
|
200
|
+
description: "What you are building, in a few words - it is echoed back so your summary can name it.",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
required: ["family"],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
176
206
|
{
|
|
177
207
|
name: "request_component",
|
|
178
208
|
description: "File a component request when nothing in the catalogue covers what you need. This is the OTHER HALF of the refusal rule: you already say which entry you considered and why it did not fit - said in chat, that reasoning evaporates; filed here, it becomes the queue the system's author works from. File it at the moment you build the workaround, while the reasoning is still yours.",
|
|
@@ -254,7 +284,10 @@ export const MCP_TOOL_COUNT = TOOLS.length;
|
|
|
254
284
|
async function checkFile(root, path) {
|
|
255
285
|
const { table } = await loadSystem(root);
|
|
256
286
|
if (table.byName.size === 0)
|
|
257
|
-
return
|
|
287
|
+
return {
|
|
288
|
+
body: "No design system installed here, so there is nothing to check against. Run `synthesisui init` or `synthesisui adopt`.",
|
|
289
|
+
source: { kind: "none", reason: "system", detail: "no system installed" },
|
|
290
|
+
};
|
|
258
291
|
const scope = resolve(root, path);
|
|
259
292
|
const reports = [];
|
|
260
293
|
for await (const file of walkAll([scope])) {
|
|
@@ -263,7 +296,7 @@ async function checkFile(root, path) {
|
|
|
263
296
|
reports.push(scanSource(relative(root, file), src, table));
|
|
264
297
|
}
|
|
265
298
|
if (reports.length === 0)
|
|
266
|
-
return `Nothing readable at ${path}
|
|
299
|
+
return fromContract(`Nothing readable at ${path}.`);
|
|
267
300
|
const d = diagnose(reports);
|
|
268
301
|
const out = [
|
|
269
302
|
`${table.name ?? table.slug}: ${d.coverage}% of design values come from the system.`,
|
|
@@ -284,12 +317,16 @@ async function checkFile(root, path) {
|
|
|
284
317
|
}
|
|
285
318
|
if (d.findings.length === 0 && phantoms.length === 0)
|
|
286
319
|
out.push("", "Nothing to fix here.");
|
|
287
|
-
return out.join("\n");
|
|
320
|
+
return fromContract(out.join("\n"));
|
|
288
321
|
}
|
|
289
322
|
async function findToken(root, value) {
|
|
290
323
|
const { table } = await loadSystem(root);
|
|
291
|
-
if (table.byName.size === 0)
|
|
292
|
-
return
|
|
324
|
+
if (table.byName.size === 0) {
|
|
325
|
+
return {
|
|
326
|
+
body: "No design system installed here.",
|
|
327
|
+
source: { kind: "none", reason: "system", detail: "no system installed" },
|
|
328
|
+
};
|
|
329
|
+
}
|
|
293
330
|
/**
|
|
294
331
|
* O NOME DELE PRIMEIRO, como no doctor (`nameToWrite`): `loadSystem` já devolve a tabela COM os
|
|
295
332
|
* aliases dele (`withTheirNames`) e este era o único leitor que nunca os consultava. Sem o `kind`
|
|
@@ -306,16 +343,27 @@ async function findToken(root, value) {
|
|
|
306
343
|
const exact = tokenFor(table, value);
|
|
307
344
|
if (exact) {
|
|
308
345
|
const shown = theirNameFor(value) ?? exact;
|
|
309
|
-
return `${value} is ${shown} in this project. Use var(${shown})
|
|
346
|
+
return fromContract(`${value} is ${shown} in this project. Use var(${shown}).`);
|
|
310
347
|
}
|
|
311
348
|
const near = nearestToken(table, value);
|
|
312
349
|
if (near) {
|
|
313
350
|
const shown = theirNameFor(near.value) ?? near.name;
|
|
314
|
-
|
|
351
|
+
/** Perto não é o valor: o sistema NÃO tem este, e a métrica precisa saber. */
|
|
352
|
+
return {
|
|
353
|
+
body: `No token holds ${value}. The closest is ${shown} at ${near.value}. If that is what you meant, use it - if it genuinely is not, say so rather than inventing a token.`,
|
|
354
|
+
source: {
|
|
355
|
+
kind: "none",
|
|
356
|
+
reason: "token",
|
|
357
|
+
detail: `no token for ${value}`,
|
|
358
|
+
},
|
|
359
|
+
};
|
|
315
360
|
}
|
|
316
361
|
// Same words the managed block uses. A refusal is only useful if it is the
|
|
317
362
|
// same refusal every time.
|
|
318
|
-
return
|
|
363
|
+
return {
|
|
364
|
+
body: `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.`,
|
|
365
|
+
source: { kind: "none", reason: "token", detail: `no token for ${value}` },
|
|
366
|
+
};
|
|
319
367
|
}
|
|
320
368
|
/**
|
|
321
369
|
* A DOUTRINA DO SISTEMA - as regras e a filosofia, servidas em vez de materializadas.
|
|
@@ -801,18 +849,28 @@ async function validateRecipes(root, entries) {
|
|
|
801
849
|
* had 26 names against a catalogue of 41.
|
|
802
850
|
*/
|
|
803
851
|
/**
|
|
804
|
-
* O CONTADOR DO AGENTE, fire-and-forget
|
|
805
|
-
*
|
|
806
|
-
*
|
|
807
|
-
*
|
|
852
|
+
* O CONTADOR DO AGENTE, fire-and-forget e agora CENTRAL.
|
|
853
|
+
*
|
|
854
|
+
* Ele nasceu para uma tela: o UI Kit mostra "vezes que esta receita foi consultada pelo
|
|
855
|
+
* Agente". E era chamado à mão em duas ferramentas de treze, o que quer dizer que onze
|
|
856
|
+
* ferramentas nunca contaram nada - inclusive a única que sabe dizer que o sistema NÃO
|
|
857
|
+
* tinha o que foi pedido.
|
|
858
|
+
*
|
|
859
|
+
* Agora o `callTool` o chama uma vez, para toda ferramenta, com a procedência da
|
|
860
|
+
* resposta. O dono foi explícito sobre por que centralizar (18/08): *"isso vira
|
|
861
|
+
* comportamento padrão e não depende de disciplina futura."* Um contador chamado dentro
|
|
862
|
+
* de um `case` é um contador que a próxima ferramenta esquece.
|
|
863
|
+
*
|
|
864
|
+
* Nunca bloqueia a resposta: sem sessão ou sem rede, não conta e não quebra - telemetria
|
|
865
|
+
* que atrasa a ferramenta é telemetria que alguém desliga.
|
|
808
866
|
*/
|
|
809
|
-
function
|
|
867
|
+
function recordAgentCall(root, call,
|
|
810
868
|
/** A versão que está reportando - ver `repoStateOf`. Ausente = não reporta estado. */
|
|
811
869
|
cli) {
|
|
812
870
|
void (async () => {
|
|
813
871
|
try {
|
|
814
872
|
const token = await readToken();
|
|
815
|
-
if (!token
|
|
873
|
+
if (!token)
|
|
816
874
|
return;
|
|
817
875
|
// O slug vem do .lock do sistema instalado - a mesma resolução do sync.
|
|
818
876
|
const dsDir = join(root, "_synthesisui", "ds");
|
|
@@ -853,8 +911,10 @@ cli) {
|
|
|
853
911
|
*/
|
|
854
912
|
body: JSON.stringify({
|
|
855
913
|
slug,
|
|
856
|
-
|
|
857
|
-
|
|
914
|
+
tool: toolSlug(call.tool),
|
|
915
|
+
source: provenanceAction(call.source),
|
|
916
|
+
...(call.component ? { component: call.component } : {}),
|
|
917
|
+
...(call.action ? { action: call.action } : {}),
|
|
858
918
|
...(repo ? { repo } : {}),
|
|
859
919
|
}),
|
|
860
920
|
});
|
|
@@ -912,7 +972,31 @@ async function describeComponent(root, name) {
|
|
|
912
972
|
break;
|
|
913
973
|
}
|
|
914
974
|
if (!recipe) {
|
|
915
|
-
|
|
975
|
+
/**
|
|
976
|
+
* O EVENTO MAIS VALIOSO DA PLATAFORMA, e até 18/08 ele não era contado.
|
|
977
|
+
*
|
|
978
|
+
* Aqui é onde o agente descobre que vai ter que improvisar. A frase já
|
|
979
|
+
* existia; o que não existia era o REGISTRO - então ninguém sabia quantas
|
|
980
|
+
* vezes, nem para quê. É este número que decide se compor conhecimento
|
|
981
|
+
* vale ser construído, e para quais famílias primeiro.
|
|
982
|
+
*/
|
|
983
|
+
return {
|
|
984
|
+
/**
|
|
985
|
+
* ANTES ESTA FRASE TERMINAVA EM "não escreva do zero" E DEIXAVA O AGENTE
|
|
986
|
+
* SOZINHO - e o `request_component` completava com "siga com a sua solução
|
|
987
|
+
* alternativa". Juntas, elas diziam à esteira para registrar o improviso
|
|
988
|
+
* em vez de guiá-lo (dono, 18/08).
|
|
989
|
+
*
|
|
990
|
+
* Não ter a receita não é cair no zero: as camadas base existem, e agora
|
|
991
|
+
* há um playbook que as percorre em ordem. A frase aponta para ele.
|
|
992
|
+
*/
|
|
993
|
+
body: `This system has no component called "${name}". Run list_components to see what it does have - a near miss still means composing WITH it rather than around it.\n\nIf nothing covers it, do not write from scratch and do not improvise: call playbook { "skill": "compose" }. It walks what this system DOES have, in order - its rules and voice, its tokens, the family this belongs to and what that family requires, and the laws that decide the markup. File request_component along the way so the gap becomes a decision somebody can make.`,
|
|
994
|
+
source: {
|
|
995
|
+
kind: "none",
|
|
996
|
+
reason: "recipe",
|
|
997
|
+
detail: `no recipe for "${name}"`,
|
|
998
|
+
},
|
|
999
|
+
};
|
|
916
1000
|
}
|
|
917
1001
|
const out = [
|
|
918
1002
|
`${name}${recipe.description ? ` - ${recipe.description}` : ""}`,
|
|
@@ -1036,53 +1120,168 @@ async function describeComponent(root, name) {
|
|
|
1036
1120
|
if (laws.length === 0 && needed.length === 0) {
|
|
1037
1121
|
out.push("", "No rules govern this component yet. Build with it, and if you find yourself working around it, file request_component with the reasoning.");
|
|
1038
1122
|
}
|
|
1039
|
-
return out.join("\n");
|
|
1123
|
+
return fromContract(out.join("\n"));
|
|
1040
1124
|
}
|
|
1041
1125
|
async function addComponent(root, name) {
|
|
1042
1126
|
const { table } = await loadSystem(root);
|
|
1043
|
-
if (!table.slug)
|
|
1044
|
-
return
|
|
1127
|
+
if (!table.slug) {
|
|
1128
|
+
return {
|
|
1129
|
+
body: "No design system installed here.",
|
|
1130
|
+
source: { kind: "none", reason: "system", detail: "no system installed" },
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1045
1133
|
const { out } = await capturing(() => component(table.slug, name, { dir: root }));
|
|
1046
|
-
return `${out}\n\nIt is real code in this project now - import it and extend it rather than writing your own
|
|
1134
|
+
return fromContract(`${out}\n\nIt is real code in this project now - import it and extend it rather than writing your own.`);
|
|
1047
1135
|
}
|
|
1048
1136
|
// ── The protocol ─────────────────────────────────────────────────────────────
|
|
1137
|
+
/**
|
|
1138
|
+
* AS FERRAMENTAS QUE FALAM DE UM COMPONENTE PELO NOME - as únicas cujo alvo é
|
|
1139
|
+
* um slug de receita, e por isso as únicas que gravam o alvo.
|
|
1140
|
+
*
|
|
1141
|
+
* Um valor como `#2563eb` (o alvo de `find_token`) não é nome de componente e
|
|
1142
|
+
* seria recusado pela rota, que valida a coluna como slug. Então o alvo só
|
|
1143
|
+
* viaja quando é um: para o resto, o eixo da procedência é a própria
|
|
1144
|
+
* ferramenta.
|
|
1145
|
+
*/
|
|
1146
|
+
const BY_COMPONENT = {
|
|
1147
|
+
describe_component: "describe",
|
|
1148
|
+
add_component: "add",
|
|
1149
|
+
};
|
|
1150
|
+
/**
|
|
1151
|
+
* O ÚNICO PONTO DE INSTRUMENTAÇÃO - e é isto que o dono pediu (18/08): *"vira
|
|
1152
|
+
* comportamento padrão e não depende de disciplina futura."*
|
|
1153
|
+
*
|
|
1154
|
+
* A ferramenta responde, a chamada é registrada com a procedência daquela
|
|
1155
|
+
* resposta, e a linha `Source:` é colada ao texto a partir do MESMO dado que
|
|
1156
|
+
* foi gravado. Não há como o número e a frase discordarem, porque não há duas
|
|
1157
|
+
* fontes.
|
|
1158
|
+
*
|
|
1159
|
+
* Uma ferramenta nova entra no switch e passa a contar sem tocar nesta função.
|
|
1160
|
+
*/
|
|
1049
1161
|
async function callTool(root, name, args,
|
|
1050
|
-
/** A versão que reporta o estado do repo no ping - ver `
|
|
1162
|
+
/** A versão que reporta o estado do repo no ping - ver `repoStateOf`. */
|
|
1051
1163
|
cli) {
|
|
1164
|
+
const answer = await runTool(root, name, args);
|
|
1165
|
+
const gesture = BY_COMPONENT[name];
|
|
1166
|
+
const target = gesture
|
|
1167
|
+
? String(args.name ?? "")
|
|
1168
|
+
.trim()
|
|
1169
|
+
.toLowerCase()
|
|
1170
|
+
: "";
|
|
1171
|
+
recordAgentCall(root, {
|
|
1172
|
+
tool: name,
|
|
1173
|
+
source: answer.source,
|
|
1174
|
+
...(gesture ? { action: gesture } : {}),
|
|
1175
|
+
...(target ? { component: target } : {}),
|
|
1176
|
+
}, cli);
|
|
1177
|
+
return text(withSource(answer), answer.isError);
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* O SWITCH PURO - ele resolve a ferramenta e devolve a resposta COM a
|
|
1181
|
+
* procedência. Nenhuma linha dele conta nada: quem instrumenta é `callTool`,
|
|
1182
|
+
* uma vez, para todas.
|
|
1183
|
+
*/
|
|
1184
|
+
async function runTool(root, name, args) {
|
|
1052
1185
|
switch (name) {
|
|
1053
1186
|
case "check_file":
|
|
1054
|
-
return
|
|
1187
|
+
return checkFile(root, String(args.path ?? "."));
|
|
1055
1188
|
case "find_token":
|
|
1056
|
-
return
|
|
1189
|
+
return findToken(root, String(args.value ?? ""));
|
|
1057
1190
|
case "system_doctrine":
|
|
1058
|
-
return
|
|
1191
|
+
return fromContract(await systemDoctrine(root));
|
|
1059
1192
|
case "list_components":
|
|
1060
|
-
return
|
|
1193
|
+
return fromContract(await listComponents(root));
|
|
1061
1194
|
case "describe_component":
|
|
1062
|
-
|
|
1063
|
-
return text(await describeComponent(root, String(args.name ?? "")));
|
|
1195
|
+
return describeComponent(root, String(args.name ?? ""));
|
|
1064
1196
|
case "add_component":
|
|
1065
|
-
|
|
1066
|
-
return text(await addComponent(root, String(args.name ?? "")));
|
|
1197
|
+
return addComponent(root, String(args.name ?? ""));
|
|
1067
1198
|
case "playbook": {
|
|
1068
1199
|
const skill = String(args?.skill ?? "");
|
|
1069
1200
|
const section = args?.section ? String(args.section) : undefined;
|
|
1070
|
-
return
|
|
1201
|
+
return fromContract(await playbook(skill, section));
|
|
1071
1202
|
}
|
|
1072
1203
|
case "recipe_vocabulary":
|
|
1073
|
-
return
|
|
1204
|
+
return fromContract(await recipeVocabulary());
|
|
1074
1205
|
case "validate_recipe": {
|
|
1075
1206
|
// Same resolution as the batch: an `at` the validator cannot see reads as
|
|
1076
1207
|
// an empty node and earns a false floor verdict (dono, 01/08).
|
|
1077
1208
|
const [only] = await resolveAtNodes(root, [
|
|
1078
1209
|
{ name: String(args.name ?? "this component"), recipe: args.recipe },
|
|
1079
1210
|
]);
|
|
1080
|
-
return
|
|
1211
|
+
return fromContract(await validateRecipe(String(args.name ?? "this component"), only?.recipe ?? args.recipe));
|
|
1081
1212
|
}
|
|
1082
1213
|
case "validate_recipes":
|
|
1083
|
-
return
|
|
1214
|
+
return fromContract(await validateRecipes(root, Array.isArray(args.recipes)
|
|
1084
1215
|
? args.recipes
|
|
1085
1216
|
: []));
|
|
1217
|
+
case "compose_context": {
|
|
1218
|
+
/**
|
|
1219
|
+
* A ORQUESTRAÇÃO - e o que ela reúne é do SISTEMA DELE, com o nosso
|
|
1220
|
+
* esqueleto de julgamento por cima.
|
|
1221
|
+
*
|
|
1222
|
+
* Uma chamada em vez de cinco, e a procedência disto é `composed`: o
|
|
1223
|
+
* primeiro caminho da esteira que emite esse kind, que até aqui existia no
|
|
1224
|
+
* vocabulário e não em resposta nenhuma.
|
|
1225
|
+
*/
|
|
1226
|
+
const asked = String(args.family ?? "")
|
|
1227
|
+
.trim()
|
|
1228
|
+
.toLowerCase();
|
|
1229
|
+
const intent = String(args.intent ?? "").trim();
|
|
1230
|
+
if (!isFamily(asked)) {
|
|
1231
|
+
return {
|
|
1232
|
+
body: noFamilyAnswer(asked || "(nothing)"),
|
|
1233
|
+
source: {
|
|
1234
|
+
kind: "none",
|
|
1235
|
+
reason: "family",
|
|
1236
|
+
detail: `no family fits "${asked}"`,
|
|
1237
|
+
},
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
const { documents, table, doctrines, requires } = await loadSystem(root);
|
|
1241
|
+
if (documents.length === 0) {
|
|
1242
|
+
return {
|
|
1243
|
+
body: "No design system installed here, so there is nothing to compose from. Run `synthesisui add <slug>` first - composing against nothing would be inventing.",
|
|
1244
|
+
source: {
|
|
1245
|
+
kind: "none",
|
|
1246
|
+
reason: "system",
|
|
1247
|
+
detail: "no system installed",
|
|
1248
|
+
},
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
const plan = composePlan({
|
|
1252
|
+
documents,
|
|
1253
|
+
table,
|
|
1254
|
+
rules: doctrines.flatMap((d) => d.rules),
|
|
1255
|
+
/**
|
|
1256
|
+
* A regra tem a FRASE e o PACOTE em campos separados (`text`,
|
|
1257
|
+
* `requires`) - mandar a frase sozinha esconderia o que instalar, e
|
|
1258
|
+
* mandar só o pacote esconderia o porquê.
|
|
1259
|
+
*/
|
|
1260
|
+
requires: requires.map((r) => ({
|
|
1261
|
+
rule: r.requires ? `${r.requires} - ${r.text}` : r.text,
|
|
1262
|
+
})),
|
|
1263
|
+
family: asked,
|
|
1264
|
+
intent,
|
|
1265
|
+
});
|
|
1266
|
+
/** O checklist é servido; sem rede o resto do plano continua valendo. */
|
|
1267
|
+
const answer = await askCatalogue("vocabulary");
|
|
1268
|
+
const v = answer.ok
|
|
1269
|
+
? answer.body
|
|
1270
|
+
: null;
|
|
1271
|
+
const vocabulary = v
|
|
1272
|
+
? {
|
|
1273
|
+
floor: v.floor?.[asked] ?? [],
|
|
1274
|
+
checklist: v.checklist?.[asked] ?? [],
|
|
1275
|
+
}
|
|
1276
|
+
: null;
|
|
1277
|
+
return {
|
|
1278
|
+
body: renderPlan(plan, vocabulary),
|
|
1279
|
+
source: {
|
|
1280
|
+
kind: "composed",
|
|
1281
|
+
detail: `${asked} · ${plan.siblings.length} recipe${plan.siblings.length === 1 ? "" : "s"} and ${plan.tokens.length} token${plan.tokens.length === 1 ? "" : "s"} from this system`,
|
|
1282
|
+
},
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1086
1285
|
case "request_component": {
|
|
1087
1286
|
const r = await fileRequest(root, {
|
|
1088
1287
|
kind: "component",
|
|
@@ -1091,7 +1290,7 @@ cli) {
|
|
|
1091
1290
|
considered: args.considered ? String(args.considered) : undefined,
|
|
1092
1291
|
file: args.file ? String(args.file) : undefined,
|
|
1093
1292
|
});
|
|
1094
|
-
return
|
|
1293
|
+
return fromContract(`Filed as ${r.id}. It shows up in \`synthesisui doctor\` until a person authors it or closes it.\n\nNow build it properly rather than working around it: playbook { "skill": "compose" } walks the layers this system already declares, so what you hand back matches it even without a recipe. Say in your summary that the request exists, and which parts were composed rather than taken from the system.`);
|
|
1095
1294
|
}
|
|
1096
1295
|
case "refresh_system": {
|
|
1097
1296
|
/**
|
|
@@ -1111,10 +1310,10 @@ cli) {
|
|
|
1111
1310
|
*/
|
|
1112
1311
|
const slug = await installedSlug(root);
|
|
1113
1312
|
if (!slug)
|
|
1114
|
-
return
|
|
1313
|
+
return fromContract("No system installed here that belongs to a dashboard row, so there is nothing to refresh. `synthesisui add <slug>` installs one; an adopted system is described here and owned elsewhere.");
|
|
1115
1314
|
const token = await readToken();
|
|
1116
1315
|
if (!token)
|
|
1117
|
-
return
|
|
1316
|
+
return fromContract("Not signed in, so nothing could be sent. A person has to run `synthesisui login` once in the terminal - it opens a browser, and I cannot complete it for them.");
|
|
1118
1317
|
const { out } = await capturing(() => remeasure({
|
|
1119
1318
|
root,
|
|
1120
1319
|
base: resolveRegistry(),
|
|
@@ -1137,7 +1336,7 @@ cli) {
|
|
|
1137
1336
|
const behind = pinned && pinned !== VERSION ? pinned : null;
|
|
1138
1337
|
const said = out.trim() ||
|
|
1139
1338
|
"Nothing was reported by the refresh - which means it found nothing to measure.";
|
|
1140
|
-
return
|
|
1339
|
+
return fromContract(behind
|
|
1141
1340
|
? `${said}\n\nUPDATE THE DESIGN SYSTEM BEFORE CONTINUING.\nThe check that runs after your writes is pinned to ${behind} and these tools are ${VERSION}.\n\n npx synthesisui@latest connect\n\nAsk the person, run it for them, and then say this: close and reopen the editor. The hook and the tools only rewire on a new session, so nothing you write before that is checked by the reader you are holding.`
|
|
1142
1341
|
: said);
|
|
1143
1342
|
}
|
|
@@ -1159,7 +1358,7 @@ cli) {
|
|
|
1159
1358
|
file: args.file ? String(args.file) : undefined,
|
|
1160
1359
|
...(area ? { area } : null),
|
|
1161
1360
|
});
|
|
1162
|
-
return
|
|
1361
|
+
return fromContract(area
|
|
1163
1362
|
? `Filed as ${r.id} and routed to the synthesisui platform team - the system's contract already promises this and the shipped css does not deliver it. No one needs to act: it closes itself when an update lands. Keep the quiet base meanwhile.`
|
|
1164
1363
|
: `Filed as ${r.id}. Do not add the token yourself - the request shows up in \`synthesisui doctor\` for a person to decide.`);
|
|
1165
1364
|
}
|
|
@@ -1179,10 +1378,14 @@ cli) {
|
|
|
1179
1378
|
considered: args.considered ? String(args.considered) : undefined,
|
|
1180
1379
|
file: args.file ? String(args.file) : undefined,
|
|
1181
1380
|
});
|
|
1182
|
-
return
|
|
1381
|
+
return fromContract(`Filed as ${r.id}. Do not adopt the convention as if it were a rule - it shows up in \`synthesisui doctor\` and travels to the system's queue, for the owner to make it a rule or decline it.`);
|
|
1183
1382
|
}
|
|
1184
1383
|
default:
|
|
1185
|
-
return
|
|
1384
|
+
return {
|
|
1385
|
+
body: `No tool named ${name}.`,
|
|
1386
|
+
source: { kind: "none", reason: "tool", detail: "unknown tool" },
|
|
1387
|
+
isError: true,
|
|
1388
|
+
};
|
|
1186
1389
|
}
|
|
1187
1390
|
}
|
|
1188
1391
|
/**
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O CONTEXTO MÍNIMO PARA CONSTRUIR ALGO QUE O SISTEMA NÃO TEM.
|
|
3
|
+
*
|
|
4
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* AS DUAS FONTES, E A PROPORÇÃO ENTRE ELAS SE INVERTE SOZINHA
|
|
6
|
+
*
|
|
7
|
+
* O material que enche este contexto é DELE - os tokens que as receitas dele já
|
|
8
|
+
* usam, as regras que ele declarou, as bibliotecas que o repositório dele
|
|
9
|
+
* carrega. É isso que faz o componente novo nascer parecido com o sistema dele
|
|
10
|
+
* em vez de parecido com o nosso gosto.
|
|
11
|
+
*
|
|
12
|
+
* O que é NOSSO é pequeno de propósito e não fala de nenhum componente
|
|
13
|
+
* específico: as sete famílias, o checklist de cada uma e as leis de marcação.
|
|
14
|
+
* Sem essa metade, um sistema de dois componentes - e existem quatro deles em
|
|
15
|
+
* produção, medido em 18/08 - não teria nada a oferecer justamente a quem mais
|
|
16
|
+
* precisa de orientação.
|
|
17
|
+
*
|
|
18
|
+
* Sistema maduro: "o seu input usa estes sete tokens, use os mesmos" (paridade).
|
|
19
|
+
* Sistema novo: "um campo precisa destas cinco coisas, o seu sistema ainda não
|
|
20
|
+
* nomeia três delas" (orientação, com a lacuna declarada).
|
|
21
|
+
*
|
|
22
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
23
|
+
* A FAMÍLIA VEM DE QUEM PEDE, e a plataforma valida (decisão do dono, 18/08)
|
|
24
|
+
*
|
|
25
|
+
* Quem chama esta ferramenta é um modelo que acabou de ler o pedido em linguagem
|
|
26
|
+
* natural. Ele classifica melhor que qualquer heurística de palavras que a gente
|
|
27
|
+
* escrevesse aqui, e classificar com IA hospedada gastaria Ink para responder o
|
|
28
|
+
* que já foi respondido do outro lado da chamada.
|
|
29
|
+
*
|
|
30
|
+
* Então o agente informa a família, a gente confere contra o contrato, e quando
|
|
31
|
+
* ela não é nenhuma das sete isso não é erro: é uma lacuna de FAMÍLIA, que é uma
|
|
32
|
+
* decisão diferente de um componente faltando.
|
|
33
|
+
*/
|
|
34
|
+
/** As sete que o contrato conhece - o mesmo enum de `componentPreviewKind`. */
|
|
35
|
+
export const FAMILIES = [
|
|
36
|
+
"action",
|
|
37
|
+
"control",
|
|
38
|
+
"field",
|
|
39
|
+
"surface",
|
|
40
|
+
"pill",
|
|
41
|
+
"indicator",
|
|
42
|
+
"text",
|
|
43
|
+
];
|
|
44
|
+
export function isFamily(value) {
|
|
45
|
+
return FAMILIES.includes(value);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* OS TOKENS QUE ESTA FAMÍLIA JÁ USA, lidos das receitas dela.
|
|
49
|
+
*
|
|
50
|
+
* Uma referência de token num documento é `{color.semantic.canvas}` - a mesma
|
|
51
|
+
* forma que o compilador resolve. Então "quais tokens um campo usa neste
|
|
52
|
+
* sistema" é uma pergunta que o próprio documento responde, sem tabela nossa e
|
|
53
|
+
* sem palpite.
|
|
54
|
+
*
|
|
55
|
+
* MEDIDO no signalui (37 componentes, 74 referências no total): field 37,
|
|
56
|
+
* surface 37, control 33, action 24, pill 17, text 15, indicator 12. O corte por
|
|
57
|
+
* volume é modesto num sistema pequeno - o valor aqui não é economia de
|
|
58
|
+
* contexto, é PARIDADE: o agente copia o que existe em vez de escolher.
|
|
59
|
+
*/
|
|
60
|
+
export function tokensOfFamily(documents, family) {
|
|
61
|
+
const refs = new Set();
|
|
62
|
+
for (const doc of documents) {
|
|
63
|
+
const components = doc
|
|
64
|
+
.components;
|
|
65
|
+
if (!components)
|
|
66
|
+
continue;
|
|
67
|
+
for (const recipe of Object.values(components)) {
|
|
68
|
+
if (recipe?.preview?.kind !== family)
|
|
69
|
+
continue;
|
|
70
|
+
for (const ref of JSON.stringify(recipe).match(/\{[a-z0-9.]+\}/gi) ??
|
|
71
|
+
[]) {
|
|
72
|
+
/**
|
|
73
|
+
* `{onPressedChange}` e afins aparecem no mesmo formato e NÃO são
|
|
74
|
+
* tokens - são nomes de prop que a receita cita. Um token tem caminho:
|
|
75
|
+
* família, e depois pelo menos um degrau.
|
|
76
|
+
*/
|
|
77
|
+
if (ref.includes("."))
|
|
78
|
+
refs.add(ref);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [...refs].sort();
|
|
83
|
+
}
|
|
84
|
+
/** As receitas desta família que o sistema já tem - de onde copiar. */
|
|
85
|
+
export function recipesOfFamily(documents, family) {
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const doc of documents) {
|
|
88
|
+
const components = doc
|
|
89
|
+
.components;
|
|
90
|
+
if (!components)
|
|
91
|
+
continue;
|
|
92
|
+
for (const [name, recipe] of Object.entries(components)) {
|
|
93
|
+
if (recipe?.preview?.kind === family) {
|
|
94
|
+
out.push({
|
|
95
|
+
name,
|
|
96
|
+
...(recipe.description ? { description: recipe.description } : null),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* O VALOR DE CADA REFERÊNCIA, quando a tabela o conhece.
|
|
105
|
+
*
|
|
106
|
+
* `{radius.md}` sozinho não diz nada a quem vai escrever css; `{radius.md} =
|
|
107
|
+
* 0.5rem` diz. A tabela de tokens é a mesma que o `find_token` consulta, então
|
|
108
|
+
* a resposta aqui e a resposta dele nunca divergem.
|
|
109
|
+
*/
|
|
110
|
+
export function valueOf(table, ref) {
|
|
111
|
+
const path = ref.slice(1, -1);
|
|
112
|
+
/** `color.semantic.canvas` -> `--ds-color-semantic-canvas`. */
|
|
113
|
+
const name = `--ds-${path.replace(/\./g, "-")}`;
|
|
114
|
+
return table.byName.get(name) ?? null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* A MONTAGEM - pura, e é por isso que ela mora fora do handler.
|
|
118
|
+
*
|
|
119
|
+
* O handler faz I/O (`loadSystem`, a rede do catalogue). O que decide o que
|
|
120
|
+
* entra no contexto é isto, e é o que um spec consegue exercer com um documento
|
|
121
|
+
* de verdade sem subir nada.
|
|
122
|
+
*/
|
|
123
|
+
export function composePlan(input) {
|
|
124
|
+
const refs = tokensOfFamily(input.documents, input.family);
|
|
125
|
+
return {
|
|
126
|
+
family: input.family,
|
|
127
|
+
intent: input.intent,
|
|
128
|
+
siblings: recipesOfFamily(input.documents, input.family),
|
|
129
|
+
tokens: refs.map((ref) => ({ ref, value: valueOf(input.table, ref) })),
|
|
130
|
+
rules: [...input.rules],
|
|
131
|
+
requires: [...input.requires],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* O PLANO COMO O AGENTE O LÊ - e a ordem é a das camadas, não a da comodidade.
|
|
136
|
+
*
|
|
137
|
+
* Cada bloco diz de onde veio: o que é do sistema dele, o que é checklist nosso,
|
|
138
|
+
* e o que ninguém decidiu ainda. É a mesma gramática da linha `Source:` que toda
|
|
139
|
+
* resposta carrega, um nível abaixo - e é o que permite ao agente relatar
|
|
140
|
+
* procedência por parte em vez de por resposta.
|
|
141
|
+
*/
|
|
142
|
+
export function renderPlan(plan,
|
|
143
|
+
/** O checklist e o piso desta família, servidos do catálogo. `null` sem rede. */
|
|
144
|
+
vocabulary) {
|
|
145
|
+
const out = [
|
|
146
|
+
`COMPOSING: ${plan.intent || plan.family}`,
|
|
147
|
+
`FAMILY: ${plan.family} - every part below answers to this family's checklist.`,
|
|
148
|
+
"",
|
|
149
|
+
];
|
|
150
|
+
out.push("── FROM THIS SYSTEM ────────────────────────────────────────");
|
|
151
|
+
if (plan.siblings.length > 0) {
|
|
152
|
+
out.push(`Recipes in this family, to copy from rather than to choose against (${plan.siblings.length}):`, ...plan.siblings.map((s) => ` ${s.name}${s.description ? ` - ${s.description}` : ""}`));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
out.push("This system has NO recipe in this family yet. Nothing here to copy, so", "every decision below is one you are making for the first time - name them", "in your summary, and file what the system should have named.");
|
|
156
|
+
}
|
|
157
|
+
out.push("");
|
|
158
|
+
if (plan.tokens.length > 0) {
|
|
159
|
+
out.push(`Tokens those recipes already use (${plan.tokens.length}) - use these before any literal:`, ...plan.tokens.map((t) => ` ${t.ref}${t.value ? ` = ${t.value}` : " (name declared, value not on disk)"}`));
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
out.push("No token is used by this family yet. Ask `find_token` for each value you", "need, and file `request_token` for the ones nothing holds - never invent a name.");
|
|
163
|
+
}
|
|
164
|
+
if (plan.rules.length > 0) {
|
|
165
|
+
out.push("", `Rules - maximum authority, ${plan.rules.length} of them:`, ...plan.rules.map((r) => ` - ${r}`));
|
|
166
|
+
}
|
|
167
|
+
if (plan.requires.length > 0) {
|
|
168
|
+
out.push("", "Packages this system says the project needs:", ...plan.requires.map((r) => ` - ${r.rule}`));
|
|
169
|
+
}
|
|
170
|
+
out.push("", "── THE FLOOR FOR THIS FAMILY ───────────────────────────────");
|
|
171
|
+
if (vocabulary) {
|
|
172
|
+
if (vocabulary.floor.length > 0) {
|
|
173
|
+
out.push("Below this it cannot be told apart from the page:");
|
|
174
|
+
out.push(...vocabulary.floor.map((f) => ` ${f}`));
|
|
175
|
+
}
|
|
176
|
+
out.push("", "Work this list and say which items this system's own recipes answer:", ...vocabulary.checklist.map((c) => ` - ${c}`));
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
out.push("The checklist is served from the platform and could not be reached right", "now. Everything above still holds - fetch it with `recipe_vocabulary`", "before you call the component finished.");
|
|
180
|
+
}
|
|
181
|
+
out.push("", "── WHAT IS NOT HERE ────────────────────────────────────────", "The laws that decide the markup (which tag, whose layout wins, why", "accessibility moves the ink) are one fetch away and not repeated here:", ' playbook { "skill": "compose", "section": "path-2-layer-3-the-laws-that-decide-the-markup" }', "", "The voice - how this product speaks - is not in this answer either. Call", "`system_doctrine` before writing any user-facing copy inside this component.", "", "When you are done: `validate_recipe` if you are writing one, then", "`request_component` for what the system should have had, and report which", "parts came from the system and which you composed.");
|
|
182
|
+
return out.join("\n");
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* A RESPOSTA QUANDO A FAMÍLIA NÃO É NENHUMA DAS SETE.
|
|
186
|
+
*
|
|
187
|
+
* Não é erro e não é uma lista de sugestões parecidas: é um fato sobre o
|
|
188
|
+
* contrato, e o pedido de quem o lê é diferente do pedido de um componente. Um
|
|
189
|
+
* gráfico, um mapa, uma grade de calendário não são `surface` mal classificado -
|
|
190
|
+
* forçá-los ali produziria um piso que não significa nada.
|
|
191
|
+
*/
|
|
192
|
+
export function noFamilyAnswer(asked) {
|
|
193
|
+
return [
|
|
194
|
+
`"${asked}" is not one of the seven families this contract can draw: ${FAMILIES.join(", ")}.`,
|
|
195
|
+
"",
|
|
196
|
+
"That is a real answer, not a rejection. Two things are true at once:",
|
|
197
|
+
"",
|
|
198
|
+
" 1. Everything ABOVE the family layer still holds in full - the system's",
|
|
199
|
+
" rules, its tokens, and the laws of markup. Ask `find_token` per value,",
|
|
200
|
+
" `system_doctrine` for the rules, and fetch the laws chapter of the",
|
|
201
|
+
" compose playbook.",
|
|
202
|
+
" 2. What is missing is a KIND, not a component - and that is a different",
|
|
203
|
+
" decision for the system's owner. File it:",
|
|
204
|
+
" request_component with what the thing is and why no family fits.",
|
|
205
|
+
"",
|
|
206
|
+
"Then decompose: parts of it probably DO have families. A region that holds",
|
|
207
|
+
"things is a surface, a control inside it is a control - ask again for those",
|
|
208
|
+
"parts, and compose the rest without a family, saying so.",
|
|
209
|
+
].join("\n");
|
|
210
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O STUB DA SKILL DE COMPOSIÇÃO - o corpo é SERVIDO, como os outros três.
|
|
3
|
+
*
|
|
4
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* POR QUE ESTA SKILL EXISTE (dono, 18/08)
|
|
6
|
+
*
|
|
7
|
+
* As três primeiras cobrem trazer um sistema para dentro e mantê-lo. Nenhuma
|
|
8
|
+
* cobria o gesto mais comum do dia a dia: alguém pede um componente que o
|
|
9
|
+
* sistema não tem.
|
|
10
|
+
*
|
|
11
|
+
* E o que a plataforma respondia nesse caso eram duas frases, medidas:
|
|
12
|
+
* `describe_component` dizia *"não escreva do zero"* e `request_component`
|
|
13
|
+
* dizia, na frase seguinte, *"siga com a sua solução alternativa"*. Ou seja, a
|
|
14
|
+
* esteira registrava que o agente improvisou em vez de guiar a composição - o
|
|
15
|
+
* oposto de uma hierarquia de conhecimento.
|
|
16
|
+
*
|
|
17
|
+
* A tese do dono: não ter a receita não é cair no zero. As camadas base
|
|
18
|
+
* existem - tokens, regras, a família e o que o tipo dela exige, as leis de
|
|
19
|
+
* marcação - e compor a partir delas é tão valioso quanto ter a receita pronta.
|
|
20
|
+
*/
|
|
21
|
+
export const COMPOSE_SKILL_PATH = ".claude/skills/sui-compose/SKILL.md";
|
|
22
|
+
export const COMPOSE_SKILL = '---\nname: sui-compose\ndescription: Build ANY UI element the installed design system has no recipe for - without inventing values, tags or accessibility. Use whenever you are about to write a UI element from scratch, when `describe_component` answers that the system does not have it, or when what was asked for is a composite nobody has written yet - whatever it is called (an editor, a palette, a table, a chart, a layout region: the playbook does not care which). Walks the layers the system DOES have, in order, decomposes the request into families, and declares what it could not cover.\n---\n\n# Compose what the system does not have - served live\n\nThis playbook is served from the platform, not shipped in this file - it is\nalways current, and your context only carries the step you are on.\n\n1. Call the `playbook` tool on the `synthesisui` MCP server with\n { "skill": "compose" } - you get the framing and a table of contents.\n2. Fetch ONLY the chapter for the step you are on:\n { "skill": "compose", "section": "<id from the toc>" }. Never fetch more\n than the current step needs.\n3. Follow it exactly. When the step is done, fetch the next chapter.\n\nThe first chapter tells you which of the two paths you are on - the system has\na recipe, or nobody has written one yet - and the rest walk the layers in order.\n\nIf the tool answers that you are not signed in, run `npx synthesisui login`\nin the terminal and call it again. If the `synthesisui` MCP server is not\navailable at all, run `npx synthesisui connect`, restart the session, and\ninvoke this skill again.\n';
|
package/dist/skills.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ADAPT_SKILL, ADAPT_SKILL_PATH } from "./skill-adapt.js";
|
|
2
|
+
import { COMPOSE_SKILL, COMPOSE_SKILL_PATH } from "./skill-compose.js";
|
|
2
3
|
import { IMPORT_SKILL, IMPORT_SKILL_PATH } from "./skill-import.js";
|
|
3
4
|
import { INIT_SKILL, INIT_SKILL_PATH } from "./skill-init.js";
|
|
4
5
|
/**
|
|
@@ -36,4 +37,20 @@ export const SKILLS = [
|
|
|
36
37
|
label: "/sui-adapt",
|
|
37
38
|
what: "one component against the system, and what to do about it",
|
|
38
39
|
},
|
|
40
|
+
/**
|
|
41
|
+
* A DE CONSTRUÇÃO, e ela é a que responde ao pedido mais comum de todos:
|
|
42
|
+
* "faz um componente X" quando o sistema não tem X.
|
|
43
|
+
*
|
|
44
|
+
* As três acima são sobre o sistema - trazer, manter, conferir. Esta é sobre o
|
|
45
|
+
* TRABALHO de quem usa o sistema, e é a única que roda todo dia. Ela existe
|
|
46
|
+
* porque a resposta anterior a esse pedido era o agente improvisar sozinho: a
|
|
47
|
+
* esteira dizia "não escreva do zero" e, na frase seguinte, "siga com a sua
|
|
48
|
+
* solução alternativa" (dono, 18/08).
|
|
49
|
+
*/
|
|
50
|
+
{
|
|
51
|
+
path: COMPOSE_SKILL_PATH,
|
|
52
|
+
source: COMPOSE_SKILL,
|
|
53
|
+
label: "/sui-compose",
|
|
54
|
+
what: "build what the system does not have, layer by layer",
|
|
55
|
+
},
|
|
39
56
|
];
|
package/package.json
CHANGED