@neoloopy/cld-canvas 0.1.3 → 0.1.7
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 +1 -1
- package/README.md +30 -3
- package/dist/engine/engine.d.ts +26 -4
- package/dist/engine/exporters.d.ts +1 -1
- package/dist/engine/exporters.js +1 -1
- package/dist/engine/loopGraph.d.ts +8 -4
- package/dist/engine/loopGraph.js +0 -0
- package/dist/engine/loopKey.d.ts +9 -0
- package/dist/engine/loopKey.js +14 -0
- package/dist/engine/nativeEngine.d.ts +10 -2
- package/dist/engine/nativeEngine.js +226 -70
- package/dist/engine/noteCodec.js +57 -5
- package/dist/engine/publicInterface.d.ts +43 -0
- package/dist/engine/publicInterface.js +52 -0
- package/dist/engine/quantCanvasLoops.d.ts +36 -0
- package/dist/engine/quantCanvasLoops.js +694 -0
- package/dist/engine/sfd.d.ts +47 -0
- package/dist/engine/sfd.js +320 -0
- package/dist/engine/specHash.js +4 -0
- package/dist/engine/types.d.ts +54 -3
- package/dist/engine/types.js +66 -5
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/view/geometry.d.ts +35 -1
- package/dist/view/geometry.js +174 -7
- package/dist/view/painter.d.ts +10 -1
- package/dist/view/painter.js +243 -15
- package/dist/view/sceneCache.d.ts +2 -1
- package/dist/view/sceneCache.js +35 -13
- package/package.json +12 -2
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -5,6 +5,10 @@ engine used by neoloopy surfaces, including the Obsidian plugin and web app.
|
|
|
5
5
|
Pure TypeScript, ESM-only, with no runtime dependency on Obsidian, React, Vue,
|
|
6
6
|
or a server.
|
|
7
7
|
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
<sub>The canvas this package renders — an SIR epidemic model open in Obsidian with the link-editing toolbar.</sub>
|
|
11
|
+
|
|
8
12
|
**This README is ordered easy → hard** — start at the top with the one-tag React
|
|
9
13
|
drop-in and go only as deep as you need:
|
|
10
14
|
|
|
@@ -463,22 +467,45 @@ import { buildMermaid, render } from "@neoloopy/cld-canvas";
|
|
|
463
467
|
|
|
464
468
|
## 5. Analysis helpers
|
|
465
469
|
|
|
470
|
+

