Skip to content

docs(adr-006): ratify canonical path identity — stored keys are data, not commands - #19

Merged
qmarcelle merged 3 commits into
mainfrom
adr/006-canonical-path-identity
Aug 10, 2026
Merged

qmarcelle merged 3 commits into
mainfrom
adr/006-canonical-path-identity

Conversation

@qmarcelle

@qmarcelle qmarcelle commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Ratifies ADR-006. Decision-only. No implementation, no schema bytes, no matcher replaced.

Rebuilt on current main (bd14f395) — the previous base f85039b was two merges stale, and the branch was CONFLICTING.

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.

Status in this PR
1. Decision ratification This PR. ProposedAccepted, steward acceptance recorded, decision date 2026-08-09.
2. v0.4 tolerant reader behavior Specified here, unimplemented. Under §9 a v0.4.x reader reports a malformed stored key and declines to match it — including declining to match the value normalization would have produced — while continuing over well-formed data. No reader in this repository implements that yet.
3. Implementation work Not in this PR. The executable corpus, the standard-owned validateStoredKey, producer and reader convergence, and the public-documentation sync are separate work items under META-278.
4. Windows host-query semantics Deferred, by scoping rather than by measurement. §7a scopes the accepted host-query profile to POSIX. The stored-key grammar stays universal — drive letters, UNC and backslashes are forbidden everywhere — and validateStoredKey touches no filesystem, so reading on Windows is unaffected. Windows fixtures are owed before a Windows host profile is accepted, not before this record.
5. Document-profile v0.5 narrowing Explicitly separate. Making 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:

  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. validateStoredKey must not reject every U+FFFD — lossy-decoding detection belongs at raw-byte acquisition, where re-encoded bytes can be compared against the originals.
  4. Acquisition repair is a separate change from stored-key validation, requiring its own caller trace and watched-red evidence.

Diff scope — 8 files

.github/workflows/adr-006-evidence.yml                        +59  -0
docs/adr/006-canonical-path-identity.md                      +540  -0
docs/adr/README.md                                             +1  -0
docs/adr/experiments/006-path-identity/receipts-darwin.json  +242  -0
docs/adr/experiments/006-path-identity/receipts-linux.json   +255  -0
docs/adr/experiments/006-path-identity/run.mjs               +335  -0
docs/adr/index.json                                           +13  -2
scripts/check-docs.mjs                                         +1  -0

docs/adr/index.json was the sole rebase conflict and was resolved exclusively by regenerationscripts/adr-index.mjs is its only author, and no hash or blob pin was hand-edited. Regeneration also enriched ADR-003's revision to bd14f395, 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 in scripts/check-docs.mjs.

Watched receipts — measured on this head, not carried over

Gate Result
pnpm -r build exit 0
pnpm run typecheck exit 0
pnpm -r test spec 115/115 (2 files), rules 173/173 (22 files)
check:architecture 0 violations
check:architecture:test 25 passed, 0 failed
check:adr 6/6 blob SHAs resolved
check:adr:test 19 passed, 0 failed
check:schema exit 0
check:examples 10/10 positive, 11/11 negative
check:docs exit 0
release:verify-packs exit 0

The earlier PR body's counts (8 files, check:adr:test 19/19) predated #20 and #21 and were re-measured rather than carried forward. check:adr moved 5/5 → 6/6 because ADR-006 is now a sixth record.

Evidence

docs/adr/experiments/006-path-identity/run.mjs with 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.

@qmarcelle
qmarcelle force-pushed the adr/006-canonical-path-identity branch from c86c23f to d90e07b Compare August 3, 2026 23:48
Base automatically changed from adr/003-ratify-and-revision-index to main August 4, 2026 02:26
@qmarcelle
qmarcelle force-pushed the adr/006-canonical-path-identity branch from 49d6f61 to f7463b8 Compare August 4, 2026 02:32
…, 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
qmarcelle force-pushed the adr/006-canonical-path-identity branch from 61b22d5 to bc18355 Compare August 9, 2026 22:06
@qmarcelle qmarcelle changed the title docs(adr-006): propose canonical path identity — stored keys are data, not commands docs(adr-006): ratify canonical path identity — stored keys are data, not commands Aug 10, 2026
@qmarcelle
qmarcelle marked this pull request as ready for review August 10, 2026 01:14
Copilot AI lite review requested due to automatic review settings August 10, 2026 01:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@qmarcelle
qmarcelle merged commit 4fb0ede into main Aug 10, 2026
4 checks passed
@qmarcelle
qmarcelle deleted the adr/006-canonical-path-identity branch August 10, 2026 01:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants