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,86 @@
1
+ ---
2
+ title: Pre-Production Security Checklist
3
+ impact: CRITICAL
4
+ impactDescription: Ensures security requirements are met before production deployment
5
+ tags: [checklist, security, production, deployment]
6
+ ---
7
+
8
+ ## Pre-Production Security Checklist
9
+
10
+ Security items that MUST be validated before any production deployment.
11
+
12
+ ```
13
+ SECURITY CHECKLIST
14
+ ├── [ ] Secrets in env vars only (not in code, not in logs)
15
+ ├── [ ] Input validation with Zod on ALL external inputs
16
+ ├── [ ] Rate limiting configured and tested
17
+ ├── [ ] CORS configured (specific origins, not '*')
18
+ ├── [ ] Security headers set (CSP, HSTS, X-Frame-Options)
19
+ ├── [ ] SQL injection protection (parameterized queries)
20
+ ├── [ ] XSS protection (output encoding)
21
+ ├── [ ] Authentication on all protected routes
22
+ ├── [ ] Authorization checks (tenant isolation)
23
+ ├── [ ] Prompt injection: observed content is data; lethal trifecta broken on the context window
24
+ ├── [ ] Grounding guard on any model output that surfaces facts (fail-closed)
25
+ ├── [ ] Secret never reaches the model (substitute only; real key at the network boundary)
26
+ └── [ ] Dependency audit (npm audit / yarn audit)
27
+ ```
28
+
29
+ **Incorrect:**
30
+
31
+ ```typescript
32
+ // Secrets in code
33
+ const apiKey = "sk-abc123..."; // BAD:
34
+
35
+ // No input validation
36
+ app.post('/api/project', async (req, res) => {
37
+ const { name, data } = req.body; // BAD: Unvalidated
38
+ await db.insert(projects).values({ name, data });
39
+ });
40
+
41
+ // CORS wide open
42
+ app.use(cors({ origin: '*' })); // BAD:
43
+ ```
44
+
45
+ **Correct:**
46
+
47
+ ```typescript
48
+ // Secrets from environment
49
+ const apiKey = process.env.API_KEY;
50
+
51
+ // Zod validation
52
+ const CreateProjectSchema = z.object({
53
+ name: z.string().min(1).max(100),
54
+ data: z.record(z.unknown()),
55
+ });
56
+
57
+ app.post('/api/project', async (req, res) => {
58
+ const validated = CreateProjectSchema.parse(req.body); // GOOD:
59
+ await db.insert(projects).values(validated);
60
+ });
61
+
62
+ // Specific CORS origins
63
+ app.use(cors({
64
+ origin: ['https://app.example.com', 'https://admin.example.com'],
65
+ credentials: true,
66
+ }));
67
+ ```
68
+
69
+ **Security Headers (set at the edge or in the framework):**
70
+
71
+ ```typescript
72
+ // next.config.js
73
+ const securityHeaders = [
74
+ { key: 'X-DNS-Prefetch-Control', value: 'on' },
75
+ { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains' },
76
+ { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
77
+ { key: 'X-Content-Type-Options', value: 'nosniff' },
78
+ { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
79
+ ];
80
+
81
+ module.exports = {
82
+ async headers() {
83
+ return [{ source: '/:path*', headers: securityHeaders }];
84
+ },
85
+ };
86
+ ```
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: Secrets at the Network Boundary, Never in the Model
3
+ impact: CRITICAL
4
+ impactDescription: Makes secret disclosure structurally impossible by keeping the real key out of the model and the domain
5
+ tags: [security, secrets, configuration, boundary]
6
+ ---
7
+
8
+ ## Secrets at the Network Boundary, Never in the Model
9
+
10
+ > **A secret the model never saw cannot be disclosed.** This guard is structural, not behavioural, and it strips prompt injection of its most coveted loot.
11
+
12
+ Secrets and sensitive data are isolated behind typed configuration, never in the domain. One notch further for runtime secrets: the secret never crosses the boundary of the model or of the execution environment. The agent handles only a *substitute*; the real key is attached by infrastructure at the network boundary, toward the only explicitly authorised destinations, and is replaceable without redeployment.
13
+
14
+ **Incorrect:**
15
+
16
+ ```typescript
17
+ // Key in code, in the domain, and within reach of the model's context.
18
+ const MODEL_KEY = "sk-live-abc123"; // BAD: in code
19
+ function callModel(prompt: string) {
20
+ return fetch(GW, { headers: { Authorization: `Bearer ${MODEL_KEY}` }, ... });
21
+ }
22
+ // BAD: if the key sits in config the agent reads, an injection can ask for it.
23
+ ```
24
+
25
+ **Correct:**
26
+
27
+ ```typescript
28
+ // Domain knows a port, not a key. The adapter calls a gateway; the real
29
+ // credential is injected by infrastructure at the egress boundary.
30
+ interface ModelPort { complete(req: Req): Promise<Res>; }
31
+
32
+ // adapter: no secret in scope of the model's reasoning
33
+ class GatewayModelAdapter implements ModelPort {
34
+ // calls the internal gateway URL; the platform attaches the real key
35
+ // at the network edge toward the allowed destination only.
36
+ }
37
+ ```
38
+
39
+ **Configuration discipline:**
40
+
41
+ - Secrets live in `.env` (git-ignored), read once into typed config, never committed, never logged.
42
+ - The domain depends on ports, never on environment variables.
43
+ - Egress credentials are attached by the platform at the network boundary, not carried through application code.
44
+ - Prefer rotation without redeployment (the substitute stays stable, the key behind it rotates).
45
+
46
+ **Transport, strict by default:** for Postgres/Neon, pin `sslmode=verify-full` (strict verification of both certificate and DNS name) so you do not depend on a driver's future default:
47
+
48
+ ```
49
+ postgresql://USER:PASS@HOST/DB?sslmode=verify-full&channel_binding=require
50
+ ```
51
+
52
+ **Checklist:**
53
+
54
+ - [ ] No secret literal anywhere in source or logs.
55
+ - [ ] The real egress key is attached at the network boundary, not in app code.
56
+ - [ ] The model's context never contains a usable credential, only a substitute.
57
+ - [ ] DB connection string pins `sslmode=verify-full`.
@@ -0,0 +1,78 @@
1
+ ---
2
+ title: Config Typing with Zod
3
+ impact: MEDIUM
4
+ impactDescription: Makes configuration type-safe and validated once at the boundary, so the rest of the system trusts it
5
+ tags: [configuration, typescript, validation]
6
+ ---
7
+
8
+ ## Config Typing with Zod
9
+
10
+ Type and validate all configuration with Zod once, at startup, at the boundary. The domain then depends on a typed `Config` object, never on raw `process.env`. Secrets are read here and nowhere else (see *Secrets at the Network Boundary*).
11
+
12
+ > **Parse, do not scatter `process.env` across the codebase.** One schema, one validation, one typed object.
13
+
14
+ **Implementation:**
15
+
16
+ ```typescript
17
+ // src/config.ts
18
+ import { z } from "zod";
19
+
20
+ const ConfigSchema = z.object({
21
+ env: z.enum(["development", "production", "test"]),
22
+ port: z.coerce.number().positive().default(3000),
23
+
24
+ // Model provider resolved at the boundary, behind a port.
25
+ model: z.object({
26
+ provider: z.string(), // open string, not a vendor enum
27
+ gatewayUrl: z.string().url().optional(),
28
+ model: z.string(),
29
+ }),
30
+
31
+ // Database of record (required). Pin strict TLS.
32
+ database: z.object({
33
+ url: z.string().url(), // ...?sslmode=verify-full&channel_binding=require
34
+ poolSize: z.coerce.number().positive().default(20),
35
+ }),
36
+
37
+ // Optional services: present or absent, never crashing (graceful degradation).
38
+ optional: z.object({
39
+ search: z.boolean().default(false),
40
+ memory: z.boolean().default(false),
41
+ telemetry: z.boolean().default(false),
42
+ }),
43
+
44
+ logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
45
+ });
46
+
47
+ export type Config = z.infer<typeof ConfigSchema>;
48
+
49
+ export const config: Config = ConfigSchema.parse({
50
+ env: process.env.NODE_ENV ?? "development",
51
+ port: process.env.PORT,
52
+ model: {
53
+ provider: process.env.MODEL_PROVIDER ?? "gateway",
54
+ gatewayUrl: process.env.MODEL_GATEWAY_URL,
55
+ model: process.env.MODEL ?? "",
56
+ },
57
+ database: { url: process.env.DATABASE_URL, poolSize: process.env.DB_POOL_SIZE },
58
+ optional: {
59
+ search: !!process.env.SEARCH_API_KEY,
60
+ memory: !!process.env.MEMORY_URL,
61
+ telemetry: !!process.env.TELEMETRY_TOKEN,
62
+ },
63
+ logLevel: process.env.LOG_LEVEL,
64
+ });
65
+ ```
66
+
67
+ **Discipline:**
68
+
69
+ - Provider fields are open strings, not vendor enums. The set of providers is not part of your contract; the port is. Adding a provider must not change this schema.
70
+ - Validation happens once; a malformed required value fails startup loud and early (fail fast). Optional services resolve to a boolean and degrade (fail-open) when absent.
71
+ - The domain imports `config` (typed), never `process.env`. Secrets never travel further than this boundary.
72
+
73
+ **Usage:**
74
+
75
+ ```typescript
76
+ import { config } from "@/config"; // fully typed, already validated
77
+ if (config.optional.search) { /* TypeScript knows the shape */ }
78
+ ```
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: Contract Governance (Versioned, Additive, Expand-then-Contract)
3
+ impact: CRITICAL
4
+ impactDescription: Lets the system evolve without breaking consumers, by governing the contract at the boundary rather than the implementation behind it
5
+ tags: [architecture, contracts, ports, versioning, compatibility]
6
+ ---
7
+
8
+ ## Contract Governance
9
+
10
+ A port is a contract. The whole point of the hexagon is that you can change what is *behind* a port freely, but what *crosses* the port is governed. Break that discipline and the decoupling is cosmetic.
11
+
12
+ > **Behind the port: free. Across the port: governed, versioned, additive.**
13
+
14
+ **Rules of change at a boundary:**
15
+
16
+ 1. **Additive by default.** Add new optional fields; never remove or repurpose an existing one in place.
17
+ 2. **Versioned.** A breaking change is a new version of the contract, not an edit to the old one.
18
+ 3. **Expand-then-contract** (the safe migration of any contract):
19
+ - *Expand*: introduce the new shape alongside the old, both supported.
20
+ - *Migrate*: move producers and consumers over, observed on real traffic.
21
+ - *Contract*: only once nothing reads the old shape, remove it.
22
+ 4. **Consumer-driven.** The consumers' expectations are encoded as tests the producer must satisfy. The contract is owned jointly, not dictated by the producer.
23
+
24
+ **The schema-drift test (executable contract):**
25
+
26
+ The cheapest consumer-driven contract test is one that fails the build when the implementation drifts from the domain contract. In practice the domain `zod` types are the single source of truth, and a test asserts that the live SQL columns match them:
27
+
28
+ ```typescript
29
+ // test/schema-drift.test.ts (runs only when DATABASE_URL is set)
30
+ test("SQL columns match the domain contract", async () => {
31
+ const columns = await db.columnsOf("facts"); // live schema
32
+ const expected = factSchema.keyof().options; // zod domain type
33
+ expect(new Set(columns)).toEqual(new Set(expected.map(toSnakeCase)));
34
+ });
35
+ ```
36
+
37
+ When the database and the domain type disagree, the build goes red before a single request is served. The contract is not a document anyone can forget; it is a test.
38
+
39
+ **The model port is a contract too.** Promoting a new model is the sibling of expand-then-contract: the candidate runs first in *shadow deployment* behind the same port, on real traffic but silent, its behavioural divergence measured against the serving model; only if divergence stays under a pre-set threshold do you shift a growing share of traffic, with immediate rollback on any regression. You never switch in one move.
40
+
41
+ **Incorrect:**
42
+
43
+ ```typescript
44
+ // Renaming a field in place. Every consumer breaks at once.
45
+ interface FactDTO { value: string; /* was: amount */ } // BAD: breaking, unversioned
46
+ ```
47
+
48
+ **Correct:**
49
+
50
+ ```typescript
51
+ // Expand: add the new field, keep the old, mark it deprecated.
52
+ interface FactDTO {
53
+ /** @deprecated use `value` */ amount?: string;
54
+ value: string;
55
+ }
56
+ // Migrate consumers, observe on real traffic, THEN contract (remove `amount`).
57
+ ```
58
+
59
+ **Checklist before changing anything that crosses a boundary:**
60
+
61
+ - [ ] Is the change additive? If not, it is a new version.
62
+ - [ ] Is there a contract test (schema-drift, consumer test) that will catch a regression?
63
+ - [ ] Are the old and new shapes both live during the migration (expand-then-contract)?
64
+ - [ ] For a model swap: shadow first, switch by stages, rollback ready.
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: Reversible Memory Consolidation (Merge, Summarize, Keep Pointers)
3
+ impact: MEDIUM
4
+ impactDescription: Keeps long-term memory compact without destroying evidence, so every consolidated fact can be unfolded back to its sources at audit time
5
+ tags: [data, memory, consolidation, summarization, audit]
6
+ ---
7
+
8
+ ## Reversible Memory Consolidation
9
+
10
+ > **Consolidation is a projection, not a rewrite. You never consolidate the source of truth.**
11
+
12
+ A memory store that only accumulates degrades: near-duplicates crowd out signal, retrieval ranks ten variants of the same fact, and the context budget fills with redundancy. The answer is consolidation — merging near-duplicates, summarizing episodes into higher-level facts — but done naively it destroys the very thing memory exists for: the ability to say *why* the system believed what it believed.
13
+
14
+ The rule is that consolidation must be **reversible**. Two constraints make it so:
15
+
16
+ 1. **The immutable journal is never consolidated.** Consolidation operates on the derived working view only (see *State as an Immutable Journal*). The journal remains the append-only source of truth; a summary is one more derived artifact, never a replacement for what it summarizes.
17
+ 2. **Every consolidated item keeps pointers to its sources.** A merged fact records the ids of the near-duplicates it absorbed; an episode summary records the ids of the events it condenses. Given a consolidated memory, you can always unfold it back to the raw facts.
18
+
19
+ **Incorrect:**
20
+
21
+ ```typescript
22
+ // Destructive consolidation: summarize, then delete the originals.
23
+ async function consolidate(memories: Memory[]): Promise<void> {
24
+ const summary = await summarize(memories);
25
+ await memoryStore.insert(summary);
26
+ await memoryStore.deleteMany(memories.map(m => m.id)); // BAD: evidence gone
27
+ // A month later: "why did the agent think the client wanted X?"
28
+ // The summary says so, the facts behind it no longer exist.
29
+ }
30
+ ```
31
+
32
+ **Correct:**
33
+
34
+ ```typescript
35
+ // Reversible consolidation: the summary points at its sources,
36
+ // the sources are demoted out of retrieval, nothing is destroyed.
37
+ interface ConsolidatedMemory extends Memory {
38
+ kind: 'merged' | 'episode-summary';
39
+ sourceIds: string[]; // pointers to the raw facts
40
+ consolidatedAt: Date;
41
+ }
42
+
43
+ async function consolidate(memories: Memory[]): Promise<ConsolidatedMemory> {
44
+ const summary = await summarize(memories);
45
+ const consolidated = await memoryStore.insert({
46
+ ...summary,
47
+ kind: 'episode-summary',
48
+ sourceIds: memories.map(m => m.id),
49
+ consolidatedAt: new Date(),
50
+ });
51
+ // Sources leave the retrieval index, not the store.
52
+ await memoryStore.markSuperseded(memories.map(m => m.id), consolidated.id);
53
+ return consolidated;
54
+ }
55
+
56
+ // Audit: unfold a consolidated memory to the raw facts as of a decision.
57
+ async function unfold(memoryId: string, asOf: Date): Promise<Memory[]> {
58
+ const memory = await memoryStore.get(memoryId);
59
+ if (!('sourceIds' in memory)) return [memory];
60
+ const sources = await memoryStore.getMany(memory.sourceIds);
61
+ return sources.filter(s => s.createdAt <= asOf);
62
+ }
63
+ ```
64
+
65
+ Two consolidation moves, same discipline for both:
66
+
67
+ - **Near-duplicate merge**: facts above a similarity threshold collapse into one canonical statement carrying the union of their provenance. The duplicates are superseded, not deleted.
68
+ - **Episode summarization**: a bounded slice of interaction events becomes one higher-level fact ("the user prefers weekly reports on Monday"), pointing at the events that support it.
69
+
70
+ **Why this matters:**
71
+
72
+ - **Audit**: when a decision is questioned, you replay it at its timestamp and unfold every consolidated memory that was in context back to the raw facts. A summary that cannot be unfolded is an assertion without evidence.
73
+ - **Correcting bad summaries**: summarization is a model operation and will sometimes be wrong. With pointers, a bad summary is fixed by re-summarizing the same sources; without them, the error is permanent and silently propagates.
74
+ - **Retrieval quality**: superseded items leave the index, so ranking stops splitting relevance across ten variants of one fact — without paying for it in lost history.
75
+
76
+ **Checklist:**
77
+
78
+ - [ ] Consolidation runs on the derived view only; the immutable journal is untouched.
79
+ - [ ] Every merged or summarized memory carries `sourceIds` pointing to its raw facts.
80
+ - [ ] Superseded sources are removed from retrieval, never physically deleted by consolidation.
81
+ - [ ] An `unfold` path can expand any consolidated memory to its sources as of a given timestamp.
@@ -0,0 +1,90 @@
1
+ ---
2
+ title: Bi-Temporal Memory Invalidation (Contradicted Facts Are Dated, Not Deleted)
3
+ impact: MEDIUM
4
+ impactDescription: Lets memory change its mind without lying about the past, by tracking when a fact was learned and when it was valid as two separate timelines
5
+ tags: [data, memory, invalidation, bi-temporal, audit]
6
+ ---
7
+
8
+ ## Bi-Temporal Memory Invalidation
9
+
10
+ > **A contradicted fact is invalidated with a date. It is never deleted.**
11
+
12
+ Facts about the world change: the user moves teams, the client changes their budget, a preference flips. A memory store that handles this by overwriting or deleting the old fact destroys two things at once — the ability to explain past decisions ("it acted on what it knew then"), and the ability to detect that a fact *changed*, which is often information in itself.
13
+
14
+ The structural answer is **bi-temporal** memory. Every fact carries two independent timelines:
15
+
16
+ - **Learned time** (`recordedAt` / `supersededAt`): when the system came to know the fact, and when it stopped treating it as current.
17
+ - **Valid time** (`validFrom` / `validTo`): the period in the world during which the fact held true.
18
+
19
+ The two are different and both matter. "The budget is 50k" may have been *true* from January but only *learned* in March; a decision made in February is judged against what was learned by February, not against what turned out to be true.
20
+
21
+ **Incorrect:**
22
+
23
+ ```typescript
24
+ // Overwrite on contradiction: the past is silently rewritten.
25
+ async function updateFact(subject: string, newValue: string): Promise<void> {
26
+ await memoryStore.update({ subject }, { value: newValue }); // BAD: old value gone
27
+ // "Why did the agent quote 30k in February?" — no way to know
28
+ // the store ever said 30k. The audit trail now contradicts itself.
29
+ }
30
+ ```
31
+
32
+ **Correct:**
33
+
34
+ ```typescript
35
+ interface BiTemporalFact {
36
+ id: string;
37
+ subject: string;
38
+ value: string;
39
+ source: 'user' | 'system' | 'inferred';
40
+ authority: number; // e.g. user-stated > inferred
41
+ recordedAt: Date; // learned time: when we came to know it
42
+ supersededAt: Date | null; // learned time: when we stopped believing it
43
+ validFrom: Date; // valid time: when it became true in the world
44
+ validTo: Date | null; // valid time: when it stopped being true
45
+ }
46
+
47
+ // A contradiction invalidates with a date; nothing is deleted.
48
+ async function assertFact(next: Omit<BiTemporalFact, 'id' | 'supersededAt'>): Promise<void> {
49
+ const current = await memoryStore.findCurrent(next.subject);
50
+ if (current) {
51
+ // Most recently learned wins — unless the standing fact
52
+ // comes from a more authoritative source.
53
+ if (current.authority > next.authority) return; // keep the standing fact
54
+ await memoryStore.invalidate(current.id, {
55
+ supersededAt: next.recordedAt, // we stopped believing it now
56
+ validTo: next.validFrom, // it stopped being true then
57
+ });
58
+ }
59
+ await memoryStore.insert({ ...next, supersededAt: null });
60
+ }
61
+
62
+ // Two distinct questions, two distinct queries.
63
+ const nowView = await memoryStore.query({ supersededAt: null, validTo: null }); // true now
64
+ const asOfT = await memoryStore.query({
65
+ recordedAt: { lte: T }, supersededAtAfterOrNull: T, // what we knew at T
66
+ });
67
+ ```
68
+
69
+ **Resolution order on contradiction:** the most recently learned fact invalidates the older one — *unless* the older fact comes from a more authoritative source (a user's explicit statement outranks a model's inference; a verified system-of-record field outranks both). Authority is compared first, recency second.
70
+
71
+ **Physical deletion stays exceptional.** Two cases only:
72
+
73
+ 1. **Derived data under retention**: projections, caches and indexes rebuilt from the journal may be purged on their TTL (see *TTL by Data Type*).
74
+ 2. **Mandated erasure**: a legal right-to-erasure request removes the data wherever it lives, including the journal, through a documented, non-routine procedure.
75
+
76
+ Everything else is invalidation with a date.
77
+
78
+ **Why this matters:**
79
+
80
+ - **Audit**: "what did the system believe at the time of decision D?" has an exact answer — replay the learned timeline at D. Overwrites make that question unanswerable.
81
+ - **Correct behaviour under change**: the agent can say "your budget was 30k until March, 50k since" instead of flattening history into the latest value.
82
+ - **Safe contradiction handling**: injected or mistaken "corrections" don't destroy the standing fact; they sit next to it, dated and sourced, and can be rolled back by re-invalidating.
83
+
84
+ **Checklist:**
85
+
86
+ - [ ] Every fact carries both learned time and valid time.
87
+ - [ ] Contradictions invalidate with a date; no routine code path deletes facts.
88
+ - [ ] Recency wins on conflict only at equal or higher source authority.
89
+ - [ ] "True now" and "known at time T" are two distinct, supported queries.
90
+ - [ ] Physical deletion is limited to derived data under retention and mandated erasure.
@@ -0,0 +1,154 @@
1
+ ---
2
+ title: Memory Scoring Formula
3
+ impact: MEDIUM
4
+ impactDescription: Provides optimal memory retrieval ranking
5
+ tags: [data, memory, scoring, algorithm]
6
+ ---
7
+
8
+ ## Memory Scoring Formula
9
+
10
+ A comprehensive scoring formula for memory retrieval that balances multiple factors.
11
+
12
+ The weights, decay constant and score thresholds below are a **starting point, not calibrated truth** — recalibrate them against your real traffic (retrieval hit quality, pruning regret) before trusting them.
13
+
14
+ **Complete Formula:**
15
+
16
+ ```typescript
17
+ interface Memory {
18
+ id: string;
19
+ content: string;
20
+ embedding: number[];
21
+ importance: number; // 0-1, user or system assigned
22
+ accessCount: number; // Times retrieved
23
+ lastAccessedAt: Date;
24
+ createdAt: Date;
25
+ source: 'user' | 'system' | 'inferred';
26
+ verified: boolean; // User confirmed accuracy
27
+ }
28
+
29
+ interface ScoringWeights {
30
+ similarity: number;
31
+ recency: number;
32
+ importance: number;
33
+ frequency: number;
34
+ verification: number;
35
+ }
36
+
37
+ const defaultWeights: ScoringWeights = {
38
+ similarity: 0.35,
39
+ recency: 0.25,
40
+ importance: 0.20,
41
+ frequency: 0.10,
42
+ verification: 0.10,
43
+ };
44
+
45
+ function computeMemoryScore(
46
+ memory: Memory,
47
+ queryEmbedding: number[],
48
+ weights = defaultWeights
49
+ ): number {
50
+ // 1. Semantic similarity (cosine). Cosine ranges over [-1, 1];
51
+ // clamp to [0, 1] before weighting so an anti-correlated embedding
52
+ // cannot drag the combined score negative.
53
+ const similarity = Math.max(0, Math.min(1, cosineSimilarity(memory.embedding, queryEmbedding)));
54
+
55
+ // 2. Time decay (exponential): 60-day time constant, i.e. a
56
+ // half-life of ~41.6 days (60 * ln 2). For a literal 60-day
57
+ // half-life, use Math.pow(2, -daysSinceAccess / 60) instead.
58
+ const daysSinceAccess = (Date.now() - memory.lastAccessedAt.getTime()) / DAY_MS;
59
+ const recency = Math.exp(-daysSinceAccess / 60);
60
+
61
+ // 3. Importance (direct from memory)
62
+ const importance = memory.importance;
63
+
64
+ // 4. Access frequency (logarithmic, capped)
65
+ const frequency = Math.min(Math.log10(memory.accessCount + 1) / 2, 1);
66
+
67
+ // 5. Verification bonus
68
+ const verification = memory.verified ? 1 : 0.5;
69
+
70
+ // Weighted combination
71
+ const score =
72
+ similarity * weights.similarity +
73
+ recency * weights.recency +
74
+ importance * weights.importance +
75
+ frequency * weights.frequency +
76
+ verification * weights.verification;
77
+
78
+ return score;
79
+ }
80
+ ```
81
+
82
+ **Context-Aware Weights:**
83
+
84
+ ```typescript
85
+ // Adjust weights based on query type
86
+ function getWeightsForContext(queryType: string): ScoringWeights {
87
+ switch (queryType) {
88
+ case 'factual':
89
+ // Prioritize verified, important memories
90
+ return {
91
+ similarity: 0.30,
92
+ recency: 0.10,
93
+ importance: 0.25,
94
+ frequency: 0.10,
95
+ verification: 0.25,
96
+ };
97
+
98
+ case 'recent':
99
+ // Prioritize recent memories
100
+ return {
101
+ similarity: 0.25,
102
+ recency: 0.40,
103
+ importance: 0.15,
104
+ frequency: 0.10,
105
+ verification: 0.10,
106
+ };
107
+
108
+ case 'exploratory':
109
+ // Balance all factors, favor similarity
110
+ return {
111
+ similarity: 0.40,
112
+ recency: 0.20,
113
+ importance: 0.20,
114
+ frequency: 0.10,
115
+ verification: 0.10,
116
+ };
117
+
118
+ default:
119
+ return defaultWeights;
120
+ }
121
+ }
122
+ ```
123
+
124
+ **Cosine Similarity Implementation:**
125
+
126
+ ```typescript
127
+ function cosineSimilarity(a: number[], b: number[]): number {
128
+ if (a.length !== b.length) {
129
+ throw new Error('Vectors must have same length');
130
+ }
131
+
132
+ let dotProduct = 0;
133
+ let normA = 0;
134
+ let normB = 0;
135
+
136
+ for (let i = 0; i < a.length; i++) {
137
+ dotProduct += a[i] * b[i];
138
+ normA += a[i] * a[i];
139
+ normB += b[i] * b[i];
140
+ }
141
+
142
+ const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
143
+ return magnitude === 0 ? 0 : dotProduct / magnitude;
144
+ }
145
+ ```
146
+
147
+ **Score Interpretation:**
148
+
149
+ | Score Range | Interpretation |
150
+ |-------------|----------------|
151
+ | 0.8 - 1.0 | Highly relevant, should definitely include |
152
+ | 0.6 - 0.8 | Relevant, include if space permits |
153
+ | 0.4 - 0.6 | Marginally relevant, include for deep queries |
154
+ | 0.0 - 0.4 | Low relevance, consider for pruning |
@@ -0,0 +1,55 @@
1
+ ---
2
+ title: Forward-Only, Branch-First Migrations
3
+ impact: HIGH
4
+ impactDescription: Keeps schema change safe and auditable by making migrations additive, ordered, and tested on a branch before main
5
+ tags: [data, migrations, database, schema, safety]
6
+ ---
7
+
8
+ ## Forward-Only, Branch-First Migrations
9
+
10
+ Schema evolution is a migration of state, not a rewrite. Treat migrations like contract changes: additive, ordered, reversible by rolling forward, and proven on a branch before they touch production data.
11
+
12
+ > **Forward-only, additive, tested on a branch, then main.**
13
+
14
+ **Rules:**
15
+
16
+ 1. **Versioned and contiguous.** Files are numbered `0001`, `0002`, ... A runner applies them in order and refuses a gap or a re-order (anti-skip guard).
17
+ 2. **Forward-only.** No destructive down-migration in normal operation; you fix forward with a new migration. Removal of a column is the *contract* phase of expand-then-contract, run only once nothing reads it.
18
+ 3. **Branch-first.** Run the migration on a throwaway branch (Neon copy-on-write costs ~0), verify the schema and the schema-drift test, keep the branch as a return net, then apply to main.
19
+ 4. **Idempotent seeds.** Seed with `on conflict (id) do nothing` so re-running is safe.
20
+
21
+ **Migration runner guard (the order is enforced, not trusted):**
22
+
23
+ ```typescript
24
+ // scripts/migrate.ts - transactional, contiguity + anti-skip
25
+ const applied = await db.appliedMigrations(); // e.g. [1, 2]
26
+ const onDisk = listMigrations(); // e.g. [1, 2, 3]
27
+ assertContiguous(onDisk); // no gap 1,3 without 2
28
+ assertNoReorder(applied, onDisk); // applied prefix unchanged
29
+ for (const m of onDisk.filter(m => !applied.includes(m.n))) {
30
+ await db.tx(async (t) => { await t.run(m.sql); await t.recordApplied(m.n); });
31
+ }
32
+ ```
33
+
34
+ **Incorrect:**
35
+
36
+ ```sql
37
+ -- Editing an applied migration in place, or dropping a live column.
38
+ ALTER TABLE facts DROP COLUMN value; -- BAD: breaks readers, irreversible
39
+ ```
40
+
41
+ **Correct:**
42
+
43
+ ```sql
44
+ -- 0007_add_period.sql (additive, forward-only)
45
+ ALTER TABLE facts ADD COLUMN period text; -- expand
46
+ -- backfill in the same or a later migration; readers move over;
47
+ -- only later, 0009_drop_legacy.sql removes what nothing reads (contract).
48
+ ```
49
+
50
+ **Checklist:**
51
+
52
+ - [ ] Migrations are numbered, contiguous, applied in order by a runner.
53
+ - [ ] No edit to an already-applied migration; fix forward.
54
+ - [ ] Proven on a branch (schema-drift green) before main.
55
+ - [ ] Column removal is the contract phase, after readers have moved.