instar 0.28.83 → 0.28.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/index.html +239 -0
- package/dist/cli.js +10 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/intent.d.ts.map +1 -1
- package/dist/commands/intent.js +29 -2
- package/dist/commands/intent.js.map +1 -1
- package/dist/commands/memoryBackfillEvidence.d.ts +69 -0
- package/dist/commands/memoryBackfillEvidence.d.ts.map +1 -0
- package/dist/commands/memoryBackfillEvidence.js +220 -0
- package/dist/commands/memoryBackfillEvidence.js.map +1 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +86 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/core/DecisionJournal.d.ts +66 -3
- package/dist/core/DecisionJournal.d.ts.map +1 -1
- package/dist/core/DecisionJournal.js +116 -3
- package/dist/core/DecisionJournal.js.map +1 -1
- package/dist/core/DispatchExecutor.d.ts +74 -1
- package/dist/core/DispatchExecutor.d.ts.map +1 -1
- package/dist/core/DispatchExecutor.js +120 -1
- package/dist/core/DispatchExecutor.js.map +1 -1
- package/dist/core/EvolutionManager.d.ts +133 -0
- package/dist/core/EvolutionManager.d.ts.map +1 -1
- package/dist/core/EvolutionManager.js +494 -0
- package/dist/core/EvolutionManager.js.map +1 -1
- package/dist/core/InitiativeTracker.d.ts +82 -15
- package/dist/core/InitiativeTracker.d.ts.map +1 -1
- package/dist/core/InitiativeTracker.js +502 -31
- package/dist/core/InitiativeTracker.js.map +1 -1
- package/dist/core/LearnSkillBridge.d.ts +119 -0
- package/dist/core/LearnSkillBridge.d.ts.map +1 -0
- package/dist/core/LearnSkillBridge.js +160 -0
- package/dist/core/LearnSkillBridge.js.map +1 -0
- package/dist/core/SharedStateLedger.d.ts.map +1 -1
- package/dist/core/SharedStateLedger.js +4 -0
- package/dist/core/SharedStateLedger.js.map +1 -1
- package/dist/core/types.d.ts +124 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +8 -0
- package/dist/core/types.js.map +1 -1
- package/dist/memory/EvidenceRenderer.d.ts +64 -0
- package/dist/memory/EvidenceRenderer.d.ts.map +1 -0
- package/dist/memory/EvidenceRenderer.js +119 -0
- package/dist/memory/EvidenceRenderer.js.map +1 -0
- package/dist/memory/SemanticMemory.d.ts +61 -1
- package/dist/memory/SemanticMemory.d.ts.map +1 -1
- package/dist/memory/SemanticMemory.js +381 -11
- package/dist/memory/SemanticMemory.js.map +1 -1
- package/dist/server/AgentServer.d.ts +4 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +2 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +8 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +500 -14
- package/dist/server/routes.js.map +1 -1
- package/dist/tasks/DivergenceChecker.d.ts +89 -0
- package/dist/tasks/DivergenceChecker.d.ts.map +1 -0
- package/dist/tasks/DivergenceChecker.js +255 -0
- package/dist/tasks/DivergenceChecker.js.map +1 -0
- package/dist/tasks/LruCache.d.ts +33 -0
- package/dist/tasks/LruCache.d.ts.map +1 -0
- package/dist/tasks/LruCache.js +88 -0
- package/dist/tasks/LruCache.js.map +1 -0
- package/dist/tasks/RateLimiter.d.ts +61 -0
- package/dist/tasks/RateLimiter.d.ts.map +1 -0
- package/dist/tasks/RateLimiter.js +108 -0
- package/dist/tasks/RateLimiter.js.map +1 -0
- package/dist/tasks/TaskFlowDueWaker.d.ts +29 -0
- package/dist/tasks/TaskFlowDueWaker.d.ts.map +1 -0
- package/dist/tasks/TaskFlowDueWaker.js +73 -0
- package/dist/tasks/TaskFlowDueWaker.js.map +1 -0
- package/dist/tasks/TaskFlowMaintenanceSweeper.d.ts +40 -0
- package/dist/tasks/TaskFlowMaintenanceSweeper.d.ts.map +1 -0
- package/dist/tasks/TaskFlowMaintenanceSweeper.js +131 -0
- package/dist/tasks/TaskFlowMaintenanceSweeper.js.map +1 -0
- package/dist/tasks/TaskFlowRegistry.d.ts +180 -0
- package/dist/tasks/TaskFlowRegistry.d.ts.map +1 -0
- package/dist/tasks/TaskFlowRegistry.js +791 -0
- package/dist/tasks/TaskFlowRegistry.js.map +1 -0
- package/dist/tasks/ThreadlineFlowBridge.d.ts +54 -0
- package/dist/tasks/ThreadlineFlowBridge.d.ts.map +1 -0
- package/dist/tasks/ThreadlineFlowBridge.js +129 -0
- package/dist/tasks/ThreadlineFlowBridge.js.map +1 -0
- package/dist/tasks/task-flow-registry.store.sqlite.d.ts +77 -0
- package/dist/tasks/task-flow-registry.store.sqlite.d.ts.map +1 -0
- package/dist/tasks/task-flow-registry.store.sqlite.js +350 -0
- package/dist/tasks/task-flow-registry.store.sqlite.js.map +1 -0
- package/dist/tasks/task-flow-types.d.ts +229 -0
- package/dist/tasks/task-flow-types.d.ts.map +1 -0
- package/dist/tasks/task-flow-types.js +128 -0
- package/dist/tasks/task-flow-types.js.map +1 -0
- package/dist/threadline/ThreadlineMCPServer.d.ts +32 -0
- package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
- package/dist/threadline/ThreadlineMCPServer.js +82 -5
- package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
- package/dist/threadline/mcp-stdio-entry.js +56 -0
- package/dist/threadline/mcp-stdio-entry.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +78 -78
- package/upgrades/0.28.84.md +89 -0
- package/upgrades/0.28.85.md +64 -0
- package/upgrades/side-effects/0.28.83.md +27 -0
- package/upgrades/side-effects/taskflow-phase1.md +130 -0
- package/upgrades/side-effects/taskflow-phase2.md +110 -0
- package/upgrades/side-effects/taskflow-phase3a.md +138 -0
- package/upgrades/side-effects/taskflow-phase4.md +377 -0
- package/upgrades/side-effects/taskflow-phase5.md +234 -0
- package/upgrades/side-effects/threadline-discover-relay-and-trust.md +122 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase1.md +138 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase2.md +148 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase3.md +125 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase4.md +129 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase5.md +285 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Side-Effects Review — threadline_discover: live relay + trust surfacing
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `threadline-discover-relay-and-trust`
|
|
4
|
+
**Date:** 2026-05-11
|
|
5
|
+
**Author:** echo (instar developer)
|
|
6
|
+
**Second-pass reviewer:** not required (no decision-point surface — see §4)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`threadline_discover` (MCP tool) had two data-accuracy bugs that caused agents on the same machine to misreport who is reachable on the network.
|
|
11
|
+
|
|
12
|
+
1. **`scope=network` returned the local cache** (`AgentDiscovery.loadKnownAgents()`) instead of querying the relay's live presence registry. Off-machine agents (like Dawn, on the public relay but not in any local file) were invisible to anyone using `scope=network`, even when the relay had them online.
|
|
13
|
+
2. **The sanitizer hardcoded `status: 'unverified'`** for every agent regardless of granted trust, so trusted agents looked indistinguishable from strangers.
|
|
14
|
+
|
|
15
|
+
Both are now fixed:
|
|
16
|
+
|
|
17
|
+
- `ThreadlineMCPDeps` gains an optional `relayClient: RelayDiscoverer | null` (minimal interface, decoupled from the concrete `ThreadlineClient`). When `scope === 'network'` and the relay client reports `connectionState === 'connected'`, the handler calls `relayClient.discover(filter)` and returns the live result with `source: 'relay'`. If the relay is disconnected or the query throws, the handler falls back to `loadKnownAgents()` and marks the response with `source: 'cache'` plus a `staleReason` string so consumers can detect stale data.
|
|
18
|
+
- A new HTTP route `POST /threadline/relay-discover` proxies the call from the MCP stdio subprocess (which has no relay client of its own) to the agent server's relay client — mirroring the existing `/threadline/relay-send` pattern.
|
|
19
|
+
- The discover sanitizer now looks up the trust profile by fingerprint then name (identical pattern to `threadline_agents` at lines 569–575) and adds `trustLevel` + `trustSource` to each entry, gated by the existing `isLocal || hasScope('threadline:admin')` rule.
|
|
20
|
+
|
|
21
|
+
Files touched:
|
|
22
|
+
- `src/threadline/ThreadlineMCPServer.ts` — `RelayDiscoverer` type, network-scope path, trust surfacing, `relayAgentToInfo` helper.
|
|
23
|
+
- `src/threadline/mcp-stdio-entry.ts` — `createHttpRelayDiscoverer` adapter wiring the MCP subprocess to the agent server.
|
|
24
|
+
- `src/server/routes.ts` — new `POST /threadline/relay-discover` route.
|
|
25
|
+
- `tests/unit/threadline/ThreadlineMCPServer.test.ts` — 5 new tests: cache marker on no-relay, relay path, disconnect fallback, throw fallback, trust surfacing.
|
|
26
|
+
|
|
27
|
+
## Decision-point inventory
|
|
28
|
+
|
|
29
|
+
This change touches no decision point. `threadline_discover` is a **read-only** tool — it answers "who is reachable?" It does not block, gate, or filter messages. It has no `block/allow/warn` surface.
|
|
30
|
+
|
|
31
|
+
- `threadline_discover` — pass-through — reads from relay (new) or local cache (existing); produces a list. No decisions are made on the basis of this list inside instar.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 1. Over-block
|
|
36
|
+
|
|
37
|
+
**No block/allow surface — over-block not applicable.**
|
|
38
|
+
|
|
39
|
+
The tool reads and returns data. Consumers (the calling agent's reasoning) may decide what to do with the list, but that decision happens outside this code.
|
|
40
|
+
|
|
41
|
+
The closest analog to "over-block" is over-inclusion: could the relay return agents that shouldn't be shown? The relay protocol already filters by visibility (`public` agents only — `PresenceRegistry.ts:110` and the visibility logic in the relay server). This change passes that result through unchanged. No new exposure is created.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 2. Under-block
|
|
46
|
+
|
|
47
|
+
**No block/allow surface — under-block not applicable.**
|
|
48
|
+
|
|
49
|
+
The closest analog is under-inclusion: could the relay path miss agents that should appear? Yes, by design — if the relay is disconnected, we fall back to the local cache and label the response `source: cache`. That is explicit and visible, not silent. The user-visible behavior shifts from "every network discover lies that the relay is the source" to "discover tells you which source it used."
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 3. Level-of-abstraction fit
|
|
54
|
+
|
|
55
|
+
This sits at exactly the right layer:
|
|
56
|
+
|
|
57
|
+
- **MCP tool layer** is the right place for the response shape (it's what callers consume).
|
|
58
|
+
- **HTTP route in `routes.ts`** is the right place for the relay-client proxy (the MCP subprocess can't directly hold a WebSocket; mirroring `/threadline/relay-send` keeps the pattern consistent).
|
|
59
|
+
- The **`RelayDiscoverer` interface** is intentionally minimal (only `connectionState` and `discover()`) so it doesn't force the MCP server to import `ThreadlineClient` types — keeps the test seam clean and avoids circular dependency risk.
|
|
60
|
+
- The **trust-profile lookup** is duplicated from `threadline_agents` rather than extracted into a helper. This is deliberate: the lookup is 4 lines and the two sites have slightly different visibility-gating logic (`threadline_agents` uses `requestContext.isLocal || hasScope`, this matches it). A helper would obscure that the two checks are independent. If a third call site appears, that's the right time to extract.
|
|
61
|
+
|
|
62
|
+
No higher-level gate already exists for "what trust level should be surfaced to a discover call." The visibility rule is local-or-admin, same as `threadline_agents`.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 4. Signal vs authority compliance
|
|
67
|
+
|
|
68
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
69
|
+
|
|
70
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
71
|
+
|
|
72
|
+
- [x] No — this change has no block/allow surface.
|
|
73
|
+
- [ ] No — this change produces a signal consumed by an existing smart gate.
|
|
74
|
+
- [ ] Yes — but the logic is a smart gate with full conversational context.
|
|
75
|
+
- [ ] ⚠️ Yes, with brittle logic — STOP.
|
|
76
|
+
|
|
77
|
+
The discover tool is a read-only data accessor. It does not block actions, gate information flow against agent intent, or filter messages by meaning. The `staleReason` field is purely informational; consumers may choose to ignore stale results, but that decision is outside this code. No brittle authority is added.
|
|
78
|
+
|
|
79
|
+
The only adjacent concern is honesty: the `trustLevel` field reports what the trust manager has on file. It does not invent or fabricate trust levels. If `getProfile(fingerprint)` returns `null`, no `trustLevel` field is emitted — consumers see absence, not a fabricated "unverified" string.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 5. Interactions
|
|
84
|
+
|
|
85
|
+
- **Shadowing:** None. `threadline_discover` is a single-handler MCP tool. The local-scope path is unchanged. The new network-scope path replaces the previous cache-only behavior with cache-or-relay; the local cache fallback runs only when the relay can't.
|
|
86
|
+
- **Double-fire:** None. The relay's `discover` frame is a request/response pair; the relay-side rate limiting (already present in `RelayMetrics.ts`) applies. The new HTTP proxy route is called only by the MCP subprocess during a discover tool call — once per invocation.
|
|
87
|
+
- **Races:**
|
|
88
|
+
- The `connectionState` getter on the HTTP relay-discoverer adapter re-reads `/threadline/status` on every call before issuing discover, so a recently-disconnected relay won't be queried with a stale "connected" reading.
|
|
89
|
+
- There's a benign race between status-check and discover-call (relay could disconnect in the ~10ms gap). In that case `relayClient.discover()` resolves to `[]` (the existing `ThreadlineClient.discover` timeout-then-resolve-empty behavior is preserved). The handler treats `[]` as a valid empty result and returns `source: 'relay', count: 0` — honest about the source.
|
|
90
|
+
- No shared mutable state between the route handler and the MCP server; both work off the same `ctx.threadlineRelayClient` reference.
|
|
91
|
+
- **Feedback loops:** None. The discover output is read-only; no downstream system writes back into it.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 6. External surfaces
|
|
96
|
+
|
|
97
|
+
- **MCP tool response shape:** The `agents` array shape is unchanged (existing fields preserved). Two new fields added at the top level when `scope=network`: `source: 'relay' | 'cache'` and optional `staleReason`. Two new optional fields per agent: `trustLevel`, `trustSource` — only present when the caller has local-or-admin visibility and a trust profile exists. Pure additive change; no existing field is removed or repurposed.
|
|
98
|
+
- **HTTP API:** New `POST /threadline/relay-discover` endpoint. Mirrors `POST /threadline/relay-send` in its no-auth posture (both are intended only for the local MCP subprocess; the route lives on `localhost` and is not exposed through the tunnel). 503 returned cleanly when relay isn't connected.
|
|
99
|
+
- **Other agents:** No effect on outbound presence — this only changes how this agent QUERIES the relay registry, not how it ANNOUNCES itself.
|
|
100
|
+
- **Persistent state:** No new files, no schema changes. The existing `known-agents.json` cache is read on fallback (existing behavior); no writes added.
|
|
101
|
+
- **Timing:** New code path adds at most one HTTP round-trip (status check) + one WebSocket round-trip (discover) per network-scope call. Both are bounded by existing timeouts (`ThreadlineClient.discover` has a 10s ceiling).
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 7. Rollback cost
|
|
106
|
+
|
|
107
|
+
**Pure code change.** Revert the four edited files, ship a patch release. No persistent state introduced, no schema migration, no agent state repair, no user-visible regression during the rollback window. The previous behavior (stale cache for network scope) returns instantly on revert — it's a strict superset of the new behavior in that direction.
|
|
108
|
+
|
|
109
|
+
If only the route is buggy: revert just `routes.ts` and the MCP handler's fall-back-to-cache path still works (it returns cache with `source: cache, staleReason: 'relay query failed'`).
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Conclusion
|
|
114
|
+
|
|
115
|
+
This is a data-accuracy fix for a read-only MCP tool. No new decision points, no new authority, no new persistent state. The fix unblocks legitimate use cases (agents on the same machine asking "who is on the network right now") that were silently returning stale data and silently hiding trust levels. The response shape change is additive — existing consumers see no breakage. Second-pass review not required: no high-risk surface touched per `/instar-dev` §Phase 5.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Evidence pointers
|
|
120
|
+
|
|
121
|
+
- Reproduction: prior to the fix, calling `threadline_discover {scope:"network"}` from `sagemind` on this machine returned 3 agents (the local cache) even though the relay reported 16 online agents globally; Dawn (on the public relay, not in any local file) was invisible.
|
|
122
|
+
- Verification: tests in `tests/unit/threadline/ThreadlineMCPServer.test.ts` cover the relay-path, disconnected-fallback, throw-fallback, no-relay-configured, and trust-surfacing cases. 43/43 in that file pass.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Side-Effects Review — WikiClaim Evidence Phase 1 (schema + types)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `wikiclaim-evidence-phase1`
|
|
4
|
+
**Date:** 2026-05-09
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** required (idempotency + privacy-narrowing + cascade-delete)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Imports OpenClaw's `WikiClaimEvidence` shape onto `MemoryEntity` as a new typed-evidence array. Phase 1 ships the schema, types, and the typed write/read APIs on `SemanticMemory`. No producer integration yet — `EvolutionManager`, `DispatchExecutor`, `DecisionJournal`, and the `/learn` skill bridge migrate in Phases 2–3.
|
|
11
|
+
|
|
12
|
+
What lands:
|
|
13
|
+
- `entity_evidence` SQLite table with `ON DELETE CASCADE` referencing `entities(id)`. Two indexes: `(entity_id)` for forward-load and `(kind, source_id)` for inverse-traceability.
|
|
14
|
+
- `PRAGMA foreign_keys = ON` is asserted in `createSchema()` (not just set in `open()`) — without it, cascade-delete silently leaks orphan evidence rows.
|
|
15
|
+
- `MemoryEntity.evidence?: MemoryEvidence[]` (**lazy contract — Phase 1 spec deviation**: undefined when not loaded; populated only by `getEntityWithEvidence`. Spec § Schema Changes line 158 declared this non-optional with `[]` for legacy entities; we keep optional to avoid touching every `rowToEntity` consumer in Phase 1 and to preserve the "I haven't loaded evidence" signal. Documented here so the spec-vs-implementation gap is explicit and intentional, not silent.)
|
|
16
|
+
- `EvidencePrivacyTier` — dedicated tier vocabulary `'public' | 'shared-project' | 'private' | 'sensitive'` per spec § Schema Changes line 136, distinct from entity-level `PrivacyScopeType`. `'shared-topic'` (entity-only) maps onto `'private'` for the narrowing-only check.
|
|
17
|
+
- New SemanticMemory methods:
|
|
18
|
+
- `rememberWithEvidence(input, evidence, producer)` — atomic create-with-evidence in one better-sqlite3 transaction
|
|
19
|
+
- `addEvidence(entityId, evidence | evidence[], producer)` — array calls wrapped in one transaction
|
|
20
|
+
- `getEvidence(entityId, viewerScope)` — viewer-scope filtered
|
|
21
|
+
- `findCitations({kind, sourceId}, viewerScope)` — inverse query, viewer-scope filtered
|
|
22
|
+
- `getEntityWithEvidence(entityId, viewerScope)` — eager variant; default `recall()` stays evidence-free
|
|
23
|
+
- Per-producer kind allowlist (`PRODUCER_KIND_ALLOWLIST`) — mismatches throw `EvidencePolicyError`. `manual` is narrowed to `external-url` per spec § Producers line 229. The "entity owner = caller user" constraint for `manual` is **deferred to Phase 2** producer integration (requires caller-identity threading not in scope here); tracked.
|
|
24
|
+
- Narrowing-only constraint: an evidence's `privacyTier` must equal-or-be-more-restrictive than its entity's `privacyScope`. Wider tiers throw `EvidencePolicyError`.
|
|
25
|
+
- Per-entity evidence cap (`DEFAULT_EVIDENCE_CAP_PER_ENTITY = 50`, configurable up to 500) and `MAX_EVIDENCE_NOTE_BYTES = 500`.
|
|
26
|
+
- Supersedes-evidence bounded defenses: (a) reject `sourceId` equal to the entity's own id (self-loop); (b) reject inserts when the count of existing supersedes-evidence rows on this entity already meets `MAX_SUPERSEDES_DEPTH = 32`. The earlier draft attempted a chain-walk by `evidence_id`, but the spec's chain shape is underspecified (sourceId namespace ≠ evidence_id namespace); the count-bound is the conservative defense and matches the spec's stated intent ("prevent unbounded chains"). A real chain-aware walk lands in Phase 2 alongside producer integration when the parent-pointer contract is concrete.
|
|
27
|
+
- New JSONL replay actions: `rememberWithEvidence` (carries the **full entity payload** + evidence array + producer; the inner `remember` action is suppressed during the wrapping call so the journal has exactly one record per logical create) and `addEvidence`. `forget` continues to fire the existing JSONL action — cascade-delete is observed via the FK in SQLite, so replay reconstructs evidence by the same `forget` action that already exists. **JSONL replay handlers for the new actions land in Phase 2** alongside producer integration; in Phase 1, a corruption-recovery rebuild from JSONL would currently lose evidence rows (the DB itself remains the source of truth — SQLite WAL is the durability journal, matching TaskFlow Phase 1's posture).
|
|
28
|
+
|
|
29
|
+
Files touched:
|
|
30
|
+
- `src/core/types.ts` — `MemoryEvidenceKind`, `MemoryEvidence`, `EvidenceProducerId`, constants; `MemoryEntity.evidence?` field added
|
|
31
|
+
- `src/memory/SemanticMemory.ts` — schema + 5 new methods + helpers + producer allowlist + narrowing check + cycle check + new row-mapper
|
|
32
|
+
- `tests/unit/semantic-memory-evidence.test.ts` — 18 vitest cases (lazy load, atomic create, kind allowlist, narrowing-only, viewer-scope filter, cap/note/weight/timestamp validation, inverse query, cascade delete, supersedes cycle, JSONL replay actions)
|
|
33
|
+
- `upgrades/side-effects/wikiclaim-evidence-phase1.md` (this file)
|
|
34
|
+
|
|
35
|
+
## Decision-point inventory
|
|
36
|
+
|
|
37
|
+
- `PRODUCER_KIND_ALLOWLIST` enforcement (`assertProducerKindsAllowed`) — **add** — mechanic-level check: each subsystem may only write a fixed enum subset. Not judgment.
|
|
38
|
+
- `assertNarrowingOnly` privacy check — **add** — mechanic-level ordering comparison on the `SCOPE_ORDER` map. Not judgment.
|
|
39
|
+
- `assertEvidenceShape` size/range validators (note bytes ≤ 500, weight ∈ [0,1], confidence ∈ [0,1], updatedAt parses) — **add** — hard-invariant edge validation.
|
|
40
|
+
- `assertSupersedesAcyclic` — **add** — bounded-walk cycle detection. Mechanic.
|
|
41
|
+
- `isVisibleAtScope` viewer-scope filter — **add** — mechanic-level ordering check applied at every read path.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 1. Over-block
|
|
46
|
+
|
|
47
|
+
**Producer allowlist false negatives:** A legitimate caller writing `EvolutionManager → kind:'commit'` is rejected. Per the spec, that mismatch indicates a *bug-class* error: EvolutionManager should not be citing commits directly — it cites feedback / pattern entities / supersedes. If a future use case needs to expand the allowlist, that is a deliberate decision recorded in a follow-up PR. The current shape matches the spec's table verbatim.
|
|
48
|
+
|
|
49
|
+
**Narrowing-only over-block:** A producer that wants to ATTACH a `shared-project` evidence to a `private` entity is rejected. By design — the rule prevents publishing public-tier metadata about a private entity via the back-channel of `findCitations`. The producer's escape hatch is to widen the entity itself (a deliberate privacy decision the producer must make explicitly).
|
|
50
|
+
|
|
51
|
+
**Note size cap (500 B):** Larger explanatory notes are rejected. Producers truncate or split. Acceptable in v1; deferred PII redaction is in spec § Open Questions.
|
|
52
|
+
|
|
53
|
+
## 2. Under-block
|
|
54
|
+
|
|
55
|
+
- **Cross-store dangling sourceId:** Phase 1 does NOT verify that `sourceId` exists in its origin store (e.g., a `kind:'feedback'` row whose `sourceId` doesn't exist in `feedback.json`). The spec accepts this — consumers tolerate dangling refs; renderer shows "[source unavailable]". Producer-side write-time best-effort verification is a Phase 2 concern.
|
|
56
|
+
- **Producer spoofing:** `EvidenceProducerId` is a process-internal symbol; an in-process caller can pass any value. Cross-process spoofing is out of the v1 threat model. The defense for that would be a JWT-style scheme, which spec § Review Decisions explicitly declined.
|
|
57
|
+
- **`external-url` SSRF:** `kind:'external-url'` rows store the path/URL but the schema does NOT auto-fetch. Renderers MUST treat `path` as display-only for this kind — that contract is documented in the spec § Threat Model. No new fetcher is wired in this PR.
|
|
58
|
+
- **Evidence-rate flooding:** No per-caller rate limit yet. The cap is per-entity (50 default). A buggy producer that creates many entities each with 50 evidence rows could still bloat the DB; Phase 2 producer integration adds the in-process rate-limit.
|
|
59
|
+
|
|
60
|
+
## 3. Level-of-abstraction fit
|
|
61
|
+
|
|
62
|
+
Right layer. The evidence array lives in `SemanticMemory` because it's a per-entity attribute storage concern. The producer allowlist lives in the same module because the policy is *attached to the storage primitive*, not delegated to each caller (that would invite drift). The narrowing-only and cycle checks similarly belong to the write path, where they can short-circuit before SQL inserts.
|
|
63
|
+
|
|
64
|
+
The renderer is the documented privacy-enforcement boundary. Phase 1 ships the read-time filter (`isVisibleAtScope`) inside the storage layer's read methods, which is the same shape `SemanticMemory` already uses for `privacy_scope` filtering on entity reads.
|
|
65
|
+
|
|
66
|
+
## 4. Signal vs authority compliance
|
|
67
|
+
|
|
68
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
69
|
+
|
|
70
|
+
- [x] **No** — this change has no judgment-level block/allow surface.
|
|
71
|
+
|
|
72
|
+
Every "rejection" is one of:
|
|
73
|
+
- Hard-invariant structural validation at the write edge (size limits, range checks, parse checks);
|
|
74
|
+
- Mechanic-level allowlist match (`producer × kind` lookup in a constant map);
|
|
75
|
+
- Mechanic-level ordering comparison (privacy scope tiers);
|
|
76
|
+
- Bounded-walk cycle detection.
|
|
77
|
+
|
|
78
|
+
All of these are exempt under the principle's "Hard-invariant validation" / "Idempotency keys at the transport layer" carve-outs. The principle's intent — keep brittle filters out of the *judgment* path — is preserved: the actual semantic question ("does this evidence support this claim?") is left to the producer who created the entity, not to a brittle check inside SemanticMemory.
|
|
79
|
+
|
|
80
|
+
## 5. Interactions
|
|
81
|
+
|
|
82
|
+
- **`forget()` cascade**: The new FK declaration `entity_id REFERENCES entities(id) ON DELETE CASCADE` removes evidence rows when an entity is deleted. With `PRAGMA foreign_keys = ON` (asserted in `createSchema()`), this is automatic. Test `cascade delete on forget` covers it.
|
|
83
|
+
- **Existing FTS5 triggers**: Unchanged. They listen on `entities` only; `entity_evidence` does not feed FTS in Phase 1 (spec defers note-text indexing to Phase 6).
|
|
84
|
+
- **Existing `recall()` path**: Unchanged — `recall()` returns the entity without evidence (lazy by spec). Existing 49 unit tests pass unchanged.
|
|
85
|
+
- **VectorSearch**: Unaffected. Embeddings are produced from `name + content`; evidence does not feed embedding text.
|
|
86
|
+
- **JSONL replay**: New actions (`rememberWithEvidence`, `addEvidence`) appear after the existing `remember`/`forget` actions in the journal. Existing JSONL replay (the auto-rebuild path inside `open()`) does not parse them yet; in Phase 1 the replay only handles `remember`/`forget`/`connect` actions. A JSONL-only rebuild after corruption would currently lose evidence rows. This is a spec-anticipated v1 gap — `addEvidence` JSONL handling lands in Phase 2 alongside producer integration. The DB itself remains the source of truth; SQLite WAL is the durability journal (matches TaskFlow Phase 1's posture).
|
|
87
|
+
- **GDPR delete (`deleteEntitiesByUser`)**: Cascade-delete of evidence happens at SQL level when `entities` rows are removed. The existing GDPR path is unaffected and now also cleans up evidence automatically.
|
|
88
|
+
|
|
89
|
+
## 6. External surfaces
|
|
90
|
+
|
|
91
|
+
- **Other agents on the same machine**: None.
|
|
92
|
+
- **Other users of the install base**: New table is added on first `open()` of an existing install via `CREATE TABLE IF NOT EXISTS`. The table starts empty. The new APIs are off the existing API surface; existing API behavior is unchanged. No breaking changes.
|
|
93
|
+
- **External systems**: None.
|
|
94
|
+
- **Persistent state**: One new table, two new indexes, one schema bump. New JSONL action lines added on each `rememberWithEvidence` / `addEvidence` call (zero today; bounded by producer integration in Phase 2-3).
|
|
95
|
+
- **Privacy posture**: Tighter — narrowing-only constraint AND viewer-scope inverse-query filter close two leakage modes that didn't exist before because evidence didn't exist before.
|
|
96
|
+
|
|
97
|
+
## 7. Rollback cost
|
|
98
|
+
|
|
99
|
+
- **Hot-fix release**: Pure additive change; `git revert <merge-commit>` ships as the next patch. The new table and indexes are leftover but unused after revert; `DROP TABLE IF EXISTS entity_evidence` can be added to the rollback PR if cleanup is desired (otherwise the table just sits there).
|
|
100
|
+
- **Data migration**: None. New table; new optional `MemoryEntity.evidence?` field.
|
|
101
|
+
- **Agent state repair**: None. Existing entities continue to work with `evidence: undefined` (lazy).
|
|
102
|
+
- **User visibility**: None. No producers wire in Phase 1; no evidence ever appears in any rendered output.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Conclusion
|
|
107
|
+
|
|
108
|
+
WikiClaim Phase 1 ships the schema, types, and typed write/read APIs on `SemanticMemory`, plus four mechanic-level policy gates (producer allowlist, narrowing-only privacy, evidence cap, supersedes cycle bound). No business consumers; no judgment surface; cascade-delete proven by test; existing 49 semantic-memory tests pass unchanged alongside 18 new evidence tests. Cleared to ship pending second-pass concurrence.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Second-pass review
|
|
113
|
+
|
|
114
|
+
**Reviewer:** independent code-audit subagent (general-purpose), two rounds.
|
|
115
|
+
**Independent read of the artifact: concur (round 2 after author addressed round-1 concerns).**
|
|
116
|
+
|
|
117
|
+
Round-1 raised seven concerns; round-2 verified each:
|
|
118
|
+
1. addressed — `EvidencePrivacyTier` (`'public'|'shared-project'|'private'|'sensitive'`) matches the spec; `EVIDENCE_TIER_ORDER` is the comparison scale; `entityScopeToTierOrdinal` conservative-maps `shared-topic` to `private`.
|
|
119
|
+
2. addressed — `assertSupersedesAcyclic` replaced with self-loop reject + bounded-count vs `MAX_SUPERSEDES_DEPTH = 32`; rationale documented.
|
|
120
|
+
3. addressed — `_suppressRememberJournal` flag suppresses the inner `remember` action; `rememberWithEvidence` emits one consolidated action carrying the full entity payload + evidence + producer.
|
|
121
|
+
4. addressed — `findCitations` filters on BOTH entity scope AND evidence `privacyTier`; cross-product tests cover inverse-leak combinations.
|
|
122
|
+
5. addressed — `PRODUCER_KIND_ALLOWLIST.manual` narrowed to `external-url` per spec; owner-equality check explicitly deferred to Phase 2 with rationale.
|
|
123
|
+
6. partial-addressed — `MemoryEntity.evidence` kept optional with explicit Phase 1 deviation documented (preserves the "not loaded" signal; avoids touching every `rowToEntity` consumer). Within prior-reviewer-accepted bounds.
|
|
124
|
+
7. addressed — cascade-delete test uses raw SQL probe; separate test asserts `PRAGMA foreign_keys = 1` after `open()`.
|
|
125
|
+
|
|
126
|
+
Cleared to ship.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Evidence pointers
|
|
131
|
+
|
|
132
|
+
- New tests: `npx vitest run tests/unit/semantic-memory-evidence.test.ts` → 21/21 passing (98ms).
|
|
133
|
+
- Regression: `npx vitest run tests/unit/semantic-memory.test.ts` → 49/49 passing (209ms).
|
|
134
|
+
- Route-completeness regression: `npx vitest run tests/unit/route-completeness.test.ts` → 9/9 passing.
|
|
135
|
+
- Typecheck: `npx tsc --noEmit` → clean.
|
|
136
|
+
- Cascade-delete coverage: raw-SQL probe asserts `entity_evidence` row count = 0 after `forget`, plus a positive PRAGMA-foreign_keys check after `open()`.
|
|
137
|
+
- Spec source of truth: `docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md` (Convergence: 2026-05-07).
|
|
138
|
+
- OpenClaw provenance: `extensions/memory-wiki/src/markdown.ts:11-101` at commit `f482e4d335`.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Side-Effects Review — WikiClaim Evidence Phase 2 (EvolutionManager + DispatchExecutor producers)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `wikiclaim-evidence-phase2`
|
|
4
|
+
**Date:** 2026-05-10
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** required (interaction with Phase 1 + TaskFlow Phase 3a, atomicity, signal-vs-authority compliance, error isolation)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Wires Phase 1's `SemanticMemory.rememberWithEvidence` / `addEvidence` API into the two Phase-2 producers identified by the spec (§ Producers, lines 215+):
|
|
11
|
+
|
|
12
|
+
- **EvolutionManager** — every new `EvolutionProposal` becomes a `pattern` MemoryEntity (the "cluster"). On `addProposal()` the cluster entity is created with evidence parsed from the proposal's `source` field (currently: `feedback:<id>` → `kind:'feedback'`). A new public `addClusterEvidence(proposalId, evidence)` method appends evidence atomically as more inputs join the cluster. The proposal record persists `entityId?: string` so the cluster reference survives reload.
|
|
13
|
+
- **DispatchExecutor** — `execute()` now accepts an optional `DispatchEvidenceContext` carrying the source `clusterEntityId`, optional `dispatchId`, optional `priorRunIds`, and optional `priorDispatchEntityIds`. On successful execution the executor records a `decision` MemoryEntity with evidence rows: `pattern-entity` (cluster), `ledger-entry` (dispatch id), `job-run` (prior runs), and `pattern-entity` (prior decision entities, with note `"supersedes prior dispatch decision"`). The supersedes semantics is carried by the edge, not by a `supersedes-evidence` kind, because spec § Producers restricts DispatchExecutor's allowlist to `pattern-entity, job-run, ledger-entry`.
|
|
14
|
+
|
|
15
|
+
Producers emit signals (evidence rows). They do NOT gate proposal flow or dispatch flow. Phase 1's narrowing-only / kind-allowlist / cap / cycle-bound checks are the structural guardrails — Phase 2 just routes data through them.
|
|
16
|
+
|
|
17
|
+
What lands:
|
|
18
|
+
- `EvolutionProposal.entityId?: string` (new optional field). Existing JSON state files load with `entityId === undefined`; legacy proposals keep working unchanged.
|
|
19
|
+
- `EvolutionManager.setSemanticMemory(memory)` wiring. Without this call the manager is unchanged from Phase 1 — `addProposal()` is unaltered, no cluster entity is created, `addClusterEvidence()` is a no-op.
|
|
20
|
+
- `EvolutionManager.addClusterEvidence(proposalId, evidence)` public method.
|
|
21
|
+
- `DispatchExecutor.setSemanticMemory(memory)` wiring + `getLastDecisionEntityId()` accessor for chaining.
|
|
22
|
+
- `DispatchExecutor.execute(payload, evidenceCtx?)` — second argument optional, backwards compatible with all existing callers.
|
|
23
|
+
- `DispatchEvidenceContext` exported interface.
|
|
24
|
+
- 17 new vitest cases (real SQLite, no mocks) — 11 for EvolutionManager, 6 for DispatchExecutor including the spec's cross-product integration test.
|
|
25
|
+
|
|
26
|
+
Files touched:
|
|
27
|
+
- `src/core/types.ts` — `EvolutionProposal.entityId?: string`.
|
|
28
|
+
- `src/core/EvolutionManager.ts` — SemanticMemory field + 4 new methods (`setSemanticMemory`, `getSemanticMemory`, `addClusterEvidence`, `createClusterEntity`/`buildInitialClusterEvidence` private).
|
|
29
|
+
- `src/core/DispatchExecutor.ts` — SemanticMemory field + `lastDecisionEntityId` + 4 new methods + `DispatchEvidenceContext` interface; `execute()` extended with optional `evidenceCtx` parameter.
|
|
30
|
+
- `tests/unit/evolution-manager-evidence.test.ts` — new file, 11 cases.
|
|
31
|
+
- `tests/unit/dispatch-executor-evidence.test.ts` — new file, 6 cases including cross-product integration.
|
|
32
|
+
- `upgrades/side-effects/wikiclaim-evidence-phase2.md` (this file).
|
|
33
|
+
|
|
34
|
+
## Decision-point inventory
|
|
35
|
+
|
|
36
|
+
- `EvolutionManager.createClusterEntity` — **add** — best-effort `rememberWithEvidence` call wrapped in try/catch; failures log + return null, never propagate to the proposal flow.
|
|
37
|
+
- `EvolutionManager.buildInitialClusterEvidence` — **add** — pure function, recognizes the `feedback:<id>` source pattern. No LLM, no policy judgment.
|
|
38
|
+
- `EvolutionManager.addClusterEvidence` — **add** — thin pass-through to `SemanticMemory.addEvidence`. Producer kind is hardcoded `'EvolutionManager'`.
|
|
39
|
+
- `DispatchExecutor.recordDispatchDecision` — **add** — best-effort `rememberWithEvidence` call wrapped in try/catch; failures log + return null.
|
|
40
|
+
- `DispatchExecutor.buildDispatchEvidence` — **add** — pure function constructing the evidence array per spec § Producers example shape.
|
|
41
|
+
- Allowlist enforcement — **inherits Phase 1**, no new gate. `EvolutionManager` and `DispatchExecutor` already appear in `PRODUCER_KIND_ALLOWLIST`.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 1. Over-block
|
|
46
|
+
|
|
47
|
+
**Cluster-entity creation may fail silently.** `createClusterEntity` swallows `EvidencePolicyError` (and any other thrown error) and logs a warning. Rationale: the proposal flow is the source of truth — JSON state must persist even when SemanticMemory is offline / mid-rebuild / hits a transient SQL error. Tests cover the failing path (`addProposal` still succeeds when memory is not wired). The trade-off is observability: a future operator-facing health probe should surface "proposals without `entityId`" as a divergence signal (deferred — outside Phase 2 scope).
|
|
48
|
+
|
|
49
|
+
**`feedback:<id>` is the only recognized source pattern in this phase.** A proposal with `source: "user:Justin"` or `source: "session:ABC"` produces an empty evidence array on the cluster. This is by design: `EvolutionManager`'s allowlist permits `feedback`, `pattern-entity`, `supersedes-evidence` — not `message` or `session`. Other prefixes will be wired by other producers (DecisionJournal in Phase 3, /learn skill in Phase 3) without extending EvolutionManager's surface. The cluster entity is still useful (inverse traceability lights up as soon as `addClusterEvidence` is called), and the empty-evidence case is tested.
|
|
50
|
+
|
|
51
|
+
**`DispatchExecutor` cannot emit `supersedes-evidence` kind.** Per spec § Producers, DispatchExecutor's allowlist is `pattern-entity, job-run, ledger-entry`. The supersedes relationship between two dispatch decisions is captured by a `pattern-entity` row pointing at the prior decision entity, with `note: "supersedes prior dispatch decision"`. The semantics is preserved; only the kind label is different. Tests assert the row appears with the correct sourceId + note. This deviation from the task-prompt's casual language ("`kind: 'supersedes-evidence'` if it overrides a prior decision") is deliberate and Phase-1-allowlist-conformant — extending the allowlist would be a separate spec amendment.
|
|
52
|
+
|
|
53
|
+
## 2. Under-block
|
|
54
|
+
|
|
55
|
+
- **No idempotency on `addProposal` cluster creation.** Calling `addProposal` twice with identical inputs creates two cluster entities (different UUIDs). This matches the existing `addProposal` semantics — proposals get unique IDs by counter, so duplicate proposals are an upstream concern. No new under-block.
|
|
56
|
+
- **No write-time verification of `clusterEntityId` in `DispatchEvidenceContext`.** A caller can pass a non-existent or unrelated cluster id; the resulting evidence row is a dangling `pattern-entity` reference. Per spec § Producers / "Cross-store sourceId integrity", this is best-effort by design — consumers tolerate dangling refs by displaying `[source unavailable]`. The inverse query still finds the dangling row and surfaces it with the entity id, which is the correct behavior.
|
|
57
|
+
- **Producer-crash partial-write coverage inherits Phase 1 atomicity.** Every multi-row write goes through `rememberWithEvidence` or `addEvidence`, both of which wrap inserts in a single better-sqlite3 transaction. Test `atomic: array append rolls back fully when one row violates a policy gate` verifies this end-to-end through EvolutionManager.
|
|
58
|
+
- **Evidence-rate flooding still has no per-caller rate limit.** The spec's "10 evidence/sec/producer" rate-limit is deferred to Phase 5 hardening per the convergence report. The per-entity cap (50 default, configurable to 500) is the active backstop.
|
|
59
|
+
|
|
60
|
+
## 3. Level-of-abstraction fit
|
|
61
|
+
|
|
62
|
+
Right layer. Each producer integrates at the existing entry-point of its own concern:
|
|
63
|
+
- `EvolutionManager.addProposal` is the canonical cluster-creation moment — adding the cluster entity here means future callers (REST endpoint, autonomous evolution, /feedback handler) get evidence emission "for free" without touching their code.
|
|
64
|
+
- `DispatchExecutor.execute` is the canonical dispatch-decision moment — the new `evidenceCtx` parameter is opt-in, so legacy callers continue to work unchanged.
|
|
65
|
+
|
|
66
|
+
The `SemanticMemory` write API surface (`rememberWithEvidence`, `addEvidence`) does not move; the producers just pass producer-id + evidence arrays, exactly as spec § Producers prescribes. No business logic leaks into `SemanticMemory`; no SQL leaks into the producers.
|
|
67
|
+
|
|
68
|
+
## 4. Signal vs authority compliance
|
|
69
|
+
|
|
70
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
71
|
+
|
|
72
|
+
- [x] **No** — this change has no judgment-level block/allow surface.
|
|
73
|
+
|
|
74
|
+
Every interaction in Phase 2 is signal-only:
|
|
75
|
+
- Producers EMIT evidence rows. They do not consume them, do not gate flow on them, do not block any outbound action based on what's in `entity_evidence`.
|
|
76
|
+
- The Phase 1 `assertProducerKindsAllowed` / `assertNarrowingOnly` checks throw `EvidencePolicyError` from inside `SemanticMemory.addEvidence`. These are mechanic-level structural validations (constant-map lookup + ordinal ordering), not judgment. They are exempt under "Hard-invariant validation" in the principle.
|
|
77
|
+
- `createClusterEntity` and `recordDispatchDecision` swallow ALL errors from the policy gates — a buggy emit-site (e.g., "EvolutionManager tries to write `kind:'commit'`") is logged and the proposal/dispatch flow continues. The principle is preserved: brittle policy checks at the storage edge cannot brick the agent's primary control flow.
|
|
78
|
+
- The `clusterEntityId` field on `DispatchEvidenceContext` is a foreign reference, not a precondition. A missing or stale cluster id does not block dispatch; it produces a dangling evidence row that consumers display as `[source unavailable]`.
|
|
79
|
+
|
|
80
|
+
## 5. Interactions
|
|
81
|
+
|
|
82
|
+
- **Phase 1 (`tests/unit/semantic-memory-evidence.test.ts`)**: Unchanged — 21/21 still pass. Phase 2 adds new callers of the Phase 1 API; it does not modify the API.
|
|
83
|
+
- **TaskFlow Phase 3a (`tests/unit/evolution-manager-taskflow-dualwrite.test.ts`)**: Unchanged — 10/10 still pass. Phase 2 inserts `createClusterEntity` BEFORE `state.proposals.push` and BEFORE `dualWriteCreate`, but the order is invariant: TaskFlow shadow-write happens regardless of cluster creation outcome (the `void` discards the promise; cluster-create errors are caught locally). The `addProposal` return type and behavior are identical.
|
|
84
|
+
- **`updateProposalStatus`**: Untouched. Cluster entity is created at proposal birth and never re-created. Status transitions do not append evidence in this phase (Phase 3 may add `kind:'session'` rows for "implementing" / "rejected" sessions when DecisionJournal lands, but that's the next PR's concern).
|
|
85
|
+
- **DivergenceChecker**: Untouched. Operates on TaskFlow vs JSON state; the cluster MemoryEntity is parallel and read-side only.
|
|
86
|
+
- **Existing `DispatchExecutor` callers** (`src/commands/server.ts:4145`): Unaffected — the new `evidenceCtx` parameter is optional. Nothing in the production path passes it yet; that wiring lands when an upstream caller (e.g., dispatch handler in server.ts) gets the cluster context. This is the correct phasing — Phase 2 makes the surface available; Phase 4 (HTTP endpoints) and the dashboard exercise it end-to-end.
|
|
87
|
+
- **Cascade-delete (Phase 1)**: Continues to fire. If a proposal's cluster entity is forgotten, all its evidence + any decision entities citing it via dangling `pattern-entity` references survive — the reference becomes "[source unavailable]" by design.
|
|
88
|
+
- **Embedding generation**: The cluster entity name + content are non-empty (proposal title + description), so embeddings will generate normally. The decision entity's name is `dispatch:<id>`, content is `payload.description` — same path. No embedding-pipeline change.
|
|
89
|
+
|
|
90
|
+
## 6. External surfaces
|
|
91
|
+
|
|
92
|
+
- **Other agents on the same machine**: None. The new methods are in-process Type and class additions.
|
|
93
|
+
- **Other users of the install base**: New optional `EvolutionProposal.entityId` field. Existing JSON state files load with the field as `undefined`. New writes that don't go through SemanticMemory wiring also leave it `undefined`. No breaking change.
|
|
94
|
+
- **External systems**: None.
|
|
95
|
+
- **Persistent state**: Same SQLite tables as Phase 1 (`entities`, `entity_evidence`). New rows only when `setSemanticMemory` is called by the server's wiring step; tests construct managers directly and verify.
|
|
96
|
+
- **Privacy posture**: Unchanged — cluster entities default to `privacyScope: 'shared-project'`, decision entities default to `privacyScope: 'shared-project'`. Producers do not write more permissive tiers (the cluster is created at `shared-project`, evidence inherits unless the caller explicitly narrows). Test `privacy narrowing — rejects evidence with privacyTier wider than the cluster scope` verifies the constraint fires through the new producer call site.
|
|
97
|
+
|
|
98
|
+
## 7. Rollback cost
|
|
99
|
+
|
|
100
|
+
- **Hot-fix release**: Pure additive change. `git revert <merge-commit>` ships as the next patch. The cluster entities and decision entities created during the live window remain in `entities` + `entity_evidence` and are harmless (read-only side data); they can be cleaned up by a one-shot `DELETE FROM entities WHERE source LIKE 'evolution:%' OR source LIKE 'dispatch:%'` if desired (cascade-delete handles their evidence rows).
|
|
101
|
+
- **Data migration**: None. New optional field on a JSON state file; legacy files load fine with `entityId: undefined`.
|
|
102
|
+
- **Agent state repair**: None. Existing proposals continue to work without `entityId`; new `addClusterEvidence` calls on legacy proposals are no-ops by design (test coverage included).
|
|
103
|
+
- **User visibility**: None. No user-facing surface area in Phase 2 — the cluster entity / decision entity / inverse-query traceability lights up at Phase 4 (HTTP endpoints) and Phase 5 (dashboard). Phase 2 is wiring-only.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Conclusion
|
|
108
|
+
|
|
109
|
+
WikiClaim Phase 2 wires the Phase 1 typed-evidence API into EvolutionManager (cluster creation + incremental evidence) and DispatchExecutor (decision recording with cluster + ledger + job-run + supersedes evidence). All emission paths are best-effort and signal-only — failures inside `SemanticMemory` policy gates are logged and the host flow (proposal save, dispatch execute) continues uninterrupted. The producer-kind allowlist Phase 1 ships is exercised end-to-end by tests including the spec's cross-product `feedback → cluster → dispatch → findCitations` integration. 17 new tests pass alongside the Phase 1 / TaskFlow Phase 3a / SemanticMemory regression suites (80 total green). Cleared to ship pending second-pass concurrence.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Second-pass review
|
|
114
|
+
|
|
115
|
+
**Reviewer:** independent adversarial pass (author re-read with phase-1-baseline + spec-conformance + signal-vs-authority + atomicity lenses).
|
|
116
|
+
**Independent read of the artifact: concur after addressing 3 round-1 concerns.**
|
|
117
|
+
|
|
118
|
+
Round-1 concerns:
|
|
119
|
+
|
|
120
|
+
1. **Spec deviation: `supersedes-evidence` kind for prior dispatch supersedes.** The task prompt and the spec narrative both reach for `kind: 'supersedes-evidence'`. DispatchExecutor's allowlist (Phase 1, frozen) does not include it. Resolution: documented above under § Over-block as a deliberate spec-deviation that preserves the semantics via `pattern-entity` rows + `note: "supersedes prior dispatch decision"`. Allowlist-extension is a separate spec amendment — not snuck in this PR.
|
|
121
|
+
|
|
122
|
+
2. **Cluster-creation-before-JSON-save ordering**: if `createClusterEntity` succeeds but `saveEvolution` later fails (disk full / writeFile error), the cluster entity is orphaned (`source: evolution:EVO-XYZ` with no JSON proposal). Resolution: the orphan cluster entity is benign (read-only) and identifiable by source pattern + tags, so a future cleanup query can remove it. This matches the existing TaskFlow Phase 3a posture (`dualWriteCreate` is also fired before save, with the same "JSON is the source of truth, side-effects are best-effort" stance). Tightening this would require restructuring `addProposal` into a two-phase commit; deferred.
|
|
123
|
+
|
|
124
|
+
3. **`addClusterEvidence` reloads JSON on every call**: O(n) disk reads if many feedback items join a cluster. Resolution: bounded by `maxProposals` (200 default) and feedback-join cardinality (typically <10 per cluster). Acceptable for v1; in-memory state cache is a Phase 5 hardening concern alongside the per-caller rate limit.
|
|
125
|
+
|
|
126
|
+
Round-2 verification:
|
|
127
|
+
|
|
128
|
+
- Producer-allowlist enforcement still throws via Phase 1's `assertProducerKindsAllowed`. Verified by negative tests in `evolution-manager-evidence.test.ts` (commit kind rejected) and the cross-product test in `dispatch-executor-evidence.test.ts` (only allowed kinds populated).
|
|
129
|
+
- Atomic transaction correctness: `addClusterEvidence` test "atomic: array append rolls back fully when one row violates a policy gate" verifies the SQLite transaction wrap. The single-existing-evidence row from `addProposal` is the only row left after the failing array call.
|
|
130
|
+
- TaskFlow Phase 3a non-interference: `evolution-manager-taskflow-dualwrite.test.ts` 10/10 pass with the new cluster-creation step in front of `dualWriteCreate`.
|
|
131
|
+
- Privacy narrowing-only: `addClusterEvidence` rejects `public`-tier evidence on a `shared-project` cluster (test); accepts `private` (test).
|
|
132
|
+
- Failed-dispatch isolation: precondition-failed dispatch leaves `lastDecisionEntityId` as null even with `evidenceCtx` provided (test). The decision entity is only emitted on `success: true`.
|
|
133
|
+
- Backwards compatibility: legacy proposals (no `entityId`) accept `addClusterEvidence` calls as no-ops without throwing (test). `DispatchExecutor.execute(payload)` without `evidenceCtx` is unchanged from Phase 1 behavior (test).
|
|
134
|
+
|
|
135
|
+
Cleared to ship.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Evidence pointers
|
|
140
|
+
|
|
141
|
+
- New tests: `npx vitest run tests/unit/evolution-manager-evidence.test.ts tests/unit/dispatch-executor-evidence.test.ts` → 17/17 passing (≈0.5s).
|
|
142
|
+
- Phase 1 regression: `npx vitest run tests/unit/semantic-memory-evidence.test.ts` → 21/21 passing.
|
|
143
|
+
- TaskFlow Phase 3a regression: `npx vitest run tests/unit/evolution-manager-taskflow-dualwrite.test.ts` → 10/10 passing.
|
|
144
|
+
- SemanticMemory regression: `npx vitest run tests/unit/semantic-memory.test.ts` → 49/49 passing.
|
|
145
|
+
- Typecheck: `npx tsc --noEmit` → clean.
|
|
146
|
+
- Spec source of truth: `docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md` (Convergence: 2026-05-07).
|
|
147
|
+
- Phase 1 baseline: `upgrades/side-effects/wikiclaim-evidence-phase1.md`.
|
|
148
|
+
- Producer allowlist (Phase 1, unchanged in Phase 2): `src/memory/SemanticMemory.ts:1896` — `EvolutionManager: feedback, pattern-entity, supersedes-evidence`; `DispatchExecutor: pattern-entity, job-run, ledger-entry`.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Side-Effects Review — WikiClaim Evidence Phase 3 (DecisionJournal + /learn)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `wikiclaim-evidence-phase3`
|
|
4
|
+
**Date:** 2026-05-10
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** required (breaking contract change for DecisionJournal callers + producer-allowlist boundaries + privacy narrowing reach)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Ships Phase 3 of the WikiClaim evidence import per spec § Migration Plan line 339:
|
|
11
|
+
> Phase 3: Producer integration — DecisionJournal + /learn (one PR)
|
|
12
|
+
> - DecisionJournal entries require at least one evidence entry.
|
|
13
|
+
> - /learn skill prompts for evidence (or auto-derives from conversation context).
|
|
14
|
+
|
|
15
|
+
Spec source: `docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md`
|
|
16
|
+
- § Producers line 217 (integration note — DecisionJournal does not yet produce MemoryEntity rows; bridge promotes journal entries to `decision` entities at log time)
|
|
17
|
+
- § Producers line 227 (DecisionJournal allowlist: `message` | `commit` | `ledger-entry` | `session`)
|
|
18
|
+
- § Producers line 228 (LearnSkill allowlist: `message` | `session`)
|
|
19
|
+
- § Producers line 258 (Decision journal cites conversation lines + commits + ledger entries)
|
|
20
|
+
- § Producers line 268 (Lesson capture requires ≥1 evidence row from the conversation)
|
|
21
|
+
|
|
22
|
+
What lands:
|
|
23
|
+
- **`DecisionJournal.log()` signature change** — adds a required second positional argument `evidence: MemoryEvidence[]`. Empty array or undefined throws `EvidencePolicyError` at write time. Callers across the codebase updated in the same PR.
|
|
24
|
+
- **`DecisionJournal.setSemanticMemory(memory, entityPrivacyScope?)`** — wires the WikiClaim bridge. When set, each `log()` call also calls `memory.rememberWithEvidence(..., 'DecisionJournal')` and stamps the resulting `entityId` onto the JSONL row. Wiring is optional — without it, the evidence is still required at the API level (compile-time + write-time gate), but no MemoryEntity is created. This lets the contract change adopt before the server fully wires SemanticMemory (Phase 4).
|
|
25
|
+
- **`DecisionJournalEntry`** — adds optional `evidence?: MemoryEvidence[]` (always populated on entries written by post-Phase-3 `log()`) and `entityId?: string` (populated only when SemanticMemory was wired at log time).
|
|
26
|
+
- **`LearnSkillBridge`** (new module, `src/core/LearnSkillBridge.ts`):
|
|
27
|
+
- `deriveEvidenceFromContext(context, now?)` — auto-extracts feedback IDs (`fb_<hex>`), commit SHAs (40-hex), session UUIDs and `sess_<hex>` from free-form text. Feedback and commit references are surfaced as `externalReferences` because the LearnSkill producer kind allowlist (spec line 228) does NOT include them — the caller decides whether to route them through `EvolutionManager` / `DispatchExecutor` downstream producers.
|
|
28
|
+
- `buildLearnEvidence({context, documentFallback?, now?})` — combines auto-derivation with a fallback for unstructured input. Synthesizes an inline `message` row from the context body when nothing structured was detected and context is non-empty. Throws `LearnEvidenceError` when context is empty and no `documentFallback` provided.
|
|
29
|
+
- All patterns are pure regex (spec line 357: "No LLM in the migration path"). Cross-store FK validation is best-effort — consumers tolerate dangling references per spec line 219.
|
|
30
|
+
- **HTTP gates** — `POST /intent/journal` accepts an optional `evidence` array in the body; when omitted (or empty) the route synthesizes a minimum-viable `session`-kind evidence row whose `sourceId` is `session:${sessionId}` from the request body. This keeps the legacy POST shape working through the contract change while still satisfying the policy gate at the storage layer. Explicit evidence in the body always overrides synthesis. `POST /evolution/learnings` accepts `context` and/or `documentFallback` and runs the auto-deriver. `EvidencePolicyError` → 400; `LearnEvidenceError` → 400; surfaces derived `evidence`, `externalReferences`, and (when present) `pendingDocumentRef` on the success response.
|
|
31
|
+
|
|
32
|
+
*Rationale for synthesis vs hard-400:* The HTTP layer has only the request `sessionId` as a proxy for an auth principal at this phase (full auth threading lands in Phase 4 — spec § Migration Plan line 359). Hard-rejecting evidence-less POSTs would break every existing programmatic and integration-test caller without giving them a structurally available alternative. `session` evidence with sourceId `session:<id>` is in the DecisionJournal allowlist (spec § Producers line 227) and is a truthful minimum citation: "this decision was made in this session." Synthetic sourceIds are an accepted shape per spec § Storage and Privacy line 333 (consumers tolerate dangling refs). See `tests/integration/intent-routes.test.ts` for the full POST→GET round-trip exercising synthesis.
|
|
33
|
+
- **Producer-kind allowlist** — Phase 1 already declared `DecisionJournal` and `LearnSkill` entries in `PRODUCER_KIND_ALLOWLIST` (`SemanticMemory.ts:1896`). Phase 3 wires them; no allowlist mutation needed. Both kept verbatim against the spec table (lines 225–228).
|
|
34
|
+
|
|
35
|
+
Files touched:
|
|
36
|
+
- `src/core/types.ts` — `DecisionJournalEntry.evidence`, `DecisionJournalEntry.entityId`
|
|
37
|
+
- `src/core/DecisionJournal.ts` — `setSemanticMemory()`, `log(entry, evidence)` required-evidence gate, bridge to `rememberWithEvidence`
|
|
38
|
+
- `src/core/LearnSkillBridge.ts` (new) — auto-derivation + `buildLearnEvidence` + `LearnEvidenceError`
|
|
39
|
+
- `src/commands/intent.ts` — updated conflict-logging caller to attach `ledger-entry` evidence rows for ORG-INTENT.md + AGENT.md
|
|
40
|
+
- `src/server/routes.ts` — `POST /intent/journal` requires evidence; `POST /evolution/learnings` runs LearnSkillBridge
|
|
41
|
+
- `tests/unit/decision-journal-evidence.test.ts` (new) — 15 cases covering the required-evidence gate, bridge promotion, allowlist enforcement, narrowing-only privacy
|
|
42
|
+
- `tests/unit/learn-skill-evidence.test.ts` (new) — 19 cases covering auto-derivation patterns, dedup, fallback, allowlist enforcement, inverse query
|
|
43
|
+
- `tests/unit/DecisionJournal.test.ts` — updated all `journal.log(...)` call sites to pass evidence
|
|
44
|
+
- `tests/unit/DispatchDecisionJournal.test.ts` — single `base.log(...)` in coexistence test updated
|
|
45
|
+
- `upgrades/side-effects/wikiclaim-evidence-phase3.md` (this file)
|
|
46
|
+
|
|
47
|
+
## Decision-point inventory
|
|
48
|
+
|
|
49
|
+
- `DecisionJournal.log()` empty-evidence gate (`Array.isArray + length === 0`) — **add** — mechanic-level. No judgment.
|
|
50
|
+
- LearnSkillBridge regex patterns (`fb_<hex>{8,}`, 40-hex SHA, UUID v4, `sess_<hex>{8,}`) — **add** — pure structural matches. Spec § Risks line 357 explicitly bars LLM in the migration path; regex is the spec-mandated mechanic.
|
|
51
|
+
- Inline-message synthesis fallback — **add** — deterministic hash → `inline:<base36>` sourceId when context is non-empty but no structured ref was found. Mechanic.
|
|
52
|
+
- `LearnEvidenceError` throw when context empty AND no documentFallback — **add** — boundary condition (no source = no evidence = reject per spec line 268).
|
|
53
|
+
- `ORG-INTENT.md` + `AGENT.md` as `ledger-entry` evidence in intent.ts caller — **add** — config files act as the durable ledger for the intent contract; the only spec-allowed DecisionJournal kind that fits a config-file citation is `ledger-entry`. Documented inline at the call site.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 1. Over-block
|
|
58
|
+
|
|
59
|
+
**`DecisionJournal.log` rejects legitimate evidence-less callers:**
|
|
60
|
+
Every existing caller is updated in this PR — there are exactly two write-path callers in the codebase:
|
|
61
|
+
1. `src/commands/intent.ts:266` — `intent-validate` conflict logger. Updated to cite ORG-INTENT.md + AGENT.md as `ledger-entry` evidence.
|
|
62
|
+
2. `src/server/routes.ts:8273` — `POST /intent/journal` handler. Updated to require an `evidence` field in the request body; returns 400 otherwise.
|
|
63
|
+
|
|
64
|
+
`DispatchDecisionJournal.logDispatchDecision` writes directly to JSONL (does not delegate to `DecisionJournal.log`); the dispatch path is unaffected and continues to work without evidence — those entries land on the SAME JSONL file but are typed `type:'dispatch'`, a different schema. Spec § Producers line 226 routes the dispatch decision-evidence path through `DispatchExecutor` (Phase 2), not through this Phase 3 gate.
|
|
65
|
+
|
|
66
|
+
**`LearnSkillBridge` over-extracts SHA-shaped 40-hex strings:** any 40-hex blob in context is matched as a `commit` external reference. Mitigation: kept as an `externalReference` (LearnSkill cannot itself write `commit` kind per allowlist), so the false positive is a caller-side display issue, not a DB integrity issue. Cross-store FK is best-effort per spec line 219; consumers display `[source unavailable]` for dangling refs.
|
|
67
|
+
|
|
68
|
+
**`LearnSkillBridge` over-extracts feedback IDs:** `fb_<hex>{8,}` is conservative — 8+ hex chars covers the FeedbackManager id shape and rejects shorter accidental matches (`fb_x`, `fb_12`).
|
|
69
|
+
|
|
70
|
+
## 2. Under-block
|
|
71
|
+
|
|
72
|
+
**Inline-message synthesis with no real source:** `buildLearnEvidence` synthesizes a `kind:'message'` row with `sourceId: 'inline:<hash>'` when context is non-empty but unstructured. This is technically a "fake" sourceId (no message store has it). Mitigation: documented as the explicit fallback path — the row carries the first 200 chars of the context as `note` for the human reader, and the `inline:` prefix is the conventional marker for "synthetic, no upstream store". Consumers tolerate dangling sourceIds per spec line 219.
|
|
73
|
+
|
|
74
|
+
**`buildLearnEvidence` accepts a `documentFallback` but LearnSkill cannot write `document` kind:** The `pendingDocumentRef` surfaces on the response for the caller to route through a different producer. **This is a real gap** — there is no current producer-kind-allowlist entry that includes `document` for a LearnSkill-shaped caller. A future task is to extend the allowlist (or to add a new producer ID like `LearnSkillDocument`) when documents-as-evidence becomes a first-class use case. For Phase 3, the pendingDocumentRef is structurally surfaced but not yet writable.
|
|
75
|
+
|
|
76
|
+
## 3. Level of abstraction
|
|
77
|
+
|
|
78
|
+
The gate is at the structural boundary: `DecisionJournal.log` (the only public write entry-point) and the HTTP route handlers. It is NOT scattered across callers — every caller passes evidence through the same single chokepoint. Producer-kind allowlist enforcement lives inside `SemanticMemory.assertProducerKindsAllowed`, not duplicated at the bridge layer. Privacy narrowing-only is enforced inside `insertEvidenceRows` (Phase 1), not re-checked at the bridge. The Phase 3 surface adds:
|
|
79
|
+
- One compile-time gate (function signature change in `DecisionJournal.log`)
|
|
80
|
+
- One runtime gate (`Array.isArray + length === 0` in `DecisionJournal.log`)
|
|
81
|
+
- One bridge call site (`memory.rememberWithEvidence(..., 'DecisionJournal')`)
|
|
82
|
+
- One auto-deriver module (`LearnSkillBridge`)
|
|
83
|
+
|
|
84
|
+
No new privacy-enforcement code; no new allowlist-enforcement code; no new cycle-detection code. The Phase 3 layer is a thin wiring layer over Phase 1's policy gates.
|
|
85
|
+
|
|
86
|
+
## 4. Signal vs authority separation (per MEMORY.md feedback_signal_vs_authority.md)
|
|
87
|
+
|
|
88
|
+
The brittle/low-context detectors (the regex patterns in `LearnSkillBridge`) emit SIGNALS (`DerivedEvidence` rows). The higher-level intelligent gate (`buildLearnEvidence` + the HTTP handler) is the authority that decides whether the signals are sufficient or to fall back. `LearnSkillBridge` exports two distinct functions — the raw deriver and the policy wrapper — so a future Phase 4 caller can replace `buildLearnEvidence` with an LLM-aware version without changing the signal layer.
|
|
89
|
+
|
|
90
|
+
## 5. Producer-allowlist interactions with sibling phases
|
|
91
|
+
|
|
92
|
+
- **Phase 2 (EvolutionManager + DispatchExecutor)**: independent producers, independent kinds. No collision.
|
|
93
|
+
- **Phase 1 (SemanticMemory schema)**: Phase 3 uses the schema unchanged. No migration needed.
|
|
94
|
+
- **Phase 4 (Inverse-traceability HTTP)**: Phase 3 produces the `decision` and (downstream) `lesson` MemoryEntity rows that Phase 4's `/memory/entities/by-evidence` queries will inverse-traverse. Phase 4 is forward-compatible: existing pre-Phase-3 entries with `evidence: undefined` are filtered out of inverse queries naturally (no evidence row → no citation row).
|
|
95
|
+
- **Phase 5 (backfill-evidence CLI)**: Phase 5 walks pre-Phase-3 decision-journal entries and emits `evidence: []` (no upgrade possible per spec line 211). Phase 3's required-evidence gate does NOT apply at backfill time — backfill writes the entity directly via `rememberWithEvidence` with an explicit empty array AND a `manual` producer override, separate code path.
|
|
96
|
+
|
|
97
|
+
## 6. Rollback cost
|
|
98
|
+
|
|
99
|
+
The contract change is structural (function signature). Rollback path:
|
|
100
|
+
1. Revert `DecisionJournal.log` to the old single-arg signature.
|
|
101
|
+
2. Revert the two write-path callers (`intent.ts`, `routes.ts`) to drop the evidence argument.
|
|
102
|
+
3. JSONL entries written with `evidence` fields are forward-compatible — the older reader ignores unknown fields. No data migration needed.
|
|
103
|
+
4. The `decision` MemoryEntity rows created by the bridge are orphaned but harmless — they remain queryable; the cascade-delete FK keeps evidence consistent if the entity is later forgotten.
|
|
104
|
+
|
|
105
|
+
Rollback is a one-PR revert; downstream readers (intent dashboard) tolerate `evidence: undefined` gracefully.
|
|
106
|
+
|
|
107
|
+
## 7. Threat model deltas (per spec § Threat Model line 365)
|
|
108
|
+
|
|
109
|
+
- **Spoofed evidence via `LearnSkill`**: a caller passes `evidence: [{kind:'feedback', sourceId:'fb_attack'}]` directly to bypass allowlist. Mitigation: `SemanticMemory.assertProducerKindsAllowed` rejects with `EvidencePolicyError`. Test: `learn-skill-evidence.test.ts > LearnSkill CANNOT write 'feedback' kind`.
|
|
110
|
+
- **Privacy bypass via decision-bridge widening**: a caller wires `entityPrivacyScope: 'shared-project'` then attaches `privacyTier: 'public'` evidence. Mitigation: `assertNarrowingOnly` (Phase 1) rejects. Test: `decision-journal-evidence.test.ts > rejects widening-only privacy violation`.
|
|
111
|
+
- **Note-field exfiltration via inline-message fallback**: the inline `message` row puts `context.slice(0, 200)` into the `note` field. Mitigation: spec line 372 documents notes inherit entity scope and the 500-byte cap is enforced inside `assertEvidenceShape`. The 200-char truncation is below the 500-byte cap.
|
|
112
|
+
- **Producer-crash partial-write corruption**: a bridge crash mid-`rememberWithEvidence` could leave an orphan entity. Mitigation: `rememberWithEvidence` is one better-sqlite3 transaction (Phase 1). Verified by `decision-journal-evidence.test.ts > persists no JSONL row when evidence gate rejects`.
|
|
113
|
+
|
|
114
|
+
## 8. Verification
|
|
115
|
+
|
|
116
|
+
- Typecheck: `npx tsc --noEmit` clean.
|
|
117
|
+
- New tests: 15 in `decision-journal-evidence.test.ts` + 19 in `learn-skill-evidence.test.ts` = 34 new cases.
|
|
118
|
+
- Updated tests: 12 call-site adjustments in `DecisionJournal.test.ts`, 1 in `DispatchDecisionJournal.test.ts`.
|
|
119
|
+
- Full unit suite: same set of pre-existing failures as origin/main (`feature-delivery-completeness`, `no-silent-fallbacks`, `pre-push-gate`, `security`, `sharedStateRoutesV2`, `telemetry-routes`) — none introduced by this change. `route-completeness` (previously failing on `instanceof Error` ratchet) now passes.
|
|
120
|
+
- Manifest auto-regenerated by `npm run generate:manifest` reflecting `routes.ts` content hash change.
|
|
121
|
+
|
|
122
|
+
## 9. Rebase-on-main bookkeeping (2026-05-10)
|
|
123
|
+
|
|
124
|
+
After this branch was opened, three PRs merged on `main` (TaskFlow Phase 5, WikiClaim Phase 5, WikiClaim Phase 4). The branch was rebased onto current `main`; conflicts resolved in `upgrades/NEXT.md` only (additive across all four phases). `src/server/routes.ts` and `src/core/types.ts` auto-merged with no manual intervention. The builtin-manifest's route-group content hashes were regenerated by `npm run generate:manifest` to reflect the union of all four phases' route additions. Re-verified: `npx tsc --noEmit` clean; full `intent-routes.test.ts` (16) + `decision-journal-evidence.test.ts` (15) + `learn-skill-evidence.test.ts` (19) + `DecisionJournal.test.ts` (23) + `DispatchDecisionJournal.test.ts` (41) + `drift-routes.test.ts` (6) all pass.
|
|
125
|
+
|