@procedures/dag 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Importing this file registers the procedures of @procedures/dag in the global registry:
3
+ * `import "@procedures/dag/register"`. The package root and the `./module` entry register nothing
4
+ * (ARCHITECTURE-PROPOSALS-2026-10, P9). `dag.traverse` calls `lib.scan`: also import
5
+ * `@procedures/lib/register`.
6
+ */
7
+ import { registerDagProcedures } from "./module.js";
8
+ export * from "./module.js";
9
+ registerDagProcedures();
10
+ //# sourceMappingURL=register.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpD,cAAc,aAAa,CAAC;AAE5B,qBAAqB,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@procedures/dag",
4
- "version": "0.2.3",
5
- "description": "Dependency graph algorithms for TypeScript: build a DAG, detect cycles, compute topological levels and traverse in parallel with a concurrency limit.",
4
+ "version": "0.4.0",
5
+ "description": "Dependency graph algorithms for TypeScript: build a DAG, detect cycles, compute topological levels and traverse in parallel with a concurrency limit. The procedure dag.traverse visits workspace packages in dependency order.",
6
6
  "keywords": [
7
7
  "dag",
8
8
  "graph",
@@ -30,6 +30,14 @@
30
30
  ".": {
31
31
  "types": "./dist/index.d.ts",
32
32
  "import": "./dist/index.js"
33
+ },
34
+ "./module": {
35
+ "types": "./dist/module.d.ts",
36
+ "import": "./dist/module.js"
37
+ },
38
+ "./register": {
39
+ "types": "./dist/register.d.ts",
40
+ "import": "./dist/register.js"
33
41
  }
34
42
  },
35
43
  "files": [
@@ -40,7 +48,9 @@
40
48
  "!src/**/__tests__",
41
49
  "CHANGELOG.md"
42
50
  ],
43
- "sideEffects": false,
51
+ "sideEffects": [
52
+ "./dist/register.js"
53
+ ],
44
54
  "engines": {
45
55
  "node": ">=22"
46
56
  },
@@ -54,6 +64,13 @@
54
64
  "@mark1russell7/cue": "^1.0.0",
55
65
  "vitest": "^5.0.3"
56
66
  },
