umtri-mcp 1.0.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 +201 -0
- package/README.md +88 -0
- package/core/api-client.js +54 -0
- package/core/policy.js +376 -0
- package/core/resources/plan.md +49 -0
- package/core/resources/seasons-human-only.md +33 -0
- package/core/resources/system-structure.md +229 -0
- package/core/resources/transplant.md +63 -0
- package/core/resources/vision.md +46 -0
- package/core/resources/vocabulary-detailed.md +278 -0
- package/core/resources/vocabulary.md +180 -0
- package/core/resources.js +74 -0
- package/core/tools.js +950 -0
- package/package.json +51 -0
- package/server.js +54 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Rule: Nodes Represent Information Structures, Not Code Files
|
|
2
|
+
|
|
3
|
+
When modelling a project in Umtri, nodes should be **information units** —
|
|
4
|
+
the parts a *user or external tool* can perceive — not implementation files.
|
|
5
|
+
|
|
6
|
+
## The distinction
|
|
7
|
+
|
|
8
|
+
- ❌ **Wrong (developer view)**: nodes named after code files
|
|
9
|
+
— `index.jsx`, `NodeLayer.jsx`, `routes/projects.js`.
|
|
10
|
+
- ✅ **Right (user view)**: nodes named after the information unit
|
|
11
|
+
— "tree diagram", "header", "node table", "projects API".
|
|
12
|
+
|
|
13
|
+
A single information unit may be implemented by several files. The mapping
|
|
14
|
+
from concept to code lives in `metadata.implements` (array of file paths),
|
|
15
|
+
not in the node label.
|
|
16
|
+
|
|
17
|
+
## Completeness — map the whole project, don't summarise
|
|
18
|
+
|
|
19
|
+
The "information unit, not file" rule is about **labelling grain, not
|
|
20
|
+
coverage.** It tells you *how to name and group* nodes — it does **not** tell
|
|
21
|
+
you to leave parts of the project out. The goal is a **faithful, navigable map
|
|
22
|
+
of the entire codebase**: every meaningful module, screen, endpoint, table,
|
|
23
|
+
external integration, and piece of non-obvious logic should be locatable in the
|
|
24
|
+
tree (a node with `metadata.implements`).
|
|
25
|
+
|
|
26
|
+
- **Cover everything, at the right grain.** Group files into information units
|
|
27
|
+
so it's not one node per file — but do not drop whole capabilities. If a
|
|
28
|
+
maintainer would expect to find feature X in the repo, X must be findable in
|
|
29
|
+
the tree.
|
|
30
|
+
- **Don't abstract a project down to a sketch.** A handful of trunks with a few
|
|
31
|
+
leaves each is almost always *under*-captured. Real projects have many leaves;
|
|
32
|
+
a faithful tree is large. Breadth is expected — `get_graph` slicing (rootId /
|
|
33
|
+
maxType / descriptions) exists precisely so a big, complete tree stays cheap
|
|
34
|
+
to read.
|
|
35
|
+
- **The restraint rules are not licence to omit.** "Reserve veins" (vocabulary)
|
|
36
|
+
means don't turn *every function* into a vein — not skip the logic worth
|
|
37
|
+
finding. "Bushy is fine, don't over-nest" means don't add empty grouping
|
|
38
|
+
layers — not prune real branches. Group and pick good grain; keep the coverage.
|
|
39
|
+
- **Completeness pass.** After modelling, walk the repo and check: is there a
|
|
40
|
+
directory, module, route file, table, or integration with **no** node? If so,
|
|
41
|
+
it's a gap — add it. Aim for "every real part of the system is represented,"
|
|
42
|
+
not "the gist is represented."
|
|
43
|
+
- **Completeness has a boundary.** It covers what *runs*, not everything in the
|
|
44
|
+
repo — documents, build output and inert config stay out. See "What is NOT a
|
|
45
|
+
node" below before treating an uncovered path as a gap.
|
|
46
|
+
|
|
47
|
+
## What is NOT a node — the boundary of completeness
|
|
48
|
+
|
|
49
|
+
Completeness pushes you to cover everything that *runs*. It is not a licence to
|
|
50
|
+
map everything that *exists in the repo*. The tree answers "what is this system
|
|
51
|
+
made of," so anything that only **describes** the system stays out.
|
|
52
|
+
|
|
53
|
+
**The test: if you deleted it, would the system behave differently?** If no, it
|
|
54
|
+
is not a node.
|
|
55
|
+
|
|
56
|
+
Keep out:
|
|
57
|
+
|
|
58
|
+
- **Internal documents** — PRDs, RFCs/ADRs, READMEs, CHANGELOGs, design notes,
|
|
59
|
+
roadmaps. These are *about* the system. Mixing them in makes the map answer
|
|
60
|
+
two questions at once ("what runs" and "what we wrote down") and it stops
|
|
61
|
+
being a reliable map of either.
|
|
62
|
+
- **Build output and vendored code** — `dist/`, `build/`, lockfiles,
|
|
63
|
+
`node_modules/`.
|
|
64
|
+
- **Tooling config with no runtime behaviour** — linter/formatter configs,
|
|
65
|
+
editor settings, `.gitignore`.
|
|
66
|
+
|
|
67
|
+
Point at documents instead of turning them into nodes: put the reference in the
|
|
68
|
+
`description` of the node it explains (e.g. "propagation rules:
|
|
69
|
+
`PRD-impact-analysis.md`"). The document stays findable exactly where someone
|
|
70
|
+
would look for it, without becoming a branch.
|
|
71
|
+
|
|
72
|
+
**The exception that matters: documentation you ship is a system.** A docs site
|
|
73
|
+
served to users (its pages, its i18n, its shell) is a product surface and
|
|
74
|
+
belongs in the tree. The line is not "markdown vs code" — it is *served to
|
|
75
|
+
users* vs *read by us*.
|
|
76
|
+
|
|
77
|
+
## Trunk and limb rules
|
|
78
|
+
|
|
79
|
+
- `trunk` = a big domain (e.g. `www`, `server`, `db`). Pick stable,
|
|
80
|
+
long-lived names — usually matching a top-level code area.
|
|
81
|
+
- `limb` = a category inside a trunk (e.g. `graph`, `table`, `dashboard`).
|
|
82
|
+
- Do **not** create one limb per time period. Time is expressed through
|
|
83
|
+
`season` and `created_at`, not by carving the tree into temporal slices.
|
|
84
|
+
- Infrastructure (servers, DNS) is *not* a trunk — it is the environment the
|
|
85
|
+
system runs in, not a part of the system. For documents, see "What is NOT a
|
|
86
|
+
node" above.
|
|
87
|
+
|
|
88
|
+
## Useful metadata keys
|
|
89
|
+
|
|
90
|
+
These conventions let other tools (including this MCP) answer richer
|
|
91
|
+
questions about a ground.
|
|
92
|
+
|
|
93
|
+
| Key | Applies to | Shape |
|
|
94
|
+
| -------------------- | ----------------------------------- | -------------------------------------------------- |
|
|
95
|
+
| `implements` | all nodes | `string[]` — file paths, optionally `path#identifier` |
|
|
96
|
+
| `created_by_commit` | all nodes | `string` — git short hash |
|
|
97
|
+
| `replaces` | follow-up node | `string[]` — node ids that were absorbed |
|
|
98
|
+
| `replaced_by` | deprecated node | `string[]` — node ids that took over |
|
|
99
|
+
| `endpoints` | API-domain twigs | `{method, path, summary}[]` |
|
|
100
|
+
| `columns` | DB-domain twigs | `{name, type, pk?, fk?}[]` |
|
|
101
|
+
| `represents` | UI twigs reflecting data | `string` — corresponding DB twig id |
|
|
102
|
+
| `reparented_at` | reparented nodes | `string` — ISO date when the parent changed |
|
|
103
|
+
| `reparented_from` | reparented nodes | `string` — prior parent id |
|
|
104
|
+
| `reparented_to` | reparented nodes | `string` — new parent id |
|
|
105
|
+
| `extracted_from` | nodes decomposed from a parent file | `string` — original node id |
|
|
106
|
+
| `role_change_at` | type-changed nodes | `string` — ISO date |
|
|
107
|
+
| `role_change_reason` | type-changed nodes | `string` — short prose explaining the shift |
|
|
108
|
+
| `placeholder` | intentionally-empty structure | `true` — opts out of dormant cleanup suggestions |
|
|
109
|
+
|
|
110
|
+
## One file, multiple nodes
|
|
111
|
+
|
|
112
|
+
A single source file may legitimately decompose into multiple nodes when it
|
|
113
|
+
contains several distinct information units. Example: `AppShell.jsx` exports
|
|
114
|
+
`Sidebar`, `NavItem`, `UserMenu`, `Intro`, `MobileDrawer` — each is its own
|
|
115
|
+
component and earns its own `leaf` node. The split uses two metadata keys:
|
|
116
|
+
|
|
117
|
+
- `metadata.implements`: array of `file#identifier` references — e.g.
|
|
118
|
+
`"www/src/components/layout/AppShell.jsx#Sidebar"`. The `#identifier`
|
|
119
|
+
suffix points at the exported function/class within the file.
|
|
120
|
+
- `metadata.extracted_from`: id of the node that originally represented the
|
|
121
|
+
whole file. Preserves the decomposition history so the prior shape stays
|
|
122
|
+
inspectable.
|
|
123
|
+
|
|
124
|
+
Decompose when the units are independently reusable, separately addressable,
|
|
125
|
+
or named distinctly by users. Do **not** decompose just because a file has
|
|
126
|
+
many internal helpers — pure implementation details stay inside the leaf.
|
|
127
|
+
|
|
128
|
+
## Restructuring history
|
|
129
|
+
|
|
130
|
+
Trees evolve. When a node moves to a different parent, record the move in
|
|
131
|
+
metadata so the restructure stays inspectable later:
|
|
132
|
+
|
|
133
|
+
- `metadata.reparented_at` — ISO date (e.g. `"2026-05-21"`)
|
|
134
|
+
- `metadata.reparented_from` — prior parent id
|
|
135
|
+
- `metadata.reparented_to` — new parent id
|
|
136
|
+
|
|
137
|
+
The MCP `update_node` tool emits a `reparent-metadata-hint` info warning
|
|
138
|
+
when you change `parent` without including these keys. Recording the move
|
|
139
|
+
matters because the ltree path silently rewrites on parent change — without
|
|
140
|
+
these keys, you can't tell from the current state that the node ever moved.
|
|
141
|
+
|
|
142
|
+
When the type also changes (twig → leaf, limb → twig, …), add
|
|
143
|
+
`role_change_at` and a short `role_change_reason` so the reclassification
|
|
144
|
+
stays explainable. Pair with `replaces` / `replaced_by` when one node
|
|
145
|
+
absorbs another (e.g. an ad-slot twig absorbing its sole leaf child).
|
|
146
|
+
|
|
147
|
+
## When adding a node
|
|
148
|
+
|
|
149
|
+
Ask first: *"What part of the system does this represent for someone using
|
|
150
|
+
or maintaining it?"* If the answer is a file, you're probably one level too
|
|
151
|
+
deep — go up to the information unit and add the file under
|
|
152
|
+
`metadata.implements`. If the answer is a single exported component within
|
|
153
|
+
a larger file, the node is still legitimate — use `path#identifier` in
|
|
154
|
+
`implements` and add `extracted_from` pointing at the file-level node.
|
|
155
|
+
|
|
156
|
+
## Connections — consider an edge or api each time you add a node
|
|
157
|
+
|
|
158
|
+
The tree shows *containment* (what lives under what). It does **not** show how
|
|
159
|
+
things relate across branches — that's what edges and apis are for. After you
|
|
160
|
+
add a leaf or vein, pause and ask: **does this node call, feed, or depend on
|
|
161
|
+
another node?** If so, record the connection — the graph is only half-true
|
|
162
|
+
without it.
|
|
163
|
+
|
|
164
|
+
- **api** (`create_api`) — a **request / call / data flow** between two nodes:
|
|
165
|
+
a screen calling an endpoint, an endpoint hitting a table, an external
|
|
166
|
+
integration (NicePay, SMS, a webhook). First-class because flows are the
|
|
167
|
+
story of how the system runs. Direction is caller → callee (`start` → `end`).
|
|
168
|
+
- **edge** (`create_edge`) — a **structural relation**: `dependency` ("A is
|
|
169
|
+
built on / needs B") or `data_flow` ("data moves A → B" outside a request).
|
|
170
|
+
Use for module/library dependencies, a route depending on the data store, a
|
|
171
|
+
job writing to a table. Direction is `source` → `target`.
|
|
172
|
+
|
|
173
|
+
Rule of thumb: if it's a *runtime request*, it's an api; if it's a *build-time
|
|
174
|
+
or structural reliance*, it's a dependency edge. Don't connect everything —
|
|
175
|
+
add the connections that a maintainer would actually want to trace (cross-module
|
|
176
|
+
calls, external integrations, the few key dependencies). A standalone leaf with
|
|
177
|
+
no real relation needs no edge; skipping is fine. But the common failure is the
|
|
178
|
+
opposite — adding nodes and never their connections, leaving a tree of isolated
|
|
179
|
+
islands.
|
|
180
|
+
|
|
181
|
+
### How problems propagate along a connection
|
|
182
|
+
|
|
183
|
+
The arrow records *who calls / depends on whom*. **Impact — which node is hurt
|
|
184
|
+
when another breaks — does not always flow with the arrow.** Read each
|
|
185
|
+
connection type as follows when tracing a blast radius:
|
|
186
|
+
|
|
187
|
+
| Connection | Arrow means | A problem propagates |
|
|
188
|
+
| ----------------- | ------------------------------- | ------------------------------------------------------- |
|
|
189
|
+
| edge `dependency` | `source` is built on / needs `target` | **against the arrow** — if `target` breaks, `source` is affected (target → source) |
|
|
190
|
+
| edge `data_flow` | data moves `source` → `target` | **with the arrow** — bad data at `source` corrupts `target` (source → target) |
|
|
191
|
+
| api (`start`→`end`) | caller → callee | **against the arrow** — if the callee (`end`) is down, the caller (`start`) fails (end → start) |
|
|
192
|
+
|
|
193
|
+
So "what is affected if X breaks?" = walk **dependency edges backward** (to their
|
|
194
|
+
sources) ∪ **data_flow edges forward** (to their targets) ∪ **apis backward**
|
|
195
|
+
(to their callers), transitively. This is the rule the `get_impact` tool
|
|
196
|
+
implements.
|
|
197
|
+
|
|
198
|
+
Two cautions:
|
|
199
|
+
- The result is a **list of nodes to check, not a proven failure set** — it is
|
|
200
|
+
only as complete as the connections you have recorded. A sparse graph yields
|
|
201
|
+
a falsely small blast radius; treat an empty result as "nothing recorded,"
|
|
202
|
+
not "nothing affected."
|
|
203
|
+
- A connection can carry impact **both ways** in reality (a dependency that also
|
|
204
|
+
passes data back). Record the dominant direction; when in doubt, trace impact
|
|
205
|
+
in both directions rather than trusting a single arrow.
|
|
206
|
+
|
|
207
|
+
## The file-as-leaf antipattern
|
|
208
|
+
|
|
209
|
+
A common drift: a twig groups a concept (e.g. `Bug log API`), and its only
|
|
210
|
+
child is a leaf named after the implementation file (`bugs.js`). This is
|
|
211
|
+
**one information unit split into two nodes** — the twig and the leaf
|
|
212
|
+
describe the same thing at different vocabularies.
|
|
213
|
+
|
|
214
|
+
Symptoms:
|
|
215
|
+
- Twig with exactly one leaf child whose label is a filename.
|
|
216
|
+
- Leaf label echoes the twig label plus a file extension.
|
|
217
|
+
- The leaf carries no additional information the twig doesn't already have.
|
|
218
|
+
|
|
219
|
+
Fix: absorb the file into the twig's `metadata.implements`, then soft-delete
|
|
220
|
+
the leaf. The twig itself becomes the information unit; the file is its
|
|
221
|
+
implementation detail, not a separate node. Decompose into multiple leaves
|
|
222
|
+
*only* when the file contains several independently-meaningful units (see
|
|
223
|
+
"One file, multiple nodes" above).
|
|
224
|
+
|
|
225
|
+
The MCP `create_node` / `update_node` tools emit a `file-as-leaf` info
|
|
226
|
+
warning when a leaf is created with a filename- or path-shaped label, and
|
|
227
|
+
an `implementation-jargon` warning when any label uses code-architecture
|
|
228
|
+
words (router/handler/middleware/layer/store/...) — both push toward
|
|
229
|
+
naming the information unit rather than the implementation.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Rule: Transplanting a ground (옮겨심기)
|
|
2
|
+
|
|
3
|
+
A ground that was **not** born in Umtri — an existing project imported for the
|
|
4
|
+
first time — needs heavy restructuring before its shape settles. To allow
|
|
5
|
+
that, a freshly created ground starts in a **transplanting** state, where the
|
|
6
|
+
normal guardrails are relaxed. When the human is satisfied with the shape they
|
|
7
|
+
**root** the ground (뿌리내리기), and the guardrails snap back on.
|
|
8
|
+
|
|
9
|
+
`get_graph` returns `project.transplanting` (boolean). Check it before doing
|
|
10
|
+
anything that the normal rules would block.
|
|
11
|
+
|
|
12
|
+
## While `transplanting = true`
|
|
13
|
+
|
|
14
|
+
You MAY:
|
|
15
|
+
- **Add or edit nodes in any season, including past seasons.** The usual
|
|
16
|
+
"cannot create/move into a past season" and "grown (past-season) nodes are
|
|
17
|
+
immutable" rules are lifted, so you can reconstruct the project's history
|
|
18
|
+
season by season.
|
|
19
|
+
- **Hard-delete import mistakes.** `delete_node` accepts `hard: true` and the
|
|
20
|
+
active-descendant guard is lifted (subtree cascades). Use this to remove
|
|
21
|
+
wrongly-imported nodes for good. Soft delete is still the default; reach for
|
|
22
|
+
`hard` only to clean up genuine mistakes — it is irreversible.
|
|
23
|
+
|
|
24
|
+
You MAY NOT:
|
|
25
|
+
- **Create or modify seasons.** Seasons are always human-only
|
|
26
|
+
(see umtri://rules/seasons-human-only). The human lays down the historical
|
|
27
|
+
seasons; you fill nodes into them.
|
|
28
|
+
- **Root (settle) a ground.** Ending transplant is human-only — done from the
|
|
29
|
+
UI ("Root this ground"). You cannot root via MCP.
|
|
30
|
+
|
|
31
|
+
## Re-opening transplant (`reopen_transplant`)
|
|
32
|
+
|
|
33
|
+
A rooted ground can be put back into transplant with the `reopen_transplant`
|
|
34
|
+
tool — but **only when the user explicitly asks for it** (e.g. "switch this
|
|
35
|
+
ground to transplanting", "옮겨심기로 바꿔줘"). This removes safety guardrails,
|
|
36
|
+
so you must **never** decide to re-open on your own initiative, however
|
|
37
|
+
convenient it would be for a restructuring task. No explicit request → do not
|
|
38
|
+
call it; instead tell the user the ground is rooted and ask whether they want
|
|
39
|
+
to re-open transplant. After re-opening, the user roots it again from the UI
|
|
40
|
+
when the work is done.
|
|
41
|
+
|
|
42
|
+
## Audit stamp
|
|
43
|
+
|
|
44
|
+
Every node you create while the ground is transplanting is automatically
|
|
45
|
+
stamped `metadata.transplanted = true`. The stamp persists after rooting so a
|
|
46
|
+
human can later see which nodes entered during the unsettled period. Do not
|
|
47
|
+
remove it.
|
|
48
|
+
|
|
49
|
+
## After rooting (`transplanting = false`)
|
|
50
|
+
|
|
51
|
+
All normal guards return: past seasons reject new/moved nodes, grown nodes are
|
|
52
|
+
immutable, `delete_node` is soft-only and refuses nodes with active children.
|
|
53
|
+
Treat the tree as settled — small, observable changes only
|
|
54
|
+
(see umtri://about/vision).
|
|
55
|
+
|
|
56
|
+
## How to behave
|
|
57
|
+
|
|
58
|
+
- Use the relaxed window to get the **structure** right — it is still an
|
|
59
|
+
information-structure diagram, not a work log (see umtri://rules/vocabulary).
|
|
60
|
+
Reconstruct what the project *is*, not the history of commits.
|
|
61
|
+
- Prefer soft delete; reserve `hard` for clear mistakes.
|
|
62
|
+
- When the shape looks settled, tell the human it's ready to root — don't keep
|
|
63
|
+
operating in transplant mode longer than needed.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# About Umtri
|
|
2
|
+
|
|
3
|
+
Umtri is a platform for visualising **how a project grows over time**.
|
|
4
|
+
A project is a *ground*; from it rise *trunks* (apps, services); seasons
|
|
5
|
+
pass; branches and leaves accumulate; bugs erode; APIs connect.
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
Complex systems forget their own past. As code and decisions pile up, the
|
|
10
|
+
question *"why is it like this?"* becomes harder to answer than *"how does
|
|
11
|
+
it work?"*. Umtri is a project archaeology tool: a place to trace and
|
|
12
|
+
back-trace structure, dependencies, and decisions across the lifetime of a
|
|
13
|
+
project.
|
|
14
|
+
|
|
15
|
+
## Primary purposes
|
|
16
|
+
|
|
17
|
+
- **Track growth** — see how a project's structure expanded season by season.
|
|
18
|
+
- **Trace dependencies** — follow API flows and references across the tree.
|
|
19
|
+
- **Locate origins** — when something is broken or surprising, find the
|
|
20
|
+
point in time and space where it entered the system.
|
|
21
|
+
- **Open to tools** — Umtri exposes its data through a REST API and this
|
|
22
|
+
MCP server, so AI assistants, CI pipelines, issue trackers, and IDEs can
|
|
23
|
+
read and contribute alongside humans.
|
|
24
|
+
|
|
25
|
+
## Tone for tools
|
|
26
|
+
|
|
27
|
+
When you (an AI tool) work on an Umtri ground:
|
|
28
|
+
|
|
29
|
+
- Prefer the language of the metaphor over generic terms — *grounds, trunks,
|
|
30
|
+
limbs, twigs, leaves, veins, seasons, bugs*. It is intentional.
|
|
31
|
+
- Treat the project as alive. Names, season transitions, and bugs carry
|
|
32
|
+
meaning the user assigned; do not "tidy up" without asking.
|
|
33
|
+
- Default to small, observable changes. The point of Umtri is to make the
|
|
34
|
+
history of a project legible — silent bulk edits defeat that.
|
|
35
|
+
|
|
36
|
+
## What this MCP is not
|
|
37
|
+
|
|
38
|
+
- It is not a code-editing tool. It does not read or write source files —
|
|
39
|
+
an external agent does that, using the plan tree as its brief
|
|
40
|
+
(see `umtri://rules/plan`).
|
|
41
|
+
- It does not run CI/CD jobs. Instead it is the **configuration-record hub**
|
|
42
|
+
that CI integrates with: commits are recorded back onto the nodes they
|
|
43
|
+
touched (`metadata.implements`, `metadata.commits`) via the API — e.g. a
|
|
44
|
+
GitHub Action calling `record_commit` on merge. Umtri stores the form; it
|
|
45
|
+
does not execute the pipeline.
|
|
46
|
+
- It does not create seasons (see `umtri://rules/seasons-human-only`).
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# Umtri Vocabulary — Detailed Guidance
|
|
2
|
+
|
|
3
|
+
Read this only when you are restructuring an existing tree, auditing legacy
|
|
4
|
+
data, or wrestling with a borderline classification. The core rules live in
|
|
5
|
+
`umtri://rules/vocabulary`; this resource expands them.
|
|
6
|
+
|
|
7
|
+
## Hierarchy rules (convention, not schema-enforced)
|
|
8
|
+
|
|
9
|
+
| Parent role | Allowed child role(s) | Note |
|
|
10
|
+
| ----------- | ----------------------------- | ---------------------------------------------------------- |
|
|
11
|
+
| structure (trunk/limb/twig) | structure / object | can hold each other or jump straight to a `leaf` |
|
|
12
|
+
| object (`leaf`) | action (`vein`) only | a leaf cannot branch back into structure |
|
|
13
|
+
| action (`vein`) | — | **vein is terminal — no children** |
|
|
14
|
+
|
|
15
|
+
The schema accepts any combination. The MCP warns when violated but lets
|
|
16
|
+
the data through (legacy migration, experimental use). For new authoring,
|
|
17
|
+
follow the table strictly.
|
|
18
|
+
|
|
19
|
+
**Skip-level traversal is allowed within structure**: `trunk → leaf`,
|
|
20
|
+
`limb → leaf`, even `trunk → twig` are valid. The full descent
|
|
21
|
+
`trunk → limb → twig → leaf → vein` is canonical, not mandatory.
|
|
22
|
+
|
|
23
|
+
## Trunk = system-unit, not domain
|
|
24
|
+
|
|
25
|
+
A `trunk` must be a **deployable system or independent service** — something
|
|
26
|
+
you could point at, deploy, or replace as a whole. Apps, databases, servers,
|
|
27
|
+
external services. Examples that fit: `user-app`, `api-server`, `iot-server`,
|
|
28
|
+
`db`, `www`, `kiosk-app`.
|
|
29
|
+
|
|
30
|
+
Domains or feature areas (auth, payments, reservations, dashboard, …) are
|
|
31
|
+
**`limb`**, not trunk. They live inside a trunk. "Authentication is a domain
|
|
32
|
+
that spans the user-app and admin trunks" — fine. "Authentication is a
|
|
33
|
+
trunk" — no.
|
|
34
|
+
|
|
35
|
+
Naming convention (recommended, not enforced):
|
|
36
|
+
- Apps: `<name>-app` (e.g. `user-app`, `kiosk-app`)
|
|
37
|
+
- Servers: `<name>-server` (e.g. `api-server`, `iot-server`)
|
|
38
|
+
- Infra: short single words (`db`, `www`, `cdn`)
|
|
39
|
+
|
|
40
|
+
## Leaf vs vein — extended heuristics
|
|
41
|
+
|
|
42
|
+
`leaf` (object) and `vein` (action) look adjacent but carry opposite intents:
|
|
43
|
+
|
|
44
|
+
- **leaf** — the thing that exists. A component, class, domain entity,
|
|
45
|
+
screen, card. Noun phrase.
|
|
46
|
+
- **vein** — what the object *does*: a method, function, or operation that
|
|
47
|
+
exists on it. Verb phrase naming a standing **capability** — not a task,
|
|
48
|
+
patch, migration step, or change that was performed. A vein is `환불 처리`
|
|
49
|
+
("processes refunds"), never `환불 버그 수정` (a completed task).
|
|
50
|
+
|
|
51
|
+
Korean label patterns:
|
|
52
|
+
|
|
53
|
+
| Pattern | Type | Example |
|
|
54
|
+
| -------------------------------------------------------------------- | ---- | -------------------------------------------------------- |
|
|
55
|
+
| Noun-final / domain model | leaf | `예약 카드`, `결제 정책`, `사용자 메뉴` |
|
|
56
|
+
| UI components (모달·카드·페이지·대시보드·패널·화면·표·뷰) | leaf | `랜딩 페이지`, `매출 대시보드`, `공지 이미지 리사이저` |
|
|
57
|
+
| Verb-final capability (`…충전`·`…차감`·`…환불`·`…조회`·`…발송`·`…등록`·`…검증`·`…동기화`·`…집계`) | vein | `포인트 충전`, `세션 검증`, `방문 집계` |
|
|
58
|
+
| Handlers / methods / crons / jobs (standing capabilities) | vein | `레슨 자동 완료 cron`, `방문 집계 핑` |
|
|
59
|
+
|
|
60
|
+
Note the boundary with the work-log rule: a vein names an operation the
|
|
61
|
+
object *performs* (a capability that stays true tomorrow), not the act of
|
|
62
|
+
fixing/adding/migrating it. `정적 파일 캐시 버스팅` (a capability the build
|
|
63
|
+
has) is a vein; `캐시 버스터 추가` (work done) is not a node at all.
|
|
64
|
+
|
|
65
|
+
**Vein litmus — one vein ≈ one locatable code unit.** A vein earns its place
|
|
66
|
+
when you can point to where it lives (a function, guard, calculation, cron) —
|
|
67
|
+
it should carry `metadata.implements`. Reserve veins for the *non-obvious,
|
|
68
|
+
worth-finding-again* logic: business invariants, security, money math
|
|
69
|
+
(`코트·시간대 중복 예약 방지`, `보너스 포인트 FIFO 차감`, `휴대폰 AES-256-GCM
|
|
70
|
+
암호화`). Do **not** explode every endpoint into CRUD veins, and do **not**
|
|
71
|
+
turn a requirement or rule you can't point to in code into a vein — that
|
|
72
|
+
belongs in the leaf's `description`, or as a `bug`/plan node. A good tree uses
|
|
73
|
+
veins sparingly (often <10% of nodes); a forest of veins under every leaf is
|
|
74
|
+
the over-veining smell.
|
|
75
|
+
|
|
76
|
+
## Reclassification policy (for AI auditors)
|
|
77
|
+
|
|
78
|
+
When migrating or auditing legacy data, AI tools **may reclassify** a `leaf`
|
|
79
|
+
node to `vein` (or vice-versa) **only if both** hold:
|
|
80
|
+
|
|
81
|
+
1. The label clearly fits the opposite category by the heuristics above.
|
|
82
|
+
2. The node has **no children** (vein cannot have children).
|
|
83
|
+
|
|
84
|
+
Always make reclassification edits **one node at a time**, with the label
|
|
85
|
+
visible in the diff. Bulk regex sweeps over labels are explicitly disallowed
|
|
86
|
+
— the Korean heuristics are advisory, not deterministic. When unsure, leave
|
|
87
|
+
the node alone and surface it as a question.
|
|
88
|
+
|
|
89
|
+
## Build order — leaf-driven, depth-first
|
|
90
|
+
|
|
91
|
+
The canonical descent `trunk → limb → twig → leaf → vein` describes the
|
|
92
|
+
*shape* of a finished tree. It is **not** an instruction to build the tree
|
|
93
|
+
breadth-first (all trunks, then all limbs, then all leaves). Building that
|
|
94
|
+
way produces a skeleton of empty structure and forces you to guess the IA
|
|
95
|
+
top-down before you know what actually lives in it.
|
|
96
|
+
|
|
97
|
+
**Author one leaf at a time, depth-first.** The leaf (or vein) is the only
|
|
98
|
+
node that names a thing that genuinely exists; trunk/limb/twig exist solely
|
|
99
|
+
to hold leaves. So let the leaf pull its ancestors into being:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
for each information unit you want to record (a leaf or vein):
|
|
103
|
+
path = [trunk, limb, twig?] it belongs under
|
|
104
|
+
create_node for each ancestor in path that does NOT exist yet (reuse the rest)
|
|
105
|
+
create_node for the leaf, with metadata.implements
|
|
106
|
+
create_node for each vein the leaf owns
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Worked example — recording two screens in a `user-app`:
|
|
110
|
+
|
|
111
|
+
1. First leaf `예약 카드` lives under `user-app › 예약 › 예약 화면`.
|
|
112
|
+
- `user-app` (trunk) missing → create it.
|
|
113
|
+
- `예약` (limb) missing → create it.
|
|
114
|
+
- `예약 화면` (twig) missing → create it.
|
|
115
|
+
- create leaf `예약 카드`.
|
|
116
|
+
2. Next leaf `결제 버튼` lives under `user-app › 결제 › 결제 화면`.
|
|
117
|
+
- `user-app` already exists → reuse.
|
|
118
|
+
- `결제` (limb) missing → create it.
|
|
119
|
+
- `결제 화면` (twig) missing → create it.
|
|
120
|
+
- create leaf `결제 버튼`.
|
|
121
|
+
|
|
122
|
+
Each new leaf reuses the ancestors earlier leaves already built and only
|
|
123
|
+
creates structure where the path diverges.
|
|
124
|
+
|
|
125
|
+
**Why depth-first by leaf beats breadth-first by layer:**
|
|
126
|
+
|
|
127
|
+
- **No dormant scaffolding.** Every trunk/limb/twig gets its first leaf in
|
|
128
|
+
the same pass, so the `structure-without-children` warning fires only for
|
|
129
|
+
genuinely-intended placeholders — not for half-built layers.
|
|
130
|
+
- **Structure is earned, not guessed.** You decide a `limb` exists *because*
|
|
131
|
+
a concrete leaf needs a home, not by sketching an org chart first. This is
|
|
132
|
+
also when the twig-vs-skip-to-leaf decision (next section) is actually
|
|
133
|
+
answerable — you can see how many siblings the concept really has.
|
|
134
|
+
- **Recoverable if interrupted.** Stop after any leaf and the tree is a set
|
|
135
|
+
of complete branches, not a headless skeleton.
|
|
136
|
+
|
|
137
|
+
A trunk or limb with no leaf descendant *during authoring* is the signal
|
|
138
|
+
you've slipped back into layer-by-layer mode. Add its first leaf before
|
|
139
|
+
moving on, or don't create the structure node yet.
|
|
140
|
+
|
|
141
|
+
## Twig vs jump straight to leaf
|
|
142
|
+
|
|
143
|
+
`twig` is for **information groups** — a cluster of related leaves under a
|
|
144
|
+
shared concept. Use it only when you have (or expect to soon have) multiple
|
|
145
|
+
sibling `leaf` nodes that belong together.
|
|
146
|
+
|
|
147
|
+
If the concept boils down to *one* component, file, or object, **skip twig
|
|
148
|
+
and put the leaf directly under the limb**.
|
|
149
|
+
|
|
150
|
+
| Case | Use |
|
|
151
|
+
| ------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
|
|
152
|
+
| One React component, one file, one service | `limb → leaf` directly. No twig. |
|
|
153
|
+
| Several leaves that share a clear theme (e.g. design system = Button, Modal, Field, …) | `limb → twig → leaf+` |
|
|
154
|
+
| Several components composing one flow (e.g. auth = SignIn, Login, SignUp) | `limb → twig → leaf+` |
|
|
155
|
+
| A single file that decomposes into multiple meaningful units | Multiple leaves with `metadata.implements` carrying `path#identifier` (see system-structure) |
|
|
156
|
+
|
|
157
|
+
A twig with no leaves is dormant. Skipping twig when only one object lives
|
|
158
|
+
underneath keeps the tree readable.
|
|
159
|
+
|
|
160
|
+
The MCP `create_node` tool emits a `structure-without-children` info
|
|
161
|
+
warning when you create a `limb` or `twig`, reminding you that the node
|
|
162
|
+
renders dormant until a leaf/vein appears.
|
|
163
|
+
|
|
164
|
+
## Tree shape — bushy is fine, don't over-nest
|
|
165
|
+
|
|
166
|
+
A healthy Umtri tree is **bushy, not deep**: most nodes sit at the middle
|
|
167
|
+
levels (limbs and their leaves), and the full `trunk → limb → twig → leaf →
|
|
168
|
+
vein` descent is reached only where the detail genuinely warrants it. Depth
|
|
169
|
+
is *earned by content*, not added for tidiness.
|
|
170
|
+
|
|
171
|
+
The biggest pressure toward unnecessary depth is the urge to group siblings
|
|
172
|
+
that merely share a name. **Sharing a label prefix is not, by itself, a
|
|
173
|
+
reason to create a twig.** Korean domain labels naturally share prefixes
|
|
174
|
+
(`예약 생성` / `예약 취소`, `포인트 충전` / `포인트 환불`) — two or three such
|
|
175
|
+
siblings under a limb are perfectly legible as-is. Insert a twig only when a
|
|
176
|
+
parent is genuinely crowded (≈4+ leaves) *and* a real sub-theme groups
|
|
177
|
+
several of them. Wrapping every pair in a twig makes the tree taller and
|
|
178
|
+
harder to read, not easier.
|
|
179
|
+
|
|
180
|
+
`get_graph` (summary) returns a `shape` block — `maxDepth`, nodes per level,
|
|
181
|
+
and `wideBranches` — plus a `childCount` on each branch node. Use it to judge
|
|
182
|
+
the tree before restructuring: mass concentrated at mid-levels with a shallow
|
|
183
|
+
`maxDepth` is the goal, not a smell. Over-wide trunks are usually fine (a
|
|
184
|
+
trunk's children are top-level domains); an over-wide **limb** (many direct
|
|
185
|
+
leaves) is the case the `junk-drawer-limb` / `sibling-cluster` hints flag.
|
|
186
|
+
|
|
187
|
+
## Promotion signals — when a twig should be a limb
|
|
188
|
+
|
|
189
|
+
Some concepts start as twigs nested inside a domain limb but actually
|
|
190
|
+
deserve sibling-limb status. Treat these as **promotion signals** when
|
|
191
|
+
authoring or restructuring:
|
|
192
|
+
|
|
193
|
+
| Signal | Example |
|
|
194
|
+
| ------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
195
|
+
| Route-independent global shell | A sidebar shown on every screen, a global header |
|
|
196
|
+
| Distinct entry flow (splash → continuation) | Login + signup pages reached before the main app |
|
|
197
|
+
| Globally reusable assets | Design tokens + shared components + global toasts |
|
|
198
|
+
| Domain limb growing into a "junk drawer" (>5 unrelated twigs) | A `dashboard` limb absorbing sidebar + auth + UI + pages |
|
|
199
|
+
|
|
200
|
+
When any apply, create the unit as a **limb sibling** of the original
|
|
201
|
+
domain limb rather than burying it as a twig.
|
|
202
|
+
|
|
203
|
+
The MCP `create_node` and `update_node` tools emit a
|
|
204
|
+
`twig-promotion-candidate` info warning when a twig's `description`
|
|
205
|
+
mentions promotion signals (전역, 라우트 비종속, 스플래시, 재사용 자산,
|
|
206
|
+
global, route-independent, splash, …).
|
|
207
|
+
|
|
208
|
+
## Labels as information units (anti-patterns)
|
|
209
|
+
|
|
210
|
+
A label should name the thing a user or external observer perceives, not
|
|
211
|
+
the code construct implementing it.
|
|
212
|
+
|
|
213
|
+
| Anti-pattern label | Problem | Better |
|
|
214
|
+
| --------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------- |
|
|
215
|
+
| `Bugs router`, `Auth middleware`, `Webhook handler` | names the Express artifact | `Bug log API`, `Request authorization`, `Provider webhooks` |
|
|
216
|
+
| `API layer`, `Service tier`, `Data store` | layer/tier/store = architectural plumbing | split by domain, name the surface |
|
|
217
|
+
| `store.js`, `middleware/auth.js`, `index.jsx` | filenames belong in `metadata.implements` | `Persistence`, `Auth guard`, `Graph root` |
|
|
218
|
+
| `Helper`, `Manager`, `Bootstrap`, `Wrapper` | generic dev nouns, no info content | the actual responsibility it carries |
|
|
219
|
+
|
|
220
|
+
The `file-as-leaf` warning also catches the **redundant-child antipattern**:
|
|
221
|
+
a twig whose only child is a leaf named after the same concept's file is
|
|
222
|
+
just one node split into two. Absorb the file into the twig's
|
|
223
|
+
`metadata.implements` and remove the leaf. The twig itself *is* the
|
|
224
|
+
information unit; the file is its implementation.
|
|
225
|
+
|
|
226
|
+
Apply these on existing data only when a node's label clearly violates the
|
|
227
|
+
rule. Coordinated bulk renames go one node at a time with the diff visible.
|
|
228
|
+
|
|
229
|
+
## Dormant cleanup — group by branch
|
|
230
|
+
|
|
231
|
+
When proposing dormant cleanup, group by parent so the user can decide on
|
|
232
|
+
whole branches at once rather than one node at a time. Common reasons a
|
|
233
|
+
node is dormant:
|
|
234
|
+
|
|
235
|
+
- Placeholder for upcoming work (intentional outline)
|
|
236
|
+
- A plan sketched before any code landed
|
|
237
|
+
- A stale outline that was never filled in (cleanup candidate)
|
|
238
|
+
|
|
239
|
+
If a node's season is `past` and the label is vague, the user may choose
|
|
240
|
+
to soft-delete. Nodes with `metadata.placeholder = true` are excluded —
|
|
241
|
+
respect that marker and do not flag them.
|
|
242
|
+
|
|
243
|
+
## Back-filling historical projects (`sproutedAt`)
|
|
244
|
+
|
|
245
|
+
When importing a project that existed **before Umtri** — past commits,
|
|
246
|
+
migrated trees, archaeological work on legacy code — the node's "creation
|
|
247
|
+
moment" is the original event, not the moment the record landed in Umtri.
|
|
248
|
+
|
|
249
|
+
`create_node` (and `update_node`) accept an optional `sproutedAt` field
|
|
250
|
+
(ISO 8601 with offset, e.g. `"2024-03-15T09:00:00Z"`). When set, **the
|
|
251
|
+
visualization timeline follows this value** — sibling order, season
|
|
252
|
+
visibility masking, and the events feed all use `sproutedAt` as the
|
|
253
|
+
effective creation moment. The system audit timestamp (`created_at`,
|
|
254
|
+
which records when the row landed in the database) stays untouched and is
|
|
255
|
+
not surfaced in graph responses.
|
|
256
|
+
|
|
257
|
+
Guidelines:
|
|
258
|
+
|
|
259
|
+
- **Default behavior is correct for live work.** Omit `sproutedAt` for any
|
|
260
|
+
node created in the present — the system timestamp is the truth.
|
|
261
|
+
- **Use only for back-fill.** Importing past commit history, recovering a
|
|
262
|
+
deleted project, migrating from another tool, dogfooding a project's
|
|
263
|
+
history into Umtri. Do not back-date "for clarity" on live work.
|
|
264
|
+
- **Order matters.** When importing many nodes, set `sproutedAt` to their
|
|
265
|
+
real historical timestamps so sibling order and season visibility line
|
|
266
|
+
up. Random or batch-identical timestamps produce a deformed timeline.
|
|
267
|
+
- **Pair with `season`.** Place each back-filled node in the season that
|
|
268
|
+
was active at its `sproutedAt`. Otherwise the slider will hide nodes
|
|
269
|
+
whose season has not yet started at T = `sproutedAt`.
|
|
270
|
+
- **Clearing.** `update_node` with `patch.sproutedAt = null` clears the
|
|
271
|
+
override; the node falls back to its system timestamp.
|
|
272
|
+
- **Cannot back-fill `removed_at` or `grown_at`.** These follow the system
|
|
273
|
+
clock; only the *birth* of a node can be antedated.
|
|
274
|
+
|
|
275
|
+
Past-season nodes are still rejected from new `create_node` calls (the
|
|
276
|
+
"cannot create node in past season" rule). For full back-fill of an
|
|
277
|
+
archived season, set the target season to `now` first, import, then
|
|
278
|
+
human-promote it back to `past` via the seasons UI.
|