@tanishqkancharla/diffmap 0.3.0 → 0.4.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/src/host/home.md CHANGED
@@ -1,156 +1,202 @@
1
1
  # diffmap
2
2
 
3
- A viewer for specs and walkthroughs. Call stacks, mermaid that links into source, and a Diff panel. This page is one — `parseViewerDocument` plus `ViewerApp`, same path as `diffmap serve`.
3
+ diffmap turns a Markdown file into a page like this one. Your coding agent writes it to show you a change, before and after building it: diagrams you can play, call stacks of what changes, and links that open the code.
4
4
 
5
- TOC on the left, article here, Diff on the right. Click a stack row or a mermaid node.
6
-
7
- ## Install the skill
5
+ Install the skill, then ask your agent to write a diffmap for a change you are planning or for a branch it just built:
8
6
 
9
7
  ```sh
10
8
  npx skills add tanishqkancharla/diffmap --skill generate-diffmap
11
9
  ```
12
10
 
13
- That keeps a living spec in `specs/<name>.md`. Same file for the life of the work: what’s already in the tree, and what’s still planned. Don’t start a second walkthrough later.
14
-
15
- ## Serve locally
16
-
17
- ```sh
18
- npx @tanishqkancharla/diffmap serve specs/<name>.md
19
- ```
11
+ The agent keeps the spec in `specs/<name>.md`, serves it on your machine, and can share it as a link like this page.
20
12
 
21
- Bare `npx @tanishqkancharla/diffmap specs/<name>.md` is the same as `serve`. `list` shows running viewers — reuse the URL instead of starting a second server.
13
+ Everything below is an example spec: diffmap's own features, one per phase. Each phase uses the feature it describes. Click a call stack row, a diagram node, or anything marked ↗ to open its code.
22
14
 
23
- **Close server** is only on that local page. This host has no process to stop.
15
+ ## Show a change, not a wall of text
24
16
 
25
- ## Share
17
+ A coding agent explains a change in a long message. You read it, then open each file it names to check that the code says the same thing.
26
18
 
27
- ### Gist
19
+ A diffmap puts the explanation next to the code. Each phase says what changes for you, then shows the change as call stacks and diagrams whose parts open the code they name.
28
20
 
29
- ```sh
30
- npx @tanishqkancharla/diffmap share specs/<name>.md
31
- ```
21
+ ### ✅ Phase 1: Call stacks that open the code
32
22
 
33
- Prints `https://diffmap.dev/g/<gistId>` (optional `/<file.md>`). Secret gist: unlisted, not private. Anyone with the id can read it. Does not start a local server.
23
+ **Today**
34
24
 
35
- ### GitHub PR vs commit
25
+ The agent describes the call path in a paragraph. You search for each function it names and guess how they connect.
36
26
 
37
- A spec on a public PR:
27
+ **Proposed**
38
28
 
39
- `https://diffmap.dev/<owner>/<repo>/pull/<n>/specs/<name>.md`
29
+ The agent draws the path as data flowing through calls: each row reads `result = call(inputs)`, `+` rows are new, `−` rows go away, and each row opens its code. This is how diffmap draws the tree below:
40
30
 
41
- Pinned commit:
31
+ ```callstack
32
+ fence = parseFence(lang, source) [[src/parseFence.ts#parseFence]]
33
+ +└── lines = parseCallStack(source) # one row per line, with its links [[src/annotations.ts#parseCallStack]]
34
+ view = Fence({ fence }) [[src/viewer/Fence.tsx#Fence]]
35
+ -└── return CodeBlock(fence.source) # the tree as plain text
36
+ +├── if fence.kind === "callstack":
37
+ +│ └── return CallStackDiff(fence.lines) # rows with + and − marks [[src/viewer/CallStackDiff.tsx#CallStackDiff]]
38
+ +└── return CodeBlock(fence.source) # other languages stay pseudocode
39
+ ```
42
40
 
