@a9n-shoji/rvw 0.4.0 → 0.4.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
@@ -5,6 +5,15 @@
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.4.1] - 2026-09-01
9
+
10
+ ### Added
11
+
12
+ - repository fileから、そのfileをNode sourceとして参照するStructureをrename-awareに逆引きし、対応Nodeへ
13
+ 閲覧状態を維持したまま移動できる機能
14
+ - Structure全体を現在のNode配置のままSVGまたは2倍基準のPNGへエクスポートする機能。focus、近傍、
15
+ viewportにかかわらず全Node、全Relation、全Edge labelを含み、画面の閲覧状態を変更しない
16
+
8
17
  ## [0.4.0] - 2026-09-01
9
18
 
10
19
  ### Added
package/README.md CHANGED
@@ -181,6 +181,10 @@ viewerではfocusがある時に1-hop / 2-hopへ絞り、Allでは全Node / Edge
181
181
  保存しません。同じsubjectの更新は同じURIを完全置換し、存続するIDの位置を保ちます。別subjectは新しい
182
182
  Structureとしてpublishします。
183
183
 
184
+ Structure headerの`Export`から、現在のNode配置を保った図全体をstandalone SVGまたは2倍基準のPNGとして
185
+ 保存できます。focusや1-hop / 2-hopで画面上に絞り込んでいても、出力には全Node、全Relation、全Edge labelが
186
+ 含まれ、pan / zoomや選択状態は持ち込みません。
187
+
184
188
  ## Codex / Claude Code Skills
185
189
 
186
190
  アプリ本体からローカルSkillをインストールします。一度のinstallで、コメント処理用の`rvw`、
package/dist/cli.mjs CHANGED
@@ -20187,7 +20187,7 @@ import path5 from "node:path";
20187
20187
  import { TextDecoder as TextDecoder2 } from "node:util";
20188
20188
 
20189
20189
  // src/shared/constants.ts
20190
- var APP_VERSION = "0.4.0";
20190
+ var APP_VERSION = "0.4.1";
20191
20191
  var PROTOCOL_VERSION = 4;
20192
20192
  var VIEWER_ID_HEADER = "x-rvw-viewer-id";
20193
20193
  var VIEWER_OPEN_LEASE_HEADER = "x-rvw-viewer-open-lease";
@@ -28477,6 +28477,34 @@ function structureSourceAnchor(structure, locator) {
28477
28477
  const edge = structure.edges.find((candidate) => candidate.id === locator.edgeId);
28478
28478
  return edge?.anchors[locator.anchorIndex] ?? null;
28479
28479
  }
28480
+ function compareStableIds(left, right) {
28481
+ if (left === right) return 0;
28482
+ return left < right ? -1 : 1;
28483
+ }
28484
+ function closestMatchingStructureNode(structure, matchingNodeIds) {
28485
+ if (matchingNodeIds.size === 0) return null;
28486
+ const distances = /* @__PURE__ */ new Map([[structure.originNodeId, 0]]);
28487
+ const neighbors = new Map(structure.nodes.map((node2) => [node2.id, /* @__PURE__ */ new Set()]));
28488
+ for (const edge of structure.edges) {
28489
+ neighbors.get(edge.from)?.add(edge.to);
28490
+ neighbors.get(edge.to)?.add(edge.from);
28491
+ }
28492
+ const queue = [structure.originNodeId];
28493
+ for (let index2 = 0; index2 < queue.length; index2 += 1) {
28494
+ const nodeId = queue[index2];
28495
+ const distance = distances.get(nodeId);
28496
+ for (const neighbor of neighbors.get(nodeId) ?? []) {
28497
+ if (distances.has(neighbor)) continue;
28498
+ distances.set(neighbor, distance + 1);
28499
+ queue.push(neighbor);
28500
+ }
28501
+ }
28502
+ return structure.nodes.filter((node2) => matchingNodeIds.has(node2.id)).sort((left, right) => {
28503
+ const leftDistance = distances.get(left.id) ?? Number.POSITIVE_INFINITY;
28504
+ const rightDistance = distances.get(right.id) ?? Number.POSITIVE_INFINITY;
28505
+ return leftDistance - rightDistance || compareStableIds(left.id, right.id);
28506
+ })[0] ?? null;
28507
+ }
28480
28508
 
28481
28509
  // src/domain/source-excerpt.ts
28482
28510
  var SOURCE_EXCERPT_CONTEXT_LINES = 20;
@@ -41445,6 +41473,18 @@ function assertLinePair(startLine, endLine) {
41445
41473
  }
41446
41474
  var codeReferenceIdPattern = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
41447
41475
  var walkthroughDiagramNodePattern = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
