@davmash/bygone 0.8.14 → 0.9.2

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
@@ -2,6 +2,46 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.9.2
6
+
7
+ - Documented the stable v3 tour contract, compatibility policy, and evidence-linked review notes
8
+ - Updated Electron and vulnerable packaging dependencies within their existing major versions
9
+ - Shared the expanded commit navigator across tours, History, and Compare, with explicit draft submission and loaded-panel badges
10
+ - Kept History's commit axis and expanded panels stable across files, with markers for commits that changed the current file
11
+ - Preserved navigator tabs and comparison drafts across mode switches
12
+ - Replaced tour mode and comparison endpoint drop-downs with direct mode buttons and multi-select revision comparison
13
+ - Added two-or-more-panel Compare views ordered from oldest to newest with adjacent diffs
14
+ - Unified Present navigation into one Tour, Files, or Commits navigator
15
+ - Made tours a continuous reading flow through title, chapters, scene overviews, and steps, with a collapsible outline and scene context above each step
16
+ - Added shared workspace tour discovery, editable skill-based v3 authoring prompts, and same-repository transitions into History
17
+ - Clarified tour mode restoration and directory overview scope while preserving first-visit History context
18
+
19
+ macOS desktop downloads are unsigned and not notarized. If Gatekeeper reports
20
+ the app as damaged, run `xattr -cr /Applications/Bygone.app`, then reopen it.
21
+
22
+ ## 0.9.1
23
+
24
+ - Restored authored Final walkthroughs and legacy Final deep links for version 2 deconstructed tours
25
+ - Reused the full revision rail and toolbar in tour History mode while preserving the selected file on first entry
26
+ - Expanded Files and Tour details by default and kept Scene context available alongside the active step
27
+
28
+ ## 0.9.0
29
+
30
+ - Added independently authored Historical and Deconstructed tours with stable per-mode reading positions
31
+ - Added optional scene-level directory overviews with safe repository-relative scoping and authored comparison endpoints
32
+ - Added evidence-linked review notes for concepts, boundaries, tradeoffs, and open questions
33
+ - Introduced source and manifest format v3 for the new tour metadata while preserving v1 and v2 compatibility
34
+
35
+ ## 0.8.16
36
+
37
+ - Clarified authored-tour errors when a document requires a newer format version
38
+ - Made tag-triggered npm release checks fetch repository history needed by checked-in tour fixtures
39
+
40
+ ## 0.8.15
41
+
42
+ - Fixed v2 deconstructed-tour coverage so authored Final-mode walkthrough anchors determine coverage while Explanation assignment completeness remains a separate metric
43
+ - Added distinct Final walkthrough coverage and Explanation assignment indicators to authored tours in the Present sidebar
44
+
5
45
  ## 0.8.14
6
46
 
7
47
  - Reorganized desktop menus around familiar File, Edit, Git, Present, Navigate, View, and Window responsibilities while exposing file, directory, revision, branch, history, tour, and blank-comparison workflows
package/README.md CHANGED
@@ -123,7 +123,7 @@ Change tours can layer an authored, code-connected narrative over the complete g
123
123
 
124
124
  Packaged macOS builds register `.bygone` with the desktop app, so Finder can open a presentation directly. Windows and Linux builds currently require opening the file through Bygone or the `bygone` command; they do not install an operating-system file association.
125
125
 
126
- Agents can validate anchors and structure with `bygone tour validate <file.bygone> --json`, compile a portable manifest with `bygone tour compile`, and retrieve the authoring contract with `bygone tour schema`. See [Generating change tours with an LLM](./docs/generating-change-tours.md).
126
+ Agents can validate anchors and structure with `bygone tour validate <file.bygone> --json`, compile a repository-bound manifest with `bygone tour compile`, and retrieve the authoring contract with `bygone tour schema`. See [Generating change tours with an LLM](./docs/generating-change-tours.md).
127
127
 
128
128
  Install the agent skill:
129
129
 
@@ -210,7 +210,7 @@ Publishing is intentionally explicit:
210
210
  npm run release:publish
