@nac3/forge-cli 1.14.0 → 1.16.0

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.
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nac3/forge-cli",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "Yujin Forge -- voice-first NAC-3 React development framework. CLI + chat panel + spec ingest + 10-format document reader + voice loop.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Pablo Kuschnirof <pablo@rpaforce.com>",
@@ -11,7 +11,8 @@
11
11
  "test:watch": "vitest",
12
12
  "test:e2e": "playwright test",
13
13
  "lint": "next lint",
14
- "typecheck": "tsc --noEmit",
14
+ "typecheck": "tsc --noEmit && yf audit persistence",
15
+ "audit:persistence": "yf audit persistence",
15
16
  "yf": "yf"
16
17
  },
17
18
  "dependencies": {
@@ -11,7 +11,8 @@
11
11
  "test": "vitest run",
12
12
  "test:watch": "vitest",
13
13
  "test:e2e": "playwright test",
14
- "lint": "tsc --noEmit",
14
+ "lint": "tsc --noEmit && yf audit persistence",
15
+ "audit:persistence": "yf audit persistence",
15
16
  "yf": "yf"
16
17
  },
17
18
  "dependencies": {