@neoloopy/cld-canvas 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/dist/engine/analysis.d.ts +15 -0
- package/dist/engine/analysis.js +30 -0
- package/dist/engine/engine.d.ts +203 -0
- package/dist/engine/engine.js +7 -0
- package/dist/engine/exporters.d.ts +45 -0
- package/dist/engine/exporters.js +110 -0
- package/dist/engine/layout.d.ts +19 -0
- package/dist/engine/layout.js +142 -0
- package/dist/engine/loopGraph.d.ts +51 -0
- package/dist/engine/loopGraph.js +0 -0
- package/dist/engine/loopKey.d.ts +24 -0
- package/dist/engine/loopKey.js +32 -0
- package/dist/engine/loopNote.d.ts +40 -0
- package/dist/engine/loopNote.js +130 -0
- package/dist/engine/nativeEngine.d.ts +175 -0
- package/dist/engine/nativeEngine.js +1120 -0
- package/dist/engine/noteCodec.d.ts +29 -0
- package/dist/engine/noteCodec.js +254 -0
- package/dist/engine/noteNaming.d.ts +18 -0
- package/dist/engine/noteNaming.js +28 -0
- package/dist/engine/specHash.d.ts +37 -0
- package/dist/engine/specHash.js +86 -0
- package/dist/engine/storage.d.ts +49 -0
- package/dist/engine/storage.js +116 -0
- package/dist/engine/subsystemLinks.d.ts +29 -0
- package/dist/engine/subsystemLinks.js +55 -0
- package/dist/engine/types.d.ts +103 -0
- package/dist/engine/types.js +155 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/view/camera.d.ts +42 -0
- package/dist/view/camera.js +75 -0
- package/dist/view/geometry.d.ts +83 -0
- package/dist/view/geometry.js +408 -0
- package/dist/view/painter.d.ts +63 -0
- package/dist/view/painter.js +471 -0
- package/dist/view/sceneCache.d.ts +61 -0
- package/dist/view/sceneCache.js +134 -0
- package/dist/view/theme.d.ts +57 -0
- package/dist/view/theme.js +90 -0
- package/package.json +17 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The resolved-map key for a feedback loop: `<R|B>:<sorted unique member
|
|
3
|
+
* labels>`. This is the key every UI consumer indexes loop notes by; it is NOT
|
|
4
|
+
* the loop's on-disk identity (that lives in each `Loops/*.md` frontmatter as
|
|
5
|
+
* the ordered member ids — see `loopNote.ts`). Mirrors Dart `fmt.loopNoteKey`.
|
|
6
|
+
*
|
|
7
|
+
* Pure and dependency-free so both the engine (`nativeEngine`) and the view
|
|
8
|
+
* (`view/loopKeys`) derive the key from one definition instead of each carrying
|
|
9
|
+
* its own copy.
|
|
10
|
+
*/
|
|
11
|
+
export function loopKey(labels, type) {
|
|
12
|
+
const letter = type.toUpperCase().startsWith("R") ? "R" : "B";
|
|
13
|
+
const uniq = [...new Set(labels.map((l) => String(l)))].sort();
|
|
14
|
+
return `${letter}:${uniq.join("|")}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Human-readable, non-link echo for a loop note's `loop:` frontmatter field:
|
|
18
|
+
* `<R|B> · <sorted unique labels joined by " | ">`. Same membership rule as
|
|
19
|
+
* `loopKey` (dedupe + sort) but rendered for humans — it deliberately avoids a
|
|
20
|
+
* leading `scheme:` so Obsidian's property view does not show it as a (dead)
|
|
21
|
+
* external link. Display only; NEVER an identity/lookup/export/API key (that is
|
|
22
|
+
* `loopKey`). Mirrors Dart `fmt.loopEchoLabel`.
|
|
23
|
+
*
|
|
24
|
+
* The Set dedupe absorbs a closed-cycle duplicate, so no explicit `first==last`
|
|
25
|
+
* trim is needed. Keep this symmetric with the Dart twin: do not add a trim to
|
|
26
|
+
* only one side — divergent membership handling would break byte parity.
|
|
27
|
+
*/
|
|
28
|
+
export function loopEchoLabel(labels, type) {
|
|
29
|
+
const letter = type.toUpperCase().startsWith("R") ? "R" : "B";
|
|
30
|
+
const uniq = [...new Set(labels.map((l) => String(l)))].sort();
|
|
31
|
+
return uniq.length === 0 ? letter : `${letter} · ${uniq.join(" | ")}`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One feedback-loop note (`Loops/<slug>.md`): an Obsidian-native markdown file
|
|
3
|
+
* whose identity is the loop's (type, ordered member variable-ids) — held in
|
|
4
|
+
* frontmatter, NEVER derived from the filename or from variable labels. The body
|
|
5
|
+
* owns the narrative; `loop` is a human-readable echo only.
|
|
6
|
+
*
|
|
7
|
+
* TypeScript port of `core/lib/vault/loop_note.dart`. The serializer matches the
|
|
8
|
+
* Dart output byte-for-byte (field order, `members: []` empty form, raw
|
|
9
|
+
* `valence`, scalar quoting via the shared note codec) so a `Loops/*.md` file
|
|
10
|
+
* written by the plugin and by the app/CLI round-trips losslessly on one vault.
|
|
11
|
+
*/
|
|
12
|
+
import { YamlParse } from "./noteCodec";
|
|
13
|
+
export interface LoopNote {
|
|
14
|
+
/** 'R' (reinforcing) or 'B' (balancing). */
|
|
15
|
+
type: string;
|
|
16
|
+
/** Canonical-ordered member variable ids (the stable identity). */
|
|
17
|
+
members: string[];
|
|
18
|
+
/** Free-text alias shown beside the R/B badge (was `loopTitles`). */
|
|
19
|
+
title: string;
|
|
20
|
+
/** 'virtuous' | 'vicious' | '' (was `loopValence`). */
|
|
21
|
+
valence: string;
|
|
22
|
+
/** Human-readable label echo (`R:Label|Label`). Non-authoritative. */
|
|
23
|
+
loopEcho: string;
|
|
24
|
+
body: string;
|
|
25
|
+
/** Unknown frontmatter keys, preserved verbatim (carries app-only `archetype`). */
|
|
26
|
+
extra: Record<string, unknown>;
|
|
27
|
+
/** True when frontmatter was present but unparseable (body still served). */
|
|
28
|
+
malformed: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function emptyLoopNote(partial?: Partial<LoopNote>): LoopNote;
|
|
31
|
+
export declare function parseLoopNote(source: string, yaml: YamlParse): LoopNote;
|
|
32
|
+
export declare function serializeLoopNote(n: LoopNote): string;
|
|
33
|
+
/**
|
|
34
|
+
* Rotate a cycle's ids so it starts at its lexicographically smallest id. Loop
|
|
35
|
+
* identity is rotation-invariant but routing-sensitive (order matters), so two
|
|
36
|
+
* different routings through the same node set stay distinct.
|
|
37
|
+
*/
|
|
38
|
+
export declare function canonicalLoopMembers(nodeIds: string[]): string[];
|
|
39
|
+
export declare function loopMatchesNote(type: string, nodeIds: string[], note: LoopNote): boolean;
|
|
40
|
+
export declare function loopSlug(type: string, title: string, memberLabels: string[]): string;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One feedback-loop note (`Loops/<slug>.md`): an Obsidian-native markdown file
|
|
3
|
+
* whose identity is the loop's (type, ordered member variable-ids) — held in
|
|
4
|
+
* frontmatter, NEVER derived from the filename or from variable labels. The body
|
|
5
|
+
* owns the narrative; `loop` is a human-readable echo only.
|
|
6
|
+
*
|
|
7
|
+
* TypeScript port of `core/lib/vault/loop_note.dart`. The serializer matches the
|
|
8
|
+
* Dart output byte-for-byte (field order, `members: []` empty form, raw
|
|
9
|
+
* `valence`, scalar quoting via the shared note codec) so a `Loops/*.md` file
|
|
10
|
+
* written by the plugin and by the app/CLI round-trips losslessly on one vault.
|
|
11
|
+
*/
|
|
12
|
+
import { emitExtra, scalar, splitFrontmatter } from "./noteCodec";
|
|
13
|
+
const FENCE = "---";
|
|
14
|
+
const KNOWN = new Set(["type", "members", "title", "valence", "loop"]);
|
|
15
|
+
export function emptyLoopNote(partial = {}) {
|
|
16
|
+
return {
|
|
17
|
+
type: "R",
|
|
18
|
+
members: [],
|
|
19
|
+
title: "",
|
|
20
|
+
valence: "",
|
|
21
|
+
loopEcho: "",
|
|
22
|
+
body: "",
|
|
23
|
+
extra: {},
|
|
24
|
+
malformed: false,
|
|
25
|
+
...partial,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function parseLoopNote(source, yaml) {
|
|
29
|
+
const [fm, body] = splitFrontmatter(source);
|
|
30
|
+
if (fm === null)
|
|
31
|
+
return emptyLoopNote({ body });
|
|
32
|
+
let node;
|
|
33
|
+
let malformed = false;
|
|
34
|
+
try {
|
|
35
|
+
node = yaml(fm);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
malformed = true;
|
|
39
|
+
}
|
|
40
|
+
const isMap = node !== null && typeof node === "object" && !Array.isArray(node);
|
|
41
|
+
if (node !== null && node !== undefined && !isMap)
|
|
42
|
+
malformed = true;
|
|
43
|
+
const m = isMap ? node : {};
|
|
44
|
+
const type = String(m["type"] ?? "R").toUpperCase().startsWith("B") ? "B" : "R";
|
|
45
|
+
const members = [];
|
|
46
|
+
if (Array.isArray(m["members"])) {
|
|
47
|
+
for (const e of m["members"]) {
|
|
48
|
+
const s = String(e).trim();
|
|
49
|
+
if (s.length > 0)
|
|
50
|
+
members.push(s);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const valenceRaw = String(m["valence"] ?? "").trim().toLowerCase();
|
|
54
|
+
const extra = {};
|
|
55
|
+
for (const [k, v] of Object.entries(m))
|
|
56
|
+
if (!KNOWN.has(k))
|
|
57
|
+
extra[k] = v;
|
|
58
|
+
return {
|
|
59
|
+
type,
|
|
60
|
+
members,
|
|
61
|
+
title: String(m["title"] ?? "").trim(),
|
|
62
|
+
valence: valenceRaw === "virtuous" || valenceRaw === "vicious" ? valenceRaw : "",
|
|
63
|
+
loopEcho: String(m["loop"] ?? "").trim(),
|
|
64
|
+
body,
|
|
65
|
+
extra,
|
|
66
|
+
malformed,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function serializeLoopNote(n) {
|
|
70
|
+
const lines = [FENCE];
|
|
71
|
+
lines.push(`type: ${n.type}`);
|
|
72
|
+
if (n.members.length === 0) {
|
|
73
|
+
lines.push("members: []");
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
lines.push("members:");
|
|
77
|
+
for (const id of n.members)
|
|
78
|
+
lines.push(` - ${scalar(id)}`);
|
|
79
|
+
}
|
|
80
|
+
if (n.title.length > 0)
|
|
81
|
+
lines.push(`title: ${scalar(n.title)}`);
|
|
82
|
+
if (n.valence.length > 0)
|
|
83
|
+
lines.push(`valence: ${n.valence}`);
|
|
84
|
+
if (n.loopEcho.length > 0)
|
|
85
|
+
lines.push(`loop: ${scalar(n.loopEcho)}`);
|
|
86
|
+
for (const [k, v] of Object.entries(n.extra))
|
|
87
|
+
emitExtra(lines, k, v, 0);
|
|
88
|
+
lines.push(FENCE);
|
|
89
|
+
let out = lines.join("\n") + "\n";
|
|
90
|
+
if (n.body.trim().length > 0)
|
|
91
|
+
out += "\n" + n.body.trim() + "\n";
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Rotate a cycle's ids so it starts at its lexicographically smallest id. Loop
|
|
96
|
+
* identity is rotation-invariant but routing-sensitive (order matters), so two
|
|
97
|
+
* different routings through the same node set stay distinct.
|
|
98
|
+
*/
|
|
99
|
+
export function canonicalLoopMembers(nodeIds) {
|
|
100
|
+
if (nodeIds.length === 0)
|
|
101
|
+
return [];
|
|
102
|
+
let min = 0;
|
|
103
|
+
for (let i = 1; i < nodeIds.length; i++) {
|
|
104
|
+
if (nodeIds[i] < nodeIds[min])
|
|
105
|
+
min = i;
|
|
106
|
+
}
|
|
107
|
+
return [...nodeIds.slice(min), ...nodeIds.slice(0, min)];
|
|
108
|
+
}
|
|
109
|
+
export function loopMatchesNote(type, nodeIds, note) {
|
|
110
|
+
if (note.type !== type)
|
|
111
|
+
return false;
|
|
112
|
+
const a = canonicalLoopMembers(nodeIds);
|
|
113
|
+
const b = canonicalLoopMembers(note.members);
|
|
114
|
+
if (a.length !== b.length)
|
|
115
|
+
return false;
|
|
116
|
+
for (let i = 0; i < a.length; i++)
|
|
117
|
+
if (a[i] !== b[i])
|
|
118
|
+
return false;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
const SLUG_STRIP = /[^a-z0-9]+/g;
|
|
122
|
+
export function loopSlug(type, title, memberLabels) {
|
|
123
|
+
const basis = title.trim().length > 0 ? title : memberLabels.join("-");
|
|
124
|
+
const body = basis
|
|
125
|
+
.toLowerCase()
|
|
126
|
+
.replace(SLUG_STRIP, "-")
|
|
127
|
+
.replace(/^-+|-+$/g, "");
|
|
128
|
+
const t = type.toUpperCase().startsWith("B") ? "b" : "r";
|
|
129
|
+
return body.length === 0 ? t : `${t}-${body}`;
|
|
130
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeEngine — the default, fully-open-source TypeScript implementation of the
|
|
3
|
+
* qualitative neoloopy surface, working directly on vault files through a
|
|
4
|
+
* `VaultStorage`. It reuses the Phase-2 engine modules (codec, loop graph,
|
|
5
|
+
* layout, exporters, signature) so reads/writes are byte-compatible with the
|
|
6
|
+
* Dart CLI/app and the Python bridge. No `obsidian` import — unit-testable in
|
|
7
|
+
* plain Node against `MemoryStorage`.
|
|
8
|
+
*
|
|
9
|
+
* Vault conventions mirrored from `core/lib/vault/desktop_vault_storage.dart`
|
|
10
|
+
* and `core/lib/cli/vault_engine.dart`:
|
|
11
|
+
* - variable note file = `Nodes/<id>.md`, id = `var_` + 8 hex. Legacy vaults
|
|
12
|
+
* wrote `<id>.md` flat at the model root; those are still read (a `Nodes/`
|
|
13
|
+
* copy wins on a duplicate id) and swept into `Nodes/` when next written.
|
|
14
|
+
* - loop notes live in the model's `Loops/<slug>.md` files (see below).
|
|
15
|
+
* - model folder name = slug(name) (`-2`, `-3` … on collision), `model.json`
|
|
16
|
+
* manifest pretty-printed (2-space indent)
|
|
17
|
+
* - `System.md` / `Futures.md` / `CLA.md` are not variables
|
|
18
|
+
* - discovery: any folder with a `model.json` is a model and is NOT recursed
|
|
19
|
+
* into, so a model's own `Nodes/`/`Loops/` subfolders are never mistaken for
|
|
20
|
+
* nested models; grouped/nested model folders elsewhere are still found.
|
|
21
|
+
*/
|
|
22
|
+
import { BuildSpec, ExportFormat, GraphView, LinkInit, LinkPatch, ModelRef, NeoloopyEngine, NewVariable, QuantPatch, VariablePatch } from "./engine";
|
|
23
|
+
import { VariableFile, Viewport } from "./types";
|
|
24
|
+
import { YamlParse } from "./noteCodec";
|
|
25
|
+
import { contentSignature } from "./specHash";
|
|
26
|
+
import { Rendered } from "./exporters";
|
|
27
|
+
import { VaultStorage } from "./storage";
|
|
28
|
+
import { ParentAnchor } from "./subsystemLinks";
|
|
29
|
+
export interface NativeEngineOptions {
|
|
30
|
+
/** Vault-relative base folder for newly created models (may be ""). */
|
|
31
|
+
modelsRoot: string;
|
|
32
|
+
}
|
|
33
|
+
export declare class NativeEngine implements NeoloopyEngine {
|
|
34
|
+
private readonly storage;
|
|
35
|
+
private readonly yaml;
|
|
36
|
+
private readonly opts;
|
|
37
|
+
constructor(storage: VaultStorage, yaml: YamlParse, opts: NativeEngineOptions);
|
|
38
|
+
listModels(): Promise<ModelRef[]>;
|
|
39
|
+
loadGraph(folder: string): Promise<GraphView>;
|
|
40
|
+
createModel(name: string): Promise<ModelRef>;
|
|
41
|
+
renameModel(folder: string, name: string): Promise<ModelRef>;
|
|
42
|
+
retitleModel(folder: string, name: string): Promise<ModelRef>;
|
|
43
|
+
/**
|
|
44
|
+
* Sync a node's label to its filename after the user renamed the file in the
|
|
45
|
+
* vault (`Nodes/<stem>.md`). The node-level inverse of {@link retitleModel}:
|
|
46
|
+
* the file is already where the user put it, so the label follows it (the
|
|
47
|
+
* de-slugged stem, underscores → spaces). {@link writeNote} may then
|
|
48
|
+
* re-normalize the on-disk name (spaces → underscores) so it converges. The
|
|
49
|
+
* stable `var_…` id and every link to it are untouched, so links never break.
|
|
50
|
+
*/
|
|
51
|
+
relabelNodeFromFilename(folder: string, fileStem: string): Promise<void>;
|
|
52
|
+
/** Target folder for renaming `folder`'s model to `name`: the new slug under
|
|
53
|
+
* the same parent, suffixed on collision; unchanged when the slug matches. */
|
|
54
|
+
private destForRename;
|
|
55
|
+
deleteModel(folder: string): Promise<void>;
|
|
56
|
+
/** Resolve a folder to its model id (read from `model.json`). */
|
|
57
|
+
modelId(folder: string): Promise<string>;
|
|
58
|
+
/**
|
|
59
|
+
* Duplicate a model as a brand-new model: a byte-for-byte copy of the folder
|
|
60
|
+
* tree (so prose, layout, loop notes, and System note all come along), then a
|
|
61
|
+
* full re-key so the copy has a fresh model id, fresh variable ids, and fresh
|
|
62
|
+
* loop/System identity anchors. `shared` keys are PRESERVED, so the copy stays
|
|
63
|
+
* joined to the cross-model graph. Lands as a sibling of the source, titled
|
|
64
|
+
* "<name> (copy)" (bumping to "(copy 2)…" on a name clash).
|
|
65
|
+
*/
|
|
66
|
+
duplicateModel(srcFolder: string): Promise<ModelRef>;
|
|
67
|
+
/**
|
|
68
|
+
* Heal model-id collisions: a raw Obsidian copy of a model (or a folder of
|
|
69
|
+
* models) clones their ids verbatim, so two folders end up claiming the same
|
|
70
|
+
* `mdl_…`. Group folders by id; for each colliding group keep the OLDEST (by
|
|
71
|
+
* `created`) and re-key every newer sibling. Returns one row per re-keyed
|
|
72
|
+
* folder. A no-op (empty result) when every id is already unique.
|
|
73
|
+
*/
|
|
74
|
+
healDuplicateIds(): Promise<Array<{
|
|
75
|
+
folder: string;
|
|
76
|
+
oldId: string;
|
|
77
|
+
newId: string;
|
|
78
|
+
}>>;
|
|
79
|
+
addVariable(folder: string, init: NewVariable): Promise<VariableFile>;
|
|
80
|
+
updateVariable(folder: string, id: string, patch: VariablePatch): Promise<VariableFile>;
|
|
81
|
+
setEquation(folder: string, id: string, patch: QuantPatch): Promise<VariableFile>;
|
|
82
|
+
setSubsystem(folder: string, varId: string, child: {
|
|
83
|
+
folder: string;
|
|
84
|
+
name: string;
|
|
85
|
+
} | null): Promise<void>;
|
|
86
|
+
moveVariable(folder: string, id: string, x: number, y: number): Promise<void>;
|
|
87
|
+
removeVariable(folder: string, id: string): Promise<void>;
|
|
88
|
+
addLink(folder: string, from: string, to: string, init?: LinkInit): Promise<void>;
|
|
89
|
+
updateLink(folder: string, from: string, to: string, patch: LinkPatch): Promise<void>;
|
|
90
|
+
removeLink(folder: string, from: string, to: string): Promise<void>;
|
|
91
|
+
buildModel(folder: string, spec: BuildSpec): Promise<void>;
|
|
92
|
+
relayout(folder: string): Promise<void>;
|
|
93
|
+
setViewport(folder: string, viewport: Viewport): Promise<void>;
|
|
94
|
+
getLoopNotes(folder: string): Promise<Record<string, string>>;
|
|
95
|
+
setLoopNote(folder: string, key: string, text: string): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* The vault-relative path of the canonical `Loops/*.md` file for `key`,
|
|
98
|
+
* creating an empty-but-anchored note if none exists yet. Returns null when no
|
|
99
|
+
* live loop carries that key (e.g. the graph changed). Used by the canvas to
|
|
100
|
+
* open the real markdown file for editing.
|
|
101
|
+
*/
|
|
102
|
+
loopNotePath(folder: string, key: string): Promise<string | null>;
|
|
103
|
+
ensureSystemNote(folder: string): Promise<string>;
|
|
104
|
+
deriveParents(folder: string): Promise<ParentAnchor[]>;
|
|
105
|
+
export(folder: string, format: ExportFormat): Promise<Rendered>;
|
|
106
|
+
/** The model's `Nodes/` subfolder, where variable notes now live. */
|
|
107
|
+
private nodesDir;
|
|
108
|
+
/** A variable note's path for a filename stem: `<folder>/Nodes/<stem>.md`. */
|
|
109
|
+
private notePath;
|
|
110
|
+
/** Pre-`Nodes/` location of a variable note, flat at the model root. */
|
|
111
|
+
private legacyNotePath;
|
|
112
|
+
private readNote;
|
|
113
|
+
private writeNote;
|
|
114
|
+
private readManifest;
|
|
115
|
+
private writeManifest;
|
|
116
|
+
/** Bump `model.json` modified after a content change. */
|
|
117
|
+
private touchManifest;
|
|
118
|
+
/**
|
|
119
|
+
* Variable-note id → vault path, gathered from the model's `Nodes/` subfolder
|
|
120
|
+
* and any legacy flat notes at the model root. A `Nodes/` note wins on a
|
|
121
|
+
* duplicate id (mirrors the Dart reader). `model.json` and the special notes
|
|
122
|
+
* (System/Futures/CLA) are excluded.
|
|
123
|
+
*/
|
|
124
|
+
private noteFilesById;
|
|
125
|
+
private listNoteFiles;
|
|
126
|
+
private loadNotes;
|
|
127
|
+
private loopsDir;
|
|
128
|
+
/** Sorted `.md` basenames in `Loops/`; tolerant of a missing folder. */
|
|
129
|
+
private listLoopNoteFiles;
|
|
130
|
+
private readLoopNoteFile;
|
|
131
|
+
private writeLoopNoteFile;
|
|
132
|
+
private uniqueLoopFilename;
|
|
133
|
+
/**
|
|
134
|
+
* One-way, idempotent migration of legacy loop annotations from the
|
|
135
|
+
* `model.json` maps into `Loops/*.md` files. Guard: if any `Loops/` file
|
|
136
|
+
* exists the model is treated as migrated and this returns immediately. Each
|
|
137
|
+
* legacy key is matched to a live loop by its `loopKey`; a matched key keeps
|
|
138
|
+
* the loop's canonical member ids, an unmatched key becomes a pre-flagged
|
|
139
|
+
* orphan (`members: []`, original key kept in `loop:`) so nothing is lost.
|
|
140
|
+
* After writing, the four legacy keys are stripped from the manifest.
|
|
141
|
+
*/
|
|
142
|
+
private migrateLoopNotesIfNeeded;
|
|
143
|
+
/** Find the live loop whose `loopKey` == `key`, then upsert its file. No-op
|
|
144
|
+
* (and no file) when the graph carries no such loop. */
|
|
145
|
+
private upsertLoopFileByKey;
|
|
146
|
+
/** Find-or-create the identity-matched `Loops/` file and apply `fields`
|
|
147
|
+
* (undefined leaves a field unchanged; `archetype: ""` clears it). Returns the
|
|
148
|
+
* filename written. Title/valence/extra of an existing file are preserved. */
|
|
149
|
+
private writeLoopFile;
|
|
150
|
+
/** Recursively find folders containing a `model.json` (dot-folders pruned). */
|
|
151
|
+
private findModelFolders;
|
|
152
|
+
/**
|
|
153
|
+
* Re-key a model in place: assign a fresh model id, fresh variable ids, and
|
|
154
|
+
* re-point every internal reference (links, loop-note members, the System
|
|
155
|
+
* note's identity anchor). Structure, prose, layout, `shared` keys, and
|
|
156
|
+
* subsystem wikilinks are untouched. Returns the new model id. Used by both
|
|
157
|
+
* {@link duplicateModel} (on a fresh copy) and {@link healDuplicateIds} (on a
|
|
158
|
+
* collided clone).
|
|
159
|
+
*/
|
|
160
|
+
private rekeyModel;
|
|
161
|
+
/** Recursively copy a folder tree byte-for-byte (the source is untouched). */
|
|
162
|
+
private copyTree;
|
|
163
|
+
/** Remove every variable note (flat root + `Nodes/`); keep the special notes. */
|
|
164
|
+
private removeAllVariableFiles;
|
|
165
|
+
/** Rewrite `System.md`'s `model:` identity anchor to `newId` (if it exists). */
|
|
166
|
+
private swapSystemModelId;
|
|
167
|
+
/** "<base> (copy)" — bumping "(copy 2)…" against existing model titles. */
|
|
168
|
+
private uniqueModelName;
|
|
169
|
+
/** A free folder `<parent>/<leaf>` (suffixing `-2/-3…` like createModel). */
|
|
170
|
+
private uniqueSiblingFolder;
|
|
171
|
+
}
|
|
172
|
+
/** Re-exported from `./loopKey` so existing importers keep their path. */
|
|
173
|
+
export { loopKey } from "./loopKey";
|
|
174
|
+
/** Re-export so callers don't need a second import for the signature helper. */
|
|
175
|
+
export { contentSignature };
|