@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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.1
4
+
5
+ - Added pinned evidence reviews in the CLI and VS Code, with explicit refresh, source validation, and return-to-review navigation
6
+ - Connected evidence ranges to synchronized working-file and symbol editing while preserving historical review snapshots
7
+ - Added dependency-guided review navigation and improved modern TypeScript syntax coverage, metadata resolution, and report validation
8
+
9
+ ## 0.11.0
10
+
11
+ - Added experimental JS/TS and Python symbol evidence with declaration diffs, symbol trees, analysis diagnostics, and shared Files/Symbols navigation
12
+ - Added pinned symbol tour stops and full-file context in live presentations and portable exports
13
+ - Added the local Strand declaration matcher for JS/TS, Python, Java, Go, Rust, C#, Ruby, PHP, C, Lua, and experimental Mojo, with bounded analysis and explicit ambiguity reporting
14
+ - Kept Java and RefactoringMiner optional: exact-only generation records partial coverage, and Strand provides local inference without a JVM
15
+ - Added opt-in VS Code active-editor following with a live HEAD-to-buffer diff, Pin/Resume/Refresh controls, and safe handoffs to history and manual comparison
16
+ - Fixed following for macOS directory aliases and files at the 5,000-line limit, with regression coverage on current and minimum-supported VS Code
17
+ - Hardened correspondence artifact validation, parser budgets, and symbol owner matching
18
+ - Updated Electron, DOMPurify, fast-uri, and packaging dependencies to resolve security advisories
19
+
20
+ macOS desktop downloads are unsigned and not notarized. If Gatekeeper reports
21
+ the app as damaged, run `xattr -cr /Applications/Bygone.app`, then reopen it.
22
+
23
+ ## 0.10.2
24
+
25
+ - Prioritized matching named lines before brackets to preserve block correspondences across nesting and indentation changes
26
+ - Aligned changed indentation highlights to the start of each line while preserving highlights on edited text
27
+
3
28
  ## 0.10.1
4
29
 
5
30
  - Fixed desktop tours showing unstyled HTML after temporary asset cleanup during long-running app sessions
