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,269 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { sourceLine } from "../agent-provenance.js";
|
|
3
|
+
import { FAMILIES } from "../compose-context.js";
|
|
4
|
+
import { TASKS, } from "./contract.js";
|
|
5
|
+
import { prepareWrite, rejectUnknown } from "./write.js";
|
|
6
|
+
/**
|
|
7
|
+
* AS DUAS FERRAMENTAS QUE O AGENTE VÊ - e o schema é o contrato, não o runtime.
|
|
8
|
+
*
|
|
9
|
+
* Recusar um campo derivado em tempo de execução é a segunda linha de defesa. A primeira é o agente
|
|
10
|
+
* NUNCA VER esse campo como opção: um schema que oferece `confidence` convida a preenchê-lo, e a
|
|
11
|
+
* recusa depois vira atrito que ele aprende a contornar. Um spec percorre estes schemas e reprova se
|
|
12
|
+
* qualquer nome de `DERIVED_FIELDS` aparecer neles.
|
|
13
|
+
*
|
|
14
|
+
* DUAS FERRAMENTAS ESTREITAS, e não uma genérica: quanto menos combinatória o schema oferece, menos
|
|
15
|
+
* estado impossível existe para validar depois. `kind` é enum fechado aqui dentro, para que um campo
|
|
16
|
+
* inválido para aquele tipo seja estruturalmente impossível em vez de ser um erro de validação.
|
|
17
|
+
*/
|
|
18
|
+
export const MEMORY_TOOLS = [
|
|
19
|
+
{
|
|
20
|
+
name: "remember",
|
|
21
|
+
description: "Record what this project DECIDED, what it deliberately does NOT do, or what someone is working on right now - so the next session does not decide it again. You propose the fact and where it came from; identity, confidence, authority and state are derived from measurement on our side. A work state must carry an exit condition the repository itself can prove, or it is refused: memory that cannot be closed by reality starts lying within weeks.",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
request_id: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Optional id for THIS call, for your own logs. It does not decide anything: writing the same thing twice is already harmless - identity comes from the content, so a retry finds what is there instead of adding to it.",
|
|
28
|
+
},
|
|
29
|
+
kind: {
|
|
30
|
+
type: "string",
|
|
31
|
+
enum: ["decision", "rationale", "work-state"],
|
|
32
|
+
description: "decision = what we do · rationale = why we do NOT do something · work-state = what someone is doing now",
|
|
33
|
+
},
|
|
34
|
+
text: {
|
|
35
|
+
type: "string",
|
|
36
|
+
description: "The decision or the reasoning, in one sentence, as a person would say it. Not needed for work-state.",
|
|
37
|
+
},
|
|
38
|
+
applies: {
|
|
39
|
+
type: "array",
|
|
40
|
+
items: { type: "string" },
|
|
41
|
+
description: "decision only. Empty = the whole system · one name = that component · two = the relation between them.",
|
|
42
|
+
},
|
|
43
|
+
rule_kind: {
|
|
44
|
+
type: "string",
|
|
45
|
+
enum: ["limit", "implementation"],
|
|
46
|
+
description: "decision only. limit = a boundary a measurement can check · implementation = how it is built, which no linter catches.",
|
|
47
|
+
},
|
|
48
|
+
when: {
|
|
49
|
+
type: "array",
|
|
50
|
+
items: { type: "string" },
|
|
51
|
+
description: "decision only. Stack members this holds in - empty means all of them.",
|
|
52
|
+
},
|
|
53
|
+
family: {
|
|
54
|
+
type: "string",
|
|
55
|
+
enum: [...FAMILIES],
|
|
56
|
+
description: "decision only, and only when it is about one family.",
|
|
57
|
+
},
|
|
58
|
+
tasks: {
|
|
59
|
+
type: "array",
|
|
60
|
+
items: { type: "string", enum: [...TASKS] },
|
|
61
|
+
description: "decision only. Which kinds of work this is relevant to - omit when it is relevant to all.",
|
|
62
|
+
},
|
|
63
|
+
subject: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "What this is ABOUT - a token, a component, a library. Required for rationale.",
|
|
66
|
+
},
|
|
67
|
+
exit: {
|
|
68
|
+
type: "object",
|
|
69
|
+
description: "work-state only. The condition the REPOSITORY proves, so nobody has to remember to close this.",
|
|
70
|
+
properties: {
|
|
71
|
+
type: {
|
|
72
|
+
type: "string",
|
|
73
|
+
enum: [
|
|
74
|
+
"reference_replaced",
|
|
75
|
+
"inbound_references_zero",
|
|
76
|
+
"reference_present",
|
|
77
|
+
"reference_absent",
|
|
78
|
+
"token_present",
|
|
79
|
+
"token_absent",
|
|
80
|
+
"component_present",
|
|
81
|
+
"component_absent",
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
subject: { type: "string" },
|
|
85
|
+
from: { type: "string" },
|
|
86
|
+
to: { type: "string" },
|
|
87
|
+
},
|
|
88
|
+
required: ["type", "subject"],
|
|
89
|
+
},
|
|
90
|
+
created_by: {
|
|
91
|
+
type: "string",
|
|
92
|
+
enum: ["developer", "agent", "import"],
|
|
93
|
+
description: "work-state only. Who put this state here.",
|
|
94
|
+
},
|
|
95
|
+
provenance: {
|
|
96
|
+
type: "string",
|
|
97
|
+
enum: ["developer_declared", "agent_inferred", "imported"],
|
|
98
|
+
description: "Where the claim came from. `measured` is not on this list on purpose: only whoever measures can confer it.",
|
|
99
|
+
},
|
|
100
|
+
confirmation: {
|
|
101
|
+
type: "string",
|
|
102
|
+
enum: ["spontaneous", "confirmed_suggestion"],
|
|
103
|
+
description: "rationale only. Did they say it unprompted, or confirm something you suggested? Both are human; the difference is worth keeping.",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
required: ["kind", "provenance"],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "recall",
|
|
111
|
+
description: "What this project already decided about what you are doing NOW - filtered by scope, stack, family and task, ordered by specificity and confidence, and cut to a token budget YOU declare. The answer says what was left out and by which filter, so nothing disappears silently. Decisions about the same subject always travel together: half of a discussion reads as the whole of it.",
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
scope: {
|
|
116
|
+
type: "array",
|
|
117
|
+
items: { type: "string" },
|
|
118
|
+
description: "The components in play. Leave empty when the question is about the system.",
|
|
119
|
+
},
|
|
120
|
+
family: { type: "string", enum: [...FAMILIES] },
|
|
121
|
+
task: {
|
|
122
|
+
type: "string",
|
|
123
|
+
enum: [...TASKS],
|
|
124
|
+
description: "What kind of work this is - it filters, it does not rank.",
|
|
125
|
+
},
|
|
126
|
+
budget: {
|
|
127
|
+
type: "number",
|
|
128
|
+
description: "How many tokens of memory you can afford right now. Say it honestly: the platform chooses what fits, most trustworthy first.",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
required: ["budget"],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
/** Todo nome que aparece num schema, em qualquer profundidade - o spec usa isto. */
|
|
136
|
+
export function fieldNamesIn(schema, found = []) {
|
|
137
|
+
if (!schema || typeof schema !== "object")
|
|
138
|
+
return found;
|
|
139
|
+
const node = schema;
|
|
140
|
+
if (node.properties && typeof node.properties === "object")
|
|
141
|
+
for (const [key, value] of Object.entries(node.properties)) {
|
|
142
|
+
found.push(key);
|
|
143
|
+
fieldNamesIn(value, found);
|
|
144
|
+
}
|
|
145
|
+
if (node.items)
|
|
146
|
+
fieldNamesIn(node.items, found);
|
|
147
|
+
return found;
|
|
148
|
+
}
|
|
149
|
+
/** `rule_kind` na porta, `ruleKind` por dentro: o agente escreve snake, o contrato interno é camel. */
|
|
150
|
+
const toWrite = (args) => ({
|
|
151
|
+
kind: args.kind,
|
|
152
|
+
text: args.text,
|
|
153
|
+
applies: args.applies ?? [],
|
|
154
|
+
ruleKind: args.rule_kind ?? "implementation",
|
|
155
|
+
when: args.when,
|
|
156
|
+
family: args.family,
|
|
157
|
+
tasks: args.tasks,
|
|
158
|
+
subject: args.subject,
|
|
159
|
+
exit: args.exit,
|
|
160
|
+
createdBy: args.created_by ?? "agent",
|
|
161
|
+
provenance: args.provenance,
|
|
162
|
+
});
|
|
163
|
+
/** O payload como o contrato interno o nomeia - é o que `rejectUnknown` sabe julgar. */
|
|
164
|
+
const RENAMED = {
|
|
165
|
+
rule_kind: "ruleKind",
|
|
166
|
+
created_by: "createdBy",
|
|
167
|
+
request_id: "requestId",
|
|
168
|
+
};
|
|
169
|
+
const answer = (body, detail) => ({
|
|
170
|
+
body: `${body}\n\n${sourceLine({ kind: "composed", detail })}`,
|
|
171
|
+
source: { kind: "composed", detail },
|
|
172
|
+
});
|
|
173
|
+
const refuse = (r) => ({
|
|
174
|
+
body: `Not recorded: ${r.reason}\n${r.details}`,
|
|
175
|
+
source: { kind: "none", reason: "tool", detail: r.reason },
|
|
176
|
+
});
|
|
177
|
+
/**
|
|
178
|
+
* A ESCRITA, DO LADO QUE VÊ O REPOSITÓRIO.
|
|
179
|
+
*
|
|
180
|
+
* Validação e normalização acontecem aqui porque a observabilidade de uma condição depende do censo,
|
|
181
|
+
* que está no disco dele. Deduplicação, orçamento e persistência ficam do outro lado, onde o estado
|
|
182
|
+
* vive - e o `request_id` atravessa intacto para que um retry devolva o resultado da PRIMEIRA
|
|
183
|
+
* execução em vez de reexecutar contra o estado de agora.
|
|
184
|
+
*/
|
|
185
|
+
export async function handleRemember(args, obs, send,
|
|
186
|
+
/** O rastro local, opcional - ver `kind: "decision"` em `CheckEvent`. */
|
|
187
|
+
note) {
|
|
188
|
+
const internal = {};
|
|
189
|
+
for (const [key, value] of Object.entries(args)) {
|
|
190
|
+
if (value === undefined)
|
|
191
|
+
continue;
|
|
192
|
+
internal[RENAMED[key] ?? key] = value;
|
|
193
|
+
}
|
|
194
|
+
/** O `requestId` não é campo de memória: ele é da chamada, e sai antes da checagem de schema. */
|
|
195
|
+
const requestId = String(internal.requestId ?? randomUUID());
|
|
196
|
+
delete internal.requestId;
|
|
197
|
+
const unknown = rejectUnknown(internal);
|
|
198
|
+
if (unknown)
|
|
199
|
+
return refuse(unknown);
|
|
200
|
+
const ready = prepareWrite(toWrite(args), obs);
|
|
201
|
+
if ("accepted" in ready)
|
|
202
|
+
return refuse(ready);
|
|
203
|
+
/**
|
|
204
|
+
* A IDENTIDADE VEM DE VOLTA, e não daqui. O cliente não sabe quem é o usuário - só tem um token -,
|
|
205
|
+
* então o fingerprint e o dono são atribuídos do outro lado, onde a sessão existe.
|
|
206
|
+
*/
|
|
207
|
+
const out = await send({
|
|
208
|
+
requestId,
|
|
209
|
+
draft: ready.draft,
|
|
210
|
+
memory: toWrite(args),
|
|
211
|
+
});
|
|
212
|
+
if (!out.accepted)
|
|
213
|
+
return refuse(out);
|
|
214
|
+
/**
|
|
215
|
+
* E O LEDGER LOCAL REGISTRA, sem governar nada.
|
|
216
|
+
*
|
|
217
|
+
* `kind: "decision"` é rastro: ele diz que isto foi proposto neste repositório, neste instante, e é
|
|
218
|
+
* o que o `sync` sobe junto com as checagens. A memória que VALE está do outro lado - apagar o
|
|
219
|
+
* ledger não muda uma linha do que o `recall` devolve.
|
|
220
|
+
*/
|
|
221
|
+
await note?.({
|
|
222
|
+
kind: "decision",
|
|
223
|
+
at: new Date().toISOString(),
|
|
224
|
+
file: out.record.subject ?? out.record.kind,
|
|
225
|
+
});
|
|
226
|
+
const said = out.action === "existing"
|
|
227
|
+
? "This was already known, unchanged."
|
|
228
|
+
: out.action === "evidence_updated"
|
|
229
|
+
? "Already known, and this observation was added to its evidence."
|
|
230
|
+
: out.action === "co_scoped"
|
|
231
|
+
? "Recorded, and it sits alongside another memory on the same subject - both were kept, and neither claims the other is wrong."
|
|
232
|
+
: "Recorded.";
|
|
233
|
+
return answer(`${said}\nid ${out.record.fingerprint} · ${out.record.kind}${out.record.subject ? ` · ${out.record.subject}` : ""}`, `memory:${out.action}`);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* A LEITURA - e o CLI não tem as memórias.
|
|
237
|
+
*
|
|
238
|
+
* Ele monta o pedido com o teto que o agente declarou e FORMATA o que volta. Filtro, ordem e
|
|
239
|
+
* orçamento são da plataforma, que é quem tem o acervo, a confiança e o estado - e é ela quem sabe o
|
|
240
|
+
* que ficou de fora e por qual filtro.
|
|
241
|
+
*/
|
|
242
|
+
export async function handleRecall(args, stack, ask) {
|
|
243
|
+
const budget = Number(args.budget ?? 0);
|
|
244
|
+
if (!Number.isFinite(budget) || budget <= 0)
|
|
245
|
+
return refuse({
|
|
246
|
+
accepted: false,
|
|
247
|
+
reason: "unknown_field",
|
|
248
|
+
details: "budget has to be a positive number of tokens you can afford",
|
|
249
|
+
});
|
|
250
|
+
const out = await ask({
|
|
251
|
+
scope: Array.isArray(args.scope) ? args.scope : [],
|
|
252
|
+
stack,
|
|
253
|
+
...(args.family ? { family: String(args.family) } : {}),
|
|
254
|
+
...(args.task ? { task: args.task } : {}),
|
|
255
|
+
budget,
|
|
256
|
+
});
|
|
257
|
+
if (out.carried.length === 0)
|
|
258
|
+
return {
|
|
259
|
+
body: `Nothing recorded that applies here. ${out.because}`,
|
|
260
|
+
source: { kind: "none", reason: "system", detail: "nothing applies" },
|
|
261
|
+
};
|
|
262
|
+
const lines = out.carried.map((m) => `- ${m.text}${m.applies.length > 0 ? ` [${m.applies.join(", ")}]` : ""} (${m.rung})`);
|
|
263
|
+
const grouped = out.groups.length > 0
|
|
264
|
+
? `\n\nSame subject, so they travel together: ${out.groups
|
|
265
|
+
.map((g) => g.members.join(" + "))
|
|
266
|
+
.join(" · ")}`
|
|
267
|
+
: "";
|
|
268
|
+
return answer(`${lines.join("\n")}${grouped}\n\n${out.because}`, "memory:recall");
|
|
269
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/** Dias distintos com medição, sem progresso, antes de o estado adormecer. */
|
|
2
|
+
export const DORMANT_AFTER_MEASURED_DAYS = 3;
|
|
3
|
+
const day = (iso) => iso.slice(0, 10);
|
|
4
|
+
/** Estados que uma medição não move mais. Uma regressão é um trabalho NOVO, não este voltando. */
|
|
5
|
+
const SETTLED = new Set(["completed", "abandoned"]);
|
|
6
|
+
/**
|
|
7
|
+
* O QUE A MEDIÇÃO FAZ COM UM ESTADO - a única porta por onde `completed` e `orphaned` entram.
|
|
8
|
+
*
|
|
9
|
+
* PROGRESSO É O VEREDITO TER MUDADO, e isso é determinístico sem guardar o repositório inteiro: o
|
|
10
|
+
* `because` de um predicado de transição sai de "still references X" para "dropped X but does not
|
|
11
|
+
* reference Y yet" quando metade do trabalho aconteceu. Comparar o veredito anterior com o atual
|
|
12
|
+
* captura isso sem inventar heurística de diff.
|
|
13
|
+
*/
|
|
14
|
+
export function onMeasurement(state, verdict, at, by, dormantAfter = DORMANT_AFTER_MEASURED_DAYS) {
|
|
15
|
+
if (SETTLED.has(state.status))
|
|
16
|
+
return state;
|
|
17
|
+
/**
|
|
18
|
+
* INVARIANTE 1 e 2, no mesmo lugar: indeterminado NÃO PODE concluir, e ficar órfão preserva
|
|
19
|
+
* tudo. `lastVerdict` não é sobrescrito - a última coisa que a gente soube de verdade sobrevive
|
|
20
|
+
* ao período em que a gente deixou de saber.
|
|
21
|
+
*/
|
|
22
|
+
if (verdict.state === "indeterminate")
|
|
23
|
+
return {
|
|
24
|
+
...state,
|
|
25
|
+
status: "orphaned",
|
|
26
|
+
lastMeasuredAt: at,
|
|
27
|
+
by: by ?? state.by,
|
|
28
|
+
resolution: {
|
|
29
|
+
type: "orphaned",
|
|
30
|
+
source: "measurement",
|
|
31
|
+
at,
|
|
32
|
+
reason: `${verdict.reason}: ${verdict.because}`,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
const progressed = !state.lastVerdict ||
|
|
36
|
+
state.lastVerdict.state !== verdict.state ||
|
|
37
|
+
state.lastVerdict.because !== verdict.because;
|
|
38
|
+
if (verdict.state === "met")
|
|
39
|
+
return {
|
|
40
|
+
...state,
|
|
41
|
+
status: "completed",
|
|
42
|
+
evidenceSource: "measurement",
|
|
43
|
+
lastMeasuredAt: at,
|
|
44
|
+
lastProgressAt: at,
|
|
45
|
+
measuredDays: [],
|
|
46
|
+
lastVerdict: { state: "met", because: verdict.because },
|
|
47
|
+
by: by ?? state.by,
|
|
48
|
+
resolution: {
|
|
49
|
+
type: "completed",
|
|
50
|
+
source: "measurement",
|
|
51
|
+
at,
|
|
52
|
+
reason: verdict.because,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* INVARIANTE 3: uma medição nova NÃO acorda um estado dormente. Só progresso acorda - senão
|
|
57
|
+
* rodar `sync` faria todo trabalho parado parecer vivo outra vez.
|
|
58
|
+
*/
|
|
59
|
+
if (progressed)
|
|
60
|
+
return {
|
|
61
|
+
...state,
|
|
62
|
+
status: "active",
|
|
63
|
+
evidenceSource: "measurement",
|
|
64
|
+
lastMeasuredAt: at,
|
|
65
|
+
lastProgressAt: at,
|
|
66
|
+
measuredDays: [],
|
|
67
|
+
lastVerdict: { state: "unmet", because: verdict.because },
|
|
68
|
+
by: by ?? state.by,
|
|
69
|
+
/** Um estado que estava órfão e voltou a ser mensurável perde a resolução antiga. */
|
|
70
|
+
...(state.status === "orphaned" ? { resolution: undefined } : {}),
|
|
71
|
+
};
|
|
72
|
+
const days = state.measuredDays.includes(day(at))
|
|
73
|
+
? state.measuredDays
|
|
74
|
+
: [...state.measuredDays, day(at)];
|
|
75
|
+
return {
|
|
76
|
+
...state,
|
|
77
|
+
status: days.length >= dormantAfter
|
|
78
|
+
? "dormant"
|
|
79
|
+
: state.status === "orphaned"
|
|
80
|
+
? "active"
|
|
81
|
+
: state.status,
|
|
82
|
+
evidenceSource: "measurement",
|
|
83
|
+
lastMeasuredAt: at,
|
|
84
|
+
measuredDays: days,
|
|
85
|
+
lastVerdict: { state: "unmet", because: verdict.because },
|
|
86
|
+
by: by ?? state.by,
|
|
87
|
+
...(state.status === "orphaned" ? { resolution: undefined } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** A ÚNICA porta para `abandoned`, e ela é uma pessoa. */
|
|
91
|
+
export function declareAbandoned(state, by, at, reason) {
|
|
92
|
+
return {
|
|
93
|
+
...state,
|
|
94
|
+
status: "abandoned",
|
|
95
|
+
resolution: {
|
|
96
|
+
type: "abandoned",
|
|
97
|
+
source: "developer",
|
|
98
|
+
at,
|
|
99
|
+
...(reason ? { reason } : {}),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* COMO O RECALL FALA DE UM ESTADO - invariante 4.
|
|
105
|
+
*
|
|
106
|
+
* Um estado que o agente criou e que ninguém mediu é hipótese, e a frase tem que dizer isso. Um
|
|
107
|
+
* estado medido há seis dias é a última coisa que a gente sabe, não o que está acontecendo agora.
|
|
108
|
+
*/
|
|
109
|
+
export function authorityOf(state) {
|
|
110
|
+
if (state.createdBy === "agent" && state.evidenceSource === "none")
|
|
111
|
+
return {
|
|
112
|
+
authority: "hypothesis",
|
|
113
|
+
label: "hypothesis, written by an agent and never measured",
|
|
114
|
+
};
|
|
115
|
+
if (!state.lastMeasuredAt)
|
|
116
|
+
return { authority: "hypothesis", label: "declared, not measured yet" };
|
|
117
|
+
if (state.status === "orphaned")
|
|
118
|
+
return {
|
|
119
|
+
authority: "last-known",
|
|
120
|
+
label: `last known before it became unmeasurable on ${day(state.lastMeasuredAt)}`,
|
|
121
|
+
};
|
|
122
|
+
if (state.status === "dormant")
|
|
123
|
+
return {
|
|
124
|
+
authority: "last-known",
|
|
125
|
+
label: `no progress observed since ${state.lastProgressAt ? day(state.lastProgressAt) : day(state.createdAt)}`,
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
authority: "confirmed",
|
|
129
|
+
label: `measured ${day(state.lastMeasuredAt)}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A FRONTEIRA DE ESCRITA, A METADE DO CLIENTE - onde o schema para de confiar no agente.
|
|
3
|
+
*
|
|
4
|
+
* O recall já está protegido; aqui o risco não é "a máquina de estados sabe o que fazer" e sim "o
|
|
5
|
+
* agente consegue inserir dado ruim pela porta". Um agente entusiasmado, um retry de rede ou uma
|
|
6
|
+
* compactação de conversa não podem contaminar a memória.
|
|
7
|
+
*
|
|
8
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
9
|
+
* O PIPELINE, e a ordem é a garantia
|
|
10
|
+
*
|
|
11
|
+
* request -> validate -> normalize -> [fronteira] -> fingerprint -> deduplicate -> budget
|
|
12
|
+
* -> persist
|
|
13
|
+
*
|
|
14
|
+
* O QUE ESTE ARQUIVO FAZ TERMINA NA FRONTEIRA. Validar e normalizar são daqui porque a
|
|
15
|
+
* observabilidade de uma condição depende do censo, que está no disco dele - a plataforma não tem
|
|
16
|
+
* como saber se `Loader` é composto por alguém naquele repositório. Identidade, deduplicação,
|
|
17
|
+
* orçamento e persistência são do outro lado, porque dependem do estado e da sessão.
|
|
18
|
+
*
|
|
19
|
+
* O CLIENTE OBSERVA E TESTEMUNHA. A PLATAFORMA LEMBRA E DECIDE O EFEITO DA EVIDÊNCIA.
|
|
20
|
+
*
|
|
21
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
22
|
+
* O AGENTE NÃO FABRICA MEDIÇÃO
|
|
23
|
+
*
|
|
24
|
+
* `verdict`, `progress` e `snapshotId` estão em `DERIVED_FIELDS`: eles chegam pelo canal de medição
|
|
25
|
+
* (o `sync`/`doctor`, que roda o avaliador contra o censo), nunca por `remember`. Sem essa linha, um
|
|
26
|
+
* agente poderia escrever `verdict: "met"` e se conceder a autoridade de ter medido - a mesma
|
|
27
|
+
* escalada que `measured` como procedência já não permite.
|
|
28
|
+
*/
|
|
29
|
+
import { DECLARABLE, DERIVED_FIELDS, } from "./contract.js";
|
|
30
|
+
import { validateCondition } from "./predicate.js";
|
|
31
|
+
const squash = (v) => v.trim().replace(/\s+/g, " ");
|
|
32
|
+
/** `Button`, `button` e ` Button ` são o mesmo sujeito - e a chave é o que o fingerprint verá. */
|
|
33
|
+
const canonKey = (v) => squash(v)
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[\s_]+/g, "-");
|
|
36
|
+
const rejected = (reason, details) => ({
|
|
37
|
+
accepted: false,
|
|
38
|
+
reason,
|
|
39
|
+
details,
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* O SCHEMA É FECHADO, e campo que ninguém conhece é ERRO - nunca silêncio.
|
|
43
|
+
*
|
|
44
|
+
* Se amanhã um agente mandar `priority: "permanent"` e o servidor ignorar, ele segue acreditando que
|
|
45
|
+
* aquela semântica foi aceita. É a mesma filosofia do campo derivado: recusar em voz alta é a única
|
|
46
|
+
* resposta que não mente.
|
|
47
|
+
*/
|
|
48
|
+
const ALLOWED = {
|
|
49
|
+
decision: [
|
|
50
|
+
"kind",
|
|
51
|
+
"text",
|
|
52
|
+
"applies",
|
|
53
|
+
"ruleKind",
|
|
54
|
+
"when",
|
|
55
|
+
"family",
|
|
56
|
+
"tasks",
|
|
57
|
+
"subject",
|
|
58
|
+
"provenance",
|
|
59
|
+
],
|
|
60
|
+
rationale: ["kind", "text", "subject", "provenance", "confirmation"],
|
|
61
|
+
"work-state": ["kind", "exit", "createdBy", "provenance"],
|
|
62
|
+
};
|
|
63
|
+
export function rejectDerived(payload) {
|
|
64
|
+
const sent = Object.keys(payload).filter((k) => DERIVED_FIELDS.includes(k));
|
|
65
|
+
return sent.length === 0
|
|
66
|
+
? null
|
|
67
|
+
: rejected(sent.some((k) => k.startsWith("verdict") ||
|
|
68
|
+
k.startsWith("snapshot") ||
|
|
69
|
+
k === "progress")
|
|
70
|
+
? "fabricated_verdict"
|
|
71
|
+
: "derived_field_supplied", `${sent.join(", ")} ${sent.length === 1 ? "is" : "are"} derived from measurement on our side - propose the fact, never its authority`);
|
|
72
|
+
}
|
|
73
|
+
export function rejectUnknown(payload) {
|
|
74
|
+
const kind = String(payload.kind ?? "");
|
|
75
|
+
const allowed = ALLOWED[kind];
|
|
76
|
+
if (!allowed)
|
|
77
|
+
return rejected("unknown_field", `"${kind || "(nothing)"}" is not a kind of memory this platform stores`);
|
|
78
|
+
const derived = rejectDerived(payload);
|
|
79
|
+
if (derived)
|
|
80
|
+
return derived;
|
|
81
|
+
const strange = Object.keys(payload).filter((k) => !allowed.includes(k));
|
|
82
|
+
return strange.length === 0
|
|
83
|
+
? null
|
|
84
|
+
: rejected("unknown_field", `${strange.join(", ")} ${strange.length === 1 ? "is not a field" : "are not fields"} of a ${kind} - a field nobody reads would look accepted`);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* VALIDAR E NORMALIZAR - e parar antes da identidade.
|
|
88
|
+
*
|
|
89
|
+
* O CLI não sabe quem é o usuário: as credenciais guardam um token, não um id. E isso não é uma
|
|
90
|
+
* limitação a contornar, é a regra deste desenho: a identidade só pode ser fechada no primeiro lugar
|
|
91
|
+
* que tem tudo para prová-la.
|
|
92
|
+
*/
|
|
93
|
+
export function prepareWrite(m, obs) {
|
|
94
|
+
if (!DECLARABLE.includes(m.provenance))
|
|
95
|
+
return rejected("invalid_provenance", `"${m.provenance}" is not something an agent can claim - measurement is conferred by whoever measures`);
|
|
96
|
+
if (m.kind !== "work-state") {
|
|
97
|
+
if (!squash(m.text))
|
|
98
|
+
return rejected("empty_text", "a memory with no text says nothing");
|
|
99
|
+
/**
|
|
100
|
+
* `rationale` FALA DO QUE NÃO ESTÁ NO CÓDIGO, então nenhuma medição pode confirmá-lo nem
|
|
101
|
+
* desmenti-lo - ele nunca decai. Por isso ele não nasce de inferência: o agente PROPÕE, e uma
|
|
102
|
+
* pessoa confirma. Sem esta linha, dois anos de "parece que vocês evitam X" seriam tratados
|
|
103
|
+
* como conhecimento atual.
|
|
104
|
+
*/
|
|
105
|
+
if (m.kind === "rationale" && m.provenance === "agent_inferred")
|
|
106
|
+
return rejected("inferred_rationale_not_allowed", "a rationale never starts as inference - propose it and let a person confirm");
|
|
107
|
+
}
|
|
108
|
+
if (m.kind === "work-state") {
|
|
109
|
+
const check = validateCondition(m.exit, obs);
|
|
110
|
+
if (!check.ok) {
|
|
111
|
+
/**
|
|
112
|
+
* O MOTIVO DA RECUSA É TRADUZIDO PARA O ENUM, porque o agente do outro lado precisa de um
|
|
113
|
+
* código - não de uma frase que pode mudar de redação.
|
|
114
|
+
*/
|
|
115
|
+
const reason = check.because.includes("reference_replaced")
|
|
116
|
+
? "weak_migration_predicate"
|
|
117
|
+
: check.because.includes("cannot be observed") ||
|
|
118
|
+
check.because.includes("no readable recipe")
|
|
119
|
+
? "unobservable_subject"
|
|
120
|
+
: "invalid_condition";
|
|
121
|
+
return rejected(reason, check.because);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const subject = m.kind === "work-state" ? m.exit.subject : m.subject;
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
draft: {
|
|
128
|
+
kind: m.kind,
|
|
129
|
+
...(subject ? { subject: squash(subject) } : {}),
|
|
130
|
+
key: subject ? canonKey(subject) : "",
|
|
131
|
+
...(m.kind === "work-state"
|
|
132
|
+
? { exit: m.exit }
|
|
133
|
+
: { text: squash(m.text) }),
|
|
134
|
+
applies: m.kind === "decision" ? m.applies.map(squash) : [],
|
|
135
|
+
...(m.kind === "decision" ? { ruleKind: m.ruleKind } : {}),
|
|
136
|
+
provenance: m.provenance,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { anatomyFromSketch } from "./anatomy-from-sketch.js";
|
|
2
|
+
/** O nó que a derivação não soube nomear. `box` é o último recurso dela. */
|
|
3
|
+
const UNNAMED = "box";
|
|
4
|
+
function walk(read, onNode) {
|
|
5
|
+
for (const node of read) {
|
|
6
|
+
onNode(node);
|
|
7
|
+
if (Array.isArray(node.children) && node.children.length > 0)
|
|
8
|
+
walk(node.children, onNode);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A fila, derivada dos mesmos insumos que o envio usa.
|
|
13
|
+
*
|
|
14
|
+
* `defines` e `versionOf` entram porque a derivação os usa para distinguir um componente DELES de
|
|
15
|
+
* uma peça de terceiro, e um nó que vira `component` ou `external` é um nó nomeado: computar a fila
|
|
16
|
+
* sem eles a inflaria com trabalho que não existe.
|
|
17
|
+
*/
|
|
18
|
+
export function namingQueue(looks, defines, versionOf) {
|
|
19
|
+
const pending = [];
|
|
20
|
+
let components = 0;
|
|
21
|
+
let nodes = 0;
|
|
22
|
+
let named = 0;
|
|
23
|
+
let settled = 0;
|
|
24
|
+
for (const component of Object.keys(looks)) {
|
|
25
|
+
const sketch = looks[component]?.sketch;
|
|
26
|
+
if (!Array.isArray(sketch) || sketch.length === 0)
|
|
27
|
+
continue;
|
|
28
|
+
const derived = anatomyFromSketch(sketch, defines, versionOf);
|
|
29
|
+
if (derived.read.length === 0)
|
|
30
|
+
continue;
|
|
31
|
+
components += 1;
|
|
32
|
+
const unnamed = [];
|
|
33
|
+
walk(derived.read, (node) => {
|
|
34
|
+
nodes += 1;
|
|
35
|
+
if (node.name !== UNNAMED) {
|
|
36
|
+
named += 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const at = node.at;
|
|
40
|
+
if (typeof at !== "number")
|
|
41
|
+
return;
|
|
42
|
+
const raw = sketch[at];
|
|
43
|
+
unnamed.push({
|
|
44
|
+
tag: raw?.tag ?? "div",
|
|
45
|
+
at,
|
|
46
|
+
...(raw?.classes ? { classes: raw.classes } : {}),
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
if (unnamed.length === 0) {
|
|
50
|
+
settled += 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
pending.push({ component, nodes: unnamed });
|
|
54
|
+
}
|
|
55
|
+
return { components, nodes, named, settled, pending };
|
|
56
|
+
}
|
package/package.json
CHANGED