@platformos/platformos-mcp-supervisor 0.1.0 → 0.2.1
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/CHANGELOG.md +1076 -0
- package/README.md +20 -5
- package/dist/bin/args.d.ts +15 -2
- package/dist/bin/args.js +31 -3
- package/dist/bin/platformos-mcp-supervisor.js +6 -2
- package/dist/context.d.ts +16 -8
- package/dist/context.js +7 -6
- package/dist/cost-model.d.ts +60 -0
- package/dist/cost-model.js +60 -0
- package/dist/enrich/enrich.d.ts +18 -0
- package/dist/enrich/enrich.js +39 -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 +17 -5
- package/dist/impact/impact.js +135 -164
- package/dist/impact/project-scan.d.ts +12 -10
- package/dist/impact/project-scan.js +12 -10
- package/dist/result/assemble.d.ts +1 -2
- package/dist/result/assemble.js +1 -2
- package/dist/result/blocking.d.ts +10 -4
- package/dist/result/blocking.js +23 -3
- package/dist/result/impact-states.d.ts +12 -9
- package/dist/result/impact-states.js +9 -17
- package/dist/result/response-budget.js +5 -3
- package/dist/result/types.d.ts +42 -40
- package/dist/transport/instructions.js +53 -86
- package/dist/transport/server.d.ts +5 -0
- package/dist/transport/server.js +8 -4
- package/dist/transport/validate-code.d.ts +23 -2
- package/dist/transport/validate-code.js +53 -18
- package/dist/validate/batch-bounds.js +5 -0
- package/dist/validate/validate-buffers.js +81 -27
- package/package.json +15 -14
|
@@ -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
|
package/dist/impact/impact.d.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type DocsetVocabulary } from '../enrich/enrich.js';
|
|
2
|
+
import type { BatchBuffer, runBatchLint } from '../lint/lint-batch.js';
|
|
2
3
|
import type { ValidateCodeImpact } from '../result/types.js';
|
|
3
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
|
+
}
|
|
4
15
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
8
20
|
*/
|
|
9
|
-
export declare function runImpact(
|
|
21
|
+
export declare function runImpact(input: ImpactInput): Promise<Map<string, ValidateCodeImpact>>;
|
package/dist/impact/impact.js
CHANGED
|
@@ -1,189 +1,160 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Impact
|
|
3
|
-
* to `lint/`).
|
|
2
|
+
* Impact — what this change breaks in files the agent is NOT editing.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `layout:` synthesizes no edge — see `traverse.ts`). So a file's dependents can only be
|
|
12
|
-
* among the edge sources whose TEXT contains its logical name, and filtering on that name is
|
|
13
|
-
* a sound over-approximation leaving few survivors (measured p50 1, p90 6 candidates on a
|
|
14
|
-
* 2,615-file project). Those are parsed and resolved with {@link extractFileReferences}, the
|
|
15
|
-
* SAME resolver `buildAppGraph` runs, so this answer cannot drift from the graph's — 80
|
|
16
|
-
* targets sampled across two real projects produced 0 mismatches on `(source, kind, args)`.
|
|
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
|
|
17
11
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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.
|
|
21
18
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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.
|
|
24
34
|
*/
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import { extractFileReferences, toSourceCode, } from '@platformos/platformos-graph';
|
|
35
|
+
import { path } from '@platformos/platformos-check-common';
|
|
36
|
+
import { uriToName } from '@platformos/platformos-common';
|
|
28
37
|
import { toAbsoluteFilePath } from '../adapter-input.js';
|
|
29
|
-
|
|
30
|
-
|
|
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';
|
|
31
43
|
/**
|
|
32
|
-
* The
|
|
33
|
-
* `{% doc %}` `@param` contract — EXACTLY the kinds the `PartialCallArguments`
|
|
34
|
-
* lint check validates (`render`/`include` via its `RenderMarkup` handler,
|
|
35
|
-
* `function` via `FunctionMarkup`). `background`/`graphql`/`layout`/`asset` edges
|
|
36
|
-
* carry scheduling/operation arguments that are NOT `@param`s, so signature-impact
|
|
37
|
-
* must ignore them or it would flag correct calls (a false positive that the
|
|
38
|
-
* forward `PartialCallArguments` check never produces).
|
|
39
|
-
*/
|
|
40
|
-
const SIGNATURE_EDGE_KINDS = new Set(['render', 'include', 'function']);
|
|
41
|
-
/**
|
|
42
|
-
* Whether the graph can model incoming references to `uri` — i.e. `uri` can be a
|
|
43
|
-
* resolvable edge TARGET (a Liquid page/layout/partial, or a GraphQL operation).
|
|
44
|
-
* Reuses check-common's canonical classifiers so this cannot drift from the
|
|
45
|
-
* graph's own edge resolution.
|
|
44
|
+
* The impact of the whole changeset, per requested buffer key.
|
|
46
45
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* (ADR 004), so the graph has no dependents for them and `total: 0` would be a
|
|
50
|
-
* false "safe to change". Those get `status: 'not_applicable'` instead.
|
|
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.
|
|
51
48
|
*/
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
// YAML case visibly excluded, which is the distinction the docblock above turns on.
|
|
55
|
-
const type = sourceCodeTypeOf(uri);
|
|
56
|
-
return type === SourceCodeType.LiquidHtml || type === SourceCodeType.GraphQL;
|
|
57
|
-
}
|
|
58
|
-
/** A fresh zeroed dependents shape for every non-`computed` status. */
|
|
59
|
-
const noDependents = () => ({
|
|
60
|
-
total: 0,
|
|
61
|
-
by_kind: {},
|
|
62
|
-
sample: [],
|
|
63
|
-
});
|
|
64
|
-
/**
|
|
65
|
-
* Compute the edited file's blast radius from the project as it is right now.
|
|
66
|
-
* Reports `not_applicable` when nothing could reference the file by name, and
|
|
67
|
-
* throws only on real I/O failure — the caller degrades that to `unavailable`.
|
|
68
|
-
*/
|
|
69
|
-
export async function runImpact(params, scan) {
|
|
70
|
-
const { projectDir, filePath, content } = params;
|
|
49
|
+
export async function runImpact(input) {
|
|
50
|
+
const { projectDir, buffers, scan } = input;
|
|
71
51
|
const rootUri = path.normalize(path.URI.file(projectDir));
|
|
72
|
-
const
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
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' });
|
|
79
78
|
}
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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;
|
|
89
107
|
}
|
|
90
108
|
/**
|
|
91
|
-
*
|
|
109
|
+
* The findings each dependant gained because of the changeset, keyed by its URI.
|
|
92
110
|
*
|
|
93
|
-
* The
|
|
94
|
-
*
|
|
95
|
-
* filter is exact — the survivors are resolved by the graph's own resolver, so a candidate
|
|
96
|
-
* that merely MENTIONS the name contributes no reference.
|
|
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.
|
|
97
113
|
*/
|
|
98
|
-
async function
|
|
114
|
+
async function introducedByDependant(input, dependants) {
|
|
115
|
+
const { projectDir, buffers, scan, lint, docset, log } = input;
|
|
99
116
|
const sources = await scan.sources();
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
kinds.add(ref.kind);
|
|
121
|
-
callers.set(caller, kinds);
|
|
122
|
-
}
|
|
123
|
-
const by_kind = {};
|
|
124
|
-
for (const kinds of callers.values()) {
|
|
125
|
-
for (const kind of kinds)
|
|
126
|
-
by_kind[kind] = (by_kind[kind] ?? 0) + 1;
|
|
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);
|
|
127
137
|
}
|
|
128
|
-
|
|
129
|
-
return { total: callers.size, by_kind, sample };
|
|
138
|
+
return introduced;
|
|
130
139
|
}
|
|
131
140
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* `{% doc %}` block. Reuses check-common's `extractDocDefinition` — the same
|
|
135
|
-
* primitive the `PartialCallArguments` check reads for the doc case — so the
|
|
136
|
-
* two never diverge. `null` deliberately disables signature-impact: without an
|
|
137
|
-
* explicit contract we do NOT guess a signature (no false positives).
|
|
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.
|
|
138
143
|
*/
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
required: parameters.filter((p) => p.required).map((p) => p.name),
|
|
149
|
-
allowed: parameters.map((p) => p.name),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* The dependent callers whose passed arguments violate `signature` — missing a
|
|
154
|
-
* required `@param`, or passing one the `{% doc %}` block does not declare. The
|
|
155
|
-
* cross-file inverse of `PartialCallArguments`: it checks the edited file's
|
|
156
|
-
* contract against every existing caller at once. Deduplicated per caller,
|
|
157
|
-
* sorted, and capped.
|
|
158
|
-
*/
|
|
159
|
-
function computeSignatureRisk(references, rootUri, signature) {
|
|
160
|
-
const byCaller = new Map();
|
|
161
|
-
for (const ref of references) {
|
|
162
|
-
// Only the kinds whose args ARE `@param`s (see {@link SIGNATURE_EDGE_KINDS}) —
|
|
163
|
-
// a `{% background %}`/`{% graphql %}`/layout edge's args are not, and flagging
|
|
164
|
-
// them would be a false positive the forward check never makes.
|
|
165
|
-
if (!ref.kind || !SIGNATURE_EDGE_KINDS.has(ref.kind))
|
|
166
|
-
continue;
|
|
167
|
-
const args = ref.args ?? [];
|
|
168
|
-
const missing = signature.required.filter((param) => !args.includes(param));
|
|
169
|
-
const unexpected = args.filter((arg) => !signature.allowed.includes(arg));
|
|
170
|
-
if (missing.length === 0 && unexpected.length === 0)
|
|
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)
|
|
171
152
|
continue;
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
for (const u of unexpected)
|
|
177
|
-
entry.unexpected.add(u);
|
|
178
|
-
byCaller.set(caller, entry);
|
|
153
|
+
diagnostics.set(key, found);
|
|
154
|
+
const source = pass.sources?.get(key);
|
|
155
|
+
if (source)
|
|
156
|
+
sources.set(key, source);
|
|
179
157
|
}
|
|
180
|
-
return
|
|
181
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
182
|
-
.slice(0, SAMPLE_LIMIT)
|
|
183
|
-
.map(([caller, { missing, unexpected }]) => ({
|
|
184
|
-
caller,
|
|
185
|
-
missing_required: [...missing].sort((a, b) => a.localeCompare(b)),
|
|
186
|
-
unexpected_args: [...unexpected].sort((a, b) => a.localeCompare(b)),
|
|
187
|
-
}));
|
|
158
|
+
return { diagnostics, sources, notChecked: pass.notChecked };
|
|
188
159
|
}
|
|
189
160
|
//# sourceMappingURL=impact.js.map
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The project's edge-source files as TEXT, read ONCE per request.
|
|
3
3
|
*
|
|
4
|
-
* This is the whole of
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* This is the whole of impact's project state, and it is read only when some buffer in the
|
|
5
|
+
* request could HAVE dependants — decidable from the paths alone, before any I/O. The trade
|
|
6
|
+
* is deliberate:
|
|
7
|
+
* pay a project READ per request — I/O, on the threadpool, overlapping the lint's CPU —
|
|
8
|
+
* instead of a whole-project PARSE once and a lifetime of keeping it honest. Measured on a
|
|
9
|
+
* real project (2,615 edge sources, 3.0 MB): 235 ms. Being derived per request, it cannot be
|
|
10
|
+
* stale, which is why `impact` has no `computing` state.
|
|
9
11
|
*
|
|
10
12
|
* SCOPE. Only EDGE SOURCES are read (`enumerateEdgeSources` — page/layout/partial), because
|
|
11
13
|
* only their content can declare an edge; `.graphql`/`.yml`/asset files are leaves and can
|
|
12
14
|
* never be the SOURCE of a reference. That is the graph's own definition, imported rather
|
|
13
15
|
* than restated.
|
|
14
16
|
*
|
|
15
|
-
* BUFFERS WIN OVER DISK. The changeset under validation is overlaid, so a
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* BUFFERS WIN OVER DISK. The changeset under validation is overlaid, so a call a buffer has
|
|
18
|
+
* only just added counts, and one it has just deleted stops counting — otherwise this stage
|
|
19
|
+
* would answer from disk while the lint beside it answers from the buffer. A buffer for a
|
|
20
|
+
* file not yet on disk is ADDED when it is an edge source, so a brand-new page's
|
|
21
|
+
* `{% render %}` is seen.
|
|
20
22
|
*/
|
|
21
23
|
import { type UriString } from '@platformos/platformos-check-common';
|
|
22
24
|
import type { AbstractFileSystem } from '@platformos/platformos-common';
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The project's edge-source files as TEXT, read ONCE per request.
|
|
3
3
|
*
|
|
4
|
-
* This is the whole of
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* This is the whole of impact's project state, and it is read only when some buffer in the
|
|
5
|
+
* request could HAVE dependants — decidable from the paths alone, before any I/O. The trade
|
|
6
|
+
* is deliberate:
|
|
7
|
+
* pay a project READ per request — I/O, on the threadpool, overlapping the lint's CPU —
|
|
8
|
+
* instead of a whole-project PARSE once and a lifetime of keeping it honest. Measured on a
|
|
9
|
+
* real project (2,615 edge sources, 3.0 MB): 235 ms. Being derived per request, it cannot be
|
|
10
|
+
* stale, which is why `impact` has no `computing` state.
|
|
9
11
|
*
|
|
10
12
|
* SCOPE. Only EDGE SOURCES are read (`enumerateEdgeSources` — page/layout/partial), because
|
|
11
13
|
* only their content can declare an edge; `.graphql`/`.yml`/asset files are leaves and can
|
|
12
14
|
* never be the SOURCE of a reference. That is the graph's own definition, imported rather
|
|
13
15
|
* than restated.
|
|
14
16
|
*
|
|
15
|
-
* BUFFERS WIN OVER DISK. The changeset under validation is overlaid, so a
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* BUFFERS WIN OVER DISK. The changeset under validation is overlaid, so a call a buffer has
|
|
18
|
+
* only just added counts, and one it has just deleted stops counting — otherwise this stage
|
|
19
|
+
* would answer from disk while the lint beside it answers from the buffer. A buffer for a
|
|
20
|
+
* file not yet on disk is ADDED when it is an edge source, so a brand-new page's
|
|
21
|
+
* `{% render %}` is seen.
|
|
20
22
|
*/
|
|
21
23
|
import { path } from '@platformos/platformos-check-common';
|
|
22
24
|
import { enumerateEdgeSources, isEdgeSource } from '@platformos/platformos-graph';
|
|
@@ -10,7 +10,6 @@ export declare function assembleResult(diagnostics: ValidateCodeDiagnostic[], im
|
|
|
10
10
|
* own validation failed would make the tool a liability. `reason` lands in `next_step`;
|
|
11
11
|
* `code` lands in `not_applicable_reason` for an agent to branch on.
|
|
12
12
|
*
|
|
13
|
-
* `impact` is `not_applicable
|
|
14
|
-
* depends on this".
|
|
13
|
+
* `impact` is `not_applicable`: nothing cross-file was compared either.
|
|
15
14
|
*/
|
|
16
15
|
export declare function assembleNotApplicableResult(declined: Declined): ValidateCodeResult;
|
package/dist/result/assemble.js
CHANGED
|
@@ -55,8 +55,7 @@ export function assembleResult(diagnostics, impact) {
|
|
|
55
55
|
* own validation failed would make the tool a liability. `reason` lands in `next_step`;
|
|
56
56
|
* `code` lands in `not_applicable_reason` for an agent to branch on.
|
|
57
57
|
*
|
|
58
|
-
* `impact` is `not_applicable
|
|
59
|
-
* depends on this".
|
|
58
|
+
* `impact` is `not_applicable`: nothing cross-file was compared either.
|
|
60
59
|
*/
|
|
61
60
|
export function assembleNotApplicableResult(declined) {
|
|
62
61
|
return {
|
|
@@ -65,10 +65,16 @@ export declare const BLOCKING_CHECKS: ReadonlySet<string>;
|
|
|
65
65
|
* - `UniqueDocParamNames`, `ValidDocParamTypes` — doc hygiene with no runtime effect, since
|
|
66
66
|
* `{% doc %}` is inert at runtime.
|
|
67
67
|
* - `ImgWidthAndHeight`, `ParserBlockingScript` — performance advice.
|
|
68
|
-
* - `
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
68
|
+
* - `DeprecatedFrontmatterField` — measured: a deprecated key (`layout_name` naming a layout
|
|
69
|
+
* that exists, `redirect_url`) and a deprecated `home.liquid` filename are all ACCEPTED by
|
|
70
|
+
* the converter. Superseded, not broken.
|
|
71
|
+
*
|
|
72
|
+
* `ValidFrontmatter` used to be listed here, recorded as blocked on a discriminator
|
|
73
|
+
* (TASK-26) because "two of its three findings" were fatal and blocking the code would have
|
|
74
|
+
* created one false block. Both halves were wrong. It had seven reachable shapes, not three,
|
|
75
|
+
* and `layout: false` — the finding named as the harmless one — is itself a converter
|
|
76
|
+
* rejection. It has since been split into per-shape codes, which is the discriminator the
|
|
77
|
+
* task was waiting for: three of them are in the set above, two are named here.
|
|
72
78
|
*/
|
|
73
79
|
/** A diagnostic, narrowed to what the gate reads. */
|
|
74
80
|
interface GateInput {
|
package/dist/result/blocking.js
CHANGED
|
@@ -41,10 +41,15 @@ export const BLOCKING_CHECKS = new Set([
|
|
|
41
41
|
// Does not parse. Nothing downstream can be trusted about the file at all.
|
|
42
42
|
'LiquidHTMLSyntaxError',
|
|
43
43
|
// Unparseable YAML. Measured: `pos-cli deploy --dry-run` REJECTS the file ("Body
|
|
44
|
-
// contains invalid YAML"), failing the WHOLE changeset. Scoped to SYNTAX
|
|
45
|
-
//
|
|
46
|
-
//
|
|
44
|
+
// contains invalid YAML"), failing the WHOLE changeset. Scoped to SYNTAX, which is a
|
|
45
|
+
// narrower claim than it used to make here: a REAL deploy rejects an unknown property
|
|
46
|
+
// TYPE (now `InvalidSchemaPropertyType`) and an unknown top-level key, both of which
|
|
47
|
+
// `--dry-run` misses by returning before the nested converter. Duplicate property names
|
|
48
|
+
// ARE accepted — measured.
|
|
47
49
|
'YAMLSyntaxError',
|
|
50
|
+
// `Attribute type `x` is not allowed`, from the nested `CustomAttributeConverter`.
|
|
51
|
+
// Measured by a REAL deploy; `--dry-run` accepts it.
|
|
52
|
+
'InvalidSchemaPropertyType',
|
|
48
53
|
// Broken reference: the target does not exist, so the render fails at runtime.
|
|
49
54
|
'MissingPartial',
|
|
50
55
|
// platformOS raises on an unknown filter rather than ignoring it.
|
|
@@ -82,6 +87,21 @@ export const BLOCKING_CHECKS = new Set([
|
|
|
82
87
|
// content, so one that cannot is a defeated contract — and a silently blank page
|
|
83
88
|
// is harder to diagnose than an error.
|
|
84
89
|
'MissingContentForLayout',
|
|
90
|
+
// --- Frontmatter, split out of `ValidFrontmatter`. Each is a measured converter
|
|
91
|
+
// rejection, which fails the WHOLE changeset. ---
|
|
92
|
+
// `Body contains invalid YAML: <parser error>`. `YAMLSyntaxError` cannot cover this —
|
|
93
|
+
// it is YAML-typed and never sees a `.liquid` file.
|
|
94
|
+
'InvalidFrontmatterSyntax',
|
|
95
|
+
// `Unknown properties: <key>.`
|
|
96
|
+
'UnknownFrontmatterField',
|
|
97
|
+
// `Request method 'POST' is not allowed`, and `layout: false` → `undefined method 'sub'
|
|
98
|
+
// for false` (YAML reads it as a boolean; `set_layout` guards `nil`, not `false`).
|
|
99
|
+
'InvalidFrontmatterValue',
|
|
100
|
+
// `Could not find Layout with layout: <name>`.
|
|
101
|
+
'MissingLayout',
|
|
102
|
+
// `<page> tries to assign authorization_policies which do not exist: <name>`. Measured by
|
|
103
|
+
// a REAL deploy — `--dry-run` accepts it, returning before the association write.
|
|
104
|
+
'MissingFrontmatterAssociation',
|
|
85
105
|
]);
|
|
86
106
|
/**
|
|
87
107
|
* Whether this set of diagnostics should stop the agent writing the file.
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The non-`computed`
|
|
2
|
+
* The non-`computed` impact states.
|
|
3
3
|
*
|
|
4
|
-
* Each is a FACTORY, not a shared constant: the
|
|
5
|
-
*
|
|
6
|
-
* past and future response.
|
|
4
|
+
* Each is a FACTORY, not a shared constant: the object ends up inside a returned result, so
|
|
5
|
+
* one shared instance would let a mutation reach every past and future response.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* that is what `status` is for.
|
|
7
|
+
* NEITHER IS A CLEARANCE, and neither is `computed` — impact publishes findings only, so
|
|
8
|
+
* nothing here or anywhere else says "nothing depends on this". See `ValidateCodeImpact`.
|
|
11
9
|
*/
|
|
12
10
|
import type { ValidateCodeImpact } from './types.js';
|
|
13
|
-
/** The
|
|
11
|
+
/** The cross-file comparison could not be run at all — a failure, or a deadline. */
|
|
14
12
|
export declare const UNAVAILABLE_IMPACT: () => ValidateCodeImpact;
|
|
15
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The server was started with `--no-impact`, so nothing cross-file was attempted. Distinct
|
|
15
|
+
* from `unavailable` on purpose: a retry cannot change this one.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DISABLED_IMPACT: () => ValidateCodeImpact;
|
|
18
|
+
/** This file has no dependants the graph can find — see `ValidateCodeImpactStatus`. */
|
|
16
19
|
export declare const NOT_APPLICABLE_IMPACT: () => ValidateCodeImpact;
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
dependents: noDependents(),
|
|
11
|
-
});
|
|
12
|
-
/** This file has no dependency edges to speak of — see `ValidateCodeImpactStatus`. */
|
|
13
|
-
export const NOT_APPLICABLE_IMPACT = () => ({
|
|
14
|
-
scope: 'direct',
|
|
15
|
-
status: 'not_applicable',
|
|
16
|
-
dependents: noDependents(),
|
|
17
|
-
});
|
|
1
|
+
/** The cross-file comparison could not be run at all — a failure, or a deadline. */
|
|
2
|
+
export const UNAVAILABLE_IMPACT = () => ({ status: 'unavailable' });
|
|
3
|
+
/**
|
|
4
|
+
* The server was started with `--no-impact`, so nothing cross-file was attempted. Distinct
|
|
5
|
+
* from `unavailable` on purpose: a retry cannot change this one.
|
|
6
|
+
*/
|
|
7
|
+
export const DISABLED_IMPACT = () => ({ status: 'disabled' });
|
|
8
|
+
/** This file has no dependants the graph can find — see `ValidateCodeImpactStatus`. */
|
|
9
|
+
export const NOT_APPLICABLE_IMPACT = () => ({ status: 'not_applicable' });
|
|
18
10
|
//# sourceMappingURL=impact-states.js.map
|