## [2026-05-29T04:58:03.293Z]
Initial placeholder for the structure-aspect runtime entry point. The public subpath export and build wiring (tsup entry) are in place; concrete exports are populated as the feature is built incrementally. The node and structure-adapter type are introduced now to establish graph coverage and ensure the entry point is verifiable during development.
## [2026-05-29T05:07:42.841Z]
Added normalizeMappingPath and isPathInMapping for membership testing in collectAllowedReadsForAspect. These pure functions provide string-level mapping validation without filesystem I/O; concrete file enumeration happens later in the runner.
## [2026-05-29T06:06:39.257Z]
Added collectAllowedReadsForAspect, which computes the set of repo-relative paths a structure aspect on a given node is allowed to read. The allowed set is composed from four sources: the node's own mapping (minus paths exclusively claimed by immediate child nodes when the parent selectively enumerates sibling files), relation target mappings and their transitive descendants, ancestor node mappings, and descendant node mappings. This pure-sync function reuses GraphNode object references from the loaded Graph to avoid redundant string-to-node lookups beyond relation target resolution, satisfying the single-source-graph-queries design requirement.
## [2026-05-29T06:14:23.637Z]
Simplify the sibling carve-out logic in collectAllowedReadsForAspect by replacing the fragile dot-based file-vs-dir heuristic with a simpler directory-containment check. Instead of detecting files by looking for a period in the last segment, compute parentDirs (directories present in parent's own mapping) and carve out child paths that are siblings of those directories. This eliminates false positives on directories named v2.0 and files named Makefile.
## [2026-05-29T06:17:11.097Z]
Added public type definitions for the structure-aspect check.mjs ctx parameter. Defines flat GraphNode/File/Port/Relation interfaces exposed to aspect authors—distinct from internal model/graph.ts types which use object refs and nested metadata. Includes Ctx with fs, graph, and parser utilities. Documents the prewarmup decision (Option A): parseAst is synchronous via prewarmed AST cache; dispatcher ensures all reachable AST files are parsed before invoking check. Violation interface and CheckFunction signature round out the author API.
## [2026-05-29T06:21:12.346Z]
Synchronous fs facade for structure-aspect check.mjs. createCtxFs guards file reads with an allowed-set, throws UndeclaredFsReadError for unmapped paths, and tracks all access attempts in touchedFiles array for drift tracking.
## [2026-05-29T06:27:03.177Z]
Added ctx-graph.ts implementing createCtxGraph and UndeclaredGraphReadError. The structure-aspect runtime needs a guarded graph surface so that check.mjs authors can only read nodes that are structurally related to the current node (direct relations, hierarchy ancestors, own descendants, and transitively the descendants of relation targets). Without this guard, a check.mjs could accidentally read arbitrary graph nodes, producing coupling outside declared dependencies and making enforcement unreliable. The allowed-node-id computation is deterministic: it walks declared relations (and their subtrees), the ancestor chain, and the current node's own subtree — all from the in-memory graph, no I/O.
## [2026-05-29T06:46:03.600Z]
Added ctx-parsers module that implements the four parser helpers (parseAst, parseYaml, parseJson, parseToml) for structure aspect check functions. The PREWARMUP decision (locked) makes parseAst synchronous at the boundary: the dispatcher prewarms astCache before invoking check(ctx), so parseAst is a synchronous cache lookup. Cache miss throws structure-aspect-parseast-not-prewarmed to give check authors a clear error. YAML and JSON and TOML parsers are natively synchronous. All four record accessed paths into touchedFiles for drift tracking. The prewarmupAstCache helper is exported for use by the dispatcher (Task 17) to populate the cache before check invocation.
## [2026-05-29T06:55:02.137Z]
Added runner.ts implementing runStructureAspect — the core integration point that loads a check.mjs via dynamic import, builds ctx (ctxFs, ctxGraph, ctxParsers), prewarms the AST cache for own and relation-target files, invokes check synchronously, and validates the return value. Guards mirror the AST runner contract: named-export-only (STRUCTURE_CHECK_NOT_EXPORTED, STRUCTURE_CHECK_DEFAULT_EXPORT), type check (STRUCTURE_CHECK_NOT_FUNCTION), arity (STRUCTURE_CHECK_WRONG_ARITY), loader failure (STRUCTURE_LOADER_RESOLVE_FAILED), synchronous enforcement via duck-typed Promise detection (STRUCTURE_CHECK_ASYNC), array-shape enforcement (STRUCTURE_CHECK_RETURN_SHAPE), and file-boundary enforcement (STRUCTURE_CHECK_FILE_NOT_IN_CONTEXT only for violations carrying a file field). Constraint errors from ctxFs and ctxGraph are converted to structured violations with kind prefix rather than thrown, so they appear in the approval output without terminating the run. Updated index.ts barrel to export runner types, structure types, and AST helpers (walk, closest, report, inFile, findComments) for structure aspect authors. The path.sep usage was avoided — uses split(/[\/]/) per the posix-paths-source enforcement rule.
## [2026-05-29T14:09:41.987Z]
The structure-aspect runtime exposes a ctx object to untrusted check.mjs aspect code, whose filesystem and graph reads must be confined to a declared allow-set so an aspect cannot exfiltrate files or graph metadata outside its scope. Three confinement gaps were closed. First, the JSON/YAML/TOML parser helpers accepted a raw string path and read it with no allow-set check at all, so an aspect could parse any file on disk (for example a system password file or any absolute path); the string-path branch now goes through the same allow-set-enforcing resolver the filesystem surface uses. Second, the filesystem guard matched allowed paths by prefix but never collapsed parent-directory (..) segments and never re-checked that the resolved path stayed inside the repository, so a path that began with an allowed directory followed by enough .. segments resolved outside the repository and was read; resolution now normalizes the path, rejects absolute inputs, and rejects any path that escapes the repository root before applying the allow-set. Third, one graph accessor that returns a node's outgoing relations skipped the allow-set assertion that the node and children accessors enforce, letting an aspect read relation metadata of arbitrary out-of-scope nodes by passing a hand-built node identifier; that accessor now asserts the node is in scope first. All read paths now funnel through a single escape-safe, allow-set-enforcing resolver shared between the filesystem and parser surfaces so the two cannot drift apart, and the relation accessor enforces the same scope check as its siblings.
## [2026-05-29T18:15:39.894Z]
ctx.parseAst called on a file not pre-warmed by the dispatcher previously threw a plain Error that fell through to the generic STRUCTURE_CHECK_THROWN hard runner error, making it indistinguishable from an arbitrary check.mjs crash. The fix introduces a typed ParseAstNotPrewarmedError class in ctx-parsers.ts; the runner's catch block detects it via instanceof and converts it to a structure-aspect-parseast-not-prewarmed violation (succeeded: false), matching the pattern already established for UndeclaredFsReadError and UndeclaredGraphReadError. This surfaces actionable guidance to the author rather than a hard crash, and keeps parity across all three guard-error kinds.
## [2026-05-30T04:57:35.922Z]
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.174Z]
Routed the structure file-access gate and the violation file-in-context check onto one shared mapping-path normalizer, and onto a shared check.mjs export-shape validator, so a path one gate accepts the other can no longer reject and so a malformed check.mjs surfaces the same what/why/next guidance the AST runtime already gave. Previously the two gates normalized paths differently — one stripped a leading ./ and one did not — which could spuriously reject a violation whose file began with ./; and structure export-shape errors rendered as a bare code plus sentence, dropping the actionable next-step guidance authors get elsewhere. The structure error type now carries the structured what/why/next payload.
## [2026-05-30T12:20:30.759Z]
Deterministic structure reviewers now receive each of a node's own source files already parsed into a syntax tree and tagged with its programming language, so a check can read the file's tree directly instead of re-parsing it on demand. A file whose extension has no registered grammar is passed through without a tree and without a language tag. This surfaces, on every own file, the same parsed-tree access that the single-file syntactic reviewers already had — the first step toward unifying the two kinds of deterministic rule under one execution model, without changing what any existing check observes.
## [2026-05-30T12:46:19.855Z]
The structure reviewer now honors yg-suppress source comments: a violation on a line waived by a yg-suppress(<aspect-id>) marker (or a wildcard / bracket disable-enable range) for the matching aspect is filtered out before the verdict is recorded. Previously only the single-file (AST) deterministic reviewer respected these waivers, so the same documented waiver behaved differently depending on whether the rule happened to be implemented as a single-file or a graph-aware deterministic check. That inconsistency is a correctness gap: a human-approved waiver must hold uniformly across deterministic reviewers, otherwise authors cannot reliably silence known, accepted debt. The filtering reuses the existing shared suppression module unchanged; graph-level findings that name no source line remain non-waivable by design, because a line-anchored marker cannot scope a whole-graph assertion.
## [2026-05-30T14:09:01.899Z]
The per-aspect language declaration was removed from the system. An aspect that ships a parsed-tree check used to carry a `language:` list naming the languages it targeted, and the system validated that list with four dedicated checks. Nothing in the runtime ever read it: the engine already determines each source file's language from its file extension through one shared registry, then loads the matching grammar. The declaration was therefore inert metadata that could silently disagree with what the engine actually parsed — an authoritative-looking field that governed nothing.

