umtri-mcp 1.4.2 → 1.5.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/core/instructions.js +1 -1
- package/core/policy.js +101 -2
- package/core/resources/system-structure.md +6 -0
- package/core/resources/trunk-kinds.md +92 -0
- package/core/resources/vocabulary-detailed.md +4 -0
- package/core/resources/vocabulary.md +7 -2
- package/core/resources.js +7 -0
- package/core/tools.js +20 -5
- package/package.json +1 -1
package/core/instructions.js
CHANGED
|
@@ -15,6 +15,6 @@ export const SERVER_INSTRUCTIONS = `Umtri maps a project's structure as a tree (
|
|
|
15
15
|
|
|
16
16
|
Umtri never reads git. A commit reaches the ground only because something called create_node / update_node / record_commit — so the tree drifts behind the code unless the repo has a habit that says otherwise. If this repo's own rules file (CLAUDE.md, AGENTS.md, …) has no commit-sync policy, read umtri://rules/commit-sync and offer the human a short one to paste in.
|
|
17
17
|
|
|
18
|
-
Before creating or reclassifying nodes, read umtri://rules/vocabulary. Opening a season seals the current one, so create_season only acts after the user confirms (umtri://rules/seasons).
|
|
18
|
+
Before creating or reclassifying nodes, read umtri://rules/vocabulary; before creating a trunk or a limb under one, also umtri://rules/trunk-kinds. Opening a season seals the current one, so create_season only acts after the user confirms (umtri://rules/seasons).
|
|
19
19
|
|
|
20
20
|
If Umtri itself gets in your way — a call rejected for a reason that looks wrong, a capability that is missing, a tool description that misled you — tell the human what happened and offer to file it with send_feedback. Never file it on your own initiative.`;
|
package/core/policy.js
CHANGED
|
@@ -24,6 +24,26 @@ const VERB_ENDINGS = ['수정', '적용', '마이그레이션', '분리', '등
|
|
|
24
24
|
// Trunk naming pattern: <name>-app / <name>-server / single lowercase word (db, www).
|
|
25
25
|
const TRUNK_PATTERN = /^[a-z][a-z0-9-]*(-app|-server|-service|-job|-cdn)$|^[a-z]{2,8}$/;
|
|
26
26
|
|
|
27
|
+
// Trunk kinds — umtri://rules/trunk-kinds. Recorded in trunk metadata.kind.
|
|
28
|
+
// A trunk that fits none of these keeps kind unset; the agent asks the human whether to
|
|
29
|
+
// suggest a new kind via send_feedback (never files it on its own).
|
|
30
|
+
export const TRUNK_KINDS = ['front', 'server', 'database', 'library', 'device'];
|
|
31
|
+
|
|
32
|
+
// Suffixes that repeat what the trunk kind already says ("예약 API", "Reservation tables").
|
|
33
|
+
// Matched at the end of a limb label directly under a trunk.
|
|
34
|
+
const KIND_SUFFIX_PATTERN = /(\s|·)?(api|apis|응답|테이블|table|tables|관리|admin|management|endpoints?|엔드포인트)$/i;
|
|
35
|
+
|
|
36
|
+
// Technical-layer limb names — how code is arranged, not what the system is made of.
|
|
37
|
+
// Whole-label match (after trimming) so "상태 관리 화면" or "Store locator" don't trip it.
|
|
38
|
+
const LAYER_LIMB_LABELS = [
|
|
39
|
+
'state', 'state management', 'store', 'stores', 'utils', 'util', 'utilities', 'helpers',
|
|
40
|
+
'hooks', 'routes', 'routing', 'router', 'middleware', 'middlewares', 'models',
|
|
41
|
+
'services', 'controllers', 'components', 'handlers', 'lib', 'core', 'http routes',
|
|
42
|
+
'data store', 'app bootstrap', 'bootstrap',
|
|
43
|
+
'상태 관리', '상태', '유틸', '유틸리티', '헬퍼', '훅', '라우트', '라우팅', '미들웨어',
|
|
44
|
+
'모델', '서비스', '컨트롤러', '컴포넌트', '핸들러',
|
|
45
|
+
];
|
|
46
|
+
|
|
27
47
|
// Promotion signals — when a twig's description contains these, it might belong at limb level.
|
|
28
48
|
// Derived from sidebar/auth/ui-system limb promotions on 2026-05-21.
|
|
29
49
|
const PROMOTION_KEYWORDS = [
|
|
@@ -100,6 +120,71 @@ export function trunkNamingCheck(type, label) {
|
|
|
100
120
|
};
|
|
101
121
|
}
|
|
102
122
|
|
|
123
|
+
// trunk에 kind가 없거나 다섯 유형 밖이면 알린다. 거부하지 않는다 — 기존 나무가 대부분 kind 없이 있다.
|
|
124
|
+
export function trunkKindCheck(type, metadata) {
|
|
125
|
+
if (type !== 'trunk') return null;
|
|
126
|
+
const kind = metadata?.kind;
|
|
127
|
+
const fallback = 'If none of the five fits, leave kind unset, tell the human which trunk did not fit and why, and offer to suggest a new kind with send_feedback (kind "improvement") — do not file it on your own. See umtri://rules/trunk-kinds.';
|
|
128
|
+
if (kind == null || kind === '') {
|
|
129
|
+
return {
|
|
130
|
+
rule: 'trunk-kind-missing',
|
|
131
|
+
severity: 'info',
|
|
132
|
+
message: `Trunk has no metadata.kind. Set one of ${TRUNK_KINDS.join(' / ')} — the kind decides how its limbs split. ${fallback}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (!TRUNK_KINDS.includes(kind)) {
|
|
136
|
+
return {
|
|
137
|
+
rule: 'trunk-kind-unknown',
|
|
138
|
+
severity: 'info',
|
|
139
|
+
message: `metadata.kind "${kind}" is not one of ${TRUNK_KINDS.join(' / ')}. ${fallback}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// trunk 바로 아래 limb의 이름 규칙 — 접미사 금지, 레이어 이름 금지, 공통 limb는 하나.
|
|
146
|
+
// parentKind: 부모 trunk의 metadata.kind(없으면 null). siblingCommonCount: 같은 trunk 아래
|
|
147
|
+
// 이 노드를 뺀 metadata.common=true limb 수.
|
|
148
|
+
export function limbUnderTrunkChecks({ type, label, parentType, parentKind, metadata, siblingCommonCount = 0 }) {
|
|
149
|
+
const out = [];
|
|
150
|
+
if (type !== 'limb' || parentType !== 'trunk' || !label) return out;
|
|
151
|
+
const trimmed = label.trim();
|
|
152
|
+
|
|
153
|
+
const isLayer = LAYER_LIMB_LABELS.includes(trimmed.toLowerCase());
|
|
154
|
+
// 레이어 이름("상태 관리")은 접미사 문제가 아니라 축 문제라 layer 경고 하나만 낸다.
|
|
155
|
+
if (!isLayer && KIND_SUFFIX_PATTERN.test(trimmed) && trimmed.replace(KIND_SUFFIX_PATTERN, '').trim()) {
|
|
156
|
+
out.push({
|
|
157
|
+
rule: 'limb-domain-suffix',
|
|
158
|
+
severity: 'info',
|
|
159
|
+
message: `Limb "${label}" ends in a kind suffix. Name the domain alone ("${trimmed.replace(KIND_SUFFIX_PATTERN, '').trim()}") and reuse the exact label other trunks use for it — the trunk already says what kind of thing it is. See umtri://rules/trunk-kinds.`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (isLayer) {
|
|
164
|
+
const axis = {
|
|
165
|
+
front: 'user-facing features or flows (or pages for a public site)',
|
|
166
|
+
server: 'domains / resources',
|
|
167
|
+
database: 'domains, named like the server',
|
|
168
|
+
library: 'capabilities or concepts',
|
|
169
|
+
device: 'physical parts or screen regions',
|
|
170
|
+
}[parentKind] ?? "the trunk kind's axis (umtri://rules/trunk-kinds)";
|
|
171
|
+
out.push({
|
|
172
|
+
rule: 'limb-layer-axis',
|
|
173
|
+
severity: 'warn',
|
|
174
|
+
message: `Limb "${label}" names a technical layer. Split this trunk by ${axis}; put what this would hold into the feature/domain limb it serves, or into the trunk's one common limb.`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (metadata?.common === true && siblingCommonCount > 0) {
|
|
179
|
+
out.push({
|
|
180
|
+
rule: 'common-limb-duplicate',
|
|
181
|
+
severity: 'warn',
|
|
182
|
+
message: 'This trunk already has a common limb (metadata.common=true). Keep one per trunk — make this a twig of that limb, or a real feature/domain limb.',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
103
188
|
export function leafVsVeinCheck(type, label) {
|
|
104
189
|
if (!label) return null;
|
|
105
190
|
if (type !== 'leaf' && type !== 'vein') return null;
|
|
@@ -356,7 +441,7 @@ export function hierarchyCheck(parentType, childType) {
|
|
|
356
441
|
// phase: 'create' enables dormancy hint (irrelevant on update where children may already exist).
|
|
357
442
|
// siblingLeafCount: # of active leaf siblings already under the resolved parent (for parallel-leaves rule).
|
|
358
443
|
// metadata: this node's metadata (for missing-implements rule).
|
|
359
|
-
export function validateNode({ type, label, parentType, description, metadata, siblingLeafCount, siblingLeafLabels, phase = 'create' }) {
|
|
444
|
+
export function validateNode({ type, label, parentType, parentKind = null, siblingCommonCount = 0, description, metadata, siblingLeafCount, siblingLeafLabels, phase = 'create' }) {
|
|
360
445
|
const warnings = [];
|
|
361
446
|
let rejectReason = null;
|
|
362
447
|
|
|
@@ -366,12 +451,26 @@ export function validateNode({ type, label, parentType, description, metadata, s
|
|
|
366
451
|
else warnings.push(h);
|
|
367
452
|
}
|
|
368
453
|
|
|
369
|
-
|
|
454
|
+
let r = reservedDomainCheck(label);
|
|
455
|
+
// 유형이 정해진 trunk 바로 아래 limb는 도메인 이름이다 — Umtri 자신의 server·db에는 "버그" 도메인이
|
|
456
|
+
// 있다. 그 코드 영역을 가리키는 거라면 정당하니 info로 낮추고 그 뜻을 말해 준다.
|
|
457
|
+
if (r?.rule === 'reserved-domain' && type === 'limb' && parentType === 'trunk' && TRUNK_KINDS.includes(parentKind)) {
|
|
458
|
+
r = {
|
|
459
|
+
rule: 'reserved-domain',
|
|
460
|
+
severity: 'info',
|
|
461
|
+
message: `"${label}" is also a first-class record (bugs/apis/seasons/edges). As a limb under a ${parentKind} trunk it is fine if it names the code that handles that domain — not individual records.`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
370
464
|
if (r) warnings.push(r);
|
|
371
465
|
|
|
372
466
|
const t = trunkNamingCheck(type, label);
|
|
373
467
|
if (t) warnings.push(t);
|
|
374
468
|
|
|
469
|
+
const tk = trunkKindCheck(type, metadata);
|
|
470
|
+
if (tk) warnings.push(tk);
|
|
471
|
+
|
|
472
|
+
warnings.push(...limbUnderTrunkChecks({ type, label, parentType, parentKind, metadata, siblingCommonCount }));
|
|
473
|
+
|
|
375
474
|
const l = leafVsVeinCheck(type, label);
|
|
376
475
|
if (l) warnings.push(l);
|
|
377
476
|
|
|
@@ -79,6 +79,10 @@ users* vs *read by us*.
|
|
|
79
79
|
- `trunk` = a big domain (e.g. `www`, `server`, `db`). Pick stable,
|
|
80
80
|
long-lived names — usually matching a top-level code area.
|
|
81
81
|
- `limb` = a category inside a trunk (e.g. `graph`, `table`, `dashboard`).
|
|
82
|
+
How a trunk splits into limbs depends on its kind (`metadata.kind`): features
|
|
83
|
+
for a front, domains for a server and its database (same names, no suffix),
|
|
84
|
+
capabilities for a library, parts for a device — one common limb per trunk.
|
|
85
|
+
See `umtri://rules/trunk-kinds`.
|
|
82
86
|
- Do **not** create one limb per time period. Time is expressed through
|
|
83
87
|
`season` and `created_at`, not by carving the tree into temporal slices.
|
|
84
88
|
- Infrastructure (servers, DNS) is *not* a trunk — it is the environment the
|
|
@@ -106,6 +110,8 @@ questions about a ground.
|
|
|
106
110
|
| `role_change_at` | type-changed nodes | `string` — ISO date |
|
|
107
111
|
| `role_change_reason` | type-changed nodes | `string` — short prose explaining the shift |
|
|
108
112
|
| `placeholder` | intentionally-empty structure | `true` — opts out of dormant cleanup suggestions |
|
|
113
|
+
| `kind` | trunks | `front` \| `server` \| `database` \| `library` \| `device` |
|
|
114
|
+
| `common` | the one shared limb of a trunk | `true` — see `umtri://rules/trunk-kinds` |
|
|
109
115
|
|
|
110
116
|
## One file, multiple nodes
|
|
111
117
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Trunk Kinds — How Each Kind of Trunk Branches
|
|
2
|
+
|
|
3
|
+
Every trunk is one of five kinds, and each kind has its own rule for what its
|
|
4
|
+
limbs are. Recording the kind lets the same domain line up across trunks —
|
|
5
|
+
`Reservations` as a screen area, as endpoints, and as tables — instead of each
|
|
6
|
+
trunk being carved along a different axis.
|
|
7
|
+
|
|
8
|
+
## Record the kind
|
|
9
|
+
|
|
10
|
+
Set `metadata.kind` on the trunk. Do not leave it to be guessed from the
|
|
11
|
+
label: the kind is what the MCP reads to check the limbs under it.
|
|
12
|
+
|
|
13
|
+
| kind | What it is | Limbs split by | The common limb holds | Leaves are |
|
|
14
|
+
| --- | --- | --- | --- | --- |
|
|
15
|
+
| `front` | Something people use on a screen — app, web app, admin, static site | User-facing feature or flow (Reservations, My page). A public site splits by page | App shell (layout, routing, boot), design system, shared components | Screens, components |
|
|
16
|
+
| `server` | A service that takes requests — API, webhooks, jobs | Domain / resource. Non-domain consumers (external integrations, inbound webhooks) get their own limb | Request handling — authentication, authorization, errors, boot | Endpoint groups (`metadata.endpoints`) |
|
|
17
|
+
| `database` | A data store — relational DB, document DB, cache | Domain, **with the same names as the server** | Tables shared by many domains (sessions, audit log) | Tables (`metadata.columns`), labelled by their logical name |
|
|
18
|
+
| `library` | Shared code or a package that other trunks import | Capability or concept (Money, Signal pipeline) | Shared types and constants | Modules |
|
|
19
|
+
| `device` | Physical hardware, an embedded screen, an enclosure | Physical part or screen region | Device-wide parts (power, communication) | Parts, screen elements |
|
|
20
|
+
|
|
21
|
+
Static sites and admin panels are `front`. A monorepo package that apps import
|
|
22
|
+
is `library`.
|
|
23
|
+
|
|
24
|
+
### When none of the five fits
|
|
25
|
+
|
|
26
|
+
Do not force it. Leave `metadata.kind` unset, tell the human which trunk did
|
|
27
|
+
not fit and why, and **offer** to report it with `send_feedback` (kind
|
|
28
|
+
`improvement`) so the kind can be added. Never file that feedback on your own
|
|
29
|
+
initiative — the human decides.
|
|
30
|
+
|
|
31
|
+
## One common limb per trunk
|
|
32
|
+
|
|
33
|
+
Each trunk has **at most one** common limb, marked `metadata.common: true`.
|
|
34
|
+
Its name is free (`Common`, `Shell`, `공통`, `요청 처리` …); what it holds is
|
|
35
|
+
defined by the kind (table above). Anything that belongs to no single feature
|
|
36
|
+
or domain goes there — as twigs if it grows.
|
|
37
|
+
|
|
38
|
+
If a second common limb seems necessary, it is either a real feature/domain
|
|
39
|
+
limb or a twig of the existing common limb.
|
|
40
|
+
|
|
41
|
+
**Never split a trunk by technical layer.** `State`, `Utils`, `Hooks`,
|
|
42
|
+
`Routes`, `Middleware`, `Store`, `Models`, `Services`, `Components`
|
|
43
|
+
(`상태 관리`, `유틸`, `훅`, `라우트`, `미들웨어`, `모델` …) are how the code is
|
|
44
|
+
arranged, not what the system is made of. Their contents belong in the feature
|
|
45
|
+
or domain limb they serve, or in the common limb.
|
|
46
|
+
|
|
47
|
+
## Same domain, same name — no suffixes
|
|
48
|
+
|
|
49
|
+
A domain is named the same in every trunk, without a suffix:
|
|
50
|
+
|
|
51
|
+
| ✅ | ❌ |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `Reservations` (front) · `Reservations` (server) · `Reservations` (database) | `Reservations API`, `Reservation tables`, `Reservation admin` |
|
|
54
|
+
| `예약` · `예약` · `예약` | `예약 API`, `예약 응답`, `예약 테이블`, `예약 관리` |
|
|
55
|
+
|
|
56
|
+
The trunk already says what kind of thing it is, so a suffix repeats it — and
|
|
57
|
+
suffixes drift (`API` in one ground, `응답` in another), which breaks the
|
|
58
|
+
alignment the shared name is for. An admin `front` names its limbs by the
|
|
59
|
+
resource too: `Members`, not `Member management`.
|
|
60
|
+
|
|
61
|
+
When adding a limb, look at the other trunks first. If the domain already
|
|
62
|
+
exists there, reuse its exact label.
|
|
63
|
+
|
|
64
|
+
## Connections between kinds
|
|
65
|
+
|
|
66
|
+
| From → to | Record as |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| front screen → server endpoint | api (`create_api`, caller → callee) |
|
|
69
|
+
| server endpoint → database table | edge `data_flow` (writes) or `dependency` (reads) |
|
|
70
|
+
| table → table (foreign key) | edge `dependency` (referencing → referenced) |
|
|
71
|
+
| any trunk → library module | edge `dependency` |
|
|
72
|
+
|
|
73
|
+
## Warnings
|
|
74
|
+
|
|
75
|
+
`create_node` and `update_node` never reject on these — they come back in
|
|
76
|
+
`warnings[]`:
|
|
77
|
+
|
|
78
|
+
| Rule | Meaning |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `trunk-kind-missing` | A trunk has no `metadata.kind`. Pick one of the five, or follow "When none of the five fits". |
|
|
81
|
+
| `trunk-kind-unknown` | `metadata.kind` is not one of the five. Same advice. |
|
|
82
|
+
| `limb-domain-suffix` | A limb directly under a trunk ends in a kind suffix (`API`, `응답`, `테이블`, `관리`, `Table(s)`, `Admin`, `Management`). Drop it and reuse the domain name. |
|
|
83
|
+
| `limb-layer-axis` | A limb is named after a technical layer. Split by the trunk kind's axis instead. |
|
|
84
|
+
| `common-limb-duplicate` | The trunk already has a limb with `metadata.common: true`. |
|
|
85
|
+
|
|
86
|
+
## Reshaping an existing tree
|
|
87
|
+
|
|
88
|
+
Grounds built before these rules exist. Bring them in line one node at a
|
|
89
|
+
time: set each trunk's `kind`, then rename or regroup limbs with the label
|
|
90
|
+
change visible, recording `reparented_*` when nodes move (see
|
|
91
|
+
`umtri://rules/system-structure`). Ask the human before a large regrouping —
|
|
92
|
+
the shape of their tree is theirs to approve.
|
|
@@ -32,6 +32,10 @@ Domains or feature areas (auth, payments, reservations, dashboard, …) are
|
|
|
32
32
|
that spans the user-app and admin trunks" — fine. "Authentication is a
|
|
33
33
|
trunk" — no.
|
|
34
34
|
|
|
35
|
+
Record which of the five kinds the trunk is in `metadata.kind` (`front`,
|
|
36
|
+
`server`, `database`, `library`, `device`). The kind decides how its limbs
|
|
37
|
+
split — see `umtri://rules/trunk-kinds`.
|
|
38
|
+
|
|
35
39
|
Naming convention (recommended, not enforced):
|
|
36
40
|
- Apps: `<name>-app` (e.g. `user-app`, `kiosk-app`)
|
|
37
41
|
- Servers: `<name>-server` (e.g. `api-server`, `iot-server`)
|
|
@@ -24,8 +24,8 @@ guideline; see `umtri://rules/system-structure`.
|
|
|
24
24
|
|
|
25
25
|
| Type | Role | What it represents |
|
|
26
26
|
| ------- | --------- | ------------------------------------------------------------------ |
|
|
27
|
-
| `trunk` | structure | A deployable system or independent service (app, server, DB).
|
|
28
|
-
| `limb` | structure | A domain or feature area inside a trunk (auth, payments, dashboard)
|
|
27
|
+
| `trunk` | structure | A deployable system or independent service (app, server, DB). Carries `metadata.kind` — see `umtri://rules/trunk-kinds`. |
|
|
28
|
+
| `limb` | structure | A domain or feature area inside a trunk (auth, payments, dashboard) — the axis depends on the trunk's kind. |
|
|
29
29
|
| `twig` | structure | A cluster of related leaves (a module, a feature group). |
|
|
30
30
|
| `leaf` | object | The thing itself — class, component, screen, entity. Noun phrase. |
|
|
31
31
|
| `vein` | action | A capability the object performs — a method/function that exists now. Verb phrase. NOT a task or a change that was made. |
|
|
@@ -172,9 +172,14 @@ If you see one of these in the response, read it before proceeding:
|
|
|
172
172
|
| `structure-without-children` | New limb/twig will render dormant until a leaf/vein appears. |
|
|
173
173
|
| `twig-promotion-candidate` | Twig description suggests it should be a sibling limb. |
|
|
174
174
|
| `redundant-child` | A twig and its only leaf duplicate the same concept. |
|
|
175
|
+
| `trunk-kind-missing` / `trunk-kind-unknown` | Trunk has no / an unknown `metadata.kind` (front, server, database, library, device). |
|
|
176
|
+
| `limb-domain-suffix` | Limb under a trunk ends in `API`/`응답`/`테이블`/`관리`… — name the domain alone. |
|
|
177
|
+
| `limb-layer-axis` | Limb is a technical layer (State, Utils, Routes, Middleware…). |
|
|
178
|
+
| `common-limb-duplicate` | The trunk already has its one common limb (`metadata.common`). |
|
|
175
179
|
|
|
176
180
|
## Further reading (read only when relevant)
|
|
177
181
|
|
|
182
|
+
- `umtri://rules/trunk-kinds` — the five trunk kinds and how each splits into limbs
|
|
178
183
|
- `umtri://rules/system-structure` — metadata key conventions, file→leaf mapping
|
|
179
184
|
- `umtri://rules/vocabulary-detailed` — promotion signals, reclassification
|
|
180
185
|
policy, twig-vs-leaf decision matrix, redundant-child antipattern details
|
package/core/resources.js
CHANGED
|
@@ -22,6 +22,13 @@ const RESOURCES = [
|
|
|
22
22
|
title: 'Umtri vocabulary — detailed guidance',
|
|
23
23
|
description: 'Reclassification policy, twig vs leaf decision matrix, twig promotion signals, redundant-child antipattern, Korean label heuristics. Read only when restructuring an existing tree or wrestling with a borderline classification.',
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
name: 'trunk-kinds',
|
|
27
|
+
uri: 'umtri://rules/trunk-kinds',
|
|
28
|
+
file: 'trunk-kinds.md',
|
|
29
|
+
title: 'Trunk kinds — how each kind of trunk branches',
|
|
30
|
+
description: 'Five trunk kinds (front, server, database, library, device) recorded in metadata.kind, the limb axis for each, one common limb per trunk (metadata.common), the same domain name across trunks with no suffix, and how kinds connect. Read before creating a trunk or a limb directly under one, and when restructuring.',
|
|
31
|
+
},
|
|
25
32
|
{
|
|
26
33
|
name: 'seasons',
|
|
27
34
|
uri: 'umtri://rules/seasons',
|
package/core/tools.js
CHANGED
|
@@ -666,6 +666,7 @@ export function registerTools(server, { api }) {
|
|
|
666
666
|
'parent ∈ existing node id; omit to create a root-level node.',
|
|
667
667
|
'season ∈ existing season id; omit to use the active "now" season. Past seasons are normally rejected, but allowed while the ground is transplanting (project.transplanting=true) — nodes added then are auto-stamped metadata.transplanted=true for audit. See umtri://rules/transplant.',
|
|
668
668
|
'The tool validates against protocol policies. Hierarchy violations are rejected. Soft issues (reserved-domain labels, leaf↔vein heuristic, trunk naming) come back as warnings in the response — reconsider before continuing if warnings appear.',
|
|
669
|
+
'A trunk carries metadata.kind ∈ front · server · database · library · device, and its limbs follow that kind\'s axis: same domain name across trunks with no suffix ("예약", not "예약 API"), no technical-layer limbs, and at most one common limb (metadata.common=true). Read umtri://rules/trunk-kinds before creating a trunk or a limb under one.',
|
|
669
670
|
'After creating a leaf/vein, consider its connections: if it calls/feeds another node add an api (create_api), if it depends on/is built on another add an edge (create_edge). The response carries a connectionCheck reminder. See umtri://rules/system-structure (Connections).',
|
|
670
671
|
'Do not open a new season just because none fits — attach to the now-season, or ask the user. create_season needs their confirmation (umtri://rules/seasons).',
|
|
671
672
|
'When realizing a human-drawn plan brief, any detail nodes you add should carry metadata.plan=true and the realized node needs metadata.implements — see umtri://rules/plan. A trunk is never plan: metadata.plan on a trunk is rejected.',
|
|
@@ -685,19 +686,25 @@ export function registerTools(server, { api }) {
|
|
|
685
686
|
async ({ slug, type, label, parent, season, description, tags, metadata, sproutedAt }) => {
|
|
686
687
|
try {
|
|
687
688
|
let parentType = null;
|
|
689
|
+
let parentKind = null;
|
|
690
|
+
let siblingCommonCount = 0;
|
|
688
691
|
let siblingLeafLabels;
|
|
689
692
|
if (parent) {
|
|
690
693
|
const graph = await api.get(`/api/projects/${encodeURIComponent(slug)}/graph`);
|
|
691
694
|
const parentNode = (graph.nodes || []).find(n => n.id === parent);
|
|
692
695
|
if (!parentNode) throw new Error(`Parent node "${parent}" not found in ground "${slug}".`);
|
|
693
696
|
parentType = parentNode.type;
|
|
697
|
+
// trunk 유형별 limb 규칙(umtri://rules/trunk-kinds)에 쓰는 부모 kind와 공통 limb 수.
|
|
698
|
+
parentKind = parentNode.metadata?.kind ?? null;
|
|
699
|
+
siblingCommonCount = (graph.nodes || [])
|
|
700
|
+
.filter(n => n.parent === parent && n.type === 'limb' && n.metadata?.common === true).length;
|
|
694
701
|
siblingLeafLabels = (graph.nodes || [])
|
|
695
702
|
.filter(n => n.parent === parent && n.type === 'leaf')
|
|
696
703
|
.map(n => n.label);
|
|
697
704
|
}
|
|
698
705
|
|
|
699
706
|
const { ok, rejectReason, warnings } = validateNode({
|
|
700
|
-
type, label, parentType, description, metadata, siblingLeafLabels,
|
|
707
|
+
type, label, parentType, parentKind, siblingCommonCount, description, metadata, siblingLeafLabels,
|
|
701
708
|
});
|
|
702
709
|
if (!ok) throw new Error(`Rejected by protocol: ${rejectReason}`);
|
|
703
710
|
|
|
@@ -749,7 +756,9 @@ export function registerTools(server, { api }) {
|
|
|
749
756
|
warnings = warnings.concat(patchResult.warnings);
|
|
750
757
|
warnings = warnings.concat(toolMarkupCheck({ description: patch.description }));
|
|
751
758
|
|
|
752
|
-
|
|
759
|
+
// metadata.kind·common이 바뀌면 trunk 유형 경고도 다시 본다.
|
|
760
|
+
const kindTouched = patch.metadata && ('kind' in patch.metadata || 'common' in patch.metadata);
|
|
761
|
+
if (patch.type !== undefined || patch.parent !== undefined || patch.label !== undefined || kindTouched) {
|
|
753
762
|
const graph = await api.get(`/api/projects/${encodeURIComponent(slug)}/graph`);
|
|
754
763
|
const current = (graph.nodes || []).find(n => n.id === id);
|
|
755
764
|
if (!current) throw new Error(`Node "${id}" not found in ground "${slug}".`);
|
|
@@ -761,14 +770,20 @@ export function registerTools(server, { api }) {
|
|
|
761
770
|
const newMetadata = patch.metadata !== undefined
|
|
762
771
|
? Object.fromEntries(Object.entries({ ...(current.metadata || {}), ...patch.metadata }).filter(([, v]) => v !== null))
|
|
763
772
|
: current.metadata;
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
773
|
+
const parentNode = newParentId ? (graph.nodes || []).find(n => n.id === newParentId) : null;
|
|
774
|
+
const parentType = parentNode?.type ?? null;
|
|
775
|
+
const parentKind = parentNode?.metadata?.kind ?? null;
|
|
776
|
+
const siblingCommonCount = newParentId
|
|
777
|
+
? (graph.nodes || []).filter(n => n.parent === newParentId && n.id !== id
|
|
778
|
+
&& n.type === 'limb' && n.metadata?.common === true).length
|
|
779
|
+
: 0;
|
|
767
780
|
|
|
768
781
|
const result = validateNode({
|
|
769
782
|
type: newType,
|
|
770
783
|
label: newLabel,
|
|
771
784
|
parentType,
|
|
785
|
+
parentKind,
|
|
786
|
+
siblingCommonCount,
|
|
772
787
|
description: newDescription,
|
|
773
788
|
metadata: newMetadata,
|
|
774
789
|
phase: 'update',
|
package/package.json
CHANGED