@xingular/harness 2.1.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/NOTICE.md +22 -0
  3. package/README.md +67 -36
  4. package/dist/cli.js +6023 -6621
  5. package/dist/terminal-pi.js +19 -24
  6. package/docs/terminal-tui.md +27 -7
  7. package/package.json +2 -2
  8. package/src/accounts/accounts.ts +4 -3
  9. package/src/capabilities/api.ts +1 -1
  10. package/src/cli.ts +7 -3
  11. package/src/core/caller.ts +34 -0
  12. package/src/core/fs.ts +17 -1
  13. package/src/harness.ts +55 -66
  14. package/src/{memory → history}/recall.test.ts +1 -1
  15. package/src/{memory → history}/recall.ts +1 -1
  16. package/src/{memory → history}/trajectory.test.ts +4 -4
  17. package/src/{memory → history}/trajectory.ts +1 -1
  18. package/src/hub/hub.test.ts +4 -5
  19. package/src/hub/hub.ts +2 -2
  20. package/src/index.ts +28 -14
  21. package/src/mcp/connections.ts +1 -1
  22. package/src/mcp/service.ts +1 -1
  23. package/src/pi/proxy.test.ts +68 -0
  24. package/src/pi/runtime.test.ts +61 -0
  25. package/src/pi/runtime.ts +50 -0
  26. package/src/plugins/api.ts +11 -9
  27. package/src/plugins/builtin.ts +22 -40
  28. package/src/plugins/catalogue.test.ts +13 -41
  29. package/src/plugins/catalogue.ts +3 -32
  30. package/src/plugins/discover.ts +2 -1
  31. package/src/plugins/firstParty.test.ts +1 -1
  32. package/src/plugins/hooks.test.ts +21 -71
  33. package/src/plugins/hooks.ts +6 -7
  34. package/src/plugins/identity.ts +16 -2
  35. package/src/plugins/install.test.ts +1 -1
  36. package/src/plugins/install.ts +2 -1
  37. package/src/plugins/inventory.ts +19 -18
  38. package/src/plugins/plugins.ts +38 -2
  39. package/src/prompt/compile.test.ts +82 -193
  40. package/src/prompt/compile.ts +98 -363
  41. package/src/refine/cli.ts +114 -0
  42. package/src/refine/demo.ts +124 -0
  43. package/src/refine/evidence.ts +349 -0
  44. package/src/refine/host.ts +102 -0
  45. package/src/refine/ledger.ts +104 -0
  46. package/src/refine/library.ts +192 -0
  47. package/src/refine/pi.test.ts +90 -0
  48. package/src/refine/pi.ts +218 -0
  49. package/src/refine/prompts.ts +430 -0
  50. package/src/refine/recursion.ts +353 -0
  51. package/src/refine/refine.test.ts +140 -0
  52. package/src/refine/runner.ts +205 -0
  53. package/src/refine/selection.ts +291 -0
  54. package/src/refine/service.ts +194 -0
  55. package/src/refine/trajectory.ts +71 -0
  56. package/src/refine/types.ts +91 -0
  57. package/src/server/methods.ts +137 -123
  58. package/src/server/server.test.ts +37 -30
  59. package/src/sessions/contracts.ts +6 -1
  60. package/src/sessions/conversation.test.ts +2 -27
  61. package/src/sessions/policy.ts +1 -1
  62. package/src/sessions/pricing.ts +3 -1
  63. package/src/sessions/registry.ts +5 -1
  64. package/src/sessions/runStore.ts +3 -1
  65. package/src/sessions/runner.ts +10 -5
  66. package/src/sessions/sessions.test.ts +45 -6
  67. package/src/sessions/sessions.ts +40 -17
  68. package/src/sessions/subagentService.test.ts +22 -1
  69. package/src/sessions/subagentService.ts +14 -1
  70. package/src/sessions/subagents.ts +2 -0
  71. package/src/terminal/cli.ts +24 -48
  72. package/src/terminal/pluginControl.test.ts +7 -8
  73. package/src/terminal/socket.ts +3 -3
  74. package/src/terminal/terminal.test.ts +21 -39
  75. package/src/terminal/terminals.ts +20 -13
  76. package/src/terminal/tui/application.test.ts +58 -0
  77. package/src/terminal/tui/application.ts +10 -4
  78. package/src/terminal/tui/clipboardFiles.test.ts +94 -0
  79. package/src/terminal/tui/clipboardFiles.ts +86 -0
  80. package/src/terminal/tui/piAdapter.ts +7 -4
  81. package/src/terminal/tui/runtime.ts +2 -0
  82. package/src/tools/definitions.ts +49 -132
  83. package/src/tools/dispatch.ts +78 -0
  84. package/src/tools/harnessTools.ts +23 -0
  85. package/src/tools/spawnSubagent.ts +3 -3
  86. package/src/tools/types.ts +1 -1
  87. package/src/tree/components.ts +221 -0
  88. package/src/tree/materialise.ts +122 -0
  89. package/src/tree/seed.ts +123 -0
  90. package/src/tree/store.ts +193 -0
  91. package/src/tree/surface.ts +135 -0
  92. package/src/tree/tree.test.ts +126 -0
  93. package/src/tree/tree.ts +346 -0
  94. package/src/tree/trust.ts +86 -0
  95. package/src/wiki/notes.ts +313 -0
  96. package/src/memory/api.test.ts +0 -82
  97. package/src/memory/api.ts +0 -303
  98. package/src/memory/blocks.test.ts +0 -78
  99. package/src/memory/blocks.ts +0 -408
  100. package/src/memory/caller.ts +0 -69
  101. package/src/memory/curation.test.ts +0 -206
  102. package/src/memory/curation.ts +0 -64
  103. package/src/memory/dispatch.ts +0 -64
  104. package/src/memory/facts.test.ts +0 -41
  105. package/src/memory/facts.ts +0 -248
  106. package/src/memory/graph.ts +0 -103
  107. package/src/memory/health.test.ts +0 -130
  108. package/src/memory/health.ts +0 -74
  109. package/src/memory/maintainer.ts +0 -272
  110. package/src/memory/maintenance.test.ts +0 -192
  111. package/src/memory/maintenance.ts +0 -679
  112. package/src/memory/migrate.ts +0 -79
  113. package/src/memory/operations.ts +0 -150
  114. package/src/memory/proposals.test.ts +0 -168
  115. package/src/memory/proposals.ts +0 -584
  116. package/src/memory/references.ts +0 -77
  117. package/src/memory/runContext.ts +0 -93
  118. package/src/memory/store.test.ts +0 -66
  119. package/src/memory/store.ts +0 -385
  120. package/src/memory/wiki.test.ts +0 -47
  121. package/src/memory/wiki.ts +0 -423
  122. package/src/memory/wikiIndex.test.ts +0 -88
  123. package/src/memory/wikiIndex.ts +0 -117
  124. package/src/plugins/proposal.ts +0 -121
  125. package/src/skills/registry.test.ts +0 -104
  126. package/src/skills/registry.ts +0 -413
  127. package/src/tools/maintenance.ts +0 -29
  128. package/src/tools/memoryTools.ts +0 -22
  129. package/src/tools/pluginPropose.ts +0 -41
  130. package/src/tools/registryWrite.test.ts +0 -101
  131. package/src/tools/registryWrite.ts +0 -182
  132. /package/src/{memory → core}/secrets.ts +0 -0
