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.
- package/README.md +8 -2
- package/dist/agent-commands.js +10821 -26458
- package/dist/bin.js +1 -1
- package/dist/boundary-rules.js +1 -0
- package/dist/boundary-rules2.js +52 -0
- package/dist/cli.js +6 -1
- package/dist/commit-documents.js +81 -0
- package/dist/dist.js +22246 -11722
- package/dist/dist2.js +9 -0
- package/dist/esm.js +3 -3
- package/dist/forge-follow.js +4 -4
- package/dist/grammars/LICENSE-tree-sitter-wasm +21 -0
- package/dist/grammars/LICENSE-web-tree-sitter +21 -0
- package/dist/grammars/tree-sitter-go.wasm +0 -0
- package/dist/grammars/tree-sitter-java.wasm +0 -0
- package/dist/grammars/tree-sitter-javascript.wasm +0 -0
- package/dist/grammars/tree-sitter-php.wasm +0 -0
- package/dist/grammars/tree-sitter-python.wasm +0 -0
- package/dist/grammars/tree-sitter-rust.wasm +0 -0
- package/dist/grammars/tree-sitter-tsx.wasm +0 -0
- package/dist/grammars/tree-sitter-typescript.wasm +0 -0
- package/dist/grammars/web-tree-sitter.wasm +0 -0
- package/dist/hash.js +17937 -2
- package/dist/helpers.js +165 -156
- package/dist/launcher.js +3 -3
- package/dist/main.js +9692 -2616
- package/dist/map-cache.js +8096 -0
- package/dist/map-update.js +16593 -0
- package/dist/map-update2.js +1 -0
- package/dist/method/agents/alex/agent.yaml +2 -2
- package/dist/method/agents/iris/SKILL.md +7 -7
- package/dist/method/agents/iris/agent.yaml +4 -7
- package/dist/method/agents/maya/agent.yaml +1 -1
- package/dist/method/agents/noah/SKILL.md +41 -45
- package/dist/method/agents/noah/agent.yaml +1 -4
- package/dist/method/agents/noah/evals/routes.yaml +238 -0
- package/dist/method/agents/noah/references/decisions.md +1 -3
- package/dist/method/agents/noah/references/environment.md +24 -29
- package/dist/method/agents/noah/references/journey.md +1 -1
- package/dist/method/agents/noah/references/team.md +7 -3
- package/dist/method/agents/omar/SKILL.md +2 -2
- package/dist/method/agents/omar/agent.yaml +11 -5
- package/dist/method/doctor.yaml +530 -9
- package/dist/method/skills/audit-pillars/SKILL.md +1 -0
- package/dist/method/skills/audit-pillars/references/demo.md +35 -0
- package/dist/method/skills/audit-pillars/references/tech-debt.md +38 -0
- package/dist/method/skills/develop-story/SKILL.md +1 -1
- package/dist/method/skills/generate-knowledge-base/SKILL.md +1 -1
- package/dist/method/skills/ideation/SKILL.md +1 -1
- package/dist/method/skills/plan-sprint/SKILL.md +2 -1
- package/dist/method/skills/plan-sprint/references/rituals.md +40 -0
- package/dist/method/skills/prepare-go-live/SKILL.md +59 -0
- package/dist/method/skills/run-acceptance/SKILL.md +5 -5
- package/dist/method/skills/run-acceptance/references/template.md +1 -1
- package/dist/method/skills/security-review-architecture/SKILL.md +3 -4
- package/dist/method/skills/setup-ci/SKILL.md +2 -1
- package/dist/method/skills/setup-ci/assets/access-check.mjs +116 -0
- package/dist/method/skills/setup-ci/assets/security-github.yml +41 -0
- package/dist/method/skills/setup-ci/assets/security-gitlab.yml +22 -0
- package/dist/method/skills/setup-ci/assets/stack-dotnet-github.yml +118 -0
- package/dist/method/skills/setup-ci/assets/stack-dotnet-gitlab.yml +63 -0
- package/dist/method/skills/setup-ci/assets/stack-go-github.yml +118 -0
- package/dist/method/skills/setup-ci/assets/stack-go-gitlab.yml +63 -0
- package/dist/method/skills/setup-ci/assets/stack-jvm-gradle-github.yml +134 -0
- package/dist/method/skills/setup-ci/assets/stack-jvm-gradle-gitlab.yml +71 -0
- package/dist/method/skills/setup-ci/assets/stack-jvm-maven-github.yml +133 -0
- package/dist/method/skills/setup-ci/assets/stack-jvm-maven-gitlab.yml +70 -0
- package/dist/method/skills/setup-ci/assets/stack-python-github.yml +123 -0
- package/dist/method/skills/setup-ci/assets/stack-python-gitlab.yml +68 -0
- package/dist/method/skills/setup-ci/assets/stack-rust-github.yml +107 -0
- package/dist/method/skills/setup-ci/assets/stack-rust-gitlab.yml +64 -0
- package/dist/method/skills/setup-ci/references/security.md +39 -0
- package/dist/method/skills/setup-ci/references/stacks.md +22 -0
- package/dist/method/skills/strategy/SKILL.md +1 -2
- package/dist/method/skills/write-architecture/references/guard.md +2 -0
- package/dist/method/skills/write-brief/SKILL.md +16 -17
- package/dist/method/skills/write-brief/assets/brief-template.md +7 -5
- package/dist/method/skills/write-brief/references/brief-checklist.md +1 -1
- package/dist/method/skills/write-content/SKILL.md +1 -1
- package/dist/method/skills/write-positioning/SKILL.md +1 -1
- package/dist/method/skills/write-prd/SKILL.md +2 -2
- package/dist/method/skills/write-sales/SKILL.md +1 -1
- package/dist/method/skills/write-ux-spec/SKILL.md +1 -1
- package/dist/product-root.js +89 -20839
- package/dist/project-command.js +185 -917
- package/dist/project-command2.js +1 -1
- package/dist/read-command.js +3 -3
- package/dist/sheet-command.js +4 -3
- package/dist/webauthn-verify.js +1 -2
- package/package.json +10 -9
- package/dist/method/skills/round-table/SKILL.md +0 -31
- package/dist/method/skills/round-table/evals/cases.yaml +0 -27
- package/dist/method/skills/round-table/references/closing.md +0 -18
- package/dist/method/skills/round-table/references/guests.md +0 -17
- package/dist/method/skills/round-table/references/rooms.md +0 -16
- package/dist/method/skills/round-table/references/table.md +0 -19
package/dist/project-command.js
CHANGED
|
@@ -1,886 +1,158 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
13
|
-
*
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
/**
|
|
19
|
-
const
|
|
20
|
-
/**
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
673
|
-
|
|
90
|
+
bricks: list,
|
|
91
|
+
relevance,
|
|
92
|
+
mentioned,
|
|
93
|
+
centrality: Math.log(1 + leaned),
|
|
94
|
+
matched
|
|
674
95
|
});
|
|
675
96
|
}
|
|
676
|
-
return
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
return
|
|
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
|
|
766
|
-
*
|
|
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
|
|
769
|
-
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
const
|
|
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
|
-
|
|
883
|
-
|
|
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
|
-
|
|
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 <
|
|
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 <
|
|
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 (
|
|
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
|
|
1436
|
-
const
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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 };
|