|
|
471
|
+
|
|
472
|
+
<sub>The same loop detection these helpers expose, surfaced in a neoloopy surface — a project-dynamics model with its reinforcing and balancing loops and the insight panel.</sub>
|
|
473
|
+
|
|
466
474
|
```ts
|
|
467
|
-
import { LoopGraph, endogeneity } from "@neoloopy/cld-canvas";
|
|
475
|
+
import { LoopGraph, discoverCanvasLoops, endogeneity } from "@neoloopy/cld-canvas";
|
|
468
476
|
|
|
469
477
|
const loopGraph = new LoopGraph(graph.nodes);
|
|
470
|
-
const
|
|
478
|
+
const qualitative = loopGraph.detectLoops();
|
|
479
|
+
const { loops, analysisError } = discoverCanvasLoops(graph.nodes, qualitative, {
|
|
480
|
+
manifest: graph.manifest,
|
|
481
|
+
});
|
|
471
482
|
const metrics = loopGraph.metrics();
|
|
472
483
|
const summary = endogeneity(graph.nodes, loops);
|
|
473
484
|
```
|
|
474
485
|
|
|
486
|
+
`discoverCanvasLoops` first enriches each declared qualitative loop whose whole
|
|
487
|
+
authored route resolves to exact visible connectors or material pipe legs, even
|
|
488
|
+
when quantitative analysis is unavailable or incomplete. It adds a synthesized
|
|
489
|
+
quantitative loop only when its complete executable route resolves just as
|
|
490
|
+
exactly.
|
|
491
|
+
Material endpoints may be explicit or use the unique, direct, signed legacy
|
|
492
|
+
flow-to-stock encoding already supported by SFD rendering. It preserves
|
|
493
|
+
qualitative loop identity and deduplicates exact counterparts. Quantitative
|
|
494
|
+
discovery failures surface as `analysisError` and add no partial quantitative
|
|
495
|
+
badge. A declared qualitative loop whose exact canvas route cannot be resolved
|
|
496
|
+
keeps its established CLD identity and badge but is excluded from SFD; that
|
|
497
|
+
qualitative exclusion by itself does not set `analysisError`. SFD retains every
|
|
498
|
+
exactly resolved qualitative loop; a quantitative-only SFD badge additionally
|
|
499
|
+
requires at least one first-class material leg.
|
|
500
|
+
|
|
475
501
|
## What is exported
|
|
476
502
|
|
|
477
503
|
The package exports the public modules from `src/index.ts`, including:
|
|
478
504
|
|
|
479
505
|
- Engine: `NativeEngine`, `NeoloopyEngine`, `MemoryStorage`, `VaultStorage`
|
|
480
506
|
- Domain types: `VariableFile`, `VaultLink`, `ModelManifest`, `GraphView`
|
|
481
|
-
- Graph logic: `LoopGraph`, `DetectedLoop`, `LoopType`, `labelLoopsByKey
|
|
507
|
+
- Graph logic: `LoopGraph`, `DetectedLoop`, `LoopType`, `labelLoopsByKey`,
|
|
508
|
+
`discoverCanvasLoops`
|
|
482
509
|
- Rendering: `paint`, `SceneCache`, `Camera`, `LIGHT`, `DARK`, geometry helpers
|
|
483
510
|
- File codecs: `parseNote`, `serializeNote`, `manifestFromJson`
|
|
484
511
|
- Exporters: `render`, `buildMermaid`, `loopNoteKey`
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { DetectedLoop, ModelManifest, VarType, VariableFile, Viewport } from "./types";
|
|
8
8
|
import { Rendered } from "./exporters";
|
|
9
9
|
import { ParentAnchor } from "./subsystemLinks";
|
|
10
|
+
import { ChildInterface } from "./publicInterface";
|
|
10
11
|
/** Lightweight model descriptor for pickers/lists (no notes loaded). */
|
|
11
12
|
export interface ModelRef {
|
|
12
13
|
id: string;
|
|
@@ -33,6 +34,8 @@ export interface GraphView {
|
|
|
33
34
|
/** loop.key -> "R1" / "B2" badge label. */
|
|
34
35
|
labels: Map<string, string>;
|
|
35
36
|
quant: boolean;
|
|
37
|
+
/** Non-null when quantitative canvas-loop discovery could not complete. */
|
|
38
|
+
analysisError?: string | null;
|
|
36
39
|
}
|
|
37
40
|
export interface NewVariable {
|
|
38
41
|
label: string;
|
|
@@ -157,7 +160,17 @@ export interface NeoloopyEngine {
|
|
|
157
160
|
setEquation(folder: string, id: string, patch: QuantPatch): Promise<VariableFile>;
|
|
158
161
|
/** Cosmetic position change — never bumps rev/modified. */
|
|
159
162
|
moveVariable(folder: string, id: string, x: number, y: number): Promise<void>;
|
|
163
|
+
/** Cosmetic SFD-view position change (`extra.sfd`) — never bumps rev/modified. */
|
|
164
|
+
moveVariableSfd(folder: string, id: string, x: number, y: number): Promise<void>;
|
|
165
|
+
/**
|
|
166
|
+
* If a model has no authored SFD coordinates, persist the deterministic SFD
|
|
167
|
+
* fallback positions into `extra.sfd` so the first manual SFD edit does not
|
|
168
|
+
* make the diagram jump back to CLD coordinates.
|
|
169
|
+
*/
|
|
170
|
+
pinSfdLayout(folder: string): Promise<void>;
|
|
160
171
|
removeVariable(folder: string, id: string): Promise<void>;
|
|
172
|
+
/** Validated SFD material topology write for a flow note (`extra.flow`). */
|
|
173
|
+
setFlowEndpoints(folder: string, flowId: string, from: string, to: string): Promise<void>;
|
|
161
174
|
/**
|
|
162
175
|
* Link (or clear, with `child=null`) a node's subsystem anchor — the same
|
|
163
176
|
* `[[../<childDir>/System|<Child Name>]]` wikilink the app/CLI write, so the
|
|
@@ -176,10 +189,10 @@ export interface NeoloopyEngine {
|
|
|
176
189
|
export(folder: string, format: ExportFormat): Promise<Rendered>;
|
|
177
190
|
/**
|
|
178
191
|
* Loop notes — one `Loops/<slug>.md` file per feedback loop, identity-anchored
|
|
179
|
-
* in frontmatter (the same files the Dart app/CLI write).
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
192
|
+
* in frontmatter (the same files the Dart app/CLI write). Qualitative entries
|
|
193
|
+
* retain the established `<R|B>:<sorted unique variable names>` map key;
|
|
194
|
+
* quantitative-only entries use their exact directed `loop.key` so distinct
|
|
195
|
+
* routes cannot collide. `setLoopNote` accepts that one resolved key.
|
|
183
196
|
*/
|
|
184
197
|
getLoopNotes(folder: string): Promise<Record<string, string>>;
|
|
185
198
|
setLoopNote(folder: string, key: string, text: string): Promise<void>;
|
|
@@ -200,4 +213,13 @@ export interface NeoloopyEngine {
|
|
|
200
213
|
* Each anchor names the parent model + the linking variable. Empty when none.
|
|
201
214
|
*/
|
|
202
215
|
deriveParents(folder: string): Promise<ParentAnchor[]>;
|
|
216
|
+
/**
|
|
217
|
+
* The public interface of the child model linked from `varId`'s subsystem
|
|
218
|
+
* anchor: a display qualifier (the link alias, else the child model name) plus
|
|
219
|
+
* the labels of the child's public OUTPUTS and INPUTS. Null when the node has
|
|
220
|
+
* no subsystem link, or the child can't be resolved/loaded (fail closed —
|
|
221
|
+
* callers render a muted fallback). Read-only: the qualitative engine never
|
|
222
|
+
* edits the quant interface (publish/bind live in the app/CLI/MCP).
|
|
223
|
+
*/
|
|
224
|
+
childInterface(folder: string, varId: string): Promise<ChildInterface | null>;
|
|
203
225
|
}
|
package/dist/engine/exporters.js
CHANGED
|
@@ -26,7 +26,7 @@ export function buildMermaid(nodes, edges) {
|
|
|
26
26
|
const t = key.get(String(e.target));
|
|
27
27
|
if (!s || !t)
|
|
28
28
|
continue;
|
|
29
|
-
const sym = (e.polarity ?? 1) >= 0 ? "+" : "−";
|
|
29
|
+
const sym = e.polarity === "?" ? "?" : (e.polarity ?? 1) >= 0 ? "+" : "−";
|
|
30
30
|
const arrow = e.delay === true ? "-.->" : "-->";
|
|
31
31
|
lines.push(` ${s} ${arrow}|${sym}| ${t}`);
|
|
32
32
|
}
|
|
@@ -23,9 +23,9 @@ export interface VarMetrics {
|
|
|
23
23
|
loopCount: number;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* Deterministic loop labels: reinforcing -> R1,R2,… and balancing -> B1,B2
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Deterministic loop labels: reinforcing -> R1,R2,… and balancing -> B1,B2,….
|
|
27
|
+
* Existing qualitative loops keep their historical ordering and numbering;
|
|
28
|
+
* quantitative-only loops follow them in the same deterministic name order.
|
|
29
29
|
*/
|
|
30
30
|
export declare function labelLoopsByKey(loops: DetectedLoop[], name: (id: string) => string): Map<string, string>;
|
|
31
31
|
export declare class LoopGraph {
|
|
@@ -37,7 +37,11 @@ export declare class LoopGraph {
|
|
|
37
37
|
adjacency(reverse?: boolean): Map<string, TracedEdge[]>;
|
|
38
38
|
/** Direct (loop-relevant) adjacency: source -> [target, polaritySign]. */
|
|
39
39
|
private directAdjacency;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* All simple directed cycles, R/B classified, deduped by the legacy node
|
|
42
|
+
* set. When distinct directed routes collapse to that compatibility key, the
|
|
43
|
+
* retained CLD loop is marked ambiguous so SFD resolution can fail closed.
|
|
44
|
+
*/
|
|
41
45
|
detectLoops(): DetectedLoop[];
|
|
42
46
|
/**
|
|
43
47
|
* The Shortest Independent Loop Set (SILS): the minimum-weight basis of the
|
package/dist/engine/loopGraph.js
CHANGED
|
Binary file
|
package/dist/engine/loopKey.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DetectedLoop } from "./types";
|
|
1
2
|
/**
|
|
2
3
|
* The resolved-map key for a feedback loop: `<R|B>:<sorted unique member
|
|
3
4
|
* labels>`. This is the key every UI consumer indexes loop notes by; it is NOT
|
|
@@ -9,6 +10,14 @@
|
|
|
9
10
|
* its own copy.
|
|
10
11
|
*/
|
|
11
12
|
export declare function loopKey(labels: string[], type: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* The one UI/engine lookup key for a detected loop note. Existing qualitative
|
|
15
|
+
* loops, including a qualitative counterpart enriched with a quantitative
|
|
16
|
+
* canvas path, retain the sorted-label key shared with earlier releases.
|
|
17
|
+
* Quantitative-only loops use exact directed id identity so different routes
|
|
18
|
+
* through the same displayed members remain independent.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolvedLoopNoteKey(loop: DetectedLoop, nameOf: (id: string) => string): string;
|
|
12
21
|
/**
|
|
13
22
|
* Human-readable, non-link echo for a loop note's `loop:` frontmatter field:
|
|
14
23
|
* `<R|B> · <sorted unique labels joined by " | ">`. Same membership rule as
|
package/dist/engine/loopKey.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LoopType } from "./types";
|
|
1
2
|
/**
|
|
2
3
|
* The resolved-map key for a feedback loop: `<R|B>:<sorted unique member
|
|
3
4
|
* labels>`. This is the key every UI consumer indexes loop notes by; it is NOT
|
|
@@ -13,6 +14,19 @@ export function loopKey(labels, type) {
|
|
|
13
14
|
const uniq = [...new Set(labels.map((l) => String(l)))].sort();
|
|
14
15
|
return `${letter}:${uniq.join("|")}`;
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The one UI/engine lookup key for a detected loop note. Existing qualitative
|
|
19
|
+
* loops, including a qualitative counterpart enriched with a quantitative
|
|
20
|
+
* canvas path, retain the sorted-label key shared with earlier releases.
|
|
21
|
+
* Quantitative-only loops use exact directed id identity so different routes
|
|
22
|
+
* through the same displayed members remain independent.
|
|
23
|
+
*/
|
|
24
|
+
export function resolvedLoopNoteKey(loop, nameOf) {
|
|
25
|
+
if (loop.identityMode === "quantitative")
|
|
26
|
+
return loop.exactKey;
|
|
27
|
+
const type = loop.type === LoopType.reinforcing ? "R" : "B";
|
|
28
|
+
return loopKey(loop.nodeIds.map(nameOf), type);
|
|
29
|
+
}
|
|
16
30
|
/**
|
|
17
31
|
* Human-readable, non-link echo for a loop note's `loop:` frontmatter field:
|
|
18
32
|
* `<R|B> · <sorted unique labels joined by " | ">`. Same membership rule as
|
|
@@ -26,6 +26,7 @@ import { contentSignature } from "./specHash";
|
|
|
26
26
|
import { Rendered } from "./exporters";
|
|
27
27
|
import { VaultStorage } from "./storage";
|
|
28
28
|
import { ParentAnchor } from "./subsystemLinks";
|
|
29
|
+
import { ChildInterface } from "./publicInterface";
|
|
29
30
|
export interface NativeEngineOptions {
|
|
30
31
|
/** Vault-relative base folder for newly created models (may be ""). */
|
|
31
32
|
modelsRoot: string;
|
|
@@ -84,13 +85,17 @@ export declare class NativeEngine implements NeoloopyEngine {
|
|
|
84
85
|
name: string;
|
|
85
86
|
} | null): Promise<void>;
|
|
86
87
|
moveVariable(folder: string, id: string, x: number, y: number): Promise<void>;
|
|
88
|
+
moveVariableSfd(folder: string, id: string, x: number, y: number): Promise<void>;
|
|
89
|
+
pinSfdLayout(folder: string): Promise<void>;
|
|
87
90
|
removeVariable(folder: string, id: string): Promise<void>;
|
|
91
|
+
setFlowEndpoints(folder: string, flowId: string, from: string, to: string): Promise<void>;
|
|
88
92
|
addLink(folder: string, from: string, to: string, init?: LinkInit): Promise<void>;
|
|
89
93
|
updateLink(folder: string, from: string, to: string, patch: LinkPatch): Promise<void>;
|
|
90
94
|
removeLink(folder: string, from: string, to: string): Promise<void>;
|
|
91
95
|
buildModel(folder: string, spec: BuildSpec): Promise<void>;
|
|
92
96
|
relayout(folder: string): Promise<void>;
|
|
93
97
|
setViewport(folder: string, viewport: Viewport): Promise<void>;
|
|
98
|
+
private liveLoopContext;
|
|
94
99
|
getLoopNotes(folder: string): Promise<Record<string, string>>;
|
|
95
100
|
setLoopNote(folder: string, key: string, text: string): Promise<void>;
|
|
96
101
|
/**
|
|
@@ -102,6 +107,7 @@ export declare class NativeEngine implements NeoloopyEngine {
|
|
|
102
107
|
loopNotePath(folder: string, key: string): Promise<string | null>;
|
|
103
108
|
ensureSystemNote(folder: string): Promise<string>;
|
|
104
109
|
deriveParents(folder: string): Promise<ParentAnchor[]>;
|
|
110
|
+
childInterface(folder: string, varId: string): Promise<ChildInterface | null>;
|
|
105
111
|
export(folder: string, format: ExportFormat): Promise<Rendered>;
|
|
106
112
|
/** The model's `Nodes/` subfolder, where variable notes now live. */
|
|
107
113
|
private nodesDir;
|
|
@@ -110,6 +116,8 @@ export declare class NativeEngine implements NeoloopyEngine {
|
|
|
110
116
|
/** Pre-`Nodes/` location of a variable note, flat at the model root. */
|
|
111
117
|
private legacyNotePath;
|
|
112
118
|
private readNote;
|
|
119
|
+
private removeNoteById;
|
|
120
|
+
private detachStockFlowEndpoints;
|
|
113
121
|
private writeNote;
|
|
114
122
|
private readManifest;
|
|
115
123
|
private writeManifest;
|
|
@@ -134,13 +142,13 @@ export declare class NativeEngine implements NeoloopyEngine {
|
|
|
134
142
|
* One-way, idempotent migration of legacy loop annotations from the
|
|
135
143
|
* `model.json` maps into `Loops/*.md` files. Guard: if any `Loops/` file
|
|
136
144
|
* exists the model is treated as migrated and this returns immediately. Each
|
|
137
|
-
* legacy key is matched to a live loop by its
|
|
145
|
+
* legacy key is matched to a live loop by its resolved note key; a match keeps
|
|
138
146
|
* the loop's canonical member ids, an unmatched key becomes a pre-flagged
|
|
139
147
|
* orphan (`members: []`, original key kept in `loop:`) so nothing is lost.
|
|
140
148
|
* After writing, the four legacy keys are stripped from the manifest.
|
|
141
149
|
*/
|
|
142
150
|
private migrateLoopNotesIfNeeded;
|
|
143
|
-
/** Find the live loop whose
|
|
151
|
+
/** Find the live loop whose resolved note key == `key`, then upsert its file. No-op
|
|
144
152
|
* (and no file) when the graph carries no such loop. */
|
|
145
153
|
private upsertLoopFileByKey;
|
|
146
154
|
/** Find-or-create the identity-matched `Loops/` file and apply `fields`
|