@davmash/bygone 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +32 -0
- package/bin/bygone.js +8 -1
- package/cli/commandSpec.js +11 -2
- package/cli/completions.js +9 -8
- package/cli/correspondence.js +71 -0
- package/cli/present.js +61 -16
- package/cli/tour.js +8 -1
- package/cli/tourExport.js +1 -1
- package/completions/_bygone +5 -3
- package/completions/bygone +5 -5
- package/completions/bygone.fish +2 -0
- package/docs/change-tour-format.md +60 -6
- package/docs/correspondence.md +259 -0
- package/docs/generating-change-tours.md +3 -0
- package/docs/present.md +3 -0
- package/docs/releasing.md +11 -0
- package/media/diff.worker.js +10 -9
- package/media/webview.css +1 -1
- package/out/changeCorrespondence.js +739 -0
- package/out/changeTour.js +1359 -960
- package/out/gitComparison.js +4 -0
- package/out/standalone-main.js +3268 -1337
- package/out/tourCoverage.js +1852 -1448
- package/package.json +2 -2
- package/schemas/change-tour-source.schema.json +48 -3
- package/web/export-runtime.json +5 -5
- package/web/index.html +12 -0
- package/web/presenter.css +81 -0
- package/web/web-host.js +23 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
- Added experimental JS/TS and Python symbol evidence with declaration diffs, symbol trees, analysis diagnostics, and shared Files/Symbols navigation
|
|
6
|
+
- Added pinned symbol tour stops and full-file context in live presentations and portable exports
|
|
7
|
+
- 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
|
|
8
|
+
- Kept Java and RefactoringMiner optional: exact-only generation records partial coverage, and Strand provides local inference without a JVM
|
|
9
|
+
- 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
|
|
10
|
+
- Fixed following for macOS directory aliases and files at the 5,000-line limit, with regression coverage on current and minimum-supported VS Code
|
|
11
|
+
- Hardened correspondence artifact validation, parser budgets, and symbol owner matching
|
|
12
|
+
- Updated Electron, DOMPurify, fast-uri, and packaging dependencies to resolve security advisories
|
|
13
|
+
|
|
14
|
+
macOS desktop downloads are unsigned and not notarized. If Gatekeeper reports
|
|
15
|
+
the app as damaged, run `xattr -cr /Applications/Bygone.app`, then reopen it.
|
|
16
|
+
|
|
17
|
+
## 0.10.2
|
|
18
|
+
|
|
19
|
+
- Prioritized matching named lines before brackets to preserve block correspondences across nesting and indentation changes
|
|
20
|
+
- Aligned changed indentation highlights to the start of each line while preserving highlights on edited text
|
|
21
|
+
|
|
3
22
|
## 0.10.1
|
|
4
23
|
|
|
5
24
|
- Fixed desktop tours showing unstyled HTML after temporary asset cleanup during long-running app sessions
|
package/README.md
CHANGED
|
@@ -352,6 +352,10 @@ The wrapper scripts are:
|
|
|
352
352
|
|
|
353
353
|
## Commands
|
|
354
354
|
|
|
355
|
+
- `Bygone: Follow Active Editor`
|
|
356
|
+
- `Bygone: Pin Following File`
|
|
357
|
+
- `Bygone: Resume Following`
|
|
358
|
+
- `Bygone: Refresh Following Comparison`
|
|
355
359
|
- `Bygone: Compare Active File With…`
|
|
356
360
|
- `Bygone: Compare Selected Files`
|
|
357
361
|
- `Bygone: Compare with Selected`
|
|
@@ -370,6 +374,34 @@ panes edit through VS Code documents, so changes participate in dirty state,
|
|
|
370
374
|
undo, save, and external-document updates. Historical and non-local inputs are
|
|
371
375
|
read-only. Desktop hand-offs are unavailable in remote extension hosts.
|
|
372
376
|
|
|
377
|
+
## Follow the active VS Code editor
|
|
378
|
+
|
|
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.
|
|
404
|
+
|
|
373
405
|
## Limitations
|
|
374
406
|
|
|
375
407
|
- Bygone is intentionally diff-focused; merge tooling is not exposed as a product feature.
|
package/bin/bygone.js
CHANGED
|
@@ -40,7 +40,14 @@ if (tokenMatches('completion', args[0])) {
|
|
|
40
40
|
process.exit(0);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
if (
|
|
43
|
+
if (args[0] === 'correspondence') {
|
|
44
|
+
try {
|
|
45
|
+
require('../cli/correspondence').importCorrespondenceCommand(args.slice(1), cliCwd);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
process.stderr.write(`Could not import correspondence: ${error.message}\n`);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
} else if (tokenMatches('tourCommand', args[0])) {
|
|
44
51
|
try {
|
|
45
52
|
runTourCommand(args.slice(1), cliCwd, packageRoot);
|
|
46
53
|
} catch (error) {
|
package/cli/commandSpec.js
CHANGED
|
@@ -13,10 +13,11 @@ 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>]',
|
|
20
|
+
'bygone correspondence import <report.json> [<head>] [--base <base>] [--case <id>] --output <artifact.json>',
|
|
20
21
|
'bygone tour coverage <file.bygone> [--json] [--minimum-coverage <0-100>]',
|
|
21
22
|
'bygone tour export <file.bygone> --output <tour.html> [--profile minimal|full] [--runtime embedded|cdn]',
|
|
22
23
|
'bygone <bygone://open/v1?...>',
|
|
@@ -33,6 +34,10 @@ const CLI_SPEC = Object.freeze({
|
|
|
33
34
|
description: 'Review a committed branch against its merge base',
|
|
34
35
|
argument: 'git-ref'
|
|
35
36
|
},
|
|
37
|
+
{
|
|
38
|
+
id: 'correspondence', kind: 'command', tokens: ['correspondence'],
|
|
39
|
+
description: 'Import externally produced symbol correspondence evidence', argument: 'path'
|
|
40
|
+
},
|
|
36
41
|
{
|
|
37
42
|
id: 'completion',
|
|
38
43
|
kind: 'command',
|
|
@@ -110,6 +115,10 @@ const CLI_SPEC = Object.freeze({
|
|
|
110
115
|
description: 'Use an authored .bygone presentation',
|
|
111
116
|
argument: 'path'
|
|
112
117
|
},
|
|
118
|
+
{
|
|
119
|
+
id: 'correspondenceEvidence', kind: 'option', tokens: ['--correspondence'],
|
|
120
|
+
description: 'Use pinned symbol evidence in present or tour context', argument: 'path'
|
|
121
|
+
},
|
|
113
122
|
{
|
|
114
123
|
id: 'readOnly',
|
|
115
124
|
kind: 'global',
|
package/cli/completions.js
CHANGED
|
@@ -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,10 +93,10 @@ ${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
|
|
100
100
|
_bygone_git_refs
|
|
101
101
|
fi
|
|
102
102
|
elif [[ "$words[3]" == "coverage" || "$words[3]" == "validate" || "$words[3]" == "compile" ]]; then
|
|
@@ -112,7 +112,7 @@ ${historyOptions}
|
|
|
112
112
|
fi
|
|
113
113
|
;;
|
|
114
114
|
present)
|
|
115
|
-
if [[ "$previous" == ${shellAlternation(tokensFor('tour'))} ]]; then
|
|
115
|
+
if [[ "$previous" == ${shellAlternation(tokensFor('tour'))} || "$previous" == "--correspondence" ]]; then
|
|
116
116
|
_files -g '*.bygone' -g '*.bygone.yaml'
|
|
117
117
|
elif [[ "$previous" == ${shellAlternation(tokensFor('base'))} ]]; then
|
|
118
118
|
_bygone_git_refs
|
|
@@ -152,7 +152,7 @@ compdef _bygone bygone
|
|
|
152
152
|
function generateBashCompletion() {
|
|
153
153
|
const rootTokens = completionEntries().flatMap((entry) => entry.tokens).join(' ');
|
|
154
154
|
const reviewTokens = entriesByIds(['base', 'help']).flatMap((entry) => entry.tokens).join(' ');
|
|
155
|
-
const presentTokens = entriesByIds(['base', 'tour', 'help']).flatMap((entry) => entry.tokens).join(' ');
|
|
155
|
+
const presentTokens = entriesByIds(['base', 'tour', 'correspondenceEvidence', 'help']).flatMap((entry) => entry.tokens).join(' ');
|
|
156
156
|
const legacyTokens = entriesByIds(['branch', 'base', 'help']).flatMap((entry) => entry.tokens).join(' ');
|
|
157
157
|
const historyTokens = entriesByIds(['includeStaged', 'help']).flatMap((entry) => entry.tokens).join(' ');
|
|
158
158
|
const baseTokens = tokensFor('base').join('|');
|
|
@@ -188,11 +188,11 @@ _bygone() {
|
|
|
188
188
|
refs="$(_bygone_git_refs)"
|
|
189
189
|
if [[ "$prev" =~ ^(${baseTokens})$ ]]; then
|
|
190
190
|
COMPREPLY=( $(compgen -W "$refs" -- "$cur") )
|
|
191
|
-
elif [[ "$prev" == "--output" || "$prev" == "-o" ]]; then
|
|
191
|
+
elif [[ "$prev" == "--output" || "$prev" == "-o" || "$prev" == "--correspondence" ]]; then
|
|
192
192
|
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
193
193
|
compopt -o filenames 2>/dev/null || true
|
|
194
194
|
else
|
|
195
|
-
COMPREPLY=( $(compgen -W '--base --output -o --max-patch-bytes --max-total-patch-bytes '"$refs" -- "$cur") )
|
|
195
|
+
COMPREPLY=( $(compgen -W '--base --output -o --max-patch-bytes --max-total-patch-bytes --correspondence '"$refs" -- "$cur") )
|
|
196
196
|
fi
|
|
197
197
|
else
|
|
198
198
|
COMPREPLY=( $(compgen -W '--json --output -o' -- "$cur") $(compgen -f -- "$cur") )
|
|
@@ -209,7 +209,7 @@ _bygone() {
|
|
|
209
209
|
;;
|
|
210
210
|
present)
|
|
211
211
|
refs="$(_bygone_git_refs)"
|
|
212
|
-
if [[ "$prev" == "--tour" ]]; then
|
|
212
|
+
if [[ "$prev" == "--tour" || "$prev" == "--correspondence" ]]; then
|
|
213
213
|
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
214
214
|
compopt -o filenames 2>/dev/null || true
|
|
215
215
|
elif [[ "$prev" =~ ^(${baseTokens})$ ]]; then
|
|
@@ -274,6 +274,7 @@ function generateFishCompletion() {
|
|
|
274
274
|
"complete -c bygone -n '__fish_seen_subcommand_from review' -a '(__bygone_git_refs)' -d 'Git ref'",
|
|
275
275
|
`complete -c bygone -n '__fish_seen_subcommand_from present' ${fishTokens(tokensFor('base'))} -r -a '(__bygone_git_refs)' -d '${escapeSingleQuoted(getCliEntry('base').description)}'`,
|
|
276
276
|
`complete -c bygone -n '__fish_seen_subcommand_from present' ${fishTokens(tokensFor('tour'))} -r -d '${escapeSingleQuoted(getCliEntry('tour').description)}'`,
|
|
277
|
+
`complete -c bygone ${fishTokens(tokensFor('correspondenceEvidence'))} -r -d 'Use pinned symbol correspondence evidence'`,
|
|
277
278
|
"complete -c bygone -n '__fish_seen_subcommand_from present' -a '(__bygone_git_refs)' -d 'Git ref'",
|
|
278
279
|
`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
280
|
`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 };
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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('&', '&')
|
package/cli/tour.js
CHANGED
|
@@ -37,6 +37,7 @@ function runTourCommand(args, cwd, packageRoot, output = process.stdout) {
|
|
|
37
37
|
const context = buildChangeTourContext(cwd, {
|
|
38
38
|
headRef: options.headRef,
|
|
39
39
|
baseRef: options.baseRef,
|
|
40
|
+
correspondencePath: options.correspondencePath ? path.resolve(cwd, options.correspondencePath) : undefined,
|
|
40
41
|
maxPatchBytes: options.maxPatchBytes,
|
|
41
42
|
maxTotalPatchBytes: options.maxTotalPatchBytes
|
|
42
43
|
});
|
|
@@ -147,6 +148,7 @@ function parseContextArgs(args) {
|
|
|
147
148
|
let baseRef;
|
|
148
149
|
let outputPath;
|
|
149
150
|
let maxPatchBytes;
|
|
151
|
+
let correspondencePath;
|
|
150
152
|
let maxTotalPatchBytes;
|
|
151
153
|
for (let index = 0; index < args.length; index += 1) {
|
|
152
154
|
const arg = args[index];
|
|
@@ -160,6 +162,11 @@ function parseContextArgs(args) {
|
|
|
160
162
|
outputPath = args[++index];
|
|
161
163
|
continue;
|
|
162
164
|
}
|
|
165
|
+
if (arg === '--correspondence') {
|
|
166
|
+
if (!args[index + 1] || args[index + 1].startsWith('-')) throw new Error('--correspondence requires a file path.');
|
|
167
|
+
correspondencePath = args[++index];
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
163
170
|
if (arg === '--max-patch-bytes') {
|
|
164
171
|
if (!args[index + 1]) throw new Error(`${arg} requires a positive integer.`);
|
|
165
172
|
maxPatchBytes = Number.parseInt(args[++index], 10);
|
|
@@ -180,7 +187,7 @@ function parseContextArgs(args) {
|
|
|
180
187
|
if (headRef) throw new Error('tour context accepts at most one head ref.');
|
|
181
188
|
headRef = arg;
|
|
182
189
|
}
|
|
183
|
-
return { action: 'context', headRef, baseRef, outputPath, maxPatchBytes, maxTotalPatchBytes };
|
|
190
|
+
return { action: 'context', headRef, baseRef, outputPath, maxPatchBytes, maxTotalPatchBytes, ...(correspondencePath ? { correspondencePath } : {}) };
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
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;
|
package/completions/_bygone
CHANGED
|
@@ -14,6 +14,7 @@ _bygone() {
|
|
|
14
14
|
local -a root_items review_options present_options legacy_options history_options shells tour_actions
|
|
15
15
|
root_items=(
|
|
16
16
|
'review:Review a committed branch against its merge base'
|
|
17
|
+
'correspondence:Import externally produced symbol correspondence evidence'
|
|
17
18
|
'completion:Print shell completion source'
|
|
18
19
|
'present:Open an app-hosted tour of a committed branch range'
|
|
19
20
|
'tour:Validate, compile, or inspect authored change tours'
|
|
@@ -41,6 +42,7 @@ _bygone() {
|
|
|
41
42
|
'-m:Set the branch-review base ref'
|
|
42
43
|
'--main:Set the branch-review base ref'
|
|
43
44
|
'--tour:Use an authored .bygone presentation'
|
|
45
|
+
'--correspondence:Use pinned symbol evidence in present or tour context'
|
|
44
46
|
'-h:Show command help'
|
|
45
47
|
'--help:Show command help'
|
|
46
48
|
)
|
|
@@ -78,10 +80,10 @@ _bygone() {
|
|
|
78
80
|
elif [[ "$words[3]" == "context" ]]; then
|
|
79
81
|
if [[ "$previous" == (--base|-m|--main) ]]; then
|
|
80
82
|
_bygone_git_refs
|
|
81
|
-
elif [[ "$previous" == "--output" || "$previous" == "-o" ]]; then
|
|
83
|
+
elif [[ "$previous" == "--output" || "$previous" == "-o" || "$previous" == "--correspondence" ]]; then
|
|
82
84
|
_files
|
|
83
85
|
else
|
|
84
|
-
compadd -- --base --output -o --max-patch-bytes --max-total-patch-bytes
|
|
86
|
+
compadd -- --base --output -o --max-patch-bytes --max-total-patch-bytes --correspondence
|
|
85
87
|
_bygone_git_refs
|
|
86
88
|
fi
|
|
87
89
|
elif [[ "$words[3]" == "coverage" || "$words[3]" == "validate" || "$words[3]" == "compile" ]]; then
|
|
@@ -97,7 +99,7 @@ _bygone() {
|
|
|
97
99
|
fi
|
|
98
100
|
;;
|
|
99
101
|
present)
|
|
100
|
-
if [[ "$previous" == (--tour) ]]; then
|
|
102
|
+
if [[ "$previous" == (--tour) || "$previous" == "--correspondence" ]]; then
|
|
101
103
|
_files -g '*.bygone' -g '*.bygone.yaml'
|
|
102
104
|
elif [[ "$previous" == (--base|-m|--main) ]]; then
|
|
103
105
|
_bygone_git_refs
|
package/completions/bygone
CHANGED
|
@@ -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 'review 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,11 +28,11 @@ _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 '"$refs" -- "$cur") )
|
|
36
36
|
fi
|
|
37
37
|
else
|
|
38
38
|
COMPREPLY=( $(compgen -W '--json --output -o' -- "$cur") $(compgen -f -- "$cur") )
|
|
@@ -49,13 +49,13 @@ _bygone() {
|
|
|
49
49
|
;;
|
|
50
50
|
present)
|
|
51
51
|
refs="$(_bygone_git_refs)"
|
|
52
|
-
if [[ "$prev" == "--tour" ]]; then
|
|
52
|
+
if [[ "$prev" == "--tour" || "$prev" == "--correspondence" ]]; then
|
|
53
53
|
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
54
54
|
compopt -o filenames 2>/dev/null || true
|
|
55
55
|
elif [[ "$prev" =~ ^(--base|-m|--main)$ ]]; then
|
|
56
56
|
COMPREPLY=( $(compgen -W "$refs" -- "$cur") )
|
|
57
57
|
else
|
|
58
|
-
COMPREPLY=( $(compgen -W '--base -m --main --tour -h --help '"$refs" -- "$cur") )
|
|
58
|
+
COMPREPLY=( $(compgen -W '--base -m --main --tour --correspondence -h --help '"$refs" -- "$cur") )
|
|
59
59
|
fi
|
|
60
60
|
;;
|
|
61
61
|
--branch-diff)
|
package/completions/bygone.fish
CHANGED
|
@@ -5,6 +5,7 @@ function __bygone_git_refs
|
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
complete -c bygone -n '__fish_use_subcommand' -a review -d 'Review a committed branch against its merge base'
|
|
8
|
+
complete -c bygone -n '__fish_use_subcommand' -a correspondence -r -d 'Import externally produced symbol correspondence evidence'
|
|
8
9
|
complete -c bygone -n '__fish_use_subcommand' -a completion -d 'Print shell completion source'
|
|
9
10
|
complete -c bygone -n '__fish_use_subcommand' -a present -d 'Open an app-hosted tour of a committed branch range'
|
|
10
11
|
complete -c bygone -n '__fish_use_subcommand' -a tour -d 'Validate, compile, or inspect authored change tours'
|
|
@@ -30,6 +31,7 @@ complete -c bygone -n '__fish_seen_subcommand_from review' -l base -s m -l main
|
|
|
30
31
|
complete -c bygone -n '__fish_seen_subcommand_from review' -a '(__bygone_git_refs)' -d 'Git ref'
|
|
31
32
|
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
33
|
complete -c bygone -n '__fish_seen_subcommand_from present' -l tour -r -d 'Use an authored .bygone presentation'
|
|
34
|
+
complete -c bygone -l correspondence -r -d 'Use pinned symbol correspondence evidence'
|
|
33
35
|
complete -c bygone -n '__fish_seen_subcommand_from present' -a '(__bygone_git_refs)' -d 'Git ref'
|
|
34
36
|
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
37
|
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'
|