@nac3/forge-cli 1.15.0 → 1.16.1
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/audit/persistence_async.d.ts +101 -0
- package/dist/audit/persistence_async.d.ts.map +1 -0
- package/dist/audit/persistence_async.js +435 -0
- package/dist/audit/persistence_async.js.map +1 -0
- package/dist/bin/yf.d.ts.map +1 -1
- package/dist/bin/yf.js +2 -0
- package/dist/bin/yf.js.map +1 -1
- package/dist/chat/doctrine_digest.d.ts.map +1 -1
- package/dist/chat/doctrine_digest.js +17 -9
- package/dist/chat/doctrine_digest.js.map +1 -1
- package/dist/chat/server.d.ts.map +1 -1
- package/dist/chat/server.js +8 -2
- package/dist/chat/server.js.map +1 -1
- package/dist/commands/audit-persistence.d.ts +33 -0
- package/dist/commands/audit-persistence.d.ts.map +1 -0
- package/dist/commands/audit-persistence.js +173 -0
- package/dist/commands/audit-persistence.js.map +1 -0
- package/dist/docs/doctrine/persistence-interface.md +113 -0
- package/dist/relay/in_flight_turn.d.ts +42 -9
- package/dist/relay/in_flight_turn.d.ts.map +1 -1
- package/dist/relay/in_flight_turn.js +57 -13
- package/dist/relay/in_flight_turn.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/doctrine/persistence-interface.md +113 -0
- package/package.json +1 -1
- package/templates/nextjs-app/package.json +2 -1
- package/templates/react-app/package.json +2 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `yf audit persistence [path]` -- la interfaz de persistencia es ASINCRONICA.
|
|
3
|
+
*
|
|
4
|
+
* El candado que le faltaba a Forge, y la razon esta en ADIP-Kikan (Pablo,
|
|
5
|
+
* 13-oct-2026): el brain construyo la capa de repositorio bien y persiste en
|
|
6
|
+
* MariaDB desde el primer commit, pero definio la interfaz SINCRONICA. Eso
|
|
7
|
+
* obligo al store de MariaDB a mantener el working set en RAM para servir
|
|
8
|
+
* lecturas sincronas, y de ahi el esquema degenero en una tabla generica de
|
|
9
|
+
* JSON. Una firma de funcion, 1.543 puntos de deuda y nueve slices de
|
|
10
|
+
* conversion.
|
|
11
|
+
*
|
|
12
|
+
* El analisis vive en `src/audit/persistence_async.ts`, puro y con bateria.
|
|
13
|
+
* Este archivo es solo el recorrido del arbol y el reporte.
|
|
14
|
+
*
|
|
15
|
+
* Corre sobre CUALQUIER proyecto -- recien nacido o heredado -- y no necesita
|
|
16
|
+
* configuracion: el detector reconoce un store por su nombre, por su carpeta o
|
|
17
|
+
* por su forma.
|
|
18
|
+
*
|
|
19
|
+
* ASCII-only.
|
|
20
|
+
*/
|
|
21
|
+
import type { Command } from 'commander';
|
|
22
|
+
export interface AuditPersistenceOptions {
|
|
23
|
+
json?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface Recolectado {
|
|
26
|
+
fuentes: Array<[string, string]>;
|
|
27
|
+
/** Salteados por generados/minificados. Se REPORTAN: un salteo invisible es
|
|
28
|
+
* un agujero del mismo tipo que un ilegible que cuenta cero. */
|
|
29
|
+
generados: string[];
|
|
30
|
+
}
|
|
31
|
+
export declare function recolectarFuentes(raiz: string): Recolectado;
|
|
32
|
+
export declare function registerAuditPersistenceCommand(program: Command): void;
|
|
33
|
+
//# sourceMappingURL=audit-persistence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-persistence.d.ts","sourceRoot":"","sources":["../../src/commands/audit-persistence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwCzC,MAAM,WAAW,uBAAuB;IACtC,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjC;qEACiE;IACjE,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CA4B3D;AAoED,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyBtE"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readdirSync, statSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { c, header } from '../ui/colors.js';
|
|
4
|
+
import { analizar, aprueba } from '../audit/persistence_async.js';
|
|
5
|
+
const OMITIR = new Set([
|
|
6
|
+
'node_modules', 'dist', 'coverage', '.git', '.next', 'build', '.expo',
|
|
7
|
+
'.turbo', 'out', 'vendor', 'third_party', '__pycache__', 'site-packages',
|
|
8
|
+
'bower_components', '.cache', '.tox', 'target', '.gradle', '.idea',
|
|
9
|
+
]);
|
|
10
|
+
/* Los entornos virtuales de Python se llaman `.venv`, `venv`, `.venv_test`,
|
|
11
|
+
* `env`... y adentro traen el chromium de Playwright con sus bundles. */
|
|
12
|
+
const OMITIR_PREFIJO = ['.venv', 'venv', '.env-', 'env-'];
|
|
13
|
+
const EXT = /\.(js|mjs|cjs|ts|tsx)$/;
|
|
14
|
+
/* Las baterias quedan afuera: un doble de test SINCRONICO es legitimo -- lo que
|
|
15
|
+
* no puede ser sincronico es el store del que depende el producto. */
|
|
16
|
+
const ES_TEST = /\.(test|spec)\.[jt]sx?$/;
|
|
17
|
+
const ES_MINIFICADO = /\.(min|bundle|chunk)\.[jt]s$/;
|
|
18
|
+
/* Una linea de mas de 1000 caracteres no la escribio una persona: es un bundle,
|
|
19
|
+
* un minificado o un blob generado. Nunca es la capa de persistencia de nadie,
|
|
20
|
+
* y ademas no se puede analizar con sentido.
|
|
21
|
+
*
|
|
22
|
+
* Calibrado contra ADIP-Kikan (13-oct-2026): la primera corrida reporto 26
|
|
23
|
+
* hallazgos dentro de `xtermModule-COQkjINf.js`, un bundle de Playwright
|
|
24
|
+
* metido en un `.venv_harness`. Ese ruido es exactamente lo que hace que un
|
|
25
|
+
* equipo apague un control, y un control apagado no vigila nada. */
|
|
26
|
+
const LARGO_DE_LINEA_GENERADO = 1000;
|
|
27
|
+
function pareceGenerado(texto) {
|
|
28
|
+
let largo = 0;
|
|
29
|
+
for (let i = 0; i < texto.length; i++) {
|
|
30
|
+
if (texto[i] === '\n') {
|
|
31
|
+
largo = 0;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (++largo > LARGO_DE_LINEA_GENERADO)
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
export function recolectarFuentes(raiz) {
|
|
40
|
+
const fuentes = [];
|
|
41
|
+
const generados = [];
|
|
42
|
+
const omitirDir = (n) => OMITIR.has(n) || OMITIR_PREFIJO.some((p) => n.startsWith(p));
|
|
43
|
+
const rec = (dir) => {
|
|
44
|
+
let items;
|
|
45
|
+
try {
|
|
46
|
+
items = readdirSync(dir);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
for (const nombre of items.sort()) {
|
|
52
|
+
const p = path.join(dir, nombre);
|
|
53
|
+
let st;
|
|
54
|
+
try {
|
|
55
|
+
st = statSync(p);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (st.isDirectory()) {
|
|
61
|
+
if (!omitirDir(nombre))
|
|
62
|
+
rec(p);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!EXT.test(nombre) || ES_TEST.test(nombre))
|
|
66
|
+
continue;
|
|
67
|
+
const rel = path.relative(raiz, p).split(path.sep).join('/');
|
|
68
|
+
if (ES_MINIFICADO.test(nombre)) {
|
|
69
|
+
generados.push(rel);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
let texto;
|
|
73
|
+
try {
|
|
74
|
+
texto = readFileSync(p, 'utf8');
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (pareceGenerado(texto)) {
|
|
80
|
+
generados.push(rel);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
fuentes.push([rel, texto]);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
rec(raiz);
|
|
87
|
+
return { fuentes, generados };
|
|
88
|
+
}
|
|
89
|
+
function imprimir(raiz, r, archivos, generados) {
|
|
90
|
+
header('Yujin Forge -- audit persistence');
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(' Proyecto: ' + c.dim(raiz));
|
|
93
|
+
console.log(' ' + c.dim(archivos + ' archivos leidos, ' + r.modulos.length + ' modulo(s) de persistencia'
|
|
94
|
+
+ (generados ? ', ' + generados + ' generado(s)/minificado(s) salteado(s)' : '')));
|
|
95
|
+
console.log('');
|
|
96
|
+
if (r.modulos.length === 0) {
|
|
97
|
+
console.log(c.dim(' No se detecto ninguna capa de persistencia.'));
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log(c.success(' OK') + c.dim(' -- nada que vigilar todavia.'));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
console.log(' Metodos de acceso a datos: ' + r.vistos
|
|
103
|
+
+ c.dim(' (async ' + r.asincronicos + ' / sincronicos ' + r.hallazgos.length + ')'));
|
|
104
|
+
console.log('');
|
|
105
|
+
if (r.hallazgos.length > 0) {
|
|
106
|
+
console.log(c.error(' SINCRONICOS -- cada uno es una interfaz que no puede envolver una base real:'));
|
|
107
|
+
const porArchivo = new Map();
|
|
108
|
+
for (const h of r.hallazgos) {
|
|
109
|
+
const lista = porArchivo.get(h.archivo) ?? [];
|
|
110
|
+
lista.push(h);
|
|
111
|
+
porArchivo.set(h.archivo, lista);
|
|
112
|
+
}
|
|
113
|
+
for (const [archivo, lista] of porArchivo) {
|
|
114
|
+
console.log(' ' + c.dim(archivo));
|
|
115
|
+
for (const h of lista) {
|
|
116
|
+
console.log(' ' + c.error('x') + ' linea ' + String(h.linea).padStart(4)
|
|
117
|
+
+ ' ' + h.metodo + '()' + c.dim(' [' + h.forma + ']'));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
console.log('');
|
|
121
|
+
}
|
|
122
|
+
if (r.exentos.length > 0) {
|
|
123
|
+
console.log(c.warn(' Eximidos a mano (' + r.exentos.length + '):'));
|
|
124
|
+
for (const e of r.exentos) {
|
|
125
|
+
console.log(' ' + c.dim(e.archivo + ':' + e.linea) + ' ' + e.metodo + '() -- ' + e.motivo);
|
|
126
|
+
}
|
|
127
|
+
console.log('');
|
|
128
|
+
}
|
|
129
|
+
if (r.ilegibles.length > 0) {
|
|
130
|
+
console.log(c.error(' NO SE PUDIERON ANALIZAR (' + r.ilegibles.length + '):'));
|
|
131
|
+
for (const f of r.ilegibles)
|
|
132
|
+
console.log(' ' + c.error('?') + ' ' + f);
|
|
133
|
+
console.log(c.dim(' Un archivo que no se puede afirmar NO cuenta como cero: el control falla.'));
|
|
134
|
+
console.log('');
|
|
135
|
+
}
|
|
136
|
+
if (aprueba(r)) {
|
|
137
|
+
console.log(c.success(' OK') + ' -- toda la interfaz de persistencia es asincronica.');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
console.log(c.error(' FALLA'));
|
|
141
|
+
console.log('');
|
|
142
|
+
console.log(c.dim(' Por que esto importa, y por que se arregla AHORA y no despues:'));
|
|
143
|
+
console.log(c.dim(' `await` sobre un valor que no es Promise no hace nada, asi que declarar'));
|
|
144
|
+
console.log(c.dim(' el store async hoy cuesta CERO -- incluso con el store en memoria. El'));
|
|
145
|
+
console.log(c.dim(' camino inverso es cada punto de llamada del sistema: en ADIP-Kikan'));
|
|
146
|
+
console.log(c.dim(' fueron 1.543, en nueve slices. Las dos opciones no son simetricas.'));
|
|
147
|
+
}
|
|
148
|
+
export function registerAuditPersistenceCommand(program) {
|
|
149
|
+
const audit = program.commands.find((cmd) => cmd.name() === 'audit')
|
|
150
|
+
?? program.command('audit').description('Controles estaticos sobre un proyecto.');
|
|
151
|
+
audit
|
|
152
|
+
.command('persistence [path]')
|
|
153
|
+
.description('Exige que todo metodo de un store/repositorio este declarado async.')
|
|
154
|
+
.option('--json', 'emitir JSON en vez del reporte legible')
|
|
155
|
+
.action(async (where, opts) => {
|
|
156
|
+
const raiz = path.resolve(process.cwd(), where ?? '.');
|
|
157
|
+
const { fuentes, generados } = recolectarFuentes(raiz);
|
|
158
|
+
const r = analizar(fuentes);
|
|
159
|
+
if (opts.json) {
|
|
160
|
+
process.stdout.write(JSON.stringify({
|
|
161
|
+
root: raiz, files: fuentes.length, skipped_generated: generados,
|
|
162
|
+
ok: aprueba(r), ...r,
|
|
163
|
+
}, null, 2) + '\n');
|
|
164
|
+
if (!aprueba(r))
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
imprimir(raiz, r, fuentes.length, generados.length);
|
|
169
|
+
if (!aprueba(r))
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=audit-persistence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-persistence.js","sourceRoot":"","sources":["../../src/commands/audit-persistence.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# The persistence interface is async from line one
|
|
2
|
+
|
|
3
|
+
A rule with no sizing round, no questions to the owner, and no exceptions
|
|
4
|
+
worth arguing about. It is free to obey and it is the most expensive thing
|
|
5
|
+
in this repo to disobey.
|
|
6
|
+
|
|
7
|
+
## The rule
|
|
8
|
+
|
|
9
|
+
**Every method of every store, repository or DAO is declared `async`, from
|
|
10
|
+
the first commit, whatever is behind it.** In memory, on disk, over a
|
|
11
|
+
network, a stub that returns `null` -- all of them.
|
|
12
|
+
|
|
13
|
+
Mechanically enforced by `yf audit persistence`. New projects carry it in
|
|
14
|
+
their own CI. The check fails on a synchronous method and it also fails on a
|
|
15
|
+
file it cannot analyse: a store whose signature cannot be asserted is not a
|
|
16
|
+
store that passed.
|
|
17
|
+
|
|
18
|
+
## Why it needs no information to decide
|
|
19
|
+
|
|
20
|
+
The two directions are not symmetric:
|
|
21
|
+
|
|
22
|
+
| Direction | Cost |
|
|
23
|
+
|---|---|
|
|
24
|
+
| async interface, synchronous implementation behind it | **zero.** `await` on a non-Promise is a no-op |
|
|
25
|
+
| synchronous interface, later made async | **every call site in the system** |
|
|
26
|
+
|
|
27
|
+
An asymmetric decision is always taken on the recoverable side. There is no
|
|
28
|
+
volumetry that makes the sync interface the better bet, because the sync
|
|
29
|
+
interface's only advantage is that it is marginally shorter to type.
|
|
30
|
+
|
|
31
|
+
So this rule does not wait for G9, does not wait for the owner to answer how
|
|
32
|
+
many rows per year, and does not appear in the options catalogue. It is not a
|
|
33
|
+
choice.
|
|
34
|
+
|
|
35
|
+
## What it actually prevents: ADIP-Kikan
|
|
36
|
+
|
|
37
|
+
Measured, 13-oct-2026. Kikan's first commit reads *"nucleo kawa/sazanami +
|
|
38
|
+
persistencia MariaDB"*. The brain built the repository layer, built two
|
|
39
|
+
implementations, and wired production to MariaDB with write-through and
|
|
40
|
+
boot-time hydration. The data was durable and always had been.
|
|
41
|
+
|
|
42
|
+
One decision was wrong, and it was a **signature**: the repository interface
|
|
43
|
+
was synchronous. Everything else followed mechanically.
|
|
44
|
+
|
|
45
|
+
1. A real SQL driver is asynchronous, and a synchronous interface cannot
|
|
46
|
+
wrap one.
|
|
47
|
+
2. So the "MariaDB store" had to keep the working set in RAM to serve reads
|
|
48
|
+
synchronously. Its own header says so: *"lecturas sincronas, interfaz
|
|
49
|
+
IDENTICA a createMemoryStore -> no reescribe el motor"*.
|
|
50
|
+
3. And because an in-memory working set needs no columns, the schema
|
|
51
|
+
degenerated into **one generic table of JSON rows** (`engine_rows`), with
|
|
52
|
+
the 57 normalised tables demoted to a derived read-model instead of being
|
|
53
|
+
the system of record.
|
|
54
|
+
|
|
55
|
+
The bill: **1.543 call sites**, nine conversion slices, a static ratchet
|
|
56
|
+
built to hold the line, and a reengineering plan of its own.
|
|
57
|
+
|
|
58
|
+
## Why a brain walks into it
|
|
59
|
+
|
|
60
|
+
Not from ignorance. Every step is locally correct:
|
|
61
|
+
|
|
62
|
+
- When the engine is being written there is **no database yet**. The first
|
|
63
|
+
store you write so the engine is testable is the in-memory one, and its
|
|
64
|
+
natural signature is synchronous -- correct, at that moment.
|
|
65
|
+
- The engine is written against that signature. Now every future store must
|
|
66
|
+
match it or the engine breaks.
|
|
67
|
+
- When the real SQL store finally gets written, that signature is
|
|
68
|
+
load-bearing for hundreds of call sites. Making it async is a repo-wide
|
|
69
|
+
change no single turn can justify and nobody asked for. So the SQL store
|
|
70
|
+
contorts to fit the signature instead.
|
|
71
|
+
|
|
72
|
+
Each step is reasonable. The aggregate is wrong. That is the failure mode of
|
|
73
|
+
incremental construction without an invariant that is checked, and it is why
|
|
74
|
+
this one is a mechanical gate and not a paragraph of advice.
|
|
75
|
+
|
|
76
|
+
Note also what the model never wrote: it never wrote "in-memory
|
|
77
|
+
persistence". It wrote "a repository with a synchronous interface", and that
|
|
78
|
+
shape is everywhere in any training corpus -- every test double, every
|
|
79
|
+
in-process repo, every small app without an ORM. The dangerous default is
|
|
80
|
+
the *common* one, not an exotic one.
|
|
81
|
+
|
|
82
|
+
## The decision that was never taken
|
|
83
|
+
|
|
84
|
+
There was no turn whose subject was "choose the persistence architecture".
|
|
85
|
+
It accumulated. `docs/doctrine/sizing.md` carries the same finding for
|
|
86
|
+
volumetry; this file carries it for the interface, because the interface is
|
|
87
|
+
the half that costs nothing to get right and everything to get wrong.
|
|
88
|
+
|
|
89
|
+
## Exemptions
|
|
90
|
+
|
|
91
|
+
A genuinely synchronous accessor that will never cross a process boundary
|
|
92
|
+
can be marked in place:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
// persistence-async-ok: in-RAM index, never touches disk or network
|
|
96
|
+
get(key) { return this.index.get(key); }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The checker **counts and reports** exemptions. An invisible exemption is a
|
|
100
|
+
hole; a counted one is a decision someone can review. Blanket-disabling the
|
|
101
|
+
check is not an exemption, it is the defect.
|
|
102
|
+
|
|
103
|
+
## What this rule does NOT say
|
|
104
|
+
|
|
105
|
+
- It does not say the store must be SQL. A memory store is fine for a
|
|
106
|
+
prototype; the sizing gate decides that. This rule is only about the
|
|
107
|
+
**signature**.
|
|
108
|
+
- It does not require returning a real Promise. `async` on something that
|
|
109
|
+
already returns one changes nothing; the rule is "declared `async`"
|
|
110
|
+
because that is trivial to satisfy and trivial to verify, and a strict
|
|
111
|
+
simple rule beats a clever one that needs interpreting.
|
|
112
|
+
- It does not apply to test doubles. A synchronous fake inside a battery is
|
|
113
|
+
legitimate; the checker skips `*.test.*` and `*.spec.*` for that reason.
|
|
@@ -33,23 +33,56 @@ export interface InFlightTurn {
|
|
|
33
33
|
started_at: string;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* Cada cuanto el turno da senal de vida. Es el periodo del latido de actividad
|
|
37
|
+
* del servidor (`startRelayHeartbeat`), que corre SOLO entre el arranque y el
|
|
38
|
+
* cierre del turno.
|
|
38
39
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* Mentir sobre lo que el sistema esta haciendo es peor que no decir nada.
|
|
40
|
+
* Vive aca y no en server.ts a proposito: es la mitad de un par acoplado, y la
|
|
41
|
+
* otra mitad es `SILENCIO_MAX_MS`. El bug anterior nacio de tener dos numeros
|
|
42
|
+
* acoplados en dos archivos distintos sin nada que los ligara.
|
|
43
43
|
*/
|
|
44
|
-
export declare const
|
|
45
|
-
|
|
44
|
+
export declare const PULSO_DE_PROGRESO_MS: number;
|
|
45
|
+
/**
|
|
46
|
+
* Tope de SILENCIO. Un turno que no da senal de vida por mas que esto se
|
|
47
|
+
* considera muerto y deja de anunciarse, aunque nadie lo haya limpiado.
|
|
48
|
+
*
|
|
49
|
+
* ANTES ERA UN TOPE DE EDAD, y eso estaba mal (Pablo, 13-oct-2026: "hay turnos
|
|
50
|
+
* de mas de 3 horas en un desarrollo"). La proteccion que se queria es contra
|
|
51
|
+
* un turno COLGADO -- un proceso que muere a mitad de camino, o una excepcion
|
|
52
|
+
* por un camino que no pasa por `clearInFlightTurn` -- porque mentir sobre lo
|
|
53
|
+
* que el sistema esta haciendo es peor que no decir nada. Pero "colgado" no es
|
|
54
|
+
* "viejo": es "sin progreso". Un tope de edad confunde las dos cosas y castiga
|
|
55
|
+
* justo al turno largo, que es el que mas necesita anunciarse.
|
|
56
|
+
*
|
|
57
|
+
* Medido: un turno de 29 minutos en ADIP-Kikan dejaba al telefono diciendo que
|
|
58
|
+
* no habia nada corriendo durante sus ultimos 19 minutos. Con turnos de 3 horas
|
|
59
|
+
* el anuncio serviria para el 5% del turno.
|
|
60
|
+
*
|
|
61
|
+
* Diez pulsos de margen: tolera que se pierdan nueve latidos seguidos antes de
|
|
62
|
+
* dar por muerto un turno que esta vivo.
|
|
63
|
+
*/
|
|
64
|
+
export declare const SILENCIO_MAX_MS: number;
|
|
65
|
+
export declare function setInFlightTurn(t: InFlightTurn, now?: number): void;
|
|
66
|
+
/**
|
|
67
|
+
* Senal de vida del turno en curso. La llama el latido de actividad del
|
|
68
|
+
* servidor, que corre solo mientras hay turno.
|
|
69
|
+
*
|
|
70
|
+
* No crea nada: si no hay turno registrado, no hace nada. Asi un latido
|
|
71
|
+
* rezagado no puede resucitar un turno ya cerrado.
|
|
72
|
+
*/
|
|
73
|
+
export declare function touchInFlightTurn(now?: number): void;
|
|
46
74
|
/**
|
|
47
75
|
* Limpia el turno en vuelo. Con `id`, SOLO si coincide: dos turnos encadenados
|
|
48
76
|
* pueden cerrar fuera de orden, y un cierre tardio del viejo no tiene que
|
|
49
77
|
* borrar el anuncio del que recien arranco.
|
|
50
78
|
*/
|
|
51
79
|
export declare function clearInFlightTurn(id?: string): void;
|
|
52
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* El turno en vuelo, o null si no hay o si se quedo CALLADO.
|
|
82
|
+
*
|
|
83
|
+
* La edad del turno no entra en la decision: un turno de tres horas que sigue
|
|
84
|
+
* latiendo es un turno vivo y se anuncia. Lo que lo saca es el silencio.
|
|
85
|
+
*/
|
|
53
86
|
export declare function inFlightTurn(now?: number): InFlightTurn | null;
|
|
54
87
|
/** Solo para tests. */
|
|
55
88
|
export declare function _resetInFlightTurn(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in_flight_turn.d.ts","sourceRoot":"","sources":["../../src/relay/in_flight_turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"in_flight_turn.d.ts","sourceRoot":"","sources":["../../src/relay/in_flight_turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,QAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,eAAe,QAA4B,CAAC;AASzD,wBAAgB,eAAe,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,GAAE,MAAmB,GAAG,IAAI,CAQ/E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAmB,GAAG,IAAI,CAGhE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAInD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAmB,GAAG,YAAY,GAAG,IAAI,CAU1E;AAED,uBAAuB;AACvB,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
|
|
@@ -27,20 +27,57 @@
|
|
|
27
27
|
* ASCII puro.
|
|
28
28
|
*/
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* Cada cuanto el turno da senal de vida. Es el periodo del latido de actividad
|
|
31
|
+
* del servidor (`startRelayHeartbeat`), que corre SOLO entre el arranque y el
|
|
32
|
+
* cierre del turno.
|
|
32
33
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* Mentir sobre lo que el sistema esta haciendo es peor que no decir nada.
|
|
34
|
+
* Vive aca y no en server.ts a proposito: es la mitad de un par acoplado, y la
|
|
35
|
+
* otra mitad es `SILENCIO_MAX_MS`. El bug anterior nacio de tener dos numeros
|
|
36
|
+
* acoplados en dos archivos distintos sin nada que los ligara.
|
|
37
37
|
*/
|
|
38
|
-
export const
|
|
38
|
+
export const PULSO_DE_PROGRESO_MS = 60 * 1000;
|
|
39
|
+
/**
|
|
40
|
+
* Tope de SILENCIO. Un turno que no da senal de vida por mas que esto se
|
|
41
|
+
* considera muerto y deja de anunciarse, aunque nadie lo haya limpiado.
|
|
42
|
+
*
|
|
43
|
+
* ANTES ERA UN TOPE DE EDAD, y eso estaba mal (Pablo, 13-oct-2026: "hay turnos
|
|
44
|
+
* de mas de 3 horas en un desarrollo"). La proteccion que se queria es contra
|
|
45
|
+
* un turno COLGADO -- un proceso que muere a mitad de camino, o una excepcion
|
|
46
|
+
* por un camino que no pasa por `clearInFlightTurn` -- porque mentir sobre lo
|
|
47
|
+
* que el sistema esta haciendo es peor que no decir nada. Pero "colgado" no es
|
|
48
|
+
* "viejo": es "sin progreso". Un tope de edad confunde las dos cosas y castiga
|
|
49
|
+
* justo al turno largo, que es el que mas necesita anunciarse.
|
|
50
|
+
*
|
|
51
|
+
* Medido: un turno de 29 minutos en ADIP-Kikan dejaba al telefono diciendo que
|
|
52
|
+
* no habia nada corriendo durante sus ultimos 19 minutos. Con turnos de 3 horas
|
|
53
|
+
* el anuncio serviria para el 5% del turno.
|
|
54
|
+
*
|
|
55
|
+
* Diez pulsos de margen: tolera que se pierdan nueve latidos seguidos antes de
|
|
56
|
+
* dar por muerto un turno que esta vivo.
|
|
57
|
+
*/
|
|
58
|
+
export const SILENCIO_MAX_MS = PULSO_DE_PROGRESO_MS * 10;
|
|
39
59
|
let current = null;
|
|
40
|
-
export function setInFlightTurn(t) {
|
|
60
|
+
export function setInFlightTurn(t, now = Date.now()) {
|
|
41
61
|
if (!t || !t.id)
|
|
42
62
|
return;
|
|
43
|
-
current = {
|
|
63
|
+
current = {
|
|
64
|
+
id: String(t.id),
|
|
65
|
+
user: String(t.user || ''),
|
|
66
|
+
started_at: String(t.started_at || new Date(now).toISOString()),
|
|
67
|
+
visto: now,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Senal de vida del turno en curso. La llama el latido de actividad del
|
|
72
|
+
* servidor, que corre solo mientras hay turno.
|
|
73
|
+
*
|
|
74
|
+
* No crea nada: si no hay turno registrado, no hace nada. Asi un latido
|
|
75
|
+
* rezagado no puede resucitar un turno ya cerrado.
|
|
76
|
+
*/
|
|
77
|
+
export function touchInFlightTurn(now = Date.now()) {
|
|
78
|
+
if (!current)
|
|
79
|
+
return;
|
|
80
|
+
current.visto = now;
|
|
44
81
|
}
|
|
45
82
|
/**
|
|
46
83
|
* Limpia el turno en vuelo. Con `id`, SOLO si coincide: dos turnos encadenados
|
|
@@ -54,16 +91,23 @@ export function clearInFlightTurn(id) {
|
|
|
54
91
|
return;
|
|
55
92
|
current = null;
|
|
56
93
|
}
|
|
57
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* El turno en vuelo, o null si no hay o si se quedo CALLADO.
|
|
96
|
+
*
|
|
97
|
+
* La edad del turno no entra en la decision: un turno de tres horas que sigue
|
|
98
|
+
* latiendo es un turno vivo y se anuncia. Lo que lo saca es el silencio.
|
|
99
|
+
*/
|
|
58
100
|
export function inFlightTurn(now = Date.now()) {
|
|
59
101
|
if (!current)
|
|
60
102
|
return null;
|
|
61
|
-
|
|
62
|
-
|
|
103
|
+
/* Un reloj que salta hacia atras da silencio negativo: no vence. Preferible
|
|
104
|
+
* anunciar un turno de mas que descartar uno vivo por un salto de NTP. */
|
|
105
|
+
if (now - current.visto > SILENCIO_MAX_MS) {
|
|
63
106
|
current = null;
|
|
64
107
|
return null;
|
|
65
108
|
}
|
|
66
|
-
|
|
109
|
+
const { id, user, started_at } = current;
|
|
110
|
+
return { id, user, started_at };
|
|
67
111
|
}
|
|
68
112
|
/** Solo para tests. */
|
|
69
113
|
export function _resetInFlightTurn() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in_flight_turn.js","sourceRoot":"","sources":["../../src/relay/in_flight_turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,GAAG,
|
|
1
|
+
{"version":3,"file":"in_flight_turn.js","sourceRoot":"","sources":["../../src/relay/in_flight_turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,GAAG,IAAI,CAAC;AAE9C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,oBAAoB,GAAG,EAAE,CAAC;AAOzD,IAAI,OAAO,GAAoB,IAAI,CAAC;AAEpC,MAAM,UAAU,eAAe,CAAC,CAAe,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IACvE,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;QAAE,OAAO;IACxB,OAAO,GAAG;QACR,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;QAC1B,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;QAC/D,KAAK,EAAE,GAAG;KACX,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;IACxD,IAAI,CAAC,OAAO;QAAE,OAAO;IACrB,OAAO,CAAC,KAAK,GAAG,GAAG,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAW;IAC3C,IAAI,CAAC,OAAO;QAAE,OAAO;IACrB,IAAI,EAAE,IAAI,OAAO,CAAC,EAAE,KAAK,EAAE;QAAE,OAAO;IACpC,OAAO,GAAG,IAAI,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;IACnD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B;8EAC0E;IAC1E,IAAI,GAAG,GAAG,OAAO,CAAC,KAAK,GAAG,eAAe,EAAE,CAAC;QAC1C,OAAO,GAAG,IAAI,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IACzC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAClC,CAAC;AAED,uBAAuB;AACvB,MAAM,UAAU,kBAAkB;IAChC,OAAO,GAAG,IAAI,CAAC;AACjB,CAAC"}
|
package/dist/version.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Yujin Forge CLI version. Kept in sync with package.json by the npm
|
|
3
3
|
* "version" lifecycle (scripts/sync_version.mjs) so the two never drift.
|
|
4
4
|
*/
|
|
5
|
-
export declare const VERSION = "1.
|
|
5
|
+
export declare const VERSION = "1.16.1";
|
|
6
6
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# The persistence interface is async from line one
|
|
2
|
+
|
|
3
|
+
A rule with no sizing round, no questions to the owner, and no exceptions
|
|
4
|
+
worth arguing about. It is free to obey and it is the most expensive thing
|
|
5
|
+
in this repo to disobey.
|
|
6
|
+
|
|
7
|
+
## The rule
|
|
8
|
+
|
|
9
|
+
**Every method of every store, repository or DAO is declared `async`, from
|
|
10
|
+
the first commit, whatever is behind it.** In memory, on disk, over a
|
|
11
|
+
network, a stub that returns `null` -- all of them.
|
|
12
|
+
|
|
13
|
+
Mechanically enforced by `yf audit persistence`. New projects carry it in
|
|
14
|
+
their own CI. The check fails on a synchronous method and it also fails on a
|
|
15
|
+
file it cannot analyse: a store whose signature cannot be asserted is not a
|
|
16
|
+
store that passed.
|
|
17
|
+
|
|
18
|
+
## Why it needs no information to decide
|
|
19
|
+
|
|
20
|
+
The two directions are not symmetric:
|
|
21
|
+
|
|
22
|
+
| Direction | Cost |
|
|
23
|
+
|---|---|
|
|
24
|
+
| async interface, synchronous implementation behind it | **zero.** `await` on a non-Promise is a no-op |
|
|
25
|
+
| synchronous interface, later made async | **every call site in the system** |
|
|
26
|
+
|
|
27
|
+
An asymmetric decision is always taken on the recoverable side. There is no
|
|
28
|
+
volumetry that makes the sync interface the better bet, because the sync
|
|
29
|
+
interface's only advantage is that it is marginally shorter to type.
|
|
30
|
+
|
|
31
|
+
So this rule does not wait for G9, does not wait for the owner to answer how
|
|
32
|
+
many rows per year, and does not appear in the options catalogue. It is not a
|
|
33
|
+
choice.
|
|
34
|
+
|
|
35
|
+
## What it actually prevents: ADIP-Kikan
|
|
36
|
+
|
|
37
|
+
Measured, 13-oct-2026. Kikan's first commit reads *"nucleo kawa/sazanami +
|
|
38
|
+
persistencia MariaDB"*. The brain built the repository layer, built two
|
|
39
|
+
implementations, and wired production to MariaDB with write-through and
|
|
40
|
+
boot-time hydration. The data was durable and always had been.
|
|
41
|
+
|
|
42
|
+
One decision was wrong, and it was a **signature**: the repository interface
|
|
43
|
+
was synchronous. Everything else followed mechanically.
|
|
44
|
+
|
|
45
|
+
1. A real SQL driver is asynchronous, and a synchronous interface cannot
|
|
46
|
+
wrap one.
|
|
47
|
+
2. So the "MariaDB store" had to keep the working set in RAM to serve reads
|
|
48
|
+
synchronously. Its own header says so: *"lecturas sincronas, interfaz
|
|
49
|
+
IDENTICA a createMemoryStore -> no reescribe el motor"*.
|
|
50
|
+
3. And because an in-memory working set needs no columns, the schema
|
|
51
|
+
degenerated into **one generic table of JSON rows** (`engine_rows`), with
|
|
52
|
+
the 57 normalised tables demoted to a derived read-model instead of being
|
|
53
|
+
the system of record.
|
|
54
|
+
|
|
55
|
+
The bill: **1.543 call sites**, nine conversion slices, a static ratchet
|
|
56
|
+
built to hold the line, and a reengineering plan of its own.
|
|
57
|
+
|
|
58
|
+
## Why a brain walks into it
|
|
59
|
+
|
|
60
|
+
Not from ignorance. Every step is locally correct:
|
|
61
|
+
|
|
62
|
+
- When the engine is being written there is **no database yet**. The first
|
|
63
|
+
store you write so the engine is testable is the in-memory one, and its
|
|
64
|
+
natural signature is synchronous -- correct, at that moment.
|
|
65
|
+
- The engine is written against that signature. Now every future store must
|
|
66
|
+
match it or the engine breaks.
|
|
67
|
+
- When the real SQL store finally gets written, that signature is
|
|
68
|
+
load-bearing for hundreds of call sites. Making it async is a repo-wide
|
|
69
|
+
change no single turn can justify and nobody asked for. So the SQL store
|
|
70
|
+
contorts to fit the signature instead.
|
|
71
|
+
|
|
72
|
+
Each step is reasonable. The aggregate is wrong. That is the failure mode of
|
|
73
|
+
incremental construction without an invariant that is checked, and it is why
|
|
74
|
+
this one is a mechanical gate and not a paragraph of advice.
|
|
75
|
+
|
|
76
|
+
Note also what the model never wrote: it never wrote "in-memory
|
|
77
|
+
persistence". It wrote "a repository with a synchronous interface", and that
|
|
78
|
+
shape is everywhere in any training corpus -- every test double, every
|
|
79
|
+
in-process repo, every small app without an ORM. The dangerous default is
|
|
80
|
+
the *common* one, not an exotic one.
|
|
81
|
+
|
|
82
|
+
## The decision that was never taken
|
|
83
|
+
|
|
84
|
+
There was no turn whose subject was "choose the persistence architecture".
|
|
85
|
+
It accumulated. `docs/doctrine/sizing.md` carries the same finding for
|
|
86
|
+
volumetry; this file carries it for the interface, because the interface is
|
|
87
|
+
the half that costs nothing to get right and everything to get wrong.
|
|
88
|
+
|
|
89
|
+
## Exemptions
|
|
90
|
+
|
|
91
|
+
A genuinely synchronous accessor that will never cross a process boundary
|
|
92
|
+
can be marked in place:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
// persistence-async-ok: in-RAM index, never touches disk or network
|
|
96
|
+
get(key) { return this.index.get(key); }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The checker **counts and reports** exemptions. An invisible exemption is a
|
|
100
|
+
hole; a counted one is a decision someone can review. Blanket-disabling the
|
|
101
|
+
check is not an exemption, it is the defect.
|
|
102
|
+
|
|
103
|
+
## What this rule does NOT say
|
|
104
|
+
|
|
105
|
+
- It does not say the store must be SQL. A memory store is fine for a
|
|
106
|
+
prototype; the sizing gate decides that. This rule is only about the
|
|
107
|
+
**signature**.
|
|
108
|
+
- It does not require returning a real Promise. `async` on something that
|
|
109
|
+
already returns one changes nothing; the rule is "declared `async`"
|
|
110
|
+
because that is trivial to satisfy and trivial to verify, and a strict
|
|
111
|
+
simple rule beats a clever one that needs interpreting.
|
|
112
|
+
- It does not apply to test doubles. A synchronous fake inside a battery is
|
|
113
|
+
legitimate; the checker skips `*.test.*` and `*.spec.*` for that reason.
|