From cbd79c776943e1ea548e7a9c758d3e3a3a9539a1 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 06:00:30 +0700 Subject: [PATCH] t27f: deps view writes the use lines t27c does not resolve as unknown (Closes #10702) deps_unknown reads the focus spec's source next to the use-edges list and writes one `unknownpath` line per use line with no edge to its whole path or its single fallback, mirroring bootstrap/src/use_resolve.rs. Measured on the Railway lab, master f33f269d5: 2489 use lines, 23 unknown in 13 specs, byte for byte the `t27c gen` "resolves to no spec" warnings; the drop-one and empty controls both fail. Spec 25/25 tests, 0 vacuous; corpus ratchet CLEAN. The t27f skill gains the oracle recipe. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/t27f/SKILL.md | 20 ++- .trinity/seals/t27f_t27f_spec_deps.json | 16 +- specs/t27f/spec_deps.t27 | 222 +++++++++++++++++++++++- 3 files changed, 247 insertions(+), 11 deletions(-) diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index 41202f3b4d..918e37c92f 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -81,7 +81,7 @@ the diagram core in detail, then the other surfaces and the topic-map applicatio | Spec -> graph IR schema (the core) | `specs/t27f/spec_graph.t27` (t27#10020) | spec with tests: node / edge kinds, labels, spans, `validate_graph`, line -> node mapping, node -> ui_plan widget, one hand-read fixture (`specs/tutorial/08_modules_and_arrays.t27`) | | Graph view: layout of a spec graph | `specs/t27f/spec_graph_view.t27` (t27#10034) | spec with tests: one column per node kind, size / colour / dash / row order from the inference label, UNKNOWN nodes alone in a band, every edge with a source line, node -> widget through `node_widget`; numbers only, laid out on the hand-read fixture; drawn by the MCP tool `t27f_graph` (gHashTag/t27-mcp#9) | | Graph view for big specs: wrap, fold, expand, hit | `specs/t27f/spec_graph_fold.t27` (t27#10218) | spec with tests: a kind column taller than N = 12 rows wraps into sub-columns so the canvas aspect is near phi (identical to the view when nothing wraps); a fold to one box per kind with the exact count and one arc per pair of kinds with the exact edge count, each labelled by its weakest member; `expand_graph` = one kind plus its direct neighbours, copied verbatim with an origin map; `wrap_hit` / `wrap_hit_line_start` / `wrap_hit_line_end` take a click to a node and its lines; numbers only, the view's colours are not touched | -| Dependencies view: the specs around one spec | `specs/t27f/spec_deps.t27` (t27#10680) | spec with tests: reads `t27c use-edges --specs-dir specs`; layer 0 is the focus, +k the specs k imports away (right), -k the specs k importers away (left), each spec once in its shortest-path layer; every edge-list line between two drawn specs is one `exact` edge, a repeated line drawn once; `cycle=1` when the focus imports itself or is imported by a spec in its own import closure; a walk past the caller's room or depth is refused whole (`refused\tfull` / `refused\tdeep`), never cut. Limits: `use` lines t27c does not resolve are not drawn (#7176); the view flags only the focus, and `deps_cycles` (t27#10693) lists every spec on a cycle, each once, refused whole (`cap + 1`) if anything does not fit; a spec on a cycle through the focus is drawn on both sides; no host draws it yet. Corpus on master e607228f7: 806 specs, 2461 edge lines, nothing refused at depth 32, deepest walks 6, one real cycle (igla race `backend` <-> `rtl`), which `deps_cycles` lists exactly on master f7efbc232 and lists as empty with #10689 | +| Dependencies view: the specs around one spec | `specs/t27f/spec_deps.t27` (t27#10680) | spec with tests: reads `t27c use-edges --specs-dir specs`; layer 0 is the focus, +k the specs k imports away (right), -k the specs k importers away (left), each spec once in its shortest-path layer; every edge-list line between two drawn specs is one `exact` edge, a repeated line drawn once; `cycle=1` when the focus imports itself or is imported by a spec in its own import closure; a walk past the caller's room or depth is refused whole (`refused\tfull` / `refused\tdeep`), never cut. A `use` line t27c does not resolve is not in the edge list, so `deps_unknown` (t27#10702) reads the focus source and writes each such line as `unknown\tpath`, mirroring `use_resolve.rs` (whole path, then one fallback level) without looking for a file; on master f33f269d5 that was 23 lines in 13 specs, byte for byte the `t27c gen` warnings. Limits: the view flags only the focus, and `deps_cycles` (t27#10693) lists every spec on a cycle, each once, refused whole (`cap + 1`) if anything does not fit; a spec on a cycle through the focus is drawn on both sides; no host draws it yet. Corpus on master e607228f7: 806 specs, 2461 edge lines, nothing refused at depth 32, deepest walks 6, one real cycle (igla race `backend` <-> `rtl`), which `deps_cycles` lists exactly on master f7efbc232 and lists as empty with #10689 | | Spec -> graph IR exporter | `specs/t27f/spec_graph_export.t27` (t27#10162) | spec with tests: `export_graph(path, src)` is a deterministic byte scanner over the source, `export_verdict` is `validate_graph` of the result. One node per top-level declaration (a leading `pub ` is allowed), spans to the matching closing brace, every node and edge EXACT or (unclassifiable) OPAQUE + UNKNOWN, CALLS / USES / CONSTRAINS edges with the line of the use, refusal past 1024 nodes or 4096 edges. On the tutorial 08 snapshot it equals the hand-read fixture on all 14 declaration nodes and all 9 EXACT edges; it does not emit the fixture's `std.testing` UNKNOWN node, nor its 7 DERIVED edges (4 data flow, 3 tested_by), because a scanner never derives; the derive pass below adds them. Limits: textual matching without scope or type resolution, no block comments, no struct/enum bodies, no cross-spec `use` resolution, capacity 1024 nodes and 4096 edges, u16 indices (all 2225 specs in specs/ fit, largest 775 nodes and 3194 edges after derive; anything past the cap is refused, never truncated), takes a `[]const u8` slice rather than `[65536]u8` plus a length. AST/IR audit is trinity#1650, pilot choice #1651 | | Graph IR derive pass | `specs/t27f/spec_graph_derive.t27` (t27#10191) | spec with tests: `derive_graph(g, src)` appends what the exporter must not claim and changes nothing it holds. R1 DERIVED `tested_by` = every EXACT CALLS edge from a test, invariant or bench to a fn read backwards (no line). R2 DERIVED `data_flow` = a const or `use` that an EXACT USES edge ties to a fn and that is named inside one of the fn's return statements (from `return` to the next `;`), on the line of the first such name. R3 UNKNOWN opaque node = an undeclared qualified call target of three or more segments (`std.testing.expectEqual(` names `std.testing`), spanning the lines that call through it. Exporter plus derive equal the tutorial 08 fixture as sets: 15 of 15 nodes, 16 of 16 edges, no special case. Limits: names matched as text; call results and flow through locals are not derived; R3 needs 3 or more segments (two cannot be told from a method call); a graph that does not validate, or a source of another line count, comes back unchanged; past 1024 nodes or 4096 edges the result is refused with the too-many verdict. Not wired to a host: the MCP tool does not call it | | UI plan IR + trusted widget catalog | `specs/t27f/ui_plan.t27` (t27#9962) | spec with tests; no renderer draws a plan yet | @@ -366,6 +366,24 @@ Railway lab. Do not commit it. 4. Remove the scratch spec, and `git checkout` any spec you swapped in, before you seal anything. +### Check that the unknown imports match t27c (t27#10702) + +`deps_unknown` has an oracle. `t27c gen ` prints `warning: :: use X resolves +to no spec` on stderr for every `use` line it drops (#7176), so the two lists must agree byte +for byte. + +1. On the lab, before any scratch spec is in `specs/`, write three files: + - the gen warnings over every spec (`t27c gen $f > o.zig 2>> warn.txt` over + `git ls-files 'specs/*.t27'`; never write to or read from `/dev/null` there); + - the `use-edges` list; + - every `use` line as `specline` (`grep -E '^[[:space:]]*use '`). + Check that none of them holds `"`, `\` or `$` before you embed it. +2. The scratch spec copies each use line into a small buffer and calls `deps_unknown` with that + line's spec as the focus. It prefixes each output line with the spec and expects + `specunknownX` in `git ls-files` order, which is the order gen ran in. +3. Controls: the same list minus one line must FAIL, and so must `w == 0`. On master f33f269d5 + it was 23 lines in 13 specs (#7176 had counted 119 in 70), and both controls failed. + ## What this skill does not claim The graph IR is a schema (`specs/t27f/spec_graph.t27`) and `specs/t27f/spec_graph_export.t27` turns source into it, but only exact declarations, spans and edges within the IR's capacity, with `specs/t27f/spec_graph_derive.t27` adding the derived and unknown part by stated rules, and no host calls either yet; no renderer draws a T27F view in the Trinity UI, no diff --git a/.trinity/seals/t27f_t27f_spec_deps.json b/.trinity/seals/t27f_t27f_spec_deps.json index a711a4adc5..861fdba625 100644 --- a/.trinity/seals/t27f_t27f_spec_deps.json +++ b/.trinity/seals/t27f_t27f_spec_deps.json @@ -15,23 +15,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:24377e650e9303759aa6b0b805604ae6d9529c45c10e33242c8be73da902067e", - "gen_hash_rust": "sha256:1753610b6d2287d2dec15cd904813eeb22db4208ecfb98b90cec0e34b76274e9", - "gen_hash_verilog": "sha256:ca51cd614290ebde62e75fa09618241eb221062882dde6d86248a1c28666469b", - "gen_hash_zig": "sha256:add0c9a74743b291f6e2ff110a93868e2a777a7ba99d3126745dc07922ac444e", + "gen_hash_c": "sha256:a827ac501c5bc3a6cb34fb9eed1a765efaf6c65dcaef25c64ff4ab34e053f47f", + "gen_hash_rust": "sha256:ecd73ef257fce64599a99a522559ac91b0fe6a1a364863f163584bdd253e0673", + "gen_hash_verilog": "sha256:47a568b333eb16e44c62b907f678ab98e42ea629611d36a2d52edd91a95e4023", + "gen_hash_zig": "sha256:8082c63e31383157aaa1771ec8d4feaf6b1090da42127267c41b8a8afad06251", "module": "t27f_spec_deps", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T22:30:43Z", + "sealed_at": "2026-10-18T22:58:06Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:b4a87ecfd782bbd552d561fbb39a43b23d8ab4f174b30e1045d52c5cef16d2bb", + "spec_hash": "sha256:c7b44f43776b8a7b0d21aee8a73502e996ec9fb35cd56935bd300b4358f8d4be", "spec_path": "specs/t27f/spec_deps.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 19, - "total": 19, + "passed": 25, + "total": 25, "vacuous": 0 }, "toolchain": { diff --git a/specs/t27f/spec_deps.t27 b/specs/t27f/spec_deps.t27 index e9323116b3..ab0659e6c0 100644 --- a/specs/t27f/spec_deps.t27 +++ b/specs/t27f/spec_deps.t27 @@ -17,12 +17,16 @@ ; the list repeats it, and labelled exact: t27c resolved it, nothing here is inferred; ; - the focus is flagged as on a cycle when it imports itself or a spec it imports imports it. ; deps_cycles asks the same question of every importer and lists each spec flagged, once; +; - a `use` line of the focus is unknown when the focus has no edge to the spec its path names, nor +; to the one module an item form falls back to (a brace list's module, or the path one level up); ; - a neighbourhood that does not fit the caller's room, or is deeper than the depth asked for, is ; refused whole. Nothing is cut off. ; -- ; The honest limits: -; - a `use` line t27c does not resolve is not in the edge list, so it is not drawn (#7176 counted -; 119 such lines in 70 specs); +; - a `use` line t27c does not resolve is not in the edge list. deps_unknown (#10702) reads the +; focus's own source beside the list and writes each such line as `unknownpath`: an import +; whose target nobody knows, not left out. It reads a line the way bootstrap/src/use_resolve.rs +; does, and never looks for a file; ; - the view flags only the focus. deps_cycles (#10693) lists every spec on a cycle, but nothing runs ; it on every PR yet: no t27c subcommand runs a main (#10692). A spec on a cycle through the focus ; is drawn on both sides; @@ -37,6 +41,10 @@ ; Measured again for deps_cycles, master f7efbc232 (2462 edge lines), with the list embedded in a ; scratch test that was not committed: it lists exactly those two specs. With #10689's rtl.t27, ; which drops its `use igla::race::backend;`, it lists none. +; Measured for deps_unknown, master f33f269d5 (2466 edge lines, 2489 `use` lines), in the same kind of +; scratch test: over every spec it writes 23 unknown lines in 13 specs, byte for byte the +; `use X resolves to no spec` warnings `t27c gen` prints for every spec (#7176 counted 119 in 70). +; The control that drops one expected line fails. ; phi^2 + 1/phi^2 = 3 | TRINITY module t27f_spec_deps; @@ -73,6 +81,10 @@ pub const DEPS_EXACT : str = "\texact\n$"; pub const DEPS_REFUSED : str = "refused\t$"; pub const DEPS_WHY_FULL : str = "full\t$"; pub const DEPS_WHY_DEEP : str = "deep\t$"; +pub const DEPS_UNKNOWN : str = "unknown\t$"; + +/// The directory `t27c use-edges --specs-dir specs` keys its paths under. +pub const DEPS_ROOT : str = "specs$"; /// Is s[f..t] one of the newline-terminated lines of set[lo..hi]? pub fn deps_in_range(set: []const u8, lo: usize, hi: usize, s: []const u8, f: usize, t: usize) -> bool { @@ -321,6 +333,151 @@ pub fn deps_view(buf: []const u8, n: usize, focus: []const u8, ff: usize, ft: us return w; } +// ---- use lines the edge list does not resolve (#10702) ---- +// The resolver is bootstrap/src/use_resolve.rs. These functions read a `use` line the way its +// use_path_expr, use_path, brace_module and use_target do. They never look for a file: a line counts +// as resolved when the edge list has an edge from the focus to the spec the line names. + +/// Is c a byte the resolver trims? +pub fn deps_space(c: u8) -> bool { + return c == ' ' or c == '\t' or c == '\r'; +} + +/// Where the module path of the line src[s..t] starts, or t when the line is not `use ` after spaces. +pub fn deps_use_from(src: []const u8, s: usize, t: usize) -> usize { + var i : usize = s; + while (i < t and deps_space(src[i])) { i += 1; } + if (i + 4 > t) { return t; } + if (src[i] != 'u' or src[i + 1] != 's' or src[i + 2] != 'e' or src[i + 3] != ' ') { return t; } + i += 4; + while (i < t and deps_space(src[i])) { i += 1; } + return i; +} + +/// Where the module path from f ends on the line ending at t: before a `//` comment, then before +/// trailing spaces, the trailing ';'s and the spaces before them. +pub fn deps_use_to(src: []const u8, f: usize, t: usize) -> usize { + var e : usize = t; + var j : usize = f; + while (j + 1 < t and e == t) { + if (src[j] == '/' and src[j + 1] == '/') { e = j; } + j += 1; + } + while (e > f and deps_space(src[e - 1])) { e -= 1; } + while (e > f and src[e - 1] == ';') { e -= 1; } + while (e > f and deps_space(src[e - 1])) { e -= 1; } + return e; +} + +/// Does src[f..e] read as an import: not empty, and a path with `::` or a word with no space? +pub fn deps_use_ok(src: []const u8, f: usize, e: usize) -> bool { + if (e <= f) { return false; } + var sep : bool = false; + var sp : bool = false; + var i : usize = f; + while (i < e) { + if (src[i] == ':' and i + 1 < e and src[i + 1] == ':') { sep = true; } + if (src[i] == ' ') { sp = true; } + i += 1; + } + return sep or sp == false; +} + +/// Is buf[ts..te] the path root[0..rn]/.t27? +pub fn deps_mod_eq(buf: []const u8, ts: usize, te: usize, root: []const u8, rn: usize, src: []const u8, ps: usize, pe: usize) -> bool { + if (te < ts + rn + 5) { return false; } + if (same(buf, ts, ts + rn, root, 0, rn) == false) { return false; } + if (buf[ts + rn] != '/') { return false; } + var b : usize = ts + rn + 1; + var i : usize = ps; + while (i < pe) { + var c : u8 = src[i]; + var step : usize = 1; + if (c == ':' and i + 1 < pe and src[i + 1] == ':') { c = '/'; step = 2; } + if (c == '.') { c = '/'; } + if (b >= te) { return false; } + if (buf[b] != c) { return false; } + b += 1; + i += step; + } + return same(buf, b, te, ".t27", 0, 4); +} + +/// The end of the module the line's item form falls back to, from ps: the path before a brace list +/// (trailing spaces, then `::`s, then '.'s cut off), or else the path before its last `::` or '.'. +/// Returns ps when there is none, so nothing is matched: one level only, never two. +pub fn deps_fallback_to(src: []const u8, ps: usize, pe: usize) -> usize { + var i : usize = ps; + while (i < pe) { + if (src[i] == '{') { + var e : usize = i; + while (e > ps and deps_space(src[e - 1])) { e -= 1; } + while (e >= ps + 2 and src[e - 1] == ':' and src[e - 2] == ':') { e -= 2; } + while (e > ps and src[e - 1] == '.') { e -= 1; } + return e; + } + i += 1; + } + var last : usize = ps; + i = ps; + while (i < pe) { + if (src[i] == ':' and i + 1 < pe and src[i + 1] == ':') { + last = i; + i += 2; + } else { + if (src[i] == '.') { last = i; } + i += 1; + } + } + return last; +} + +/// Does the edge list buf[0..n] hold an edge from focus[ff..ft] to the module src[ps..pe]? +pub fn deps_imports_mod(buf: []const u8, n: usize, focus: []const u8, ff: usize, ft: usize, root: []const u8, rn: usize, src: []const u8, ps: usize, pe: usize) -> bool { + if (pe <= ps) { return false; } + var s : usize = 0; + while (s < n) { + const e : usize = line_end(buf, s, n); + const t : usize = text_end(buf, s, e); + const p : usize = first_tab(buf, s, t); + if (p < t and p > s) { + if (same(buf, s, p, focus, ff, ft) and deps_mod_eq(buf, p + 1, t, root, rn, src, ps, pe)) { return true; } + } + s = e + 1; + } + return false; +} + +/// One `unknownpath` line for each `use` line of the focus's source src[0..sn] that the edge list +/// buf[0..n] does not resolve, in source order, into out[0..cap]; root[0..rn] is the directory the edge +/// list's paths start with (DEPS_ROOT). A line is resolved when the focus has an edge to the spec its +/// whole path names, or to its one fallback (deps_fallback_to). Returns the bytes written, or cap + 1 +/// when they do not fit: refused whole. +pub fn deps_unknown(buf: []const u8, n: usize, focus: []const u8, ff: usize, ft: usize, root: []const u8, rn: usize, src: []const u8, sn: usize, out: []u8, cap: usize) -> usize { + var w : usize = 0; + var s : usize = 0; + while (s < sn) { + const e : usize = line_end(src, s, sn); + const f : usize = deps_use_from(src, s, e); + if (f < e) { + const pe : usize = deps_use_to(src, f, e); + if (deps_use_ok(src, f, pe)) { + const fb : usize = deps_fallback_to(src, f, pe); + const whole : bool = deps_imports_mod(buf, n, focus, ff, ft, root, rn, src, f, pe); + const part : bool = deps_imports_mod(buf, n, focus, ff, ft, root, rn, src, f, fb); + if (whole == false and part == false) { + w = deps_put_lit(out, w, cap, DEPS_UNKNOWN); + w = deps_put_span(out, w, cap, src, f, pe); + w = put(out, w, cap, '\n'); + } + } + } + s = e + 1; + } + if (w > cap) { return cap + 1; } + return w; +} + // ---- tests ---- // A diamond under the focus a (a imports b and c, both import d), two importers above it (e imports @@ -541,3 +698,64 @@ test negative_control_a_list_that_does_not_fit_is_refused_whole { assert deps_cycles(g, lit_len(g), &set, 64, &b, &done, 9, &out, 64) == 65; assert deps_cycles(g, lit_len(g), &set, 3, &b, &done, 64, &out, 64) == 65; } + +// The focus specs/f.t27 imports specs/a/b.t27 and specs/m.t27. specs/g.t27 imports specs/z.t27, an +// edge that is not the focus's. +pub const DEPS_UNK_EDGES : str = "specs/f.t27\tspecs/a/b.t27\nspecs/f.t27\tspecs/m.t27\nspecs/g.t27\tspecs/z.t27\n$"; + +test every_form_that_resolves_writes_nothing { + var out : [256]u8 = undefined; + const g : str = DEPS_UNK_EDGES; + const src : str = "module f;\nuse a::b;\nuse a::b::Item;\nuse m::{X, Y};\nuse m.k;\nuse a::b ;; // whole, with a comment\n$"; + assert deps_unknown(g, lit_len(g), "specs/f.t27", 0, 11, DEPS_ROOT, 5, src, lit_len(src), &out, 256) == 0; +} + +test each_line_that_resolves_to_no_spec_is_one_unknown_line { + var out : [256]u8 = undefined; + const g : str = DEPS_UNK_EDGES; + const src : str = "module f;\nuse q::r; // a note\nuse a::b;\nuse a::b::c::Item;\nuse z;\nuse n::{P};\n use base::types;\nlet use_count = 1;\n; use q::s;\n$"; + const w : usize = deps_unknown(g, lit_len(g), "specs/f.t27", 0, 11, DEPS_ROOT, 5, src, lit_len(src), &out, 256); + const want : str = "unknown\tq::r\nunknown\ta::b::c::Item\nunknown\tz\nunknown\tn::{P}\nunknown\tbase::types\n$"; + assert w == lit_len(want); + assert same(&out, 0, w, want, 0, lit_len(want)) == true; +} + +test negative_control_an_item_form_falls_back_one_level_only { + var out : [64]u8 = undefined; + const g : str = DEPS_UNK_EDGES; + const one : str = "use a::b::Item;\n$"; + const two : str = "use a::b::c::Item;\n$"; + assert deps_unknown(g, lit_len(g), "specs/f.t27", 0, 11, DEPS_ROOT, 5, one, lit_len(one), &out, 64) == 0; + assert deps_unknown(g, lit_len(g), "specs/f.t27", 0, 11, DEPS_ROOT, 5, two, lit_len(two), &out, 64) == 22; + assert deps_fallback_to(two, 4, 17) == 11; + assert deps_fallback_to("use m::{X}\n$", 4, 10) == 5; + assert deps_fallback_to("use z\n$", 4, 5) == 4; +} + +test negative_control_another_spec_edge_does_not_resolve_the_focus_line { + var out : [64]u8 = undefined; + const g : str = DEPS_UNK_EDGES; + const src : str = "use z;\n$"; + assert deps_unknown(g, lit_len(g), "specs/g.t27", 0, 11, DEPS_ROOT, 5, src, lit_len(src), &out, 64) == 0; + assert deps_unknown(g, lit_len(g), "specs/f.t27", 0, 11, DEPS_ROOT, 5, src, lit_len(src), &out, 64) == 10; +} + +test negative_control_an_unknown_list_that_does_not_fit_is_refused_whole { + var out : [64]u8 = undefined; + const g : str = DEPS_UNK_EDGES; + const src : str = "use q::r;\nuse z;\n$"; + assert deps_unknown(g, lit_len(g), "specs/f.t27", 0, 11, DEPS_ROOT, 5, src, lit_len(src), &out, 23) == 23; + assert deps_unknown(g, lit_len(g), "specs/f.t27", 0, 11, DEPS_ROOT, 5, src, lit_len(src), &out, 22) == 23; +} + +test the_path_reader_matches_the_resolver { + const l : str = " use a::b.c ;; // x\n$"; + const f : usize = deps_use_from(l, 0, 21); + assert f == 7; + assert deps_use_to(l, f, 21) == 13; + assert deps_use_ok(l, f, 13) == true; + assert deps_use_ok("use a b\n$", 4, 7) == false; + assert deps_use_from("use\tx\n$", 0, 5) == 5; + assert deps_mod_eq("specs/a/b/c.t27$", 0, 15, DEPS_ROOT, 5, l, 7, 13) == true; + assert deps_mod_eq("specs/a/b/c.t2$", 0, 14, DEPS_ROOT, 5, l, 7, 13) == false; +}