port-control-graph-logic 0.0.0-stage → 0.1.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.
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
- # Temporary Holding Version
1
+ # port-control-graph-logic
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 纯泛型控制图结构化 processor;契约由 port-control-graph-contract 拥有,SingleEntryGraph 通过 depa-processor/algo 正式消费。没有 XNL、UI、文件系统或 effect 依赖。
4
+
5
+ `lowerPortControlGraph(runtime, input, config)` 按 entry/enter/next/ordered branches/body/exit 构建 statements 与共享 block 森林。输入保持不变;失败返回带 node/edge identity 的 diagnostics。输出原节点数据仍由 carrier owner 管理。
6
+
7
+ 语义、预算与公开消费示例见仓库 docs/flow-dsl/spec/port-control-graph/index.md。独立 tarball 闭包由 tools/verify-port-control-package.ts 验证。
package/package.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "name": "port-control-graph-logic",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts"
7
+ },
8
+ "files": [
9
+ "src"
10
+ ],
11
+ "dependencies": {
12
+ "port-control-graph-contract": "0.1.0"
13
+ }
14
+ }
@@ -0,0 +1,29 @@
1
+ /** 节点邻接仅用于结构分析;命名端口和业务边仍由调用方持有。 */
2
+ export function createAdjacencyProjection<I>() {
3
+ const adjacency = new Map<I, Set<I>>();
4
+ const addVertex = (id: I) => {
5
+ if (!adjacency.has(id)) adjacency.set(id, new Set());
6
+ };
7
+ const addEdge = (from: I, to: I) => {
8
+ if (adjacency.has(to)) adjacency.get(from)?.add(to);
9
+ };
10
+ const nextIds = (id: I): ReadonlySet<I> => adjacency.get(id) ?? new Set<I>();
11
+ // 包含起点。每个节点只入队一次,环和共享尾部不会造成无界遍历。
12
+ const reachableFrom = (start: I): Set<I> => {
13
+ const reached = new Set<I>([start]);
14
+ const queue = [start];
15
+ for (let cursor = 0; cursor < queue.length; cursor++) {
16
+ for (const next of nextIds(queue[cursor])) {
17
+ if (reached.has(next)) continue;
18
+ reached.add(next);
19
+ queue.push(next);
20
+ }
21
+ }
22
+ return reached;
23
+ };
24
+ const unreachableFrom = (start: I): I[] => {
25
+ const reached = reachableFrom(start);
26
+ return [...adjacency.keys()].filter((id) => !reached.has(id));
27
+ };
28
+ return { addVertex, addEdge, nextIds, reachableFrom, unreachableFrom };
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,317 @@
1
+ import { createAdjacencyProjection } from "./adjacency";
2
+ import type {
3
+ ControlGraphRuntime,
4
+ ControlShape,
5
+ GraphDiagnostic,
6
+ LowerGraphResult,
7
+ PortControlGraph,
8
+ StructuredStatement,
9
+ } from "port-control-graph-contract";
10
+ export type * from "port-control-graph-contract";
11
+
12
+ /** Pure, immutable port-preserving structuring. Node adjacency is only an analysis projection. */
13
+ export function lowerPortControlGraph<N, I, P, E>(
14
+ runtime: ControlGraphRuntime<N, I, P>,
15
+ graph: PortControlGraph<N, I, P, E>,
16
+ config: { maxNodes?: number; maxEdges?: number; maxUnits?: number } = {},
17
+ ): LowerGraphResult<N, I, P, E> {
18
+ const diagnostics: GraphDiagnostic<I, E>[] = [];
19
+ const fail = (code: string, message: string, node?: I, edge?: E) =>
20
+ diagnostics.push({ code, message, node, edge });
21
+ if (
22
+ graph.nodes.length > (config.maxNodes ?? 1000) ||
23
+ graph.edges.length > (config.maxEdges ?? 5000)
24
+ ) {
25
+ return {
26
+ ok: false,
27
+ diagnostics: [
28
+ { code: "graph-budget", message: "graph exceeds structural budget" },
29
+ ],
30
+ };
31
+ }
32
+ const nodes = new Map<I, N>();
33
+ const shapes = new Map<I, ControlShape<P>>();
34
+ const outgoing = new Map<I, Map<P, I>>();
35
+ const projection = createAdjacencyProjection<I>();
36
+ for (const n of graph.nodes) {
37
+ const id = runtime.nodeId(n);
38
+ if (nodes.has(id))
39
+ fail("duplicate-node", "node identity must be unique", id);
40
+ nodes.set(id, n);
41
+ shapes.set(id, runtime.shape(n));
42
+ projection.addVertex(id);
43
+ }
44
+ if (!nodes.has(graph.entry))
45
+ fail("missing-entry", "entry must name a node", graph.entry);
46
+ const edgeIds = new Set<E>();
47
+ for (const edge of graph.edges) {
48
+ if (edgeIds.has(edge.id))
49
+ fail(
50
+ "duplicate-edge",
51
+ "edge identity must be unique",
52
+ undefined,
53
+ edge.id,
54
+ );
55
+ edgeIds.add(edge.id);
56
+ const from = shapes.get(edge.from.node),
57
+ to = shapes.get(edge.to.node);
58
+ if (!from || !to) {
59
+ fail("dangling-edge", "edge endpoint does not exist", undefined, edge.id);
60
+ continue;
61
+ }
62
+ const valid =
63
+ from.role !== "exit" &&
64
+ (edge.from.port === from.next ||
65
+ edge.from.port === from.body ||
66
+ from.branches?.some((b) => b.port === edge.from.port));
67
+ if (!valid || edge.to.port !== to.enter)
68
+ fail(
69
+ "invalid-port",
70
+ "edge direction or port is invalid",
71
+ edge.from.node,
72
+ edge.id,
73
+ );
74
+ const ports = outgoing.get(edge.from.node) ?? new Map<P, I>();
75
+ if (ports.has(edge.from.port))
76
+ fail(
77
+ "multiple-port-edges",
78
+ "one control output cannot fan out implicitly",
79
+ edge.from.node,
80
+ edge.id,
81
+ );
82
+ ports.set(edge.from.port, edge.to.node);
83
+ outgoing.set(edge.from.node, ports);
84
+ projection.addEdge(edge.from.node, edge.to.node);
85
+ }
86
+ for (const [id, shape] of shapes) {
87
+ const branches = shape.branches ?? [];
88
+ if (shape.role !== "choice" && branches.length)
89
+ fail(
90
+ "unexpected-branch-ports",
91
+ "only choice nodes can declare branches",
92
+ id,
93
+ );
94
+ if (shape.role !== "loop" && shape.body !== undefined)
95
+ fail("unexpected-body-port", "only loop nodes can declare a body", id);
96
+ if (shape.next !== undefined && shape.next === shape.enter)
97
+ fail("ambiguous-port-schema", "enter and next must be distinct", id);
98
+ if (
99
+ shape.body !== undefined &&
100
+ (shape.body === shape.enter || shape.body === shape.next)
101
+ )
102
+ fail("ambiguous-port-schema", "body must differ from enter and next", id);
103
+ if (shape.role === "choice") {
104
+ const orders = new Set(branches.map((b) => b.order));
105
+ const ports = new Set(branches.map((b) => b.port));
106
+ if (branches.some((b) => b.port === shape.next || b.port === shape.enter))
107
+ fail(
108
+ "reserved-branch-port",
109
+ "branch port cannot shadow enter/next",
110
+ id,
111
+ );
112
+ if (
113
+ !branches.length ||
114
+ orders.size !== branches.length ||
115
+ ports.size !== branches.length ||
116
+ branches.some((b) => !Number.isInteger(b.order) || b.order < 0)
117
+ )
118
+ fail(
119
+ "invalid-branch-order",
120
+ "branches require unique ports and nonnegative integer orders",
121
+ id,
122
+ );
123
+ const sorted = [...branches].sort((a, b) => a.order - b.order);
124
+ if (
125
+ branches.filter((b) => b.default).length > 1 ||
126
+ sorted.some((b, i) => b.default && i !== sorted.length - 1)
127
+ )
128
+ fail("invalid-default", "default must be unique and last", id);
129
+ }
130
+ if (shape.role === "loop" && shape.body === undefined)
131
+ fail("missing-loop-body-port", "loop requires body port schema", id);
132
+ if (shape.role === "exit" && outgoing.has(id))
133
+ fail("exit-has-next", "flow exit cannot have outgoing edges", id);
134
+ }
135
+ if (diagnostics.length) return { ok: false, diagnostics };
136
+ for (const id of projection.unreachableFrom(graph.entry))
137
+ fail("unreachable-node", "node is not reachable from entry", id);
138
+ const color = new Map<I, number>();
139
+ const visit = (id: I) => {
140
+ if (color.get(id) === 1) {
141
+ fail(
142
+ "unstructured-cycle",
143
+ "use structured loop body; arbitrary back-edge is not supported",
144
+ id,
145
+ );
146
+ return;
147
+ }
148
+ if (color.get(id) === 2) return;
149
+ color.set(id, 1);
150
+ for (const next of projection.nextIds(id)) visit(next);
151
+ color.set(id, 2);
152
+ };
153
+ visit(graph.entry);
154
+ if (diagnostics.length) return { ok: false, diagnostics };
155
+ const nextOf = (id: I, port: P | undefined) =>
156
+ port === undefined ? undefined : outgoing.get(id)?.get(port);
157
+ // Postdom sets include a virtual natural/flow exit; layout or insertion order never chooses a join.
158
+ const exit = Symbol("exit");
159
+ const post = new Map<I, Set<I | typeof exit>>();
160
+ const postOf = (id: I | undefined): Set<I | typeof exit> => {
161
+ if (id === undefined) return new Set([exit]);
162
+ const cached = post.get(id);
163
+ if (cached) return cached;
164
+ const shape = shapes.get(id)!;
165
+ const next = nextOf(id, shape.next);
166
+ const successors =
167
+ shape.role === "exit"
168
+ ? [undefined]
169
+ : shape.role === "choice"
170
+ ? [
171
+ ...(shape.branches ?? []).map((b) => nextOf(id, b.port) ?? next),
172
+ ...((shape.branches ?? []).some((b) => b.default) ? [] : [next]),
173
+ ]
174
+ : [next]; // Loop body returns to owner; loop.next is its normal continuation.
175
+ let common = postOf(successors[0]);
176
+ for (const successor of successors.slice(1)) {
177
+ const s = postOf(successor);
178
+ common = new Set([...common].filter((x) => s.has(x)));
179
+ }
180
+ const result = new Set<I | typeof exit>([id, ...common]);
181
+ post.set(id, result);
182
+ return result;
183
+ };
184
+ const inferredNext: { owner: I; next: I }[] = [];
185
+ const joinOf = (id: I): I | undefined => {
186
+ const shape = shapes.get(id)!;
187
+ const explicit = nextOf(id, shape.next);
188
+ if (explicit !== undefined) {
189
+ const branches = shape.branches ?? [];
190
+ if (
191
+ branches.every((b) => nextOf(id, b.port) !== undefined) &&
192
+ branches.some((b) => b.default)
193
+ ) {
194
+ const paths = branches.map((b) => postOf(nextOf(id, b.port)));
195
+ const common = [...paths[0]].filter(
196
+ (x): x is I => x !== exit && paths.every((p) => p.has(x)),
197
+ );
198
+ if (
199
+ common.length &&
200
+ !common.some(
201
+ (c) =>
202
+ c === explicit ||
203
+ projection.reachableFrom(c).has(explicit),
204
+ )
205
+ )
206
+ fail(
207
+ "next-conflict",
208
+ "explicit next conflicts with common branch continuation",
209
+ id,
210
+ );
211
+ }
212
+ return explicit;
213
+ }
214
+ const candidates = [...postOf(id)].filter(
215
+ (x): x is I => x !== exit && x !== id,
216
+ );
217
+ const nearest = candidates.find((c) =>
218
+ candidates.every((other) => other === c || postOf(c).has(other)),
219
+ );
220
+ if (nearest !== undefined) inferredNext.push({ owner: id, next: nearest });
221
+ return nearest;
222
+ };
223
+ type Unit = {
224
+ id: number;
225
+ source: I;
226
+ stop?: I;
227
+ node: N;
228
+ role: ControlShape<P>["role"];
229
+ branches: { port: P; unit?: Unit }[];
230
+ body?: Unit;
231
+ next?: Unit;
232
+ uses: number;
233
+ };
234
+ const units: Unit[] = [];
235
+ const memo = new Map<I, Map<I | undefined, Unit>>();
236
+ const build = (id: I | undefined, stop?: I): Unit | undefined => {
237
+ if (id === undefined || id === stop) return;
238
+ const existing = memo.get(id)?.get(stop);
239
+ if (existing) {
240
+ existing.uses++;
241
+ return existing;
242
+ }
243
+ if (units.length >= (config.maxUnits ?? 10000))
244
+ throw new Error("structuring budget");
245
+ const shape = shapes.get(id)!;
246
+ const unit: Unit = {
247
+ id: units.length,
248
+ source: id,
249
+ stop,
250
+ node: nodes.get(id)!,
251
+ role: shape.role,
252
+ branches: [],
253
+ uses: 1,
254
+ };
255
+ units.push(unit);
256
+ const contexts = memo.get(id) ?? new Map();
257
+ contexts.set(stop, unit);
258
+ memo.set(id, contexts);
259
+ const next = shape.role === "choice" ? joinOf(id) : nextOf(id, shape.next);
260
+ if (shape.role === "choice")
261
+ unit.branches = [...(shape.branches ?? [])]
262
+ .sort((a, b) => a.order - b.order)
263
+ .map((b) => ({
264
+ port: b.port,
265
+ unit: build(nextOf(id, b.port), next ?? stop),
266
+ }));
267
+ if (shape.role === "loop") unit.body = build(nextOf(id, shape.body), stop);
268
+ if (shape.role !== "exit") unit.next = build(next, stop);
269
+ return unit;
270
+ };
271
+ let root: Unit | undefined;
272
+ try {
273
+ root = build(graph.entry);
274
+ } catch {
275
+ return {
276
+ ok: false,
277
+ diagnostics: [
278
+ { code: "structuring-budget", message: "too many structural contexts" },
279
+ ],
280
+ };
281
+ }
282
+ if (diagnostics.length) return { ok: false, diagnostics };
283
+ type S = StructuredStatement<N, I, P>;
284
+ const materialize = (unit?: Unit, definition = false): S[] => {
285
+ if (!unit) return [];
286
+ if (unit.uses > 1 && !definition)
287
+ return [{ kind: "block-ref", block: unit.id, source: unit.source }];
288
+ const stmt: S =
289
+ unit.role === "choice"
290
+ ? {
291
+ kind: "choice",
292
+ node: unit.node,
293
+ branches: unit.branches.map((b) => ({
294
+ port: b.port,
295
+ body: materialize(b.unit),
296
+ })),
297
+ }
298
+ : unit.role === "loop"
299
+ ? { kind: "loop", node: unit.node, body: materialize(unit.body) }
300
+ : { kind: "node", node: unit.node };
301
+ return [stmt, ...materialize(unit.next)];
302
+ };
303
+ return {
304
+ ok: true,
305
+ forest: {
306
+ statements: materialize(root),
307
+ blocks: units
308
+ .filter((u) => u.uses > 1)
309
+ .map((u) => ({
310
+ id: u.id,
311
+ source: u.source,
312
+ statements: materialize(u, true),
313
+ })),
314
+ inferredNext,
315
+ },
316
+ };
317
+ }