@lucasviola/specwiki 1.0.1 → 1.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/README.md +3 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +1 -0
- package/dist/commands/generate.js.map +1 -1
- package/dist/output/html/assets/specwiki.css +85 -1
- package/dist/output/html/nav-grouping-catalog.d.ts +32 -0
- package/dist/output/html/nav-grouping-catalog.d.ts.map +1 -0
- package/dist/output/html/nav-grouping-catalog.js +294 -0
- package/dist/output/html/nav-grouping-catalog.js.map +1 -0
- package/dist/output/html/nav-grouping.d.ts +28 -27
- package/dist/output/html/nav-grouping.d.ts.map +1 -1
- package/dist/output/html/nav-grouping.js +404 -521
- package/dist/output/html/nav-grouping.js.map +1 -1
- package/dist/output/html/renderer.d.ts +25 -0
- package/dist/output/html/renderer.d.ts.map +1 -1
- package/dist/output/html/renderer.js +72 -26
- package/dist/output/html/renderer.js.map +1 -1
- package/dist/output/html/templates/article.mustache +7 -1
- package/dist/output/html/templates/index.mustache +8 -2
- package/dist/output/html/templates/partials/nav-subgroup-nested.mustache +23 -0
- package/dist/output/html/templates/partials/nav-subgroup.mustache +23 -35
- package/dist/output/wiki.d.ts.map +1 -1
- package/dist/output/wiki.js +10 -2
- package/dist/output/wiki.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const
|
|
1
|
+
import { loadNavGroupingContext, } from "./nav-grouping-catalog.js";
|
|
2
|
+
export { loadNavGroupingContext };
|
|
3
|
+
const HYBRID_GROUPS = [
|
|
4
|
+
{ key: "your-team", label: "Your team" },
|
|
5
|
+
{ key: "analysis", label: "Analysis" },
|
|
6
|
+
{ key: "planning", label: "Planning" },
|
|
7
|
+
{ key: "solutioning", label: "Solutioning" },
|
|
8
|
+
{ key: "implementation", label: "Implementation" },
|
|
9
|
+
{ key: "core-utilities", label: "Core utilities" },
|
|
10
|
+
{ key: "deprecated", label: "Deprecated" },
|
|
11
|
+
{ key: "uncatalogued", label: "Uncatalogued" },
|
|
12
|
+
];
|
|
4
13
|
const CATEGORY_PATH_PREFIXES = {
|
|
5
|
-
"agent-skills": ".agents/skills/",
|
|
6
|
-
"cursor-skills": ".cursor/skills/",
|
|
7
14
|
"cursor-rules": ".cursor/rules/",
|
|
15
|
+
"cursor-skills": ".cursor/skills/",
|
|
16
|
+
"agent-skills": ".agents/skills/",
|
|
8
17
|
"bmad-output": "_bmad-output/",
|
|
9
18
|
specs: "specs/",
|
|
10
19
|
spec: "spec/",
|
|
@@ -14,595 +23,469 @@ const CATEGORY_PATH_PREFIXES = {
|
|
|
14
23
|
plans: "docs/plans/",
|
|
15
24
|
requirements: "requirements/",
|
|
16
25
|
github: ".github/",
|
|
26
|
+
root: "",
|
|
17
27
|
};
|
|
18
28
|
const FOLDER_LABELS = {
|
|
29
|
+
planning: "Planning",
|
|
19
30
|
"planning-artifacts": "Planning",
|
|
20
31
|
"implementation-artifacts": "Implementation Stories",
|
|
32
|
+
"implementation-stories": "Implementation Stories",
|
|
33
|
+
"epic-context": "Epic Context",
|
|
21
34
|
discovery: "Discovery",
|
|
22
35
|
research: "Research",
|
|
23
|
-
|
|
24
|
-
architecture: "Architecture",
|
|
25
|
-
readiness: "Readiness",
|
|
26
|
-
epics: "Epics",
|
|
27
|
-
};
|
|
28
|
-
const PHASE_GROUPS = {
|
|
29
|
-
"1-analysis": { key: "phase-analysis", label: "Analysis", sortOrder: 1 },
|
|
30
|
-
"2-planning": { key: "phase-planning", label: "Planning", sortOrder: 2 },
|
|
31
|
-
"3-solutioning": {
|
|
32
|
-
key: "phase-solutioning",
|
|
33
|
-
label: "Solutioning",
|
|
34
|
-
sortOrder: 3,
|
|
35
|
-
},
|
|
36
|
-
"4-implementation": {
|
|
37
|
-
key: "phase-implementation",
|
|
38
|
-
label: "Implementation",
|
|
39
|
-
sortOrder: 4,
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
const YOUR_TEAM_GROUP = {
|
|
43
|
-
key: "your-team",
|
|
44
|
-
label: "Your team",
|
|
45
|
-
sortOrder: 0,
|
|
46
|
-
};
|
|
47
|
-
const CORE_UTILITIES_GROUP = {
|
|
48
|
-
key: "core-utilities",
|
|
49
|
-
label: "Core utilities",
|
|
50
|
-
sortOrder: 5,
|
|
51
|
-
};
|
|
52
|
-
const DEPRECATED_GROUP = {
|
|
53
|
-
key: "deprecated",
|
|
54
|
-
label: "Deprecated",
|
|
55
|
-
sortOrder: 6,
|
|
56
|
-
};
|
|
57
|
-
const UNCATALOGUED_GROUP = {
|
|
58
|
-
key: "uncatalogued",
|
|
59
|
-
label: "Uncatalogued",
|
|
60
|
-
sortOrder: 7,
|
|
36
|
+
other: "Other",
|
|
61
37
|
};
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
38
|
+
const STORY_FILENAME_PATTERN = /^(\d+)-(\d+)-/;
|
|
39
|
+
function storyNumbers(sourcePath) {
|
|
40
|
+
const filename = sourcePath.split("/").pop() ?? sourcePath;
|
|
41
|
+
const match = filename.match(STORY_FILENAME_PATTERN);
|
|
42
|
+
return {
|
|
43
|
+
epic: parseInt(match?.[1] ?? "0", 10),
|
|
44
|
+
story: parseInt(match?.[2] ?? "0", 10),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
let subgroupOrderCounter = 0;
|
|
48
|
+
export function buildCategoryNavSubgroups(pages, options) {
|
|
49
|
+
subgroupOrderCounter = 0;
|
|
50
|
+
if (options.categoryKey === "agent-skills" &&
|
|
51
|
+
options.context?.loaded === true) {
|
|
52
|
+
return buildHybridAgentSkillsGrouping(pages, options);
|
|
70
53
|
}
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
54
|
+
const rootPages = [];
|
|
55
|
+
const rootChildren = new Map();
|
|
56
|
+
for (const page of pages) {
|
|
57
|
+
const navPage = toNavPage(page);
|
|
58
|
+
const segments = resolveSubgroupSegments(page.sourcePath, options.categoryKey);
|
|
59
|
+
if (segments.length === 0) {
|
|
60
|
+
rootPages.push(navPage);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
insertIntoTree(rootChildren, segments, navPage);
|
|
76
64
|
}
|
|
77
|
-
|
|
78
|
-
|
|
65
|
+
if (options.categoryKey === "bmad-output") {
|
|
66
|
+
sortImplementationStoryPages(rootChildren);
|
|
79
67
|
}
|
|
68
|
+
const subgroups = finalizeSubgroups(rootChildren, options);
|
|
69
|
+
const promotedPages = [...rootPages, ...subgroups.rootPromoted];
|
|
70
|
+
const visibleSubgroups = subgroups.subgroups;
|
|
71
|
+
const hasSubgroups = visibleSubgroups.length > 0;
|
|
72
|
+
return {
|
|
73
|
+
pages: promotedPages,
|
|
74
|
+
subgroups: visibleSubgroups,
|
|
75
|
+
hasSubgroups,
|
|
76
|
+
};
|
|
80
77
|
}
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
subgroups: [],
|
|
100
|
-
pages: sortNavPages(promoted.pages),
|
|
101
|
-
};
|
|
78
|
+
function buildHybridAgentSkillsGrouping(pages, options) {
|
|
79
|
+
const skillsById = options.context?.skillsById ?? new Map();
|
|
80
|
+
/** phase → skillId ("" for orphans) → wiki pages */
|
|
81
|
+
const buckets = new Map();
|
|
82
|
+
for (const page of pages) {
|
|
83
|
+
const skillId = agentSkillIdFromPath(page.sourcePath);
|
|
84
|
+
const entry = skillId ? skillsById.get(skillId) : undefined;
|
|
85
|
+
const groupKey = resolveHybridGroupKey(entry);
|
|
86
|
+
const skillKey = skillId ?? "";
|
|
87
|
+
let phaseSkills = buckets.get(groupKey);
|
|
88
|
+
if (!phaseSkills) {
|
|
89
|
+
phaseSkills = new Map();
|
|
90
|
+
buckets.set(groupKey, phaseSkills);
|
|
91
|
+
}
|
|
92
|
+
const list = phaseSkills.get(skillKey) ?? [];
|
|
93
|
+
list.push(page);
|
|
94
|
+
phaseSkills.set(skillKey, list);
|
|
102
95
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
96
|
+
const rootChildren = new Map();
|
|
97
|
+
for (const group of HYBRID_GROUPS) {
|
|
98
|
+
const phaseSkills = buckets.get(group.key);
|
|
99
|
+
if (!phaseSkills || phaseSkills.size === 0) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const phaseNode = {
|
|
103
|
+
key: group.key,
|
|
104
|
+
label: group.label,
|
|
105
|
+
pages: [],
|
|
106
|
+
children: new Map(),
|
|
107
|
+
order: subgroupOrderCounter++,
|
|
110
108
|
};
|
|
109
|
+
const skillEntries = [...phaseSkills.entries()].sort(([idA, pagesA], [idB, pagesB]) => {
|
|
110
|
+
const labelA = resolveHybridSkillLabel(idA, pagesA[0], skillsById.get(idA));
|
|
111
|
+
const labelB = resolveHybridSkillLabel(idB, pagesB[0], skillsById.get(idB));
|
|
112
|
+
return labelA.localeCompare(labelB, undefined, { sensitivity: "base" });
|
|
113
|
+
});
|
|
114
|
+
for (const [skillKey, skillPages] of skillEntries) {
|
|
115
|
+
const entry = skillKey ? skillsById.get(skillKey) : undefined;
|
|
116
|
+
// Paths outside .agents/skills/ stay as direct Uncatalogued leaves.
|
|
117
|
+
if (!skillKey) {
|
|
118
|
+
for (const page of skillPages) {
|
|
119
|
+
phaseNode.pages.push(toNavPage(page));
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const skillLabel = resolveHybridSkillLabel(skillKey, skillPages[0], entry);
|
|
124
|
+
// Single-page skills: L4 title on the phase leaf (no L2 wrapper).
|
|
125
|
+
if (skillPages.length === 1) {
|
|
126
|
+
phaseNode.pages.push(toNavPage(skillPages[0], resolveAgentSkillTitle(skillPages[0], entry)));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const nestedPages = skillPages
|
|
130
|
+
.map((page) => toNavPage(page))
|
|
131
|
+
.sort((a, b) => a.title.localeCompare(b.title, undefined, { sensitivity: "base" }));
|
|
132
|
+
phaseNode.children.set(skillKey, {
|
|
133
|
+
key: `${group.key}/${skillKey}`,
|
|
134
|
+
label: skillLabel,
|
|
135
|
+
pages: nestedPages,
|
|
136
|
+
children: new Map(),
|
|
137
|
+
order: subgroupOrderCounter++,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
phaseNode.pages.sort((a, b) => a.title.localeCompare(b.title, undefined, { sensitivity: "base" }));
|
|
141
|
+
rootChildren.set(group.key, phaseNode);
|
|
111
142
|
}
|
|
143
|
+
const subgroups = finalizeSubgroups(rootChildren, options);
|
|
112
144
|
return {
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
pages: subgroups.rootPromoted,
|
|
146
|
+
subgroups: subgroups.subgroups,
|
|
147
|
+
hasSubgroups: subgroups.subgroups.length > 0,
|
|
115
148
|
};
|
|
116
149
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return undefined;
|
|
150
|
+
function resolveHybridSkillLabel(skillId, page, entry) {
|
|
151
|
+
if (!skillId) {
|
|
152
|
+
return page.title;
|
|
121
153
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
function assignPageGroups(page, category, context) {
|
|
125
|
-
if (category === "agent-skills" && context.bmadCatalog) {
|
|
126
|
-
const bmadAssignment = assignAgentSkillGroups(page, context.bmadCatalog);
|
|
127
|
-
if (bmadAssignment) {
|
|
128
|
-
return bmadAssignment;
|
|
129
|
-
}
|
|
154
|
+
if (entry?.isAgent && entry.agentName && entry.agentTitle) {
|
|
155
|
+
return resolveAgentSkillTitle(page, entry);
|
|
130
156
|
}
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
if (bmadOutputAssignment) {
|
|
134
|
-
return bmadOutputAssignment;
|
|
135
|
-
}
|
|
157
|
+
if (entry?.displayName) {
|
|
158
|
+
return entry.displayName;
|
|
136
159
|
}
|
|
137
|
-
|
|
160
|
+
// Stable label for uncatalogued multi-page skills (not first-page wiki title).
|
|
161
|
+
return humanizeFolderLabel(skillId, [skillId]);
|
|
138
162
|
}
|
|
139
|
-
function
|
|
140
|
-
const
|
|
141
|
-
|
|
163
|
+
function agentSkillIdFromPath(sourcePath) {
|
|
164
|
+
const normalized = normalizePath(sourcePath);
|
|
165
|
+
const prefix = ".agents/skills/";
|
|
166
|
+
if (!normalized.startsWith(prefix)) {
|
|
142
167
|
return null;
|
|
143
168
|
}
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
displayTitle,
|
|
151
|
-
};
|
|
169
|
+
const skillId = normalized.slice(prefix.length).split("/")[0];
|
|
170
|
+
return skillId || null;
|
|
171
|
+
}
|
|
172
|
+
function resolveHybridGroupKey(entry) {
|
|
173
|
+
if (entry?.isAgent) {
|
|
174
|
+
return "your-team";
|
|
152
175
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
segments: [UNCATALOGUED_GROUP],
|
|
157
|
-
pageSortKey: `99-${page.title.toLowerCase()}`,
|
|
158
|
-
};
|
|
176
|
+
if (entry?.description?.toLowerCase().includes("deprecated")) {
|
|
177
|
+
return "deprecated";
|
|
159
178
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
179
|
+
switch (entry?.phase) {
|
|
180
|
+
case "1-analysis":
|
|
181
|
+
return "analysis";
|
|
182
|
+
case "2-planning":
|
|
183
|
+
return "planning";
|
|
184
|
+
case "3-solutioning":
|
|
185
|
+
return "solutioning";
|
|
186
|
+
case "4-implementation":
|
|
187
|
+
return "implementation";
|
|
188
|
+
default:
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
if (entry?.module === "Core" || entry?.phase === "anytime") {
|
|
192
|
+
return "core-utilities";
|
|
193
|
+
}
|
|
194
|
+
if (entry?.inCsv) {
|
|
195
|
+
return "core-utilities";
|
|
196
|
+
}
|
|
197
|
+
return "uncatalogued";
|
|
198
|
+
}
|
|
199
|
+
function resolveAgentSkillTitle(page, entry) {
|
|
200
|
+
if (entry?.isAgent && entry.agentName && entry.agentTitle) {
|
|
201
|
+
return entry.agentIcon
|
|
202
|
+
? `${entry.agentIcon} ${entry.agentName} — ${entry.agentTitle}`
|
|
203
|
+
: `${entry.agentName} — ${entry.agentTitle}`;
|
|
166
204
|
}
|
|
167
|
-
|
|
205
|
+
if (entry?.displayName) {
|
|
206
|
+
return entry.displayName;
|
|
207
|
+
}
|
|
208
|
+
return page.title;
|
|
209
|
+
}
|
|
210
|
+
function toNavPage(page, titleOverride) {
|
|
168
211
|
return {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
212
|
+
title: titleOverride ?? page.title,
|
|
213
|
+
slug: page.slug,
|
|
214
|
+
href: `${page.slug}.html`,
|
|
215
|
+
sourcePath: page.sourcePath,
|
|
172
216
|
};
|
|
173
217
|
}
|
|
174
|
-
function
|
|
175
|
-
|
|
176
|
-
|
|
218
|
+
function normalizePath(sourcePath) {
|
|
219
|
+
return sourcePath.replace(/\\/g, "/");
|
|
220
|
+
}
|
|
221
|
+
function resolveSubgroupSegments(sourcePath, categoryKey) {
|
|
222
|
+
const normalized = normalizePath(sourcePath);
|
|
223
|
+
if (categoryKey === "bmad-output") {
|
|
224
|
+
const bmadSegments = resolveBmadOutputSegments(normalized);
|
|
225
|
+
if (bmadSegments !== null) {
|
|
226
|
+
return bmadSegments;
|
|
227
|
+
}
|
|
177
228
|
}
|
|
178
|
-
return
|
|
229
|
+
return resolveL0Segments(normalized, categoryKey);
|
|
179
230
|
}
|
|
180
|
-
function
|
|
181
|
-
const normalized = page.sourcePath.replace(/\\/g, "/");
|
|
231
|
+
function resolveBmadOutputSegments(normalized) {
|
|
182
232
|
if (!normalized.startsWith("_bmad-output/")) {
|
|
183
233
|
return null;
|
|
184
234
|
}
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
label: "Planning",
|
|
193
|
-
sortOrder: 0,
|
|
194
|
-
};
|
|
195
|
-
if (dirParts.length <= 1) {
|
|
196
|
-
return {
|
|
197
|
-
segments: [planning],
|
|
198
|
-
pageSortKey: basename,
|
|
199
|
-
};
|
|
235
|
+
const relative = normalized.slice("_bmad-output/".length);
|
|
236
|
+
const dirParts = relative.split("/");
|
|
237
|
+
const filename = dirParts.pop() ?? "";
|
|
238
|
+
if (dirParts[0] === "planning-artifacts" || dirParts[0] === "planning") {
|
|
239
|
+
if (dirParts[0] === "planning-artifacts") {
|
|
240
|
+
const childKey = dirParts[1] ?? "other";
|
|
241
|
+
return ["planning", childKey];
|
|
200
242
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
segments: [
|
|
205
|
-
planning,
|
|
206
|
-
{
|
|
207
|
-
key: `planning-${subKey}`,
|
|
208
|
-
label: subLabel,
|
|
209
|
-
sortOrder: 0,
|
|
210
|
-
},
|
|
211
|
-
],
|
|
212
|
-
pageSortKey: basename,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
if (remainder.startsWith("implementation-artifacts/")) {
|
|
216
|
-
const implementation = {
|
|
217
|
-
key: "implementation",
|
|
218
|
-
label: "Implementation Stories",
|
|
219
|
-
sortOrder: 1,
|
|
220
|
-
};
|
|
221
|
-
if (basename.startsWith("epic-")) {
|
|
222
|
-
return {
|
|
223
|
-
segments: [
|
|
224
|
-
{
|
|
225
|
-
key: "epic-context",
|
|
226
|
-
label: "Epic Context",
|
|
227
|
-
sortOrder: 2,
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
pageSortKey: basename,
|
|
231
|
-
};
|
|
243
|
+
if (dirParts.length === 1) {
|
|
244
|
+
return ["planning"];
|
|
232
245
|
}
|
|
233
|
-
|
|
234
|
-
if (epicMatch) {
|
|
235
|
-
const epicNumber = epicMatch[1];
|
|
236
|
-
return {
|
|
237
|
-
segments: [
|
|
238
|
-
implementation,
|
|
239
|
-
{
|
|
240
|
-
key: `epic-${epicNumber}`,
|
|
241
|
-
label: `Epic ${epicNumber}`,
|
|
242
|
-
sortOrder: Number.parseInt(epicNumber, 10),
|
|
243
|
-
},
|
|
244
|
-
],
|
|
245
|
-
pageSortKey: basename,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
return {
|
|
249
|
-
segments: [
|
|
250
|
-
implementation,
|
|
251
|
-
{
|
|
252
|
-
key: "other-stories",
|
|
253
|
-
label: "Other Stories",
|
|
254
|
-
sortOrder: 999,
|
|
255
|
-
},
|
|
256
|
-
],
|
|
257
|
-
pageSortKey: basename,
|
|
258
|
-
};
|
|
246
|
+
return ["planning", dirParts[1]];
|
|
259
247
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const normalized = page.sourcePath.replace(/\\/g, "/");
|
|
264
|
-
const prefix = CATEGORY_PATH_PREFIXES[category];
|
|
265
|
-
let remainder = normalized;
|
|
266
|
-
if (prefix && normalized.startsWith(prefix)) {
|
|
267
|
-
remainder = normalized.slice(prefix.length);
|
|
268
|
-
}
|
|
269
|
-
const dirname = path.posix.dirname(remainder);
|
|
270
|
-
if (dirname === ".") {
|
|
271
|
-
if (category === "other" && normalized.includes("/")) {
|
|
272
|
-
const topSegment = normalized.split("/")[0];
|
|
273
|
-
return {
|
|
274
|
-
segments: [
|
|
275
|
-
{
|
|
276
|
-
key: topSegment,
|
|
277
|
-
label: labelForPathSegment(topSegment),
|
|
278
|
-
sortOrder: 0,
|
|
279
|
-
},
|
|
280
|
-
],
|
|
281
|
-
pageSortKey: normalized,
|
|
282
|
-
};
|
|
248
|
+
if (dirParts[0] === "implementation-artifacts") {
|
|
249
|
+
if (dirParts.length === 1 && filename.startsWith("epic-")) {
|
|
250
|
+
return ["epic-context"];
|
|
283
251
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
};
|
|
252
|
+
const storyMatch = filename.match(STORY_FILENAME_PATTERN);
|
|
253
|
+
if (storyMatch) {
|
|
254
|
+
const epicNum = storyMatch[1];
|
|
255
|
+
return ["implementation-stories", `epic-${epicNum}`];
|
|
289
256
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
pageSortKey: page.title.toLowerCase(),
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
const dirParts = dirname.split("/").filter(Boolean);
|
|
296
|
-
const segments = dirParts.slice(0, 2).map((part, index) => ({
|
|
297
|
-
key: dirParts.slice(0, index + 1).join("/"),
|
|
298
|
-
label: labelForPathSegment(part),
|
|
299
|
-
sortOrder: 0,
|
|
300
|
-
}));
|
|
301
|
-
return {
|
|
302
|
-
segments,
|
|
303
|
-
pageSortKey: normalized,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
function buildTreeFromAssignments(assignments) {
|
|
307
|
-
const roots = new Map();
|
|
308
|
-
for (const { page, assignment } of assignments) {
|
|
309
|
-
if (assignment.segments.length === 0) {
|
|
310
|
-
const rootKey = "__root_pages__";
|
|
311
|
-
const root = roots.get(rootKey) ?? createMutableSubgroup(rootKey, "", 0);
|
|
312
|
-
root.pages.push(toNavPage(page, assignment.displayTitle));
|
|
313
|
-
roots.set(rootKey, root);
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
let level = roots;
|
|
317
|
-
let parent;
|
|
318
|
-
for (const [index, segment] of assignment.segments.entries()) {
|
|
319
|
-
const existing = level.get(segment.key);
|
|
320
|
-
const node = existing ??
|
|
321
|
-
createMutableSubgroup(segment.key, segment.label, segment.sortOrder);
|
|
322
|
-
if (!existing) {
|
|
323
|
-
level.set(segment.key, node);
|
|
324
|
-
if (parent) {
|
|
325
|
-
parent.children.set(segment.key, node);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
if (index === assignment.segments.length - 1) {
|
|
329
|
-
node.pages.push(toNavPage(page, assignment.displayTitle));
|
|
330
|
-
node.pageSortKeys.push(assignment.pageSortKey);
|
|
331
|
-
}
|
|
332
|
-
parent = node;
|
|
333
|
-
level = node.children;
|
|
257
|
+
if (dirParts.length > 1) {
|
|
258
|
+
return resolveL0Segments(normalized, "bmad-output");
|
|
334
259
|
}
|
|
260
|
+
return ["other"];
|
|
335
261
|
}
|
|
336
|
-
|
|
337
|
-
const rootPages = roots.get("__root_pages__")?.pages.sort(compareNavPages) ?? [];
|
|
338
|
-
const subgroups = sortMutableSubgroups(topLevel).map(finalizeSubgroup);
|
|
339
|
-
if (rootPages.length > 0) {
|
|
340
|
-
return [
|
|
341
|
-
...subgroups,
|
|
342
|
-
{
|
|
343
|
-
key: "ungrouped",
|
|
344
|
-
label: "Ungrouped",
|
|
345
|
-
anchor: "ungrouped",
|
|
346
|
-
pageCount: rootPages.length,
|
|
347
|
-
collapsible: rootPages.length > 1,
|
|
348
|
-
open: false,
|
|
349
|
-
active: false,
|
|
350
|
-
pages: rootPages,
|
|
351
|
-
subgroups: [],
|
|
352
|
-
hasNestedSubgroups: false,
|
|
353
|
-
},
|
|
354
|
-
];
|
|
355
|
-
}
|
|
356
|
-
return subgroups;
|
|
357
|
-
}
|
|
358
|
-
function createMutableSubgroup(key, label, sortOrder) {
|
|
359
|
-
return {
|
|
360
|
-
key,
|
|
361
|
-
label,
|
|
362
|
-
sortOrder,
|
|
363
|
-
pages: [],
|
|
364
|
-
pageSortKeys: [],
|
|
365
|
-
children: new Map(),
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
function finalizeSubgroup(node) {
|
|
369
|
-
const subgroups = sortMutableSubgroups([...node.children.values()]).map(finalizeSubgroup);
|
|
370
|
-
const pages = sortNavPages(node.pages);
|
|
371
|
-
const pageCount = pages.length + subgroups.reduce((sum, child) => sum + child.pageCount, 0);
|
|
372
|
-
return {
|
|
373
|
-
key: node.key,
|
|
374
|
-
label: node.label,
|
|
375
|
-
anchor: slugify(node.key),
|
|
376
|
-
pageCount,
|
|
377
|
-
collapsible: pageCount > 1 || subgroups.length > 0,
|
|
378
|
-
open: false,
|
|
379
|
-
active: false,
|
|
380
|
-
pages,
|
|
381
|
-
subgroups,
|
|
382
|
-
hasNestedSubgroups: subgroups.length > 0,
|
|
383
|
-
};
|
|
262
|
+
return null;
|
|
384
263
|
}
|
|
385
|
-
function
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
subgroups: nested,
|
|
392
|
-
hasNestedSubgroups: nested.length > 0,
|
|
393
|
-
};
|
|
394
|
-
if (current.subgroups.length === 1 &&
|
|
395
|
-
current.pages.length === 0 &&
|
|
396
|
-
current.subgroups[0]) {
|
|
397
|
-
const child = current.subgroups[0];
|
|
398
|
-
current = {
|
|
399
|
-
...child,
|
|
400
|
-
key: `${current.key}-${child.key}`,
|
|
401
|
-
label: `${current.label} › ${child.label}`,
|
|
402
|
-
anchor: slugify(`${current.key}-${child.key}`),
|
|
403
|
-
};
|
|
264
|
+
function resolveL0Segments(normalized, categoryKey) {
|
|
265
|
+
const prefix = CATEGORY_PATH_PREFIXES[categoryKey];
|
|
266
|
+
if (categoryKey === "other") {
|
|
267
|
+
const parts = normalized.split("/");
|
|
268
|
+
if (parts.length <= 1) {
|
|
269
|
+
return [];
|
|
404
270
|
}
|
|
405
|
-
|
|
406
|
-
|
|
271
|
+
return parts.slice(0, Math.min(2, parts.length - 1));
|
|
272
|
+
}
|
|
273
|
+
if (prefix === undefined) {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
if (prefix === "") {
|
|
277
|
+
const parts = normalized.split("/");
|
|
278
|
+
if (parts.length <= 1) {
|
|
279
|
+
return [];
|
|
407
280
|
}
|
|
408
|
-
|
|
281
|
+
return parts.slice(0, Math.min(2, parts.length - 1));
|
|
409
282
|
}
|
|
410
|
-
|
|
283
|
+
if (!normalized.startsWith(prefix)) {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
const relative = normalized.slice(prefix.length);
|
|
287
|
+
const dirParts = relative.split("/");
|
|
288
|
+
dirParts.pop();
|
|
289
|
+
if (dirParts.length === 0) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
return dirParts.slice(0, 2);
|
|
411
293
|
}
|
|
412
|
-
function
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
294
|
+
function insertIntoTree(root, segments, page) {
|
|
295
|
+
const capped = segments.slice(0, 2);
|
|
296
|
+
let current = root;
|
|
297
|
+
let path = [];
|
|
298
|
+
for (let i = 0; i < capped.length; i++) {
|
|
299
|
+
const segment = capped[i];
|
|
300
|
+
path = [...path, segment];
|
|
301
|
+
const key = path.join("/");
|
|
302
|
+
if (!current.has(segment)) {
|
|
303
|
+
current.set(segment, {
|
|
304
|
+
key,
|
|
305
|
+
label: humanizeFolderLabel(segment, path),
|
|
306
|
+
pages: [],
|
|
307
|
+
children: new Map(),
|
|
308
|
+
order: subgroupOrderCounter++,
|
|
309
|
+
});
|
|
423
310
|
}
|
|
424
|
-
|
|
425
|
-
|
|
311
|
+
const node = current.get(segment);
|
|
312
|
+
if (i === capped.length - 1) {
|
|
313
|
+
node.pages.push(page);
|
|
426
314
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
pages,
|
|
430
|
-
subgroups: children,
|
|
431
|
-
pageCount,
|
|
432
|
-
collapsible: pageCount > 1 || children.length > 0,
|
|
433
|
-
hasNestedSubgroups: children.length > 0,
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
return {
|
|
437
|
-
subgroups: kept,
|
|
438
|
-
pages: promotedPages,
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
function applyNavSubgroupState(subgroups, category, activePageSlug) {
|
|
442
|
-
return subgroups.map((subgroup) => {
|
|
443
|
-
const nested = applyNavSubgroupState(subgroup.subgroups, category, activePageSlug);
|
|
444
|
-
const containsActivePage = subgroup.pages.some((page) => page.slug === activePageSlug) ||
|
|
445
|
-
nested.some((child) => child.active || child.open);
|
|
446
|
-
return {
|
|
447
|
-
...subgroup,
|
|
448
|
-
subgroups: nested,
|
|
449
|
-
hasNestedSubgroups: nested.length > 0,
|
|
450
|
-
active: containsActivePage,
|
|
451
|
-
open: containsActivePage,
|
|
452
|
-
};
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
function collectPages(subgroup) {
|
|
456
|
-
return [
|
|
457
|
-
...subgroup.pages,
|
|
458
|
-
...subgroup.subgroups.flatMap((child) => collectPages(child)),
|
|
459
|
-
];
|
|
460
|
-
}
|
|
461
|
-
function sortMutableSubgroups(nodes) {
|
|
462
|
-
return [...nodes].sort((left, right) => {
|
|
463
|
-
if (left.sortOrder !== right.sortOrder) {
|
|
464
|
-
return left.sortOrder - right.sortOrder;
|
|
315
|
+
else {
|
|
316
|
+
current = node.children;
|
|
465
317
|
}
|
|
466
|
-
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
function sortNavPages(pages) {
|
|
470
|
-
return [...pages].sort((left, right) => left.title.localeCompare(right.title));
|
|
471
|
-
}
|
|
472
|
-
function compareNavPages(left, right) {
|
|
473
|
-
return left.title.localeCompare(right.title);
|
|
474
|
-
}
|
|
475
|
-
function toNavPage(page, displayTitle) {
|
|
476
|
-
return {
|
|
477
|
-
title: displayTitle ?? page.title,
|
|
478
|
-
slug: page.slug,
|
|
479
|
-
href: `${page.slug}.html`,
|
|
480
|
-
sourcePath: page.sourcePath,
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
function extractSkillFolder(sourcePath) {
|
|
484
|
-
const normalized = sourcePath.replace(/\\/g, "/");
|
|
485
|
-
const match = normalized.match(/^\.agents\/skills\/([^/]+)\/SKILL\.md$/);
|
|
486
|
-
return match?.[1] ?? null;
|
|
318
|
+
}
|
|
487
319
|
}
|
|
488
|
-
function
|
|
320
|
+
function humanizeFolderLabel(segment, path) {
|
|
321
|
+
if (segment.startsWith("epic-")) {
|
|
322
|
+
const num = segment.slice("epic-".length);
|
|
323
|
+
return `Epic ${num}`;
|
|
324
|
+
}
|
|
489
325
|
if (FOLDER_LABELS[segment]) {
|
|
490
326
|
return FOLDER_LABELS[segment];
|
|
491
327
|
}
|
|
328
|
+
if (path[0] === "planning" && path.length === 2) {
|
|
329
|
+
return (FOLDER_LABELS[segment] ??
|
|
330
|
+
segment
|
|
331
|
+
.split("-")
|
|
332
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
333
|
+
.join(" "));
|
|
334
|
+
}
|
|
492
335
|
return segment
|
|
493
336
|
.split("-")
|
|
494
|
-
.map((
|
|
337
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
495
338
|
.join(" ");
|
|
496
339
|
}
|
|
497
|
-
function
|
|
498
|
-
const
|
|
499
|
-
if (
|
|
500
|
-
return
|
|
340
|
+
function sortImplementationStoryPages(root) {
|
|
341
|
+
const implNode = root.get("implementation-stories");
|
|
342
|
+
if (!implNode) {
|
|
343
|
+
return;
|
|
501
344
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
508
|
-
.replace(/^-+|-+$/g, "");
|
|
509
|
-
}
|
|
510
|
-
async function parseBmadHelpCsv(csvContent, projectRoot) {
|
|
511
|
-
const skills = new Map();
|
|
512
|
-
const lines = csvContent.split(/\r?\n/).filter(Boolean);
|
|
513
|
-
if (lines.length <= 1) {
|
|
514
|
-
return { skills, agents: new Map() };
|
|
515
|
-
}
|
|
516
|
-
for (const line of lines.slice(1)) {
|
|
517
|
-
const row = parseCsvLine(line);
|
|
518
|
-
if (row.length < 8) {
|
|
519
|
-
continue;
|
|
345
|
+
const epicEntries = [...implNode.children.entries()].sort(([, a], [, b]) => {
|
|
346
|
+
const epicA = parseInt(a.key.match(/epic-(\d+)/)?.[1] ?? "0", 10);
|
|
347
|
+
const epicB = parseInt(b.key.match(/epic-(\d+)/)?.[1] ?? "0", 10);
|
|
348
|
+
if (epicA !== epicB) {
|
|
349
|
+
return epicA - epicB;
|
|
520
350
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
351
|
+
return a.order - b.order;
|
|
352
|
+
});
|
|
353
|
+
implNode.children = new Map(epicEntries);
|
|
354
|
+
for (const [, epicNode] of epicEntries) {
|
|
355
|
+
epicNode.pages.sort((a, b) => {
|
|
356
|
+
const numsA = storyNumbers(a.sourcePath);
|
|
357
|
+
const numsB = storyNumbers(b.sourcePath);
|
|
358
|
+
if (numsA.epic !== numsB.epic) {
|
|
359
|
+
return numsA.epic - numsB.epic;
|
|
360
|
+
}
|
|
361
|
+
return numsA.story - numsB.story;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function finalizeSubgroups(nodes, options) {
|
|
366
|
+
const result = [];
|
|
367
|
+
const rootPromoted = [];
|
|
368
|
+
const sortedNodes = [...nodes.values()].sort((a, b) => a.order - b.order);
|
|
369
|
+
for (const node of sortedNodes) {
|
|
370
|
+
const finalized = finalizeNode(node, options);
|
|
371
|
+
rootPromoted.push(...finalized.promoted);
|
|
372
|
+
if (finalized.subgroup) {
|
|
373
|
+
result.push(finalized.subgroup);
|
|
528
374
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
375
|
+
}
|
|
376
|
+
return { subgroups: result, rootPromoted };
|
|
377
|
+
}
|
|
378
|
+
function finalizeNode(node, options) {
|
|
379
|
+
const promoted = [...node.pages];
|
|
380
|
+
const childSubgroups = [];
|
|
381
|
+
const sortedChildren = [...node.children.values()].sort((a, b) => a.order - b.order);
|
|
382
|
+
for (const child of sortedChildren) {
|
|
383
|
+
const finalizedChild = finalizeNode(child, options);
|
|
384
|
+
promoted.push(...finalizedChild.promoted);
|
|
385
|
+
if (finalizedChild.subgroup) {
|
|
386
|
+
childSubgroups.push(finalizedChild.subgroup);
|
|
537
387
|
}
|
|
538
388
|
}
|
|
539
|
-
const
|
|
540
|
-
|
|
389
|
+
const totalPageCount = promoted.length + childSubgroups.reduce((sum, sg) => sum + sg.pageCount, 0);
|
|
390
|
+
if (totalPageCount === 0) {
|
|
391
|
+
return { promoted: [] };
|
|
392
|
+
}
|
|
393
|
+
if (totalPageCount === 1 && promoted.length === 1) {
|
|
394
|
+
return { promoted };
|
|
395
|
+
}
|
|
396
|
+
const containsActive = nodeOrChildrenContainActive(promoted, childSubgroups, options);
|
|
397
|
+
const collapsible = totalPageCount > 1;
|
|
398
|
+
const subgroup = {
|
|
399
|
+
key: node.key,
|
|
400
|
+
label: node.label,
|
|
401
|
+
pageCount: totalPageCount,
|
|
402
|
+
collapsible,
|
|
403
|
+
open: containsActive,
|
|
404
|
+
pages: promoted,
|
|
405
|
+
};
|
|
406
|
+
if (childSubgroups.length > 0) {
|
|
407
|
+
subgroup.subgroups = childSubgroups;
|
|
408
|
+
applyIndexOpenState(subgroup, options);
|
|
409
|
+
}
|
|
410
|
+
if (options.indexBuild && collapsible && !containsActive) {
|
|
411
|
+
subgroup.open = false;
|
|
412
|
+
}
|
|
413
|
+
return { promoted: [], subgroup };
|
|
541
414
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
415
|
+
function applyIndexOpenState(subgroup, options) {
|
|
416
|
+
if (!options.indexBuild) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
for (const child of subgroup.subgroups ?? []) {
|
|
420
|
+
const childActive = subgroupContainsActiveInChild(child, options);
|
|
421
|
+
if (child.collapsible && !childActive) {
|
|
422
|
+
child.open = false;
|
|
547
423
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
424
|
+
applyIndexOpenState(child, options);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function nodeOrChildrenContainActive(pages, childSubgroups, options) {
|
|
428
|
+
if (!options.activePageSlug && !options.activeSourcePath) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
for (const page of pages) {
|
|
432
|
+
if (page.slug === options.activePageSlug ||
|
|
433
|
+
page.sourcePath === options.activeSourcePath) {
|
|
434
|
+
return true;
|
|
555
435
|
}
|
|
556
|
-
|
|
557
|
-
|
|
436
|
+
}
|
|
437
|
+
for (const child of childSubgroups) {
|
|
438
|
+
if (subgroupContainsActiveInChild(child, options)) {
|
|
439
|
+
return true;
|
|
558
440
|
}
|
|
559
441
|
}
|
|
560
|
-
return
|
|
442
|
+
return false;
|
|
561
443
|
}
|
|
562
|
-
function
|
|
563
|
-
if (
|
|
564
|
-
return
|
|
444
|
+
function subgroupContainsActiveInChild(subgroup, options) {
|
|
445
|
+
if (!options.activePageSlug && !options.activeSourcePath) {
|
|
446
|
+
return false;
|
|
565
447
|
}
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const pattern = new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m");
|
|
579
|
-
const match = section.match(pattern);
|
|
580
|
-
return match?.[1];
|
|
448
|
+
for (const page of subgroup.pages) {
|
|
449
|
+
if (page.slug === options.activePageSlug ||
|
|
450
|
+
page.sourcePath === options.activeSourcePath) {
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
for (const child of subgroup.subgroups ?? []) {
|
|
455
|
+
if (subgroupContainsActiveInChild(child, options)) {
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
581
460
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
461
|
+
/**
|
|
462
|
+
* Walk finalized category subgroups and return the ordered ancestor trail
|
|
463
|
+
* (outer → inner) for the active page. Does not include the page itself.
|
|
464
|
+
*/
|
|
465
|
+
export function resolveActiveSubgroupTrail(subgroups, options) {
|
|
466
|
+
const trail = [];
|
|
467
|
+
function walk(nodes) {
|
|
468
|
+
for (const node of nodes) {
|
|
469
|
+
if (!subgroupContainsActiveInChild(node, options)) {
|
|
470
|
+
continue;
|
|
592
471
|
}
|
|
593
|
-
|
|
594
|
-
|
|
472
|
+
trail.push({ key: node.key, label: node.label });
|
|
473
|
+
if (node.subgroups?.length) {
|
|
474
|
+
walk(node.subgroups);
|
|
595
475
|
}
|
|
596
|
-
|
|
476
|
+
return true;
|
|
597
477
|
}
|
|
598
|
-
|
|
599
|
-
values.push(current);
|
|
600
|
-
current = "";
|
|
601
|
-
continue;
|
|
602
|
-
}
|
|
603
|
-
current += char;
|
|
478
|
+
return false;
|
|
604
479
|
}
|
|
605
|
-
|
|
606
|
-
return
|
|
480
|
+
walk(subgroups);
|
|
481
|
+
return trail;
|
|
482
|
+
}
|
|
483
|
+
/** Exported for unit tests — maps category keys to path strip prefixes. */
|
|
484
|
+
export function categoryPathPrefix(categoryKey) {
|
|
485
|
+
return CATEGORY_PATH_PREFIXES[categoryKey];
|
|
486
|
+
}
|
|
487
|
+
/** Exported for unit tests — humanizes a folder segment. */
|
|
488
|
+
export function humanizeSegment(segment) {
|
|
489
|
+
return humanizeFolderLabel(segment, [segment]);
|
|
607
490
|
}
|
|
608
491
|
//# sourceMappingURL=nav-grouping.js.map
|