runward 0.6.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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +13 -0
  3. package/README.md +128 -0
  4. package/dist/cli.js +76 -0
  5. package/dist/commands/check.js +50 -0
  6. package/dist/commands/doctor.js +75 -0
  7. package/dist/commands/init.js +90 -0
  8. package/dist/commands/status.js +49 -0
  9. package/dist/commands/update.js +48 -0
  10. package/dist/lib/constants.js +2 -0
  11. package/dist/lib/mission.js +96 -0
  12. package/dist/lib/paths.js +28 -0
  13. package/dist/lib/styles.js +60 -0
  14. package/dist/lib/tools.js +59 -0
  15. package/dist/lib/write.js +30 -0
  16. package/package.json +29 -0
  17. package/templates/mission/adr/ADR-0000-template.md +38 -0
  18. package/templates/mission/architecture.md +60 -0
  19. package/templates/mission/decision-matrix.md +37 -0
  20. package/templates/mission/evaluation-rubric.md +60 -0
  21. package/templates/mission/floor.md +47 -0
  22. package/templates/mission/framing.md +58 -0
  23. package/templates/mission/mission-contract.md +58 -0
  24. package/templates/mission/observability-schema.md +53 -0
  25. package/templates/mission/port-contract.md +63 -0
  26. package/templates/mission/reference-stack.md +70 -0
  27. package/templates/mission/runbook.md +70 -0
  28. package/templates/mission/shared-bricks.md +74 -0
  29. package/templates/mission/threat-model.md +53 -0
  30. package/templates/rules/async-job-guardrails.md +95 -0
  31. package/templates/rules/async-post-turn-pipeline.md +96 -0
  32. package/templates/rules/async-scheduled-maintenance.md +95 -0
  33. package/templates/rules/cache-three-tier-architecture.md +44 -0
  34. package/templates/rules/checklist-day-zero-project.md +97 -0
  35. package/templates/rules/checklist-pre-production-observability.md +99 -0
  36. package/templates/rules/checklist-pre-production-performance.md +109 -0
  37. package/templates/rules/checklist-pre-production-resilience.md +99 -0
  38. package/templates/rules/checklist-pre-production-security.md +86 -0
  39. package/templates/rules/config-secrets-boundary.md +57 -0
  40. package/templates/rules/config-typing-zod.md +78 -0
  41. package/templates/rules/contracts-governance.md +64 -0
  42. package/templates/rules/data-memory-consolidation.md +81 -0
  43. package/templates/rules/data-memory-invalidation.md +90 -0
  44. package/templates/rules/data-memory-scoring.md +154 -0
  45. package/templates/rules/data-migrations-forward-only.md +55 -0
  46. package/templates/rules/data-orphan-cleanup.md +142 -0
  47. package/templates/rules/data-ttl-types.md +100 -0
  48. package/templates/rules/eval-loop.md +52 -0
  49. package/templates/rules/frontier-deterministic-boundary.md +91 -0
  50. package/templates/rules/hexa-adapter-pattern.md +85 -0
  51. package/templates/rules/hexa-architecture.md +84 -0
  52. package/templates/rules/hexa-move-deterministic-out.md +93 -0
  53. package/templates/rules/hexa-recommended-stack.md +33 -0
  54. package/templates/rules/hexa-typescript-native.md +80 -0
  55. package/templates/rules/observability-alert-configuration.md +160 -0
  56. package/templates/rules/observability-llm-metrics.md +150 -0
  57. package/templates/rules/observability-startup-provider-log.md +128 -0
  58. package/templates/rules/observability-structured-json-logs.md +138 -0
  59. package/templates/rules/patterns-memory-router-tiered.md +139 -0
  60. package/templates/rules/patterns-prompt-compiler.md +126 -0
  61. package/templates/rules/patterns-request-id-propagation.md +137 -0
  62. package/templates/rules/process-adr-and-journal.md +50 -0
  63. package/templates/rules/provider-llm-auto-detection.md +54 -0
  64. package/templates/rules/provider-no-crash-missing-env.md +67 -0
  65. package/templates/rules/quality-codebase-metrics.md +115 -0
  66. package/templates/rules/quality-zod-input-validation.md +131 -0
  67. package/templates/rules/resilience-fail-open.md +65 -0
  68. package/templates/rules/resilience-multi-provider-fallback.md +100 -0
  69. package/templates/rules/resilience-retry-backoff.md +115 -0
  70. package/templates/rules/resilience-retryable-errors.md +107 -0
  71. package/templates/rules/routing-confidence-upgrade.md +89 -0
  72. package/templates/rules/routing-model-cost-ratios.md +68 -0
  73. package/templates/rules/routing-smart-complexity.md +131 -0
  74. package/templates/rules/scaling-db-connection-pooling.md +121 -0
  75. package/templates/rules/scaling-distributed-rate-limiting.md +141 -0
  76. package/templates/rules/scaling-state-externalization.md +84 -0
  77. package/templates/rules/security-prompt-injection.md +63 -0
  78. package/templates/rules/state-event-sourcing.md +66 -0
  79. package/templates/rules/tools-registry-pattern.md +153 -0
  80. package/templates/rules/tools-scope-atomicity.md +103 -0
  81. package/templates/targets/AGENTS.md +32 -0
  82. package/templates/workflows/architect.md +50 -0
  83. package/templates/workflows/brownfield.md +52 -0
  84. package/templates/workflows/decision-loop.md +52 -0
  85. package/templates/workflows/floor.md +60 -0
  86. package/templates/workflows/frame.md +69 -0
  87. package/templates/workflows/govern.md +55 -0
  88. package/templates/workflows/handover.md +55 -0
  89. package/templates/workflows/iterate.md +52 -0
  90. package/templates/workflows/method.md +53 -0
  91. package/templates/workflows/review.md +59 -0
