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.
- package/LICENSE +21 -0
- package/NOTICE.md +13 -0
- package/README.md +128 -0
- package/dist/cli.js +76 -0
- package/dist/commands/check.js +50 -0
- package/dist/commands/doctor.js +75 -0
- package/dist/commands/init.js +90 -0
- package/dist/commands/status.js +49 -0
- package/dist/commands/update.js +48 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/mission.js +96 -0
- package/dist/lib/paths.js +28 -0
- package/dist/lib/styles.js +60 -0
- package/dist/lib/tools.js +59 -0
- package/dist/lib/write.js +30 -0
- package/package.json +29 -0
- package/templates/mission/adr/ADR-0000-template.md +38 -0
- package/templates/mission/architecture.md +60 -0
- package/templates/mission/decision-matrix.md +37 -0
- package/templates/mission/evaluation-rubric.md +60 -0
- package/templates/mission/floor.md +47 -0
- package/templates/mission/framing.md +58 -0
- package/templates/mission/mission-contract.md +58 -0
- package/templates/mission/observability-schema.md +53 -0
- package/templates/mission/port-contract.md +63 -0
- package/templates/mission/reference-stack.md +70 -0
- package/templates/mission/runbook.md +70 -0
- package/templates/mission/shared-bricks.md +74 -0
- package/templates/mission/threat-model.md +53 -0
- package/templates/rules/async-job-guardrails.md +95 -0
- package/templates/rules/async-post-turn-pipeline.md +96 -0
- package/templates/rules/async-scheduled-maintenance.md +95 -0
- package/templates/rules/cache-three-tier-architecture.md +44 -0
- package/templates/rules/checklist-day-zero-project.md +97 -0
- package/templates/rules/checklist-pre-production-observability.md +99 -0
- package/templates/rules/checklist-pre-production-performance.md +109 -0
- package/templates/rules/checklist-pre-production-resilience.md +99 -0
- package/templates/rules/checklist-pre-production-security.md +86 -0
- package/templates/rules/config-secrets-boundary.md +57 -0
- package/templates/rules/config-typing-zod.md +78 -0
- package/templates/rules/contracts-governance.md +64 -0
- package/templates/rules/data-memory-consolidation.md +81 -0
- package/templates/rules/data-memory-invalidation.md +90 -0
- package/templates/rules/data-memory-scoring.md +154 -0
- package/templates/rules/data-migrations-forward-only.md +55 -0
- package/templates/rules/data-orphan-cleanup.md +142 -0
- package/templates/rules/data-ttl-types.md +100 -0
- package/templates/rules/eval-loop.md +52 -0
- package/templates/rules/frontier-deterministic-boundary.md +91 -0
- package/templates/rules/hexa-adapter-pattern.md +85 -0
- package/templates/rules/hexa-architecture.md +84 -0
- package/templates/rules/hexa-move-deterministic-out.md +93 -0
- package/templates/rules/hexa-recommended-stack.md +33 -0
- package/templates/rules/hexa-typescript-native.md +80 -0
- package/templates/rules/observability-alert-configuration.md +160 -0
- package/templates/rules/observability-llm-metrics.md +150 -0
- package/templates/rules/observability-startup-provider-log.md +128 -0
- package/templates/rules/observability-structured-json-logs.md +138 -0
- package/templates/rules/patterns-memory-router-tiered.md +139 -0
- package/templates/rules/patterns-prompt-compiler.md +126 -0
- package/templates/rules/patterns-request-id-propagation.md +137 -0
- package/templates/rules/process-adr-and-journal.md +50 -0
- package/templates/rules/provider-llm-auto-detection.md +54 -0
- package/templates/rules/provider-no-crash-missing-env.md +67 -0
- package/templates/rules/quality-codebase-metrics.md +115 -0
- package/templates/rules/quality-zod-input-validation.md +131 -0
- package/templates/rules/resilience-fail-open.md +65 -0
- package/templates/rules/resilience-multi-provider-fallback.md +100 -0
- package/templates/rules/resilience-retry-backoff.md +115 -0
- package/templates/rules/resilience-retryable-errors.md +107 -0
- package/templates/rules/routing-confidence-upgrade.md +89 -0
- package/templates/rules/routing-model-cost-ratios.md +68 -0
- package/templates/rules/routing-smart-complexity.md +131 -0
- package/templates/rules/scaling-db-connection-pooling.md +121 -0
- package/templates/rules/scaling-distributed-rate-limiting.md +141 -0
- package/templates/rules/scaling-state-externalization.md +84 -0
- package/templates/rules/security-prompt-injection.md +63 -0
- package/templates/rules/state-event-sourcing.md +66 -0
- package/templates/rules/tools-registry-pattern.md +153 -0
- package/templates/rules/tools-scope-atomicity.md +103 -0
- package/templates/targets/AGENTS.md +32 -0
- package/templates/workflows/architect.md +50 -0
- package/templates/workflows/brownfield.md +52 -0
- package/templates/workflows/decision-loop.md +52 -0
- package/templates/workflows/floor.md +60 -0
- package/templates/workflows/frame.md +69 -0
- package/templates/workflows/govern.md +55 -0
- package/templates/workflows/handover.md +55 -0
- package/templates/workflows/iterate.md +52 -0
- package/templates/workflows/method.md +53 -0
- 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
|
+
```
|