agentplugged 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +8 -2
  2. package/dist/agent-commands.js +10821 -26458
  3. package/dist/bin.js +1 -1
  4. package/dist/boundary-rules.js +1 -0
  5. package/dist/boundary-rules2.js +52 -0
  6. package/dist/cli.js +6 -1
  7. package/dist/commit-documents.js +81 -0
  8. package/dist/dist.js +22246 -11722
  9. package/dist/dist2.js +9 -0
  10. package/dist/esm.js +3 -3
  11. package/dist/forge-follow.js +4 -4
  12. package/dist/grammars/LICENSE-tree-sitter-wasm +21 -0
  13. package/dist/grammars/LICENSE-web-tree-sitter +21 -0
  14. package/dist/grammars/tree-sitter-go.wasm +0 -0
  15. package/dist/grammars/tree-sitter-java.wasm +0 -0
  16. package/dist/grammars/tree-sitter-javascript.wasm +0 -0
  17. package/dist/grammars/tree-sitter-php.wasm +0 -0
  18. package/dist/grammars/tree-sitter-python.wasm +0 -0
  19. package/dist/grammars/tree-sitter-rust.wasm +0 -0
  20. package/dist/grammars/tree-sitter-tsx.wasm +0 -0
  21. package/dist/grammars/tree-sitter-typescript.wasm +0 -0
  22. package/dist/grammars/web-tree-sitter.wasm +0 -0
  23. package/dist/hash.js +17937 -2
  24. package/dist/helpers.js +165 -156
  25. package/dist/launcher.js +3 -3
  26. package/dist/main.js +9692 -2616
  27. package/dist/map-cache.js +8096 -0
  28. package/dist/map-update.js +16593 -0
  29. package/dist/map-update2.js +1 -0
  30. package/dist/method/agents/alex/agent.yaml +2 -2
  31. package/dist/method/agents/iris/SKILL.md +7 -7
  32. package/dist/method/agents/iris/agent.yaml +4 -7
  33. package/dist/method/agents/maya/agent.yaml +1 -1
  34. package/dist/method/agents/noah/SKILL.md +41 -45
  35. package/dist/method/agents/noah/agent.yaml +1 -4
  36. package/dist/method/agents/noah/evals/routes.yaml +238 -0
  37. package/dist/method/agents/noah/references/decisions.md +1 -3
  38. package/dist/method/agents/noah/references/environment.md +24 -29
  39. package/dist/method/agents/noah/references/journey.md +1 -1
  40. package/dist/method/agents/noah/references/team.md +7 -3
  41. package/dist/method/agents/omar/SKILL.md +2 -2
  42. package/dist/method/agents/omar/agent.yaml +11 -5
  43. package/dist/method/doctor.yaml +530 -9
  44. package/dist/method/skills/audit-pillars/SKILL.md +1 -0
  45. package/dist/method/skills/audit-pillars/references/demo.md +35 -0
  46. package/dist/method/skills/audit-pillars/references/tech-debt.md +38 -0
  47. package/dist/method/skills/develop-story/SKILL.md +1 -1
  48. package/dist/method/skills/generate-knowledge-base/SKILL.md +1 -1
  49. package/dist/method/skills/ideation/SKILL.md +1 -1
  50. package/dist/method/skills/plan-sprint/SKILL.md +2 -1
  51. package/dist/method/skills/plan-sprint/references/rituals.md +40 -0
  52. package/dist/method/skills/prepare-go-live/SKILL.md +59 -0
  53. package/dist/method/skills/run-acceptance/SKILL.md +5 -5
  54. package/dist/method/skills/run-acceptance/references/template.md +1 -1
  55. package/dist/method/skills/security-review-architecture/SKILL.md +3 -4
  56. package/dist/method/skills/setup-ci/SKILL.md +2 -1
  57. package/dist/method/skills/setup-ci/assets/access-check.mjs +116 -0
  58. package/dist/method/skills/setup-ci/assets/security-github.yml +41 -0
  59. package/dist/method/skills/setup-ci/assets/security-gitlab.yml +22 -0
  60. package/dist/method/skills/setup-ci/assets/stack-dotnet-github.yml +118 -0
  61. package/dist/method/skills/setup-ci/assets/stack-dotnet-gitlab.yml +63 -0
  62. package/dist/method/skills/setup-ci/assets/stack-go-github.yml +118 -0
  63. package/dist/method/skills/setup-ci/assets/stack-go-gitlab.yml +63 -0
  64. package/dist/method/skills/setup-ci/assets/stack-jvm-gradle-github.yml +134 -0
  65. package/dist/method/skills/setup-ci/assets/stack-jvm-gradle-gitlab.yml +71 -0
  66. package/dist/method/skills/setup-ci/assets/stack-jvm-maven-github.yml +133 -0
  67. package/dist/method/skills/setup-ci/assets/stack-jvm-maven-gitlab.yml +70 -0
  68. package/dist/method/skills/setup-ci/assets/stack-python-github.yml +123 -0
  69. package/dist/method/skills/setup-ci/assets/stack-python-gitlab.yml +68 -0
  70. package/dist/method/skills/setup-ci/assets/stack-rust-github.yml +107 -0
  71. package/dist/method/skills/setup-ci/assets/stack-rust-gitlab.yml +64 -0
  72. package/dist/method/skills/setup-ci/references/security.md +39 -0
  73. package/dist/method/skills/setup-ci/references/stacks.md +22 -0
  74. package/dist/method/skills/strategy/SKILL.md +1 -2
  75. package/dist/method/skills/write-architecture/references/guard.md +2 -0
  76. package/dist/method/skills/write-brief/SKILL.md +16 -17
  77. package/dist/method/skills/write-brief/assets/brief-template.md +7 -5
  78. package/dist/method/skills/write-brief/references/brief-checklist.md +1 -1
  79. package/dist/method/skills/write-content/SKILL.md +1 -1
  80. package/dist/method/skills/write-positioning/SKILL.md +1 -1
  81. package/dist/method/skills/write-prd/SKILL.md +2 -2
  82. package/dist/method/skills/write-sales/SKILL.md +1 -1
  83. package/dist/method/skills/write-ux-spec/SKILL.md +1 -1
  84. package/dist/product-root.js +89 -20839
  85. package/dist/project-command.js +185 -917
  86. package/dist/project-command2.js +1 -1
  87. package/dist/read-command.js +3 -3
  88. package/dist/sheet-command.js +4 -3
  89. package/dist/webauthn-verify.js +1 -2
  90. package/package.json +10 -9
  91. package/dist/method/skills/round-table/SKILL.md +0 -31
  92. package/dist/method/skills/round-table/evals/cases.yaml +0 -27
  93. package/dist/method/skills/round-table/references/closing.md +0 -18
  94. package/dist/method/skills/round-table/references/guests.md +0 -17
  95. package/dist/method/skills/round-table/references/rooms.md +0 -16
  96. package/dist/method/skills/round-table/references/table.md +0 -19
@@ -1,886 +1,158 @@
1
- import { Fo as issueName, ts as ErrorCode } from "./dist.js";
2
- // ../../packages/core/dist/init/layout.js
3
- /** Folder holding the method inside the product's repository. */
4
- const METHOD_DIR = ".agentplugged";
5
- const METHOD_CONFIG_PATH = `${METHOD_DIR}/method.yaml`;
6
- const METHOD_GITIGNORE_PATH = `${METHOD_DIR}/.gitignore`;
7
- /** Copy of the shipped model table, generated by `init`; the creator's values go in `method.yaml`. */
8
- const MODELS_PATH = `${METHOD_DIR}/models.yaml`;
9
- const METHOD_CONSTITUTION_PATH = `${METHOD_DIR}/AGENTS.md`;
10
- const ROOT_CONSTITUTION_PATH = "AGENTS.md";
1
+ import { Vs as ErrorCode } from "./hash.js";
2
+ import { B as AgentPluggedError, C as nameWords, F as PROFILE_PATH, I as acceptanceText, L as parseKept, P as ACCEPTANCE_PLACES, R as renderProfile, _ as findBricks, d as bricksOfMap, r as loadCodeMap, v as matchingSources, x as stem, y as searchWords, z as withSource } from "./map-cache.js";
3
+ // ../../packages/core/dist/project-profile/repo-map.js
11
4
  /**
12
- * The detail of the constitution, generated, read on demand (corrections B; one file
13
- * per section): the `AGENTS.md` that the tools load at every request stays short.
5
+ * The map of a repository for one piece of work: the files that matter for a request, each with the
6
+ * symbols it exports (functions, classes, types) and their signatures, within a budget of tokens.
7
+ * It replaces the wide reads an agent makes to find its way. Pure and deterministic: the same
8
+ * bricks and the same request always give the same text, and the text never exceeds the budget.
9
+ *
10
+ * Files are ranked the way a reader would: the files the request names come first, then those whose
11
+ * path or symbols carry its words (a word that is everywhere weighs little), and the files the rest
12
+ * of the code leans on break ties and fill what is left.
14
13
  */
