@davmash/bygone 0.10.1 → 0.11.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +64 -0
  3. package/bin/bygone.js +18 -1
  4. package/cli/commandSpec.js +22 -2
  5. package/cli/completions.js +30 -8
  6. package/cli/correspondence.js +71 -0
  7. package/cli/dependencies.js +62 -0
  8. package/cli/evidence.js +67 -0
  9. package/cli/present.js +61 -16
  10. package/cli/tour.js +18 -1
  11. package/cli/tourExport.js +1 -1
  12. package/completions/_bygone +15 -3
  13. package/completions/bygone +13 -5
  14. package/completions/bygone.fish +9 -0
  15. package/docs/change-tour-format.md +60 -6
  16. package/docs/correspondence.md +259 -0
  17. package/docs/dependencies.md +94 -0
  18. package/docs/evidence-review.md +163 -0
  19. package/docs/generating-change-tours.md +3 -0
  20. package/docs/present.md +3 -0
  21. package/docs/product-surface.md +2 -1
  22. package/docs/releasing.md +11 -0
  23. package/docs/vscode-integration.md +183 -0
  24. package/media/diff.worker.js +10 -9
  25. package/media/webview.css +1 -1
  26. package/media/webview.js +167 -167
  27. package/out/changeCorrespondence.js +739 -0
  28. package/out/changeTour.js +1366 -967
  29. package/out/dependencyGraph.js +213939 -0
  30. package/out/gitComparison.js +4 -0
  31. package/out/requirements.js +427 -0
  32. package/out/reviewDependencies.js +669 -0
  33. package/out/reviewWorkspace.js +1129 -0
  34. package/out/standalone-main.js +3382 -1341
  35. package/out/tourCoverage.js +1852 -1448
  36. package/package.json +2 -2
  37. package/schemas/change-tour-source.schema.json +48 -3
  38. package/web/export-runtime.json +6 -6
  39. package/web/index.html +13 -0
  40. package/web/presenter.css +81 -0
  41. package/web/web-host.js +23 -20
package/cli/present.js CHANGED
@@ -1,8 +1,9 @@
1
- const { existsSync, mkdirSync, readFile, readFileSync, realpathSync, writeFileSync } = require('fs');
1
+ const { existsSync, mkdirSync, readFile, readFileSync, realpathSync, statSync, writeFileSync } = require('fs');
2
2
  const { createServer } = require('http');
3
3
  const path = require('path');
4
4
  const { spawn } = require('child_process');
5
5
  const { buildChangeTourManifest, parseChangeTourStory } = require('../out/changeTour.js');
6
+ const { loadChangeCorrespondence } = require('../out/changeCorrespondence');
6
7
  const { buildTourAuthoringCoverage, buildTourCoverageReport } = require('../out/tourCoverage.js');
7
8
  const { buildTourWindowTitle } = require('../out/windowTitle.js');
8
9
  const { tokenMatches } = require('./commandSpec.js');
@@ -22,7 +23,7 @@ const MIME_TYPES = new Map([
22
23
  ]);
23
24
 