@@ -1,79 +0,0 @@
1
- // Storage migration to version 3: memory blocks, facts, and the Library.
2
- //
3
- // Version 3 starts from an empty Wiki. Whatever came before is kept whole as a backup and is
4
- // not carried over: a version 2 `wiki/` becomes `wiki.pre-v3/`, and a version 1 `knowledge/`
5
- // becomes `knowledge.pre-v2/`. The new tree is staged in `wiki.migrating/` before anything is
6
- // renamed; on failure the staged tree is removed and the earlier Wiki is left in place.
7
- import { existsSync, renameSync, rmSync } from 'node:fs'
8
- import { join } from 'node:path'
9
-
10
- import type { HarnessEnv } from '../core/env'
11
- import { HarnessError } from '../core/errors'
12
- import { now, readJsonOr, writeJson } from '../core/fs'
13
- import { listProjects } from '../core/projects'
14
- import * as registry from '../skills/registry'
15
- import * as blocks from './blocks'
16
- import { writeIndex } from './proposals'
17
- import * as store from './store'
18
-
19
- export const WIKI_VERSION = 3
20
-
21
- const storagePath = (root: string): string => join(root, 'config/storage.json')
22
-
23
- export function version(root: string): number {
24
- const stored = readJsonOr<{ wikiVersion?: unknown } | null>(storagePath(root), null)?.wikiVersion
25
- if (typeof stored === 'number' && Number.isInteger(stored) && stored >= 0) return stored
26
- const wiki = store.wikiDir(root)
27
- if (store.isDir(join(root, 'knowledge'))) return 1
28
- // A version 2 Wiki whose marker is missing still has its old folders and no blocks.
29
- if (!existsSync(join(wiki, 'blocks')) && (store.isDir(join(wiki, 'docs')) || store.isDir(join(wiki, 'memory')))) return 2
30
- return WIKI_VERSION
31
- }
32
-
33
- const stamp = (): string => now().replace(/[:.]/g, '-')
34
-
35
- /** Moves `from` to `<name>` beside it, or `<name>-<time>` when that backup already exists. */
36
- function backUp(root: string, from: string, name: string): void {
37
- if (!existsSync(from)) return
38
- renameSync(from, existsSync(join(root, name)) ? join(root, `${name}-${stamp()}`) : join(root, name))
39
- }
40
-
41
- /**
42
- * Brings a data root to the current Wiki layout. Safe to call on every start. `agentIds` are
43
- * the agents that get their own blocks. Returns whether a migration happened.
44
- */
45
- export function run(env: HarnessEnv, agentIds: readonly string[] = []): boolean {
46
- const { root } = env
47
- if (version(root) >= WIKI_VERSION) return false
48
- const failed = (detail: string): HarnessError => new HarnessError('migration_failed', `The Wiki was not migrated to version 3; your Wiki is unchanged. ${detail}`)
49
- const staging = join(root, 'wiki.migrating')
50
- rmSync(staging, { recursive: true, force: true })
51
- try {
52
- store.ensureLayoutAt(staging, env.productName)
53
- blocks.ensureUser(staging)
54
- for (const agentId of agentIds) blocks.ensureAgent(staging, agentId)
55
- for (const project of listProjects(root)) blocks.ensureProject(staging, project.slug, project.name)
56
- store.log(staging, `- ${now()} · ${env.productName} · started storage version 3; the earlier Wiki is kept as a backup beside it`)
57
- backUp(root, store.wikiDir(root), 'wiki.pre-v3')
58
- } catch (error) {
59
- rmSync(staging, { recursive: true, force: true })
60
- throw failed(error instanceof Error ? error.message : String(error))
61
- }
62
- renameSync(staging, store.wikiDir(root))
63
- backUp(root, join(root, 'knowledge'), 'knowledge.pre-v2')
64
-
65
- store.ensureLayout(root, env.productName)
66
- for (const project of listProjects(root)) store.ensureProjectNamespace(root, project.slug, project.name)
67
- // Registry items that version 1 enabled for personal agents are enabled for everyone.
68
- const enablement = registry.enablement(root)
69
- if ('personal' in enablement) {
70
- const ids = registry.enablementIds(enablement, registry.GLOBAL_KEY)
71
- for (const id of registry.enablementIds(enablement, 'personal')) if (!ids.includes(id)) ids.push(id)
72
- delete enablement.personal
73
- enablement[registry.GLOBAL_KEY] = ids
74
- writeJson(join(root, 'config/registry-enabled.json'), enablement)
75
- }
76
- writeIndex(store.wikiDir(root))
77
- writeJson(storagePath(root), { wikiVersion: WIKI_VERSION, migratedAt: now() })
78
- return true
79
- }
@@ -1,150 +0,0 @@
1
- // Operations: one applied change to the Wiki, recorded so it can be undone. Undo writes new
2
- // revisions and never erases history. The Inbox holds what was applied, parked, or flagged.
3
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync } from 'node:fs'
4
- import { join } from 'node:path'
5
-
6
- import { noteChange } from '../core/changes'
7
- import { invalid, notFound } from '../core/errors'
8
- import { atomicWrite, newId, now, readJson, safeId, writeJson } from '../core/fs'
9
- import { compareText } from '../core/text'
10
- import * as store from './store'
11
- import * as attachments from '../plugins/attachments'
12
- import { dirname } from 'node:path'
13
-
14
- export interface OperationPage {
15
- pageId: string
16
- path: string
17
- /** Revision holding the content before this operation; `null` when the entry was created. */
18
- beforeRevisionId: string | null
19
- afterRevisionId: string
20
- }
21
-
22
- export interface Operation {
23
- id: string
24
- createdAt: string
25
- trigger: string
26
- pages: OperationPage[]
27
- undone: boolean
28
- protected?: boolean
29
- attachmentsBefore?: attachments.Attachments
30
- attachmentsAfter?: attachments.Attachments
31
- }
32
-
33
- export function operationsDir(wiki: string): string {
34
- return join(wiki, 'operations')
35
- }
36
-
37
- export function listOperations(wiki: string): Operation[] {
38
- let names: string[]
39
- try {
40
- names = readdirSync(operationsDir(wiki))
41
- } catch {
42
- return []
43
- }
44
- const operations: Operation[] = []
45
- for (const name of names) {
46
- try {
47
- const operation = readJson<Operation>(join(operationsDir(wiki), name))
48
- if (operation && typeof operation.id === 'string' && Array.isArray(operation.pages)) operations.push({ ...operation, undone: operation.undone ?? false })
49
- } catch {
50
- // Not an operation record.
51
- }
52
- }
53
- return operations
54
- }
55
-
56
- export function lastOperationAt(wiki: string): string | null {
57
- return listOperations(wiki).map((operation) => operation.createdAt).sort(compareText).at(-1) ?? null
58
- }
59
-
60
- export interface InboxItem {
61
- id: string
62
- kind: string
63
- title: string
64
- detail: string
65
- pageIds: string[]
66
- createdAt: string
67
- /** Operation this item reports, so it can be undone from the Inbox. */
68
- operationId: string | null
69
- /** A parked proposal the user can accept (`kind`, `proposal`, `namespace`). */
70
- payload?: unknown
71
- }
72
-
73
- export function inbox(wiki: string, kind: string, title: string, detail: string, pageIds: string[], operationId: string | null = null, payload?: unknown): void {
74
- const item: InboxItem = { id: newId(), kind, title, detail, pageIds, createdAt: now(), operationId, ...(payload === undefined || payload === null ? {} : { payload }) }
75
- writeJson(join(wiki, 'inbox', `${item.id}.json`), item)
76
- }
77
-
78
- /** Inbox items, newest first. */
79
- export function listInbox(wiki: string): InboxItem[] {
80
- let names: string[]
81
- try {
82
- names = readdirSync(join(wiki, 'inbox'))
83
- } catch {
84
- return []
85
- }
86
- const items: InboxItem[] = []
87
- for (const name of names) {
88
- try {
89
- const item = readJson<InboxItem>(join(wiki, 'inbox', name))
90
- if (item && typeof item.id === 'string' && typeof item.kind === 'string') items.push({ ...item, operationId: item.operationId ?? null })
91
- } catch {
92
- // Folders (`dismissed/`) and unreadable files are not items.
93
- }
94
- }
95
- return items.sort((left, right) => compareText(right.createdAt, left.createdAt))
96
- }
97
-
98
- export function dismissInboxItem(wiki: string, itemId: string): void {
99
- const source = join(wiki, 'inbox', `${safeId(itemId)}.json`)
100
- const target = join(wiki, 'inbox', 'dismissed')
101
- mkdirSync(target, { recursive: true })
102
- try {
103
- renameSync(source, join(target, `${itemId}.json`))
104
- } catch {
105
- throw notFound('Inbox item')
106
- }
107
- }
108
-
109
- /** Reverts an operation through new revisions. Entries it created move to `trash/`. */
110
- export function undo(wiki: string, operationId: string): Operation {
111
- const path = join(operationsDir(wiki), `${safeId(operationId)}.json`)
112
- const operation = readJson<Operation>(path)
113
- if (!operation) throw notFound('Maintenance operation')
114
- if (operation.undone) throw invalid('This operation was already undone.')
115
- if (operation.protected) {
116
- for (const page of operation.pages) {
117
- if (!existsSync(page.path) || readFileSync(page.path, 'utf8') !== store.revisionText(wiki, page.pageId, page.afterRevisionId)) throw invalid('An entry changed after this operation; review it before undoing.')
118
- }
119
- if (operation.attachmentsAfter && JSON.stringify(attachments.load(dirname(wiki))) !== JSON.stringify(operation.attachmentsAfter)) throw invalid('Attachment settings changed after this operation; review them before undoing.')
120
- }
121
- const short = operation.id.slice(0, 8)
122
- if (operation.attachmentsBefore) attachments.save(dirname(wiki), operation.attachmentsBefore)
123
- for (const page of operation.pages) {
124
- let current = ''
125
- try {
126
- current = readFileSync(page.path, 'utf8')
127
- } catch {
128
- current = ''
129
- }
130
- if (page.beforeRevisionId) {
131
- const previous = store.revisionText(wiki, page.pageId, page.beforeRevisionId)
132
- store.snapshot(wiki, page.pageId, current, 'user', `Before undo of ${short}`)
133
- atomicWrite(page.path, previous)
134
- store.snapshot(wiki, page.pageId, previous, 'user', `Undo of update ${short}`)
135
- noteChange({ wiki, id: page.pageId, path: page.path, text: previous, actor: 'user', reason: 'Undid an update' })
136
- } else {
137
- const trash = join(wiki, 'trash')
138
- mkdirSync(trash, { recursive: true })
139
- if (existsSync(page.path)) {
140
- const target = join(trash, `${page.pageId}-${now().replace(/[:.]/g, '-')}.md`)
141
- mkdirSync(join(target, '..'), { recursive: true })
142
- renameSync(page.path, target)
143
- }
144
- }
145
- }
146
- operation.undone = true
147
- writeJson(path, operation)
148
- store.log(wiki, `- ${now()} · user · undo · op \`${operation.id}\``)
149
- return operation
150
- }
@@ -1,168 +0,0 @@
1
- import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
2
- import { join } from 'node:path'
3
- import { expect, test } from 'vitest'
4
-
5
- import { dataRoot } from '../../tests/helpers'
6
- import * as blocks from './blocks'
7
- import { findMemory } from './facts'
8
- import { listInbox, undo } from './operations'
9
- import { accept, apply, captureHashes, parkRegistryProposal, parseProposalSet, quotedIn, type Evidence, type Trigger } from './proposals'
10
- import { readManifest, wikiDir } from './store'
11
-
12
- function setup(): { root: string; wiki: string } {
13
- const root = dataRoot()
14
- return { root, wiki: wikiDir(root) }
15
- }
16
-
17
- const proposal = (id: string, refs: [string, string][]) => ({
18
- id,
19
- title: `Title ${id}`,
20
- type: 'decision',
21
- body: `Body of ${id}.`,
22
- sourceRefs: refs.map(([kind, ref]) => ({ kind, ref, excerpt: 'we decided to use Tauri' }))
23
- })
24
-
25
- const memoryProposal = (id: string, confidence: string) => ({
26
- id,
27
- title: `Memory ${id}`,
28
- kind: 'preference',
29
- body: 'Prefer small commits.',
30
- confidence,
31
- sourceRefs: [{ kind: 'run', ref: 'run-1', excerpt: 'prefer small commits' as string | undefined }]
32
- })
33
-
34
- /** What run-1 recorded: the text excerpts are checked against. */
35
- const ev = (): Evidence => ({ runText: 'User: which shell?\nAssistant: We decided to use Tauri. I prefer small commits.\n' })
36
- const agent = (): Trigger => ({ kind: 'agent', runId: 'run-1', actor: 'Chat' })
37
- const set = (value: unknown) => parseProposalSet(value)
38
- const inboxItems = (wiki: string) => listInbox(wiki).sort((left, right) => (left.kind < right.kind ? -1 : left.kind > right.kind ? 1 : 0))
39
-
40
- test('applies cited pages into the namespace and parks uncited ones', () => {
41
- const { wiki } = setup()
42
- const withKey = { ...proposal('with-key', [['run', 'run-1']]), body: 'token sk-ant-abc123' }
43
- const proposals = set({ pages: [proposal('use-tauri', [['run', 'run-1']]), proposal('rumor', [['run', 'other-run']]), withKey], flags: [{ claim: 'x', kind: 'uncited', reason: 'none' }] })
44
- const outcome = apply(wiki, '/projects/app', agent(), proposals, new Map(), ev())
45
- const operation = outcome.operation!
46
- expect(operation.pages).toHaveLength(1)
47
- expect(outcome.parked).toBe(2)
48
- expect(readFileSync(join(wiki, 'projects/app/use-tauri.md'), 'utf8')).toContain('run:run-1')
49
- expect(readFileSync(join(wiki, 'index.md'), 'utf8')).toContain('[/projects/app](projects/app/index.md) — 1 page')
50
- expect(readFileSync(join(wiki, 'projects/app/index.md'), 'utf8')).toContain('- [Title use-tauri](use-tauri.md) (decision, `use-tauri`)')
51
- expect(readFileSync(join(wiki, 'log.md'), 'utf8')).toContain(operation.id)
52
- const items = inboxItems(wiki)
53
- expect(items.map((item) => item.kind)).toEqual(['applied', 'blocked', 'unresolved', 'unresolved'])
54
- // The parked uncited page can be accepted; the credential one carries no payload.
55
- expect(items.some((item) => item.kind === 'unresolved' && item.payload !== undefined)).toBe(true)
56
- expect(items.every((item) => item.kind !== 'blocked' || item.payload === undefined)).toBe(true)
57
- })
58
-
59
- test('memories apply with evidence and low confidence waits for acceptance', () => {
60
- const { root, wiki } = setup()
61
- const outcome = apply(wiki, '/projects/app', { kind: 'agent', runId: 'run-1', actor: 'Code' }, set({ memories: [memoryProposal('small-commits', 'high'), memoryProposal('maybe', 'low')] }), new Map(), ev())
62
- expect(outcome.operation!.pages).toHaveLength(1)
63
- const saved = findMemory(wiki, 'small-commits')
64
- expect(saved.front.namespace).toBe('/projects/app')
65
- expect(saved.front.sourceIds).toEqual(['run:run-1'])
66
- expect(readManifest(wiki, 'small-commits')[0].actor).toBe('Code')
67
- const parked = listInbox(wiki).find((item) => item.payload !== undefined)!
68
- accept(root, parked.id)
69
- expect(findMemory(wiki, 'maybe').front.sourceIds).toContain('user:accepted')
70
- expect(() => accept(root, parked.id)).toThrow()
71
- })
72
-
73
- test('superseding memory retires the old one', () => {
74
- const { wiki } = setup()
75
- const trigger: Trigger = { kind: 'run', runId: 'run-1' }
76
- apply(wiki, '/library', trigger, set({ memories: [memoryProposal('old', 'high')] }), new Map(), ev())
77
- apply(wiki, '/library', trigger, set({ memories: [{ ...memoryProposal('new', 'high'), supersedes: 'old' }] }), captureHashes(wiki), ev())
78
- expect(findMemory(wiki, 'old').front.status).toBe('superseded')
79
- expect(findMemory(wiki, 'new').front.supersedes).toBe('old')
80
- })
81
-
82
- test('registry proposals always wait and accept writes a revision', () => {
83
- const { root, wiki } = setup()
84
- const id = parkRegistryProposal(wiki, { kind: 'agent', runId: null, actor: 'Chat' }, { kind: 'skill', name: 'Release notes', description: 'Write release notes', body: 'Steps.', reason: '' })
85
- expect(id).toBe('skills/release-notes')
86
- expect(existsSync(join(root, 'registry/skills/release-notes'))).toBe(false)
87
- accept(root, listInbox(wiki).find((item) => item.kind === 'proposed')!.id)
88
- const text = readFileSync(join(root, 'registry/skills/release-notes/SKILL.md'), 'utf8')
89
- expect(text).toContain('Write release notes')
90
- expect(text).toContain('Steps.')
91
- expect(readManifest(wiki, 'skills/release-notes')).toHaveLength(1)
92
- })
93
-
94
- test('never overwrites a page edited during the job', () => {
95
- const { wiki } = setup()
96
- apply(wiki, '/library', agent(), set({ pages: [proposal('p', [['run', 'run-1']])] }), new Map(), ev())
97
- const captured = captureHashes(wiki)
98
- const path = join(wiki, 'library/p.md')
99
- appendFileSync(path, '\nUser edit.\n')
100
- const outcome = apply(wiki, '/library', agent(), set({ pages: [proposal('p', [['run', 'run-1']])] }), captured, ev())
101
- expect(outcome.operation).toBeNull()
102
- expect(readFileSync(path, 'utf8')).toContain('User edit.')
103
- })
104
-
105
- test('undo restores through new revisions', () => {
106
- const { wiki } = setup()
107
- apply(wiki, '/library', agent(), set({ pages: [proposal('p', [['run', 'run-1']])] }), new Map(), ev())
108
- const path = join(wiki, 'library/p.md')
109
- const original = readFileSync(path, 'utf8')
110
- const second = apply(wiki, '/library', agent(), set({ pages: [{ ...proposal('p', [['run', 'run-1']]), body: 'Changed body.' }] }), captureHashes(wiki), ev()).operation!
111
- const revisionsBefore = readManifest(wiki, 'p').length
112
- undo(wiki, second.id)
113
- expect(readFileSync(path, 'utf8')).toBe(original)
114
- expect(readManifest(wiki, 'p').length).toBeGreaterThan(revisionsBefore)
115
- expect(() => undo(wiki, second.id)).toThrow()
116
- })
117
-
118
- test('a run reference counts only with an excerpt found in the run', () => {
119
- expect(quotedIn('We decided to use\n Tauri.', 'decided to use tauri')).toBe(true)
120
- expect(quotedIn('We decided to use Tauri.', 'Tauri')).toBe(false)
121
- expect(quotedIn('We decided to use Tauri.', 'decided to use Electron')).toBe(false)
122
- expect(quotedIn(null, 'decided to use tauri')).toBe(false)
123
- const { wiki } = setup()
124
- const bare = { ...memoryProposal('bare', 'high'), sourceRefs: [{ kind: 'run', ref: 'run-1' }] }
125
- const invented = { ...memoryProposal('invented', 'high'), sourceRefs: [{ kind: 'run', ref: 'run-1', excerpt: 'the user loves large commits' }] }
126
- const outcome = apply(wiki, '/library', agent(), set({ memories: [bare, invented, memoryProposal('quoted', 'high')] }), new Map(), ev())
127
- expect(outcome.parked).toBe(2)
128
- expect(findMemory(wiki, 'quoted').front.id).toBe('quoted')
129
- expect(() => findMemory(wiki, 'bare')).toThrow()
130
- expect(() => findMemory(wiki, 'invented')).toThrow()
131
- })
132
-
133
- test('consolidating a run updates pages and blocks but creates no page', () => {
134
- const { wiki } = setup()
135
- apply(wiki, '/library', agent(), set({ pages: [proposal('shell', [['run', 'run-1']])] }), new Map(), ev())
136
- const user = blocks.ensureUser(wiki)
137
- const notes = blocks.ensureAgent(wiki, 'a1')[1]
138
- const quoted = (block: string, body: string, excerpt: string) => ({ block, body, sourceRefs: [{ kind: 'run', ref: 'run-1', excerpt }] })
139
- const proposals = set({
140
- pages: [{ ...proposal('shell', [['run', 'run-1']]), body: 'We use Tauri.' }, proposal('observed-in-run', [['run', 'run-1']])],
141
- blocks: [quoted(user.id, 'Prefers small commits.', 'I prefer small commits'), quoted(user.id, 'Made up.', 'something never said at all'), quoted(notes.id, 'Not attached.', 'I prefer small commits')]
142
- })
143
- const outcome = apply(wiki, '/library', { kind: 'run', runId: 'run-1' }, proposals, captureHashes(wiki), { ...ev(), blocks: [user.id] })
144
- const operation = outcome.operation!
145
- expect(operation.pages.map((page) => page.pageId)).toEqual(['shell', 'blocks/shared/user'])
146
- expect(outcome.parked).toBe(3)
147
- expect(existsSync(join(wiki, 'library/observed-in-run.md'))).toBe(false)
148
- expect(blocks.find(wiki, user.id).body).toBe('Prefers small commits.')
149
- expect(readManifest(wiki, user.id).at(-1)!.actor).toBe('Sleep-time')
150
- expect(blocks.find(wiki, notes.id).body).toBe('')
151
- // The whole consolidation undoes as one operation.
152
- undo(wiki, operation.id)
153
- expect(blocks.find(wiki, user.id).body).toBe('')
154
- })
155
-
156
- test('idle consolidation only shortens a block', () => {
157
- const { wiki } = setup()
158
- const user = blocks.ensureUser(wiki)
159
- writeFileSync(user.path, `${readFileSync(user.path, 'utf8')}Likes pnpm. Likes pnpm a lot. Uses pnpm everywhere.\n`)
160
- const evidence: Evidence = { blocks: [user.id] }
161
- const rewrite = (body: string) => set({ blocks: [{ block: user.id, body }] })
162
- const longer = apply(wiki, '/library', { kind: 'consolidate' }, rewrite('Likes pnpm. Likes pnpm a lot. Uses pnpm everywhere. Also loves yarn and npm.'), new Map(), evidence)
163
- expect(longer.operation).toBeNull()
164
- expect(longer.parked).toBe(1)
165
- const shorter = apply(wiki, '/library', { kind: 'consolidate' }, rewrite('Uses pnpm everywhere.'), new Map(), evidence)
166
- expect(shorter.operation).not.toBeNull()
167
- expect(blocks.find(wiki, user.id).body).toBe('Uses pnpm everywhere.')
168
- })