wyrm-mcp 7.3.2 → 7.3.3

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 (184) hide show
  1. package/README.md +8 -14
  2. package/dist/activation.js +59 -1
  3. package/dist/agent-daemon.js +281 -4
  4. package/dist/agent-loop.js +332 -7
  5. package/dist/analytics.js +236 -13
  6. package/dist/attribution.js +49 -1
  7. package/dist/audit.js +457 -2
  8. package/dist/auto-capture.js +138 -3
  9. package/dist/auto-orchestrator.js +325 -1
  10. package/dist/autoconfig.d.ts +50 -0
  11. package/dist/autoconfig.d.ts.map +1 -1
  12. package/dist/autoconfig.js +1115 -39
  13. package/dist/autoconfig.js.map +1 -1
  14. package/dist/buddy-runner.js +109 -1
  15. package/dist/buddy.js +564 -14
  16. package/dist/build-flags.js +15 -1
  17. package/dist/capabilities.js +183 -3
  18. package/dist/capture.js +56 -1
  19. package/dist/causality.js +148 -8
  20. package/dist/cli.js +281 -20
  21. package/dist/cloud/cli.js +541 -5
  22. package/dist/cloud/client.js +221 -1
  23. package/dist/cloud/crypto.js +85 -1
  24. package/dist/cloud/machine-id.js +113 -2
  25. package/dist/cloud/recovery.js +60 -1
  26. package/dist/cloud/sync-engine.js +543 -7
  27. package/dist/cloud-backup.js +579 -5
  28. package/dist/cloud-profile.js +138 -1
  29. package/dist/cloud-sync-entrypoint.js +47 -1
  30. package/dist/cloud-sync.js +309 -2
  31. package/dist/connectors/bridge-source.d.ts +46 -0
  32. package/dist/connectors/bridge-source.d.ts.map +1 -0
  33. package/dist/connectors/bridge-source.js +77 -0
  34. package/dist/connectors/bridge-source.js.map +1 -0
  35. package/dist/connectors/index.d.ts +24 -0
  36. package/dist/connectors/index.d.ts.map +1 -0
  37. package/dist/connectors/index.js +69 -0
  38. package/dist/connectors/index.js.map +1 -0
  39. package/dist/connectors/ingest.d.ts +16 -0
  40. package/dist/connectors/ingest.d.ts.map +1 -0
  41. package/dist/connectors/ingest.js +116 -0
  42. package/dist/connectors/ingest.js.map +1 -0
  43. package/dist/connectors/types.d.ts +99 -0
  44. package/dist/connectors/types.d.ts.map +1 -0
  45. package/dist/connectors/types.js +17 -0
  46. package/dist/connectors/types.js.map +1 -0
  47. package/dist/constellation.js +168 -12
  48. package/dist/content-signature.js +45 -1
  49. package/dist/context-build-budgeted.js +144 -4
  50. package/dist/context-ranking.js +69 -1
  51. package/dist/crypto.js +179 -1
  52. package/dist/daemon-write-endpoint.js +290 -1
  53. package/dist/daemon-writer.js +406 -2
  54. package/dist/database.js +1278 -53
  55. package/dist/deprecations.js +162 -2
  56. package/dist/design.js +141 -13
  57. package/dist/event-replication.js +112 -1
  58. package/dist/events-sse.js +43 -7
  59. package/dist/events.js +238 -6
  60. package/dist/failure-patterns.d.ts +107 -0
  61. package/dist/failure-patterns.d.ts.map +1 -1
  62. package/dist/failure-patterns.js +924 -43
  63. package/dist/failure-patterns.js.map +1 -1
  64. package/dist/federation.js +236 -12
  65. package/dist/goals.js +101 -13
  66. package/dist/golden.js +355 -3
  67. package/dist/handlers/agent.js +165 -4
  68. package/dist/handlers/alias-adapters.js +129 -1
  69. package/dist/handlers/aliases.js +171 -1
  70. package/dist/handlers/audit.js +87 -1
  71. package/dist/handlers/boundary.js +221 -1
  72. package/dist/handlers/capture.js +1114 -73
  73. package/dist/handlers/causality.js +119 -9
  74. package/dist/handlers/cloud.js +382 -85
  75. package/dist/handlers/companion.js +459 -28
  76. package/dist/handlers/datalake.js +187 -7
  77. package/dist/handlers/dispatch-context.js +22 -0
  78. package/dist/handlers/entity.js +256 -25
  79. package/dist/handlers/events.js +335 -16
  80. package/dist/handlers/failure.d.ts.map +1 -1
  81. package/dist/handlers/failure.js +408 -13
  82. package/dist/handlers/failure.js.map +1 -1
  83. package/dist/handlers/goals.js +296 -4
  84. package/dist/handlers/intelligence.js +681 -126
  85. package/dist/handlers/invoicing.js +70 -1
  86. package/dist/handlers/mcpclient.js +137 -6
  87. package/dist/handlers/orchestration.js +125 -40
  88. package/dist/handlers/output-schemas.js +24 -1
  89. package/dist/handlers/presence.js +99 -3
  90. package/dist/handlers/project.js +182 -28
  91. package/dist/handlers/prompts.js +157 -6
  92. package/dist/handlers/quest.js +224 -4
  93. package/dist/handlers/recall.js +237 -13
  94. package/dist/handlers/registry.js +167 -1
  95. package/dist/handlers/resources.js +288 -1
  96. package/dist/handlers/review.js +74 -11
  97. package/dist/handlers/run.js +498 -16
  98. package/dist/handlers/search.js +338 -15
  99. package/dist/handlers/session.js +643 -31
  100. package/dist/handlers/share.js +184 -8
  101. package/dist/handlers/shims.js +464 -1
  102. package/dist/handlers/skill.js +449 -67
  103. package/dist/handlers/survivors.js +120 -1
  104. package/dist/handlers/symbols.js +109 -8
  105. package/dist/handlers/syncops.js +302 -4
  106. package/dist/handlers/types.js +27 -1
  107. package/dist/harvest.js +191 -5
  108. package/dist/hours.js +156 -7
  109. package/dist/http-auth.js +321 -3
  110. package/dist/http-fast.js +1302 -22
  111. package/dist/icons.js +47 -1
  112. package/dist/importers.js +268 -1
  113. package/dist/index.js +840 -2
  114. package/dist/indexer.js +145 -4
  115. package/dist/intelligence.js +261 -31
  116. package/dist/internal-dispatch.js +212 -3
  117. package/dist/keyset.js +110 -1
  118. package/dist/knowledge-graph.js +176 -12
  119. package/dist/license.js +441 -2
  120. package/dist/logger.js +199 -2
  121. package/dist/maintenance.js +148 -2
  122. package/dist/mcp-client.js +262 -6
  123. package/dist/memory-artifacts.js +596 -32
  124. package/dist/migrate-prompt.js +124 -2
  125. package/dist/migrations.d.ts.map +1 -1
  126. package/dist/migrations.js +799 -42
  127. package/dist/migrations.js.map +1 -1
  128. package/dist/performance.js +228 -1
  129. package/dist/presence.js +140 -11
  130. package/dist/priority-embed.js +164 -5
  131. package/dist/providers/embedding-provider.js +196 -1
  132. package/dist/readonly-gate.js +29 -1
  133. package/dist/receipt.js +43 -1
  134. package/dist/rehydration.js +157 -9
  135. package/dist/reindex.js +88 -1
  136. package/dist/render-target.js +544 -21
  137. package/dist/render.js +280 -4
  138. package/dist/repl-guard.js +173 -1
  139. package/dist/replication-daemon-entrypoint.js +31 -1
  140. package/dist/replication-daemon.js +262 -2
  141. package/dist/rerank.js +142 -1
  142. package/dist/resilience.js +591 -1
  143. package/dist/reverse-bridge.js +360 -5
  144. package/dist/security.js +244 -1
  145. package/dist/session-seen.js +51 -3
  146. package/dist/setup.js +260 -1
  147. package/dist/skill-author.js +168 -5
  148. package/dist/spec-kit.js +191 -1
  149. package/dist/sqlite-busy.js +154 -1
  150. package/dist/statusline.js +315 -11
  151. package/dist/sub-agent.js +262 -13
  152. package/dist/summarizer.js +139 -13
  153. package/dist/symbols.js +283 -7
  154. package/dist/sync.js +359 -5
  155. package/dist/tasks-dispatch.js +84 -1
  156. package/dist/tasks.js +282 -1
  157. package/dist/token-budget.js +143 -1
  158. package/dist/tool-analytics.js +129 -7
  159. package/dist/tool-annotations.js +365 -1
  160. package/dist/tool-manifest-v2.json +1 -1
  161. package/dist/tool-manifest.json +1 -1
  162. package/dist/tool-profiles.js +75 -1
  163. package/dist/trace-harvest.js +244 -6
  164. package/dist/types.js +30 -1
  165. package/dist/ui-dashboard.js +50 -41
  166. package/dist/ulid.js +81 -1
  167. package/dist/usage-tracker.js +66 -1
  168. package/dist/validate.js +129 -1
  169. package/dist/vault.js +534 -1
  170. package/dist/vector-init.js +67 -1
  171. package/dist/vectors.js +184 -3
  172. package/dist/version-check.js +136 -4
  173. package/dist/visibility.js +155 -19
  174. package/dist/wyrm-cli.js +2845 -101
  175. package/dist/wyrm-cli.js.map +1 -1
  176. package/dist/wyrm-guard.d.ts.map +1 -1
  177. package/dist/wyrm-guard.js +475 -14
  178. package/dist/wyrm-guard.js.map +1 -1
  179. package/dist/wyrm-loop.js +150 -3
  180. package/dist/wyrm-manifest.json +1 -1
  181. package/dist/wyrm-statusline-daemon.js +11 -1
  182. package/dist/wyrm-statusline.js +56 -4
  183. package/dist/wyrm-ui.js +77 -9
  184. package/package.json +1 -1
