synthesisui 0.16.261 → 0.16.264
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/claude-md.js +117 -10
- package/dist/commands/import.js +38 -3
- package/dist/commands/mcp.js +9 -0
- package/dist/commands/sync.js +73 -0
- package/dist/doctor/architecture.js +65 -3
- package/dist/index.js +13 -1
- package/dist/install-marks.js +16 -1
- package/dist/memory/availability.js +71 -0
- package/dist/memory/contract.js +106 -0
- package/dist/memory/observation.js +77 -0
- package/dist/memory/predicate.js +243 -0
- package/dist/memory/recall.js +161 -0
- package/dist/memory/report.js +31 -0
- package/dist/memory/snapshot.js +47 -0
- package/dist/memory/tools.js +269 -0
- package/dist/memory/work-state.js +131 -0
- package/dist/memory/write.js +139 -0
- package/dist/naming-queue.js +56 -0
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O CONTRATO DA MEMÓRIA - e é a ÚNICA coisa desta feature que existe nos dois lados.
|
|
3
|
+
*
|
|
4
|
+
* O CLI é publicado standalone e não pode importar os contratos da plataforma, então este arquivo é
|
|
5
|
+
* um GÊMEO BYTE-IDÊNTICO de `apps/web/src/lib/memory/contract.ts`, garantido por spec - o mesmo
|
|
6
|
+
* padrão de `frontier-kind.ts`.
|
|
7
|
+
*
|
|
8
|
+
* A REGRA QUE DECIDE O QUE ENTRA AQUI (dono, 20/08):
|
|
9
|
+
*
|
|
10
|
+
* contrato pode ser duplicado e verificado; LÓGICA nunca.
|
|
11
|
+
*
|
|
12
|
+
* Entram tipos, enums, códigos de recusa e a metadata declarativa dos predicados. NÃO entram
|
|
13
|
+
* `evaluate`, `compareProgress`, `canonicalize`, `recall` nem qualquer função que decida algo: cada
|
|
14
|
+
* uma dessas tem um dono só, e duplicar semântica é como duas implementações começam a divergir sem
|
|
15
|
+
* ninguém perceber.
|
|
16
|
+
*
|
|
17
|
+
* E a divisão que este arquivo serve:
|
|
18
|
+
*
|
|
19
|
+
* O CLI OBSERVA E TESTEMUNHA. A PLATAFORMA LEMBRA E DECIDE O EFEITO DA EVIDÊNCIA.
|
|
20
|
+
*/
|
|
21
|
+
export const CATALOGUE = {
|
|
22
|
+
reference_replaced: {
|
|
23
|
+
needs: ["from", "to"],
|
|
24
|
+
requiresAlive: ["subject"],
|
|
25
|
+
requiresReadableStyles: false,
|
|
26
|
+
},
|
|
27
|
+
inbound_references_zero: {
|
|
28
|
+
needs: [],
|
|
29
|
+
requiresAlive: [],
|
|
30
|
+
requiresReadableStyles: false,
|
|
31
|
+
progress: { field: "remaining", direction: "decrease" },
|
|
32
|
+
},
|
|
33
|
+
reference_present: {
|
|
34
|
+
needs: ["to"],
|
|
35
|
+
requiresAlive: ["subject"],
|
|
36
|
+
requiresReadableStyles: false,
|
|
37
|
+
},
|
|
38
|
+
reference_absent: {
|
|
39
|
+
needs: ["from"],
|
|
40
|
+
requiresAlive: ["subject"],
|
|
41
|
+
requiresReadableStyles: false,
|
|
42
|
+
},
|
|
43
|
+
token_present: {
|
|
44
|
+
needs: ["to"],
|
|
45
|
+
requiresAlive: [],
|
|
46
|
+
requiresReadableStyles: true,
|
|
47
|
+
},
|
|
48
|
+
token_absent: {
|
|
49
|
+
needs: ["from"],
|
|
50
|
+
requiresAlive: [],
|
|
51
|
+
requiresReadableStyles: true,
|
|
52
|
+
},
|
|
53
|
+
component_present: {
|
|
54
|
+
needs: [],
|
|
55
|
+
requiresAlive: [],
|
|
56
|
+
requiresReadableStyles: false,
|
|
57
|
+
},
|
|
58
|
+
component_absent: {
|
|
59
|
+
needs: [],
|
|
60
|
+
requiresAlive: [],
|
|
61
|
+
requiresReadableStyles: false,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
export const DECLARABLE = [
|
|
65
|
+
"developer_declared",
|
|
66
|
+
"agent_inferred",
|
|
67
|
+
"imported",
|
|
68
|
+
];
|
|
69
|
+
/**
|
|
70
|
+
* AS TAREFAS, LISTA FECHADA - derivada do que a plataforma JÁ MEDE.
|
|
71
|
+
*
|
|
72
|
+
* Mesma regra do catálogo: a linguagem cresce quando a capacidade de medição cresce.
|
|
73
|
+
*/
|
|
74
|
+
export const TASKS = [
|
|
75
|
+
"tokens",
|
|
76
|
+
"spacing",
|
|
77
|
+
"typography",
|
|
78
|
+
"contrast",
|
|
79
|
+
"accessibility",
|
|
80
|
+
"migration",
|
|
81
|
+
];
|
|
82
|
+
/**
|
|
83
|
+
* O QUE O AGENTE NÃO PODE PROPOR: "ele pode propor fatos, nunca a autoridade dos fatos".
|
|
84
|
+
*
|
|
85
|
+
* `verdict` e `snapshotId` entraram nesta lista quando a fronteira de medição passou a existir: um
|
|
86
|
+
* agente que pudesse enviar `verdict: "met"` se concederia a autoridade de ter medido.
|
|
87
|
+
*/
|
|
88
|
+
export const DERIVED_FIELDS = [
|
|
89
|
+
"owner",
|
|
90
|
+
"fingerprint",
|
|
91
|
+
"confidence",
|
|
92
|
+
"authority",
|
|
93
|
+
"rung",
|
|
94
|
+
"status",
|
|
95
|
+
"measuredAt",
|
|
96
|
+
"measured_at",
|
|
97
|
+
"lastProgressAt",
|
|
98
|
+
"lastMeasuredAt",
|
|
99
|
+
"evidenceSource",
|
|
100
|
+
"resolution",
|
|
101
|
+
"active",
|
|
102
|
+
"verdict",
|
|
103
|
+
"progress",
|
|
104
|
+
"snapshotId",
|
|
105
|
+
"snapshot_id",
|
|
106
|
+
];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A PONTE ENTRE O CENSO E O AVALIADOR - e ela é LOOKUP, não varredura.
|
|
3
|
+
*
|
|
4
|
+
* Uma condição de saída é avaliada quando existe medição (`import`, `sync`, `doctor`), e nesse
|
|
5
|
+
* momento o censo já está em memória: doze estados abertos são doze consultas a estruturas que a
|
|
6
|
+
* medição acabou de produzir. É por isso que isto cabe no `sync` e NUNCA no hook, que roda depois
|
|
7
|
+
* de cada escrita com orçamento de ~50ms e sem rede.
|
|
8
|
+
*
|
|
9
|
+
* Cada campo aqui responde a uma pergunta do catálogo, e todos vêm de coisas que o censo já
|
|
10
|
+
* carregava antes deste recurso existir:
|
|
11
|
+
*
|
|
12
|
+
* composes `looks[X].composes` - 206 das 323 receitas do dono carregam
|
|
13
|
+
* hasRecipe as chaves de `looks`
|
|
14
|
+
* refusedByGate `skipped` - 400 recusas naquele censo, com o motivo de cada uma
|
|
15
|
+
* tokenDeclared `declared`
|
|
16
|
+
* tokenUses `observed`, que já traz `count` e `token` por valor
|
|
17
|
+
* stylesReadable `coverage.shapes[].reads` - `sx` e `styled` chegam como "none"
|
|
18
|
+
*/
|
|
19
|
+
export function fromCensus(census) {
|
|
20
|
+
const looks = (census.looks ?? {});
|
|
21
|
+
const refused = new Map((census.skipped ?? []).map((s) => [
|
|
22
|
+
s.name,
|
|
23
|
+
{ why: s.why, because: s.because },
|
|
24
|
+
]));
|
|
25
|
+
const declared = new Set(Object.keys(census.declared ?? {}));
|
|
26
|
+
/**
|
|
27
|
+
* O USO POR TOKEN, e não por valor: `observed` lista o valor com a contagem e o token a que ele
|
|
28
|
+
* corresponde, então a soma por token é o que responde "alguém ainda usa isto".
|
|
29
|
+
*/
|
|
30
|
+
const uses = new Map();
|
|
31
|
+
for (const v of census.observed ?? []) {
|
|
32
|
+
const token = v.token;
|
|
33
|
+
if (!token)
|
|
34
|
+
continue;
|
|
35
|
+
uses.set(token, (uses.get(token) ?? 0) + (v.count ?? 0));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A ÂNCORA: alguma forma de escrever estilo está sendo lida de verdade?
|
|
39
|
+
*
|
|
40
|
+
* `reads: "none"` é uma forma que o pipeline VÊ e não interpreta. Se todas as formas presentes
|
|
41
|
+
* neste projeto forem dessas, a ausência de um token não é remoção - é a lacuna de leitura que o
|
|
42
|
+
* relatório já declara em outro lugar.
|
|
43
|
+
*/
|
|
44
|
+
const shapes = census.coverage?.shapes ?? [];
|
|
45
|
+
const readable = shapes.length === 0 || shapes.some((s) => s.reads !== "none");
|
|
46
|
+
/**
|
|
47
|
+
* O ÍNDICE INVERSO, montado UMA vez por medição: quem compõe cada alvo.
|
|
48
|
+
*
|
|
49
|
+
* É o que responde a população de uma migração sem varrer o censo por estado aberto - doze
|
|
50
|
+
* estados abertos continuam sendo doze consultas.
|
|
51
|
+
*/
|
|
52
|
+
const inbound = new Map();
|
|
53
|
+
for (const [name, look] of Object.entries(looks)) {
|
|
54
|
+
for (const ref of look?.composes ?? []) {
|
|
55
|
+
const list = inbound.get(ref);
|
|
56
|
+
if (list)
|
|
57
|
+
list.push(name);
|
|
58
|
+
else
|
|
59
|
+
inbound.set(ref, [name]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const known = new Set([...Object.keys(looks), ...inbound.keys()]);
|
|
63
|
+
return {
|
|
64
|
+
composes: (name) => {
|
|
65
|
+
const look = looks[name];
|
|
66
|
+
if (!look)
|
|
67
|
+
return null;
|
|
68
|
+
return look.composes ?? [];
|
|
69
|
+
},
|
|
70
|
+
hasRecipe: (name) => Boolean(looks[name]),
|
|
71
|
+
refusedByGate: (name) => refused.get(name) ?? null,
|
|
72
|
+
tokenDeclared: (token) => declared.has(token),
|
|
73
|
+
tokenUses: (token) => uses.get(token) ?? 0,
|
|
74
|
+
inboundRefs: (target) => known.has(target) ? (inbound.get(target) ?? []) : null,
|
|
75
|
+
stylesReadable: () => readable,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AS CONDIÇÕES DE SAÍDA DE UM TRABALHO, e o catálogo é FECHADO.
|
|
3
|
+
*
|
|
4
|
+
* Um work-state ("estamos migrando o Checkout para o novo Button") só é memória confiável se a
|
|
5
|
+
* própria realidade do repositório puder encerrá-lo. Sem isso ele fica aberto para sempre e passa a
|
|
6
|
+
* mentir com autoridade: três semanas depois o agente lê "estamos migrando" e age em cima disso.
|
|
7
|
+
*
|
|
8
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
9
|
+
* O PRINCÍPIO QUE GOVERNA O CRESCIMENTO DESTE ARQUIVO
|
|
10
|
+
*
|
|
11
|
+
* A linguagem das condições cresce quando a CAPACIDADE DE MEDIÇÃO cresce, nunca quando alguém
|
|
12
|
+
* imagina um caso de uso. Um predicado sem medição por trás é uma promessa que o observador não
|
|
13
|
+
* pode cumprir - e a alternativa que isto recusa é escrever uma linguagem de consulta dentro do
|
|
14
|
+
* produto, que começa com dois operadores e em seis meses é uma linguagem de programação.
|
|
15
|
+
*
|
|
16
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
17
|
+
* TRÊS ESTADOS, E NÃO DOIS - é o que impede o falso positivo de conclusão
|
|
18
|
+
*
|
|
19
|
+
* Toda condição negativa é satisfeita por duas coisas OPOSTAS: o trabalho terminou, ou o sujeito
|
|
20
|
+
* desapareceu. Este defeito já custou uma correção neste produto em outra forma - apagar um sistema
|
|
21
|
+
* fazia toda classe de bug dele virar "resolvida" sem ninguém consertar nada -, e a lição foi que
|
|
22
|
+
* `fixed` passou a exigir testemunha em vez de ser inferido de "a contagem zerou".
|
|
23
|
+
*
|
|
24
|
+
* Então o veredito é `met`, `unmet` ou INDETERMINADO. E há TRÊS causas de indeterminação, medidas no
|
|
25
|
+
* monorepo do dono em 20/08:
|
|
26
|
+
*
|
|
27
|
+
* 1. o sujeito saiu do inventário -> pode ter sido deletado
|
|
28
|
+
* 2. o PORTÃO recusou o componente -> 400 recusas naquele censo, com 7 motivos
|
|
29
|
+
* (`no-styling` 190, `route` 137, `provider` 45, ...). `AddUserIcon` é recusado por
|
|
30
|
+
* "renders but decides nothing about how anything looks" - ele EXISTE no repositório e não
|
|
31
|
+
* tem receita. Ausência do inventário não é ausência do repositório.
|
|
32
|
+
* 3. a forma que declararia aquilo não é lida -> `sx` (14 arquivos) e `styled` (1) chegam com
|
|
33
|
+
* `reads: "none"`. Um token declarado ali nunca foi lido, e chamar isso de "removido" é
|
|
34
|
+
* transformar ignorância em prova.
|
|
35
|
+
*
|
|
36
|
+
* Quando a plataforma não consegue saber, ela representa que não sabe. Indeterminado vira
|
|
37
|
+
* `orphaned` no work-state, que pede uma pessoa - e é REVERSÍVEL, porque a capacidade de medição
|
|
38
|
+
* pode voltar.
|
|
39
|
+
*/
|
|
40
|
+
import { CATALOGUE, } from "./contract.js";
|
|
41
|
+
/**
|
|
42
|
+
* PODE ESTE TRABALHO NASCER COM ESTA CONDIÇÃO? - validado na CRIAÇÃO, não na avaliação.
|
|
43
|
+
*
|
|
44
|
+
* Uma condição que nunca poderia ser verdadeira não é uma condição que falha depois: é um estado
|
|
45
|
+
* órfão desde o nascimento. Medido: 206 de 323 receitas carregam `composes`; as outras 117 são
|
|
46
|
+
* folhas, e um predicado de referência sobre uma folha nunca teria resposta.
|
|
47
|
+
*/
|
|
48
|
+
export function validateCondition(condition, obs) {
|
|
49
|
+
const contract = CATALOGUE[condition.type];
|
|
50
|
+
if (!contract)
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
because: `"${condition.type}" is not a predicate this platform measures.`,
|
|
54
|
+
};
|
|
55
|
+
if (!condition.subject.trim())
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
because: "A condition needs a subject - whose property is being checked.",
|
|
59
|
+
};
|
|
60
|
+
for (const field of contract.needs) {
|
|
61
|
+
if (!condition[field]?.trim())
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
because: `\`${condition.type}\` needs \`${field}\` - without it the condition cannot be answered.`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `reference_absent` COM UM ALVO NOVO É UMA TRANSIÇÃO MAL DECLARADA, e ela é recusada aqui.
|
|
69
|
+
*
|
|
70
|
+
* É uma regra de FORMA, não de intenção: quem informou para onde está migrando já tem a evidência
|
|
71
|
+
* positiva na mão, e pedir a versão fraca só pode piorar a prova.
|
|
72
|
+
*/
|
|
73
|
+
if (condition.type === "reference_absent" && condition.to?.trim())
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
because: "You named where this is going, so use `reference_replaced` - proving the old reference is gone does not prove the new one arrived.",
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* A SÉTIMA RECUSA: um predicado cuja capacidade de observação este projeto não oferece não pode ser
|
|
80
|
+
* registrado. A âncora é "consigo ver referências ENTRANTES para este identificador?", nunca "ele
|
|
81
|
+
* está no inventário" - medido: 119 alvos são compostos por alguém e não têm receita, 37 deles
|
|
82
|
+
* recusados pelo portão.
|
|
83
|
+
*/
|
|
84
|
+
if (condition.type === "inbound_references_zero" &&
|
|
85
|
+
obs.inboundRefs(condition.subject) === null)
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
because: `nothing in this measurement knows \`${condition.subject}\`, so the number of components composing it cannot be observed here.`,
|
|
89
|
+
};
|
|
90
|
+
if (condition.type.startsWith("reference_") &&
|
|
91
|
+
condition.type !== "inbound_references_zero" &&
|
|
92
|
+
obs.composes(condition.subject) === null)
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
because: `\`${condition.subject}\` has no readable recipe, so nothing can be observed about what it references.`,
|
|
96
|
+
};
|
|
97
|
+
return { ok: true };
|
|
98
|
+
}
|
|
99
|
+
/** As referências de um componente - a comparação é por nome exato. */
|
|
100
|
+
const refs = (obs, name) => obs.composes(name);
|
|
101
|
+
export function evaluate(condition, obs) {
|
|
102
|
+
const contract = CATALOGUE[condition.type];
|
|
103
|
+
const { subject, from, to } = condition;
|
|
104
|
+
/**
|
|
105
|
+
* A ÂNCORA PRIMEIRO, porque ela decide se a pergunta é respondível - e não a resposta.
|
|
106
|
+
*/
|
|
107
|
+
if (contract.requiresReadableStyles && !obs.stylesReadable())
|
|
108
|
+
return {
|
|
109
|
+
state: "indeterminate",
|
|
110
|
+
reason: "unreadable",
|
|
111
|
+
because: "no style form in this project is being read, so an absent token means we did not look - not that it is gone",
|
|
112
|
+
};
|
|
113
|
+
/** O que o contrato exige vivo, checado antes de qualquer `met`. */
|
|
114
|
+
for (const role of contract.requiresAlive) {
|
|
115
|
+
const name = role === "subject" ? subject : to;
|
|
116
|
+
if (!name)
|
|
117
|
+
continue;
|
|
118
|
+
if (obs.hasRecipe(name))
|
|
119
|
+
continue;
|
|
120
|
+
const refused = obs.refusedByGate(name);
|
|
121
|
+
if (refused)
|
|
122
|
+
return {
|
|
123
|
+
state: "indeterminate",
|
|
124
|
+
reason: "gate-refused",
|
|
125
|
+
because: `\`${name}\` is not in the inventory because the gate left it out (${refused.why}) - it exists in the repository, so its absence proves nothing about this work`,
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
state: "indeterminate",
|
|
129
|
+
reason: "subject-gone",
|
|
130
|
+
because: `\`${name}\` is no longer in the inventory - this condition cannot say whether the work finished or the component went away`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
switch (condition.type) {
|
|
134
|
+
case "reference_replaced": {
|
|
135
|
+
const list = refs(obs, subject) ?? [];
|
|
136
|
+
const oldGone = !list.includes(from);
|
|
137
|
+
const newThere = list.includes(to);
|
|
138
|
+
if (oldGone && newThere)
|
|
139
|
+
return {
|
|
140
|
+
state: "met",
|
|
141
|
+
because: `\`${subject}\` no longer references \`${from}\` and now references \`${to}\``,
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
state: "unmet",
|
|
145
|
+
because: oldGone
|
|
146
|
+
? `\`${subject}\` dropped \`${from}\` but does not reference \`${to}\` yet`
|
|
147
|
+
: `\`${subject}\` still references \`${from}\``,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* A ÚNICA AVALIAÇÃO QUE DEVOLVE ASSINATURA. O alvo pode ter sumido do inventário e continuar
|
|
152
|
+
* sendo composto por alguém - por isso a resposta é sobre os SÍTIOS, não sobre o inventário.
|
|
153
|
+
*/
|
|
154
|
+
case "inbound_references_zero": {
|
|
155
|
+
const sites = obs.inboundRefs(subject);
|
|
156
|
+
if (sites === null)
|
|
157
|
+
return {
|
|
158
|
+
state: "indeterminate",
|
|
159
|
+
reason: "subject-gone",
|
|
160
|
+
because: `nothing in this measurement knows \`${subject}\`, so its retirement cannot be observed`,
|
|
161
|
+
};
|
|
162
|
+
return sites.length === 0
|
|
163
|
+
? {
|
|
164
|
+
state: "met",
|
|
165
|
+
because: `no component composes \`${subject}\` any more`,
|
|
166
|
+
progress: { remaining: 0 },
|
|
167
|
+
}
|
|
168
|
+
: {
|
|
169
|
+
state: "unmet",
|
|
170
|
+
because: `${sites.length} component${sites.length === 1 ? "" : "s"} still compose${sites.length === 1 ? "s" : ""} \`${subject}\``,
|
|
171
|
+
progress: { remaining: sites.length },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
case "reference_present": {
|
|
175
|
+
const list = refs(obs, subject) ?? [];
|
|
176
|
+
return list.includes(to)
|
|
177
|
+
? { state: "met", because: `\`${subject}\` references \`${to}\`` }
|
|
178
|
+
: {
|
|
179
|
+
state: "unmet",
|
|
180
|
+
because: `\`${subject}\` does not reference \`${to}\``,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
case "reference_absent": {
|
|
184
|
+
const list = refs(obs, subject) ?? [];
|
|
185
|
+
return list.includes(from)
|
|
186
|
+
? {
|
|
187
|
+
state: "unmet",
|
|
188
|
+
because: `\`${subject}\` still references \`${from}\``,
|
|
189
|
+
}
|
|
190
|
+
: {
|
|
191
|
+
state: "met",
|
|
192
|
+
because: `\`${subject}\` no longer references \`${from}\``,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
case "token_present": {
|
|
196
|
+
const token = to;
|
|
197
|
+
return obs.tokenDeclared(token)
|
|
198
|
+
? { state: "met", because: `\`${token}\` is declared` }
|
|
199
|
+
: { state: "unmet", because: `\`${token}\` is not declared yet` };
|
|
200
|
+
}
|
|
201
|
+
case "token_absent": {
|
|
202
|
+
const token = from;
|
|
203
|
+
const uses = obs.tokenUses(token);
|
|
204
|
+
if (obs.tokenDeclared(token))
|
|
205
|
+
return { state: "unmet", because: `\`${token}\` is still declared` };
|
|
206
|
+
return uses > 0
|
|
207
|
+
? {
|
|
208
|
+
state: "unmet",
|
|
209
|
+
because: `\`${token}\` is undeclared but still used in ${uses} place${uses === 1 ? "" : "s"}`,
|
|
210
|
+
}
|
|
211
|
+
: {
|
|
212
|
+
state: "met",
|
|
213
|
+
because: `\`${token}\` is neither declared nor used`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
case "component_present":
|
|
217
|
+
return obs.hasRecipe(subject)
|
|
218
|
+
? { state: "met", because: `\`${subject}\` is in the inventory` }
|
|
219
|
+
: { state: "unmet", because: `\`${subject}\` is not in the inventory` };
|
|
220
|
+
/**
|
|
221
|
+
* A AUSÊNCIA DE COMPONENTE É A MAIS ARMADILHADA DAS SETE, e por isso ela checa o portão antes
|
|
222
|
+
* de responder: `AddUserIcon` sai do inventário por "no-styling" e continua no repositório.
|
|
223
|
+
*/
|
|
224
|
+
case "component_absent": {
|
|
225
|
+
if (obs.hasRecipe(subject))
|
|
226
|
+
return {
|
|
227
|
+
state: "unmet",
|
|
228
|
+
because: `\`${subject}\` is still in the inventory`,
|
|
229
|
+
};
|
|
230
|
+
const refused = obs.refusedByGate(subject);
|
|
231
|
+
if (refused)
|
|
232
|
+
return {
|
|
233
|
+
state: "indeterminate",
|
|
234
|
+
reason: "gate-refused",
|
|
235
|
+
because: `\`${subject}\` left the inventory because the gate refused it (${refused.why}), not because it was removed`,
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
state: "met",
|
|
239
|
+
because: `\`${subject}\` is not in the inventory`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUANTO DA MEMÓRIA MERECE ENTRAR NO CONTEXTO AGORA - e a resposta é explicável linha por linha.
|
|
3
|
+
*
|
|
4
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* FILTRO, ORDEM, ORÇAMENTO - nesta ordem, e nenhum score
|
|
6
|
+
*
|
|
7
|
+
* A alternativa considerada e recusada era `relevância × confiança × especificidade`. Ela responde
|
|
8
|
+
* ao mesmo pedido e cobra um preço que só aparece no primeiro dia em que a memória atrapalha: o
|
|
9
|
+
* desenvolvedor pergunta *"por que essa regra apareceu e aquela não"*, e um produto de três floats
|
|
10
|
+
* não tem resposta. Um filtro booleano tem.
|
|
11
|
+
*
|
|
12
|
+
* Confiança responde "quanto acreditamos nisto". Relevância responde "quanto isto importa para o
|
|
13
|
+
* que estou fazendo agora". São perguntas diferentes, e a segunda é um FILTRO de contrato fechado,
|
|
14
|
+
* nunca um peso.
|
|
15
|
+
*
|
|
16
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
17
|
+
* A INVARIANTE 5 (dono, 20/08): ORÇAMENTO NÃO PODE MUDAR SEMÂNTICA
|
|
18
|
+
*
|
|
19
|
+
* Duas decisões sobre o mesmo assunto formam um GRUPO indivisível. Se só uma couber, ela não entra
|
|
20
|
+
* sozinha - entregar metade de uma discussão como se fosse a visão inteira é reintroduzir
|
|
21
|
+
* contradição silenciosa por causa do orçamento, que é o defeito que o co-escopo existe para
|
|
22
|
+
* impedir. Ou o grupo entra inteiro, ou ele é declarado omitido.
|
|
23
|
+
*
|
|
24
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
25
|
+
* CO-ESCOPO NÃO É CONTRADIÇÃO, e a diferença é dita em voz alta
|
|
26
|
+
*
|
|
27
|
+
* Duas regras no mesmo `applies` e `kind` não são necessariamente contraditórias: são duas decisões
|
|
28
|
+
* que merecem aparecer juntas. Ver que duas FRASES se contradizem é leitura de linguagem, ou seja
|
|
29
|
+
* IA, e isso está fora do v1 de propósito. O que este arquivo promete é co-escopo, e é isso que a
|
|
30
|
+
* resposta diz - prometer um detector de contradição que não existe seria a lacuna calada que a
|
|
31
|
+
* lei 8 proíbe.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* AS TAREFAS, LISTA FECHADA - derivada do que a plataforma JÁ MEDE, nunca de casos imaginados.
|
|
35
|
+
*
|
|
36
|
+
* Mesma regra do catálogo de predicados: a linguagem cresce quando a capacidade de medição cresce.
|
|
37
|
+
* Estas seis existem porque há medição por trás de cada uma - as três categorias de otimização, o
|
|
38
|
+
* contraste que o doctor calcula, a acessibilidade que os anéis medem, e a migração, que é o que os
|
|
39
|
+
* predicados de transição observam.
|
|
40
|
+
*/
|
|
41
|
+
export const TASKS = [
|
|
42
|
+
"tokens",
|
|
43
|
+
"spacing",
|
|
44
|
+
"typography",
|
|
45
|
+
"contrast",
|
|
46
|
+
"accessibility",
|
|
47
|
+
"migration",
|
|
48
|
+
];
|
|
49
|
+
/** ~4 caracteres por token: aproximação suficiente para um teto, e barata. */
|
|
50
|
+
const cost = (m) => Math.ceil(m.text.length / 4) + 8;
|
|
51
|
+
const norm = (v) => v.trim().toLowerCase();
|
|
52
|
+
/**
|
|
53
|
+
* A STACK É O ÚNICO FILTRO QUE MANTÉM O QUE NÃO CONSEGUE JULGAR.
|
|
54
|
+
*
|
|
55
|
+
* Uma regra cuja condição não bate é removida, porque obedecê-la seria um erro. Uma que a gente não
|
|
56
|
+
* consegue avaliar - a stack veio vazia, ou ela nomeia algo que não detectamos - FICA, porque uma
|
|
57
|
+
* regra que ninguém pode julgar é mais segura presente que silenciosamente ausente. São duas
|
|
58
|
+
* situações diferentes e elas ganham respostas diferentes.
|
|
59
|
+
*/
|
|
60
|
+
const stackHolds = (m, stack) => {
|
|
61
|
+
if (m.when.length === 0)
|
|
62
|
+
return true;
|
|
63
|
+
if (!stack || stack.length === 0)
|
|
64
|
+
return true;
|
|
65
|
+
const has = new Set(stack.map(norm));
|
|
66
|
+
return m.when.some((w) => has.has(norm(w)));
|
|
67
|
+
};
|
|
68
|
+
/** Especificidade: a relação vence o componente, que vence o sistema - e sai do dado, sem peso. */
|
|
69
|
+
const specificity = (m) => m.applies.length >= 2 ? 3 : m.applies.length === 1 ? 2 : 1;
|
|
70
|
+
const RUNG_ORDER = { fact: 0, habit: 1, candidate: 2 };
|
|
71
|
+
/**
|
|
72
|
+
* A CHAVE DO GRUPO: o sujeito estruturado quando existe, senão o escopo mais o tipo de regra.
|
|
73
|
+
*
|
|
74
|
+
* Sem sujeito o agrupamento é mais largo e pode juntar coisas que só dividem o escopo - e é por
|
|
75
|
+
* isso que a resposta diz "há mais de uma decisão neste escopo" em vez de afirmar conflito.
|
|
76
|
+
*/
|
|
77
|
+
const groupKey = (m) => m.subject
|
|
78
|
+
? `subject:${norm(m.subject)}`
|
|
79
|
+
: `scope:${m.applies.map(norm).sort().join("+")}|${m.kind}`;
|
|
80
|
+
export function recall(all, ask) {
|
|
81
|
+
const omitted = new Map();
|
|
82
|
+
const drop = (why) => omitted.set(why, (omitted.get(why) ?? 0) + 1);
|
|
83
|
+
const wantScope = (ask.scope ?? []).map(norm);
|
|
84
|
+
const kept = all.filter((m) => {
|
|
85
|
+
if (!stackHolds(m, ask.stack))
|
|
86
|
+
return drop("stack"), false;
|
|
87
|
+
if (ask.family && m.family && norm(m.family) !== norm(ask.family))
|
|
88
|
+
return drop("family"), false;
|
|
89
|
+
if (ask.task &&
|
|
90
|
+
m.tasks &&
|
|
91
|
+
m.tasks.length > 0 &&
|
|
92
|
+
!m.tasks.includes(ask.task))
|
|
93
|
+
return drop("task"), false;
|
|
94
|
+
/**
|
|
95
|
+
* ESCOPO: uma regra DO SISTEMA vale sempre - ela é o que governa tudo. Uma regra de componente
|
|
96
|
+
* só entra quando aquele componente está em jogo.
|
|
97
|
+
*/
|
|
98
|
+
if (m.applies.length > 0 && wantScope.length > 0) {
|
|
99
|
+
const touches = m.applies.some((a) => wantScope.includes(norm(a)));
|
|
100
|
+
if (!touches)
|
|
101
|
+
return drop("scope"), false;
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
/** Ordem: mais específico, depois degrau, depois confiança, e decisão antes de rationale. */
|
|
106
|
+
const ranked = [...kept].sort((a, b) => {
|
|
107
|
+
if (a.type !== b.type)
|
|
108
|
+
return a.type === "decision" ? -1 : 1;
|
|
109
|
+
const s = specificity(b) - specificity(a);
|
|
110
|
+
if (s !== 0)
|
|
111
|
+
return s;
|
|
112
|
+
const r = RUNG_ORDER[a.rung] - RUNG_ORDER[b.rung];
|
|
113
|
+
if (r !== 0)
|
|
114
|
+
return r;
|
|
115
|
+
return b.confidence - a.confidence;
|
|
116
|
+
});
|
|
117
|
+
/**
|
|
118
|
+
* O ORÇAMENTO É GASTO POR GRUPO, nunca por item - é a invariante 5.
|
|
119
|
+
*
|
|
120
|
+
* Os grupos entram na ordem do seu melhor membro, e um grupo que não cabe INTEIRO não entra:
|
|
121
|
+
* metade de uma discussão lida como a visão completa.
|
|
122
|
+
*/
|
|
123
|
+
const order = [];
|
|
124
|
+
const byGroup = new Map();
|
|
125
|
+
for (const m of ranked) {
|
|
126
|
+
const key = groupKey(m);
|
|
127
|
+
if (!byGroup.has(key)) {
|
|
128
|
+
byGroup.set(key, []);
|
|
129
|
+
order.push(key);
|
|
130
|
+
}
|
|
131
|
+
byGroup.get(key)?.push(m);
|
|
132
|
+
}
|
|
133
|
+
const carried = [];
|
|
134
|
+
const groups = [];
|
|
135
|
+
let spent = 0;
|
|
136
|
+
for (const key of order) {
|
|
137
|
+
const members = byGroup.get(key) ?? [];
|
|
138
|
+
const price = members.reduce((n, m) => n + cost(m), 0);
|
|
139
|
+
if (spent + price > ask.budget) {
|
|
140
|
+
for (const _ of members)
|
|
141
|
+
drop("budget");
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
spent += price;
|
|
145
|
+
carried.push(...members);
|
|
146
|
+
if (members.length > 1)
|
|
147
|
+
groups.push({ key, members: members.map((m) => m.id) });
|
|
148
|
+
}
|
|
149
|
+
const cut = [...omitted.entries()].map(([by, count]) => ({ by, count }));
|
|
150
|
+
const total = all.length;
|
|
151
|
+
const parts = cut.map(({ by, count }) => `${count} by ${by}`).join(", ");
|
|
152
|
+
const conflictNote = groups.length > 0
|
|
153
|
+
? ` ${groups.length} group${groups.length === 1 ? "" : "s"} carry more than one decision on the same subject - they travel together, and this is co-scope, not a claim that they contradict.`
|
|
154
|
+
: "";
|
|
155
|
+
return {
|
|
156
|
+
carried,
|
|
157
|
+
groups,
|
|
158
|
+
omitted: cut,
|
|
159
|
+
because: `${carried.length} of ${total} carried within ${ask.budget} tokens${parts ? `; left out: ${parts}` : ""}.${conflictNote}`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { measure, snapshotOf } from "./snapshot.js";
|
|
2
|
+
export async function reportMeasurement(args) {
|
|
3
|
+
const answer = await args.open().catch((e) => ({
|
|
4
|
+
ok: false,
|
|
5
|
+
reason: "error",
|
|
6
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
7
|
+
}));
|
|
8
|
+
if (!answer.ok) {
|
|
9
|
+
if (answer.reason === "not-installed")
|
|
10
|
+
return { measured: 0, skipped: "not-installed" };
|
|
11
|
+
return {
|
|
12
|
+
measured: 0,
|
|
13
|
+
skipped: null,
|
|
14
|
+
failed: {
|
|
15
|
+
...(answer.status ? { status: answer.status } : {}),
|
|
16
|
+
...(answer.detail ? { detail: answer.detail } : {}),
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (answer.open.length === 0)
|
|
21
|
+
return { measured: 0, skipped: "no-open-work" };
|
|
22
|
+
const snapshot = snapshotOf({
|
|
23
|
+
at: args.at,
|
|
24
|
+
...(args.cli ? { cli: args.cli } : {}),
|
|
25
|
+
...(args.interpretation ? { interpretation: args.interpretation } : {}),
|
|
26
|
+
...(args.scope ? { scope: args.scope } : {}),
|
|
27
|
+
});
|
|
28
|
+
const { verdicts } = measure(answer.open, args.obs, snapshot);
|
|
29
|
+
await args.send({ snapshot, verdicts });
|
|
30
|
+
return { measured: verdicts.length, skipped: null };
|
|
31
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { evaluate } from "./predicate.js";
|
|
3
|
+
export function snapshotOf(input) {
|
|
4
|
+
const id = createHash("sha256")
|
|
5
|
+
.update([
|
|
6
|
+
input.cli ?? "",
|
|
7
|
+
input.interpretation ?? "",
|
|
8
|
+
input.scope ?? "",
|
|
9
|
+
input.at,
|
|
10
|
+
].join("|"))
|
|
11
|
+
.digest("hex")
|
|
12
|
+
.slice(0, 16);
|
|
13
|
+
return {
|
|
14
|
+
id,
|
|
15
|
+
at: input.at,
|
|
16
|
+
by: {
|
|
17
|
+
...(input.cli ? { cli: input.cli } : {}),
|
|
18
|
+
...(input.interpretation ? { interpretation: input.interpretation } : {}),
|
|
19
|
+
},
|
|
20
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* O QUE O CLIENTE MANDA PELO CANAL DE MEDIÇÃO - veredictos, nunca o censo.
|
|
25
|
+
*
|
|
26
|
+
* A plataforma não precisa saber QUAIS eram os 32 componentes que ainda compõem o `Loader`: precisa
|
|
27
|
+
* saber que eram 32, medidos por qual leitor, em qual snapshot. Menos dado atravessando, menos
|
|
28
|
+
* acoplamento entre versões, e menos exposição do repositório de quem confiou na gente.
|
|
29
|
+
*
|
|
30
|
+
* Todos os veredictos de uma execução saem do MESMO snapshot: é o que impede dois estados avaliados
|
|
31
|
+
* juntos de enxergarem repositórios diferentes.
|
|
32
|
+
*/
|
|
33
|
+
export function measure(conditions, obs, snapshot) {
|
|
34
|
+
return {
|
|
35
|
+
snapshot,
|
|
36
|
+
verdicts: conditions.map(({ id, exit }) => ({
|
|
37
|
+
id,
|
|
38
|
+
snapshotId: snapshot.id,
|
|
39
|
+
predicate: exit.type,
|
|
40
|
+
subject: exit.subject,
|
|
41
|
+
...(exit.from ? { from: exit.from } : {}),
|
|
42
|
+
...(exit.to ? { to: exit.to } : {}),
|
|
43
|
+
verdict: evaluate(exit, obs),
|
|
44
|
+
by: snapshot.by,
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
}
|