@blkc-code/mapgen 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tree.js ADDED
@@ -0,0 +1,252 @@
1
+ /**
2
+ * A project as a world, before anything is placed. A folder holding an application is a
3
+ * building. A folder holding none is a road, at any depth, named by its literal path. Loose
4
+ * files sit on the road of their own folder, grouped by type. Files that look like secrets
5
+ * are kept apart, for the vault.
6
+ *
7
+ * Only names and counts are read here, and package manifests for links. Never source code.
8
+ */
9
+ import { execFileSync } from "node:child_process";
10
+ import { readdirSync } from "node:fs";
11
+ import path from "node:path";
12
+ import { DEFAULT_APP_MARKERS, IGNORE, gitignoredNames, isOwnRepo, markerMatcher, readPackage } from "./project-files.js";
13
+ export const SECRETS_GROUP = "Secrets";
14
+ /** Secrets comes first, so a file like .env.json is never left in Config. */
15
+ export const DEFAULT_FILE_GROUPS = {
16
+ [SECRETS_GROUP]: [".env", ".env.*", "*.pem", "*.key", "*.p12", "*.pfx", "id_rsa", "id_ed25519", ".npmrc", ".netrc"],
17
+ Docs: ["*.md", "*.mdx", "*.rst", "LICENSE*", "NOTICE*"],
18
+ Config: [".env.*", "*.json", "*.jsonc", "*.yaml", "*.yml", "*.toml", "*.config.*", ".*ignore", ".*rc", ".*rc.*", ".editorconfig", "Dockerfile*", "Makefile"],
19
+ Pages: ["*.html", "*.htm"],
20
+ Images: ["*.png", "*.jpg", "*.jpeg", "*.gif", "*.svg", "*.webp", "*.ico"],
21
+ Logs: ["*.log", "*.txt", "*history*"],
22
+ Code: ["*.ts", "*.tsx", "*.js", "*.mjs", "*.cjs", "*.py", "*.go", "*.rs", "*.java", "*.rb", "*.php", "*.sh", "*.css", "*.sql"],
23
+ };
24
+ const OTHER_GROUP = "Other";
25
+ /** Templates are meant to be committed, and hold no real values. */
26
+ const SECRET_TEMPLATE = /\.(example|sample|template|dist)$/i;
27
+ /** What a folder inside an application is, by its name. Anything else under src/ is a Desk. */
28
+ export const DEFAULT_PART_TYPES = {
29
+ "Server rack": ["api", "server", "servers", "backend", "supabase", "migrations", "db", "database", "prisma"],
30
+ Workbench: ["lib", "libs", "shared", "utils", "core", "common", "kernel"],
31
+ Whiteboard: ["test", "tests", "__tests__", "e2e", "spec", "specs"],
32
+ Bookshelf: ["docs", "doc", "documentation"],
33
+ Toolbox: ["scripts", "tools", "bin", "skills"],
34
+ Plants: ["public", "assets", "static", "images"],
35
+ };
36
+ const FEATURE_ITEM = "Desk";
37
+ const OTHER_ITEM = "Filing cabinet";
38
+ /** Folders inside an application that hold its parts, rather than being one. */
39
+ const PART_CONTAINERS = new Set(["src", "app", "pages", "features", "modules"]);
40
+ const MAX_ENTRIES = 400000;
41
+ const MAX_DEPTH = 12;
42
+ const clean = (value) => value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
43
+ const join = (base, name) => (base ? `${base}/${name}` : name);
44
+ const within = (rel, parent) => rel === parent || rel.startsWith(`${parent}/`);
45
+ export function scanTree(root, options = {}) {
46
+ const gitignored = gitignoredNames(root);
47
+ const skipped = new Set((options.ignore ?? []).map(clean));
48
+ const isSkipped = (rel) => skipped.has(rel) || skipped.has(path.posix.basename(rel));
49
+ const roads = (options.roads ?? []).map(clean);
50
+ /** An added road, somewhere above or below this folder, opens the way through. */
51
+ const onAddedRoad = (rel) => roads.some((road) => within(rel, road) || within(road, rel));
52
+ const markers = [...(options.replaceDefaults ? [] : DEFAULT_APP_MARKERS), ...(options.markers ?? [])].map(markerMatcher);
53
+ const includes = new Set((options.include ?? []).map(clean));
54
+ const groupMatchers = Object.entries({ ...DEFAULT_FILE_GROUPS, ...options.fileGroups })
55
+ .sort(([a], [b]) => Number(b === SECRETS_GROUP) - Number(a === SECRETS_GROUP))
56
+ .map(([name, patterns]) => ({ name, patterns: patterns.map(markerMatcher) }));
57
+ const groupOf = (file) => groupMatchers.find((group) => !(group.name === SECRETS_GROUP && SECRET_TEMPLATE.test(file)) && group.patterns.some((pattern) => pattern.test(file)))?.name ?? OTHER_GROUP;
58
+ const customParts = Object.entries(options.partTypes ?? {}).map(([item, folders]) => ({ item, folders: folders.map(clean) }));
59
+ const itemFor = (rel, nested) => {
60
+ const name = path.posix.basename(rel);
61
+ const custom = customParts.find(({ folders }) => folders.some((folder) => folder === name || rel === folder || rel.endsWith(`/${folder}`)));
62
+ if (custom)
63
+ return custom.item;
64
+ const known = Object.entries(DEFAULT_PART_TYPES).find(([, folders]) => folders.includes(name));
65
+ if (known)
66
+ return known[0];
67
+ return nested ? FEATURE_ITEM : OTHER_ITEM;
68
+ };
69
+ // What git tracks is the truth, repo by repo, since an added road can hold separate repos.
70
+ const trackedIn = new Map();
71
+ const tracked = (repo) => {
72
+ let files = trackedIn.get(repo);
73
+ if (!files) {
74
+ try {
75
+ files = execFileSync("git", ["ls-files", "-z"], { cwd: path.join(root, repo), encoding: "utf8", maxBuffer: 512 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] }).split("\0").filter(Boolean);
76
+ }
77
+ catch {
78
+ files = [];
79
+ }
80
+ trackedIn.set(repo, files);
81
+ }
82
+ return files;
83
+ };
84
+ let budget = MAX_ENTRIES;
85
+ let truncated = false;
86
+ const list = (rel) => {
87
+ try {
88
+ const entries = readdirSync(path.join(root, rel), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
89
+ budget -= entries.length;
90
+ if (budget <= 0)
91
+ truncated = true;
92
+ return entries;
93
+ }
94
+ catch {
95
+ return [];
96
+ }
97
+ };
98
+ const countOnDisk = (rel, depth = 0) => {
99
+ if (budget <= 0 || depth > MAX_DEPTH)
100
+ return 0;
101
+ let total = 0;
102
+ for (const entry of list(rel)) {
103
+ if (entry.isFile())
104
+ total += 1;
105
+ else if (entry.isDirectory() && !entry.name.startsWith(".") && !IGNORE.has(entry.name) && !gitignored.has(entry.name))
106
+ total += countOnDisk(join(rel, entry.name), depth + 1);
107
+ }
108
+ return total;
109
+ };
110
+ const countFiles = (rel, repo) => {
111
+ const inside = repo ? rel.slice(repo.length + 1) : rel;
112
+ const prefix = inside ? `${inside}/` : "";
113
+ const count = tracked(repo).filter((file) => file.startsWith(prefix)).length;
114
+ return count > 0 ? count : countOnDisk(rel);
115
+ };
116
+ const secrets = [];
117
+ const looseFiles = (owner, files) => {
118
+ const groups = new Map();
119
+ for (const file of files) {
120
+ const rel = join(owner, file);
121
+ const node = { id: `file:${rel}`, kind: "file", name: file, path: rel, children: [] };
122
+ const group = groupOf(file);
123
+ if (group === SECRETS_GROUP) {
124
+ secrets.push(node);
125
+ continue;
126
+ }
127
+ groups.set(group, [...(groups.get(group) ?? []), node]);
128
+ }
129
+ return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, children]) => ({ id: `group:${owner}:${name}`, kind: "group", name, path: owner, children }));
130
+ };
131
+ /** A folder is shown unless it's hidden, junk, ignored, or another repo no road opened. */
132
+ const showFolder = (rel, name) => {
133
+ if (IGNORE.has(name) || isSkipped(rel))
134
+ return false;
135
+ const opened = onAddedRoad(rel);
136
+ if (name.startsWith(".") && name !== ".github" && !opened)
137
+ return false;
138
+ if (gitignored.has(name) && !opened)
139
+ return false;
140
+ return opened || !isOwnRepo(path.join(root, rel));
141
+ };
142
+ const isApp = (rel, files) => includes.has(rel) || files.some((file) => markers.some((marker) => marker.test(file)));
143
+ const partsOf = (rel, repo, nested, depth) => {
144
+ const parts = [];
145
+ for (const entry of list(rel)) {
146
+ if (!entry.isDirectory())
147
+ continue;
148
+ const child = join(rel, entry.name);
149
+ if (entry.name.startsWith(".") || !showFolder(child, entry.name))
150
+ continue;
151
+ if (PART_CONTAINERS.has(entry.name) && depth < 3) {
152
+ const inner = partsOf(child, repo, true, depth + 1);
153
+ if (inner.length > 0) {
154
+ parts.push(...inner);
155
+ continue;
156
+ }
157
+ }
158
+ const files = countFiles(child, repo);
159
+ if (files > 0)
160
+ parts.push({ id: `part:${child}`, kind: "part", name: path.posix.basename(child), path: child, files, item: itemFor(child, nested), children: [] });
161
+ }
162
+ return parts;
163
+ };
164
+ const building = (rel, repo, files) => ({
165
+ id: `building:${rel}`,
166
+ kind: "building",
167
+ name: path.posix.basename(rel) || path.basename(path.resolve(root)),
168
+ path: rel,
169
+ files: countFiles(rel, repo),
170
+ children: [...partsOf(rel, repo, false, 0), ...looseFiles(rel, files)],
171
+ });
172
+ const walk = (rel, repo, depth) => {
173
+ const entries = list(rel);
174
+ const files = entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
175
+ const children = [];
176
+ if (depth < MAX_DEPTH && budget > 0) {
177
+ for (const entry of entries) {
178
+ if (!entry.isDirectory())
179
+ continue;
180
+ const child = join(rel, entry.name);
181
+ if (!showFolder(child, entry.name))
182
+ continue;
183
+ const childRepo = isOwnRepo(path.join(root, child)) ? child : repo;
184
+ const childFiles = list(child).filter((item) => item.isFile()).map((item) => item.name);
185
+ if (isApp(child, childFiles)) {
186
+ children.push(building(child, childRepo, childFiles));
187
+ }
188
+ else {
189
+ const road = walk(child, childRepo, depth + 1);
190
+ if (road.children.length > 0)
191
+ children.push(road);
192
+ }
193
+ }
194
+ }
195
+ else if (depth >= MAX_DEPTH) {
196
+ truncated = true;
197
+ }
198
+ return {
199
+ id: `road:${rel}`,
200
+ kind: "road",
201
+ name: rel ? `${path.posix.basename(rel)}/` : "/",
202
+ path: rel,
203
+ children: [...children, ...looseFiles(rel, files)],
204
+ };
205
+ };
206
+ const hasBuildings = (node) => node.children.some((child) => child.kind === "building" || (child.kind === "road" && hasBuildings(child)));
207
+ const topFiles = list("").filter((entry) => entry.isFile()).map((entry) => entry.name);
208
+ let top = walk("", "", 0);
209
+ // With no application inside it, the whole project is one, marked or not.
210
+ if (!hasBuildings(top)) {
211
+ secrets.length = 0;
212
+ top = building("", "", topFiles);
213
+ }
214
+ const buildings = [];
215
+ const collect = (node) => {
216
+ if (node.kind === "building")
217
+ buildings.push(node);
218
+ for (const child of node.children)
219
+ if (child.kind === "road")
220
+ collect(child);
221
+ if (node.kind === "road")
222
+ for (const child of node.children)
223
+ if (child.kind === "building")
224
+ buildings.push(child);
225
+ };
226
+ collect(top);
227
+ const owners = new Map();
228
+ const manifests = new Map();
229
+ for (const node of buildings) {
230
+ const manifest = readPackage(path.join(root, node.path));
231
+ if (!manifest)
232
+ continue;
233
+ manifests.set(node.id, manifest);
234
+ if (typeof manifest["name"] === "string")
235
+ owners.set(manifest["name"], node.id);
236
+ }
237
+ const links = [];
238
+ for (const [from, manifest] of manifests) {
239
+ const seen = new Set();
240
+ for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
241
+ for (const name of Object.keys((manifest[field] ?? {}))) {
242
+ const to = owners.get(name);
243
+ if (to && to !== from && !seen.has(to)) {
244
+ seen.add(to);
245
+ links.push({ from, to });
246
+ }
247
+ }
248
+ }
249
+ }
250
+ return { root: top, secrets, links, truncated };
251
+ }
252
+ //# sourceMappingURL=tree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.js","sourceRoot":"","sources":["../src/tree.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blkc-code/mapgen",
3
- "version": "0.3.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Grows a unique map from a project, and keeps it in step as the project changes",
6
6
  "main": "dist/index.js",