43
- `https://diffmap.dev/<owner>/<repo>/commit/<sha>/specs/<name>.md`
41
+ ### ✅ Phase 2: Diagrams you can click
44
42
 
45
- The pin is one SHA. The markdown and every `[[path]]`, mermaid `%% ref`, and excerpt load from that commit, not `main`. Commit references load their diffs from the GitHub API. A PR number only chooses the SHA (head at load, then pin). Refresh stays on the commit URL.
43
+ **Today**
46
44
 
47
- ## What the fences are
45
+ The agent draws a flow in ASCII boxes. The boxes do not link to anything, and they break when a label gets long.
48
46
 
49
- ### System flow
47
+ **Proposed**
50
48
 
51
- Click a node. `%% ref` is a mermaid comment; `[[path]]`, `[[path#symbol]]`, and `[[<sha>:path#L12-L30]]` are the same references as on a stack row.
49
+ Mermaid diagrams render in the page, and each node or arrow can open its code. Here is what happens when you click a reference:
52
50
 
53
51
  ```mermaid
54
52
  flowchart LR
55
- Spec[Living spec] --> Serve[serve]
56
- Spec --> Share[share]
57
- Spec --> Pin[GitHub pin]
58
- %% ref node:Spec [[src/parseViewer.ts#parseViewerDocument]]
59
- %% ref node:Serve [[src/cli.ts]]
60
- %% ref node:Share [[src/share.ts#shareMarkdownFile]]
61
- %% ref node:Pin [[src/github/route.ts]]
62
- %% ref edge:0 [[1257fe8:src/cli.ts#L49-L50]]
53
+ Click[Click a reference] --> Find[useSourceReference]
54
+ Find -->|diffmap serve| Local[Read the repo with git]
55
+ Find -->|diffmap.dev| GitHub[fetchPinnedBlob]
56
+ Local --> Panel[SourceDiffPanel]
57
+ GitHub --> Panel
58
+ %% ref node:Find [[src/viewer/sourceNavigation.ts#useSourceReference]]
59
+ %% ref node:GitHub [[src/github/fetchRepo.ts#fetchPinnedBlob]]
60
+ %% ref node:Panel [[src/viewer/SourceDiffPanel.tsx#SourceDiffPanel]]
61
+ %% ref edge:1 [[src/viewer/sourceNavigation.ts#requestSource]]
63
62
  ```
64
63
 
65
- ````md
66
- ```mermaid
67
- flowchart LR
68
- Spec[Living spec] --> Serve[serve]
69
- %% ref node:Serve [[src/cli.ts]]
64
+ ### ✅ Phase 3: Services you can play
65
+
66
+ **Today**
67
+
68
+ A sequence diagram shows the messages between services, but not what each message changes. You keep the state of every service in your head.
69
+
70
+ **Proposed**
71
+
72
+ A service diagram shows each service with its state, then steps through the events. Press **Play** to watch `diffmap serve` open a spec, or hover a dotted name to read what it is:
73
+
74
+ ```services
75
+ service Laptop
76
+ > Your machine. serve reads files here and uploads nothing.
77
+ service Agent "coding agent"
78
+ > Writes the spec and runs diffmap for you.
79
+ service CLI "diffmap serve" [[src/serve.ts#startServer]]
80
+ > A small server that reads your repo with git.
81
+ state url = none
82
+ service Repo "your repo"
83
+ > Holds the spec and the commits it links to.
84
+ service Browser
85
+ service Page "the spec" [[src/viewer/ViewerApp.tsx#ViewerApp]]
86
+ state page = empty
87
+ service Panel "Diff panel" [[src/viewer/SourceDiffPanel.tsx#SourceDiffPanel]]
88
+ > Opens beside the spec and shows the code a reference names.
89
+ state shows = none
90
+
91
+ step "The agent serves the spec"
92
+ Agent -> CLI: serve specs/search.md
93
+ step "serve checks every commit the spec links to"
94
+ CLI -> Repo: readDocumentChanges [[src/git.ts#readDocumentChanges]]
95
+ > A wrong SHA or line range fails here, so you never meet a broken link.
96
+ step "serve prints a URL for you"
97
+ CLI -> Agent: localhost URL
98
+ CLI.url = localhost:4321
99
+ step "You open the page"
100
+ Page -> CLI: GET /
101
+ CLI -> Page: spec
102
+ Page.page = rendered
103
+ step "You click a call stack row"
104
+ Panel -> CLI: /__diffmap/source
105
+ CLI -> Repo: git show
106
+ > Reads the file at the commit the row names, not the working tree.
107
+ step "The Diff panel shows the code"
108
+ CLI -> Panel: lines
109
+ Panel.shows = code
70
110
  ```
71
- ````
72
111
 
73
- ### Call stacks
112
+ ### ✅ Phase 4: Open code from prose
74
113
 
75
- `-` is current, `+` is proposed. After a phase lands, commit it and retarget `[[path]]` to the commit: `[[<sha>:path#L12-L30]]`. Line numbers are the file after the commit.
114
+ **Today**
76
115
 
77
- ```callstack
78
- ViewerApp
79
- -└── DoneButton # a loud Done control
80
- +└── CloseServerButton # quiet, local mode only [[dae6b98:src/viewer/ViewerApp.tsx#L219-L227]] [[dae6b98:src/viewer/CloseServerButton.tsx#L8-L16]]
81
- └── DiffButton
82
- ```
116
+ A paragraph names a function as plain code, like `shareMarkdownFile`. You search the repo to find it.
83
117
 
84
- ```callstack
85
- shareMarkdownFile [[src/share.ts#shareMarkdownFile]]
86
- -└── print the gist URL
87
- +└── gistViewerUrl # https://diffmap.dev/g/<id> [[d14ae1a:src/share.ts#L55-L59]]
88
- ```
118
+ **Proposed**
89
119
 
90
- ### Changes
120
+ A function named in a paragraph opens its code too: [[src/share.ts#shareMarkdownFile]].
91
121
 
92
- A `changes` fence names a commit. It renders a card with every file that commit changed. **Review** opens the Diff panel on all of them, with a sticky header per file; a row jumps to its file. An empty body means uncommitted changes.
122
+ This phase is done, so it ends the way a finished phase does: with a card for the commit that built it, and references pinned to that commit. Pinned links show the code as the commit left it, even after the file moves on.
93
123
 
94
124
  ```changes
95
- dae6b98
125
+ 6e4b5d3
96
126
  ```
97
127
 
98
- Commit references must name a file the commit changed, and the range must fit the file after the commit. `serve` and `share` check this at startup.
99
-
100
- ### File excerpts
101
-
102
- `start:end:path`. Locally the server reads the workspace file. Here the bundled file is sliced the same way.
103
-
104
- ```58:72:src/cli.ts
105
- const cli = Cli.create("diffmap", {
106
- description: "Serve a spec locally, or share it as a gist",
107
- version: packageVersion,
108
- args: serveArgs,
109
- options: serveOptions,
110
- output: serveOutput,
111
- hint: "`serve <spec.md>` runs the local viewer. `share <spec.md>` publishes a secret gist. `update` (or `--update`) installs the latest diffmap.",
112
- examples: [
113
- {
114
- args: { file: "spec.md" },
115
- description: "Same as `diffmap serve spec.md`",
116
- },
117
- ],
118
- run: runServe,
119
- })
128
+ ```callstack
129
+ nodes = fromNode(text) [[6e4b5d3:src/parseViewer.ts#L318-L326]]
130
+ -└── return [{ type: "text", value: text }] # prose stays plain text
131
+ +└── return proseText(text) [[6e4b5d3:src/parseViewer.ts#L329-L353]]
132
+ + ├── reference = parseReference(match) # each link in double brackets
133
+ + └── return [...textBefore, { type: "reference", annotation }]
134
+ element = renderNode(node) [[6e4b5d3:src/viewer/ComarkView.tsx#L64-L68]]
135
+ +└── if node.type === "reference":
136
+ + └── return InlineReference(node.annotation) # a code button that opens the Diff panel [[6e4b5d3:src/viewer/ComarkView.tsx#L173-L189]]
120
137
  ```
121
138
 
122
- ### HTML
139
+ ### ✅ Phase 5: Share a spec as a link
123
140
 
124
- Local `serve` trusts `html` in the file you pointed at. This host does not. Gist and GitHub pages are the same — no `dangerouslySetInnerHTML` from untrusted markdown.
141
+ **Today**
125
142
 
126
- ```html
127
- <aside>If this renders, the host leaked HTML.</aside>
128
- ```
143
+ To read a spec, a teammate needs diffmap and a checkout of your repo.
129
144
 
130
- ## How a spec stays alive
145
+ **Proposed**
131
146
 
132
- Write `specs/<name>.md`. Serve it while you plan. When the plan changes, edit this file. When a phase lands, commit it, add a `changes` fence, and retarget the links to the commit so the Diff panel opens the change, not just the file.
147
+ One command uploads the spec as a secret GitHub gist and prints a link anyone can open:
133
148
 
134
- ### Phase 1: Spec and serve
149
+ ```sh
150
+ npx @tanishqkancharla/diffmap share specs/search.md
151
+ # prints https://diffmap.dev/g/<gistId>
152
+ ```
135
153
 
136
- ```callstack
137
- generate-diffmap
138
- └── specs/name.md
139
- └── diffmap serve [[src/cli.ts]]
154
+ The page that opens has no git and no checkout, so the gist carries what the spec reads: a snapshot of each commit it links, and the current contents of each file it quotes. That is why this page can show code at all:
155
+
156
+ ```mermaid
157
+ flowchart LR
158
+ subgraph Gist
159
+ Spec[spec.md]
160
+ Changes[diffmap-changes.json]
161
+ Files[diffmap-files.json]
162
+ end
163
+ Spec -->|Markdown| Page[The spec page]
164
+ Changes -->|commits| Cards[Changes cards and commit links]
165
+ Files -->|file contents| Excerpts[Excerpts and plain links]
166
+ %% ref node:Changes [[2f96051:src/share.ts#L44-L53]]
167
+ %% ref node:Files [[2f96051:src/share.ts#L105-L125]]
168
+ %% ref node:Excerpts [[src/host/files.ts#HostFilesContext]]
140
169
  ```
141
170
 
142
- - [x] Install the skill.
143
- - [x] Keep one living spec.
144
- - [x] `diffmap serve` / `list`.
171
+ ```changes
172
+ 2f96051
173
+ ```
145
174
 
146
- ### Phase 2: Share it
175
+ `share` builds both snapshots before it calls `gh`, so diffmap never stores a GitHub token:
147
176
 
148
177
  ```callstack
149
- specs/name.md
150
- -└── localhost only
151
- +├── diffmap share [[d14ae1a:src/share.ts#L55-L59]]
152
- +└── GitHub PR URL [[src/github/route.ts]]
178
+ result = shareMarkdownFile({ filePath, workspaceRoot }) [[2f96051:src/share.ts#L26-L103]]
179
+ ├── parsed = parseViewerDocument(source)
180
+ ├── if parsed is an error: return parsed # a broken diagram stops here
181
+ ├── changes = readDocumentChanges(workspaceRoot, parsed) # each linked commit
182
+ +├── files = readDocumentFiles(workspaceRoot, parsed) # each quoted file [[2f96051:src/share.ts#L105-L125]]
183
+ ├── await gh("auth", "status")
184
+ -├── created = await gh("gist", "create", spec, changes)
185
+ +├── created = await gh("gist", "create", spec, changes, files)
186
+ └── return { gistId, viewerUrl: gistViewerUrl(gistId) }
153
187
  ```
154
188
 
155
- - [x] Gist URL for a snapshot.
156
- - [x] PR URL when the spec lives in the repo (pin to one SHA).
189
+ This excerpt is one of the quoted files, so a shared copy of this page shows it too:
190
+
191
+ ```54:63:src/share.ts
192
+ // It has no files either, so excerpts and references without a commit read these.
193
+ const files = readDocumentFiles(input.workspaceRoot, parsed);
194
+ if (files instanceof Error) return files;
195
+
196
+ const auth = await runGh(["auth", "status"]);
197
+ if (auth instanceof DiffmapShareError) return auth;
198
+
199
+ const args = ["gist", "create"];
200
+ if (input.isPublic) args.push("--public");
201
+ args.push("--desc", extractTitle(source), filePath);
202
+ ```
@@ -32,7 +32,7 @@ test("homepage markdown is a real viewer document", async () => {
32
32
  assert.equal(kinds.has("callstack"), true);
33
33
  assert.equal(kinds.has("changes"), true);
34
34
  assert.equal(kinds.has("file"), true);
35
- assert.equal(kinds.has("html"), true);
35
+ assert.equal(kinds.has("services"), true);
36
36
 
37
37
  const mermaid = fences.find((fence) => fence.kind === "mermaid");
38
38
  assert.notEqual(mermaid, undefined);
@@ -68,16 +68,15 @@ test("homepage markdown is a real viewer document", async () => {
68
68
  );
69
69
 
70
70
  const excerpt = fences.find(
71
- (fence) => fence.kind === "file" && fence.path === "src/cli.ts",
71
+ (fence) => fence.kind === "file" && fence.path === "src/share.ts",
72
72
  );
73
73
  assert.equal(excerpt?.kind, "file");
74
74
  if (excerpt?.kind !== "file") return;
75
- const cliPath = path.join(path.dirname(homePath), "..", "cli.ts");
76
- const cli = await fs.readFile(cliPath, "utf8");
77
- const lines = cli.split(/\n/);
75
+ const sharePath = path.join(path.dirname(homePath), "..", "share.ts");
76
+ const share = await fs.readFile(sharePath, "utf8");
77
+ const lines = share.split(/\n/);
78
78
  const slice = lines.slice(excerpt.start - 1, excerpt.end).join("\n");
79
79
  assert.equal(lines[excerpt.start - 1]?.trim().length !== 0, true);
80
- assert.match(slice, /packageVersion/);
81
- assert.match(slice, /\}\)\s*$/);
80
+ assert.match(slice, /"gist", "create"/);
82
81
  assert.equal(excerpt.source, slice);
83
82
  });
package/src/parseFence.ts CHANGED
@@ -1,18 +1,43 @@
1
1
  import { parseCallStack, type SourceAnnotation } from "./annotations.js";
2
2
  import { WORKING, type Rev } from "./changes.js";
3
+ import { parseCodeNotes, type CodeNote } from "./codeNotes.js";
3
4
  import {
4
5
  parseDiagramAnnotations,
5
6
  type DiagramAnnotation,
6
7
  } from "./diagramAnnotations.js";
8
+ import { parseServiceDiagram, type ServicesParse } from "./services.js";
7
9
 
8
10
  export type Fence =
9
11
  | { kind: "mermaid"; source: string; annotations: DiagramAnnotation[] }
12
+ | { kind: "services"; source: string; parsed: ServicesParse }
10
13
  | { kind: "html"; source: string }
11
- | { kind: "callstack"; source: string; lines: SourceAnnotation[] }
12
- | { kind: "changes"; rev: Rev; source: string }
14
+ | {
15
+ kind: "callstack";
16
+ /** Rows with trailing note lines removed. */
17
+ source: string;
18
+ lines: SourceAnnotation[];
19
+ /** 1-based rows from the info string, as in ```callstack {2}. */
20
+ highlights: number[];
21
+ notes: CodeNote[];
22
+ }
23
+ | {
24
+ kind: "changes";
25
+ rev: Rev;
26
+ /** Paths to review first, in order; other changed files fold away. */
27
+ review: string[];
28
+ source: string;
29
+ }
13
30
  | { kind: "file"; path: string; start: number; end: number; source: string }
14
31
  | { kind: "diff"; path: string | undefined; source: string }
15
- | { kind: "code"; lang: string; source: string };
32
+ | {
33
+ kind: "code";
34
+ lang: string;
35
+ /** Code with trailing note lines removed. */
36
+ source: string;
37
+ /** 1-based lines from the info string, as in ```ts {2,4-6}. */
38
+ highlights: number[];
39
+ notes: CodeNote[];
40
+ };
16
41
 
17
42
  const fileRefPattern = /^(\d+):(\d+):(.+)$/;
18
43
  const diffPathPattern = /^diff:(.+)$/;
@@ -31,7 +56,11 @@ export function pathFromDiffSource(source: string) {
31
56
  return undefined;
32
57
  }
33
58
 
34
- export function parseFence(lang: string, source: string): Fence {
59
+ export function parseFence(
60
+ lang: string,
61
+ source: string,
62
+ highlights: number[] = [],
63
+ ): Fence {
35
64
  const trimmed = source.replace(/\n$/, "");
36
65
  if (lang === "mermaid")
37
66
  return {
@@ -39,19 +68,22 @@ export function parseFence(lang: string, source: string): Fence {
39
68
  source: trimmed,
40
69
  annotations: parseDiagramAnnotations(trimmed),
41
70
  };
42
- if (lang === "html") return { kind: "html", source: trimmed };
43
- if (lang === "callstack")
71
+ if (lang === "services")
44
72
  return {
45
- kind: "callstack",
73
+ kind: "services",
46
74
  source: trimmed,
47
- lines: parseCallStack(trimmed),
75
+ parsed: parseServiceDiagram(trimmed),
48
76
  };
77
+ if (lang === "html") return { kind: "html", source: trimmed };
78
+ if (lang === "callstack") return callStackFence(trimmed, highlights);
49
79
 
80
+ // The first line is the rev (blank for uncommitted); each later line is a path to review.
50
81
  if (lang === "changes") {
51
- const rev = trimmed.trim();
82
+ const [rev = "", ...paths] = trimmed.split("\n").map((line) => line.trim());
52
83
  return {
53
84
  kind: "changes",
54
85
  rev: rev.length === 0 ? WORKING : rev,
86
+ review: paths.filter((line) => line.length > 0),
55
87
  source: trimmed,
56
88
  };
57
89
  }
@@ -69,13 +101,7 @@ export function parseFence(lang: string, source: string): Fence {
69
101
 
70
102
  const diffPath = diffPathPattern.exec(lang);
71
103
  if (lang === "diff" || diffPath !== null) {
72
- if (isCallStackSource(trimmed)) {
73
- return {
74
- kind: "callstack",
75
- source: trimmed,
76
- lines: parseCallStack(trimmed),
77
- };
78
- }
104
+ if (isCallStackSource(trimmed)) return callStackFence(trimmed, highlights);
79
105
  return {
80
106
  kind: "diff",
81
107
  path: diffPath === null ? pathFromDiffSource(trimmed) : diffPath[1],
@@ -83,9 +109,23 @@ export function parseFence(lang: string, source: string): Fence {
83
109
  };
84
110
  }
85
111
 
112
+ const { code, notes } = parseCodeNotes(trimmed);
86
113
  return {
87
114
  kind: "code",
88
115
  lang: lang.length === 0 ? "text" : lang,
89
- source: trimmed,
116
+ source: code,
117
+ highlights,
118
+ notes,
119
+ };
120
+ }
121
+
122
+ function callStackFence(source: string, highlights: number[]): Fence {
123
+ const { code, notes } = parseCodeNotes(source);
124
+ return {
125
+ kind: "callstack",
126
+ source: code,
127
+ lines: parseCallStack(code),
128
+ highlights,
129
+ notes,
90
130
  };
91
131
  }