41476
+ function structureSummary(structure) {
41477
+ return {
41478
+ id: structure.id,
41479
+ ref: structure.ref,
41480
+ pullRequestId: structure.pullRequestId,
41481
+ sourceOid: structure.sourceOid,
41482
+ title: structure.title,
41483
+ scope: structure.scope,
41484
+ createdAt: structure.createdAt,
41485
+ updatedAt: structure.updatedAt
41486
+ };
41487
+ }
41448
41488
  var mermaidIdentifierPattern = /[A-Za-z][A-Za-z0-9_-]{0,63}/g;
41449
41489
  var mermaidEdgePattern = /[<|o*x]*[-.=~]{2,}[|o*x>]*/g;
41450
41490
  var mermaidClassShorthandPattern = /:::[A-Za-z][A-Za-z0-9_-]*(?:,[A-Za-z][A-Za-z0-9_-]*)*/g;
@@ -42509,6 +42549,71 @@ var RvwService = class {
42509
42549
  this.getPullRequest(pullRequestId);
42510
42550
  return this.database.listStructures(pullRequestId);
42511
42551
  }
42552
+ async listFileStructureReferences(pullRequestId, targetSourceOid, targetPath) {
42553
+ const pullRequest = this.getPullRequest(pullRequestId);
42554
+ assertCodeReferencePath(targetPath);
42555
+ const structures = this.database.listStructures(pullRequestId).flatMap((summary) => {
42556
+ const structure = this.database.getStructure(summary.id);
42557
+ return structure?.pullRequestId === pullRequestId ? [structure] : [];
42558
+ });
42559
+ await this.assertCommitAvailable(pullRequest, targetSourceOid);
42560
+ const targetPaths = new Set(
42561
+ (await this.git.tree(pullRequest.localRepositoryPath, targetSourceOid)).map(
42562
+ (entry) => entry.path
42563
+ )
42564
+ );
42565
+ if (!targetPaths.has(targetPath)) return [];
42566
+ const successorIndexes = /* @__PURE__ */ new Map();
42567
+ const successorIndex = (sourceOid) => {
42568
+ const key2 = JSON.stringify([sourceOid, targetSourceOid]);
42569
+ const cached2 = successorIndexes.get(key2);
42570
+ if (cached2) return cached2;
42571
+ const index2 = this.git.changedFilesWithCopies(pullRequest.localRepositoryPath, sourceOid, targetSourceOid).then((changes) => {
42572
+ const successors = /* @__PURE__ */ new Map();
42573
+ for (const candidate of changes) {
42574
+ if (!candidate.status.startsWith("R") && !candidate.status.startsWith("C") || candidate.oldPath === null || candidate.newPath === null) {
42575
+ continue;
42576
+ }
42577
+ const paths = successors.get(candidate.oldPath) ?? /* @__PURE__ */ new Set();
42578
+ paths.add(candidate.newPath);
42579
+ successors.set(candidate.oldPath, paths);
42580
+ }
42581
+ return successors;
42582
+ });
42583
+ successorIndexes.set(key2, index2);
42584
+ return index2;
42585
+ };
42586
+ const resolvePath = (sourceOid, sourcePath) => {
42587
+ if (targetPaths.has(sourcePath)) return Promise.resolve(sourcePath);
42588
+ if (sourceOid === targetSourceOid) return Promise.resolve(null);
42589
+ return successorIndex(sourceOid).then((index2) => {
42590
+ const successors = index2.get(sourcePath);
42591
+ if (successors?.size !== 1) return null;
42592
+ const successor = [...successors][0];
42593
+ return targetPaths.has(successor) ? successor : null;
42594
+ });
42595
+ };
42596
+ const references = [];
42597
+ for (const structure of structures) {
42598
+ const matchingNodeIds = /* @__PURE__ */ new Set();
42599
+ await Promise.all(
42600
+ structure.nodes.map(async (node2) => {
42601
+ if (!node2.anchor) return;
42602
+ const resolvedPath = await resolvePath(structure.sourceOid, node2.anchor.path);
42603
+ if (resolvedPath === targetPath) matchingNodeIds.add(node2.id);
42604
+ })
42605
+ );
42606
+ const targetNode = closestMatchingStructureNode(structure, matchingNodeIds);
42607
+ if (!targetNode) continue;
42608
+ references.push({
42609
+ structure: structureSummary(structure),
42610
+ targetNodeId: targetNode.id,
42611
+ targetNodeLabel: targetNode.label,
42612
+ matchingNodeCount: matchingNodeIds.size
42613
+ });
42614
+ }
42615
+ return references;
42616
+ }
42512
42617
  listStructuresByReference(reference) {
42513
42618
  const pullRequest = this.resolveStoredPullRequest(reference);
42514
42619
  return { pullRequest, structures: this.database.listStructures(pullRequest.id) };
@@ -42867,38 +42972,15 @@ var RvwService = class {
42867
42972
  path: reference.path
42868
42973
  });
42869
42974
  const latestHeadOid = pullRequest.latestHeadOid;
