## [2026-05-15T13:00:20.230Z]
Initial creation: split from cli/io parent (wide-node false positive blocked silent-missing-files approve with 14 files). Stores child contains 6 files responsible for drift state persistence, find index, artifact reading, git-ignore scanning, atomic writes, and low-level fs utilities; inherits yaml-parser-contract, silent-missing-files, diagnostic-logging from parent via channel 2.
## [2026-05-15T13:32:21.184Z]
R0.9: add writeTextFile to graph-fs.ts — io/stores now exports both read and write fs utilities. Used by migrator.ts to avoid direct node:fs imports in engine types.
## [2026-05-15T13:56:01.081Z]
R0.1 Phase 4: io/find-index.ts removes buildIssueMessage import — inline template literals replace formatter calls for direct stderr diagnostic writes.
## [2026-05-15T16:21:06.596Z]
Add log-store.ts — readLogSafe, statLogFile, writeLogFile — fs operations for log.md that core/log modules route through instead of using node:fs directly.
## [2026-05-15T16:28:17.782Z]
R0.10: added log-store.ts — fs wrapper for log.md files (readLogSafe, statLogFile, writeLogFile) with diagnostic-logging on ENOENT catch blocks.
## [2026-05-15T17:44:23.192Z]
Phase 2: reclassified from adapter to persistence-adapter. Removed atomic-write.ts (split to cli/io/atomic-write).
## [2026-05-16T05:58:12.005Z]
Phase 4.7 (no-direct-fs): moved hash.ts and paths.ts here from utils/ (now persistence-adapter); added debug-log-writer.ts (appendFileSync DI implementation); expanded graph-fs.ts with fileAccess, lstatFile, statPath, fileExistsSync wrappers
## [2026-05-16T08:19:17.767Z]
Add calls relation to cli/io/atomic-write with consumes: [write-atomic] — declares the port contract so channel 6 propagates atomic-write-contract to this node
## [2026-05-16T17:17:49.788Z]
Migrated log-store.readLogSafe to the new readFileOrDefault helper (io/read-or-default.ts). The helper handles ENOENT uniformly, rethrows other errors, and emits a single debugWrite line on miss. Other IO sites (secrets-parser, repo-scanner, find-index, log-store.statLogFile) intentionally retain their existing semantics — they catch all errors silently (different from ENOENT-only) or operate on stat instead of readFile, so they do not fit the helper. The persistence-adapter type's when predicate and the cli/io/stores node mapping were extended to include the new file.
## [2026-05-16T19:44:32.383Z]
Updated context-files import path to core/graph/files following the file move (collectTrackedFiles + TrackedFile). cli/core/context node drops context-files from its mapping; cli/core/graph node claims it.
## [2026-05-26T10:40:16.945Z]
Rewrote atomic-write-contract check.mjs against raw tree-sitter API (walk + report + inFile from @chrisdudek/yg/ast). The old code used ast.imports() and accessed imp.specifiers which does not exist on ImportInfo — it was effectively a no-op. The new code walks import_statement nodes directly to collect fs-module specifiers, then walks call_expression nodes to detect direct raw write calls. Added graph-fs.ts to the exemption list to preserve the existing enforcement boundary: graph-fs.ts is the low-level fs facade layer that wraps raw node:fs/promises for other adapters and is intentionally allowed to call writeFile directly. Verified behavior-identical via ast-test diff.
## [2026-05-27T07:22:31.582Z]
Phase 6 type-bridge: find-index.ts updated reviewer comparison from aspect.reviewer !== 'ast' to aspect.reviewer.type !== 'ast' to match AspectReviewerSpec object shape.
## [2026-05-28T13:25:32.437Z]
Add clearDraftAspectsFromDriftState helper. Removes specified aspect IDs from the per-node baseline's aspectVerdicts map. Used after a successful approve to evict stale verdicts for aspects that have transitioned to draft status — draft aspects are dormant (no reviewer call), so any leftover verdict from a prior approve must be cleared to keep the baseline consistent with what the reviewer actually evaluated. No-op when the node has no stored state, no aspectVerdicts, or no overlap with the input IDs. Drops the aspectVerdicts field entirely when all entries are removed, matching the initial-write shape.
## [2026-05-28T14:01:53.721Z]
Extend IndexedDocument with an optional status field carrying aspect-default enforcement status. Populated by buildIndex only for aspect-kind documents (nodes do not carry status). The field is added to storeFields so search consumers can read it directly from result hits without re-querying the graph.
## [2026-05-29T07:19:58.014Z]
Added clarifying JSDoc comment to clearDraftAspectsFromDriftState explicitly documenting that structureTouchedFiles is not cleared, preserving the D8.3 invariant across draft-toggle operations. No behavior change.
## [2026-05-30T04:57:35.754Z]
Removed leaked internal planning references — task, phase, and decision identifiers and section citations — from source-code comments. These tokens named ephemeral planning artifacts that mean nothing to a future reader and violated the project's comment self-containment rule. The substantive explanation each comment carried was preserved; no code or behavior changed.
## [2026-05-30T07:12:18.492Z]
The array-form mapping-path normalizer now delegates to the shared single-path normalizer used by the aspect runtimes, so mapping entries normalize identically everywhere. The practical effect is that a leading ./ on a mapping entry is now stripped repo-wide, matching how the aspect file-access gate already treated such paths; mapping entries in practice never carry that prefix, so behavior is preserved while the divergence between three near-identical normalizers is removed.
## [2026-05-30T18:08:10.281Z]
The vocabulary for how a rule is verified was reduced from three kinds to two. Previously a rule was checked by one of: a human-language reviewer, a single-file programmatic check, or a graph-aware programmatic check. The two programmatic kinds are now a single "deterministic" kind, leaving just deterministic-or-reviewer.

