Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ file contents and publication tokens:

## Documentation

## Documentation

- [Usage](https://github.com/minipai/diffwalk/blob/main/docs/usage.md): manual authoring, commands, YAML format, and review service settings.
- [Agent skill](skills/diffwalk/SKILL.md): the workflow your agent follows.
- [Development](https://github.com/minipai/diffwalk/blob/main/docs/development.md): local development, testing, and self-hosting.
15 changes: 10 additions & 5 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,14 @@ exact corresponding diffs in a deliberate order.
- `src/format/types.ts`: independent TypeScript types used by internal logic.
- `src/format/schema.ts`: boundary-only Zod schemas for the machine-owned capture and the author-edited
explanations, plus the version 1 ExplainDocument and its optional attribution metadata.
- `src/authoring/git.ts`: captures staged, unstaged, deleted, renamed, and untracked UTF-8
files from an immutable Git base commit, optionally reading the index or limiting the
capture to named paths.
- `src/authoring/capture.ts`: derives change blocks and the content `captureId`, and
materializes exact section patches from capture plus explanations.
- `src/authoring/git.ts`: captures staged, unstaged, deleted, renamed, and untracked text
and binary files from an immutable Git base commit, optionally reading the index or
limiting the capture to named paths. A text side keeps its UTF-8 content; a binary side
keeps only its byte size and SHA-256 content hash.
- `src/authoring/capture.ts`: derives text change blocks, file-level binary change blocks,
and the content `captureId`, and materializes exact section patches from capture plus
explanations. Text-only files hash into `captureId` exactly as before binary support, so
existing captures and their explanations stay matched.
- `src/cli/explanations.ts`: strict safe YAML 1.2 parsing into the explanations schema.
- `src/cli/commands/`: each command owns its option schema and validates inputs before
calling internal logic. `cli.ts` registers commands and forwards their arguments.
Expand All @@ -138,6 +141,8 @@ exact corresponding diffs in a deliberate order.
- `src/report/index.ts`: atomic report writes and client-bundle loading.
- `src/report/render.ts`: the one report shell, embedded-data escaping, and shell styles,
rendered with inlined assets for the offline file or linked assets for the hosted page.
A binary change renders as a metadata card carrying its path, status, and before/after
sizes instead of a diff.
- `src/cli/service.ts`: review service configuration and origin checks.
- `src/publish/client.ts`: publish, update, and unpublish requests, and adding the Git user name as
`metadata.publishedBy` without mutating the authoring files.
Expand Down
34 changes: 29 additions & 5 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ diffwalk inspect
diffwalk changes
```

`inspect` captures staged, unstaged, and untracked UTF-8 file changes relative to
`HEAD`. It creates a walk under `.diffwalk/` containing:
`inspect` captures staged, unstaged, and untracked file changes relative to `HEAD`,
including binary assets. It creates a walk under `.diffwalk/` containing:

- `capture.json` — generated capture data. Do not edit it.
- `explanations.yaml` — the file you edit to explain and order the changes.
Expand Down Expand Up @@ -122,11 +122,33 @@ diffwalk file src/a.ts --after # full captured new file

`changes --json` includes change blocks, not full file snapshots.

Text changes list line coordinates and their before/after blocks. Binary changes keep
only an identity, never the bytes: `changes` shows the status and each existing side's
byte size, `changes --json` and `change <id>` add the content hash, and
`file <path> --before/--after` prints that metadata instead of bytes. A file that
switches between text and binary keeps both side identities. Binary change IDs are
assigned to steps like any other change.

## Binary assets

Diffwalk represents binary files at file level. It records the path, change status,
file modes, and each existing side's byte size and SHA-256 content hash, and assigns
the file a change ID rendered as a metadata card instead of a textual patch. Because
the hash participates in `captureId`, two binary revisions of the same size are still
distinguished, and `check` re-validates the captured metadata before it passes.

Diffwalk treats a file side as binary when its bytes contain a NUL byte or do not
decode as UTF-8, so a non-UTF-8 text file is captured as an opaque card rather than
shown as text. Diffwalk does not generate binary patches, preview images, or decode
binary contents, so a binary card shows identity rather than the changed bytes.
Symbolic links and non-file Git paths are still rejected at capture time.

## Validation

`diffwalk check` rejects stale capture IDs, malformed YAML, unknown change IDs,
unexplained changes, and blocks that cannot produce an exact patch. It reports
section, step, change, and file counts, including repeated changes.
unexplained changes, and blocks that cannot produce an exact patch or whose binary
metadata no longer matches the captured file. It reports section, step, change, and
file counts, including repeated changes.

Explanations use YAML 1.2. Custom tags, duplicate keys, anchors, and aliases are not
allowed; `yes` and `on` remain strings.
Expand All @@ -143,6 +165,9 @@ HTML reviews are standalone files that work offline with JavaScript enabled. JSO
export produces an ExplainDocument (format version 1) for integrations or archiving;
its default filename is `diffwalk.json` in the current walk.

Both exports, and the hosted review, keep a binary change's card with its path, status,
and before/after sizes.

`text` and `summary` support Markdown and inline HTML. Use inline SVG or `data:` URIs
for images; hosted reviews block remote image URLs. Authored HTML is not sanitized,
so only preview, export, or publish explanations you or a trusted agent authored.
Expand Down Expand Up @@ -228,4 +253,3 @@ diffwalk check --input path/to/capture.json --explanations path/to/explanations.

`view`, `export`, and `publish` accept the same options. With explicit input files,
`publish` saves `published.json` alongside the authoring pair.

136 changes: 136 additions & 0 deletions fixtures/report-preview.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,142 @@
}
]
},
{
"title": "Explain binary assets with metadata cards",
"steps": [
{
"text": "### Binary changes carry identity, not text\n\nA binary asset has no lines to diff, so Diffwalk records each existing side's kind, byte size, and SHA-256 hash instead of decoding the bytes, then draws a metadata card in place of a patch. The card names the path, the status, and the before/after identity, so a reviewer can still separate an addition from a modification and can confirm that an edit changed the bytes even when it kept the byte size.\n\nNo raw bytes reach the page. The steps below show an addition, a same-size modification, a deletion, a rename, and both directions of a text/binary transition."
},
{
"text": "The added asset ships beside an ordinary code change, so one step shows both card types at once. The text file still renders its patch; the PNG gets a card. Both `changes` entries belong to this step, and the review map counts one step with two files.",
"diff": "diff --git a/src/report/binary-card.ts b/src/report/binary-card.ts\n--- a/src/report/binary-card.ts\n+++ b/src/report/binary-card.ts\n@@ -1,3 +1,4 @@\n export function cardKind(): string {\n- return \"patch\"\n+ return \"metadata\"\n }\n+export const cardDelayMs = 0\n",
"binary": [
{
"kind": "binary",
"id": "change-051",
"path": "assets/expressions-v2/afraid.box.png",
"status": "added",
"oldMode": "000000",
"newMode": "100644",
"after": {
"kind": "binary",
"size": 48211,
"hash": "4bf34f69726bfcda81145cd78a11d71581a58c69f7aeddcd19dec2497b007baa"
}
}
],
"changes": [
"change-050",
"change-051"
]
},
{
"text": "An edit can keep the byte size and still change the asset. Both sides report **51,204 B**, but the before and after hashes differ, which is why the content hash participates in capture identity. A deletion is the other extreme: only the before side exists, so the after row reads `absent`.",
"binary": [
{
"kind": "binary",
"id": "change-052",
"path": "assets/expressions-v2/curious.box.png",
"status": "modified",
"oldMode": "100644",
"newMode": "100644",
"before": {
"kind": "binary",
"size": 51204,
"hash": "d30d18a489265bf6234144b7f0b6a613392307c3da8ebe3fb80aa565dded9c36"
},
"after": {
"kind": "binary",
"size": 51204,
"hash": "3bc9aee6796dfde881c424f1e0ec7945fd107843416429d055dda2a3904a96c8"
}
},
{
"kind": "binary",
"id": "change-053",
"path": "assets/expressions-v2/retired.box.png",
"status": "deleted",
"oldMode": "100644",
"newMode": "000000",
"before": {
"kind": "binary",
"size": 30122,
"hash": "be59af98df030c4738c2e74d332177b3948551248ec8fd91457ab910ec369828"
}
}
],
"changes": [
"change-052",
"change-053"
]
},
{
"text": "A rename moves the path without changing the bytes, so the card shows the old path, an arrow, and the new path. A transition changes the kind of the file itself: a readable config becomes a compiled blob, and a packed asset becomes readable source. Diffwalk keeps both sides in either direction, so the card shows what the file was and what it became.",
"binary": [
{
"kind": "binary",
"id": "change-054",
"path": "assets/expressions-v2/excited.box.png",
"status": "renamed",
"oldPath": "assets/expressions-v1/excited.box.png",
"oldMode": "100644",
"newMode": "100644",
"before": {
"kind": "binary",
"size": 44987,
"hash": "3bc0977e7fc8fa58e79e43cc6e0d330c957586d0a555e68006d6d0bfaef8042c"
},
"after": {
"kind": "binary",
"size": 44987,
"hash": "3bc0977e7fc8fa58e79e43cc6e0d330c957586d0a555e68006d6d0bfaef8042c"
}
},
{
"kind": "binary",
"id": "change-055",
"path": "assets/loader.conf",
"status": "modified",
"oldMode": "100644",
"newMode": "100644",
"before": {
"kind": "text",
"size": 812,
"hash": "bbf096f2ae6671689f175738f04246ad1b23b7bfc3749fd4e1b04647393b33a5"
},
"after": {
"kind": "binary",
"size": 6104,
"hash": "37418d6597fe0b07e72d44abc55a7162e8ee9371594c988ba55cf30ce1f6ee95"
}
},
{
"kind": "binary",
"id": "change-056",
"path": "assets/theme.tokens",
"status": "modified",
"oldMode": "100644",
"newMode": "100644",
"before": {
"kind": "binary",
"size": 256,
"hash": "e4cd60c730974ce82775d6526d807c126616302a66281e5916dfe4fa5e4a56de"
},
"after": {
"kind": "text",
"size": 318,
"hash": "3b084f8f3fe6b12ee44c9eccd1ff29f7eeac5efe8c8f43b09815b76329769eed"
}
}
],
"changes": [
"change-054",
"change-055",
"change-056"
]
}
]
},
{
"title": "Review a code-only step without an explanation heading",
"steps": [
Expand Down
15 changes: 13 additions & 2 deletions skills/diffwalk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ explained.
- `diffwalk changes` for a concise summary, or `diffwalk changes --json` for structured IDs, paths, coordinates, before, and after.
- `diffwalk change <id>` to read one captured change block.
- `diffwalk file <path> --before` / `diffwalk file <path> --after` to read one captured file side.
- A binary change has no coordinates or bytes; `changes`, `change`, and `file` report its
path, status, and each existing side's byte size and hash instead. Assign its ID like
any other change.
4. Edit the generated explanations path printed by `inspect`. Diffwalk stores each
authoring pair under `.diffwalk/<walkId>/` and records the selected walk in
`.diffwalk/current`. When the user asks to switch or clean up walks, run
Expand Down Expand Up @@ -163,8 +166,16 @@ hide ownership or order; otherwise let Diffwalk's exact diff carry the code.
and never overwrites an authored `explanations.yaml`.
- Executable modes are preserved for additions, deletions, renames, and content
changes. A chmod-only change has no representable explanation block, so stop when
Diffwalk reports it. Also stop for binary files, symbolic links, or non-file Git
paths; do not bypass these boundaries.
Diffwalk reports it. Symbolic links and non-file Git paths are still rejected at
capture time; do not bypass that boundary.
- Binary files are represented at file level, not by their bytes. The capture records
the path, status, modes, and each side's byte size and SHA-256 hash; the review shows
a metadata card instead of a patch. A side counts as binary when its bytes contain a
NUL or do not decode as UTF-8, so a non-UTF-8 text file also becomes an opaque card.
Binary IDs must be assigned like any other change, and `check` re-validates the
captured metadata. Diffwalk does not generate binary patches or image previews, so
explain a binary change from its identity rather than expecting its contents in the
diff. Never reconstruct the missing bytes or paste them into `text`.
- Treat a pure rename as a real assignable change. Diffwalk renders it as a move rather
than an empty textual diff.
- The capture contains full file contents. Treat it as potentially sensitive and do not publish or send it without the user's authorization.
Expand Down
Loading