OverviewFilesCommitsIssuesDocsReleasesPeers

codetree commits / b4cd6d5

Wires get their own lane; and say what the edges cannot about dependencies
## Lanes Every wire took the shortest channel, so unrelated wires stacked exactly on top of each other and read as one wire — or as a short between two circuits that are not connected at all. A finished route now CLAIMS the cells it runs through, and a later route pays laneCost to reuse them, so wires spread into parallel lanes. Wires from the SAME source are exempt and still share their trunk: that is what makes a junction a junction rather than a crossing. Both are asserted, by measuring how much collinear length two routes share. ## The dependency gap, measured "are you sure we are handling full deps list and even deps of deps?" — no, and the numbers say so plainly: declared dependencies across 56 modules 684 drawn as edges (both ends in the scan) 105 NOT drawn (external packages) 579 A graph can only draw an edge when both ends are nodes on screen. atproto-identity declares ten dependencies and has ZERO in the scan, so the drawing alone says it depends on nothing. 85% of the dependency data was invisible, and nothing said so. Selecting a module now opens a panel with the FULL list: every direct dependency marked in-scan or external, the count actually drawn as edges, and the transitive ones with their hop counts. For ap-bridge: 23 declared, 9 drawn, one reachable at two hops. The closure highlighting already worked — it just had little to show, because most of a module's dependencies were never in the graph to begin with. ## Still one hop The GRAPH tab highlights immediate connections only; it has no closure and no panel. That is the remaining half of "i am not seeing it in graph or node view", and it is untouched here rather than half-done. Suite: 235 tests, 5131 asserts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
b4cd6d59a8a1babedea52e2f8726e84965d01e74
by plankatron <fleeky@gmail.com> · 2026-09-21T01:11:17.000Z

Changed files (3) +184 -7

