From c124065690050670d2f80930e7d5d801dfcdeedf Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 13 May 2026 08:29:08 -0700 Subject: [PATCH 01/13] docs: add runtime truth standards audit --- docs/INFRASTRUCTURE_DOCTRINE.md | 337 +++++++++----- .../2026-05-13_runtime-truth-code-standard.md | 429 ++++++++++++++++++ .../bad-code/CORE_boundary-codec-cutover.md | 28 ++ .../bad-code/CORE_hexagonal-store-boundary.md | 30 ++ .../CORE_mcp-service-dependency-injection.md | 28 ++ .../CORE_runtime-domain-model-cutover.md | 28 ++ .../CORE_runtime-truth-standard-ratchet.md | 28 ++ .../SURFACE_browse-tui-strict-limits.md | 30 ++ ...SURFACE_macos-appstate-composition-root.md | 24 + 9 files changed, 839 insertions(+), 123 deletions(-) create mode 100644 docs/audit/2026-05-13_runtime-truth-code-standard.md create mode 100644 docs/method/backlog/bad-code/CORE_boundary-codec-cutover.md create mode 100644 docs/method/backlog/bad-code/CORE_hexagonal-store-boundary.md create mode 100644 docs/method/backlog/bad-code/CORE_mcp-service-dependency-injection.md create mode 100644 docs/method/backlog/bad-code/CORE_runtime-domain-model-cutover.md create mode 100644 docs/method/backlog/bad-code/CORE_runtime-truth-standard-ratchet.md create mode 100644 docs/method/backlog/bad-code/SURFACE_browse-tui-strict-limits.md create mode 100644 docs/method/backlog/bad-code/SURFACE_macos-appstate-composition-root.md diff --git a/docs/INFRASTRUCTURE_DOCTRINE.md b/docs/INFRASTRUCTURE_DOCTRINE.md index 5329bd1..fda63c4 100644 --- a/docs/INFRASTRUCTURE_DOCTRINE.md +++ b/docs/INFRASTRUCTURE_DOCTRINE.md @@ -1,201 +1,292 @@ -# How to write TypeScript infrastructure that *actually* lasts. +# Runtime Truth Infrastructure Doctrine -This is the authoritative doctrine for Think. It is a refined, battle-tested version of the original "Runtime Truth Wins" philosophy. Infrastructure code (persistence, replication, crypto, conflict resolution, migrations, audit logs) cannot afford weak assumptions. They create long-lived, expensive bugs. +This is the authoritative engineering doctrine for Think. It applies to +all long-lived infrastructure code: capture, storage, graph repair, +replication, codecs, migrations, CLI/MCP surfaces, and macOS adapters. ---- +Existing violations are debt, not precedent. New code must move the repo +toward this standard. -### Rule 0: Runtime Truth Wins (Non-Negotiable) +## Rule 0: Runtime Truth Wins When the program is running, only one question matters: **What is actually true right now, in memory, under execution?** -Everything else — types, comments, tests, design docs — is secondary. If they disagree with runtime reality, they are lying. Fix the reality first, then update the documentation. +Types, tests, docs, comments, schemas, and generated artifacts are +secondary documentation. If they disagree with runtime behavior, they are +lying. Fix the runtime first, then update every layer that describes it. -**Hierarchy of Truth** +Truth hierarchy: -``` -1. Runtime (constructors, invariants, methods, errors) -2. Boundary parsers & schemas -3. Tests (executable specification) -4. TypeScript types (checked documentation) -5. IDE / static analysis -6. Design docs & comments -``` +1. Runtime model: constructors, invariants, methods, errors, live state. +2. Boundary parsers, schemas, and codecs. +3. Tests as executable specification. +4. Static types and IDE analysis. +5. Design docs, comments, and diagrams. + +## Core Philosophy + +- Truth-seeking over cleverness. +- Explicit, boring, and robust. +- Immutability by default. +- Hexagonal architecture and dependency injection are mandatory. +- Portability is a first-class feature. +- Code should stay small, focused, and human-scale. -TypeScript is #4 — a powerful servant, never the master. +Infrastructure should feel like a well-engineered, inspectable, +long-lived machine, not clever glue code. ---- +## Mandatory Architecture Rules -### Core Philosophy +### 1. Hexagonal Architecture: Ports And Adapters -- Prioritize **truth-seeking** over cleverness. -- Favor **boring, explicit, and robust**. -- Default to **immutability**. -- Treat **portability as a feature** (browser-first mindset). -- Make correctness cheap; performance comes after. +Core domain logic must not depend on host APIs, external libraries with +side effects, protocol transports, concrete storage engines, process +state, environment variables, clocks, random number generators, or UI +frameworks. ---- +All external capabilities enter through ports. Adapters implement those +ports for specific environments. -### Language Policy +Examples of capabilities that require ports: -**TypeScript is the primary language.** Strong IDE support and ecosystem make it the right default. +- Time. +- Randomness. +- Hostname and process metadata. +- Filesystem access. +- Git and WARP access. +- Network or upstream backup. +- CLI, MCP, macOS, and browser I/O. +- Encoding, decoding, and content storage. -**Banned without mercy:** -- `any` -- `unknown` escaping boundaries -- Type assertions (`as`) -- `enum` -- `throw new Error("string")` -- Magic numbers & strings -- Boolean trap parameters -- Anonymous option bags in public APIs +### 2. Dependency Injection -**Encouraged:** -- Classes for domain concepts with invariants or behavior -- `readonly` + `private` fields + `Object.freeze()` -- Branded classes for cross-realm safety -- Rust → Wasm when TypeScript is insufficient (performance, memory safety, hostile parsing) +Dependencies are injected through constructors or semantically named +options objects. Core code must not instantiate concrete adapters, import +adapter modules, read globals, or use service locators. -**Canonical Boundary Pattern** +Allowed: ```typescript -function parsePatchFromWire(bytes: Uint8Array): PatchV2 { - const raw = cborDecode(bytes); // untrusted - return PatchV2.fromDecoded(raw); // validates + constructs trusted domain object -} +const service = new CaptureService({ + clock, + ids, + thoughtStore, + backupPort, +}); +``` -function applyPatch(patch: PatchV2): Result { ... } +Forbidden in core: + +```typescript +const store = new GitWarpStore(process.env.THINK_REPO_DIR); +const now = new Date(); +const id = crypto.randomUUID(); ``` ---- +Composition roots are the exception. Binaries, CLI entrypoints, MCP +servers, app delegates, and test fixtures may construct concrete +adapters and inject them inward. + +### 3. Encoding And Decoding Only At Boundaries + +Serialization, deserialization, and codec work happen only in adapters or +dedicated boundary codec ports. Core works with validated runtime domain +objects. + +Allowed boundary pattern: + +```typescript +function parsePatchFromWire(bytes: Uint8Array): Patch { + const decoded = patchCodec.decode(bytes); + return Patch.fromDecoded(decoded); +} + +function applyPatch(patch: Patch): PatchOutcome { + return patch.applyTo(state); +} +``` -### Architecture +Forbidden in core: -**Hexagonal (Ports & Adapters) — Mandatory** +```typescript +const decoded = JSON.parse(value); +const text = Buffer.from(payload, 'base64').toString('utf8'); +``` -Core domain logic must never depend on host-specific APIs (Node globals, `fs`, `Buffer`, `process`, etc.). All external concerns go behind clean ports. +## Object Model And Modeling Rules -**Browser-First Mindset** +Prefer classes with constructors for domain concepts. -Prefer web-standard primitives: -- `Uint8Array`, `TextEncoder`, `URL`, `crypto.subtle` -- Keep core logic portable across browsers, Node, Deno, and workers. +Value objects, entities, outcomes, and domain errors should normally be +runtime-backed classes. This gives Think: ---- +- Invariant enforcement at construction time. +- Natural `instanceof` dispatch inside one realm. +- RAII-style initialization. +- Better protection through private fields, readonly fields, and + `Object.freeze()`. -### Object Model – The Four Pillars +The lighter "interface plus factory plus brand" pattern is discouraged +for domain modeling. It is allowed only for: -1. **Value Objects** — Invariant-rich, immutable, equality by value -2. **Entities** — Identity + lifecycle -3. **Outcomes / Results** — Rich classes (preferred over tagged unions when behavior differs) -4. **Domain Errors** — Typed, contextual, first-class +- Pure wire or DTO types. +- Extremely hot-path primitives where allocation measurably matters. +- Deliberate structural typing at external boundaries. -**Example: Value Object** +Preferred style: ```typescript -class EventId { +export class EventId { readonly writerId: WriterId; readonly lamport: Lamport; - private readonly brand = Symbol.for('grok.EventId'); - constructor(writerId: string, lamport: number) { this.writerId = WriterId.from(writerId); this.lamport = Lamport.from(lamport); Object.freeze(this); } + static from(writerId: string, lamport: number): EventId { + return new EventId(writerId, lamport); + } + static is(value: unknown): value is EventId { - return value instanceof EventId - || (value != null && (value as any)[EventId.prototype.brand] === true); + return value instanceof EventId; } equals(other: EventId): boolean { - return this.writerId.equals(other.writerId) && this.lamport === other.lamport; + return this.writerId.equals(other.writerId) + && this.lamport.equals(other.lamport); } } ``` -**Preferred Outcomes** +For cross-realm values, normalize through adapters and construct +validated domain objects before entering core. -```typescript -class OpApplied { ... } -class OpSuperseded { ... } +## Strict Code Limits -// Clean polymorphic dispatch -if (outcome instanceof OpSuperseded) { ... } -``` +These limits are mandatory for new and touched infrastructure code. The +current codebase has legacy violations; those must be tracked and reduced +with a ratchet until the limits are hard CI gates everywhere. ---- +| Limit | Maximum | +| --- | ---: | +| File size | 1000 lines, aim below 600 | +| Function or method size | 35 non-blank, non-comment lines | +| Nesting depth | 4 | +| Cyclomatic complexity | 8 | +| Parameters | 5, otherwise use a named options class or object | +| Class size | 400 lines | +| Public methods per class | 15 | +| Imports per file | 12 | -### Principles +Each file should have one primary domain concept. If a file needs more +than one primary concept, it is probably hiding a boundary or ownership +problem. -**P1: Domain Concepts Demand Runtime Forms** -If it has invariants, identity, or behavior — give it a class. +## Language Policy -**P2: Validation at Construction & Boundaries** -Constructors are synchronous and establish invariants or throw. Raw data becomes trusted only here. +Banned without exception: -**P3: Behavior Belongs on the Owner** -Prefer polymorphism over type-tag switching. +- `any`. +- Unvalidated `unknown` escaping a boundary. +- Type assertions with `as`. +- `enum`. +- Generic `throw new Error("string")`. +- Magic numbers and strings. +- Boolean trap parameters. +- Anonymous option bags in public APIs. -**P4: Schemas Are Boundary Guards Only** -Use Zod (or similar) at system edges. Keep domain classes clean. +`unknown` is allowed at external boundaries and parsers only. It must be +validated and normalized before entering core. -**P5: Serialization Is Codec Territory** -Domain objects should not know about JSON, CBOR, protobuf, etc. +Encouraged: -**P6: Immutability by Default** -Trusted objects should be difficult to mutate after construction. Use `readonly`, `freeze`, and return new values for transformations. +- Constructor-based validation. +- Domain-specific error classes. +- `readonly`, `private`, and `Object.freeze()`. +- Polymorphism over type-tag switching. +- Web-standard primitives where possible: `Uint8Array`, `TextEncoder`, + `TextDecoder`, `URL`, and `crypto.subtle`. -**P7: Determinism & Replayability** -- All time comes from `ClockPort` -- All randomness from `RandomPort` -- All side effects through ports -Your core should be deterministic and replayable. +## flyingrobots Principles -**P8: Single Source of Truth** -The runtime model rules. Types, tests, and docs document it. +P1. Domain concepts with invariants or behavior deserve runtime-backed +classes. -**P9: Runtime Dispatch When Appropriate** -`instanceof` is excellent inside the same realm. Use branding + `static is()` for cross-realm (workers, iframes). +P2. Validation happens at construction and system boundaries. ---- +P3. Behavior belongs on the type that owns it. -### Practices +P4. Schemas such as Zod are boundary guards only. -- One meaningful class or export per file, named after the concept. -- Parameter objects must have semantic meaning. -- Branch on error types, never `err.message`. -- Prefer composition over deep inheritance. -- No floating promises. -- Raw plain objects are for transport/logging only — not for domain meaning. +P5. Encoding and decoding are codec or adapter territory. ---- +P6. Immutability is the default. -### Anti-Patterns I Strongly Dislike +P7. Determinism and replayability require ports for time, randomness, +side effects, and host state. -- Shape soup (giant unions + endless type guards) -- God classes -- Leaking host APIs into core -- Treating types as the source of truth -- Parsing error messages like a raccoon in a dumpster +P8. The runtime model is the single source of truth. ---- +P9. Use `instanceof` for same-realm dispatch. For cross-realm values, +normalize at the boundary before constructing domain types. -### Review Checklist (Before Merging) +## Review Checklist + +Every PR must answer these questions: + +- Does it follow hexagonal architecture? +- Are dependencies injected rather than discovered from globals? +- Is encoding and decoding limited to boundaries? +- Are files, functions, nesting, complexity, and parameter counts within + limits? +- Are important domain concepts modeled as classes with constructor + validation? +- Are invariants protected at runtime? +- Are `any`, unsafe assertions, and unvalidated `unknown` absent? +- Could the core run in a browser or worker? +- Are time, randomness, and side effects abstracted? +- Is runtime behavior the source of truth when docs or types disagree? + +## ESLint Direction + +When TypeScript enforcement is available, use the following shape as the +baseline. JavaScript and Swift code must meet the same design standard +even when the exact linter differs. + +```json +{ + "rules": { + "max-lines": ["error", 1000], + "max-lines-per-function": ["error", { "max": 35, "skipBlankLines": true, "skipComments": true }], + "max-depth": ["error", 4], + "max-params": ["error", 5], + "complexity": ["error", 8], + "max-statements": ["error", 25], + + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-unsafe-assignment": "error", + "@typescript-eslint/no-unsafe-member-access": "error", + "@typescript-eslint/no-unsafe-return": "error", + "@typescript-eslint/no-unsafe-call": "error", + "@typescript-eslint/only-throw-error": "error", + "@typescript-eslint/switch-exhaustiveness-check": "error", + "no-floating-promises": "error" + } +} +``` -- Does every important domain concept have a runtime-backed class? -- Any `any`, `unknown`, or `as` sneaking in? -- Are invariants enforced at construction time? -- Does behavior live on the owning type? -- Could this core logic run in a browser? -- Are time, randomness, and side effects properly abstracted? -- Are we mutating trusted domain objects? +## Exceptions ---- +An exception must be explicit, local, and temporary: -**This is infrastructure.** It should feel like building a reliable, inspectable machine — not gluing components together with hope. +- State the runtime reason. +- Link the backlog item that removes it. +- Add regression coverage for the behavior being protected. +- Do not use a legacy violation as precedent for new code. -**Runtime truth wins.** Types are there to help you stay honest, not to replace reality. +The standard exists because this code is meant to last. diff --git a/docs/audit/2026-05-13_runtime-truth-code-standard.md b/docs/audit/2026-05-13_runtime-truth-code-standard.md new file mode 100644 index 0000000..3ee2266 --- /dev/null +++ b/docs/audit/2026-05-13_runtime-truth-code-standard.md @@ -0,0 +1,429 @@ +# AUDIT: Runtime Truth Code Standard Baseline (2026-05-13) + +## Scope + +This audit applies the updated `docs/INFRASTRUCTURE_DOCTRINE.md` to the +current Think codebase after PR #14 was merged to `main`. + +The audit used the Git index as the repository manifest, not an ad hoc +filesystem walk. + +Code inventory from `git ls-files`: + +| Area | Count | +| --- | ---: | +| JavaScript files | 116 | +| MJS scripts | 2 | +| Swift files | 36 | +| TypeScript files | 0 | +| Source and scripts | 94 | +| Tests | 55 | +| Benchmarks | 3 | + +## Commands Used + +```sh +git ls-files '*.js' '*.mjs' '*.cjs' '*.ts' '*.tsx' '*.swift' +git ls-files '*.[jt]s' '*.mjs' '*.cjs' '*.ts' '*.tsx' '*.swift' | xargs wc -l +npx eslint . \ + --rule 'max-lines:["error",1000]' \ + --rule 'max-lines-per-function:["error",{"max":35,"skipBlankLines":true,"skipComments":true}]' \ + --rule 'max-depth:["error",4]' \ + --rule 'max-params:["error",5]' \ + --rule 'complexity:["error",8]' \ + --rule 'max-statements:["error",25]' \ + --format json +rg -n 'throw new Error\(' src bin scripts --glob '*.{js,mjs}' +rg -n "from 'node:(fs|fs/promises|child_process|os|path|url)'|from '@git-stunts|from '@modelcontextprotocol|from 'zod'" src bin scripts --glob '*.{js,mjs}' +swift test --package-path macos --list-tests +``` + +`swift test --package-path macos --list-tests` built the Swift package and +listed the macOS tests successfully. + +## Executive Summary + +Think already has strong runtime-truth instincts: explicit domain error +classes, immutable model objects in important paths, focused test +coverage, and real port seams in parts of the macOS adapter and store. + +The repo does not yet meet the updated doctrine as a hard standard. The +highest-risk gaps are architectural, not cosmetic: + +- Core-ish store modules still import concrete WARP/Git/Node adapters. +- CLI and MCP services discover process state directly instead of + receiving dependencies from composition roots. +- Codec and serialization work is mixed into store/domain modules. +- Large command and TUI functions exceed the strict complexity limits by + large margins. +- The linter does not yet enforce the new doctrine; a ratchet is needed + before the limits can become a hard CI gate. + +This audit is a baseline. It should drive backlog work, not normalize the +violations. + +## Mechanical Limit Results + +Applying the proposed strict JavaScript ESLint size and complexity rules +without committing them produced: + +| Metric | Count | +| --- | ---: | +| Files with violations | 47 | +| Total violations | 195 | +| Source/script/benchmark files with violations | 36 | +| Source/script/benchmark violations | 150 | +| Test files with violations | 11 | +| Test violations | 45 | + +Violations by rule: + +| Rule | Total | +| --- | ---: | +| `max-lines-per-function` | 93 | +| `complexity` | 59 | +| `max-statements` | 28 | +| `max-depth` | 11 | +| `max-params` | 3 | +| `max-lines` | 1 | + +Largest files: + +| Lines | File | Status | +| ---: | --- | --- | +| 1537 | `test/acceptance/read-modes.test.js` | Violates hard 1000-line limit | +| 934 | `src/cli/commands/read.js` | Below hard limit, above 600-line target | +| 813 | `scripts/repair-v17-mind.mjs` | Below hard limit, above 600-line target | +| 679 | `test/acceptance/graph-migration.test.js` | Below hard limit, above 600-line target | +| 567 | `src/store/runtime.js` | Near 600-line target | + +Highest-density strict-rule hotspots: + +| Violations | File | Main issue | +| ---: | --- | --- | +| 17 | `src/cli/commands/read.js` | Command orchestration, TUI launch, browse script handling | +| 17 | `src/splash-shader.js` | Rendering math and frame composition in one module | +| 15 | `test/acceptance/read-modes.test.js` | Oversized acceptance fixture suite | +| 9 | `src/store/queries.js` | Read/query branching and traversal complexity | +| 8 | `scripts/repair-v17-mind.mjs` | Repair workflow orchestration in one script | +| 8 | `src/cli/options.js` | Parsing and validation complexity | +| 8 | `src/store/runtime.js` | Concrete WARP runtime orchestration | +| 5 | `src/store/enrichment/runner.js` | Multi-artifact enrichment pipeline | +| 4 | `src/store/migrations.js` | Graph migration orchestration | + +## Findings + +### F1. Doctrine is documented, but enforcement is not active + +Severity: High + +Evidence: + +- `eslint.config.js` currently enforces many safety rules, but not the + new size/complexity limits. +- The TypeScript-specific rules in the doctrine cannot run because the + repository currently has no TypeScript source or `@typescript-eslint` + setup. +- The strict JavaScript profile reports 195 current violations. + +Impact: + +The standard is now normative, but CI will not stop new drift unless the +repo adds a ratchet. Without a ratchet, new work can add more violations +while old debt remains invisible. + +Required direction: + +- Add a ratcheted strict-limits report. +- Fail new or worsened violations first. +- Convert ratchet to full hard gate after the hotspots are split. + +Backlog: + +- `CORE_runtime-truth-standard-ratchet` + +### F2. Store modules are not clean hexagonal core + +Severity: High + +Evidence: + +- `src/store/runtime.js` imports `@git-stunts/plumbing` and + `@git-stunts/git-warp` directly. +- `src/store/checkpoint-state.js` imports concrete WARP/Git adapters + directly. +- `src/store/model.js` imports `node:crypto`, `node:os`, reads + `process.env.THINK_TEST_NOW`, and uses `crypto.randomUUID()` through a + default global port object. +- `src/store/queries.js` defaults `cwd = process.cwd()`. +- `src/store/prompt-metrics.js` imports `node:fs/promises`. + +Impact: + +The current store layer is partly domain and partly adapter. It works, +but it cannot honestly claim browser-portable core semantics. Runtime +truth is anchored to Node and WARP concrete implementations too early. + +Required direction: + +- Define explicit ports for WARP graph access, content storage, clocks, + random IDs, host metadata, prompt metrics storage, and ambient project + context. +- Move concrete WARP/Git/Node adapters into composition roots. +- Keep store/domain functions operating on injected ports and runtime + domain objects. + +Backlog: + +- `CORE_hexagonal-store-boundary` + +### F3. MCP service uses globals and direct imports instead of DI + +Severity: High + +Evidence: + +- `src/mcp/service.js` imports `paths`, `git`, `policies`, + `project-context`, and the store facade directly. +- `captureThought()` reads `process.cwd()` during capture and follow-up. +- `rememberThoughtsForMcp()` defaults `cwd = process.cwd()`. +- `src/mcp/server.js` constructs `McpServer` directly and wires service + functions by import instead of receiving a service object. + +Impact: + +MCP behavior is hard to instantiate with alternate storage, alternate +project context, or browser/worker-style boundaries. The Zod schemas are +correctly boundary-shaped, but the service itself is still a composition +root and domain workflow mixed together. + +Required direction: + +- Introduce a `ThinkMcpService` or equivalent class whose constructor + receives the capture, read, health, migration, path, and backup ports. +- Keep Zod schemas in `server.js`. +- Let `server.js` map validated wire input to domain service calls. + +Backlog: + +- `CORE_mcp-service-dependency-injection` + +### F4. Encoding and decoding still leak past boundaries + +Severity: Medium + +Evidence: + +- `src/store/content.js` encodes text with `Buffer.from()`. +- `src/store/model.js` includes `parseJsonArray()`. +- `src/store/derivation.js` writes and reads prompt-family lists as JSON + strings in graph properties. +- `src/store/checkpoint-read.js` decodes content bytes into text while + also presenting read-model behavior. +- Swift adapter code such as `ThinkCLIAdapter` correctly decodes JSONL at + the boundary, but the JavaScript store has not achieved the same split. + +Impact: + +Codec choices are embedded in store logic. That makes future browser +support and alternate storage harder, and it weakens the rule that core +works only with validated runtime objects. + +Required direction: + +- Introduce codec ports for text content, graph property payloads, and + checkpoint content reads. +- Keep JSON/Buffer/TextDecoder work in adapters. +- Pass rich domain objects into store workflows. + +Backlog: + +- `CORE_boundary-codec-cutover` + +### F5. Domain modeling is partially class-backed but still shape-heavy + +Severity: Medium + +Evidence: + +- Good class-backed anchors exist: `Entry`, `ReflectSession`, + `CaptureProvenance`, `ThinkError` subclasses, runtime read entries, MCP + outcome classes, and Swift value structs/protocols. +- Many store/query functions still return plain frozen objects with + implicit contracts. +- MCP and browse workflows frequently move broad object shapes across + layers rather than named domain outcomes. +- Graph property records are used as untyped shape maps until late in the + flow. + +Impact: + +Runtime truth is present but uneven. Important state often exists as +shape conventions rather than constructor-validated domain objects. + +Required direction: + +- Promote capture results, graph status, remember scopes, browse windows, + migration outcomes, and prompt metrics into runtime-backed classes or + equivalent Swift value types. +- Keep DTOs at boundaries only. + +Backlog: + +- `CORE_runtime-domain-model-cutover` + +### F6. Generic errors still appear in source + +Severity: Medium + +Evidence: + +`rg -n 'throw new Error\(' src bin scripts --glob '*.{js,mjs}'` finds +nine source occurrences, including: + +- `src/minds.js` +- `src/store/content-reader.js` +- `src/store/model.js` +- `src/store/ports.js` +- `src/store/checkpoint-product-read.js` +- `src/store/checkpoint-state.js` + +Impact: + +Generic errors make runtime dispatch depend on message text or broad +`instanceof Error` checks. That is weaker than domain-specific error +classes with stable codes. + +Required direction: + +- Replace generic source errors with domain-specific error classes. +- Port base classes should throw typed `PortNotImplementedError` or avoid + callable abstract methods in JavaScript. + +Backlog: + +- `CORE_runtime-domain-model-cutover` + +### F7. Browse/TUI code has the largest human-scale violations + +Severity: Medium + +Evidence: + +- `src/cli/commands/read.js` is 934 lines and has 17 strict-rule + violations. +- `src/browse-tui/actions.js` has `applyBrowseAction()` at 284 lines + with complexity 58. +- `src/browse-tui/app.js` still owns terminal I/O and animation + orchestration. +- Existing backlog already identifies `SURFACE_splash-monolith` and + `SURFACE_mind-switch-loop-in-command`. + +Impact: + +The browse surface is functionally covered but hard to inspect and +extend. It is the most obvious place where "human-scale code" is not yet +true. + +Required direction: + +- Split command orchestration from pure browse state transitions. +- Keep terminal I/O in adapters. +- Move action handling into small command objects or state-machine + reducers. + +Backlog: + +- `SURFACE_browse-tui-strict-limits` +- Existing: `SURFACE_splash-monolith` +- Existing: `SURFACE_mind-switch-loop-in-command` + +### F8. Swift adapter is closer to hexagonal shape, but app state is a +composition hotspot + +Severity: Medium + +Evidence: + +- Positive: `ThinkCLIAdapter` receives `ProcessRunning` and + `ThinkCLICommand`; `ThinkMCPAdapter` receives `MCPTransport`; + command resolvers receive path-search dependencies. +- Positive: many Swift domain-ish values are `struct`, `Equatable`, and + `Sendable`. +- Gap: `CaptureAppState.makeClient()` constructs concrete MCP/CLI + adapters directly. +- Gap: `CaptureAppState.restartToLoadLatestBuild()` constructs + `Process()` directly. +- Gap: `CaptureAppState` is 353 lines and coordinates UI state, + notifications, metrics, retry behavior, update polling, and process + restart. + +Impact: + +The macOS surface has good adapter seams but still centralizes too much +runtime orchestration in one app-state class. + +Required direction: + +- Introduce explicit app composition root/factory objects. +- Inject process restart and build update ports. +- Split capture retry orchestration from menu-bar app state. + +Backlog: + +- `SURFACE_macos-appstate-composition-root` + +## Positive Compliance Anchors + +- `docs/INFRASTRUCTURE_DOCTRINE.md` is already referenced by + `AGENTS.md` as mandatory. +- `src/store/ports.js` establishes Clock, Host, and Random port names, + even though the implementation needs stronger boundaries. +- `src/store/model.js`, `src/capture-provenance.js`, `src/errors.js`, + and `src/mcp/result.js` show class-backed runtime modeling. +- `Object.freeze()` appears heavily in store and MCP paths. +- Swift adapters already use protocols for process and MCP transport. +- Existing tests exercise runtime behavior across CLI, MCP, store, and + macOS adapters. + +## Ratchet Plan + +Phase 1: Documentation and measurement. + +- Completed in this audit. +- Keep the strict ESLint profile as a reproducible command until it + becomes a committed ratchet. + +Phase 2: Enforcement ratchet. + +- Add a strict-limits report to CI. +- Fail only new or worsened violations at first. +- Track full violation count in a committed baseline. + +Phase 3: Boundary cleanup. + +- Move concrete WARP/Git/Node dependencies out of store core. +- Inject environment, cwd, path, time, randomness, and storage ports. +- Keep schemas and codecs at CLI/MCP/storage boundaries. + +Phase 4: Runtime model hardening. + +- Replace major plain-object contracts with domain classes or Swift value + types. +- Replace generic errors with typed domain errors. + +Phase 5: Full gate. + +- Turn strict code limits into hard CI failures. +- Add TypeScript-specific unsafe-type rules when the repo has TypeScript + source. +- Add Swift lint or a Swift-native equivalent for size and complexity + gates. + +## Bottom Line + +Think is directionally aligned with Runtime Truth, but it is not yet +structurally compliant with the stricter doctrine. The next real quality +cut is not cosmetic linting. It is moving store/MCP boundaries behind +ports, injecting dependencies from composition roots, and converting +shape-heavy workflows into constructor-validated runtime domain models. diff --git a/docs/method/backlog/bad-code/CORE_boundary-codec-cutover.md b/docs/method/backlog/bad-code/CORE_boundary-codec-cutover.md new file mode 100644 index 0000000..8db7f00 --- /dev/null +++ b/docs/method/backlog/bad-code/CORE_boundary-codec-cutover.md @@ -0,0 +1,28 @@ +--- +id: CORE_boundary-codec-cutover +blocks: [] +blocked_by: + - CORE_hexagonal-store-boundary +--- + +# Encoding and decoding are mixed into store logic + +The Runtime Truth doctrine requires serialization and codec work to stay +at boundaries. Current store code still performs text, JSON, and content +payload encoding directly in domain-adjacent modules. + +Examples include `Buffer.from()` in `src/store/content.js`, +`parseJsonArray()` in `src/store/model.js`, JSON-string graph +properties in derivation flows, and content byte decoding in checkpoint +read models. + +## Acceptance Criteria + +- Introduce named codec ports for text content, graph property payloads, + and checkpoint content reads. +- Keep `Buffer`, `TextEncoder`, `TextDecoder`, JSON parsing, and future + CBOR work in adapters or codec modules only. +- Core store workflows receive validated domain objects, not raw decoded + transport shapes. +- Existing content and checkpoint-read tests pass through the new codec + ports. diff --git a/docs/method/backlog/bad-code/CORE_hexagonal-store-boundary.md b/docs/method/backlog/bad-code/CORE_hexagonal-store-boundary.md new file mode 100644 index 0000000..c67a9c5 --- /dev/null +++ b/docs/method/backlog/bad-code/CORE_hexagonal-store-boundary.md @@ -0,0 +1,30 @@ +--- +id: CORE_hexagonal-store-boundary +blocks: [] +blocked_by: + - CORE_runtime-truth-standard-ratchet +--- + +# Store core imports concrete Node, Git, and WARP adapters + +The store layer is currently both domain logic and infrastructure +adapter. Examples include direct imports of `@git-stunts/git-warp`, +`@git-stunts/plumbing`, `node:crypto`, `node:os`, `node:fs/promises`, +and ambient `process` reads inside `src/store/*`. + +That violates the Runtime Truth doctrine: core behavior should operate +on injected ports and runtime domain objects, not on concrete host APIs. + +## Acceptance Criteria + +- Define explicit ports for graph persistence, content storage, clocks, + random IDs, host metadata, prompt metrics storage, and ambient project + context. +- Move concrete WARP/Git/Node implementations into adapters or + composition roots. +- Store workflows accept ports through constructors or named options + objects. +- Store/domain modules no longer import Node host APIs or + `@git-stunts/*` concrete adapters directly. +- Add at least one browser-like unit test that runs core store logic + without Node filesystem, process, or Git globals. diff --git a/docs/method/backlog/bad-code/CORE_mcp-service-dependency-injection.md b/docs/method/backlog/bad-code/CORE_mcp-service-dependency-injection.md new file mode 100644 index 0000000..a3c8264 --- /dev/null +++ b/docs/method/backlog/bad-code/CORE_mcp-service-dependency-injection.md @@ -0,0 +1,28 @@ +--- +id: CORE_mcp-service-dependency-injection +blocks: [] +blocked_by: + - CORE_hexagonal-store-boundary +--- + +# MCP service is a composition root instead of an injected service + +`src/mcp/service.js` imports paths, Git operations, policies, project +context, and store functions directly. `src/mcp/server.js` imports those +service functions and registers them with concrete MCP transport +schemas. + +This makes the MCP surface hard to instantiate with alternate storage, +alternate project context, or non-Node hosts. + +## Acceptance Criteria + +- Introduce a `ThinkMcpService` or equivalent runtime-backed service + class. +- The service constructor receives capture, read, migration, health, + backup, path, and project-context ports. +- `src/mcp/server.js` owns Zod schemas and MCP registration only. +- Wire input is decoded at the MCP boundary and normalized before it + enters the service. +- Tests can instantiate the MCP service with in-memory ports and no + process globals. diff --git a/docs/method/backlog/bad-code/CORE_runtime-domain-model-cutover.md b/docs/method/backlog/bad-code/CORE_runtime-domain-model-cutover.md new file mode 100644 index 0000000..dc528d1 --- /dev/null +++ b/docs/method/backlog/bad-code/CORE_runtime-domain-model-cutover.md @@ -0,0 +1,28 @@ +--- +id: CORE_runtime-domain-model-cutover +blocks: [] +blocked_by: + - CORE_hexagonal-store-boundary +--- + +# Important runtime contracts still move as plain object shapes + +Think has good class-backed anchors such as `Entry`, +`ReflectSession`, `CaptureProvenance`, domain errors, runtime read +entries, and MCP outcomes. The broader store/read/MCP workflows still +move many implicit plain-object contracts between layers. + +Runtime Truth requires important concepts to be constructor-validated +runtime models, not informal shape conventions. + +## Acceptance Criteria + +- Promote graph model status, capture results, remember scopes, browse + windows, migration outcomes, prompt metric summaries, and repair + outcomes into runtime-backed classes or equivalent Swift value types. +- Replace generic `throw new Error(...)` in source with domain-specific + error classes. +- Boundary DTOs remain plain only at adapters and wire schemas. +- Tests assert `instanceof` or equivalent runtime identity for important + outcomes inside one realm. +- Cross-realm values normalize at boundaries before entering core. diff --git a/docs/method/backlog/bad-code/CORE_runtime-truth-standard-ratchet.md b/docs/method/backlog/bad-code/CORE_runtime-truth-standard-ratchet.md new file mode 100644 index 0000000..57718c5 --- /dev/null +++ b/docs/method/backlog/bad-code/CORE_runtime-truth-standard-ratchet.md @@ -0,0 +1,28 @@ +--- +id: CORE_runtime-truth-standard-ratchet +blocks: [] +blocked_by: [] +--- + +# Runtime Truth standard is documented but not enforced + +`docs/INFRASTRUCTURE_DOCTRINE.md` now defines strict Runtime Truth +architecture and code-shape rules. A measured baseline on 2026-05-13 +showed that applying the proposed JavaScript size and complexity rules +would currently report 195 violations across 47 files. + +The standard must become executable without breaking the repo all at +once. New work should not increase the violation count while legacy +hotspots are paid down deliberately. + +## Acceptance Criteria + +- Add a committed strict-limits report or baseline generated from the + same rule set used in `docs/audit/2026-05-13_runtime-truth-code-standard.md`. +- CI fails when a PR adds new or worsened violations. +- The ratchet separately reports source, test, and benchmark violations. +- Once source violations reach zero, the strict limits move into + `eslint.config.js` as hard errors for JavaScript. +- TypeScript-specific unsafe-type rules are added when TypeScript source + enters the repo. +- A Swift equivalent is chosen for file/function/complexity limits. diff --git a/docs/method/backlog/bad-code/SURFACE_browse-tui-strict-limits.md b/docs/method/backlog/bad-code/SURFACE_browse-tui-strict-limits.md new file mode 100644 index 0000000..ee366f5 --- /dev/null +++ b/docs/method/backlog/bad-code/SURFACE_browse-tui-strict-limits.md @@ -0,0 +1,30 @@ +--- +id: SURFACE_browse-tui-strict-limits +blocks: [] +blocked_by: + - CORE_runtime-truth-standard-ratchet +--- + +# Browse TUI exceeds strict human-scale limits + +The 2026-05-13 Runtime Truth audit found the browse surface to be the +largest source of size and complexity violations: + +- `src/cli/commands/read.js` is 934 lines. +- `src/browse-tui/actions.js` has a 284-line `applyBrowseAction()` + with complexity 58. +- `src/browse-tui/app.js` mixes terminal I/O, animation, raw input, and + state orchestration. + +Existing backlog already calls out the splash monolith and mind-switch +loop; this item is the broader strict-limits cleanup. + +## Acceptance Criteria + +- Split browse command orchestration from pure browse state transitions. +- Replace the large action switch with small command handlers or a + reducer table of runtime-backed actions. +- Move terminal I/O behind a TUI adapter port. +- `src/cli/commands/read.js` falls below 600 lines. +- All browse source files satisfy the 35-line function and complexity 8 + limits. diff --git a/docs/method/backlog/bad-code/SURFACE_macos-appstate-composition-root.md b/docs/method/backlog/bad-code/SURFACE_macos-appstate-composition-root.md new file mode 100644 index 0000000..748e8f7 --- /dev/null +++ b/docs/method/backlog/bad-code/SURFACE_macos-appstate-composition-root.md @@ -0,0 +1,24 @@ +--- +id: SURFACE_macos-appstate-composition-root +blocks: [] +blocked_by: [] +--- + +# CaptureAppState constructs concrete macOS dependencies directly + +The Swift adapter layer has good protocol seams, but +`CaptureAppState` remains a composition hotspot. It constructs concrete +MCP/CLI clients, metrics recorders, panel controllers, hotkey monitors, +notification tasks, retry behavior, update polling, and process restart +logic in one app-state class. + +## Acceptance Criteria + +- Introduce an explicit macOS composition root or factory. +- Inject capture client, metrics recorder, panel controller, hotkey + monitor, restart port, and build-update reader into app state. +- Move process restart behind a port instead of constructing `Process` + inside `CaptureAppState`. +- Split retry orchestration from menu-bar state. +- `CaptureAppState.swift` falls below 250 lines or is divided by one + primary responsibility per file. From 826906d81c9c7adfb7d81a7ccdd4ca6d70c22631 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 13 May 2026 10:29:35 -0700 Subject: [PATCH 02/13] refactor(errors): replace generic source failures --- .../2026-05-13_runtime-truth-code-standard.md | 17 ++++++---- .../CORE_runtime-domain-model-cutover.md | 4 +-- .../CORE_runtime-truth-standard-ratchet.md | 2 ++ src/errors.js | 16 ++++++++++ src/minds.js | 3 +- src/store/checkpoint-product-read.js | 7 ++-- src/store/checkpoint-state.js | 3 +- src/store/content-reader.js | 4 ++- src/store/model.js | 2 +- src/store/ports.js | 8 +++-- test/ports/content-reader.test.js | 30 +++++++++++++++++ test/ports/minds.test.js | 5 +-- test/ports/model.test.js | 17 +++++++++- test/ports/ports.test.js | 32 +++++++++++++++++++ 14 files changed, 129 insertions(+), 21 deletions(-) create mode 100644 test/ports/content-reader.test.js create mode 100644 test/ports/ports.test.js diff --git a/docs/audit/2026-05-13_runtime-truth-code-standard.md b/docs/audit/2026-05-13_runtime-truth-code-standard.md index 3ee2266..0dd2429 100644 --- a/docs/audit/2026-05-13_runtime-truth-code-standard.md +++ b/docs/audit/2026-05-13_runtime-truth-code-standard.md @@ -272,9 +272,9 @@ Backlog: - `CORE_runtime-domain-model-cutover` -### F6. Generic errors still appear in source +### F6. Generic source errors were present at baseline and should stay cleared -Severity: Medium +Severity: Cleared by follow-up refactor on this branch Evidence: @@ -288,6 +288,10 @@ nine source occurrences, including: - `src/store/checkpoint-product-read.js` - `src/store/checkpoint-state.js` +The follow-up refactor on `cycle/runtime-truth-standards-audit` replaced +those generic source errors with `ValidationError`, `NotFoundError`, +`DependencyError`, and `PortNotImplementedError`. + Impact: Generic errors make runtime dispatch depend on message text or broad @@ -296,13 +300,14 @@ classes with stable codes. Required direction: -- Replace generic source errors with domain-specific error classes. -- Port base classes should throw typed `PortNotImplementedError` or avoid - callable abstract methods in JavaScript. +- Keep source free of generic `throw new Error(...)` and + `throw new TypeError(...)`. +- Add the check to the Runtime Truth ratchet so future source code cannot + reintroduce generic runtime failures. Backlog: -- `CORE_runtime-domain-model-cutover` +- `CORE_runtime-truth-standard-ratchet` ### F7. Browse/TUI code has the largest human-scale violations diff --git a/docs/method/backlog/bad-code/CORE_runtime-domain-model-cutover.md b/docs/method/backlog/bad-code/CORE_runtime-domain-model-cutover.md index dc528d1..061543f 100644 --- a/docs/method/backlog/bad-code/CORE_runtime-domain-model-cutover.md +++ b/docs/method/backlog/bad-code/CORE_runtime-domain-model-cutover.md @@ -20,8 +20,8 @@ runtime models, not informal shape conventions. - Promote graph model status, capture results, remember scopes, browse windows, migration outcomes, prompt metric summaries, and repair outcomes into runtime-backed classes or equivalent Swift value types. -- Replace generic `throw new Error(...)` in source with domain-specific - error classes. +- Keep source failures represented by domain-specific error classes, not + generic runtime errors. - Boundary DTOs remain plain only at adapters and wire schemas. - Tests assert `instanceof` or equivalent runtime identity for important outcomes inside one realm. diff --git a/docs/method/backlog/bad-code/CORE_runtime-truth-standard-ratchet.md b/docs/method/backlog/bad-code/CORE_runtime-truth-standard-ratchet.md index 57718c5..fa8cf1a 100644 --- a/docs/method/backlog/bad-code/CORE_runtime-truth-standard-ratchet.md +++ b/docs/method/backlog/bad-code/CORE_runtime-truth-standard-ratchet.md @@ -21,6 +21,8 @@ hotspots are paid down deliberately. same rule set used in `docs/audit/2026-05-13_runtime-truth-code-standard.md`. - CI fails when a PR adds new or worsened violations. - The ratchet separately reports source, test, and benchmark violations. +- CI fails if source reintroduces generic `throw new Error(...)` or + `throw new TypeError(...)`. - Once source violations reach zero, the strict limits move into `eslint.config.js` as hard errors for JavaScript. - TypeScript-specific unsafe-type rules are added when TypeScript source diff --git a/src/errors.js b/src/errors.js index ecd5904..e520cb4 100644 --- a/src/errors.js +++ b/src/errors.js @@ -40,3 +40,19 @@ export class CaptureError extends ThinkError { this.name = 'CaptureError'; } } + +export class DependencyError extends ThinkError { + constructor(message) { + super(message, 'DEPENDENCY_ERROR'); + this.name = 'DependencyError'; + } +} + +export class PortNotImplementedError extends ThinkError { + constructor(portName, methodName) { + super(`${portName}.${methodName} is not implemented`, 'PORT_NOT_IMPLEMENTED'); + this.name = 'PortNotImplementedError'; + this.portName = portName; + this.methodName = methodName; + } +} diff --git a/src/minds.js b/src/minds.js index dbca609..49e33db 100644 --- a/src/minds.js +++ b/src/minds.js @@ -1,6 +1,7 @@ import { existsSync, readdirSync, statSync } from 'node:fs'; import path from 'node:path'; +import { ValidationError } from './errors.js'; import { hasGitRepo } from './git.js'; import { getThinkDir } from './paths.js'; @@ -51,7 +52,7 @@ export function discoverMinds(thinkDir = getThinkDir()) { */ export function shaderForMind(name, shaderCount) { if (shaderCount <= 0) { - throw new Error(`shaderForMind: shaderCount must be > 0 (got ${shaderCount})`); + throw new ValidationError(`shaderForMind: shaderCount must be > 0 (got ${shaderCount})`); } let hash = 5381; for (const ch of name) { diff --git a/src/store/checkpoint-product-read.js b/src/store/checkpoint-product-read.js index 42eefe0..3a5cc3e 100644 --- a/src/store/checkpoint-product-read.js +++ b/src/store/checkpoint-product-read.js @@ -1,3 +1,4 @@ +import { NotFoundError, ValidationError } from '../errors.js'; import { openCheckpointStateRead } from './checkpoint-state.js'; const DEFAULT_PATTERN = '*'; @@ -64,7 +65,7 @@ class CheckpointProductQuery { return this._applyPredicateWhere(strand, criteria); } if (!isPlainWhereObject(criteria)) { - throw new TypeError('checkpoint product query where() expects an object or predicate'); + throw new ValidationError('checkpoint product query where() expects an object or predicate'); } const filtered = []; @@ -129,7 +130,7 @@ class CheckpointProductTraversal { bfs(start, options = {}) { if (!this._reader.hasNode(start)) { - throw new Error(`Start node not found: ${start}`); + throw new NotFoundError(`Start node not found: ${start}`); } const direction = normalizeTraversalDirection(options.dir); @@ -278,7 +279,7 @@ function normalizeTraversalDirection(direction = 'out') { if (direction === 'both') { return 'both'; } - throw new Error(`Unsupported traversal direction: ${direction}`); + throw new ValidationError(`Unsupported traversal direction: ${direction}`); } function normalizeLabelFilter(labelFilter) { diff --git a/src/store/checkpoint-state.js b/src/store/checkpoint-state.js index bbbdcf4..ba09ea3 100644 --- a/src/store/checkpoint-state.js +++ b/src/store/checkpoint-state.js @@ -1,5 +1,6 @@ import Plumbing from '@git-stunts/plumbing'; import WarpApp, * as GitWarp from '@git-stunts/git-warp'; +import { DependencyError } from '../errors.js'; import { createAppContentReader } from './content-reader.js'; import { CHECKPOINT_POLICY, GRAPH_NAME } from './constants.js'; import { createWriterId } from './model.js'; @@ -49,7 +50,7 @@ async function resolveApp({ app, persistence }) { function createCheckpointStateReader(state) { const createReader = GitWarp.createStateReader ?? GitWarp.createStateReaderV5; if (typeof createReader !== 'function') { - throw new Error('Installed @git-stunts/git-warp does not expose a public state reader factory'); + throw new DependencyError('Installed @git-stunts/git-warp does not expose a public state reader factory'); } return createReader(state); } diff --git a/src/store/content-reader.js b/src/store/content-reader.js index 1727ccc..90132b8 100644 --- a/src/store/content-reader.js +++ b/src/store/content-reader.js @@ -1,3 +1,5 @@ +import { DependencyError } from '../errors.js'; + export function createAppContentReader(app) { if (typeof app?.getContent === 'function') { return async (nodeId) => await app.getContent(nodeId); @@ -8,5 +10,5 @@ export function createAppContentReader(app) { return async (nodeId) => await core.getContent(nodeId); } - throw new Error('Installed @git-stunts/git-warp does not expose a public content reader'); + throw new DependencyError('Installed @git-stunts/git-warp does not expose a public content reader'); } diff --git a/src/store/model.js b/src/store/model.js index 9c87f1a..06c4520 100644 --- a/src/store/model.js +++ b/src/store/model.js @@ -58,7 +58,7 @@ export function parseSince(since, now) { export function formatBucketKey(date, bucket) { if (!BUCKET_PERIODS.includes(bucket)) { - throw new Error(`formatBucketKey: invalid bucket "${bucket}" (expected ${BUCKET_PERIODS.join(', ')})`); + throw new ValidationError(`formatBucketKey: invalid bucket "${bucket}" (expected ${BUCKET_PERIODS.join(', ')})`); } const iso = date.toISOString(); diff --git a/src/store/ports.js b/src/store/ports.js index e5e2a28..3021b5b 100644 --- a/src/store/ports.js +++ b/src/store/ports.js @@ -1,10 +1,12 @@ +import { PortNotImplementedError } from '../errors.js'; + /** * ClockPort interface for deterministic time. * Adheres to Infrastructure Doctrine P7. */ export class ClockPort { /** @returns {Date} */ - now() { throw new Error('now() not implemented'); } + now() { throw new PortNotImplementedError('ClockPort', 'now'); } } /** @@ -13,7 +15,7 @@ export class ClockPort { */ export class HostPort { /** @returns {string} */ - hostname() { throw new Error('hostname() not implemented'); } + hostname() { throw new PortNotImplementedError('HostPort', 'hostname'); } } /** @@ -22,7 +24,7 @@ export class HostPort { */ export class RandomPort { /** @returns {string} */ - uuid() { throw new Error('uuid() not implemented'); } + uuid() { throw new PortNotImplementedError('RandomPort', 'uuid'); } } class SystemClock extends ClockPort { diff --git a/test/ports/content-reader.test.js b/test/ports/content-reader.test.js new file mode 100644 index 0000000..c684317 --- /dev/null +++ b/test/ports/content-reader.test.js @@ -0,0 +1,30 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { DependencyError } from '../../src/errors.js'; +import { createAppContentReader } from '../../src/store/content-reader.js'; + +test('content reader uses app getContent when available', async () => { + const reader = createAppContentReader({ + getContent: (nodeId) => Promise.resolve(new TextEncoder().encode(`app:${nodeId}`)), + }); + + assert.equal(new TextDecoder().decode(await reader('node:1')), 'app:node:1'); +}); + +test('content reader uses core getContent when app getContent is unavailable', async () => { + const reader = createAppContentReader({ + core: () => ({ + getContent: (nodeId) => Promise.resolve(new TextEncoder().encode(`core:${nodeId}`)), + }), + }); + + assert.equal(new TextDecoder().decode(await reader('node:2')), 'core:node:2'); +}); + +test('content reader reports missing git-warp content API as dependency error', () => { + assert.throws( + () => createAppContentReader({}), + (error) => error instanceof DependencyError && /content reader/.test(error.message) + ); +}); diff --git a/test/ports/minds.test.js b/test/ports/minds.test.js index e742597..06ba33f 100644 --- a/test/ports/minds.test.js +++ b/test/ports/minds.test.js @@ -5,6 +5,7 @@ import test from 'node:test'; import { execSync } from 'node:child_process'; import { createTempDir } from '../fixtures/tmp.js'; +import { ValidationError } from '../../src/errors.js'; import { discoverMinds, shaderForMind } from '../../src/minds.js'; // --------------------------------------------------------------------------- @@ -134,7 +135,7 @@ test('shaderForMind stays within the shader count range', () => { test('shaderForMind throws when shaderCount is zero', () => { assert.throws( () => shaderForMind('test', 0), - { message: /shaderCount/ }, + (error) => error instanceof ValidationError && /shaderCount/.test(error.message), 'Expected shaderForMind to reject zero shaderCount.' ); }); @@ -142,7 +143,7 @@ test('shaderForMind throws when shaderCount is zero', () => { test('shaderForMind throws when shaderCount is negative', () => { assert.throws( () => shaderForMind('test', -1), - { message: /shaderCount/ }, + (error) => error instanceof ValidationError && /shaderCount/.test(error.message), 'Expected shaderForMind to reject negative shaderCount.' ); }); diff --git a/test/ports/model.test.js b/test/ports/model.test.js index 15b2699..066b399 100644 --- a/test/ports/model.test.js +++ b/test/ports/model.test.js @@ -1,7 +1,15 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { Entry, ReflectSession, createEntry, createReflectSession, storesTextContent } from '../../src/store/model.js'; +import { ValidationError } from '../../src/errors.js'; +import { + Entry, + ReflectSession, + createEntry, + createReflectSession, + formatBucketKey, + storesTextContent, +} from '../../src/store/model.js'; import { ENTRY_KINDS, BUCKET_PERIODS } from '../../src/store/constants.js'; test('createEntry returns an Entry instance', () => { @@ -83,3 +91,10 @@ test('storesTextContent validates against ENTRY_KINDS', () => { } assert.equal(storesTextContent('invalid_kind'), false, 'Expected false for invalid kind.'); }); + +test('formatBucketKey rejects unsupported buckets with a validation error', () => { + assert.throws( + () => formatBucketKey(new Date('2026-05-13T00:00:00.000Z'), 'month'), + (error) => error instanceof ValidationError && /invalid bucket/.test(error.message) + ); +}); diff --git a/test/ports/ports.test.js b/test/ports/ports.test.js new file mode 100644 index 0000000..5a4a892 --- /dev/null +++ b/test/ports/ports.test.js @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { PortNotImplementedError } from '../../src/errors.js'; +import { ClockPort, HostPort, RandomPort } from '../../src/store/ports.js'; + +test('abstract clock port reports a typed not-implemented error', () => { + assert.throws( + () => new ClockPort().now(), + (error) => isPortError(error, 'ClockPort', 'now') + ); +}); + +test('abstract host port reports a typed not-implemented error', () => { + assert.throws( + () => new HostPort().hostname(), + (error) => isPortError(error, 'HostPort', 'hostname') + ); +}); + +test('abstract random port reports a typed not-implemented error', () => { + assert.throws( + () => new RandomPort().uuid(), + (error) => isPortError(error, 'RandomPort', 'uuid') + ); +}); + +function isPortError(error, portName, methodName) { + return error instanceof PortNotImplementedError + && error.portName === portName + && error.methodName === methodName; +} From 6db72c89b85decc2b0ef468b93a93e461d1772f3 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 13 May 2026 11:02:57 -0700 Subject: [PATCH 03/13] chore: add runtime truth ratchet --- .github/PULL_REQUEST_TEMPLATE.md | 9 + .../2026-05-13_runtime-truth-code-standard.md | 53 ++- .../audit/runtime-truth-ratchet-baseline.json | 428 ++++++++++++++++++ .../CORE_runtime-truth-standard-ratchet.md | 35 +- package.json | 5 +- scripts/runtime-truth-ratchet.mjs | 342 ++++++++++++++ 6 files changed, 844 insertions(+), 28 deletions(-) create mode 100644 docs/audit/runtime-truth-ratchet-baseline.json create mode 100644 scripts/runtime-truth-ratchet.mjs diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ed268cf..ae58d77 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,3 +8,12 @@ - [ ] `npm run test:ports` passes - [ ] `npm run test:m1` passes - [ ] Docs updated if user-facing + +## Runtime Truth checklist + +- [ ] Core/domain code stays behind ports and adapters +- [ ] Dependencies are injected from composition roots +- [ ] Encoding/decoding stays at boundaries +- [ ] Important runtime concepts use constructor-validated models +- [ ] No new generic source `Error`/`TypeError` throws +- [ ] No new strict-limit ratchet regressions diff --git a/docs/audit/2026-05-13_runtime-truth-code-standard.md b/docs/audit/2026-05-13_runtime-truth-code-standard.md index 0dd2429..ffdad8b 100644 --- a/docs/audit/2026-05-13_runtime-truth-code-standard.md +++ b/docs/audit/2026-05-13_runtime-truth-code-standard.md @@ -41,6 +41,10 @@ swift test --package-path macos --list-tests `swift test --package-path macos --list-tests` built the Swift package and listed the macOS tests successfully. +Follow-up enforcement added `scripts/runtime-truth-ratchet.mjs`, which +uses the same strict JavaScript rule set against tracked JavaScript files +and is wired into `npm run lint`. + ## Executive Summary Think already has strong runtime-truth instincts: explicit domain error @@ -87,6 +91,19 @@ Violations by rule: | `max-params` | 3 | | `max-lines` | 1 | +The committed ratchet baseline differs slightly from the original audit +command because it uses `--no-ignore` and separately accounts for tracked +benchmark files. Current ratcheted counts are: + +| Category | Violations | +| --- | ---: | +| Source/scripts | 150 | +| Tests | 45 | +| Benchmarks | 5 | +| Total | 200 | + +The ratchet also records zero generic source `Error`/`TypeError` throws. + Largest files: | Lines | File | Status | @@ -113,30 +130,36 @@ Highest-density strict-rule hotspots: ## Findings -### F1. Doctrine is documented, but enforcement is not active +### F1. Doctrine has a ratchet; full hard gates remain -Severity: High +Severity: Medium Evidence: -- `eslint.config.js` currently enforces many safety rules, but not the - new size/complexity limits. +- `package.json` runs `scripts/runtime-truth-ratchet.mjs` from + `npm run lint`. +- `docs/audit/runtime-truth-ratchet-baseline.json` stores the current + strict-limit and generic-source-error baseline. +- The ratchet fails new or worsened strict-limit counts by total, + category, rule, file, and per-file rule. +- The ratchet fails any source reintroduction of generic + `throw new Error(...)` or `throw new TypeError(...)`. - The TypeScript-specific rules in the doctrine cannot run because the repository currently has no TypeScript source or `@typescript-eslint` setup. -- The strict JavaScript profile reports 195 current violations. +- Swift size and complexity limits still need a native equivalent. Impact: -The standard is now normative, but CI will not stop new drift unless the -repo adds a ratchet. Without a ratchet, new work can add more violations -while old debt remains invisible. +CI now stops new JavaScript drift, but existing violations remain real +debt. Runtime Truth is not fully enforced until source counts are paid +down and the limits move into the normal lint profile as hard errors. Required direction: -- Add a ratcheted strict-limits report. -- Fail new or worsened violations first. -- Convert ratchet to full hard gate after the hotspots are split. +- Keep ratcheted counts stable or decreasing. +- Convert ratcheted limits to full hard gates after hotspots are split. +- Add TypeScript and Swift equivalents when those surfaces need them. Backlog: @@ -401,9 +424,11 @@ Phase 1: Documentation and measurement. Phase 2: Enforcement ratchet. -- Add a strict-limits report to CI. -- Fail only new or worsened violations at first. -- Track full violation count in a committed baseline. +- Completed for tracked JavaScript/MJS/CJS files in + `scripts/runtime-truth-ratchet.mjs`. +- `npm run lint` now fails new or worsened ratcheted violations. +- `docs/audit/runtime-truth-ratchet-baseline.json` tracks the full + current violation count. Phase 3: Boundary cleanup. diff --git a/docs/audit/runtime-truth-ratchet-baseline.json b/docs/audit/runtime-truth-ratchet-baseline.json new file mode 100644 index 0000000..03a14de --- /dev/null +++ b/docs/audit/runtime-truth-ratchet-baseline.json @@ -0,0 +1,428 @@ +{ + "generatedFrom": "scripts/runtime-truth-ratchet.mjs", + "genericSourceThrows": { + "byCategory": {}, + "byFile": {}, + "total": 0 + }, + "strictLimits": { + "byCategory": { + "benchmark": 5, + "source": 150, + "test": 45 + }, + "byFile": { + "benchmarks/browse-bootstrap.js": { + "byRule": { + "complexity": 1, + "max-lines-per-function": 1 + }, + "category": "benchmark", + "total": 2 + }, + "benchmarks/capture-latency.js": { + "byRule": { + "complexity": 1, + "max-lines-per-function": 2 + }, + "category": "benchmark", + "total": 3 + }, + "scripts/repair-v17-mind.mjs": { + "byRule": { + "complexity": 4, + "max-lines-per-function": 3, + "max-statements": 1 + }, + "category": "source", + "total": 8 + }, + "src/browse-benchmark.js": { + "byRule": { + "complexity": 1, + "max-lines-per-function": 2, + "max-statements": 1 + }, + "category": "source", + "total": 4 + }, + "src/browse-tui/actions.js": { + "byRule": { + "complexity": 1, + "max-lines-per-function": 1, + "max-statements": 1 + }, + "category": "source", + "total": 3 + }, + "src/browse-tui/app.js": { + "byRule": { + "complexity": 2, + "max-lines-per-function": 4 + }, + "category": "source", + "total": 6 + }, + "src/browse-tui/commands.js": { + "byRule": { + "max-lines-per-function": 1, + "max-params": 1 + }, + "category": "source", + "total": 2 + }, + "src/browse-tui/keys.js": { + "byRule": { + "complexity": 3, + "max-lines-per-function": 3 + }, + "category": "source", + "total": 6 + }, + "src/browse-tui/loaders.js": { + "byRule": { + "complexity": 1 + }, + "category": "source", + "total": 1 + }, + "src/browse-tui/model.js": { + "byRule": { + "max-lines-per-function": 1 + }, + "category": "source", + "total": 1 + }, + "src/browse-tui/overlays.js": { + "byRule": { + "complexity": 1, + "max-lines-per-function": 1 + }, + "category": "source", + "total": 2 + }, + "src/browse-tui/page.js": { + "byRule": { + "complexity": 2, + "max-lines-per-function": 2 + }, + "category": "source", + "total": 4 + }, + "src/browse-tui/panels.js": { + "byRule": { + "max-lines-per-function": 1 + }, + "category": "source", + "total": 1 + }, + "src/browse-tui/reflect.js": { + "byRule": { + "complexity": 1 + }, + "category": "source", + "total": 1 + }, + "src/browse-tui/resolve.js": { + "byRule": { + "complexity": 1, + "max-lines-per-function": 1 + }, + "category": "source", + "total": 2 + }, + "src/browse-tui/script.js": { + "byRule": { + "complexity": 3, + "max-lines-per-function": 2, + "max-statements": 1 + }, + "category": "source", + "total": 6 + }, + "src/browse-tui/view.js": { + "byRule": { + "max-lines-per-function": 2 + }, + "category": "source", + "total": 2 + }, + "src/cli.js": { + "byRule": { + "complexity": 1, + "max-lines-per-function": 1, + "max-statements": 1 + }, + "category": "source", + "total": 3 + }, + "src/cli/commands/capture.js": { + "byRule": { + "complexity": 1, + "max-lines-per-function": 1, + "max-statements": 1 + }, + "category": "source", + "total": 3 + }, + "src/cli/commands/read.js": { + "byRule": { + "complexity": 6, + "max-lines-per-function": 6, + "max-statements": 5 + }, + "category": "source", + "total": 17 + }, + "src/cli/commands/reflect.js": { + "byRule": { + "complexity": 1, + "max-lines-per-function": 3, + "max-statements": 1 + }, + "category": "source", + "total": 5 + }, + "src/cli/graph-gate.js": { + "byRule": { + "max-lines-per-function": 1 + }, + "category": "source", + "total": 1 + }, + "src/cli/options.js": { + "byRule": { + "complexity": 3, + "max-lines-per-function": 2, + "max-statements": 3 + }, + "category": "source", + "total": 8 + }, + "src/git.js": { + "byRule": { + "max-lines-per-function": 2 + }, + "category": "source", + "total": 2 + }, + "src/mcp/server.js": { + "byRule": { + "max-lines-per-function": 1 + }, + "category": "source", + "total": 1 + }, + "src/mcp/service.js": { + "byRule": { + "max-lines-per-function": 2 + }, + "category": "source", + "total": 2 + }, + "src/splash-shader.js": { + "byRule": { + "complexity": 3, + "max-depth": 8, + "max-lines-per-function": 1, + "max-params": 1, + "max-statements": 4 + }, + "category": "source", + "total": 17 + }, + "src/store/checkpoint-product-read.js": { + "byRule": { + "complexity": 1 + }, + "category": "source", + "total": 1 + }, + "src/store/derivation.js": { + "byRule": { + "complexity": 2, + "max-lines-per-function": 3 + }, + "category": "source", + "total": 5 + }, + "src/store/enrichment/runner.js": { + "byRule": { + "complexity": 2, + "max-lines-per-function": 2, + "max-statements": 1 + }, + "category": "source", + "total": 5 + }, + "src/store/enrichment/semantic-parse.js": { + "byRule": { + "complexity": 1 + }, + "category": "source", + "total": 1 + }, + "src/store/migrations.js": { + "byRule": { + "complexity": 1, + "max-lines-per-function": 1, + "max-params": 1, + "max-statements": 1 + }, + "category": "source", + "total": 4 + }, + "src/store/model.js": { + "byRule": { + "complexity": 1 + }, + "category": "source", + "total": 1 + }, + "src/store/prompt-metrics.js": { + "byRule": { + "complexity": 2 + }, + "category": "source", + "total": 2 + }, + "src/store/queries.js": { + "byRule": { + "complexity": 3, + "max-depth": 3, + "max-lines-per-function": 2, + "max-statements": 1 + }, + "category": "source", + "total": 9 + }, + "src/store/reflect.js": { + "byRule": { + "max-lines-per-function": 3 + }, + "category": "source", + "total": 3 + }, + "src/store/remember.js": { + "byRule": { + "complexity": 1, + "max-lines-per-function": 1, + "max-statements": 1 + }, + "category": "source", + "total": 3 + }, + "src/store/runtime.js": { + "byRule": { + "complexity": 7, + "max-lines-per-function": 1 + }, + "category": "source", + "total": 8 + }, + "test/acceptance/graph-migration.test.js": { + "byRule": { + "complexity": 2, + "max-lines-per-function": 7 + }, + "category": "test", + "total": 9 + }, + "test/acceptance/json-output.test.js": { + "byRule": { + "max-lines-per-function": 1 + }, + "category": "test", + "total": 1 + }, + "test/acceptance/mcp.test.js": { + "byRule": { + "max-lines-per-function": 1 + }, + "category": "test", + "total": 1 + }, + "test/acceptance/prompt-metrics.test.js": { + "byRule": { + "max-lines-per-function": 2 + }, + "category": "test", + "total": 2 + }, + "test/acceptance/read-modes.test.js": { + "byRule": { + "max-lines": 1, + "max-lines-per-function": 13, + "max-statements": 1 + }, + "category": "test", + "total": 15 + }, + "test/acceptance/reflect.test.js": { + "byRule": { + "max-lines-per-function": 6, + "max-statements": 1 + }, + "category": "test", + "total": 7 + }, + "test/acceptance/repair-v17-mind.test.js": { + "byRule": { + "max-lines-per-function": 2, + "max-statements": 1 + }, + "category": "test", + "total": 3 + }, + "test/ports/capture-context.test.js": { + "byRule": { + "max-lines-per-function": 1, + "max-statements": 1 + }, + "category": "test", + "total": 2 + }, + "test/ports/checkpoint-read.test.js": { + "byRule": { + "max-lines-per-function": 1, + "max-statements": 1 + }, + "category": "test", + "total": 2 + }, + "test/ports/docs-consistency.test.js": { + "byRule": { + "max-lines-per-function": 1 + }, + "category": "test", + "total": 1 + }, + "test/ports/repair-v17-mind.test.js": { + "byRule": { + "complexity": 1, + "max-lines-per-function": 1 + }, + "category": "test", + "total": 2 + } + }, + "byRule": { + "complexity": 61, + "max-depth": 11, + "max-lines": 1, + "max-lines-per-function": 96, + "max-params": 3, + "max-statements": 28 + }, + "total": 200 + }, + "strictRuleIds": [ + "complexity", + "max-depth", + "max-lines", + "max-lines-per-function", + "max-params", + "max-statements" + ], + "version": 1 +} diff --git a/docs/method/backlog/bad-code/CORE_runtime-truth-standard-ratchet.md b/docs/method/backlog/bad-code/CORE_runtime-truth-standard-ratchet.md index fa8cf1a..ed13116 100644 --- a/docs/method/backlog/bad-code/CORE_runtime-truth-standard-ratchet.md +++ b/docs/method/backlog/bad-code/CORE_runtime-truth-standard-ratchet.md @@ -4,27 +4,38 @@ blocks: [] blocked_by: [] --- -# Runtime Truth standard is documented but not enforced +# Runtime Truth ratchet is active; full hard gates remain `docs/INFRASTRUCTURE_DOCTRINE.md` now defines strict Runtime Truth architecture and code-shape rules. A measured baseline on 2026-05-13 showed that applying the proposed JavaScript size and complexity rules -would currently report 195 violations across 47 files. +would report 195 violations across 47 files with the default ESLint +ignore profile. -The standard must become executable without breaking the repo all at -once. New work should not increase the violation count while legacy -hotspots are paid down deliberately. +The first executable ratchet now lives in +`scripts/runtime-truth-ratchet.mjs` and runs from `npm run lint`. Its +committed baseline uses `--no-ignore` against tracked JavaScript files, +so it also accounts for benchmarks: + +- 200 strict-limit violations total. +- 150 source/script violations. +- 45 test violations. +- 5 benchmark violations. +- 0 generic source `Error`/`TypeError` throws. + +New work should not increase the ratcheted counts while legacy hotspots +are paid down deliberately. ## Acceptance Criteria -- Add a committed strict-limits report or baseline generated from the +- [x] Add a committed strict-limits report or baseline generated from the same rule set used in `docs/audit/2026-05-13_runtime-truth-code-standard.md`. -- CI fails when a PR adds new or worsened violations. -- The ratchet separately reports source, test, and benchmark violations. -- CI fails if source reintroduces generic `throw new Error(...)` or +- [x] CI fails when a PR adds new or worsened violations. +- [x] The ratchet separately reports source, test, and benchmark violations. +- [x] CI fails if source reintroduces generic `throw new Error(...)` or `throw new TypeError(...)`. -- Once source violations reach zero, the strict limits move into +- [ ] Once source violations reach zero, the strict limits move into `eslint.config.js` as hard errors for JavaScript. -- TypeScript-specific unsafe-type rules are added when TypeScript source +- [ ] TypeScript-specific unsafe-type rules are added when TypeScript source enters the repo. -- A Swift equivalent is chosen for file/function/complexity limits. +- [ ] A Swift equivalent is chosen for file/function/complexity limits. diff --git a/package.json b/package.json index ec0f0ee..52f5d4c 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "think-mcp": "./bin/think-mcp.js" }, "scripts": { - "lint": "eslint .", + "lint": "eslint . && npm run runtime-truth:ratchet", "macos": "node ./scripts/package-macos-app.mjs --open", "macos:bundle": "node ./scripts/package-macos-app.mjs", "macos:dev": "swift run --package-path macos ThinkMenuBarApp", @@ -30,7 +30,8 @@ "tui": "node ./bin/think.js --browse", "benchmark:browse": "node benchmarks/browse-bootstrap.js", "benchmark:capture": "node benchmarks/capture-latency.js", - "repair:v17-mind": "node ./scripts/repair-v17-mind.mjs" + "repair:v17-mind": "node ./scripts/repair-v17-mind.mjs", + "runtime-truth:ratchet": "node ./scripts/runtime-truth-ratchet.mjs" }, "engines": { "node": ">=22.0.0" diff --git a/scripts/runtime-truth-ratchet.mjs b/scripts/runtime-truth-ratchet.mjs new file mode 100644 index 0000000..873755a --- /dev/null +++ b/scripts/runtime-truth-ratchet.mjs @@ -0,0 +1,342 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const baselinePath = path.join(repoRoot, 'docs', 'audit', 'runtime-truth-ratchet-baseline.json'); +const sourcePrefixes = Object.freeze(['src/', 'bin/', 'scripts/']); +const strictRuleIds = Object.freeze([ + 'complexity', + 'max-depth', + 'max-lines', + 'max-lines-per-function', + 'max-params', + 'max-statements', +]); +const strictRuleSet = new Set(strictRuleIds); +const strictRuleArgs = Object.freeze([ + 'max-lines:["error",1000]', + 'max-lines-per-function:["error",{"max":35,"skipBlankLines":true,"skipComments":true}]', + 'max-depth:["error",4]', + 'max-params:["error",5]', + 'complexity:["error",8]', + 'max-statements:["error",25]', +]); +const genericThrowPattern = /\bthrow\s+new\s+(Error|TypeError)\s*\(/u; + +class RuntimeTruthRatchetError extends Error { + constructor(message) { + super(message); + this.name = 'RuntimeTruthRatchetError'; + Object.freeze(this); + } +} + +function parseArgs(argv) { + const parsed = { + json: false, + writeBaseline: false, + }; + + for (const arg of argv) { + if (arg === '--json') { + parsed.json = true; + continue; + } + if (arg === '--write-baseline') { + parsed.writeBaseline = true; + continue; + } + throw new RuntimeTruthRatchetError(`Unknown argument: ${arg}`); + } + + return parsed; +} + +function run(command, args, { allowFailure = false } = {}) { + const result = spawnSync(command, args, { + cwd: repoRoot, + encoding: 'utf8', + maxBuffer: 32 * 1024 * 1024, + }); + + if (result.error) { + throw result.error; + } + if (!allowFailure && result.status !== 0) { + throw new RuntimeTruthRatchetError([ + `Command failed: ${command} ${args.join(' ')}`, + result.stdout, + result.stderr, + ].filter(Boolean).join('\n')); + } + return result; +} + +function trackedCodeFiles() { + const result = run('git', ['ls-files', '*.js', '*.mjs', '*.cjs']); + return result.stdout + .split('\n') + .map(line => line.trim()) + .filter(Boolean) + .sort(compareStrings); +} + +function sourceCodeFiles(files) { + return files.filter(file => sourcePrefixes.some(prefix => file.startsWith(prefix))); +} + +function eslintBinPath() { + const executable = process.platform === 'win32' ? 'eslint.cmd' : 'eslint'; + return path.join(repoRoot, 'node_modules', '.bin', executable); +} + +function collectStrictLimitFindings(files) { + const args = [ + '--no-ignore', + '--format', + 'json', + ...strictRuleArgs.flatMap(rule => ['--rule', rule]), + ...files, + ]; + const result = run(eslintBinPath(), args, { allowFailure: true }); + if (!result.stdout.trim()) { + throw new RuntimeTruthRatchetError(`ESLint did not produce JSON output.\n${result.stderr}`); + } + + const reports = JSON.parse(result.stdout); + return reports.flatMap((report) => { + const file = normalizePath(path.relative(repoRoot, report.filePath)); + return report.messages + .filter(message => strictRuleSet.has(message.ruleId)) + .map(message => Object.freeze({ + category: classifyFile(file), + column: message.column, + file, + line: message.line, + message: message.message, + ruleId: message.ruleId, + })); + }).sort(compareFindings); +} + +function collectGenericThrowFindings(files) { + const findings = []; + for (const file of files) { + const lines = readFileSync(path.join(repoRoot, file), 'utf8').split('\n'); + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const match = genericThrowPattern.exec(line); + if (match === null) { + continue; + } + findings.push(Object.freeze({ + category: classifyFile(file), + column: match.index + 1, + file, + kind: match[1], + line: index + 1, + text: line.trim(), + })); + } + } + return findings.sort(compareFindings); +} + +function classifyFile(file) { + if (file.startsWith('test/')) { + return 'test'; + } + if (file.startsWith('benchmarks/')) { + return 'benchmark'; + } + if (sourcePrefixes.some(prefix => file.startsWith(prefix))) { + return 'source'; + } + return 'config'; +} + +function summarizeStrictFindings(findings) { + const byCategory = countBy(findings, finding => finding.category); + const byRule = countBy(findings, finding => finding.ruleId); + const byFile = {}; + + for (const finding of findings) { + byFile[finding.file] ??= { category: finding.category, total: 0, byRule: {} }; + byFile[finding.file].total += 1; + byFile[finding.file].byRule[finding.ruleId] = (byFile[finding.file].byRule[finding.ruleId] ?? 0) + 1; + } + + return deepSort({ + byCategory, + byFile, + byRule, + total: findings.length, + }); +} + +function summarizeGenericThrows(findings) { + return deepSort({ + byCategory: countBy(findings, finding => finding.category), + byFile: countBy(findings, finding => finding.file), + total: findings.length, + }); +} + +function countBy(values, selectKey) { + const counts = {}; + for (const value of values) { + const key = selectKey(value); + counts[key] = (counts[key] ?? 0) + 1; + } + return counts; +} + +function createSnapshot() { + const files = trackedCodeFiles(); + const strictFindings = collectStrictLimitFindings(files); + const genericThrowFindings = collectGenericThrowFindings(sourceCodeFiles(files)); + + return deepSort({ + version: 1, + generatedFrom: 'scripts/runtime-truth-ratchet.mjs', + strictRuleIds, + strictLimits: summarizeStrictFindings(strictFindings), + genericSourceThrows: summarizeGenericThrows(genericThrowFindings), + }); +} + +function loadBaseline() { + if (!existsSync(baselinePath)) { + throw new RuntimeTruthRatchetError(`Missing Runtime Truth ratchet baseline: ${baselinePath}`); + } + return JSON.parse(readFileSync(baselinePath, 'utf8')); +} + +function compareToBaseline(current, baseline) { + return [ + ...compareStrictLimits(current.strictLimits, baseline.strictLimits), + ...compareGenericThrows(current.genericSourceThrows, baseline.genericSourceThrows), + ]; +} + +function compareStrictLimits(current, baseline) { + const problems = []; + compareTotals(problems, 'strict total', current.total, baseline.total); + compareCountMap(problems, 'strict category', current.byCategory, baseline.byCategory); + compareCountMap(problems, 'strict rule', current.byRule, baseline.byRule); + + for (const [file, currentFile] of Object.entries(current.byFile)) { + const baselineFile = baseline.byFile[file] ?? { byRule: {}, total: 0 }; + compareTotals(problems, `strict file ${file}`, currentFile.total, baselineFile.total); + compareCountMap(problems, `strict file ${file}`, currentFile.byRule, baselineFile.byRule); + } + + return problems; +} + +function compareGenericThrows(current, baseline) { + const problems = []; + compareTotals(problems, 'generic source throw total', current.total, baseline.total); + compareCountMap(problems, 'generic source throw category', current.byCategory, baseline.byCategory); + compareCountMap(problems, 'generic source throw file', current.byFile, baseline.byFile); + return problems; +} + +function compareTotals(problems, label, current, baseline) { + if (current > baseline) { + problems.push(`${label}: ${current} > baseline ${baseline}`); + } +} + +function compareCountMap(problems, label, current = {}, baseline = {}) { + for (const [key, value] of Object.entries(current)) { + const allowed = baseline[key] ?? 0; + if (value > allowed) { + problems.push(`${label} ${key}: ${value} > baseline ${allowed}`); + } + } +} + +function writeBaseline(snapshot) { + writeFileSync(baselinePath, `${JSON.stringify(snapshot, null, 2)}\n`); +} + +function printHumanSummary(snapshot, problems) { + if (problems.length > 0) { + process.stderr.write('Runtime Truth ratchet failed.\n'); + for (const problem of problems) { + process.stderr.write(`- ${problem}\n`); + } + process.stderr.write('\nRun `npm run runtime-truth:ratchet -- --json` for the current counts.\n'); + return; + } + + process.stdout.write([ + 'Runtime Truth ratchet passed.', + `Strict limit violations: ${snapshot.strictLimits.total}`, + `Generic source throws: ${snapshot.genericSourceThrows.total}`, + `Strict by category: ${JSON.stringify(snapshot.strictLimits.byCategory)}`, + ].join('\n')); + process.stdout.write('\n'); +} + +function deepSort(value) { + if (Array.isArray(value)) { + return value.map(deepSort); + } + if (value === null || typeof value !== 'object') { + return value; + } + + return Object.fromEntries( + Object.entries(value) + .sort(([left], [right]) => compareStrings(left, right)) + .map(([key, entry]) => [key, deepSort(entry)]) + ); +} + +function normalizePath(file) { + return file.split(path.sep).join('/'); +} + +function compareStrings(left, right) { + return left.localeCompare(right); +} + +function compareFindings(left, right) { + return compareStrings(left.file, right.file) + || left.line - right.line + || left.column - right.column + || compareStrings(left.ruleId ?? left.kind ?? '', right.ruleId ?? right.kind ?? ''); +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + const snapshot = createSnapshot(); + + if (args.writeBaseline) { + writeBaseline(snapshot); + if (!args.json) { + process.stdout.write(`Wrote Runtime Truth ratchet baseline: ${normalizePath(path.relative(repoRoot, baselinePath))}\n`); + } + } + + const baseline = args.writeBaseline ? snapshot : loadBaseline(); + const problems = compareToBaseline(snapshot, baseline); + + if (args.json) { + process.stdout.write(`${JSON.stringify({ ok: problems.length === 0, problems, snapshot }, null, 2)}\n`); + } else { + printHumanSummary(snapshot, problems); + } + + if (problems.length > 0) { + process.exitCode = 1; + } +} + +main(); From e7df15dada44c73a11b3859cc96731ea034f4c7c Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 13 May 2026 20:55:14 -0700 Subject: [PATCH 04/13] feat: add think echo contract proof seam --- contracts/think-memory.graphql | 60 +++++ docs/BEARING.md | 19 +- .../think-echo-contract-proof.md | 97 +++++++ .../asap/CORE_think-echo-contract-proof.md | 16 +- ...E_think-echo-toolchain-capability-probe.md | 15 +- ...RE_think-echo-phase-0-direction-charter.md | 13 +- .../CORE_think-echo-phase-1-app-contract.md | 16 +- ...RE_think-echo-phase-2-runtime-roundtrip.md | 2 + package.json | 1 + scripts/think-echo-capability-probe.mjs | 247 ++++++++++++++++++ test/ports/think-echo-contract.test.js | 23 ++ 11 files changed, 486 insertions(+), 23 deletions(-) create mode 100644 contracts/think-memory.graphql create mode 100644 docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md create mode 100644 scripts/think-echo-capability-probe.mjs create mode 100644 test/ports/think-echo-contract.test.js diff --git a/contracts/think-memory.graphql b/contracts/think-memory.graphql new file mode 100644 index 0000000..a3bc1d5 --- /dev/null +++ b/contracts/think-memory.graphql @@ -0,0 +1,60 @@ +# Think owns these application nouns. Echo must only receive generated +# generic intents and observation requests for this family. + +directive @wes_op(name: String!) on FIELD_DEFINITION +directive @wes_footprint(reads: [String!], writes: [String!]) on FIELD_DEFINITION + +enum ThoughtIngress { + CLI + MCP + MACOS + IMPORT + TEST + UNKNOWN +} + +type CaptureProvenance { + ingress: ThoughtIngress! + source: String + url: String + application: String +} + +input CaptureProvenanceInput { + ingress: ThoughtIngress! + source: String + url: String + application: String +} + +input CaptureThoughtInput { + mindId: ID! + actorId: ID + text: String! + capturedAt: String! + provenance: CaptureProvenanceInput +} + +type ThoughtEntry { + entryId: ID! + mindId: ID! + actorId: ID + text: String! + capturedAt: String! + provenance: CaptureProvenance +} + +type CaptureThoughtResult { + entry: ThoughtEntry! +} + +type Query { + inspectThought(mindId: ID!, entryId: ID!): ThoughtEntry! + @wes_op(name: "InspectThought") +} + +type Mutation { + captureThought(input: CaptureThoughtInput!): CaptureThoughtResult! + @wes_op(name: "CaptureThought") + @wes_footprint(reads: ["ThoughtEntry"], writes: ["ThoughtEntry"]) +} diff --git a/docs/BEARING.md b/docs/BEARING.md index 719354a..3fb5f38 100644 --- a/docs/BEARING.md +++ b/docs/BEARING.md @@ -16,16 +16,24 @@ timeline - Making the git-warp v17 checkpoint repair path repeatable for local minds. - Keeping version-specific repair logic outside normal capture and read flows. -### 1. Performance Hardening +### 1. Think-on-Echo Runtime Proof +- Proving raw capture plus exact inspect as a Think-owned application contract + hosted by Echo. +- Keeping Think domain nouns in Think while Echo stays a generic dispatch and + observation substrate. +- Using the proof to shape the next store boundary refactor without switching + production capture prematurely. + +### 2. Performance Hardening - Profiling CLI capture to identify Node startup and WARP graph bottlenecks. - Benchmark harness maturation for warm-path regression detection. - Sub-second capture latency as a non-negotiable target. -### 2. Domain Integrity (SSJD) +### 3. Domain Integrity (SSJD) - Refactoring the MCP service layer to move from "shape soup" to runtime-backed domain types. - Standardizing function signatures and boundary validation across the store and CLI layers. -### 3. Orientation & Re-entry +### 4. Orientation & Re-entry - Learning where the browse and remember surfaces fail through re-entry friction tracking. - Tuning hotkey ergonomics and macOS URL scheme reliability. @@ -38,4 +46,7 @@ timeline ## Next Target -The immediate focus is **Local Mind Repairability**: turn the manual git-warp v17 checkpoint recovery into an explicit, testable repair script for broken local minds. +The immediate architecture focus is **Think-on-Echo contract proof**: prove raw +capture plus exact inspect through Echo before any default production store +switch. Local Mind Repairability remains the data-rescue lane for existing +`git-warp` minds. diff --git a/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md b/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md new file mode 100644 index 0000000..2f63139 --- /dev/null +++ b/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md @@ -0,0 +1,97 @@ +--- +title: "Think on Echo contract proof" +legend: "CORE" +cycle: "0067-think-echo-contract-proof" +source_backlog: "docs/method/backlog/asap/CORE_think-echo-contract-proof.md" +--- + +# Think on Echo contract proof + +Source backlog item: `docs/method/backlog/asap/CORE_think-echo-contract-proof.md` +Legend: CORE + +## Sponsors + +- Human: James +- Agent: Codex + +## Hill + +Think can capture and inspect one thought as a Continuum application hosted by +Echo, without moving Think product nouns into Echo and without replacing the +current production store path. + +## Ownership Split + +Think owns: + +- application/domain nouns such as thought, mind, capture provenance, inspect, + remember, browse, and session; +- `contracts/think-memory.graphql`; +- product policy and user-facing workflows; +- the adapter that chooses when an Echo-backed proof path is used. + +Echo owns: + +- generic runtime dispatch and observation; +- intent admission evidence; +- scheduler and witnessed causal substrate behavior; +- `ReadingEnvelope` and generic observation artifacts. + +Continuum owns: + +- shared runtime-boundary families; +- causal-history vocabulary that can be spoken by Echo, future Think runtimes, + and sibling runtimes; +- WARPspace coordination when the proof grows beyond a single local runtime. + +Wesley owns: + +- generated helpers, codecs, registries, operation ids, and witnesses derived + from Think-authored contracts; +- the compiler boundary that keeps GraphQL app contracts out of Echo core. + +## First Witness + +The first witness is deliberately small: + +```text +CaptureThought -> Echo dispatch_intent(...) +InspectThought -> Echo observe(...) +ReadingEnvelope + decoded ThoughtEntry +``` + +The reproducible command for this slice is: + +```sh +npm run echo:probe -- --json +``` + +That command checks the local sibling Echo/Wesley toolchain and verifies that +`contracts/think-memory.graphql` can generate Echo-facing Rust helper output. + +## Playback Questions + +### Agent + +- [x] Does Think have a local app contract for raw capture and exact inspect? +- [x] Does the contract name `mindId` before multi-mind migration work starts? +- [x] Does Think have a local probe for Echo/Wesley readiness? +- [ ] Does a runtime round trip dispatch and observe one thought through Echo? +- [ ] Does the production CLI stay on the existing store until the proof works? + +## Non-goals + +- Do not switch the CLI, MCP server, macOS app, or default store to Echo in this + phase. +- Do not migrate existing `~/.think/*` minds. +- Do not make Echo or Continuum own Think-specific schema nouns. +- Do not include remember, browse, annotations, reflection, tags, embeddings, + summaries, ranking, migration, or sibling exchange in the first proof. + +## Backlog Context + +This packet covers Phase 0 and anchors Phase 1 from the Think-on-Echo backlog +lane. Phase 2 remains the first runtime proof: a separate witness must dispatch +`CaptureThought`, observe `InspectThought`, inspect the `ReadingEnvelope`, and +decode a Think-owned `ThoughtEntry`. diff --git a/docs/method/backlog/asap/CORE_think-echo-contract-proof.md b/docs/method/backlog/asap/CORE_think-echo-contract-proof.md index 454ced4..46d0b43 100644 --- a/docs/method/backlog/asap/CORE_think-echo-contract-proof.md +++ b/docs/method/backlog/asap/CORE_think-echo-contract-proof.md @@ -64,15 +64,21 @@ outside the hot CLI path. ## Acceptance Criteria -- Each phase has its own backlog card. -- Phase 1 and Phase 2 are small enough to pull into a single METHOD cycle. -- No card requires changing the production capture path before the round-trip +- [x] Each phase has its own backlog card. +- [x] Phase 1 and Phase 2 are small enough to pull into a single METHOD cycle. +- [x] No card requires changing the production capture path before the round-trip proof exists. -- Existing `git-warp` repair work remains framed as data rescue and continuity, +- [x] Existing `git-warp` repair work remains framed as data rescue and continuity, not the long-term architecture. -- The first executable witness is raw capture plus exact inspect, not remember, +- [x] The first executable witness is raw capture plus exact inspect, not remember, browse, migration, or cross-runtime sync. +## Current Evidence + +- Phase 0 charter: `docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md` +- Phase 1 contract: `contracts/think-memory.graphql` +- Toolchain probe: `npm run echo:probe -- --json` + ## Non-Goals - Do not remove `git-warp` from Think in this lane. diff --git a/docs/method/backlog/bad-code/CORE_think-echo-toolchain-capability-probe.md b/docs/method/backlog/bad-code/CORE_think-echo-toolchain-capability-probe.md index 64739e3..ef35daf 100644 --- a/docs/method/backlog/bad-code/CORE_think-echo-toolchain-capability-probe.md +++ b/docs/method/backlog/bad-code/CORE_think-echo-toolchain-capability-probe.md @@ -27,13 +27,18 @@ assumptions from becoming folklore. ## Acceptance Criteria -- Think has a command, script, or test helper that reports the available +- [x] Think has a command, script, or test helper that reports the available Wesley/Echo contract-hosting capability in JSON. -- The probe distinguishes "generator unavailable", "Echo runtime unavailable", +- [x] The probe distinguishes "generator unavailable", "Echo runtime unavailable", "generated target unsupported", and "ready enough for Phase 2". -- The probe records exact paths or versions for any sibling checkout or local +- [x] The probe records exact paths or versions for any sibling checkout or local binary it uses. -- Phase 2 can invoke the probe or documents why it replaced the probe with a +- [x] Phase 2 can invoke the probe or documents why it replaced the probe with a stronger witness. -- Missing capabilities become explicit follow-on backlog items, not inline +- [x] Missing capabilities become explicit follow-on backlog items, not inline TODO comments in the round-trip proof. + +## Evidence + +- `scripts/think-echo-capability-probe.mjs` +- `npm run echo:probe -- --json` diff --git a/docs/method/backlog/up-next/CORE_think-echo-phase-0-direction-charter.md b/docs/method/backlog/up-next/CORE_think-echo-phase-0-direction-charter.md index 9e19271..61d47b9 100644 --- a/docs/method/backlog/up-next/CORE_think-echo-phase-0-direction-charter.md +++ b/docs/method/backlog/up-next/CORE_think-echo-phase-0-direction-charter.md @@ -43,8 +43,13 @@ or schema is added. ## Acceptance Criteria -- A design packet exists for the Think-on-Echo proof. -- The packet names the first witness command or test shape. -- The packet explicitly excludes remember, browse, migration, multi-mind UX, +- [x] A design packet exists for the Think-on-Echo proof. +- [x] The packet names the first witness command or test shape. +- [x] The packet explicitly excludes remember, browse, migration, multi-mind UX, and `git-warp` exchange from the first proof. -- The packet references this backlog phase map. +- [x] The packet references this backlog phase map. + +## Evidence + +- `docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md` +- `npm run echo:probe -- --json` diff --git a/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md b/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md index 04dad15..8135a6e 100644 --- a/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md +++ b/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md @@ -53,9 +53,15 @@ reflection outputs to the first contract. ## Acceptance Criteria -- A Think-owned GraphQL contract file exists. -- The contract supports one capture mutation and one exact inspect query. -- The contract names `mindId` explicitly, even if only `default` is used. -- Generated-artifact locations are decided but generated output is not treated +- [x] A Think-owned GraphQL contract file exists. +- [x] The contract supports one capture mutation and one exact inspect query. +- [x] The contract names `mindId` explicitly, even if only `default` is used. +- [x] Generated-artifact locations are decided but generated output is not treated as semantic source truth. -- No Echo or Continuum schema is modified to add Think domain nouns. +- [x] No Echo or Continuum schema is modified to add Think domain nouns. + +## Evidence + +- `contracts/think-memory.graphql` +- `test/ports/think-echo-contract.test.js` +- `scripts/think-echo-capability-probe.mjs` diff --git a/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md b/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md index 03a7abd..374533f 100644 --- a/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md +++ b/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md @@ -25,6 +25,8 @@ production capture path. The proof should: +0. Run `npm run echo:probe -- --json` and require + `ready_enough_for_phase_2`. 1. Build a `CaptureThought` input through generated or minimally generated contract helpers. 2. Dispatch the canonical intent through Echo. diff --git a/package.json b/package.json index 52f5d4c..84df539 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "tui": "node ./bin/think.js --browse", "benchmark:browse": "node benchmarks/browse-bootstrap.js", "benchmark:capture": "node benchmarks/capture-latency.js", + "echo:probe": "node ./scripts/think-echo-capability-probe.mjs", "repair:v17-mind": "node ./scripts/repair-v17-mind.mjs", "runtime-truth:ratchet": "node ./scripts/runtime-truth-ratchet.mjs" }, diff --git a/scripts/think-echo-capability-probe.mjs b/scripts/think-echo-capability-probe.mjs new file mode 100644 index 0000000..4b1f0b8 --- /dev/null +++ b/scripts/think-echo-capability-probe.mjs @@ -0,0 +1,247 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process'; +import { + existsSync, + mkdtempSync, + readFileSync, + rmSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const defaultEchoRepo = path.resolve(repoRoot, '..', 'echo'); +const contractPath = path.join(repoRoot, 'contracts', 'think-memory.graphql'); +const generatorMarkers = Object.freeze([ + 'pub const OP_CAPTURE_THOUGHT', + 'pub const OP_INSPECT_THOUGHT', + 'pub struct CaptureThoughtInput', + 'pub struct ThoughtEntry', + 'pub fn pack_capture_thought_intent', + 'pub fn inspect_thought_observation_request', +]); +const status = Object.freeze({ + contractUnavailable: 'contract_unavailable', + generatorUnavailable: 'generator_unavailable', + generatedTargetUnsupported: 'generated_target_unsupported', + ready: 'ready_enough_for_phase_2', + runtimeUnavailable: 'echo_runtime_unavailable', +}); + +class ThinkEchoProbeError extends Error { + constructor(message) { + super(message); + this.name = 'ThinkEchoProbeError'; + Object.freeze(this); + } +} + +function parseArgs(argv) { + const parsed = { echoRepo: process.env.THINK_ECHO_REPO ?? defaultEchoRepo, json: false }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--json') { + parsed.json = true; + continue; + } + if (arg === '--echo-repo') { + index = readEchoRepoArg(argv, index, parsed); + continue; + } + throw new ThinkEchoProbeError(`Unknown argument: ${arg}`); + } + + return Object.freeze({ + echoRepo: path.resolve(parsed.echoRepo), + json: parsed.json, + }); +} + +function readEchoRepoArg(argv, index, parsed) { + const value = argv[index + 1]; + if (typeof value === 'string' && value.length > 0) { + parsed.echoRepo = value; + return index + 1; + } + throw new ThinkEchoProbeError('--echo-repo requires a path'); +} + +function createPaths(echoRepo) { + return Object.freeze({ + contract: contractPath, + echoRepo, + generatorManifest: path.join(echoRepo, 'crates', 'echo-wesley-gen', 'Cargo.toml'), + kernelPort: path.join(echoRepo, 'crates', 'echo-wasm-abi', 'src', 'kernel_port.rs'), + runtimeManifest: path.join(echoRepo, 'crates', 'warp-wasm', 'Cargo.toml'), + wasmAbiManifest: path.join(echoRepo, 'crates', 'echo-wasm-abi', 'Cargo.toml'), + }); +} + +function runCommand(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: options.cwd ?? repoRoot, + encoding: 'utf8', + maxBuffer: 8 * 1024 * 1024, + timeout: options.timeoutMs ?? 30_000, + }); + + return Object.freeze({ + error: result.error?.message ?? null, + signal: result.signal, + status: result.status, + stderr: trimOutput(result.stderr), + stdout: trimOutput(result.stdout), + }); +} + +function trimOutput(value) { + if (typeof value !== 'string') { + return ''; + } + return value.trim().slice(0, 4_000); +} + +function checkCargo() { + const result = runCommand('cargo', ['--version']); + return Object.freeze({ + ok: result.status === 0, + result, + version: result.stdout, + }); +} + +function checkGenerator(paths, cargo) { + const echoRepoExists = existsSync(paths.echoRepo); + const manifestExists = existsSync(paths.generatorManifest); + return Object.freeze({ + echoRepoExists, + manifestExists, + ok: echoRepoExists && manifestExists && cargo.ok, + }); +} + +function checkRuntime(paths) { + const manifests = Object.freeze({ + kernelPort: existsSync(paths.kernelPort), + runtime: existsSync(paths.runtimeManifest), + wasmAbi: existsSync(paths.wasmAbiManifest), + }); + return Object.freeze({ + manifests, + ok: manifests.kernelPort && manifests.runtime && manifests.wasmAbi, + }); +} + +function runGenerator(paths, generator) { + if (generator.ok === true && existsSync(paths.contract)) { + return runGeneratorUnchecked(paths); + } + return Object.freeze({ + markers: [], + ok: false, + result: null, + skipped: true, + }); +} + +function runGeneratorUnchecked(paths) { + const tempDir = mkdtempSync(path.join(tmpdir(), 'think-echo-probe-')); + const generatedPath = path.join(tempDir, 'think_memory.generated.rs'); + const result = runCommand('cargo', generatorArgs(paths.contract, generatedPath), { + cwd: paths.echoRepo, + timeoutMs: 180_000, + }); + const markers = readGeneratedMarkers(generatedPath); + rmSync(tempDir, { force: true, recursive: true }); + + return Object.freeze({ + markers, + ok: result.status === 0 && markers.length === generatorMarkers.length, + result, + skipped: false, + }); +} + +function generatorArgs(schemaPath, outputPath) { + return Object.freeze([ + 'run', + '-q', + '-p', + 'echo-wesley-gen', + '--', + '--schema', + schemaPath, + '--out', + outputPath, + ]); +} + +function readGeneratedMarkers(generatedPath) { + if (existsSync(generatedPath) !== true) { + return []; + } + const source = readFileSync(generatedPath, 'utf8'); + return generatorMarkers.filter(marker => source.includes(marker)); +} + +function createReport(config) { + const paths = createPaths(config.echoRepo); + const contract = Object.freeze({ ok: existsSync(paths.contract), path: paths.contract }); + const cargo = checkCargo(); + const generator = checkGenerator(paths, cargo); + const runtime = checkRuntime(paths); + const generated = runGenerator(paths, generator); + const reportStatus = statusFromChecks({ contract, generated, generator, runtime }); + + return Object.freeze({ + ok: reportStatus === status.ready, + paths, + status: reportStatus, + checks: Object.freeze({ cargo, contract, generated, generator, runtime }), + }); +} + +function statusFromChecks(checks) { + if (checks.contract.ok !== true) { + return status.contractUnavailable; + } + if (checks.generator.ok !== true) { + return status.generatorUnavailable; + } + if (checks.runtime.ok !== true) { + return status.runtimeUnavailable; + } + if (checks.generated.ok === true) { + return status.ready; + } + return status.generatedTargetUnsupported; +} + +function printHuman(report) { + process.stdout.write([ + `Think Echo capability: ${report.status}`, + `Echo repo: ${report.paths.echoRepo}`, + `Contract: ${report.paths.contract}`, + `Cargo: ${report.checks.cargo.version || report.checks.cargo.result.error}`, + `Generated markers: ${report.checks.generated.markers.length}/${generatorMarkers.length}`, + ].join('\n')); + process.stdout.write('\n'); +} + +function main() { + const config = parseArgs(process.argv.slice(2)); + const report = createReport(config); + if (config.json) { + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); + } else { + printHuman(report); + } + if (report.ok !== true) { + process.exitCode = 1; + } +} + +main(); diff --git a/test/ports/think-echo-contract.test.js b/test/ports/think-echo-contract.test.js new file mode 100644 index 0000000..da2de37 --- /dev/null +++ b/test/ports/think-echo-contract.test.js @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import test from 'node:test'; + +const contractPath = path.resolve('contracts', 'think-memory.graphql'); +const requiredFragments = Object.freeze([ + 'type Mutation', + 'captureThought(input: CaptureThoughtInput!): CaptureThoughtResult!', + '@wes_op(name: "CaptureThought")', + '@wes_footprint(reads: ["ThoughtEntry"], writes: ["ThoughtEntry"])', + 'type Query', + 'inspectThought(mindId: ID!, entryId: ID!): ThoughtEntry!', + '@wes_op(name: "InspectThought")', + 'mindId: ID!', +]); + +test('Think Echo contract owns raw capture and exact inspect nouns', async () => { + const source = await readFile(contractPath, 'utf8'); + for (const fragment of requiredFragments) { + assert.ok(source.includes(fragment), `Expected contract to include ${fragment}`); + } +}); From f29694baf9a349c7e38451e8891cf4604c48f146 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 13 May 2026 21:52:56 -0700 Subject: [PATCH 05/13] docs: define think memory data model --- contracts/think-memory.graphql | 11 +- .../think-echo-contract-proof.md | 18 +- .../think-memory-data-model.md | 746 ++++++++++++++++++ docs/design/README.md | 2 + .../asap/CORE_think-echo-contract-proof.md | 6 +- .../CORE_think-echo-phase-1-app-contract.md | 14 +- ...RE_think-echo-phase-2-runtime-roundtrip.md | 5 + .../up-next/CORE_think-memory-data-model.md | 45 ++ test/ports/think-echo-model-doc.test.js | 36 + 9 files changed, 873 insertions(+), 10 deletions(-) create mode 100644 docs/design/0068-think-memory-data-model/think-memory-data-model.md create mode 100644 docs/method/backlog/up-next/CORE_think-memory-data-model.md create mode 100644 test/ports/think-echo-model-doc.test.js diff --git a/contracts/think-memory.graphql b/contracts/think-memory.graphql index a3bc1d5..befaad9 100644 --- a/contracts/think-memory.graphql +++ b/contracts/think-memory.graphql @@ -1,5 +1,12 @@ -# Think owns these application nouns. Echo must only receive generated -# generic intents and observation requests for this family. +# Provisional toolchain probe fixture. +# +# The source of truth for this family is the Think memory data model in +# docs/design/0068-think-memory-data-model/think-memory-data-model.md. +# GraphQL must express that model, not invent it. Before Phase 2 runtime +# round-trip work, revise this schema against the model checklist. +# +# Think owns these application nouns. Echo must only receive generated generic +# intents and observation requests for this family. directive @wes_op(name: String!) on FIELD_DEFINITION directive @wes_footprint(reads: [String!], writes: [String!]) on FIELD_DEFINITION diff --git a/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md b/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md index 2f63139..b304871 100644 --- a/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md +++ b/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md @@ -27,7 +27,10 @@ Think owns: - application/domain nouns such as thought, mind, capture provenance, inspect, remember, browse, and session; -- `contracts/think-memory.graphql`; +- the memory model in + `docs/design/0068-think-memory-data-model/think-memory-data-model.md`; +- `contracts/think-memory.graphql` as a generated-contract expression of that + model, currently only a provisional toolchain probe fixture; - product policy and user-facing workflows; - the adapter that chooses when an Echo-backed proof path is used. @@ -53,7 +56,8 @@ Wesley owns: ## First Witness -The first witness is deliberately small: +The first witness is deliberately small, but it must follow the pinned data +model: ```text CaptureThought -> Echo dispatch_intent(...) @@ -69,6 +73,7 @@ npm run echo:probe -- --json That command checks the local sibling Echo/Wesley toolchain and verifies that `contracts/think-memory.graphql` can generate Echo-facing Rust helper output. +Before Phase 2, the schema must be revised from the data model. ## Playback Questions @@ -77,6 +82,8 @@ That command checks the local sibling Echo/Wesley toolchain and verifies that - [x] Does Think have a local app contract for raw capture and exact inspect? - [x] Does the contract name `mindId` before multi-mind migration work starts? - [x] Does Think have a local probe for Echo/Wesley readiness? +- [x] Does Think have a data model before the runtime proof? +- [ ] Has the GraphQL contract been revised from that data model? - [ ] Does a runtime round trip dispatch and observe one thought through Echo? - [ ] Does the production CLI stay on the existing store until the proof works? @@ -92,6 +99,7 @@ That command checks the local sibling Echo/Wesley toolchain and verifies that ## Backlog Context This packet covers Phase 0 and anchors Phase 1 from the Think-on-Echo backlog -lane. Phase 2 remains the first runtime proof: a separate witness must dispatch -`CaptureThought`, observe `InspectThought`, inspect the `ReadingEnvelope`, and -decode a Think-owned `ThoughtEntry`. +lane. The data model packet now sits before Phase 2. Phase 2 remains the first +runtime proof: a separate witness must dispatch `CaptureThought`, observe +`InspectThought`, inspect the `ReadingEnvelope`, and decode a Think-owned +`ThoughtEntry` whose fields are defined by the model, not by ad hoc GraphQL. diff --git a/docs/design/0068-think-memory-data-model/think-memory-data-model.md b/docs/design/0068-think-memory-data-model/think-memory-data-model.md new file mode 100644 index 0000000..b293d1b --- /dev/null +++ b/docs/design/0068-think-memory-data-model/think-memory-data-model.md @@ -0,0 +1,746 @@ +--- +title: "Think memory data model before Echo GraphQL" +legend: "CORE" +cycle: "0068-think-memory-data-model" +source_backlog: "docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md" +--- + +# Think memory data model before Echo GraphQL + +Source backlog item: `docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md` +Legend: CORE + +## Sponsors + +- Human: James +- Agent: Codex + +## Hill + +Think pins its memory model before the Echo round trip. GraphQL expresses this +model; GraphQL does not invent it. + +GraphQL expresses this model. + +The current `contracts/think-memory.graphql` is a toolchain probe fixture until +it is reviewed against this model. Phase 2 must not prove a contract whose +nouns are not pinned here. + +## Model Doctrine + +Think is a memory product. Echo is the causal runtime. Wesley compiles Think's +application contract. Continuum supplies shared causal-history vocabulary. + +The model follows these rules: + +- Think domain nouns live in Think. +- Echo receives canonical intent bytes and returns observation evidence. +- A thought is not a graph node implementation detail. +- A capture is immutable once admitted. +- Derived facets are append-only observations over captured content. +- Query results are read products, not mutations of the thought. +- Inspect exposes a bounded, sanitized product view plus enough causal evidence + to debug the runtime path. + +## Core Questions + +### What is a thought? + +A thought is a user- or agent-authored memory claim captured into a mind. In the +product model it is represented as a `ThoughtEntry` with immutable content, +capture metadata, provenance, and causal evidence. + +The current `git-warp` graph has both `entry:*` nodes and `thought:*` content +identity nodes. The Echo model should normalize that split: + +- `ThoughtEntry` is the product-facing record. +- `ThoughtContent` is the immutable captured body and content identity. +- `ThoughtCapture` is the causal event that admitted the entry. + +### Who or what owns it? + +`Mind` owns the memory lane. `ActorId` or `WriterId` identifies who submitted +the capture. Echo owns neither; Echo witnesses admission into causal history. + +For Phase 2, `mindId` may be `default`, but it must be explicit. + +### What is immutable? + +Immutable after admission: + +- `thoughtId` +- `mindId` +- captured content bytes and content digest +- `capturedAt` +- `actorId` / `writerId` +- ingress provenance captured at the boundary +- causal admission reference + +Append-only after admission: + +- derivation artifacts +- facets and tags +- inspection/read receipts +- migration receipts +- annotations and links +- repaired or imported causal references + +### What is derived? + +Derived values include: + +- `ThoughtTags` +- `ThoughtFacets` +- keyword and topic edges +- semantic classifications +- seed-quality receipts +- session attribution +- remember ranking and match reasons +- browse windows and cursors +- reflection prompts and responses +- stats buckets + +Derived values must name their input, derivation method, version, timestamp, and +causal basis. + +### What is queryable? + +Queryable fields for the first Echo proof: + +- `mindId` +- `thoughtId` +- `capturedAt` +- content digest +- exact `thoughtId` inspection + +Queryable fields after the first proof: + +- time windows +- ingress/source +- actor/writer +- text search +- tags +- facets/classifications +- session/cursor position +- causal frontier or imported origin + +### What is causal provenance? + +Causal provenance answers: "Which witnessed event made this visible, under what +basis, from which boundary input?" + +For Echo this means a `causalRef` that can point at admission and observation +evidence without making Think depend on Echo internals in the domain model. + +### What is safe to expose through InspectThought? + +Safe by default: + +- `thoughtId` +- `mindId` +- `capturedAt` +- `content` for the requested thought +- normalized source/provenance +- public derivation summaries +- `causalRef` summary + +Not safe by default: + +- raw local filesystem paths +- unsanitized environment values +- private git remotes unless explicitly requested +- full hidden prompt or model telemetry +- sibling-runtime import secrets +- opaque Echo/Wesley implementation blobs + +## Domain Objects + +### Mind + +`Mind` is the ownership and query boundary for a coherent memory lane. + +Fields: + +- `mindId`: stable explicit id, `default` for Phase 2. +- `displayName`: optional human label. +- `owner`: human, agent, or shared owner descriptor. +- `createdAt`: first known creation time. +- `causalHome`: current runtime home, such as `git-warp` or `echo`. +- `migrationState`: current import/export posture. + +### ThoughtEntry + +`ThoughtEntry` is the product-facing memory record. + +Fields: + +- `thoughtId`: stable product id. +- `mindId`: owning mind. +- `capturedAt`: immutable capture timestamp. +- `content`: `ThoughtContent`. +- `capture`: `ThoughtCapture`. +- `provenance`: `ThoughtProvenance`. +- `tags`: `ThoughtTags`. +- `facets`: `ThoughtFacets`. +- `causalRef`: `CausalRef`. + +Phase 2 minimum: + +```text +ThoughtEntry { + thoughtId + mindId + capturedAt + content + source + metadata + causalRef +} +``` + +### ThoughtContent + +`ThoughtContent` is the immutable body of the thought. + +Fields: + +- `text`: normalized captured text. +- `mime`: usually `text/plain; charset=utf-8`. +- `digest`: content digest. +- `length`: byte or character length. +- `redactionState`: whether this view is full, redacted, or unavailable. + +### ThoughtCapture + +`ThoughtCapture` is the admitted capture event. + +Fields: + +- `captureId`: capture event id. +- `thoughtId`: captured thought. +- `actorId`: submitter. +- `writerId`: runtime writer where applicable. +- `capturedAt`: boundary timestamp. +- `ingress`: CLI, MCP, macOS URL/share/shortcut, import, repair, or test. +- `boundary`: command/tool/surface that admitted the capture. +- `causalRef`: admission evidence. + +### ThoughtInspection + +`ThoughtInspection` is a read product for a single thought. + +Fields: + +- `thought`: inspected entry. +- `inspectionId`: read receipt id if retained. +- `observerId`: observer or read surface. +- `observedAt`: read timestamp. +- `basis`: causal basis/frontier used for the read. +- `posture`: complete, residual, plural, obstructed, or redacted. +- `safeFields`: fields included in the returned product view. + +### ThoughtCursor + +`ThoughtCursor` is a stable read position for list-like surfaces. + +Fields: + +- `mindId` +- `sortKey` +- `thoughtId` +- `basis` +- `direction`: newer, older, same-session, search, or topic. + +### ThoughtQuery + +`ThoughtQuery` is a request for a read view. + +Fields: + +- `mindId` +- `kind`: recent, remember, browse, stats, inspect, topics, annotations, or + reflection-context. +- `text`: optional query text. +- `timeWindow`: optional time bound. +- `facets`: optional filters. +- `cursor`: optional position. +- `limit`: bounded count. +- `basis`: optional causal frontier. + +### ThoughtProvenance + +`ThoughtProvenance` is normalized boundary context. + +Fields: + +- `ingress` +- `sourceApp` +- `sourceUrl` +- `ambientCwd` +- `ambientGitRoot` +- `ambientGitRemote` +- `ambientGitBranch` +- `importOrigin` +- `repairOrigin` + +Only safe normalized fields are included in default inspect output. + +### ThoughtTags + +`ThoughtTags` are user- or system-visible labels. + +Kinds: + +- user labels +- keyword labels +- topic labels +- classification labels +- project labels + +Tags are derived or asserted. They do not mutate captured content. + +### ThoughtFacets + +`ThoughtFacets` are structured derived dimensions used for search, browse, and +reflection. + +Initial facets: + +- `classification`: question, decision, observation, action_item, idea, + reference, unclassified. +- `topics`: promoted topic nodes. +- `keywords`: inverted-index terms. +- `session`: session attribution. +- `reflectability`: seed-quality verdict. +- `relations`: annotations, links, reflection responses, and evolution edges. + +### CausalRef + +`CausalRef` is the product-facing reference to runtime evidence. + +Fields: + +- `runtime`: `git-warp`, `echo`, or `imported`. +- `coordinate`: runtime-neutral coordinate string or object. +- `intentId`: Echo intent/admission id when available. +- `entryNodeId`: legacy `git-warp` node id when imported or dual-read. +- `basis`: read basis/frontier. +- `witness`: witness or receipt reference. +- `migration`: optional migration/import receipt. + +## End-To-End Flow + +```mermaid +flowchart TD + User["Human or agent"] --> Surface["CLI / MCP / macOS / import"] + Surface --> Boundary["Boundary normalizes input"] + Boundary --> Model["Construct Think runtime model"] + Model --> Contract["GraphQL contract generated from model"] + Contract --> Wesley["Wesley helpers and codecs"] + Wesley --> Echo["Echo dispatch_intent"] + Echo --> Causal["Witnessed causal history"] + Causal --> Observe["Echo observe"] + Observe --> Envelope["ReadingEnvelope"] + Envelope --> Decode["Generated/application decode"] + Decode --> Product["Think product read"] + + Boundary --> Legacy["Current git-warp store"] + Legacy --> Migration["Migration replay/export"] + Migration --> Echo +``` + +## Capture And Inspect Sequence + +```mermaid +sequenceDiagram + participant Client as Think CLI/MCP/macOS + participant Boundary as Think Boundary Adapter + participant Model as Think Memory Model + participant Gen as Wesley Generated Client + participant Echo as Echo KernelPort + participant Runtime as Echo Runtime + participant Read as Inspect Adapter + + Client->>Boundary: capture(text, source context) + Boundary->>Model: new ThoughtCapture(input) + Model-->>Boundary: ThoughtEntry draft + Boundary->>Gen: CaptureThought variables + Gen->>Echo: dispatch_intent(EINT bytes) + Echo->>Runtime: admit causal input + Runtime-->>Echo: admission evidence + Echo-->>Boundary: DispatchResponse + Boundary-->>Client: thoughtId + causalRef + + Client->>Read: inspectThought(thoughtId) + Read->>Gen: InspectThought variables + Gen->>Echo: observe(ObservationRequest) + Echo->>Runtime: resolve basis and payload + Runtime-->>Echo: ObservationArtifact + Echo-->>Gen: ReadingEnvelope + payload + Gen-->>Read: decoded ThoughtEntry + Read-->>Client: safe ThoughtInspection +``` + +## Class Model + +```mermaid +classDiagram + class Mind { + +MindId mindId + +String displayName + +Owner owner + +RuntimeId causalHome + +MigrationState migrationState + } + + class ThoughtEntry { + +ThoughtId thoughtId + +MindId mindId + +Instant capturedAt + +ThoughtContent content + +ThoughtCapture capture + +ThoughtProvenance provenance + +ThoughtTags tags + +ThoughtFacets facets + +CausalRef causalRef + } + + class ThoughtContent { + +String text + +String mime + +Digest digest + +Number length + +RedactionState redactionState + } + + class ThoughtCapture { + +CaptureId captureId + +ActorId actorId + +WriterId writerId + +Ingress ingress + +Instant capturedAt + +CausalRef causalRef + } + + class ThoughtInspection { + +InspectionId inspectionId + +ObserverId observerId + +Instant observedAt + +ReadPosture posture + +CausalBasis basis + } + + class ThoughtQuery { + +MindId mindId + +QueryKind kind + +String text + +TimeWindow timeWindow + +Number limit + +CausalBasis basis + } + + class ThoughtCursor { + +MindId mindId + +String sortKey + +ThoughtId thoughtId + +CausalBasis basis + +Direction direction + } + + class ThoughtProvenance { + +Ingress ingress + +String sourceApp + +Url sourceUrl + +AmbientContext ambientContext + +ImportOrigin importOrigin + } + + class ThoughtTags { + +String[] user + +String[] keywords + +String[] topics + +String[] classifications + } + + class ThoughtFacets { + +Classification classification + +SessionAttribution session + +SeedQuality reflectability + +Relation[] relations + } + + class CausalRef { + +RuntimeId runtime + +String coordinate + +String intentId + +String basis + +String witness + } + + Mind "1" --> "*" ThoughtEntry + ThoughtEntry "1" --> "1" ThoughtContent + ThoughtEntry "1" --> "1" ThoughtCapture + ThoughtEntry "1" --> "1" ThoughtProvenance + ThoughtEntry "1" --> "1" ThoughtTags + ThoughtEntry "1" --> "1" ThoughtFacets + ThoughtEntry "1" --> "1" CausalRef + ThoughtInspection "1" --> "1" ThoughtEntry + ThoughtQuery "1" --> "0..1" ThoughtCursor +``` + +## Entity Relationship Model + +```mermaid +erDiagram + MIND ||--o{ THOUGHT_ENTRY : owns + THOUGHT_ENTRY ||--|| THOUGHT_CONTENT : has + THOUGHT_ENTRY ||--|| THOUGHT_CAPTURE : admitted_by + THOUGHT_ENTRY ||--|| THOUGHT_PROVENANCE : came_from + THOUGHT_ENTRY ||--o{ THOUGHT_TAG : labeled_by + THOUGHT_ENTRY ||--o{ THOUGHT_FACET : described_by + THOUGHT_ENTRY ||--o{ THOUGHT_INSPECTION : observed_as + THOUGHT_ENTRY ||--o{ ANNOTATION : annotated_by + THOUGHT_ENTRY ||--o{ REFLECTION_RESPONSE : seed_of + THOUGHT_ENTRY ||--o{ MIGRATION_RECEIPT : imported_by + THOUGHT_QUERY ||--o{ THOUGHT_INSPECTION : produces + THOUGHT_CURSOR ||--o{ THOUGHT_QUERY : positions + CAUSAL_REF ||--o{ THOUGHT_CAPTURE : witnesses + CAUSAL_REF ||--o{ THOUGHT_INSPECTION : supports + + MIND { + string mindId PK + string displayName + string owner + string causalHome + } + + THOUGHT_ENTRY { + string thoughtId PK + string mindId FK + string capturedAt + string causalRefId FK + } + + THOUGHT_CONTENT { + string contentDigest PK + string thoughtId FK + string mime + string text + } + + THOUGHT_CAPTURE { + string captureId PK + string thoughtId FK + string actorId + string ingress + } + + THOUGHT_PROVENANCE { + string provenanceId PK + string thoughtId FK + string sourceApp + string sourceUrl + } + + THOUGHT_TAG { + string tagId PK + string thoughtId FK + string kind + string value + } + + THOUGHT_FACET { + string facetId PK + string thoughtId FK + string kind + string value + } + + THOUGHT_INSPECTION { + string inspectionId PK + string thoughtId FK + string posture + string basis + } + + CAUSAL_REF { + string causalRefId PK + string runtime + string coordinate + string witness + } +``` + +## Feature Coverage + +| Feature | Model object | Query shape | Derived or immutable | +| --- | --- | --- | --- | +| Capture | `ThoughtCapture`, `ThoughtEntry` | create by input | Immutable | +| Ingest/stdin | `ThoughtCapture` | create by input | Immutable | +| macOS URL/share/shortcut | `ThoughtProvenance` | create by input | Immutable normalized provenance | +| MCP capture | `ThoughtCapture` | create by input | Immutable | +| Recent | `ThoughtQuery`, `ThoughtCursor` | time/cursor window | Read product | +| Inspect | `ThoughtInspection` | exact thought id | Read product | +| Remember | `ThoughtQuery`, `ThoughtFacets` | text/ambient scope | Derived ranking | +| Browse | `ThoughtCursor`, `ThoughtQuery` | cursor/session/topic | Read product | +| Stats | `ThoughtQuery` | time buckets | Derived aggregation | +| Reflect | `ThoughtFacets`, `ReflectionResponse` | seed thought | Derived response | +| Annotate | `Annotation` relation | target thought | Append-only | +| Auto tags | `ThoughtTags` | keyword/topic | Derived | +| Semantic parse | `ThoughtFacets` | classification | Derived | +| Sessions | `ThoughtFacets` | temporal proximity | Derived | +| Repair | `MigrationReceipt`, `CausalRef` | mind/runtime | Append-only evidence | +| Migration | `MigrationReceipt`, `CausalRef` | import/export | Append-only evidence | +| Prompt metrics | read telemetry | time/model/tool filters | Operational, not core thought | + +## GraphQL Contract Rules + +The next GraphQL revision must be a projection of this model. + +Allowed Phase 2 GraphQL nouns: + +- `Mind` +- `ThoughtEntry` +- `ThoughtContent` +- `ThoughtCapture` +- `ThoughtProvenance` +- `CausalRef` +- `CaptureThought` +- `InspectThought` + +Deferred GraphQL nouns: + +- `ThoughtTags` +- `ThoughtFacets` +- `ThoughtQuery` +- `ThoughtCursor` +- annotations +- reflection responses +- migration receipts + +Required Phase 2 contract fields: + +- `thoughtId` +- `mindId` +- `capturedAt` +- `content.text` +- `content.digest` +- `provenance.ingress` +- `provenance.sourceApp` +- `provenance.sourceUrl` +- `causalRef.runtime` +- `causalRef.coordinate` +- `causalRef.witness` + +## Echo Causal WARP Graph Preparation + +Think should not model Echo as a graph database. Think should model causal +memory and let Echo host the witnessed runtime. + +The application graph projection for Echo is: + +```text +Mind + owns ThoughtEntry +ThoughtEntry + has ThoughtContent + admitted_by ThoughtCapture + came_from ThoughtProvenance + witnessed_by CausalRef + described_by ThoughtFacet* + labeled_by ThoughtTag* + observed_as ThoughtInspection* +``` + +Echo receives: + +- canonical `CaptureThought` intent bytes; +- generated operation ids and variables; +- observation requests for `InspectThought`; +- retained payloads or readings as generic runtime artifacts. + +Echo does not receive: + +- Think-specific handwritten APIs; +- direct `remember`, `browse`, or `reflect` methods; +- local filesystem path semantics; +- `git-warp` checkpoint semantics. + +## Migration Plan From git-warp To Echo + +### Phase M0: Freeze The Model + +- Treat this document as source truth. +- Mark current GraphQL as provisional until revised against this model. +- Add model-level tests before runtime round-trip code. + +### Phase M1: Read Legacy Minds Into Model Objects + +- Map `entry:*` capture nodes to `ThoughtEntry`. +- Map attached text content to `ThoughtContent`. +- Map `captureIngress`, `captureSourceApp`, `captureSourceURL`, and ambient + context fields to `ThoughtProvenance`. +- Map `thought:*` nodes and `expresses` edges to canonical `thoughtId`. +- Map `artifact:*` receipts to `ThoughtFacets`. +- Preserve `entry:*` ids inside `CausalRef.entryNodeId`. + +### Phase M2: Generate GraphQL From Model + +- Revise `contracts/think-memory.graphql` to match `ThoughtEntry`, + `ThoughtContent`, `ThoughtCapture`, `ThoughtProvenance`, and `CausalRef`. +- Run `npm run echo:probe -- --json`. +- Keep generated files out of semantic source truth unless a later build step + requires checked-in fixtures. + +### Phase M3: Echo Round Trip + +- Dispatch one `CaptureThought`. +- Observe one `InspectThought`. +- Verify the `ReadingEnvelope` posture. +- Decode the payload into `ThoughtEntry`. +- Assert content, provenance, and causal evidence. + +### Phase M4: Dual Read / Shadow Write + +- Keep `git-warp` authoritative. +- Optionally shadow-write captures to Echo. +- Compare ids, timestamps, content digests, provenance, latency, and inspect + results. +- Failure in Echo must not break local capture. + +### Phase M5: Replay Import + +- Export legacy captures in stable chronological order. +- Replay captures into Echo as `CaptureThought` intents with migration + provenance. +- Retain legacy ids and checkpoint refs in migration receipts. +- Verify counts, digest equality, and inspect parity. + +### Phase M6: Product Cutover + +- Make Echo opt-in for a named mind first. +- Require read parity for recent, inspect, remember, browse, stats, and + annotations before default cutover. +- Keep `git-warp` repair and export tools as data-rescue paths. + +## Open Model Decisions + +- Whether `thoughtId` is content-derived, capture-event-derived, or both via + separate `contentDigest` and `captureId`. +- Whether `actorId` and `writerId` remain separate product fields. +- How much ambient project context is safe by default in shared or imported + minds. +- Whether retained inspections become first-class product receipts or only + runtime evidence. +- How GraphQL schema generation should be governed so the model remains source + truth. + +## Acceptance + +This slice is complete when: + +- Phase 2 is documented as blocked on this model. +- The existing GraphQL fixture is marked provisional. +- The next GraphQL revision has a concrete field checklist. +- The migration path from `git-warp` to Echo has named phases and parity + checks. diff --git a/docs/design/README.md b/docs/design/README.md index 499807b..7c633e4 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -34,6 +34,8 @@ This review is meant to answer five questions: - [`0027-m5-additional-ingress-surfaces.md`](./0027-m5-additional-ingress-surfaces.md): milestone note for extending raw capture into additional human and agent ingress surfaces without changing the sacred capture core. The explicit-ingest and URL-capture slices are implemented, and the next planned slice is selected-text / share-based capture. - [`0028-m5-shortcuts-url-capture.md`](./0028-m5-shortcuts-url-capture.md): implemented `M5` slice note for local URL-triggered capture and Apple Shortcuts as a thin human wrapper over the same raw-capture core. - [`0029-m5-selected-text-share-capture.md`](./0029-m5-selected-text-share-capture.md): current `M5` slice note for explicit selected-text and share/send capture on macOS without drifting into clipping or import semantics. +- [`0067-think-echo-contract-proof/think-echo-contract-proof.md`](./0067-think-echo-contract-proof/think-echo-contract-proof.md): Think-on-Echo ownership split and proof boundary. +- [`0068-think-memory-data-model/think-memory-data-model.md`](./0068-think-memory-data-model/think-memory-data-model.md): source-of-truth memory data model that GraphQL must express before the Echo round trip. - [`ROADMAP.md`](./ROADMAP.md): milestone sequence, hill mapping, exit criteria, and review checkpoints. ## Archived Slice History diff --git a/docs/method/backlog/asap/CORE_think-echo-contract-proof.md b/docs/method/backlog/asap/CORE_think-echo-contract-proof.md index 46d0b43..f3f79c1 100644 --- a/docs/method/backlog/asap/CORE_think-echo-contract-proof.md +++ b/docs/method/backlog/asap/CORE_think-echo-contract-proof.md @@ -76,9 +76,13 @@ outside the hot CLI path. ## Current Evidence - Phase 0 charter: `docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md` -- Phase 1 contract: `contracts/think-memory.graphql` +- Data model source truth: `docs/design/0068-think-memory-data-model/think-memory-data-model.md` +- Phase 1 provisional contract: `contracts/think-memory.graphql` - Toolchain probe: `npm run echo:probe -- --json` +The provisional GraphQL contract must be revised from the data model before the +Phase 2 Echo runtime round trip. + ## Non-Goals - Do not remove `git-warp` from Think in this lane. diff --git a/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md b/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md index 8135a6e..b7a38b4 100644 --- a/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md +++ b/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md @@ -15,6 +15,12 @@ Legend: CORE Author the smallest Think-owned contract family needed for a raw capture and exact inspect round trip. +Update: `contracts/think-memory.graphql` exists and proves the local +Echo/Wesley toolchain can compile the family, but it is now explicitly a +provisional probe fixture. The semantic source of truth is the data model in +`docs/design/0068-think-memory-data-model/think-memory-data-model.md`. +Before Phase 2, revise this GraphQL contract from the model. + The likely first file is: ```text @@ -54,11 +60,15 @@ reflection outputs to the first contract. ## Acceptance Criteria - [x] A Think-owned GraphQL contract file exists. -- [x] The contract supports one capture mutation and one exact inspect query. -- [x] The contract names `mindId` explicitly, even if only `default` is used. +- [ ] The contract supports one capture mutation and one exact inspect query + using model-derived fields. +- [x] The provisional contract names `mindId` explicitly, even if only + `default` is used. - [x] Generated-artifact locations are decided but generated output is not treated as semantic source truth. - [x] No Echo or Continuum schema is modified to add Think domain nouns. +- [ ] The contract exposes `ThoughtContent`, `ThoughtCapture`, + `ThoughtProvenance`, and `CausalRef` from the pinned model. ## Evidence diff --git a/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md b/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md index 374533f..4419eaf 100644 --- a/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md +++ b/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md @@ -5,6 +5,7 @@ blocks: - CORE_think-echo-phase-4-read-observers - CORE_think-echo-phase-5-migration-and-sibling-exchange blocked_by: + - CORE_think-memory-data-model - CORE_think-echo-phase-1-app-contract - CORE_think-echo-toolchain-capability-probe --- @@ -25,6 +26,9 @@ production capture path. The proof should: +0. Confirm `docs/design/0068-think-memory-data-model/think-memory-data-model.md` + is the source truth for the contract. +0. Revise `contracts/think-memory.graphql` from that model. 0. Run `npm run echo:probe -- --json` and require `ready_enough_for_phase_2`. 1. Build a `CaptureThought` input through generated or minimally generated @@ -46,6 +50,7 @@ usable migration path. ## Constraints - Do not switch the CLI, MCP server, macOS app, or default store to Echo. +- Do not treat the current GraphQL probe fixture as semantic source truth. - Do not require existing `~/.think/*` minds to migrate. - Do not depend on `git-warp` in the hot proof path. - Do not hand-roll runtime bytes if the current Wesley/Echo toolchain can diff --git a/docs/method/backlog/up-next/CORE_think-memory-data-model.md b/docs/method/backlog/up-next/CORE_think-memory-data-model.md new file mode 100644 index 0000000..52bcfef --- /dev/null +++ b/docs/method/backlog/up-next/CORE_think-memory-data-model.md @@ -0,0 +1,45 @@ +--- +id: CORE_think-memory-data-model +blocks: + - CORE_think-echo-phase-2-runtime-roundtrip +blocked_by: [] +--- + +# CORE - Think memory data model before Echo round trip + +Legend: CORE + +## Idea + +Pin the Think memory data model before proving the Echo runtime round trip. +GraphQL must express the model, not become the source of domain nouns. + +The design packet lives at: + +```text +docs/design/0068-think-memory-data-model/think-memory-data-model.md +``` + +## Why + +The first Echo proof should not succeed against an accidental schema. Think +needs a model that answers what a thought is, who owns it, what is immutable, +what is derived, what is queryable, what carries causal provenance, and what is +safe to expose through `InspectThought`. + +## Acceptance Criteria + +- [x] Define `Mind`, `ThoughtEntry`, `ThoughtContent`, `ThoughtCapture`, + `ThoughtInspection`, `ThoughtCursor`, `ThoughtQuery`, + `ThoughtProvenance`, `ThoughtTags`, and `ThoughtFacets`. +- [x] Include flow, sequence, class, and entity-relationship diagrams. +- [x] Define the minimum Phase 2 model fields. +- [x] Name what is immutable, derived, queryable, causal, and safe to inspect. +- [x] Add a migration plan from the current `git-warp` graph to Echo. +- [x] Mark the current GraphQL contract as provisional until it is revised + against the model. + +## Follow-up + +Phase 1 GraphQL should be revised from the data model before Phase 2 dispatch +and observe code is written. diff --git a/test/ports/think-echo-model-doc.test.js b/test/ports/think-echo-model-doc.test.js new file mode 100644 index 0000000..2aa3a52 --- /dev/null +++ b/test/ports/think-echo-model-doc.test.js @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import test from 'node:test'; + +const modelDocPath = path.resolve( + 'docs', + 'design', + '0068-think-memory-data-model', + 'think-memory-data-model.md' +); +const requiredFragments = Object.freeze([ + 'Mind', + 'ThoughtEntry', + 'ThoughtContent', + 'ThoughtCapture', + 'ThoughtInspection', + 'ThoughtCursor', + 'ThoughtQuery', + 'ThoughtProvenance', + 'ThoughtTags', + 'ThoughtFacets', + '```mermaid\nflowchart TD', + '```mermaid\nsequenceDiagram', + '```mermaid\nclassDiagram', + '```mermaid\nerDiagram', + 'Migration Plan From git-warp To Echo', + 'GraphQL expresses this model', +]); + +test('Think Echo data model is pinned before the runtime round trip', async () => { + const source = await readFile(modelDocPath, 'utf8'); + for (const fragment of requiredFragments) { + assert.ok(source.includes(fragment), `Expected model doc to include ${fragment}`); + } +}); From 138ee36c9b7d675f46ae6764bdd22424af4d8141 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 13 May 2026 23:43:11 -0700 Subject: [PATCH 06/13] docs: clarify sponsor label convention --- .../think-echo-contract-proof.md | 4 ++-- .../think-memory-data-model.md | 4 ++-- docs/design/README.md | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md b/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md index b304871..04a6947 100644 --- a/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md +++ b/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md @@ -12,8 +12,8 @@ Legend: CORE ## Sponsors -- Human: James -- Agent: Codex +- Human: sponsored human user +- Agent: sponsored agent user ## Hill diff --git a/docs/design/0068-think-memory-data-model/think-memory-data-model.md b/docs/design/0068-think-memory-data-model/think-memory-data-model.md index b293d1b..8e18f36 100644 --- a/docs/design/0068-think-memory-data-model/think-memory-data-model.md +++ b/docs/design/0068-think-memory-data-model/think-memory-data-model.md @@ -12,8 +12,8 @@ Legend: CORE ## Sponsors -- Human: James -- Agent: Codex +- Human: sponsored human user +- Agent: sponsored agent user ## Hill diff --git a/docs/design/README.md b/docs/design/README.md index 7c633e4..5187aaf 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -201,6 +201,13 @@ Approve these docs only if they preserve the core doctrine: If a design choice improves technical sophistication but adds friction to capture, it should be rejected. +## Sponsor Labels + +`Human` and `Agent` sponsor labels are sponsored-user abstractions, not literal +people or tool identities. New design docs should use abstract roles such as +`sponsored human user` and `sponsored agent user` unless a specific person or +agent identity is itself part of the design. + ## Out Of Scope For This Review - final UI mockups From 1245c062fcdbb255a0cc97956b271a3976071cb0 Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 14 May 2026 22:53:05 -0700 Subject: [PATCH 07/13] feat: derive Think Echo contract from model --- CHANGELOG.md | 2 + contracts/think-memory.graphql | 109 ++++++++++++++---- .../think-echo-contract-proof.md | 2 +- .../asap/CORE_think-echo-contract-proof.md | 5 +- .../CORE_think-echo-phase-1-app-contract.md | 13 +-- ...RE_think-echo-phase-2-runtime-roundtrip.md | 2 +- scripts/think-echo-capability-probe.mjs | 4 + test/ports/think-echo-contract.test.js | 15 ++- 8 files changed, 114 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e317a2..c6fb3a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ Release discipline: ## Unreleased +- added a Runtime Truth ratchet to `npm run lint`, including strict-limit baseline enforcement and a generic source-error guard +- added the Think-on-Echo proof seam with a model-derived GraphQL memory contract, Echo/Wesley capability probe, and pinned Think memory data model - fixed MCP tool result envelopes so structured content matches each registered output schema again - fixed checkpoint-backed reads to use public `@git-stunts/git-warp` package exports instead of private `node_modules` internals - fixed cached writer retries across raw capture follow-through, annotations, reflect writes, migrations, and enrichment patches diff --git a/contracts/think-memory.graphql b/contracts/think-memory.graphql index befaad9..5b135fe 100644 --- a/contracts/think-memory.graphql +++ b/contracts/think-memory.graphql @@ -1,12 +1,10 @@ -# Provisional toolchain probe fixture. +# Think-owned memory contract for the Echo proof. # -# The source of truth for this family is the Think memory data model in -# docs/design/0068-think-memory-data-model/think-memory-data-model.md. -# GraphQL must express that model, not invent it. Before Phase 2 runtime -# round-trip work, revise this schema against the model checklist. +# Source truth: +# docs/design/0068-think-memory-data-model/think-memory-data-model.md # -# Think owns these application nouns. Echo must only receive generated generic -# intents and observation requests for this family. +# GraphQL expresses the Think memory model. Echo must only receive generated +# generic intents and observation requests for this family. directive @wes_op(name: String!) on FIELD_DEFINITION directive @wes_footprint(reads: [String!], writes: [String!]) on FIELD_DEFINITION @@ -20,43 +18,110 @@ enum ThoughtIngress { UNKNOWN } -type CaptureProvenance { +enum CausalRuntime { + ECHO + GIT_WARP + IMPORTED +} + +enum RedactionState { + FULL + REDACTED + UNAVAILABLE +} + +type ThoughtContent { + text: String! + mime: String! + digest: String! + length: Int + redactionState: RedactionState! +} + +input ThoughtContentInput { + text: String! + mime: String! + digest: String! + length: Int +} + +type CausalRef { + runtime: CausalRuntime! + coordinate: String + intentId: String + entryNodeId: String + basis: String + witness: String + migration: String +} + +type ThoughtProvenance { ingress: ThoughtIngress! - source: String - url: String - application: String + sourceApp: String + sourceUrl: String + ambientCwd: String + ambientGitRoot: String + ambientGitRemote: String + ambientGitBranch: String + importOrigin: String + repairOrigin: String } -input CaptureProvenanceInput { +input ThoughtProvenanceInput { ingress: ThoughtIngress! - source: String - url: String - application: String + sourceApp: String + sourceUrl: String + ambientCwd: String + ambientGitRoot: String + ambientGitRemote: String + ambientGitBranch: String + importOrigin: String + repairOrigin: String } -input CaptureThoughtInput { +type ThoughtCapture { + captureId: ID! + thoughtId: ID! mindId: ID! actorId: ID - text: String! + writerId: ID capturedAt: String! - provenance: CaptureProvenanceInput + ingress: ThoughtIngress! + boundary: String + causalRef: CausalRef! } type ThoughtEntry { - entryId: ID! + thoughtId: ID! + mindId: ID! + capturedAt: String! + content: ThoughtContent! + capture: ThoughtCapture! + provenance: ThoughtProvenance! + source: String + metadataJson: String + causalRef: CausalRef! +} + +input CaptureThoughtInput { mindId: ID! actorId: ID - text: String! + writerId: ID capturedAt: String! - provenance: CaptureProvenance + content: ThoughtContentInput! + provenance: ThoughtProvenanceInput! + source: String + metadataJson: String } type CaptureThoughtResult { entry: ThoughtEntry! + capture: ThoughtCapture! + causalRef: CausalRef! } type Query { - inspectThought(mindId: ID!, entryId: ID!): ThoughtEntry! + inspectThought(mindId: ID!, thoughtId: ID!): ThoughtEntry! @wes_op(name: "InspectThought") } diff --git a/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md b/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md index 04a6947..0526dda 100644 --- a/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md +++ b/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md @@ -83,7 +83,7 @@ Before Phase 2, the schema must be revised from the data model. - [x] Does the contract name `mindId` before multi-mind migration work starts? - [x] Does Think have a local probe for Echo/Wesley readiness? - [x] Does Think have a data model before the runtime proof? -- [ ] Has the GraphQL contract been revised from that data model? +- [x] Has the GraphQL contract been revised from that data model? - [ ] Does a runtime round trip dispatch and observe one thought through Echo? - [ ] Does the production CLI stay on the existing store until the proof works? diff --git a/docs/method/backlog/asap/CORE_think-echo-contract-proof.md b/docs/method/backlog/asap/CORE_think-echo-contract-proof.md index f3f79c1..ec13a50 100644 --- a/docs/method/backlog/asap/CORE_think-echo-contract-proof.md +++ b/docs/method/backlog/asap/CORE_think-echo-contract-proof.md @@ -77,12 +77,9 @@ outside the hot CLI path. - Phase 0 charter: `docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md` - Data model source truth: `docs/design/0068-think-memory-data-model/think-memory-data-model.md` -- Phase 1 provisional contract: `contracts/think-memory.graphql` +- Phase 1 model-derived contract: `contracts/think-memory.graphql` - Toolchain probe: `npm run echo:probe -- --json` -The provisional GraphQL contract must be revised from the data model before the -Phase 2 Echo runtime round trip. - ## Non-Goals - Do not remove `git-warp` from Think in this lane. diff --git a/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md b/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md index b7a38b4..88662cd 100644 --- a/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md +++ b/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md @@ -15,11 +15,10 @@ Legend: CORE Author the smallest Think-owned contract family needed for a raw capture and exact inspect round trip. -Update: `contracts/think-memory.graphql` exists and proves the local -Echo/Wesley toolchain can compile the family, but it is now explicitly a -provisional probe fixture. The semantic source of truth is the data model in -`docs/design/0068-think-memory-data-model/think-memory-data-model.md`. -Before Phase 2, revise this GraphQL contract from the model. +Update: `contracts/think-memory.graphql` now expresses the pinned data model in +`docs/design/0068-think-memory-data-model/think-memory-data-model.md` for the +Phase 2 proof. The GraphQL file is still a contract expression, not semantic +source truth. The likely first file is: @@ -60,14 +59,14 @@ reflection outputs to the first contract. ## Acceptance Criteria - [x] A Think-owned GraphQL contract file exists. -- [ ] The contract supports one capture mutation and one exact inspect query +- [x] The contract supports one capture mutation and one exact inspect query using model-derived fields. - [x] The provisional contract names `mindId` explicitly, even if only `default` is used. - [x] Generated-artifact locations are decided but generated output is not treated as semantic source truth. - [x] No Echo or Continuum schema is modified to add Think domain nouns. -- [ ] The contract exposes `ThoughtContent`, `ThoughtCapture`, +- [x] The contract exposes `ThoughtContent`, `ThoughtCapture`, `ThoughtProvenance`, and `CausalRef` from the pinned model. ## Evidence diff --git a/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md b/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md index 4419eaf..3fa7c72 100644 --- a/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md +++ b/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md @@ -28,7 +28,7 @@ The proof should: 0. Confirm `docs/design/0068-think-memory-data-model/think-memory-data-model.md` is the source truth for the contract. -0. Revise `contracts/think-memory.graphql` from that model. +0. Use the model-derived `contracts/think-memory.graphql` contract. 0. Run `npm run echo:probe -- --json` and require `ready_enough_for_phase_2`. 1. Build a `CaptureThought` input through generated or minimally generated diff --git a/scripts/think-echo-capability-probe.mjs b/scripts/think-echo-capability-probe.mjs index 4b1f0b8..e60bc2d 100644 --- a/scripts/think-echo-capability-probe.mjs +++ b/scripts/think-echo-capability-probe.mjs @@ -19,6 +19,10 @@ const generatorMarkers = Object.freeze([ 'pub const OP_INSPECT_THOUGHT', 'pub struct CaptureThoughtInput', 'pub struct ThoughtEntry', + 'pub struct ThoughtContent', + 'pub struct ThoughtCapture', + 'pub struct ThoughtProvenance', + 'pub struct CausalRef', 'pub fn pack_capture_thought_intent', 'pub fn inspect_thought_observation_request', ]); diff --git a/test/ports/think-echo-contract.test.js b/test/ports/think-echo-contract.test.js index da2de37..faea1cd 100644 --- a/test/ports/think-echo-contract.test.js +++ b/test/ports/think-echo-contract.test.js @@ -5,17 +5,26 @@ import test from 'node:test'; const contractPath = path.resolve('contracts', 'think-memory.graphql'); const requiredFragments = Object.freeze([ + 'type ThoughtContent', + 'type ThoughtCapture', + 'type ThoughtProvenance', + 'type CausalRef', + 'content: ThoughtContent!', + 'capture: ThoughtCapture!', + 'provenance: ThoughtProvenance!', + 'causalRef: CausalRef!', + 'thoughtId: ID!', + 'mindId: ID!', 'type Mutation', 'captureThought(input: CaptureThoughtInput!): CaptureThoughtResult!', '@wes_op(name: "CaptureThought")', '@wes_footprint(reads: ["ThoughtEntry"], writes: ["ThoughtEntry"])', 'type Query', - 'inspectThought(mindId: ID!, entryId: ID!): ThoughtEntry!', + 'inspectThought(mindId: ID!, thoughtId: ID!): ThoughtEntry!', '@wes_op(name: "InspectThought")', - 'mindId: ID!', ]); -test('Think Echo contract owns raw capture and exact inspect nouns', async () => { +test('Think Echo contract expresses the pinned memory model', async () => { const source = await readFile(contractPath, 'utf8'); for (const fragment of requiredFragments) { assert.ok(source.includes(fragment), `Expected contract to include ${fragment}`); From 210337e660484e7933fc923ea61ed619699f85a6 Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 14 May 2026 23:38:12 -0700 Subject: [PATCH 08/13] docs: define Think Echo integration plan --- .../think-echo-contract-proof.md | 3 +- .../think-echo-integration-plan.md | 423 ++++++++++++++++++ docs/design/README.md | 1 + .../CORE_think-echo-integration-plan.md | 39 ++ ...RE_think-echo-phase-2-runtime-roundtrip.md | 2 + test/ports/think-echo-integration-doc.test.js | 33 ++ 6 files changed, 499 insertions(+), 2 deletions(-) create mode 100644 docs/design/0069-think-echo-integration-plan/think-echo-integration-plan.md create mode 100644 docs/method/backlog/up-next/CORE_think-echo-integration-plan.md create mode 100644 test/ports/think-echo-integration-doc.test.js diff --git a/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md b/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md index 0526dda..142166c 100644 --- a/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md +++ b/docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md @@ -30,7 +30,7 @@ Think owns: - the memory model in `docs/design/0068-think-memory-data-model/think-memory-data-model.md`; - `contracts/think-memory.graphql` as a generated-contract expression of that - model, currently only a provisional toolchain probe fixture; + model; - product policy and user-facing workflows; - the adapter that chooses when an Echo-backed proof path is used. @@ -73,7 +73,6 @@ npm run echo:probe -- --json That command checks the local sibling Echo/Wesley toolchain and verifies that `contracts/think-memory.graphql` can generate Echo-facing Rust helper output. -Before Phase 2, the schema must be revised from the data model. ## Playback Questions diff --git a/docs/design/0069-think-echo-integration-plan/think-echo-integration-plan.md b/docs/design/0069-think-echo-integration-plan/think-echo-integration-plan.md new file mode 100644 index 0000000..f4aeb01 --- /dev/null +++ b/docs/design/0069-think-echo-integration-plan/think-echo-integration-plan.md @@ -0,0 +1,423 @@ +--- +title: "Think Echo integration plan" +legend: "CORE" +cycle: "0069-think-echo-integration-plan" +source_backlog: "docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md" +--- + +# Think Echo integration plan + +Source backlog item: `docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md` +Legend: CORE + +## Sponsors + +- Human: sponsored human user +- Agent: sponsored agent user + +## Hill + +Think integrates with Echo by putting a model-derived contract and generated +adapter behind a Think-owned memory port. The current `git-warp` store remains +authoritative until Echo proves capture, inspect, read parity, shadow-write +safety, and migration replay. + +The first executable proof is still deliberately small: + +```text +CaptureThought -> Echo dispatch_intent(...) +InspectThought -> Echo observe(...) +ReadingEnvelope + decoded ThoughtEntry +``` + +## Design Inputs + +- `docs/design/0068-think-memory-data-model/think-memory-data-model.md` is the + memory model source of truth. +- `contracts/think-memory.graphql` is the model-derived Wesley/Echo contract. +- `scripts/think-echo-capability-probe.mjs` proves the sibling Echo/Wesley + toolchain can generate helper output. +- `/Users/james/git/echo/docs/architecture/application-contract-hosting.md` + defines the Echo boundary: applications own nouns, Wesley emits helpers, + Echo receives generic intents and observations. + +## Non-goals + +- Do not switch default capture to Echo during the first runtime proof. +- Do not migrate existing `~/.think/*` minds until replay parity is measured. +- Do not expose Echo internals through CLI, MCP, or macOS product APIs. +- Do not teach Echo Think-specific nouns. +- Do not make GraphQL the source of truth over the data model. +- Do not make `git-warp` repair and export work disappear; it remains the + continuity path for existing minds. + +## Runtime Boundary + +Think should depend on a product-facing `MemoryRuntimePort`, not directly on +Echo, `git-warp`, generated code, filesystems, process globals, or sibling repo +paths. + +```text +Think product workflow + -> MemoryRuntimePort + -> GitWarpMemoryRuntime | EchoMemoryRuntime + -> concrete runtime adapter +``` + +Minimum port methods: + +- `captureThought(input): CaptureThoughtOutcome` +- `inspectThought(mindId, thoughtId): ThoughtInspection` +- `probe(): RuntimeCapabilityReport` + +Later methods: + +- `recent(query): ThoughtInspection[]` +- `remember(query): RememberResult` +- `browse(cursor): BrowseWindow` +- `annotate(command): AnnotationReceipt` +- `migrate(plan): MigrationReceipt` + +## Architecture + +```mermaid +flowchart TD + Product["Think CLI / MCP / macOS workflows"] + Model["Think memory model"] + Port["MemoryRuntimePort"] + Legacy["GitWarpMemoryRuntime"] + EchoAdapter["EchoMemoryRuntime"] + Contract["contracts/think-memory.graphql"] + Wesley["Wesley generated helpers"] + Echo["Echo KernelPort"] + Runtime["Echo runtime"] + Envelope["ReadingEnvelope"] + Decode["Think decode / normalize"] + + Product --> Model + Model --> Port + Port --> Legacy + Port --> EchoAdapter + EchoAdapter --> Contract + Contract --> Wesley + Wesley --> Echo + Echo --> Runtime + Runtime --> Envelope + Envelope --> Decode + Decode --> Model + + Legacy -. "authoritative until cutover" .-> Product + EchoAdapter -. "proof / opt-in / shadow-write" .-> Product +``` + +## Integration Layers + +### Layer 1: Think Model + +Owns domain classes and invariants: + +- `Mind` +- `ThoughtEntry` +- `ThoughtContent` +- `ThoughtCapture` +- `ThoughtInspection` +- `ThoughtProvenance` +- `CausalRef` + +Construction validates invariants before data crosses into runtime adapters. +The model is portable and must not import Echo, Wesley, `git-warp`, Node +filesystem APIs, or process globals. + +### Layer 2: Contract Expression + +Owns GraphQL contract source: + +- `contracts/think-memory.graphql` +- `CaptureThought` +- `InspectThought` +- DTOs that directly express the model + +The contract is allowed to use GraphQL-friendly shapes, but it must stay a +projection of the model. Generated output is build/proof material, not semantic +source truth. + +### Layer 3: Generated Boundary + +Owns Wesley-generated helper usage: + +- operation ids +- canonical variables +- EINT packing +- observation request construction +- generated DTO encode/decode helpers + +Temporary generated files should live under a test/proof temp directory unless +a later build step deliberately checks in fixtures. + +### Layer 4: Echo Adapter + +Owns runtime calls: + +- `dispatch_intent(intent_bytes)` +- `observe(observation_request)` +- reading-envelope verification +- conversion from Echo evidence to `CausalRef` +- conversion from decoded payload to Think model objects + +This adapter is a boundary adapter. It may import generated helpers and Echo +ABI types. Core model code must not. + +### Layer 5: Product Composition + +Owns runtime selection: + +- default: `GitWarpMemoryRuntime` +- proof command/test: `EchoMemoryRuntime` +- later opt-in: named mind with Echo runtime +- later shadow-write: dual runtime adapter + +Composition roots inject the chosen runtime. Product workflows do not call +concrete runtime constructors inside core logic. + +## Capture Sequence + +```mermaid +sequenceDiagram + participant Client as Think product workflow + participant Model as Think model constructor + participant Port as MemoryRuntimePort + participant EchoAdapter as EchoMemoryRuntime + participant Gen as Wesley generated helpers + participant Echo as Echo KernelPort + + Client->>Model: build CaptureThought command + Model-->>Client: validated ThoughtCapture input + Client->>Port: captureThought(input) + Port->>EchoAdapter: captureThought(input) + EchoAdapter->>Gen: encode CaptureThought variables + Gen-->>EchoAdapter: EINT bytes + EchoAdapter->>Echo: dispatch_intent(bytes) + Echo-->>EchoAdapter: DispatchResponse + EchoAdapter-->>Port: CaptureThoughtOutcome + CausalRef + Port-->>Client: product-safe capture receipt +``` + +## Inspect Sequence + +```mermaid +sequenceDiagram + participant Client as Think product workflow + participant Port as MemoryRuntimePort + participant EchoAdapter as EchoMemoryRuntime + participant Gen as Wesley generated helpers + participant Echo as Echo KernelPort + participant Model as Think model constructor + + Client->>Port: inspectThought(mindId, thoughtId) + Port->>EchoAdapter: inspectThought(mindId, thoughtId) + EchoAdapter->>Gen: build InspectThought observation request + Gen-->>EchoAdapter: ObservationRequest + EchoAdapter->>Echo: observe(request) + Echo-->>EchoAdapter: ObservationArtifact + EchoAdapter->>EchoAdapter: verify ReadingEnvelope posture + EchoAdapter->>Gen: decode payload + Gen-->>EchoAdapter: generated ThoughtEntry DTO + EchoAdapter->>Model: construct ThoughtEntry + Model-->>Port: validated ThoughtInspection + Port-->>Client: product-safe inspection +``` + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> ModelPinned + ModelPinned --> ContractDerived + ContractDerived --> ToolchainReady + ToolchainReady --> RuntimeWitnessed + RuntimeWitnessed --> ShadowWrite + ShadowWrite --> ReplayImport + ReplayImport --> OptInMind + OptInMind --> DefaultRuntime + + RuntimeWitnessed --> ContractDerived: model mismatch + ShadowWrite --> GitWarpAuthoritative: parity failure + ReplayImport --> GitWarpAuthoritative: import failure + GitWarpAuthoritative --> ShadowWrite: fix and retry +``` + +## Adapter Class Shape + +```mermaid +classDiagram + class MemoryRuntimePort { + <> + +probe() + +captureThought(input) + +inspectThought(mindId, thoughtId) + } + + class GitWarpMemoryRuntime { + +probe() + +captureThought(input) + +inspectThought(mindId, thoughtId) + } + + class EchoMemoryRuntime { + -generatedClient + -kernel + +probe() + +captureThought(input) + +inspectThought(mindId, thoughtId) + } + + class ThinkMemoryModel { + +createCapture(input) + +createThoughtEntry(dto) + +createInspection(dto) + } + + class EchoReadingVerifier { + +verifyComplete(artifact) + +toCausalRef(artifact) + } + + MemoryRuntimePort <|.. GitWarpMemoryRuntime + MemoryRuntimePort <|.. EchoMemoryRuntime + EchoMemoryRuntime --> ThinkMemoryModel + EchoMemoryRuntime --> EchoReadingVerifier +``` + +## Rollout Phases + +### Phase 0: Capability Probe + +Status: implemented. + +- Run `npm run echo:probe -- --json`. +- Confirm Echo sibling checkout exists. +- Confirm `echo-wesley-gen` can compile `contracts/think-memory.graphql`. +- Confirm generated markers include Phase 2 model DTOs and helpers. + +### Phase 1: Runtime Witness + +- Generate helper output in a temp proof crate or test harness. +- Create an in-memory or local Echo kernel. +- Dispatch one `CaptureThought`. +- Observe one `InspectThought`. +- Verify `ReadingEnvelope` completeness. +- Decode a model-correct `ThoughtEntry`. +- Assert content digest, text, provenance, mind id, and causal ref. + +### Phase 2: Think Adapter + +- Introduce `EchoMemoryRuntime`. +- Keep it outside production capture path. +- Make proof tests instantiate it via constructor injection. +- Convert generated DTOs into constructor-validated Think model objects. + +### Phase 3: Product Experiment + +- Add an opt-in proof command or hidden flag. +- Keep `GitWarpMemoryRuntime` authoritative. +- Record Echo admission and read evidence in proof output. +- Do not fail normal local capture on Echo failure. + +### Phase 4: Shadow Write + +- Write capture to `git-warp` first. +- Attempt Echo capture second. +- Compare model fields: + - `thoughtId` + - `mindId` + - `capturedAt` + - content digest + - provenance + - causal references +- Log mismatch receipts. + +### Phase 5: Replay Import + +- Export legacy `git-warp` captures in chronological order. +- Convert each capture to model objects. +- Replay as `CaptureThought` intents into Echo. +- Preserve legacy ids and checkpoint refs in migration receipts. +- Verify counts and content digests. + +### Phase 6: Opt-in Mind Runtime + +- Allow a named mind to choose Echo as its runtime. +- Gate on read parity for `recent`, `inspect`, and bounded `remember`. +- Keep export and rescue paths available. + +### Phase 7: Default Runtime Cutover + +- Make Echo default only after: + - capture parity is stable; + - read parity is stable; + - migration replay is documented and tested; + - rollback/export path exists; + - local capture latency remains acceptable. + +## Verification Gates + +| Gate | Command or proof | Required result | +| --- | --- | --- | +| Contract generation | `npm run echo:probe -- --json` | `ready_enough_for_phase_2` | +| Model docs | `node --test test/ports/think-echo-model-doc.test.js` | Pass | +| Contract docs | `node --test test/ports/think-echo-contract.test.js` | Pass | +| Local fast gate | `npm run test:fast` | Pass | +| Runtime witness | future Phase 2 command | Capture + inspect round trip | +| Shadow write | future comparison report | No model-field drift | +| Migration replay | future import witness | Count and digest parity | + +## Failure Handling + +### Generator Unavailable + +Report `generator_unavailable` from the probe. Do not hand-roll GraphQL +operation ids unless the missing generated helper work is explicitly logged and +bounded. + +### Runtime Unavailable + +Report `echo_runtime_unavailable`. Keep the current `git-warp` path unchanged. + +### Reading Incomplete + +Reject the inspection result. Return an explicit typed outcome that preserves +the `ReadingEnvelope` posture for debugging. Do not present incomplete payloads +as product truth. + +### Model Mismatch + +Fail the proof. The fix is to align model, GraphQL, generated DTO mapping, or +adapter decoding. Do not patch around mismatch in product code. + +### Shadow Drift + +Keep `git-warp` authoritative. Record the drift as a comparison receipt and +block runtime cutover. + +## Open Decisions + +- Whether Phase 2 should live as a Node script that shells into Echo generation + or as a Rust proof crate under Echo with Think-owned contract input. +- Whether generated helper output should remain temp-only or gain checked-in + fixture status for deterministic PR review. +- Whether `thoughtId` is digest-derived, capture-event-derived, or represented + as both `thoughtId` and `captureId`. +- Which product command first exposes the proof result. +- How much `CausalRef` detail is safe in default CLI/MCP output. + +## Completion Criteria + +Echo integration is achieved when: + +- Think model objects are the source of truth. +- GraphQL expresses the model. +- Wesley generation is deterministic and verified locally. +- Echo capture and inspect round trip through generic dispatch and observe. +- Product code depends on `MemoryRuntimePort`, not runtime-specific globals. +- `git-warp` and Echo can run side by side during shadow-write and migration. +- Cutover is gated by parity evidence, not by optimism. diff --git a/docs/design/README.md b/docs/design/README.md index 5187aaf..146dd8e 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -36,6 +36,7 @@ This review is meant to answer five questions: - [`0029-m5-selected-text-share-capture.md`](./0029-m5-selected-text-share-capture.md): current `M5` slice note for explicit selected-text and share/send capture on macOS without drifting into clipping or import semantics. - [`0067-think-echo-contract-proof/think-echo-contract-proof.md`](./0067-think-echo-contract-proof/think-echo-contract-proof.md): Think-on-Echo ownership split and proof boundary. - [`0068-think-memory-data-model/think-memory-data-model.md`](./0068-think-memory-data-model/think-memory-data-model.md): source-of-truth memory data model that GraphQL must express before the Echo round trip. +- [`0069-think-echo-integration-plan/think-echo-integration-plan.md`](./0069-think-echo-integration-plan/think-echo-integration-plan.md): integration path for putting Echo behind a Think-owned memory runtime port. - [`ROADMAP.md`](./ROADMAP.md): milestone sequence, hill mapping, exit criteria, and review checkpoints. ## Archived Slice History diff --git a/docs/method/backlog/up-next/CORE_think-echo-integration-plan.md b/docs/method/backlog/up-next/CORE_think-echo-integration-plan.md new file mode 100644 index 0000000..7382c19 --- /dev/null +++ b/docs/method/backlog/up-next/CORE_think-echo-integration-plan.md @@ -0,0 +1,39 @@ +--- +id: CORE_think-echo-integration-plan +blocks: + - CORE_think-echo-phase-2-runtime-roundtrip +blocked_by: + - CORE_think-memory-data-model + - CORE_think-echo-phase-1-app-contract +--- + +# CORE - Think Echo integration plan + +Legend: CORE + +## Idea + +Define how Think will integrate with Echo after the memory model and GraphQL +contract are pinned. + +The design packet lives at: + +```text +docs/design/0069-think-echo-integration-plan/think-echo-integration-plan.md +``` + +## Why + +The Echo runtime witness needs an architecture target before code starts. The +integration path must keep Think product workflows behind a Think-owned memory +port, keep Echo as a generic causal substrate, and keep `git-warp` +authoritative until parity and migration evidence are real. + +## Acceptance Criteria + +- [x] Define the `MemoryRuntimePort` boundary. +- [x] Define the Echo adapter layers and generated-helper boundary. +- [x] Include capture and inspect sequence diagrams. +- [x] Define rollout phases from capability probe through default cutover. +- [x] Define verification gates and failure handling. +- [x] Keep production capture unchanged until the runtime witness passes. diff --git a/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md b/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md index 3fa7c72..636fa6e 100644 --- a/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md +++ b/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md @@ -28,6 +28,8 @@ The proof should: 0. Confirm `docs/design/0068-think-memory-data-model/think-memory-data-model.md` is the source truth for the contract. +0. Follow `docs/design/0069-think-echo-integration-plan/think-echo-integration-plan.md` + for the runtime port and adapter shape. 0. Use the model-derived `contracts/think-memory.graphql` contract. 0. Run `npm run echo:probe -- --json` and require `ready_enough_for_phase_2`. diff --git a/test/ports/think-echo-integration-doc.test.js b/test/ports/think-echo-integration-doc.test.js new file mode 100644 index 0000000..20375c9 --- /dev/null +++ b/test/ports/think-echo-integration-doc.test.js @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import test from 'node:test'; + +const integrationDocPath = path.resolve( + 'docs', + 'design', + '0069-think-echo-integration-plan', + 'think-echo-integration-plan.md' +); +const requiredFragments = Object.freeze([ + 'MemoryRuntimePort', + 'EchoMemoryRuntime', + 'GitWarpMemoryRuntime', + 'CaptureThought -> Echo dispatch_intent', + 'InspectThought -> Echo observe', + 'ReadingEnvelope + decoded ThoughtEntry', + '```mermaid\nflowchart TD', + '```mermaid\nsequenceDiagram', + '```mermaid\nstateDiagram-v2', + '```mermaid\nclassDiagram', + 'Verification Gates', + 'Failure Handling', + 'Rollout Phases', +]); + +test('Think Echo integration plan defines the runtime port and proof path', async () => { + const source = await readFile(integrationDocPath, 'utf8'); + for (const fragment of requiredFragments) { + assert.ok(source.includes(fragment), `Expected integration doc to include ${fragment}`); + } +}); From 4dd6e3ebe00c3e37314ccf61f3b3f77c475d5619 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 15 May 2026 01:16:32 -0700 Subject: [PATCH 09/13] fix: return MCP capture after local save --- CHANGELOG.md | 1 + .../bad-code/CORE_large-mind-read-timeouts.md | 24 +++ src/mcp/service.js | 150 +++++++++++++----- src/policies.js | 8 - test/ports/mcp-service.test.js | 56 +++++++ 5 files changed, 188 insertions(+), 51 deletions(-) create mode 100644 docs/method/backlog/bad-code/CORE_large-mind-read-timeouts.md create mode 100644 test/ports/mcp-service.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index c6fb3a3..f32c613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Release discipline: - added a Runtime Truth ratchet to `npm run lint`, including strict-limit baseline enforcement and a generic source-error guard - added the Think-on-Echo proof seam with a model-derived GraphQL memory contract, Echo/Wesley capability probe, and pinned Think memory data model +- fixed MCP capture to return after the raw local save when post-save graph follow-through is slow, matching the CLI trapdoor behavior - fixed MCP tool result envelopes so structured content matches each registered output schema again - fixed checkpoint-backed reads to use public `@git-stunts/git-warp` package exports instead of private `node_modules` internals - fixed cached writer retries across raw capture follow-through, annotations, reflect writes, migrations, and enrichment patches diff --git a/docs/method/backlog/bad-code/CORE_large-mind-read-timeouts.md b/docs/method/backlog/bad-code/CORE_large-mind-read-timeouts.md new file mode 100644 index 0000000..e52a002 --- /dev/null +++ b/docs/method/backlog/bad-code/CORE_large-mind-read-timeouts.md @@ -0,0 +1,24 @@ +# CORE: Large-mind read paths exceed agent timeout budgets + +## Problem + +Large repaired git-warp minds can exceed normal MCP client timeout budgets on +read-heavy paths such as `recent`, `stats`, and `doctor`. The Claude mind at +`~/.think/claude` was observed with roughly 43k loose Git objects and read +commands taking 14-21 seconds even after the raw capture path itself was fast. + +## Why It Matters + +Capture can return after local raw save, but agents still need reliable re-entry +surfaces. If reads routinely exceed client budgets, agents experience Think as +unavailable even when the underlying mind is intact. + +## Acceptance Criteria + +- [ ] Add a deterministic large-mind fixture or synthetic benchmark for MCP read + timeout budgets. +- [ ] Establish target budgets for `recent`, `stats`, `doctor`, and `remember` + against repaired checkpoint-backed minds. +- [ ] Document and automate safe maintenance for high-loose-object minds. +- [ ] Prefer checkpoint-backed bounded reads where whole-graph observer startup is + not required. diff --git a/src/mcp/service.js b/src/mcp/service.js index 19790fd..0ea6d22 100644 --- a/src/mcp/service.js +++ b/src/mcp/service.js @@ -2,7 +2,6 @@ import { runDiagnostics } from '../doctor.js'; import { ValidationError, NotFoundError, GraphError } from '../errors.js'; import { ensureGitRepo, hasGitRepo, lsRemote, pushWarpRefs } from '../git.js'; import { getLocalRepoDir, getThinkDir, getUpstreamUrl } from '../paths.js'; -import { capturePolicy } from '../policies.js'; import { normalizeCaptureProvenance } from '../capture-provenance.js'; import { getCaptureAmbientContext, getAmbientProjectContext } from '../project-context.js'; import { @@ -35,62 +34,127 @@ import { StatsOutcome, } from './result.js'; -export async function captureThought(text, { provenance = null } = {}) { +const CAPTURE_FOLLOWTHROUGH_TIMEOUT_MS = 3_000; +const CAPTURE_FOLLOWTHROUGH_DEFERRED = Object.freeze({ status: 'deferred' }); +const CAPTURE_FOLLOWTHROUGH_DEFERRED_WARNING = + 'Capture followthrough deferred; raw thought saved locally before derived graph updates completed.'; +const NO_GRAPH_MIGRATION_STATUS = Object.freeze({ + currentGraphModelVersion: null, + requiredGraphModelVersion: null, + migrationRequired: false, +}); + +const defaultCaptureDeps = Object.freeze({ + ensureGitRepo, + finalizeCapturedThought, + getAmbientProjectContext, + getCaptureAmbientContext, + getCwd: () => process.cwd(), + getGraphModelStatus, + getLocalRepoDir, + getUpstreamUrl, + graphName: GRAPH_NAME, + hasGitRepo, + pushWarpRefs, + saveRawCapture, + waitForFollowthrough: waitForCaptureFollowthrough, +}); + +export const captureThought = createCaptureThoughtService(); + +export function createCaptureThoughtService(deps = defaultCaptureDeps) { + return async function captureThoughtWithDeps(text, { provenance = null } = {}) { + const thought = normalizeThoughtText(text); + const captureProvenance = normalizeCaptureProvenance(provenance); + const repoDir = deps.getLocalRepoDir(); + const repoAlreadyExists = deps.hasGitRepo(repoDir); + + await deps.ensureGitRepo(repoDir); + const entry = await deps.saveRawCapture(repoDir, thought, { + provenance: captureProvenance, + ambientContext: deps.getCaptureAmbientContext(deps.getCwd()), + }); + const followthrough = await runCaptureFollowthrough(deps, repoDir, entry.id, repoAlreadyExists); + const backupStatus = await runCaptureBackup(deps, repoDir); + + return new CaptureOutcome({ + backupStatus, + entryId: entry.id, + migration: followthrough.migration, + repoBootstrapped: !repoAlreadyExists, + status: 'saved_locally', + warnings: followthrough.warnings, + }); + }; +} + +function normalizeThoughtText(text) { const thought = String(text ?? ''); if (thought.trim() === '') { throw new ValidationError('Thought cannot be empty'); } - const captureProvenance = normalizeCaptureProvenance(provenance); + return thought; +} - const repoDir = getLocalRepoDir(); - const repoAlreadyExists = hasGitRepo(repoDir); +async function runCaptureFollowthrough(deps, repoDir, entryId, repoAlreadyExists) { + const warnings = []; + const followthroughPromise = buildCaptureFollowthrough(deps, repoDir, entryId, repoAlreadyExists); + + try { + const followthrough = await deps.waitForFollowthrough(followthroughPromise); + if (!isDeferredCaptureFollowthrough(followthrough)) { + return { migration: followthrough?.migration ?? null, warnings }; + } + } catch (error) { + warnings.push(error instanceof Error ? error.message : String(error)); + return { migration: null, warnings }; + } - await ensureGitRepo(repoDir); + followthroughPromise.catch(() => {}); + warnings.push(CAPTURE_FOLLOWTHROUGH_DEFERRED_WARNING); + return { migration: null, warnings }; +} +async function buildCaptureFollowthrough(deps, repoDir, entryId, repoAlreadyExists) { const graphStatus = repoAlreadyExists - ? await getGraphModelStatus(repoDir) - : { - currentGraphModelVersion: null, - requiredGraphModelVersion: null, - migrationRequired: false, - }; - - const { entry, migration, warnings } = await capturePolicy.execute(async () => { - const saved = await saveRawCapture(repoDir, thought, { - provenance: captureProvenance, - ambientContext: getCaptureAmbientContext(process.cwd()), - }); - let mig = null; - const warns = []; - - try { - const followthrough = await finalizeCapturedThought(repoDir, saved.id, { - migrateIfNeeded: graphStatus.migrationRequired, - ambientContext: getAmbientProjectContext(process.cwd()), - }); - mig = followthrough.migration ?? null; - } catch (error) { - warns.push(error instanceof Error ? error.message : String(error)); - } + ? await deps.getGraphModelStatus(repoDir) + : NO_GRAPH_MIGRATION_STATUS; - return { entry: saved, migration: mig, warnings: warns }; + return await deps.finalizeCapturedThought(repoDir, entryId, { + migrateIfNeeded: graphStatus.migrationRequired, + ambientContext: deps.getAmbientProjectContext(deps.getCwd()), }); +} - const upstreamUrl = getUpstreamUrl(); - let backupStatus = 'skipped'; - if (upstreamUrl) { - const backedUp = await pushWarpRefs(repoDir, upstreamUrl, GRAPH_NAME); - backupStatus = backedUp ? 'backed_up' : 'pending'; +function isDeferredCaptureFollowthrough(followthrough) { + return followthrough?.status === CAPTURE_FOLLOWTHROUGH_DEFERRED.status; +} + +async function runCaptureBackup(deps, repoDir) { + const upstreamUrl = deps.getUpstreamUrl(); + if (!upstreamUrl) { + return 'skipped'; } - return new CaptureOutcome({ - backupStatus, - entryId: entry.id, - migration, - repoBootstrapped: !repoAlreadyExists, - status: 'saved_locally', - warnings, + const backedUp = await deps.pushWarpRefs(repoDir, upstreamUrl, deps.graphName); + return backedUp ? 'backed_up' : 'pending'; +} + +async function waitForCaptureFollowthrough(followthroughPromise) { + let timeoutId = null; + const timeout = new Promise((resolve) => { + timeoutId = setTimeout( + () => resolve(CAPTURE_FOLLOWTHROUGH_DEFERRED), + CAPTURE_FOLLOWTHROUGH_TIMEOUT_MS + ); + timeoutId.unref?.(); }); + + try { + return await Promise.race([followthroughPromise, timeout]); + } finally { + clearTimeout(timeoutId); + } } export async function listRecentThoughts({ count = null, query = null } = {}) { diff --git a/src/policies.js b/src/policies.js index 5839044..52fbb59 100644 --- a/src/policies.js +++ b/src/policies.js @@ -4,8 +4,6 @@ const PUSH_TIMEOUT_MS = 1500; const PUSH_RETRIES = 1; const PUSH_RETRY_DELAY_MS = 100; -const CAPTURE_TIMEOUT_MS = 10_000; - /** * Upstream backup push: timeout per attempt, then retry once with * exponential backoff and full jitter. Transient network failures @@ -22,9 +20,3 @@ export function createPushPolicy({ shouldRetry, onTimeout, onRetry } = {}) { onRetry, })); } - -/** - * MCP capture service: timeout around the full WARP graph write + - * finalization path so a hung store operation doesn't block forever. - */ -export const capturePolicy = Policy.timeout(CAPTURE_TIMEOUT_MS); diff --git a/test/ports/mcp-service.test.js b/test/ports/mcp-service.test.js new file mode 100644 index 0000000..6d51a11 --- /dev/null +++ b/test/ports/mcp-service.test.js @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { createCaptureThoughtService } from '../../src/mcp/service.js'; + +test('MCP capture returns saved locally when post-save followthrough defers', async () => { + const calls = []; + const captureThought = createCaptureThoughtService(createDeferredFollowthroughDeps(calls)); + + const outcome = await captureThought('Claude should get a fast raw save', { + provenance: { ingress: 'selected_text', sourceApp: 'Claude' }, + }); + + assert.deepEqual(calls, [ + 'repoDir', + 'hasGitRepo', + 'ensureGitRepo', + 'getCwd', + 'getCaptureAmbientContext', + 'saveRawCapture', + 'getGraphModelStatus', + 'waitForFollowthrough', + 'getUpstreamUrl', + ]); + assert.equal(outcome.structuredContent.status, 'saved_locally'); + assert.equal(outcome.structuredContent.entryId, 'entry:test-claude'); + assert.equal(outcome.structuredContent.backupStatus, 'skipped'); + assert.equal(outcome.structuredContent.migration, null); + assert.equal(outcome.structuredContent.warnings.length, 1); +}); + +function createDeferredFollowthroughDeps(calls) { + return { + ensureGitRepo: (repoDir) => record(calls, 'ensureGitRepo', repoDir), + finalizeCapturedThought: () => record(calls, 'finalizeCapturedThought'), + getAmbientProjectContext: () => record(calls, 'getAmbientProjectContext'), + getCaptureAmbientContext: (cwd) => ({ cwd: record(calls, 'getCaptureAmbientContext', cwd) }), + getCwd: () => record(calls, 'getCwd', '/tmp/project'), + getGraphModelStatus: () => { + record(calls, 'getGraphModelStatus'); + return new Promise(() => {}); + }, + getLocalRepoDir: () => record(calls, 'repoDir', '/tmp/think-claude'), + getUpstreamUrl: () => record(calls, 'getUpstreamUrl', ''), + graphName: 'think', + hasGitRepo: () => record(calls, 'hasGitRepo', true), + pushWarpRefs: () => record(calls, 'pushWarpRefs', true), + saveRawCapture: () => record(calls, 'saveRawCapture', { id: 'entry:test-claude' }), + waitForFollowthrough: () => record(calls, 'waitForFollowthrough', { status: 'deferred' }), + }; +} + +function record(calls, label, value) { + calls.push(label); + return value; +} From b52e42343f9b0eb0d32b3bd6df6fc2f29d54f97c Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 15 May 2026 01:44:17 -0700 Subject: [PATCH 10/13] fix: skip graph migration when current --- CHANGELOG.md | 1 + src/cli/commands/capture.js | 15 ++++++++++++- src/mcp/service.js | 40 ++++++++++++++++++++++++++++------ test/ports/mcp-service.test.js | 30 ++++++++++++++++++++++++- 4 files changed, 77 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f32c613..74446ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Release discipline: - added a Runtime Truth ratchet to `npm run lint`, including strict-limit baseline enforcement and a generic source-error guard - added the Think-on-Echo proof seam with a model-derived GraphQL memory contract, Echo/Wesley capability probe, and pinned Think memory data model - fixed MCP capture to return after the raw local save when post-save graph follow-through is slow, matching the CLI trapdoor behavior +- fixed graph migration commands to return a fast no-op when the graph model is already current - fixed MCP tool result envelopes so structured content matches each registered output schema again - fixed checkpoint-backed reads to use public `@git-stunts/git-warp` package exports instead of private `node_modules` internals - fixed cached writer retries across raw capture follow-through, annotations, reflect writes, migrations, and enrichment patches diff --git a/src/cli/commands/capture.js b/src/cli/commands/capture.js index 1c378d2..3d6bcff 100644 --- a/src/cli/commands/capture.js +++ b/src/cli/commands/capture.js @@ -162,7 +162,10 @@ export async function runMigrateGraph(output, reporter) { return 1; } - const result = await migrateGraphModel(repoDir); + const status = await getGraphModelStatus(repoDir); + const result = status.migrationRequired + ? await migrateGraphModel(repoDir) + : createNoopMigrationResult(status); reporter.event('migrate_graph.done', result); if (output.json) { @@ -187,6 +190,16 @@ export async function runMigrateGraph(output, reporter) { return 0; } +function createNoopMigrationResult(status) { + return Object.freeze({ + changed: false, + graphModelVersion: status.currentGraphModelVersion ?? status.requiredGraphModelVersion, + edgesAdded: 0, + edgesRemoved: 0, + metadataUpdated: false, + }); +} + async function readStdinText(stdin) { if (!stdin || stdin.isTTY) { return ''; diff --git a/src/mcp/service.js b/src/mcp/service.js index 0ea6d22..1ed18c3 100644 --- a/src/mcp/service.js +++ b/src/mcp/service.js @@ -308,14 +308,30 @@ export async function checkThinkHealthForMcp() { return new HealthOutcome(diagnostics); } -export async function migrateThoughtGraph() { - const repoDir = getLocalRepoDir(); - if (!hasGitRepo(repoDir)) { - throw new GraphError('No local thought repo found to migrate'); - } +const defaultMigrationDeps = Object.freeze({ + getGraphModelStatus, + getLocalRepoDir, + hasGitRepo, + migrateGraphModel, +}); - const result = await migrateGraphModel(repoDir); - return new MigrationOutcome(result); +export const migrateThoughtGraph = createMigrateThoughtGraphService(); + +export function createMigrateThoughtGraphService(deps = defaultMigrationDeps) { + return async function migrateThoughtGraphWithDeps() { + const repoDir = deps.getLocalRepoDir(); + if (!deps.hasGitRepo(repoDir)) { + throw new GraphError('No local thought repo found to migrate'); + } + + const status = await deps.getGraphModelStatus(repoDir); + if (!status.migrationRequired) { + return new MigrationOutcome(createNoopMigrationResult(status)); + } + + const result = await deps.migrateGraphModel(repoDir); + return new MigrationOutcome(result); + }; } async function assertGraphReady(command) { @@ -345,6 +361,16 @@ function buildRememberScope({ cwd, query, limit, brief }) { }; } +function createNoopMigrationResult(status) { + return Object.freeze({ + changed: false, + graphModelVersion: status.currentGraphModelVersion ?? status.requiredGraphModelVersion, + edgesAdded: 0, + edgesRemoved: 0, + metadataUpdated: false, + }); +} + function toMcpEntry(entry) { if (!entry) { return null; diff --git a/test/ports/mcp-service.test.js b/test/ports/mcp-service.test.js index 6d51a11..640f9d4 100644 --- a/test/ports/mcp-service.test.js +++ b/test/ports/mcp-service.test.js @@ -1,7 +1,10 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { createCaptureThoughtService } from '../../src/mcp/service.js'; +import { + createCaptureThoughtService, + createMigrateThoughtGraphService, +} from '../../src/mcp/service.js'; test('MCP capture returns saved locally when post-save followthrough defers', async () => { const calls = []; @@ -29,6 +32,31 @@ test('MCP capture returns saved locally when post-save followthrough defers', as assert.equal(outcome.structuredContent.warnings.length, 1); }); +test('MCP migrate_graph returns a no-op when the graph model is current', async () => { + const calls = []; + const migrateThoughtGraph = createMigrateThoughtGraphService({ + getGraphModelStatus: () => record(calls, 'getGraphModelStatus', { + currentGraphModelVersion: 4, + requiredGraphModelVersion: 4, + migrationRequired: false, + }), + getLocalRepoDir: () => record(calls, 'repoDir', '/tmp/think-claude'), + hasGitRepo: () => record(calls, 'hasGitRepo', true), + migrateGraphModel: () => record(calls, 'migrateGraphModel'), + }); + + const outcome = await migrateThoughtGraph(); + + assert.deepEqual(calls, ['repoDir', 'hasGitRepo', 'getGraphModelStatus']); + assert.deepEqual(outcome.structuredContent, { + changed: false, + edgesAdded: 0, + edgesRemoved: 0, + graphModelVersion: 4, + metadataUpdated: false, + }); +}); + function createDeferredFollowthroughDeps(calls) { return { ensureGitRepo: (repoDir) => record(calls, 'ensureGitRepo', repoDir), From 4e72709135c536d75a2a9497e09fb5421ce15ae3 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 15 May 2026 02:52:10 -0700 Subject: [PATCH 11/13] fix: address PR review cleanup --- CHANGELOG.md | 1 + contracts/think-memory.graphql | 7 +- docs/BEARING.md | 5 + docs/INFRASTRUCTURE_DOCTRINE.md | 2 +- .../2026-05-13_runtime-truth-code-standard.md | 3 +- .../think-memory-data-model.md | 35 +++- ...SURFACE_macos-appstate-composition-root.md | 6 +- ...RE_think-echo-phase-2-runtime-roundtrip.md | 24 +-- package.json | 2 +- scripts/lint.mjs | 34 ++++ scripts/runtime-truth-ratchet.mjs | 44 +++-- scripts/think-echo-capability-probe.mjs | 29 ++-- test/ports/ports.test.js | 2 + test/ports/think-echo-contract.test.js | 155 +++++++++++++++--- 14 files changed, 277 insertions(+), 72 deletions(-) create mode 100644 scripts/lint.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 74446ef..847b984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Release discipline: - added the Think-on-Echo proof seam with a model-derived GraphQL memory contract, Echo/Wesley capability probe, and pinned Think memory data model - fixed MCP capture to return after the raw local save when post-save graph follow-through is slow, matching the CLI trapdoor behavior - fixed graph migration commands to return a fast no-op when the graph model is already current +- tightened PR review cleanup around lint arg forwarding, runtime ratchet hardening, Echo probe cleanup, and the Think memory contract timestamp scalar - fixed MCP tool result envelopes so structured content matches each registered output schema again - fixed checkpoint-backed reads to use public `@git-stunts/git-warp` package exports instead of private `node_modules` internals - fixed cached writer retries across raw capture follow-through, annotations, reflect writes, migrations, and enrichment patches diff --git a/contracts/think-memory.graphql b/contracts/think-memory.graphql index 5b135fe..5f8b502 100644 --- a/contracts/think-memory.graphql +++ b/contracts/think-memory.graphql @@ -8,6 +8,7 @@ directive @wes_op(name: String!) on FIELD_DEFINITION directive @wes_footprint(reads: [String!], writes: [String!]) on FIELD_DEFINITION +scalar DateTime enum ThoughtIngress { CLI @@ -85,7 +86,7 @@ type ThoughtCapture { mindId: ID! actorId: ID writerId: ID - capturedAt: String! + capturedAt: DateTime! ingress: ThoughtIngress! boundary: String causalRef: CausalRef! @@ -94,7 +95,7 @@ type ThoughtCapture { type ThoughtEntry { thoughtId: ID! mindId: ID! - capturedAt: String! + capturedAt: DateTime! content: ThoughtContent! capture: ThoughtCapture! provenance: ThoughtProvenance! @@ -107,7 +108,7 @@ input CaptureThoughtInput { mindId: ID! actorId: ID writerId: ID - capturedAt: String! + capturedAt: DateTime! content: ThoughtContentInput! provenance: ThoughtProvenanceInput! source: String diff --git a/docs/BEARING.md b/docs/BEARING.md index 3fb5f38..484c5ae 100644 --- a/docs/BEARING.md +++ b/docs/BEARING.md @@ -12,11 +12,13 @@ timeline ## Active Gravity ### 0. Local Mind Repairability + - Pulling `CORE_repair-v17-git-warp-minds` into cycle `0066`. - Making the git-warp v17 checkpoint repair path repeatable for local minds. - Keeping version-specific repair logic outside normal capture and read flows. ### 1. Think-on-Echo Runtime Proof + - Proving raw capture plus exact inspect as a Think-owned application contract hosted by Echo. - Keeping Think domain nouns in Think while Echo stays a generic dispatch and @@ -25,15 +27,18 @@ timeline production capture prematurely. ### 2. Performance Hardening + - Profiling CLI capture to identify Node startup and WARP graph bottlenecks. - Benchmark harness maturation for warm-path regression detection. - Sub-second capture latency as a non-negotiable target. ### 3. Domain Integrity (SSJD) + - Refactoring the MCP service layer to move from "shape soup" to runtime-backed domain types. - Standardizing function signatures and boundary validation across the store and CLI layers. ### 4. Orientation & Re-entry + - Learning where the browse and remember surfaces fail through re-entry friction tracking. - Tuning hotkey ergonomics and macOS URL scheme reliability. diff --git a/docs/INFRASTRUCTURE_DOCTRINE.md b/docs/INFRASTRUCTURE_DOCTRINE.md index fda63c4..017f475 100644 --- a/docs/INFRASTRUCTURE_DOCTRINE.md +++ b/docs/INFRASTRUCTURE_DOCTRINE.md @@ -275,7 +275,7 @@ even when the exact linter differs. "@typescript-eslint/no-unsafe-call": "error", "@typescript-eslint/only-throw-error": "error", "@typescript-eslint/switch-exhaustiveness-check": "error", - "no-floating-promises": "error" + "@typescript-eslint/no-floating-promises": "error" } } ``` diff --git a/docs/audit/2026-05-13_runtime-truth-code-standard.md b/docs/audit/2026-05-13_runtime-truth-code-standard.md index ffdad8b..adddd18 100644 --- a/docs/audit/2026-05-13_runtime-truth-code-standard.md +++ b/docs/audit/2026-05-13_runtime-truth-code-standard.md @@ -366,8 +366,7 @@ Backlog: - Existing: `SURFACE_splash-monolith` - Existing: `SURFACE_mind-switch-loop-in-command` -### F8. Swift adapter is closer to hexagonal shape, but app state is a -composition hotspot +### F8. Swift adapter is closer to hexagonal shape, but app state is a composition hotspot Severity: Medium diff --git a/docs/design/0068-think-memory-data-model/think-memory-data-model.md b/docs/design/0068-think-memory-data-model/think-memory-data-model.md index 8e18f36..2c05a51 100644 --- a/docs/design/0068-think-memory-data-model/think-memory-data-model.md +++ b/docs/design/0068-think-memory-data-model/think-memory-data-model.md @@ -192,12 +192,18 @@ ThoughtEntry { mindId capturedAt content - source + provenance metadata causalRef } ``` +For Phase 2, `provenance` is the canonical model field. Any `source` field on a +temporary GraphQL or adapter surface is only a flattened display alias derived +from `ThoughtProvenance.ingress`, `sourceApp`, `sourceUrl`, or `importOrigin`. +It is not a separate domain fact and must not replace the structured provenance +object at the core boundary. + ### ThoughtContent `ThoughtContent` is the immutable body of the thought. @@ -517,7 +523,7 @@ erDiagram THOUGHT_ENTRY { string thoughtId PK string mindId FK - string capturedAt + datetime capturedAt string causalRefId FK } @@ -725,8 +731,6 @@ Echo does not receive: ## Open Model Decisions -- Whether `thoughtId` is content-derived, capture-event-derived, or both via - separate `contentDigest` and `captureId`. - Whether `actorId` and `writerId` remain separate product fields. - How much ambient project context is safe by default in shared or imported minds. @@ -735,6 +739,29 @@ Echo does not receive: - How GraphQL schema generation should be governed so the model remains source truth. +## Phase 2 ID Contract + +For the first Echo proof, `thoughtId` is the stable product id for one captured +thought entry inside a `Mind`. It is capture-event-derived, unique within +`mindId`, and safe to use as the `InspectThought` lookup key. + +`captureId` remains the admission-event id exposed by `ThoughtCapture`. +`content.digest` is the content-derived identity and is not unique by itself: +two repeated captures can have the same digest while still producing distinct +`thoughtId` and `captureId` values. + +GraphQL mapping: + +- `ThoughtEntry.thoughtId`: unique product entry id scoped by `mindId`. +- `ThoughtCapture.captureId`: unique capture admission id. +- `ThoughtContent.digest`: content equality fingerprint. +- `actorId`: sponsored user abstraction that initiated capture, when known. +- `writerId`: runtime writer identity, when the backing runtime exposes one. + +Migration note: if later product work introduces a first-class canonical content +identity, it should be added as a separate field rather than overloading the +Phase 2 `thoughtId` contract. + ## Acceptance This slice is complete when: diff --git a/docs/method/backlog/bad-code/SURFACE_macos-appstate-composition-root.md b/docs/method/backlog/bad-code/SURFACE_macos-appstate-composition-root.md index 748e8f7..a729804 100644 --- a/docs/method/backlog/bad-code/SURFACE_macos-appstate-composition-root.md +++ b/docs/method/backlog/bad-code/SURFACE_macos-appstate-composition-root.md @@ -20,5 +20,7 @@ logic in one app-state class. - Move process restart behind a port instead of constructing `Process` inside `CaptureAppState`. - Split retry orchestration from menu-bar state. -- `CaptureAppState.swift` falls below 250 lines or is divided by one - primary responsibility per file. +- `CaptureAppState.swift` is no more than 250 lines, or is split into + multiple files with exactly one primary responsibility per file. +- A primary responsibility is a single class, struct, actor, or small cohesive + set of closely related functions that implement one feature or concern. diff --git a/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md b/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md index 636fa6e..47d61f0 100644 --- a/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md +++ b/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md @@ -26,22 +26,22 @@ production capture path. The proof should: -0. Confirm `docs/design/0068-think-memory-data-model/think-memory-data-model.md` +1. Confirm `docs/design/0068-think-memory-data-model/think-memory-data-model.md` is the source truth for the contract. -0. Follow `docs/design/0069-think-echo-integration-plan/think-echo-integration-plan.md` +2. Follow `docs/design/0069-think-echo-integration-plan/think-echo-integration-plan.md` for the runtime port and adapter shape. -0. Use the model-derived `contracts/think-memory.graphql` contract. -0. Run `npm run echo:probe -- --json` and require +3. Use the model-derived `contracts/think-memory.graphql` contract. +4. Run `npm run echo:probe -- --json` and require `ready_enough_for_phase_2`. -1. Build a `CaptureThought` input through generated or minimally generated +5. Build a `CaptureThought` input through generated or minimally generated contract helpers. -2. Dispatch the canonical intent through Echo. -3. Receive admission evidence for the capture. -4. Build an exact `InspectThought` observation by entry id or coordinate. -5. Receive a `ReadingEnvelope` or equivalent Echo observation artifact. -6. Verify the reading posture is complete. -7. Decode the payload into a Think-owned `ThoughtEntry`. -8. Assert that raw text and capture metadata survived the round trip. +6. Dispatch the canonical intent through Echo. +7. Receive admission evidence for the capture. +8. Build an exact `InspectThought` observation by entry id or coordinate. +9. Receive a `ReadingEnvelope` or equivalent Echo observation artifact. +10. Verify the reading posture is complete. +11. Decode the payload into a Think-owned `ThoughtEntry`. +12. Assert that raw text and capture metadata survived the round trip. ## Why diff --git a/package.json b/package.json index 84df539..4abfbd4 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "think-mcp": "./bin/think-mcp.js" }, "scripts": { - "lint": "eslint . && npm run runtime-truth:ratchet", + "lint": "node ./scripts/lint.mjs", "macos": "node ./scripts/package-macos-app.mjs --open", "macos:bundle": "node ./scripts/package-macos-app.mjs", "macos:dev": "swift run --package-path macos ThinkMenuBarApp", diff --git a/scripts/lint.mjs b/scripts/lint.mjs new file mode 100644 index 0000000..e477901 --- /dev/null +++ b/scripts/lint.mjs @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const forwardedArgs = process.argv.slice(2); + +const eslintStatus = runChecked('eslint', ['.', ...forwardedArgs]); +if (eslintStatus !== 0) { + process.exit(eslintStatus); +} + +process.exit(runChecked(process.execPath, ['./scripts/runtime-truth-ratchet.mjs'])); + +function runChecked(command, args) { + const result = spawnSync(command, args, { + cwd: repoRoot, + shell: process.platform === 'win32', + stdio: 'inherit', + }); + + if (result.error) { + process.stderr.write(`${command} failed: ${result.error.message}\n`); + return 1; + } + if (result.signal) { + process.stderr.write(`${command} exited via signal ${result.signal}\n`); + return 1; + } + + return result.status ?? 1; +} diff --git a/scripts/runtime-truth-ratchet.mjs b/scripts/runtime-truth-ratchet.mjs index 873755a..f575c6e 100644 --- a/scripts/runtime-truth-ratchet.mjs +++ b/scripts/runtime-truth-ratchet.mjs @@ -7,6 +7,7 @@ import { fileURLToPath } from 'node:url'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const baselinePath = path.join(repoRoot, 'docs', 'audit', 'runtime-truth-ratchet-baseline.json'); +const COMMAND_TIMEOUT_MS = 120_000; const sourcePrefixes = Object.freeze(['src/', 'bin/', 'scripts/']); const strictRuleIds = Object.freeze([ 'complexity', @@ -25,7 +26,7 @@ const strictRuleArgs = Object.freeze([ 'complexity:["error",8]', 'max-statements:["error",25]', ]); -const genericThrowPattern = /\bthrow\s+new\s+(Error|TypeError)\s*\(/u; +const genericThrowPattern = /\bthrow\s+new\s+(Error|TypeError)\s*\(/gu; class RuntimeTruthRatchetError extends Error { constructor(message) { @@ -61,11 +62,22 @@ function run(command, args, { allowFailure = false } = {}) { cwd: repoRoot, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, + timeout: COMMAND_TIMEOUT_MS, }); if (result.error) { + if (result.error.code === 'ETIMEDOUT') { + throw new RuntimeTruthRatchetError( + `Runtime truth ratchet: command timed out after ${COMMAND_TIMEOUT_MS}ms: ${command} ${args.join(' ')}` + ); + } throw result.error; } + if (result.signal) { + throw new RuntimeTruthRatchetError( + `Runtime truth ratchet: command exited via signal ${result.signal}: ${command} ${args.join(' ')}` + ); + } if (!allowFailure && result.status !== 0) { throw new RuntimeTruthRatchetError([ `Command failed: ${command} ${args.join(' ')}`, @@ -126,26 +138,36 @@ function collectStrictLimitFindings(files) { function collectGenericThrowFindings(files) { const findings = []; for (const file of files) { - const lines = readFileSync(path.join(repoRoot, file), 'utf8').split('\n'); - for (let index = 0; index < lines.length; index += 1) { - const line = lines[index]; - const match = genericThrowPattern.exec(line); - if (match === null) { - continue; - } + const content = readFileSync(path.join(repoRoot, file), 'utf8'); + genericThrowPattern.lastIndex = 0; + for (const match of content.matchAll(genericThrowPattern)) { + const position = locateTextPosition(content, match.index ?? 0); findings.push(Object.freeze({ category: classifyFile(file), - column: match.index + 1, + column: position.column, file, kind: match[1], - line: index + 1, - text: line.trim(), + line: position.line, + text: firstMatchedLine(match[0]), })); } } return findings.sort(compareFindings); } +function locateTextPosition(content, offset) { + const prefix = content.slice(0, offset); + const lastNewlineIndex = prefix.lastIndexOf('\n'); + return Object.freeze({ + column: offset - lastNewlineIndex, + line: prefix.split('\n').length, + }); +} + +function firstMatchedLine(text) { + return text.split('\n')[0]?.trim() ?? ''; +} + function classifyFile(file) { if (file.startsWith('test/')) { return 'test'; diff --git a/scripts/think-echo-capability-probe.mjs b/scripts/think-echo-capability-probe.mjs index e60bc2d..a9e0c73 100644 --- a/scripts/think-echo-capability-probe.mjs +++ b/scripts/think-echo-capability-probe.mjs @@ -154,19 +154,22 @@ function runGenerator(paths, generator) { function runGeneratorUnchecked(paths) { const tempDir = mkdtempSync(path.join(tmpdir(), 'think-echo-probe-')); const generatedPath = path.join(tempDir, 'think_memory.generated.rs'); - const result = runCommand('cargo', generatorArgs(paths.contract, generatedPath), { - cwd: paths.echoRepo, - timeoutMs: 180_000, - }); - const markers = readGeneratedMarkers(generatedPath); - rmSync(tempDir, { force: true, recursive: true }); - - return Object.freeze({ - markers, - ok: result.status === 0 && markers.length === generatorMarkers.length, - result, - skipped: false, - }); + try { + const result = runCommand('cargo', generatorArgs(paths.contract, generatedPath), { + cwd: paths.echoRepo, + timeoutMs: 180_000, + }); + const markers = readGeneratedMarkers(generatedPath); + + return Object.freeze({ + markers, + ok: result.status === 0 && markers.length === generatorMarkers.length, + result, + skipped: false, + }); + } finally { + rmSync(tempDir, { force: true, recursive: true }); + } } function generatorArgs(schemaPath, outputPath) { diff --git a/test/ports/ports.test.js b/test/ports/ports.test.js index 5a4a892..fdff177 100644 --- a/test/ports/ports.test.js +++ b/test/ports/ports.test.js @@ -27,6 +27,8 @@ test('abstract random port reports a typed not-implemented error', () => { function isPortError(error, portName, methodName) { return error instanceof PortNotImplementedError + && error.code === 'PORT_NOT_IMPLEMENTED' + && error.message === `${portName}.${methodName} is not implemented` && error.portName === portName && error.methodName === methodName; } diff --git a/test/ports/think-echo-contract.test.js b/test/ports/think-echo-contract.test.js index faea1cd..3f6d202 100644 --- a/test/ports/think-echo-contract.test.js +++ b/test/ports/think-echo-contract.test.js @@ -4,29 +4,138 @@ import path from 'node:path'; import test from 'node:test'; const contractPath = path.resolve('contracts', 'think-memory.graphql'); -const requiredFragments = Object.freeze([ - 'type ThoughtContent', - 'type ThoughtCapture', - 'type ThoughtProvenance', - 'type CausalRef', - 'content: ThoughtContent!', - 'capture: ThoughtCapture!', - 'provenance: ThoughtProvenance!', - 'causalRef: CausalRef!', - 'thoughtId: ID!', - 'mindId: ID!', - 'type Mutation', - 'captureThought(input: CaptureThoughtInput!): CaptureThoughtResult!', - '@wes_op(name: "CaptureThought")', - '@wes_footprint(reads: ["ThoughtEntry"], writes: ["ThoughtEntry"])', - 'type Query', - 'inspectThought(mindId: ID!, thoughtId: ID!): ThoughtEntry!', - '@wes_op(name: "InspectThought")', -]); test('Think Echo contract expresses the pinned memory model', async () => { - const source = await readFile(contractPath, 'utf8'); - for (const fragment of requiredFragments) { - assert.ok(source.includes(fragment), `Expected contract to include ${fragment}`); - } + const schema = parseSchema(await readFile(contractPath, 'utf8')); + + assert.ok(schema.scalars.has('DateTime'), 'Expected DateTime scalar.'); + assert.ok(schema.directives.has('wes_op'), 'Expected @wes_op directive.'); + assert.ok(schema.directives.has('wes_footprint'), 'Expected @wes_footprint directive.'); + + assertModelTypes(schema); + assertCaptureMutation(schema); + assertInspectQuery(schema); }); + +function assertModelTypes(schema) { + const thoughtEntry = requireBlock(schema, 'type', 'ThoughtEntry'); + assertField(thoughtEntry, 'thoughtId', 'ID!'); + assertField(thoughtEntry, 'mindId', 'ID!'); + assertField(thoughtEntry, 'capturedAt', 'DateTime!'); + assertField(thoughtEntry, 'content', 'ThoughtContent!'); + assertField(thoughtEntry, 'capture', 'ThoughtCapture!'); + assertField(thoughtEntry, 'provenance', 'ThoughtProvenance!'); + assertField(thoughtEntry, 'causalRef', 'CausalRef!'); + + assertField(requireBlock(schema, 'type', 'ThoughtContent'), 'digest', 'String!'); + assertField(requireBlock(schema, 'input', 'ThoughtContentInput'), 'digest', 'String!'); + assertField(requireBlock(schema, 'type', 'CausalRef'), 'runtime', 'CausalRuntime!'); + assertField(requireBlock(schema, 'type', 'CausalRef'), 'witness', 'String'); + assertField(requireBlock(schema, 'type', 'ThoughtCapture'), 'capturedAt', 'DateTime!'); + assertField(requireBlock(schema, 'input', 'CaptureThoughtInput'), 'capturedAt', 'DateTime!'); +} + +function assertCaptureMutation(schema) { + const mutation = requireBlock(schema, 'type', 'Mutation'); + const captureThought = assertField(mutation, 'captureThought', 'CaptureThoughtResult!'); + assert.deepEqual(captureThought.args, { input: 'CaptureThoughtInput!' }); + assert.ok(captureThought.directives.has('wes_op'), 'Expected captureThought @wes_op.'); + assert.ok(captureThought.directives.has('wes_footprint'), 'Expected captureThought @wes_footprint.'); +} + +function assertInspectQuery(schema) { + const query = requireBlock(schema, 'type', 'Query'); + const inspectThought = assertField(query, 'inspectThought', 'ThoughtEntry!'); + assert.deepEqual(inspectThought.args, { mindId: 'ID!', thoughtId: 'ID!' }); + assert.ok(inspectThought.directives.has('wes_op'), 'Expected inspectThought @wes_op.'); +} + +function parseSchema(source) { + return Object.freeze({ + blocks: parseBlocks(source), + directives: parseDirectives(source), + scalars: parseScalars(source), + }); +} + +function parseBlocks(source) { + const blocks = new Map(); + const blockPattern = /^(type|input|enum)\s+(\w+)\s*\{([\s\S]*?)^}/gm; + for (const match of source.matchAll(blockPattern)) { + blocks.set(`${match[1]}:${match[2]}`, Object.freeze({ + fields: parseFields(match[3]), + kind: match[1], + name: match[2], + })); + } + return blocks; +} + +function parseFields(body) { + const fields = new Map(); + let current = null; + for (const rawLine of body.split('\n')) { + const line = rawLine.trim(); + current = applySchemaLine(fields, current, line); + } + return fields; +} + +function applySchemaLine(fields, current, line) { + if (line === '') { + return current; + } + if (line.startsWith('@') && current) { + current.directives.add(line.match(/^@(\w+)/u)?.[1] ?? ''); + return current; + } + const parsed = parseFieldLine(line); + if (parsed) { + fields.set(parsed.name, parsed); + return parsed; + } + return current; +} + +function parseFieldLine(line) { + const match = /^(\w+)\s*(?:\(([^)]*)\))?\s*:\s*([^\s@]+)/u.exec(line); + if (match) { + return Object.freeze({ + args: parseArgs(match[2] ?? ''), + directives: new Set(), + name: match[1], + type: match[3], + }); + } + return null; +} + +function parseArgs(rawArgs) { + const args = {}; + for (const arg of rawArgs.split(',').map(value => value.trim()).filter(Boolean)) { + const [name, type] = arg.split(':').map(value => value.trim()); + args[name] = type; + } + return args; +} + +function parseDirectives(source) { + return new Set([...source.matchAll(/^directive\s+@(\w+)/gm)].map(match => match[1])); +} + +function parseScalars(source) { + return new Set([...source.matchAll(/^scalar\s+(\w+)$/gm)].map(match => match[1])); +} + +function requireBlock(schema, kind, name) { + const block = schema.blocks.get(`${kind}:${name}`); + assert.ok(block, `Expected ${kind} ${name}.`); + return block; +} + +function assertField(block, fieldName, fieldType) { + const field = block.fields.get(fieldName); + assert.ok(field, `Expected ${block.name}.${fieldName}.`); + assert.equal(field.type, fieldType, `Expected ${block.name}.${fieldName} type.`); + return field; +} From 3a27ff4f9519df0537ea50161df8ba7101bb45be Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 15 May 2026 02:55:32 -0700 Subject: [PATCH 12/13] Fix: correct source of truth wording --- .../backlog/up-next/CORE_think-echo-phase-1-app-contract.md | 4 ++-- .../up-next/CORE_think-echo-phase-2-runtime-roundtrip.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md b/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md index 88662cd..62a3817 100644 --- a/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md +++ b/docs/method/backlog/up-next/CORE_think-echo-phase-1-app-contract.md @@ -18,7 +18,7 @@ exact inspect round trip. Update: `contracts/think-memory.graphql` now expresses the pinned data model in `docs/design/0068-think-memory-data-model/think-memory-data-model.md` for the Phase 2 proof. The GraphQL file is still a contract expression, not semantic -source truth. +source of truth. The likely first file is: @@ -64,7 +64,7 @@ reflection outputs to the first contract. - [x] The provisional contract names `mindId` explicitly, even if only `default` is used. - [x] Generated-artifact locations are decided but generated output is not treated - as semantic source truth. + as semantic source of truth. - [x] No Echo or Continuum schema is modified to add Think domain nouns. - [x] The contract exposes `ThoughtContent`, `ThoughtCapture`, `ThoughtProvenance`, and `CausalRef` from the pinned model. diff --git a/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md b/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md index 47d61f0..33f1db1 100644 --- a/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md +++ b/docs/method/backlog/up-next/CORE_think-echo-phase-2-runtime-roundtrip.md @@ -27,7 +27,7 @@ production capture path. The proof should: 1. Confirm `docs/design/0068-think-memory-data-model/think-memory-data-model.md` - is the source truth for the contract. + is the source of truth for the contract. 2. Follow `docs/design/0069-think-echo-integration-plan/think-echo-integration-plan.md` for the runtime port and adapter shape. 3. Use the model-derived `contracts/think-memory.graphql` contract. @@ -52,7 +52,7 @@ usable migration path. ## Constraints - Do not switch the CLI, MCP server, macOS app, or default store to Echo. -- Do not treat the current GraphQL probe fixture as semantic source truth. +- Do not treat the current GraphQL probe fixture as semantic source of truth. - Do not require existing `~/.think/*` minds to migrate. - Do not depend on `git-warp` in the hot proof path. - Do not hand-roll runtime bytes if the current Wesley/Echo toolchain can From 1621502556873beb00ca40c347b2391d39dfc333 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 15 May 2026 03:23:10 -0700 Subject: [PATCH 13/13] Fix: normalize source of truth wording --- contracts/think-memory.graphql | 2 +- .../think-memory-data-model.md | 4 ++-- .../think-echo-integration-plan.md | 2 +- .../asap/CORE_think-echo-contract-proof.md | 2 +- test/ports/think-echo-model-doc.test.js | 21 +++++++++++++++++++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/contracts/think-memory.graphql b/contracts/think-memory.graphql index 5f8b502..b10e584 100644 --- a/contracts/think-memory.graphql +++ b/contracts/think-memory.graphql @@ -1,6 +1,6 @@ # Think-owned memory contract for the Echo proof. # -# Source truth: +# Source of truth: # docs/design/0068-think-memory-data-model/think-memory-data-model.md # # GraphQL expresses the Think memory model. Echo must only receive generated diff --git a/docs/design/0068-think-memory-data-model/think-memory-data-model.md b/docs/design/0068-think-memory-data-model/think-memory-data-model.md index 2c05a51..1625997 100644 --- a/docs/design/0068-think-memory-data-model/think-memory-data-model.md +++ b/docs/design/0068-think-memory-data-model/think-memory-data-model.md @@ -676,7 +676,7 @@ Echo does not receive: ### Phase M0: Freeze The Model -- Treat this document as source truth. +- Treat this document as the source of truth. - Mark current GraphQL as provisional until revised against this model. - Add model-level tests before runtime round-trip code. @@ -695,7 +695,7 @@ Echo does not receive: - Revise `contracts/think-memory.graphql` to match `ThoughtEntry`, `ThoughtContent`, `ThoughtCapture`, `ThoughtProvenance`, and `CausalRef`. - Run `npm run echo:probe -- --json`. -- Keep generated files out of semantic source truth unless a later build step +- Keep generated files out of semantic source of truth unless a later build step requires checked-in fixtures. ### Phase M3: Echo Round Trip diff --git a/docs/design/0069-think-echo-integration-plan/think-echo-integration-plan.md b/docs/design/0069-think-echo-integration-plan/think-echo-integration-plan.md index f4aeb01..be7ea43 100644 --- a/docs/design/0069-think-echo-integration-plan/think-echo-integration-plan.md +++ b/docs/design/0069-think-echo-integration-plan/think-echo-integration-plan.md @@ -139,7 +139,7 @@ Owns GraphQL contract source: The contract is allowed to use GraphQL-friendly shapes, but it must stay a projection of the model. Generated output is build/proof material, not semantic -source truth. +source of truth. ### Layer 3: Generated Boundary diff --git a/docs/method/backlog/asap/CORE_think-echo-contract-proof.md b/docs/method/backlog/asap/CORE_think-echo-contract-proof.md index ec13a50..f2a5cd9 100644 --- a/docs/method/backlog/asap/CORE_think-echo-contract-proof.md +++ b/docs/method/backlog/asap/CORE_think-echo-contract-proof.md @@ -76,7 +76,7 @@ outside the hot CLI path. ## Current Evidence - Phase 0 charter: `docs/design/0067-think-echo-contract-proof/think-echo-contract-proof.md` -- Data model source truth: `docs/design/0068-think-memory-data-model/think-memory-data-model.md` +- Data model source of truth: `docs/design/0068-think-memory-data-model/think-memory-data-model.md` - Phase 1 model-derived contract: `contracts/think-memory.graphql` - Toolchain probe: `npm run echo:probe -- --json` diff --git a/test/ports/think-echo-model-doc.test.js b/test/ports/think-echo-model-doc.test.js index 2aa3a52..f8e2a7b 100644 --- a/test/ports/think-echo-model-doc.test.js +++ b/test/ports/think-echo-model-doc.test.js @@ -9,6 +9,17 @@ const modelDocPath = path.resolve( '0068-think-memory-data-model', 'think-memory-data-model.md' ); +const sourceOfTruthWordingPaths = Object.freeze([ + path.resolve('contracts', 'think-memory.graphql'), + modelDocPath, + path.resolve( + 'docs', + 'design', + '0069-think-echo-integration-plan', + 'think-echo-integration-plan.md' + ), + path.resolve('docs', 'method', 'backlog', 'asap', 'CORE_think-echo-contract-proof.md'), +]); const requiredFragments = Object.freeze([ 'Mind', 'ThoughtEntry', @@ -34,3 +45,13 @@ test('Think Echo data model is pinned before the runtime round trip', async () = assert.ok(source.includes(fragment), `Expected model doc to include ${fragment}`); } }); + +test('Think Echo contract docs use source of truth wording consistently', async () => { + const docs = await Promise.all(sourceOfTruthWordingPaths.map(async (docPath) => Object.freeze({ + docPath, + source: await readFile(docPath, 'utf8'), + }))); + for (const { docPath, source } of docs) { + assert.doesNotMatch(source, /\bsource truth\b/iu, `Expected ${docPath} to avoid source truth.`); + } +});