docs(adr-006): ratify canonical path identity — stored keys are data, not commands - #19
Merged
Merged
Conversation
qmarcelle
force-pushed
the
adr/006-canonical-path-identity
branch
from
August 3, 2026 23:48
c86c23f to
d90e07b
Compare
qmarcelle
force-pushed
the
adr/006-canonical-path-identity
branch
from
August 4, 2026 02:32
49d6f61 to
f7463b8
Compare
…, not commands Draft record, status Proposed. No implementation, no schema change, no producer behaviour change. Rebuilt on merged main (f85039b) after ADR-003 landed. The schema calls path-bearing values "repo-root-relative POSIX paths" and stops. Case, encoding, ./ prefixes, trailing slashes, symlink resolution order, and what "repo-root-relative" means in a submodule or linked worktree are unwritten, so two implementers can guess differently and both conform. They already have: both shipped consumers route stored keys AND host queries through node:path.normalize, so src/../a.ts matches stored a.ts. The extension's isValidRelativeKey runs after normalization, so it cannot see malformation normalization already erased. The record specifies every path-bearing location, not just fileIndex, and separates the two operations the implementations conflate: validateStoredKey(rawKey) total, no filesystem, never repairs canonicalizeHostQuery(root, inputPath) may touch disk, may fail, never invents Evidence rather than assertion. The harness builds throwaway Git repositories and records what Git, Node and JSON do; receipts for both platforms are committed. git ls-files -> "caf\303\251.ts" quoted, octal-escaped, literal quotes git ls-files -z -> café.ts raw bytes link/a.ts is NOT a tracked entry under a symlinked directory, so it has no stored key; realpath() would rewrite it to real/a.ts, answering a different question. A host repo tracks .gitmodules, h.ts and vendor — not vendor/s.ts. Five of fourteen malformed stored keys normalize into a valid-LOOKING key. Linux run closes the three questions darwin could not answer: case ["A.ts", "a.ts"] — two tracked entries unicode NFC and NFD distinct: 63 61 66 65 301 … vs 63 61 66 e9 … non-UTF-8 626164ff2e7473 -> "bad<U+FFFD>.ts", re-encode == original: false so §4 and §5 rest on observation rather than on reasoning about a failure mode. Windows is resolved by scoping, not measurement: §7a scopes the accepted host-query profile to POSIX and states Windows canonicalization is not yet specified. The stored-key grammar stays universal and validateStoredKey touches no filesystem, so reading on Windows is unaffected. Control characters narrowed to NUL alone — U+000A and U+0009 are legal in POSIX filenames, and forbidding them would make real repositories unrepresentable. Stored text defined as Unicode scalar values from lossless UTF-8 decoding, rejecting unpaired surrogates and U+FFFD substitution. Release sequencing made explicit: a v0.4.x reader reports a malformed key and declines to match it, including declining to match the value it would have become under normalization. Tolerance governs whether the reader continues, never whether it rewrites. scripts/check-docs.mjs gains one PROVENANCE_FILES entry because this record names its ratification issue in metadata; without it check:docs fails. docs/adr/index.json is regenerated, which also enriches ADR-003's now-derivable revision to f85039b and PR 18 — the post-merge enrichment path working as designed, on a main that was never red.
…ceipt files The symlink section stated two different rules and was not ratifiable as written. The case table said a query traversing a symlinked directory is unsupported, full stop. The closing sentence said a traversal that "cannot prove both containment and tracked identity" is unsupported — which implies that a traversal which CAN prove them yields a key. §2 compounds it by granting canonicalizeHostQuery filesystem access specifically to establish identity. Resolved on the proof-based rule. Traversal is permitted only against proof: 1. resolve the target 2. prove it remains within the repository root 3. prove its exact tracked entry 4. return that tracked entry Any step failing — target escapes, untracked, ambiguous — yields unsupported. No fallback, no nearest-match. So link/a.ts may map to real/a.ts, but only when all four succeed. The distinction that makes this consistent with reject-don't-resolve is now stated outright: canonicalizing a HOST QUERY into an existing tracked identity is not repairing a STORED KEY. The input is a host path, not an artifact key, and nothing in the artifact is rewritten. The record previously left those two operations close enough to be conflated. One constraint added that the ruling implies but did not spell out: a lexical alias is never replaced by its target when the alias is ITSELF tracked. Two tracked entries collapsing to one key would destroy the artifact's ability to distinguish them. Resolution applies only where the lexical path has no tracked entry of its own, and therefore no identity to lose. Provenance now cites receipts-darwin.json AND receipts-linux.json, and says what each carries: darwin answers symlinks, repository roots, malformed keys and display quoting; Linux answers case distinction, NFC/NFD and non-UTF-8 decoding, which APFS cannot express. Neither alone supports §4 and §5. index.json regenerated. Full gate set green.
Steward acceptance given 2026-08-09 by Qwynn Marcelle, sole steward, in
explicit terms. Status moves Proposed → Accepted, the decision date is the date
acceptance was given rather than the drafting date, the README index row is
updated, and docs/adr/index.json is regenerated.
Acceptance authorized ratification only. It did not authorize implementation:
the executable corpus, the standard-owned validation contract, producer and
reader convergence, and the public-documentation sync remain separate work
items under the ratification issue.
Four scope corrections were issued with the acceptance and are recorded in the
record itself, because they narrow what it authorizes and a reader who has only
the record should see them:
1. validateStoredKey is standard-owned, pure, total and filesystem-free.
2. canonicalizeHostQuery is not the schema package's to implement. Hosts and
integrations implement it against these semantics; a filesystem-aware
implementation in the spec package needs its own architectural decision.
3. A genuine U+FFFD in a filename is valid. Lossy-decoding detection belongs
at raw-byte acquisition, where re-encoded bytes can be compared against
the originals, not in a pure string validator that has no bytes.
4. Acquisition repair is a separate change from stored-key validation and
needs its own caller trace and watched-red evidence first.
Rebuilt on bd14f39 rather than merged from the stale f85039b base. The only
conflict was docs/adr/index.json, resolved exclusively by regeneration — the
generator is its sole author. Regeneration also enriched ADR-003's revision pin
to bd14f39, now that #21 has published those bytes.
No file from #20 or #21 is reverted or reintroduced. The two edits to shared
files are pure additions: one README index row and one path in
scripts/check-docs.mjs.
Refs: META-278
qmarcelle
force-pushed
the
adr/006-canonical-path-identity
branch
from
August 9, 2026 22:06
61b22d5 to
bc18355
Compare
qmarcelle
marked this pull request as ready for review
August 10, 2026 01:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ratifies ADR-006. Decision-only. No implementation, no schema bytes, no matcher replaced.
Rebuilt on current
main(bd14f395) — the previous basef85039bwas two merges stale, and the branch wasCONFLICTING.What this PR is, and the four things it is not
Each row is a distinct decision boundary. Conflating any two of them is how a ratification turns into an unreviewed implementation.
Proposed→Accepted, steward acceptance recorded, decision date2026-08-09.validateStoredKey, producer and reader convergence, and the public-documentation sync are separate work items under META-278.validateStoredKeytouches no filesystem, so reading on Windows is unaffected. Windows fixtures are owed before a Windows host profile is accepted, not before this record.validate()reject malformed keys is a v0.5 document-profile transition sequenced under ADR-003 §5. It is not authorized here, and it is unrelated to the pending package version@workspacejson/spec@0.5.0, which is a package number and not a profile.Scope corrections issued with the acceptance
Recorded in the record itself, because they narrow what it authorizes:
validateStoredKeyis standard-owned, pure, total and filesystem-free.canonicalizeHostQueryis not the schema package's to implement. Hosts and integrations implement it against these semantics; a filesystem-aware implementation in the spec package needs its own architectural decision.validateStoredKeymust not reject every U+FFFD — lossy-decoding detection belongs at raw-byte acquisition, where re-encoded bytes can be compared against the originals.Diff scope — 8 files
docs/adr/index.jsonwas the sole rebase conflict and was resolved exclusively by regeneration —scripts/adr-index.mjsis its only author, and no hash or blob pin was hand-edited. Regeneration also enriched ADR-003'srevisiontobd14f395, now that #21 has published those bytes.No file from #20 or #21 is reverted or reintroduced. This PR touches no
packages/path. The two edits to shared files are pure additions: one README index row, one path inscripts/check-docs.mjs.Watched receipts — measured on this head, not carried over
pnpm -r buildpnpm run typecheckpnpm -r testcheck:architecturecheck:architecture:testcheck:adrcheck:adr:testcheck:schemacheck:examplescheck:docsrelease:verify-packsThe earlier PR body's counts (8 files,
check:adr:test19/19) predated #20 and #21 and were re-measured rather than carried forward.check:adrmoved 5/5 → 6/6 because ADR-006 is now a sixth record.Evidence
docs/adr/experiments/006-path-identity/run.mjswith committed receipts from both platforms. Both are load-bearing: darwin answers symlink, repository-root, malformed-key and display-quoting questions; linux answers case distinction, NFC/NFD distinction and non-UTF-8 decoding, which APFS cannot express. The linux run is reproducible from.github/workflows/adr-006-evidence.yml.Refs: META-278.