@counterfact-labs/cli 0.2.6 → 0.2.9
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 +20 -0
- package/bundle/client-release.json +1 -1
- package/dist/adapters/analysis-results.js +87 -0
- package/dist/adapters/combined-results.js +27 -0
- package/dist/adapters/completion-lock.js +95 -0
- package/dist/adapters/completion.js +132 -17
- package/dist/adapters/entry-files.js +129 -0
- package/dist/adapters/entry-packing.js +161 -0
- package/dist/adapters/entry-selection.js +57 -0
- package/dist/adapters/files.js +111 -5
- package/dist/adapters/git.js +10 -1
- package/dist/adapters/relay-plan.js +5 -7
- package/dist/adapters/release.js +4 -1
- package/dist/adapters/results.js +18 -3
- package/dist/adapters/selection.js +114 -0
- package/dist/adapters/settings.js +62 -4
- package/dist/adapters/source-relay-plan.js +7 -0
- package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -1
- package/dist/analysis-core/analysis/file-analysis-plan.js +1 -0
- package/dist/analysis-core/analysis/file-dependencies.js +1 -0
- package/dist/analysis-core/analysis/finding-activity-switch.js +1 -0
- package/dist/analysis-core/analysis/finding-identity.js +1 -0
- package/dist/analysis-core/analysis/finding-quality/policy.js +1 -0
- package/dist/analysis-core/analysis/folder-analysis-plan.js +1 -0
- package/dist/analysis-core/analysis/passes/full-file-passes.js +1 -0
- package/dist/analysis-core/analysis/project-architecture.js +1 -0
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/chunks/2AASSZRO.js +35 -0
- package/dist/analysis-core/chunks/2YREYGOG.js +1 -0
- package/dist/analysis-core/chunks/33HRXJ5Q.js +1 -0
- package/dist/analysis-core/chunks/33IGHOYT.js +1 -0
- package/dist/analysis-core/chunks/4PT5C4IT.js +7 -0
- package/dist/analysis-core/chunks/4RXRAB5S.js +1 -0
- package/dist/analysis-core/chunks/4UDQBDDE.js +1 -0
- package/dist/analysis-core/chunks/{OPMA32A6.js → 5B5UCWD3.js} +1 -1
- package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
- package/dist/analysis-core/chunks/CQQ4TTXE.js +1 -0
- package/dist/analysis-core/chunks/CTLHKFPH.js +19 -0
- package/dist/analysis-core/chunks/F54DOWKN.js +6 -0
- package/dist/analysis-core/chunks/FRXOQ2M6.js +3 -0
- package/dist/analysis-core/chunks/HCPO6QPU.js +1 -0
- package/dist/analysis-core/chunks/HHFDNY6V.js +14 -0
- package/dist/analysis-core/chunks/{3JWEFCTJ.js → HVQJKPQ4.js} +1 -1
- package/dist/analysis-core/chunks/INVTU73D.js +1 -0
- package/dist/analysis-core/chunks/JEWBPIQI.js +1 -0
- package/dist/analysis-core/chunks/JGK37AWN.js +1 -0
- package/dist/analysis-core/chunks/LFYTD7JF.js +1 -0
- package/dist/analysis-core/chunks/LMUXOKIM.js +1 -0
- package/dist/analysis-core/chunks/LOBIDUCA.js +1 -0
- package/dist/analysis-core/chunks/M5YWJY7P.js +1 -0
- package/dist/analysis-core/chunks/MGVIKA74.js +1 -0
- package/dist/analysis-core/chunks/MKYLK6DB.js +1 -0
- package/dist/analysis-core/chunks/MZOKZ3N6.js +1 -0
- package/dist/analysis-core/chunks/NXHVBJL4.js +1 -0
- package/dist/analysis-core/chunks/OKBK3JL3.js +1 -0
- package/dist/analysis-core/chunks/PBHHWFR6.js +1 -0
- package/dist/analysis-core/chunks/Q5WR6B2H.js +2 -0
- package/dist/analysis-core/chunks/QVXARKP7.js +1 -0
- package/dist/analysis-core/chunks/RQTNTIR4.js +1 -0
- package/dist/analysis-core/chunks/RUVMFAST.js +1 -0
- package/dist/analysis-core/chunks/S27VIQMZ.js +1 -0
- package/dist/analysis-core/chunks/{YDBO7P4X.js → S27Y2TP5.js} +1 -1
- package/dist/analysis-core/chunks/SJESFYBA.js +10 -0
- package/dist/analysis-core/chunks/SP6XFHNH.js +1 -0
- package/dist/analysis-core/chunks/VDUFPTDD.js +1 -0
- package/dist/analysis-core/chunks/W24SFYZM.js +67 -0
- package/dist/analysis-core/chunks/YTHF2E2S.js +1 -0
- package/dist/analysis-core/chunks/YWL52FVW.js +2 -0
- package/dist/analysis-core/contracts/analysis-selection.js +1 -0
- package/dist/analysis-core/contracts/analysis-size.js +1 -0
- package/dist/analysis-core/contracts/execution-request.js +1 -0
- package/dist/analysis-core/contracts/intake-finding.js +1 -0
- package/dist/analysis-core/contracts/model-call-identity.js +1 -0
- package/dist/analysis-core/contracts/module-resolution.js +1 -0
- package/dist/analysis-core/contracts/relay-analysis.js +1 -1
- package/dist/analysis-core/contracts/requests.js +1 -1
- package/dist/analysis-core/contracts/selection-paths.js +1 -0
- package/dist/analysis-core/conventions/analysis-fit.js +1 -1
- package/dist/analysis-core/conventions/file-admission.js +1 -1
- package/dist/analysis-core/conventions/read-limits.js +1 -0
- package/dist/analysis-core/import/calls/modules.js +1 -0
- package/dist/analysis-core/import/entry-selection.js +1 -0
- package/dist/analysis-core/import/outside-loader-sources.js +1 -1
- package/dist/analysis-core/index.js +3 -3
- package/dist/analysis-core/requests/answer-formats.js +1 -1
- package/dist/analysis-core/requests/local-plan.js +1 -1
- package/dist/analysis-core/requests/planned-limits.js +1 -0
- package/dist/analysis-core/requests/registry.js +1 -1
- package/dist/analysis-core/requests/template-version.js +1 -1
- package/dist/analysis-core/requests/tools.js +1 -1
- package/dist/analysis-core/settings.js +1 -1
- package/dist/analysis-core/stages/engine-entry.js +1 -1
- package/dist/bundle/seal/engine.js +1 -1
- package/dist/cli/analysis-selection.js +133 -0
- package/dist/cli/api.js +1 -1
- package/dist/cli/commands/analyze.js +82 -14
- package/dist/cli/commands/skipped.js +65 -0
- package/dist/cli/completion-delivery.js +63 -0
- package/dist/cli/completion-recovery.js +68 -14
- package/dist/cli/context.js +4 -2
- package/dist/cli/finding-display.js +102 -7
- package/dist/cli/finding-settings.js +3 -1
- package/dist/cli/intake-payload.js +26 -2
- package/dist/cli/main.js +37 -11
- package/dist/cli/mcp-failure.js +6 -5
- package/dist/cli/mcp.js +191 -26
- package/dist/cli/prior-completion.js +35 -0
- package/dist/cli/relay-check.js +260 -110
- package/dist/cli/relay-failure.js +32 -0
- package/dist/cli/relay-preparation.js +72 -21
- package/dist/cli/result-check.js +31 -12
- package/dist/cli/results-upload.js +131 -0
- package/dist/cli/saved-results.js +10 -0
- package/dist/cli/scope-choices.js +57 -0
- package/dist/cli/sign-in.js +2 -2
- package/dist/cli/skipped-files.js +22 -0
- package/dist/cli/summary.js +15 -1
- package/dist/contract/agent-choice.js +25 -2
- package/dist/contract/cli-api-messages.js +3 -0
- package/dist/contract/cli-refusals.js +39 -5
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console-url.js +3 -0
- package/dist/contract/console.js +24 -40
- package/dist/contract/engine.js +1 -1
- package/dist/contract/messages.js +4 -3
- package/dist/contract/project-architecture.js +1 -0
- package/dist/contract/scope-choice.js +98 -0
- package/dist/contract/selection-metadata.js +9 -0
- package/dist/contract/selection-offer.js +40 -0
- package/dist/gatekeeper/cli-api.js +70 -4
- package/dist/gatekeeper/console.js +55 -28
- package/dist/gatekeeper/model.js +5 -23
- package/dist/gatekeeper/trust/policy.js +2 -1
- package/package.json +5 -3
- package/dist/analysis-core/analysis/source-notes/types.js +0 -1
- package/dist/analysis-core/chunks/2PSNOSJ2.js +0 -1
- package/dist/analysis-core/chunks/377MLK4M.js +0 -21
- package/dist/analysis-core/chunks/5DGPLXYB.js +0 -1
- package/dist/analysis-core/chunks/6B2HILSY.js +0 -1
- package/dist/analysis-core/chunks/6QY5BV5B.js +0 -14
- package/dist/analysis-core/chunks/6U2IR4XB.js +0 -1
- package/dist/analysis-core/chunks/DLPVYWHZ.js +0 -1
- package/dist/analysis-core/chunks/FOKDE64Y.js +0 -1
- package/dist/analysis-core/chunks/FSLWD2PE.js +0 -1
- package/dist/analysis-core/chunks/K2GNDX3W.js +0 -1
- package/dist/analysis-core/chunks/KTNA2KTN.js +0 -7
- package/dist/analysis-core/chunks/KWECQJ6X.js +0 -1
- package/dist/analysis-core/chunks/NIOS7U6C.js +0 -41
- package/dist/analysis-core/chunks/PECX3OXB.js +0 -1
- package/dist/analysis-core/chunks/PYKAERZZ.js +0 -1
- package/dist/analysis-core/chunks/QFY6A3MS.js +0 -66
- package/dist/analysis-core/chunks/SKVEBOGA.js +0 -1
- package/dist/analysis-core/chunks/STI4JE7L.js +0 -1
- package/dist/analysis-core/chunks/SVMEWGD3.js +0 -1
- package/dist/analysis-core/chunks/X7WZAA7S.js +0 -1
- package/dist/analysis-core/chunks/ZTOVBAG7.js +0 -1
- package/dist/analysis-core/data/finding-types.js +0 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { MAX_ANALYSIS_ENTRIES } from "../analysis-core/contracts/analysis-selection.js";
|
|
2
|
+
import { entryGroupChoice } from "../analysis-core/import/entry-selection.js";
|
|
3
|
+
import { analysisSizeLimits, assessAnalysisSize, measureAnalysisSize, measureAnalysisSizeLowerBound } from "../analysis-core/conventions/analysis-fit.js";
|
|
4
|
+
const defaultBudget = { measurements: 4_096, candidates: 16_384, searchSteps: 100_000 };
|
|
5
|
+
/** Size stays at this boundary; core supplies and validates the complete entry import sets. */
|
|
6
|
+
export function fittingEntryGroups(settings, membership, currentPaths, budget = defaultBudget) {
|
|
7
|
+
if (membership.kind !== "file" || !membership.entrySets)
|
|
8
|
+
return [];
|
|
9
|
+
let optimizing = false, exhausted = false, measurements = 0, candidates = 0, searchSteps = 0;
|
|
10
|
+
const current = new Set(currentPaths);
|
|
11
|
+
const byteFloors = new Map(membership.files.map(file => [file.path, measureAnalysisSizeLowerBound(settings, [file])]));
|
|
12
|
+
const pools = [["totalBytes", analysisSizeLimits.totalLimitBytes], ["instructionBytes", analysisSizeLimits.instructionLimitBytes]];
|
|
13
|
+
const cache = new Map();
|
|
14
|
+
const candidate = (entries) => {
|
|
15
|
+
const key = JSON.stringify([...entries].sort());
|
|
16
|
+
if (cache.has(key))
|
|
17
|
+
return cache.get(key);
|
|
18
|
+
const choice = entryGroupChoice(membership, entries);
|
|
19
|
+
let result;
|
|
20
|
+
if (choice && choice.includes.every(path => current.has(path))) {
|
|
21
|
+
const floors = pools.map(([pool, limit]) => choice.includes.reduce((sum, path) => sum + byteFloors.get(path)[pool], 0) / limit);
|
|
22
|
+
const floorWeight = Math.max(...floors), floorFits = floorWeight <= 1;
|
|
23
|
+
const paths = new Set(choice.includes);
|
|
24
|
+
if (floorFits && optimizing && measurements++ >= budget.measurements) {
|
|
25
|
+
exhausted = true;
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const size = !floorFits ? undefined
|
|
29
|
+
: measureAnalysisSize(settings, membership.files.filter(file => paths.has(file.path)));
|
|
30
|
+
result = { choice, fits: size ? assessAnalysisSize(size).fits : false, floorFits,
|
|
31
|
+
weight: size ? Math.max(size.totalBytes / analysisSizeLimits.totalLimitBytes, size.instructionBytes / analysisSizeLimits.instructionLimitBytes)
|
|
32
|
+
: floorWeight };
|
|
33
|
+
}
|
|
34
|
+
cache.set(key, result);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
const sets = new Map(membership.entrySets.map(set => [set.entry, set.includes]));
|
|
38
|
+
const inputOrder = membership.entrySets.map(set => set.entry).filter(entry => candidate([entry]));
|
|
39
|
+
const entries = [...inputOrder].sort((left, right) => candidate([right]).weight - candidate([left]).weight);
|
|
40
|
+
if (!entries.length)
|
|
41
|
+
return [];
|
|
42
|
+
const minimumParts = (roots) => {
|
|
43
|
+
const owners = new Map();
|
|
44
|
+
for (const entry of roots)
|
|
45
|
+
for (const path of sets.get(entry))
|
|
46
|
+
owners.set(path, (owners.get(path) ?? 0) + 1);
|
|
47
|
+
let minimum = Math.ceil(roots.length / MAX_ANALYSIS_ENTRIES);
|
|
48
|
+
for (const [pool, limit] of pools) {
|
|
49
|
+
const exclusiveBytes = roots.map(entry => sets.get(entry).reduce((sum, path) => sum + (owners.get(path) === 1 ? byteFloors.get(path)[pool] : 0), 0));
|
|
50
|
+
minimum = Math.max(minimum, Math.ceil([...owners.keys()].reduce((sum, path) => sum + byteFloors.get(path)[pool], 0) / limit));
|
|
51
|
+
for (let count = 1; count < MAX_ANALYSIS_ENTRIES; count++)
|
|
52
|
+
minimum = Math.max(minimum, Math.ceil(exclusiveBytes.filter(bytes => bytes > limit / (count + 1)).length / count));
|
|
53
|
+
}
|
|
54
|
+
return minimum;
|
|
55
|
+
};
|
|
56
|
+
const pack = (order) => {
|
|
57
|
+
const groups = [];
|
|
58
|
+
let complete = true;
|
|
59
|
+
for (const entry of order) {
|
|
60
|
+
if (groups.some(group => sets.get(entry).every(path => candidate(group).choice.includes.includes(path))))
|
|
61
|
+
continue;
|
|
62
|
+
const group = groups.find(group => candidate([...group, entry])?.fits);
|
|
63
|
+
if (exhausted)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (group)
|
|
66
|
+
group.push(entry);
|
|
67
|
+
else if (candidate([entry]).fits)
|
|
68
|
+
groups.push([entry]);
|
|
69
|
+
else
|
|
70
|
+
complete = false;
|
|
71
|
+
}
|
|
72
|
+
return complete ? groups : undefined;
|
|
73
|
+
};
|
|
74
|
+
let best = pack(entries);
|
|
75
|
+
const ordered = (groups) => groups.map(group => [...group].sort((a, b) => membership.entries.indexOf(a) - membership.entries.indexOf(b)))
|
|
76
|
+
.sort((a, b) => membership.entries.indexOf(a[0]) - membership.entries.indexOf(b[0]))
|
|
77
|
+
.map(group => candidate(group).choice);
|
|
78
|
+
const minimum = minimumParts(entries);
|
|
79
|
+
if (best && best.length === minimum)
|
|
80
|
+
return ordered(best);
|
|
81
|
+
// Keep the measured baseline before spending bounded extra work on fewer parts.
|
|
82
|
+
optimizing = true;
|
|
83
|
+
// Size sorting can miss a fitting arrangement that the user's entry order finds immediately.
|
|
84
|
+
// Proving either pass optimal avoids enumerating the full combination frontier.
|
|
85
|
+
const alternate = pack(inputOrder);
|
|
86
|
+
if (alternate && (!best || alternate.length < best.length))
|
|
87
|
+
best = alternate;
|
|
88
|
+
if (exhausted || best?.length === minimum)
|
|
89
|
+
return ordered(best ?? []);
|
|
90
|
+
const choices = new Map();
|
|
91
|
+
const extend = (group, start) => {
|
|
92
|
+
if (exhausted || candidates++ >= budget.candidates) {
|
|
93
|
+
exhausted = true;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const value = candidate(group);
|
|
97
|
+
// Only an inventory-independent floor can rule out every larger combination.
|
|
98
|
+
if (!value || !value.floorFits)
|
|
99
|
+
return;
|
|
100
|
+
if (value.fits) {
|
|
101
|
+
const included = new Set(value.choice.includes);
|
|
102
|
+
const covered = new Set(entries.filter(entry => sets.get(entry).every(path => included.has(path))));
|
|
103
|
+
const key = JSON.stringify([...covered].sort());
|
|
104
|
+
if (!choices.has(key))
|
|
105
|
+
choices.set(key, { entries: group, covered, weight: value.weight });
|
|
106
|
+
}
|
|
107
|
+
if (group.length === MAX_ANALYSIS_ENTRIES)
|
|
108
|
+
return;
|
|
109
|
+
for (let index = start; index < entries.length && !exhausted; index++)
|
|
110
|
+
extend([...group, entries[index]], index + 1);
|
|
111
|
+
};
|
|
112
|
+
for (let index = 0; index < entries.length && !exhausted; index++)
|
|
113
|
+
extend([entries[index]], index + 1);
|
|
114
|
+
if (exhausted)
|
|
115
|
+
return ordered(best ?? []);
|
|
116
|
+
const groups = [...choices.values()].sort((left, right) => right.weight - left.weight || right.covered.size - left.covered.size);
|
|
117
|
+
const byEntry = new Map(entries.map(entry => [entry, groups.filter(group => group.covered.has(entry))]));
|
|
118
|
+
const eligible = entries.filter(entry => byEntry.get(entry).length);
|
|
119
|
+
if (!eligible.length)
|
|
120
|
+
return [];
|
|
121
|
+
const eligibleMinimum = minimumParts(eligible);
|
|
122
|
+
const visited = new Map();
|
|
123
|
+
const search = (remaining, selected) => {
|
|
124
|
+
if (exhausted || searchSteps++ >= budget.searchSteps) {
|
|
125
|
+
exhausted = true;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (best?.length === eligibleMinimum)
|
|
129
|
+
return;
|
|
130
|
+
if (!remaining.length) {
|
|
131
|
+
best = selected.map(group => [...group]);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const key = JSON.stringify(remaining);
|
|
135
|
+
if ((visited.get(key) ?? Infinity) <= selected.length)
|
|
136
|
+
return;
|
|
137
|
+
visited.set(key, selected.length);
|
|
138
|
+
if (selected.length + minimumParts(remaining) >= (best?.length ?? Infinity))
|
|
139
|
+
return;
|
|
140
|
+
const entry = remaining.reduce((fewest, entry) => byEntry.get(entry).length < byEntry.get(fewest).length ? entry : fewest);
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
for (const group of byEntry.get(entry)) {
|
|
143
|
+
if (exhausted || searchSteps++ >= budget.searchSteps) {
|
|
144
|
+
exhausted = true;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const next = remaining.filter(entry => !group.covered.has(entry));
|
|
148
|
+
const nextKey = JSON.stringify(next);
|
|
149
|
+
if (seen.has(nextKey))
|
|
150
|
+
continue;
|
|
151
|
+
seen.add(nextKey);
|
|
152
|
+
selected.push(group.entries);
|
|
153
|
+
search(next, selected);
|
|
154
|
+
selected.pop();
|
|
155
|
+
if (best?.length === eligibleMinimum)
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
search(eligible, []);
|
|
160
|
+
return ordered(best ?? []);
|
|
161
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { discoverLocalSources, fileScopeEnabled, readEntrySelection } from "../analysis-core/index.js";
|
|
2
|
+
import { measureAnalysisSize, assessAnalysisSize } from "../analysis-core/conventions/analysis-fit.js";
|
|
3
|
+
import { FileReadRefusal } from "../analysis-core/conventions/file-admission.js";
|
|
4
|
+
import { localRepoKeyFor } from "../analysis-core/github/repo-key.js";
|
|
5
|
+
import { cliRefusals } from "../contract/cli-refusals.js";
|
|
6
|
+
import { INVALID_SELECTION_MESSAGE, INVALID_SCOPE_MESSAGE, SINGLE_FILE_TOO_LARGE_MESSAGE, SCOPE_PROPOSAL_CONFLICT_MESSAGE, STALE_SCOPE_MESSAGE } from "../contract/scope-choice.js";
|
|
7
|
+
import { createEntryFiles } from "./entry-files.js";
|
|
8
|
+
import { selectionSizeOffer } from "./selection.js";
|
|
9
|
+
import { MAX_ANALYSIS_ENTRIES } from "../analysis-core/contracts/analysis-selection.js";
|
|
10
|
+
import { ScopeChoiceError } from "../contract/selection-offer.js";
|
|
11
|
+
/** The CLI consumes the core membership answer and captures only its retained files. */
|
|
12
|
+
export const entrySelectionProvider = async (settings, request, options) => {
|
|
13
|
+
if (!fileScopeEnabled(settings) || request.kind !== "file")
|
|
14
|
+
throw new Error(cliRefusals.release_unavailable);
|
|
15
|
+
const port = await createEntryFiles(settings, request.launchRoot, options.admission, options.signal, request.entries ?? request.entry).catch(() => {
|
|
16
|
+
options.signal?.throwIfAborted();
|
|
17
|
+
throw new Error(INVALID_SELECTION_MESSAGE);
|
|
18
|
+
});
|
|
19
|
+
const input = { ...port, request: { ...request, scope: undefined }, admission: options.admission };
|
|
20
|
+
const full = await readEntrySelection(settings, input);
|
|
21
|
+
options.signal?.throwIfAborted();
|
|
22
|
+
const refusal = (reason) => {
|
|
23
|
+
if (reason === "over-limit")
|
|
24
|
+
throw new Error(SINGLE_FILE_TOO_LARGE_MESSAGE);
|
|
25
|
+
if (reason === "stale-snapshot")
|
|
26
|
+
throw new Error(STALE_SCOPE_MESSAGE);
|
|
27
|
+
if (reason === "invalid-scope")
|
|
28
|
+
throw new Error(options.proposal ? SCOPE_PROPOSAL_CONFLICT_MESSAGE : INVALID_SCOPE_MESSAGE);
|
|
29
|
+
if (reason === "excluded" || reason === "hidden" || reason === "secret" || reason === "secret-content")
|
|
30
|
+
throw new FileReadRefusal(reason, request.entry);
|
|
31
|
+
throw new Error(INVALID_SELECTION_MESSAGE);
|
|
32
|
+
};
|
|
33
|
+
if (full.kind !== "ready")
|
|
34
|
+
return refusal(full.reason);
|
|
35
|
+
// Replay the same admitted captures through the owner to validate a narrower scope.
|
|
36
|
+
const selected = request.scope || options.proposal ? await readEntrySelection(settings, { ...input, request, proposal: options.proposal }) : full;
|
|
37
|
+
options.signal?.throwIfAborted();
|
|
38
|
+
if (selected.kind !== "ready")
|
|
39
|
+
return refusal(selected.reason);
|
|
40
|
+
if (request.entries && (selected.selection.files.filter(file => file.role === "entry").length > MAX_ANALYSIS_ENTRIES
|
|
41
|
+
|| !request.scope && request.entries.some(entry => !selected.selection.files.some(file => file.path === entry)))) {
|
|
42
|
+
const choices = selectionSizeOffer(settings, full.selection, { files: selected.files }, { force: true });
|
|
43
|
+
if (choices)
|
|
44
|
+
throw new ScopeChoiceError(choices);
|
|
45
|
+
throw new Error(INVALID_SCOPE_MESSAGE);
|
|
46
|
+
}
|
|
47
|
+
const { files } = selected;
|
|
48
|
+
const entries = selected.selection.files.map(({ path, size }) => ({ path, size, ref: path }));
|
|
49
|
+
const selection = { selected: entries, refused: [], totalBytes: entries.reduce((sum, file) => sum + file.size, 0) };
|
|
50
|
+
const repoKey = localRepoKeyFor(request.launchRoot);
|
|
51
|
+
const discovered = await discoverLocalSources(settings, { repoKey, entries, selection, membership: selected.selection, admission: options.admission,
|
|
52
|
+
read: async (entry) => files[entry.path] });
|
|
53
|
+
const analysisSize = measureAnalysisSize(settings, entries, options.admission);
|
|
54
|
+
return { membership: full.selection, source: { ...discovered, repoKey, root: ".", sourceBase: request.launchRoot, files, selection,
|
|
55
|
+
snapshot: Object.fromEntries(entries.map(({ path }) => [path, port.snapshot[path]])), analysisSize,
|
|
56
|
+
...(settings.CF_REFUSE_TOO_LARGE === "0" ? {} : { analysisFit: assessAnalysisSize(analysisSize) }) } };
|
|
57
|
+
};
|
package/dist/adapters/files.js
CHANGED
|
@@ -1,22 +1,42 @@
|
|
|
1
|
+
import { folderPartsEnabled, skippedFilesEnabled } from "../analysis-core/settings.js";
|
|
2
|
+
import { ModuleResolver, moduleResolutionLimits } from "../analysis-core/import/calls/modules.js";
|
|
1
3
|
import { opendir, realpath, stat, readFile } from "node:fs/promises";
|
|
2
4
|
import { dirname, join, posix } from "node:path";
|
|
3
5
|
import { admitFile, FileReadRefusal, fileRefusalWarning, localSymlinkGuardEnabled, respectGitignore, readRepoFile, resolveLocalExclusions } from "../analysis-core/conventions/file-admission.js";
|
|
4
6
|
import { assessAnalysisSize, measureAnalysisSize } from "../analysis-core/conventions/analysis-fit.js";
|
|
5
7
|
import { localRepoKeyFor } from "../analysis-core/github/repo-key.js";
|
|
8
|
+
import { fileScopeOf, planFileAnalyses } from "../analysis-core/analysis/file-analysis-plan.js";
|
|
9
|
+
import { planFolderAnalyses } from "../analysis-core/analysis/folder-analysis-plan.js";
|
|
10
|
+
import { fileDependencies } from "../analysis-core/analysis/file-dependencies.js";
|
|
11
|
+
import { mapLimit } from "../analysis-core/map-limit.js";
|
|
12
|
+
import { fileEvidenceBytes } from "../analysis-core/analysis/passes/full-file-passes.js";
|
|
6
13
|
import { fetchLocalSources } from "../analysis-core/index.js";
|
|
7
14
|
import { repoPathFrom } from "../analysis-core/import/outside-loader-sources.js";
|
|
15
|
+
import { READ_LIMITS } from "../analysis-core/conventions/read-limits.js";
|
|
8
16
|
import { mergeSkippedFiles, mergeUnlistedFileWarnings } from "../analysis-core/analysis/code-agents/skipped-files.js";
|
|
9
17
|
import { createGitIgnore } from "./git-ignore.js";
|
|
10
18
|
import { createIgnoredFileCollector } from "./ignored-files.js";
|
|
11
19
|
import { resolveFolderPath } from "./folder-path.js";
|
|
12
20
|
import { createRepositoryFiles } from "./repository-files.js";
|
|
13
|
-
export const FOLDER_ENTRY_LIMIT =
|
|
21
|
+
export const FOLDER_ENTRY_LIMIT = moduleResolutionLimits.inventoryEntries;
|
|
14
22
|
export const FOLDER_ENTRY_LIMIT_MESSAGE = "The folder has too many entries to analyze. Choose a smaller folder.";
|
|
15
|
-
export async function readLocalFolder(
|
|
23
|
+
export async function readLocalFolder(...args) {
|
|
24
|
+
try {
|
|
25
|
+
return await readFolder(...args);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
// An open failure is a read failure here; outside this owner it may belong to a write.
|
|
29
|
+
if (error instanceof Error)
|
|
30
|
+
Object.assign(error, { localOperation: "read" });
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function readFolder(settings, folder, admission = {}, options = {}) {
|
|
16
35
|
const root = await resolveFolderPath(folder);
|
|
17
36
|
if (!(await stat(root)).isDirectory())
|
|
18
37
|
throw new Error("Select a folder to analyze.");
|
|
19
38
|
const entries = [];
|
|
39
|
+
const knownPaths = new Set(), opaquePaths = new Set();
|
|
20
40
|
const warnings = [];
|
|
21
41
|
const refused = [];
|
|
22
42
|
const gitSkipped = [];
|
|
@@ -75,12 +95,17 @@ export async function readLocalFolder(settings, folder, admission = {}, options
|
|
|
75
95
|
throw new Error(FOLDER_ENTRY_LIMIT_MESSAGE);
|
|
76
96
|
items.push(entry);
|
|
77
97
|
}
|
|
98
|
+
opaquePaths.delete(prefix);
|
|
78
99
|
const names = items.map(item => item.name);
|
|
79
100
|
const facts = await ignoredPaths(names.map(name => join(target, name)));
|
|
80
101
|
const gitIgnored = facts && new Set(names.filter(name => facts.has(join(target, name))).map(name => prefix ? `${prefix}/${name}` : name));
|
|
81
102
|
for (const item of items.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
82
103
|
const name = item.name;
|
|
83
104
|
const path = prefix ? `${prefix}/${name}` : name;
|
|
105
|
+
if (item.isDirectory() || item.isSymbolicLink())
|
|
106
|
+
opaquePaths.add(path);
|
|
107
|
+
if (item.isFile() || item.isSymbolicLink())
|
|
108
|
+
knownPaths.add(path);
|
|
84
109
|
// A link's filename-only refusal waits until its target type is known.
|
|
85
110
|
const listed = { path, directory: item.isDirectory() || item.isSymbolicLink() };
|
|
86
111
|
if (!admitFile(settings, listed, { purpose: "listing", route: "local-folder", ...admission }).admitted)
|
|
@@ -112,6 +137,7 @@ export async function readLocalFolder(settings, folder, admission = {}, options
|
|
|
112
137
|
if (info.isDirectory())
|
|
113
138
|
await walk(file, path, parents);
|
|
114
139
|
else if (info.isFile()) {
|
|
140
|
+
opaquePaths.delete(path);
|
|
115
141
|
entries.push({ path, size: info.size, ref: path });
|
|
116
142
|
targets.set(path, file);
|
|
117
143
|
}
|
|
@@ -121,9 +147,11 @@ export async function readLocalFolder(settings, folder, admission = {}, options
|
|
|
121
147
|
gitIgnored: await ignoredPaths(["."]), localTarget: { root, path: root } });
|
|
122
148
|
if (selectedRoot.admitted)
|
|
123
149
|
await walk(root, "", new Set());
|
|
124
|
-
else
|
|
150
|
+
else {
|
|
151
|
+
opaquePaths.add("");
|
|
125
152
|
gitSkipped.push(...await ignoredFiles(root, ""));
|
|
126
|
-
|
|
153
|
+
}
|
|
154
|
+
const analysisSize = measureAnalysisSize(settings, entries, { ...admission, paths: fileScopeOf(settings) });
|
|
127
155
|
// The service makes the final decision; local prediction never changes retained files.
|
|
128
156
|
const analysisFit = settings.CF_REFUSE_TOO_LARGE === "0" ? undefined : assessAnalysisSize(analysisSize);
|
|
129
157
|
const listing = new Set(entries.map(entry => entry.path));
|
|
@@ -141,6 +169,77 @@ export async function readLocalFolder(settings, folder, admission = {}, options
|
|
|
141
169
|
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
142
170
|
}, { ...admission, listing, route: "local-folder", localTarget: { root, path: target, excludedTargets } }) : null;
|
|
143
171
|
};
|
|
172
|
+
let filePlan;
|
|
173
|
+
const planParts = options.planParts && folderPartsEnabled(settings);
|
|
174
|
+
if (options.planSkipped && skippedFilesEnabled(settings) || planParts) {
|
|
175
|
+
// The ordinary inventory owns exclusions and targets; planning never opens a second file walker.
|
|
176
|
+
const byPath = new Map(entries.map(entry => [entry.path, entry]));
|
|
177
|
+
const files = Object.create(null), visited = new Set(), unavailable = new Set();
|
|
178
|
+
const paths = planParts ? entries.map(entry => entry.path) : [...options.planSkipped];
|
|
179
|
+
const metadataPaths = new Set();
|
|
180
|
+
let metadataBytes = 0;
|
|
181
|
+
let pending = paths, dependencies = {}, retainedBytes = 0;
|
|
182
|
+
while (pending.length) {
|
|
183
|
+
const next = [...new Set(pending)].filter(path => !visited.has(path));
|
|
184
|
+
next.forEach(path => visited.add(path));
|
|
185
|
+
await mapLimit(next, 4, async (path) => {
|
|
186
|
+
const entry = byPath.get(path);
|
|
187
|
+
if (!entry || !admitFile(settings, entry, { ...admission, purpose: "proposal", route: "local-folder" }).admitted)
|
|
188
|
+
return;
|
|
189
|
+
if (planParts && entry.size > READ_LIMITS.archive.bytes - retainedBytes) {
|
|
190
|
+
unavailable.add(path);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const text = await read(entry, "proposal");
|
|
195
|
+
const size = text === null ? 0 : Buffer.byteLength(text);
|
|
196
|
+
if (text === null || planParts && size > READ_LIMITS.archive.bytes - retainedBytes)
|
|
197
|
+
unavailable.add(path);
|
|
198
|
+
else {
|
|
199
|
+
files[path] = text;
|
|
200
|
+
retainedBytes += size;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
unavailable.add(path);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
// The archive allowance counts kept text; mapLimit bounds the additional reads in flight.
|
|
208
|
+
if (planParts) {
|
|
209
|
+
delete snapshot[path];
|
|
210
|
+
snapshotTargets.delete(path);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
dependencies = fileDependencies({ entries, files, settings });
|
|
215
|
+
const metadata = settings.CF_LOCAL_IMPORT_RESOLUTION === "0" ? [] : ModuleResolver.settingsFiles(listing, new Map(Object.entries(files)), Object.keys(files));
|
|
216
|
+
const settingsPaths = metadata.filter(path => {
|
|
217
|
+
if (visited.has(path) || metadataPaths.has(path))
|
|
218
|
+
return false;
|
|
219
|
+
const size = byPath.get(path)?.size;
|
|
220
|
+
if (size === undefined || metadataPaths.size >= moduleResolutionLimits.settingsFiles || size > moduleResolutionLimits.settingsBytes - metadataBytes)
|
|
221
|
+
return false;
|
|
222
|
+
metadataPaths.add(path);
|
|
223
|
+
metadataBytes += size;
|
|
224
|
+
return true;
|
|
225
|
+
});
|
|
226
|
+
pending = [...settingsPaths, ...Object.values(dependencies).flat()].filter(path => !visited.has(path));
|
|
227
|
+
}
|
|
228
|
+
const plannedEntries = entries.filter(entry => planParts ? Object.hasOwn(files, entry.path) : !unavailable.has(entry.path)).map(entry => Object.hasOwn(files, entry.path)
|
|
229
|
+
? { ...entry, size: Buffer.byteLength(files[entry.path]) } : entry);
|
|
230
|
+
const input = { entries: plannedEntries, paths: planParts ? plannedEntries.map(entry => entry.path) : paths,
|
|
231
|
+
inputBytes: Object.fromEntries(Object.entries(files).map(([path, text]) => [path, fileEvidenceBytes(path, text)])),
|
|
232
|
+
dependencies };
|
|
233
|
+
const discovered = planParts ? await fetchLocalSources(settings, { repoKey, entries: plannedEntries, admission, includeAgentPromptPaths: true,
|
|
234
|
+
read: async (entry) => files[entry.path] ?? null }) : undefined;
|
|
235
|
+
const analysisParts = discovered ? planFolderAnalyses(settings, { ...input, fragment: discovered.fragment,
|
|
236
|
+
agentPromptPaths: discovered.agentPromptPaths ?? [], codeAgents: discovered.codeAgents }) : undefined;
|
|
237
|
+
if (!planParts)
|
|
238
|
+
filePlan = planFileAnalyses(settings, input);
|
|
239
|
+
return { repoKey, ...(filePlan ? { filePlan } : {}), ...(analysisParts ? { analysisParts } : {}), files: {}, fragment: { nodes: [], edges: [], warnings: [] }, skippedFiles: [], warnings: [],
|
|
240
|
+
selection: { selected: [], refused: [], totalBytes: 0 }, analysisSize, ...(analysisFit ? { analysisFit } : {}),
|
|
241
|
+
...(options.captureSnapshot ? { snapshot: {} } : {}) };
|
|
242
|
+
}
|
|
144
243
|
const discovered = await fetchLocalSources(settings, { repoKey, entries, admission, repository,
|
|
145
244
|
proposal: options.proposal, proposalRefusals: [...gitSkipped, ...refused],
|
|
146
245
|
read: entry => read(entry, "override"), proposalRead: entry => read(entry, "proposal"),
|
|
@@ -148,8 +247,15 @@ export async function readLocalFolder(settings, folder, admission = {}, options
|
|
|
148
247
|
const expanded = discovered.root !== undefined;
|
|
149
248
|
const ignored = mergeSkippedFiles(expanded ? gitSkipped.map(file => ({ ...file, path: repoPathFrom(file.path, discovered.root) })) : gitSkipped, repository.skippedFiles.map(file => ({ ...file, path: expanded ? file.path : posix.relative(repository.selectedRoot ?? "", file.path) })));
|
|
150
249
|
const captured = { ...Object.fromEntries(Object.entries(snapshot).map(([path, bytes]) => [repoPathFrom(path, discovered.root), bytes])), ...repository.snapshot };
|
|
151
|
-
return { repoKey, ...(discovered.agentChoice ? { agentChoice: discovered.agentChoice } : {}), files: discovered.files, analysisSize, ...(analysisFit ? { analysisFit } : {}), ...(expanded ? { root: discovered.root, sourceBase: repository.sourceBase } : {}),
|
|
250
|
+
return { repoKey, ...(filePlan ? { filePlan } : {}), ...(discovered.agentChoice ? { agentChoice: discovered.agentChoice } : {}), files: discovered.files, analysisSize, ...(analysisFit ? { analysisFit } : {}), ...(expanded ? { root: discovered.root, sourceBase: repository.sourceBase } : {}),
|
|
152
251
|
...(discovered.proposalCounts ? { proposalCounts: discovered.proposalCounts } : {}),
|
|
252
|
+
...(settings.CF_MECHANICAL_CHECKS === "0" ? {} : { mechanicalInventory: {
|
|
253
|
+
paths: [...knownPaths].map(path => repoPathFrom(path, discovered.root)), complete: !expanded,
|
|
254
|
+
admittedPaths: entries.map(entry => repoPathFrom(entry.path, discovered.root)),
|
|
255
|
+
opaquePaths: [...opaquePaths].map(path => repoPathFrom(path, discovered.root)),
|
|
256
|
+
asOf: new Intl.DateTimeFormat("en-CA", { timeZone: "America/Los_Angeles", year: "numeric", month: "2-digit", day: "2-digit" }).format(new Date()),
|
|
257
|
+
incompletePaths: discovered.selection.selected.filter(entry => entry.readBytes !== undefined).map(entry => repoPathFrom(entry.path, discovered.root)),
|
|
258
|
+
} }),
|
|
153
259
|
...(options.captureSnapshot ? { snapshot: Object.fromEntries(Object.keys(discovered.files).map(path => [path, captured[path]])) } : {}), fragment: discovered.fragment, selection: discovered.selection,
|
|
154
260
|
skippedFiles: mergeSkippedFiles(discovered.skippedFiles, ignored, expanded ? refused.map(file => ({ ...file, path: repoPathFrom(file.path, discovered.root) })) : refused),
|
|
155
261
|
warnings: [...new Set(mergeUnlistedFileWarnings([...warnings.map(file => fileRefusalWarning(repoPathFrom(file.path, discovered.root), file.reason)), ...discovered.warnings], ignored.length))] };
|
package/dist/adapters/git.js
CHANGED
|
@@ -14,7 +14,16 @@ export async function runGit(folder, args, options = {}) {
|
|
|
14
14
|
});
|
|
15
15
|
if (input)
|
|
16
16
|
running.child.stdin?.end(input);
|
|
17
|
-
|
|
17
|
+
try {
|
|
18
|
+
return (await running).stdout;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
options.signal?.throwIfAborted();
|
|
22
|
+
// Callers still need exit codes for ordinary Git misses, but public errors must not expose stderr.
|
|
23
|
+
if (error instanceof Error)
|
|
24
|
+
Object.assign(error, { reason: "local_git_failed" });
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
18
27
|
};
|
|
19
28
|
// Reading configuration runs no filters. Disable every filter and diff driver before any command touches working files.
|
|
20
29
|
let configured;
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { bundleContentHash } from "../bundle/seal/signature.js";
|
|
2
2
|
import { checkPlannedCallsAgainstBundle } from "../bundle/seal/planned-calls.js";
|
|
3
|
-
import { localSourcePlan } from "../analysis-core/requests/local-plan.js";
|
|
4
3
|
import { relayPlanSchema } from "../contract/console.js";
|
|
5
|
-
/** Callers authenticate the
|
|
6
|
-
export function
|
|
7
|
-
const { files, calls } = localSourcePlan(settings, source);
|
|
4
|
+
/** Callers authenticate the bundle; validate its richer descriptions before exposing aggregate fields. */
|
|
5
|
+
export function prepareRelayPlanFromCalls({ bundle, relayVersion, calls }) {
|
|
8
6
|
checkPlannedCallsAgainstBundle(bundle, calls);
|
|
9
|
-
return
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
return relayPlanSchema.parse({ bundleVersion: bundle.version,
|
|
8
|
+
bundleHash: bundleContentHash(bundle.sealed), relayVersion,
|
|
9
|
+
calls: calls.map(({ routeId, model, calls, maxInputBytes, maxInputTokens, maxOutputTokens }) => ({ routeId, model, calls, maxInputBytes, maxInputTokens, maxOutputTokens })) });
|
|
12
10
|
}
|
package/dist/adapters/release.js
CHANGED
|
@@ -8,6 +8,7 @@ import { z } from "zod";
|
|
|
8
8
|
import { cliRefusals } from "../contract/cli-refusals.js";
|
|
9
9
|
import { RELEASE_KEY_SPKI_SHA256 } from "../contract/release-key.js";
|
|
10
10
|
import { findingAudienceSettings } from "../analysis-core/analysis/finding-audience-switch.js";
|
|
11
|
+
import { findingActivitySettings } from "../analysis-core/analysis/finding-activity-switch.js";
|
|
11
12
|
const releasePins = z.object({ verificationKey: z.string().min(1), relayVersion: z.string().min(1), localVersion: z.string().min(1),
|
|
12
13
|
relayUrl: z.string().min(1), projectContext: z.literal("checks") });
|
|
13
14
|
export function readClientReleasePins(value) {
|
|
@@ -21,7 +22,9 @@ export function validateClientRelease(release, settings = {}) {
|
|
|
21
22
|
const tools = loadPublicRequestTools(bundle, release.verificationKey, settings);
|
|
22
23
|
if (!bundle.relayVersions.includes(release.relayVersion))
|
|
23
24
|
throw new Error();
|
|
24
|
-
|
|
25
|
+
// Request-only relays keep the earlier CLI output; static findings ship with whole-analysis engines.
|
|
26
|
+
const compatible = bundle.metadata.analysis ? settings : { ...settings, CF_COST_FINDINGS: "0" };
|
|
27
|
+
return { bundle, tools, settings: findingActivitySettings(findingAudienceSettings(compatible, bundle), bundle) };
|
|
25
28
|
}
|
|
26
29
|
export function readClientReleaseKey(pem) {
|
|
27
30
|
const key = createPublicKey(pem);
|
package/dist/adapters/results.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
|
+
import { skippedFilesEnabled } from "../analysis-core/settings.js";
|
|
2
|
+
import { combineLocalReports } from "./combined-results.js";
|
|
3
|
+
import { invalidateAnalysisContext, loadPublicReport } from "./analysis-results.js";
|
|
1
4
|
import { formatSummary } from "../cli/summary.js";
|
|
2
5
|
import { ignoreDefaultOutput } from "./gitignore-output.js";
|
|
3
6
|
import { writeOutputFiles } from "./output-paths.js";
|
|
4
7
|
import { projectPublicReport } from "../contract/public-report.js";
|
|
8
|
+
import { INCLUDED_FILES_CAPTION, INCOMPLETE_IMPORTS_MESSAGE, SELECTION_COVERAGE_DESCRIPTION } from "../contract/scope-choice.js";
|
|
5
9
|
export async function saveLocalResults(folder, report, refusedFiles, outputDir, options) {
|
|
6
|
-
const
|
|
10
|
+
const next = projectPublicReport(report, refusedFiles);
|
|
11
|
+
const publicReport = options.append && skippedFilesEnabled(options.settings ?? {})
|
|
12
|
+
? combineLocalReports(await loadPublicReport(folder, outputDir), next, options.append.assessments, options.append.readPaths).report : next;
|
|
13
|
+
if (!options.append)
|
|
14
|
+
await invalidateAnalysisContext(folder, outputDir);
|
|
15
|
+
const scope = options.scope;
|
|
16
|
+
const incomplete = options.selection && !options.selection.completeness.complete
|
|
17
|
+
? [INCOMPLETE_IMPORTS_MESSAGE, "", INCLUDED_FILES_CAPTION, ...options.includedPaths.map(path => ` ${path}`), "", ""].join("\n") : "";
|
|
18
|
+
const coverage = scope && scope.imports !== "all" ? SELECTION_COVERAGE_DESCRIPTION
|
|
19
|
+
.replace("{entry}", () => scope.entry).replace("{count}", String(Array.isArray(scope.imports) ? scope.imports.length : 0)) + "\n\n" : "";
|
|
7
20
|
const [findingsPath, summaryPath] = await writeOutputFiles(folder, {
|
|
8
21
|
"findings.json": `${JSON.stringify(publicReport, null, 2)}\n`,
|
|
9
|
-
"summary.md": formatSummary(publicReport, options),
|
|
22
|
+
"summary.md": incomplete + coverage + formatSummary(publicReport, { ...options, analyzedFolder: options.analyzedFolder ?? folder, skippedOffer: !options.append, skippedFilesEnabled: skippedFilesEnabled(options.settings ?? {}) }),
|
|
23
|
+
...(options.selection ? { "selection.json": JSON.stringify({ schemaVersion: 1, selection: options.selection,
|
|
24
|
+
...(scope ? { scope } : {}) }, null, 2) + "\n" } : {}),
|
|
10
25
|
}, outputDir);
|
|
11
|
-
const ignoreAdded = await ignoreDefaultOutput(folder, outputDir, options);
|
|
26
|
+
const ignoreAdded = await ignoreDefaultOutput(folder, options.defaultFileOutput ? undefined : outputDir, options);
|
|
12
27
|
return { findingsPath, summaryPath, ignoreAdded };
|
|
13
28
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
import { analysisSelectionRequestSchema, analysisSelectionResultSchema, ANALYSIS_SELECTION_LIMITS, MAX_ANALYSIS_ENTRIES } from "../analysis-core/contracts/analysis-selection.js";
|
|
4
|
+
import { findEntryScopeChoice } from "../analysis-core/import/entry-selection.js";
|
|
5
|
+
import { INVALID_SCOPE_MESSAGE, INVALID_SELECTION_MESSAGE, SCOPE_PROPOSAL_CONFLICT_MESSAGE, STALE_SCOPE_MESSAGE } from "../contract/scope-choice.js";
|
|
6
|
+
import { assessAnalysisSize, measureAnalysisSize } from "../analysis-core/conventions/analysis-fit.js";
|
|
7
|
+
import { ScopeChoiceError, selectionScopeChoices } from "../contract/selection-offer.js";
|
|
8
|
+
import { readLocalFolder } from "./files.js";
|
|
9
|
+
import { scopeChoicesEnabled, selectionModeEnabled } from "./settings.js";
|
|
10
|
+
import { entrySelectionProvider } from "./entry-selection.js";
|
|
11
|
+
import { fittingEntryGroups } from "./entry-packing.js";
|
|
12
|
+
const samePaths = (left, right) => left.length === right.length
|
|
13
|
+
&& new Set(left).size === left.length && left.every(path => right.includes(path));
|
|
14
|
+
function selectionRequest(selection) {
|
|
15
|
+
const parsed = analysisSelectionRequestSchema.safeParse(selection.request);
|
|
16
|
+
if (!parsed.success)
|
|
17
|
+
throw new Error(INVALID_SELECTION_MESSAGE);
|
|
18
|
+
const request = parsed.data;
|
|
19
|
+
const path = request.kind === "file" ? request.entry : request.folder;
|
|
20
|
+
if (request.launchRoot !== selection.launchRoot || request.kind !== selection.kind
|
|
21
|
+
|| join(request.launchRoot, path) !== selection.path
|
|
22
|
+
|| request.kind === "file" && request.entry !== selection.entryPath)
|
|
23
|
+
throw new Error(INVALID_SELECTION_MESSAGE);
|
|
24
|
+
return request;
|
|
25
|
+
}
|
|
26
|
+
/** The provider owns membership and admission; this boundary only verifies its captured answer. */
|
|
27
|
+
export async function readAnalysisSelection(settings, selection, options = {}) {
|
|
28
|
+
options.signal?.throwIfAborted();
|
|
29
|
+
const request = selectionRequest(selection);
|
|
30
|
+
const { source, membership: rawMembership } = await (options.provider ?? entrySelectionProvider)(settings, request, {
|
|
31
|
+
proposal: options.proposal, admission: options.admission, signal: options.signal,
|
|
32
|
+
});
|
|
33
|
+
options.signal?.throwIfAborted();
|
|
34
|
+
const parsed = analysisSelectionResultSchema.safeParse(rawMembership);
|
|
35
|
+
if (!parsed.success)
|
|
36
|
+
throw new Error(INVALID_SELECTION_MESSAGE);
|
|
37
|
+
const membership = parsed.data;
|
|
38
|
+
if (membership.launchRoot !== request.launchRoot || membership.kind !== request.kind
|
|
39
|
+
|| request.kind === "file" && (membership.kind !== "file" || membership.entry !== request.entry)
|
|
40
|
+
|| request.kind === "file" && membership.kind === "file" && JSON.stringify(request.entries) !== JSON.stringify(membership.entries)
|
|
41
|
+
|| request.kind === "folder" && (membership.kind !== "folder" || membership.folder !== request.folder))
|
|
42
|
+
throw new Error(INVALID_SELECTION_MESSAGE);
|
|
43
|
+
let paths = membership.files.map(file => file.path);
|
|
44
|
+
if (request.kind === "file" && request.scope) {
|
|
45
|
+
if (request.scope.snapshot !== membership.snapshot)
|
|
46
|
+
throw new Error(STALE_SCOPE_MESSAGE);
|
|
47
|
+
const choice = findEntryScopeChoice(membership, request.scope);
|
|
48
|
+
if (!choice)
|
|
49
|
+
throw new Error(INVALID_SCOPE_MESSAGE);
|
|
50
|
+
paths = choice.includes;
|
|
51
|
+
}
|
|
52
|
+
if (options.proposal?.files.some(file => !paths.includes(file.path)))
|
|
53
|
+
throw new Error(SCOPE_PROPOSAL_CONFLICT_MESSAGE);
|
|
54
|
+
if (!samePaths(Object.keys(source.files), paths) || !source.snapshot || !samePaths(Object.keys(source.snapshot), paths)
|
|
55
|
+
|| source.sourceBase !== undefined && source.sourceBase !== request.launchRoot
|
|
56
|
+
|| source.root !== undefined && source.root !== ".")
|
|
57
|
+
throw new Error(INVALID_SELECTION_MESSAGE);
|
|
58
|
+
const snapshot = Object.create(null);
|
|
59
|
+
for (const path of paths) {
|
|
60
|
+
const file = membership.files.find(file => file.path === path);
|
|
61
|
+
const bytes = source.snapshot[path];
|
|
62
|
+
// Decode the capture itself: a clipped string or replacement character cannot prove complete evidence.
|
|
63
|
+
let text;
|
|
64
|
+
try {
|
|
65
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new Error(INVALID_SELECTION_MESSAGE);
|
|
69
|
+
}
|
|
70
|
+
if (!Buffer.isBuffer(bytes) || bytes.length !== file.size || text !== source.files[path]
|
|
71
|
+
|| createHash("sha256").update(bytes).digest("hex") !== file.hash)
|
|
72
|
+
throw new Error(INVALID_SELECTION_MESSAGE);
|
|
73
|
+
snapshot[path] = Buffer.from(bytes);
|
|
74
|
+
}
|
|
75
|
+
if (!samePaths(source.selection.selected.map(file => file.path), paths)
|
|
76
|
+
|| source.selection.selected.some(file => file.size !== membership.files.find(member => member.path === file.path)?.size))
|
|
77
|
+
throw new Error(INVALID_SELECTION_MESSAGE);
|
|
78
|
+
// All downstream paths use launch-relative coordinates, including imported files outside the entry's parent.
|
|
79
|
+
const selectedPath = relative(request.launchRoot, selection.path).split(sep).join("/");
|
|
80
|
+
if (selectedPath === ".." || selectedPath.startsWith("../"))
|
|
81
|
+
throw new Error(INVALID_SELECTION_MESSAGE);
|
|
82
|
+
return { source: { ...source, sourceBase: request.launchRoot, files: Object.freeze({ ...source.files }), snapshot }, membership };
|
|
83
|
+
}
|
|
84
|
+
/** Local byte admission can prove a group fits; ordinary run admission still validates its captured source. */
|
|
85
|
+
export function selectionSizeOffer(settings, membership, source, options = {}) {
|
|
86
|
+
const currentPaths = Object.keys(source.files);
|
|
87
|
+
const entryCount = membership.kind === "file" && membership.entries
|
|
88
|
+
? membership.files.filter(file => file.role === "entry" && currentPaths.includes(file.path)).length : 1;
|
|
89
|
+
if (!options.force && source.analysisFit?.fits !== false && entryCount <= MAX_ANALYSIS_ENTRIES)
|
|
90
|
+
return undefined;
|
|
91
|
+
const oversizedPaths = membership.files.filter(file => file.role === "entry"
|
|
92
|
+
&& !assessAnalysisSize(measureAnalysisSize(settings, [file])).fits).map(file => file.path);
|
|
93
|
+
const groups = fittingEntryGroups(settings, membership, currentPaths);
|
|
94
|
+
const choices = [...groups, ...membership.choices].slice(0, ANALYSIS_SELECTION_LIMITS.choices);
|
|
95
|
+
const offer = selectionScopeChoices({ ...membership, choices }, { currentPaths, oversizedPaths, allowCurrent: options.force });
|
|
96
|
+
if (offer)
|
|
97
|
+
for (const choice of offer.choices)
|
|
98
|
+
if (groups.some(group => group.id === choice.id))
|
|
99
|
+
choice.fits = true;
|
|
100
|
+
return offer;
|
|
101
|
+
}
|
|
102
|
+
/** Both execution paths use this owner to select the reader and stop before a nonfitting run. */
|
|
103
|
+
export async function readAnalysisSource(settings, folder, options = {}) {
|
|
104
|
+
const selection = selectionModeEnabled(settings) ? options.selection : undefined;
|
|
105
|
+
const selected = selection && (selection.kind === "file" || options.provider && scopeChoicesEnabled(settings))
|
|
106
|
+
? await readAnalysisSelection(settings, selection, options) : undefined;
|
|
107
|
+
const source = selected?.source ?? await (options.readFolder?.() ?? readLocalFolder(settings, folder, options.admission));
|
|
108
|
+
if (selected && scopeChoicesEnabled(settings)) {
|
|
109
|
+
const offer = selectionSizeOffer(settings, selected.membership, source);
|
|
110
|
+
if (offer)
|
|
111
|
+
throw new ScopeChoiceError(offer);
|
|
112
|
+
}
|
|
113
|
+
return { source, ...(selected ? { membership: selected.membership } : {}) };
|
|
114
|
+
}
|