package/README.md CHANGED
@@ -214,6 +214,8 @@ Contributors should consult the internal [product surface overview](./docs/produ
214
214
 
215
215
  `bygone tour context [<head>] --base <base>` produces a provider-neutral change dossier with bounded patches, changed ranges, file roles, symbol hints, commits, renames, and explicit binary omissions for an LLM to consume.
216
216
 
217
+ `bygone dependencies [<head>] --base <base>` compares static JS/TS module dependencies, labels initialization versus deferred use, and lists unchanged consumers of changed modules. Add `--json` for exact source evidence, or use `bygone tour context --base <base> --dependencies` to include it in a change dossier. See [dependency evidence](docs/dependencies.md) for resolution rules and coverage limits. These analysis commands run through the npm/source Node CLI.
218
+
217
219
  The desktop app also includes `Help -> Install Command Line Tools...` for non-npm installs. Homebrew cask installs can provide the `bygone` command automatically.
218
220
 
219
221
  ### macOS says the app is damaged
@@ -352,6 +354,10 @@ The wrapper scripts are:
352
354
 
353
355
  ## Commands
354
356
 
357
+ - `Bygone: Follow Active Editor`
358
+ - `Bygone: Pin Following File`
359
+ - `Bygone: Resume Following`
360
+ - `Bygone: Refresh Following Comparison`
355
361
  - `Bygone: Compare Active File With…`
356
362
  - `Bygone: Compare Selected Files`
357
363
  - `Bygone: Compare with Selected`
@@ -370,6 +376,64 @@ panes edit through VS Code documents, so changes participate in dirty state,
370
376
  undo, save, and external-document updates. Historical and non-local inputs are
371
377
  read-only. Desktop hand-offs are unavailable in remote extension hosts.
372
378
 
379
+ ## Follow the active VS Code editor
380
+
381
+ Run **Bygone: Follow Active Editor** to open one comparison beside your source
382
+ editor without moving keyboard focus. Switching source files updates that pane.
383
+ The left side is HEAD; the right side is the VS Code document, including unsaved
384
+ edits. Edit either the source pane or Bygone's working pane: both update the same
385
+ document. Save, Undo and Redo in Bygone use that document's save and edit history.
386
+ Typing neither saves the file nor stages it.
387
+
388
+ Source Control offers **Open Staged Changes in Bygone** (HEAD → index) and
389
+ **Open Working Changes in Bygone** (index → working buffer), in the file's inline
390
+ and context menus. Staged views exclude unsaved edits and are read-only. Native
391
+ stage, unstage, discard, commit and checkout operations refresh live comparisons.
392
+ The selected policy stays in place when a file leaves its SCM group; an empty
393
+ comparison shows that there are now no changes. Enable `bygone.followScmDiffs`
394
+ to follow supported native Git diff tabs during a following session. This does
395
+ not change the SCM tree's default click behavior.
396
+
397
+ Use **Bygone: Compare Symbol in Bygone**, or enable `bygone.followSymbol` to follow
398
+ Outline, Go to Symbol and cursor selections. Symbol focus preserves the selected
399
+ comparison and uses the existing Strand matching engine. JavaScript and TypeScript
400
+ are supported within the selected file pair, including Git-reported renames.
401
+ Dirty-buffer selections in staged views require a verified mapping to the index.
402
+ Unsupported or ambiguous selections show a reason and retain file context or a
403
+ known one-sided symbol. **Show File Context** returns to the whole comparison;
404
+ **Reveal Source** opens the working document or the exact read-only snapshot.
405
+
406
+ Working symbol slices are editable. Edits before a declaration move its tracked
407
+ range; temporary syntax errors keep its editing range usable. Replacing or deleting
408
+ the declaration externally returns to file context. If a concurrent edit makes a
409
+ pending write stale, Bygone preserves the attempted text in an untitled recovery
410
+ document instead of overwriting the source. Pending drafts are also recovered
411
+ when the panel closes or the extension starts again after a reload.
412
+
413
+ **Pin** keeps the target, baseline and symbol focus. A pinned working endpoint
414
+ continues to reflect edits; a pinned staged view holds both snapshots. Git changes
415
+ show a notice until **Resume Following** resolves current endpoints. **Refresh**
416
+ updates live comparisons; pinned snapshots remain fixed. Source navigation within
417
+ the selected file preserves SCM policy; a different ordinary file selects HEAD →
418
+ working. **View File History** and **Manual Compare…** pause following and open
419
+ separate views. A restored following panel starts paused.
420
+
421
+ Labels identify the repository, paths, comparison policy and saved/unsaved state.
422
+ New/deleted files and unborn HEAD use explicit absent endpoints. Following supports
423
+ trusted local Git worktrees, including linked worktrees and multiple repositories.
424
+ Each selected endpoint must be text, at most 128 KiB and 5,000 lines; Git snapshots
425
+ must be UTF-8. Conflicts, unsupported diff inputs, symlinks, binary, remote,
426
+ virtual and notebook inputs show an explicit unavailable state. Git failures clear
427
+ the previous comparison. Arbitrary cross-file symbol moves and hunk staging inside
428
+ Bygone are outside this integration.
429
+
430
+ Use **Bygone: Open Evidence Review** to inspect pinned requirement evidence, edit
431
+ the working counterpart, and return to the original review. See the
432
+ [evidence workflow and PR Reviewer bridge](docs/evidence-review.md).
433
+
434
+ See the [integration contract and verification record](docs/vscode-integration.md)
435
+ for snapshot/edit safeguards, analysis budgets and reproducible Extension Host tests.
436
+
373
437
  ## Limitations
374
438
 
375
439
  - Bygone is intentionally diff-focused; merge tooling is not exposed as a product feature.
package/bin/bygone.js CHANGED
@@ -40,7 +40,24 @@ if (tokenMatches('completion', args[0])) {
40
40
  process.exit(0);
41
41
  }
42
42
 
43
- if (tokenMatches('tourCommand', args[0])) {
43
+ if (args[0] === 'evidence') {
44
+ try { require('../cli/evidence').runEvidenceCommand(args.slice(1), cliCwd); }
45
+ catch (error) { process.stderr.write(`Could not process evidence: ${error.message}\n`); process.exitCode = 1; }
46
+ } else if (args[0] === 'dependencies') {
47
+ try {
48
+ require('../cli/dependencies').runDependenciesCommand(args.slice(1), cliCwd);
49
+ } catch (error) {
50
+ process.stderr.write(`Could not analyze dependencies: ${error.message}\n`);
51
+ process.exitCode = 1;
52
+ }
53
+ } else if (args[0] === 'correspondence') {
54
+ try {
55
+ require('../cli/correspondence').importCorrespondenceCommand(args.slice(1), cliCwd);
56
+ } catch (error) {
57
+ process.stderr.write(`Could not import correspondence: ${error.message}\n`);
58
+ process.exitCode = 1;
59
+ }
60
+ } else if (tokenMatches('tourCommand', args[0])) {
44
61
  try {
45
62
  runTourCommand(args.slice(1), cliCwd, packageRoot);
46
63
  } catch (error) {
@@ -13,10 +13,13 @@ const CLI_SPEC = Object.freeze({
13
13
  'bygone --read-only <desktop-arguments...>',
14
14
  'bygone review [<head>] [--base <base>]',
15
15
  'bygone -C <directory> <command-or-path> [...]',
16
- 'bygone present [<head>] [--base <base>] [--tour <file.bygone>]',
16
+ 'bygone present [<head>] [--base <base>] [--tour <file.bygone>] [--correspondence <artifact.json>]',
17
17
  'bygone tour validate <file.bygone> [--json]',
18
18
  'bygone tour compile <file.bygone> [--output <tour.json>]',
19
- 'bygone tour context [<head>] [--base <base>] [--output <context.json>]',
19
+ 'bygone tour context [<head>] [--base <base>] [--output <context.json>] [--correspondence <artifact.json>] [--dependencies]',
20
+ 'bygone evidence <inspect|import|refresh|tour> <review.json> [--head <revision>] [--output <new-file>]',
21
+ 'bygone dependencies [<head>] [--base <base>] [--json] [--output <graph.json>]',
22
+ 'bygone correspondence import <report.json> [<head>] [--base <base>] [--case <id>] --output <artifact.json>',
20
23
  'bygone tour coverage <file.bygone> [--json] [--minimum-coverage <0-100>]',
21
24
  'bygone tour export <file.bygone> --output <tour.html> [--profile minimal|full] [--runtime embedded|cdn]',
22
25
  'bygone <bygone://open/v1?...>',
@@ -26,6 +29,7 @@ const CLI_SPEC = Object.freeze({
26
29
  'bygone --test'
27
30
  ],
28
31
  entries: [
32
+ { id: 'evidence', kind: 'command', tokens: ['evidence'], description: 'Inspect, import, refresh, or present pinned review evidence', argument: 'path' },
29
33
  {
30
34
  id: 'review',
31
35
  kind: 'command',
@@ -33,6 +37,18 @@ const CLI_SPEC = Object.freeze({
33
37
  description: 'Review a committed branch against its merge base',
34
38
  argument: 'git-ref'
35
39
  },
40
+ {
41
+ id: 'dependencies', kind: 'command', tokens: ['dependencies'],
42
+ description: 'Compare static JS/TS module dependencies at committed revisions', argument: 'git-ref'
43
+ },
44
+ {
45
+ id: 'dependencyContext', kind: 'option', tokens: ['--dependencies'],
46
+ description: 'Include static module dependency evidence in tour context', argument: 'none'
47
+ },
48
+ {
49
+ id: 'correspondence', kind: 'command', tokens: ['correspondence'],
50
+ description: 'Import externally produced symbol correspondence evidence', argument: 'path'
51
+ },
36
52
  {
37
53
  id: 'completion',
38
54
  kind: 'command',
@@ -110,6 +126,10 @@ const CLI_SPEC = Object.freeze({
110
126
  description: 'Use an authored .bygone presentation',
111
127
  argument: 'path'
112
128
  },
129
+ {
130
+ id: 'correspondenceEvidence', kind: 'option', tokens: ['--correspondence'],
131
+ description: 'Use pinned symbol evidence in present or tour context', argument: 'path'
132
+ },
113
133
  {
114
134
  id: 'readOnly',
115
135
  kind: 'global',
@@ -35,7 +35,7 @@ function generateZshCompletion() {
35
35
  const reviewOptions = entriesByIds(['base', 'help'])
36
36
  .flatMap((entry) => entry.tokens.map((token) => ` '${token}:${escapeSingleQuoted(entry.description)}'`))
37
37
  .join('\n');
38
- const presentOptions = entriesByIds(['base', 'tour', 'help'])
38
+ const presentOptions = entriesByIds(['base', 'tour', 'correspondenceEvidence', 'help'])
39
39
  .flatMap((entry) => entry.tokens.map((token) => ` '${token}:${escapeSingleQuoted(entry.description)}'`))
40
40
  .join('\n');
41
41
  const legacyOptions = entriesByIds(['branch', 'base', 'help'])
@@ -93,16 +93,24 @@ ${historyOptions}
93
93
  elif [[ "$words[3]" == "context" ]]; then
94
94
  if [[ "$previous" == ${shellAlternation(tokensFor('base'))} ]]; then
95
95
  _bygone_git_refs
96
- elif [[ "$previous" == "--output" || "$previous" == "-o" ]]; then
96
+ elif [[ "$previous" == "--output" || "$previous" == "-o" || "$previous" == "--correspondence" ]]; then
97
97
  _files
98
98
  else
99
- compadd -- --base --output -o --max-patch-bytes --max-total-patch-bytes
99
+ compadd -- --base --output -o --max-patch-bytes --max-total-patch-bytes --correspondence --dependencies
100
100
  _bygone_git_refs
101
101
  fi
102
102
  elif [[ "$words[3]" == "coverage" || "$words[3]" == "validate" || "$words[3]" == "compile" ]]; then
103
103
  _files -g '*.bygone' -g '*.bygone.yaml'
104
104
  fi
105
105
  ;;
106
+ dependencies)
107
+ if [[ "$previous" == "--output" || "$previous" == "-o" ]]; then
108
+ _files
109
+ else
110
+ compadd -- --base --json --output -o
111
+ _bygone_git_refs
112
+ fi
113
+ ;;
106
114
  review)
107
115
  if [[ "$previous" == ${shellAlternation(tokensFor('base'))} ]]; then
108
116
  _bygone_git_refs
@@ -112,7 +120,7 @@ ${historyOptions}
112
120
  fi
113
121
  ;;
114
122
  present)
115
- if [[ "$previous" == ${shellAlternation(tokensFor('tour'))} ]]; then
123
+ if [[ "$previous" == ${shellAlternation(tokensFor('tour'))} || "$previous" == "--correspondence" ]]; then
116
124
  _files -g '*.bygone' -g '*.bygone.yaml'
117
125
  elif [[ "$previous" == ${shellAlternation(tokensFor('base'))} ]]; then
118
126
  _bygone_git_refs
@@ -152,7 +160,7 @@ compdef _bygone bygone
152
160
  function generateBashCompletion() {
153
161
  const rootTokens = completionEntries().flatMap((entry) => entry.tokens).join(' ');
154
162
  const reviewTokens = entriesByIds(['base', 'help']).flatMap((entry) => entry.tokens).join(' ');
155
- const presentTokens = entriesByIds(['base', 'tour', 'help']).flatMap((entry) => entry.tokens).join(' ');
163
+ const presentTokens = entriesByIds(['base', 'tour', 'correspondenceEvidence', 'help']).flatMap((entry) => entry.tokens).join(' ');
156
164
  const legacyTokens = entriesByIds(['branch', 'base', 'help']).flatMap((entry) => entry.tokens).join(' ');
157
165
  const historyTokens = entriesByIds(['includeStaged', 'help']).flatMap((entry) => entry.tokens).join(' ');
158
166
  const baseTokens = tokensFor('base').join('|');
@@ -188,17 +196,25 @@ _bygone() {
188
196
  refs="$(_bygone_git_refs)"
189
197
  if [[ "$prev" =~ ^(${baseTokens})$ ]]; then
190
198
  COMPREPLY=( $(compgen -W "$refs" -- "$cur") )
191
- elif [[ "$prev" == "--output" || "$prev" == "-o" ]]; then
199
+ elif [[ "$prev" == "--output" || "$prev" == "-o" || "$prev" == "--correspondence" ]]; then
192
200
  COMPREPLY=( $(compgen -f -- "$cur") )
193
201
  compopt -o filenames 2>/dev/null || true
194
202
  else
195
- COMPREPLY=( $(compgen -W '--base --output -o --max-patch-bytes --max-total-patch-bytes '"$refs" -- "$cur") )
203
+ COMPREPLY=( $(compgen -W '--base --output -o --max-patch-bytes --max-total-patch-bytes --correspondence --dependencies '"$refs" -- "$cur") )
196
204
  fi
197
205
  else
198
206
  COMPREPLY=( $(compgen -W '--json --output -o' -- "$cur") $(compgen -f -- "$cur") )
199
207
  compopt -o filenames 2>/dev/null || true
200
208
  fi
201
209
  ;;
210
+ dependencies)
211
+ if [[ "$prev" == "--output" || "$prev" == "-o" ]]; then
212
+ COMPREPLY=( $(compgen -f -- "$cur") )
213
+ compopt -o filenames 2>/dev/null || true
214
+ else
215
+ COMPREPLY=( $(compgen -W '--base --json --output -o '"$(_bygone_git_refs)" -- "$cur") )
216
+ fi
217
+ ;;
202
218
  review)
203
219
  refs="$(_bygone_git_refs)"
204
220
  if [[ "$prev" =~ ^(${baseTokens})$ ]]; then
@@ -209,7 +225,7 @@ _bygone() {
209
225
  ;;
210
226
  present)
211
227
  refs="$(_bygone_git_refs)"
212
- if [[ "$prev" == "--tour" ]]; then
228
+ if [[ "$prev" == "--tour" || "$prev" == "--correspondence" ]]; then
213
229
  COMPREPLY=( $(compgen -f -- "$cur") )
214
230
  compopt -o filenames 2>/dev/null || true
215
231
  elif [[ "$prev" =~ ^(${baseTokens})$ ]]; then
@@ -261,6 +277,11 @@ function generateFishCompletion() {
261
277
  }
262
278
 
263
279
  lines.push(
280
+ "complete -c bygone -n 'string match -q \"*tour context*\" -- (commandline -opc)' -l dependencies -d 'Include static module dependency evidence'",
281
+ "complete -c bygone -n '__fish_seen_subcommand_from dependencies' -l base -r -a '(__bygone_git_refs)' -d 'Set the dependency comparison base'",
282
+ "complete -c bygone -n '__fish_seen_subcommand_from dependencies' -l json -d 'Print dependency evidence as JSON'",
283
+ "complete -c bygone -n '__fish_seen_subcommand_from dependencies' -l output -s o -r -d 'Write dependency evidence'",
284
+ "complete -c bygone -n '__fish_seen_subcommand_from dependencies' -a '(__bygone_git_refs)' -d 'Git ref'",
264
285
  "complete -c bygone -n '__fish_seen_subcommand_from completion' -a 'zsh bash fish' -d 'Shell'",
265
286
  "complete -c bygone -n '__fish_seen_subcommand_from tour' -a 'context coverage validate compile export schema' -d 'Tour action'",
266
287
  "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'",
@@ -274,6 +295,7 @@ function generateFishCompletion() {
274
295
  "complete -c bygone -n '__fish_seen_subcommand_from review' -a '(__bygone_git_refs)' -d 'Git ref'",
275
296
  `complete -c bygone -n '__fish_seen_subcommand_from present' ${fishTokens(tokensFor('base'))} -r -a '(__bygone_git_refs)' -d '${escapeSingleQuoted(getCliEntry('base').description)}'`,
276
297
  `complete -c bygone -n '__fish_seen_subcommand_from present' ${fishTokens(tokensFor('tour'))} -r -d '${escapeSingleQuoted(getCliEntry('tour').description)}'`,
298
+ `complete -c bygone ${fishTokens(tokensFor('correspondenceEvidence'))} -r -d 'Use pinned symbol correspondence evidence'`,
277
299
  "complete -c bygone -n '__fish_seen_subcommand_from present' -a '(__bygone_git_refs)' -d 'Git ref'",
278
300
  `complete -c bygone -n 'string match -q "*--branch-diff*" -- (commandline -opc)' ${fishTokens(tokensFor('branch'))} -r -a '(__bygone_git_refs)' -d '${escapeSingleQuoted(getCliEntry('branch').description)}'`,
279
301
  `complete -c bygone -n 'string match -q "*--branch-diff*" -- (commandline -opc)' ${fishTokens(tokensFor('base'))} -r -a '(__bygone_git_refs)' -d '${escapeSingleQuoted(getCliEntry('base').description)}'`,
@@ -0,0 +1,71 @@
1
+ const path = require('path');
2
+ const { lstatSync, mkdirSync, statSync, writeFileSync } = require('fs');
3
+ const { resolveBranchReviewRange } = require('../out/gitComparison');
4
+ const { importChangeCorrespondence, readCorrespondenceJson } = require('../out/changeCorrespondence');
5
+
6
+ function importCorrespondenceCommand(args, cwd, output = process.stdout) {
7
+ if (args[0] !== 'import') throw new Error('Usage: bygone correspondence import <report.json> [<head>] --base <base> [--case <id>] --output <artifact.json>');
8
+ const options = {};
9
+ const positional = [];
10
+ for (let i = 1; i < args.length; i++) {
11
+ const arg = args[i];
12
+ if (['--base', '--case', '--output', '-o'].includes(arg)) {
13
+ if (!args[i + 1] || args[i + 1].startsWith('-')) throw new Error(`${arg} requires a value.`);
14
+ options[arg === '-o' ? '--output' : arg] = args[++i];
15
+ } else if (arg.startsWith('-')) throw new Error(`Unknown correspondence option: ${arg}`);
16
+ else positional.push(arg);
17
+ }
18
+ if (!positional.length || positional.length > 2 || !options['--output']) throw new Error('Provide a producer report, at most one head ref, and --output.');
19
+ const reportPath = path.resolve(cwd, positional[0]);
20
+ const destination = path.resolve(cwd, options['--output']);
21
+ assertDestinationAvailable(reportPath, destination);
22
+ const report = readCorrespondenceJson(reportPath);
23
+ if (!report || typeof report !== 'object') throw new Error('Invalid correspondence report.');
24
+ let selected = report;
25
+ if (Array.isArray(report.cases)) {
26
+ if (!options['--case']) throw new Error('A multi-case producer report requires --case.');
27
+ const matches = report.cases.filter(c => c.id === options['--case']);
28
+ if (matches.length !== 1) throw new Error('Select exactly one existing producer case.');
29
+ selected = matches[0];
30
+ } else if (options['--case']) throw new Error('--case requires a multi-case producer report.');
31
+ const range = resolveBranchReviewRange(cwd, positional[1] || 'HEAD', options['--base']);
32
+ const artifact = importChangeCorrespondence(range.repoRoot, selected, { baseOid: range.mergeBaseOid, headOid: range.headOid });
33
+ mkdirSync(path.dirname(destination), { recursive: true });
34
+ try {
35
+ writeFileSync(destination, JSON.stringify(artifact, null, 2) + '\n', { encoding: 'utf8', flag: 'wx', mode: 0o600 });
36
+ } catch (error) {
37
+ if (error?.code === 'EEXIST') throw new Error(`The correspondence output already exists at ${destination}; choose a new path.`, { cause: error });
38
+ throw new Error(`Could not create correspondence output at ${destination}: ${error.message}`, { cause: error });
39
+ }
40
+ output.write(`Wrote ${artifact.relations.length} inferred correspondences (${artifact.status}) to ${destination}\n`);
41
+ return artifact;
42
+ }
43
+
44
+ function assertDestinationAvailable(reportPath, destination) {
45
+ if (sameActualFile(reportPath, destination)) {
46
+ throw new Error('The imported artifact must not overwrite the producer report or any of its symlink/hardlink aliases.');
47
+ }
48
+ try {
49
+ lstatSync(destination);
50
+ } catch (error) {
51
+ if (error?.code === 'ENOENT') return;
52
+ throw new Error(`Could not inspect correspondence output at ${destination}: ${error.message}`, { cause: error });
53
+ }
54
+ throw new Error(`The correspondence output already exists at ${destination}; choose a new path.`);
55
+ }
56
+
57
+ function sameActualFile(first, second) {
58
+ if (first === second) return true;
59
+ try {
60
+ const left = statSync(first);
61
+ const right = statSync(second);
62
+ if (left.dev !== undefined && left.ino !== undefined
63
+ && left.dev === right.dev && left.ino === right.ino
64
+ && left.ino !== 0 && right.ino !== 0) return true;
65
+ } catch {
66
+ return false;
67
+ }
68
+ return false;
69
+ }
70
+
71
+ module.exports = { importCorrespondenceCommand };
@@ -0,0 +1,62 @@
1
+ const { mkdirSync, writeFileSync } = require('fs');
2
+ const path = require('path');
3
+
4
+ function runDependenciesCommand(args, cwd, output = process.stdout) {
5
+ let headRef, baseRef, outputPath;
6
+ let json = false;
7
+ for (let index = 0; index < args.length; index++) {
8
+ const arg = args[index];
9
+ if (arg === '--json') { json = true; continue; }
10
+ if (['--base', '-m', '--main', '--output', '-o'].includes(arg)) {
11
+ const value = args[++index];
12
+ if (!value || value.startsWith('-')) throw new Error(`${arg} requires a value.`);
13
+ if (['--output', '-o'].includes(arg)) outputPath = value;
14
+ else baseRef = value;
15
+ } else if (arg.startsWith('-') || headRef) throw new Error(`Unexpected dependencies argument: ${arg}`);
16
+ else headRef = arg;
17
+ }
18
+ const { buildDependencyGraph } = require('../out/dependencyGraph.js');
19
+ const report = buildDependencyGraph(cwd, { headRef, baseRef });
20
+ if (outputPath) {
21
+ const resolved = path.resolve(cwd, outputPath);
22
+ mkdirSync(path.dirname(resolved), { recursive: true });
23
+ writeFileSync(resolved, `${JSON.stringify(report, null, 2)}\n`);
24
+ output.write(`Wrote dependency evidence to ${resolved}\n`);
25
+ } else output.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderDependencyReport(report));
26
+ return report;
27
+ }
28
+
29
+ function renderDependencyReport(report) {
30
+ const complete = report.base.coverage.indexComplete && report.head.coverage.indexComplete;
31
+ const lines = [
32
+ `Module dependencies: ${report.range.baseOid.slice(0, 7)} → ${report.range.headOid.slice(0, 7)}`,
33
+ `Static JS/TS evidence; initialization labels describe syntax, not observed execution.`,
34
+ `Index coverage: ${complete ? 'complete within supported scope' : 'PARTIAL — deltas and consumer lists may be incomplete'}`
35
+ ];
36
+ for (const side of ['base', 'head']) {
37
+ const snapshot = report[side];
38
+ const coverage = snapshot.coverage;
39
+ lines.push(`${side}: ${coverage.indexedFiles}/${coverage.candidateFiles} source files, ${snapshot.edges.length} sites, ${coverage.unresolvedEdges} unresolved, ${snapshot.diagnostics.length + coverage.omittedDiagnostics} diagnostics`);
40
+ }
41
+ const site = edge => `${edge.source.file}:${edge.source.line}:${edge.source.column} → ${edge.target ?? edge.specifier ?? '<computed>'} [${edge.relationship}; ${edge.phase}; ${edge.loading}; ${edge.resolution}]`;
42
+ const groups = [
43
+ ['Added module edges', report.changes.added], ['Removed module edges', report.changes.removed],
44
+ ['Unchanged consumers (base)', report.unchangedConsumers.base], ['Unchanged consumers (head)', report.unchangedConsumers.head]
45
+ ];
46
+ for (const [label, edges] of groups) {
47
+ lines.push(`\n${label}: ${edges.length}`);
48
+ for (const edge of edges.slice(0, 50)) {
49
+ lines.push(` ${site(edge)}`);
50
+ if (edge.source.link) lines.push(` ${edge.source.link}`);
51
+ }
52
+ if (edges.length > 50) lines.push(` ${edges.length - 50} more sites; use --json for the bounded full report.`);
53
+ }
54
+ for (const side of ['base', 'head']) {
55
+ lines.push(`\n${side} diagnostics:`);
56
+ for (const item of report[side].diagnostics.slice(0, 10)) lines.push(` ${item.file ? `${item.file}: ` : ''}${item.code}: ${item.message}`);
57
+ if (report[side].diagnostics.length > 10) lines.push(' More diagnostics in --json.');
58
+ }
59
+ lines.push('\nUse --json or --output <file> for all indexed edges, potential initialization calls, exact source ranges, and scope assumptions.');
60
+ return `${lines.join('\n')}\n`;
61
+ }
62
+ module.exports = { runDependenciesCommand, renderDependencyReport };
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const workspace = require('../out/reviewWorkspace');
5
+ const { parseReview } = require('../out/requirements');
6
+ const { inspectDependencies } = require('../out/reviewDependencies');
7
+
8
+ function runEvidenceCommand(args, cwd) {
9
+ const [action, input, ...rest] = args;
10
+ if (!['inspect', 'import', 'refresh', 'tour'].includes(action) || !input) throw new Error('Usage: bygone evidence <inspect|import|refresh|tour> <review.json> [--head <revision>] [--requirements <review.json>] [--correspondence <artifact.json>] [--dependencies <report.json>] [--output <new-file>]');
11
+ const options = {};
12
+ for (let i = 0; i < rest.length; i += 2) {
13
+ if (!['--head', '--requirements', '--correspondence', '--dependencies', '--output'].includes(rest[i]) || !rest[i + 1] || options[rest[i]]) throw new Error('Invalid or duplicate evidence option');
14
+ options[rest[i]] = rest[i + 1];
15
+ }
16
+ const root = workspace.repositoryRoot(cwd), file = path.resolve(cwd, input);
17
+ const loaded = workspace.loadReview(root, file);
18
+ // Reports must describe the sanitized artifact that is actually imported.
19
+ const review = action === 'import' ? { ...loaded, decisions: [] } : loaded;
20
+ const absolute = key => options[key] ? path.resolve(cwd, options[key]) : undefined;
21
+ const output = absolute('--output');
22
+ if (action === 'refresh') {
23
+ if (!output || !options['--head']) throw new Error('Refresh requires --head and a new --output file');
24
+ const revised = options['--requirements'] ? parseReview(workspace.readEvidenceJson(absolute('--requirements'))) : review;
25
+ if (revised.repository !== review.repository || revised.base !== review.base || revised.head !== review.head) throw new Error('Requirement update must identify the original repository and comparison');
26
+ const requirements = revised.requirements;
27
+ const result = workspace.refreshWorkspace(root, review, options['--head'], absolute('--correspondence'), requirements);
28
+ workspace.writeReview(output, result.review);
29
+ process.stdout.write(JSON.stringify({ output, transitions: result.transitions }, null, 2) + '\n'); return;
30
+ }
31
+ const report = workspace.inspectWorkspace(root, review);
32
+ const dependencies = options['--dependencies'] ? inspectDependencies(root, review, workspace.readEvidenceJson(absolute('--dependencies'))) : undefined;
33
+ if (action === 'import') {
34
+ if (!output) throw new Error('Import requires a new --output file');
35
+ // Compatible producers export this versioned shape directly. Never manufacture an inspection decision.
36
+ workspace.writeReview(output, review);
37
+ process.stdout.write(JSON.stringify({ output, report, dependencies }, null, 2) + '\n'); return;
38
+ }
39
+ if (action === 'tour') {
40
+ if (!output) throw new Error('Tour requires a new --output file');
41
+ const anchors = {}, steps = [];
42
+ for (const requirement of report.requirements) for (const link of requirement.links) {
43
+ if (link.resolution.status !== 'valid') continue;
44
+ const text = workspace.sourceText(root, link.ref), needle = link.resolution.text;
45
+ let at = -1, occurrence = 0;
46
+ do { at = text.indexOf(needle, at + 1); occurrence++; } while (at >= 0 && at < link.ref.start);
47
+ if (at !== link.ref.start) throw new Error('Cannot represent source reference as a tour anchor');
48
+ const id = `evidence-${steps.length + 1}`;
49
+ anchors[id] = { file: link.ref.path, revision: link.ref.revision === review.base ? 'base' : 'head', contains: needle, occurrence };
50
+ steps.push({ id, title: `${requirement.id}: ${link.role}`, body: `${link.rationale}\nVerification: ${requirement.verification}; inspection: ${requirement.reviewer}. Reference validity does not establish fulfillment.`, focus: id,
51
+ requirement: { id: requirement.id, text: requirement.text, source: requirement.sources.map(s => s.title).join('; ') } });
52
+ }
53
+ if (!steps.length) throw new Error('No valid evidence links to present; use evidence inspect for missing/broken evidence');
54
+ const tour = { version: 6, title: 'Requirement evidence', range: { base: review.base, head: review.head }, anchors, connections: [],
55
+ chapters: [{ id: 'requirements', title: 'Requirement evidence', scenes: [{ id: 'evidence', kind: 'walkthrough', title: 'Inspect recorded evidence',
56
+ summary: 'Pinned references, execution records, and human inspection remain separate.', tags: [],
57
+ bullets: report.requirements.filter(r => !r.implementationLinked || !r.testLinked).map(r => `${r.id}: missing implementation or test links`),
58
+ takeaway: 'This tour supplies inspection context, not an approval decision.', steps }] }] };
59
+ // Validate before publishing an artifact. The existing tour engine owns presentation.
60
+ require('../out/changeTour').buildChangeTourManifest(root, { source: tour });
61
+ fs.writeFileSync(output, JSON.stringify(tour, null, 2) + '\n', { flag: 'wx' });
62
+ process.stdout.write(`Created ${output}\nOpen: bygone -C ${JSON.stringify(root)} present --tour ${JSON.stringify(output)}\n`); return;
63
+ }
64
+ const result = JSON.stringify({ review, report, dependencies }, null, 2) + '\n';
65
+ if (output) fs.writeFileSync(output, result, { flag: 'wx' }); else process.stdout.write(result);
66
+ }
67
+ module.exports = { runEvidenceCommand };