@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
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# `@platformos/platformos-mcp-supervisor`
|
|
2
|
+
|
|
3
|
+
An MCP server that validates platformOS code **before it is written**. It exposes one
|
|
4
|
+
tool, `validate_code`, over stdio.
|
|
5
|
+
|
|
6
|
+
An agent sends the buffer it is about to write; the server lints it in the context of the
|
|
7
|
+
real project and answers with the findings, the fixes the engine already computed, and a
|
|
8
|
+
single gate — `must_fix_before_write` — telling the agent whether writing the file would
|
|
9
|
+
produce something broken.
|
|
10
|
+
|
|
11
|
+
## Running it
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
platformos-mcp-supervisor --project /path/to/project
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The project root may also come from `PLATFORMOS_PROJECT_DIR`. The process speaks MCP over
|
|
18
|
+
stdio and logs to **stderr only** — stdout is the protocol.
|
|
19
|
+
|
|
20
|
+
## The one tool
|
|
21
|
+
|
|
22
|
+
`validate_code` takes either a single buffer or a changeset:
|
|
23
|
+
|
|
24
|
+
```jsonc
|
|
25
|
+
{ "file_path": "app/views/pages/index.liquid", "content": "…" }
|
|
26
|
+
// or
|
|
27
|
+
{ "files": [ { "file_path": "…", "content": "…" }, … ] }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Send a coordinated edit as one call.** Buffers in one request are overlaid on the
|
|
31
|
+
project together, so a partial being created alongside its caller resolves. Sent one at a
|
|
32
|
+
time, that same edit is reported broken.
|
|
33
|
+
|
|
34
|
+
The answer carries `status`, `must_fix_before_write`, the findings split into
|
|
35
|
+
`errors` / `warnings` / `infos`, the cross-file `impact` of the edit, and — when findings
|
|
36
|
+
were withheld to bound the response — `truncated`. `status: not_applicable` means the file
|
|
37
|
+
was **not checked**, which is neither approval nor refusal; `not_applicable_reason` says
|
|
38
|
+
why. See `src/result/types.ts` for the full contract and
|
|
39
|
+
`src/transport/instructions.ts` for what the server tells an agent about reading it.
|
|
40
|
+
|
|
41
|
+
## Request flow
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
input { file_path, content } | { files: [...] }
|
|
45
|
+
→ validate/ decline what needs no lint (outside root, wrong type, too big) pure
|
|
46
|
+
→ lint/ ONE lintBuffers pass, every buffer overlaid at once I/O
|
|
47
|
+
→ diagnostics + the engine's fixes/suggestions + the buffer's AST
|
|
48
|
+
→ enrich/ each check's documentation URL · docset signature where it helps pure
|
|
49
|
+
→ result/ status · must_fix_before_write · next_step · impact pure
|
|
50
|
+
→ response budget, applied LAST to finished results pure
|
|
51
|
+
→ ValidateCodeResult
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Impact reports one thing: **what your change breaks in files you are not editing**. It finds
|
|
55
|
+
the edited file's dependants through `platformos-graph`, lints them twice — once with your
|
|
56
|
+
changeset applied, once without — and reports only the findings the change INTRODUCED. Those
|
|
57
|
+
findings are the check engine's own, so they arrive with the same message, severity,
|
|
58
|
+
documentation link and fixes as any other diagnostic; nothing about "what is broken" is
|
|
59
|
+
re-derived here.
|
|
60
|
+
|
|
61
|
+
The project READ overlaps the primary lint, but the two impact lint passes cannot: they share
|
|
62
|
+
check-node's process-wide `App`, which has no lock. So the primary lint runs first and is
|
|
63
|
+
never delayed by, or lost to, impact.
|
|
64
|
+
|
|
65
|
+
Two bounds keep it affordable, both measured and both reported when hit — `IMPACT_DEADLINE_MS`
|
|
66
|
+
cannot bound this work, since a lint is synchronous CPU and no timer preempts it:
|
|
67
|
+
`MAX_CANDIDATE_BYTES` caps the text discovery will parse, and `MAX_DEPENDANTS_LINTED` caps how
|
|
68
|
+
many dependants get linted. Start the server with `--no-impact` (or
|
|
69
|
+
`POS_SUPERVISOR_NO_IMPACT=1`) to switch the whole stage off; results then carry
|
|
70
|
+
`impact.status: "disabled"`.
|
|
71
|
+
|
|
72
|
+
It never answers "who depends on this file": `{% render var %}` names its target at runtime,
|
|
73
|
+
so that question has no sound static answer and no count is published in place of one.
|
|
74
|
+
|
|
75
|
+
## What this package is not
|
|
76
|
+
|
|
77
|
+
It is a **leaf consumer** of the linting engine, and deliberately thin:
|
|
78
|
+
|
|
79
|
+
- **It detects nothing.** Every check lives in `platformos-check-common`, where the CLI,
|
|
80
|
+
the editors and the browser build all get it too.
|
|
81
|
+
- **It authors no fixes.** `Offense.fix` / `suggest` are the engine's; this package only
|
|
82
|
+
materialises them into edits against the buffer they were computed for.
|
|
83
|
+
- **It ships no documentation.** What it says about a filter, tag or object is read from
|
|
84
|
+
`filters.json` / `tags.json` / `objects.json`; what it says about a check comes from that
|
|
85
|
+
check's `meta.docs`. There is no `data/` directory and a guard fails the build if one
|
|
86
|
+
appears. A gap in the docset is fixed upstream, in
|
|
87
|
+
[platformos-documentation](https://documentation.platformos.com), never patched here.
|
|
88
|
+
- **It never speaks the LSP protocol.** Linting is a direct library call. (Pure helpers
|
|
89
|
+
from the language-server package — the docset markdown renderer — are allowed under an
|
|
90
|
+
allowlist; the protocol, a server and a transport are not.)
|
|
91
|
+
|
|
92
|
+
These are enforced, not aspirational: `test/guards/architecture-invariants.spec.ts` fails
|
|
93
|
+
the build on each. [`ARCHITECTURE.md`](./ARCHITECTURE.md) states them in full and explains
|
|
94
|
+
why each exists.
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
yarn workspace @platformos/platformos-mcp-supervisor build # tsc -b
|
|
100
|
+
yarn workspace @platformos/platformos-mcp-supervisor type-check
|
|
101
|
+
yarn vitest run packages/platformos-mcp-supervisor # unit + integration
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The integration suite builds the package and drives the **real stdio bin** with the
|
|
105
|
+
official MCP SDK client, so the transport and the JSON envelope are covered end to end.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Declined } from './result/types.js';
|
|
2
|
+
export interface AdapterInput {
|
|
3
|
+
/** Absolute project root the buffer is validated against. */
|
|
4
|
+
projectDir: string;
|
|
5
|
+
/** File under edit — absolute, or relative to `projectDir`. */
|
|
6
|
+
filePath: string;
|
|
7
|
+
/** In-memory buffer contents. */
|
|
8
|
+
content: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the file under edit to an absolute path: returned as-is when already absolute,
|
|
12
|
+
* else joined onto the project root.
|
|
13
|
+
*
|
|
14
|
+
* Both branches normalize `.`/`..` segments — `join` directly, and
|
|
15
|
+
* {@link fileApplicability}'s `relative` by resolving its arguments — so a traversal
|
|
16
|
+
* cannot survive the containment check by hiding inside an unnormalized string.
|
|
17
|
+
*/
|
|
18
|
+
export declare function toAbsoluteFilePath(projectDir: string, filePath: string): string;
|
|
19
|
+
/** Whether `validate_code` has anything to say about a file — and if not, why. */
|
|
20
|
+
export type FileApplicability = {
|
|
21
|
+
applicable: true;
|
|
22
|
+
} | ({
|
|
23
|
+
applicable: false;
|
|
24
|
+
} & Declined);
|
|
25
|
+
/**
|
|
26
|
+
* Decide whether the buffer is a file this server can validate.
|
|
27
|
+
*
|
|
28
|
+
* A declined file gets its own terminal status (`not_applicable`) rather than an `ok` or an
|
|
29
|
+
* `error`: the only honest answer is "not mine to judge", which neither blocks nor approves
|
|
30
|
+
* the write. Without this gate every path is linted, and since check-common's
|
|
31
|
+
* `toSourceCode` types an unrecognized extension as JSON, an off-project file holding `{}`
|
|
32
|
+
* comes back `ok` — the false approval this status exists to prevent.
|
|
33
|
+
*
|
|
34
|
+
* THREE INDEPENDENT RULES, in order:
|
|
35
|
+
*
|
|
36
|
+
* 1. CONTAINMENT. The resolved path must lie strictly inside `projectDir`. Checked on the
|
|
37
|
+
* `relative()` result rather than by string prefix, which would accept a sibling root
|
|
38
|
+
* whose name merely extends the project's (`/srv/app-backup` vs `/srv/app`). A leading
|
|
39
|
+
* `..` means the path climbed out; an empty result means the path IS the root; an
|
|
40
|
+
* absolute result means a different Windows drive. This is a correctness gate, not a
|
|
41
|
+
* security boundary — the server only ever READS — so symlinks are not resolved.
|
|
42
|
+
*
|
|
43
|
+
* 2. NOT AN ASSET. Assets are deployed and served, never rendered.
|
|
44
|
+
*
|
|
45
|
+
* 3. SUPPORTED TYPE. Reuses platformos-common's `sourceCodeTypeOf`, so the gate cannot
|
|
46
|
+
* drift away from what the linter would have looked at.
|
|
47
|
+
*/
|
|
48
|
+
export declare function fileApplicability(projectDir: string, filePath: string): FileApplicability;
|
|
49
|
+
/**
|
|
50
|
+
* Largest buffer this server will validate, in bytes.
|
|
51
|
+
*
|
|
52
|
+
* MEASURED against the FULL LINT, not the parse alone — every enabled check walks the AST
|
|
53
|
+
* after it. `lintBuffer` over a real 162-file project, warm cache, runs ~61 ms/KiB, so this
|
|
54
|
+
* bound costs ~7 s isolated; further out the parser itself falls apart (~30 s at 1 MiB, a
|
|
55
|
+
* stack overflow at 2 MiB, a native V8 abort at 4 MiB). It still admits 1.7x the largest
|
|
56
|
+
* real source file found across local projects.
|
|
57
|
+
*
|
|
58
|
+
* THIS BOUND, NOT THE DEADLINE, IS THE GUARD against CPU-bound input: a deadline cannot
|
|
59
|
+
* interrupt a synchronous parse and its timer cannot even fire during one. `cost-model.ts`
|
|
60
|
+
* is an independent measurement; `cost-model.spec.ts` pins that the two agree.
|
|
61
|
+
*/
|
|
62
|
+
export declare const MAX_BUFFER_BYTES: number;
|
|
63
|
+
/**
|
|
64
|
+
* A refusal when the buffer is too large to validate, else `undefined`.
|
|
65
|
+
*
|
|
66
|
+
* Measured in BYTES, not string length: `content.length` under-counts multi-byte content
|
|
67
|
+
* by up to 3x, admitting a buffer three times the intended cost.
|
|
68
|
+
*/
|
|
69
|
+
export declare function bufferTooLarge(content: string): Declined | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* The refusal for a file the project's config excludes from linting.
|
|
72
|
+
*
|
|
73
|
+
* Not an error and not an `ok`: `check()` skips ignored files silently, so "no offenses"
|
|
74
|
+
* for one means "never looked at", and reporting that as `ok` would approve an ignored page
|
|
75
|
+
* containing unparseable Liquid.
|
|
76
|
+
*/
|
|
77
|
+
export declare function ignoredByProjectConfig(relativePath: string): Declined;
|
|
78
|
+
/**
|
|
79
|
+
* The refusal for a path that is not a platformOS source at all.
|
|
80
|
+
*
|
|
81
|
+
* ROUTINE, and the prose is deliberately mild: a project legitimately holds READMEs, `.jsx`
|
|
82
|
+
* components, CI config and build output. This must never advise moving the file anywhere —
|
|
83
|
+
* see {@link misplacedSource}, the opposite advice.
|
|
84
|
+
*
|
|
85
|
+
* Reached both from this server's pre-lint gate ({@link fileApplicability}) and from
|
|
86
|
+
* check-node's `not-a-platformos-file` classification. One factory, so the two cannot
|
|
87
|
+
* describe the same situation differently.
|
|
88
|
+
*/
|
|
89
|
+
export declare function notPlatformOSFile(relativePath: string): Declined;
|
|
90
|
+
/**
|
|
91
|
+
* The refusal for an ASSET: an app file the toolchain has no parser for.
|
|
92
|
+
*
|
|
93
|
+
* The same machine-readable code as {@link notPlatformOSFile}, since an agent does not act
|
|
94
|
+
* differently on the two, but its own prose: an asset IS part of the app and is deployed,
|
|
95
|
+
* so calling `app/assets/logo.png` "not a platformOS file" would be false.
|
|
96
|
+
*/
|
|
97
|
+
export declare function assetNotLinted(relativePath: string): Declined;
|
|
98
|
+
/**
|
|
99
|
+
* The refusal for a real platformOS source sitting where the platform will never load it.
|
|
100
|
+
*
|
|
101
|
+
* THE OPPOSITE ADVICE FROM {@link notPlatformOSFile}, which is why the two are separate
|
|
102
|
+
* codes: this file IS something the toolchain parses, but it is outside every deployed
|
|
103
|
+
* subtree, so it is dead code.
|
|
104
|
+
*
|
|
105
|
+
* IT DOES NOT BLOCK THE WRITE, and is not a `warning` either: someone may be authoring a
|
|
106
|
+
* module, generator template or fixture on purpose. `not_applicable` states the honest
|
|
107
|
+
* thing — nothing was checked — while the prose carries the warning.
|
|
108
|
+
*
|
|
109
|
+
* KNOWN OVER-BROAD FOR `.yml`: the classification behind it treats any parseable extension
|
|
110
|
+
* outside the deployed subtrees as misplaced, so a repository's CI or container config is
|
|
111
|
+
* told "likely misplaced". Narrowing it belongs in check-node, where classification happens.
|
|
112
|
+
*/
|
|
113
|
+
export declare function misplacedSource(relativePath: string): Declined;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared input for the request-path I/O adapters (`lint/`, `impact/`).
|
|
3
|
+
*
|
|
4
|
+
* Both adapters receive the identical `{ projectDir, filePath, content }` and
|
|
5
|
+
* must agree on the buffer's absolute path, so the shape, the absolute-path
|
|
6
|
+
* resolution, and the "is this file ours to judge?" decision all live here
|
|
7
|
+
* rather than being duplicated per adapter.
|
|
8
|
+
*/
|
|
9
|
+
import { isAbsolute, join, relative, sep } from 'node:path';
|
|
10
|
+
import { path as pathUtils } from '@platformos/platformos-check-common';
|
|
11
|
+
import { APP_SOURCE_SUBTREES, getFileType, isParsedFileType, sourceCodeTypeOf, } from '@platformos/platformos-common';
|
|
12
|
+
import { LINT_MS_PER_KIB } from './cost-model.js';
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the file under edit to an absolute path: returned as-is when already absolute,
|
|
15
|
+
* else joined onto the project root.
|
|
16
|
+
*
|
|
17
|
+
* Both branches normalize `.`/`..` segments — `join` directly, and
|
|
18
|
+
* {@link fileApplicability}'s `relative` by resolving its arguments — so a traversal
|
|
19
|
+
* cannot survive the containment check by hiding inside an unnormalized string.
|
|
20
|
+
*/
|
|
21
|
+
export function toAbsoluteFilePath(projectDir, filePath) {
|
|
22
|
+
return isAbsolute(filePath) ? filePath : join(projectDir, filePath);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Decide whether the buffer is a file this server can validate.
|
|
26
|
+
*
|
|
27
|
+
* A declined file gets its own terminal status (`not_applicable`) rather than an `ok` or an
|
|
28
|
+
* `error`: the only honest answer is "not mine to judge", which neither blocks nor approves
|
|
29
|
+
* the write. Without this gate every path is linted, and since check-common's
|
|
30
|
+
* `toSourceCode` types an unrecognized extension as JSON, an off-project file holding `{}`
|
|
31
|
+
* comes back `ok` — the false approval this status exists to prevent.
|
|
32
|
+
*
|
|
33
|
+
* THREE INDEPENDENT RULES, in order:
|
|
34
|
+
*
|
|
35
|
+
* 1. CONTAINMENT. The resolved path must lie strictly inside `projectDir`. Checked on the
|
|
36
|
+
* `relative()` result rather than by string prefix, which would accept a sibling root
|
|
37
|
+
* whose name merely extends the project's (`/srv/app-backup` vs `/srv/app`). A leading
|
|
38
|
+
* `..` means the path climbed out; an empty result means the path IS the root; an
|
|
39
|
+
* absolute result means a different Windows drive. This is a correctness gate, not a
|
|
40
|
+
* security boundary — the server only ever READS — so symlinks are not resolved.
|
|
41
|
+
*
|
|
42
|
+
* 2. NOT AN ASSET. Assets are deployed and served, never rendered.
|
|
43
|
+
*
|
|
44
|
+
* 3. SUPPORTED TYPE. Reuses platformos-common's `sourceCodeTypeOf`, so the gate cannot
|
|
45
|
+
* drift away from what the linter would have looked at.
|
|
46
|
+
*/
|
|
47
|
+
export function fileApplicability(projectDir, filePath) {
|
|
48
|
+
const absolute = toAbsoluteFilePath(projectDir, filePath);
|
|
49
|
+
const relativeToRoot = relative(projectDir, absolute);
|
|
50
|
+
if (relativeToRoot === '' ||
|
|
51
|
+
isAbsolute(relativeToRoot) ||
|
|
52
|
+
relativeToRoot.split(sep)[0] === '..') {
|
|
53
|
+
return {
|
|
54
|
+
applicable: false,
|
|
55
|
+
code: 'outside_project',
|
|
56
|
+
reason: `\`${filePath}\` resolves outside the project root (${projectDir}), so there is no ` +
|
|
57
|
+
`project context to validate it against. This server validates only files inside the ` +
|
|
58
|
+
`project it was started for. Nothing was checked — treat this as "unknown", not as approval.`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const uri = pathUtils.toUri(absolute);
|
|
62
|
+
// 2. AN ASSET IS NEVER JUDGED. It is SERVED, not rendered, so there is no template in it
|
|
63
|
+
// to check. A TYPE question, not an extension one: `app/assets/x.liquid` is an asset
|
|
64
|
+
// the platform hands back verbatim, while `theme.css.liquid` is processed.
|
|
65
|
+
// `platformos-common` owns the rule as `isParsedFileType`; asking IT is what keeps
|
|
66
|
+
// this gate and the lint from disagreeing about a path.
|
|
67
|
+
const rootUri = pathUtils.toUri(projectDir);
|
|
68
|
+
const fileType = getFileType(uri, rootUri);
|
|
69
|
+
if (fileType !== undefined && !isParsedFileType(fileType)) {
|
|
70
|
+
return { applicable: false, ...assetNotLinted(displayPathOf(uri, projectDir)) };
|
|
71
|
+
}
|
|
72
|
+
// 3. SUPPORTED TYPE — extension-only, and deliberately NOT the anchored
|
|
73
|
+
// `isSupportedSourceFile(uri, root)`: this asks "is this a type we parse at all", and
|
|
74
|
+
// a `.liquid` in an undeployed subtree IS one. Anchoring here would answer
|
|
75
|
+
// `unsupported_type` where check-node's classifier says `misplaced-source`, which is
|
|
76
|
+
// what the agent needs to hear. See `NotApplicableReason`.
|
|
77
|
+
if (sourceCodeTypeOf(uri) === undefined) {
|
|
78
|
+
return { applicable: false, ...notPlatformOSFile(displayPathOf(uri, projectDir)) };
|
|
79
|
+
}
|
|
80
|
+
return { applicable: true };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A path as the agent should SEE it: project-relative, forward slashes on every platform.
|
|
84
|
+
*
|
|
85
|
+
* `node:path.relative` is right for the containment decision above but yields
|
|
86
|
+
* `app\notes.txt` on Windows; check-common's `relative` is URI-based and forward-slash by
|
|
87
|
+
* construction.
|
|
88
|
+
*/
|
|
89
|
+
function displayPathOf(uri, projectDir) {
|
|
90
|
+
return pathUtils.relative(uri, pathUtils.toUri(projectDir));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Largest buffer this server will validate, in bytes.
|
|
94
|
+
*
|
|
95
|
+
* MEASURED against the FULL LINT, not the parse alone — every enabled check walks the AST
|
|
96
|
+
* after it. `lintBuffer` over a real 162-file project, warm cache, runs ~61 ms/KiB, so this
|
|
97
|
+
* bound costs ~7 s isolated; further out the parser itself falls apart (~30 s at 1 MiB, a
|
|
98
|
+
* stack overflow at 2 MiB, a native V8 abort at 4 MiB). It still admits 1.7x the largest
|
|
99
|
+
* real source file found across local projects.
|
|
100
|
+
*
|
|
101
|
+
* THIS BOUND, NOT THE DEADLINE, IS THE GUARD against CPU-bound input: a deadline cannot
|
|
102
|
+
* interrupt a synchronous parse and its timer cannot even fire during one. `cost-model.ts`
|
|
103
|
+
* is an independent measurement; `cost-model.spec.ts` pins that the two agree.
|
|
104
|
+
*/
|
|
105
|
+
export const MAX_BUFFER_BYTES = 128 * 1024;
|
|
106
|
+
/**
|
|
107
|
+
* A refusal when the buffer is too large to validate, else `undefined`.
|
|
108
|
+
*
|
|
109
|
+
* Measured in BYTES, not string length: `content.length` under-counts multi-byte content
|
|
110
|
+
* by up to 3x, admitting a buffer three times the intended cost.
|
|
111
|
+
*/
|
|
112
|
+
export function bufferTooLarge(content) {
|
|
113
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
114
|
+
if (bytes <= MAX_BUFFER_BYTES)
|
|
115
|
+
return undefined;
|
|
116
|
+
return {
|
|
117
|
+
code: 'too_large',
|
|
118
|
+
reason: `The buffer is ${Math.round(bytes / 1024)} KiB, above the ${Math.round(MAX_BUFFER_BYTES / 1024)} KiB limit this server will validate, so it was refused before parsing. ` +
|
|
119
|
+
`Liquid validation costs about ${LINT_MS_PER_KIB} ms per KiB and the parser stops ` +
|
|
120
|
+
`completing at all a few ` +
|
|
121
|
+
`MiB out, so checking this would risk hanging the server rather than answering. Nothing ` +
|
|
122
|
+
`was checked — consider splitting the file, which is almost certainly worth doing at this ` +
|
|
123
|
+
`size regardless.`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The refusal for a file the project's config excludes from linting.
|
|
128
|
+
*
|
|
129
|
+
* Not an error and not an `ok`: `check()` skips ignored files silently, so "no offenses"
|
|
130
|
+
* for one means "never looked at", and reporting that as `ok` would approve an ignored page
|
|
131
|
+
* containing unparseable Liquid.
|
|
132
|
+
*/
|
|
133
|
+
export function ignoredByProjectConfig(relativePath) {
|
|
134
|
+
return {
|
|
135
|
+
code: 'ignored',
|
|
136
|
+
reason: `\`${relativePath}\` is excluded by the \`ignore\` list in this project's ` +
|
|
137
|
+
`\`.platformos-check.yml\`, so no check ran against it. Nothing was checked — this is not ` +
|
|
138
|
+
`a pass. Remove it from \`ignore\` if you want this file validated.`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** The subtrees the platform deploys from, as prose. DERIVED, never spelled out. */
|
|
142
|
+
const DEPLOYED = APP_SOURCE_SUBTREES.map((subtree) => `${subtree}/`).join(', ');
|
|
143
|
+
/**
|
|
144
|
+
* Where an agent can read the whole rule rather than infer it from one message. In the
|
|
145
|
+
* prose because `see_also` hangs off a diagnostic, and "the file is in the wrong place"
|
|
146
|
+
* produces none.
|
|
147
|
+
*/
|
|
148
|
+
const DIRECTORY_STRUCTURE = 'Directory structure: ' +
|
|
149
|
+
'https://documentation.platformos.com/developer-guide/platformos-workflow/directory-structure';
|
|
150
|
+
/**
|
|
151
|
+
* The refusal for a path that is not a platformOS source at all.
|
|
152
|
+
*
|
|
153
|
+
* ROUTINE, and the prose is deliberately mild: a project legitimately holds READMEs, `.jsx`
|
|
154
|
+
* components, CI config and build output. This must never advise moving the file anywhere —
|
|
155
|
+
* see {@link misplacedSource}, the opposite advice.
|
|
156
|
+
*
|
|
157
|
+
* Reached both from this server's pre-lint gate ({@link fileApplicability}) and from
|
|
158
|
+
* check-node's `not-a-platformos-file` classification. One factory, so the two cannot
|
|
159
|
+
* describe the same situation differently.
|
|
160
|
+
*/
|
|
161
|
+
export function notPlatformOSFile(relativePath) {
|
|
162
|
+
return {
|
|
163
|
+
code: 'unsupported_type',
|
|
164
|
+
reason: `\`${relativePath}\` is not a platformOS source file, so there is nothing to check. ` +
|
|
165
|
+
`The platform deploys ${DEPLOYED} only. Nothing was checked — writing this file is ` +
|
|
166
|
+
`your call, not a validated pass. ${DIRECTORY_STRUCTURE}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The refusal for an ASSET: an app file the toolchain has no parser for.
|
|
171
|
+
*
|
|
172
|
+
* The same machine-readable code as {@link notPlatformOSFile}, since an agent does not act
|
|
173
|
+
* differently on the two, but its own prose: an asset IS part of the app and is deployed,
|
|
174
|
+
* so calling `app/assets/logo.png` "not a platformOS file" would be false.
|
|
175
|
+
*/
|
|
176
|
+
export function assetNotLinted(relativePath) {
|
|
177
|
+
return {
|
|
178
|
+
code: 'unsupported_type',
|
|
179
|
+
reason: `\`${relativePath}\` is an asset, not a source file the linter understands — it ` +
|
|
180
|
+
`checks Liquid, GraphQL and YAML — so no check ran against it. The file is still ` +
|
|
181
|
+
`deployed and served; there is simply nothing here to validate. Nothing was ` +
|
|
182
|
+
`checked, which is not the same as a pass.`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The refusal for a real platformOS source sitting where the platform will never load it.
|
|
187
|
+
*
|
|
188
|
+
* THE OPPOSITE ADVICE FROM {@link notPlatformOSFile}, which is why the two are separate
|
|
189
|
+
* codes: this file IS something the toolchain parses, but it is outside every deployed
|
|
190
|
+
* subtree, so it is dead code.
|
|
191
|
+
*
|
|
192
|
+
* IT DOES NOT BLOCK THE WRITE, and is not a `warning` either: someone may be authoring a
|
|
193
|
+
* module, generator template or fixture on purpose. `not_applicable` states the honest
|
|
194
|
+
* thing — nothing was checked — while the prose carries the warning.
|
|
195
|
+
*
|
|
196
|
+
* KNOWN OVER-BROAD FOR `.yml`: the classification behind it treats any parseable extension
|
|
197
|
+
* outside the deployed subtrees as misplaced, so a repository's CI or container config is
|
|
198
|
+
* told "likely misplaced". Narrowing it belongs in check-node, where classification happens.
|
|
199
|
+
*/
|
|
200
|
+
export function misplacedSource(relativePath) {
|
|
201
|
+
return {
|
|
202
|
+
code: 'misplaced_source',
|
|
203
|
+
reason: `\`${relativePath}\` is a platformOS source file outside every subtree the platform ` +
|
|
204
|
+
`deploys (${DEPLOYED}). Nothing checked it, and nothing will load it either — a ` +
|
|
205
|
+
`partial, page or query here is dead code. Move it under one of those directories ` +
|
|
206
|
+
`unless it is deliberately a fixture or a build input. This is neither a pass nor a ` +
|
|
207
|
+
`block. ${DIRECTORY_STRUCTURE}`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=adapter-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|