The motivation: the three-way split was drawn on the wrong axis. It described HOW a programmatic check reached its context (one file at a time, versus the whole graph), but the distinction that actually matters to a rule author and to cost is whether verification is local-and-free or requires the paid, non-deterministic reviewer. The single-file kind was already a strict subset of the graph-aware kind — every input the former could see, the latter also provides — so maintaining two of them forced rule authors to make a false choice up front and forced the engine to carry two parallel handling paths for one concept. Collapsing them removes that false choice and the duplicated handling, and routes every programmatic check through the one graph-aware path.

The language a programmatic check infers for a source file is determined solely from that file's extension, so a check no longer declares which languages it targets. A rule's verification kind being deterministic is also no longer carried as a separate synthetic identity signal — a deterministic rule's identity is fully covered by the files it already tracks — which keeps re-verification of such rules free.
## [2026-05-30T21:54:03.787Z]
Internal names that still referred to the retired single-file and graph-aware reviewer kinds were renamed to reflect the one unified deterministic kind: the per-aspect record of which files a deterministic check read, the synthetic drift-identity key summarising that set, the tracking layer those cross-node files sit on, and the error tag for a crash inside a non-reviewer (deterministic) runner. This is a clarity-only rename with no behaviour change — the verification mechanics, drift detection, and carry-forward are identical; only the identifiers and the persisted token strings changed so the code no longer speaks of reviewer kinds that no longer exist. Baselines were migrated in place to the new token names.
## [2026-05-31T06:24:46.841Z]
An internal name was changed for clarity: the per-aspect record of files a deterministic check read, its synthetic drift-identity key, and the tracking layer for those files were renamed from a deterministic-prefixed form to a check-prefixed form, so the identifiers name the check.mjs artifact they describe. This is a clarity-only rename with no behaviour change — drift detection, attribution, and carry-forward are identical; only the identifiers and the persisted token strings changed, and in-repo baselines were migrated in place to the new tokens.
## [2026-05-31T16:03:34.975Z]
Replaced the hand-inlined path-separator normalization with calls to a single shared helper. The same small idiom — convert backslash separators to forward slashes, and in most places also strip a trailing slash — had been copied across many modules, so the normalization rule lived in dozens of places at once and any change to it risked drifting them out of step. Consolidating it behind one well-named helper means the rule lives in exactly one spot and each call site reads by intent instead of by a repeated regex. Behavior is unchanged: the helper bodies are byte-for-byte equivalent to the expressions they replace, and the full test suite passes identically.
## [2026-06-01T06:21:24.099Z]
Taught the drift-state store to validate and stamp the typed baseline format. On read it now requires the on-disk format version and the typed shape (typed upstream-identity block plus the required per-aspect verdict map), throwing an upgrade-pointing error for any baseline that predates this format so a stale baseline cannot silently pass; on write it stamps the current format version. The canonical-hash helper folds the typed identity over the same logical inputs as before, and is the single source of truth shared by the runtime and the re-key transform so a re-keyed baseline over unchanged inputs matches a fresh computation.
## [2026-06-01T07:03:47.739Z]
Distinguished two previously conflated error paths in validateBaselineShape. Previously both an old/absent schemaVersion AND a current-version baseline with missing required fields threw OutdatedDriftBaselineError pointing at 'yg init --upgrade'. The 'yg init --upgrade' advice is wrong for the second case: the schema version is already current, so migration cannot help. The corrupt-v1 path now throws a plain Error whose next instruction tells the user to restore the baseline from git or delete it and re-approve. The old/absent-schemaVersion path continues to point at 'yg init --upgrade'.
## [2026-06-01T09:41:49.966Z]
A drift-state baseline that is present on disk but malformed — either carrying a schema version this CLI no longer parses, or carrying the current version yet missing required typed fields because it was hand-edited or written by other tooling — is an operator-fixable state condition, not an internal defect. The recovery instructions printed for the corrupt-current-version case must name the actual affected node so an operator can copy and run them verbatim; an un-substituted placeholder leaves a command that does nothing. The malformed-current-version case is kept as a distinct named error from the outdated-version case so the two can carry different recovery paths — restore-or-rebuild for corruption versus migrate-the-graph for an old format — and so the command layer can recognize and render them as recoverable state problems rather than crashes.
## [2026-06-01T10:45:44.847Z]
The canonical drift hash must be identical across machines regardless of the operating system or runtime locale: it is the value that decides whether a baseline still matches the code, and a baseline computed on one machine is read and compared on another. The file-digest ordering that feeds this hash therefore uses a code-unit (byte-order) comparison rather than a locale-aware one. A locale-aware ordering can reorder the same set of paths differently depending on the host locale (case folding, punctuation weighting), which would make two machines compute different hashes for identical code and produce spurious drift that no source change explains. Code-unit ordering is locale-independent and matches how the typed identity portion of the same hash is already serialized, so both halves of the hash are ordered consistently.
## [2026-06-03T07:05:22.616Z]
The canonical drift hash now folds in the recorded aspect verdict and its error source, not just the source-file bytes. A baseline file whose stored verdict was edited to a forged pass no longer reproduces the hash, so the gate detects the tampering and stays red instead of trusting the altered verdict.
## [2026-06-03T07:48:59.910Z]
yg check now re-hashes content instead of trusting mtime, closing a touch-mtime green-over-changed-code hole in the gate. Added reuseByMtime boolean param (default true) to hashTrackedFiles so the check gate can always read disk content while the approve-time path retains the mtime optimization for performance.
## [2026-06-08T07:50:31.052Z]
Added excludeNestedGraphSubtrees helper to the repo-scanner module. The function detects directories below the repo root that contain their own .yggdrasil folder — indicating a separately-governed nested graph — and strips every file under those directories from the returned list. The top-level .yggdrasil folder is explicitly not treated as a nested root. Applied the filter in walkRepoFiles so all callers (checkStrictBackwardCoverage, checkFileMappingGitignored, yg impact) automatically inherit the exclusion. This ensures the parent graph does not attempt to enforce coverage or drift rules on code that belongs to a child graph.
## [2026-06-08T08:11:32.927Z]
Removed a footgun from the excludeNestedGraphSubtrees function in repo-scanner.ts: the endsWith('/.yggdrasil') branch was wrong because a real nested graph always has files under its own .yggdrasil/ directory, so the mid-path segment detection (idx > 0) is the complete and correct signal. The endsWith branch could only fire on a stray file literally named .yggdrasil, and would then silently drop that file's entire parent directory from the walk. Removed the branch and the now-unused tail constant, replacing with a comment explaining the invariant.
## [2026-06-08T15:11:09.870Z]
Mapping-path expansion to concrete files now expands glob entries: it walks from the fixed leading directory of the pattern (the segments before the first one containing a glob metacharacter) and keeps the files matching the full pattern, honoring gitignore. Directory and exact-file entries expand as before. A glob that matches nothing expands to an empty set rather than erroring. This is what lets hashing and the deterministic runner see exactly the files a pattern-scoped node owns, so a rule can target a subset of a directory by naming pattern.
## [2026-06-08T16:05:31.120Z]
Drift hashing now expands a glob mapping entry to the concrete files it matches instead of treating the pattern as a literal path. Previously a glob mapping contributed no files to the drift baseline, so edits to those files were invisible to drift detection and the files were never handed to the reviewer — enforcement silently skipped them. The descendant carve-out for the child-wins model now also matches files against the child mapping pattern, so a child that owns files by glob is correctly excluded from its parent set.
## [2026-06-12T12:10:11.259Z]
Added hashBytes(bytes: Buffer): string to hash.ts — a direct sha256-of-raw-bytes helper that does not pass through string encoding. This is needed because hashReadObservation must produce sha256(bytes) without any text-decoding round-trip; using toString('binary') followed by UTF-8 hashing corrupts byte values >= 0x80. The new export is the canonical, encoding-free primitive for hashing binary file content from Buffers.
## [2026-06-13T05:48:33.591Z]
The filesystem store now exposes byte-accurate, missing-tolerant primitives that the deterministic verifier needs to record what a check observed. Reading a file returns its raw bytes (a Buffer) so the verifier can hash content without a text round-trip that would corrupt byte values at or above 0x80, and a missing file is reported as an empty (null) result rather than an error, treating an absent optional file as valid empty state. Listing a directory returns each entry with whether it is a file or a directory, again reporting a missing directory as empty rather than failing, so a check can record a directory listing or a negative existence probe as part of its observation set. The canonical input-hash serialization helpers were moved out of this layer so this module stays focused on raw file and directory access.
## [2026-06-13T07:02:18.515Z]
File-system access is centralized in this layer so callers never reach for the platform's file modules directly. A readability probe was added here: it reports whether a file can be read without reading its contents, returning the failure reason when it cannot. Higher layers use it to enforce that any file written into a component's file list must be readable — an unreadable file becomes a blocking error rather than being silently skipped — and the probe lives at this boundary so that rule routes through the one sanctioned file-access layer instead of importing platform file modules elsewhere.
