@a9n-shoji/rvw 0.4.0-beta.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,33 @@
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
+
17
+ ## [0.4.0] - 2026-09-01
18
+
19
+ ### Added
20
+
21
+ - 一つの具体的なbehavior / processing flowをsource-anchored origin、stable Node / Edge ID、exact source
22
+ anchorで提示し、1/2-hop / All、focus、pan / zoom / dragから探索できるproduction `Structure` document
23
+ - Structureのpublish / get / whole-value update / confirmation付きdeleteを行うCLI、Agent socket capability、
24
+ SQLite migration、HTTP API、Codex / Claude Code共通の`rvw-structure` Skill
25
+ - 変更表示の原文と行番号を保ちながら、追加・削除fileを除く空白だけの変更を`…`メニューから非表示にする
26
+ `Hide Whitespace`設定
27
+
28
+ ### Changed
29
+
30
+ - 固定サイズのStructure Node内をscroll可能にし、表記ごとの余白、canvasのpan / zoom感度、source anchorの
31
+ fallback、stale判定、明示的な再解決を改善
32
+ - light / dark themeのdiff canvas、文字、追加・削除行、gutter、inline emphasisの配色をGitHubへ合わせる
33
+ - Hide Whitespace切替時に現在のsource lineをviewportへ保持し、空白差分がすべて隠れた場合は解除方法を表示
34
+
8
35
  ## [0.4.0-beta.0] - 2026-08-31
9
36
 
10
37
  ### 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-beta.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";
@@ -28466,10 +28466,45 @@ function placeMutableDocumentComment(anchor, currentText) {
28466
28466
  return range ? { outdated: false, range } : { outdated: true, range: null };
28467
28467
  }
28468
28468
 
28469
- // src/domain/walkthrough-reference.ts
28470
- function walkthroughReferenceFingerprint(sourceOid, reference) {
28469
+ // src/domain/source-reference.ts
28470
+ function sourceAnchorFingerprint(sourceOid, reference) {
28471
28471
  return JSON.stringify([sourceOid, reference.path, reference.startLine, reference.endLine]);
28472
28472
  }
28473
+ function structureSourceAnchor(structure, locator) {
28474
+ if (locator.kind === "node") {
28475
+ return structure.nodes.find((node2) => node2.id === locator.nodeId)?.anchor ?? null;
28476
+ }
28477
+ const edge = structure.edges.find((candidate) => candidate.id === locator.edgeId);
28478
+ return edge?.anchors[locator.anchorIndex] ?? null;
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
+ }
28473
28508
 
28474
28509
  // src/domain/source-excerpt.ts
28475
28510
  var SOURCE_EXCERPT_CONTEXT_LINES = 20;
@@ -41438,6 +41473,18 @@ function assertLinePair(startLine, endLine) {
41438
41473
  }
41439
41474
  var codeReferenceIdPattern = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
41440
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
+ }
41441
41488
  var mermaidIdentifierPattern = /[A-Za-z][A-Za-z0-9_-]{0,63}/g;
41442
41489
  var mermaidEdgePattern = /[<|o*x]*[-.=~]{2,}[|o*x>]*/g;
41443
41490
  var mermaidClassShorthandPattern = /:::[A-Za-z][A-Za-z0-9_-]*(?:,[A-Za-z][A-Za-z0-9_-]*)*/g;
@@ -42502,6 +42549,71 @@ var RvwService = class {
42502
42549
  this.getPullRequest(pullRequestId);
42503
42550
  return this.database.listStructures(pullRequestId);
42504
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
+ }
42505
42617
  listStructuresByReference(reference) {
42506
42618
  const pullRequest = this.resolveStoredPullRequest(reference);
42507
42619
  return { pullRequest, structures: this.database.listStructures(pullRequest.id) };
@@ -42796,7 +42908,7 @@ var RvwService = class {
42796
42908
  }
42797
42909
  return walkthrough;
42798
42910
  }
42799
- async walkthroughReferenceFallbackTarget(pullRequest, sourceOid, filePath) {
42911
+ async sourceReferenceFallbackTarget(pullRequest, sourceOid, filePath) {
42800
42912
  const diffBaseOid = await this.git.firstParent(pullRequest.localRepositoryPath, sourceOid);
42801
42913
  if (!diffBaseOid) {
42802
42914
  return {
@@ -42827,46 +42939,48 @@ var RvwService = class {
42827
42939
  status: 404
42828
42940
  });
42829
42941
  }
42830
- const referenceFingerprint = walkthroughReferenceFingerprint(walkthrough.sourceOid, reference);
42942
+ const referenceFingerprint = sourceAnchorFingerprint(walkthrough.sourceOid, reference);
42943
+ return await this.resolveSourceAnchor(
42944
+ pullRequest,
42945
+ walkthrough.sourceOid,
42946
+ reference,
42947
+ referenceFingerprint
42948
+ );
42949
+ }
42950
+ async resolveStructureSource(pullRequestId, structureId, locator) {
42951
+ const pullRequest = this.getPullRequest(pullRequestId);
42952
+ const structure = this.getStructure(pullRequestId, structureId);
42953
+ const anchor = structureSourceAnchor(structure, locator);
42954
+ if (!anchor) {
42955
+ throw new RvwError("NOT_FOUND", "Structure\u306E\u53C2\u7167\u5143claim\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002", {
42956
+ status: 404
42957
+ });
42958
+ }
42959
+ const resolution = await this.resolveSourceAnchor(
42960
+ pullRequest,
42961
+ structure.sourceOid,
42962
+ anchor,
42963
+ sourceAnchorFingerprint(structure.sourceOid, anchor)
42964
+ );
42965
+ return { ...resolution, resolvedAnchor: anchor };
42966
+ }
42967
+ async resolveSourceAnchor(pullRequest, sourceOid, reference, referenceFingerprint) {
42831
42968
  const sourceDocument = await this.getDocument({
42832
42969
  kind: "repository-file",
42833
- pullRequestId,
42834
- sourceOid: walkthrough.sourceOid,
42970
+ pullRequestId: pullRequest.id,
42971
+ sourceOid,
42835
42972
  path: reference.path
42836
42973
  });
42837
42974
  const latestHeadOid = pullRequest.latestHeadOid;
42838
- let latestPath = reference.path;
42839
- let latestDocument;
42840
- if (walkthrough.sourceOid === latestHeadOid) {
42841
- latestDocument = sourceDocument;
42842
- } else {
42843
- latestDocument = await this.getDocument({
42844
- kind: "repository-file",
42845
- pullRequestId,
42846
- sourceOid: latestHeadOid,
42847
- path: reference.path
42848
- });
42849
- if (latestDocument.availability === "missing") {
42850
- const successorPaths = [
42851
- ...new Set(
42852
- (await this.git.changedFilesWithCopies(
42853
- pullRequest.localRepositoryPath,
42854
- walkthrough.sourceOid,
42855
- latestHeadOid
42856
- )).filter(
42857
- (candidate) => (candidate.status.startsWith("R") || candidate.status.startsWith("C")) && candidate.oldPath === reference.path && candidate.newPath !== null
42858
- ).map((candidate) => candidate.newPath)
42859
- )
42860
- ];
42861
- latestPath = successorPaths.length === 1 ? successorPaths[0] : null;
42862
- latestDocument = latestPath === null ? null : await this.getDocument({
42863
- kind: "repository-file",
42864
- pullRequestId,
42865
- sourceOid: latestHeadOid,
42866
- path: latestPath
42867
- });
42868
- }
42869
- }
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;
42870
42984
  const latestFileExists = latestDocument !== null && latestDocument.availability !== "missing";
42871
42985
  const latestFileDisplayable = latestDocument?.availability === "available";
42872
42986
  const mappedRange = reference.startLine === null || reference.endLine === null ? latestFileDisplayable ? null : void 0 : sourceDocument.availability === "available" && latestDocument?.availability === "available" ? mapUnchangedLineRange(
@@ -42879,7 +42993,7 @@ var RvwService = class {
42879
42993
  if (resolvedToLatest && latestPath !== null && latestDocument !== null) {
42880
42994
  return {
42881
42995
  outcome: "latest",
42882
- anchorSourceOid: walkthrough.sourceOid,
42996
+ anchorSourceOid: sourceOid,
42883
42997
  latestHeadOid,
42884
42998
  referenceFingerprint,
42885
42999
  target: {
@@ -42896,9 +43010,9 @@ var RvwService = class {
42896
43010
  document: latestDocument
42897
43011
  };
42898
43012
  }
42899
- const targetFile = await this.walkthroughReferenceFallbackTarget(
43013
+ const targetFile = await this.sourceReferenceFallbackTarget(
42900
43014
  pullRequest,
42901
- walkthrough.sourceOid,
43015
+ sourceOid,
42902
43016
  reference.path
42903
43017
  );
42904
43018
  const latestFile = latestFileExists && latestPath !== null ? {
@@ -42911,7 +43025,7 @@ var RvwService = class {
42911
43025
  } : null;
42912
43026
  return {
42913
43027
  outcome: "source-fallback",
42914
- anchorSourceOid: walkthrough.sourceOid,
43028
+ anchorSourceOid: sourceOid,
42915
43029
  latestHeadOid,
42916
43030
  referenceFingerprint,
42917
43031
  target: {
@@ -42923,6 +43037,38 @@ var RvwService = class {
42923
43037
  document: sourceDocument
42924
43038
  };
42925
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
+ }
42926
43072
  getWalkthroughByUri(uri) {
42927
43073
  const walkthrough = this.database.getWalkthrough(parseWalkthroughUri(uri));
42928
43074
  if (!walkthrough) {
@@ -47567,6 +47713,7 @@ var editCommentPostSchema = external_exports.object({
47567
47713
 
47568
47714
  // src/server/app.ts
47569
47715
  var oidSchema = external_exports.string().regex(GIT_OBJECT_ID_PATTERN);
47716
+ var nonnegativeIndexSchema = external_exports.coerce.number().int().nonnegative();
47570
47717
  var svgAssetContentSecurityPolicy = "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; sandbox";
47571
47718
  function requiredQuery(value, name) {
47572
47719
  if (!value) throw new RvwError("INVALID_INPUT", `${name} query\u304C\u5FC5\u8981\u3067\u3059\u3002`);
@@ -47575,6 +47722,23 @@ function requiredQuery(value, name) {
47575
47722
  function oidQuery(value, name) {
47576
47723
  return oidSchema.parse(requiredQuery(value, name));
47577
47724
  }
47725
+ function structureSourceLocator(query) {
47726
+ if (query.locatorKind === "node") {
47727
+ return { kind: "node", nodeId: requiredQuery(query.nodeId, "nodeId") };
47728
+ }
47729
+ if (query.locatorKind === "edge") {
47730
+ const anchorIndex = nonnegativeIndexSchema.safeParse(query.anchorIndex);
47731
+ if (!anchorIndex.success) {
47732
+ throw new RvwError("INVALID_INPUT", "anchorIndex query\u306F0\u4EE5\u4E0A\u306E\u6574\u6570\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
47733
+ }
47734
+ return {
47735
+ kind: "edge",
47736
+ edgeId: requiredQuery(query.edgeId, "edgeId"),
47737
+ anchorIndex: anchorIndex.data
47738
+ };
47739
+ }
47740
+ throw new RvwError("INVALID_INPUT", "locatorKind query\u306Fnode\u307E\u305F\u306Fedge\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
47741
+ }
47578
47742
  function isWriteMethod(method) {
47579
47743
  return method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
47580
47744
  }
@@ -47866,6 +48030,18 @@ function createApp(service, options) {
47866
48030
  structures: service.listStructures(context.req.param("id"))
47867
48031
  })
47868
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
+ });
47869
48045
  app.get(
47870
48046
  "/api/pull-requests/:id/structures/:structureId",
47871
48047
  (context) => context.json({
@@ -47873,6 +48049,21 @@ function createApp(service, options) {
47873
48049
  structure: service.getStructure(context.req.param("id"), context.req.param("structureId"))
47874
48050
  })
47875
48051
  );
48052
+ app.get("/api/pull-requests/:id/structures/:structureId/anchors/resolve", async (context) => {
48053
+ return context.json({
48054
+ ok: true,
48055
+ resolution: await service.resolveStructureSource(
48056
+ context.req.param("id"),
48057
+ context.req.param("structureId"),
48058
+ structureSourceLocator({
48059
+ locatorKind: context.req.query("locatorKind"),
48060
+ nodeId: context.req.query("nodeId"),
48061
+ edgeId: context.req.query("edgeId"),
48062
+ anchorIndex: context.req.query("anchorIndex")
48063
+ })
48064
+ )
48065
+ });
48066
+ });
47876
48067
  app.delete("/api/pull-requests/:id/structures/:structureId", async (context) => {
47877
48068
  const input = structureDeleteSchema.parse(await context.req.json());
47878
48069
  return context.json({