instar 0.28.82 → 0.28.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dashboard/index.html +239 -0
  2. package/dist/cli.js +10 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/memoryBackfillEvidence.d.ts +69 -0
  5. package/dist/commands/memoryBackfillEvidence.d.ts.map +1 -0
  6. package/dist/commands/memoryBackfillEvidence.js +220 -0
  7. package/dist/commands/memoryBackfillEvidence.js.map +1 -0
  8. package/dist/commands/server.d.ts.map +1 -1
  9. package/dist/commands/server.js +72 -1
  10. package/dist/commands/server.js.map +1 -1
  11. package/dist/core/EvolutionManager.d.ts +73 -0
  12. package/dist/core/EvolutionManager.d.ts.map +1 -1
  13. package/dist/core/EvolutionManager.js +376 -0
  14. package/dist/core/EvolutionManager.js.map +1 -1
  15. package/dist/core/SharedStateLedger.d.ts.map +1 -1
  16. package/dist/core/SharedStateLedger.js +4 -0
  17. package/dist/core/SharedStateLedger.js.map +1 -1
  18. package/dist/core/types.d.ts +90 -1
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/core/types.js +8 -0
  21. package/dist/core/types.js.map +1 -1
  22. package/dist/memory/EvidenceRenderer.d.ts +64 -0
  23. package/dist/memory/EvidenceRenderer.d.ts.map +1 -0
  24. package/dist/memory/EvidenceRenderer.js +119 -0
  25. package/dist/memory/EvidenceRenderer.js.map +1 -0
  26. package/dist/memory/SemanticMemory.d.ts +61 -1
  27. package/dist/memory/SemanticMemory.d.ts.map +1 -1
  28. package/dist/memory/SemanticMemory.js +381 -11
  29. package/dist/memory/SemanticMemory.js.map +1 -1
  30. package/dist/server/AgentServer.d.ts +4 -0
  31. package/dist/server/AgentServer.d.ts.map +1 -1
  32. package/dist/server/AgentServer.js +2 -0
  33. package/dist/server/AgentServer.js.map +1 -1
  34. package/dist/server/routes.d.ts +8 -0
  35. package/dist/server/routes.d.ts.map +1 -1
  36. package/dist/server/routes.js +493 -3
  37. package/dist/server/routes.js.map +1 -1
  38. package/dist/tasks/DivergenceChecker.d.ts +89 -0
  39. package/dist/tasks/DivergenceChecker.d.ts.map +1 -0
  40. package/dist/tasks/DivergenceChecker.js +255 -0
  41. package/dist/tasks/DivergenceChecker.js.map +1 -0
  42. package/dist/tasks/LruCache.d.ts +33 -0
  43. package/dist/tasks/LruCache.d.ts.map +1 -0
  44. package/dist/tasks/LruCache.js +88 -0
  45. package/dist/tasks/LruCache.js.map +1 -0
  46. package/dist/tasks/RateLimiter.d.ts +61 -0
  47. package/dist/tasks/RateLimiter.d.ts.map +1 -0
  48. package/dist/tasks/RateLimiter.js +108 -0
  49. package/dist/tasks/RateLimiter.js.map +1 -0
  50. package/dist/tasks/TaskFlowDueWaker.d.ts +29 -0
  51. package/dist/tasks/TaskFlowDueWaker.d.ts.map +1 -0
  52. package/dist/tasks/TaskFlowDueWaker.js +73 -0
  53. package/dist/tasks/TaskFlowDueWaker.js.map +1 -0
  54. package/dist/tasks/TaskFlowMaintenanceSweeper.d.ts +40 -0
  55. package/dist/tasks/TaskFlowMaintenanceSweeper.d.ts.map +1 -0
  56. package/dist/tasks/TaskFlowMaintenanceSweeper.js +131 -0
  57. package/dist/tasks/TaskFlowMaintenanceSweeper.js.map +1 -0
  58. package/dist/tasks/TaskFlowRegistry.d.ts +180 -0
  59. package/dist/tasks/TaskFlowRegistry.d.ts.map +1 -0
  60. package/dist/tasks/TaskFlowRegistry.js +791 -0
  61. package/dist/tasks/TaskFlowRegistry.js.map +1 -0
  62. package/dist/tasks/ThreadlineFlowBridge.d.ts +54 -0
  63. package/dist/tasks/ThreadlineFlowBridge.d.ts.map +1 -0
  64. package/dist/tasks/ThreadlineFlowBridge.js +129 -0
  65. package/dist/tasks/ThreadlineFlowBridge.js.map +1 -0
  66. package/dist/tasks/task-flow-registry.store.sqlite.d.ts +77 -0
  67. package/dist/tasks/task-flow-registry.store.sqlite.d.ts.map +1 -0
  68. package/dist/tasks/task-flow-registry.store.sqlite.js +350 -0
  69. package/dist/tasks/task-flow-registry.store.sqlite.js.map +1 -0
  70. package/dist/tasks/task-flow-types.d.ts +229 -0
  71. package/dist/tasks/task-flow-types.d.ts.map +1 -0
  72. package/dist/tasks/task-flow-types.js +128 -0
  73. package/dist/tasks/task-flow-types.js.map +1 -0
  74. package/dist/threadline/ThreadlineNicknames.d.ts +88 -0
  75. package/dist/threadline/ThreadlineNicknames.d.ts.map +1 -0
  76. package/dist/threadline/ThreadlineNicknames.js +195 -0
  77. package/dist/threadline/ThreadlineNicknames.js.map +1 -0
  78. package/package.json +1 -1
  79. package/src/data/builtin-manifest.json +78 -78
  80. package/upgrades/0.28.83.md +45 -0
  81. package/upgrades/0.28.84.md +89 -0
  82. package/upgrades/side-effects/taskflow-phase1.md +130 -0
  83. package/upgrades/side-effects/taskflow-phase2.md +110 -0
  84. package/upgrades/side-effects/taskflow-phase3a.md +138 -0
  85. package/upgrades/side-effects/taskflow-phase5.md +234 -0
  86. package/upgrades/side-effects/threadline-nickname-resolver-authority.md +81 -0
  87. package/upgrades/side-effects/wikiclaim-evidence-phase1.md +138 -0
  88. package/upgrades/side-effects/wikiclaim-evidence-phase4.md +129 -0
  89. package/upgrades/side-effects/wikiclaim-evidence-phase5.md +285 -0