lib/floor/routing.js +49 -7
……@@ -21,6 +21,14 @@ const DEFAULTS = {
2121 grid: 20, // routing resolution; also the minimum clearance around a node
2222 clearance: 10, // how far a route stays off a node's edge
2323 turnCost: 6, // a turn costs this many grid steps, so paths stay straight
24
25 // What it costs to run down a lane another wire is already using.
26 //
27 // Without this every wire takes the same shortest channel and they stack
28 // exactly on top of each other, which reads as one wire — or as a short
29 // between two circuits that are not connected at all. Siblings are exempt:
30 // wires from one source SHOULD share a trunk, that is what makes a junction.
31 laneCost: 9,
2432 maxCells: 400000 // refuse to search a board this big rather than hang the tab
2533}
2634
……@@ -82,19 +90,44 @@ export function routeEdges (nodes, links, opts = {}) {
8290 trunkOf.set(l.from, n.x + n.w + O.grid * 2)
8391 }
8492
93 // Which source last claimed each cell, so a later route can tell "my own
94 // trunk" from "somebody else's lane".
95 const laneOwner = new Map()
96
8597 const routes = []
8698 let failed = 0
8799 for (const l of valid) {
88100 const a = byId.get(l.from)
89101 const b = byId.get(l.to)
90 const path = routeOne(a, b, trunkOf.get(l.from), l.fromPoint, l.toPoint)
91 if (path) routes.push({ from: l.from, to: l.to, points: path })
92 else failed++
102 const path = routeOne(a, b, trunkOf.get(l.from), l.fromPoint, l.toPoint, l.from)
103 if (path) {
104 routes.push({ from: l.from, to: l.to, points: path })
105 claimLanes(path, l.from)
106 } else failed++
93107 }
94108 return { routes, failed }
95109
110 // Mark every cell a finished route runs through, so the next one prefers a
111 // free lane beside it rather than the same one.
112 function claimLanes (points, source) {
113 for (let i = 1; i < points.length; i++) {
114 const from = points[i - 1]
115 const to = points[i]
116 const steps = Math.max(
117 Math.abs(toCellX(to.x) - toCellX(from.x)),
118 Math.abs(toCellY(to.y) - toCellY(from.y))
119 )
120 for (let step = 0; step <= steps; step++) {
121 const t = steps === 0 ? 0 : step / steps
122 const cx = clampCell(toCellX(from.x + (to.x - from.x) * t), cols)
123 const cy = clampCell(toCellY(from.y + (to.y - from.y) * t), rows)
124 laneOwner.set(cy * cols + cx, source)
125 }
126 }
127 }
128
96129 // --- one route, as grid cells, then simplified back into world points.
97 function routeOne (a, b, trunkX, fromPoint, toPoint) {
130 function routeOne (a, b, trunkX, fromPoint, toPoint, source) {
98131 // A caller with real SOCKETS supplies the exact points the wire must meet;
99132 // otherwise the ports sit where a header socket would be. Either way the
100133 // first ROUTABLE cell is a full grid step clear of the node, outside its
……@@ -123,7 +156,8 @@ export function routeEdges (nodes, links, opts = {}) {
123156 }
124157
125158 const trunkCell = trunkX === undefined ? null : clampX(toCellX(trunkX))
126 const cells = astar(start, goal, strict, trunkCell) || astar(start, goal, permissive, trunkCell)
159 const cells = astar(start, goal, strict, trunkCell, source) ||
160 astar(start, goal, permissive, trunkCell, source)
127161 if (!cells) return null
128162
129163 const points = cells.map(c => ({ x: minX + c.cx * O.grid, y: minY + c.cy * O.grid }))
……@@ -142,6 +176,7 @@ export function routeEdges (nodes, links, opts = {}) {
142176 return simplify([...head, ...points, ...tail])
143177 }
144178
179 function clampCell (v, limit) { return Math.max(0, Math.min(limit - 1, v)) }
145180 function clampX (cx) { return Math.max(0, Math.min(cols - 1, cx)) }
146181 function clampY (cy) { return Math.max(0, Math.min(rows - 1, cy)) }
147182
……@@ -151,7 +186,7 @@ export function routeEdges (nodes, links, opts = {}) {
151186 * A small bonus for running along the source's trunk keeps sibling edges
152187 * together so they can share it.
153188 */
154 function astar (start, goal, passable, trunkCx) {
189 function astar (start, goal, passable, trunkCx, source) {
155190 const startKey = start.cy * cols + start.cx
156191 const goalKey = goal.cy * cols + goal.cx
157192
……@@ -181,7 +216,14 @@ export function routeEdges (nodes, links, opts = {}) {
181216
182217 const turn = current.dir !== 0 && current.dir !== d.id ? O.turnCost : 0
183218 const trunkBonus = trunkCx !== null && nx === trunkCx ? -0.4 : 0
184 const tentative = (gScore.get(current.key + ':' + current.dir) || 0) + 1 + turn + trunkBonus
219
220 // Somebody else's lane costs extra; my own source's trunk does not,
221 // because sharing it is the point.
222 const claimed = laneOwner.get(ny * cols + nx)
223 const lane = claimed !== undefined && claimed !== source ? O.laneCost : 0
224
225 const tentative = (gScore.get(current.key + ':' + current.dir) || 0) +
226 1 + turn + trunkBonus + lane
185227
186228 const nKey = ny * cols + nx
187229 const stateKey = nKey + ':' + d.id
lib/render-nodes.js +76 -0
……@@ -77,6 +77,19 @@ export const NODE_VIEW_CSS = `
7777}
7878#node-legend label { pointer-events: auto; }
7979#node-legend b { color: #c9d1d9; }
80#nv-deps {
81 position: absolute; top: 12px; right: 12px; width: 300px; max-height: 70%;
82 overflow-y: auto; background: #1c1d21; border: 1px solid #2f3136;
83 border-radius: 6px; padding: 10px 12px; font-size: 11px; color: #8b949e;
84 display: none; line-height: 1.6;
85}
86#nv-deps.open { display: block; }
87#nv-deps h4 { margin: 0 0 6px; color: #e8e8ea; font-size: 11px; }
88#nv-deps .grp { margin-top: 8px; color: #c9d1d9; font-weight: 600; }
89#nv-deps .row { display: flex; justify-content: space-between; gap: 8px; }
90#nv-deps .ext { color: #6e7076; }
91#nv-deps .hops { font-family: ui-monospace, monospace; font-size: 9px; }
92
8093#node-hint {
8194 position: absolute; bottom: 12px; right: 12px; background: #1c1d21;
8295 border: 1px solid #2f3136; border-radius: 6px; padding: 8px 12px;
……@@ -89,6 +102,7 @@ export const NODE_VIEW_HTML = `
89102 <svg id="node-svg"><defs></defs></svg>
90103 <div id="node-breadcrumb"></div>
91104 <div id="node-legend"></div>
105 <div id="nv-deps"></div>
92106 <div id="node-hint">drag to pan &middot; scroll to zoom &middot; click a node title to open it</div>
93107 </div>
94108`
……@@ -680,6 +694,8 @@ function nvRender () {
680694 nvDrawFrames(frameG)
681695 for (var i = 0; i < NV.nodes.length; i++) nvDrawNode(nodeG, NV.nodes[i])
682696
697 nvDepsPanel()
698
683699 // Dependencies of dependencies: clicking a node traces its WHOLE closure, not
684700 // one hop, with distance carried by colour. Same shared implementation the
685701 // floor uses, so both views agree about what "depends on" means.
……@@ -803,6 +819,66 @@ function nvDrawNode (parent, n) {
803819 NV.nodeEls[n.id] = g
804820}
805821
822/**
823 * The selected node's FULL dependency list.
824 *
825 * The graph can only draw an edge when both ends are nodes on screen, and most
826 * dependencies are not: measured on this scan, 684 dependencies are declared and
827 * only 105 have both ends in the scan. A module like atproto-identity declares
828 * ten and has NONE in-scan, so the drawing alone says it depends on nothing.
829 * This panel says what the edges cannot.
830 */
831function nvDepsPanel () {
832 var panel = document.getElementById('nv-deps')
833 if (!panel) return
834 var node = null
835 for (var i = 0; i < NV.nodes.length; i++) if (NV.nodes[i].id === NV.selected) node = NV.nodes[i]
836 if (!node || node.kind !== 'module' || !tree.modules[node.title]) {
837 panel.className = ''
838 panel.innerHTML = ''
839 return
840 }
841
842 var mod = tree.modules[node.title]
843 var direct = Object.keys(mod.dependencies || {}).sort()
844 var inScan = {}
845 for (var m = 0; m < tree.system.modules.length; m++) inScan[tree.system.modules[m]] = true
846
847 var html = '<h4>' + escapeHtml(node.title) + ' dependencies</h4>'
848 html += '<div>' + direct.length + ' declared &middot; ' +
849 direct.filter(function (d) { return inScan[d] }).length + ' drawn as edges</div>'
850
851 html += '<div class="grp">direct</div>'
852 for (var d = 0; d < direct.length; d++) {
853 var isIn = !!inScan[direct[d]]
854 html += '<div class="row"><span' + (isIn ? '' : ' class="ext"') + '>' +
855 escapeHtml(direct[d]) + '</span><span class="hops">' +
856 (isIn ? 'in scan' : 'external') + '</span></div>'
857 }
858
859 // Transitive, through the modules the scan actually has.
860 if (window.__ct) {
861 var closure = window.__ct.dependencyClosure(NV.links, NV.selected)
862 var rows = []
863 for (var j = 0; j < NV.nodes.length; j++) {
864 var other = NV.nodes[j]
865 var depth = closure.nodes.get(other.id)
866 if (depth === undefined || depth < 2) continue
867 rows.push({ name: other.title, depth: depth })
868 }
869 rows.sort(function (a, b) { return a.depth - b.depth || (a.name < b.name ? -1 : 1) })
870 html += '<div class="grp">through those (' + rows.length + ')</div>'
871 if (rows.length === 0) html += '<div class="ext">none - every dependency is direct</div>'
872 for (var r = 0; r < rows.length; r++) {
873 html += '<div class="row"><span>' + escapeHtml(rows[r].name) +
874 '</span><span class="hops">' + rows[r].depth + ' hops</span></div>'
875 }
876 }
877
878 panel.innerHTML = html
879 panel.className = 'open'
880}
881
806882function nvFocus (node) {
807883 var connected = {}
808884 connected[node.id] = true
test/routing.test.js +59 -0
……@@ -122,6 +122,65 @@ test('routing: a backward edge goes around rather than through', (t) => {
122122 }
123123})
124124
125// How much of segment A is covered by a collinear segment of B.
126function sharedLength (a1, a2, b1, b2) {
127 const aVert = Math.abs(a1.x - a2.x) < 0.01
128 const bVert = Math.abs(b1.x - b2.x) < 0.01
129 if (aVert !== bVert) return 0
130 if (aVert) {
131 if (Math.abs(a1.x - b1.x) > 0.01) return 0
132 const lo = Math.max(Math.min(a1.y, a2.y), Math.min(b1.y, b2.y))
133 const hi = Math.min(Math.max(a1.y, a2.y), Math.max(b1.y, b2.y))
134 return Math.max(0, hi - lo)
135 }
136 if (Math.abs(a1.y - b1.y) > 0.01) return 0
137 const lo = Math.max(Math.min(a1.x, a2.x), Math.min(b1.x, b2.x))
138 const hi = Math.min(Math.max(a1.x, a2.x), Math.max(b1.x, b2.x))
139 return Math.max(0, hi - lo)
140}
141
142function overlapBetween (r1, r2) {
143 let total = 0
144 for (let i = 1; i < r1.points.length; i++) {
145 for (let j = 1; j < r2.points.length; j++) {
146 total += sharedLength(r1.points[i - 1], r1.points[i], r2.points[j - 1], r2.points[j])
147 }
148 }
149 return total
150}
151
152test('routing: wires from DIFFERENT sources get their own lane', (t) => {
153 // Unrelated wires drawn down the same channel sit exactly on top of each
154 // other, which reads as one wire — or worse, as a short between two circuits
155 // that are not connected at all.
156 const nodes = [
157 rect('a1', 0, 0), rect('a2', 0, 200), rect('a3', 0, 400),
158 rect('b1', 700, 400), rect('b2', 700, 200), rect('b3', 700, 0)
159 ]
160 const links = [
161 { from: 'a1', to: 'b1' }, { from: 'a2', to: 'b2' }, { from: 'a3', to: 'b3' }
162 ]
163 const { routes } = routeEdges(nodes, links)
164
165 for (let i = 0; i < routes.length; i++) {
166 for (let j = i + 1; j < routes.length; j++) {
167 const shared = overlapBetween(routes[i], routes[j])
168 t.ok(shared < 60, routes[i].from + ' and ' + routes[j].from +
169 ' share only ' + Math.round(shared) + 'px of lane')
170 }
171 }
172})
173
174test('routing: wires from the SAME source still share their trunk', (t) => {
175 // Lane separation must not break the bus: siblings SHOULD run together, that
176 // is what makes a junction a junction rather than a crossing.
177 const nodes = [rect('src', 0, 200), rect('t1', 600, 0), rect('t2', 600, 400)]
178 const { routes } = routeEdges(nodes, [{ from: 'src', to: 't1' }, { from: 'src', to: 't2' }])
179
180 t.is(routes.length, 2, 'both routed')
181 t.ok(overlapBetween(routes[0], routes[1]) > 0, 'siblings do share a trunk')
182})
183
125184test('routing: deterministic, and degenerate input does not throw', (t) => {
126185 const nodes = [rect('a', 0, 0), rect('b', 300, 0)]
127186 const links = [{ from: 'a', to: 'b' }]