@davmash/bygone 0.11.0 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## 0.11.0
4
10
 
5
11
  - Added experimental JS/TS and Python symbol evidence with declaration diffs, symbol trees, analysis diagnostics, and shared Files/Symbols navigation
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
@@ -376,31 +378,61 @@ read-only. Desktop hand-offs are unavailable in remote extension hosts.
376
378
 
377
379
  ## Follow the active VS Code editor
378
380
 
379
- Run **Bygone: Follow Active Editor** to open one read-only comparison beside your
380
- source editor without moving keyboard focus. Switching source files updates that
381
- same pane. The left side is the file at HEAD; the right side is your editor buffer,
382
- including unsaved edits. Edit, undo, and save in the source editor as usual.
383
-
384
- **Pin** keeps the current file and baseline commit while buffer edits continue to
385
- update. **Resume Following** selects the active or last source editor. **Refresh**
386
- reads HEAD again when following; a pinned comparison retains its baseline. HEAD
387
- also refreshes on file activation and when the VS Code window regains focus.
388
- After external Git changes while the window stays focused, use Refresh.
389
-
390
- The controls show the repository, relative file path, commit, and saved/unsaved
391
- state. Clean files say **No changes from HEAD**. Untracked files, files absent at
392
- the current path in HEAD, and unborn repositories use an explicitly absent
393
- baseline. Renames are compared at the new path; following does not infer history.
394
- **View File History** pauses following and opens the history viewer. **Manual
395
- Compare…** pauses following and compares the source with another selected file.
396
- Closing the pane stops following. A restored pane starts paused after reload.
397
-
398
- Following supports trusted local Git worktrees, including linked worktrees and
399
- files in multiple repositories. Both versions must be text, at most 128 KiB and
400
- 5,000 lines; Git baselines must be UTF-8. Untitled, remote, virtual, notebook,
401
- symlink, binary, and oversized files show an unavailable state. Open the source
402
- file outside a native SCM diff to follow it. Git failures clear the old comparison
403
- and show an error instead of treating a failed read as an empty baseline.
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.
404
436
 
405
437
  ## Limitations
406
438
 
package/bin/bygone.js CHANGED
@@ -40,7 +40,17 @@ if (tokenMatches('completion', args[0])) {
40
40
  process.exit(0);
41
41
  }
42
42
 
