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
59 changes: 59 additions & 0 deletions .github/workflows/adr-006-evidence.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: ADR-006 path identity evidence

# Runs the ADR-006 experiment harness on Linux.
#
# Three of its questions are unanswerable on darwin: APFS is case-insensitive,
# collapses NFC and NFD to one tracked entry, and refuses a non-UTF-8 pathname.
# ext4 answers all three. Rather than assert what Linux "would" do, this runs the
# same harness there and publishes the receipts, so the claim is reproducible by
# anyone with a fork.
#
# This workflow only reads and reports. It publishes no package, holds no
# credential, and writes nothing back to the repository.

on:
workflow_dispatch:
push:
paths:
- "docs/adr/experiments/006-path-identity/**"
- ".github/workflows/adr-006-evidence.yml"

permissions:
contents: read

jobs:
evidence:
name: Path identity evidence (linux)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v7
with:
node-version: 22

- name: Filesystem under test
run: |
echo "kernel: $(uname -srm)"
echo "filesystem: $(stat -f -c %T .)"
echo "git: $(git --version)"
echo "locale: ${LANG:-unset}"

- name: Run the harness
run: node docs/adr/experiments/006-path-identity/run.mjs

# Printed to the log as well as uploaded: the log is the citable receipt,
# the artifact is what gets committed alongside the darwin run.
- name: Raw receipts
run: |
node docs/adr/experiments/006-path-identity/run.mjs --json \
| tee "$RUNNER_TEMP/receipts-linux.json"

- name: Upload receipts
uses: actions/upload-artifact@v4
with:
name: adr-006-receipts-linux
path: ${{ runner.temp }}/receipts-linux.json
if-no-files-found: error
540 changes: 540 additions & 0 deletions docs/adr/006-canonical-path-identity.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ repository under version control.
| [003](./003-field-lifecycle-and-admission.md) | Field lifecycle and admission | Accepted | 2026-08-03 |
| [004](./004-root-version-compatibility.md) | Root `version` compatibility profile for v0.4.x | Accepted | 2026-07-28 |
| [005](./005-schema-identity.md) | Schema identity — canonical host, filename, and single-file profile carriage | Accepted | 2026-07-28 |
| [006](./006-canonical-path-identity.md) | Canonical path identity — stored keys are data, not commands | Accepted | 2026-08-09 |

## When an ADR is required