24
25
  async function startPresentation(args, cwd, packageRoot, options = {}) {
25
- const { headRef, baseRef, tourPath, explicitHeadRef } = parsePresentArgs(args);
26
+ const { headRef, baseRef, tourPath, explicitHeadRef, correspondencePath } = parsePresentArgs(args);
26
27
  const story = !options.ignoreEnvironment && process.env.BYGONE_TOUR_STORY
27
28
  ? parseChangeTourStory(JSON.parse(readFileSync(path.resolve(cwd, process.env.BYGONE_TOUR_STORY), 'utf8')))
28
29
  : undefined;
@@ -48,6 +49,21 @@ async function startPresentation(args, cwd, packageRoot, options = {}) {
48
49
  const contained = relative && relative !== '..' && !relative.startsWith('../') && !path.isAbsolute(relative);
49
50
  manifest.localSource = { repo: pathToFileURL(root).href, tour: contained ? relative : pathToFileURL(documentPath).href };
50
51
  }
52
+ const repositoryRoot = builtManifest.repository?.root || cwd;
53
+ const authoredCorrespondencePath = source?.correspondence
54
+ ? path.resolve(repositoryRoot, source.correspondence)
55
+ : undefined;
56
+ const explicitCorrespondencePath = correspondencePath
57
+ ? path.resolve(cwd, correspondencePath)
58
+ : undefined;
59
+ if (authoredCorrespondencePath && explicitCorrespondencePath
60
+ && !sameActualFile(authoredCorrespondencePath, explicitCorrespondencePath)) {
61
+ throw new Error(`Conflicting correspondence artifacts: source.correspondence (${authoredCorrespondencePath}) and --correspondence (${explicitCorrespondencePath}) must identify the same actual file.`);
62
+ }
63
+ const correspondencePathToLoad = explicitCorrespondencePath || authoredCorrespondencePath;
64
+ const correspondence = correspondencePathToLoad
65
+ ? loadChangeCorrespondence(repositoryRoot, correspondencePathToLoad, { baseOid: manifest.range.mergeBaseOid, headOid: manifest.range.headOid })
66
+ : null;
51
67
  const history = createTourHistory(manifest);
52
68
  const serializedManifest = `${JSON.stringify(manifest, null, 2)}\n`;
53
69
  const outputPath = options.ignoreEnvironment ? undefined : process.env.BYGONE_TOUR_OUTPUT;
@@ -60,13 +76,16 @@ async function startPresentation(args, cwd, packageRoot, options = {}) {
60
76
 
61
77
  const tourWindowTitle = buildTourWindowTitle(manifest, 'Bygone');
62
78
  const presenterIndexPath = path.join(packageRoot, 'web', 'index.html');
63
- let presenterIndexTemplate;
64
79
  // Uploaded documents remain immutable, scoped to this existing repository,
65
80
  // and addressed independently so opening one cannot mutate another tab.
66
81
  const uploadedTours = new Map();
67
82
 
68
83
  const server = createServer((request, response) => {
69
84
  const requestUrl = new URL(request.url || '/', 'http://127.0.0.1');
85
+ if (requestUrl.pathname === '/correspondence.json') {
86
+ if (request.method !== 'GET') return respondJson(response, 405, { error: 'Method not allowed' });
87
+ return respondJson(response, correspondence ? 200 : 404, correspondence || { error: 'No correspondence supplied' });
88
+ }
70
89
  if (requestUrl.pathname === '/tour/open') {
71
90
  if (request.method !== 'POST') return respondJson(response, 405, { error: 'Method not allowed' });
72
91
  if (!isSameOriginLoopbackRequest(request)) return respondJson(response, 403, { error: 'Forbidden' });
@@ -159,18 +178,20 @@ async function startPresentation(args, cwd, packageRoot, options = {}) {
159
178
  return;
160
179
  }
161
180
  if (requestUrl.pathname === '/' || requestUrl.pathname === '/index.html') {
162
- if (!presenterIndexTemplate) {
163
- presenterIndexTemplate = readFileSync(presenterIndexPath, 'utf8');
164
- }
165
- const html = presenterIndexTemplate.replace(
166
- /<title>Bygone Tour<\/title>/,
167
- `<title>${escapeHtml(tourWindowTitle)}</title>`
168
- );
169
- response.writeHead(200, {
170
- 'Content-Type': 'text/html; charset=utf-8',
171
- 'Cache-Control': 'no-store'
181
+ // Read HTML like the runtime assets so a rebuild cannot leave a
182
+ // running presentation serving old controls with new JavaScript.
183
+ readFile(presenterIndexPath, 'utf8', (error, template) => {
184
+ if (error) { respond(response, 500, 'Read failed'); return; }
185
+ const html = template.replace(
186
+ /<title>Bygone Tour<\/title>/,
187
+ `<title>${escapeHtml(tourWindowTitle)}</title>`
188
+ );
189
+ response.writeHead(200, {
190
+ 'Content-Type': 'text/html; charset=utf-8',
191
+ 'Cache-Control': 'no-store'
192
+ });
193
+ response.end(html);
172
194
  });
173
- response.end(html);
174
195
  return;
175
196
  }
176
197
  const targetPath = resolveAssetPath(packageRoot, requestUrl.pathname);
@@ -203,7 +224,7 @@ async function startPresentation(args, cwd, packageRoot, options = {}) {
203
224
  server.close();
204
225
  throw new Error('Could not determine the presentation server address.');
205
226
  }
206
- const url = `http://127.0.0.1:${address.port}/?manifest=/tour.json${options.location ? serializeDocumentFragment(options.location.mode, options.location.focus) : ''}`;
227
+ const url = `http://127.0.0.1:${address.port}/?manifest=/tour.json${correspondence ? '&correspondence=1' : ''}${options.location ? serializeDocumentFragment(options.location.mode, options.location.focus) : ''}`;
207
228
  if (options.announce !== false) {
208
229
  process.stdout.write(`Bygone change tour running at ${url}\n`);
209
230
  process.stdout.write('Press Ctrl+C to stop.\n');
@@ -225,6 +246,7 @@ function parsePresentArgs(args) {
225
246
  let explicitHeadRef;
226
247
  let baseRef;
227
248
  let tourPath;
249
+ let correspondencePath;
228
250
  for (let index = 0; index < args.length; index += 1) {
229
251
  const arg = args[index];
230
252
  if (tokenMatches('base', arg)) {
@@ -234,6 +256,11 @@ function parsePresentArgs(args) {
234
256
  baseRef = args[++index];
235
257
  continue;
236
258
  }
259
+ if (arg === '--correspondence') {
260
+ if (!args[index + 1] || args[index + 1].startsWith('-')) throw new Error('--correspondence requires a file path.');
261
+ correspondencePath = args[++index];
262
+ continue;
263
+ }
237
264
  if (tokenMatches('tour', arg)) {
238
265
  if (!args[index + 1]) throw new Error(`${arg} requires a YAML file.`);
239
266
  tourPath = args[++index];
@@ -248,7 +275,7 @@ function parsePresentArgs(args) {
248
275
  headRef = arg;
249
276
  explicitHeadRef = arg;
250
277
  }
251
- return { headRef, baseRef, tourPath, explicitHeadRef };
278
+ return { headRef, baseRef, tourPath, explicitHeadRef, ...(correspondencePath ? { correspondencePath } : {}) };
252
279
  }
253
280
 
254
281
  function resolveAssetPath(packageRoot, requestPath) {
@@ -315,6 +342,24 @@ function isSameOriginLoopbackRequest(request) {
315
342
  }
316
343
  }
317
344
 
345
+ function sameActualFile(first, second) {
346
+ if (first === second) return true;
347
+ try {
348
+ const left = statSync(first);
349
+ const right = statSync(second);
350
+ if (left.dev !== undefined && left.ino !== undefined
351
+ && left.dev === right.dev && left.ino === right.ino
352
+ && left.ino !== 0 && right.ino !== 0) return true;
353
+ } catch {
354
+ // Fall back to canonical paths on filesystems without usable identity metadata.
355
+ }
356
+ try {
357
+ return realpathSync(first) === realpathSync(second);
358
+ } catch {
359
+ return false;
360
+ }
361
+ }
362
+
318
363
  function escapeHtml(text) {
319
364
  return text
320
365
  .replaceAll('&', '&amp;')
package/cli/tour.js CHANGED
@@ -34,12 +34,21 @@ function runTourCommand(args, cwd, packageRoot, output = process.stdout) {
34
34
  return { action: 'schema' };
35
35
  }
36
36
  if (options.action === 'context') {
37
+ const range = options.dependencies
38
+ ? require('../out/gitComparison.js').resolveBranchReviewRange(cwd, options.headRef, options.baseRef)
39
+ : undefined;
37
40
  const context = buildChangeTourContext(cwd, {
41
+ range,
38
42
  headRef: options.headRef,
39
43
  baseRef: options.baseRef,
44
+ correspondencePath: options.correspondencePath ? path.resolve(cwd, options.correspondencePath) : undefined,
40
45
  maxPatchBytes: options.maxPatchBytes,
41
46
  maxTotalPatchBytes: options.maxTotalPatchBytes
42
47
  });
48
+ if (options.dependencies) {
49
+ const { buildDependencyGraph } = require('../out/dependencyGraph.js');
50
+ context.dependencies = buildDependencyGraph(cwd, { range });
51
+ }
43
52
  return writeJsonResult('change-tour context', context, options.outputPath, cwd, output);
44
53
  }
45
54
 
@@ -147,7 +156,9 @@ function parseContextArgs(args) {
147
156
  let baseRef;
148
157
  let outputPath;
149
158
  let maxPatchBytes;
159
+ let correspondencePath;
150
160
  let maxTotalPatchBytes;
161
+ let dependencies = false;
151
162
  for (let index = 0; index < args.length; index += 1) {
152
163
  const arg = args[index];
153
164
  if (arg === '--base' || arg === '-m' || arg === '--main') {
@@ -160,6 +171,12 @@ function parseContextArgs(args) {
160
171
  outputPath = args[++index];
161
172
  continue;
162
173
  }
174
+ if (arg === '--dependencies') { dependencies = true; continue; }
175
+ if (arg === '--correspondence') {
176
+ if (!args[index + 1] || args[index + 1].startsWith('-')) throw new Error('--correspondence requires a file path.');
177
+ correspondencePath = args[++index];
178
+ continue;
179
+ }
163
180
  if (arg === '--max-patch-bytes') {
164
181
  if (!args[index + 1]) throw new Error(`${arg} requires a positive integer.`);
165
182
  maxPatchBytes = Number.parseInt(args[++index], 10);
@@ -180,7 +197,7 @@ function parseContextArgs(args) {
180
197
  if (headRef) throw new Error('tour context accepts at most one head ref.');
181
198
  headRef = arg;
182
199
  }
183
- return { action: 'context', headRef, baseRef, outputPath, maxPatchBytes, maxTotalPatchBytes };
200
+ return { action: 'context', headRef, baseRef, outputPath, maxPatchBytes, maxTotalPatchBytes, ...(correspondencePath ? { correspondencePath } : {}), ...(dependencies ? { dependencies } : {}) };
184
201
  }
185
202
 
186
203
  function writeJsonResult(label, value, outputPath, cwd, output) {
package/cli/tourExport.js CHANGED
@@ -78,7 +78,7 @@ function materializeTour(sourcePath, profile) {
78
78
  delete manifest.localSource;
79
79
  // A source URL is optional provenance and may carry credentials or local bindings.
80
80
  delete manifest.sourceUrl;
81
- const contentKeys = new Set(['leftContent', 'rightContent', 'content']);
81
+ const contentKeys = new Set(['leftContent', 'rightContent', 'content', 'text']);
82
82
  function pack(value) {
83
83
  if (Array.isArray(value)) return value.map(pack);
84
84
  if (!value || typeof value !== 'object') return value;
@@ -13,7 +13,10 @@ _bygone() {
13
13
  local previous="${words[CURRENT-1]}"
14
14
  local -a root_items review_options present_options legacy_options history_options shells tour_actions
15
15
  root_items=(
16
+ 'evidence:Inspect, import, refresh, or present pinned review evidence'
16
17
  'review:Review a committed branch against its merge base'
18
+ 'dependencies:Compare static JS/TS module dependencies at committed revisions'
19
+ 'correspondence:Import externally produced symbol correspondence evidence'
17
20
  'completion:Print shell completion source'
18
21
  'present:Open an app-hosted tour of a committed branch range'
19
22
  'tour:Validate, compile, or inspect authored change tours'
@@ -41,6 +44,7 @@ _bygone() {
41
44
  '-m:Set the branch-review base ref'
42
45
  '--main:Set the branch-review base ref'
43
46
  '--tour:Use an authored .bygone presentation'
47
+ '--correspondence:Use pinned symbol evidence in present or tour context'
44
48
  '-h:Show command help'
45
49
  '--help:Show command help'
46
50
  )
@@ -78,16 +82,24 @@ _bygone() {
78
82
  elif [[ "$words[3]" == "context" ]]; then
79
83
  if [[ "$previous" == (--base|-m|--main) ]]; then
80
84
  _bygone_git_refs
81
- elif [[ "$previous" == "--output" || "$previous" == "-o" ]]; then
85
+ elif [[ "$previous" == "--output" || "$previous" == "-o" || "$previous" == "--correspondence" ]]; then
82
86
  _files
83
87
  else
84
- compadd -- --base --output -o --max-patch-bytes --max-total-patch-bytes
88
+ compadd -- --base --output -o --max-patch-bytes --max-total-patch-bytes --correspondence --dependencies
85
89
  _bygone_git_refs
86
90
  fi
87
91
  elif [[ "$words[3]" == "coverage" || "$words[3]" == "validate" || "$words[3]" == "compile" ]]; then
88
92
  _files -g '*.bygone' -g '*.bygone.yaml'
89
93
  fi
90
94
  ;;
95
+ dependencies)
96
+ if [[ "$previous" == "--output" || "$previous" == "-o" ]]; then
97
+ _files
98
+ else
99
+ compadd -- --base --json --output -o
100
+ _bygone_git_refs
101
+ fi
102
+ ;;
91
103
  review)
92
104
  if [[ "$previous" == (--base|-m|--main) ]]; then
93
105
  _bygone_git_refs
@@ -97,7 +109,7 @@ _bygone() {
97
109
  fi
98
110
  ;;
99
111
  present)
100
- if [[ "$previous" == (--tour) ]]; then
112
+ if [[ "$previous" == (--tour) || "$previous" == "--correspondence" ]]; then
101
113
  _files -g '*.bygone' -g '*.bygone.yaml'
102
114
  elif [[ "$previous" == (--base|-m|--main) ]]; then
103
115
  _bygone_git_refs
@@ -12,7 +12,7 @@ _bygone() {
12
12
  first="${COMP_WORDS[1]}"
13
13
 
14
14
  if (( COMP_CWORD == 1 )); then
15
- COMPREPLY=( $(compgen -W 'review completion present tour --diff --history --git-diff --branch-diff --test --read-only -C -h --help -v --version' -- "$cur") $(compgen -f -- "$cur") )
15
+ COMPREPLY=( $(compgen -W 'evidence review dependencies correspondence completion present tour --diff --history --git-diff --branch-diff --test --read-only -C -h --help -v --version' -- "$cur") $(compgen -f -- "$cur") )
16
16
  compopt -o filenames 2>/dev/null || true
17
17
  return
18
18
  fi
@@ -28,17 +28,25 @@ _bygone() {
28
28
  refs="$(_bygone_git_refs)"
29
29
  if [[ "$prev" =~ ^(--base|-m|--main)$ ]]; then
30
30
  COMPREPLY=( $(compgen -W "$refs" -- "$cur") )
31
- elif [[ "$prev" == "--output" || "$prev" == "-o" ]]; then
31
+ elif [[ "$prev" == "--output" || "$prev" == "-o" || "$prev" == "--correspondence" ]]; then
32
32
  COMPREPLY=( $(compgen -f -- "$cur") )
33
33
  compopt -o filenames 2>/dev/null || true
34
34
  else
35
- COMPREPLY=( $(compgen -W '--base --output -o --max-patch-bytes --max-total-patch-bytes '"$refs" -- "$cur") )
35
+ COMPREPLY=( $(compgen -W '--base --output -o --max-patch-bytes --max-total-patch-bytes --correspondence --dependencies '"$refs" -- "$cur") )
36
36
  fi
37
37
  else
38
38
  COMPREPLY=( $(compgen -W '--json --output -o' -- "$cur") $(compgen -f -- "$cur") )
39
39
  compopt -o filenames 2>/dev/null || true
40
40
  fi
41
41
  ;;
42
+ dependencies)
43
+ if [[ "$prev" == "--output" || "$prev" == "-o" ]]; then
44
+ COMPREPLY=( $(compgen -f -- "$cur") )
45
+ compopt -o filenames 2>/dev/null || true
46
+ else
47
+ COMPREPLY=( $(compgen -W '--base --json --output -o '"$(_bygone_git_refs)" -- "$cur") )
48
+ fi
49
+ ;;
42
50
  review)
43
51
  refs="$(_bygone_git_refs)"
44
52
  if [[ "$prev" =~ ^(--base|-m|--main)$ ]]; then
@@ -49,13 +57,13 @@ _bygone() {
49
57
  ;;
50
58
  present)
51
59
  refs="$(_bygone_git_refs)"
52
- if [[ "$prev" == "--tour" ]]; then
60
+ if [[ "$prev" == "--tour" || "$prev" == "--correspondence" ]]; then
53
61
  COMPREPLY=( $(compgen -f -- "$cur") )
54
62
  compopt -o filenames 2>/dev/null || true
55
63
  elif [[ "$prev" =~ ^(--base|-m|--main)$ ]]; then
56
64
  COMPREPLY=( $(compgen -W "$refs" -- "$cur") )
57
65
  else
58
- COMPREPLY=( $(compgen -W '--base -m --main --tour -h --help '"$refs" -- "$cur") )
66
+ COMPREPLY=( $(compgen -W '--base -m --main --tour --correspondence -h --help '"$refs" -- "$cur") )
59
67
  fi
60
68
  ;;
61
69
  --branch-diff)
@@ -4,7 +4,10 @@ function __bygone_git_refs
4
4
  printf "%s\n" HEAD INDEX WORKTREE
5
5
  end
6
6
 
7
+ complete -c bygone -n '__fish_use_subcommand' -a evidence -r -d 'Inspect, import, refresh, or present pinned review evidence'
7
8
  complete -c bygone -n '__fish_use_subcommand' -a review -d 'Review a committed branch against its merge base'
9
+ complete -c bygone -n '__fish_use_subcommand' -a dependencies -d 'Compare static JS/TS module dependencies at committed revisions'
10
+ complete -c bygone -n '__fish_use_subcommand' -a correspondence -r -d 'Import externally produced symbol correspondence evidence'
8
11
  complete -c bygone -n '__fish_use_subcommand' -a completion -d 'Print shell completion source'
9
12
  complete -c bygone -n '__fish_use_subcommand' -a present -d 'Open an app-hosted tour of a committed branch range'
10
13
  complete -c bygone -n '__fish_use_subcommand' -a tour -d 'Validate, compile, or inspect authored change tours'
@@ -17,6 +20,11 @@ complete -c bygone -n '__fish_use_subcommand' -l read-only -d 'Open live Explore
17
20
  complete -c bygone -n '__fish_use_subcommand' -s C -r -d 'Run as if Bygone started in this directory'
18
21
  complete -c bygone -n '__fish_use_subcommand' -s h -l help -d 'Show command help'
19
22
  complete -c bygone -n '__fish_use_subcommand' -s v -l version -d 'Show the installed version'
23
+ complete -c bygone -n 'string match -q "*tour context*" -- (commandline -opc)' -l dependencies -d 'Include static module dependency evidence'
24
+ complete -c bygone -n '__fish_seen_subcommand_from dependencies' -l base -r -a '(__bygone_git_refs)' -d 'Set the dependency comparison base'
25
+ complete -c bygone -n '__fish_seen_subcommand_from dependencies' -l json -d 'Print dependency evidence as JSON'
26
+ complete -c bygone -n '__fish_seen_subcommand_from dependencies' -l output -s o -r -d 'Write dependency evidence'
27
+ complete -c bygone -n '__fish_seen_subcommand_from dependencies' -a '(__bygone_git_refs)' -d 'Git ref'
20
28
  complete -c bygone -n '__fish_seen_subcommand_from completion' -a 'zsh bash fish' -d 'Shell'
21
29
  complete -c bygone -n '__fish_seen_subcommand_from tour' -a 'context coverage validate compile export schema' -d 'Tour action'
22
30
  complete -c bygone -n 'string match -q "*tour context*" -- (commandline -opc)' -l base -r -a '(__bygone_git_refs)' -d 'Set the change-context base ref'
@@ -30,6 +38,7 @@ complete -c bygone -n '__fish_seen_subcommand_from review' -l base -s m -l main
30
38
  complete -c bygone -n '__fish_seen_subcommand_from review' -a '(__bygone_git_refs)' -d 'Git ref'
31
39
  complete -c bygone -n '__fish_seen_subcommand_from present' -l base -s m -l main -r -a '(__bygone_git_refs)' -d 'Set the branch-review base ref'
32
40
  complete -c bygone -n '__fish_seen_subcommand_from present' -l tour -r -d 'Use an authored .bygone presentation'
41
+ complete -c bygone -l correspondence -r -d 'Use pinned symbol correspondence evidence'
33
42
  complete -c bygone -n '__fish_seen_subcommand_from present' -a '(__bygone_git_refs)' -d 'Git ref'
34
43
  complete -c bygone -n 'string match -q "*--branch-diff*" -- (commandline -opc)' -s b -l branch -r -a '(__bygone_git_refs)' -d 'Set the branch-review head ref'
35
44
  complete -c bygone -n 'string match -q "*--branch-diff*" -- (commandline -opc)' -l base -s m -l main -r -a '(__bygone_git_refs)' -d 'Set the branch-review base ref'
@@ -8,16 +8,16 @@ suffix, and explicitly supplied files are validated by content rather than
8
8
  rejected by extension.
9
9
 
10
10
  An authored source is Git-backed, not portable: resolving refs and anchors
11
- requires the corresponding local repository and Git objects. Version 2, 4, and 5
11
+ requires the corresponding local repository and Git objects. Version 2, 4, 5, and 6
12
12
  compiled manifests remain bound to that repository so the presenter can load
13
13
  live file history. Version 1 manifests remain readable as legacy portable
14
14
  artifacts, without the mode switcher. Version 4 supports independent authored
15
15
  tours and scene directory overviews; versions 1 and 2 remain accepted without
16
16
  those fields.
17
17
 
18
- ## Version 5 stability and compatibility
18
+ ## Version stability and compatibility
19
19
 
20
- Format version **5** is the current authoring contract. Application and document
20
+ Format version **6** is the current authoring contract. Application and document
21
21
  versions are separate; UI fixes do not require rewriting a tour.
22
22
 
23
23
  | Document version | Reader support | Contract |
@@ -26,8 +26,9 @@ versions are separate; UI fixes do not require rewriting a tour.
26
26
  | 2 | Supported | Repository-bound manifests, real revision stacks, and endpoint walkthroughs |
27
27
  | 3 | Retired | Remove the top-level `review` block, set source `version: 4`, and recompile |
28
28
  | 4 | Supported | Independent authored tours and scene directory overviews; no Review Notes |
29
- | 5 | Use for new sources | Optional opening, conclusion, and chapter previews; explicit overview purpose |
30
- | Greater than 5 | Rejected with an upgrade message | Never interpreted as version 5 |
29
+ | 5 | Supported | Optional opening, conclusion, and chapter previews; explicit overview purpose |
30
+ | 6 | Use for new sources | Optional pinned symbol comparisons in walkthrough stops |
31
+ | Greater than 6 | Rejected with an upgrade message | Never interpreted as version 6 |
31
32
 
32
33
  Version 4 removes the `review` field and the separate Review Notes panel.
33
34
  Concepts, boundaries, tradeoffs, and unresolved questions belong in scene or
@@ -49,7 +50,7 @@ does not prove that the source compiles. Validate against the intended local
49
50
  repository before sharing a tour.
50
51
 
51
52
  Keep authored YAML as the editable source. Compiled JSON is generated evidence,
52
- not a second authoring syntax; v2/v4/v5 manifests retain repository identity and
53
+ not a second authoring syntax; v2/v4/v5/v6 manifests retain repository identity and
53
54
  resolved Git objects. Stable format support does not make missing repositories
54
55
  or pruned objects available, and does not promise byte-identical generated
55
56
  timestamps or presentation layout. Pin full commit IDs in `range` when the
@@ -61,6 +62,59 @@ v3 rejection, removed-field rejection, and rejection of future versions. Run
61
62
  `npm test` before releasing a parser, compiler, or schema change; update those
62
63
  checks with any new contract.
63
64
 
65
+ ## Symbol-focused stops
66
+
67
+ Version 6 adds an optional repository-relative `correspondence` artifact and a
68
+ `symbol` selector on walkthrough steps. Generate the artifact as described in
69
+ [Symbol correspondence evidence](correspondence.md), then use an accepted
70
+ relation ID or one unmatched symbol ID from its inventory:
71
+
72
+ ```yaml
73
+ version: 6
74
+ correspondence: evidence/symbols.json
75
+ # Pin range.base and range.head to the artifact's comparison.
76
+ anchors:
77
+ moved-declaration:
78
+ file: src/moved.py
79
+ revision: head
80
+ contains: "def transfer("
81
+ # Within a walkthrough scene's steps:
82
+ # - id: transfer
83
+ # title: Read the moved declaration
84
+ # body: Compare the complete declaration before and after the move.
85
+ # focus: moved-declaration
86
+ # symbol: { relation: relation-transfer }
87
+ ```
88
+
89
+ The ordinary `focus` anchor remains required and must resolve inside the selected
90
+ declaration on its revision side. It preserves source navigation and existing
91
+ anchor validation. `symbol: { symbol: after-uncertain }` selects one unmatched
92
+ endpoint instead. IDs are unique across the artifact's symbol inventory; matched
93
+ endpoints must use their accepted relation ID. An ambiguous endpoint remains
94
+ one-sided, with no invented origin. Symbol stops cannot also use an image or
95
+ connection.
96
+
97
+ Compilation revalidates the artifact against the exact Git comparison and
98
+ embeds complete declaration text, source spans, revisions, and both full file
99
+ contexts. The source artifact path must stay inside the repository, including
100
+ through symlinks. Stale artifacts and selectors outside the chosen declaration
101
+ are errors. An accepted relation is still an inferred correspondence; behavior
102
+ remains unassessed.
103
+
104
+ The viewer opens the declaration comparison. **Show file context** widens to the
105
+ complete source and destination files, highlights both declarations, and keeps
106
+ the current tour stop. **Return to symbol comparison** restores the slices;
107
+ **Return to tour** restores the authored stop after exploring another file.
108
+ The compiled evidence also works in an offline HTML export without the artifact
109
+ or repository. Export packs repeated text once. Coverage counts only changed
110
+ units intersecting either selected declaration, or the single available endpoint
111
+ for an unmatched symbol; overlapping stops do not multiply coverage.
112
+
113
+ Existing sources and ordinary anchor stops keep their existing behavior. Live
114
+ presentations load the source's artifact into Symbols and Files automatically;
115
+ offline exports retain the selected stops and their context, rather than the
116
+ entire symbol inventory.
117
+
64
118
  ## Opening and conclusion passages
65
119
 
66
120
  Version 5 adds optional `opening` and `conclusion` at the document root and