@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.
Files changed (92) hide show
  1. package/README.md +105 -0
  2. package/dist/adapter-input.d.ts +113 -0
  3. package/dist/adapter-input.js +210 -0
  4. package/dist/adapter-input.spec.d.ts +1 -0
  5. package/dist/adapter-input.spec.js +275 -0
  6. package/dist/bin/args.d.ts +15 -2
  7. package/dist/bin/args.js +31 -3
  8. package/dist/bin/args.spec.d.ts +1 -0
  9. package/dist/bin/args.spec.js +35 -0
  10. package/dist/bin/platformos-mcp-supervisor.js +6 -2
  11. package/dist/check-docs.d.ts +21 -0
  12. package/dist/check-docs.js +38 -0
  13. package/dist/check-docs.spec.d.ts +1 -0
  14. package/dist/check-docs.spec.js +87 -0
  15. package/dist/context.d.ts +34 -0
  16. package/dist/context.js +12 -0
  17. package/dist/cost-model.d.ts +188 -0
  18. package/dist/cost-model.js +197 -0
  19. package/dist/cost-model.spec.d.ts +1 -0
  20. package/dist/cost-model.spec.js +91 -0
  21. package/dist/deadline.d.ts +27 -0
  22. package/dist/deadline.js +40 -0
  23. package/dist/deadline.spec.d.ts +1 -0
  24. package/dist/deadline.spec.js +83 -0
  25. package/dist/enrich/enrich.d.ts +105 -0
  26. package/dist/enrich/enrich.js +136 -0
  27. package/dist/enrich/enrich.spec.d.ts +1 -0
  28. package/dist/enrich/enrich.spec.js +184 -0
  29. package/dist/enrich/symbol.d.ts +16 -0
  30. package/dist/enrich/symbol.js +58 -0
  31. package/dist/enrich/symbol.spec.d.ts +1 -0
  32. package/dist/enrich/symbol.spec.js +89 -0
  33. package/dist/impact/dependants.d.ts +75 -0
  34. package/dist/impact/dependants.js +104 -0
  35. package/dist/impact/diff.d.ts +27 -0
  36. package/dist/impact/diff.js +33 -0
  37. package/dist/impact/impact.d.ts +21 -0
  38. package/dist/impact/impact.js +160 -0
  39. package/dist/impact/impact.spec.d.ts +1 -0
  40. package/dist/impact/impact.spec.js +357 -0
  41. package/dist/impact/project-scan.d.ts +39 -0
  42. package/dist/impact/project-scan.js +62 -0
  43. package/dist/impact/project-scan.spec.d.ts +1 -0
  44. package/dist/impact/project-scan.spec.js +121 -0
  45. package/dist/index.d.ts +1 -1
  46. package/dist/lint/fixes.d.ts +28 -0
  47. package/dist/lint/fixes.js +30 -0
  48. package/dist/lint/lint-batch.d.ts +100 -0
  49. package/dist/lint/lint-batch.js +98 -0
  50. package/dist/lint/lint-batch.spec.d.ts +1 -0
  51. package/dist/lint/lint-batch.spec.js +503 -0
  52. package/dist/lint/lint.d.ts +11 -1
  53. package/dist/lint/lint.js +9 -3
  54. package/dist/result/assemble.d.ts +11 -9
  55. package/dist/result/assemble.js +63 -16
  56. package/dist/result/assemble.spec.d.ts +1 -0
  57. package/dist/result/assemble.spec.js +319 -0
  58. package/dist/result/blocking.d.ts +92 -0
  59. package/dist/result/blocking.js +116 -0
  60. package/dist/result/blocking.spec.d.ts +1 -0
  61. package/dist/result/blocking.spec.js +201 -0
  62. package/dist/result/impact-states.d.ts +19 -0
  63. package/dist/result/impact-states.js +10 -0
  64. package/dist/result/response-budget.d.ts +9 -0
  65. package/dist/result/response-budget.js +133 -0
  66. package/dist/result/response-budget.spec.d.ts +1 -0
  67. package/dist/result/response-budget.spec.js +222 -0
  68. package/dist/result/types.d.ts +212 -104
  69. package/dist/result/types.js +4 -13
  70. package/dist/transport/instructions-coverage.spec.d.ts +1 -0
  71. package/dist/transport/instructions-coverage.spec.js +176 -0
  72. package/dist/transport/instructions.d.ts +1 -0
  73. package/dist/transport/instructions.js +150 -0
  74. package/dist/transport/process-guards.d.ts +43 -0
  75. package/dist/transport/process-guards.js +63 -0
  76. package/dist/transport/process-guards.spec.d.ts +1 -0
  77. package/dist/transport/process-guards.spec.js +200 -0
  78. package/dist/transport/server.d.ts +6 -1
  79. package/dist/transport/server.js +21 -10
  80. package/dist/transport/validate-code.d.ts +50 -15
  81. package/dist/transport/validate-code.js +238 -33
  82. package/dist/transport/validate-code.spec.d.ts +1 -0
  83. package/dist/transport/validate-code.spec.js +2303 -0
  84. package/dist/validate/batch-bounds.d.ts +34 -0
  85. package/dist/validate/batch-bounds.js +65 -0
  86. package/dist/validate/batch-coherence.d.ts +21 -0
  87. package/dist/validate/batch-coherence.js +54 -0
  88. package/dist/validate/batch-coherence.spec.d.ts +1 -0
  89. package/dist/validate/batch-coherence.spec.js +111 -0
  90. package/dist/validate/validate-buffers.d.ts +27 -0
  91. package/dist/validate/validate-buffers.js +319 -0
  92. 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 {};