voidforge-build 23.9.2 → 23.11.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.
Files changed (75) hide show
  1. package/dist/.claude/agents/bashir-field-medic.md +1 -0
  2. package/dist/.claude/agents/coulson-release.md +3 -0
  3. package/dist/.claude/agents/irulan-historian.md +3 -0
  4. package/dist/.claude/agents/kusanagi-devops.md +8 -0
  5. package/dist/.claude/agents/leia-secrets.md +10 -0
  6. package/dist/.claude/agents/loki-chaos.md +1 -0
  7. package/dist/.claude/agents/picard-architecture.md +11 -0
  8. package/dist/.claude/agents/silver-surfer-herald.md +17 -0
  9. package/dist/.claude/agents/sisko-campaign.md +3 -0
  10. package/dist/.claude/agents/thufir-protocol-parsing.md +10 -0
  11. package/dist/.claude/commands/architect.md +56 -0
  12. package/dist/.claude/commands/campaign.md +26 -1
  13. package/dist/.claude/commands/deploy.md +31 -0
  14. package/dist/.claude/commands/gauntlet.md +11 -0
  15. package/dist/.claude/commands/git.md +13 -3
  16. package/dist/.claude/commands/prd.md +8 -0
  17. package/dist/CHANGELOG.md +107 -0
  18. package/dist/CLAUDE.md +13 -4
  19. package/dist/VERSION.md +3 -1
  20. package/dist/docs/methods/AI_INTELLIGENCE.md +15 -0
  21. package/dist/docs/methods/BACKEND_ENGINEER.md +48 -0
  22. package/dist/docs/methods/BUILD_PROTOCOL.md +19 -0
  23. package/dist/docs/methods/CAMPAIGN.md +204 -1
  24. package/dist/docs/methods/DEVOPS_ENGINEER.md +80 -0
  25. package/dist/docs/methods/FORGE_KEEPER.md +80 -3
  26. package/dist/docs/methods/GAUNTLET.md +2 -0
  27. package/dist/docs/methods/PRD_GENERATOR.md +15 -0
  28. package/dist/docs/methods/QA_ENGINEER.md +46 -0
  29. package/dist/docs/methods/RELEASE_MANAGER.md +59 -0
  30. package/dist/docs/methods/SECURITY_AUDITOR.md +53 -0
  31. package/dist/docs/methods/SPEC_HANDOFF.md +53 -0
  32. package/dist/docs/methods/SUB_AGENTS.md +90 -0
  33. package/dist/docs/methods/SYSTEMS_ARCHITECT.md +55 -2
  34. package/dist/docs/methods/TESTING.md +17 -0
  35. package/dist/docs/methods/TIME_VAULT.md +17 -0
  36. package/dist/docs/methods/TROUBLESHOOTING.md +27 -0
  37. package/dist/docs/patterns/adr-verification-gate.md +80 -0
  38. package/dist/docs/patterns/ai-eval.ts +87 -0
  39. package/dist/docs/patterns/ai-prompt-safety.ts +242 -0
  40. package/dist/docs/patterns/audit-log.ts +132 -0
  41. package/dist/docs/patterns/deploy-preflight.ts +195 -0
  42. package/dist/docs/patterns/llm-state-dedup.ts +246 -0
  43. package/dist/docs/patterns/middleware.ts +83 -0
  44. package/dist/docs/patterns/multi-tenant-pool-bypass.ts +134 -0
  45. package/dist/docs/patterns/multi-tenant-property-test.ts +127 -0
  46. package/dist/docs/patterns/refactor-extraction.md +96 -0
  47. package/dist/scripts/voidforge.js +0 -0
  48. package/dist/wizard/lib/anomaly-detection.d.ts +59 -0
  49. package/dist/wizard/lib/anomaly-detection.js +122 -0
  50. package/dist/wizard/lib/asset-scanner.d.ts +23 -0
  51. package/dist/wizard/lib/asset-scanner.js +107 -0
  52. package/dist/wizard/lib/build-analytics.d.ts +39 -0
  53. package/dist/wizard/lib/build-analytics.js +91 -0
  54. package/dist/wizard/lib/codegen/erd-gen.d.ts +16 -0
  55. package/dist/wizard/lib/codegen/erd-gen.js +98 -0
  56. package/dist/wizard/lib/codegen/openapi-gen.d.ts +15 -0
  57. package/dist/wizard/lib/codegen/openapi-gen.js +79 -0
  58. package/dist/wizard/lib/codegen/prisma-types.d.ts +15 -0
  59. package/dist/wizard/lib/codegen/prisma-types.js +44 -0
  60. package/dist/wizard/lib/codegen/seed-gen.d.ts +16 -0
  61. package/dist/wizard/lib/codegen/seed-gen.js +128 -0
  62. package/dist/wizard/lib/correlation-engine.d.ts +59 -0
  63. package/dist/wizard/lib/correlation-engine.js +152 -0
  64. package/dist/wizard/lib/desktop-notify.d.ts +27 -0
  65. package/dist/wizard/lib/desktop-notify.js +98 -0
  66. package/dist/wizard/lib/image-gen.d.ts +56 -0
  67. package/dist/wizard/lib/image-gen.js +159 -0
  68. package/dist/wizard/lib/natural-language-deploy.d.ts +30 -0
  69. package/dist/wizard/lib/natural-language-deploy.js +186 -0
  70. package/dist/wizard/lib/project-init.js +57 -0
  71. package/dist/wizard/lib/route-optimizer.d.ts +28 -0
  72. package/dist/wizard/lib/route-optimizer.js +93 -0
  73. package/dist/wizard/lib/service-install.d.ts +18 -0
  74. package/dist/wizard/lib/service-install.js +182 -0
  75. package/package.json +1 -1
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Pattern: LLM State Dedup — IDs are NOT keys
3
+ *
4
+ * Rule: LLM-emitted identifiers are display labels, not primary keys.
5
+ *
6
+ * Why: each LLM invocation is stateless from the model's perspective. Two
7
+ * cycles that propose the same fix will produce DIFFERENT id strings, even
8
+ * for substantively identical commands. The model has no memory of prior
9
+ * ids; it generates a fresh string from current context, drifts every cycle.
10
+ *
11
+ * Field report #330 (threadplex-ops): an hourly run asked Claude to emit
12
+ * `approval_needed[]` entries with an `id` field. The runtime keyed dedup
13
+ * on `id`. Over 5 hours of identical context, Claude emitted ids:
14
+ *
15
+ * `a3f9c2` (cycle 1)
16
+ * `a3f7c2` (cycle 2)
17
+ * `a3f7b2` (cycle 3)
18
+ * `a3f9c1` (cycle 4)
19
+ *
20
+ * Four proposals to stop the same container. Four Telegram approval cards.
21
+ * Zero collapse. The dedup key was wrong by construction.
22
+ *
23
+ * This pattern applies to ANY VoidForge project using an LLM as a decision
24
+ * engine that emits actionable items (approvals, tickets, tasks, queued ops).
25
+ *
26
+ * Agents: Hari Seldon (AI architecture), Bayta Darell (eval), Stark (backend)
27
+ */
28
+
29
+ import { createHash } from 'node:crypto'
30
+
31
+ // --- The rule ---
32
+
33
+ /**
34
+ * Dedup keys must be derived from the OPERATIVE CONTENT, not from the LLM's
35
+ * id field. The operative content is the part of the proposal that, if
36
+ * executed, would produce the same observable outcome.
37
+ *
38
+ * For shell commands: the canonical command string.
39
+ * For HTTP requests: (method, path, normalized body).
40
+ * For database operations: (table, primary key, op-type).
41
+ * For user notifications: (recipient, channel, message-hash).
42
+ */
43
+
44
+ export interface ProposalDedupKey {
45
+ /** Content-hash of the operative payload — the actual dedup key. */
46
+ contentHash: string
47
+
48
+ /**
49
+ * Optional looser key for command-string drift collapse — `docker stop X`,
50
+ * `docker compose stop X`, `docker rm -f X` all collapse to the same
51
+ * (verb, target) tuple even though contentHash differs.
52
+ */
53
+ logicalKey?: string
54
+
55
+ /** The LLM-emitted id, retained as a display label only. NEVER as primary key. */
56
+ displayId?: string
57
+ }
58
+
59
+ // --- Hash the operative content ---
60
+
61
+ /**
62
+ * For shell commands: hash the canonical command string. Normalize whitespace
63
+ * and quoting before hashing so cosmetically-different but semantically-
64
+ * identical commands collapse.
65
+ */
66
+ export function shellCommandHash(command: string): string {
67
+ const canonical = command
68
+ .trim()
69
+ .replace(/\s+/g, ' ') // Collapse whitespace
70
+ .replace(/(['"])\s+/g, '$1 ') // Normalize quote-adjacent spaces
71
+
72
+ return createHash('sha256').update(canonical).digest('hex').slice(0, 12)
73
+ }
74
+
75
+ /**
76
+ * For HTTP request proposals: hash (method, path, sorted-body-keys).
77
+ * Sort body keys so `{a: 1, b: 2}` and `{b: 2, a: 1}` hash identically.
78
+ */
79
+ export function httpRequestHash(req: {
80
+ method: string
81
+ path: string
82
+ body?: Record<string, unknown>
83
+ }): string {
84
+ const sortedBody = req.body
85
+ ? JSON.stringify(req.body, Object.keys(req.body).sort())
86
+ : ''
87
+ const canonical = `${req.method.toUpperCase()} ${req.path} ${sortedBody}`
88
+ return createHash('sha256').update(canonical).digest('hex').slice(0, 12)
89
+ }
90
+
91
+ // --- Logical-key fallback for command-string drift ---
92
+
93
+ /**
94
+ * Some commands have multiple syntactic forms that produce the same outcome.
95
+ * Extract (verb, target) tuple so all forms collapse to the same logical key.
96
+ *
97
+ * Examples that all map to ('stop', 'kometa-run'):
98
+ * docker stop kometa-run
99
+ * docker compose stop kometa-run
100
+ * docker rm -f kometa-run (different verb but same target — flag separately)
101
+ */
102
+ export function dockerLogicalKey(command: string): string | null {
103
+ const verbs = ['stop', 'start', 'restart', 'rm', 'kill', 'pause']
104
+ for (const verb of verbs) {
105
+ const re = new RegExp(`\\bdocker\\s+(?:compose\\s+)?${verb}\\b\\s+(?:-\\S+\\s+)*([\\w.-]+)`, 'i')
106
+ const m = command.match(re)
107
+ if (m) return `${verb}:${m[1]}`
108
+ }
109
+ return null
110
+ }
111
+
112
+ // --- Lifecycle states must enumerate every in-flight status ---
113
+
114
+ /**
115
+ * Even with correct dedup keys, the snapshot used for dedup-comparison must
116
+ * cover ALL operator-visible in-flight states — not just `pending`.
117
+ *
118
+ * Field report #330: the threadplex-ops snapshot filtered only
119
+ * `status == "pending"`, missing `executing` and `interrupted` rows that
120
+ * were also operator-visible. The dedup key was correct but the snapshot
121
+ * was incomplete, producing the same duplication symptom.
122
+ *
123
+ * The lifecycle table below is the reference. Extend per-project.
124
+ */
125
+ export const PROPOSAL_LIFECYCLE_STATES = [
126
+ 'pending', // Awaiting operator approval
127
+ 'executing', // Operator approved; runtime executing the action
128
+ 'interrupted', // Execution paused (operator pause, system pause, retry-backoff)
129
+ 'completed', // Execution succeeded
130
+ 'failed', // Execution failed (terminal — operator must re-issue)
131
+ 'cancelled', // Operator cancelled before execution
132
+ 'expired', // Approval window timed out
133
+ ] as const
134
+
135
+ export type LifecycleState = typeof PROPOSAL_LIFECYCLE_STATES[number]
136
+
137
+ /** In-flight states the dedup snapshot must include to prevent duplicate proposals. */
138
+ export const IN_FLIGHT_STATES: readonly LifecycleState[] = [
139
+ 'pending',
140
+ 'executing',
141
+ 'interrupted',
142
+ ]
143
+
144
+ // --- AUTHORITY-style contract: tell the LLM the key shape ---
145
+
146
+ /**
147
+ * The LLM cannot enforce a dedup contract it doesn't know about. Document
148
+ * the contract in the agent's authority/instruction document so the LLM
149
+ * understands what "same target" means.
150
+ *
151
+ * Example AUTHORITY.md fragment:
152
+ *
153
+ * ## Approval Identifier Contract
154
+ *
155
+ * Each proposal you emit MUST include both:
156
+ *
157
+ * id — a human-readable display label. NOT a key. You may
158
+ * emit any short label that helps the operator scan.
159
+ *
160
+ * cmd_hash — sha256(command)[:12]. The runtime keys dedup on this.
161
+ * Two proposals with the same cmd_hash collapse into one
162
+ * approval card.
163
+ *
164
+ * The runtime also computes a logical_key from the command verb + target
165
+ * name. Proposals with the same logical_key are surfaced as a cluster
166
+ * even if cmd_hash differs (e.g., `docker stop X` and `docker rm -f X`
167
+ * both target X with different verbs — operator sees both, decides once).
168
+ */
169
+
170
+ export const AUTHORITY_FRAGMENT_TEMPLATE = `
171
+ ## Approval Identifier Contract
172
+
173
+ Each proposal MUST include:
174
+
175
+ id — display label. Not a key. You may emit any short label.
176
+ cmd_hash — sha256(command)[:12]. The runtime keys dedup on this.
177
+
178
+ The runtime also computes a logical_key from (verb, target). Proposals
179
+ sharing logical_key are surfaced as a cluster even with different
180
+ cmd_hash values.
181
+ `.trim()
182
+
183
+ // --- Putting it together ---
184
+
185
+ export interface ApprovalProposal {
186
+ id: string // Display only — DO NOT USE AS KEY
187
+ cmdHash: string // Primary dedup key
188
+ logicalKey: string | null // Secondary cluster key
189
+ command: string
190
+ proposedAt: string // ISO timestamp
191
+ state: LifecycleState
192
+ }
193
+
194
+ export function dedupProposals(
195
+ newProposal: { id: string; command: string },
196
+ existing: ApprovalProposal[]
197
+ ): { duplicate: boolean; collapsedInto?: ApprovalProposal; logicalCluster?: ApprovalProposal[] } {
198
+ const cmdHash = shellCommandHash(newProposal.command)
199
+ const logicalKey = dockerLogicalKey(newProposal.command)
200
+
201
+ // Snapshot covers ALL in-flight states — not just pending
202
+ const inFlight = existing.filter((p) => IN_FLIGHT_STATES.includes(p.state))
203
+
204
+ // Hard duplicate: same cmd_hash
205
+ const exact = inFlight.find((p) => p.cmdHash === cmdHash)
206
+ if (exact) {
207
+ return { duplicate: true, collapsedInto: exact }
208
+ }
209
+
210
+ // Soft cluster: same logical_key, different command form
211
+ if (logicalKey) {
212
+ const cluster = inFlight.filter((p) => p.logicalKey === logicalKey)
213
+ if (cluster.length > 0) {
214
+ return { duplicate: false, logicalCluster: cluster }
215
+ }
216
+ }
217
+
218
+ return { duplicate: false }
219
+ }
220
+
221
+ // --- Anti-patterns ---
222
+
223
+ /* ANTI-PATTERN 1: LLM ids as primary keys
224
+ * `INSERT INTO approvals (id, ...) VALUES (?, ...)` where `id` is the
225
+ * LLM-emitted string. Two LLM calls with substantively identical input
226
+ * will produce different ids; the database rows do NOT collapse.
227
+ *
228
+ * Fix: store `cmd_hash` as the PK and `display_id` as a label column.
229
+ */
230
+
231
+ /* ANTI-PATTERN 2: Dedup snapshot filtered to a single state
232
+ * `SELECT * FROM approvals WHERE state = 'pending'` for dedup comparison.
233
+ * Misses `executing` and `interrupted` rows that are operator-visible.
234
+ *
235
+ * Fix: use IN_FLIGHT_STATES list. Document which states are excluded
236
+ * from dedup (typically `completed`, `failed`, `cancelled`, `expired`).
237
+ */
238
+
239
+ /* ANTI-PATTERN 3: Hash the LLM's whole emitted JSON
240
+ * `sha256(JSON.stringify(proposal))` includes display_id, timestamps,
241
+ * reasoning prose — all of which drift per cycle even when the action
242
+ * is identical. Hash explodes; collapse never happens.
243
+ *
244
+ * Fix: hash only the operative payload (the command, the request body,
245
+ * the target identifier — never the LLM's free-text fields).
246
+ */
@@ -154,6 +154,89 @@ export function withRequestLogging(
154
154
  }
155
155
  }
156
156
 
157
+ // --- Hot-path logging gate (fire-once / rate-limited) ---
158
+ //
159
+ // Source: Field report #319 §5. Stark's RlsDeadlineMiddleware originally
160
+ // emitted `logger.critical(...)` on every 503 — at 100 rps × 24h = 8.6M
161
+ // critical-level lines/day. No rate-limit, no fire-once. Would crater the
162
+ // log aggregator and Sentry quota.
163
+ //
164
+ // ANY middleware that emits log lines on a hot path (every request, every
165
+ // connection) MUST gate the emission. Two acceptable patterns:
166
+ //
167
+ // 1. Fire-once flag (preferred for state transitions): emit once when
168
+ // state changes, then suppress until reset. Pair with an audit row
169
+ // + Sentry capture inside the same fire-once branch.
170
+ // 2. Rate-limit window (sample-based): emit at most N per window via a
171
+ // token-bucket or last-emit-timestamp gate.
172
+ //
173
+ // Naked `logger.critical(...)` per-request is a denial-of-service vector
174
+ // against your own observability pipeline.
175
+
176
+ type FireOnceState = { fired: boolean; firedAt: number | null };
177
+ const fireOnceStates = new Map<string, FireOnceState>();
178
+
179
+ /**
180
+ * Fire-once gate. Returns true if the caller should emit; false if
181
+ * emission has already happened for this key (until reset()).
182
+ *
183
+ * Use for state-transition events (deadline tripped, circuit opened,
184
+ * degraded mode entered) where the climactic event matters once.
185
+ */
186
+ export function fireOnce(key: string): boolean {
187
+ const state = fireOnceStates.get(key) ?? { fired: false, firedAt: null };
188
+ if (state.fired) return false;
189
+ state.fired = true;
190
+ state.firedAt = Date.now();
191
+ fireOnceStates.set(key, state);
192
+ return true;
193
+ }
194
+
195
+ export function resetFireOnce(key: string): void {
196
+ fireOnceStates.delete(key);
197
+ }
198
+
199
+ /**
200
+ * Token-bucket rate limiter for hot-path logs. Returns true if the caller
201
+ * should emit; false if the bucket is empty.
202
+ *
203
+ * Use for sampled logging where you want N emissions per window
204
+ * (e.g., 1 per minute, 10 per hour).
205
+ */
206
+ const tokenBuckets = new Map<string, { tokens: number; lastRefill: number }>();
207
+
208
+ export function shouldEmit(
209
+ key: string,
210
+ maxPerWindow: number,
211
+ windowMs: number,
212
+ ): boolean {
213
+ const now = Date.now();
214
+ const bucket = tokenBuckets.get(key) ?? { tokens: maxPerWindow, lastRefill: now };
215
+ const elapsed = now - bucket.lastRefill;
216
+ if (elapsed >= windowMs) {
217
+ bucket.tokens = maxPerWindow;
218
+ bucket.lastRefill = now;
219
+ }
220
+ if (bucket.tokens > 0) {
221
+ bucket.tokens -= 1;
222
+ tokenBuckets.set(key, bucket);
223
+ return true;
224
+ }
225
+ tokenBuckets.set(key, bucket);
226
+ return false;
227
+ }
228
+
229
+ // Usage example: 503 deadline middleware
230
+ //
231
+ // if (deadlinePassed) {
232
+ // if (fireOnce('rls-deadline-tripped')) {
233
+ // logger.fatal({ deadline_iso, evidence }, 'RLS migration deadline tripped');
234
+ // writeAuditRow({ action: 'rls_deadline_tripped', decisions: { ... } });
235
+ // Sentry.captureMessage('rls_deadline_tripped', 'fatal');
236
+ // }
237
+ // return new Response('Service Unavailable', { status: 503 });
238
+ // }
239
+
157
240
  // --- Rate limiting middleware ---
158
241
  // Simple in-memory rate limiter. Replace with Redis for multi-instance.
159
242
  const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Pattern: Multi-Tenant Pool Bypass (pre-org-resolution scope)
3
+ *
4
+ * Source: Field report #316 §8 (Union Station, M-04c W2). FORCE RLS with a
5
+ * non-owner runtime role means every connection acquired from the tenant
6
+ * pool MUST have `app.current_org_id` set before the first query. But some
7
+ * code paths legitimately need cross-tenant access:
8
+ *
9
+ * - Auth pre-resolution (looking up which org a session belongs to)
10
+ * - System daemons (queue cleanup, retention sweeps, leader-elected work)
11
+ * - Admin endpoints (cross-tenant reports, ops tooling)
12
+ *
13
+ * These can't set org_id (they don't have one), so they need to bypass the
14
+ * tenant pool entirely and acquire from the admin pool. The
15
+ * `pre_org_resolution_scope` ContextVar wrapper makes this explicit and
16
+ * mechanically enforceable.
17
+ *
18
+ * The TS version below is illustrative; the canonical implementation in
19
+ * Union Station is Python (asyncpg). Same shape ports cleanly.
20
+ */
21
+
22
+ import { AsyncLocalStorage } from 'node:async_hooks';
23
+
24
+ // ── ContextVar / AsyncLocalStorage ────────────────────────────────────────
25
+
26
+ type TenantContext = {
27
+ org_id: number | null; // null when in pre-resolution scope
28
+ pre_resolution: boolean; // true ⇒ acquire from admin pool, not tenant pool
29
+ };
30
+
31
+ const tenantContext = new AsyncLocalStorage<TenantContext>();
32
+
33
+ // ── Tenant scope (per-request, normal path) ──────────────────────────────
34
+
35
+ export async function withTenant<T>(
36
+ org_id: number,
37
+ fn: () => Promise<T>,
38
+ ): Promise<T> {
39
+ return tenantContext.run({ org_id, pre_resolution: false }, fn);
40
+ }
41
+
42
+ // ── Pre-org-resolution scope (cross-tenant or auth lookup) ───────────────
43
+
44
+ export async function preOrgResolutionScope<T>(fn: () => Promise<T>): Promise<T> {
45
+ return tenantContext.run({ org_id: null, pre_resolution: true }, fn);
46
+ }
47
+
48
+ // ── Pool acquisition routes by ContextVar ─────────────────────────────────
49
+
50
+ import type { Pool, PoolClient } from 'pg'; // illustrative — real types vary
51
+
52
+ declare const tenantPool: Pool; // BYPASSRLS=f, RLS enforced
53
+ declare const adminPool: Pool; // BYPASSRLS=t, cross-tenant work
54
+
55
+ export async function acquireConnection(): Promise<PoolClient> {
56
+ const ctx = tenantContext.getStore();
57
+
58
+ if (!ctx) {
59
+ throw new Error(
60
+ 'acquireConnection called outside any tenant context. ' +
61
+ 'Wrap caller with withTenant(orgId, ...) or preOrgResolutionScope(...).',
62
+ );
63
+ }
64
+
65
+ if (ctx.pre_resolution) {
66
+ // Cross-tenant work — acquire from the admin pool.
67
+ return adminPool.connect();
68
+ }
69
+
70
+ // Normal request — acquire from the tenant pool. The pool callback is
71
+ // expected to SET app.current_org_id so RLS policies can reference it.
72
+ if (ctx.org_id === null) {
73
+ throw new Error(
74
+ 'Tenant context missing org_id outside pre_resolution scope. ' +
75
+ 'This indicates a callsite that should have called preOrgResolutionScope().',
76
+ );
77
+ }
78
+ return tenantPool.connect();
79
+ }
80
+
81
+ // ── Usage examples ────────────────────────────────────────────────────────
82
+
83
+ // 1. HTTP middleware (per-request)
84
+ //
85
+ // app.use(async (req, res, next) => {
86
+ // await withTenant(req.user.org_id, () => next());
87
+ // });
88
+ //
89
+ // 2. Daemon (cross-tenant queue cleanup)
90
+ //
91
+ // cron.schedule('*/5 * * * *', async () => {
92
+ // await preOrgResolutionScope(async () => {
93
+ // const conn = await acquireConnection(); // → admin pool
94
+ // await conn.query('DELETE FROM job_queue WHERE completed_at < NOW() - INTERVAL \'30 days\'');
95
+ // conn.release();
96
+ // });
97
+ // });
98
+ //
99
+ // 3. Auth lookup (caller doesn't yet know org_id)
100
+ //
101
+ // async function resolveSession(sessionToken: string): Promise<{ org_id: number; user_id: string }> {
102
+ // return preOrgResolutionScope(async () => {
103
+ // const conn = await acquireConnection(); // → admin pool
104
+ // try {
105
+ // const row = await conn.query(
106
+ // 'SELECT org_id, user_id FROM sessions WHERE token = $1 AND expires_at > NOW()',
107
+ // [sessionToken],
108
+ // );
109
+ // return row.rows[0];
110
+ // } finally {
111
+ // conn.release();
112
+ // }
113
+ // });
114
+ // }
115
+
116
+ // ── Anti-patterns ─────────────────────────────────────────────────────────
117
+ //
118
+ // 1. Acquiring from the tenant pool in a daemon. Without org_id set, the RLS
119
+ // policy denies every query → daemon crashes on first tick. Or worse:
120
+ // the policy uses a fail-open arm and the daemon silently sees zero rows.
121
+ //
122
+ // 2. Bypassing FORCE RLS by hard-coding the connection string with the
123
+ // runtime role's password. The whole point of the admin pool is the
124
+ // BYPASSRLS=t identity — preserve that boundary.
125
+ //
126
+ // 3. preOrgResolutionScope wrapping per-request handlers. The middleware
127
+ // already set the tenant context; switching to admin pool there is a
128
+ // privilege escalation. preOrgResolutionScope is for code paths that
129
+ // legitimately don't have an org_id yet (or never will).
130
+ //
131
+ // 4. Forgetting to wrap lifespan startup. Field report #319 §2: 4 lifespan
132
+ // paths in Union Station's M-05 cutover failed-fast immediately because
133
+ // the RLS-strict role rejected unscoped queries. See BACKEND_ENGINEER.md
134
+ // "Lifespan & Daemon ContextVar Coverage" for the sweep checklist.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Pattern: Multi-Tenant Property Test
3
+ *
4
+ * Source: Field report #315 M4 (Caroline first-user-test, 2026-03-31).
5
+ * Caroline found 10 multi-tenant bugs that prior gauntlets missed because
6
+ * regression tests lock known cases — they don't test the underlying
7
+ * property: "for any two orgs A and B, A's writes never appear in B's reads."
8
+ *
9
+ * This pattern provides the property-based test that closes the gap. Use it
10
+ * on every project with org_id (or tenant_id, workspace_id) scoping.
11
+ *
12
+ * The TS version below is illustrative (vitest + fast-check). Python
13
+ * (Hypothesis) and Go variants follow the same shape — generate random
14
+ * org pairs and write payloads, assert no cross-tenant leak.
15
+ */
16
+
17
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
18
+ import fc from 'fast-check';
19
+
20
+ // ── Test harness contract ────────────────────────────────────────────────
21
+ //
22
+ // The harness must provide:
23
+ // - createOrg() → { id, apiKey, userId } (fresh tenant per call)
24
+ // - writeAsOrg(org, endpoint, payload) (authenticated POST/PUT)
25
+ // - readAsOrg(org, endpoint) (authenticated GET, paginated)
26
+ // - listAllReadEndpoints() → string[] (every GET that returns rows)
27
+ // - listAllWriteEndpoints() → string[] (every POST/PUT/DELETE)
28
+ // - resetDb() (drop + reseed schema)
29
+
30
+ declare const harness: {
31
+ createOrg(): Promise<{ id: number; apiKey: string; userId: string }>;
32
+ writeAsOrg(org: { apiKey: string }, endpoint: string, payload: unknown): Promise<{ id: string }>;
33
+ readAsOrg(org: { apiKey: string }, endpoint: string): Promise<Array<{ id: string; org_id?: number }>>;
34
+ listAllReadEndpoints(): string[];
35
+ listAllWriteEndpoints(): string[];
36
+ resetDb(): Promise<void>;
37
+ };
38
+
39
+ // ── The Property ─────────────────────────────────────────────────────────
40
+
41
+ describe('multi-tenant isolation property', () => {
42
+ beforeEach(async () => harness.resetDb());
43
+
44
+ test('writes by org A never appear in reads by org B', async () => {
45
+ await fc.assert(
46
+ fc.asyncProperty(
47
+ // Random pair of orgs (always distinct)
48
+ fc.tuple(fc.constant(null), fc.constant(null)),
49
+ // Random write endpoint
50
+ fc.constantFrom(...harness.listAllWriteEndpoints()),
51
+ // Random payload — your codebase's payload generator goes here
52
+ randomPayload(),
53
+ async (_pair, writeEndpoint, payload) => {
54
+ const orgA = await harness.createOrg();
55
+ const orgB = await harness.createOrg();
56
+
57
+ // 1. Org A writes
58
+ const written = await harness.writeAsOrg(orgA, writeEndpoint, payload);
59
+
60
+ // 2. Every read endpoint, queried as Org B, must NOT contain the write
61
+ for (const readEndpoint of harness.listAllReadEndpoints()) {
62
+ const rowsB = await harness.readAsOrg(orgB, readEndpoint);
63
+ const leaked = rowsB.find((row) => row.id === written.id);
64
+ if (leaked) {
65
+ throw new Error(
66
+ `LEAK: ${writeEndpoint} write by org ${orgA.id} surfaced in ` +
67
+ `${readEndpoint} read by org ${orgB.id}. Row: ${JSON.stringify(leaked)}`,
68
+ );
69
+ }
70
+ }
71
+ },
72
+ ),
73
+ { numRuns: 100, timeout: 60_000 },
74
+ );
75
+ });
76
+
77
+ test('superuser/admin pool acquisition does NOT bypass per-org reads', async () => {
78
+ // Companion property: admin-pool callers (cross-tenant by design) must
79
+ // still respect org_id when calling tenant endpoints. Field report #318
80
+ // §5: SUPERUSER + BYPASSRLS=t hides policy bugs. Test under non-owner role.
81
+ const orgA = await harness.createOrg();
82
+ const orgB = await harness.createOrg();
83
+
84
+ await harness.writeAsOrg(orgA, '/api/people', { name: 'A1' });
85
+ const rowsB = await harness.readAsOrg(orgB, '/api/people');
86
+ expect(rowsB.find((r) => r.org_id === orgA.id)).toBeUndefined();
87
+ });
88
+ });
89
+
90
+ function randomPayload(): fc.Arbitrary<unknown> {
91
+ // Generic structure — narrow per-endpoint in real implementations.
92
+ return fc.record({
93
+ name: fc.string({ minLength: 1, maxLength: 50 }),
94
+ note: fc.option(fc.string({ maxLength: 200 })),
95
+ tags: fc.array(fc.string(), { maxLength: 5 }),
96
+ });
97
+ }
98
+
99
+ // ── Python (Hypothesis) sketch ───────────────────────────────────────────
100
+ //
101
+ // from hypothesis import given, strategies as st, settings
102
+ //
103
+ // @given(write_endpoint=st.sampled_from(WRITE_ENDPOINTS),
104
+ // payload=payload_strategy())
105
+ // @settings(max_examples=100, deadline=None)
106
+ // def test_no_cross_tenant_leak(write_endpoint, payload):
107
+ // reset_db()
108
+ // org_a, org_b = create_org(), create_org()
109
+ // written = write_as_org(org_a, write_endpoint, payload)
110
+ // for read_endpoint in READ_ENDPOINTS:
111
+ // rows_b = read_as_org(org_b, read_endpoint)
112
+ // assert not any(r['id'] == written['id'] for r in rows_b), \
113
+ // f"LEAK: {write_endpoint} -> {read_endpoint}"
114
+ //
115
+ // ── Anti-patterns ────────────────────────────────────────────────────────
116
+ //
117
+ // 1. Testing isolation only on known endpoints. The bug is in the endpoint
118
+ // you forgot. Property tests enumerate the full surface.
119
+ // 2. Using SUPERUSER fixtures. They silently bypass FORCE RLS at the engine
120
+ // level. Use the runtime non-owner role (`{project}_app`, BYPASSRLS=f).
121
+ // See /docs/patterns/rls-test-fixture.py.
122
+ // 3. Locking the property to "100% pass" without expanding the endpoint
123
+ // list as the codebase grows. listAll{Read,Write}Endpoints() must be
124
+ // derived dynamically (route enumeration, not hardcoded).
125
+ // 4. Testing only "row id leaks." Add field-level checks for any column
126
+ // holding semi-sensitive data (emails, internal notes) — leaks of
127
+ // *content* without row visibility are equally bad.