@@ -1,10 +1,72 @@
1
- import{existsSync as E,lstatSync as N,mkdirSync as D,readFileSync as k,realpathSync as L,writeFileSync as A}from"fs";import{dirname as _,isAbsolute as m,join as f,relative as w,resolve as R,sep as S}from"path";const O=200,$="<!-- wyrm:render:start -->",h="<!-- wyrm:render:end -->";function V(e){return{truths(r){return e.prepare(`
1
+ /**
2
+ * Wyrm Render Target (v7 F4 — T038) — the deterministic DB→memory-file compiler.
3
+ *
4
+ * `wyrm render` is the ZERO-MCP-TOKEN path for casual sessions: instead of an
5
+ * agent spending tool calls to load context, Wyrm compiles the project's
6
+ * authoritative state (ground truths, open quests, validated patterns,
7
+ * unresolved failures) straight into the harness-native memory slot —
8
+ * `MEMORY.md` (hard 200-line budget) + per-topic files + a lean SessionStart
9
+ * brief — plus per-client adapters (Claude Code / Cursor / Copilot / AGENTS.md).
10
+ *
11
+ * Design law (this module is the NET-NEW, TEMPLATE-ISOLATED writer per the spec):
12
+ * - DETERMINISTIC / BYTE-STABLE: there is NO Date.now()/Math.random() in the
13
+ * OUTPUT. Every volatile value (the Wyrm version, the compiled-at stamp, the
14
+ * artifact count) is PASSED IN via {@link RenderStamp}. Same model + same
15
+ * stamp ⇒ byte-identical bytes, forever (golden-replayable).
16
+ * - PROVENANCE-STAMPED: every emitted file carries a header that says it was
17
+ * compiled by Wyrm and that edits are HARVESTED, not lost (the reverse bridge,
18
+ * T039, picks them up). Wyrm-managed regions are MARKER-BOUNDED so a writer
19
+ * NEVER clobbers operator prose outside the markers (the autoconfig.ts
20
+ * idempotent-block discipline, reused here).
21
+ * - SAFE (Article VII): {@link writeRenderTarget} refuses to escape its target
22
+ * directory (no `..`, no absolute re-roots) and refuses to overwrite a file
23
+ * that is NOT Wyrm-managed unless explicitly forced — a human's hand-written
24
+ * MEMORY.md is never silently destroyed.
25
+ * - OFFLINE (Article III): pure DB read + string building. No network, no LLM,
26
+ * no clock of its own.
27
+ *
28
+ * The DB read is injected ({@link RenderDeps}) so the COMPILER is unit-testable
29
+ * against an in-memory fixture, exactly like harvest.ts.
30
+ *
31
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
32
+ * @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
33
+ */
34
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'fs';
35
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'path';
36
+ // ──────────────────────────────────────────────────────────────────────────
37
+ // Budgets & markers
38
+ // ──────────────────────────────────────────────────────────────────────────
39
+ /** MEMORY.md hard line budget (spec FR-6). The compiler trims to fit. */
40
+ export const MEMORY_MD_LINE_BUDGET = 200;
41
+ /** Marker delimiting the Wyrm-managed region inside any rendered file. */
42
+ export const RENDER_MARKER_START = '<!-- wyrm:render:start -->';
43
+ export const RENDER_MARKER_END = '<!-- wyrm:render:end -->';
44
+ /**
45
+ * Default DB-backed deps. Pure SQL, bound params, deterministic ORDER BY on a
46
+ * TOTAL order (every query appends an id/key tiebreak so two rows never tie on
47
+ * the wire — the bare-sort-key trap). All listings are hard-capped.
48
+ */
49
+ export function makeRenderDeps(db) {
50
+ return {
51
+ truths(projectId) {
52
+ const rows = db.prepare(`
2
53
  SELECT category, key, value, rationale, confidence, ttl_days,
3
54
  CAST((julianday('now') - julianday(last_verified_at)) AS REAL) AS age_days
4
55
  FROM ground_truths
5
56
  WHERE project_id = ? AND is_current = 1
6
57
  ORDER BY category, key
7
- `).all(r).map(t=>({category:t.category,key:t.key,value:t.value,rationale:t.rationale,confidence:t.confidence,stale:t.ttl_days!=null&&t.age_days>t.ttl_days}))},quests(r){return e.prepare(`
58
+ `).all(projectId);
59
+ return rows.map((r) => ({
60
+ category: r.category,
61
+ key: r.key,
62
+ value: r.value,
63
+ rationale: r.rationale,
64
+ confidence: r.confidence,
65
+ stale: r.ttl_days != null && r.age_days > r.ttl_days,
66
+ }));
67
+ },
68
+ quests(projectId) {
69
+ return db.prepare(`
8
70
  SELECT id, title, description, priority
9
71
  FROM quests
10
72
  WHERE project_id = ? AND status IN ('pending','in_progress')
@@ -13,30 +75,491 @@ import{existsSync as E,lstatSync as N,mkdirSync as D,readFileSync as k,realpathS
13
75
  WHEN 'medium' THEN 2 ELSE 3 END,
14
76
  id ASC
15
77
  LIMIT 100
16
- `).all(r)},artifacts(r){try{return e.prepare(`
78
+ `).all(projectId);
79
+ },
80
+ artifacts(projectId) {
81
+ try {
82
+ return db.prepare(`
17
83
  SELECT id, problem, validated_fix
18
84
  FROM memory_artifacts
19
85
  WHERE project_id = ? AND (needs_review = 0 OR needs_review IS NULL)
20
86
  ORDER BY id DESC LIMIT 50
21
- `).all(r)}catch{return[]}},failures(r){try{return e.prepare(`
87
+ `).all(projectId);
88
+ }
89
+ catch {
90
+ return [];
91
+ }
92
+ },
93
+ failures(projectId) {
94
+ try {
95
+ return db.prepare(`
22
96
  SELECT id, scope, target, description, severity, occurrences
23
97
  FROM failure_patterns
24
98
  WHERE (project_id = ? OR project_id IS NULL) AND resolved = 0
25
99
  ORDER BY last_seen DESC, id DESC LIMIT 50
26
- `).all(r)}catch{return[]}}}}function b(e,r){return{project_id:r.id,project_name:r.name,project_path:r.path,truths:e.truths(r.id),quests:e.quests(r.id),artifacts:e.artifacts(r.id),failures:e.failures(r.id)}}function M(e){return e.truths.length+e.quests.length+e.artifacts.length+e.failures.length}function x(e,r){const n=M(e);return[`<!-- Compiled by Wyrm v${r.wyrm_version} from ${n} memory artifact${n===1?"":"s"}.`,` Project: ${e.project_name}. Compiled at ${r.compiled_at}.`," Edits inside the wyrm:render markers are HARVESTED to the review queue, not lost \u2014"," but they are OVERWRITTEN on the next `wyrm render`; capture durable notes with `wyrm capture`. -->"]}function u(e){return e.replace(/\s*\n\s*/g," ").trim()}function p(e,r){return e.length>r?`${e.slice(0,r-1)}\u2026`:e}function T(e,r){const n=x(e,r),t=[];t.push(`# ${e.project_name} \u2014 Project Memory`),t.push("");const s=O-4,a=[];e.truths.length>0&&a.push({heading:`## Ground truths (${e.truths.length})`,refTool:"wyrm_truth_get",lines:e.truths.map(i=>{const l=i.stale?" [STALE]":"",d=i.confidence<1?` (conf ${i.confidence})`:"";return`- **${i.category}.${i.key}**${l}: ${p(u(i.value),160)}${d}`})}),e.failures.length>0&&a.push({heading:`## Failures \u2014 DO NOT repeat (${e.failures.length})`,refTool:"wyrm_failure_check",lines:e.failures.map(i=>`- [${i.severity}] ${i.scope}:${i.target} \xD7${i.occurrences} \u2014 ${p(u(i.description),140)}`)}),e.quests.length>0&&a.push({heading:`## Open quests (${e.quests.length})`,refTool:"wyrm_quest",lines:e.quests.map(i=>`- #${i.id} [${i.priority}] ${p(u(i.title),140)}`)}),e.artifacts.length>0&&a.push({heading:`## Validated patterns (${e.artifacts.length})`,refTool:"wyrm_recall",lines:e.artifacts.map(i=>`- ${p(u(i.problem),120)}${i.validated_fix?` \u2192 ${p(u(i.validated_fix),120)}`:""}`)});let o=0;for(const i of a){if(o+4>s)break;t.push(i.heading),t.push(""),o+=2;let l=0;for(const d of i.lines){if(o+1+(l<i.lines.length?1:0)+1>s)break;t.push(d),o+=1,l+=1}l<i.lines.length&&(t.push(`- _\u2026 ${i.lines.length-l} more \u2014 query \`${i.refTool}\`._`),o+=1),t.push(""),o+=1}return t.push("---"),t.push("*Full memory: `wyrm recall` / `wyrm session prime`. This file is a deterministic digest.*"),[...n,...t].join(`
27
- `)}function W(e,r){const n=x(e,r),t=[],c=(s,a,o)=>{o.length!==0&&t.push({filename:s,content:[...n,`# ${e.project_name} \u2014 ${a}`,"",...o].join(`
28
- `)})};return c("truths.md","Ground truths",e.truths.map(s=>{const a=s.stale?" [STALE]":"";return`- **${s.category}.${s.key}**${a}: ${u(s.value)}`+(s.rationale?`
29
- - rationale: ${u(s.rationale)}`:"")+(s.confidence<1?`
30
- - confidence: ${s.confidence}`:"")})),c("quests.md","Open quests",e.quests.map(s=>`- #${s.id} [${s.priority}] ${u(s.title)}`+(s.description?`
31
- - ${u(s.description)}`:""))),c("patterns.md","Validated patterns",e.artifacts.map(s=>`- ${u(s.problem)}`+(s.validated_fix?`
32
- - fix: ${u(s.validated_fix)}`:""))),c("failures.md","Failures \u2014 DO NOT repeat",e.failures.map(s=>`- [${s.severity}] ${s.scope}:${s.target} \xD7${s.occurrences}
33
- - ${u(s.description)}`)),t}function C(e,r){const n=[];if(n.push(`Wyrm memory for ${e.project_name} (compiled by Wyrm v${r.wyrm_version}):`),e.failures.length>0){n.push(""),n.push("Known failures (do not repeat):");for(const t of e.failures.slice(0,5))n.push(`- ${t.scope}:${t.target} \u2014 ${p(u(t.description),120)}`);e.failures.length>5&&n.push(`- (+${e.failures.length-5} more \u2014 wyrm_failure_check)`)}if(e.truths.length>0){n.push(""),n.push("Ground truths:");for(const t of e.truths.slice(0,8))n.push(`- ${t.category}.${t.key}: ${p(u(t.value),120)}`);e.truths.length>8&&n.push(`- (+${e.truths.length-8} more \u2014 wyrm_truth_get)`)}if(e.quests.length>0){n.push(""),n.push("Open quests:");for(const t of e.quests.slice(0,5))n.push(`- #${t.id} [${t.priority}] ${p(u(t.title),100)}`);e.quests.length>5&&n.push(`- (+${e.quests.length-5} more \u2014 wyrm_quest)`)}return n.push(""),n.push("Load full context with wyrm_session_prime. (Wyrm is this session's memory; consult it first.)"),n.join(`
34
- `)}function I(e){switch(e){case"claude":return"CLAUDE.md";case"cursor":return f(".cursor","rules","wyrm-memory.md");case"copilot":return f(".github","copilot-instructions.md");case"agents":return"AGENTS.md";case"windsurf":return f(".windsurf","rules","wyrm-memory.md");case"cline":return f(".clinerules","wyrm-memory.md");case"zed":return".rules";case"gemini":return"GEMINI.md";default:{const r=e;throw new Error(`unknown render client: ${String(r)}`)}}}function q(e,r,n){return{client:e,relPath:I(e),block:T(r,n)}}const j=["claude","cursor","copilot","agents","windsurf","cline","zed","gemini"],v={claude:["CLAUDE.md",".claude"],cursor:[".cursor",".cursorrules"],copilot:[".github/copilot-instructions.md",".github"],agents:["AGENTS.md"],windsurf:[".windsurf",".windsurfrules"],cline:[".clinerules"],zed:[".rules",".zed"],gemini:["GEMINI.md",".gemini"]};function P(e){const r=new Set(e);return j.filter(n=>v[n].some(t=>r.has(t)))}const J=[...new Set(Object.values(v).flat())];function H(e,r){const n=`${$}
35
- ${r}
36
- ${h}`;if(!e)return`${n}
37
- `;const t=e.indexOf($),c=e.indexOf(h);if(t!==-1&&c!==-1&&c>t){const a=e.slice(0,t),o=e.slice(c+h.length);return`${a}${n}${o}`}const s=e.endsWith(`
38
- `)?`
39
- `:`
40
-
41
- `;return`${e}${s}${n}
42
- `}function F(e){const r=e.indexOf($),n=e.indexOf(h);return r!==-1&&n!==-1&&n>r}class y extends Error{}const B={existsSync:E,lstatSync:N,realpathSync:L};function G(e,r,n=B){if(m(r))throw new y(`render target must be relative, got absolute: ${r}`);const t=R(e),c=R(t,r),s=w(t,c);if(s===""||s.startsWith("..")||s.startsWith(`..${S}`)||m(s))throw new y(`render target escapes root: ${r}`);let a;try{a=n.realpathSync(t)}catch{return c}if(n.existsSync(c)){let d=!1;try{d=n.lstatSync(c).isSymbolicLink()}catch{d=!1}if(d)throw new y(`render target is a symlink (refusing to follow): ${r}`)}let o=c;for(;!n.existsSync(o);){const d=_(o);if(d===o)break;o=d}let i;try{i=n.realpathSync(o)}catch{return c}const l=w(a,i);if(l===""&&o===t)return c;if(l.startsWith("..")||l.startsWith(`..${S}`)||m(l))throw new y(`render target escapes root via symlink: ${r}`);return c}const Y={existsSync:E,readFileSync:k,writeFileSync:A,mkdirSync:D};function g(e,r,n,t={}){const c=t.fs??Y,s=G(e,r),a=c.existsSync(s),o=a?c.readFileSync(s,"utf-8"):null;if(a&&o!=null&&!F(o)&&!t.force)return{path:s,action:"skipped",reason:"file exists and is not Wyrm-managed (pass force to append)"};const i=H(o,n);return o!=null&&i===o?{path:s,action:"skipped",reason:"unchanged"}:(c.mkdirSync(_(s),{recursive:!0}),c.writeFileSync(s,i,"utf-8"),{path:s,action:a?"updated":"created"})}function K(e,r,n){const t=b(e,r);return{model:t,memoryMd:T(t,n),topics:W(t,n),sessionBrief:C(t,n)}}function Q(e,r,n,t={}){const c=t.rootDir??r.path,s=t.topicsDir??f(".wyrm","memory"),a=K(e,r,n),o=[];o.push(g(c,"MEMORY.md",a.memoryMd,t));for(const i of a.topics)o.push(g(c,f(s,i.filename),i.content,{...t,force:!0}));for(const i of t.clients??[]){const l=q(i,a.model,n);o.push(g(c,l.relPath,l.block,t))}return{plan:a,writes:o}}function X(e=process.env){const r=(e.WYRM_RENDER_WATCH??"").toLowerCase();return r==="1"||r==="true"||r==="yes"}class Z{rendered;constructor(r=0){this.rendered=r}get last(){return this.rendered}advance(r){return!Number.isFinite(r)||r<=this.rendered?!1:(this.rendered=r,!0)}}export{J as ALL_CLIENT_MARKERS,j as ALL_RENDER_CLIENTS,O as MEMORY_MD_LINE_BUDGET,h as RENDER_MARKER_END,$ as RENDER_MARKER_START,Z as RenderCursorTracker,y as RenderPathError,K as buildRenderPlan,I as clientRelPath,b as compileRenderModel,P as detectClients,F as hasWyrmRegion,V as makeRenderDeps,x as provenanceHeader,q as renderForClient,T as renderMemoryMd,C as renderSessionBrief,Q as renderToDisk,W as renderTopicFiles,X as renderWatchEnabled,G as resolveInsideRoot,H as spliceWyrmRegion,g as writeRenderTarget};
100
+ `).all(projectId);
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ },
106
+ };
107
+ }
108
+ /** Compile the full structured model for a project (pure given deps). */
109
+ export function compileRenderModel(deps, project) {
110
+ return {
111
+ project_id: project.id,
112
+ project_name: project.name,
113
+ project_path: project.path,
114
+ truths: deps.truths(project.id),
115
+ quests: deps.quests(project.id),
116
+ artifacts: deps.artifacts(project.id),
117
+ failures: deps.failures(project.id),
118
+ };
119
+ }
120
+ // ──────────────────────────────────────────────────────────────────────────
121
+ // Provenance
122
+ // ──────────────────────────────────────────────────────────────────────────
123
+ function artifactCount(m) {
124
+ return m.truths.length + m.quests.length + m.artifacts.length + m.failures.length;
125
+ }
126
+ /**
127
+ * The provenance lines. Deterministic given (model, stamp). States plainly that
128
+ * edits are HARVESTED, not lost (the reverse bridge picks up changes between the
129
+ * markers and routes them to the review queue, T039).
130
+ */
131
+ export function provenanceHeader(m, stamp) {
132
+ const n = artifactCount(m);
133
+ return [
134
+ `<!-- Compiled by Wyrm v${stamp.wyrm_version} from ${n} memory artifact${n === 1 ? '' : 's'}.`,
135
+ ` Project: ${m.project_name}. Compiled at ${stamp.compiled_at}.`,
136
+ ` Edits inside the wyrm:render markers are HARVESTED to the review queue, not lost —`,
137
+ ` but they are OVERWRITTEN on the next \`wyrm render\`; capture durable notes with \`wyrm capture\`. -->`,
138
+ ];
139
+ }
140
+ // ──────────────────────────────────────────────────────────────────────────
141
+ // MEMORY.md — the 200-line-budgeted master file.
142
+ // ──────────────────────────────────────────────────────────────────────────
143
+ /** Sanitise a one-line cell: collapse newlines so a value never spends >1 line. */
144
+ function oneLine(s) {
145
+ return s.replace(/\s*\n\s*/g, ' ').trim();
146
+ }
147
+ /** Truncate to a max length with an ellipsis (deterministic). */
148
+ function clip(s, max) {
149
+ return s.length > max ? `${s.slice(0, max - 1)}…` : s;
150
+ }
151
+ /**
152
+ * Render MEMORY.md body (the lines BETWEEN the markers, provenance included).
153
+ * Hard 200-line budget: sections are emitted in priority order and the LAST
154
+ * section to overflow is truncated with an explicit "N more" note, so the file
155
+ * never exceeds the budget no matter how large the corpus.
156
+ *
157
+ * Returns the full marker-bounded block (markers + provenance + body), ready to
158
+ * splice into a file by {@link spliceWyrmRegion}.
159
+ */
160
+ export function renderMemoryMd(m, stamp) {
161
+ const header = provenanceHeader(m, stamp);
162
+ // The marker + provenance lines count against NOTHING extra here; the budget
163
+ // governs the human-readable body. We reserve a few lines for the title and
164
+ // the footer so the total stays at/under the budget.
165
+ const body = [];
166
+ body.push(`# ${m.project_name} — Project Memory`);
167
+ body.push('');
168
+ // Footer is always 2 lines; title+blank is 2 lines. Keep the content within
169
+ // the remaining budget.
170
+ const reserved = 4;
171
+ const contentBudget = MEMORY_MD_LINE_BUDGET - reserved;
172
+ const sections = [];
173
+ if (m.truths.length > 0) {
174
+ sections.push({
175
+ heading: `## Ground truths (${m.truths.length})`,
176
+ refTool: 'wyrm_truth_get',
177
+ lines: m.truths.map((t) => {
178
+ const marker = t.stale ? ' [STALE]' : '';
179
+ const conf = t.confidence < 1 ? ` (conf ${t.confidence})` : '';
180
+ return `- **${t.category}.${t.key}**${marker}: ${clip(oneLine(t.value), 160)}${conf}`;
181
+ }),
182
+ });
183
+ }
184
+ if (m.failures.length > 0) {
185
+ sections.push({
186
+ heading: `## Failures — DO NOT repeat (${m.failures.length})`,
187
+ refTool: 'wyrm_failure_check',
188
+ lines: m.failures.map((f) => `- [${f.severity}] ${f.scope}:${f.target} ×${f.occurrences} — ${clip(oneLine(f.description), 140)}`),
189
+ });
190
+ }
191
+ if (m.quests.length > 0) {
192
+ sections.push({
193
+ heading: `## Open quests (${m.quests.length})`,
194
+ refTool: 'wyrm_quest',
195
+ lines: m.quests.map((q) => `- #${q.id} [${q.priority}] ${clip(oneLine(q.title), 140)}`),
196
+ });
197
+ }
198
+ if (m.artifacts.length > 0) {
199
+ sections.push({
200
+ heading: `## Validated patterns (${m.artifacts.length})`,
201
+ refTool: 'wyrm_recall',
202
+ lines: m.artifacts.map((a) => `- ${clip(oneLine(a.problem), 120)}${a.validated_fix ? ` → ${clip(oneLine(a.validated_fix), 120)}` : ''}`),
203
+ });
204
+ }
205
+ // Greedily emit sections within the budget. Each section costs: heading + a
206
+ // blank line + N item lines + a trailing blank. When a section would overflow,
207
+ // emit as many items as fit and a single "N more — query <tool>" line.
208
+ let used = 0;
209
+ for (const s of sections) {
210
+ // Minimum cost to open a section: heading + blank + at least the "more" note
211
+ // + trailing blank = 4 lines. If we can't even do that, stop.
212
+ if (used + 4 > contentBudget)
213
+ break;
214
+ body.push(s.heading);
215
+ body.push('');
216
+ used += 2;
217
+ let emitted = 0;
218
+ for (const line of s.lines) {
219
+ // Reserve 1 line for a potential "more" note + 1 trailing blank.
220
+ if (used + 1 + (emitted < s.lines.length ? 1 : 0) + 1 > contentBudget)
221
+ break;
222
+ body.push(line);
223
+ used += 1;
224
+ emitted += 1;
225
+ }
226
+ if (emitted < s.lines.length) {
227
+ body.push(`- _… ${s.lines.length - emitted} more — query \`${s.refTool}\`._`);
228
+ used += 1;
229
+ }
230
+ body.push('');
231
+ used += 1;
232
+ }
233
+ body.push('---');
234
+ body.push(`*Full memory: \`wyrm recall\` / \`wyrm session prime\`. This file is a deterministic digest.*`);
235
+ return [...header, ...body].join('\n');
236
+ }
237
+ /**
238
+ * Render the per-topic files (the full, un-budgeted corpus). Each is its own
239
+ * marker-bounded, provenance-stamped file. Deterministic given (model, stamp).
240
+ */
241
+ export function renderTopicFiles(m, stamp) {
242
+ const header = provenanceHeader(m, stamp);
243
+ const out = [];
244
+ const mk = (filename, title, lines) => {
245
+ if (lines.length === 0)
246
+ return;
247
+ out.push({
248
+ filename,
249
+ content: [...header, `# ${m.project_name} — ${title}`, '', ...lines].join('\n'),
250
+ });
251
+ };
252
+ mk('truths.md', 'Ground truths', m.truths.map((t) => {
253
+ const marker = t.stale ? ' [STALE]' : '';
254
+ return `- **${t.category}.${t.key}**${marker}: ${oneLine(t.value)}` +
255
+ (t.rationale ? `\n - rationale: ${oneLine(t.rationale)}` : '') +
256
+ (t.confidence < 1 ? `\n - confidence: ${t.confidence}` : '');
257
+ }));
258
+ mk('quests.md', 'Open quests', m.quests.map((q) => `- #${q.id} [${q.priority}] ${oneLine(q.title)}` +
259
+ (q.description ? `\n - ${oneLine(q.description)}` : '')));
260
+ mk('patterns.md', 'Validated patterns', m.artifacts.map((a) => `- ${oneLine(a.problem)}` + (a.validated_fix ? `\n - fix: ${oneLine(a.validated_fix)}` : '')));
261
+ mk('failures.md', 'Failures — DO NOT repeat', m.failures.map((f) => `- [${f.severity}] ${f.scope}:${f.target} ×${f.occurrences}\n - ${oneLine(f.description)}`));
262
+ return out;
263
+ }
264
+ // ──────────────────────────────────────────────────────────────────────────
265
+ // SessionStart brief — the lean, byte-stable injection payload.
266
+ // ──────────────────────────────────────────────────────────────────────────
267
+ /**
268
+ * The SessionStart brief: a compact, deterministic summary a harness can inject
269
+ * at session start with near-zero cost. Caps each section so the brief stays
270
+ * lean (it is NOT the full corpus — that's the topic files). Byte-stable.
271
+ */
272
+ export function renderSessionBrief(m, stamp) {
273
+ const lines = [];
274
+ lines.push(`Wyrm memory for ${m.project_name} (compiled by Wyrm v${stamp.wyrm_version}):`);
275
+ if (m.failures.length > 0) {
276
+ lines.push('');
277
+ lines.push(`Known failures (do not repeat):`);
278
+ for (const f of m.failures.slice(0, 5)) {
279
+ lines.push(`- ${f.scope}:${f.target} — ${clip(oneLine(f.description), 120)}`);
280
+ }
281
+ if (m.failures.length > 5)
282
+ lines.push(`- (+${m.failures.length - 5} more — wyrm_failure_check)`);
283
+ }
284
+ if (m.truths.length > 0) {
285
+ lines.push('');
286
+ lines.push(`Ground truths:`);
287
+ for (const t of m.truths.slice(0, 8)) {
288
+ lines.push(`- ${t.category}.${t.key}: ${clip(oneLine(t.value), 120)}`);
289
+ }
290
+ if (m.truths.length > 8)
291
+ lines.push(`- (+${m.truths.length - 8} more — wyrm_truth_get)`);
292
+ }
293
+ if (m.quests.length > 0) {
294
+ lines.push('');
295
+ lines.push(`Open quests:`);
296
+ for (const q of m.quests.slice(0, 5)) {
297
+ lines.push(`- #${q.id} [${q.priority}] ${clip(oneLine(q.title), 100)}`);
298
+ }
299
+ if (m.quests.length > 5)
300
+ lines.push(`- (+${m.quests.length - 5} more — wyrm_quest)`);
301
+ }
302
+ lines.push('');
303
+ lines.push(`Load full context with wyrm_session_prime. (Wyrm is this session's memory; consult it first.)`);
304
+ return lines.join('\n');
305
+ }
306
+ /**
307
+ * Per-client memory-file layout. Each adapter targets that client's native
308
+ * memory slot with its native filename — no Anthropic-only behaviour leaks into
309
+ * the others. The BLOCK content is identical (the MEMORY.md digest); only the
310
+ * destination differs, so the digest is one source of truth.
311
+ */
312
+ /** The native memory-slot path for a client, relative to the project root. */
313
+ export function clientRelPath(client) {
314
+ switch (client) {
315
+ // Claude Code reads a project-root CLAUDE.md as its canonical memory slot.
316
+ case 'claude': return 'CLAUDE.md';
317
+ case 'cursor': return join('.cursor', 'rules', 'wyrm-memory.md');
318
+ case 'copilot': return join('.github', 'copilot-instructions.md');
319
+ case 'agents': return 'AGENTS.md';
320
+ case 'windsurf': return join('.windsurf', 'rules', 'wyrm-memory.md');
321
+ case 'cline': return join('.clinerules', 'wyrm-memory.md');
322
+ case 'zed': return '.rules';
323
+ case 'gemini': return 'GEMINI.md';
324
+ default: {
325
+ const never = client;
326
+ throw new Error(`unknown render client: ${String(never)}`);
327
+ }
328
+ }
329
+ }
330
+ export function renderForClient(client, m, stamp) {
331
+ // The BLOCK is identical across clients (the MEMORY.md digest, one source of
332
+ // truth); only the destination slot differs. Codex shares 'agents' (AGENTS.md).
333
+ return { client, relPath: clientRelPath(client), block: renderMemoryMd(m, stamp) };
334
+ }
335
+ /** Every client Wyrm can render into, in a stable detection order. */
336
+ export const ALL_RENDER_CLIENTS = ['claude', 'cursor', 'copilot', 'agents', 'windsurf', 'cline', 'zed', 'gemini'];
337
+ /** Per-client repo markers that signal "this harness is in use here". */
338
+ const CLIENT_MARKERS = {
339
+ claude: ['CLAUDE.md', '.claude'],
340
+ cursor: ['.cursor', '.cursorrules'],
341
+ copilot: ['.github/copilot-instructions.md', '.github'],
342
+ agents: ['AGENTS.md'],
343
+ windsurf: ['.windsurf', '.windsurfrules'],
344
+ cline: ['.clinerules'],
345
+ zed: ['.rules', '.zed'],
346
+ gemini: ['GEMINI.md', '.gemini'],
347
+ };
348
+ /**
349
+ * Zero-config harness fingerprint: given the repo-relative paths that EXIST in a
350
+ * project, return the clients to render for — exactly the harnesses present, in
351
+ * a stable order. `wyrm init` scans the project then calls this; rendering only
352
+ * into slots that exist means Wyrm never litters a repo with foreign files.
353
+ */
354
+ export function detectClients(present) {
355
+ const set = new Set(present);
356
+ return ALL_RENDER_CLIENTS.filter((c) => CLIENT_MARKERS[c].some((m) => set.has(m)));
357
+ }
358
+ /** The deduped union of every harness marker — what `wyrm init`/`render` scans
359
+ * the project root for (existsSync each) before calling detectClients. */
360
+ export const ALL_CLIENT_MARKERS = [...new Set(Object.values(CLIENT_MARKERS).flat())];
361
+ // ──────────────────────────────────────────────────────────────────────────
362
+ // Marker splicing + safe writing.
363
+ // ──────────────────────────────────────────────────────────────────────────
364
+ /**
365
+ * Splice a marker-bounded Wyrm block into existing file content WITHOUT touching
366
+ * operator prose outside the markers. If a marker region already exists it is
367
+ * replaced; otherwise the block is appended. Returns the new file content.
368
+ *
369
+ * The returned content wraps `block` in the start/end markers exactly once.
370
+ */
371
+ export function spliceWyrmRegion(existing, block) {
372
+ const region = `${RENDER_MARKER_START}\n${block}\n${RENDER_MARKER_END}`;
373
+ if (!existing)
374
+ return `${region}\n`;
375
+ const startIdx = existing.indexOf(RENDER_MARKER_START);
376
+ const endIdx = existing.indexOf(RENDER_MARKER_END);
377
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
378
+ const before = existing.slice(0, startIdx);
379
+ const after = existing.slice(endIdx + RENDER_MARKER_END.length);
380
+ return `${before}${region}${after}`;
381
+ }
382
+ const sep = existing.endsWith('\n') ? '\n' : '\n\n';
383
+ return `${existing}${sep}${region}\n`;
384
+ }
385
+ /** Does this file content already carry a Wyrm-managed region? */
386
+ export function hasWyrmRegion(content) {
387
+ const s = content.indexOf(RENDER_MARKER_START);
388
+ const e = content.indexOf(RENDER_MARKER_END);
389
+ return s !== -1 && e !== -1 && e > s;
390
+ }
391
+ export class RenderPathError extends Error {
392
+ }
393
+ const NODE_STAT_FS = { existsSync, lstatSync, realpathSync };
394
+ /**
395
+ * Resolve `relPath` strictly inside `rootDir`. Rejects absolute paths and any
396
+ * traversal that escapes the root (Article VII: a writer/watcher must never
397
+ * escape its target dir). Returns the absolute, validated path.
398
+ *
399
+ * The lexical `resolve`/`relative` gate blocks `..` traversal, but it does NOT
400
+ * stop a SYMLINK that lives inside the root and points outside it — `writeFile`/
401
+ * `readFile` follow symlinks, so a target like `MEMORY.md` symlinked to
402
+ * `~/.ssh/config` would be written/read THROUGH the link, escaping the dir. So
403
+ * we additionally:
404
+ * 1. refuse when the existing target is itself a symlink (lstat isSymbolicLink),
405
+ * 2. re-assert the REAL path of the target (or its nearest existing ancestor,
406
+ * for a not-yet-created target) is inside the REAL path of the root —
407
+ * defeating an in-root symlinked ancestor directory.
408
+ * The stat surface is injected so the symlink lens is unit-testable without a
409
+ * live filesystem.
410
+ */
411
+ export function resolveInsideRoot(rootDir, relPath, statFs = NODE_STAT_FS) {
412
+ if (isAbsolute(relPath)) {
413
+ throw new RenderPathError(`render target must be relative, got absolute: ${relPath}`);
414
+ }
415
+ const root = resolve(rootDir);
416
+ const target = resolve(root, relPath);
417
+ const rel = relative(root, target);
418
+ if (rel === '' || rel.startsWith('..') || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
419
+ throw new RenderPathError(`render target escapes root: ${relPath}`);
420
+ }
421
+ // Symlink defence (Article VII). The lexical check above passed, so now guard
422
+ // against in-root symlinks that resolve outside the root.
423
+ let realRoot;
424
+ try {
425
+ realRoot = statFs.realpathSync(root);
426
+ }
427
+ catch {
428
+ // Root itself does not resolve (does not exist / broken) — fall back to the
429
+ // lexically validated path; nothing on disk to escape through yet.
430
+ return target;
431
+ }
432
+ // 1. The target, if it already exists, must not BE a symlink.
433
+ if (statFs.existsSync(target)) {
434
+ let isLink = false;
435
+ try {
436
+ isLink = statFs.lstatSync(target).isSymbolicLink();
437
+ }
438
+ catch {
439
+ isLink = false;
440
+ }
441
+ if (isLink) {
442
+ throw new RenderPathError(`render target is a symlink (refusing to follow): ${relPath}`);
443
+ }
444
+ }
445
+ // 2. The REAL path of the target's nearest existing ancestor must stay inside
446
+ // the real root — catches a symlinked intermediate directory.
447
+ let probe = target;
448
+ // Walk up to the nearest ancestor that exists on disk.
449
+ while (!statFs.existsSync(probe)) {
450
+ const parent = dirname(probe);
451
+ if (parent === probe)
452
+ break; // reached filesystem root
453
+ probe = parent;
454
+ }
455
+ let realProbe;
456
+ try {
457
+ realProbe = statFs.realpathSync(probe);
458
+ }
459
+ catch {
460
+ return target; // ancestor vanished mid-check — degrade to the lexical path
461
+ }
462
+ const realRel = relative(realRoot, realProbe);
463
+ if (realRel === '' && probe === root) {
464
+ return target; // the existing ancestor IS the root — fine
465
+ }
466
+ if (realRel.startsWith('..') || realRel.startsWith(`..${sep}`) || isAbsolute(realRel)) {
467
+ throw new RenderPathError(`render target escapes root via symlink: ${relPath}`);
468
+ }
469
+ return target;
470
+ }
471
+ const NODE_FS = { existsSync, readFileSync, writeFileSync, mkdirSync };
472
+ /**
473
+ * Write a marker-bounded block into `rootDir/relPath` safely:
474
+ * - the path is validated to stay inside rootDir (no escape),
475
+ * - an existing Wyrm region is replaced; operator prose outside is untouched,
476
+ * - a file that exists but is NOT Wyrm-managed is SKIPPED unless `force` (then
477
+ * the block is APPENDED, never overwriting the human content),
478
+ * - the write is idempotent: identical content ⇒ 'skipped' (no churn, lets the
479
+ * daemon re-render cheaply).
480
+ */
481
+ export function writeRenderTarget(rootDir, relPath, block, opts = {}) {
482
+ const fs = opts.fs ?? NODE_FS;
483
+ const abs = resolveInsideRoot(rootDir, relPath);
484
+ const exists = fs.existsSync(abs);
485
+ const existing = exists ? fs.readFileSync(abs, 'utf-8') : null;
486
+ if (exists && existing != null && !hasWyrmRegion(existing) && !opts.force) {
487
+ return { path: abs, action: 'skipped', reason: 'file exists and is not Wyrm-managed (pass force to append)' };
488
+ }
489
+ const next = spliceWyrmRegion(existing, block);
490
+ if (existing != null && next === existing) {
491
+ return { path: abs, action: 'skipped', reason: 'unchanged' };
492
+ }
493
+ fs.mkdirSync(dirname(abs), { recursive: true });
494
+ fs.writeFileSync(abs, next, 'utf-8');
495
+ return { path: abs, action: exists ? 'updated' : 'created' };
496
+ }
497
+ /** Build the full deterministic render plan for a project (no I/O). */
498
+ export function buildRenderPlan(deps, project, stamp) {
499
+ const model = compileRenderModel(deps, project);
500
+ return {
501
+ model,
502
+ memoryMd: renderMemoryMd(model, stamp),
503
+ topics: renderTopicFiles(model, stamp),
504
+ sessionBrief: renderSessionBrief(model, stamp),
505
+ };
506
+ }
507
+ /**
508
+ * Render a project's memory to disk: MEMORY.md + topic files (+ optional client
509
+ * adapter files). All writes are safe (no escape, no clobber of human prose).
510
+ */
511
+ export function renderToDisk(deps, project, stamp, opts = {}) {
512
+ const root = opts.rootDir ?? project.path;
513
+ const topicsDir = opts.topicsDir ?? join('.wyrm', 'memory');
514
+ const plan = buildRenderPlan(deps, project, stamp);
515
+ const writes = [];
516
+ writes.push(writeRenderTarget(root, 'MEMORY.md', plan.memoryMd, opts));
517
+ for (const t of plan.topics) {
518
+ writes.push(writeRenderTarget(root, join(topicsDir, t.filename), t.content, { ...opts, force: true }));
519
+ }
520
+ for (const client of opts.clients ?? []) {
521
+ const target = renderForClient(client, plan.model, stamp);
522
+ writes.push(writeRenderTarget(root, target.relPath, target.block, opts));
523
+ }
524
+ return { plan, writes };
525
+ }
526
+ // ──────────────────────────────────────────────────────────────────────────
527
+ // Daemon re-render on Live Memory events (opt-in).
528
+ // ──────────────────────────────────────────────────────────────────────────
529
+ /**
530
+ * Whether daemon re-render is enabled. Off by default (Article VII) — the
531
+ * operator opts in with WYRM_RENDER_WATCH=1. Kept as a function so tests and the
532
+ * daemon read a fresh value, never a cached one.
533
+ */
534
+ export function renderWatchEnabled(env = process.env) {
535
+ const v = (env.WYRM_RENDER_WATCH ?? '').toLowerCase();
536
+ return v === '1' || v === 'true' || v === 'yes';
537
+ }
538
+ /**
539
+ * Tracks a project's Live Memory cursor and decides whether a re-render is due.
540
+ * Pure given the cursor inputs — the daemon supplies "the latest cursor I saw"
541
+ * and this returns whether it ADVANCED past the last render. No clock, no I/O.
542
+ *
543
+ * The daemon pattern: keep a RenderCursorTracker per project; on each tick poll
544
+ * eventsSince(db, projectId, tracker.last) for the head cursor; call
545
+ * tracker.advance(headCursor) — if it returns true, re-render and the tracker
546
+ * records the new head as rendered.
547
+ */
548
+ export class RenderCursorTracker {
549
+ rendered;
550
+ constructor(initialCursor = 0) { this.rendered = initialCursor; }
551
+ /** The cursor value last rendered. */
552
+ get last() { return this.rendered; }
553
+ /**
554
+ * Given the current head cursor, returns true (and records it) iff it is
555
+ * strictly greater than the last rendered cursor — i.e. new events arrived
556
+ * since the last render. Idempotent: a stale/equal cursor never re-renders.
557
+ */
558
+ advance(headCursor) {
559
+ if (!Number.isFinite(headCursor) || headCursor <= this.rendered)
560
+ return false;
561
+ this.rendered = headCursor;
562
+ return true;
563
+ }
564
+ }
565
+ //# sourceMappingURL=render-target.js.map