fix: require a compiled artifact for entrypoints.jobs
Module discovery gated the compiled-ESM rule on an allowlist, `["schema", "server", "ui"]`, so `entrypoints.jobs` accepted `./src/jobs/index.ts`. Every other executable entrypoint required a built `.js` or `.mjs`; `jobs` did not, purely because it was never added to that list. It is the second entrypoint to slip through the same way — `ui` did too, until Phase 6A added it explicitly — which is the argument against the allowlist rather than against `jobs`. The rule is now expressed as "every entrypoint except a directory entrypoint", with `migrations` the only member of that set. A new manifest entrypoint therefore defaults to the stricter validation instead of silently escaping it. The same constant replaces the two existing `=== "migrations"` special cases for the file/directory kind check and its message, so the one thing that distinguishes `migrations` is stated once. The directory message is now templated and produces byte-identical text. Every other entrypoint guarantee is untouched: package-relative paths, no traversal, inside-package before and after symlink resolution, the artifact must exist, and it must be a file. No runtime was added. `entrypoints.jobs` still has no consumer — it is declared, validated, and never executed — exactly as it has been since ADR 0006, and as `ui` was before its host loader existed. The manifest schema is unchanged and this stays within v4: the field, its type, and its optionality are all the same, and only discovery's validation of the declared path tightened. Compatibility impact is nil in first-party code: no module, template, or fixture declares a `jobs` entrypoint at all. A third-party module declaring a source-TypeScript path would now fail discovery, which is the intended correction. Tests cover accepted `.js` and `.mjs`, rejected `.ts`/`.tsx`/`.cjs`, a missing artifact, a directory, a symlink escaping the package, and that discovery still never evaluates the artifact. The three rejection cases were confirmed to fail against the previous implementation. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ff159a04dc
commit
3f9b5e3cb0
@@ -11,6 +11,22 @@ import {
|
||||
|
||||
export const MODULE_MANIFEST_FILENAME = "ekklesia.module.json";
|
||||
|
||||
/**
|
||||
* The only manifest entrypoint that resolves to a directory rather than a compiled file.
|
||||
*
|
||||
* Every other entrypoint is an executable build artifact, so the compiled-ESM rule is expressed as
|
||||
* "not a directory entrypoint" rather than as a list of the entrypoints it covers. An allowlist is
|
||||
* what let `jobs` and, before it, `ui` be added to the manifest and silently escape this check;
|
||||
* defaulting a new entrypoint to *stricter* validation is the safer failure mode.
|
||||
*/
|
||||
const DIRECTORY_ENTRYPOINTS: ReadonlySet<string> = new Set(["migrations"]);
|
||||
|
||||
/** Whether an entrypoint must reference compiled ESM JavaScript (ADR 0006's production loading
|
||||
* boundary): true for every entrypoint that is a file rather than a directory. */
|
||||
function isCompiledEntrypoint(entrypointName: string): boolean {
|
||||
return !DIRECTORY_ENTRYPOINTS.has(entrypointName);
|
||||
}
|
||||
|
||||
export type ModuleDiscoveryIssueCode =
|
||||
| "DISCOVERY_CONFIGURATION_INVALID"
|
||||
| "MODULE_CAPABILITY_INCOMPATIBLE"
|
||||
@@ -247,11 +263,8 @@ async function resolveEntrypoints(
|
||||
if (relativeEntrypoint === undefined) {
|
||||
continue;
|
||||
}
|
||||
// `ui` joins `schema`/`server` here: a presentation entrypoint is a build output like any
|
||||
// other module artifact, so source TypeScript is rejected at discovery rather than relied upon
|
||||
// to be compiled later (ADR 0006's compiled-artifact rule, extended by ADR 0021).
|
||||
if (
|
||||
["schema", "server", "ui"].includes(entrypointName) &&
|
||||
isCompiledEntrypoint(entrypointName) &&
|
||||
![".js", ".mjs"].includes(path.extname(relativeEntrypoint).toLowerCase())
|
||||
) {
|
||||
issues.push({
|
||||
@@ -288,15 +301,15 @@ async function resolveEntrypoints(
|
||||
continue;
|
||||
}
|
||||
const entrypointStats = await stat(canonicalEntrypoint);
|
||||
const hasExpectedKind =
|
||||
entrypointName === "migrations" ? entrypointStats.isDirectory() : entrypointStats.isFile();
|
||||
const hasExpectedKind = DIRECTORY_ENTRYPOINTS.has(entrypointName)
|
||||
? entrypointStats.isDirectory()
|
||||
: entrypointStats.isFile();
|
||||
if (!hasExpectedKind) {
|
||||
issues.push({
|
||||
code: "MODULE_ENTRYPOINT_INVALID",
|
||||
message:
|
||||
entrypointName === "migrations"
|
||||
? "migrations must resolve to a directory."
|
||||
: `${entrypointName} must resolve to a file.`,
|
||||
message: DIRECTORY_ENTRYPOINTS.has(entrypointName)
|
||||
? `${entrypointName} must resolve to a directory.`
|
||||
: `${entrypointName} must resolve to a file.`,
|
||||
moduleDirectory: directoryName,
|
||||
moduleId: manifest.id,
|
||||
path: `entrypoints.${entrypointName}`,
|
||||
|
||||
Reference in New Issue
Block a user