loopgraph 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/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/adapters/agenthub/extract.d.ts +63 -0
- package/dist/adapters/agenthub/extract.js +144 -0
- package/dist/adapters/agenthub/facts.d.ts +27 -0
- package/dist/adapters/agenthub/facts.js +89 -0
- package/dist/adapters/agenthub/index.d.ts +13 -0
- package/dist/adapters/agenthub/index.js +18 -0
- package/dist/adapters/registry.d.ts +4 -0
- package/dist/adapters/registry.js +17 -0
- package/dist/adapters/types.d.ts +40 -0
- package/dist/adapters/types.js +2 -0
- package/dist/cache/content-cache.d.ts +46 -0
- package/dist/cache/content-cache.js +123 -0
- package/dist/cli/args.d.ts +7 -0
- package/dist/cli/args.js +26 -0
- package/dist/cli/assets/agent-kit.d.ts +15 -0
- package/dist/cli/assets/agent-kit.js +167 -0
- package/dist/cli/commands/check.d.ts +38 -0
- package/dist/cli/commands/check.js +70 -0
- package/dist/cli/commands/import.d.ts +6 -0
- package/dist/cli/commands/import.js +34 -0
- package/dist/cli/commands/init.d.ts +13 -0
- package/dist/cli/commands/init.js +70 -0
- package/dist/cli/commands/inspect.d.ts +23 -0
- package/dist/cli/commands/inspect.js +35 -0
- package/dist/cli/commands/snapshot.d.ts +121 -0
- package/dist/cli/commands/snapshot.js +0 -0
- package/dist/cli/commands/view.d.ts +34 -0
- package/dist/cli/commands/view.js +45 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +19 -0
- package/dist/cli/run.d.ts +11 -0
- package/dist/cli/run.js +329 -0
- package/dist/loader/find-yaml-files.d.ts +26 -0
- package/dist/loader/find-yaml-files.js +51 -0
- package/dist/loader/index.d.ts +4 -0
- package/dist/loader/index.js +4 -0
- package/dist/loader/load-model.d.ts +32 -0
- package/dist/loader/load-model.js +54 -0
- package/dist/loader/model-graph.d.ts +43 -0
- package/dist/loader/model-graph.js +78 -0
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.js +53 -0
- package/dist/query/diff.d.ts +47 -0
- package/dist/query/diff.js +130 -0
- package/dist/query/effective-constraints.d.ts +48 -0
- package/dist/query/effective-constraints.js +81 -0
- package/dist/query/index.d.ts +7 -0
- package/dist/query/index.js +7 -0
- package/dist/query/queries.d.ts +111 -0
- package/dist/query/queries.js +172 -0
- package/dist/query/run-query.d.ts +16 -0
- package/dist/query/run-query.js +181 -0
- package/dist/query/side-channel.d.ts +25 -0
- package/dist/query/side-channel.js +46 -0
- package/dist/query/slice.d.ts +57 -0
- package/dist/query/slice.js +124 -0
- package/dist/query/summary.d.ts +9 -0
- package/dist/query/summary.js +180 -0
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.js +3 -0
- package/dist/schema/model.d.ts +661 -0
- package/dist/schema/model.js +183 -0
- package/dist/schema/status.d.ts +18 -0
- package/dist/schema/status.js +28 -0
- package/dist/staleness.d.ts +57 -0
- package/dist/staleness.js +148 -0
- package/dist/validate/anchor.d.ts +4 -0
- package/dist/validate/anchor.js +32 -0
- package/dist/validate/baseline.d.ts +11 -0
- package/dist/validate/baseline.js +18 -0
- package/dist/validate/checks.d.ts +59 -0
- package/dist/validate/checks.js +303 -0
- package/dist/validate/index.d.ts +6 -0
- package/dist/validate/index.js +6 -0
- package/dist/validate/inv1/check.d.ts +36 -0
- package/dist/validate/inv1/check.js +96 -0
- package/dist/validate/inv1/config.d.ts +75 -0
- package/dist/validate/inv1/config.js +63 -0
- package/dist/validate/inv1/scan.d.ts +49 -0
- package/dist/validate/inv1/scan.js +172 -0
- package/dist/validate/t0.d.ts +27 -0
- package/dist/validate/t0.js +31 -0
- package/dist/validate/types.d.ts +16 -0
- package/dist/validate/types.js +4 -0
- package/dist/validate/unregistered.d.ts +70 -0
- package/dist/validate/unregistered.js +111 -0
- package/dist/views/flow-mermaid.d.ts +30 -0
- package/dist/views/flow-mermaid.js +89 -0
- package/dist/views/index.d.ts +3 -0
- package/dist/views/index.js +3 -0
- package/dist/views/validate-mermaid.d.ts +39 -0
- package/dist/views/validate-mermaid.js +110 -0
- package/package.json +46 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { getNode } from "../loader/model-graph.js";
|
|
2
|
+
import { resolveApplicableScenarios, scenarioApplies } from "./effective-constraints.js";
|
|
3
|
+
/** Every related node id (forward + relevant reverse edges), deduped, kind-tagged. */
|
|
4
|
+
function neighbors(graph, node) {
|
|
5
|
+
const out = [];
|
|
6
|
+
const add = (id, kind) => out.push({ id, kind });
|
|
7
|
+
switch (node.kind) {
|
|
8
|
+
case "feature":
|
|
9
|
+
for (const f of node.contains)
|
|
10
|
+
add(f, "flow");
|
|
11
|
+
break;
|
|
12
|
+
case "flow":
|
|
13
|
+
for (const l of node.traverses)
|
|
14
|
+
add(l, "loop");
|
|
15
|
+
for (const l of node.guarded_by)
|
|
16
|
+
add(l, "loop");
|
|
17
|
+
for (const j of node.crosses)
|
|
18
|
+
add(j, "junction");
|
|
19
|
+
for (const f of node.references)
|
|
20
|
+
add(f, "flow");
|
|
21
|
+
break;
|
|
22
|
+
case "loop": {
|
|
23
|
+
if (node.parent)
|
|
24
|
+
add(node.parent, "loop");
|
|
25
|
+
for (const s of node.scenarios)
|
|
26
|
+
add(s, "scenario");
|
|
27
|
+
// reverse: junctions this loop sits between, flows that traverse/guard it
|
|
28
|
+
for (const j of graph.byKind.junction.values())
|
|
29
|
+
if (j.between.includes(node.id))
|
|
30
|
+
add(j.id, "junction");
|
|
31
|
+
for (const f of graph.byKind.flow.values())
|
|
32
|
+
if (f.traverses.includes(node.id) || f.guarded_by.includes(node.id))
|
|
33
|
+
add(f.id, "flow");
|
|
34
|
+
// effective constraints: invariant scenarios that apply via applies_to
|
|
35
|
+
for (const s of resolveApplicableScenarios(graph, node.id))
|
|
36
|
+
add(s.id, "scenario");
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case "junction":
|
|
40
|
+
for (const l of node.between)
|
|
41
|
+
add(l, "loop");
|
|
42
|
+
for (const s of node.scenarios)
|
|
43
|
+
add(s, "scenario");
|
|
44
|
+
break;
|
|
45
|
+
case "scenario": {
|
|
46
|
+
for (const targetId of node.applies_to?.nodes ?? []) {
|
|
47
|
+
const target = getNode(graph, targetId);
|
|
48
|
+
if (target)
|
|
49
|
+
add(targetId, target.kind);
|
|
50
|
+
}
|
|
51
|
+
// reverse: nodes that list this scenario, plus loops it applies to via owner_match
|
|
52
|
+
for (const l of graph.byKind.loop.values()) {
|
|
53
|
+
if (l.scenarios.includes(node.id))
|
|
54
|
+
add(l.id, "loop");
|
|
55
|
+
else if (scenarioApplies(node, l.id, graph))
|
|
56
|
+
add(l.id, "loop");
|
|
57
|
+
}
|
|
58
|
+
for (const j of graph.byKind.junction.values())
|
|
59
|
+
if (j.scenarios.includes(node.id))
|
|
60
|
+
add(j.id, "junction");
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case "debt":
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
return out.filter((n) => (seen.has(n.id) ? false : seen.add(n.id)));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Depth-limited breadth-first slice from `rootId`. Returns undefined if the
|
|
71
|
+
* root id isn't a node in the graph (the CLI turns that into an "unknown id"
|
|
72
|
+
* error).
|
|
73
|
+
*
|
|
74
|
+
* Standard mark-at-enqueue BFS: a node is added to `enqueued` the moment it's
|
|
75
|
+
* first discovered, so (a) each node is enqueued exactly once and recorded at
|
|
76
|
+
* its shortest distance from the root, and (b) any node reachable within
|
|
77
|
+
* `maxDepth` is marked (via its shallower parent, which the depth-ordered FIFO
|
|
78
|
+
* dequeues first) before a depth==maxDepth sibling could add it to the
|
|
79
|
+
* frontier — so `frontierPointers` and the in-slice node set never overlap by
|
|
80
|
+
* construction, no post-hoc reconciliation needed.
|
|
81
|
+
*/
|
|
82
|
+
export function sliceModel(graph, rootId, maxDepth) {
|
|
83
|
+
const root = getNode(graph, rootId);
|
|
84
|
+
if (!root)
|
|
85
|
+
return undefined;
|
|
86
|
+
const enqueued = new Set([rootId]);
|
|
87
|
+
const nodesById = new Map();
|
|
88
|
+
const frontier = new Map();
|
|
89
|
+
const queue = [{ id: rootId, depth: 0 }];
|
|
90
|
+
while (queue.length > 0) {
|
|
91
|
+
const item = queue.shift();
|
|
92
|
+
if (!item)
|
|
93
|
+
break; // queue.length>0 guarantees this; guard, not assertion
|
|
94
|
+
const { id, depth } = item;
|
|
95
|
+
const node = getNode(graph, id);
|
|
96
|
+
if (!node)
|
|
97
|
+
continue; // dangling reference — T0's referential-integrity check owns reporting it
|
|
98
|
+
const sliceNode = { id, kind: node.kind, depth, node };
|
|
99
|
+
if (node.kind === "loop") {
|
|
100
|
+
sliceNode.effectiveConstraints = resolveApplicableScenarios(graph, id).map((s) => s.id);
|
|
101
|
+
}
|
|
102
|
+
nodesById.set(id, sliceNode);
|
|
103
|
+
for (const nb of neighbors(graph, node)) {
|
|
104
|
+
if (enqueued.has(nb.id))
|
|
105
|
+
continue; // already in-slice (or queued to be) — never a frontier pointer
|
|
106
|
+
if (depth < maxDepth) {
|
|
107
|
+
enqueued.add(nb.id);
|
|
108
|
+
queue.push({ id: nb.id, depth: depth + 1 });
|
|
109
|
+
}
|
|
110
|
+
else if (!frontier.has(nb.id)) {
|
|
111
|
+
frontier.set(nb.id, { id: nb.id, kind: nb.kind, fromId: id });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const nodes = [...nodesById.values()].sort((a, b) => a.depth - b.depth || a.id.localeCompare(b.id));
|
|
116
|
+
return {
|
|
117
|
+
rootId,
|
|
118
|
+
rootKind: root.kind,
|
|
119
|
+
maxDepth,
|
|
120
|
+
nodes,
|
|
121
|
+
frontierPointers: [...frontier.values()].sort((a, b) => a.id.localeCompare(b.id)),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=slice.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ModelSlice } from "./slice.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compact stdout summary. `outputPath` is named so the truncation hint can
|
|
4
|
+
* point the reader at the full side-channel file.
|
|
5
|
+
*/
|
|
6
|
+
export declare function summarizeSlice(slice: ModelSlice, outputPath: string): string;
|
|
7
|
+
/** Full side-channel body: banner + every node's complete detail, nothing truncated. */
|
|
8
|
+
export declare function renderSliceMarkdown(slice: ModelSlice, banner: string): string;
|
|
9
|
+
//# sourceMappingURL=summary.d.ts.map
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summary + full-render for a ModelSlice (Proposal 006 A5), factored out
|
|
3
|
+
* alongside sliceModel so the Phase 2 query family shares one truncation
|
|
4
|
+
* policy. Two outputs:
|
|
5
|
+
* - `summarizeSlice`: compact stdout digest. Keeps EVERY node's skeleton
|
|
6
|
+
* (id/title/risk_class/status + child counts); truncates only the
|
|
7
|
+
* evidence/scenario prose, with an explicit "full detail in <file>" hint.
|
|
8
|
+
* - `renderSliceMarkdown`: the full side-channel body (nothing truncated).
|
|
9
|
+
*/
|
|
10
|
+
const KIND_ORDER = ["feature", "flow", "loop", "junction", "scenario", "debt"];
|
|
11
|
+
/** A short, single-line human label for a node — its "skeleton" identity. */
|
|
12
|
+
function label(node) {
|
|
13
|
+
switch (node.kind) {
|
|
14
|
+
case "feature":
|
|
15
|
+
case "flow":
|
|
16
|
+
case "loop":
|
|
17
|
+
return node.title;
|
|
18
|
+
case "junction":
|
|
19
|
+
return node.title ?? "(untitled junction)";
|
|
20
|
+
case "scenario":
|
|
21
|
+
return truncate(node.given, 60);
|
|
22
|
+
case "debt":
|
|
23
|
+
return node.subject;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Bracketed status/classification tag for the skeleton line. */
|
|
27
|
+
function tag(node) {
|
|
28
|
+
switch (node.kind) {
|
|
29
|
+
case "feature":
|
|
30
|
+
case "flow":
|
|
31
|
+
case "loop":
|
|
32
|
+
return node.status;
|
|
33
|
+
case "junction":
|
|
34
|
+
return `${node.risk_class}/${node.status}`;
|
|
35
|
+
case "scenario":
|
|
36
|
+
return node.level;
|
|
37
|
+
case "debt":
|
|
38
|
+
return node.category;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Child-count suffix shown on the skeleton line (the counts, never the prose). */
|
|
42
|
+
function counts(sn) {
|
|
43
|
+
const n = sn.node;
|
|
44
|
+
switch (n.kind) {
|
|
45
|
+
case "flow":
|
|
46
|
+
return `loops:${n.traverses.length + n.guarded_by.length} junctions:${n.crosses.length}`;
|
|
47
|
+
case "loop":
|
|
48
|
+
return `scenarios:${n.scenarios.length} eff:${sn.effectiveConstraints?.length ?? 0}`;
|
|
49
|
+
case "junction":
|
|
50
|
+
return `evidence:${n.evidence.length} scenarios:${n.scenarios.length}`;
|
|
51
|
+
case "scenario":
|
|
52
|
+
return `verified:${n.verified_by.length}`;
|
|
53
|
+
default:
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function truncate(s, max) {
|
|
58
|
+
const oneLine = s.replace(/\s+/g, " ").trim();
|
|
59
|
+
return oneLine.length <= max ? oneLine : `${oneLine.slice(0, max - 1)}…`;
|
|
60
|
+
}
|
|
61
|
+
function groupByKind(nodes) {
|
|
62
|
+
const groups = new Map();
|
|
63
|
+
for (const sn of nodes) {
|
|
64
|
+
const list = groups.get(sn.kind) ?? [];
|
|
65
|
+
list.push(sn);
|
|
66
|
+
groups.set(sn.kind, list);
|
|
67
|
+
}
|
|
68
|
+
return groups;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Compact stdout summary. `outputPath` is named so the truncation hint can
|
|
72
|
+
* point the reader at the full side-channel file.
|
|
73
|
+
*/
|
|
74
|
+
export function summarizeSlice(slice, outputPath) {
|
|
75
|
+
const groups = groupByKind(slice.nodes);
|
|
76
|
+
const lines = [
|
|
77
|
+
`inspect ${slice.rootId} (${slice.rootKind}) — depth ${slice.maxDepth}, ${slice.nodes.length} node(s)`,
|
|
78
|
+
];
|
|
79
|
+
for (const kind of KIND_ORDER) {
|
|
80
|
+
const list = groups.get(kind);
|
|
81
|
+
if (!list || list.length === 0)
|
|
82
|
+
continue;
|
|
83
|
+
lines.push(` ${kind} (${list.length}):`);
|
|
84
|
+
for (const sn of list) {
|
|
85
|
+
const c = counts(sn);
|
|
86
|
+
lines.push(` ${sn.id} ${label(sn.node)} [${tag(sn.node)}]${c ? ` ${c}` : ""}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (slice.frontierPointers.length > 0) {
|
|
90
|
+
lines.push(` → ${slice.frontierPointers.length} node(s) beyond depth ${slice.maxDepth} not expanded (use --depth to go deeper):`);
|
|
91
|
+
lines.push(` ${slice.frontierPointers.map((p) => `${p.id}(${p.kind})`).join(", ")}`);
|
|
92
|
+
}
|
|
93
|
+
// The one place details are truncated: evidence notes and scenario
|
|
94
|
+
// given/when/then never appear above — they live in the side-channel file.
|
|
95
|
+
lines.push(` (evidence/scenario detail truncated — full slice written to ${outputPath})`);
|
|
96
|
+
return lines.join("\n");
|
|
97
|
+
}
|
|
98
|
+
function renderNodeDetail(sn) {
|
|
99
|
+
const n = sn.node;
|
|
100
|
+
const out = [`### ${n.id} — ${label(n)} [${tag(n)}] (depth ${sn.depth})`];
|
|
101
|
+
switch (n.kind) {
|
|
102
|
+
case "flow":
|
|
103
|
+
if (n.traverses.length)
|
|
104
|
+
out.push(`- traverses: ${n.traverses.join(", ")}`);
|
|
105
|
+
if (n.guarded_by.length)
|
|
106
|
+
out.push(`- guarded_by: ${n.guarded_by.join(", ")}`);
|
|
107
|
+
if (n.crosses.length)
|
|
108
|
+
out.push(`- crosses: ${n.crosses.join(", ")}`);
|
|
109
|
+
if (n.references.length)
|
|
110
|
+
out.push(`- references: ${n.references.join(", ")}`);
|
|
111
|
+
break;
|
|
112
|
+
case "loop":
|
|
113
|
+
out.push(`- boundary: ${n.boundary}`);
|
|
114
|
+
out.push(`- owner: ${n.owner ?? "(none — dormant)"}`);
|
|
115
|
+
if (n.anchors.length)
|
|
116
|
+
out.push(`- anchors: ${n.anchors.join(", ")}`);
|
|
117
|
+
if (n.scenarios.length)
|
|
118
|
+
out.push(`- scenarios: ${n.scenarios.join(", ")}`);
|
|
119
|
+
if (sn.effectiveConstraints?.length)
|
|
120
|
+
out.push(`- effective constraints (applies_to): ${sn.effectiveConstraints.join(", ")}`);
|
|
121
|
+
if (n.notes)
|
|
122
|
+
out.push(`- notes: ${n.notes}`);
|
|
123
|
+
break;
|
|
124
|
+
case "junction":
|
|
125
|
+
out.push(`- between: ${n.between.join(" → ")}`);
|
|
126
|
+
if (n.scenarios.length)
|
|
127
|
+
out.push(`- scenarios: ${n.scenarios.join(", ")}`);
|
|
128
|
+
for (const e of n.evidence)
|
|
129
|
+
out.push(`- evidence [${e.kind}] ${e.anchor}${e.note ? ` — ${e.note}` : ""}`);
|
|
130
|
+
break;
|
|
131
|
+
case "scenario":
|
|
132
|
+
out.push(`- given: ${n.given}`);
|
|
133
|
+
out.push(`- when: ${n.when}`);
|
|
134
|
+
out.push(`- then: ${n.then}`);
|
|
135
|
+
out.push(`- verified_by: ${n.verified_by.length ? n.verified_by.join(", ") : "(none)"}`);
|
|
136
|
+
if (n.applies_to)
|
|
137
|
+
out.push(`- applies_to: nodes=[${n.applies_to.nodes.join(", ")}]${n.applies_to.owner_match ? ` owner_match="${n.applies_to.owner_match}"` : ""}`);
|
|
138
|
+
break;
|
|
139
|
+
case "debt":
|
|
140
|
+
out.push(`- reality: ${n.reality}`);
|
|
141
|
+
if (n.claim)
|
|
142
|
+
out.push(`- claim: ${n.claim}`);
|
|
143
|
+
break;
|
|
144
|
+
case "feature":
|
|
145
|
+
if (n.contains.length)
|
|
146
|
+
out.push(`- contains: ${n.contains.join(", ")}`);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
/** Full side-channel body: banner + every node's complete detail, nothing truncated. */
|
|
152
|
+
export function renderSliceMarkdown(slice, banner) {
|
|
153
|
+
const groups = groupByKind(slice.nodes);
|
|
154
|
+
const lines = [
|
|
155
|
+
banner,
|
|
156
|
+
"",
|
|
157
|
+
`# inspect ${slice.rootId} (${slice.rootKind}) — depth ${slice.maxDepth}`,
|
|
158
|
+
"",
|
|
159
|
+
`${slice.nodes.length} node(s) reached; ${slice.frontierPointers.length} pointer(s) at the frontier.`,
|
|
160
|
+
"",
|
|
161
|
+
];
|
|
162
|
+
for (const kind of KIND_ORDER) {
|
|
163
|
+
const list = groups.get(kind);
|
|
164
|
+
if (!list || list.length === 0)
|
|
165
|
+
continue;
|
|
166
|
+
lines.push(`## ${kind} (${list.length})`, "");
|
|
167
|
+
for (const sn of list) {
|
|
168
|
+
lines.push(...renderNodeDetail(sn), "");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (slice.frontierPointers.length > 0) {
|
|
172
|
+
lines.push(`## frontier (beyond depth ${slice.maxDepth} — re-run with --depth to expand)`, "");
|
|
173
|
+
for (const p of slice.frontierPointers) {
|
|
174
|
+
lines.push(`- ${p.id} (${p.kind}) — referenced by ${p.fromId}`);
|
|
175
|
+
}
|
|
176
|
+
lines.push("");
|
|
177
|
+
}
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=summary.js.map
|