211
211
  ```
212
212
 
213
- Run `release:publish` after committing the version and release changes. It aborts unless the worktree is clean and the current branch is `main`, pushes `main`, waits for that exact commit's GitHub Release Check workflow, rebuilds and installs all local artifacts, publishes the npm package, creates a GitHub release for desktop artifacts, and updates the Homebrew tap. It launches npm browser login when needed and pauses immediately before an unpublished version's time-limited passkey challenge. The tap checkout defaults to the sibling `homebrew-bygone` directory; set `BYGONE_HOMEBREW_TAP` to override it. Until Marketplace trusted-publisher policies are publicly configurable, upload the packaged VSIX from the Visual Studio Marketplace publisher page.
213
+ Run `release:publish` after committing the version and release changes. It aborts unless the worktree is clean and the current branch is `main`, pushes `main`, waits for that exact commit's GitHub Release Check workflow, rebuilds and installs all local artifacts, creates a GitHub release for desktop artifacts, and updates the Homebrew tap. The release tag triggers the protected GitHub Actions Release workflow, which publishes the npm package through trusted publishing after approval. The tap checkout defaults to the sibling `homebrew-bygone` directory; set `BYGONE_HOMEBREW_TAP` to override it. Until Marketplace trusted-publisher policies are publicly configurable, upload the packaged VSIX from the Visual Studio Marketplace publisher page.
214
214
 
215
215
  Reinstall the current built artifacts without rebuilding:
216
216
 
@@ -10,6 +10,7 @@ const CLI_SPEC = Object.freeze({
10
10
  'bygone --diff <file1> <file2> <file3> [...]',
11
11
  'bygone --history <path>',
12
12
  'bygone --git-diff <ref1> <ref2> [<ref3>...]',
13
+ 'bygone --read-only <desktop-arguments...>',
13
14
  'bygone review [<head>] [--base <base>]',
14
15
  'bygone -C <directory> <command-or-path> [...]',
15
16
  'bygone present [<head>] [--base <base>] [--tour <file.bygone>]',
@@ -107,6 +108,13 @@ const CLI_SPEC = Object.freeze({
107
108
  description: 'Use an authored .bygone presentation',
108
109
  argument: 'path'
109
110
  },
111
+ {
112
+ id: 'readOnly',
113
+ kind: 'global',
114
+ tokens: ['--read-only'],
115
+ description: 'Open live Explore sources without allowing edits',
116
+ argument: 'none'
117
+ },
110
118
  {
111
119
  id: 'directory',
112
120
  kind: 'global',
@@ -144,13 +152,15 @@ const CLI_SPEC = Object.freeze({
144
152
  'Two positional paths auto-select file diff or directory compare.',
145
153
  'Three or more positional paths auto-select multi-panel file diff or multi-directory compare.',
146
154
  '`--git-diff` accepts branches, tags, SHAs, HEAD~1, stash@{0}, INDEX, and WORKTREE.',
155
+ 'In `--git-diff`, committed refs and INDEX are read-only; WORKTREE (also WORKDIR or WORKINGTREE) is live and writable.',
156
+ '`--read-only` prevents editing every live file or WORKTREE panel in the launched Explore session.',
147
157
  '`review` compares merge-base(BASE,HEAD) with HEAD and detects the default base when omitted.',
148
158
  '`present` turns the same range into an app-hosted, ordered change tour.',
149
159
  '`-C <directory>` resolves relative paths and Git refs from that directory without changing the shell working directory.',
150
160
  '`tour validate` resolves every authored anchor; add `--json` for agent-readable output.',
151
- '`tour compile` writes a portable manifest to stdout or `--output`; `tour schema` prints its source schema.',
161
+ '`tour compile` writes a repository-bound manifest to stdout or `--output`; `tour schema` prints its source schema.',
152
162
  '`tour context` emits compact, structured Git evidence for an LLM without invoking a model.',
153
- '`tour coverage` reports referenced changed hunks and author-declared explanation depth.',
163
+ '`tour coverage` reports authored walkthrough anchor coverage and author-declared explanation depth; for v2 tours it reads Final mode and reports deconstructed Explanation assignment separately, while `--minimum-coverage` gates the walkthrough metric.',
154
164
  '`--branch-diff` is retained as an alias for `review`.',
155
165
  '`completion` prints a completion script for Zsh, Bash, or Fish.',
156
166
  'In the standalone app, drop 1 file for history, 2 files/directories for compare, or 3+ matching paths for multi-panel compare.'
@@ -75,7 +75,7 @@ ${legacyOptions}
75
75
  ${historyOptions}
76
76
  )
77
77
  shells=('zsh:Z shell' 'bash:Bash' 'fish:Fish shell')
78
- tour_actions=('context:Build structured change evidence' 'coverage:Measure authored tour coverage' 'validate:Validate source and anchors' 'compile:Compile a portable manifest' 'schema:Print the source schema')
78
+ tour_actions=('context:Build structured change evidence' 'coverage:Measure authored tour coverage' 'validate:Validate source and anchors' 'compile:Compile a repository-bound manifest' 'schema:Print the source schema')
79
79
 
80
80
  if (( CURRENT == 2 )); then
81
81
  _describe 'bygone command' root_items
package/cli/present.js CHANGED
@@ -3,9 +3,11 @@ 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 { buildTourAuthoringCoverage, buildTourCoverageReport } = require('../out/tourCoverage.js');
6
7
  const { buildTourWindowTitle } = require('../out/windowTitle.js');
7
8
  const { tokenMatches } = require('./commandSpec.js');
8
- const { loadTourSource } = require('./tourFile.js');
9
+ const { loadTourSource, parseTourSourceText, buildManifestForTourSource } = require('./tourFile.js');
10
+ const { createTourHistory } = require('./tourHistory.js');
9
11
 
10
12
  const MIME_TYPES = new Map([
11
13
  ['.css', 'text/css; charset=utf-8'],
@@ -19,20 +21,24 @@ const MIME_TYPES = new Map([
19
21
 
20
22
  async function startPresentation(args, cwd, packageRoot, options = {}) {
21
23
  const { headRef, baseRef, tourPath, explicitHeadRef } = parsePresentArgs(args);
22
- const story = process.env.BYGONE_TOUR_STORY
24
+ const story = !options.ignoreEnvironment && process.env.BYGONE_TOUR_STORY
23
25
  ? parseChangeTourStory(JSON.parse(readFileSync(path.resolve(cwd, process.env.BYGONE_TOUR_STORY), 'utf8')))
24
26
  : undefined;
25
27
  const source = tourPath ? loadTourSource(cwd, tourPath).source : undefined;
26
- const manifest = buildChangeTourManifest(cwd, {
28
+ const builtManifest = buildChangeTourManifest(cwd, {
27
29
  headRef: explicitHeadRef || source?.range?.head || headRef,
28
30
  baseRef: baseRef || source?.range?.base,
29
- title: process.env.BYGONE_TOUR_TITLE,
30
- sourceUrl: process.env.BYGONE_TOUR_SOURCE_URL,
31
+ title: options.ignoreEnvironment ? undefined : process.env.BYGONE_TOUR_TITLE,
32
+ sourceUrl: options.ignoreEnvironment ? undefined : process.env.BYGONE_TOUR_SOURCE_URL,
31
33
  story,
32
34
  source
33
35
  });
36
+ const manifest = source
37
+ ? { ...builtManifest, authoringCoverage: buildTourAuthoringCoverage(buildTourCoverageReport(cwd, source)) }
38
+ : builtManifest;
39
+ const history = createTourHistory(manifest);
34
40
  const serializedManifest = `${JSON.stringify(manifest, null, 2)}\n`;
35
- const outputPath = process.env.BYGONE_TOUR_OUTPUT;
41
+ const outputPath = options.ignoreEnvironment ? undefined : process.env.BYGONE_TOUR_OUTPUT;
36
42
  if (outputPath) {
37
43
  const resolvedOutput = path.resolve(cwd, outputPath);
38
44
  mkdirSync(path.dirname(resolvedOutput), { recursive: true });
@@ -43,9 +49,75 @@ async function startPresentation(args, cwd, packageRoot, options = {}) {
43
49
  const tourWindowTitle = buildTourWindowTitle(manifest, 'Bygone');
44
50
  const presenterIndexPath = path.join(packageRoot, 'web', 'index.html');
45
51
  let presenterIndexTemplate;
52
+ // Uploaded documents remain immutable, scoped to this existing repository,
53
+ // and addressed independently so opening one cannot mutate another tab.
54
+ const uploadedTours = new Map();
46
55
 
47
56
  const server = createServer((request, response) => {
48
57
  const requestUrl = new URL(request.url || '/', 'http://127.0.0.1');
58
+ if (requestUrl.pathname === '/tour/open') {
59
+ if (request.method !== 'POST') return respondJson(response, 405, { error: 'Method not allowed' });
60
+ if (!isSameOriginLoopbackRequest(request)) return respondJson(response, 403, { error: 'Forbidden' });
61
+ if (uploadedTours.size >= 16) return respondJson(response, 409, { error: 'This presentation has reached its open-document limit. Start a new presentation to open more documents.' });
62
+ let body = '';
63
+ request.setEncoding('utf8');
64
+ request.on('data', (chunk) => {
65
+ body += chunk;
66
+ if (Buffer.byteLength(body, 'utf8') > 2 * 1024 * 1024) { respondJson(response, 413, { error: 'Request too large' }); request.destroy(); }
67
+ });
68
+ request.on('end', () => {
69
+ if (response.writableEnded) return;
70
+ try {
71
+ const uploadedSource = parseTourSourceText(JSON.parse(body).source);
72
+ const loaded = buildManifestForTourSource(cwd, uploadedSource);
73
+ const id = String(uploadedTours.size + 1);
74
+ uploadedTours.set(id, { manifest: loaded, history: createTourHistory(loaded) });
75
+ respondJson(response, 200, { manifestUrl: `/loaded/${id}/tour.json`, repository: loaded.repository?.root || cwd, range: loaded.range });
76
+ } catch (error) { respondJson(response, 400, { error: `Invalid tour for this repository: ${error.message}` }); }
77
+ });
78
+ return;
79
+ }
80
+ const loadedMatch = requestUrl.pathname.match(/^\/loaded\/(\d+)\/(tour\.json|history\/(?:list|diff|compare|compare-many|revisions))$/);
81
+ const loadedTour = loadedMatch ? uploadedTours.get(loadedMatch[1]) : null;
82
+ if (loadedMatch && !loadedTour) return respondJson(response, 404, { error: 'Loaded document not found' });
83
+ if (loadedTour && loadedMatch[2] === 'tour.json') return respondJson(response, 200, loadedTour.manifest);
84
+ const requestHistory = loadedTour ? loadedTour.history : history;
85
+ if (loadedMatch) requestUrl.pathname = `/${loadedMatch[2]}`;
86
+ if (requestUrl.pathname === '/history/list' || requestUrl.pathname === '/history/diff'
87
+ || requestUrl.pathname === '/history/compare' || requestUrl.pathname === '/history/compare-many'
88
+ || requestUrl.pathname === '/history/revisions') {
89
+ if (request.method !== 'POST') return respondJson(response, 405, { error: 'Method not allowed' });
90
+ if (!isSameOriginLoopbackRequest(request)) return respondJson(response, 403, { error: 'Forbidden' });
91
+ if (!requestHistory) return respondJson(response, 404, { error: 'History requires a version 2 tour.' });
92
+ let body = '';
93
+ request.setEncoding('utf8');
94
+ request.on('data', chunk => {
95
+ body += chunk;
96
+ if (body.length > 8192) {
97
+ respondJson(response, 413, { error: 'Request too large' });
98
+ request.destroy();
99
+ }
100
+ });
101
+ request.on('end', () => {
102
+ if (response.writableEnded) return;
103
+ try {
104
+ const input = JSON.parse(body);
105
+ const result = requestUrl.pathname === '/history/list'
106
+ ? requestHistory.list(input)
107
+ : requestUrl.pathname === '/history/diff'
108
+ ? requestHistory.diff(input)
109
+ : requestUrl.pathname === '/history/compare'
110
+ ? requestHistory.compare(input)
111
+ : requestUrl.pathname === '/history/compare-many'
112
+ ? requestHistory.compareMany(input)
113
+ : requestHistory.revisions(input);
114
+ respondJson(response, 200, result);
115
+ } catch {
116
+ respondJson(response, 400, { error: 'Could not load history for this file and revision.' });
117
+ }
118
+ });
119
+ return;
120
+ }
49
121
  if (requestUrl.pathname === '/narration/claim') {
50
122
  if (request.method !== 'POST') {
51
123
  respond(response, 405, 'Method not allowed');
@@ -94,7 +166,7 @@ async function startPresentation(args, cwd, packageRoot, options = {}) {
94
166
  });
95
167
  createReadStream(targetPath).on('error', () => respond(response, 500, 'Read failed')).pipe(response);
96
168
  });
97
- const requestedPort = readPort(process.env.BYGONE_TOUR_PORT);
169
+ const requestedPort = readPort(options.ignoreEnvironment ? undefined : process.env.BYGONE_TOUR_PORT);
98
170
  await new Promise((resolve, reject) => {
99
171
  server.once('error', reject);
100
172
  server.listen(requestedPort, '127.0.0.1', resolve);
@@ -194,6 +266,16 @@ function respond(response, statusCode, message) {
194
266
  response.end(message);
195
267
  }
196
268
 
269
+ function respondJson(response, statusCode, body) {
270
+ if (!response.headersSent) {
271
+ response.writeHead(statusCode, {
272
+ 'Content-Type': 'application/json; charset=utf-8',
273
+ 'Cache-Control': 'no-store'
274
+ });
275
+ }
276
+ response.end(JSON.stringify(body));
277
+ }
278
+
197
279
  function isSameOriginLoopbackRequest(request) {
198
280
  const origin = request.headers.origin;
199
281
  const host = request.headers.host;
package/cli/tour.js CHANGED
@@ -27,7 +27,8 @@ function runTourCommand(args, cwd, packageRoot, output = process.stdout) {
27
27
  const report = buildTourCoverageReport(cwd, source);
28
28
  output.write(options.json ? `${JSON.stringify(report, null, 2)}\n` : renderCoverageReport(report));
29
29
  if (options.minimumCoverage !== undefined && report.totals.coveragePercent < options.minimumCoverage) {
30
- throw new Error(`Tour coverage ${report.totals.coveragePercent}% is below the required ${options.minimumCoverage}%.`);
30
+ const label = report.version === 2 ? 'Final walkthrough coverage' : 'Tour coverage';
31
+ throw new Error(`${label} ${report.totals.coveragePercent}% is below the required ${options.minimumCoverage}%.`);
31
32
  }
32
33
  return { action: 'coverage', sourcePath: resolvedPath, report };
33
34
  }
@@ -98,10 +99,20 @@ function parseTourArgs(args) {
98
99
  }
99
100
 
100
101
  function renderCoverageReport(report) {
102
+ const coverageLabel = report.version === 2 ? 'Final walkthrough coverage' : 'Tour coverage';
101
103
  const lines = [
102
- `Tour coverage: ${report.totals.coveredUnits}/${report.totals.includedUnits} hunks (${report.totals.coveragePercent}%)`,
104
+ `${coverageLabel}: ${report.totals.coveredUnits}/${report.totals.includedUnits} hunks (${report.totals.coveragePercent}%)`,
103
105
  `Depth: ${report.depth.mentioned} mentioned · ${report.depth.explained} explained · ${report.depth.contextualized} contextualized`
104
106
  ];
107
+ if (report.version === 2) {
108
+ for (const assignment of report.explanationAssignments) {
109
+ const suffix = report.explanationAssignments.length > 1 ? ` (${assignment.sceneId})` : '';
110
+ lines.push(
111
+ `Explanation assignment${suffix}: ${assignment.completeUnits}/${assignment.totalUnits} hunks `
112
+ + `(${assignment.assignmentPercent}%; ${assignment.assignedUnits} assigned, ${assignment.excludedUnits} excluded)`
113
+ );
114
+ }
115
+ }
105
116
  if (report.totals.excludedUnits > 0) lines.push(`Excluded: ${report.totals.excludedUnits}/${report.totals.originalUnits} hunks`);
106
117
  for (const file of report.files.filter((entry) => entry.uncoveredHunks.length > 0)) {
107
118
  lines.push(`${file.path}: ${file.uncoveredHunks.length} uncovered (${file.uncoveredHunks.join(', ')})`);
package/cli/tourFile.js CHANGED
@@ -48,6 +48,14 @@ function readTourSourceDocument(sourcePath, options = {}) {
48
48
  throw new Error(`Could not decode Bygone source ${sourceLabel} as UTF-8: ${errorMessage(error)}`, { cause: error });
49
49
  }
50
50
 
51
+ return parseTourSourceText(text, sourceLabel, { maxBytes });
52
+ }
53
+
54
+ function parseTourSourceText(text, sourceLabel = 'uploaded tour', options = {}) {
55
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_TOUR_SOURCE_BYTES;
56
+ if (typeof text !== 'string' || Buffer.byteLength(text, 'utf8') > maxBytes || text.includes('\0')) {
57
+ throw new Error('Invalid tour text or source size limit exceeded.');
58
+ }
51
59
  let documents;
52
60
  try {
53
61
  documents = loadYamlDocuments(text);
@@ -84,5 +92,6 @@ module.exports = {
84
92
  DEFAULT_MAX_TOUR_SOURCE_BYTES,
85
93
  buildManifestForTourSource,
86
94
  loadTourSource,
95
+ parseTourSourceText,
87
96
  readTourSourceDocument
88
97
  };