sysprom 1.14.0 → 1.16.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 +19 -2
- package/dist/schema.json +18 -1
- package/dist/src/cli/commands/infer.d.ts +2 -0
- package/dist/src/cli/commands/infer.js +235 -0
- package/dist/src/cli/program.js +2 -0
- package/dist/src/endpoint-types.js +23 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +2 -2
- package/dist/src/mcp/server.js +112 -1
- package/dist/src/operations/add-node.d.ts +51 -9
- package/dist/src/operations/add-plan-task.d.ts +34 -6
- package/dist/src/operations/add-relationship.d.ts +48 -8
- package/dist/src/operations/check.d.ts +17 -3
- package/dist/src/operations/graph.d.ts +17 -3
- package/dist/src/operations/index.d.ts +4 -0
- package/dist/src/operations/index.js +5 -0
- package/dist/src/operations/infer-completeness.d.ts +428 -0
- package/dist/src/operations/infer-completeness.js +131 -0
- package/dist/src/operations/infer-derived.d.ts +389 -0
- package/dist/src/operations/infer-derived.js +158 -0
- package/dist/src/operations/infer-impact.d.ts +2299 -0
- package/dist/src/operations/infer-impact.js +262 -0
- package/dist/src/operations/infer-lifecycle.d.ts +435 -0
- package/dist/src/operations/infer-lifecycle.js +119 -0
- package/dist/src/operations/init-document.d.ts +17 -3
- package/dist/src/operations/json-to-markdown.d.ts +17 -3
- package/dist/src/operations/mark-task-done.d.ts +34 -6
- package/dist/src/operations/mark-task-undone.d.ts +34 -6
- package/dist/src/operations/markdown-to-json.d.ts +17 -3
- package/dist/src/operations/next-id.d.ts +17 -3
- package/dist/src/operations/node-history.d.ts +17 -3
- package/dist/src/operations/plan-add-task.d.ts +34 -6
- package/dist/src/operations/plan-gate.d.ts +17 -3
- package/dist/src/operations/plan-init.d.ts +17 -3
- package/dist/src/operations/plan-progress.d.ts +17 -3
- package/dist/src/operations/plan-status.d.ts +17 -3
- package/dist/src/operations/query-node.d.ts +107 -17
- package/dist/src/operations/query-nodes.d.ts +34 -6
- package/dist/src/operations/query-relationships.d.ts +31 -5
- package/dist/src/operations/remove-node.d.ts +51 -9
- package/dist/src/operations/remove-relationship.d.ts +36 -7
- package/dist/src/operations/rename.d.ts +34 -6
- package/dist/src/operations/search.d.ts +34 -6
- package/dist/src/operations/speckit-diff.d.ts +17 -3
- package/dist/src/operations/speckit-export.d.ts +17 -3
- package/dist/src/operations/speckit-import.d.ts +17 -3
- package/dist/src/operations/speckit-sync.d.ts +51 -9
- package/dist/src/operations/state-at.d.ts +17 -3
- package/dist/src/operations/stats.d.ts +17 -3
- package/dist/src/operations/sync.d.ts +51 -9
- package/dist/src/operations/task-list.d.ts +17 -3
- package/dist/src/operations/timeline.d.ts +17 -3
- package/dist/src/operations/trace-from-node.d.ts +51 -9
- package/dist/src/operations/update-metadata.d.ts +34 -6
- package/dist/src/operations/update-node.d.ts +48 -8
- package/dist/src/operations/update-plan-task.d.ts +34 -6
- package/dist/src/operations/validate.d.ts +17 -3
- package/dist/src/schema.d.ts +70 -10
- package/dist/src/schema.js +21 -0
- package/package.json +1 -1
- package/schema.json +18 -1
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { defineOperation } from "./define-operation.js";
|
|
3
|
+
import { Node, SysProMDocument, ImpactPolarity, } from "../schema.js";
|
|
4
|
+
/**
|
|
5
|
+
* Impact node in the impact trace.
|
|
6
|
+
*/
|
|
7
|
+
const ImpactNode = z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
node: Node.optional(),
|
|
10
|
+
impactType: z.enum(["direct", "transitive", "potential"]),
|
|
11
|
+
distance: z.number().int().nonnegative(),
|
|
12
|
+
polarity: ImpactPolarity.optional(),
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* Output schema for inferImpactOp.
|
|
16
|
+
*/
|
|
17
|
+
const ImpactOutput = z.object({
|
|
18
|
+
sourceId: z.string(),
|
|
19
|
+
impactedNodes: z.array(ImpactNode),
|
|
20
|
+
summary: z.object({
|
|
21
|
+
direct: z.number(),
|
|
22
|
+
transitive: z.number(),
|
|
23
|
+
potential: z.number(),
|
|
24
|
+
total: z.number(),
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Relationship types that indicate impact propagation.
|
|
29
|
+
*/
|
|
30
|
+
const IMPACT_RELATIONSHIPS = new Set([
|
|
31
|
+
"affects",
|
|
32
|
+
"depends_on",
|
|
33
|
+
"modifies",
|
|
34
|
+
"constrained_by",
|
|
35
|
+
"requires",
|
|
36
|
+
"produces",
|
|
37
|
+
"consumes",
|
|
38
|
+
"influence",
|
|
39
|
+
]);
|
|
40
|
+
/**
|
|
41
|
+
* Type guard for ImpactPolarity validation.
|
|
42
|
+
* @param val - The value to validate
|
|
43
|
+
* @returns true if the value is a valid ImpactPolarity
|
|
44
|
+
* @example
|
|
45
|
+
* if (isValidPolarity(rel.polarity)) { ... }
|
|
46
|
+
*/
|
|
47
|
+
function isValidPolarity(val) {
|
|
48
|
+
return ImpactPolarity.safeParse(val).success;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Relationship types that indicate potential impact (weaker signal).
|
|
52
|
+
*/
|
|
53
|
+
const POTENTIAL_IMPACT_RELATIONSHIPS = new Set([
|
|
54
|
+
"part_of",
|
|
55
|
+
"governed_by",
|
|
56
|
+
"must_follow",
|
|
57
|
+
]);
|
|
58
|
+
/**
|
|
59
|
+
* Infer impact from a starting node through the graph.
|
|
60
|
+
*
|
|
61
|
+
* Traces impact propagation through affect, dependency, and modification
|
|
62
|
+
* relationships, categorising nodes as directly impacted, transitively
|
|
63
|
+
* impacted, or potentially impacted. Supports bidirectional traversal
|
|
64
|
+
* (CHG40) with optional polarity annotations.
|
|
65
|
+
*/
|
|
66
|
+
export const inferImpactOp = defineOperation({
|
|
67
|
+
name: "infer-impact",
|
|
68
|
+
description: "Infer impact from a node through the graph following impact relationships",
|
|
69
|
+
input: z.object({
|
|
70
|
+
doc: SysProMDocument,
|
|
71
|
+
startId: z.string(),
|
|
72
|
+
direction: z.enum(["outgoing", "incoming", "bidirectional"]).optional(),
|
|
73
|
+
maxDepth: z.number().int().positive().optional(),
|
|
74
|
+
relationshipFilter: z.array(z.string()).optional(),
|
|
75
|
+
}),
|
|
76
|
+
output: ImpactOutput,
|
|
77
|
+
fn: (input) => {
|
|
78
|
+
const nodeMap = new Map(input.doc.nodes.map((n) => [n.id, n]));
|
|
79
|
+
const visited = new Set();
|
|
80
|
+
const impactedNodes = [];
|
|
81
|
+
const relationships = input.doc.relationships ?? [];
|
|
82
|
+
const direction = input.direction ?? "outgoing";
|
|
83
|
+
visited.add(input.startId);
|
|
84
|
+
// Helper: check if relationship type is impactful
|
|
85
|
+
const isImpactRel = (type) => input.relationshipFilter
|
|
86
|
+
? input.relationshipFilter.includes(type)
|
|
87
|
+
: IMPACT_RELATIONSHIPS.has(type);
|
|
88
|
+
const isPotentialRel = (type) => !input.relationshipFilter && POTENTIAL_IMPACT_RELATIONSHIPS.has(type);
|
|
89
|
+
// Helper: get edges based on direction
|
|
90
|
+
const getEdges = (nodeId) => {
|
|
91
|
+
const edges = [];
|
|
92
|
+
if (direction !== "incoming") {
|
|
93
|
+
// Outgoing edges
|
|
94
|
+
relationships
|
|
95
|
+
.filter((r) => r.from === nodeId)
|
|
96
|
+
.forEach((r) => {
|
|
97
|
+
if (isImpactRel(r.type) || isPotentialRel(r.type)) {
|
|
98
|
+
edges.push({ to: r.to, type: r.type, polarity: r.polarity });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (direction !== "outgoing") {
|
|
103
|
+
// Incoming edges
|
|
104
|
+
relationships
|
|
105
|
+
.filter((r) => r.to === nodeId)
|
|
106
|
+
.forEach((r) => {
|
|
107
|
+
if (isImpactRel(r.type) || isPotentialRel(r.type)) {
|
|
108
|
+
edges.push({ to: r.from, type: r.type, polarity: r.polarity });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return edges;
|
|
113
|
+
};
|
|
114
|
+
// BFS traversal
|
|
115
|
+
const queue = [];
|
|
116
|
+
// Start: add direct impacts from startId
|
|
117
|
+
const directEdges = getEdges(input.startId);
|
|
118
|
+
for (const edge of directEdges) {
|
|
119
|
+
const impactType = isPotentialRel(edge.type) ? "potential" : "direct";
|
|
120
|
+
queue.push({
|
|
121
|
+
id: edge.to,
|
|
122
|
+
distance: 1,
|
|
123
|
+
impactType,
|
|
124
|
+
polarity: edge.polarity,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
while (queue.length > 0) {
|
|
128
|
+
const current = queue.shift();
|
|
129
|
+
if (!current)
|
|
130
|
+
break;
|
|
131
|
+
if (visited.has(current.id))
|
|
132
|
+
continue;
|
|
133
|
+
if (input.maxDepth && current.distance > input.maxDepth)
|
|
134
|
+
continue;
|
|
135
|
+
visited.add(current.id);
|
|
136
|
+
const node = nodeMap.get(current.id);
|
|
137
|
+
const impactNode = {
|
|
138
|
+
id: current.id,
|
|
139
|
+
node,
|
|
140
|
+
impactType: current.impactType,
|
|
141
|
+
distance: current.distance,
|
|
142
|
+
};
|
|
143
|
+
// Assign polarity if it's valid
|
|
144
|
+
if (current.polarity && isValidPolarity(current.polarity)) {
|
|
145
|
+
impactNode.polarity = current.polarity;
|
|
146
|
+
}
|
|
147
|
+
impactedNodes.push(impactNode);
|
|
148
|
+
// Find next-hop impacts
|
|
149
|
+
const nextEdges = getEdges(current.id);
|
|
150
|
+
for (const edge of nextEdges) {
|
|
151
|
+
if (!visited.has(edge.to)) {
|
|
152
|
+
const nextImpactType = current.distance >= 1 ? "transitive" : current.impactType;
|
|
153
|
+
queue.push({
|
|
154
|
+
id: edge.to,
|
|
155
|
+
distance: current.distance + 1,
|
|
156
|
+
impactType: nextImpactType,
|
|
157
|
+
polarity: edge.polarity,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Sort by distance, then by impact type priority
|
|
163
|
+
const impactPriority = { direct: 0, transitive: 1, potential: 2 };
|
|
164
|
+
impactedNodes.sort((a, b) => {
|
|
165
|
+
if (a.distance !== b.distance)
|
|
166
|
+
return a.distance - b.distance;
|
|
167
|
+
return impactPriority[a.impactType] - impactPriority[b.impactType];
|
|
168
|
+
});
|
|
169
|
+
const summary = {
|
|
170
|
+
direct: impactedNodes.filter((n) => n.impactType === "direct").length,
|
|
171
|
+
transitive: impactedNodes.filter((n) => n.impactType === "transitive")
|
|
172
|
+
.length,
|
|
173
|
+
potential: impactedNodes.filter((n) => n.impactType === "potential")
|
|
174
|
+
.length,
|
|
175
|
+
total: impactedNodes.length,
|
|
176
|
+
};
|
|
177
|
+
return {
|
|
178
|
+
sourceId: input.startId,
|
|
179
|
+
impactedNodes,
|
|
180
|
+
summary,
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
/**
|
|
185
|
+
* Output schema for impactSummaryOp.
|
|
186
|
+
*/
|
|
187
|
+
const ImpactSummaryOutput = z.object({
|
|
188
|
+
hotspots: z.array(z.object({
|
|
189
|
+
nodeId: z.string(),
|
|
190
|
+
node: Node.optional(),
|
|
191
|
+
incomingImpactCount: z.number(),
|
|
192
|
+
outgoingImpactCount: z.number(),
|
|
193
|
+
totalImpact: z.number(),
|
|
194
|
+
})),
|
|
195
|
+
summary: z.object({
|
|
196
|
+
totalNodes: z.number(),
|
|
197
|
+
totalImpactedNodes: z.number(),
|
|
198
|
+
averageDegree: z.number(),
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
/**
|
|
202
|
+
* Analyse the entire document for impact hotspots.
|
|
203
|
+
*
|
|
204
|
+
* Identifies nodes that are heavily impacted or that impact many other nodes.
|
|
205
|
+
* Useful for identifying critical elements and dependencies.
|
|
206
|
+
*/
|
|
207
|
+
export const impactSummaryOp = defineOperation({
|
|
208
|
+
name: "infer-impact-summary",
|
|
209
|
+
description: "Analyse the entire document for impact hotspots — nodes with high incoming or outgoing impact",
|
|
210
|
+
input: z.object({
|
|
211
|
+
doc: SysProMDocument,
|
|
212
|
+
}),
|
|
213
|
+
output: ImpactSummaryOutput,
|
|
214
|
+
fn: (input) => {
|
|
215
|
+
const nodeMap = new Map(input.doc.nodes.map((n) => [n.id, n]));
|
|
216
|
+
const relationships = input.doc.relationships ?? [];
|
|
217
|
+
const impactStats = new Map();
|
|
218
|
+
// Count incoming and outgoing impact relationships
|
|
219
|
+
for (const node of input.doc.nodes) {
|
|
220
|
+
impactStats.set(node.id, { incoming: 0, outgoing: 0 });
|
|
221
|
+
}
|
|
222
|
+
for (const rel of relationships) {
|
|
223
|
+
if (IMPACT_RELATIONSHIPS.has(rel.type)) {
|
|
224
|
+
const fromStats = impactStats.get(rel.from);
|
|
225
|
+
const toStats = impactStats.get(rel.to);
|
|
226
|
+
if (fromStats)
|
|
227
|
+
fromStats.outgoing += 1;
|
|
228
|
+
if (toStats)
|
|
229
|
+
toStats.incoming += 1;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Build hotspots list
|
|
233
|
+
const hotspots = [];
|
|
234
|
+
for (const [nodeId, stats] of impactStats.entries()) {
|
|
235
|
+
const totalImpact = stats.incoming + stats.outgoing;
|
|
236
|
+
if (totalImpact > 0) {
|
|
237
|
+
hotspots.push({
|
|
238
|
+
nodeId,
|
|
239
|
+
node: nodeMap.get(nodeId),
|
|
240
|
+
incomingImpactCount: stats.incoming,
|
|
241
|
+
outgoingImpactCount: stats.outgoing,
|
|
242
|
+
totalImpact,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Sort by total impact (descending)
|
|
247
|
+
hotspots.sort((a, b) => b.totalImpact - a.totalImpact);
|
|
248
|
+
const totalImpactedNodes = hotspots.length;
|
|
249
|
+
const averageDegree = totalImpactedNodes > 0
|
|
250
|
+
? hotspots.reduce((sum, h) => sum + h.totalImpact, 0) /
|
|
251
|
+
totalImpactedNodes
|
|
252
|
+
: 0;
|
|
253
|
+
return {
|
|
254
|
+
hotspots,
|
|
255
|
+
summary: {
|
|
256
|
+
totalNodes: input.doc.nodes.length,
|
|
257
|
+
totalImpactedNodes,
|
|
258
|
+
averageDegree,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
});
|