This change deletes the field, the validators that policed its shape, and the documentation and tests that described it, and promotes the extension-to-grammar registry to the single authority for matching a file to its parser. The motivation is to remove a confusing mismatch between what an aspect claimed about languages and what the engine did, and to collapse the duplicated extension-knowledge that had drifted into more than one place.

The language a parsed-tree check sees for a given file is now derived solely from that file's extension; an unrecognized extension yields no parsed tree rather than a per-aspect error. The drift identity of a graph-aware structural check is deliberately held stable across this change so existing approvals are not invalidated by metadata that never affected behavior.
## [2026-05-30T18:08:10.439Z]
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-31T13:29:02.885Z]
An untrusted deterministic check's filesystem reads are confined to an allow-list of repo-relative paths. The path resolver enforced this only textually — rejecting parent-directory traversal and absolute paths and checking the allow-set against the literal path — but never followed symbolic links. An allowed directory that is itself a symlink pointing outside the repository therefore let a check read arbitrary files outside the project through that link, defeating the containment guarantee that lets the system run third-party check code safely. After the textual checks pass, the resolver now canonicalizes the nearest existing ancestor of the target and the repository root and requires the resolved target to remain inside the root, so a symlink that escapes the repository is rejected while a link that stays within it is still honored.
## [2026-05-31T15:05:43.072Z]
Clarified the read-boundary comment to stop calling the allow-set a "sandbox". The graph-aware deterministic check runner executes check.mjs in the main Node process with full privileges and provides no isolation — an adversarial check could still write files or open sockets. The allow-set only scopes which files count as tracked dependencies for drift detection; it is a read-tracking discipline, not a security boundary. Calling it a "sandbox" in code or docs overstates the guarantee and could lead an adopter to run untrusted aspects believing they are contained. The wording was made honest here and in the user-facing reviewer/CLI documentation and the deterministic-aspect knowledge so the same vocabulary holds everywhere.
## [2026-05-31T16:03:35.160Z]
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-03T07:05:36.577Z]
Directory-mapped deterministic checks now expand a directory mapping to its constituent files before running, and AST-walking checks skip files that have no parsed AST instead of dereferencing a missing one. Without expansion a directory mapping fed no files to the check; without the guard a non-AST file such as a markdown, shell, or JSON file in the mapping crashed the check with a type error on the first file. Both fixes keep deterministic checks correct over real, mixed-content mappings.
## [2026-06-08T15:11:10.061Z]
The structure runtime membership test (is this file part of the node mapping) now matches glob entries by pattern, consistent with how ownership and coverage resolve mappings elsewhere. This is required so a deterministic rule attached to a node scoped by naming pattern receives exactly the files matching that pattern, not every file in the directory.
## [2026-06-08T16:05:31.927Z]
The deterministic-check filesystem gate now decides read membership through the shared mapping matcher, so a node whose mapping is a glob correctly admits the files that glob matches and rejects the ones it does not when a check reads via the context filesystem. Previously the gate compared literal path prefixes, which never matched a glob pattern entry, so a glob-mapped node could be wrongly denied access to its own files.
## [2026-06-08T16:40:58.005Z]
A deterministic check that reads another node files through the graph context now sees the files a glob or directory mapping resolves to. The runner pre-expands each graph-readable node mapping to concrete files in the asynchronous layer and hands that list to the graph context, which still reads each file content lazily so the per-aspect touched-file set reflects only what the check actually accessed. Previously the graph context probed the literal mapping entry, so a glob-mapped node appeared to expose no files.
## [2026-06-08T20:29:25.807Z]
When the graph-aware deterministic runner filters out suppressed violations, it must be able to honor a waiver placed in a file that has no parser. Suppression scanning for such files works from the raw file text rather than a parse tree, so the runner now supplies that text. Parsed files already have their content available through the parse cache; files without a registered grammar are not in that cache, so their text is taken from the file sets the runner has already read for the node and its declared dependencies. A file the runner has neither parsed nor read remains non-suppressible, since there is no trustworthy text to scan. This makes inline waivers effective for content-only checks over non-parseable files during approval, matching the behavior of the single-file runner.
## [2026-06-12T13:31:43.347Z]
Replaced the locally-defined BINARY_EXTENSIONS set with an import from utils/binary-extensions.ts, the canonical single source of truth. The prior local set in runner.ts was byte-for-byte identical to the one in core/checks/mapping.ts, but there was no enforcement connecting them — adding a new format to one would not propagate to the other. A single import from the shared utility resolves this latent consistency hazard.
## [2026-06-12T14:57:53.645Z]
Added result-bearing observation recording to ctx-fs, ctx-graph, and ctx-parsers: each probe now reports what it returned (content bytes for reads, entry list for directory listings, existence token for probes) rather than only which paths were visited. A cached deterministic verdict can only be reused when every observation matches, so recording the results — not just the keys — is necessary for correct cache invalidation. Subject files (own-node mapping) are skipped from observation recording because they are already hashed as subject inputs in the deterministic pair hash.
## [2026-06-12T15:14:29.533Z]
Record graph: observations in relationsFrom and relationsTo. Both methods answer from yg-node.yaml metadata of the nodes they consult — relationsFrom reads the queried node's relation declarations; relationsTo scans every node in the allowed set including nodes with no relation to the target. Without recording these observations, a cached deterministic verdict would remain valid after a relation edit even though the relation declarations are inputs to the check result. Fix: relationsFrom now calls recordGraphNode for the node whose relations are returned; relationsTo calls recordGraphNode for every node it iterates (recording even the negative case, where no relation is declared, since absence of a relation is itself an input).
## [2026-06-13T05:33:45.160Z]
The deterministic verifier now records and re-checks the result of every probe a check makes beyond its own subject files — directory listings, negative existence checks, graph-set membership, and sibling reads. The recorded observation carries the actual result that was seen, so a cached pass cannot survive a change to anything the check actually observed, only to its own subject files. This closes the gap where a check could read a directory or test for a sibling's existence, pass, and then keep passing after that surrounding state changed.