@@ -27,7 +27,7 @@
27
27
  "devDependencies": {
28
28
  "typescript": "^5.9.3",
29
29
  "vitest": "^5.1.0",
30
- "@blkc-code/render-canvas": "^0.6.0",
30
+ "@blkc-code/render-canvas": "^0.7.0",
31
31
  "@types/node": "^22.9.0"
32
32
  },
33
33
  "engines": {
package/src/index.ts CHANGED
@@ -8,11 +8,16 @@
8
8
  * here, never source code.
9
9
  */
10
10
 
11
+ import { execFileSync } from "node:child_process";
11
12
  import { existsSync, readdirSync, readFileSync } from "node:fs";
12
13
  import path from "node:path";
14
+ import { DEFAULT_APP_MARKERS, IGNORE, gitignoredNames, isOwnRepo, markerMatcher, readPackage } from "./project-files.js";
13
15
  import { OFFICE_PLAN, worldPlan, type RoomRole, type Theme, type WorldPlan } from "./plan.js";
14
16
 
15
17
  export * from "./plan.js";
18
+ export * from "./project-files.js";
19
+ export * from "./tree.js";
20
+ export * from "./roads.js";
16
21
 
17
22
  export interface MapRoom {
18
23
  id: string;
@@ -28,15 +33,26 @@ export interface MapRoom {
28
33
  /** Edited by hand: the generator leaves it alone. */
29
34
  locked?: boolean;
30
35
  files?: number;
36
+ /** The road this sits on, as a path. */
37
+ road?: string;
38
+ }
39
+ export interface MapItem {
40
+ type: string;
41
+ x: number;
42
+ y: number;
43
+ room?: string;
44
+ /** The real folder or file group this stands for. */
45
+ part?: string;
46
+ /** Shown beside it on the map, for things like a group of loose files. */
47
+ label?: string;
31
48
  }
32
- export interface MapItem { type: string; x: number; y: number; room?: string }
33
49
  export interface MapDoor { x: number; y: number }
34
50
  export interface OfficeMap {
35
51
  version: 1;
36
52
  /** Which world this map is for. The office if left out. */
37
53
  world?: string;
38
- /** halls: rooms in even rows along corridors. Left out on older maps, whose rooms were placed one against another. */
39
- layout?: "halls";
54
+ /** roads: grows from the project's folders, with no size limit. halls: even rows along corridors. Left out on the oldest maps. */
55
+ layout?: "halls" | "roads";
40
56
  /** How tall the rooms are in the first pair of rows. */
41
57
  roomHeight?: number;
42
58
  seed: string;
@@ -63,7 +79,22 @@ export interface ProjectArea {
63
79
  files: number;
64
80
  /** How many other areas depend on this one. */
65
81
  dependents?: number;
82
+ /** The folder path it sits on, such as "apps/". */
83
+ street?: string;
84
+ }
85
+
86
+ /** How to read a project. This is the part of a Kit's world.json the scan needs. */
87
+ export interface ScanOptions {
88
+ /** File names that mark a folder as an application, such as "go.mod" or "*.xydsf". */
89
+ markers?: string[];
90
+ /** Use only `markers`, not the built-in ones as well. */
91
+ replaceDefaults?: boolean;
92
+ /** Folders that get a building even with no marker. */
93
+ include?: string[];
94
+ /** Folders that never get a building: a name, or a path from the top. */
95
+ ignore?: string[];
66
96
  }
97
+
67
98
  export interface ProjectScan {
68
99
  areas: ProjectArea[];
69
100
  rootFiles: number;
@@ -82,12 +113,14 @@ const TIGHT_ABOVE = 14;
82
113
  const MAX_ENTRIES = 200000;
83
114
  /** One huge folder can't use up the scan and leave the rest uncounted. */
84
115
  const MAX_ENTRIES_PER_AREA = 20000;
116
+ /** How deep to look for applications. */
117
+ const MAX_APP_DEPTH = 5;
118
+
85
119
  const MAX_DEPTH = 8;
86
- const IGNORE = new Set(["node_modules", ".git", "dist", "build", "out", "target", "coverage", ".next", ".turbo", ".cache", "vendor", "venv", "__pycache__", ".venv", ".idea", ".vscode", ".claude", ".blkc", ".gemini", ".cursor"]);
87
120
  const CONTAINERS = new Set(["src", "packages", "apps", "services", "libs", "lib", "modules", "crates", "cmd", "internal", "app"]);
88
121
 
89
- const slug = (value: string): string => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
90
- const titled = (value: string): string =>
122
+ export const slug = (value: string): string => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
123
+ export const titled = (value: string): string =>
91
124
  value.replace(/^\./, "").split(/[-_]/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(" ").slice(0, 18) || value.slice(0, 18);
92
125
 
93
126
  export function hashText(text: string): number {
@@ -97,7 +130,7 @@ export function hashText(text: string): number {
97
130
  }
98
131
 
99
132
  /** A small seeded generator, so the same project always grows the same way. */
100
- function rng(seed: string): () => number {
133
+ export function rng(seed: string): () => number {
101
134
  let state = hashText(seed) || 1;
102
135
  return () => {
103
136
  state ^= state << 13;
@@ -108,33 +141,8 @@ function rng(seed: string): () => number {
108
141
  };
109
142
  }
110
143
 
111
- /** The plain folder names in a .gitignore. Patterns with wildcards aren't followed. */
112
- export function gitignoredNames(root: string): Set<string> {
113
- const names = new Set<string>();
114
- try {
115
- for (const line of readFileSync(path.join(root, ".gitignore"), "utf8").split("\n")) {
116
- const name = line.trim().replace(/^\//, "").replace(/\/$/, "");
117
- if (name && !/[*!#/\s]/.test(name)) names.add(name);
118
- }
119
- } catch {
120
- // no .gitignore
121
- }
122
- return names;
123
- }
124
-
125
- /** A folder with its own .git is another project, even if it sits inside this one. */
126
- export const isOwnRepo = (dir: string): boolean => existsSync(path.join(dir, ".git"));
127
-
128
144
  type Json = Record<string, unknown>;
129
145
 
130
- const readPackage = (dir: string): Json | undefined => {
131
- try {
132
- return JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8")) as Json;
133
- } catch {
134
- return undefined;
135
- }
136
- };
137
-
138
146
  /** Top-level folders a workspace config says hold packages, such as tools from "tools/*". */
139
147
  function workspaceFolders(root: string): string[] {
140
148
  const globs: string[] = [];
@@ -179,8 +187,19 @@ function findLinks(root: string, areas: ProjectArea[]): { from: string; to: stri
179
187
  }
180
188
 
181
189
  /** Folder names, file counts and how the folders depend on each other. */
182
- export function scanProject(root: string): ProjectScan {
190
+ export function scanProject(root: string, options: ScanOptions = {}): ProjectScan {
183
191
  const ignored = new Set([...IGNORE, ...gitignoredNames(root)]);
192
+ const clean = (value: string): string => value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
193
+ const skipped = new Set((options.ignore ?? []).map(clean));
194
+ const isSkipped = (rel: string): boolean => skipped.has(rel) || skipped.has(path.posix.basename(rel));
195
+
196
+ // What git tracks is the truth. Build output on disk isn't part of the project.
197
+ let tracked: string[] | undefined;
198
+ try {
199
+ tracked = execFileSync("git", ["ls-files", "-z"], { cwd: root, encoding: "utf8", maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] }).split("\0").filter(Boolean);
200
+ } catch {
201
+ // not a git repo, so the folders are all there is
202
+ }
184
203
 
185
204
  let budget = MAX_ENTRIES;
186
205
  const countFiles = (dir: string, depth: number): number => {
@@ -196,7 +215,7 @@ export function scanProject(root: string): ProjectScan {
196
215
  if (--budget <= 0) break;
197
216
  if (entry.isDirectory()) {
198
217
  const child = path.join(dir, entry.name);
199
- if (!ignored.has(entry.name) && !isOwnRepo(child)) files += countFiles(child, depth + 1);
218
+ if (!ignored.has(entry.name) && !entry.name.startsWith(".") && !isOwnRepo(child)) files += countFiles(child, depth + 1);
200
219
  } else if (entry.isFile()) {
201
220
  files += 1;
202
221
  }
@@ -214,22 +233,60 @@ export function scanProject(root: string): ProjectScan {
214
233
  }
215
234
  };
216
235
 
236
+ const markers = [...(options.replaceDefaults ? [] : DEFAULT_APP_MARKERS), ...(options.markers ?? [])].map(markerMatcher);
237
+ const isApp = (dir: string): boolean => {
238
+ try {
239
+ return readdirSync(dir, { withFileTypes: true }).some((entry) => entry.isFile() && markers.some((marker) => marker.test(entry.name)));
240
+ } catch {
241
+ return false;
242
+ }
243
+ };
244
+ const below = (rel: string): string[] => subdirs(path.join(root, rel)).map((name) => (rel ? `${rel}/${name}` : name)).filter((child) => !isSkipped(child));
245
+
246
+ // An application is a building. A folder holding applications is a street. Everything else doesn't count.
247
+ const apps: string[] = [];
248
+ const findApps = (rel: string, depth: number): void => {
249
+ for (const child of below(rel)) {
250
+ if (isApp(path.join(root, child))) apps.push(child);
251
+ else if (depth < MAX_APP_DEPTH) findApps(child, depth + 1);
252
+ }
253
+ };
254
+ findApps("", 1);
255
+ const included = (options.include ?? []).map(clean).filter((rel) => !apps.includes(rel) && existsSync(path.join(root, rel)));
256
+
217
257
  const containers = new Set([...CONTAINERS, ...workspaceFolders(root)]);
218
258
  const areas: ProjectArea[] = [];
219
259
  let remaining = MAX_ENTRIES;
220
- const add = (rel: string): void => {
221
- budget = Math.min(MAX_ENTRIES_PER_AREA, remaining);
222
- const before = budget;
223
- const files = countFiles(path.join(root, rel), 0);
224
- remaining -= before - Math.max(budget, 0);
225
- if (files > 0) areas.push({ id: slug(rel), name: titled(path.basename(rel)), path: `${rel}/`, files });
260
+ const add = (rel: string, street?: string): void => {
261
+ let files = tracked ? tracked.filter((file) => file.startsWith(`${rel}/`)).length : 0;
262
+ if (files === 0) {
263
+ // New and not committed yet.
264
+ budget = Math.min(MAX_ENTRIES_PER_AREA, remaining);
265
+ const before = budget;
266
+ files = countFiles(path.join(root, rel), 0);
267
+ remaining -= before - Math.max(budget, 0);
268
+ }
269
+ if (files > 0) areas.push({ id: slug(rel), name: titled(path.basename(rel)), path: `${rel}/`, files, ...(street === undefined ? {} : { street }) });
226
270
  };
227
- for (const name of subdirs(root)) {
228
- const children = containers.has(name) ? subdirs(path.join(root, name)) : [];
229
- // One package in packages/ is still a package, but one folder in src/ is just a folder.
230
- const open = children.length >= 2 || children.some((child) => readPackage(path.join(root, name, child)));
231
- if (open) for (const child of children) add(`${name}/${child}`);
232
- else add(name);
271
+ const readsAsOneApp = apps.length + included.length <= 1 && (isApp(root) || apps.length === 0);
272
+ const insideOneApp = apps.length === 1 && included.length === 0 && !isApp(root);
273
+ if (readsAsOneApp || insideOneApp) {
274
+ // One application: its own folders are the rooms.
275
+ const base = insideOneApp ? (apps[0] as string) : "";
276
+ for (const rel of below(base)) {
277
+ const name = path.posix.basename(rel);
278
+ const children = containers.has(name) ? below(rel) : [];
279
+ // One package in packages/ is still a package, but one folder in src/ is just a folder.
280
+ const open = children.length >= 2 || children.some((child) => readPackage(path.join(root, child)));
281
+ if (open) for (const child of children) add(child);
282
+ else add(rel);
283
+ }
284
+ for (const rel of included) add(rel);
285
+ } else {
286
+ for (const rel of [...apps, ...included]) {
287
+ const parent = path.posix.dirname(rel);
288
+ add(rel, parent === "." ? "" : `${parent}/`);
289
+ }
233
290
  }
234
291
  let rootFiles = 0;
235
292
  try {
@@ -261,7 +318,7 @@ export function scanProject(root: string): ProjectScan {
261
318
  // --- What a folder looks like ---------------------------------------------------------------------------
262
319
 
263
320
  /** The look comes from a folder's own name, so everything in apps/ doesn't look like a front end. */
264
- function themeFor(folder: string, plan: WorldPlan, random: () => number): Theme {
321
+ export function themeFor(folder: string, plan: WorldPlan, random: () => number): Theme {
265
322
  const own = folder.replace(/\/$/, "").split("/").pop() ?? folder;
266
323
  const found = plan.themes.find(({ match }) => match.test(own));
267
324
  if (found) return found.theme;
@@ -279,7 +336,7 @@ function targetSize(id: string, files: number, seed: string, compact: false | nu
279
336
  };
280
337
  }
281
338
 
282
- const deskCount = (files: number): number => Math.min(9, Math.max(1, Math.round(Math.log2(files + 1) - 1)));
339
+ export const deskCount = (files: number): number => Math.min(9, Math.max(1, Math.round(Math.log2(files + 1) - 1)));
283
340
 
284
341
  // --- Geometry -------------------------------------------------------------------------------------------
285
342
 
@@ -401,7 +458,12 @@ function placeInHalls(
401
458
  related: MapRoom[],
402
459
  family: MapRoom[],
403
460
  random: () => number,
461
+ street?: string,
404
462
  ): { box: Box; door?: MapDoor; replaces?: MapRoom } | undefined {
463
+ const streetOf = (room: MapRoom): string => {
464
+ const parent = path.posix.dirname((room.paths[0] ?? "").replace(/\/$/, ""));
465
+ return parent === "." ? "" : `${parent}/`;
466
+ };
405
467
  const beside = (x: number, w: number, y: number, room: MapRoom): boolean => room.y === y && (room.x + room.w + 1 === x || x + w + 1 === room.x);
406
468
  const empty = map.rooms
407
469
  .filter((room) => room.role === "vacant" && !room.locked && room.w >= MIN_ROOM_W)
@@ -414,7 +476,9 @@ function placeInHalls(
414
476
  const open = hallRows(map).filter((row) => row.end + w <= row.limit);
415
477
  const score = (row: HallRow): number => row.end
416
478
  - (related.some((other) => other.y === row.y) ? 20 : 0)
417
- - (family.some((other) => other.y === row.y) ? 6 : 0)
479
+ - (family.some((other) => other.y === row.y) ? 30 : 0)
480
+ // A new street starts its own row, where it can.
481
+ + (street !== undefined && row.rooms.some((other) => !other.role && streetOf(other) !== street) ? 25 : 0)
418
482
  + random() * 3;
419
483
  const row = open.map((candidate) => ({ candidate, value: score(candidate) })).sort((a, b) => a.value - b.value)[0]?.candidate;
420
484
  if (row) return { box: { x: row.end, y: row.y, w, h: row.h }, door: doorInto(row.end, w, row.doorY, random) };
@@ -433,7 +497,7 @@ function placeInHalls(
433
497
  map.rooms.push({ id: "stairs", name: "", x: landing, y: top.y + HALL_H + 1, w: HALL_H, h: y - 1 - (top.y + HALL_H + 1), floor: "hall", paths: [], role: "hall" });
434
498
  map.rooms.push({ id: "hall-1", name: "", x: top.x, y, w: landing + HALL_H - top.x, h: HALL_H, floor: "hall", paths: [], role: "hall" });
435
499
  map.doors.push({ x: landing, y: top.y + HALL_H }, { x: landing, y: y - 1 });
436
- return placeInHalls(map, id, width, related, family, random);
500
+ return placeInHalls(map, id, width, related, family, random, street);
437
501
  }
438
502
 
439
503
  /** Corridors reach the end of their rows, and rows fill out to a straight edge with empty rooms. */
@@ -497,7 +561,7 @@ function reachable(rooms: Box[], doors: MapDoor[], from: Box): Set<Box> {
497
561
 
498
562
  // --- Furnishing ------------------------------------------------------------------------------------------
499
563
 
500
- function furnish(room: MapRoom, doors: MapDoor[], seed: string, plan: WorldPlan): MapItem[] {
564
+ export function furnish(room: MapRoom, doors: MapDoor[], seed: string, plan: WorldPlan): MapItem[] {
501
565
  const random = rng(`${seed}:furnish:${room.id}:${room.w}x${room.h}`);
502
566
  const blocked = new Set<string>();
503
567
  const reserved = new Set<string>();
@@ -716,7 +780,7 @@ export function evolveMap(previous: OfficeMap | undefined, scan: ProjectScan, se
716
780
  const prefer = related.length > 0 ? related : family;
717
781
  const pull = related.length > 0 ? 40 : 8;
718
782
  if (map.layout === "halls") {
719
- const spot = placeInHalls(map, area.id, wantedSize.w, related, family, rng(`${map.seed}:place:${area.id}`));
783
+ const spot = placeInHalls(map, area.id, wantedSize.w, related, family, rng(`${map.seed}:place:${area.id}`), area.street);
720
784
  if (!spot) continue; // the building is full; this folder's agents use the lobby
721
785
  if (spot.replaces) {
722
786
  const old = spot.replaces.id;
@@ -749,7 +813,22 @@ export function evolveMap(previous: OfficeMap | undefined, scan: ProjectScan, se
749
813
  if (previous) changes.push(`New room: ${area.name}`);
750
814
  }
751
815
 
752
- if (map.layout === "halls") squareUp(map, map.seed, touched);
816
+ if (map.layout === "halls") {
817
+ squareUp(map, map.seed, touched);
818
+ // A corridor is named for the literal paths along it.
819
+ const streetFor = new Map(scan.areas.map((area) => [area.id, area.street]));
820
+ for (const hall of corridors(map)) {
821
+ const streets = new Set<string>();
822
+ for (const row of hallRows(map).filter((candidate) => candidate.hall === hall)) {
823
+ for (const room of row.rooms) {
824
+ const street = streetFor.get(room.id);
825
+ if (street) streets.add(street);
826
+ }
827
+ }
828
+ hall.paths = [...streets];
829
+ hall.name = hall.paths.join(" + ").slice(0, 24);
830
+ }
831
+ }
753
832
 
754
833
  // A new door or a bigger room can change what fits, so those rooms are furnished again.
755
834
  for (const id of touched) {
package/src/plan.ts CHANGED
@@ -24,6 +24,8 @@ export interface WorldPlan {
24
24
  fallback: Theme & { floors: string[] };
25
25
  /** For the biggest rooms. */
26
26
  filler: [string, string];
27
+ /** What draws each kind of part in a building. Anything else uses the workstation. */
28
+ partItems?: Record<string, string>;
27
29
  }
28
30
 
29
31
  /** The office, and the world used when a map doesn't name one. */
@@ -59,6 +61,7 @@ export const OFFICE_PLAN: WorldPlan = {
59
61
  ],
60
62
  fallback: { floor: "", floors: ["wood", "carpet", "tile", "stone", "concrete"], items: ["plant", "shelf", "lamp"], decor: ["poster"] },
61
63
  filler: ["plant", "fern"],
64
+ partItems: { Desk: "desk", "Server rack": "server", Workbench: "table", Whiteboard: "whiteboard", Bookshelf: "shelf", Toolbox: "printer", Plants: "plant", "Filing cabinet": "shelf" },
62
65
  };
63
66
 
64
67
  const plans = new Map<string, WorldPlan>([[OFFICE_PLAN.id, OFFICE_PLAN]]);