@nac3/forge-cli 1.0.89 → 1.0.90

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.
@@ -1 +1 @@
1
- {"version":3,"file":"doctrine_digest.d.ts","sourceRoot":"","sources":["../../src/chat/doctrine_digest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,eAAe,EAAE,mBAAmB,EAyHhD,CAAC;AAEF,6DAA6D;AAC7D,wBAAgB,WAAW,IAAI,MAAM,EAAE,CAEtC;AAED,MAAM,WAAW,qBAAqB;IACpC;4EACwE;IACxE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;uEACuE;AACvE,wBAAgB,wBAAwB,CAAC,IAAI,GAAE,qBAA0B,GAAG,MAAM,EAAE,CAiBnF"}
1
+ {"version":3,"file":"doctrine_digest.d.ts","sourceRoot":"","sources":["../../src/chat/doctrine_digest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,eAAe,EAAE,mBAAmB,EA2IhD,CAAC;AAEF,6DAA6D;AAC7D,wBAAgB,WAAW,IAAI,MAAM,EAAE,CAEtC;AAED,MAAM,WAAW,qBAAqB;IACpC;4EACwE;IACxE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;uEACuE;AACvE,wBAAgB,wBAAwB,CAAC,IAAI,GAAE,qBAA0B,GAAG,MAAM,EAAE,CAiBnF"}
@@ -105,6 +105,23 @@ export const DOCTRINE_DIGEST = [
105
105
  + 'number (a 20-user tool stays single-tier; neither a bottleneck nor '
106
106
  + 'gold-plating).',
107
107
  },
108
+ {
109
+ slugs: ['performance-hardening'],
110
+ rule: 'harden every load-bearing component for its load in v1 -- match the '
111
+ + 'trigger (full patterns via discover): DB batch = COMMIT PER CHUNK, '
112
+ + 'never a tx open across a network call, deadlock retry w/ backoff but '
113
+ + 'never an external call inside a retry; third parties NON-idempotent -- '
114
+ + 'dedupe by id ON WRITE (UNIQUE), ACK-first + background, pace outbound, '
115
+ + 'firm reject -> dead-end not retry-burst; background loops = '
116
+ + 'backpressure cap per pass + TTL-keyed locks + start-once singletons; '
117
+ + 'lists PAGINATE server-side; growing tables INDEX filtered/sorted cols, '
118
+ + 'no fn on an indexed col, no LIKE %q%, VARCHAR(191) not TEXT; long work '
119
+ + '-> job+poll not a 504; TTL-clean growth; heartbeat+watchdog+'
120
+ + 'alert-dedupe. LAWS: cero hardcode (thresholds in config, default '
121
+ + 'preserves behavior, dangerous features born inert), producer/consumer '
122
+ + 'symmetry, fail-closed, MEASURE before a fan-out switch, PROPORTIONAL '
123
+ + '(pairs with concurrency-scale).',
124
+ },
108
125
  {
109
126
  slugs: ['verb-composition'],
110
127
  rule: 'compose native verbs in dependency order; prefer a yujin.* verb over shell. Long-running commands (dev server/watcher/tunnel) MUST use yujin.task.run, never shell.exec (which refuses them).',
@@ -1 +1 @@
1
- {"version":3,"file":"doctrine_digest.js","sourceRoot":"","sources":["../../src/chat/doctrine_digest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AASH,MAAM,CAAC,MAAM,eAAe,GAA0B;IACpD;QACE,KAAK,EAAE,CAAC,sBAAsB,CAAC;QAC/B,IAAI,EACF,8DAA8D;cAC5D,6DAA6D;cAC7D,+DAA+D;cAC/D,uDAAuD;cACvD,6DAA6D;cAC7D,yDAAyD;cACzD,gEAAgE;cAChE,4DAA4D;cAC5D,mCAAmC;KACxC;IACD;QACE,KAAK,EAAE,CAAC,gBAAgB,CAAC;QACzB,IAAI,EACF,gEAAgE;cAC9D,0DAA0D;cAC1D,4CAA4C;KACjD;IACD;QACE,KAAK,EAAE,CAAC,YAAY,CAAC;QACrB,IAAI,EAAE,2FAA2F;KAClG;IACD;QACE,KAAK,EAAE,CAAC,eAAe,CAAC;QACxB,IAAI,EAAE,2IAA2I;KAClJ;IACD;QACE,KAAK,EAAE,CAAC,OAAO,CAAC;QAChB,IAAI,EAAE,qFAAqF;KAC5F;IACD;QACE,KAAK,EAAE,CAAC,UAAU,EAAE,iBAAiB,CAAC;QACtC,IAAI,EAAE,2IAA2I;KAClJ;IACD;QACE,KAAK,EAAE,CAAC,gBAAgB,CAAC;QACzB,IAAI,EAAE,0aAA0a;KACjb;IACD;QACE,KAAK,EAAE,CAAC,OAAO,CAAC;QAChB,IAAI,EAAE,iGAAiG;KACxG;IACD;QACE,KAAK,EAAE,CAAC,iBAAiB,CAAC;QAC1B,IAAI,EAAE,qFAAqF;KAC5F;IACD;QACE,KAAK,EAAE,CAAC,iBAAiB,CAAC;QAC1B,IAAI,EAAE,4GAA4G;KACnH;IACD;QACE,KAAK,EAAE,CAAC,sBAAsB,CAAC;QAC/B,IAAI,EAAE,kJAAkJ;KACzJ;IACD;QACE,KAAK,EAAE,CAAC,uBAAuB,CAAC;QAChC,IAAI,EAAE,mFAAmF;KAC1F;IACD;QACE,KAAK,EAAE,CAAC,iBAAiB,CAAC;QAC1B,IAAI,EACF,2EAA2E;cACzE,4EAA4E;cAC5E,uEAAuE;cACvE,6EAA6E;cAC7E,yEAAyE;cACzE,yEAAyE;cACzE,yEAAyE;cACzE,0EAA0E;cAC1E,yEAAyE;cACzE,uEAAuE;cACvE,0EAA0E;cAC1E,yEAAyE;cACzE,4EAA4E;cAC5E,2EAA2E;cAC3E,gBAAgB;KACrB;IACD;QACE,KAAK,EAAE,CAAC,mBAAmB,CAAC;QAC5B,IAAI,EACF,oEAAoE;cAClE,qEAAqE;cACrE,oEAAoE;cACpE,wEAAwE;cACxE,uEAAuE;cACvE,uEAAuE;cACvE,wEAAwE;cACxE,qEAAqE;cACrE,gBAAgB;KACrB;IACD;QACE,KAAK,EAAE,CAAC,kBAAkB,CAAC;QAC3B,IAAI,EAAE,+LAA+L;KACtM;IACD;QACE,KAAK,EAAE,CAAC,UAAU,CAAC;QACnB,IAAI,EAAE,gkCAAgkC;KACvkC;IACD;QACE,KAAK,EAAE,CAAC,UAAU,CAAC;QACnB,IAAI,EAAE,iFAAiF;KACxF;IACD;QACE,KAAK,EAAE,CAAC,uBAAuB,CAAC;QAChC,IAAI,EAAE,+UAA+U;KACtV;IACD;QACE,KAAK,EAAE,CAAC,iBAAiB,EAAE,wBAAwB,CAAC;QACpD,IAAI,EAAE,wHAAwH;KAC/H;IACD;QACE,KAAK,EAAE,CAAC,cAAc,CAAC;QACvB,IAAI,EAAE,sXAAsX;KAC7X;IACD;QACE,KAAK,EAAE,CAAC,sBAAsB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,aAAa,EAAE,kBAAkB,CAAC;QAC3G,IAAI,EAAE,gLAAgL;KACvL;CACF,CAAC;AAEF,6DAA6D;AAC7D,MAAM,UAAU,WAAW;IACzB,OAAO,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AACjD,CAAC;AAQD;uEACuE;AACvE,MAAM,UAAU,wBAAwB,CAAC,OAA8B,EAAE;IACvE,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAC3C,IAAI,CAAC,cAAc,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,uBAAuB,CAAC,CAAC,CAAC;IAC9E,MAAM,KAAK,GAAa;QACtB,mEAAmE;QACnE,2DAA2D;QAC3D,mEAAmE;QACnE,EAAE;QACF,gEAAgE;QAChE,6DAA6D;QAC7D,EAAE;KACH,CAAC;IACF,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;IACzD,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"doctrine_digest.js","sourceRoot":"","sources":["../../src/chat/doctrine_digest.ts"],"names":[],"mappings":"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"}
@@ -52,3 +52,14 @@ the stated concurrency.
52
52
 
53
53
  Record the target concurrency and the tier decisions in the spec so
54
54
  they are traceable and can be revisited when the number changes.
55
+
56
+ ## 4. Then harden each component (see `performance-hardening`)
57
+
58
+ This doctrine sizes the ARCHITECTURE. Sizing the tiers right does not
59
+ save a component that holds a transaction open across a network call,
60
+ runs a batch in one monolithic commit, hammers a non-idempotent third
61
+ party, or grids a table with `LIKE '%q%'`. The construction-time
62
+ pattern library -- commit-per-chunk, send-outside-transaction, deadlock
63
+ retry, backpressure caps, server-side pagination, idempotency at the
64
+ boundary, TTL cleanup, heartbeat/watchdog -- lives in
65
+ `performance-hardening`. Size here; harden there.
@@ -0,0 +1,213 @@
1
+ # Performance & scale hardening doctrine
2
+
3
+ Every load-bearing component MUST be built for its load in its FIRST
4
+ version -- not hardened after it falls over in production. The brain
5
+ defaults to the happy path: it writes the loop, calls the API, renders
6
+ the grid, and stops. That is exactly how a component ships that
7
+ saturates the DB, hammers a third party, or deadlocks under
8
+ concurrency. This doctrine is the pattern library that closes that gap.
9
+
10
+ Apply the patterns that the component's SHAPE triggers (below). This is
11
+ the RUNTIME / construction complement to `concurrency-scale` (which
12
+ sizes the ARCHITECTURE during relevamiento). Size the tiers there;
13
+ harden each component here. Every rule below is battle-tested in a
14
+ production HITL system, not theory.
15
+
16
+ ## How to use this: match the trigger, apply the pattern
17
+
18
+ ### Trigger 1 -- a component that WRITES to a DB in a loop or batch
19
+
20
+ - **Commit per chunk, never one monolithic transaction.** A single
21
+ final commit over the whole run holds row/gap locks on every touched
22
+ table until the end, grows the undo log unbounded, and lets ONE
23
+ deadlock roll back the ENTIRE run. Commit per item (or per small
24
+ chunk): locks release continuously, a failure isolates to one item,
25
+ and the run becomes resumable.
26
+ - **Never hold a transaction open across a slow side-effect (network /
27
+ external call).** Use the TWO-TRANSACTION pattern: TX1 = change state
28
+ + set an "in-flight" marker -> COMMIT (release locks) -> perform the
29
+ external call with NO transaction open -> TX2 = record the result +
30
+ clear the marker -> COMMIT. A crash between the COMMIT and the call
31
+ must be recoverable: build a sweeper that finds rows with the marker
32
+ set but no result, past a TTL, and re-queues them.
33
+ - **Deadlock retry with backoff + jitter.** Classify transient errors
34
+ (MariaDB 1213 deadlock / 1205 lock-wait-timeout; SQLite "database is
35
+ locked") and retry the transaction with exponential backoff + jitter
36
+ (~6 attempts is a good default). Two variants: retry on a FRESH
37
+ connection (top-level request) vs retry a pure-DB sub-transaction on
38
+ the ALREADY-OPEN connection. **INVARIANT: never put an external call
39
+ inside a retry body** -- a redo would double-send / double-charge.
40
+ - **Reserve -> confirm -> return for consumable resources.** Do not
41
+ decrement-then-hope-to-refund. Reserve at arming, confirm on success,
42
+ return on reject. The decrement MUST be a single atomic UPDATE guarded
43
+ so the balance never goes negative (multiple workers touch the same
44
+ row).
45
+ - **Atomic counter / compare-and-swap, not read-modify-write, across
46
+ threads or workers.** `UPDATE ... SET flag=1 WHERE id=? AND flag=0`;
47
+ if rowcount=0 you lost the race, back off. Guard in-process shared
48
+ state with a lock (a multi-thread WSGI server runs N handlers).
49
+
50
+ ### Trigger 2 -- a component that CALLS a third party (API / provider / webhook)
51
+
52
+ - **Assume NOT idempotent until proven.** Never auto-retry a
53
+ non-idempotent write. A firm reject goes to a dead-end + manual path,
54
+ NOT a burst of retries at a real person or resource. Gate any
55
+ auto-refire behind provider-confirmed idempotency OR a read-back that
56
+ proves the prior call did not land.
57
+ - **Idempotency at the boundary = a UNIQUE constraint.** Dedupe by the
58
+ provider's real message/transaction id ON WRITE, not just on read.
59
+ Webhooks arrive "at least once" and may re-deliver even after your
60
+ ACK; an unconditional insert turns one event into six rows.
61
+ - **ACK first, work after.** Answer the webhook immediately (202) and do
62
+ the slow work (notices, downstream calls) in a background thread /
63
+ queue. A slow response makes the provider time out and re-send ->
64
+ amplification (one real incident: 62 inbound images -> 36 real).
65
+ - **Pace outbound explicitly.** Per-recipient minimum interval; a
66
+ time-window gate (do not contact outside allowed hours; exempt urgent
67
+ paths); a per-run quota + an inter-item sleep placed OUTSIDE any open
68
+ transaction. Respect the provider's rate limits as first-class config.
69
+ - **Timeout-then-escalate; never wait forever.** After a bounded wait,
70
+ escalate or advance so nothing stays stuck. Advance on the FIRST of
71
+ several signals (read / reply / timeout), idempotently.
72
+ - **The external latency and the human ARE the dominant time cost** --
73
+ far above the DB. Do not block the main thread on them; optimizing a
74
+ query that was never the bottleneck buys nothing.
75
+
76
+ ### Trigger 3 -- a background loop / scheduler / sweeper
77
+
78
+ - **Backpressure: cap dispatches per pass.** A mass dump ("run it all
79
+ now") without a cap is a stampede that collides into deadlocks. A
80
+ configurable cap per pass drains the backlog in controlled batches and
81
+ keeps each pass under the watchdog threshold.
82
+ - **One unit per key per pass** when exclusivity/order matters (e.g. one
83
+ message per recipient per sweep) so the loop never fires two at the
84
+ same target.
85
+ - **Isolate each sweep in its own transaction + its own retry.** Do not
86
+ bundle several sweeps under one commit -- one deadlock then starves all
87
+ of them for the whole interval.
88
+ - **Locks with a configurable TTL, keyed CORRECTLY.** An eternal lock is
89
+ a bug (a silent responder blocks the target forever). Key the lock by
90
+ the thing that must serialize (per type / per recipient / per delivery
91
+ target). Stamp each run with the SERVER-INSTANCE token so a recycled
92
+ PID after a restart does not leave the lock hung.
93
+ - **Singletons start EXACTLY once**, guarded against double-start (a dev
94
+ reloader or two threads can otherwise launch two schedulers -> doubled
95
+ cron and doubled sends).
96
+
97
+ ### Trigger 4 -- a list / grid / export endpoint
98
+
99
+ - **Server-side pagination**: LIMIT/OFFSET with an envelope
100
+ `{items, total, page, pages}` and a page_size cap. Filter the WHOLE set
101
+ server-side, THEN paginate -- never filter only the visible page.
102
+ - **Export fetches the complete filtered/ordered set** via an explicit
103
+ flag (e.g. `?todo=1`), never by scraping the visible DOM page. The
104
+ page_size cap still protects the screen.
105
+ - Never load the whole world into the client to render a view.
106
+
107
+ ### Trigger 5 -- a query on a table that GROWS
108
+
109
+ - **Index every column that grows and is filtered / joined / sorted.** A
110
+ UNIQUE constraint IS an index -- do not double-index its leading
111
+ column, and do not "fix" a scan that already rides a UNIQUE.
112
+ - **Never wrap an indexed column in a function inside WHERE / GROUP BY /
113
+ ORDER BY** (`substr(ts,1,10)=today`, `TRIM(col)=''`, `ORDER BY CASE`).
114
+ It disables the index. Rewrite to a RANGE (`ts >= day AND ts < next`),
115
+ normalize on write, or persist the sort key in its own indexed column.
116
+ - **`LIKE '%q%'` (leading wildcard) is unindexable by a B-tree.** Use an
117
+ anchored prefix `q%` (indexable, "starts with") or a real full-text
118
+ index (MariaDB FULLTEXT / SQLite FTS). Free-text search at scale wants
119
+ an inverted index, not SQL LIKE.
120
+ - **Engine portability: short indexed columns are VARCHAR(191), not
121
+ TEXT.** SQLite indexes TEXT happily and hides the bug; MariaDB errors
122
+ 1071 "key too long" (and FK-on-TEXT errno 150). Test against the REAL
123
+ engine before prod (a pre-push barrier catches "key too long" before
124
+ the VM does).
125
+
126
+ ### Trigger 6 -- memory / large payloads / long work
127
+
128
+ - **Do not load a whole file / dataset into memory** when you can stream
129
+ in chunks. Know the memory peak of each heavy operation and do not let
130
+ two heavy runs (import + browser bot + batch) overlap on one machine.
131
+ - **Compress large payloads (gzip)** and raise size caps ONLY on the
132
+ specific admin endpoint, not globally.
133
+ - **Long-running work -> background job + poll**, never inside the HTTP
134
+ request (that is a structural 504). Return `202 + id`; the client polls
135
+ a status endpoint. Guard it idempotently: a second start while one is
136
+ alive returns the SAME run.
137
+
138
+ ### Trigger 7 -- anything that accumulates (tables, files, logs)
139
+
140
+ - **Every fast-growing table and disk accumulation needs a TTL cleanup**,
141
+ configurable, default OFF (never silent data loss). Bounded size keeps
142
+ queries fast and disk healthy. Beware: cleanup that deletes DB rows
143
+ often forgets the files they point to -- purge both.
144
+
145
+ ### Trigger 8 -- observability of the hardening itself
146
+
147
+ - **Instrument retries.** Leave a trace when a retry saved a transaction,
148
+ so residual deadlocks are countable ("the retry doing its job, not an
149
+ error"), not invisible.
150
+ - **Heartbeat per service + heartbeat MID-PASS.** A long pass over a big
151
+ backlog exceeds the liveness threshold and trips a FALSE "hung" alarm;
152
+ refresh the heartbeat every N items inside the loop (zero DB, safe
153
+ mid-transaction).
154
+ - **A watchdog that RELAUNCHES the hung thread, not just logs** -- and
155
+ reports honestly whether it revived a dead thread or the thread is
156
+ alive-but-stuck (which needs a restart it cannot do from inside).
157
+ - **Dedupe repeated alerts within a window** (one incident emitted 891
158
+ identical alerts, one per pass). Exclude genuinely unique events from
159
+ dedupe.
160
+ - **Load-test as an acceptance gate.** Re-run the realistic peak and
161
+ compare deadlocks / orphaned handoffs / false-watchdogs / throughput
162
+ p50/p95 against a baseline; a hardening change does not "close" until
163
+ the numbers hold. You cannot tune what you do not measure -- and the
164
+ volumetry you assume is not the volumetry you have until measured.
165
+
166
+ ## The cross-cutting laws (apply to EVERY threshold you add)
167
+
168
+ 1. **Cero hardcode.** Every threshold -- cap, retry count, backoff, TTL,
169
+ timeout, poll interval, quota, sleep -- lives in config with a sane
170
+ clamp/floor. The DEFAULT preserves current behavior; a dangerous
171
+ feature is born INERT (0 / off) and hot-applied. A garbage value
172
+ collapses to the safe behavior, never to the risky one.
173
+ 2. **Producer/consumer symmetry.** When you change a producer, audit ALL
174
+ its consumers BEFORE the commit. Clamp the same value identically on
175
+ save (producer) and on read (consumer). Preview (dry-run) must walk
176
+ the EXACT same set and same locks as real execution -- the count shown
177
+ equals the count processed.
178
+ 3. **Never an external / network call inside an open transaction or a
179
+ retry.** This single rule prevents most lock storms AND all
180
+ double-sends / double-charges.
181
+ 4. **Fail-closed (cero dano silencioso).** On ambiguous or corrupt input,
182
+ collapse to the safe branch (manual review, no-op), never the
183
+ irreversible one. Lists are born empty, switches born off.
184
+ 5. **Measure first.** Before flipping a switch that fans out to real
185
+ users / resources, COUNT the burst it will release (how many are
186
+ queued right now). Benchmark before and after.
187
+ 6. **Proportionality.** Apply the pattern the component's real load
188
+ needs. A 20-user internal tool does not get a broker and sharding --
189
+ that is a defect too. Neither a bottleneck nor gold-plating. Size to
190
+ the number in the spec (see `concurrency-scale`).
191
+
192
+ ## When the numbers actually grow (the escalation ladder)
193
+
194
+ Cheapest-first; each step only when volume demands it (details +
195
+ sizing in `concurrency-scale`):
196
+
197
+ 1. Production WSGI server with a thread pool (removes the single-thread
198
+ web bottleneck -- highest return for lowest effort).
199
+ 2. Connection pool (stop opening/closing a connection per operation).
200
+ 3. **Queue-in-the-DB**: a "pending jobs" table on the DB you already run
201
+ -- gives enqueue, retry-with-wait, restart-survival, and models the
202
+ per-key lock as a ROW you lock, with zero new services (~80% of a
203
+ broker's benefit at ~20% of the operational cost).
204
+ 4. Split API vs Worker; move the in-process lock to a distributed lock
205
+ (DB `GET_LOCK()` / Redis) with a single active consumer per key.
206
+ 5. A real broker (RabbitMQ / Pub-Sub) with a dead-letter queue and
207
+ partitioning by the lock key; a circuit breaker so one failing
208
+ dependency does not take the rest down.
209
+
210
+ The expensive part of distributing is NOT the broker -- it is preserving
211
+ the per-key serialization/order invariant once the lock leaves the
212
+ process. Do not reach for a broker to solve a problem a per-run cap and a
213
+ queue-in-the-DB already solve.
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.0.89";
5
+ export declare const VERSION = "1.0.90";
6
6
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js 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 const VERSION = '1.0.89';
5
+ export const VERSION = '1.0.90';
6
6
  //# sourceMappingURL=version.js.map
@@ -52,3 +52,14 @@ the stated concurrency.
52
52
 
53
53
  Record the target concurrency and the tier decisions in the spec so
54
54
  they are traceable and can be revisited when the number changes.
55
+
56
+ ## 4. Then harden each component (see `performance-hardening`)
57
+
58
+ This doctrine sizes the ARCHITECTURE. Sizing the tiers right does not
59
+ save a component that holds a transaction open across a network call,
60
+ runs a batch in one monolithic commit, hammers a non-idempotent third
61
+ party, or grids a table with `LIKE '%q%'`. The construction-time
62
+ pattern library -- commit-per-chunk, send-outside-transaction, deadlock
63
+ retry, backpressure caps, server-side pagination, idempotency at the
64
+ boundary, TTL cleanup, heartbeat/watchdog -- lives in
65
+ `performance-hardening`. Size here; harden there.
@@ -0,0 +1,213 @@
1
+ # Performance & scale hardening doctrine
2
+
3
+ Every load-bearing component MUST be built for its load in its FIRST
4
+ version -- not hardened after it falls over in production. The brain
5
+ defaults to the happy path: it writes the loop, calls the API, renders
6
+ the grid, and stops. That is exactly how a component ships that
7
+ saturates the DB, hammers a third party, or deadlocks under
8
+ concurrency. This doctrine is the pattern library that closes that gap.
9
+
10
+ Apply the patterns that the component's SHAPE triggers (below). This is
11
+ the RUNTIME / construction complement to `concurrency-scale` (which
12
+ sizes the ARCHITECTURE during relevamiento). Size the tiers there;
13
+ harden each component here. Every rule below is battle-tested in a
14
+ production HITL system, not theory.
15
+
16
+ ## How to use this: match the trigger, apply the pattern
17
+
18
+ ### Trigger 1 -- a component that WRITES to a DB in a loop or batch
19
+
20
+ - **Commit per chunk, never one monolithic transaction.** A single
21
+ final commit over the whole run holds row/gap locks on every touched
22
+ table until the end, grows the undo log unbounded, and lets ONE
23
+ deadlock roll back the ENTIRE run. Commit per item (or per small
24
+ chunk): locks release continuously, a failure isolates to one item,
25
+ and the run becomes resumable.
26
+ - **Never hold a transaction open across a slow side-effect (network /
27
+ external call).** Use the TWO-TRANSACTION pattern: TX1 = change state
28
+ + set an "in-flight" marker -> COMMIT (release locks) -> perform the
29
+ external call with NO transaction open -> TX2 = record the result +
30
+ clear the marker -> COMMIT. A crash between the COMMIT and the call
31
+ must be recoverable: build a sweeper that finds rows with the marker
32
+ set but no result, past a TTL, and re-queues them.
33
+ - **Deadlock retry with backoff + jitter.** Classify transient errors
34
+ (MariaDB 1213 deadlock / 1205 lock-wait-timeout; SQLite "database is
35
+ locked") and retry the transaction with exponential backoff + jitter
36
+ (~6 attempts is a good default). Two variants: retry on a FRESH
37
+ connection (top-level request) vs retry a pure-DB sub-transaction on
38
+ the ALREADY-OPEN connection. **INVARIANT: never put an external call
39
+ inside a retry body** -- a redo would double-send / double-charge.
40
+ - **Reserve -> confirm -> return for consumable resources.** Do not
41
+ decrement-then-hope-to-refund. Reserve at arming, confirm on success,
42
+ return on reject. The decrement MUST be a single atomic UPDATE guarded
43
+ so the balance never goes negative (multiple workers touch the same
44
+ row).
45
+ - **Atomic counter / compare-and-swap, not read-modify-write, across
46
+ threads or workers.** `UPDATE ... SET flag=1 WHERE id=? AND flag=0`;
47
+ if rowcount=0 you lost the race, back off. Guard in-process shared
48
+ state with a lock (a multi-thread WSGI server runs N handlers).
49
+
50
+ ### Trigger 2 -- a component that CALLS a third party (API / provider / webhook)
51
+
52
+ - **Assume NOT idempotent until proven.** Never auto-retry a
53
+ non-idempotent write. A firm reject goes to a dead-end + manual path,
54
+ NOT a burst of retries at a real person or resource. Gate any
55
+ auto-refire behind provider-confirmed idempotency OR a read-back that
56
+ proves the prior call did not land.
57
+ - **Idempotency at the boundary = a UNIQUE constraint.** Dedupe by the
58
+ provider's real message/transaction id ON WRITE, not just on read.
59
+ Webhooks arrive "at least once" and may re-deliver even after your
60
+ ACK; an unconditional insert turns one event into six rows.
61
+ - **ACK first, work after.** Answer the webhook immediately (202) and do
62
+ the slow work (notices, downstream calls) in a background thread /
63
+ queue. A slow response makes the provider time out and re-send ->
64
+ amplification (one real incident: 62 inbound images -> 36 real).
65
+ - **Pace outbound explicitly.** Per-recipient minimum interval; a
66
+ time-window gate (do not contact outside allowed hours; exempt urgent
67
+ paths); a per-run quota + an inter-item sleep placed OUTSIDE any open
68
+ transaction. Respect the provider's rate limits as first-class config.
69
+ - **Timeout-then-escalate; never wait forever.** After a bounded wait,
70
+ escalate or advance so nothing stays stuck. Advance on the FIRST of
71
+ several signals (read / reply / timeout), idempotently.
72
+ - **The external latency and the human ARE the dominant time cost** --
73
+ far above the DB. Do not block the main thread on them; optimizing a
74
+ query that was never the bottleneck buys nothing.
75
+
76
+ ### Trigger 3 -- a background loop / scheduler / sweeper
77
+
78
+ - **Backpressure: cap dispatches per pass.** A mass dump ("run it all
79
+ now") without a cap is a stampede that collides into deadlocks. A
80
+ configurable cap per pass drains the backlog in controlled batches and
81
+ keeps each pass under the watchdog threshold.
82
+ - **One unit per key per pass** when exclusivity/order matters (e.g. one
83
+ message per recipient per sweep) so the loop never fires two at the
84
+ same target.
85
+ - **Isolate each sweep in its own transaction + its own retry.** Do not
86
+ bundle several sweeps under one commit -- one deadlock then starves all
87
+ of them for the whole interval.
88
+ - **Locks with a configurable TTL, keyed CORRECTLY.** An eternal lock is
89
+ a bug (a silent responder blocks the target forever). Key the lock by
90
+ the thing that must serialize (per type / per recipient / per delivery
91
+ target). Stamp each run with the SERVER-INSTANCE token so a recycled
92
+ PID after a restart does not leave the lock hung.
93
+ - **Singletons start EXACTLY once**, guarded against double-start (a dev
94
+ reloader or two threads can otherwise launch two schedulers -> doubled
95
+ cron and doubled sends).
96
+
97
+ ### Trigger 4 -- a list / grid / export endpoint
98
+
99
+ - **Server-side pagination**: LIMIT/OFFSET with an envelope
100
+ `{items, total, page, pages}` and a page_size cap. Filter the WHOLE set
101
+ server-side, THEN paginate -- never filter only the visible page.
102
+ - **Export fetches the complete filtered/ordered set** via an explicit
103
+ flag (e.g. `?todo=1`), never by scraping the visible DOM page. The
104
+ page_size cap still protects the screen.
105
+ - Never load the whole world into the client to render a view.
106
+
107
+ ### Trigger 5 -- a query on a table that GROWS
108
+
109
+ - **Index every column that grows and is filtered / joined / sorted.** A
110
+ UNIQUE constraint IS an index -- do not double-index its leading
111
+ column, and do not "fix" a scan that already rides a UNIQUE.
112
+ - **Never wrap an indexed column in a function inside WHERE / GROUP BY /
113
+ ORDER BY** (`substr(ts,1,10)=today`, `TRIM(col)=''`, `ORDER BY CASE`).
114
+ It disables the index. Rewrite to a RANGE (`ts >= day AND ts < next`),
115
+ normalize on write, or persist the sort key in its own indexed column.
116
+ - **`LIKE '%q%'` (leading wildcard) is unindexable by a B-tree.** Use an
117
+ anchored prefix `q%` (indexable, "starts with") or a real full-text
118
+ index (MariaDB FULLTEXT / SQLite FTS). Free-text search at scale wants
119
+ an inverted index, not SQL LIKE.
120
+ - **Engine portability: short indexed columns are VARCHAR(191), not
121
+ TEXT.** SQLite indexes TEXT happily and hides the bug; MariaDB errors
122
+ 1071 "key too long" (and FK-on-TEXT errno 150). Test against the REAL
123
+ engine before prod (a pre-push barrier catches "key too long" before
124
+ the VM does).
125
+
126
+ ### Trigger 6 -- memory / large payloads / long work
127
+
128
+ - **Do not load a whole file / dataset into memory** when you can stream
129
+ in chunks. Know the memory peak of each heavy operation and do not let
130
+ two heavy runs (import + browser bot + batch) overlap on one machine.
131
+ - **Compress large payloads (gzip)** and raise size caps ONLY on the
132
+ specific admin endpoint, not globally.
133
+ - **Long-running work -> background job + poll**, never inside the HTTP
134
+ request (that is a structural 504). Return `202 + id`; the client polls
135
+ a status endpoint. Guard it idempotently: a second start while one is
136
+ alive returns the SAME run.
137
+
138
+ ### Trigger 7 -- anything that accumulates (tables, files, logs)
139
+
140
+ - **Every fast-growing table and disk accumulation needs a TTL cleanup**,
141
+ configurable, default OFF (never silent data loss). Bounded size keeps
142
+ queries fast and disk healthy. Beware: cleanup that deletes DB rows
143
+ often forgets the files they point to -- purge both.
144
+
145
+ ### Trigger 8 -- observability of the hardening itself
146
+
147
+ - **Instrument retries.** Leave a trace when a retry saved a transaction,
148
+ so residual deadlocks are countable ("the retry doing its job, not an
149
+ error"), not invisible.
150
+ - **Heartbeat per service + heartbeat MID-PASS.** A long pass over a big
151
+ backlog exceeds the liveness threshold and trips a FALSE "hung" alarm;
152
+ refresh the heartbeat every N items inside the loop (zero DB, safe
153
+ mid-transaction).
154
+ - **A watchdog that RELAUNCHES the hung thread, not just logs** -- and
155
+ reports honestly whether it revived a dead thread or the thread is
156
+ alive-but-stuck (which needs a restart it cannot do from inside).
157
+ - **Dedupe repeated alerts within a window** (one incident emitted 891
158
+ identical alerts, one per pass). Exclude genuinely unique events from
159
+ dedupe.
160
+ - **Load-test as an acceptance gate.** Re-run the realistic peak and
161
+ compare deadlocks / orphaned handoffs / false-watchdogs / throughput
162
+ p50/p95 against a baseline; a hardening change does not "close" until
163
+ the numbers hold. You cannot tune what you do not measure -- and the
164
+ volumetry you assume is not the volumetry you have until measured.
165
+
166
+ ## The cross-cutting laws (apply to EVERY threshold you add)
167
+
168
+ 1. **Cero hardcode.** Every threshold -- cap, retry count, backoff, TTL,
169
+ timeout, poll interval, quota, sleep -- lives in config with a sane
170
+ clamp/floor. The DEFAULT preserves current behavior; a dangerous
171
+ feature is born INERT (0 / off) and hot-applied. A garbage value
172
+ collapses to the safe behavior, never to the risky one.
173
+ 2. **Producer/consumer symmetry.** When you change a producer, audit ALL
174
+ its consumers BEFORE the commit. Clamp the same value identically on
175
+ save (producer) and on read (consumer). Preview (dry-run) must walk
176
+ the EXACT same set and same locks as real execution -- the count shown
177
+ equals the count processed.
178
+ 3. **Never an external / network call inside an open transaction or a
179
+ retry.** This single rule prevents most lock storms AND all
180
+ double-sends / double-charges.
181
+ 4. **Fail-closed (cero dano silencioso).** On ambiguous or corrupt input,
182
+ collapse to the safe branch (manual review, no-op), never the
183
+ irreversible one. Lists are born empty, switches born off.
184
+ 5. **Measure first.** Before flipping a switch that fans out to real
185
+ users / resources, COUNT the burst it will release (how many are
186
+ queued right now). Benchmark before and after.
187
+ 6. **Proportionality.** Apply the pattern the component's real load
188
+ needs. A 20-user internal tool does not get a broker and sharding --
189
+ that is a defect too. Neither a bottleneck nor gold-plating. Size to
190
+ the number in the spec (see `concurrency-scale`).
191
+
192
+ ## When the numbers actually grow (the escalation ladder)
193
+
194
+ Cheapest-first; each step only when volume demands it (details +
195
+ sizing in `concurrency-scale`):
196
+
197
+ 1. Production WSGI server with a thread pool (removes the single-thread
198
+ web bottleneck -- highest return for lowest effort).
199
+ 2. Connection pool (stop opening/closing a connection per operation).
200
+ 3. **Queue-in-the-DB**: a "pending jobs" table on the DB you already run
201
+ -- gives enqueue, retry-with-wait, restart-survival, and models the
202
+ per-key lock as a ROW you lock, with zero new services (~80% of a
203
+ broker's benefit at ~20% of the operational cost).
204
+ 4. Split API vs Worker; move the in-process lock to a distributed lock
205
+ (DB `GET_LOCK()` / Redis) with a single active consumer per key.
206
+ 5. A real broker (RabbitMQ / Pub-Sub) with a dead-letter queue and
207
+ partitioning by the lock key; a circuit breaker so one failing
208
+ dependency does not take the rest down.
209
+
210
+ The expensive part of distributing is NOT the broker -- it is preserving
211
+ the per-key serialization/order invariant once the lock leaves the
212
+ process. Do not reach for a broker to solve a problem a per-run cap and a
213
+ queue-in-the-DB already solve.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nac3/forge-cli",
3
- "version": "1.0.89",
3
+ "version": "1.0.90",
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>",