43
- if (args[0] === 'correspondence') {
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') {
44
54
  try {
45
55
  require('../cli/correspondence').importCorrespondenceCommand(args.slice(1), cliCwd);
46
56
  } catch (error) {
@@ -16,7 +16,9 @@ const CLI_SPEC = Object.freeze({
16
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>] [--correspondence <artifact.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>]',
20
22
  'bygone correspondence import <report.json> [<head>] [--base <base>] [--case <id>] --output <artifact.json>',
21
23
  'bygone tour coverage <file.bygone> [--json] [--minimum-coverage <0-100>]',
22
24
  'bygone tour export <file.bygone> --output <tour.html> [--profile minimal|full] [--runtime embedded|cdn]',
@@ -27,6 +29,7 @@ const CLI_SPEC = Object.freeze({
27
29
  'bygone --test'
28
30
  ],
29
31
  entries: [
32
+ { id: 'evidence', kind: 'command', tokens: ['evidence'], description: 'Inspect, import, refresh, or present pinned review evidence', argument: 'path' },
30
33
  {
31
34
  id: 'review',
32
35
  kind: 'command',
@@ -34,6 +37,14 @@ const CLI_SPEC = Object.freeze({
34
37
  description: 'Review a committed branch against its merge base',
35
38
  argument: 'git-ref'
36
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
+ },
37
48
  {
38
49
  id: 'correspondence', kind: 'command', tokens: ['correspondence'],
39
50
  description: 'Import externally produced symbol correspondence evidence', argument: 'path'
@@ -96,13 +96,21 @@ ${historyOptions}
96
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 --correspondence
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
@@ -192,13 +200,21 @@ _bygone() {
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 --correspondence '"$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
@@ -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'",
@@ -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 };
package/cli/tour.js CHANGED
@@ -34,13 +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,
40
44
  correspondencePath: options.correspondencePath ? path.resolve(cwd, options.correspondencePath) : undefined,
41
45
  maxPatchBytes: options.maxPatchBytes,
42
46
  maxTotalPatchBytes: options.maxTotalPatchBytes
43
47
  });
48
+ if (options.dependencies) {
49
+ const { buildDependencyGraph } = require('../out/dependencyGraph.js');
50
+ context.dependencies = buildDependencyGraph(cwd, { range });
51
+ }
44
52
  return writeJsonResult('change-tour context', context, options.outputPath, cwd, output);
45
53
  }
46
54
 
@@ -150,6 +158,7 @@ function parseContextArgs(args) {
150
158
  let maxPatchBytes;
151
159
  let correspondencePath;
152
160
  let maxTotalPatchBytes;
161
+ let dependencies = false;
153
162
  for (let index = 0; index < args.length; index += 1) {
154
163
  const arg = args[index];
155
164
  if (arg === '--base' || arg === '-m' || arg === '--main') {
@@ -162,6 +171,7 @@ function parseContextArgs(args) {
162
171
  outputPath = args[++index];
163
172
  continue;
164
173
  }
174
+ if (arg === '--dependencies') { dependencies = true; continue; }
165
175
  if (arg === '--correspondence') {
166
176
  if (!args[index + 1] || args[index + 1].startsWith('-')) throw new Error('--correspondence requires a file path.');
167
177
  correspondencePath = args[++index];
@@ -187,7 +197,7 @@ function parseContextArgs(args) {
187
197
  if (headRef) throw new Error('tour context accepts at most one head ref.');
188
198
  headRef = arg;
189
199
  }
190
- return { action: 'context', headRef, baseRef, outputPath, maxPatchBytes, maxTotalPatchBytes, ...(correspondencePath ? { correspondencePath } : {}) };
200
+ return { action: 'context', headRef, baseRef, outputPath, maxPatchBytes, maxTotalPatchBytes, ...(correspondencePath ? { correspondencePath } : {}), ...(dependencies ? { dependencies } : {}) };
191
201
  }
192
202
 
193
203
  function writeJsonResult(label, value, outputPath, cwd, output) {
@@ -13,7 +13,9 @@ _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'
17
19
  'correspondence:Import externally produced symbol correspondence evidence'
18
20
  'completion:Print shell completion source'
19
21
  'present:Open an app-hosted tour of a committed branch range'
@@ -83,13 +85,21 @@ _bygone() {
83
85
  elif [[ "$previous" == "--output" || "$previous" == "-o" || "$previous" == "--correspondence" ]]; then
84
86
  _files
85
87
  else
86
- compadd -- --base --output -o --max-patch-bytes --max-total-patch-bytes --correspondence
88
+ compadd -- --base --output -o --max-patch-bytes --max-total-patch-bytes --correspondence --dependencies
87
89
  _bygone_git_refs
88
90
  fi
89
91
  elif [[ "$words[3]" == "coverage" || "$words[3]" == "validate" || "$words[3]" == "compile" ]]; then
90
92
  _files -g '*.bygone' -g '*.bygone.yaml'
91
93
  fi
92
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
+ ;;
93
103
  review)
94
104
  if [[ "$previous" == (--base|-m|--main) ]]; then
95
105
  _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 correspondence 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
@@ -32,13 +32,21 @@ _bygone() {
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 --correspondence '"$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
@@ -4,7 +4,9 @@ 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'
8
10
  complete -c bygone -n '__fish_use_subcommand' -a correspondence -r -d 'Import externally produced symbol correspondence evidence'
9
11
  complete -c bygone -n '__fish_use_subcommand' -a completion -d 'Print shell completion source'
10
12
  complete -c bygone -n '__fish_use_subcommand' -a present -d 'Open an app-hosted tour of a committed branch range'
@@ -18,6 +20,11 @@ complete -c bygone -n '__fish_use_subcommand' -l read-only -d 'Open live Explore
18
20
  complete -c bygone -n '__fish_use_subcommand' -s C -r -d 'Run as if Bygone started in this directory'
19
21
  complete -c bygone -n '__fish_use_subcommand' -s h -l help -d 'Show command help'
20
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'
21
28
  complete -c bygone -n '__fish_seen_subcommand_from completion' -a 'zsh bash fish' -d 'Shell'
22
29
  complete -c bygone -n '__fish_seen_subcommand_from tour' -a 'context coverage validate compile export schema' -d 'Tour action'
23
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'
@@ -0,0 +1,94 @@
1
+ # Dependency evidence
2
+
3
+ The Node CLI (npm installation or `node bin/bygone.js` in a source checkout)
4
+ compares JS/TS module dependencies at the merge base and head of a committed
5
+ review range:
6
+
7
+ ```sh
8
+ bygone dependencies HEAD --base main
9
+ bygone dependencies HEAD --base main --json
10
+ bygone dependencies HEAD --base main --output dependencies.json
11
+ bygone tour context HEAD --base main --dependencies --output context.json
12
+ ```
13
+
14
+ The text report lists added/removed module edges and direct, unchanged consumers
15
+ of changed files. Each site includes a `bygone://` link to its committed source.
16
+ JSON includes both bounded snapshot graphs, potential initialization calls and
17
+ constructors, diagnostics, and exact evidence coordinates. Ordinary tour context
18
+ generation is unchanged. The desktop and VS Code interfaces do not yet have a
19
+ graph view; these commands use the Node CLI rather than the desktop launcher.
20
+
21
+ ## Reading an edge
22
+
23
+ Relationship, phase, and loading are separate fields. Static evidence can describe
24
+ initialization or later invocation; it is not a runtime trace.
25
+
26
+ - `import`, `reexport`, `require`, and `dynamic-import` describe syntax.
27
+ - `type` marks explicitly type-only dependencies and dependencies in declaration files. `module-init` marks syntax
28
+ evaluated while a module initializes; a lazy-loaded module can initialize much
29
+ later than process startup. `invocation` marks function/callback bodies.
30
+ `unknown` retains sites where the extractor cannot assign a phase safely.
31
+ - `eager` distinguishes ordinary imports/require from `deferred` dynamic imports and namespace `import defer` declarations. Deferred namespace evaluation has `unknown` phase because namespace use determines when evaluation occurs.
32
+ A dynamic import at top level is still labeled `module-init`. A `require` inside
33
+ a function is `invocation` even though the require operation itself is eager.
34
+ - `resolution: resolved` means the TypeScript resolver found a committed target
35
+ under the selected compiler options. It does not prove execution, runtime loader
36
+ equivalence, or the identity of a shadowable `require` binding.
37
+ - `unresolved`, `computed`, and `invalid-config` preserve unknown targets. Builtins,
38
+ external packages, and computed specifiers commonly remain unresolved.
39
+
40
+ Potential initialization calls are syntax sites, not inferred function-call
41
+ edges. Registration calls can appear here; their callback bodies are classified
42
+ separately. IIFE bodies are conservatively labeled invocation with a diagnostic.
43
+ Instance field initializers have unknown timing; static fields and class static
44
+ blocks inherit the enclosing evaluation phase. Conditions are not evaluated.
45
+ Non-explicit type import elision and transpiler-specific emission are not modeled.
46
+
47
+ ## Historical resolution and coverage
48
+
49
+ Both graphs are built from Git tree/blob objects at immutable OIDs. Dirty files,
50
+ installed packages, symlink targets, compiler plugins and target code are never
51
+ read or executed for resolution. The nearest committed `tsconfig.json` (or
52
+ `jsconfig.json`) supplies compiler options, including relative `extends`, `baseUrl`
53
+ and `paths`. Missing/invalid config is diagnosed. Without a config, analysis uses
54
+ TypeScript's Node-style resolution with JavaScript enabled. The report records
55
+ the analyzer's TypeScript version; it need not match the target project's compiler.
56
+ The dependency analyzer uses an isolated TypeScript 5.9.3 parser/resolver (without changing Strand or the extension compiler), including `bundler` resolution and modern syntax such as `using`. Package-based config extension and bundler runtime plugins remain outside scope. Malformed package metadata produces partial coverage; resolution that needs that metadata is `invalid-config`, without a fallback target.
57
+ Project references do not provide symbol binding or build output reconstruction.
58
+
59
+ Limits apply per snapshot: 2,000 source/config files, 1 MiB per file, 16 MiB total
60
+ input and 20,000 dependency/initialization records. Source-ancestor package.json, tsconfig.json and jsconfig.json files, plus their relative config inheritance closure, share the input budget and are read first. Directory imports can load other required package metadata on demand through the same bounds. Unrelated JSON, package files and lockfiles do not consume this budget. Relative inheritance supports transitive and array parents within the same bounds. Only UTF-8 text is parsed.
61
+ The dependency analysis budget is 30 seconds, including standalone Git range
62
+ discovery and both snapshots. It is checked between parsing/resolution operations;
63
+ it is not a hard interrupt inside the parser. Tour context reuses its pinned range,
64
+ and its ordinary context generation is outside this budget. Git tree enumeration is
65
+ bounded to 16 MiB. Up to 1,000 diagnostics per snapshot are retained with an omitted
66
+ count. Programmatic callers may lower these limits.
67
+
68
+ `coverage` reports supported source candidates, indexed/omitted files, unresolved
69
+ edges and whether indexing completed within the supported scope. Configuration,
70
+ parser and scope diagnostics matter even when an endpoint was found. A resolved
71
+ target can itself be outside the indexed subset. Partial graphs produce provisional
72
+ deltas and consumer lists; missing edges never establish independence. Submodules,
73
+ non-JS/TS languages and installed `node_modules` are outside scope.
74
+
75
+ The delta compares module endpoints, relationship, phase and loading, aligning
76
+ Git-detected renames. Source spelling/location and repeated occurrences alone do
77
+ not constitute new module edges. When a module edge changes, all matching source
78
+ sites appear in the delta. Alias/config retargeting can change dependencies of an
79
+ unchanged file. Consumer lists are one hop and include type-only edges, with phases
80
+ retained so callers can filter them. They are not a transitive impact analysis.
81
+
82
+ Every source reference contains a commit OID, Git blob OID, repository-relative
83
+ path, UTF-16 half-open range, one-based line/column, and SHA-256 of the selected
84
+ UTF-8 text. Links are local to the repository that generated the report. Equal
85
+ base/head revisions and paths outside the deep-link grammar retain coordinates
86
+ without a link. No source content is embedded in the dependency report itself.
87
+
88
+ Runtime trace ingestion, resolved symbol/call graphs and visual graph layout are
89
+ later slices. No workload is run to generate this evidence.
90
+
91
+ The [validation corpus and measured results](../scripts/dependency-benchmark/README.md)
92
+ cover labeled module edges, source evidence, bounded scale and pinned public
93
+ repositories. The historical baseline documents compatibility and metadata gaps; the follow-up validation records their fixes. Complete indexing is not proof that runtime
94
+ loading is valid.