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.
Files changed (61) hide show
  1. package/README.md +19 -2
  2. package/dist/schema.json +18 -1
  3. package/dist/src/cli/commands/infer.d.ts +2 -0
  4. package/dist/src/cli/commands/infer.js +235 -0
  5. package/dist/src/cli/program.js +2 -0
  6. package/dist/src/endpoint-types.js +23 -0
  7. package/dist/src/index.d.ts +2 -2
  8. package/dist/src/index.js +2 -2
  9. package/dist/src/mcp/server.js +112 -1
  10. package/dist/src/operations/add-node.d.ts +51 -9
  11. package/dist/src/operations/add-plan-task.d.ts +34 -6
  12. package/dist/src/operations/add-relationship.d.ts +48 -8
  13. package/dist/src/operations/check.d.ts +17 -3
  14. package/dist/src/operations/graph.d.ts +17 -3
  15. package/dist/src/operations/index.d.ts +4 -0
  16. package/dist/src/operations/index.js +5 -0
  17. package/dist/src/operations/infer-completeness.d.ts +428 -0
  18. package/dist/src/operations/infer-completeness.js +131 -0
  19. package/dist/src/operations/infer-derived.d.ts +389 -0
  20. package/dist/src/operations/infer-derived.js +158 -0
  21. package/dist/src/operations/infer-impact.d.ts +2299 -0
  22. package/dist/src/operations/infer-impact.js +262 -0
  23. package/dist/src/operations/infer-lifecycle.d.ts +435 -0
  24. package/dist/src/operations/infer-lifecycle.js +119 -0
  25. package/dist/src/operations/init-document.d.ts +17 -3
  26. package/dist/src/operations/json-to-markdown.d.ts +17 -3
  27. package/dist/src/operations/mark-task-done.d.ts +34 -6
  28. package/dist/src/operations/mark-task-undone.d.ts +34 -6
  29. package/dist/src/operations/markdown-to-json.d.ts +17 -3
  30. package/dist/src/operations/next-id.d.ts +17 -3
  31. package/dist/src/operations/node-history.d.ts +17 -3
  32. package/dist/src/operations/plan-add-task.d.ts +34 -6
  33. package/dist/src/operations/plan-gate.d.ts +17 -3
  34. package/dist/src/operations/plan-init.d.ts +17 -3
  35. package/dist/src/operations/plan-progress.d.ts +17 -3
  36. package/dist/src/operations/plan-status.d.ts +17 -3
  37. package/dist/src/operations/query-node.d.ts +107 -17
  38. package/dist/src/operations/query-nodes.d.ts +34 -6
  39. package/dist/src/operations/query-relationships.d.ts +31 -5
  40. package/dist/src/operations/remove-node.d.ts +51 -9
  41. package/dist/src/operations/remove-relationship.d.ts +36 -7
  42. package/dist/src/operations/rename.d.ts +34 -6
  43. package/dist/src/operations/search.d.ts +34 -6
  44. package/dist/src/operations/speckit-diff.d.ts +17 -3
  45. package/dist/src/operations/speckit-export.d.ts +17 -3
  46. package/dist/src/operations/speckit-import.d.ts +17 -3
  47. package/dist/src/operations/speckit-sync.d.ts +51 -9
  48. package/dist/src/operations/state-at.d.ts +17 -3
  49. package/dist/src/operations/stats.d.ts +17 -3
  50. package/dist/src/operations/sync.d.ts +51 -9
  51. package/dist/src/operations/task-list.d.ts +17 -3
  52. package/dist/src/operations/timeline.d.ts +17 -3
  53. package/dist/src/operations/trace-from-node.d.ts +51 -9
  54. package/dist/src/operations/update-metadata.d.ts +34 -6
  55. package/dist/src/operations/update-node.d.ts +48 -8
  56. package/dist/src/operations/update-plan-task.d.ts +34 -6
  57. package/dist/src/operations/validate.d.ts +17 -3
  58. package/dist/src/schema.d.ts +70 -10
  59. package/dist/src/schema.js +21 -0
  60. package/package.json +1 -1
  61. 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
+ });