@platformos/platformos-mcp-supervisor 0.0.2 → 0.2.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 +105 -0
- package/dist/adapter-input.d.ts +113 -0
- package/dist/adapter-input.js +210 -0
- package/dist/adapter-input.spec.d.ts +1 -0
- package/dist/adapter-input.spec.js +275 -0
- package/dist/bin/args.d.ts +15 -2
- package/dist/bin/args.js +31 -3
- package/dist/bin/args.spec.d.ts +1 -0
- package/dist/bin/args.spec.js +35 -0
- package/dist/bin/platformos-mcp-supervisor.js +6 -2
- package/dist/check-docs.d.ts +21 -0
- package/dist/check-docs.js +38 -0
- package/dist/check-docs.spec.d.ts +1 -0
- package/dist/check-docs.spec.js +87 -0
- package/dist/context.d.ts +34 -0
- package/dist/context.js +12 -0
- package/dist/cost-model.d.ts +188 -0
- package/dist/cost-model.js +197 -0
- package/dist/cost-model.spec.d.ts +1 -0
- package/dist/cost-model.spec.js +91 -0
- package/dist/deadline.d.ts +27 -0
- package/dist/deadline.js +40 -0
- package/dist/deadline.spec.d.ts +1 -0
- package/dist/deadline.spec.js +83 -0
- package/dist/enrich/enrich.d.ts +105 -0
- package/dist/enrich/enrich.js +136 -0
- package/dist/enrich/enrich.spec.d.ts +1 -0
- package/dist/enrich/enrich.spec.js +184 -0
- package/dist/enrich/symbol.d.ts +16 -0
- package/dist/enrich/symbol.js +58 -0
- package/dist/enrich/symbol.spec.d.ts +1 -0
- package/dist/enrich/symbol.spec.js +89 -0
- package/dist/impact/dependants.d.ts +75 -0
- package/dist/impact/dependants.js +104 -0
- package/dist/impact/diff.d.ts +27 -0
- package/dist/impact/diff.js +33 -0
- package/dist/impact/impact.d.ts +21 -0
- package/dist/impact/impact.js +160 -0
- package/dist/impact/impact.spec.d.ts +1 -0
- package/dist/impact/impact.spec.js +357 -0
- package/dist/impact/project-scan.d.ts +39 -0
- package/dist/impact/project-scan.js +62 -0
- package/dist/impact/project-scan.spec.d.ts +1 -0
- package/dist/impact/project-scan.spec.js +121 -0
- package/dist/index.d.ts +1 -1
- package/dist/lint/fixes.d.ts +28 -0
- package/dist/lint/fixes.js +30 -0
- package/dist/lint/lint-batch.d.ts +100 -0
- package/dist/lint/lint-batch.js +98 -0
- package/dist/lint/lint-batch.spec.d.ts +1 -0
- package/dist/lint/lint-batch.spec.js +503 -0
- package/dist/lint/lint.d.ts +11 -1
- package/dist/lint/lint.js +9 -3
- package/dist/result/assemble.d.ts +11 -9
- package/dist/result/assemble.js +63 -16
- package/dist/result/assemble.spec.d.ts +1 -0
- package/dist/result/assemble.spec.js +319 -0
- package/dist/result/blocking.d.ts +92 -0
- package/dist/result/blocking.js +116 -0
- package/dist/result/blocking.spec.d.ts +1 -0
- package/dist/result/blocking.spec.js +201 -0
- package/dist/result/impact-states.d.ts +19 -0
- package/dist/result/impact-states.js +10 -0
- package/dist/result/response-budget.d.ts +9 -0
- package/dist/result/response-budget.js +133 -0
- package/dist/result/response-budget.spec.d.ts +1 -0
- package/dist/result/response-budget.spec.js +222 -0
- package/dist/result/types.d.ts +212 -104
- package/dist/result/types.js +4 -13
- package/dist/transport/instructions-coverage.spec.d.ts +1 -0
- package/dist/transport/instructions-coverage.spec.js +176 -0
- package/dist/transport/instructions.d.ts +1 -0
- package/dist/transport/instructions.js +150 -0
- package/dist/transport/process-guards.d.ts +43 -0
- package/dist/transport/process-guards.js +63 -0
- package/dist/transport/process-guards.spec.d.ts +1 -0
- package/dist/transport/process-guards.spec.js +200 -0
- package/dist/transport/server.d.ts +6 -1
- package/dist/transport/server.js +21 -10
- package/dist/transport/validate-code.d.ts +50 -15
- package/dist/transport/validate-code.js +238 -33
- package/dist/transport/validate-code.spec.d.ts +1 -0
- package/dist/transport/validate-code.spec.js +2303 -0
- package/dist/validate/batch-bounds.d.ts +34 -0
- package/dist/validate/batch-bounds.js +65 -0
- package/dist/validate/batch-coherence.d.ts +21 -0
- package/dist/validate/batch-coherence.js +54 -0
- package/dist/validate/batch-coherence.spec.d.ts +1 -0
- package/dist/validate/batch-coherence.spec.js +111 -0
- package/dist/validate/validate-buffers.d.ts +27 -0
- package/dist/validate/validate-buffers.js +319 -0
- package/package.json +8 -7
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which files depend on the one being edited — the input to the lint diff, and nothing more.
|
|
3
|
+
*
|
|
4
|
+
* This module answers only "which files should I lint to find out what this change broke".
|
|
5
|
+
* It never decides whether something IS broken: that is the check engine's job, and the
|
|
6
|
+
* whole point of the diff is to stop reimplementing it here.
|
|
7
|
+
*
|
|
8
|
+
* NO EDGE-KIND FILTER, deliberately. An earlier version kept only `render`/`include`/
|
|
9
|
+
* `function` because it compared arguments by hand and any other kind produced false
|
|
10
|
+
* positives. The engine has no such problem — a `{% background %}` caller, a layout edge or
|
|
11
|
+
* an asset reference can each be broken by an edit — so every kind is a dependant here.
|
|
12
|
+
*
|
|
13
|
+
* `null` means NOT LOOKED AT: too much candidate text to examine inside the deadline. It is
|
|
14
|
+
* not "no dependants" and must never be reported as one.
|
|
15
|
+
*
|
|
16
|
+
* SOUND, NOT COMPLETE, and the gap is not closable. Every edge platformos-graph records
|
|
17
|
+
* names its target with a STATIC STRING LITERAL, so a dependant's text must contain the
|
|
18
|
+
* target's logical name; filtering on that is a sound over-approximation leaving few
|
|
19
|
+
* survivors (measured p50 1, p90 6 candidates on a 2,615-file project), and 0 of 8,464 real
|
|
20
|
+
* edges were dropped by it. What it cannot see is a caller that names its target at runtime
|
|
21
|
+
* or does not parse — see `frontier.ts`, which reports exactly that gap rather than hiding
|
|
22
|
+
* it.
|
|
23
|
+
*/
|
|
24
|
+
import { type UriString } from '@platformos/platformos-check-common';
|
|
25
|
+
import type { ProjectScan } from './project-scan.js';
|
|
26
|
+
/**
|
|
27
|
+
* Whether this file could have dependants AT ALL — decidable from its path alone, with no
|
|
28
|
+
* project read and no parse.
|
|
29
|
+
*
|
|
30
|
+
* TWO CONDITIONS, and both are load-bearing:
|
|
31
|
+
*
|
|
32
|
+
* - it must be a resolvable edge TARGET, which means Liquid or GraphQL. Schema,
|
|
33
|
+
* custom-model-type and translation YAML are deliberately excluded: they are wired by
|
|
34
|
+
* model, table or translation-key NAME rather than by file reference (ADR 004), so the
|
|
35
|
+
* graph holds no edge to them and asking would silently answer "nothing depends on this".
|
|
36
|
+
* Giving those files real dependants is TASK-95, in the graph rather than here.
|
|
37
|
+
* - it must have a logical NAME, since a reference spells one. A source in no platformOS
|
|
38
|
+
* directory has none and so cannot be referenced.
|
|
39
|
+
*
|
|
40
|
+
* Asked twice on purpose: by the orchestrator, to skip reading the project for a changeset
|
|
41
|
+
* that cannot use it, and by impact itself. One predicate, so the two cannot disagree about
|
|
42
|
+
* whether that read was needed.
|
|
43
|
+
*/
|
|
44
|
+
export declare function canHaveDependants(uri: UriString, rootUri: UriString): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The distinct files that reference `fileUri`, excluding anything in `exclude`.
|
|
47
|
+
*
|
|
48
|
+
* `exclude` carries the changeset: a file being edited is already reported on its own terms,
|
|
49
|
+
* and linting it again as somebody's dependant would report the same finding twice. It also
|
|
50
|
+
* makes the scan's text the DISK text for every dependant returned, which is exactly what
|
|
51
|
+
* the diff's baseline pass needs — the scan overlays buffers, so excluding them is what
|
|
52
|
+
* keeps the baseline honest without a second read.
|
|
53
|
+
*/
|
|
54
|
+
export declare function dependantsOf(scan: ProjectScan, fileUri: UriString, name: string, exclude: ReadonlySet<UriString>): Promise<UriString[] | null>;
|
|
55
|
+
/**
|
|
56
|
+
* A dependant as the lint takes it: an absolute path and the text on disk.
|
|
57
|
+
*
|
|
58
|
+
* Carries its own `uri` rather than relying on position, because {@link toDependantBuffers}
|
|
59
|
+
* can DROP an entry — zipping the result back against the input by index would then pair
|
|
60
|
+
* every later buffer with the wrong file, silently.
|
|
61
|
+
*/
|
|
62
|
+
export interface DependantBuffer {
|
|
63
|
+
uri: UriString;
|
|
64
|
+
filePath: string;
|
|
65
|
+
content: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Turn dependant URIs into lint buffers, reading their text from the scan the discovery
|
|
69
|
+
* already paid for rather than going back to disk.
|
|
70
|
+
*
|
|
71
|
+
* `path.fsPath` is check-common's own URI-to-path conversion, not a local spelling of it: on
|
|
72
|
+
* Windows a URI and a filesystem path differ in separator AND in how the drive letter is
|
|
73
|
+
* encoded, and a hand-rolled conversion is the mistake that only the Windows CI job catches.
|
|
74
|
+
*/
|
|
75
|
+
export declare function toDependantBuffers(uris: readonly UriString[], sources: ReadonlyMap<UriString, string>): DependantBuffer[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which files depend on the one being edited — the input to the lint diff, and nothing more.
|
|
3
|
+
*
|
|
4
|
+
* This module answers only "which files should I lint to find out what this change broke".
|
|
5
|
+
* It never decides whether something IS broken: that is the check engine's job, and the
|
|
6
|
+
* whole point of the diff is to stop reimplementing it here.
|
|
7
|
+
*
|
|
8
|
+
* NO EDGE-KIND FILTER, deliberately. An earlier version kept only `render`/`include`/
|
|
9
|
+
* `function` because it compared arguments by hand and any other kind produced false
|
|
10
|
+
* positives. The engine has no such problem — a `{% background %}` caller, a layout edge or
|
|
11
|
+
* an asset reference can each be broken by an edit — so every kind is a dependant here.
|
|
12
|
+
*
|
|
13
|
+
* `null` means NOT LOOKED AT: too much candidate text to examine inside the deadline. It is
|
|
14
|
+
* not "no dependants" and must never be reported as one.
|
|
15
|
+
*
|
|
16
|
+
* SOUND, NOT COMPLETE, and the gap is not closable. Every edge platformos-graph records
|
|
17
|
+
* names its target with a STATIC STRING LITERAL, so a dependant's text must contain the
|
|
18
|
+
* target's logical name; filtering on that is a sound over-approximation leaving few
|
|
19
|
+
* survivors (measured p50 1, p90 6 candidates on a 2,615-file project), and 0 of 8,464 real
|
|
20
|
+
* edges were dropped by it. What it cannot see is a caller that names its target at runtime
|
|
21
|
+
* or does not parse — see `frontier.ts`, which reports exactly that gap rather than hiding
|
|
22
|
+
* it.
|
|
23
|
+
*/
|
|
24
|
+
import { path, SourceCodeType } from '@platformos/platformos-check-common';
|
|
25
|
+
import { MAX_CANDIDATE_BYTES } from '../cost-model.js';
|
|
26
|
+
import { sourceCodeTypeOf, uriToName } from '@platformos/platformos-common';
|
|
27
|
+
import { extractFileReferences, toSourceCode } from '@platformos/platformos-graph';
|
|
28
|
+
/**
|
|
29
|
+
* Whether this file could have dependants AT ALL — decidable from its path alone, with no
|
|
30
|
+
* project read and no parse.
|
|
31
|
+
*
|
|
32
|
+
* TWO CONDITIONS, and both are load-bearing:
|
|
33
|
+
*
|
|
34
|
+
* - it must be a resolvable edge TARGET, which means Liquid or GraphQL. Schema,
|
|
35
|
+
* custom-model-type and translation YAML are deliberately excluded: they are wired by
|
|
36
|
+
* model, table or translation-key NAME rather than by file reference (ADR 004), so the
|
|
37
|
+
* graph holds no edge to them and asking would silently answer "nothing depends on this".
|
|
38
|
+
* Giving those files real dependants is TASK-95, in the graph rather than here.
|
|
39
|
+
* - it must have a logical NAME, since a reference spells one. A source in no platformOS
|
|
40
|
+
* directory has none and so cannot be referenced.
|
|
41
|
+
*
|
|
42
|
+
* Asked twice on purpose: by the orchestrator, to skip reading the project for a changeset
|
|
43
|
+
* that cannot use it, and by impact itself. One predicate, so the two cannot disagree about
|
|
44
|
+
* whether that read was needed.
|
|
45
|
+
*/
|
|
46
|
+
export function canHaveDependants(uri, rootUri) {
|
|
47
|
+
const type = sourceCodeTypeOf(uri);
|
|
48
|
+
const isEdgeTarget = type === SourceCodeType.LiquidHtml || type === SourceCodeType.GraphQL;
|
|
49
|
+
return isEdgeTarget && uriToName(uri, rootUri)?.name !== undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The distinct files that reference `fileUri`, excluding anything in `exclude`.
|
|
53
|
+
*
|
|
54
|
+
* `exclude` carries the changeset: a file being edited is already reported on its own terms,
|
|
55
|
+
* and linting it again as somebody's dependant would report the same finding twice. It also
|
|
56
|
+
* makes the scan's text the DISK text for every dependant returned, which is exactly what
|
|
57
|
+
* the diff's baseline pass needs — the scan overlays buffers, so excluding them is what
|
|
58
|
+
* keeps the baseline honest without a second read.
|
|
59
|
+
*/
|
|
60
|
+
export async function dependantsOf(scan, fileUri, name, exclude) {
|
|
61
|
+
const sources = await scan.sources();
|
|
62
|
+
const candidates = [...sources].filter(([uri, source]) => !exclude.has(uri) && uri !== fileUri && source.includes(name));
|
|
63
|
+
// BOUNDED BEFORE THE EXPENSIVE PART. Parsing is what discovery costs, and the byte total
|
|
64
|
+
// is known from the substring scan already done — so a file referenced from too much text
|
|
65
|
+
// to examine says so in milliseconds instead of spending seconds to reach the same answer.
|
|
66
|
+
const bytes = candidates.reduce((total, [, source]) => total + Buffer.byteLength(source), 0);
|
|
67
|
+
if (bytes > MAX_CANDIDATE_BYTES)
|
|
68
|
+
return null;
|
|
69
|
+
const perCandidate = await Promise.all(candidates.map(async ([uri, source]) => {
|
|
70
|
+
const sourceCode = await toSourceCode(uri, source);
|
|
71
|
+
const references = await extractFileReferences(scan.rootUri, uri, sourceCode, {
|
|
72
|
+
fs: scan.fs,
|
|
73
|
+
});
|
|
74
|
+
// The name filter over-approximates; resolving through the graph's OWN resolver is
|
|
75
|
+
// what makes the answer exact. A file that merely mentions the name contributes
|
|
76
|
+
// nothing.
|
|
77
|
+
return references.some((reference) => reference.target.uri === fileUri) ? uri : undefined;
|
|
78
|
+
}));
|
|
79
|
+
return perCandidate
|
|
80
|
+
.filter((uri) => uri !== undefined)
|
|
81
|
+
.sort((a, b) => a.localeCompare(b));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Turn dependant URIs into lint buffers, reading their text from the scan the discovery
|
|
85
|
+
* already paid for rather than going back to disk.
|
|
86
|
+
*
|
|
87
|
+
* `path.fsPath` is check-common's own URI-to-path conversion, not a local spelling of it: on
|
|
88
|
+
* Windows a URI and a filesystem path differ in separator AND in how the drive letter is
|
|
89
|
+
* encoded, and a hand-rolled conversion is the mistake that only the Windows CI job catches.
|
|
90
|
+
*/
|
|
91
|
+
export function toDependantBuffers(uris, sources) {
|
|
92
|
+
const buffers = [];
|
|
93
|
+
for (const uri of uris) {
|
|
94
|
+
const content = sources.get(uri);
|
|
95
|
+
// A URI the scan does not hold cannot be linted from it. Unreachable — every dependant
|
|
96
|
+
// came OUT of this map — but defaulting to empty text would lint a file as blank and
|
|
97
|
+
// report every one of its findings as newly caused.
|
|
98
|
+
if (content === undefined)
|
|
99
|
+
continue;
|
|
100
|
+
buffers.push({ uri, filePath: path.fsPath(uri), content });
|
|
101
|
+
}
|
|
102
|
+
return buffers;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=dependants.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a change INTRODUCED in a file it did not edit.
|
|
3
|
+
*
|
|
4
|
+
* PURE, and deliberately the whole definition of "relevant". The alternative — an allowlist
|
|
5
|
+
* of check codes that count as cross-file — rots the first time a check is added, and it
|
|
6
|
+
* asks the wrong question anyway: a `DeprecatedFrontmatterField` already in a dependant is
|
|
7
|
+
* irrelevant because it was there BEFORE, not because of its code. Relevance is CAUSAL, so
|
|
8
|
+
* it is computed by comparing the same file linted twice rather than by classifying codes.
|
|
9
|
+
*
|
|
10
|
+
* IDENTITY IS (check, line, column). The dependant's own text is byte-identical across the
|
|
11
|
+
* two passes — only the files it depends on changed — so positions are stable and cannot
|
|
12
|
+
* drift under the comparison. Matching on the code alone would let a pre-existing
|
|
13
|
+
* `UnknownFilter` on line 90 mask a new one on line 3.
|
|
14
|
+
*
|
|
15
|
+
* A MULTISET, not a set: two offenses can legitimately share a code and a position is the
|
|
16
|
+
* only thing that separates them, so the count at each identity is what carries over. Three
|
|
17
|
+
* `before` and four `after` at one identity means one new, not none.
|
|
18
|
+
*/
|
|
19
|
+
import type { ValidateCodeDiagnostic } from '../result/types.js';
|
|
20
|
+
/**
|
|
21
|
+
* The diagnostics in `after` that are not in `before`, preserving `after`'s order.
|
|
22
|
+
*
|
|
23
|
+
* `before` is the file as it stands with NONE of the changeset overlaid; `after` is the same
|
|
24
|
+
* file with the changeset applied. Everything returned is therefore attributable to the
|
|
25
|
+
* change.
|
|
26
|
+
*/
|
|
27
|
+
export declare function introducedDiagnostics(after: readonly ValidateCodeDiagnostic[], before: readonly ValidateCodeDiagnostic[]): ValidateCodeDiagnostic[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** The comparison key: what makes two findings the same finding. */
|
|
2
|
+
function identityOf(diagnostic) {
|
|
3
|
+
return `${diagnostic.check}\u0000${diagnostic.line}\u0000${diagnostic.column}`;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The diagnostics in `after` that are not in `before`, preserving `after`'s order.
|
|
7
|
+
*
|
|
8
|
+
* `before` is the file as it stands with NONE of the changeset overlaid; `after` is the same
|
|
9
|
+
* file with the changeset applied. Everything returned is therefore attributable to the
|
|
10
|
+
* change.
|
|
11
|
+
*/
|
|
12
|
+
export function introducedDiagnostics(after, before) {
|
|
13
|
+
if (after.length === 0)
|
|
14
|
+
return [];
|
|
15
|
+
const remaining = new Map();
|
|
16
|
+
for (const diagnostic of before) {
|
|
17
|
+
const key = identityOf(diagnostic);
|
|
18
|
+
remaining.set(key, (remaining.get(key) ?? 0) + 1);
|
|
19
|
+
}
|
|
20
|
+
const introduced = [];
|
|
21
|
+
for (const diagnostic of after) {
|
|
22
|
+
const key = identityOf(diagnostic);
|
|
23
|
+
const unmatched = remaining.get(key) ?? 0;
|
|
24
|
+
// Each `before` occurrence cancels exactly one `after` occurrence, so a code that
|
|
25
|
+
// legitimately fires twice does not swallow a third.
|
|
26
|
+
if (unmatched > 0)
|
|
27
|
+
remaining.set(key, unmatched - 1);
|
|
28
|
+
else
|
|
29
|
+
introduced.push(diagnostic);
|
|
30
|
+
}
|
|
31
|
+
return introduced;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=diff.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type DocsetVocabulary } from '../enrich/enrich.js';
|
|
2
|
+
import type { BatchBuffer, runBatchLint } from '../lint/lint-batch.js';
|
|
3
|
+
import type { ValidateCodeImpact } from '../result/types.js';
|
|
4
|
+
import type { ProjectScan } from './project-scan.js';
|
|
5
|
+
/** Everything one impact pass needs. The lint seam is injected so the contract is testable. */
|
|
6
|
+
export interface ImpactInput {
|
|
7
|
+
projectDir: string;
|
|
8
|
+
/** The changeset, exactly as the primary lint received it. */
|
|
9
|
+
buffers: readonly BatchBuffer[];
|
|
10
|
+
scan: ProjectScan;
|
|
11
|
+
lint: typeof runBatchLint;
|
|
12
|
+
docset: () => Promise<DocsetVocabulary>;
|
|
13
|
+
log: (message: string) => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The impact of the whole changeset, per requested buffer key.
|
|
17
|
+
*
|
|
18
|
+
* Throws only on real I/O failure — the caller degrades that to `unavailable` for every
|
|
19
|
+
* buffer, because a failure here says nothing about any of them.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runImpact(input: ImpactInput): Promise<Map<string, ValidateCodeImpact>>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impact — what this change breaks in files the agent is NOT editing.
|
|
3
|
+
*
|
|
4
|
+
* Lint is per-file and forward-looking: it visits only the buffers it was sent, so a page
|
|
5
|
+
* that the edited partial has just broken is never even looked at. This stage looks at it,
|
|
6
|
+
* and it does so by ASKING THE CHECK ENGINE rather than by re-deriving the answer:
|
|
7
|
+
*
|
|
8
|
+
* pass A the dependants, linted with the changeset overlaid
|
|
9
|
+
* pass B the same dependants, linted with nothing overlaid
|
|
10
|
+
* report A \ B — exactly the findings this change introduced
|
|
11
|
+
*
|
|
12
|
+
* WHY A DIFF AND NOT A CHECK-CODE ALLOWLIST. An earlier version compared `{% doc %}`
|
|
13
|
+
* parameters by hand, which reimplemented `MissingRenderPartialArguments` with no message,
|
|
14
|
+
* no documentation link and an invented severity. The diff needs no list of "cross-file
|
|
15
|
+
* relevant" codes, because relevance is CAUSAL: a `DeprecatedFrontmatterField` already in a
|
|
16
|
+
* dependant is irrelevant for having been there BEFORE, not for its code. A check added
|
|
17
|
+
* upstream is covered on the day it ships, with no edit here.
|
|
18
|
+
*
|
|
19
|
+
* SERIALIZED, BY NECESSITY. `lintBuffers` overlays buffers into check-node's process-shared
|
|
20
|
+
* `App` and reverts them on the way out, and that App has no lock. Two lint passes therefore
|
|
21
|
+
* cannot overlap — with each other, or with the request's primary lint. The orchestrator
|
|
22
|
+
* runs the primary lint concurrently with the project SCAN (pure fs reads, no App) and only
|
|
23
|
+
* then runs these two passes, so the write gate is never delayed by, or lost to, impact.
|
|
24
|
+
*
|
|
25
|
+
* BOUNDED BY INPUT, NOT BY THE DEADLINE. `IMPACT_DEADLINE_MS` cannot bound this: a lint is
|
|
26
|
+
* synchronous CPU work and no timer preempts it. `MAX_CANDIDATE_BYTES` caps the text
|
|
27
|
+
* discovery will parse and `MAX_DEPENDANTS_LINTED` caps how many dependants are linted, both
|
|
28
|
+
* derived in `cost-model.ts` to fit inside that deadline. Hitting either is REPORTED —
|
|
29
|
+
* `status: unavailable` or `unchecked_dependants` — never silently absorbed.
|
|
30
|
+
*
|
|
31
|
+
* NOTHING HERE IS A CLEARANCE. A dependant that names its target at runtime, or that does
|
|
32
|
+
* not parse, is invisible to discovery — so an empty answer means no break was FOUND among
|
|
33
|
+
* the dependants that are visible, never that none exists.
|
|
34
|
+
*/
|
|
35
|
+
import { path } from '@platformos/platformos-check-common';
|
|
36
|
+
import { uriToName } from '@platformos/platformos-common';
|
|
37
|
+
import { toAbsoluteFilePath } from '../adapter-input.js';
|
|
38
|
+
import { MAX_DEPENDANTS_LINTED } from '../cost-model.js';
|
|
39
|
+
import { enrichBatch } from '../enrich/enrich.js';
|
|
40
|
+
import { NOT_APPLICABLE_IMPACT, UNAVAILABLE_IMPACT } from '../result/impact-states.js';
|
|
41
|
+
import { canHaveDependants, dependantsOf, toDependantBuffers } from './dependants.js';
|
|
42
|
+
import { introducedDiagnostics } from './diff.js';
|
|
43
|
+
/**
|
|
44
|
+
* The impact of the whole changeset, per requested buffer key.
|
|
45
|
+
*
|
|
46
|
+
* Throws only on real I/O failure — the caller degrades that to `unavailable` for every
|
|
47
|
+
* buffer, because a failure here says nothing about any of them.
|
|
48
|
+
*/
|
|
49
|
+
export async function runImpact(input) {
|
|
50
|
+
const { projectDir, buffers, scan } = input;
|
|
51
|
+
const rootUri = path.normalize(path.URI.file(projectDir));
|
|
52
|
+
const uriOf = (buffer) => path.normalize(path.URI.file(toAbsoluteFilePath(projectDir, buffer.filePath)));
|
|
53
|
+
// A file in the changeset is reported on its own terms. Excluding the whole changeset from
|
|
54
|
+
// every dependant set is also what makes the scan's text the DISK text for each dependant,
|
|
55
|
+
// which is exactly what pass B's baseline needs.
|
|
56
|
+
const changeset = new Set(buffers.map(uriOf));
|
|
57
|
+
const byBuffer = new Map();
|
|
58
|
+
const dependantsPerBuffer = new Map();
|
|
59
|
+
for (const buffer of buffers) {
|
|
60
|
+
const uri = uriOf(buffer);
|
|
61
|
+
// Applicability is a property of the FILE and needs no scan. The name is read separately
|
|
62
|
+
// because discovery needs the STRING while `canHaveDependants` answers a boolean; the
|
|
63
|
+
// second condition below narrows that string and is not a second rule.
|
|
64
|
+
const name = uriToName(uri, rootUri)?.name;
|
|
65
|
+
if (!canHaveDependants(uri, rootUri) || name === undefined) {
|
|
66
|
+
byBuffer.set(buffer.filePath, NOT_APPLICABLE_IMPACT());
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const dependants = await dependantsOf(scan, uri, name, changeset);
|
|
70
|
+
// `null` is "too much candidate text to examine", NOT "no dependants". Reported as
|
|
71
|
+
// `unavailable`, which is what it is: the comparison did not run.
|
|
72
|
+
if (dependants === null) {
|
|
73
|
+
byBuffer.set(buffer.filePath, UNAVAILABLE_IMPACT());
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
dependantsPerBuffer.set(buffer.filePath, dependants);
|
|
77
|
+
byBuffer.set(buffer.filePath, { status: 'computed' });
|
|
78
|
+
}
|
|
79
|
+
const found = [...new Set([...dependantsPerBuffer.values()].flat())].sort((a, b) => a.localeCompare(b));
|
|
80
|
+
if (found.length === 0)
|
|
81
|
+
return byBuffer;
|
|
82
|
+
// BOUNDED INPUT, not a deadline: a lint is synchronous CPU work and no timer preempts it
|
|
83
|
+
// (see `deadline.ts`), so the only defence is refusing to start more of it than fits.
|
|
84
|
+
// Sorted first, so which dependants survive the bound is deterministic rather than
|
|
85
|
+
// whichever order a directory walk happened to produce.
|
|
86
|
+
const linted = new Set(found.slice(0, MAX_DEPENDANTS_LINTED));
|
|
87
|
+
const introduced = await introducedByDependant(input, [...linted]);
|
|
88
|
+
for (const [key, dependants] of dependantsPerBuffer) {
|
|
89
|
+
const checked = dependants.filter((uri) => linted.has(uri));
|
|
90
|
+
const breaks = checked
|
|
91
|
+
.map((uri) => ({ uri, diagnostics: introduced.get(uri) ?? [] }))
|
|
92
|
+
.filter(({ diagnostics }) => diagnostics.length > 0)
|
|
93
|
+
.map(({ uri, diagnostics }) => ({ file: path.relative(uri, rootUri), diagnostics }));
|
|
94
|
+
const impact = { status: 'computed' };
|
|
95
|
+
// Omitted when empty. An empty list reads as "checked, nothing depends on this that
|
|
96
|
+
// could break", which a scan of the dependants that happen to be VISIBLE cannot earn.
|
|
97
|
+
if (breaks.length > 0)
|
|
98
|
+
impact.breaks = breaks;
|
|
99
|
+
// Reported per buffer rather than per request: the bound is global, but whether it cost
|
|
100
|
+
// THIS buffer anything depends on how many of its own dependants fell past it.
|
|
101
|
+
if (checked.length < dependants.length) {
|
|
102
|
+
impact.unchecked_dependants = { returned: checked.length, total: dependants.length };
|
|
103
|
+
}
|
|
104
|
+
byBuffer.set(key, impact);
|
|
105
|
+
}
|
|
106
|
+
return byBuffer;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The findings each dependant gained because of the changeset, keyed by its URI.
|
|
110
|
+
*
|
|
111
|
+
* The two passes are AWAITED IN SEQUENCE, never raced: they share check-node's process-wide
|
|
112
|
+
* `App`, and an overlay installed by one while the other is reverting would corrupt both.
|
|
113
|
+
*/
|
|
114
|
+
async function introducedByDependant(input, dependants) {
|
|
115
|
+
const { projectDir, buffers, scan, lint, docset, log } = input;
|
|
116
|
+
const sources = await scan.sources();
|
|
117
|
+
const dependantBuffers = toDependantBuffers(dependants, sources);
|
|
118
|
+
const keyOf = new Map(dependantBuffers.map((buffer) => [buffer.filePath, buffer.uri]));
|
|
119
|
+
// WITH the changeset overlaid. The changeset's own buffers are included because a
|
|
120
|
+
// dependant must see the edit to be broken by it; their results are discarded here, since
|
|
121
|
+
// the primary lint already reported them.
|
|
122
|
+
const after = await lint({ projectDir, buffers: [...buffers, ...dependantBuffers] });
|
|
123
|
+
// Only a dependant that found something in pass A needs a baseline: the diff is A \ B, so
|
|
124
|
+
// an empty A is empty whatever B holds. Worth less than it looks — measured, a page
|
|
125
|
+
// reports `DeprecatedFrontmatterField` whatever it contains, so only partial dependants
|
|
126
|
+
// are ever clean enough to skip.
|
|
127
|
+
const withFindings = dependantBuffers.filter((buffer) => (after.diagnostics.get(buffer.filePath)?.length ?? 0) > 0);
|
|
128
|
+
const before = withFindings.length
|
|
129
|
+
? await lint({ projectDir, buffers: withFindings })
|
|
130
|
+
: undefined;
|
|
131
|
+
const enriched = await enrichBatch(onlyDependants(after, keyOf), docset, log);
|
|
132
|
+
const introduced = new Map();
|
|
133
|
+
for (const [filePath, uri] of keyOf) {
|
|
134
|
+
const gained = introducedDiagnostics(enriched.get(filePath) ?? [], before?.diagnostics.get(filePath) ?? []);
|
|
135
|
+
if (gained.length > 0)
|
|
136
|
+
introduced.set(uri, gained);
|
|
137
|
+
}
|
|
138
|
+
return introduced;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Pass A narrowed to the dependants, so enrichment is not paid a second time for the
|
|
142
|
+
* changeset's own findings — the primary lint has already enriched those.
|
|
143
|
+
*/
|
|
144
|
+
function onlyDependants(pass, keyOf) {
|
|
145
|
+
const diagnostics = new Map();
|
|
146
|
+
const sources = new Map();
|
|
147
|
+
for (const key of keyOf.keys()) {
|
|
148
|
+
const found = pass.diagnostics.get(key);
|
|
149
|
+
// A dependant the lint DECLINED (excluded by config, say) has no diagnostics entry.
|
|
150
|
+
// Skipped rather than defaulted to `[]`, which would report it as checked and clean.
|
|
151
|
+
if (found === undefined)
|
|
152
|
+
continue;
|
|
153
|
+
diagnostics.set(key, found);
|
|
154
|
+
const source = pass.sources?.get(key);
|
|
155
|
+
if (source)
|
|
156
|
+
sources.set(key, source);
|
|
157
|
+
}
|
|
158
|
+
return { diagnostics, sources, notChecked: pass.notChecked };
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=impact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|