42870
- let latestPath = reference.path;
42871
- let latestDocument;
42872
- if (sourceOid === latestHeadOid) {
42873
- latestDocument = sourceDocument;
42874
- } else {
42875
- latestDocument = await this.getDocument({
42876
- kind: "repository-file",
42877
- pullRequestId: pullRequest.id,
42878
- sourceOid: latestHeadOid,
42879
- path: reference.path
42880
- });
42881
- if (latestDocument.availability === "missing") {
42882
- const successorPaths = [
42883
- ...new Set(
42884
- (await this.git.changedFilesWithCopies(
42885
- pullRequest.localRepositoryPath,
42886
- sourceOid,
42887
- latestHeadOid
42888
- )).filter(
42889
- (candidate) => (candidate.status.startsWith("R") || candidate.status.startsWith("C")) && candidate.oldPath === reference.path && candidate.newPath !== null
42890
- ).map((candidate) => candidate.newPath)
42891
- )
42892
- ];
42893
- latestPath = successorPaths.length === 1 ? successorPaths[0] : null;
42894
- latestDocument = latestPath === null ? null : await this.getDocument({
42895
- kind: "repository-file",
42896
- pullRequestId: pullRequest.id,
42897
- sourceOid: latestHeadOid,
42898
- path: latestPath
42899
- });
42900
- }
42901
- }
42975
+ const resolvedLatestFile = await this.resolveSourceFileAtCommit(
42976
+ pullRequest,
42977
+ sourceOid,
42978
+ reference.path,
42979
+ latestHeadOid,
42980
+ sourceDocument
42981
+ );
42982
+ const latestPath = resolvedLatestFile?.path ?? null;
42983
+ const latestDocument = resolvedLatestFile?.document ?? null;
42902
42984
  const latestFileExists = latestDocument !== null && latestDocument.availability !== "missing";
42903
42985
  const latestFileDisplayable = latestDocument?.availability === "available";
42904
42986
  const mappedRange = reference.startLine === null || reference.endLine === null ? latestFileDisplayable ? null : void 0 : sourceDocument.availability === "available" && latestDocument?.availability === "available" ? mapUnchangedLineRange(
@@ -42955,6 +43037,38 @@ var RvwService = class {
42955
43037
  document: sourceDocument
42956
43038
  };
42957
43039
  }
43040
+ async resolveSourceFileAtCommit(pullRequest, sourceOid, sourcePath, targetSourceOid, sourceDocument) {
43041
+ const directDocument = sourceOid === targetSourceOid ? sourceDocument : await this.getDocument({
43042
+ kind: "repository-file",
43043
+ pullRequestId: pullRequest.id,
43044
+ sourceOid: targetSourceOid,
43045
+ path: sourcePath
43046
+ });
43047
+ if (directDocument.availability !== "missing") {
43048
+ return { path: sourcePath, document: directDocument };
43049
+ }
43050
+ if (sourceOid === targetSourceOid) return null;
43051
+ const successorPaths = [
43052
+ ...new Set(
43053
+ (await this.git.changedFilesWithCopies(
43054
+ pullRequest.localRepositoryPath,
43055
+ sourceOid,
43056
+ targetSourceOid
43057
+ )).filter(
43058
+ (candidate) => (candidate.status.startsWith("R") || candidate.status.startsWith("C")) && candidate.oldPath === sourcePath && candidate.newPath !== null
43059
+ ).map((candidate) => candidate.newPath)
43060
+ )
43061
+ ];
43062
+ if (successorPaths.length !== 1) return null;
43063
+ const successorPath = successorPaths[0];
43064
+ const successorDocument = await this.getDocument({
43065
+ kind: "repository-file",
43066
+ pullRequestId: pullRequest.id,
43067
+ sourceOid: targetSourceOid,
43068
+ path: successorPath
43069
+ });
43070
+ return successorDocument.availability === "missing" ? null : { path: successorPath, document: successorDocument };
43071
+ }
42958
43072
  getWalkthroughByUri(uri) {
42959
43073
  const walkthrough = this.database.getWalkthrough(parseWalkthroughUri(uri));
42960
43074
  if (!walkthrough) {
@@ -47916,6 +48030,18 @@ function createApp(service, options) {
47916
48030
  structures: service.listStructures(context.req.param("id"))
47917
48031
  })
47918
48032
  );
48033
+ app.get("/api/pull-requests/:id/structure-references", async (context) => {
48034
+ const sourceOid = oidQuery(context.req.query("sourceOid"), "sourceOid");
48035
+ const filePath = requiredQuery(context.req.query("path"), "path");
48036
+ return context.json({
48037
+ ok: true,
48038
+ references: await service.listFileStructureReferences(
48039
+ context.req.param("id"),
48040
+ sourceOid,
48041
+ filePath
48042
+ )
48043
+ });
48044
+ });
47919
48045
  app.get(
47920
48046
  "/api/pull-requests/:id/structures/:structureId",
47921
48047
  (context) => context.json({