Expand Down
242 changes: 242 additions & 0 deletions docs/adr/experiments/006-path-identity/receipts-darwin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
{
"platform": "darwin 25.1.0",
"node": "v22.19.0",
"receipts": [
{
"question": "case-identity",
"finding": "filesystem is case-INSENSITIVE: only one tracked entry survives",
"platform": "darwin 25.1.0",
"secondWriteSucceeded": true,
"trackedEntries": [
"A.ts"
],
"gitIndexCount": 1,
"hostLookupCollides": true,
"note": "Stored-key comparison is a string operation and is unaffected by this. What varies is whether a HOST QUERY can name a file whose stored key differs only by case."
},
{
"question": "unicode-normalization",
"finding": "NFC and NFD collapse to a single tracked entry on this filesystem",
"platform": "darwin 25.1.0",
"nfcWritten": "\"café.ts\"",
"nfdWriteSucceeded": true,
"trackedCount": 1,
"trackedViaLsFilesZ": [
"café.ts"
],
"trackedCodePoints": [
"63 61 66 e9 2e 74 73"
],
"lsFilesDefaultQuoted": [
"\"caf\\303\\251.ts\""
],
"quotingObserved": true,
"note": "If `git ls-files` (no -z) quotes a name and `-z` does not, then display quoting would become artifact identity for any producer using the default form."
},
{
"question": "json-representability",
"finding": "this platform refused to create a non-UTF-8 pathname; question not answerable here",
"platform": "darwin 25.1.0",
"created": false,
"attemptedBytesHex": "626164ff2e7473",
"entry": null,
"roundTrip": null,
"surrogateProbe": {
"key": "lone high surrogate U+D800",
"survivesJsonRoundTrip": true,
"jsonEncodesIt": "\"\\ud800\"",
"reEncodedHex": "efbfbd",
"reEncodeIsLossless": false,
"isWellFormed": false
},
"note": "Lossy decode means silent identity mutation: two distinct tracked paths could serialize to the same JSON key. That is the case the rule must reject explicitly rather than absorb."
},
{
"question": "symlinks",
"finding": "tracked symlink ENTRIES are stored; Git does not store their targets as entries",
"platform": "darwin 25.1.0",
"trackedEntries": [
"alias.ts",
"escape.ts",
"link",
"real/a.ts"
],
"symlinkEntriesTracked": [
"alias.ts",
"escape.ts",
"link"
],
"directoryNotWalkedThrough": true,
"queryThroughSymlinkedDir": {
"lexical": "link/a.ts",
"realpath": "../../../../../../private/var/folders/sy/frt_v9rn73lbqr1l92qfjxhw0000gp/T/adr006-XcfSgW/real/a.ts",
"lexicalEqualsRealpath": false
},
"escapingSymlink": {
"entry": "escape.ts",
"realpathEscapesRepo": true
},
"note": "`link/a.ts` is NOT a tracked entry — Git records the symlink `link`, not a second path to `real/a.ts`. A host query arriving as `link/a.ts` therefore has no stored key, and realpath() would silently rewrite it to `real/a.ts`."
},
{
"question": "repository-roots",
"finding": "a linked worktree and a submodule are SEPARATE artifact roots",
"platform": "darwin 25.1.0",
"linkedWorktree": {
"toplevel": "/private/var/folders/sy/frt_v9rn73lbqr1l92qfjxhw0000gp/T/adr006-wt-T9qg7M/linked",
"gitCommonDir": "/private/var/folders/sy/frt_v9rn73lbqr1l92qfjxhw0000gp/T/adr006-dgpuMO/.git",
"isSeparateToplevel": true,
"tracked": [
"a.ts"
]
},
"submodule": {
"added": true,
"hostTracked": [
".gitmodules",
"h.ts",
"vendor"
],
"hostSeesSubmoduleFiles": false,
"submoduleToplevel": "/private/var/folders/sy/frt_v9rn73lbqr1l92qfjxhw0000gp/T/adr006-lnUpY7/vendor",
"hostToplevel": "/private/var/folders/sy/frt_v9rn73lbqr1l92qfjxhw0000gp/T/adr006-lnUpY7"
},
"note": "The host repo tracks `vendor` as a gitlink, NOT `vendor/s.ts`. A reader that walked upward from inside the submodule and selected the host's artifact would resolve keys against the wrong root."
},
{
"question": "malformed-stored-keys",
"finding": "every entry is non-canonical; normalization would REPAIR several into a different, valid-looking key",
"platform": "darwin 25.1.0",
"corpus": [
{
"key": "\"../x\"",
"nodeNormalize": "\"../x\"",
"isAbsolute": false,
"silentlyRepairedToCanonical": false,
"containsNul": false
},
{
"key": "\"src/../x\"",
"nodeNormalize": "\"x\"",
"isAbsolute": false,
"silentlyRepairedToCanonical": true,
"containsNul": false
},
{
"key": "\"a/b/../b/c.ts\"",
"nodeNormalize": "\"a/b/c.ts\"",
"isAbsolute": false,
"silentlyRepairedToCanonical": true,
"containsNul": false
},
{
"key": "\"/abs/posix/x.ts\"",
"nodeNormalize": "\"/abs/posix/x.ts\"",
"isAbsolute": true,
"silentlyRepairedToCanonical": false,
"containsNul": false
},
{
"key": "\"C:\\\\drive\\\\x.ts\"",
"nodeNormalize": "\"C:\\\\drive\\\\x.ts\"",
"isAbsolute": false,
"silentlyRepairedToCanonical": false,
"containsNul": false
},
{
"key": "\"\\\\\\\\unc\\\\share\\\\x.ts\"",
"nodeNormalize": "\"\\\\\\\\unc\\\\share\\\\x.ts\"",
"isAbsolute": false,
"silentlyRepairedToCanonical": false,
"containsNul": false
},
{
"key": "\"back\\\\slash.ts\"",
"nodeNormalize": "\"back\\\\slash.ts\"",
"isAbsolute": false,
"silentlyRepairedToCanonical": false,
"containsNul": false
},
{
"key": "\"./leading.ts\"",
"nodeNormalize": "\"leading.ts\"",
"isAbsolute": false,
"silentlyRepairedToCanonical": true,
"containsNul": false
},
{
"key": "\"double//sep.ts\"",
"nodeNormalize": "\"double/sep.ts\"",
"isAbsolute": false,
"silentlyRepairedToCanonical": true,
"containsNul": false
},
{
"key": "\"trailing/\"",
"nodeNormalize": "\"trailing/\"",
"isAbsolute": false,
"silentlyRepairedToCanonical": false,
"containsNul": false
},
{
"key": "\"\"",
"nodeNormalize": "\".\"",
"isAbsolute": false,
"silentlyRepairedToCanonical": false,
"containsNul": false
},
{
"key": "\".\"",
"nodeNormalize": "\".\"",
"isAbsolute": false,
"silentlyRepairedToCanonical": false,
"containsNul": false
},
{
"key": "\"..\"",
"nodeNormalize": "\"..\"",
"isAbsolute": false,
"silentlyRepairedToCanonical": false,
"containsNul": false
},
{
"key": "\"a\\u0000b.ts\"",
"nodeNormalize": "\"a\\u0000b.ts\"",
"isAbsolute": false,
"silentlyRepairedToCanonical": false,
"containsNul": true
}
],
"note": "`src/../x` -> `x` and `a/b/../b/c.ts` -> `a/b/c.ts` are the reject-don't-resolve cases: a reader that normalizes cannot report the artifact as invalid, because after normalization there is nothing left to report."
},
{
"question": "silent-redirect",
"finding": "node:path.normalize collapses traversal, so a query naming a path that is not the stored key compares EQUAL to it",
"platform": "darwin 25.1.0",
"storedKey": "a.ts",
"probes": [
{
"query": "\"src/../a.ts\"",
"normalized": "\"a.ts\"",
"wouldMatchStoredKey": true
},
{
"query": "\"a/b/../../a.ts\"",
"normalized": "\"a.ts\"",
"wouldMatchStoredKey": true
},
{
"query": "\"./a.ts\"",
"normalized": "\"a.ts\"",
"wouldMatchStoredKey": true
},
{
"query": "\"../a.ts\"",
"normalized": "\"../a.ts\"",
"wouldMatchStoredKey": false
}
],
"note": "Measured on node:path only. Consumer behavior is cited in the ADR by file:line and was reproduced separately by executing the consumers read-only; importing them here would create the cross-repository coupling the architecture guard forbids."
}
]
}
Loading
Loading