@@ -0,0 +1,139 @@
1
+ ---
2
+ title: Tiered Memory Router
3
+ impact: HIGH
4
+ impactDescription: Order-of-magnitude latency cut on simple queries by fetching only the memory tier the query needs, measured on a reference system — recalibrate on your traffic
5
+ tags: [patterns, memory, performance, architecture]
6
+ ---
7
+
8
+ ## Tiered Memory Router
9
+
10
+ Not every query needs full memory search. Route to appropriate tier based on complexity.
11
+
12
+ **The Problem:**
13
+
14
+ Fetching all memory sources for every query:
15
+ - Slow (full graph traversal, vector search)
16
+ - Expensive (embedding generation)
17
+ - Unnecessary for simple queries
18
+
19
+ **Tiered Approach:**
20
+
21
+ ```
22
+ Tier 0 (casual) - ~50ms
23
+ └── Presence, timezone, basic stats only
24
+
25
+ Tier 1 (task) - ~400ms
26
+ └── Tier 0 + vector search + recent memories
27
+
28
+ Tier 2 (deep) - ~1200ms
29
+ └── Tier 0 + Tier 1 + graph traversal + archival search
30
+ ```
31
+
32
+ **Incorrect:**
33
+
34
+ ```typescript
35
+ // Always fetch everything
36
+ async function getContext(userId: string, query: string): Promise<Context> {
37
+ const [presence, memories, graph, archives] = await Promise.all([
38
+ getPresence(userId),
39
+ searchMemories(query), // BAD: Always runs
40
+ traverseGraph(userId, query), // BAD: Always runs
41
+ searchArchives(query), // BAD: Always runs
42
+ ]);
43
+ return { presence, memories, graph, archives };
44
+ }
45
+ ```
46
+
47
+ **Correct:**
48
+
49
+ ```typescript
50
+ type MemoryTier = 0 | 1 | 2;
51
+
52
+ interface MemoryRouterConfig {
53
+ tier: MemoryTier;
54
+ sources: MemorySource[];
55
+ timeout: number;
56
+ }
57
+
58
+ const tierConfigs: Record<MemoryTier, MemoryRouterConfig> = {
59
+ 0: {
60
+ tier: 0,
61
+ sources: ['presence', 'basicStats'],
62
+ timeout: 100,
63
+ },
64
+ 1: {
65
+ tier: 1,
66
+ sources: ['presence', 'basicStats', 'vectorSearch', 'recentMemories'],
67
+ timeout: 500,
68
+ },
69
+ 2: {
70
+ tier: 2,
71
+ sources: ['presence', 'basicStats', 'vectorSearch', 'recentMemories', 'graphTraversal', 'archivalSearch'],
72
+ timeout: 2000,
73
+ },
74
+ };
75
+
76
+ function selectTier(complexity: ComplexityLevel): MemoryTier {
77
+ switch (complexity) {
78
+ case 'casual': return 0;
79
+ case 'task': return 1;
80
+ case 'planning':
81
+ case 'deep': return 2;
82
+ }
83
+ }
84
+
85
+ async function getContext(
86
+ userId: string,
87
+ query: string,
88
+ complexity: ComplexityLevel
89
+ ): Promise<Context> {
90
+ const tier = selectTier(complexity);
91
+ const config = tierConfigs[tier];
92
+
93
+ const fetchers: Record<MemorySource, () => Promise<any>> = {
94
+ presence: () => getPresence(userId),
95
+ basicStats: () => getBasicStats(userId),
96
+ vectorSearch: () => searchMemories(query),
97
+ recentMemories: () => getRecentMemories(userId),
98
+ graphTraversal: () => traverseGraph(userId, query),
99
+ archivalSearch: () => searchArchives(query),
100
+ };
101
+
102
+ // Per-source timeout with allSettled: one slow source degrades only
103
+ // itself. A global Promise.race that resolves to [] on timeout would
104
+ // throw away everything — including sources that had already answered.
105
+ const settled = await Promise.allSettled(
106
+ config.sources.map(source => withTimeout(fetchers[source](), config.timeout))
107
+ );
108
+
109
+ const results = settled
110
+ .filter((r): r is PromiseFulfilledResult<unknown> => r.status === 'fulfilled')
111
+ .map(r => r.value);
112
+
113
+ const failed = settled.length - results.length;
114
+ if (failed > 0) {
115
+ console.warn(`[MEMORY] Tier ${tier}: ${failed}/${settled.length} sources failed or timed out, degrading`);
116
+ }
117
+
118
+ return buildContext(results);
119
+ }
120
+
121
+ function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
122
+ return Promise.race([
123
+ promise,
124
+ sleep(ms).then(() => Promise.reject(new Error('memory source timeout'))),
125
+ ]);
126
+ }
127
+ ```
128
+
129
+ Tier 0 sources (`presence`, `basicStats`) are part of every tier's source list and are cheap key-value reads: a degraded response still carries them. If you need a hard guarantee, await them separately from the optional sources so they are never dropped.
130
+
131
+ **Latency Impact (measured on a reference system — recalibrate on your traffic):**
132
+
133
+ | Query Type | Old (all sources) | New (tiered) | Savings |
134
+ |------------|-------------------|--------------|---------|
135
+ | "Hi" | 1200ms | 50ms | **96%** |
136
+ | "Summarize project" | 1200ms | 400ms | **67%** |
137
+ | "Analyze market trends" | 1200ms | 1200ms | 0% |
138
+
139
+ Average latency reduction is large — an order of magnitude on simple queries — but the weighted figure depends entirely on your traffic distribution. The numbers above come from one reference system; measure your own before quoting a percentage.
@@ -0,0 +1,126 @@
1
+ ---
2
+ title: Prompt as Program (Prompt Compiler)
3
+ impact: MEDIUM
4
+ impactDescription: Enables versioning, testing, and optimization of prompts
5
+ tags: [patterns, prompts, llm, architecture]
6
+ ---
7
+
8
+ ## Prompt as Program (Prompt Compiler)
9
+
10
+ Treat prompts as compiled programs, not raw strings. This enables versioning, testing, and optimization.
11
+
12
+ **Incorrect:**
13
+
14
+ ```typescript
15
+ // Raw string concatenation
16
+ const prompt = `
17
+ You are an assistant.
18
+ ${userContext}
19
+ ${projectInfo}
20
+ ${tools.map(t => t.description).join('\n')}
21
+ ${userMessage}
22
+ `;
23
+ ```
24
+
25
+ **Correct:**
26
+
27
+ ```typescript
28
+ // Prompt as structured data
29
+ interface CompiledPrompt {
30
+ content: string;
31
+ tokenEstimate: number;
32
+ cacheKey: string;
33
+ locale: Locale;
34
+ cacheLevel: 'L1' | 'L2' | 'L3';
35
+ blocks: PromptBlock[];
36
+ version: string;
37
+ }
38
+
39
+ interface PromptBlock {
40
+ name: string;
41
+ content: string;
42
+ tokens: number;
43
+ cacheLevel: 'L1' | 'L2' | 'L3';
44
+ hash: string; // For cache invalidation
45
+ }
46
+
47
+ class PromptCompiler {
48
+ private blocks: Map<string, PromptBlock> = new Map();
49
+
50
+ // Register reusable blocks
51
+ registerBlock(name: string, content: string, cacheLevel: CacheLevel): void {
52
+ this.blocks.set(name, {
53
+ name,
54
+ content,
55
+ tokens: estimateTokens(content),
56
+ cacheLevel,
57
+ hash: computeHash(content),
58
+ });
59
+ }
60
+
61
+ // Compile prompt from blocks
62
+ compile(blockNames: string[], dynamicContent: Record<string, string>): CompiledPrompt {
63
+ const usedBlocks: PromptBlock[] = [];
64
+ let content = '';
65
+
66
+ for (const name of blockNames) {
67
+ const block = this.blocks.get(name);
68
+ if (!block) {
69
+ throw new Error(`Unknown block: ${name}`);
70
+ }
71
+ usedBlocks.push(block);
72
+ content += block.content + '\n\n';
73
+ }
74
+
75
+ // Add dynamic content
76
+ for (const [key, value] of Object.entries(dynamicContent)) {
77
+ content += `## ${key}\n${value}\n\n`;
78
+ }
79
+
80
+ return {
81
+ content,
82
+ tokenEstimate: usedBlocks.reduce((sum, b) => sum + b.tokens, 0) + estimateTokens(Object.values(dynamicContent).join('')),
83
+ cacheKey: this.computeCacheKey(usedBlocks, dynamicContent),
84
+ locale: 'en',
85
+ cacheLevel: this.determineCacheLevel(usedBlocks),
86
+ blocks: usedBlocks,
87
+ version: this.computeVersion(usedBlocks),
88
+ };
89
+ }
90
+
91
+ private computeVersion(blocks: PromptBlock[]): string {
92
+ const hashes = blocks.map(b => b.hash).join('');
93
+ return computeHash(hashes).substring(0, 8);
94
+ }
95
+ }
96
+
97
+ // Usage
98
+ const compiler = new PromptCompiler();
99
+
100
+ // Register static blocks
101
+ compiler.registerBlock('system-identity', SYSTEM_IDENTITY_PROMPT, 'L1');
102
+ compiler.registerBlock('evaluation-criteria', EVALUATION_CRITERIA_PROMPT, 'L1');
103
+ compiler.registerBlock('tool-definitions', TOOL_DEFINITIONS_PROMPT, 'L2');
104
+ compiler.registerBlock('response-format', RESPONSE_FORMAT_PROMPT, 'L1');
105
+
106
+ // Compile for request
107
+ const prompt = compiler.compile(
108
+ ['system-identity', 'evaluation-criteria', 'tool-definitions', 'response-format'],
109
+ {
110
+ 'Current Project': projectContext,
111
+ 'Recent Memories': memoriesContext,
112
+ 'User Message': userMessage,
113
+ }
114
+ );
115
+
116
+ console.log('Prompt version:', prompt.version);
117
+ console.log('Estimated tokens:', prompt.tokenEstimate);
118
+ ```
119
+
120
+ **Benefits:**
121
+
122
+ - **Versioning**: Track prompt changes like code
123
+ - **Testing**: Unit test individual blocks
124
+ - **Caching**: Precise cache key computation
125
+ - **Debugging**: Know exactly what's in each prompt
126
+ - **Optimization**: Measure and reduce token usage
@@ -0,0 +1,137 @@
1
+ ---
2
+ title: Request ID Propagation
3
+ impact: MEDIUM
4
+ impactDescription: Enables end-to-end request tracing across services
5
+ tags: [patterns, observability, debugging]
6
+ ---
7
+
8
+ ## Request ID Propagation
9
+
10
+ Propagate a unique request ID through all services for end-to-end tracing.
11
+
12
+ **Implementation:**
13
+
14
+ ```typescript
15
+ // src/lib/context.ts
16
+ import { AsyncLocalStorage } from 'async_hooks';
17
+ import { randomUUID } from 'crypto';
18
+
19
+ interface RequestContext {
20
+ requestId: string;
21
+ userId?: string;
22
+ tenantId?: string;
23
+ startTime: number;
24
+ }
25
+
26
+ const asyncLocalStorage = new AsyncLocalStorage<RequestContext>();
27
+
28
+ export function getRequestContext(): RequestContext | undefined {
29
+ return asyncLocalStorage.getStore();
30
+ }
31
+
32
+ export function getRequestId(): string {
33
+ return getRequestContext()?.requestId || 'no-request-id';
34
+ }
35
+
36
+ // Middleware
37
+ export function requestContextMiddleware(req: Request, res: Response, next: NextFunction) {
38
+ const requestId = (req.headers['x-request-id'] as string) || randomUUID();
39
+
40
+ const context: RequestContext = {
41
+ requestId,
42
+ userId: req.user?.id,
43
+ tenantId: req.user?.tenantId,
44
+ startTime: Date.now(),
45
+ };
46
+
47
+ // Set response header
48
+ res.setHeader('x-request-id', requestId);
49
+
50
+ // Run handler within context
51
+ asyncLocalStorage.run(context, () => next());
52
+ }
53
+ ```
54
+
55
+ **Logger Integration:**
56
+
57
+ ```typescript
58
+ // src/lib/logger.ts
59
+ import { getRequestContext } from './context';
60
+
61
+ class Logger {
62
+ private log(level: string, message: string, data: object = {}) {
63
+ const ctx = getRequestContext();
64
+
65
+ console.log(JSON.stringify({
66
+ level,
67
+ message,
68
+ timestamp: new Date().toISOString(),
69
+ requestId: ctx?.requestId,
70
+ userId: ctx?.userId,
71
+ tenantId: ctx?.tenantId,
72
+ ...data,
73
+ }));
74
+ }
75
+
76
+ info(message: string, data?: object) { this.log('info', message, data); }
77
+ error(message: string, data?: object) { this.log('error', message, data); }
78
+ // ...
79
+ }
80
+
81
+ export const logger = new Logger();
82
+ ```
83
+
84
+ **Propagate to External Services:**
85
+
86
+ ```typescript
87
+ // Include requestId in external API calls
88
+ async function callExternalService(endpoint: string, data: object) {
89
+ const requestId = getRequestId();
90
+
91
+ return fetch(endpoint, {
92
+ method: 'POST',
93
+ headers: {
94
+ 'Content-Type': 'application/json',
95
+ 'X-Request-Id': requestId, // Propagate to external service
96
+ },
97
+ body: JSON.stringify(data),
98
+ });
99
+ }
100
+
101
+ // Include in LLM metadata
102
+ async function callLLM(request: LLMRequest) {
103
+ const requestId = getRequestId();
104
+
105
+ return model.complete({
106
+ ...request,
107
+ metadata: {
108
+ user_id: getRequestContext()?.userId,
109
+ request_id: requestId, // Appears in the provider dashboard
110
+ },
111
+ });
112
+ }
113
+ ```
114
+
115
+ **Database Query Tagging:**
116
+
117
+ ```typescript
118
+ // Tag queries with request ID for debugging
119
+ async function query<T>(sql: string, params: unknown[]): Promise<T[]> {
120
+ const requestId = getRequestId();
121
+
122
+ // Add comment with request ID (visible in slow query logs)
123
+ const taggedSql = `/* requestId=${requestId} */ ${sql}`;
124
+
125
+ return db.query(taggedSql, params);
126
+ }
127
+ ```
128
+
129
+ **Full Request Trace Example:**
130
+
131
+ ```
132
+ [2025-01-24T10:30:00.000Z] {"level":"info","message":"Request started","requestId":"abc-123","path":"/api/chat"}
133
+ [2025-01-24T10:30:00.050Z] {"level":"info","message":"Memory retrieval","requestId":"abc-123","memories":5}
134
+ [2025-01-24T10:30:00.100Z] {"level":"info","message":"LLM request started","requestId":"abc-123","model":"balanced"}
135
+ [2025-01-24T10:30:02.500Z] {"level":"info","message":"LLM request completed","requestId":"abc-123","tokens":1234}
136
+ [2025-01-24T10:30:02.550Z] {"level":"info","message":"Request completed","requestId":"abc-123","duration":2550}
137
+ ```
@@ -0,0 +1,50 @@
1
+ ---
2
+ title: ADR Discipline and Working Journal
3
+ impact: HIGH
4
+ impactDescription: Makes structural decisions traceable and reversible-by-record, so the system can be picked up by anyone without re-litigating settled choices
5
+ tags: [process, adr, journal, traceability, governance]
6
+ ---
7
+
8
+ ## ADR Discipline and Working Journal
9
+
10
+ A structural decision that lives only in someone's head is a decision that will be re-debated. Two lightweight records keep the system legible: ADRs for structural choices, a journal for the running thread.
11
+
12
+ **ADR (Architecture Decision Record):**
13
+
14
+ - One ADR per structural decision: a boundary, a port, a model, a persistence choice, a safety guard.
15
+ - An accepted ADR is **never edited**. A new decision that changes it is a **new ADR that supersedes or refines** the old one (the trail stays intact).
16
+ - Each ADR records its **trigger**: the objective signal that would reopen it. "Default: sober; trigger: this measured condition." A decision with no trigger is a decision you cannot revisit on evidence.
17
+
18
+ ```
19
+ # ADR-008 - Grounding guard over an expanded set
20
+ Status: accepted · Refines: ADR-001 · Date: 2026-06-22
21
+ Context: the guard rejected a legitimate figure present in a served fact's label.
22
+ Decision: the grounded set = fact values ∪ numbers in served labels ∪ years.
23
+ Trigger to revisit: wiring the model in structured outputs -> switch to {fN}
24
+ slot references -> write ADR-009 at that point.
25
+ Consequences: the unit test now feeds realistic facts with their labels.
26
+ ```
27
+
28
+ **Journal (one entry per working session):**
29
+
30
+ ```
31
+ ## YYYY-MM-DD - short title
32
+ - Done: ...
33
+ - Decided: ... (if structural, point to ADR-xxx)
34
+ - Blocked / to dig: ...
35
+ - Next: ...
36
+ ```
37
+
38
+ The journal carries minor decisions and progress; structural ones graduate to an ADR. Together they answer "why is it like this?" without archaeology.
39
+
40
+ **Commits and the code as evidence:**
41
+
42
+ - Conventional commits (`feat:`, `fix:`, `refactor:`, `docs:`) so history is scannable.
43
+ - The code is the final proof: a decision that matters has a test, a guard, or a runnable check that demonstrates it. An ADR points at that proof, it does not replace it.
44
+
45
+ **Checklist:**
46
+
47
+ - [ ] Every structural choice has an ADR with an explicit revisit trigger.
48
+ - [ ] Accepted ADRs are immutable; change means a new, superseding ADR.
49
+ - [ ] A journal entry closes each session (done / decided / blocked / next).
50
+ - [ ] Decisions that matter are backed by a test or guard in the code.
@@ -0,0 +1,54 @@
1
+ ---
2
+ title: Model Provider as an Adapter (Detected, Never Hardcoded)
3
+ impact: CRITICAL
4
+ impactDescription: Keeps the model behind a port so the provider can change by configuration without touching the core
5
+ tags: [provider, llm, embeddings, ports, configuration, portability]
6
+ ---
7
+
8
+ ## Model Provider as an Adapter
9
+
10
+ > **The model is rented; the architecture is owned. So the model sits behind a port, like any other external service.**
11
+
12
+ The core depends on a `ModelPort` (and, if used, an `EmbeddingPort`), never on a vendor SDK. Which provider answers is a configuration concern resolved at the boundary, by detecting available credentials. The same core runs against any provider, a gateway, a local model, or a mock, with no code change.
13
+
14
+ **Incorrect:**
15
+
16
+ ```typescript
17
+ // Vendor hardcoded into reachable code; swapping providers is a rewrite.
18
+ import VendorSDK from "@some-vendor/llm-sdk";
19
+ const client = new VendorSDK({ apiKey: process.env.VENDOR_API_KEY! }); // BAD: a vendor wired into the core
20
+ ```
21
+
22
+ **Correct:**
23
+
24
+ ```typescript
25
+ // 1. The core knows a port, nothing else.
26
+ interface ModelPort {
27
+ complete(req: CompletionRequest): Promise<CompletionResult>;
28
+ }
29
+
30
+ // 2. The provider is resolved at the boundary from available config.
31
+ type ModelConfig = { provider: string; gatewayUrl?: string; model: string };
32
+
33
+ function resolveModelProvider(): ModelConfig {
34
+ // Prefer a gateway (one egress, one place to attach the real key), then direct.
35
+ if (env.MODEL_GATEWAY_URL) return { provider: "gateway", gatewayUrl: env.MODEL_GATEWAY_URL, model: env.MODEL };
36
+ if (env.MODEL_PROVIDER) return { provider: env.MODEL_PROVIDER, model: env.MODEL };
37
+ throw new ConfigError("No model provider configured (set MODEL_GATEWAY_URL or MODEL_PROVIDER).");
38
+ }
39
+
40
+ // 3. One adapter per provider implements the port. The core never sees the difference.
41
+ class GatewayModelAdapter implements ModelPort { /* ... */ }
42
+ class MockModelAdapter implements ModelPort { /* deterministic, for tests and the floor */ }
43
+ ```
44
+
45
+ **Why a gateway is the sober default:** a single egress point is where the real credential is attached by infrastructure (see *Secrets at the Network Boundary*), where residency and fallback live, and where you swap providers without redeploying the core.
46
+
47
+ **Embeddings are a separate port.** The embedding provider is detected the same way and is independent of the model provider; do not assume one implies the other.
48
+
49
+ **Checklist:**
50
+
51
+ - [ ] No vendor SDK imported from the core; only the adapter imports it.
52
+ - [ ] Provider resolved at the boundary from config, with a clear error if nothing is set.
53
+ - [ ] A deterministic mock adapter exists for tests and the offline floor.
54
+ - [ ] Embeddings have their own port, resolved independently.
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: Graceful Degradation of Optional Services
3
+ impact: HIGH
4
+ impactDescription: Lets the app start and run wherever it is deployed, enabling features by available config rather than crashing on a missing variable
5
+ tags: [provider, configuration, resilience, deployment]
6
+ ---
7
+
8
+ ## Graceful Degradation of Optional Services
9
+
10
+ > **Required missing: fail fast and loud. Optional missing: disable the feature, keep running.**
11
+
12
+ The application must start regardless of which optional services are configured. Only truly required dependencies (a database of record, at least one model provider) may stop startup. Everything optional degrades to a sober default.
13
+
14
+ **Required vs optional:**
15
+
16
+ | Category | Required (may stop startup) | Optional (must degrade) |
17
+ |---|---|---|
18
+ | Database of record | `DATABASE_URL` | - |
19
+ | Model | at least one provider | a specific provider |
20
+ | Search / enrichment | none | its API key |
21
+ | Vector / memory store | none | its URL |
22
+ | Telemetry sink | none | its token |
23
+
24
+ **Incorrect:**
25
+
26
+ ```typescript
27
+ // Throws at module load for an optional service.
28
+ const search = new SearchClient(env.SEARCH_API_KEY!); // BAD: crashes if absent
29
+ ```
30
+
31
+ **Correct:**
32
+
33
+ ```typescript
34
+ // Lazy, returns null when unavailable; callers fall back.
35
+ let search: SearchClient | null = null;
36
+ export function getSearch(): SearchClient | null {
37
+ if (search) return search;
38
+ if (!env.SEARCH_API_KEY) { log.info("[SEARCH] not configured - disabled"); return null; }
39
+ return (search = new SearchClient(env.SEARCH_API_KEY));
40
+ }
41
+
42
+ export async function enrich(q: string): Promise<Result[]> {
43
+ const s = getSearch();
44
+ return s ? s.search(q) : []; // sober default, not a crash
45
+ }
46
+ ```
47
+
48
+ **Log the resolved configuration at startup** so the active shape is visible (never log secrets):
49
+
50
+ ```typescript
51
+ log.info("[STARTUP] services", {
52
+ database: !!env.DATABASE_URL,
53
+ model: modelConfig.provider,
54
+ search: !!getSearch(),
55
+ memory: !!getMemory(),
56
+ });
57
+ ```
58
+
59
+ This is the "sober default plus trigger" posture applied to configuration: ship with the minimum that runs everywhere, light up a capability only when its dependency is actually present.
60
+
61
+ **This is bimodal deployment.** The same binary runs in a *minimal* mode (only required dependencies present) and a *full* mode (every optional dependency present), with the mode resolved by feature detection at startup, not by a separate build. The interface signals what is active rather than failing silently. Minimal mode is what makes the system deployable in a restricted or air-gapped environment without a code change.
62
+
63
+ **Checklist:**
64
+
65
+ - [ ] App starts with only the required variables set.
66
+ - [ ] Each optional service degrades to a defined fallback, never a crash.
67
+ - [ ] Resolved configuration (not secrets) is logged once at startup.
@@ -0,0 +1,115 @@
1
+ ---
2
+ title: Codebase Metrics Thresholds
3
+ impact: MEDIUM
4
+ impactDescription: Identifies code quality issues before they become problems
5
+ tags: [quality, metrics, maintainability]
6
+ ---
7
+
8
+ ## Codebase Metrics Thresholds
9
+
10
+ Monitor codebase health with these thresholds.
11
+
12
+ **File Size Thresholds:**
13
+
14
+ | Metric | Warning | Refactor Needed |
15
+ |--------|---------|-----------------|
16
+ | File > 300 lines | ⚠️ | > 500 lines |
17
+ | Function > 50 lines | ⚠️ | > 100 lines |
18
+ | Cyclomatic complexity > 10 | ⚠️ | > 15 |
19
+ | Nesting depth > 4 | ⚠️ | > 6 |
20
+
21
+ **Code Smell Indicators:**
22
+
23
+ | Pattern | Threshold | Action |
24
+ |---------|-----------|--------|
25
+ | Switch statement cases | > 10 | Use registry pattern |
26
+ | Function parameters | > 5 | Use options object |
27
+ | Class methods | > 15 | Split class |
28
+ | Import statements | > 20 | Check for god file |
29
+ | Tools count | > 30 | Group by domain |
30
+ | In-memory Maps | Any | Document for scaling |
31
+
32
+ **Automated Checks:**
33
+
34
+ ```typescript
35
+ // scripts/codebase-metrics.ts
36
+ import * as fs from 'fs';
37
+ import * as path from 'path';
38
+
39
+ interface FileMetrics {
40
+ path: string;
41
+ lines: number;
42
+ functions: number;
43
+ maxFunctionLength: number;
44
+ imports: number;
45
+ switchCases: number;
46
+ }
47
+
48
+ function analyzeFile(filePath: string): FileMetrics {
49
+ const content = fs.readFileSync(filePath, 'utf8');
50
+ const lines = content.split('\n');
51
+
52
+ return {
53
+ path: filePath,
54
+ lines: lines.length,
55
+ functions: (content.match(/function\s+\w+|=>\s*{|\w+\s*\([^)]*\)\s*{/g) || []).length,
56
+ maxFunctionLength: calculateMaxFunctionLength(content),
57
+ imports: (content.match(/^import\s+/gm) || []).length,
58
+ switchCases: (content.match(/case\s+/g) || []).length,
59
+ };
60
+ }
61
+
62
+ function reportIssues(metrics: FileMetrics[]): void {
63
+ const issues: string[] = [];
64
+
65
+ for (const file of metrics) {
66
+ if (file.lines > 500) {
67
+ issues.push(`${file.path}: ${file.lines} lines (max 500)`);
68
+ } else if (file.lines > 300) {
69
+ issues.push(`${file.path}: ${file.lines} lines (warning > 300)`);
70
+ }
71
+
72
+ if (file.switchCases > 20) {
73
+ issues.push(`${file.path}: ${file.switchCases} switch cases - use registry pattern`);
74
+ }
75
+
76
+ if (file.imports > 20) {
77
+ issues.push(`${file.path}: ${file.imports} imports - possible god file`);
78
+ }
79
+ }
80
+
81
+ if (issues.length > 0) {
82
+ console.log('Codebase Issues Found:');
83
+ issues.forEach(i => console.log(i));
84
+ process.exit(1); // Fail CI
85
+ } else {
86
+ console.log('yes No codebase issues found');
87
+ }
88
+ }
89
+ ```
90
+
91
+ **ESLint Rules:**
92
+
93
+ ```javascript
94
+ // .eslintrc.js
95
+ module.exports = {
96
+ rules: {
97
+ 'max-lines': ['warn', { max: 300, skipBlankLines: true, skipComments: true }],
98
+ 'max-lines-per-function': ['warn', { max: 50, skipBlankLines: true, skipComments: true }],
99
+ 'max-depth': ['warn', 4],
100
+ 'max-params': ['warn', 5],
101
+ 'complexity': ['warn', 10],
102
+ },
103
+ };
104
+ ```
105
+
106
+ **PR Checklist:**
107
+
108
+ ```markdown
109
+ ## Code Quality Checklist
110
+ - [ ] No file exceeds 500 lines
111
+ - [ ] No function exceeds 100 lines
112
+ - [ ] No switch with > 10 cases
113
+ - [ ] New Maps documented for scaling
114
+ - [ ] Complex logic has comments
115
+ ```