layero-detection
v0.1.7
Published
Layero's unified framework / build-command / output / runtime detection — one declarative spec + one algorithm (Snapshot → BuildPlan). The single source of truth shared by the Layero CLI and control-plane.
Maintainers
Readme
layero-detection
Given a snapshot of a project — package.json, the files in the root, config
files, requirements.txt — works out what it is and how to build it: the
framework, the build command, the output directory, whether it needs a
long-lived process, and where the pieces of a fullstack repository live.
import { detect } from "layero-detection";
const plan = detect(snapshot);
// → { projectKind: "static" | "spa" | "ssr" | "runtime" | "fullstack",
// units: [{ role, framework, buildCmd, outputDir, startCmd, runtimeKind, … }],
// … }snapshot is what you read off disk: packageJson, sets of files and
dirs, the text of any config files you found, requirementsTxt, hasHtml
and an optional layeroJson. Nothing is read for you — the package is pure
and has no I/O, which is what lets the same algorithm run in a CLI, a server
and a build container.
Vite, Next.js, Astro, Nuxt, SvelteKit, Remix, Gatsby, CRA, Angular, Docusaurus,
VitePress, Storybook, Eleventy, Hugo, plain static — plus the runtime kinds
(ssr_next, streamlit, gradio, flask, python_web, node_web) and
fullstack splits.
The package is extracted from Layero, a deployment platform for frontend applications, where it is the single source of truth shared by the CLI, the control plane, the builder and the backend — one declarative spec and one algorithm instead of four hand-synced re-implementations. It has no dependencies and can be used on its own.
- Platform: https://layero.ru · Docs: https://docs.layero.ru
- The CLI that consumes it:
layero
Internals
The single source of truth for how Layero classifies a project (framework,
build command, output directory, runtime kind) across every entry point. Replaces
the four hand-synced re-implementations (backend framework_detector.py, CLI
detect.ts, control-plane dropDeploy/detect.ts, builder frameworks/*.py) and
their brittle AST lockstep test (test_framework_sync.py).
What lives here
| File | Role |
|---|---|
| detection.spec.json | Source of truth. Declarative table: framework identity (signals) + build command + default output + runtime kinds + package managers. Data only — no imperative logic. |
| schema.json | JSON Schema for the spec (enforced by gen.mjs). |
| detect_core.py | The shared algorithm (Python). detect(Snapshot) → BuildPlan. stdlib-only. |
| detect_core.ts | The shared algorithm (TypeScript). In lockstep with the .py for DEFAULT behaviour — enforced by cli/test/detect_core_parity.test.ts in CI. It does NOT carry the opt-in rule set (RULES_V2): those rules are applied by the builder only, on a project's first build, and the builder is authoritative. A CLI that classifies an unusual repo the old way is corrected by the builder on that same first build. |
| gen.mjs | Fail-closed generator — vendors the canonical files into each consumer's build context (see below). |
| test_parity.py | Golden parity: detect_core vs the real builder as oracle (41 scenarios). |
| test_buildplan.py | Shape tests: fullstack, hints, dict-snapshot path, spec↔builder lockstep. |
The model
Snapshot ──detect()──▶ BuildPlan { project_kind, units[], project_type, … }
(pkg.json, │
files, configs, BuildUnit { role: static|frontend|backend,
requirements, framework, build_cmd, output_dir,
layero.json) start_cmd, runtime_kind, port }One result shape natively expresses static / SPA / SSR / runtime / fullstack
(a fullstack project is one frontend unit + one backend unit + api_prefix).
Authority: the builder runs detect_core on the cloned disk and is
authoritative for identity + output (it alone can discover_served_root over the
built tree). The server re-detect is the single writer of project_type. The
UI wizard / CLI / drop-deploy all call detect_core too, but advisorily — so
they can never persist an identity the builder then rejects (the
runtime-config-missing / look-like-mismatch prod failures).
Rules that reclassify ship OFF. A detection change re-decides existing
projects on their next deploy, which is why several correct rules were never
lowered into this module (next_no_config_is_ssr lived only in the wizard
frontend; the fullstack "two folders" rule only in the advisor). New rules are
passed in explicitly — detect(snap, hint, RULES_V2) — so one build can compute
BOTH verdicts and report the divergence without acting on it (detect_shadow).
Turn a rule on only after the shadow report over the real project corpus has
been read by a human: python -m app.cli.shadow_detect_corpus replays it over
the source archives of every CLI/dashboard project (553 of them) and splits the
divergences by "already serving" vs "never shipped". That run caught a defect in
the strength rule itself — framework_hint resolved BEFORE signal matching, so
the rule would have shipped and done nothing for the exact case it was written
for.
Refinement stays in code. Disk-only logic — discover_served_root, the
runtime start-command entry-file probe — is NOT in the JSON; it lives in the
builder/runtime-builder keyed by the identity this module produces. The spec
references such steps by name (output_dir.extract) but never carries logic.
Distribution (why the copies in */_detection/)
core/detection/ is outside every consumer's Docker build context
(./backend, ./builder, ./runtime) and outside the CLI's tsconfig
rootDir (./src) — a single shared file is mechanically un-importable. So
gen.mjs vendors verbatim copies into:
backend/app/_detection/→from app._detection import detect_corebuilder/src/_detection/→from src._detection import detect_coreruntime/builder/app/_detection/→from app._detection import detect_corecli/src/_detection/→import … from "../_detection/detect_core.js"
A stale copy is a red build: CI runs node core/detection/gen.mjs --check
(git diff of the vendored dirs). gen.mjs also refuses a spec whose CONTENT
changed without a spec_version bump — until 11.08 that assert compared 1 to 1
forever (the version had never moved since the file was created), so it could
not have caught a copy that arrived by any route other than gen.mjs.
Workflow
# edit the canonical files in core/detection/, then:
node core/detection/gen.mjs # regenerate vendored copies
python3 core/detection/test_parity.py # parity vs the real builder
python3 core/detection/test_buildplan.py
node core/detection/gen.mjs --check # what CI assertsWhen the builder gains a framework, add a frameworks[] row here and a
FrameworkConfig there; test_buildplan.py's lockstep check fails until the
names/aliases/default outputs agree.
