dsh-code-index 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-code-index",
3
- "version": "0.8.0",
4
- "description": "Project-aware live structural context engine for DSH agents — isolated workspace indexes, task-aware context, and fresh repo maps.",
3
+ "version": "0.9.1",
4
+ "description": "Edit-ready Context Packs for DSH agents: bounded source excerpts, changes, related tests, and inclusion evidence — local-first.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@deepseek-ai/cordis": "^4.0.1",
53
- "@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0"
53
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "@deepseek-ai/cordis": {
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "devDependencies": {
61
61
  "@deepseek-ai/cordis": "4.0.4",
62
- "@deepseek-ai/dsh-tools": "0.1.7-alpha.2",
62
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
63
63
  "@types/node": "^26.2.0",
64
64
  "@types/react": "^18.3.31",
65
65
  "react": "^18.3.1",
@@ -0,0 +1,49 @@
1
+ // Record actual local tool calls; no model, fake card, or invented output.
2
+ import { execFileSync } from 'node:child_process'
3
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
4
+ import path from 'node:path'
5
+ import os from 'node:os'
6
+ import { tools } from '../dist/index.js'
7
+
8
+ const root = await mkdtemp(path.join(os.tmpdir(), 'cix-demo-'))
9
+ const git = (...args) => execFileSync('git', ['-C', root, ...args], { stdio: 'ignore' })
10
+ const frames = []
11
+ const started = Date.now()
12
+ const record = text => frames.push([(Date.now() - started) / 1000, 'o', text.replace(/\n/g, '\r\n')])
13
+ const waitUntil = async seconds => new Promise(resolve => setTimeout(resolve, Math.max(0, seconds * 1000 - (Date.now() - started))))
14
+ try {
15
+ await mkdir(path.join(root, 'src')); await mkdir(path.join(root, 'tests'))
16
+ await writeFile(path.join(root, 'src/config.ts'), 'export function loadConfig(input: string) {\n return input.trim()\n}\nexport function removed() { return 0 }\n')
17
+ await writeFile(path.join(root, 'src/startup.ts'), "import { loadConfig } from './config'\nexport function startup() { return loadConfig(' input ') }\n")
18
+ await writeFile(path.join(root, 'tests/config.spec.ts'), "import { loadConfig } from '../src/config'\ntest('whitespace', () => loadConfig(' input '))\n")
19
+ git('init', '-q'); git('config', 'user.email', 'demo@example.invalid'); git('config', 'user.name', 'Context Pack demo')
20
+ git('add', '.'); git('commit', '-qm', 'demo baseline')
21
+ const tool = tools.find(tool => tool.name === 'code_context')
22
+ record('dsh-code-index v0.9.0 — actual local tool calls\nClean Git repository: loadConfig, startup, related test\n')
23
+ await waitUntil(3)
24
+ record('\n> code_context({ task: "Explain loadConfig", budgetChars: 5000 })\n')
25
+ const first = await tool.execute({ task: 'Explain loadConfig', repoRoot: root }, {})
26
+ record(first + '\n')
27
+ await waitUntil(13)
28
+ record('\x1b[2J\x1b[HExternal edit: modify loadConfig, remove removed, add betaCaller\n')
29
+ await writeFile(path.join(root, 'src/config.ts'), "export function loadConfig(input: string) {\n return input.toUpperCase()\n}\nexport function betaCaller() { return loadConfig('beta') }\n")
30
+ record('\n> code_context({ task: "Review current changes", outputFormat: "pack" })\n')
31
+ const pack = await tool.execute({ task: 'Review current changes', repoRoot: root, outputFormat: 'pack' }, {})
32
+ record(tool.output.render({}, pack).map(block => block.text ?? '').join('\n') + '\n')
33
+ await waitUntil(23)
34
+ record('\x1b[2J\x1b[H> code_context({ task: "Explain loadConfig", budgetChars: 500 })\n')
35
+ const bounded = await tool.execute({ task: 'Explain loadConfig', repoRoot: root, budgetChars: 500 }, {})
36
+ record(bounded + '\n\nActual text length: ' + bounded.length + ' / 500\n')
37
+ await waitUntil(30)
38
+ record('\nDemo complete. No publishing, model calls, or external API key.\n')
39
+ await mkdir(new URL('../assets/', import.meta.url), { recursive: true })
40
+ await writeFile(new URL('../assets/context-pack-demo.cast', import.meta.url),
41
+ [JSON.stringify({ version: 2, width: 110, height: 36, timestamp: Math.floor(started / 1000), title: 'Edit-ready Context Packs — real local calls' }), ...frames.map(frame => JSON.stringify(frame))].join('\n') + '\n')
42
+ const replay = { duration: frames.at(-1)[0], frames }
43
+ const html = `<!doctype html><html lang="en"><meta charset="utf-8"><title>Context Pack · 30-second local demo</title>
44
+ <style>body{background:#10151e;color:#e2e8f0;font:15px system-ui;max-width:1000px;margin:30px auto;padding:0 20px}button{padding:8px 16px;margin-right:12px}pre{background:#19212d;padding:20px;white-space:pre-wrap;overflow:auto;height:65vh;font:13px/1.5 monospace}small{color:#a8b3c5}</style>
45
+ <h1>Edit-ready Context Packs</h1><p>Recorded real local calls · no DSH Web Card · no model/API request</p><button id="play">Replay 30 seconds</button><span id="time"></span><pre id="terminal"></pre><small>Reproduce with node scripts/context-pack-demo.mjs after building. Output is recorded, not live.</small>
46
+ <script>const recording=${JSON.stringify(replay).replace(/</g, '\\u003c')};let timer;function run(){clearInterval(timer);let start=performance.now(),next=0;terminal.textContent='';timer=setInterval(()=>{let elapsed=(performance.now()-start)/1000;while(next<recording.frames.length&&recording.frames[next][0]<=elapsed){let text=recording.frames[next++][2];if(text.includes('\\x1b[2J\\x1b[H')){terminal.textContent='';text=text.replace('\\x1b[2J\\x1b[H','')}terminal.textContent+=text;terminal.scrollTop=terminal.scrollHeight}time.textContent=Math.min(30,elapsed).toFixed(1)+' / 30 s';if(next===recording.frames.length)clearInterval(timer)},100)}play.onclick=run;run();</script></html>`
47
+ await writeFile(new URL('../assets/context-pack-demo.html', import.meta.url), html)
48
+ console.log(`Recorded ${replay.duration.toFixed(1)}s demo; text ${bounded.length}/500; pack ${JSON.stringify(pack).length}/5000.`)
49
+ } finally { await rm(root, { recursive: true, force: true }) }
@@ -0,0 +1,88 @@
1
+ """Cut actual DSH screen footage; never generate or replace UI content.
2
+
3
+ Usage: python scripts/process-desktop-demo.py FFMPEG RAW_MP4 EDIT_JSON
4
+ EDIT_JSON: {"segments": [{"start": 0, "duration": 5}, ...]}
5
+ Segments must be chronological and within the original recording.
6
+ """
7
+ import hashlib
8
+ import json
9
+ from pathlib import Path
10
+ import subprocess
11
+ import sys
12
+
13
+
14
+ def sha256(path):
15
+ return hashlib.file_digest(path.open("rb"), "sha256").hexdigest()
16
+
17
+
18
+ def main():
19
+ ffmpeg, raw, edit_path = sys.argv[1:]
20
+ raw, edit_path = Path(raw), Path(edit_path)
21
+ edit = json.loads(edit_path.read_text(encoding="utf-8"))
22
+ segments = edit["segments"]
23
+ duration = sum(s["duration"] for s in segments)
24
+ assert 30 <= duration <= 60, duration
25
+ end = 0
26
+ for s in segments:
27
+ assert s["start"] >= end and s["duration"] > 0
28
+ end = s["start"] + s["duration"]
29
+ root = Path(__file__).resolve().parents[1]
30
+ assets = root / "assets"
31
+ video = assets / "desktop-agent-v090.mp4"
32
+ gif = assets / "desktop-agent-v090.gif"
33
+ poster = assets / "desktop-agent-v090.png"
34
+ filters = []
35
+ for i, s in enumerate(segments):
36
+ filters.append(
37
+ f"[0:v]trim=start={s['start']}:duration={s['duration']},"
38
+ f"setpts=PTS-STARTPTS[v{i}]"
39
+ )
40
+ inputs = "".join(f"[v{i}]" for i in range(len(segments)))
41
+ filters.append(
42
+ f"{inputs}concat=n={len(segments)}:v=1:a=0,"
43
+ "scale=1280:-2:flags=lanczos,setsar=1,fps=15[out]"
44
+ )
45
+ subprocess.run([
46
+ ffmpeg, "-hide_banner", "-loglevel", "error", "-y", "-i", str(raw), "-filter_complex", ";".join(filters),
47
+ "-map", "[out]", "-an", "-c:v", "libx264", "-crf", "20",
48
+ "-preset", "medium", "-pix_fmt", "yuv420p", "-movflags", "+faststart",
49
+ "-map_metadata", "-1", str(video),
50
+ ], check=True)
51
+ subprocess.run([
52
+ ffmpeg, "-hide_banner", "-loglevel", "error", "-y", "-i", str(video), "-vf",
53
+ "fps=2,split[a][b];[a]palettegen=stats_mode=diff[p];"
54
+ "[b][p]paletteuse=dither=bayer:bayer_scale=5", "-loop", "0", str(gif),
55
+ ], check=True)
56
+ subprocess.run([
57
+ ffmpeg, "-hide_banner", "-loglevel", "error", "-y", "-ss", str(edit.get("posterAt", 12)), "-i", str(video),
58
+ "-frames:v", "1", "-update", "1", str(poster),
59
+ ], check=True)
60
+ manifest = {
61
+ "method": "Actual DSH Desktop screen recording; Agent task submitted through UI",
62
+ "recordedDate": "2026-10-08",
63
+ "pluginVersion": "0.9.0 (visible installed/running in Desktop UI)",
64
+ "rawSha256": sha256(raw),
65
+ "durationSeconds": duration,
66
+ "segments": segments,
67
+ "posterAt": edit.get("posterAt", 12),
68
+ "edits": [
69
+ "Chronological cuts remove setup, waiting and redundant holds",
70
+ "No UI, tool output or Agent response generated or replaced",
71
+ "Silent video resized to 1280 pixels wide, 15 fps; GIF sampled at 2 fps",
72
+ "Elapsed wall time is not represented by edited playback duration",
73
+ ],
74
+ "captureOperation": "User started/stopped Windows Snipping Tool; assistant operated DSH",
75
+ "toolResult": {"usedChars": 2347, "budgetChars": 5000},
76
+ "outputs": {
77
+ p.name: {"sha256": sha256(p), "bytes": p.stat().st_size}
78
+ for p in [video, gif, poster]
79
+ },
80
+ }
81
+ (assets / "desktop-agent-v090.provenance.json").write_bytes(
82
+ (json.dumps(manifest, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
83
+ )
84
+ print(json.dumps(manifest, ensure_ascii=False, indent=2))
85
+
86
+
87
+ if __name__ == "__main__":
88
+ main()
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { execFileSync } from 'node:child_process'
3
- import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
3
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
4
4
  import os from 'node:os'
5
5
  import path from 'node:path'
6
6
  import { fileURLToPath, pathToFileURL } from 'node:url'
@@ -75,6 +75,7 @@ const expectedFiles = [
75
75
  'LICENSE',
76
76
  'cordis.patch.yml',
77
77
  'dist/index.js',
78
+ 'dist/index.d.ts',
78
79
  'dist/client.js',
79
80
  'scripts/fix-wsl-links.mjs',
80
81
  'scripts/release-smoke.mjs',
@@ -94,6 +95,8 @@ try {
94
95
  assert.equal(installedPackage.exports['.'].default, './dist/index.js')
95
96
  assert.match(installedPackage.engines.node, />=22/)
96
97
  assert.equal(installedPackage.license, 'MIT')
98
+ const installedTools = JSON.parse(readFileSync(path.join(smokeDir, 'node_modules', '@deepseek-ai', 'dsh-tools', 'package.json'), 'utf8'))
99
+ assert.equal(installedTools.version, '0.2.0-rc.2', 'fresh consumers must resolve the verified matching RC')
97
100
 
98
101
  const plugin = await import(
99
102
  pathToFileURL(path.join(smokeDir, 'node_modules', packageJson.name, 'dist', 'index.js')).href,
@@ -146,8 +149,38 @@ try {
146
149
  }),
147
150
  /Task context|Primary symbols:/,
148
151
  )
152
+ const contextPack = await invoke('code_context', {
153
+ task: 'Explain buildIndex', repoRoot: root, budgetChars: 5000, outputFormat: 'pack',
154
+ })
155
+ assert.equal(contextPack.version, 1)
156
+ assert.ok(contextPack.items.some(item => item.kind === 'primary' && item.name === 'buildIndex'))
157
+ assert.ok(contextPack.items.some(item => item.code !== undefined))
158
+ assert.ok(JSON.stringify(contextPack).length <= 5000)
149
159
  assert.match(await invoke('code_health', { repoRoot: root }), /repo health/)
150
160
 
161
+ // Issue #2 must also be fixed in the installed tarball, not just TS sources.
162
+ const whitelistRoot = path.join(smokeDir, 'whitelist-repo')
163
+ for (const dir of ['.git', 'src', 'extensions/keep', 'extensions/other']) {
164
+ mkdirSync(path.join(whitelistRoot, dir), { recursive: true })
165
+ }
166
+ writeFileSync(path.join(whitelistRoot, '.gitignore'), '/extensions/*\n!/extensions/keep/\n!/extensions/keep/**\n')
167
+ writeFileSync(path.join(whitelistRoot, 'src/control.ts'), 'export function CONTROL_MARKER_fn() { return 3 }\n')
168
+ writeFileSync(path.join(whitelistRoot, 'extensions/keep/kept.ts'), 'export function KEPT_MARKER_fn() { return 1 }\n')
169
+ writeFileSync(path.join(whitelistRoot, 'extensions/other/skip.ts'), 'export function SKIPPED_fn() { return 2 }\n')
170
+ assert.match(await invoke('code_index', { action: 'build', repoRoot: whitelistRoot }), /files indexed: 2/)
171
+ const keptHits = await invoke('code_search', { query: 'KEPT_MARKER_fn', repoRoot: whitelistRoot })
172
+ assert.equal(keptHits[0]?.file, 'extensions/keep/kept.ts')
173
+ assert.deepEqual(await invoke('code_search', { query: 'SKIPPED_fn', repoRoot: whitelistRoot }), [])
174
+ const keptPack = await invoke('code_context', {
175
+ task: 'Explain KEPT_MARKER_fn', repoRoot: whitelistRoot, outputFormat: 'pack',
176
+ })
177
+ assert.ok(keptPack.items.some(item => item.name === 'KEPT_MARKER_fn' && item.code?.includes('return 1')))
178
+ const freshSource = path.join(whitelistRoot, 'extensions/keep/fresh.ts')
179
+ writeFileSync(freshSource, 'export function FRESH_MARKER_fn() { return 91 }\n')
180
+ assert.equal((await invoke('code_search', { query: 'FRESH_MARKER_fn', repoRoot: whitelistRoot }))[0]?.name, 'FRESH_MARKER_fn')
181
+ rmSync(freshSource)
182
+ assert.deepEqual(await invoke('code_search', { query: 'FRESH_MARKER_fn', repoRoot: whitelistRoot }), [])
183
+
151
184
  for (const dispose of disposers.reverse()) dispose()
152
185
 
153
186
  const compactRegistered = []
@@ -0,0 +1,87 @@
1
+ """Render a 42s terminal replay from audited Desktop tool records (Pillow)."""
2
+ import hashlib
3
+ import json
4
+ import textwrap
5
+ from pathlib import Path
6
+ from PIL import Image, ImageDraw, ImageFont
7
+
8
+ ROOT = Path(__file__).resolve().parents[1]
9
+ source = ROOT / 'assets/desktop-v0.9-tool-evidence.json'
10
+ evidence = json.loads(source.read_text(encoding='utf-8'))
11
+ runs = {r['callSeq']: r for r in evidence['runs']}
12
+ clean, changed, small = (runs[k] for k in (47, 60, 73))
13
+ for run in (clean, changed, small):
14
+ assert len(run['renderedText'].encode('utf-16-le')) // 2 == run['actualTextChars']
15
+ assert run['actualTextChars'] <= run['budgetChars']
16
+
17
+ def block(run, prefix):
18
+ return next(p for p in run['renderedText'].split('\n\n') if p.startswith(prefix))
19
+
20
+ def call(run):
21
+ args = {k: v for k, v in run['arguments'].items() if k != 'repoRoot'}
22
+ return 'code_context(' + json.dumps(args, ensure_ascii=False, indent=2) + ')'
23
+
24
+ scenes = [
25
+ ('01 Task -> actual Agent tool call', '任务描述 -> 真实 Agent 工具调用',
26
+ call(clean) + '\n\nCaptured: 2026-10-05 | tool/call #47 -> tool/result #48\nTiny Git fixture | repoRoot omitted from display\n\nTask wording did not name loadConfig.\nThe Agent requested code_context to locate useful context.'),
27
+ ('02 Primary source + inclusion reason', '关键源码、准确行号和入选理由',
28
+ block(clean, 'primary:') + '\n\n' + clean['renderedText'].splitlines()[-1]),
29
+ ('03 Caller + relationship provenance', '相关调用,以及关系证据来源',
30
+ block(clean, 'caller:') + '\n\n' + block(clean, 'relationship: startup')),
31
+ ('04 Likely test + actual test source', '相关测试线索,以及测试源码',
32
+ block(clean, 'test:') + '\n\n' + block(clean, 'relationship: module') + '\n\nLikely tests are retrieval leads, not coverage proof.'),
33
+ ('05 External edit -> Git change context', '外部编辑后,查询看到 Git 变更',
34
+ call(changed) + '\n\n' + block(changed, 'primary: loadConfig') + '\n\n' + block(changed, 'primary: betaCaller')),
35
+ ('06 Deleted source + honest budget fallback', '删除项保留基线源码,小预算明确降级',
36
+ block(changed, 'change: removed') + '\n\n' + block(small, 'primary:') + '\n\nGaps: budget omitted some evidence\nBudget: 327 / 500 chars (trimmed low-priority context)'),
37
+ ('07 Context ready for the next code step', 'Agent 已获得可继续处理任务的上下文',
38
+ 'Review guide / 展示说明(不是 Agent 回复原文)\n\nloadConfig -> startup -> tests/config.spec.ts\nSource ranges + reasons + relationship provenance\nCurrent changes: modified / added / deleted\nClean result: 1294 / 5000 chars\nChange result: 2326 / 5000 chars\n\nNo edit or test-success claim in this replay.\n此回放展示上下文获取,不宣称已修复 Bug 或测试通过。\n\nTry a task in your repo. What context did it miss?\n欢迎试用:它漏掉了什么重要上下文?'),
39
+ ]
40
+
41
+ W, H = 1280, 900
42
+ mono_path = Path('C:/Windows/Fonts/consola.ttf')
43
+ cjk_path = Path('C:/Windows/Fonts/msyh.ttc')
44
+ mono = ImageFont.truetype(str(mono_path), 21)
45
+ ui = ImageFont.truetype(str(cjk_path), 23)
46
+ small_ui = ImageFont.truetype(str(cjk_path), 18)
47
+ title_font = ImageFont.truetype(str(mono_path), 28)
48
+ frames = []
49
+ for i, (title, subtitle, body) in enumerate(scenes):
50
+ for second in (0,):
51
+ im = Image.new('RGB', (W, H), '#0d1420')
52
+ d = ImageDraw.Draw(im)
53
+ d.text((34, 22), 'dsh-code-index v0.9.0 | Edit-ready Context Packs', font=title_font, fill='#f2f6fc')
54
+ d.text((34, 68), '真实 Agent 记录的终端回放 / Replay of real Agent logs', font=ui, fill='#5de4c7')
55
+ d.text((34, 107), 'Not a desktop screen recording | 非桌面录屏 | Edited pacing / 节奏经过剪辑', font=small_ui, fill='#a6b4ca')
56
+ d.rounded_rectangle((24, 151, W-24, H-71), radius=12, fill='#172235', outline='#33445e')
57
+ d.text((44, 169), title, font=title_font, fill='#86c5ff')
58
+ d.text((44, 211), subtitle, font=ui, fill='#e4ecf7')
59
+ y = 265
60
+ display_lines = [part for line in body.splitlines() for part in (textwrap.wrap(line, width=94, break_long_words=False, break_on_hyphens=False) or [''])]
61
+ for line in display_lines:
62
+ font = ui if any(ord(c) > 0x3000 for c in line) else mono
63
+ assert d.textlength(line, font=font) < W-88, f'Line too wide: {line}'
64
+ assert y + 28 < H-77, f'Scene {i+1} too tall'
65
+ color = '#5de4c7' if line.startswith(('Reason:', 'Relationship provenance:', 'Budget:', 'Change:')) else '#e4ecf7'
66
+ d.text((44, y), line, font=font, fill=color)
67
+ y += 27
68
+ elapsed = i*6 + second
69
+ d.text((34, H-57), 'DSH Desktop 0.2.0-rc.2 | Evidence: 2026-10-05 | Excerpts / 原文节选', font=small_ui, fill='#a6b4ca')
70
+ d.text((1090, H-57), f'{elapsed:02d} / 42 s', font=mono, fill='#a6b4ca')
71
+ d.rectangle((0, H-8, int(W*(elapsed+1)/42), H), fill='#5de4c7')
72
+ frames.append(im)
73
+
74
+ assets = ROOT / 'assets'
75
+ frames[1].save(assets / 'agent-context-demo.png')
76
+ frames[0].save(assets / 'agent-context-demo.gif', save_all=True, append_images=frames[1:], duration=6000, loop=0, optimize=True)
77
+ preview_dir = ROOT / '.promotion-work/demo-frames'
78
+ preview_dir.mkdir(parents=True, exist_ok=True)
79
+ for i in range(7):
80
+ frames[i].save(preview_dir / f'{i+1}.png')
81
+ cast_frames = [[i*6, 'o', '\x1b[2J\x1b[H' + title + '\r\n' + subtitle + '\r\n\r\n' + body.replace('\n', '\r\n')] for i, (title, subtitle, body) in enumerate(scenes)]
82
+ cast_frames.append([42, 'o', '\r\nReplay complete / 回放结束\r\n'])
83
+ header = {'version': 2, 'width': 110, 'height': 30, 'title': 'Real Desktop Agent log replay (not screen capture)', 'duration': 42}
84
+ (assets / 'agent-context-demo.cast').write_bytes(('\n'.join(json.dumps(x, ensure_ascii=False) for x in [header, *cast_frames]) + '\n').encode('utf-8'))
85
+ manifest = {'format': 'Terminal-style replay of actual persisted Agent records; not a screen capture', 'durationSeconds': 42, 'source': source.name, 'sourceSha256': hashlib.sha256(source.read_bytes()).hexdigest(), 'callSeqs': [47, 60, 73], 'edits': ['7 panels with 6-second holds; timing is editorial, not measured latency', 'repoRoot omitted from displayed arguments', 'Verbatim source/reason/provenance excerpts; not all returned items shown', 'First/last panel commentary is authored narration, not Agent response'], 'sceneTitles': [s[0] for s in scenes]}
86
+ (assets / 'agent-context-demo.provenance.json').write_bytes((json.dumps(manifest, ensure_ascii=False, indent=2) + '\n').encode('utf-8'))
87
+ print('Rendered 42-second bilingual replay from real Desktop callSeqs 47, 60, 73.')