15
- const REFERENCE_DIR = `${METHOD_DIR}/reference`;
16
- /** The single file the reference was: `sync` removes it when it generated it. */
17
- const REFERENCE_LEGACY_PATH = `${METHOD_DIR}/REFERENCE.md`;
18
- /** The file of one section of the reference, by its short name (`reporting`, `tool-story`…). */
19
- const referenceFilePath = (name) => `${REFERENCE_DIR}/${name}.md`;
20
- /** Sub-folders of `.agentplugged/`, each kept in Git by a `.gitkeep`. */
21
- const METHOD_SUBDIRS = [
22
- "agents",
23
- "skills",
24
- "docs",
25
- "epics",
26
- "stories",
27
- "salle",
28
- "memory",
29
- "overrides",
30
- ".cache"
31
- ];
32
- /** Tool instruction files that may belong to the creator; `init` never creates them. */
33
- const POINTER_FILES = ["CLAUDE.md", "GEMINI.md"];
34
- // ../../packages/core/dist/errors.js
35
- /** Expected, translatable failure: `{ code, message_key, details }`. */
36
- var AgentPluggedError = class extends Error {
37
- code;
38
- message_key;
39
- details;
40
- /**
41
- * What the engine adds when it answers an agent (the gesture to make, how many times the same
42
- * refusal came): sent with the details, never part of them, so that the cause of the refusal and
43
- * the advice stay apart.
44
- */
45
- advice;
46
- constructor(code, messageKey, details = {}, advice = {}) {
47
- super(`${code}: ${messageKey}`);
48
- this.name = "AgentPluggedError";
49
- this.code = code;
50
- this.message_key = messageKey;
51
- this.details = details;
52
- this.advice = advice;
53
- }
54
- toPayload() {
55
- return {
56
- code: this.code,
57
- message_key: this.message_key,
58
- details: {
59
- ...this.details,
60
- ...this.advice
61
- }
62
- };
63
- }
64
- };
65
- /**
66
- * `details` of a field error: the English `reason`, plus `reason_key` (a message key surfaces
67
- * translate) when the reason is a known schema message, or when `reasonKey` is given.
68
- */
69
- function reasonDetails(reason, reasonKey, params = {}) {
70
- const known = issueName(reason);
71
- const key = reasonKey ?? (known === void 0 ? void 0 : `reason.${known}`);
72
- return key === void 0 ? {
73
- reason,
74
- ...params
75
- } : {
76
- reason,
77
- reason_key: key,
78
- ...params
79
- };
80
- }
81
- /** `UNEXPECTED`, with the reason it states. */
82
- function unexpected(reason) {
83
- return new AgentPluggedError(ErrorCode.UNEXPECTED, "error.unexpected", { reason });
84
- }
85
- /** `UNEXPECTED`: `what` (a port the caller did not give) is not available here. */
86
- function unavailable(what) {
87
- return unexpected(`${what} is not available here`);
88
- }
89
- // ../../packages/core/dist/reuse/tokens.js
90
- /**
91
- * The words of a source file, for any language. No parser: a scanner
92
- * by regular expressions that every C-like or scripting language fits. Comments and blanks are
93
- * dropped; identifiers become one word, numbers one word, strings one word, and the keywords (the
94
- * union of the common languages') are kept: two functions that only differ by their names and
95
- * values read the same. Statements made only of references to other files (imports) are dropped:
96
- * many files import the same things, which is not a duplicate. Pure: text in, words out.
97
- */
98
- /** The interned words of a run: one number per distinct normalised word. */
99
- var WordTable = class {
100
- #ids = /* @__PURE__ */ new Map();
101
- intern(word) {
102
- const known = this.#ids.get(word);
103
- if (known !== void 0) return known;
104
- const id = this.#ids.size + 1;
105
- this.#ids.set(word, id);
106
- return id;
107
- }
108
- };
109
- const HASH = /* @__PURE__ */ new Set([
110
- "py",
111
- "rb",
112
- "sh",
113
- "bash",
114
- "zsh",
115
- "ps1",
116
- "pl",
117
- "pm",
118
- "r",
119
- "ex",
120
- "exs",
121
- "nim",
122
- "cr",
123
- "yml",
124
- "yaml",
125
- "toml"
126
- ]);
127
- const DASH = /* @__PURE__ */ new Set([
128
- "sql",
129
- "lua",
130
- "hs",
131
- "elm",
132
- "ada"
133
- ]);
134
- const SLASH_HASH = /* @__PURE__ */ new Set([
135
- "php",
136
- "pl",
137
- "pm"
138
- ]);
139
- function styleOf(path) {
140
- const extension = path.toLowerCase().split(".").at(-1) ?? "";
141
- if (SLASH_HASH.has(extension)) return "slashhash";
142
- if (HASH.has(extension)) return "hash";
143
- return DASH.has(extension) ? "dash" : "slash";
144
- }
145
- /** The words of a text, as a set: lists of names written once, space-separated. */
146
- function wordSet(text) {
147
- return new Set(text.split(/\s+/).filter((word) => word !== ""));
148
- }
149
- const KEYWORDS = wordSet(`if else elif elsif unless for foreach while do until loop switch case default match when where
150
- break continue return yield await async function def fn func fun class struct enum interface trait
151
- impl type typedef let const var val mut static final abstract public private protected internal
152
- override virtual readonly try catch except finally rescue ensure throw throws raise new delete
153
- null nil none true false undefined this self super in of is as and or not with lambda pass defer
154
- go chan select range import from export package use using module namespace extends implements
155
- instanceof typeof void goto then begin end`);
156
- /** Words that open a statement made only of references to other files. */
157
- const IMPORTS = /* @__PURE__ */ new Set([
158
- "import",
159
- "from",
160
- "using",
161
- "package",
162
- "use",
163
- "require",
164
- "require_relative"
165
- ]);
166
- const PREPROCESSOR = /* @__PURE__ */ new Set([
167
- "include",
168
- "pragma",
169
- "import",
170
- "ifndef",
171
- "ifdef",
172
- "endif"
173
- ]);
174
- const OPERATORS = ">>>=|<<=|>>=|\\*\\*=|\\.\\.\\.|===|!==|=>|->|::|&&|\\|\\||\\?\\?|\\?\\.|==|!=|<=|>=|\\+\\+|--|\\+=|-=|\\*=|/=|%=|&=|\\|=|\\^=|<<|>>|\\*\\*|\\.\\.";
175
- function scannerOf(style) {
176
- return [
177
- "(\\s+)",
178
- `(${style === "slash" ? "//[^\\n]*|/\\*[\\s\\S]*?(?:\\*/|$)" : style === "slashhash" ? "//[^\\n]*|#[^\\n]*|/\\*[\\s\\S]*?(?:\\*/|$)" : style === "hash" ? "(?<![${])#[^\\n]*" : "--\\[\\[[\\s\\S]*?(?:\\]\\]|$)|--[^\\n]*|/\\*[\\s\\S]*?(?:\\*/|$)"})`,
179
- "(\"\"\"[\\s\\S]*?(?:\"\"\"|$)|'''[\\s\\S]*?(?:'''|$))",
180
- "(\"(?:\\\\.|[^\"\\\\\\n])*\"?|'(?:\\\\.|[^'\\\\\\n])*'?|`(?:\\\\[\\s\\S]|[^`\\\\])*`?)",
181
- "([\\p{L}_$][\\p{L}\\p{N}_$]*)",
182
- "(\\d[\\w.]*)",
183
- `(${OPERATORS}|[\\s\\S])`
184
- ].join("|");
185
- }
186
- const SCANNERS = {
187
- slash: scannerOf("slash"),
188
- slashhash: scannerOf("slashhash"),
189
- hash: scannerOf("hash"),
190
- dash: scannerOf("dash")
191
- };
192
- function countLines(chunk) {
193
- let count = 0;
194
- for (let i = chunk.indexOf("\n"); i !== -1; i = chunk.indexOf("\n", i + 1)) count += 1;
195
- return count;
196
- }
197
- function scan(text, style) {
198
- const scanner = new RegExp(SCANNERS[style], "uy");
199
- const raws = [];
200
- let line = 1;
201
- let first = true;
202
- let position = 0;
203
- while (position < text.length) {
204
- scanner.lastIndex = position;
205
- const match = scanner.exec(text);
206
- if (match === null || match[0] === "") {
207
- position += 1;
208
- continue;
209
- }
210
- const from = position;
211
- position = scanner.lastIndex;
212
- const [chunk, blank, comment, , , word, number, mark] = match;
213
- const startLine = line;
214
- line += countLines(chunk);
215
- if (blank !== void 0) {
216
- if (countLines(blank) > 0) first = true;
217
- continue;
14
+ /** Tokens the map takes by default, and the most a caller may ask. */
15
+ const MAP_TOKENS = 1500;
16
+ const MAP_MAX_TOKENS = 3e3;
17
+ /** Characters per token assumed: low on purpose, code is dense, so the real count stays under. */
18
+ const CHARS_PER_TOKEN = 3;
19
+ /** Most symbols of one file, and of a file chosen only for its place in the code. */
20
+ const SYMBOLS_PER_FILE = 10;
21
+ const SYMBOLS_PER_FILLER = 3;
22
+ /** The share of the budget the files unrelated to the request may take. */
23
+ const FILLER_SHARE = .25;
24
+ /** The words of a request read at most. */
25
+ const REQUEST_CHARS = 8e3;
26
+ const REQUEST_WORDS = 300;
27
+ /** The tokens a text takes, estimated (never fewer than the real count for code). */
28
+ function mapTokens(text) {
29
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
30
+ }
31
+ const HEADER = "Repo map (files by relevance; line: exported declaration)";
32
+ /** The words of a request: its identifiers split, folded to roots, each once. */
33
+ function requestStems(request) {
34
+ const words = request.slice(0, REQUEST_CHARS).match(/[A-Za-z][A-Za-z0-9_$]{2,}/g) ?? [];
35
+ return [...new Set(searchWords(words.slice(0, REQUEST_WORDS * 4)))].slice(0, REQUEST_WORDS);
36
+ }
37
+ function candidatesOf(bricks, request) {
38
+ const byFile = /* @__PURE__ */ new Map();
39
+ for (const brick of bricks) {
40
+ if (!brick.exported) continue;
41
+ const list = byFile.get(brick.path);
42
+ if (list === void 0) byFile.set(brick.path, [brick]);
43
+ else list.push(brick);
44
+ }
45
+ const wanted = requestStems(request);
46
+ const stems = /* @__PURE__ */ new Map();
47
+ const spread = /* @__PURE__ */ new Map();
48
+ for (const [path, list] of byFile) {
49
+ const inPath = new Set(nameWords(path).map(stem));
50
+ const names = /* @__PURE__ */ new Map();
51
+ const all = new Set(inPath);
52
+ for (const brick of list) {
53
+ const own = new Set(nameWords(brick.name).map(stem));
54
+ names.set(brick, own);
55
+ for (const word of own) all.add(word);
218
56
  }
219
- if (comment !== void 0) continue;
220
- const kind = word !== void 0 ? "word" : mark !== void 0 ? "mark" : number !== void 0 ? "number" : "string";
221
- raws.push({
222
- kind,
223
- text: word ?? mark ?? "",
224
- line: startLine,
225
- first,
226
- start: from,
227
- end: position
57
+ stems.set(path, {
58
+ path: inPath,
59
+ names
228
60
  });
229
- first = false;
230
- }
231
- return raws;
232
- }
233
- const OPENING = /* @__PURE__ */ new Set([
234
- "(",
235
- "[",
236
- "{"
237
- ]);
238
- const CLOSING = /* @__PURE__ */ new Set([
239
- ")",
240
- "]",
241
- "}"
242
- ]);
243
- /**
244
- * The indexes of the raw words that belong to a statement of references (`import x from "y"`,
245
- * `use a::{b, c};`, `#include <x>`, `from a import (b,\n c)`): from its first word to the end of its
246
- * line, or of its brackets.
247
- */
248
- function referenceWords(raws) {
249
- const skipped = /* @__PURE__ */ new Set();
250
- for (let i = 0; i < raws.length; i += 1) {
251
- const raw = raws[i];
252
- if (!raw.first || skipped.has(i)) continue;
253
- const next = raws[i + 1];
254
- let reference = false;
255
- if (raw.kind === "word" && IMPORTS.has(raw.text)) reference = !(raw.text === "using" && next?.text === "(");
256
- else if (raw.kind === "word" && raw.text === "export") reference = next?.text === "*" || next?.text === "{";
257
- else if (raw.kind === "mark" && raw.text === "#" && next?.kind === "word") reference = PREPROCESSOR.has(next.text);
258
- if (!reference) continue;
259
- let depth = 0;
260
- let j = i;
261
- for (; j < raws.length; j += 1) {
262
- const current = raws[j];
263
- if (j > i && current.line !== raws[j - 1].line && depth <= 0) break;
264
- skipped.add(j);
265
- if (OPENING.has(current.text)) depth += 1;
266
- else if (CLOSING.has(current.text)) depth -= 1;
267
- else if (current.text === ";" && depth <= 0) {
268
- j += 1;
269
- break;
61
+ for (const word of all) spread.set(word, (spread.get(word) ?? 0) + 1);
62
+ }
63
+ const total = Math.max(1, byFile.size);
64
+ const weight = (word) => Math.log(1 + total / (spread.get(word) ?? total));
65
+ const out = [];
66
+ const sameName = /* @__PURE__ */ new Map();
67
+ for (const path of byFile.keys()) {
68
+ const name = path.split("/").at(-1) ?? path;
69
+ sameName.set(name, (sameName.get(name) ?? 0) + 1);
70
+ }
71
+ for (const [path, list] of byFile) {
72
+ const known = stems.get(path);
73
+ let relevance = 0;
74
+ const matched = /* @__PURE__ */ new Map();
75
+ for (const word of wanted) {
76
+ if (known.path.has(word)) relevance += 2 * weight(word);
77
+ let named = false;
78
+ for (const [brick, own] of known.names) {
79
+ if (!own.has(word)) continue;
80
+ named = true;
81
+ matched.set(brick, (matched.get(brick) ?? 0) + weight(word));
270
82
  }
83
+ if (named) relevance += 3 * weight(word);
271
84
  }
272
- i = j - 1;
273
- }
274
- return skipped;
275
- }
276
- /**
277
- * The words of a file as they stand, comments left out, with their lines and offsets: what the
278
- * mutations need to change a comparison, a constant or a return without touching a
279
- * string or a comment. `path` tells the comment style.
280
- */
281
- function sourceWords(path, text) {
282
- return scan(text, styleOf(path));
283
- }
284
- /** The normalised words of a file, with their lines. `path` tells the comment style. */
285
- function wordsOf(path, text, table) {
286
- const raws = scan(text, styleOf(path));
287
- const skipped = referenceWords(raws);
288
- const ids = [];
289
- const lines = [];
290
- raws.forEach((raw, index) => {
291
- if (skipped.has(index)) return;
292
- let word;
293
- if (raw.kind === "word") word = KEYWORDS.has(raw.text) ? raw.text : "id";
294
- else if (raw.kind === "number") word = "num";
295
- else if (raw.kind === "string") word = "str";
296
- else word = raw.text;
297
- ids.push(table.intern(word));
298
- lines.push(raw.line);
299
- });
300
- return {
301
- ids: Uint32Array.from(ids),
302
- lines: Uint32Array.from(lines)
303
- };
304
- }
305
- // ../../packages/core/dist/reuse/code-files.js
306
- /**
307
- * Which files of a repository the duplicate detector reads: the source
308
- * code a person wrote and maintains. Tests (repetitive by nature), generated code, lockfiles,
309
- * minified bundles, binaries, documents and the folders every tool ignores are left out, whatever
310
- * the language.
311
- */
312
- const CODE_EXTENSION = wordSet(`js jsx mjs cjs ts tsx mts cts vue svelte astro py pyi rb go rs java kt kts scala groovy swift m mm
313
- c h cc cpp cxx hpp hh cs php dart lua pl pm r ex exs hs nim zig cr sh bash zsh ps1 sql`);
314
- const IGNORED_FOLDER = /* @__PURE__ */ new Set([
315
- "node_modules",
316
- "dist",
317
- "build",
318
- "out",
319
- "target",
320
- "vendor",
321
- "third_party",
322
- "thirdparty",
323
- ".git",
324
- ".next",
325
- ".nuxt",
326
- ".svelte-kit",
327
- ".turbo",
328
- ".cache",
329
- ".method",
330
- ".agentplugged",
331
- "coverage",
332
- "bin",
333
- "obj",
334
- "__pycache__",
335
- ".venv",
336
- "venv",
337
- "env",
338
- "site-packages",
339
- "generated",
340
- "__generated__",
341
- "migrations"
342
- ]);
343
- const TEST_FILE = /(?:\.(?:test|spec|e2e|cy|stories)\.|_test\.|_spec\.|(?:^|\/)test_[^/]*\.py$|(?:^|\/)(?:tests?|__tests__|__mocks__|__snapshots__|spec|specs|e2e|fixtures?|testdata|testing)\/)/i;
344
- /**
345
- * Helpers that only tests use: `__helpers__/`, `pair.test-support.ts`, a `*-testkit/` folder. They
346
- * repeat the code they stand in for by design, like the tests.
347
- */
348
- const TEST_SUPPORT = /(?:\.test-(?:support|utils|helpers)\.|(?:^|\/)(?:__helpers__|(?:[\w.]+-)?testkit)\/)/i;
349
- /** A folder starting with a dot (`.claude`, `.github`, `.husky`) holds tools and settings, not the product. */
350
- const isHidden = (folder) => folder.length > 1 && folder.startsWith(".") && folder !== "..";
351
- /** `OrderServiceTest.java`, `CartSpec.scala`: a capitalised suffix after a word (`Latest.java` is not one). */
352
- const TEST_CLASS = /[a-z0-9](?:Test|Tests|Spec)\.(?:java|kt|cs|scala|swift|php)$/;
353
- const GENERATED_FILE = /(?:\.min\.|\.bundle\.|\.generated\.|\.gen\.|\.g\.(?:cs|dart)$|\.designer\.cs$|\.pb\.go$|_pb2(?:_grpc)?\.py$|_pb\.(?:js|ts)$|\.d\.ts$|\.snap$)/i;
354
- /** The largest file read (bytes): beyond, it is a bundle or a data dump, not code a person keeps. */
355
- const MAX_FILE_BYTES = 2e5;
356
- /** True when the detector reads the file: source code, not a test, not generated, not ignored. */
357
- function isCodePath(path) {
358
- const p = path.replaceAll("\\", "/");
359
- const parts = p.split("/");
360
- const name = parts.at(-1) ?? "";
361
- const extension = name.includes(".") ? (name.split(".").at(-1) ?? "").toLowerCase() : "";
362
- if (!CODE_EXTENSION.has(extension)) return false;
363
- if (parts.slice(0, -1).some((folder) => IGNORED_FOLDER.has(folder.toLowerCase()) || isHidden(folder))) return false;
364
- return !TEST_FILE.test(p) && !TEST_SUPPORT.test(p) && !TEST_CLASS.test(name) && !GENERATED_FILE.test(name);
365
- }
366
- /**
367
- * True when a file's text is not code to compare: a binary (a NUL), or minified (very long lines).
368
- */
369
- function isReadableCode(text) {
370
- if (text.length === 0 || text.length > 2e5 || text.includes("\0")) return false;
371
- const lines = text.split("\n").length;
372
- return text.length / lines <= 300;
373
- }
374
- const TEST_NAME = /(?:\.(?:tests?|specs?|e2e|cy)\.[a-z0-9]+$|[_-](?:test|spec)\.[a-z0-9]+$|(?:^|\/)test_[^/]*\.py$)/i;
375
- /** `OrderServiceTest.java`, `CartSpec.scala`: a capitalised suffix after a word; `Spec` only where it is a test style. */
376
- const TEST_CLASS_NAME = /[a-z0-9](?:Tests?\.(?:java|kt|cs|scala|swift|php|groovy)|Spec\.(?:scala|groovy|kt))$/;
377
- const TEST_FOLDER = /(?:^|\/)(?:tests?|__tests__|spec|specs|e2e)\//i;
378
- /** Folders that hold what others wrote or a tool made: no test of the product lives there. */
379
- const NOT_THE_PRODUCT = /* @__PURE__ */ new Set([
380
- "node_modules",
381
- "dist",
382
- "vendor",
383
- "third_party",
384
- "thirdparty",
385
- ".git",
386
- "coverage",
387
- "site-packages",
388
- "__pycache__",
389
- ".venv",
390
- "venv",
391
- ".next",
392
- ".nuxt",
393
- ".svelte-kit",
394
- ".turbo",
395
- ".cache",
396
- ".agentplugged"
397
- ]);
398
- /**
399
- * True when the path is a file of tests in a language (the guard before a write): named
400
- * as one (`a.test.ts`, `a_test.go`, `test_a.py`, `ATest.java`) or, when `folders` is set, inside a
401
- * folder of tests (`tests/`, `__tests__/`, `spec/`, `e2e/`). `path` is relative to the product:
402
- * the folders above it say nothing. Generated files, snapshots and what others wrote are no test files.
403
- */
404
- function isTestPath(path, folders) {
405
- const p = path.replaceAll("\\", "/");
406
- const parts = p.split("/");
407
- const name = parts.at(-1) ?? "";
408
- const extension = name.includes(".") ? (name.split(".").at(-1) ?? "").toLowerCase() : "";
409
- if (!CODE_EXTENSION.has(extension) || GENERATED_FILE.test(name)) return false;
410
- if (parts.slice(0, -1).some((folder) => NOT_THE_PRODUCT.has(folder.toLowerCase()))) return false;
411
- return TEST_NAME.test(p) || TEST_CLASS_NAME.test(name) || folders && TEST_FOLDER.test(p);
412
- }
413
- // ../../packages/core/dist/reuse/twins.js
414
- const MODIFIERS = "(?:(?:public|private|protected|internal|static|final|abstract|virtual|override|open|async|suspend|inline|operator|sealed|synchronized|export|default|pub(?:\\([^)]*\\))?|unsafe)\\s+)";
415
- const PATTERNS = [
416
- new RegExp(`^\\s*${MODIFIERS}*function\\s*\\*?\\s*([A-Za-z_$][\\w$]*)\\s*(?:<[^>(]*>)?\\s*\\(([^)]*)\\)`),
417
- /^\s*(?:async\s+)?def\s+(?:self\.)?([A-Za-z_]\w*[?!]?)\s*(?:\(([^)]*)\))?/,
418
- /^\s*func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*\(([^)]*)\)/,
419
- new RegExp(`^\\s*${MODIFIERS}*(?:fn|fun)\\s+(?:<[^>]*>\\s*)?([A-Za-z_]\\w*)\\s*(?:<[^>(]*>)?\\s*\\(([^)]*)\\)`),
420
- /^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:async\s+)?(?:\(([^)]*)\)|([A-Za-z_$][\w$]*))\s*(?::[^=]+)?=>/,
421
- new RegExp(`^\\s*${MODIFIERS}+[\\w<>\\[\\],.?*&:\\s]+?\\s+([A-Za-z_]\\w*)\\s*\\(([^)]*)\\)\\s*(?:throws [\\w, .]+)?\\s*(?:\\{|=>|$)`),
422
- /^\s{2,}(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*(?::\s*[^{=]+)?\{\s*$/
423
- ];
424
- /** Names that say nothing about what a function does. */
425
- const GENERIC = wordSet(`constructor main run init setup start stop test render handle handler get set add remove update
426
- delete create build parse format validate check apply call exec execute map filter reduce toString
427
- equals hashCode compare close open read write load save new default index each next`);
428
- const NOT_A_FUNCTION = /* @__PURE__ */ new Set([
429
- "if",
430
- "for",
431
- "while",
432
- "switch",
433
- "catch",
434
- "return",
435
- "function",
436
- "else"
437
- ]);
438
- /** The functions a file declares, one per declaration line. */
439
- function functionsOf(path, text) {
440
- const found = [];
441
- const lines = text.split("\n");
442
- for (let index = 0; index < lines.length; index += 1) {
443
- const line = lines[index];
444
- if (line.length > 400) continue;
445
- for (const pattern of PATTERNS) {
446
- const match = pattern.exec(line);
447
- const name = match?.[1];
448
- if (match === null || name === void 0 || NOT_A_FUNCTION.has(name)) continue;
449
- const raw = match[2] ?? match[3] ?? "";
450
- found.push({
451
- name,
452
- params: paramsOf(raw, path),
453
- path,
454
- line: index + 1,
455
- nested: /^\s/.test(line)
456
- });
457
- break;
458
- }
459
- }
460
- return found;
461
- }
462
- const PARAM_MODIFIERS = /* @__PURE__ */ new Set([
463
- "final",
464
- "const",
465
- "mut",
466
- "ref",
467
- "val",
468
- "var",
469
- "readonly",
470
- "in",
471
- "out"
472
- ]);
473
- /**
474
- * The names of the parameters, not their types: `name: Type` and Go's `name Type` give the first
475
- * word, `Type name` (Java, C, C#) the last.
476
- */
477
- function paramsOf(raw, path) {
478
- const nameFirst = /\.go$/i.test(path);
479
- return raw.split(",").map((part) => {
480
- const bare = part.split("=")[0] ?? "";
481
- const words = ((bare.split(":")[0] ?? "").match(/[A-Za-z_$][\w$]*/g) ?? []).filter((word) => !PARAM_MODIFIERS.has(word));
482
- return bare.includes(":") || nameFirst ? words[0] : words.at(-1);
483
- }).filter((name) => name !== void 0 && name !== "self" && name !== "this");
484
- }
485
- /** A name in lower-case words: `getUserById` and `get_user_by_id` are the same. */
486
- function nameWords(name) {
487
- return name.replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/[^a-z\d]+/).filter((word) => word !== "");
488
- }
489
- function distance(a, b) {
490
- if (a === b) return 0;
491
- let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
492
- for (let i = 1; i <= a.length; i += 1) {
493
- const row = [i];
494
- for (let j = 1; j <= b.length; j += 1) row[j] = Math.min(previous[j] + 1, row[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
495
- previous = row;
496
- }
497
- return previous[b.length];
498
- }
499
- /** Closeness (0 to 1) of two names, once split into words. */
500
- function nameLikeness(a, b) {
501
- const left = nameWords(a).join(" ");
502
- const right = nameWords(b).join(" ");
503
- const longest = Math.max(left.length, right.length);
504
- return longest === 0 ? 0 : 1 - distance(left, right) / longest;
505
- }
506
- /** Closeness of the parameters: shared names over the names of the longer list. */
507
- function paramsLikeness(a, b) {
508
- const longest = Math.max(a.length, b.length);
509
- if (longest === 0) return 0;
510
- const names = new Set(b.map((name) => nameWords(name).join(" ")));
511
- return a.filter((name) => names.has(nameWords(name).join(" "))).length / longest;
512
- }
513
- /** Languages whose functions all live inside a class: an indented declaration is then the norm. */
514
- const CLASS_ONLY = /\.(?:java|kt|kts|scala|groovy|cs|php|swift|dart)$/i;
515
- /**
516
- * Two methods (or inner functions) of the same name in different files are the two sides of an
517
- * interface, a port and its adapter: not a function written twice. Only a declaration other files
518
- * could import counts, on one side at least.
519
- */
520
- function bothInside(a, b) {
521
- return a.nested && b.nested && !CLASS_ONLY.test(a.path) && !CLASS_ONLY.test(b.path);
522
- }
523
- function meaningful(name) {
524
- const words = nameWords(name);
525
- return !GENERIC.has(name) && (words.length >= 2 || name.length >= 8);
526
- }
527
- /**
528
- * The functions of `added` (declared on a line the change added) that look like a function of the
529
- * repository: the same name in words, or a close name with close parameters. Never the function
530
- * itself; generic names (`get`, `run`, `handle`) and short ones say nothing and are left out.
531
- */
532
- function findTwins(files, added) {
533
- const all = files.flatMap((file) => functionsOf(file.path, file.text));
534
- const fresh = all.filter((fn) => added.get(fn.path)?.has(fn.line) === true && meaningful(fn.name));
535
- if (fresh.length === 0) return [];
536
- const byWord = /* @__PURE__ */ new Map();
537
- for (const fn of all) {
538
- if (!meaningful(fn.name)) continue;
539
- for (const word of new Set(nameWords(fn.name))) {
540
- if (word.length < 3) continue;
541
- const list = byWord.get(word);
542
- if (list === void 0) byWord.set(word, [fn]);
543
- else if (list.length < 300) list.push(fn);
544
- }
545
- }
546
- const twins = [];
547
- for (const fn of fresh) {
548
- const candidates = /* @__PURE__ */ new Set();
549
- for (const word of nameWords(fn.name)) for (const other of byWord.get(word) ?? []) candidates.add(other);
550
- let best;
551
- for (const other of candidates) {
552
- if (other.path === fn.path && other.line === fn.line) continue;
553
- if (bothInside(fn, other)) continue;
554
- if (added.get(other.path)?.has(other.line) === true && (other.path < fn.path || other.path === fn.path && other.line < fn.line)) continue;
555
- const likeness = nameLikeness(fn.name, other.name);
556
- const same = nameWords(fn.name).join(" ") === nameWords(other.name).join(" ");
557
- let why;
558
- if (same) why = "same_name";
559
- else if (likeness >= .8 && paramsLikeness(fn.params, other.params) >= .5) why = "close_name";
560
- if (why === void 0) continue;
561
- const score = (same ? 2 : 1) + likeness;
562
- if (best === void 0 || score > best.score) best = {
563
- fn: other,
564
- why,
565
- score
566
- };
567
- }
568
- if (best !== void 0) twins.push({
569
- name: fn.name,
570
- path: fn.path,
571
- line: fn.line,
572
- twin: {
573
- name: best.fn.name,
574
- path: best.fn.path,
575
- line: best.fn.line
576
- },
577
- why: best.why
578
- });
579
- }
580
- return twins.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line).slice(0, 10);
581
- }
582
- // ../../packages/core/dist/project-profile/bricks.js
583
- /** Most bricks kept: past it the registry is cut, never a failure. */
584
- const MAX_BRICKS = 2e4;
585
- const SIG_MAX = 100;
586
- const TYPE_PATTERN = /^\s*(?:(?:export|default|public|private|protected|internal|pub(?:\([^)]*\))?|abstract|final|sealed|static|data|open|partial|declare)\s+)*(class|interface|struct|enum|trait|type|record|protocol|object|union)\s+([A-Za-z_$][\w$]*)/;
587
- /** Names a person would not search for (the same ones the twin detector ignores as generic). */
588
- const SKIP_NAME = /* @__PURE__ */ new Set([
589
- "constructor",
590
- "main",
591
- "default",
592
- "index"
593
- ]);
594
- /**
595
- * A function whose parameter list runs over several lines: the declaration line opens the
596
- * parenthesis and does not close it (`function f(`, `def f(`, `const f = async (`).
597
- */
598
- const OPEN_DECLARATION = /^(?:export\s+)?(?:default\s+)?(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:(?:function\s*\*?|def|func|fn|fun)\s+([A-Za-z_$][\w$]*)|(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:async\s+)?)\s*(?:<[^>(]*>)?\s*\(([^)]*)$/;
599
- /** The names of the parameters listed on the lines after an opened parenthesis. */
600
- function multilineParams(first, lines, from) {
601
- let raw = first;
602
- for (let next = from; next < Math.min(lines.length, from + 14); next += 1) {
603
- const line = lines[next];
604
- const close = line.indexOf(")");
605
- if (close !== -1) {
606
- raw += ` ${line.slice(0, close)}`;
607
- break;
608
- }
609
- raw += ` ${line}`;
610
- }
611
- return splitTopLevel(raw).map((part) => /[A-Za-z_$][\w$]*/.exec((part.split(":")[0] ?? "").trim())?.[0]).filter((name) => name !== void 0 && name !== "readonly");
612
- }
613
- /** The parts of a list split at the commas that are not inside <>, () or []. */
614
- function splitTopLevel(text) {
615
- const parts = [];
616
- let depth = 0;
617
- let current = "";
618
- for (const char of text) {
619
- if ("<([{".includes(char)) depth += 1;
620
- else if (">)]}".includes(char)) depth = Math.max(0, depth - 1);
621
- if (char === "," && depth === 0) {
622
- parts.push(current);
623
- current = "";
624
- } else current += char;
625
- }
626
- parts.push(current);
627
- return parts;
628
- }
629
- /** The bricks of one file: the functions other files could import, and the types. */
630
- function bricksOf(path, text) {
631
- const found = [];
632
- const seen = /* @__PURE__ */ new Set();
633
- const lines = text.split("\n");
634
- for (const fn of functionsOf(path, text)) {
635
- if (fn.nested || SKIP_NAME.has(fn.name)) continue;
636
- seen.add(`${fn.line}`);
637
- found.push({
638
- name: fn.name,
639
- kind: "fn",
640
- path,
641
- line: fn.line,
642
- sig: `${fn.name}(${fn.params.join(", ")})`.slice(0, SIG_MAX)
643
- });
644
- }
645
- for (let index = 0; index < lines.length; index += 1) {
646
- const line = lines[index];
647
- if (line.length > 300 || seen.has(`${index + 1}`)) continue;
648
- const open = OPEN_DECLARATION.exec(line);
649
- const opened = open?.[1] ?? open?.[2];
650
- if (open === null || opened === void 0 || SKIP_NAME.has(opened)) continue;
651
- found.push({
652
- name: opened,
653
- kind: "fn",
654
- path,
655
- line: index + 1,
656
- sig: `${opened}(${multilineParams(open[3] ?? "", lines, index + 1).join(", ")})`.slice(0, SIG_MAX)
657
- });
658
- }
659
- found.sort((a, b) => a.line - b.line);
660
- for (let index = 0; index < lines.length; index += 1) {
661
- const line = lines[index];
662
- if (line.length > 300) continue;
663
- const match = TYPE_PATTERN.exec(line);
664
- const name = match?.[2];
665
- if (match === null || name === void 0 || name.length < 3) continue;
666
- if (/^\s*[,}]/.test(line.slice(match[0].length))) continue;
667
- if (match[1] === "type" && (/^\s/.test(line) || !/^(?:export\s+)?type\s+[A-Z]/.test(line)) && !(/\.go$/.test(path) && !/^\s/.test(line))) continue;
668
- found.push({
669
- name,
670
- kind: "type",
85
+ const base = path.split("/").at(-1) ?? path;
86
+ const mentioned = request.includes(path) ? 12 : base.length >= 6 && sameName.get(base) === 1 && request.includes(base) ? 6 : 0;
87
+ const leaned = list.reduce((sum, brick) => sum + brick.refs, 0);
88
+ out.push({
671
89
  path,
672
- line: index + 1,
673
- sig: line.trim().replace(/\s*[{(=].*$/, "").slice(0, SIG_MAX)
90
+ bricks: list,
91
+ relevance,
92
+ mentioned,
93
+ centrality: Math.log(1 + leaned),
94
+ matched
674
95
  });
675
96
  }
676
- return found;
677
- }
678
- /** The registry of a set of files (tests, generated code and the like are left out). */
679
- function buildBricks(files) {
680
- const all = [];
681
- for (const file of files) {
682
- if (!isCodePath(file.path) || !isReadableCode(file.text)) continue;
683
- all.push(...bricksOf(file.path, file.text));
684
- if (all.length >= 2e4) break;
685
- }
686
- return all.slice(0, MAX_BRICKS);
687
- }
688
- /** Largest file read to build the registry (bytes). */
689
- const BRICK_FILE_BYTES = MAX_FILE_BYTES;
690
- /** Lower-case search words of a request: names are split, plurals folded, filler dropped. */
691
- function searchWords(words) {
692
- const filler = new Set("the a an of for to and or in is it does do into from with that this all any how we there already exist exists".split(" "));
693
- const out = /* @__PURE__ */ new Set();
694
- for (const word of words.flatMap((entry) => nameWords(entry))) {
695
- if (word.length < 2 || filler.has(word)) continue;
696
- out.add(stem(word));
697
- }
698
- return [...out];
699
- }
700
- /**
701
- * A word folded to its root, so that « parsing dates » finds `parseDate`: plural, `-ing`, `-ed`, a
702
- * final `e` and a doubled last letter are dropped. Both sides of a comparison go through it.
703
- */
704
- const stem = (word) => {
705
- let root = word;
706
- if (root.length > 4 && root.endsWith("ies")) root = `${root.slice(0, -3)}y`;
707
- else if (root.length > 3 && root.endsWith("s") && !root.endsWith("ss")) root = root.slice(0, -1);
708
- if (root.length > 5 && root.endsWith("ing")) root = root.slice(0, -3);
709
- else if (root.length > 4 && root.endsWith("ed")) root = root.slice(0, -2);
710
- if (root.length > 3 && root.endsWith("e")) root = root.slice(0, -1);
711
- if (root.length > 4 && /([^aeiou])\1$/.test(root) && !root.endsWith("ss")) root = root.slice(0, -1);
712
- return root;
713
- };
714
- /**
715
- * The bricks that answer a request: those whose name carries the most of its words (all of them
716
- * first), then those whose path or signature does. Nothing is returned when no word matches.
717
- */
718
- function findBricks(bricks, request) {
719
- const wanted = searchWords(request);
720
- if (wanted.length === 0) return [];
721
- const hits = [];
722
- for (const brick of bricks) {
723
- const inName = new Set(nameWords(brick.name).map(stem));
724
- const inPath = new Set(nameWords(brick.path).map(stem));
725
- const inSig = new Set(nameWords(brick.sig).map(stem));
726
- let score = 0;
727
- let named = 0;
728
- let covered = 0;
729
- for (const word of wanted) if (inName.has(word)) {
730
- score += 3;
731
- named += 1;
732
- covered += 1;
733
- } else if (inPath.has(word)) {
734
- score += 1;
735
- covered += 1;
736
- } else if (inSig.has(word)) {
737
- score += .5;
738
- covered += 1;
739
- }
740
- if (covered < Math.ceil(wanted.length * .6)) continue;
741
- if (named === 0 && wanted.length > 1) continue;
742
- if (named === wanted.length) score += 2;
743
- hits.push({
744
- brick,
745
- score
746
- });
747
- }
748
- return hits.sort((a, b) => b.score - a.score || a.brick.path.localeCompare(b.brick.path) || a.brick.line - b.brick.line).slice(0, 8);
749
- }
750
- /** A single source that answers a request: every word of its concept is in the request. */
751
- function matchingSources(sources, request) {
752
- const wanted = new Set(searchWords(request));
753
- return sources.filter((source) => {
754
- const words = searchWords([source.concept]);
755
- return words.length > 0 && words.every((word) => wanted.has(word));
756
- });
97
+ return out;
757
98
  }
758
- /** True when `path` is the owner (a file) or lies under it (a folder written with a final `/`). */
759
- function isOwner(source, path) {
760
- const owner = source.owner.replaceAll("\\", "/");
761
- const here = path.replaceAll("\\", "/");
762
- return owner.endsWith("/") ? here.startsWith(owner) : here === owner;
99
+ const scoreOf = (c) => c.relevance + c.mentioned + .3 * c.centrality;
100
+ const byRank = (a, b) => scoreOf(b) - scoreOf(a) || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
101
+ /** The symbols of a file worth showing, best first: those the request names, then those most used. */
102
+ function symbolsOf(candidate, cap) {
103
+ return [...candidate.bricks].sort((a, b) => (candidate.matched.get(b) ?? 0) - (candidate.matched.get(a) ?? 0) || b.refs - a.refs || a.line - b.line).slice(0, cap);
763
104
  }
105
+ const lineOf = (brick) => ` ${brick.line}: ${brick.decl}`;
764
106
  /**
765
- * The functions of `added` (a path and its added lines) whose name carries every word of a single
766
- * source's concept while their file is not its owner: the concept has one home, and this is a second.
107
+ * The map of `bricks` for `request` (a story's title and sheet, a sentence), within `budget` tokens.
108
+ * Undefined when there is nothing exported to show.
767
109
  */
768
- function sourceClashes(files, added, sources) {
769
- if (sources.length === 0) return [];
770
- const clashes = [];
771
- for (const file of files) {
772
- const lines = added.get(file.path);
773
- if (lines === void 0) continue;
774
- for (const brick of bricksOf(file.path, file.text)) {
775
- if (!lines.has(brick.line)) continue;
776
- const words = new Set(nameWords(brick.name).map(stem));
777
- for (const source of sources) {
778
- if (isOwner(source, brick.path)) continue;
779
- const concept = searchWords([source.concept]);
780
- if (concept.length > 0 && concept.every((word) => words.has(word))) {
781
- clashes.push({
782
- name: brick.name,
783
- path: brick.path,
784
- line: brick.line,
785
- concept: source.concept,
786
- owner: source.owner
787
- });
788
- break;
789
- }
110
+ function repoMapOf(bricks, request, budget = MAP_TOKENS) {
111
+ const tokensLeft = Math.min(MAP_MAX_TOKENS, Math.max(0, Number.isFinite(budget) ? Math.floor(budget) : MAP_TOKENS));
112
+ const candidates = candidatesOf(bricks, request).sort(byRank);
113
+ if (candidates.length === 0 || tokensLeft === 0) return void 0;
114
+ const related = candidates.filter((c) => c.relevance > 0 || c.mentioned > 0);
115
+ const filler = candidates.filter((c) => c.relevance === 0 && c.mentioned === 0);
116
+ const room = tokensLeft * CHARS_PER_TOKEN - 57 - 64;
117
+ const fillerRoom = related.length === 0 ? room : Math.floor(room * FILLER_SHARE);
118
+ const blocks = [];
119
+ let chars = 0;
120
+ let shown = 0;
121
+ let fillerChars = 0;
122
+ let symbols = 0;
123
+ let left = candidates.length;
124
+ const take = (list, cap, limit, isFiller) => {
125
+ for (const candidate of list) {
126
+ const lines = symbolsOf(candidate, cap);
127
+ const fitting = [];
128
+ let size = candidate.path.length + 1;
129
+ for (const brick of lines) {
130
+ const add = lineOf(brick).length + 1;
131
+ if ((isFiller ? fillerChars : chars) + size + add > limit || chars + size + add > room) break;
132
+ fitting.push(brick);
133
+ size += add;
790
134
  }
135
+ if (fitting.length === 0) continue;
136
+ fitting.sort((a, b) => a.line - b.line);
137
+ blocks.push(`${candidate.path}\n${fitting.map(lineOf).join("\n")}`);
138
+ chars += size;
139
+ if (isFiller) fillerChars += size;
140
+ shown += 1;
141
+ symbols += fitting.length;
142
+ left -= 1;
791
143
  }
792
- }
793
- return clashes;
794
- }
795
- // ../../packages/core/dist/project-profile/profile-file.js
796
- /** The project profile: one short file, committed, written by the engine. */
797
- const PROFILE_PATH = `${METHOD_DIR}/profile.md`;
798
- /** The acceptance section for a place and how to reach it (one line each, never a secret). */
799
- function acceptanceText(place, how) {
800
- const line = how.replace(/[\r\n]+/g, " ").trim().slice(0, 200);
801
- return `- Place: ${place}${line === "" ? "" : `\n- How: ${line}`}`;
802
- }
803
- const HEADER = "<!-- Written by agentplugged: `npx agentplugged docs profile refresh` rewrites every section but Single sources, Acceptance and Notes. -->";
804
- const SOURCES_HEADING = "## Single sources";
805
- const ACCEPTANCE_HEADING = "## Acceptance";
806
- const NOTES_HEADING = "## Notes";
807
- const cell = (text) => text.replace(/[|\r\n]+/g, " ").trim();
808
- /** Rows kept per generated section: the file stays within `PROFILE_MAX_LINES` on a big product. */
809
- const ROW_CAPS = {
810
- stack: 5,
811
- commands: 10,
812
- layout: 2,
813
- conventions: 8,
814
- advice: 6
815
- };
816
- /** The generated sections of the profile, from what the scan found. */
817
- function renderProfile(scan, kept) {
818
- const lines = [
819
- HEADER,
820
- "# Project profile",
821
- ""
822
- ];
823
- const section = (title, rows, empty) => {
824
- if (rows.length === 0 && empty === void 0) return;
825
- lines.push(`## ${title}`, ...rows.length === 0 ? [empty] : rows, "");
826
- };
827
- section("Stack", scan.stack.slice(0, ROW_CAPS.stack).map((row) => `- ${row}`), "- No code yet: run `npx agentplugged docs profile refresh` once the first story is merged.");
828
- section("Commands", scan.commands.slice(0, ROW_CAPS.commands).map((command) => `- ${command.name}: \`${command.command}\` (${command.source})`), "- None found in the root manifests.");
829
- section("Layout", scan.layout.slice(0, ROW_CAPS.layout).map((row) => `- ${row}`));
830
- section("Conventions", scan.conventions.slice(0, ROW_CAPS.conventions).map((row) => `- ${row}`));
831
- section("Advice", scan.advice.slice(0, ROW_CAPS.advice).map((item) => `- ${item.advice} Why: ${item.why}`));
832
- lines.push(SOURCES_HEADING);
833
- lines.push("One owner per concept: reuse it, never rewrite it elsewhere.");
834
- lines.push("| Concept | Owner | Rule |", "| --- | --- | --- |");
835
- for (const source of kept.sources) lines.push(`| ${cell(source.concept)} | ${cell(source.owner)} | ${cell(source.rule)} |`);
836
- lines.push("", ACCEPTANCE_HEADING, kept.acceptance === "" ? "(not set)" : kept.acceptance.trimEnd());
837
- lines.push("", NOTES_HEADING, kept.notes === "" ? "(none)" : kept.notes.trimEnd(), "");
838
- return `${lines.join("\n")}`;
839
- }
840
- /** The single sources and the notes of an existing profile (what a refresh must keep). */
841
- function parseKept(text) {
842
- const body = text.replace(/\r\n/g, "\n");
843
- const start = body.indexOf(`\n${SOURCES_HEADING}\n`);
844
- const notesAt = body.lastIndexOf(`\n${NOTES_HEADING}\n`);
845
- const sources = [];
846
- if (start !== -1) {
847
- const next = body.indexOf("\n## ", start + 1);
848
- const end = next === -1 ? body.length : next;
849
- for (const row of body.slice(start, end).split("\n")) {
850
- const cells = row.split("|").slice(1, -1).map((part) => part.trim());
851
- if (cells.length !== 3 || cells[0] === "Concept" || /^-+$/.test(cells[0])) continue;
852
- const [concept, owner, rule] = cells;
853
- if (concept !== "" && owner !== "") sources.push({
854
- concept,
855
- owner,
856
- rule
857
- });
858
- }
859
- }
860
- const acceptanceAt = body.indexOf(`\n${ACCEPTANCE_HEADING}\n`);
861
- let acceptance = "";
862
- if (acceptanceAt !== -1) {
863
- const from = acceptanceAt + 13 + 2;
864
- const stop = body.indexOf("\n## ", from);
865
- acceptance = body.slice(from, stop === -1 ? body.length : stop).trim();
866
- if (acceptance === "(not set)") acceptance = "";
867
- }
868
- const afterNotes = notesAt === -1 ? "" : body.slice(notesAt + 8 + 2);
869
- const sourcesAfter = start > notesAt ? afterNotes.indexOf(`\n${SOURCES_HEADING}\n`) : -1;
870
- let notes = (sourcesAfter === -1 ? afterNotes : afterNotes.slice(0, sourcesAfter)).trim();
871
- if (notes === "(none)") notes = "";
872
- return {
873
- sources,
874
- acceptance,
875
- notes
876
144
  };
877
- }
878
- /** The same profile with one single source added or replaced (the concept names it, case apart). */
879
- function withSource(kept, source) {
880
- const same = (candidate) => candidate.concept.toLowerCase() === source.concept.toLowerCase();
145
+ take(related, SYMBOLS_PER_FILE, room, false);
146
+ take(filler, SYMBOLS_PER_FILLER, fillerRoom, true);
147
+ if (shown === 0) return void 0;
148
+ const footer = left > 0 ? `\n+${left} more files` : "";
149
+ const text = `${HEADER}\n${blocks.join("\n")}${footer}`;
881
150
  return {
882
- ...kept,
883
- sources: [...kept.sources.filter((item) => !same(item)), source]
151
+ text,
152
+ tokens: mapTokens(text),
153
+ files: shown,
154
+ symbols,
155
+ omitted_files: left
884
156
  };
885
157
  }
886
158
  // ../../packages/core/dist/project-profile/scan.js
@@ -1309,8 +581,6 @@ async function scanProject(files) {
1309
581
  return out;
1310
582
  }
1311
583
  // ../../packages/core/dist/project-profile/project-command.js
1312
- /** Where the engine keeps the registry it read: the machine's memory, outside Git. */
1313
- const BRICKS_CACHE_PATH = `${METHOD_DIR}/.cache/bricks.json`;
1314
584
  const invalid = (reason) => new AgentPluggedError(ErrorCode.INVALID_ARGUMENTS, "error.invalid_arguments", { reason });
1315
585
  async function readText(context, path) {
1316
586
  try {
@@ -1326,11 +596,12 @@ async function singleSourcesOf(context) {
1326
596
  }
1327
597
  /** The profile file for a scan, the parts people wrote kept from `current`. */
1328
598
  function profileFile(scan, current) {
1329
- return renderProfile(scan, current === void 0 ? {
599
+ const kept = current === void 0 ? {
1330
600
  sources: [],
1331
601
  acceptance: "",
1332
602
  notes: ""
1333
- } : parseKept(current));
603
+ } : parseKept(current);
604
+ return renderProfile(scan, kept);
1334
605
  }
1335
606
  /**
1336
607
  * `docs profile [refresh]`: the project profile. Without `refresh`, the file as it would be written
@@ -1339,7 +610,7 @@ function profileFile(scan, current) {
1339
610
  */
1340
611
  async function showProfile(context, words) {
1341
612
  if (words[0] === "accept") return declareAcceptance(context, words.slice(1));
1342
- if (words.length > 1 || words.length === 1 && words[0] !== "refresh") return invalid(`docs profile ${words.join(" ")} (profile [refresh], or profile accept <local|staging> [how])`);
613
+ if (words.length > 1 || words.length === 1 && words[0] !== "refresh") return invalid(`docs profile ${words.join(" ")} (profile [refresh], or profile accept <preview|staging|local> [how])`);
1343
614
  const current = await readText(context, PROFILE_PATH);
1344
615
  const fresh = profileFile(await scanProject(context.files), current);
1345
616
  const same = current !== void 0 && current.replace(/\r\n/g, "\n") === fresh;
@@ -1360,12 +631,12 @@ async function showProfile(context, words) {
1360
631
  } };
1361
632
  }
1362
633
  /**
1363
- * `docs profile accept <local|staging> [how…]`: where the acceptance run happens, decided with the
634
+ * `docs profile accept <preview|staging|local> [how…]`: where the acceptance run happens, decided with the
1364
635
  * creator and kept in the profile (a refresh never overwrites it).
1365
636
  */
1366
637
  async function declareAcceptance(context, words) {
1367
638
  const [place, ...how] = words;
1368
- if (place !== "local" && place !== "staging") return invalid("docs profile accept <local|staging> [how to reach it]");
639
+ if (!ACCEPTANCE_PLACES.some((known) => known === place)) return invalid("docs profile accept <preview|staging|local> [how to reach it]");
1369
640
  const current = await readText(context, PROFILE_PATH);
1370
641
  const kept = current === void 0 ? {
1371
642
  sources: [],
@@ -1384,45 +655,6 @@ async function declareAcceptance(context, words) {
1384
655
  lines: fresh.split("\n")
1385
656
  } };
1386
657
  }
1387
- /** The bricks at the commit the folder is on: read from the cache, or read again when it moved. */
1388
- async function loadBricks(context) {
1389
- if (context.codeSource === void 0 || context.docsBase === void 0) return "no_code_source";
1390
- const head = await context.docsBase.read("HEAD", [PROFILE_PATH]).catch(() => void 0);
1391
- if (head === void 0) return "no_repository";
1392
- if (!head.found) return head.reason === "no_commit" ? "no_commit" : "no_repository";
1393
- const cached = await readText(context, BRICKS_CACHE_PATH);
1394
- if (cached !== void 0) try {
1395
- const parsed = JSON.parse(cached);
1396
- if (parsed.v === 1 && parsed.commit === head.commit && Array.isArray(parsed.bricks)) return {
1397
- bricks: parsed.bricks.flatMap((row) => typeof row[0] === "string" && (row[1] === "fn" || row[1] === "type") && typeof row[2] === "string" && Number.isInteger(row[3]) && typeof row[4] === "string" ? [{
1398
- name: row[0],
1399
- kind: row[1],
1400
- path: row[2],
1401
- line: row[3],
1402
- sig: row[4]
1403
- }] : []),
1404
- commit: head.commit
1405
- };
1406
- } catch {}
1407
- const tree = await context.codeSource.read(head.commit, (path, bytes) => isCodePath(path) && bytes <= BRICK_FILE_BYTES).catch(() => void 0);
1408
- if (tree === void 0) return "no_repository";
1409
- const bricks = buildBricks(tree);
1410
- await context.files.writeText(BRICKS_CACHE_PATH, JSON.stringify({
1411
- v: 1,
1412
- commit: head.commit,
1413
- bricks: bricks.map((brick) => [
1414
- brick.name,
1415
- brick.kind,
1416
- brick.path,
1417
- brick.line,
1418
- brick.sig
1419
- ])
1420
- })).catch(() => void 0);
1421
- return {
1422
- bricks,
1423
- commit: head.commit
1424
- };
1425
- }
1426
658
  /**
1427
659
  * `docs bricks <words>`: does something like this already exist? The bricks (functions and types)
1428
660
  * whose names carry the words, best first, and the single source that owns the concept if one is
@@ -1432,17 +664,19 @@ async function loadBricks(context) {
1432
664
  async function findInRegistry(context, words) {
1433
665
  if (words[0] === "own") return declareSource(context, words.slice(1));
1434
666
  if (words.length === 0) return invalid("docs bricks <words> | docs bricks own <concept> <path>");
1435
- const named = matchingSources(await singleSourcesOf(context), words);
1436
- const loaded = await loadBricks(context);
667
+ const sources = await singleSourcesOf(context);
668
+ const named = matchingSources(sources, words);
669
+ const loaded = await loadCodeMap(context);
1437
670
  if (typeof loaded === "string") return { bricks: {
1438
671
  query: words,
1439
672
  hits: [],
1440
673
  sources: named,
1441
674
  unavailable: loaded
1442
675
  } };
676
+ const bricks = bricksOfMap(loaded);
1443
677
  return { bricks: {
1444
678
  query: words,
1445
- hits: findBricks(loaded.bricks, words).map(({ brick }) => ({
679
+ hits: findBricks(bricks, words).map(({ brick }) => ({
1446
680
  name: brick.name,
1447
681
  kind: brick.kind,
1448
682
  path: brick.path,
@@ -1451,7 +685,7 @@ async function findInRegistry(context, words) {
1451
685
  })),
1452
686
  sources: named,
1453
687
  searched: {
1454
- total: loaded.bricks.length,
688
+ total: bricks.length,
1455
689
  commit: loaded.commit.slice(0, 12)
1456
690
  }
1457
691
  } };
@@ -1505,4 +739,38 @@ async function profileBrief(context) {
1505
739
  return;
1506
740
  }
1507
741
  }
1508
- export { REFERENCE_DIR as A, METHOD_CONFIG_PATH as C, METHOD_SUBDIRS as D, METHOD_GITIGNORE_PATH as E, ROOT_CONSTITUTION_PATH as M, referenceFilePath as N, MODELS_PATH as O, unexpected as S, METHOD_DIR as T, sourceWords as _, showProfile as a, reasonDetails as b, PROFILE_PATH as c, findTwins as d, MAX_FILE_BYTES as f, WordTable as g, isTestPath as h, profileFile as i, REFERENCE_LEGACY_PATH as j, POINTER_FILES as k, renderProfile as l, isReadableCode as m, findInRegistry as n, singleSourcesOf as o, isCodePath as p, profileBrief as r, scanProject as s, BRICKS_CACHE_PATH as t, sourceClashes as u, wordsOf as v, METHOD_CONSTITUTION_PATH as w, unavailable as x, AgentPluggedError as y };
742
+ /**
743
+ * `docs map <request…>`: the map of the code for a piece of work, the same one `start_story` hands
744
+ * the agent for a story's sheet: the files that matter for the request and the signatures they
745
+ * export, within the same budget. Without a request, the files the rest of the code leans on.
746
+ */
747
+ async function showRepoMap(context, words) {
748
+ const loaded = await loadCodeMap(context);
749
+ if (typeof loaded === "string") return { map: {
750
+ tokens: 0,
751
+ files: 0,
752
+ symbols: 0,
753
+ omitted_files: 0,
754
+ unavailable: loaded
755
+ } };
756
+ return { map: repoMapOf(bricksOfMap(loaded), words.join(" "), 1500) ?? {
757
+ tokens: 0,
758
+ files: 0,
759
+ symbols: 0,
760
+ omitted_files: 0
761
+ } };
762
+ }
763
+ /**
764
+ * The map of the repository for a request (a story's sheet), read from the registry of the commit
765
+ * the folder is on: the files that matter and their exported signatures, within the budget.
766
+ * Undefined when the code cannot be read or holds nothing to show; never a failure.
767
+ */
768
+ async function repoMapFor(context, request, budget = MAP_TOKENS) {
769
+ try {
770
+ const loaded = await loadCodeMap(context);
771
+ return typeof loaded === "string" ? void 0 : repoMapOf(bricksOfMap(loaded), request, budget);
772
+ } catch {
773
+ return;
774
+ }
775
+ }
776
+ export { showProfile as a, scanProject as c, mapTokens as d, repoMapOf as f, repoMapFor as i, MAP_MAX_TOKENS as l, profileBrief as n, showRepoMap as o, profileFile as r, singleSourcesOf as s, findInRegistry as t, MAP_TOKENS as u };