engineering-memory 1.10.3 → 1.10.5
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/dispatcher/sections.mjs +3 -1
- package/package.json +1 -1
- package/runtime/dist/src/git/source-snapshot.js +450 -0
- package/runtime/dist/src/index.js +2 -0
- package/runtime/dist/src/mcp/onboarding-tools.js +730 -0
- package/runtime/dist/src/mcp/questionnaire-tools.js +45 -8
- package/runtime/dist/src/mcp/server.js +2 -0
- package/runtime/dist/src/mcp/tool-definitions.js +56 -2
- package/runtime/dist/src/project/project-intake.js +406 -0
- package/runtime/dist/src/runtime/api-client.js +3 -0
- package/runtime/dist/src/runtime/bridge-service.js +318 -17
- package/runtime/dist/src/runtime/onboarding-store.js +47 -0
- package/runtime/dist/src/runtime/questionnaire-store.js +92 -6
- package/skill/SKILL.md +1 -1
- package/skill/references/lifecycle.md +45 -20
- package/skill/references/project-onboarding.md +159 -0
- package/skill/references/questionnaires.md +79 -6
- package/skill/references/scaffolding.md +13 -4
package/dispatcher/sections.mjs
CHANGED
|
@@ -23,7 +23,9 @@ Do not edit until the skill lifecycle has completed discovery, its checkpoint, a
|
|
|
23
23
|
|
|
24
24
|
Use the host's native questionnaire for every question to the user, including implementation choices, names, clarification, branch/worktree decisions and delivery. For a required decision, use \`questionnaire.ask\` to open a durable native MCP form and \`questionnaire.resume\` to return to the same unanswered question. Read the skill's questionnaires reference first. Never use request_user_input_async for a required decision: it does not wait for an answer. When the durable form is unavailable, use a blocking native control only where the host permits it: request_user_input in Codex or AskUserQuestion in Claude. Never replace the questionnaire with a chat instruction such as 'type this', 'reply yes', or 'write X if you want Y'. Do not open a survey web page. If the required native control is unavailable or prohibited for that kind of question, follow the host's tool restrictions, explain the limitation, and continue only work already authorized; do not fabricate a survey or silently choose an answer. A timeout, dismissed form, empty response or ended turn is not an answer. Keep the decision pending and resume it; do not start dependent work or report it as resolved. Existing answers remain valid through retries and handoffs.
|
|
25
25
|
|
|
26
|
-
When the repository is unbound, use the native questionnaire required by the skill. Do not silently create or bind a project. Do not open a survey web page
|
|
26
|
+
When the repository is unbound, use the native questionnaire required by the skill. Do not silently create or bind a project. Do not open a survey web page.
|
|
27
|
+
|
|
28
|
+
For project creation or adoption, read the skill's project-onboarding reference. Use read-only maturity evidence, then native selection of greenfield, exhaustive adoption or phased rework. A missing folder or Git repository uses preparation:true questionnaires before project.initialize. Recognize later memory-refresh intent semantically and confirm its scope natively; never trigger it from a fixed word list. Adoption and refresh cover architecture, state, services, network, data, screens and cross-module flows, not just page logic.`;
|
|
27
29
|
|
|
28
30
|
export const dispatcherSections = Object.freeze({
|
|
29
31
|
codex: `${shared}\n\nInvoke the installed skill as \`$engineering-memory\` when an explicit skill invocation is needed.`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.5",
|
|
4
4
|
"description": "Installs the Engineering Memory skill and its local MCP bridge. Sign in after installing; your organization and project are resolved from your account.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
2
|
+
import { canonicalPath } from '../utilities/files.js';
|
|
3
|
+
import { NativeCommandRunner } from '../utilities/process.js';
|
|
4
|
+
export class SourceSnapshot {
|
|
5
|
+
runner;
|
|
6
|
+
constructor(runner = new NativeCommandRunner()) {
|
|
7
|
+
this.runner = runner;
|
|
8
|
+
}
|
|
9
|
+
async snapshot(repoRoot, ref = 'HEAD') {
|
|
10
|
+
const root = await canonicalPath(repoRoot);
|
|
11
|
+
const commit = await this.resolveCommit(root, ref);
|
|
12
|
+
const tree = await this.git(root, ['rev-parse', `${commit}^{tree}`]);
|
|
13
|
+
assertObjectId(tree, 'Git tree');
|
|
14
|
+
const output = await this.gitRaw(root, ['ls-tree', '-r', '-z', '--full-tree', commit, '--']);
|
|
15
|
+
const binaryPaths = await this.binaryPaths(root, commit);
|
|
16
|
+
const entries = parseTree(output);
|
|
17
|
+
const generatedRoots = await this.generatedRoots(root, entries);
|
|
18
|
+
const files = [];
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const binary = entry.type === 'blob' && binaryPaths.has(entry.path);
|
|
21
|
+
files.push(classify(entry, binary, generatedRoots));
|
|
22
|
+
}
|
|
23
|
+
files.sort(pathOrder);
|
|
24
|
+
return {
|
|
25
|
+
commit,
|
|
26
|
+
treeHash: tree,
|
|
27
|
+
manifestHash: sha256(stableStringify(files)),
|
|
28
|
+
files,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async read(repoRoot, commit, path, maxBytes = 1024 * 1024) {
|
|
32
|
+
const root = await canonicalPath(repoRoot);
|
|
33
|
+
assertCommit(commit);
|
|
34
|
+
const safePath = normalizePath(path);
|
|
35
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 16 * 1024 * 1024) {
|
|
36
|
+
throw new Error('Source read bound must be between 1 byte and 16 MiB');
|
|
37
|
+
}
|
|
38
|
+
const entry = await this.entry(root, commit, safePath);
|
|
39
|
+
if (!entry)
|
|
40
|
+
throw new Error(`Source path does not exist in ${commit}: ${safePath}`);
|
|
41
|
+
const classified = classify(entry, false);
|
|
42
|
+
if (classified.excludedReason)
|
|
43
|
+
throw new Error(`Source path is excluded: ${classified.excludedReason}`);
|
|
44
|
+
if (entry.type !== 'blob' || entry.mode === '120000' || entry.mode === '160000') {
|
|
45
|
+
throw new Error(`Source path is not a regular source blob: ${safePath}`);
|
|
46
|
+
}
|
|
47
|
+
const entries = [];
|
|
48
|
+
const segments = safePath.split('/');
|
|
49
|
+
for (let depth = 0; depth < segments.length; depth += 1) {
|
|
50
|
+
const prefix = segments.slice(0, depth).join('/');
|
|
51
|
+
const tree = prefix ? `${commit}:${prefix}` : commit;
|
|
52
|
+
const siblings = parseTree(await this.gitRaw(root, ['ls-tree', '-z', tree]));
|
|
53
|
+
for (const sibling of siblings) {
|
|
54
|
+
if (/^tsconfig(?:\.[^/]+)?\.json$/.test(sibling.path))
|
|
55
|
+
entries.push({ ...sibling, path: prefix ? `${prefix}/${sibling.path}` : sibling.path });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const binaryClassified = classify(entry, (await this.binaryPaths(root, commit, safePath)).has(safePath), await this.generatedRoots(root, entries));
|
|
59
|
+
if (binaryClassified.excludedReason)
|
|
60
|
+
throw new Error(`Source path is excluded: ${binaryClassified.excludedReason}`);
|
|
61
|
+
const size = await this.blobSize(root, `${commit}:${safePath}`);
|
|
62
|
+
if (size > maxBytes)
|
|
63
|
+
throw new Error(`Source path exceeds the ${maxBytes}-byte read bound`);
|
|
64
|
+
const data = await this.gitBuffer(root, ['cat-file', 'blob', `${commit}:${safePath}`]);
|
|
65
|
+
if (data.includes(0))
|
|
66
|
+
throw new Error(`Source path is binary: ${safePath}`);
|
|
67
|
+
return data.toString('utf8');
|
|
68
|
+
}
|
|
69
|
+
compare(previous, current) {
|
|
70
|
+
const before = new Map(previous.files.map((file) => [file.path, file]));
|
|
71
|
+
const after = new Map(current.files.map((file) => [file.path, file]));
|
|
72
|
+
const added = [...after.keys()].filter((path) => !before.has(path));
|
|
73
|
+
const deleted = [...before.keys()].filter((path) => !after.has(path));
|
|
74
|
+
const unchanged = [...after.keys()].filter((path) => before.get(path)?.blobHash === after.get(path)?.blobHash);
|
|
75
|
+
const changed = [...after.keys()].filter((path) => before.has(path) && before.get(path).blobHash !== after.get(path).blobHash);
|
|
76
|
+
const oldByHash = uniqueHashMap(deleted.map((path) => before.get(path)));
|
|
77
|
+
const newByHash = uniqueHashMap(added.map((path) => after.get(path)));
|
|
78
|
+
const renamed = [...oldByHash.keys()]
|
|
79
|
+
.filter((hash) => newByHash.has(hash))
|
|
80
|
+
.map((blobHash) => ({
|
|
81
|
+
from: oldByHash.get(blobHash).path,
|
|
82
|
+
to: newByHash.get(blobHash).path,
|
|
83
|
+
blobHash,
|
|
84
|
+
}))
|
|
85
|
+
.sort((left, right) => left.from.localeCompare(right.from));
|
|
86
|
+
const renamedFrom = new Set(renamed.map((item) => item.from));
|
|
87
|
+
const renamedTo = new Set(renamed.map((item) => item.to));
|
|
88
|
+
const actualAdded = added.filter((path) => !renamedTo.has(path));
|
|
89
|
+
const actualDeleted = deleted.filter((path) => !renamedFrom.has(path));
|
|
90
|
+
return {
|
|
91
|
+
changedPaths: [
|
|
92
|
+
...new Set([
|
|
93
|
+
...changed,
|
|
94
|
+
...actualAdded,
|
|
95
|
+
...actualDeleted,
|
|
96
|
+
...renamed.map((item) => item.to),
|
|
97
|
+
...renamed.map((item) => item.from),
|
|
98
|
+
]),
|
|
99
|
+
].sort(),
|
|
100
|
+
added: actualAdded.sort(),
|
|
101
|
+
deleted: actualDeleted.sort(),
|
|
102
|
+
renamed,
|
|
103
|
+
unchanged: unchanged.sort(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async history(repoRoot, previousCommit, currentCommit = 'HEAD') {
|
|
107
|
+
const root = await canonicalPath(repoRoot);
|
|
108
|
+
assertRef(previousCommit);
|
|
109
|
+
const current = await this.resolveCommit(root, currentCommit);
|
|
110
|
+
let previous;
|
|
111
|
+
try {
|
|
112
|
+
previous = await this.resolveCommit(root, previousCommit);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return {
|
|
116
|
+
previousCommit,
|
|
117
|
+
currentCommit: current,
|
|
118
|
+
changes: emptyComparison(),
|
|
119
|
+
recentRelevantCommitIds: [],
|
|
120
|
+
fallback: 'missing-commit',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const before = await this.snapshot(root, previous);
|
|
124
|
+
const after = await this.snapshot(root, current);
|
|
125
|
+
const changes = this.compare(before, after);
|
|
126
|
+
const ancestry = await this.runner.run('git', ['merge-base', '--is-ancestor', previous, current], { cwd: root });
|
|
127
|
+
const shallow = await this.git(root, ['rev-parse', '--is-shallow-repository']);
|
|
128
|
+
if (ancestry.exitCode !== 0 && ancestry.exitCode !== 1)
|
|
129
|
+
throw new Error('Git ancestry lookup failed');
|
|
130
|
+
const fallback = shallow === 'true'
|
|
131
|
+
? 'shallow-history'
|
|
132
|
+
: ancestry.exitCode === 1
|
|
133
|
+
? 'unrelated-ancestry'
|
|
134
|
+
: undefined;
|
|
135
|
+
const relevant = await this.relevantCommitIds(root, previous, current, changes);
|
|
136
|
+
return {
|
|
137
|
+
previousCommit: previous,
|
|
138
|
+
currentCommit: current,
|
|
139
|
+
changes,
|
|
140
|
+
recentRelevantCommitIds: relevant.commitIds,
|
|
141
|
+
historyTruncated: relevant.truncated,
|
|
142
|
+
...(fallback === undefined && !relevant.truncated
|
|
143
|
+
? {}
|
|
144
|
+
: { fallback: fallback ?? 'history-truncated' }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async entry(root, commit, path) {
|
|
148
|
+
const output = await this.gitRaw(root, [
|
|
149
|
+
'--literal-pathspecs',
|
|
150
|
+
'ls-tree',
|
|
151
|
+
'-z',
|
|
152
|
+
commit,
|
|
153
|
+
'--',
|
|
154
|
+
path,
|
|
155
|
+
]);
|
|
156
|
+
const entries = parseTree(output);
|
|
157
|
+
return entries.length === 1 && entries[0].path === path ? entries[0] : null;
|
|
158
|
+
}
|
|
159
|
+
async resolveCommit(root, ref) {
|
|
160
|
+
assertRef(ref);
|
|
161
|
+
const commit = await this.git(root, [
|
|
162
|
+
'rev-parse',
|
|
163
|
+
'--verify',
|
|
164
|
+
'--end-of-options',
|
|
165
|
+
`${ref}^{commit}`,
|
|
166
|
+
]);
|
|
167
|
+
assertCommit(commit);
|
|
168
|
+
return commit;
|
|
169
|
+
}
|
|
170
|
+
async git(root, args, input) {
|
|
171
|
+
const result = await this.runner.run('git', args, { cwd: root, input });
|
|
172
|
+
if (result.exitCode !== 0)
|
|
173
|
+
throw new Error(`Git command failed: ${result.stderr.trim() || args[0]}`);
|
|
174
|
+
return result.stdout.trim();
|
|
175
|
+
}
|
|
176
|
+
async gitRaw(root, args, input) {
|
|
177
|
+
const result = await this.runner.run('git', args, { cwd: root, input });
|
|
178
|
+
if (result.exitCode !== 0)
|
|
179
|
+
throw new Error(`Git command failed: ${result.stderr.trim() || args[0]}`);
|
|
180
|
+
return result.stdout;
|
|
181
|
+
}
|
|
182
|
+
async gitBuffer(root, args) {
|
|
183
|
+
const result = await this.runner.run('git', args, { cwd: root });
|
|
184
|
+
if (result.exitCode !== 0)
|
|
185
|
+
throw new Error(`Git object read failed: ${result.stderr.trim() || args[0]}`);
|
|
186
|
+
return Buffer.from(result.stdout, 'utf8');
|
|
187
|
+
}
|
|
188
|
+
async blobSize(root, object) {
|
|
189
|
+
const result = await this.runner.run('git', ['cat-file', '--batch-check=%(objecttype) %(objectsize)'], {
|
|
190
|
+
cwd: root,
|
|
191
|
+
input: `${object}\n`,
|
|
192
|
+
});
|
|
193
|
+
const match = /^(blob|tree|commit|tag) (\d+)$/.exec(result.stdout.trim());
|
|
194
|
+
if (result.exitCode !== 0 || !match || match[1] !== 'blob')
|
|
195
|
+
throw new Error(`Git object size lookup failed: ${result.stderr.trim() || object}`);
|
|
196
|
+
return Number(match[2]);
|
|
197
|
+
}
|
|
198
|
+
async binaryPaths(root, commit, path) {
|
|
199
|
+
const emptyTree = await this.git(root, ['hash-object', '-t', 'tree', '--stdin'], '');
|
|
200
|
+
assertObjectId(emptyTree, 'Git empty tree');
|
|
201
|
+
const output = await this.gitRaw(root, [
|
|
202
|
+
'--literal-pathspecs',
|
|
203
|
+
'diff',
|
|
204
|
+
'--numstat',
|
|
205
|
+
'-z',
|
|
206
|
+
'--no-renames',
|
|
207
|
+
'--no-ext-diff',
|
|
208
|
+
'--no-textconv',
|
|
209
|
+
emptyTree,
|
|
210
|
+
commit,
|
|
211
|
+
'--',
|
|
212
|
+
...(path ? [path] : []),
|
|
213
|
+
]);
|
|
214
|
+
const binary = new Set();
|
|
215
|
+
for (const record of output.split('\0').filter((value) => value.length > 0)) {
|
|
216
|
+
const match = /^-\t-\t(.*)$/.exec(record);
|
|
217
|
+
if (match)
|
|
218
|
+
binary.add(normalizePath(match[1]));
|
|
219
|
+
}
|
|
220
|
+
return binary;
|
|
221
|
+
}
|
|
222
|
+
async relevantCommitIds(root, previous, current, changes) {
|
|
223
|
+
const paths = [...new Set(changes.changedPaths)];
|
|
224
|
+
if (paths.length === 0)
|
|
225
|
+
return { commitIds: [], truncated: false };
|
|
226
|
+
const candidateIds = [];
|
|
227
|
+
const seen = new Set();
|
|
228
|
+
let truncated = false;
|
|
229
|
+
for (const chunk of pathspecChunks(paths)) {
|
|
230
|
+
const output = await this.gitRaw(root, [
|
|
231
|
+
'--literal-pathspecs',
|
|
232
|
+
'rev-list',
|
|
233
|
+
'--full-history',
|
|
234
|
+
'--topo-order',
|
|
235
|
+
'--max-count=21',
|
|
236
|
+
`${previous}..${current}`,
|
|
237
|
+
'--',
|
|
238
|
+
...chunk,
|
|
239
|
+
]);
|
|
240
|
+
const commits = output
|
|
241
|
+
.split(/\r?\n/)
|
|
242
|
+
.map((value) => value.trim())
|
|
243
|
+
.filter(Boolean);
|
|
244
|
+
if (commits.length === 21)
|
|
245
|
+
truncated = true;
|
|
246
|
+
for (const commit of commits) {
|
|
247
|
+
assertCommit(commit);
|
|
248
|
+
if (!seen.has(commit)) {
|
|
249
|
+
seen.add(commit);
|
|
250
|
+
candidateIds.push(commit);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (candidateIds.length <= 20)
|
|
255
|
+
return { commitIds: candidateIds, truncated };
|
|
256
|
+
const orderedOutput = await this.gitRaw(root, ['rev-list', '--topo-order', '--no-walk=sorted', '--stdin'], `${candidateIds.join('\n')}\n`);
|
|
257
|
+
const ordered = orderedOutput
|
|
258
|
+
.split(/\r?\n/)
|
|
259
|
+
.map((value) => value.trim())
|
|
260
|
+
.filter(Boolean);
|
|
261
|
+
for (const commit of ordered)
|
|
262
|
+
assertCommit(commit);
|
|
263
|
+
return { commitIds: ordered.slice(0, 20), truncated: true };
|
|
264
|
+
}
|
|
265
|
+
async generatedRoots(root, entries) {
|
|
266
|
+
const roots = new Set();
|
|
267
|
+
const configs = entries.filter((entry) => entry.type === 'blob' &&
|
|
268
|
+
/^100(?:644|755)$/.test(entry.mode) &&
|
|
269
|
+
/(^|\/)tsconfig(?:\.[^/]+)?\.json$/.test(entry.path) &&
|
|
270
|
+
!classify(entry).excludedReason);
|
|
271
|
+
for (const config of configs) {
|
|
272
|
+
if ((await this.blobSize(root, config.blobHash)) > 64 * 1024)
|
|
273
|
+
continue;
|
|
274
|
+
const text = await this.gitRaw(root, ['cat-file', 'blob', config.blobHash]);
|
|
275
|
+
let value;
|
|
276
|
+
try {
|
|
277
|
+
value = JSON.parse(text);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (!isRecord(value) || !isRecord(value.compilerOptions))
|
|
283
|
+
continue;
|
|
284
|
+
const directory = config.path.slice(0, config.path.lastIndexOf('/') + 1);
|
|
285
|
+
for (const output of [value.compilerOptions.outDir, value.compilerOptions.declarationDir]) {
|
|
286
|
+
if (typeof output !== 'string')
|
|
287
|
+
continue;
|
|
288
|
+
const normalized = output.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '');
|
|
289
|
+
if (!normalized ||
|
|
290
|
+
normalized.startsWith('/') ||
|
|
291
|
+
/[:\0\r\n]/.test(normalized) ||
|
|
292
|
+
normalized.split('/').some((part) => !part || part === '.' || part === '..'))
|
|
293
|
+
continue;
|
|
294
|
+
roots.add(`${directory}${normalized}/`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return roots;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function parseTree(output) {
|
|
301
|
+
return output
|
|
302
|
+
.split('\0')
|
|
303
|
+
.filter(Boolean)
|
|
304
|
+
.map((token) => {
|
|
305
|
+
const match = /^(\d{6}) (\w+) ([0-9a-f]{40,64})\t(.+)$/.exec(token);
|
|
306
|
+
if (!match)
|
|
307
|
+
throw new Error('Git returned an invalid tree entry');
|
|
308
|
+
return {
|
|
309
|
+
mode: match[1],
|
|
310
|
+
type: match[2],
|
|
311
|
+
blobHash: match[3],
|
|
312
|
+
path: normalizePath(match[4]),
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function normalizePath(path) {
|
|
317
|
+
const normalized = path.replace(/\\/g, '/');
|
|
318
|
+
if (!normalized ||
|
|
319
|
+
normalized.startsWith('/') ||
|
|
320
|
+
/^[A-Za-z]:\//.test(normalized) ||
|
|
321
|
+
normalized.split('/').includes('..') ||
|
|
322
|
+
normalized.includes('\0')) {
|
|
323
|
+
throw new Error(`Git path is unsafe: ${path}`);
|
|
324
|
+
}
|
|
325
|
+
return normalized;
|
|
326
|
+
}
|
|
327
|
+
function assertCommit(commit) {
|
|
328
|
+
if (!/^[0-9a-f]{40}$|^[0-9a-f]{64}$/.test(commit))
|
|
329
|
+
throw new Error('Git commit must be a 40- or 64-hex object ID');
|
|
330
|
+
}
|
|
331
|
+
function classify(entry, binary = false, generatedRoots = new Set()) {
|
|
332
|
+
if (entry.mode === '120000' || entry.type === 'symlink')
|
|
333
|
+
return {
|
|
334
|
+
path: entry.path,
|
|
335
|
+
blobHash: entry.blobHash,
|
|
336
|
+
category: 'symlink',
|
|
337
|
+
excludedReason: 'symbolic link is not source text',
|
|
338
|
+
};
|
|
339
|
+
if (binary)
|
|
340
|
+
return {
|
|
341
|
+
path: entry.path,
|
|
342
|
+
blobHash: entry.blobHash,
|
|
343
|
+
category: 'binary',
|
|
344
|
+
excludedReason: 'binary content is not source text',
|
|
345
|
+
};
|
|
346
|
+
const path = entry.path.toLowerCase();
|
|
347
|
+
if (entry.mode === '160000' || entry.type === 'commit')
|
|
348
|
+
return {
|
|
349
|
+
path: entry.path,
|
|
350
|
+
blobHash: entry.blobHash,
|
|
351
|
+
category: 'submodule',
|
|
352
|
+
excludedReason: 'submodule entry is not source text',
|
|
353
|
+
};
|
|
354
|
+
if (/(^|\/)(node_modules|vendor|pods)(\/|$)/.test(path))
|
|
355
|
+
return {
|
|
356
|
+
...entryFile(entry),
|
|
357
|
+
category: 'vendor',
|
|
358
|
+
excludedReason: 'third-party dependency directory',
|
|
359
|
+
};
|
|
360
|
+
if (/(^|\/)(\.git|\.dart_tool|\.next|\.nuxt)(\/|$)|\.(g|freezed)\.dart$|\.(?:[cm]?js|css)\.map$|\.(tsbuildinfo|class|pyc)$/.test(path) ||
|
|
361
|
+
[...generatedRoots].some((root) => entry.path.startsWith(root)))
|
|
362
|
+
return {
|
|
363
|
+
...entryFile(entry),
|
|
364
|
+
category: 'generated',
|
|
365
|
+
excludedReason: 'generated or build output',
|
|
366
|
+
};
|
|
367
|
+
if (isSecretPath(path))
|
|
368
|
+
return {
|
|
369
|
+
...entryFile(entry),
|
|
370
|
+
category: 'secret',
|
|
371
|
+
excludedReason: 'credential or private key material',
|
|
372
|
+
};
|
|
373
|
+
if (/(^|\/)(__tests__|test|tests|spec)(\/|$)|\.(test|spec)\.[^/]+$|(^|\/)[^/]+_test\.[^/]+$/.test(path))
|
|
374
|
+
return { ...entryFile(entry), category: 'test' };
|
|
375
|
+
if (/(^|\/)(assets?|public|static|res)(\/|$)|\.(png|jpe?g|gif|svg|webp|ico|mp3|mp4|wav|ttf|otf|woff2?)$/.test(path))
|
|
376
|
+
return { ...entryFile(entry), category: 'assets' };
|
|
377
|
+
if (/(^|\/)(docs?|documentation)(\/|$)|(^|\/)(readme|changelog|contributing)(\.|$)|\.md$/.test(path))
|
|
378
|
+
return { ...entryFile(entry), category: 'docs' };
|
|
379
|
+
if (/(^|\/)(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|pubspec\.yaml|pubspec\.lock|tsconfig[^/]*\.json|.*\.config\.[^/]+|\.gitignore|dockerfile|.*\.ya?ml|.*\.json)$/.test(path))
|
|
380
|
+
return { ...entryFile(entry), category: 'config' };
|
|
381
|
+
return { ...entryFile(entry), category: 'source' };
|
|
382
|
+
}
|
|
383
|
+
function entryFile(entry) {
|
|
384
|
+
return { path: entry.path, blobHash: entry.blobHash, category: 'source' };
|
|
385
|
+
}
|
|
386
|
+
function pathOrder(left, right) {
|
|
387
|
+
return Buffer.compare(Buffer.from(left.path, 'utf8'), Buffer.from(right.path, 'utf8'));
|
|
388
|
+
}
|
|
389
|
+
function assertObjectId(value, label) {
|
|
390
|
+
if (!/^[0-9a-f]{40}$|^[0-9a-f]{64}$/.test(value))
|
|
391
|
+
throw new Error(`${label} must be a 40- or 64-hex object ID`);
|
|
392
|
+
}
|
|
393
|
+
function uniqueHashMap(files) {
|
|
394
|
+
const result = new Map();
|
|
395
|
+
const duplicates = new Set();
|
|
396
|
+
for (const file of files) {
|
|
397
|
+
if (result.has(file.blobHash))
|
|
398
|
+
duplicates.add(file.blobHash);
|
|
399
|
+
else
|
|
400
|
+
result.set(file.blobHash, file);
|
|
401
|
+
}
|
|
402
|
+
for (const hash of duplicates)
|
|
403
|
+
result.delete(hash);
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
function isSecretPath(path) {
|
|
407
|
+
const segments = path.split('/');
|
|
408
|
+
const basename = segments.at(-1) ?? '';
|
|
409
|
+
const extension = basename.includes('.') ? basename.slice(basename.lastIndexOf('.')) : '';
|
|
410
|
+
if (/(^|\/)(\.env(?:\.[^/]*)?)(\/|$)/.test(path) ||
|
|
411
|
+
/\.(pem|key|p12|pfx|jks|keystore)$/.test(path) ||
|
|
412
|
+
/(^|\/)id_(rsa|dsa|ecdsa|ed25519)$/.test(path))
|
|
413
|
+
return true;
|
|
414
|
+
const exactSensitiveName = /^(?:secret|secrets|credential|credentials)(?:\.(json|ya?ml|toml|ini|conf|cfg|txt))?$/i.test(basename);
|
|
415
|
+
const sensitiveDirectory = segments
|
|
416
|
+
.slice(0, -1)
|
|
417
|
+
.some((segment) => /^(?:secret|secrets|credential|credentials)$/i.test(segment));
|
|
418
|
+
return (exactSensitiveName ||
|
|
419
|
+
(sensitiveDirectory && /\.(json|ya?ml|toml|ini|conf|cfg|txt)$/i.test(extension)));
|
|
420
|
+
}
|
|
421
|
+
function emptyComparison() {
|
|
422
|
+
return { changedPaths: [], added: [], deleted: [], renamed: [], unchanged: [] };
|
|
423
|
+
}
|
|
424
|
+
function assertRef(ref) {
|
|
425
|
+
if (!ref || ref.length > 256 || ref.startsWith('-') || !/^[A-Za-z0-9._/@-]+$/.test(ref))
|
|
426
|
+
throw new Error('Git ref is invalid');
|
|
427
|
+
}
|
|
428
|
+
function isRecord(value) {
|
|
429
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
430
|
+
}
|
|
431
|
+
const MAX_PATHSPEC_ARGUMENT_BYTES = 12 * 1024;
|
|
432
|
+
function pathspecChunks(paths) {
|
|
433
|
+
const chunks = [];
|
|
434
|
+
let current = [];
|
|
435
|
+
let bytes = 0;
|
|
436
|
+
for (const path of paths) {
|
|
437
|
+
const pathBytes = Buffer.byteLength(path, 'utf8') + 1;
|
|
438
|
+
if (current.length > 0 && bytes + pathBytes > MAX_PATHSPEC_ARGUMENT_BYTES) {
|
|
439
|
+
chunks.push(current);
|
|
440
|
+
current = [];
|
|
441
|
+
bytes = 0;
|
|
442
|
+
}
|
|
443
|
+
current.push(path);
|
|
444
|
+
bytes += pathBytes;
|
|
445
|
+
}
|
|
446
|
+
if (current.length > 0)
|
|
447
|
+
chunks.push(current);
|
|
448
|
+
return chunks;
|
|
449
|
+
}
|
|
450
|
+
//# sourceMappingURL=source-snapshot.js.map
|
|
@@ -17,6 +17,7 @@ import { RepositoryDecisionStore } from './runtime/repository-decision-store.js'
|
|
|
17
17
|
import { ShadowNoticeStore } from './runtime/shadow-notice-store.js';
|
|
18
18
|
import { UpdateChoiceStore } from './runtime/update-choice-store.js';
|
|
19
19
|
import { PrincipalStateGuard } from './runtime/principal-state.js';
|
|
20
|
+
import { OnboardingStore } from './runtime/onboarding-store.js';
|
|
20
21
|
import { QuestionnaireStore } from './runtime/questionnaire-store.js';
|
|
21
22
|
export function createBridgeService() {
|
|
22
23
|
const config = loadBridgeConfig();
|
|
@@ -61,6 +62,7 @@ export function createBridgeService() {
|
|
|
61
62
|
updateChoices,
|
|
62
63
|
shadowNotices,
|
|
63
64
|
questionnaires: new QuestionnaireStore(stateRoot),
|
|
65
|
+
onboarding: new OnboardingStore(stateRoot),
|
|
64
66
|
clientVersion: config.clientVersion,
|
|
65
67
|
principalState,
|
|
66
68
|
});
|