@@ -0,0 +1,129 @@
1
+ # Side-Effects Review — WikiClaim Evidence Phase 4 (HTTP + dashboard)
2
+
3
+ **Version / slug:** `wikiclaim-evidence-phase4`
4
+ **Date:** 2026-05-10
5
+ **Author:** Echo
6
+ **Second-pass reviewer:** required (auth-principal-to-viewer-scope mapping; XSS; visibility-filter correctness)
7
+
8
+ ## Summary of the change
9
+
10
+ Adds inverse-traceability HTTP endpoints + dashboard panels that consume the typed evidence APIs landed in Phase 1 and hardened in Phase 5. No new policy, no new storage shape, no new filter — this PR is a thin pass-through from HTTP/dashboard to `SemanticMemory.getEvidence` / `SemanticMemory.findCitations` / `SemanticMemory.getEntityWithEvidence`.
11
+
12
+ Spec: `docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md` § Phase 4 (line 343), Inverse traceability (line 291), Storage and Privacy (line 310).
13
+
14
+ What lands:
15
+ - `GET /memory/evidence/by-entity/:id?viewerScope=...` — returns the entity's evidence array, viewer-scope filtered. Routes through `getEntityWithEvidence` so the entity-level visibility check and the per-row evidence-tier filter both fire BEFORE any bytes leave the storage layer. 404 collapses "entity missing" and "entity hidden at viewer scope" into the same response per spec § Storage and Privacy line 316 (non-leaky inverse-query rule extended to direct fetch).
16
+ - `GET /memory/entities/by-evidence?kind=...&sourceId=...&viewerScope=...` — returns entities citing `(kind, sourceId)`. Pass-through to `findCitations`, which filters by BOTH the citing entity's `privacyScope` AND the citing evidence row's `privacyTier`. Returns `{kind, sourceId, viewerScope, entities, totalResults}`.
17
+ - `viewerScope` resolution: optional query param, default `private` (full visibility for the agent's own DB; the single-bearer-token auth model has one principal). Narrowing-only — callers can request a more restrictive view to preview "what would a topic-peer see?" Invalid scope values fall back silently to the default rather than 400'ing, mirroring how other Express handlers in this file treat unknown enum input.
18
+ - Evidence kind whitelist on the inverse endpoint — `VALID_EVIDENCE_KINDS` set matches `MemoryEvidenceKind` exactly. Unknown kinds → 400 (this is mechanic-level validation, not judgment).
19
+ - Auth: both routes sit behind the global `authMiddleware` applied at app level in `AgentServer`. No route-local auth code (and no new code to maintain that could drift).
20
+ - Dashboard "Evidence" tab: per-entity evidence panel (text input + Lookup button → render evidence list with kind, sourceId, file:line, weight, confidence, privacyTier badge, note); "what cites this?" panel (kind dropdown + sourceId input → render entities with type, name, id, scope, confidence, and an "Inspect evidence" button that pivots to the per-entity panel). One viewer-scope dropdown drives both panels.
21
+ - Dashboard XSS posture: every user-supplied string (entity id, name, content, sourceId, note, path, privacyTier label) flows through the local `escapeHtml` helper before reaching `innerHTML`. The "Inspect evidence" button uses a `data-entity-id` attribute + a delegated `addEventListener('click', …)` — NOT an inline `onclick` string interpolation — so even pathologically-shaped IDs cannot break out of the attribute context.
22
+
23
+ Files touched:
24
+ - `src/server/routes.ts` — adds `/memory/evidence/by-entity/:id` and `/memory/entities/by-evidence` after the existing `/semantic/*` routes; adds local `VALID_EVIDENCE_KINDS`, `VALID_VIEWER_SCOPES`, `resolveViewerScope`.
25
+ - `dashboard/index.html` — adds an "Evidence" tab button to the tab bar, an `evidenceTab` panel container, a registry entry in `TAB_REGISTRY`, and a JS section (`evCurrentViewerScope`, `evRenderTierBadge`, `evRenderEvidenceRow`, `evRenderEntityRow`, `loadEvidenceByEntity`, `loadEntitiesByEvidence`, `evLookupFromCitation`) inserted before `loadTokens`.
26
+ - `tests/unit/memory-evidence-routes.test.ts` — 14 vitest cases against real SQLite + a real Express app with the production `authMiddleware`.
27
+ - `upgrades/side-effects/wikiclaim-evidence-phase4.md` (this file)
28
+
29
+ ## Decision-point inventory
30
+
31
+ - `resolveViewerScope(raw)` — **add** — mechanic-level enum lookup against `VALID_VIEWER_SCOPES`. Not judgment.
32
+ - `VALID_EVIDENCE_KINDS` lookup on the inverse endpoint — **add** — mechanic-level enum match. Not judgment.
33
+ - `escapeHtml`-on-every-field render path — **add** — structural HTML escape, not judgment.
34
+ - 404-vs-200-empty collapse on `by-entity` — **add** — spec-mandated non-leak (§ Storage and Privacy line 316).
35
+
36
+ ---
37
+
38
+ ## 1. Over-block
39
+
40
+ **`viewerScope` query param ignored when invalid (default-to-private).** A caller sending `?viewerScope=garbage` does not get a 400; they get private-scope results. Rationale: viewerScope is narrowing-only and the agent's principal is already `private`; widening doesn't exist, so misspellings can only narrow OR be ignored. The safest fallback for a typo is the agent's own scope (private) — i.e., NO over-block. A stricter 400 would be a usability regression with no privacy benefit.
41
+
42
+ **`findCitations` returns an empty array on missing entities or zero matches.** Both paths look identical to the caller. By design — the inverse query is exactly the "is there any citation of this source anywhere I can see?" probe; differentiating "no matches" from "no visible matches at your scope" would itself be the leak the spec is closing.
43
+
44
+ ## 2. Under-block
45
+
46
+ - **Rate limiting:** these are read endpoints with no LLM call, no mutation, no fan-out. Per the patterns in routes.ts, hot read endpoints in `/semantic/*` (e.g. `/semantic/recall/:id`, `/semantic/search`, `/semantic/explore/:id`) ALSO don't carry a `rateLimiter` wrapper — they rely on the global auth gate. The new routes follow the same convention. If abuse appears in observability, attaching `rateLimiter(60_000, 60)` is a one-line follow-up; the per-entity evidence cap (50 default, 500 hard) caps the response size regardless.
47
+ - **viewerScope authentication binding:** the `viewerScope` query param is caller-controlled. This is acceptable because the auth model has ONE principal (the agent itself, full bearer token = `private` ceiling), and the param can only *narrow*. A caller cannot widen to `private` if they were given a `shared-project` token, because there is no such token — the system has one. When multi-tenant auth lands (not in scope), this resolver becomes the seam: it must read principal claims off the request and clamp the param to the principal's ceiling. Currently documented in the inline comment on `resolveViewerScope`.
48
+ - **`note` field rendering on the dashboard:** notes are free-form text, capped at 500 bytes at write time (Phase 1). The dashboard renders them via `escapeHtml`, NOT `marked` / `DOMPurify` — even though those libraries are loaded for the file viewer. Treating notes as plain text is the safer default; if markdown rendering is wanted later, route through `DOMPurify.sanitize(marked.parse(note))` like the file viewer does.
49
+ - **Dashboard does not paginate** when the inverse query returns many entities. The per-entity evidence cap means the forward endpoint is bounded; the inverse endpoint is bounded only by "how many entities cite this source" which has no hard cap. v1 dashboards expect human-scale lookups; a follow-up can add `limit`/`offset` if needed.
50
+
51
+ ## 3. Level-of-abstraction fit
52
+
53
+ Right layer.
54
+
55
+ - HTTP route is a 30-line pass-through to the storage primitive. Privacy decisions live in `SemanticMemory.getEvidence` / `findCitations` (read-time filter) and in `EvidenceRenderer` (the documented enforcement point for already-loaded entities, per Phase 5). The route does not re-decide visibility — it serializes whatever the storage layer returned.
56
+ - Dashboard panels invoke the route via `apiFetch` and render the result. They do not contain a copy of the privacy filter; they trust the server to have filtered.
57
+ - `viewerScope` derivation is owned by `resolveViewerScope` — one function, one place to retrofit when multi-tenant auth lands.
58
+
59
+ ## 4. Signal vs authority compliance
60
+
61
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
62
+
63
+ - [x] **No** — these are read endpoints with no judgment-level block/allow surface.
64
+
65
+ The "decisions" in this PR:
66
+ - Mechanic-level enum lookups (`VALID_VIEWER_SCOPES`, `VALID_EVIDENCE_KINDS`);
67
+ - Mechanic-level scope ordering (already owned by `EvidenceRenderer`, not duplicated here);
68
+ - Mechanic-level escape on the dashboard.
69
+
70
+ None of those answers the question "should this evidence be visible to this caller?" — that question is answered exactly once, in `SemanticMemory`. The route is dumb; the renderer is the boundary.
71
+
72
+ ## 5. Interactions
73
+
74
+ - **Phase 1 (#137):** consumes `getEntityWithEvidence`, `getEvidence`, `findCitations` — all already viewer-scope-filtered. No re-wiring of those methods.
75
+ - **Phase 5 (#141):** the `EvidenceRenderer` helper is the documented single enforcement point. This PR uses storage-layer methods that internally use the same predicates that `EvidenceRenderer` uses (`isEntityVisibleAtScope`, `isEvidenceVisibleAtScope`); the rules are colocated, no drift. If a future caller has an already-loaded entity in memory and wants to ship it to a viewer, they should call `renderEvidenceForScope` — these routes don't go through that path because they load fresh from SQLite.
76
+ - **Phase 2 (#139) and Phase 3 (#142) in flight:** they touch producer code (EvolutionManager, DispatchExecutor, DecisionJournal). They don't touch routes.ts or the dashboard. Conflict-free.
77
+ - **TaskFlow Phase 5 (#143 merged d7d4eba9):** added rate-limit middleware to `/flows/*`. This PR does NOT add rate-limit to `/memory/*` evidence routes — the convention in `/semantic/*` is no per-route limiter, relying on global auth + per-entity bound. If a follow-up wants to standardize, it's a one-liner.
78
+ - **Dashboard tab registry:** new `evidence` entry sits between `threadline` and `secrets`. The mobile nav menu / `updateNavToggleLabel` keys off `data-tab="evidence"` and works without changes (the existing logic reads the button text dynamically).
79
+ - **`authMiddleware`:** the existing global middleware sits in front of every non-public route. Both new routes inherit it without local code. Test file mounts `authMiddleware(AUTH_TOKEN)` directly to exercise the 401 path.
80
+
81
+ ## 6. External surfaces
82
+
83
+ - **Other agents on the same machine:** none. The routes return only this agent's own data.
84
+ - **Other users of the install base:** two new GET routes appear. No breaking changes to existing routes. Anyone hitting `/memory/*` paths today gets a 404 (we use those URLs first time here, no collision check needed — `grep` confirms no `/memory/evidence/*` or `/memory/entities/*` exists in routes.ts before this PR).
85
+ - **External systems:** none.
86
+ - **Persistent state:** none — read-only routes.
87
+ - **Privacy posture:** unchanged. The visibility surface is exactly what `SemanticMemory` already exposes; this PR only adds an HTTP/dashboard frontend.
88
+ - **Cloudflare tunnel:** if the agent exposes its server via Cloudflare tunnel, the new routes are reachable from the internet under the same bearer-token auth as everything else. No new exposure shape; the auth boundary is unchanged.
89
+
90
+ ## 7. Rollback cost
91
+
92
+ - **Hot-fix release:** pure additive change; `git revert <merge-commit>` ships as the next patch.
93
+ - **Data migration:** none.
94
+ - **Agent state repair:** none. No state written.
95
+ - **User visibility:** mild — the Evidence tab disappears from the dashboard. No data loss; the underlying DB is untouched.
96
+
97
+ ---
98
+
99
+ ## Conclusion
100
+
101
+ Phase 4 ships two read-only HTTP routes plus a single dashboard tab. All privacy decisions remain in the Phase 1 storage-layer filter and the Phase 5 `EvidenceRenderer` boundary; this PR does not introduce a second filter. Auth is the existing bearer-token model. XSS is structurally prevented by `escapeHtml`-on-every-interpolation plus the absence of inline `onclick` string-interpolation patterns. 14 new vitest cases cover viewer-scope filtering, auth, 404, 400, empty results, and the inverse-leak cross-product. Cleared to ship pending second-pass concurrence on auth/XSS/filter-correctness.
102
+
103
+ ---
104
+
105
+ ## Second-pass review
106
+
107
+ **Reviewer:** independent code-audit subagent (adversarial), one round.
108
+ **Round 1 findings:**
109
+
110
+ 1. **Inline `onclick` with `${id}` interpolation in `evRenderEntityRow`** — flagged at draft time. `escapeHtml` does encode `'`/`"`, so a payload like `id="x'),alert(1);//"` would render as `&#39;`, structurally inert. Still: an inline `onclick="..."` string-build is a brittle pattern; one future edit forgetting the escape is a foot-gun. **Resolution:** rewrote as `data-entity-id="${id}"` + `addEventListener` delegation. No string-interpolation into an HTML attribute that becomes a JS expression at parse time.
111
+ 2. **`viewerScope` widening attempt** — a caller could send `?viewerScope=private` even if they "shouldn't" have private access. **Resolution:** the auth model has one principal (the agent itself = private ceiling). There is no "shouldn't" right now — the bearer token IS the private credential. The resolver is the documented seam for future multi-tenant scope clamping, and the inline comment in routes.ts says so. Cleared.
112
+ 3. **Visibility-filter correctness on `by-entity` for a private entity at shared-project viewer** — confirmed: `getEntityWithEvidence` returns `null` when the entity scope exceeds the viewer scope (`SemanticMemory.ts:775`). Route returns 404 in that case. Test `returns 404 (non-leaky) when a private entity is requested at shared-project scope` proves it.
113
+ 4. **Inverse leak via `by-evidence` when entity scope is shared-project but evidence row is private** — confirmed: `findCitations` checks BOTH entity scope AND evidence tier (`SemanticMemory.ts:752-754`). Test `cross-product: shared-project viewer cannot see private-tier evidence inverse query` covers this exact shape. Cleared.
114
+ 5. **`viewerScope` enum bypass via prototype-pollution / non-string input** — `req.query.viewerScope` may be a string, an array, or undefined. `resolveViewerScope` only returns one of the three known scopes; anything else falls to `'private'`. Even with `?viewerScope=__proto__`, the `VALID_VIEWER_SCOPES.has()` check rejects (`Set.prototype.has` does not match own/prototype keys of arbitrary strings). Cleared.
115
+
116
+ Cleared to ship.
117
+
118
+ ---
119
+
120
+ ## Evidence pointers
121
+
122
+ - New tests: `npx vitest run tests/unit/memory-evidence-routes.test.ts` → 14/14 passing (102ms).
123
+ - Typecheck: `npx tsc --noEmit` → clean.
124
+ - Spec source of truth: `docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md` (Phase 4 at line 343).
125
+ - Predecessor PRs: #137 (Phase 1 schema), #141 (Phase 5 backfill + render hardening).
126
+ - Privacy enforcement evidence:
127
+ - `SemanticMemory.getEntityWithEvidence` (entity scope filter): `src/memory/SemanticMemory.ts:765-779`
128
+ - `SemanticMemory.findCitations` (dual filter on entity scope + evidence tier): `src/memory/SemanticMemory.ts:735-759`
129
+ - `EvidenceRenderer` (documented enforcement boundary): `src/memory/EvidenceRenderer.ts`
@@ -0,0 +1,285 @@
1
+ # Side-Effects Review — WikiClaim Evidence Phase 5 (backfill CLI + render hardening)
2
+
3
+ **Version / slug:** `wikiclaim-evidence-phase5`
4
+ **Date:** 2026-05-10
5
+ **Author:** Echo
6
+ **Second-pass reviewer:** required (idempotency + producer-narrowing-respected + cross-product privacy)
7
+
8
+ ## Summary of the change
9
+
10
+ Ships the final WikiClaim phase per spec § Migration Plan line 347:
11
+
12
+ 1. `instar memory backfill-evidence [--dry-run]` — one-shot, idempotent
13
+ CLI that walks every existing `MemoryEntity`, pattern-matches the legacy
14
+ `source: string` field, and synthesizes a single `MemoryEvidence` row
15
+ of `kind: 'external-url'` when (and only when) the source string is an
16
+ anchored `https?://…` URL. Any non-URL source string is left alone —
17
+ `evidence: []` per spec § Risks line 357 ("no LLM in the migration path").
18
+ 2. `src/memory/EvidenceRenderer.ts` — single privacy-enforcement helper
19
+ exposing `renderEvidenceForScope(entity, viewerScope)`. Per spec
20
+ § Storage and Privacy line 315 the renderer is THE enforcement boundary;
21
+ this lands the helper now (before Phase 4 HTTP/dashboard rendering
22
+ touches evidence in user-facing output) so every downstream consumer
23
+ has exactly one place to filter from. `SemanticMemory`'s own read paths
24
+ are switched to delegate to the helper for the entity-visible /
25
+ evidence-visible predicates — keeping one source of truth.
26
+
27
+ ### Phase 5 scope deliberately narrowed vs spec § Migration line 208–212
28
+
29
+ The spec lists four legacy-source patterns:
30
+
31
+ | Pattern | Backfilled to | Phase 5 status |
32
+ |---|---|---|
33
+ | `https?://…` | `kind: 'external-url'` | **shipped** |
34
+ | `session:ABC` | `kind: 'session'` | **skipped** |
35
+ | `user:Justin` | `kind: 'message'` | **skipped** |
36
+ | `observation` / any other | `evidence: []` | unchanged (no write) |
37
+
38
+ The narrowing is structural, not a deferral. Per spec § Producers line 229
39
+ and the Phase 1 `PRODUCER_KIND_ALLOWLIST`, the `manual` producer (the only
40
+ producer this CLI can invoke from outside any of the in-process subsystems)
41
+ is allowed to write `external-url` ONLY. The `session` and `message` kinds
42
+ require the `DecisionJournal` and `LearnSkill` producers respectively, which
43
+ have access to the originating subsystem's context (session metadata,
44
+ conversation transcript) that the CLI does not. **We deliberately do NOT
45
+ widen the allowlist to make migration easier** — that would unwind the
46
+ producer-kind capability boundary the spec installs.
47
+
48
+ A future bridge that walks legacy `session:` and `user:` sources from
49
+ inside DecisionJournal / LearnSkill (with full producer context) is a
50
+ clean follow-up and explicitly tracked in MEMORY.md.
51
+
52
+ What lands:
53
+
54
+ - `src/commands/memoryBackfillEvidence.ts` — pattern-match + dup-check +
55
+ `addEvidence('manual')` call.
56
+ - `src/cli.ts` — register `memory backfill-evidence` subcommand with
57
+ `--dry-run` flag and `-d <dir>`.
58
+ - `src/memory/EvidenceRenderer.ts` — `renderEvidenceForScope`,
59
+ `filterEvidenceArrayForScope`, `isEntityVisibleAtScope`,
60
+ `isEvidenceVisibleAtScope`. The latter two are imported by
61
+ `SemanticMemory.ts` so the read-path predicates are no longer locally
62
+ defined — they delegate to the renderer.
63
+ - `tests/unit/memory-backfill-evidence.test.ts` — 17 vitest cases:
64
+ URL pattern match, non-URL skip, producer narrowing respected,
65
+ idempotency across two runs, idempotency across entity privacy tiers,
66
+ dry-run is non-mutating, apply-after-dry-run still writes, privacy
67
+ inherits across every entity scope.
68
+ - `tests/unit/evidence-render-privacy.test.ts` — cross-product table:
69
+ 3 entity scopes × 5 evidence tiers × 3 viewer scopes (45 combinations)
70
+ plus six edge-case assertions. Spec § Risks line 356 ("render every
71
+ privacyScope × privacyTier combination and assert no leak") satisfied.
72
+ - `upgrades/side-effects/wikiclaim-evidence-phase5.md` (this file).
73
+
74
+ ## Decision-point inventory
75
+
76
+ - URL anchoring (`^https?:\/\/\S+$`) — **add** — hard-invariant pattern
77
+ match at the migration entry. Not judgment. Anchoring (no embedded URLs
78
+ inside other text) keeps the migration deterministic across re-runs as
79
+ the matcher evolves.
80
+ - Idempotency dup-check (`existing.some(ev.kind === 'external-url' &&
81
+ (ev.path === source || ev.sourceId === source))`) — **add** —
82
+ equality-based. Not judgment.
83
+ - Dup-check viewer-scope (`'private'` default, configurable) — **add** —
84
+ reads the widest scope so the dup-check sees every existing row. A
85
+ narrower default would silently allow a duplicate write against a
86
+ private-tier evidence row on a shared-project entity. Tests pin this.
87
+ - Producer narrowing (`addEvidence(..., 'manual')` only) — **enforced**
88
+ by `PRODUCER_KIND_ALLOWLIST`; the CLI inherits that gate, does not
89
+ re-implement it.
90
+ - `privacyTier: undefined` on synthesized rows — **add** — per spec
91
+ § Risks line 360, never auto-upgrade.
92
+ - `confidence: 0.5` on synthesized rows — **add** — matches spec
93
+ § Migration line 210's suggested low-confidence value for coarse
94
+ legacy provenance.
95
+ - `updatedAt: entity.createdAt` — **add** — preserves temporal ordering
96
+ and means a second run on the same entity produces identical content.
97
+ - `EvidenceRenderer` helper — **add** — single-enforcement render path;
98
+ no ad-hoc render exists today (Phase 4 HTTP/dashboard hasn't shipped)
99
+ so this is preventive. `SemanticMemory`'s existing read-path
100
+ predicates are switched to delegate to the helper so we have ONE
101
+ visibility-comparison map across the codebase.
102
+
103
+ ---
104
+
105
+ ## 1. Over-block
106
+
107
+ **URL anchoring rejects embedded URLs:** `source: "see https://example.com"`
108
+ is skipped, not backfilled. By design — extracting a partial URL creates
109
+ ambiguity ("which one of the URLs in this string is the real source?")
110
+ and re-runs could backfill different URLs as the extractor evolves.
111
+ Producers that need to retroactively cite an embedded URL can run a
112
+ follow-up migration with a more permissive extractor; we keep the v1
113
+ backfill conservative.
114
+
115
+ **Whitespace not trimmed:** `source: " https://x.com "` is skipped.
116
+ Same reason — trim semantics depend on convention; v1 keeps the rule
117
+ "the source IS the URL, not a string containing a URL."
118
+
119
+ **Producer-narrowing rejects `session:` / `user:`:** Already covered
120
+ above. These need their own producer bridges, not this CLI. Explicitly
121
+ tracked.
122
+
123
+ ## 2. Under-block
124
+
125
+ - **`external-url` is not URL-validated:** Beyond the `https?://\S+`
126
+ anchor we don't parse the URL. A malformed-but-anchored value like
127
+ `https://[invalid:` would be backfilled. The renderer treats
128
+ `external-url.path` as display-only per spec § Threat Model line 372
129
+ ("renderers MUST treat `path` for `external-url` as display-only —
130
+ never auto-fetch"), so an invalid URL is a display-time annoyance,
131
+ not an SSRF or injection vector.
132
+ - **`note` cap not retroactively applied:** We set a fixed
133
+ `'Backfilled from legacy MemoryEntity.source (Phase 5 migration)'`
134
+ note (52 bytes, well under the 500-byte cap). No risk.
135
+ - **Concurrency:** The CLI doesn't take a DB lock. If a producer is
136
+ actively writing while backfill runs, both will succeed; the dup-check
137
+ loses the race for entities that get a fresh `external-url` evidence
138
+ row from a producer mid-scan. Mitigation: dup-check on every entity
139
+ before write (already present); on race, the producer's row lands
140
+ first and ours is skipped on second run. Operationally the CLI is
141
+ intended for offline / quiet-period invocation; this is documented.
142
+
143
+ ## 3. Level-of-abstraction fit
144
+
145
+ Right layer. The backfill belongs at the CLI / command layer because:
146
+ - It uses public `SemanticMemory` APIs (`export()`, `getEvidence()`,
147
+ `addEvidence()`) — no private-state access.
148
+ - It's a one-shot operator-invoked migration, not a runtime path.
149
+ - Pattern matching is at the edge (the CLI), not inside storage.
150
+
151
+ `EvidenceRenderer` belongs in `src/memory/` because it owns evidence-shape
152
+ filtering logic; pulling it out of `SemanticMemory.ts` (without dragging
153
+ better-sqlite3 with it) lets non-SQLite render contexts (HTTP shapers,
154
+ dashboard JSON producers) use it without spurious dependencies.
155
+
156
+ ## 4. Signal vs authority compliance
157
+
158
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
159
+
160
+ - [x] **No** — this change has no judgment-level block/allow surface.
161
+
162
+ The backfill's "skip" outcomes are mechanic-level pattern misses
163
+ (`URL_SOURCE_PATTERN.test(source) === false`) or idempotency dedupes
164
+ (equality-based dup-check). The renderer's filtering is mechanic-level
165
+ ordering comparison (`tier ordinal >= evidence tier ordinal`). No
166
+ intelligent gate, no judgment surface.
167
+
168
+ The actual semantic question — "should this entity be cited by an
169
+ external URL?" — was already answered by whoever set `source` on the
170
+ entity. The CLI replays that answer into typed evidence; it does not
171
+ re-decide.
172
+
173
+ ## 5. Interactions
174
+
175
+ - **Phase 2 producers landing in parallel:** EvolutionManager /
176
+ DispatchExecutor producers write `feedback`, `pattern-entity`,
177
+ `job-run`, `ledger-entry`, `supersedes-evidence` rows. Backfill only
178
+ writes `external-url`. Zero overlap; dup-check is keyed on
179
+ `(kind === 'external-url' && path === source)` so even if Phase 2 had
180
+ attached an `external-url` row via a different producer (none of them
181
+ are allowed to per the allowlist), the URL-equality dup-check still
182
+ catches it.
183
+ - **Phase 3 DecisionJournal / LearnSkill:** Same story — different
184
+ kinds; non-overlapping dup-check keys.
185
+ - **`SemanticMemory.getEvidence` / `findCitations` / `getEntityWithEvidence`**:
186
+ Now delegate to the `EvidenceRenderer.isEntityVisibleAtScope` and
187
+ `isEvidenceVisibleAtScope` exports for the entity-visible /
188
+ evidence-visible predicates. The Phase 1 in-file copies are removed.
189
+ Verified by re-running the full Phase 1 test suite
190
+ (`semantic-memory-evidence.test.ts` 21/21, `semantic-memory.test.ts`
191
+ 49/49, `semantic-memory-privacy.test.ts` 32/32) — all green.
192
+ - **JSONL append log:** Every `addEvidence` call in the backfill emits
193
+ the existing `addEvidence` JSONL action; replay path is unchanged
194
+ (Phase 1's deferred-handler note about JSONL replay still applies —
195
+ unaffected by Phase 5).
196
+ - **No HTTP / dashboard / threadline render today:** Phase 4 hasn't
197
+ landed; there is no ad-hoc evidence renderer in current production
198
+ code to replace. The helper is preventive — landing it now means
199
+ Phase 4 has exactly one place to call into when it adds HTTP routes
200
+ and dashboard panels. **No ad-hoc paths were found and replaced.**
201
+ This is documented as the intent rather than presented as completed
202
+ retro-replacement work.
203
+
204
+ ## 6. External surfaces
205
+
206
+ - **Other agents on the same machine**: None. Backfill is per-agent,
207
+ per-state-directory.
208
+ - **Other users of the install base**: A new subcommand appears on
209
+ `instar memory --help`. Default behavior of `instar memory <other>`
210
+ is unchanged. Running the CLI is opt-in.
211
+ - **External systems**: None. Backfill is local-only; the
212
+ `external-url` rows it writes store the URL as a string for display,
213
+ never fetched (per spec § Threat Model line 372).
214
+ - **Persistent state**: One `addEvidence` JSONL row per backfilled
215
+ entity, one `entity_evidence` table row per backfilled entity.
216
+ Bounded by the number of entities with URL sources.
217
+ - **Privacy posture**: Tighter — backfilled rows inherit entity scope
218
+ (`privacyTier: undefined`); never auto-upgrade. The renderer helper
219
+ centralizes filtering; even if a Phase 4 callsite forgets to filter,
220
+ the helper's existence + spec-mandated call pattern catches the bug
221
+ at code-review time.
222
+
223
+ ## 7. Rollback cost
224
+
225
+ - **Hot-fix release**: Pure additive change. `git revert <merge-commit>`
226
+ ships as the next patch.
227
+ - Backfilled `entity_evidence` rows from the `manual` producer would
228
+ remain in the DB; they are well-formed and harmless (renderer hides
229
+ them per viewer scope, never auto-fetched). If cleanup is desired,
230
+ `DELETE FROM entity_evidence WHERE kind = 'external-url'
231
+ AND note = 'Backfilled from legacy MemoryEntity.source (Phase 5 migration)'`
232
+ is a precise rollback query.
233
+ - `EvidenceRenderer.ts` deletion is fine; `SemanticMemory` would need
234
+ to re-introduce the local predicates. The revert PR can re-add them.
235
+ - **Data migration**: None. Backfill is one-shot per entity by design;
236
+ rollback doesn't undo the writes (they're treated as legitimate
237
+ evidence rows), and re-running after revert+restore would just
238
+ produce zero writes thanks to idempotency.
239
+ - **Agent state repair**: None.
240
+ - **User visibility**: Zero today (no HTTP / dashboard renderers Phase 4
241
+ hasn't shipped); minimal after Phase 4 lands (filtered evidence rows
242
+ appear next to entities).
243
+
244
+ ---
245
+
246
+ ## Conclusion
247
+
248
+ WikiClaim Phase 5 ships an idempotent, dry-run-safe, URL-only backfill
249
+ CLI plus the renderer helper that closes the privacy-enforcement loop the
250
+ spec § Storage and Privacy line 315 calls for. The CLI's intentional
251
+ narrowness (URLs only, never `session:` / `user:`) preserves the
252
+ `PRODUCER_KIND_ALLOWLIST` boundary Phase 1 installed; widening for
253
+ migration convenience would unwind a structural defense. The renderer
254
+ helper is preventive — Phase 4 HTTP/dashboard hasn't landed yet, so
255
+ there are zero ad-hoc renderers to replace today, but the helper is in
256
+ place and `SemanticMemory`'s own read paths now delegate to it so the
257
+ codebase has exactly one visibility-comparison map. Cleared to ship
258
+ pending second-pass concurrence.
259
+
260
+ ---
261
+
262
+ ## Second-pass review
263
+
264
+ _To be filled in by independent reviewer subagent._
265
+
266
+ ---
267
+
268
+ ## Evidence pointers
269
+
270
+ - New tests:
271
+ `npx vitest run tests/unit/memory-backfill-evidence.test.ts tests/unit/evidence-render-privacy.test.ts`
272
+ → 34/34 passing (~80ms).
273
+ - Regression:
274
+ `npx vitest run tests/unit/semantic-memory-evidence.test.ts tests/unit/semantic-memory.test.ts tests/unit/semantic-memory-privacy.test.ts`
275
+ → 102/102 passing.
276
+ - Typecheck: `npx tsc --noEmit` → clean.
277
+ - Cross-product privacy coverage:
278
+ `tests/unit/evidence-render-privacy.test.ts` — 3 entity scopes × 5
279
+ evidence tiers × 3 viewer scopes (45 combinations) + edge-case
280
+ assertions.
281
+ - Spec source of truth:
282
+ `docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md` §§ Migration
283
+ of Existing MemoryEntity Records (line 202), Risks lines 357, 360,
284
+ Migration Plan Phase 5 (line 347), Storage and Privacy line 315.
285
+ - Phase 1 artifact: `upgrades/side-effects/wikiclaim-evidence-phase1.md`.