67
+ "dependencies": {
68
+ "@procedures/client": "0.4.0",
69
+ "zod": "^4.6.5"
70
+ },
71
+ "client": {
72
+ "procedures": "./dist/register.js"
73
+ },
57
74
  "scripts": {
58
75
  "build": "tsc -b",
59
76
  "typecheck": "tsc --noEmit",
package/src/index.ts CHANGED
@@ -62,3 +62,18 @@ export {
62
62
  createNode,
63
63
  buildNodeMap,
64
64
  } from "./dag/index.js";
65
+
66
+ // The procedure dag.traverse. The root registers nothing: import "@procedures/dag/register", or
67
+ // give the module to createRegistry() with the module of @procedures/lib.
68
+ export * from "./module.js";
69
+ export {
70
+ DagTraverseInputSchema,
71
+ DagTraverseOutputSchema,
72
+ type DagTraverseInput,
73
+ type DagTraverseOutput,
74
+ type TraverseNodeResult,
75
+ type ScannedPackage,
76
+ type ScanOutput,
77
+ type PackageNode,
78
+ } from "./procedures/types.js";
79
+ export { dagTraverse, packageNodes, callsGit, keyedLock, repositoryRoot } from "./procedures/traverse.js";
package/src/module.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Module (no side effects): the procedures of this package as data, for `createRegistry()`.
3
+ *
4
+ * The package has one procedure, `dag.traverse`. It came from `@procedures/lib`, and its name
5
+ * stays the same. The graph algorithms of the package root do not need this module.
6
+ */
7
+
8
+ import {
9
+ createProcedure,
10
+ zodAdapter,
11
+ RUNS_REFS_TAG,
12
+ defineModule,
13
+ PROCEDURE_REGISTRY,
14
+ registerModules,
15
+ type ProcedureContext,
16
+ type ProcedureModule,
17
+ type ProcedureRegistry,
18
+ } from "@procedures/client";
19
+ import { dagTraverse } from "./procedures/traverse.js";
20
+ import {
21
+ DagTraverseInputSchema,
22
+ DagTraverseOutputSchema,
23
+ type DagTraverseInput,
24
+ type DagTraverseOutput,
25
+ } from "./procedures/types.js";
26
+
27
+ // =============================================================================
28
+ // dag.* Procedures
29
+ // =============================================================================
30
+
31
+ const dagTraverseProcedure = createProcedure()
32
+ .path(["dag", "traverse"])
33
+ .input(zodAdapter<DagTraverseInput>(DagTraverseInputSchema))
34
+ .output(zodAdapter<DagTraverseOutput>(DagTraverseOutputSchema))
35
+ .meta({
36
+ description: "Traverse ecosystem packages in dependency order, executing visit procedure for each",
37
+ // It runs the procedure refs of its input: a server with an expose rule limits what they call
38
+ tags: [RUNS_REFS_TAG],
39
+ // It gets the packages from lib.scan (@procedures/lib). This package cannot import that one,
40
+ // because @procedures/lib depends on this package.
41
+ calls: ["lib.scan"],
42
+ // Its own effect is the scan of the workspace. The admission examines each visit call.
43
+ effects: { reads: ["fs"], writes: [] },
44
+ args: [],
45
+ shorts: { root: "r", concurrency: "j", continueOnError: "c", dryRun: "d" },
46
+ output: "streaming",
47
+ })
48
+ .handler(async (input: DagTraverseInput, ctx: ProcedureContext): Promise<DagTraverseOutput> => {
49
+ return dagTraverse(input, ctx);
50
+ })
51
+ .build();
52
+
53
+ // =============================================================================
54
+ // Registration
55
+ // =============================================================================
56
+
57
+ /**
58
+ * The module of this package, for `createRegistry()`: its procedures as data. Importing this
59
+ * file registers nothing. (`register.ts` registers the same procedures in the global registry.)
60
+ *
61
+ * `dag.traverse` uses `lib.scan`. This module cannot list the module of `@procedures/lib` as a
62
+ * dependency, because that package depends on this one. A host registers both modules
63
+ * (`bundle-dev`, `bundle-mcp` and `mark` do).
64
+ */
65
+ export const dagModule: ProcedureModule = defineModule("@procedures/dag", [dagTraverseProcedure]);
66
+
67
+ export default dagModule;
68
+
69
+ /**
70
+ * This function registers the procedures of @procedures/dag in a registry. The default is the
71
+ * global registry. Importing this file registers nothing: the `./register` entry starts this
72
+ * function (ARCHITECTURE-PROPOSALS-2026-10, P9).
73
+ */
74
+ export function registerDagProcedures(registry: ProcedureRegistry = PROCEDURE_REGISTRY): void {
75
+ registerModules(registry, [dagModule]);
76
+ }
@@ -0,0 +1,308 @@
1
+ /**
2
+ * dag.traverse procedure
3
+ *
4
+ * General-purpose DAG traversal that executes a visit procedure
5
+ * for each node in dependency order.
6
+ *
7
+ * This enables declarative composition of traversals:
8
+ * ```typescript
9
+ * // Execute git add on all packages (using $when: "$parent")
10
+ * await client.exec({
11
+ * $proc: ["dag", "traverse"],
12
+ * input: {
13
+ * visit: {
14
+ * $proc: ["git", "add"],
15
+ * input: { all: true },
16
+ * $when: "$parent" // Defer to dag.traverse
17
+ * }
18
+ * }
19
+ * });
20
+ *
21
+ * // Simple form with just procedure path
22
+ * await client.exec({
23
+ * $proc: ["dag", "traverse"],
24
+ * input: {
25
+ * visit: ["git", "add"]
26
+ * }
27
+ * });
28
+ * ```
29
+ *
30
+ * The `$when` field controls when nested $proc refs are executed:
31
+ * - "$immediate" (default): Execute during hydration
32
+ * - "$parent": Defer to parent procedure (dag.traverse executes per-node)
33
+ * - "$never": Never auto-execute, pass as pure data
34
+ *
35
+ * The procedure gets the packages of the workspace from `lib.scan` of `@procedures/lib`, through
36
+ * `ctx.client`. It does not import that package, because `@procedures/lib` depends on this one.
37
+ * Thus a registry with `dag.traverse` also needs the module of `@procedures/lib`.
38
+ */
39
+
40
+ import { existsSync } from "node:fs";
41
+ import { dirname, join, resolve } from "node:path";
42
+ import type { ProcedureContext, ProcedurePath } from "@procedures/client";
43
+ import { isAnyProcedureRef, hydrateInput, scopeOf } from "@procedures/client";
44
+ import type {
45
+ DagTraverseInput,
46
+ DagTraverseOutput,
47
+ PackageNode,
48
+ ScanOutput,
49
+ TraverseNodeResult,
50
+ } from "./types.js";
51
+ import {
52
+ buildLeveledDAG,
53
+ executeDAG,
54
+ createProcessor,
55
+ filterDAGFromRoot,
56
+ } from "../dag/index.js";
57
+
58
+ /**
59
+ * This function gives the nodes of the packages of a `lib.scan` output. A dependency on a
60
+ * package that the scan did not find is not an edge.
61
+ */
62
+ export function packageNodes(packages: ScanOutput["packages"]): Map<string, PackageNode> {
63
+ const nodes = new Map<string, PackageNode>();
64
+ for (const [name, info] of Object.entries(packages)) {
65
+ nodes.set(name, {
66
+ id: name,
67
+ name,
68
+ repoPath: info.repoPath,
69
+ dependencies: info.mark1russell7Deps.filter((dep) => packages[dep] !== undefined),
70
+ });
71
+ }
72
+ return nodes;
73
+ }
74
+
75
+ /** The folder of the git repository that holds `path` (it has `.git`), or `path` itself. */
76
+ export function repositoryRoot(path: string): string {
77
+ let current = resolve(path);
78
+ for (;;) {
79
+ if (existsSync(join(current, ".git"))) return current;
80
+ const parent = dirname(current);
81
+ if (parent === current) return resolve(path);
82
+ current = parent;
83
+ }
84
+ }
85
+
86
+ /** True when the value names a `git` procedure: a path, or a `$proc` ref at any depth. */
87
+ export function callsGit(value: unknown): boolean {
88
+ if (Array.isArray(value)) {
89
+ if (value[0] === "git" && value.every((part) => typeof part === "string")) return true;
90
+ return value.some(callsGit);
91
+ }
92
+ if (typeof value === "object" && value !== null) {
93
+ return Object.values(value).some(callsGit);
94
+ }
95
+ return false;
96
+ }
97
+
98
+ /**
99
+ * Run the tasks of one key one at a time, in the order of the calls. Tasks of other keys run
100
+ * in parallel.
101
+ */
102
+ export function keyedLock(): <T>(key: string, task: () => Promise<T>) => Promise<T> {
103
+ const tails = new Map<string, Promise<void>>();
104
+ return async <T>(key: string, task: () => Promise<T>): Promise<T> => {
105
+ const previous = tails.get(key) ?? Promise.resolve();
106
+ let release!: () => void;
107
+ const done = new Promise<void>((resolveDone) => {
108
+ release = resolveDone;
109
+ });
110
+ const tail = previous.then(() => done);
111
+ tails.set(key, tail);
112
+ await previous;
113
+ try {
114
+ return await task();
115
+ } finally {
116
+ release();
117
+ if (tails.get(key) === tail) tails.delete(key);
118
+ }
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Execute a DAG traversal with a custom visit procedure
124
+ *
125
+ * The visits of nodes in one git repository run one at a time when the visit calls git (or
126
+ * with `serialize: "repository"`). All the packages of a workspace share one repository, and
127
+ * concurrent `git add` calls failed on `index.lock` (deep dive DATA-17).
128
+ */
129
+ export async function dagTraverse(
130
+ input: DagTraverseInput,
131
+ ctx: ProcedureContext
132
+ ): Promise<DagTraverseOutput> {
133
+ const startTime = Date.now();
134
+ const results: TraverseNodeResult[] = [];
135
+
136
+ // Scan for all packages (lib.scan of @procedures/lib)
137
+ const scanResult = await ctx.client.call<{ rootPath?: string }, ScanOutput>(
138
+ ["lib", "scan"],
139
+ input.rootPath !== undefined ? { rootPath: input.rootPath } : {}
140
+ );
141
+ let allNodes = packageNodes(scanResult.packages);
142
+
143
+ // Filter to specific packages if requested
144
+ if (input.filter && input.filter.length > 0) {
145
+ const filtered = new Map<string, PackageNode>();
146
+ for (const name of input.filter) {
147
+ const node = allNodes.get(name);
148
+ if (node) {
149
+ filtered.set(name, node);
150
+ }
151
+ }
152
+ allNodes = filtered;
153
+ }
154
+
155
+ // Filter to root and its dependencies if requested
156
+ if (input.root) {
157
+ const rootNode = allNodes.get(input.root);
158
+ if (!rootNode) {
159
+ return {
160
+ success: false,
161
+ results: [],
162
+ totalDuration: Date.now() - startTime,
163
+ visited: 0,
164
+ failed: 0,
165
+ };
166
+ }
167
+ allNodes = filterDAGFromRoot(allNodes, input.root);
168
+ }
169
+
170
+ // Build DAG for dependency-ordered execution
171
+ const dag = buildLeveledDAG(allNodes);
172
+
173
+ // Determine how to execute the visit procedure
174
+ // visit can be:
175
+ // - Array: procedure path like ["git", "add"]
176
+ // - $proc ref: { $proc: [...], input: {...}, $when: "$parent" }
177
+ const visitIsRef = isAnyProcedureRef(input.visit);
178
+ let visitPath: ProcedurePath;
179
+ let baseInput: unknown = {};
180
+
181
+ if (Array.isArray(input.visit)) {
182
+ visitPath = input.visit;
183
+ } else if (visitIsRef && typeof input.visit === "object" && "$proc" in input.visit) {
184
+ visitPath = input.visit.$proc;
185
+ baseInput = input.visit.input ?? {};
186
+ } else {
187
+ throw new Error("visit must be a procedure path or $proc reference with $when");
188
+ }
189
+
190
+ const serialize = input.serialize ?? "auto";
191
+ const oneAtATime = serialize === "repository" || (serialize === "auto" && callsGit(input.visit));
192
+ const lock = keyedLock();
193
+
194
+ // Create processor that executes visit procedure per node
195
+ const processor = createProcessor(async (node: PackageNode) => {
196
+ if (oneAtATime) {
197
+ await lock(repositoryRoot(node.repoPath), () => visit(node));
198
+ } else {
199
+ await visit(node);
200
+ }
201
+ });
202
+
203
+ async function visit(node: PackageNode): Promise<void> {
204
+ if (input.dryRun) {
205
+ results.push({
206
+ name: node.name,
207
+ path: node.repoPath,
208
+ success: true,
209
+ duration: 0,
210
+ output: { dryRun: true, wouldExecute: visitPath },
211
+ });
212
+ return;
213
+ }
214
+
215
+ const nodeStartTime = Date.now();
216
+
217
+ try {
218
+ // Merge base input with node context
219
+ const visitInput = {
220
+ ...(typeof baseInput === "object" ? baseInput : {}),
221
+ cwd: node.repoPath,
222
+ node: {
223
+ name: node.name,
224
+ path: node.repoPath,
225
+ dependencies: node.dependencies,
226
+ },
227
+ };
228
+
229
+ // Create executor for hydration that injects cwd into every call
230
+ const executor = async <TIn, TOut>(path: ProcedurePath, inp: TIn): Promise<TOut> => {
231
+ // Inject cwd into the input so nested procedures run in correct directory
232
+ const inputWithCwd = typeof inp === "object" && inp !== null
233
+ ? { ...inp, cwd: node.repoPath }
234
+ : inp;
235
+ return ctx.client.call(path, inputWithCwd) as Promise<TOut>;
236
+ };
237
+
238
+ // Hydrate the input (execute any nested procedure refs like chain steps)
239
+ // Push "dag.traverse" onto context stack so refs with $when: "dag.traverse" execute
240
+ const hydratedInput = await hydrateInput(visitInput, executor, {
241
+ contextStack: ["dag.traverse"],
242
+ onRef: ctx.onRef,
243
+ // The names of the chain around this procedure: a `$ref` in the visit can read them
244
+ scope: scopeOf(input),
245
+ });
246
+
247
+ // Execute visit procedure with hydrated input
248
+ const output = await ctx.client.call(visitPath, hydratedInput);
249
+
250
+ results.push({
251
+ name: node.name,
252
+ path: node.repoPath,
253
+ success: true,
254
+ duration: Date.now() - nodeStartTime,
255
+ output,
256
+ });
257
+ } catch (error) {
258
+ const result: TraverseNodeResult = {
259
+ name: node.name,
260
+ path: node.repoPath,
261
+ success: false,
262
+ duration: Date.now() - nodeStartTime,
263
+ error: error instanceof Error ? error.message : String(error),
264
+ };
265
+ results.push(result);
266
+ throw error; // Re-throw for DAG executor to handle
267
+ }
268
+ }
269
+
270
+ // Execute DAG traversal
271
+ const dagResult = await executeDAG(dag, processor, {
272
+ concurrency: input.concurrency ?? 4,
273
+ failFast: !input.continueOnError,
274
+ });
275
+
276
+ // If dry run, we already populated results in the processor
277
+ if (!input.dryRun) {
278
+ // Ensure all nodes have results (some may have been skipped due to errors)
279
+ for (const [name, nodeResult] of dagResult.results) {
280
+ const existing = results.find((r) => r.name === name);
281
+ // No test reaches this branch: each visit adds its result before it ends or throws, and
282
+ // executeDAG gives no result for a node of a level that it skipped. The branch came with
283
+ // the procedure from client-lib (2026-10). It stays until a decision about it.
284
+ /* v8 ignore if -- @preserve */
285
+ if (!existing) {
286
+ const node = allNodes.get(name);
287
+ const result: TraverseNodeResult = {
288
+ name,
289
+ path: node?.repoPath ?? "unknown",
290
+ success: nodeResult.success,
291
+ duration: nodeResult.duration,
292
+ };
293
+ if (nodeResult.error?.message) {
294
+ result.error = nodeResult.error.message;
295
+ }
296
+ results.push(result);
297
+ }
298
+ }
299
+ }
300
+
301
+ return {
302
+ success: dagResult.success,
303
+ results,
304
+ totalDuration: Date.now() - startTime,
305
+ visited: results.filter((r) => r.success).length,
306
+ failed: results.filter((r) => !r.success).length,
307
+ };
308
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The types and schemas of the `dag.traverse` procedure.
3
+ *
4
+ * The procedure came from `@procedures/lib` (roadmap, items for a decision). Its name stays
5
+ * `dag.traverse`.
6
+ */
7
+
8
+ import { z } from "zod";
9
+ import type { DAGNode } from "../types.js";
10
+
11
+ /**
12
+ * Schema for $proc references with $when control.
13
+ */
14
+ const ProcRefSchema: z.ZodObject<{
15
+ $proc: z.ZodArray<z.ZodString>;
16
+ input: z.ZodOptional<z.ZodUnknown>;
17
+ $when: z.ZodOptional<z.ZodString>;
18
+ $name: z.ZodOptional<z.ZodString>;
19
+ }> = z.object({
20
+ $proc: z.array(z.string()),
21
+ input: z.unknown().optional(),
22
+ $when: z.string().optional(),
23
+ $name: z.string().optional(),
24
+ });
25
+
26
+ export const DagTraverseInputSchema: z.ZodObject<{
27
+ visit: z.ZodUnion<readonly [z.ZodArray<z.ZodString>, typeof ProcRefSchema]>;
28
+ filter: z.ZodOptional<z.ZodArray<z.ZodString>>;
29
+ root: z.ZodOptional<z.ZodString>;
30
+ rootPath: z.ZodOptional<z.ZodString>;
31
+ concurrency: z.ZodDefault<z.ZodNumber>;
32
+ serialize: z.ZodDefault<z.ZodEnum<{ auto: "auto"; repository: "repository"; none: "none" }>>;
33
+ continueOnError: z.ZodDefault<z.ZodBoolean>;
34
+ dryRun: z.ZodDefault<z.ZodBoolean>;
35
+ }> = z.object({
36
+ /**
37
+ * The procedure to start for each node.
38
+ * - A list: a procedure path, for example ["git", "add"]
39
+ * - A `$proc` ref with `$when: "$never"` or `"$parent"`: deferred. The traversal starts it for each node.
40
+ *
41
+ * Example:
42
+ * ```json
43
+ * { "$proc": ["git", "add"], "input": { "all": true }, "$when": "$parent" }
44
+ * ```
45
+ */
46
+ visit: z.union([
47
+ z.array(z.string()),
48
+ ProcRefSchema,
49
+ ]).describe("Procedure to execute for each node. - Array: procedure path, e.g., [\"git\", \"add\"] - $proc with $when: \"$never\" or \"$parent\": deferred, executed per-node Example: ```json { \"$proc\": [\"git\", \"add\"], \"input\": { \"all\": true }, \"$when\": \"$parent\" } ```"),
50
+ /** Filter to specific package names */
51
+ filter: z.array(z.string()).optional().describe("Filter to specific package names"),
52
+ /** Start from specific root package */
53
+ root: z.string().optional().describe("Start from specific root package"),
54
+ /** The workspace root that `lib.scan` reads (default: the pnpm workspace that contains client-lib) */
55
+ rootPath: z.string().optional().describe("Workspace root (defaults to the pnpm workspace that contains client-lib)"),
56
+ /** Max parallel operations (default: 4) */
57
+ concurrency: z.number().default(4).describe("Max parallel operations (default: 4)"),
58
+ /**
59
+ * When the visits of nodes in one git repository start one at a time (default: "auto").
60
+ * - "auto": when the visit uses a `git` procedure (git locks the index of the repository)
61
+ * - "repository": always
62
+ * - "none": not at any time
63
+ */
64
+ serialize: z.enum(["auto", "repository", "none"]).default("auto").describe("When the visits of nodes in one git repository run one at a time (default: \"auto\"). - \"auto\": when the visit calls a `git` procedure (git locks the index of the repository) - \"repository\": always - \"none\": never"),
65
+ /** Continue on error (default: false) */
66
+ continueOnError: z.boolean().default(false).describe("Continue on error (default: false)"),
67
+ /** Give the plan only, and start no visit (default: false) */
68
+ dryRun: z.boolean().default(false).describe("Preview without executing (default: false)"),
69
+ });
70
+
71
+ export type DagTraverseInput = z.infer<typeof DagTraverseInputSchema>;
72
+
73
+ export interface TraverseNodeResult {
74
+ name: string;
75
+ path: string;
76
+ success: boolean;
77
+ duration: number;
78
+ error?: string | undefined;
79
+ output?: unknown | undefined;
80
+ }
81
+
82
+ export interface DagTraverseOutput {
83
+ success: boolean;
84
+ results: TraverseNodeResult[];
85
+ totalDuration: number;
86
+ visited: number;
87
+ failed: number;
88
+ }
89
+
90
+ export const DagTraverseOutputSchema: z.ZodType<DagTraverseOutput> = z.looseObject({
91
+ success: z.boolean().describe("True when every visit succeeded"),
92
+ results: z
93
+ .array(
94
+ z.looseObject({
95
+ name: z.string().describe("The package name of the node"),
96
+ path: z.string().describe("The folder of the node"),
97
+ success: z.boolean().describe("True when the visit succeeded"),
98
+ duration: z.number().describe("The time of the visit, in milliseconds"),
99
+ error: z.string().optional().describe("The error of a failed visit"),
100
+ output: z.unknown().optional().describe("The result of the visit"),
101
+ })
102
+ )
103
+ .describe("The result of each visited node"),
104
+ totalDuration: z.number().describe("The time of the traversal, in milliseconds"),
105
+ visited: z.number().describe("The number of visited nodes"),
106
+ failed: z.number().describe("The number of failed visits"),
107
+ });
108
+
109
+ /**
110
+ * A package of the output of `lib.scan`: the fields that `dag.traverse` reads. The type is local,
111
+ * because `@procedures/lib` depends on this package, and this package must not depend on it.
112
+ */
113
+ export interface ScannedPackage {
114
+ /** The package name */
115
+ name: string;
116
+ /** The folder of the package */
117
+ repoPath: string;
118
+ /** The ecosystem packages that the package depends on */
119
+ mark1russell7Deps: string[];
120
+ }
121
+
122
+ /** The output of `lib.scan`: the fields that `dag.traverse` reads. */
123
+ export interface ScanOutput {
124
+ packages: Record<string, ScannedPackage>;
125
+ }
126
+
127
+ /** A node of the traversal: a workspace package. `id` is the package name. */
128
+ export interface PackageNode extends DAGNode {
129
+ /** The package name. It is the same as `id`. */
130
+ name: string;
131
+ /** The folder of the package */
132
+ repoPath: string;
133
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Importing this file registers the procedures of @procedures/dag in the global registry:
3
+ * `import "@procedures/dag/register"`. The package root and the `./module` entry register nothing
4
+ * (ARCHITECTURE-PROPOSALS-2026-10, P9). `dag.traverse` calls `lib.scan`: also import
5
+ * `@procedures/lib/register`.
6
+ */
7
+
8
+ import { registerDagProcedures } from "./module.js";
9
+
10
+ export * from "./module.js";
11
+
12
+ registerDagProcedures();